[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": [ "workspaces": [
"packages/*" "packages/*"
], ],
"scripts": {
"test": "cd packages/xc-admin-common && npm run test"
},
"devDependencies": { "devDependencies": {
"lerna": "^6.3.0" "lerna": "^6.3.0"
} }

View File

@ -1,24 +1,61 @@
import { PublicKey, SystemProgram } from "@solana/web3.js"; import { ChainName } from "@certusone/wormhole-sdk";
import { decodeExecutePostedVaa, decodeHeader } from ".."; 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); jest.setTimeout(60000);
let governanceHeader = decodeHeader( // Valid header 1
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0]) 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?.targetChainId).toBe("pythnet");
expect(governanceHeader?.action).toBe("ExecutePostedVaa"); expect(governanceHeader?.action).toBe("ExecutePostedVaa");
governanceHeader = decodeHeader( // Valid header 2
Buffer.from([80, 84, 71, 77, 0, 0, 0, 0, 0, 0, 0, 0]) 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?.targetChainId).toBe("unset");
expect(governanceHeader?.action).toBe("ExecutePostedVaa"); expect(governanceHeader?.action).toBe("ExecutePostedVaa");
governanceHeader = decodeHeader( // Valid header 3
Buffer.from([80, 84, 71, 77, 1, 3, 0, 1, 0, 0, 0, 0]) 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?.targetChainId).toBe("solana");
expect(governanceHeader?.action).toBe("SetFee"); expect(governanceHeader?.action).toBe("SetFee");
@ -40,27 +77,49 @@ test("GovernancePayload", (done) => {
); );
expect(governanceHeader).toBeUndefined(); expect(governanceHeader).toBeUndefined();
// Decode executePostVaa // Decode executePostVaa with empty instructions
let executePostedVaaArgs = decodeExecutePostedVaa( let expectedExecuteVaaArgs = {
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0]) targetChainId: "pythnet" as ChainName,
); instructions: [] as TransactionInstruction[],
expect(executePostedVaaArgs?.header.targetChainId).toBe("pythnet"); };
expect(executePostedVaaArgs?.header.action).toBe("ExecutePostedVaa"); 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); expect(executePostedVaaArgs?.instructions.length).toBe(0);
executePostedVaaArgs = decodeExecutePostedVaa( // Decode executePostVaa with one system instruction
Buffer.from([ expectedExecuteVaaArgs = {
80, 84, 71, 77, 0, 0, 0, 26, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, targetChainId: "pythnet" as ChainName,
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, instructions: [
141, 65, 8, 219, 216, 57, 229, 94, 74, 17, 138, 50, 121, 176, 38, 178, 50, SystemProgram.transfer({
229, 210, 103, 232, 253, 133, 66, 14, 47, 228, 224, 162, 147, 232, 251, 1, fromPubkey: new PublicKey(
1, 252, 221, 21, 33, 156, 1, 72, 252, 246, 229, 150, 218, 109, 165, 127, "AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES"
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, toPubkey: new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj"),
]) lamports: 890880,
); }),
expect(executePostedVaaArgs?.header.targetChainId).toBe("pythnet"); ] as TransactionInstruction[],
expect(executePostedVaaArgs?.header.action).toBe("ExecutePostedVaa"); };
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.length).toBe(1);
expect( expect(
executePostedVaaArgs?.instructions[0].programId.equals( 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 * as BufferLayout from "@solana/buffer-layout";
import { governanceHeaderLayout, PythGovernanceHeader, verifyHeader } from "."; import {
encodeHeader,
governanceHeaderLayout,
PythGovernanceHeader,
verifyHeader,
} from ".";
import { Layout } from "@solana/buffer-layout"; import { Layout } from "@solana/buffer-layout";
import { import {
AccountMeta, AccountMeta,
PACKET_DATA_SIZE,
PublicKey, PublicKey,
TransactionInstruction, TransactionInstruction,
} from "@solana/web3.js"; } 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); return BufferLayout.seq(this.element, length).decode(b, (offset || 0) + 4);
} }
encode(src: T[], b: Uint8Array, offset?: number | undefined): number { 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.u32("length"),
BufferLayout.seq(this.element, src.length, "elements"), 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 { getSpan(b: Buffer, offset?: number): number {
@ -72,7 +78,7 @@ export const executePostedVaaLayout: BufferLayout.Structure<
]); ]);
export type ExecutePostedVaaArgs = { export type ExecutePostedVaaArgs = {
header: PythGovernanceHeader; targetChainId: ChainName;
instructions: TransactionInstruction[]; 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"; import * as BufferLayout from "@solana/buffer-layout";
export declare const ExecutorAction: { export const ExecutorAction = {
readonly ExecutePostedVaa: 0; ExecutePostedVaa: 0,
}; } as const;
export declare const TargetAction: { export const TargetAction = {
readonly UpgradeContract: 0; UpgradeContract: 0,
readonly AuthorizeGovernanceDataSourceTransfer: 1; AuthorizeGovernanceDataSourceTransfer: 1,
readonly SetDataSources: 2; SetDataSources: 2,
readonly SetFee: 3; SetFee: 3,
readonly SetValidPeriod: 4; SetValidPeriod: 4,
readonly RequestGovernanceDataSourceTransfer: 5; RequestGovernanceDataSourceTransfer: 5,
}; } as const;
export function toActionName( export function toActionName(
deserialized: Readonly<{ moduleId: number; actionId: number }> deserialized: Readonly<{ moduleId: number; actionId: number }>
@ -75,6 +80,30 @@ export function decodeHeader(data: Buffer): PythGovernanceHeader | undefined {
return verifyHeader(deserialized); 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( export function verifyHeader(
deserialized: Readonly<{ deserialized: Readonly<{
magicNumber: number; magicNumber: number;