Skip to content

Commit 097943f

Browse files
authored
[xc-admin] Add encoder for governance messages (#462)
* Add encoder * Cleanup * Update test * Ci * CI * Cleanup * More cleanup
1 parent 5efa611 commit 097943f

File tree

5 files changed

+195
-49
lines changed

5 files changed

+195
-49
lines changed

.github/workflows/xc-admin.yaml

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
name: Check Xc Admin
2+
on:
3+
pull_request:
4+
paths: [xc-admin/**]
5+
push:
6+
branches: [main]
7+
paths: [xc-admin/**]
8+
jobs:
9+
test:
10+
runs-on: ubuntu-latest
11+
defaults:
12+
run:
13+
working-directory: xc-admin/
14+
steps:
15+
- uses: actions/checkout@v2
16+
- name: Run xc-admin tests
17+
run: |
18+
npm ci && npm run test

xc-admin/package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@
44
"workspaces": [
55
"packages/*"
66
],
7+
"scripts": {
8+
"test": "cd packages/xc-admin-common && npm run test"
9+
},
710
"devDependencies": {
811
"lerna": "^6.3.0"
912
}

xc-admin/packages/xc-admin-common/src/__tests__/GovernancePayload.test.ts

Lines changed: 90 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,61 @@
1-
import { PublicKey, SystemProgram } from "@solana/web3.js";
2-
import { decodeExecutePostedVaa, decodeHeader } from "..";
1+
import { ChainName } from "@certusone/wormhole-sdk";
2+
import {
3+
PACKET_DATA_SIZE,
4+
PublicKey,
5+
SystemProgram,
6+
TransactionInstruction,
7+
} from "@solana/web3.js";
8+
import {
9+
ActionName,
10+
decodeExecutePostedVaa,
11+
decodeHeader,
12+
encodeHeader,
13+
} from "..";
14+
import { encodeExecutePostedVaa } from "../governance_payload/ExecutePostedVaa";
315

4-
test("GovernancePayload", (done) => {
16+
test("GovernancePayload ser/de", (done) => {
517
jest.setTimeout(60000);
618

7-
let governanceHeader = decodeHeader(
8-
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])
9-
);
19+
// Valid header 1
20+
let expectedGovernanceHeader = {
21+
targetChainId: "pythnet" as ChainName,
22+
action: "ExecutePostedVaa" as ActionName,
23+
};
24+
let buffer = Buffer.alloc(PACKET_DATA_SIZE);
25+
let span = encodeHeader(expectedGovernanceHeader, buffer);
26+
expect(
27+
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26]))
28+
).toBeTruthy();
29+
30+
let governanceHeader = decodeHeader(buffer.subarray(0, span));
1031
expect(governanceHeader?.targetChainId).toBe("pythnet");
1132
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
1233

13-
governanceHeader = decodeHeader(
14-
Buffer.from([80, 84, 71, 77, 0, 0, 0, 0, 0, 0, 0, 0])
15-
);
34+
// Valid header 2
35+
expectedGovernanceHeader = {
36+
targetChainId: "unset" as ChainName,
37+
action: "ExecutePostedVaa" as ActionName,
38+
};
39+
buffer = Buffer.alloc(PACKET_DATA_SIZE);
40+
span = encodeHeader(expectedGovernanceHeader, buffer);
41+
expect(
42+
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 0]))
43+
).toBeTruthy();
44+
governanceHeader = decodeHeader(buffer.subarray(0, span));
1645
expect(governanceHeader?.targetChainId).toBe("unset");
1746
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
1847

19-
governanceHeader = decodeHeader(
20-
Buffer.from([80, 84, 71, 77, 1, 3, 0, 1, 0, 0, 0, 0])
21-
);
48+
// Valid header 3
49+
expectedGovernanceHeader = {
50+
targetChainId: "solana" as ChainName,
51+
action: "SetFee" as ActionName,
52+
};
53+
buffer = Buffer.alloc(PACKET_DATA_SIZE);
54+
span = encodeHeader(expectedGovernanceHeader, buffer);
55+
expect(
56+
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 1, 3, 0, 1]))
57+
).toBeTruthy();
58+
governanceHeader = decodeHeader(buffer.subarray(0, span));
2259
expect(governanceHeader?.targetChainId).toBe("solana");
2360
expect(governanceHeader?.action).toBe("SetFee");
2461

@@ -40,27 +77,49 @@ test("GovernancePayload", (done) => {
4077
);
4178
expect(governanceHeader).toBeUndefined();
4279

43-
// Decode executePostVaa
44-
let executePostedVaaArgs = decodeExecutePostedVaa(
45-
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])
46-
);
47-
expect(executePostedVaaArgs?.header.targetChainId).toBe("pythnet");
48-
expect(executePostedVaaArgs?.header.action).toBe("ExecutePostedVaa");
80+
// Decode executePostVaa with empty instructions
81+
let expectedExecuteVaaArgs = {
82+
targetChainId: "pythnet" as ChainName,
83+
instructions: [] as TransactionInstruction[],
84+
};
85+
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs);
86+
expect(
87+
buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0]))
88+
).toBeTruthy();
89+
let executePostedVaaArgs = decodeExecutePostedVaa(buffer);
90+
expect(executePostedVaaArgs?.targetChainId).toBe("pythnet");
4991
expect(executePostedVaaArgs?.instructions.length).toBe(0);
5092

51-
executePostedVaaArgs = decodeExecutePostedVaa(
52-
Buffer.from([
53-
80, 84, 71, 77, 0, 0, 0, 26, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
54-
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,
55-
141, 65, 8, 219, 216, 57, 229, 94, 74, 17, 138, 50, 121, 176, 38, 178, 50,
56-
229, 210, 103, 232, 253, 133, 66, 14, 47, 228, 224, 162, 147, 232, 251, 1,
57-
1, 252, 221, 21, 33, 156, 1, 72, 252, 246, 229, 150, 218, 109, 165, 127,
58-
11, 165, 252, 140, 6, 121, 57, 204, 91, 119, 165, 106, 241, 234, 131, 75,
59-
180, 0, 1, 12, 0, 0, 0, 2, 0, 0, 0, 0, 152, 13, 0, 0, 0, 0, 0,
60-
])
61-
);
62-
expect(executePostedVaaArgs?.header.targetChainId).toBe("pythnet");
63-
expect(executePostedVaaArgs?.header.action).toBe("ExecutePostedVaa");
93+
// Decode executePostVaa with one system instruction
94+
expectedExecuteVaaArgs = {
95+
targetChainId: "pythnet" as ChainName,
96+
instructions: [
97+
SystemProgram.transfer({
98+
fromPubkey: new PublicKey(
99+
"AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES"
100+
),
101+
toPubkey: new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj"),
102+
lamports: 890880,
103+
}),
104+
] as TransactionInstruction[],
105+
};
106+
buffer = encodeExecutePostedVaa(expectedExecuteVaaArgs);
107+
expect(
108+
buffer.equals(
109+
Buffer.from([
110+
80, 84, 71, 77, 0, 0, 0, 26, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
111+
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0,
112+
0, 0, 141, 65, 8, 219, 216, 57, 229, 94, 74, 17, 138, 50, 121, 176, 38,
113+
178, 50, 229, 210, 103, 232, 253, 133, 66, 14, 47, 228, 224, 162, 147,
114+
232, 251, 1, 1, 252, 221, 21, 33, 156, 1, 72, 252, 246, 229, 150, 218,
115+
109, 165, 127, 11, 165, 252, 140, 6, 121, 57, 204, 91, 119, 165, 106,
116+
241, 234, 131, 75, 180, 0, 1, 12, 0, 0, 0, 2, 0, 0, 0, 0, 152, 13, 0, 0,
117+
0, 0, 0,
118+
])
119+
)
120+
).toBeTruthy();
121+
executePostedVaaArgs = decodeExecutePostedVaa(buffer);
122+
expect(executePostedVaaArgs?.targetChainId).toBe("pythnet");
64123
expect(executePostedVaaArgs?.instructions.length).toBe(1);
65124
expect(
66125
executePostedVaaArgs?.instructions[0].programId.equals(

xc-admin/packages/xc-admin-common/src/governance_payload/ExecutePostedVaa.ts

Lines changed: 43 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,15 @@
1-
import { ChainId } from "@certusone/wormhole-sdk";
1+
import { ChainId, ChainName } from "@certusone/wormhole-sdk";
22
import * as BufferLayout from "@solana/buffer-layout";
3-
import { governanceHeaderLayout, PythGovernanceHeader, verifyHeader } from ".";
3+
import {
4+
encodeHeader,
5+
governanceHeaderLayout,
6+
PythGovernanceHeader,
7+
verifyHeader,
8+
} from ".";
49
import { Layout } from "@solana/buffer-layout";
510
import {
611
AccountMeta,
12+
PACKET_DATA_SIZE,
713
PublicKey,
814
TransactionInstruction,
915
} from "@solana/web3.js";
@@ -21,10 +27,10 @@ class Vector<T> extends Layout<T[]> {
2127
return BufferLayout.seq(this.element, length).decode(b, (offset || 0) + 4);
2228
}
2329
encode(src: T[], b: Uint8Array, offset?: number | undefined): number {
24-
return BufferLayout.struct<Readonly<{ length: number; src: T[] }>>([
30+
return BufferLayout.struct<Readonly<{ length: number; elements: T[] }>>([
2531
BufferLayout.u32("length"),
2632
BufferLayout.seq(this.element, src.length, "elements"),
27-
]).encode({ length: src.length, src }, b, offset);
33+
]).encode({ length: src.length, elements: src }, b, offset);
2834
}
2935

3036
getSpan(b: Buffer, offset?: number): number {
@@ -72,7 +78,7 @@ export const executePostedVaaLayout: BufferLayout.Structure<
7278
]);
7379

7480
export type ExecutePostedVaaArgs = {
75-
header: PythGovernanceHeader;
81+
targetChainId: ChainName;
7682
instructions: TransactionInstruction[];
7783
};
7884

@@ -103,5 +109,36 @@ export function decodeExecutePostedVaa(
103109
}
104110
);
105111

106-
return { header, instructions };
112+
return { targetChainId: header.targetChainId, instructions };
113+
}
114+
115+
/** Encode ExecutePostedVaaArgs */
116+
export function encodeExecutePostedVaa(src: ExecutePostedVaaArgs): Buffer {
117+
// PACKET_DATA_SIZE is the maximum transactin size of Solana, so our serialized payload will never be bigger than that
118+
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
119+
const offset = encodeHeader(
120+
{ action: "ExecutePostedVaa", targetChainId: src.targetChainId },
121+
buffer
122+
);
123+
let instructions: InstructionData[] = src.instructions.map((ix) => {
124+
let programId = ix.programId.toBytes();
125+
let accounts: AccountMetadata[] = ix.keys.map((acc) => {
126+
return {
127+
pubkey: acc.pubkey.toBytes(),
128+
isSigner: acc.isSigner ? 1 : 0,
129+
isWritable: acc.isWritable ? 1 : 0,
130+
};
131+
});
132+
let data = [...ix.data];
133+
return { programId, accounts, data };
134+
});
135+
136+
const span =
137+
offset +
138+
new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
139+
instructions,
140+
buffer,
141+
offset
142+
);
143+
return buffer.subarray(0, span);
107144
}

xc-admin/packages/xc-admin-common/src/governance_payload/index.ts

Lines changed: 41 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,23 @@
1-
import { ChainId, ChainName, toChainName } from "@certusone/wormhole-sdk";
1+
import {
2+
ChainId,
3+
ChainName,
4+
toChainId,
5+
toChainName,
6+
} from "@certusone/wormhole-sdk";
27
import * as BufferLayout from "@solana/buffer-layout";
38

4-
export declare const ExecutorAction: {
5-
readonly ExecutePostedVaa: 0;
6-
};
9+
export const ExecutorAction = {
10+
ExecutePostedVaa: 0,
11+
} as const;
712

8-
export declare const TargetAction: {
9-
readonly UpgradeContract: 0;
10-
readonly AuthorizeGovernanceDataSourceTransfer: 1;
11-
readonly SetDataSources: 2;
12-
readonly SetFee: 3;
13-
readonly SetValidPeriod: 4;
14-
readonly RequestGovernanceDataSourceTransfer: 5;
15-
};
13+
export const TargetAction = {
14+
UpgradeContract: 0,
15+
AuthorizeGovernanceDataSourceTransfer: 1,
16+
SetDataSources: 2,
17+
SetFee: 3,
18+
SetValidPeriod: 4,
19+
RequestGovernanceDataSourceTransfer: 5,
20+
} as const;
1621

1722
export function toActionName(
1823
deserialized: Readonly<{ moduleId: number; actionId: number }>
@@ -75,6 +80,30 @@ export function decodeHeader(data: Buffer): PythGovernanceHeader | undefined {
7580
return verifyHeader(deserialized);
7681
}
7782

83+
export function encodeHeader(
84+
src: PythGovernanceHeader,
85+
buffer: Buffer
86+
): number {
87+
let module: number;
88+
let action: number;
89+
if (src.action in ExecutorAction) {
90+
module = MODULE_EXECUTOR;
91+
action = ExecutorAction[src.action as keyof typeof ExecutorAction];
92+
} else {
93+
module = MODULE_TARGET;
94+
action = TargetAction[src.action as keyof typeof TargetAction];
95+
}
96+
return governanceHeaderLayout().encode(
97+
{
98+
magicNumber: MAGIC_NUMBER,
99+
module,
100+
action,
101+
chain: toChainId(src.targetChainId),
102+
},
103+
buffer
104+
);
105+
}
106+
78107
export function verifyHeader(
79108
deserialized: Readonly<{
80109
magicNumber: number;

0 commit comments

Comments
 (0)