[xc-admin] Add encoder for governance messages (#462)

* Add encoder

* Cleanup

* Update test

* Ci

* CI

* Cleanup

* More cleanup
This commit is contained in:
guibescos 2023-01-09 15:54:54 -06:00 committed by GitHub
parent 5efa611a97
commit 097943f657
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 195 additions and 49 deletions

18
.github/workflows/xc-admin.yaml vendored Normal file
View File

@ -0,0 +1,18 @@
name: Check Xc Admin
on:
pull_request:
paths: [xc-admin/**]
push:
branches: [main]
paths: [xc-admin/**]
jobs:
test:
runs-on: ubuntu-latest
defaults:
run:
working-directory: xc-admin/
steps:
- uses: actions/checkout@v2
- name: Run xc-admin tests
run: |
npm ci && npm run test

View File

@ -4,6 +4,9 @@
"workspaces": [
"packages/*"
],
"scripts": {
"test": "cd packages/xc-admin-common && npm run test"
},
"devDependencies": {
"lerna": "^6.3.0"
}

View File

@ -1,24 +1,61 @@
import { PublicKey, SystemProgram } from "@solana/web3.js";
import { decodeExecutePostedVaa, decodeHeader } from "..";
import { ChainName } from "@certusone/wormhole-sdk";
import {
PACKET_DATA_SIZE,
PublicKey,
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import {
ActionName,
decodeExecutePostedVaa,
decodeHeader,
encodeHeader,
} from "..";
import { encodeExecutePostedVaa } from "../governance_payload/ExecutePostedVaa";
test("GovernancePayload", (done) => {
test("GovernancePayload ser/de", (done) => {
jest.setTimeout(60000);
let governanceHeader = decodeHeader(
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])
);
// Valid header 1
let expectedGovernanceHeader = {
targetChainId: "pythnet" as ChainName,
action: "ExecutePostedVaa" as ActionName,
};
let buffer = Buffer.alloc(PACKET_DATA_SIZE);
let span = encodeHeader(expectedGovernanceHeader, buffer);
expect(
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26]))
).toBeTruthy();
let governanceHeader = decodeHeader(buffer.subarray(0, span));
expect(governanceHeader?.targetChainId).toBe("pythnet");
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
governanceHeader = decodeHeader(
Buffer.from([80, 84, 71, 77, 0, 0, 0, 0, 0, 0, 0, 0])
);
// Valid header 2
expectedGovernanceHeader = {
targetChainId: "unset" as ChainName,
action: "ExecutePostedVaa" as ActionName,
};
buffer = Buffer.alloc(PACKET_DATA_SIZE);
span = encodeHeader(expectedGovernanceHeader, buffer);
expect(
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 0]))
).toBeTruthy();
governanceHeader = decodeHeader(buffer.subarray(0, span));
expect(governanceHeader?.targetChainId).toBe("unset");
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
governanceHeader = decodeHeader(
Buffer.from([80, 84, 71, 77, 1, 3, 0, 1, 0, 0, 0, 0])
);
// Valid header 3
expectedGovernanceHeader = {
targetChainId: "solana" as ChainName,
action: "SetFee" as ActionName,
};
buffer = Buffer.alloc(PACKET_DATA_SIZE);
span = encodeHeader(expectedGovernanceHeader, buffer);
expect(
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 1, 3, 0, 1]))
).toBeTruthy();
governanceHeader = decodeHeader(buffer.subarray(0, span));
expect(governanceHeader?.targetChainId).toBe("solana");
expect(governanceHeader?.action).toBe("SetFee");
@ -40,27 +77,49 @@ test("GovernancePayload", (done) => {
);
expect(governanceHeader).toBeUndefined();
// Decode executePostVaa
let executePostedVaaArgs = decodeExecutePostedVaa(
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])
);
expect(executePostedVaaArgs?.header.targetChainId).toBe("pythnet");
expect(executePostedVaaArgs?.header.action).toBe("ExecutePostedVaa");
// Decode executePostVaa with empty instructions
let expectedExecuteVaaArgs = {
targetChainId: "pythnet" as ChainName,
instructions: [] as TransactionInstruction[],
};
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs);
expect(
buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0]))
).toBeTruthy();
let executePostedVaaArgs = decodeExecutePostedVaa(buffer);
expect(executePostedVaaArgs?.targetChainId).toBe("pythnet");
expect(executePostedVaaArgs?.instructions.length).toBe(0);
executePostedVaaArgs = decodeExecutePostedVaa(
Buffer.from([
80, 84, 71, 77, 0, 0, 0, 26, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 0, 0,
141, 65, 8, 219, 216, 57, 229, 94, 74, 17, 138, 50, 121, 176, 38, 178, 50,
229, 210, 103, 232, 253, 133, 66, 14, 47, 228, 224, 162, 147, 232, 251, 1,
1, 252, 221, 21, 33, 156, 1, 72, 252, 246, 229, 150, 218, 109, 165, 127,
11, 165, 252, 140, 6, 121, 57, 204, 91, 119, 165, 106, 241, 234, 131, 75,
180, 0, 1, 12, 0, 0, 0, 2, 0, 0, 0, 0, 152, 13, 0, 0, 0, 0, 0,
])
);
expect(executePostedVaaArgs?.header.targetChainId).toBe("pythnet");
expect(executePostedVaaArgs?.header.action).toBe("ExecutePostedVaa");
// Decode executePostVaa with one system instruction
expectedExecuteVaaArgs = {
targetChainId: "pythnet" as ChainName,
instructions: [
SystemProgram.transfer({
fromPubkey: new PublicKey(
"AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES"
),
toPubkey: new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj"),
lamports: 890880,
}),
] as TransactionInstruction[],
};
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs);
expect(
buffer.equals(
Buffer.from([
80, 84, 71, 77, 0, 0, 0, 26, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0,
0, 0, 141, 65, 8, 219, 216, 57, 229, 94, 74, 17, 138, 50, 121, 176, 38,
178, 50, 229, 210, 103, 232, 253, 133, 66, 14, 47, 228, 224, 162, 147,
232, 251, 1, 1, 252, 221, 21, 33, 156, 1, 72, 252, 246, 229, 150, 218,
109, 165, 127, 11, 165, 252, 140, 6, 121, 57, 204, 91, 119, 165, 106,
241, 234, 131, 75, 180, 0, 1, 12, 0, 0, 0, 2, 0, 0, 0, 0, 152, 13, 0, 0,
0, 0, 0,
])
)
).toBeTruthy();
executePostedVaaArgs = decodeExecutePostedVaa(buffer);
expect(executePostedVaaArgs?.targetChainId).toBe("pythnet");
expect(executePostedVaaArgs?.instructions.length).toBe(1);
expect(
executePostedVaaArgs?.instructions[0].programId.equals(

View File

@ -1,9 +1,15 @@
import { ChainId } from "@certusone/wormhole-sdk";
import { ChainId, ChainName } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout";
import { governanceHeaderLayout, PythGovernanceHeader, verifyHeader } from ".";
import {
encodeHeader,
governanceHeaderLayout,
PythGovernanceHeader,
verifyHeader,
} from ".";
import { Layout } from "@solana/buffer-layout";
import {
AccountMeta,
PACKET_DATA_SIZE,
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
@ -21,10 +27,10 @@ class Vector<T> extends Layout<T[]> {
return BufferLayout.seq(this.element, length).decode(b, (offset || 0) + 4);
}
encode(src: T[], b: Uint8Array, offset?: number | undefined): number {
return BufferLayout.struct<Readonly<{ length: number; src: T[] }>>([
return BufferLayout.struct<Readonly<{ length: number; elements: T[] }>>([
BufferLayout.u32("length"),
BufferLayout.seq(this.element, src.length, "elements"),
]).encode({ length: src.length, src }, b, offset);
]).encode({ length: src.length, elements: src }, b, offset);
}
getSpan(b: Buffer, offset?: number): number {
@ -72,7 +78,7 @@ export const executePostedVaaLayout: BufferLayout.Structure<
]);
export type ExecutePostedVaaArgs = {
header: PythGovernanceHeader;
targetChainId: ChainName;
instructions: TransactionInstruction[];
};
@ -103,5 +109,36 @@ export function decodeExecutePostedVaa(
}
);
return { header, instructions };
return { targetChainId: header.targetChainId, instructions };
}
/** Encode ExecutePostedVaaArgs */
export function encodeExecutePostedVaa(src: ExecutePostedVaaArgs): Buffer {
// PACKET_DATA_SIZE is the maximum transactin size of Solana, so our serialized payload will never be bigger than that
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
const offset = encodeHeader(
{ action: "ExecutePostedVaa", targetChainId: src.targetChainId },
buffer
);
let instructions: InstructionData[] = src.instructions.map((ix) => {
let programId = ix.programId.toBytes();
let accounts: AccountMetadata[] = ix.keys.map((acc) => {
return {
pubkey: acc.pubkey.toBytes(),
isSigner: acc.isSigner ? 1 : 0,
isWritable: acc.isWritable ? 1 : 0,
};
});
let data = [...ix.data];
return { programId, accounts, data };
});
const span =
offset +
new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
instructions,
buffer,
offset
);
return buffer.subarray(0, span);
}

View File

@ -1,18 +1,23 @@
import { ChainId, ChainName, toChainName } from "@certusone/wormhole-sdk";
import {
ChainId,
ChainName,
toChainId,
toChainName,
} from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout";
export declare const ExecutorAction: {
readonly ExecutePostedVaa: 0;
};
export const ExecutorAction = {
ExecutePostedVaa: 0,
} as const;
export declare const TargetAction: {
readonly UpgradeContract: 0;
readonly AuthorizeGovernanceDataSourceTransfer: 1;
readonly SetDataSources: 2;
readonly SetFee: 3;
readonly SetValidPeriod: 4;
readonly RequestGovernanceDataSourceTransfer: 5;
};
export const TargetAction = {
UpgradeContract: 0,
AuthorizeGovernanceDataSourceTransfer: 1,
SetDataSources: 2,
SetFee: 3,
SetValidPeriod: 4,
RequestGovernanceDataSourceTransfer: 5,
} as const;
export function toActionName(
deserialized: Readonly<{ moduleId: number; actionId: number }>
@ -75,6 +80,30 @@ export function decodeHeader(data: Buffer): PythGovernanceHeader | undefined {
return verifyHeader(deserialized);
}
export function encodeHeader(
src: PythGovernanceHeader,
buffer: Buffer
): number {
let module: number;
let action: number;
if (src.action in ExecutorAction) {
module = MODULE_EXECUTOR;
action = ExecutorAction[src.action as keyof typeof ExecutorAction];
} else {
module = MODULE_TARGET;
action = TargetAction[src.action as keyof typeof TargetAction];
}
return governanceHeaderLayout().encode(
{
magicNumber: MAGIC_NUMBER,
module,
action,
chain: toChainId(src.targetChainId),
},
buffer
);
}
export function verifyHeader(
deserialized: Readonly<{
magicNumber: number;