From 097943f657ad7cbc69d65afba81a6fb536c7cad7 Mon Sep 17 00:00:00 2001 From: guibescos <59208140+guibescos@users.noreply.github.com> Date: Mon, 9 Jan 2023 15:54:54 -0600 Subject: [PATCH] [xc-admin] Add encoder for governance messages (#462) * Add encoder * Cleanup * Update test * Ci * CI * Cleanup * More cleanup --- .github/workflows/xc-admin.yaml | 18 +++ xc-admin/package.json | 3 + .../src/__tests__/GovernancePayload.test.ts | 121 +++++++++++++----- .../governance_payload/ExecutePostedVaa.ts | 49 ++++++- .../src/governance_payload/index.ts | 53 ++++++-- 5 files changed, 195 insertions(+), 49 deletions(-) create mode 100644 .github/workflows/xc-admin.yaml diff --git a/.github/workflows/xc-admin.yaml b/.github/workflows/xc-admin.yaml new file mode 100644 index 00000000..1fc19992 --- /dev/null +++ b/.github/workflows/xc-admin.yaml @@ -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 diff --git a/xc-admin/package.json b/xc-admin/package.json index f973f5c7..9268db58 100644 --- a/xc-admin/package.json +++ b/xc-admin/package.json @@ -4,6 +4,9 @@ "workspaces": [ "packages/*" ], + "scripts": { + "test": "cd packages/xc-admin-common && npm run test" + }, "devDependencies": { "lerna": "^6.3.0" } diff --git a/xc-admin/packages/xc-admin-common/src/__tests__/GovernancePayload.test.ts b/xc-admin/packages/xc-admin-common/src/__tests__/GovernancePayload.test.ts index 99b0aba7..8dd8b908 100644 --- a/xc-admin/packages/xc-admin-common/src/__tests__/GovernancePayload.test.ts +++ b/xc-admin/packages/xc-admin-common/src/__tests__/GovernancePayload.test.ts @@ -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( diff --git a/xc-admin/packages/xc-admin-common/src/governance_payload/ExecutePostedVaa.ts b/xc-admin/packages/xc-admin-common/src/governance_payload/ExecutePostedVaa.ts index b1d1d78e..e89c0287 100644 --- a/xc-admin/packages/xc-admin-common/src/governance_payload/ExecutePostedVaa.ts +++ b/xc-admin/packages/xc-admin-common/src/governance_payload/ExecutePostedVaa.ts @@ -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 extends Layout { return BufferLayout.seq(this.element, length).decode(b, (offset || 0) + 4); } encode(src: T[], b: Uint8Array, offset?: number | undefined): number { - return BufferLayout.struct>([ + return BufferLayout.struct>([ 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(instructionDataLayout, "instructions").encode( + instructions, + buffer, + offset + ); + return buffer.subarray(0, span); } diff --git a/xc-admin/packages/xc-admin-common/src/governance_payload/index.ts b/xc-admin/packages/xc-admin-common/src/governance_payload/index.ts index f8a9bebb..0f589968 100644 --- a/xc-admin/packages/xc-admin-common/src/governance_payload/index.ts +++ b/xc-admin/packages/xc-admin-common/src/governance_payload/index.ts @@ -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;