Skip to content

Commit 5efa611

Browse files
authored
Xc admin/add wormhole message header deser (#461)
* Checkpoint * Checkpoint * Fix tests * Revert changes to remote executor * Fix precommit * Add comments * Solve typo * MetaData -> Metadata * Address feedback * Fix imports
1 parent e12567c commit 5efa611

File tree

9 files changed

+24656
-1
lines changed

9 files changed

+24656
-1
lines changed

xc-admin/package-lock.json

Lines changed: 24224 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
/** @type {import('ts-jest').JestConfigWithTsJest} */
2+
module.exports = {
3+
preset: "ts-jest",
4+
testEnvironment: "node",
5+
};

xc-admin/packages/xc-admin-common/package.json

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,25 @@
1313
"bugs": {
1414
"url": "https://github.com/pyth-network/pyth-crosschain/issues"
1515
},
16+
"scripts": {
17+
"build": "tsc",
18+
"format": "prettier --write \"src/**/*.ts\"",
19+
"test": "jest"
20+
},
1621
"dependencies": {
22+
"@certusone/wormhole-sdk": "^0.9.8",
23+
"@solana/buffer-layout": "^4.0.1",
24+
"@solana/web3.js": "^1.73.0",
25+
"@sqds/mesh": "^1.0.6",
26+
"lodash": "^4.17.21",
1727
"typescript": "^4.9.4"
28+
},
29+
"devDependencies": {
30+
"@types/bn.js": "^5.1.1",
31+
"@types/jest": "^29.2.5",
32+
"@types/lodash": "^4.14.191",
33+
"jest": "^29.3.1",
34+
"prettier": "^2.8.1",
35+
"ts-jest": "^29.0.3"
1836
}
1937
}
Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
import { PublicKey, SystemProgram } from "@solana/web3.js";
2+
import { decodeExecutePostedVaa, decodeHeader } from "..";
3+
4+
test("GovernancePayload", (done) => {
5+
jest.setTimeout(60000);
6+
7+
let governanceHeader = decodeHeader(
8+
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])
9+
);
10+
expect(governanceHeader?.targetChainId).toBe("pythnet");
11+
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
12+
13+
governanceHeader = decodeHeader(
14+
Buffer.from([80, 84, 71, 77, 0, 0, 0, 0, 0, 0, 0, 0])
15+
);
16+
expect(governanceHeader?.targetChainId).toBe("unset");
17+
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
18+
19+
governanceHeader = decodeHeader(
20+
Buffer.from([80, 84, 71, 77, 1, 3, 0, 1, 0, 0, 0, 0])
21+
);
22+
expect(governanceHeader?.targetChainId).toBe("solana");
23+
expect(governanceHeader?.action).toBe("SetFee");
24+
25+
// Wrong magic number
26+
governanceHeader = decodeHeader(
27+
Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0])
28+
);
29+
expect(governanceHeader).toBeUndefined();
30+
31+
// Wrong chain
32+
governanceHeader = decodeHeader(
33+
Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0])
34+
);
35+
expect(governanceHeader).toBeUndefined();
36+
37+
// Wrong module/action combination
38+
governanceHeader = decodeHeader(
39+
Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0])
40+
);
41+
expect(governanceHeader).toBeUndefined();
42+
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");
49+
expect(executePostedVaaArgs?.instructions.length).toBe(0);
50+
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");
64+
expect(executePostedVaaArgs?.instructions.length).toBe(1);
65+
expect(
66+
executePostedVaaArgs?.instructions[0].programId.equals(
67+
SystemProgram.programId
68+
)
69+
).toBeTruthy();
70+
expect(
71+
executePostedVaaArgs?.instructions[0].keys[0].pubkey.equals(
72+
new PublicKey("AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES")
73+
)
74+
).toBeTruthy();
75+
expect(executePostedVaaArgs?.instructions[0].keys[0].isSigner).toBeTruthy();
76+
expect(executePostedVaaArgs?.instructions[0].keys[0].isWritable).toBeTruthy();
77+
expect(
78+
executePostedVaaArgs?.instructions[0].keys[1].pubkey.equals(
79+
new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj")
80+
)
81+
).toBeTruthy();
82+
expect(!executePostedVaaArgs?.instructions[0].keys[1].isSigner).toBeTruthy();
83+
expect(executePostedVaaArgs?.instructions[0].keys[1].isWritable).toBeTruthy();
84+
expect(
85+
executePostedVaaArgs?.instructions[0].data.equals(
86+
Buffer.from([2, 0, 0, 0, 0, 152, 13, 0, 0, 0, 0, 0])
87+
)
88+
);
89+
90+
done();
91+
});
Lines changed: 107 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,107 @@
1+
import { ChainId } from "@certusone/wormhole-sdk";
2+
import * as BufferLayout from "@solana/buffer-layout";
3+
import { governanceHeaderLayout, PythGovernanceHeader, verifyHeader } from ".";
4+
import { Layout } from "@solana/buffer-layout";
5+
import {
6+
AccountMeta,
7+
PublicKey,
8+
TransactionInstruction,
9+
} from "@solana/web3.js";
10+
11+
class Vector<T> extends Layout<T[]> {
12+
private element: Layout<T>;
13+
14+
constructor(element: Layout<T>, property?: string) {
15+
super(-1, property);
16+
this.element = element;
17+
}
18+
19+
decode(b: Uint8Array, offset?: number | undefined): T[] {
20+
const length = BufferLayout.u32().decode(b, offset);
21+
return BufferLayout.seq(this.element, length).decode(b, (offset || 0) + 4);
22+
}
23+
encode(src: T[], b: Uint8Array, offset?: number | undefined): number {
24+
return BufferLayout.struct<Readonly<{ length: number; src: T[] }>>([
25+
BufferLayout.u32("length"),
26+
BufferLayout.seq(this.element, src.length, "elements"),
27+
]).encode({ length: src.length, src }, b, offset);
28+
}
29+
30+
getSpan(b: Buffer, offset?: number): number {
31+
const length = BufferLayout.u32().decode(b, offset);
32+
return 4 + this.element.span * length;
33+
}
34+
}
35+
36+
export type InstructionData = {
37+
programId: Uint8Array;
38+
accounts: AccountMetadata[];
39+
data: number[];
40+
};
41+
42+
export type AccountMetadata = {
43+
pubkey: Uint8Array;
44+
isSigner: number;
45+
isWritable: number;
46+
};
47+
48+
export const accountMetaLayout = BufferLayout.struct<AccountMetadata>([
49+
BufferLayout.blob(32, "pubkey"),
50+
BufferLayout.u8("isSigner"),
51+
BufferLayout.u8("isWritable"),
52+
]);
53+
export const instructionDataLayout = BufferLayout.struct<InstructionData>([
54+
BufferLayout.blob(32, "programId"),
55+
new Vector<AccountMetadata>(accountMetaLayout, "accounts"),
56+
new Vector<number>(BufferLayout.u8(), "data"),
57+
]);
58+
59+
export const executePostedVaaLayout: BufferLayout.Structure<
60+
Readonly<{
61+
header: Readonly<{
62+
magicNumber: number;
63+
module: number;
64+
action: number;
65+
chain: ChainId;
66+
}>;
67+
instructions: InstructionData[];
68+
}>
69+
> = BufferLayout.struct([
70+
governanceHeaderLayout(),
71+
new Vector<InstructionData>(instructionDataLayout, "instructions"),
72+
]);
73+
74+
export type ExecutePostedVaaArgs = {
75+
header: PythGovernanceHeader;
76+
instructions: TransactionInstruction[];
77+
};
78+
79+
/** Decode ExecutePostedVaaArgs and return undefined if it failed */
80+
export function decodeExecutePostedVaa(
81+
data: Buffer
82+
): ExecutePostedVaaArgs | undefined {
83+
let deserialized = executePostedVaaLayout.decode(data);
84+
85+
let header = verifyHeader(deserialized.header);
86+
87+
if (!header) {
88+
return undefined;
89+
}
90+
91+
let instructions: TransactionInstruction[] = deserialized.instructions.map(
92+
(ix) => {
93+
let programId: PublicKey = new PublicKey(ix.programId);
94+
let keys: AccountMeta[] = ix.accounts.map((acc) => {
95+
return {
96+
pubkey: new PublicKey(acc.pubkey),
97+
isSigner: Boolean(acc.isSigner),
98+
isWritable: Boolean(acc.isWritable),
99+
};
100+
});
101+
let data: Buffer = Buffer.from(ix.data);
102+
return { programId, keys, data };
103+
}
104+
);
105+
106+
return { header, instructions };
107+
}
Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,108 @@
1+
import { ChainId, ChainName, toChainName } from "@certusone/wormhole-sdk";
2+
import * as BufferLayout from "@solana/buffer-layout";
3+
4+
export declare const ExecutorAction: {
5+
readonly ExecutePostedVaa: 0;
6+
};
7+
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+
};
16+
17+
export function toActionName(
18+
deserialized: Readonly<{ moduleId: number; actionId: number }>
19+
): ActionName {
20+
if (deserialized.moduleId == MODULE_EXECUTOR && deserialized.actionId == 0) {
21+
return "ExecutePostedVaa";
22+
} else if (deserialized.moduleId == MODULE_TARGET) {
23+
switch (deserialized.actionId) {
24+
case 0:
25+
return "UpgradeContract";
26+
case 1:
27+
return "AuthorizeGovernanceDataSourceTransfer";
28+
case 2:
29+
return "SetDataSources";
30+
case 3:
31+
return "SetFee";
32+
case 4:
33+
return "SetValidPeriod";
34+
case 5:
35+
return "RequestGovernanceDataSourceTransfer";
36+
}
37+
}
38+
throw new Error("Invalid header, action doesn't match module");
39+
}
40+
export declare type ActionName =
41+
| keyof typeof ExecutorAction
42+
| keyof typeof TargetAction;
43+
44+
export type PythGovernanceHeader = {
45+
targetChainId: ChainName;
46+
action: ActionName;
47+
};
48+
49+
export const MAGIC_NUMBER = 0x4d475450;
50+
export const MODULE_EXECUTOR = 0;
51+
export const MODULE_TARGET = 1;
52+
53+
export function governanceHeaderLayout(): BufferLayout.Structure<
54+
Readonly<{
55+
magicNumber: number;
56+
module: number;
57+
action: number;
58+
chain: ChainId;
59+
}>
60+
> {
61+
return BufferLayout.struct(
62+
[
63+
BufferLayout.u32("magicNumber"),
64+
BufferLayout.u8("module"),
65+
BufferLayout.u8("action"),
66+
BufferLayout.u16be("chain"),
67+
],
68+
"header"
69+
);
70+
}
71+
72+
/** Decode Pyth Governance Header and return undefined if the header is invalid */
73+
export function decodeHeader(data: Buffer): PythGovernanceHeader | undefined {
74+
let deserialized = governanceHeaderLayout().decode(data);
75+
return verifyHeader(deserialized);
76+
}
77+
78+
export function verifyHeader(
79+
deserialized: Readonly<{
80+
magicNumber: number;
81+
module: number;
82+
action: number;
83+
chain: ChainId;
84+
}>
85+
) {
86+
if (deserialized.magicNumber !== MAGIC_NUMBER) {
87+
return undefined;
88+
}
89+
90+
if (!toChainName(deserialized.chain)) {
91+
return undefined;
92+
}
93+
94+
try {
95+
let governanceHeader: PythGovernanceHeader = {
96+
targetChainId: toChainName(deserialized.chain),
97+
action: toActionName({
98+
actionId: deserialized.action,
99+
moduleId: deserialized.module,
100+
}),
101+
};
102+
return governanceHeader;
103+
} catch {
104+
return undefined;
105+
}
106+
}
107+
108+
export { decodeExecutePostedVaa } from "./ExecutePostedVaa";
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
export * from "./multisig";
2+
export * from "./governance_payload";

0 commit comments

Comments
 (0)