Explorar el Código

[xc-admin] Add encoder for governance messages (#462)

* Add encoder

* Cleanup

* Update test

* Ci

* CI

* Cleanup

* More cleanup
guibescos hace 2 años
padre
commit
097943f657

+ 18 - 0
.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

+ 3 - 0
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"
   }

+ 90 - 31
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(

+ 43 - 6
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<T> extends Layout<T[]> {
     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[] }>>([
+    return BufferLayout.struct<Readonly<{ length: number; elements: T[] }>>([
       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<InstructionData>(instructionDataLayout, "instructions").encode(
+      instructions,
+      buffer,
+      offset
+    );
+  return buffer.subarray(0, span);
 }

+ 41 - 12
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;