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
This commit is contained in:
guibescos 2023-01-06 14:47:30 -06:00 committed by GitHub
parent e12567c544
commit 5efa611a97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 24656 additions and 1 deletions

24224
xc-admin/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,5 @@
/** @type {import('ts-jest').JestConfigWithTsJest} */
module.exports = {
preset: "ts-jest",
testEnvironment: "node",
};

View File

@ -13,7 +13,25 @@
"bugs": { "bugs": {
"url": "https://github.com/pyth-network/pyth-crosschain/issues" "url": "https://github.com/pyth-network/pyth-crosschain/issues"
}, },
"scripts": {
"build": "tsc",
"format": "prettier --write \"src/**/*.ts\"",
"test": "jest"
},
"dependencies": { "dependencies": {
"@certusone/wormhole-sdk": "^0.9.8",
"@solana/buffer-layout": "^4.0.1",
"@solana/web3.js": "^1.73.0",
"@sqds/mesh": "^1.0.6",
"lodash": "^4.17.21",
"typescript": "^4.9.4" "typescript": "^4.9.4"
},
"devDependencies": {
"@types/bn.js": "^5.1.1",
"@types/jest": "^29.2.5",
"@types/lodash": "^4.14.191",
"jest": "^29.3.1",
"prettier": "^2.8.1",
"ts-jest": "^29.0.3"
} }
} }

View File

@ -0,0 +1,91 @@
import { PublicKey, SystemProgram } from "@solana/web3.js";
import { decodeExecutePostedVaa, decodeHeader } from "..";
test("GovernancePayload", (done) => {
jest.setTimeout(60000);
let governanceHeader = decodeHeader(
Buffer.from([80, 84, 71, 77, 0, 0, 0, 26, 0, 0, 0, 0])
);
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])
);
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])
);
expect(governanceHeader?.targetChainId).toBe("solana");
expect(governanceHeader?.action).toBe("SetFee");
// Wrong magic number
governanceHeader = decodeHeader(
Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0])
);
expect(governanceHeader).toBeUndefined();
// Wrong chain
governanceHeader = decodeHeader(
Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0])
);
expect(governanceHeader).toBeUndefined();
// Wrong module/action combination
governanceHeader = decodeHeader(
Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0])
);
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");
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");
expect(executePostedVaaArgs?.instructions.length).toBe(1);
expect(
executePostedVaaArgs?.instructions[0].programId.equals(
SystemProgram.programId
)
).toBeTruthy();
expect(
executePostedVaaArgs?.instructions[0].keys[0].pubkey.equals(
new PublicKey("AWQ18oKzd187aM2oMB4YirBcdgX1FgWfukmqEX91BRES")
)
).toBeTruthy();
expect(executePostedVaaArgs?.instructions[0].keys[0].isSigner).toBeTruthy();
expect(executePostedVaaArgs?.instructions[0].keys[0].isWritable).toBeTruthy();
expect(
executePostedVaaArgs?.instructions[0].keys[1].pubkey.equals(
new PublicKey("J25GT2knN8V2Wvg9jNrYBuj9SZdsLnU6bK7WCGrL7daj")
)
).toBeTruthy();
expect(!executePostedVaaArgs?.instructions[0].keys[1].isSigner).toBeTruthy();
expect(executePostedVaaArgs?.instructions[0].keys[1].isWritable).toBeTruthy();
expect(
executePostedVaaArgs?.instructions[0].data.equals(
Buffer.from([2, 0, 0, 0, 0, 152, 13, 0, 0, 0, 0, 0])
)
);
done();
});

View File

@ -0,0 +1,107 @@
import { ChainId } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout";
import { governanceHeaderLayout, PythGovernanceHeader, verifyHeader } from ".";
import { Layout } from "@solana/buffer-layout";
import {
AccountMeta,
PublicKey,
TransactionInstruction,
} from "@solana/web3.js";
class Vector<T> extends Layout<T[]> {
private element: Layout<T>;
constructor(element: Layout<T>, property?: string) {
super(-1, property);
this.element = element;
}
decode(b: Uint8Array, offset?: number | undefined): T[] {
const length = BufferLayout.u32().decode(b, offset);
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[] }>>([
BufferLayout.u32("length"),
BufferLayout.seq(this.element, src.length, "elements"),
]).encode({ length: src.length, src }, b, offset);
}
getSpan(b: Buffer, offset?: number): number {
const length = BufferLayout.u32().decode(b, offset);
return 4 + this.element.span * length;
}
}
export type InstructionData = {
programId: Uint8Array;
accounts: AccountMetadata[];
data: number[];
};
export type AccountMetadata = {
pubkey: Uint8Array;
isSigner: number;
isWritable: number;
};
export const accountMetaLayout = BufferLayout.struct<AccountMetadata>([
BufferLayout.blob(32, "pubkey"),
BufferLayout.u8("isSigner"),
BufferLayout.u8("isWritable"),
]);
export const instructionDataLayout = BufferLayout.struct<InstructionData>([
BufferLayout.blob(32, "programId"),
new Vector<AccountMetadata>(accountMetaLayout, "accounts"),
new Vector<number>(BufferLayout.u8(), "data"),
]);
export const executePostedVaaLayout: BufferLayout.Structure<
Readonly<{
header: Readonly<{
magicNumber: number;
module: number;
action: number;
chain: ChainId;
}>;
instructions: InstructionData[];
}>
> = BufferLayout.struct([
governanceHeaderLayout(),
new Vector<InstructionData>(instructionDataLayout, "instructions"),
]);
export type ExecutePostedVaaArgs = {
header: PythGovernanceHeader;
instructions: TransactionInstruction[];
};
/** Decode ExecutePostedVaaArgs and return undefined if it failed */
export function decodeExecutePostedVaa(
data: Buffer
): ExecutePostedVaaArgs | undefined {
let deserialized = executePostedVaaLayout.decode(data);
let header = verifyHeader(deserialized.header);
if (!header) {
return undefined;
}
let instructions: TransactionInstruction[] = deserialized.instructions.map(
(ix) => {
let programId: PublicKey = new PublicKey(ix.programId);
let keys: AccountMeta[] = ix.accounts.map((acc) => {
return {
pubkey: new PublicKey(acc.pubkey),
isSigner: Boolean(acc.isSigner),
isWritable: Boolean(acc.isWritable),
};
});
let data: Buffer = Buffer.from(ix.data);
return { programId, keys, data };
}
);
return { header, instructions };
}

View File

@ -0,0 +1,108 @@
import { ChainId, ChainName, toChainName } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout";
export declare const ExecutorAction: {
readonly ExecutePostedVaa: 0;
};
export declare const TargetAction: {
readonly UpgradeContract: 0;
readonly AuthorizeGovernanceDataSourceTransfer: 1;
readonly SetDataSources: 2;
readonly SetFee: 3;
readonly SetValidPeriod: 4;
readonly RequestGovernanceDataSourceTransfer: 5;
};
export function toActionName(
deserialized: Readonly<{ moduleId: number; actionId: number }>
): ActionName {
if (deserialized.moduleId == MODULE_EXECUTOR && deserialized.actionId == 0) {
return "ExecutePostedVaa";
} else if (deserialized.moduleId == MODULE_TARGET) {
switch (deserialized.actionId) {
case 0:
return "UpgradeContract";
case 1:
return "AuthorizeGovernanceDataSourceTransfer";
case 2:
return "SetDataSources";
case 3:
return "SetFee";
case 4:
return "SetValidPeriod";
case 5:
return "RequestGovernanceDataSourceTransfer";
}
}
throw new Error("Invalid header, action doesn't match module");
}
export declare type ActionName =
| keyof typeof ExecutorAction
| keyof typeof TargetAction;
export type PythGovernanceHeader = {
targetChainId: ChainName;
action: ActionName;
};
export const MAGIC_NUMBER = 0x4d475450;
export const MODULE_EXECUTOR = 0;
export const MODULE_TARGET = 1;
export function governanceHeaderLayout(): BufferLayout.Structure<
Readonly<{
magicNumber: number;
module: number;
action: number;
chain: ChainId;
}>
> {
return BufferLayout.struct(
[
BufferLayout.u32("magicNumber"),
BufferLayout.u8("module"),
BufferLayout.u8("action"),
BufferLayout.u16be("chain"),
],
"header"
);
}
/** Decode Pyth Governance Header and return undefined if the header is invalid */
export function decodeHeader(data: Buffer): PythGovernanceHeader | undefined {
let deserialized = governanceHeaderLayout().decode(data);
return verifyHeader(deserialized);
}
export function verifyHeader(
deserialized: Readonly<{
magicNumber: number;
module: number;
action: number;
chain: ChainId;
}>
) {
if (deserialized.magicNumber !== MAGIC_NUMBER) {
return undefined;
}
if (!toChainName(deserialized.chain)) {
return undefined;
}
try {
let governanceHeader: PythGovernanceHeader = {
targetChainId: toChainName(deserialized.chain),
action: toActionName({
actionId: deserialized.action,
moduleId: deserialized.module,
}),
};
return governanceHeader;
} catch {
return undefined;
}
}
export { decodeExecutePostedVaa } from "./ExecutePostedVaa";

View File

@ -0,0 +1,2 @@
export * from "./multisig";
export * from "./governance_payload";

View File

@ -0,0 +1,99 @@
import { PublicKey } from "@solana/web3.js";
import Squads, {
DEFAULT_MULTISIG_PROGRAM_ID,
getIxPDA,
getTxPDA,
} from "@sqds/mesh";
import { InstructionAccount, TransactionAccount } from "@sqds/mesh/lib/types";
import BN from "bn.js";
import lodash from "lodash";
/**
* Find all active proposals for vault `vault` using Squads client `squad`
* @param squad Squads client
* @param vault vault public key. It needs to exist in the instance of squads that `squad` is targeting
* @param offset (optional) ignore all proposals with `proposal_index < offset`
* @returns All the proposal accounts as `TransactionAccount`
*/
export async function getActiveProposals(
squad: Squads,
vault: PublicKey,
offset: number = 1
): Promise<TransactionAccount[]> {
const msAccount = await squad.getMultisig(vault);
let txKeys = lodash
.range(offset, msAccount.transactionIndex + 1)
.map((i) => getTxPDA(vault, new BN(i), DEFAULT_MULTISIG_PROGRAM_ID)[0]);
let msTransactions = await squad.getTransactions(txKeys);
return msTransactions
.filter(
(x: TransactionAccount | null): x is TransactionAccount => x != null
)
.filter((x) => lodash.isEqual(x.status, { active: {} }));
}
/**
* Get all the instructions for many proposals in one RPC call
* @param squad Squads client
* @param txAccounts transaction (proposal) accounts
* @returns `InstructionAccount[][]`, `result[0]` is the array of all the instructions from proposal 0
*/
export async function getManyProposalsInstructions(
squad: Squads,
txAccounts: TransactionAccount[]
): Promise<InstructionAccount[][]> {
let allIxsKeys = [];
let ownerTransaction = [];
for (let [index, txAccount] of txAccounts.entries()) {
let ixKeys = lodash
.range(1, txAccount.instructionIndex + 1)
.map(
(i) =>
getIxPDA(
txAccount.publicKey,
new BN(i),
DEFAULT_MULTISIG_PROGRAM_ID
)[0]
);
for (let ixKey of ixKeys) {
allIxsKeys.push(ixKey);
ownerTransaction.push(index);
}
}
let allTxIxsAccounts = await squad.getInstructions(allIxsKeys);
let ixAccountsByTx: InstructionAccount[][] = Array.from(
Array(txAccounts.length),
() => []
);
for (let i = 0; i < allTxIxsAccounts.length; i++) {
const toAdd = allTxIxsAccounts[i];
if (toAdd) {
ixAccountsByTx[ownerTransaction[i]].push(toAdd);
}
}
return ixAccountsByTx;
}
/**
* Get all the instructions for one proposal
* @param squad Squads client
* @param txAccount transaction (proposal) account
* @returns All the instructions of the proposal
*/
export async function getProposalInstructions(
squad: Squads,
txAccount: TransactionAccount
): Promise<InstructionAccount[]> {
let ixKeys = lodash
.range(1, txAccount.instructionIndex + 1)
.map(
(i) =>
getIxPDA(txAccount.publicKey, new BN(i), DEFAULT_MULTISIG_PROGRAM_ID)[0]
);
let txIxs = await squad.getInstructions(ixKeys);
return txIxs.filter(
(x: InstructionAccount | null): x is InstructionAccount => x != null
);
}

View File

@ -11,5 +11,6 @@
"resolveJsonModule": true, "resolveJsonModule": true,
"noErrorTruncation": true "noErrorTruncation": true
}, },
"include": ["src/**/*.ts"] "include": ["src/**/*.ts"],
"exclude": ["src/__tests__/"]
} }