Guillermo Bescos Alapont 2 лет назад
Родитель
Сommit
49eedfbd74

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

@@ -1,66 +1,60 @@
-import { ChainName } from "@certusone/wormhole-sdk";
-import { PACKET_DATA_SIZE, PublicKey, SystemProgram } from "@solana/web3.js";
-import { ActionName, decodeHeader, encodeHeader, ExecutePostedVaa } from "..";
+import { PublicKey, SystemProgram } from "@solana/web3.js";
+import { PythGovernanceHeader, ExecutePostedVaa } from "..";
 
 test("GovernancePayload ser/de", (done) => {
   jest.setTimeout(60000);
 
   // 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);
+  let expectedGovernanceHeader = new PythGovernanceHeader(
+    "pythnet",
+    "ExecutePostedVaa"
+  );
+  let buffer = expectedGovernanceHeader.encode();
   expect(
-    buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26]))
+    buffer.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");
+  let governanceHeader = PythGovernanceHeader.decode(buffer);
+  expect(governanceHeader.targetChainId).toBe("pythnet");
+  expect(governanceHeader.action).toBe("ExecutePostedVaa");
 
   // 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));
+  expectedGovernanceHeader = new PythGovernanceHeader(
+    "unset",
+    "ExecutePostedVaa"
+  );
+  buffer = expectedGovernanceHeader.encode();
+  expect(buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 0]))).toBeTruthy();
+  governanceHeader = PythGovernanceHeader.decode(buffer);
   expect(governanceHeader?.targetChainId).toBe("unset");
   expect(governanceHeader?.action).toBe("ExecutePostedVaa");
 
   // 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));
+  expectedGovernanceHeader = new PythGovernanceHeader("solana", "SetFee");
+  buffer = expectedGovernanceHeader.encode();
+  expect(buffer.equals(Buffer.from([80, 84, 71, 77, 1, 3, 0, 1]))).toBeTruthy();
+  governanceHeader = PythGovernanceHeader.decode(buffer);
   expect(governanceHeader?.targetChainId).toBe("solana");
   expect(governanceHeader?.action).toBe("SetFee");
 
   // Wrong magic number
   expect(() =>
-    decodeHeader(Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0]))
+    PythGovernanceHeader.decode(
+      Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0])
+    )
   ).toThrow("Wrong magic number");
 
   // Wrong chain
   expect(() =>
-    decodeHeader(Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0]))
+    PythGovernanceHeader.decode(
+      Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0])
+    )
   ).toThrow("Chain Id not found");
 
   // Wrong module/action combination
   expect(() =>
-    decodeHeader(Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0]))
+    PythGovernanceHeader.decode(
+      Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0])
+    )
   ).toThrow("Invalid header, action doesn't match module");
 
   // Decode executePostVaa with empty instructions

+ 5 - 2
xc-admin/packages/xc-admin-common/src/__tests__/WormholeMultisigInstruction.test.ts

@@ -46,6 +46,9 @@ test("Wormhole multisig instruction parse: send message without governance paylo
     .instruction()
     .then((instruction) => {
       const parsedInstruction = parser.parseInstruction(instruction);
+      expect(
+        parsedInstruction instanceof WormholeMultisigInstruction
+      ).toBeTruthy();
       if (parsedInstruction instanceof WormholeMultisigInstruction) {
         expect(parsedInstruction.program).toBe(
           MultisigInstructionProgram.WormholeBridge
@@ -158,7 +161,7 @@ test("Wormhole multisig instruction parse: send message without governance paylo
         expect(parsedInstruction.args.targetChain).toBeUndefined();
         done();
       } else {
-        done("Not instance of WormholeMultisigInstruction");
+        done("Not instance of WormholeInstruction");
       }
     });
 });
@@ -351,7 +354,7 @@ test("Wormhole multisig instruction parse: send message with governance payload"
           done("Not instance of ExecutePostedVaa");
         }
       } else {
-        done("Not instance of WormholeMultisigInstruction");
+        done("Not instance of WormholeInstruction");
       }
     });
 });

+ 43 - 59
xc-admin/packages/xc-admin-common/src/governance_payload/ExecutePostedVaa.ts

@@ -1,11 +1,6 @@
 import { ChainId, ChainName } from "@certusone/wormhole-sdk";
 import * as BufferLayout from "@solana/buffer-layout";
-import {
-  encodeHeader,
-  governanceHeaderLayout,
-  PythGovernanceAction,
-  verifyHeader,
-} from ".";
+import { PythGovernanceAction, PythGovernanceHeader } from ".";
 import { Layout } from "@solana/buffer-layout";
 import {
   AccountMeta,
@@ -14,6 +9,7 @@ import {
   TransactionInstruction,
 } from "@solana/web3.js";
 
+/** Borsh type vector with a 4 byte vector length and then the serialized elements */
 class Vector<T> extends Layout<T[]> {
   private element: Layout<T>;
 
@@ -39,47 +35,42 @@ class Vector<T> extends Layout<T[]> {
   }
 }
 
-export type InstructionData = {
-  programId: Uint8Array;
-  accounts: AccountMetadata[];
-  data: number[];
-};
-
+/** Version of `AccountMeta` that works with buffer-layout */
 export type AccountMetadata = {
   pubkey: Uint8Array;
   isSigner: number;
   isWritable: number;
 };
 
+/** Version of `TransactionInstruction` that works with buffer-layout */
+export type InstructionData = {
+  programId: Uint8Array;
+  accounts: AccountMetadata[];
+  data: number[];
+};
+
+/** Layout for `AccountMetadata` */
 export const accountMetaLayout = BufferLayout.struct<AccountMetadata>([
   BufferLayout.blob(32, "pubkey"),
   BufferLayout.u8("isSigner"),
   BufferLayout.u8("isWritable"),
 ]);
+
+/** Layout for `InstructionData` */
 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"),
-]);
-
+/** A governance action used for executing remote instructions in Pythnet */
 export class ExecutePostedVaa implements PythGovernanceAction {
   readonly targetChainId: ChainName;
   readonly instructions: TransactionInstruction[];
+  static layout: Vector<InstructionData> = new Vector<InstructionData>(
+    instructionDataLayout,
+    "instructions"
+  );
 
   constructor(
     targetChainId: ChainName,
@@ -89,37 +80,36 @@ export class ExecutePostedVaa implements PythGovernanceAction {
     this.instructions = instructions;
   }
 
-  /** Decode ExecutePostedVaaArgs */
+  /** Decode ExecutePostedVaa */
   static decode(data: Buffer): ExecutePostedVaa {
-    let deserialized = executePostedVaaLayout.decode(data);
-
-    let header = verifyHeader(deserialized.header);
-
-    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 };
-      }
+    let header = PythGovernanceHeader.decode(data);
+    let deserialized = this.layout.decode(
+      data.subarray(PythGovernanceHeader.span)
     );
+    let instructions: TransactionInstruction[] = deserialized.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 new ExecutePostedVaa(header.targetChainId, instructions);
   }
 
-  /** Encode ExecutePostedVaaArgs */
+  /** Encode ExecutePostedVaa */
   encode(): Buffer {
-    // PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload will never be bigger than that
+    const headerBuffer = new PythGovernanceHeader(
+      this.targetChainId,
+      "ExecutePostedVaa"
+    ).encode();
+
+    // The code will crash if the payload is actually bigger than PACKET_DATA_SIZE. But PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload should never be bigger than this anyway
     const buffer = Buffer.alloc(PACKET_DATA_SIZE);
-    const offset = encodeHeader(
-      { action: "ExecutePostedVaa", targetChainId: this.targetChainId },
-      buffer
-    );
     let instructions: InstructionData[] = this.instructions.map((ix) => {
       let programId = ix.programId.toBytes();
       let accounts: AccountMetadata[] = ix.keys.map((acc) => {
@@ -133,13 +123,7 @@ export class ExecutePostedVaa implements PythGovernanceAction {
       return { programId, accounts, data };
     });
 
-    const span =
-      offset +
-      new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
-        instructions,
-        buffer,
-        offset
-      );
-    return buffer.subarray(0, span);
+    const span = ExecutePostedVaa.layout.encode(instructions, buffer);
+    return Buffer.concat([headerBuffer, buffer.subarray(0, span)]);
   }
 }

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

@@ -5,6 +5,7 @@ import {
   toChainName,
 } from "@certusone/wormhole-sdk";
 import * as BufferLayout from "@solana/buffer-layout";
+import { PACKET_DATA_SIZE } from "@solana/web3.js";
 import { ExecutePostedVaa } from "./ExecutePostedVaa";
 
 export interface PythGovernanceAction {
@@ -12,10 +13,17 @@ export interface PythGovernanceAction {
   encode(): Buffer;
 }
 
+/** Magic number */
+export const MAGIC_NUMBER = 0x4d475450;
+
+export const MODULE_EXECUTOR = 0;
+/** Each of the actions that can be directed to the Executor Module */
 export const ExecutorAction = {
   ExecutePostedVaa: 0,
 } as const;
 
+/** Each of the actions that can be directed to the Target Module */
+export const MODULE_TARGET = 1;
 export const TargetAction = {
   UpgradeContract: 0,
   AuthorizeGovernanceDataSourceTransfer: 1,
@@ -25,6 +33,11 @@ export const TargetAction = {
   RequestGovernanceDataSourceTransfer: 5,
 } as const;
 
+export declare type ActionName =
+  | keyof typeof ExecutorAction
+  | keyof typeof TargetAction;
+
+/** Helper to get the ActionName from a (moduleId, actionId) tuple*/
 export function toActionName(
   deserialized: Readonly<{ moduleId: number; actionId: number }>
 ): ActionName {
@@ -48,28 +61,19 @@ export function toActionName(
   }
   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(
+/** Governance header that should be in every Pyth crosschain governance message*/
+export class PythGovernanceHeader {
+  readonly targetChainId: ChainName;
+  readonly action: ActionName;
+  static layout: BufferLayout.Structure<
+    Readonly<{
+      magicNumber: number;
+      module: number;
+      action: number;
+      chain: ChainId;
+    }>
+  > = BufferLayout.struct(
     [
       BufferLayout.u32("magicNumber"),
       BufferLayout.u8("module"),
@@ -78,66 +82,62 @@ export function governanceHeaderLayout(): BufferLayout.Structure<
     ],
     "header"
   );
-}
+  /** Span of the serialized governance header */
+  static span = 8;
 
-/** Decode Pyth Governance Header and return undefined if the header is invalid */
-export function decodeHeader(data: Buffer): PythGovernanceHeader {
-  let deserialized = governanceHeaderLayout().decode(data);
-  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];
+  constructor(targetChainId: ChainName, action: ActionName) {
+    this.targetChainId = targetChainId;
+    this.action = action;
   }
-  return governanceHeaderLayout().encode(
-    {
-      magicNumber: MAGIC_NUMBER,
-      module,
-      action,
-      chain: toChainId(src.targetChainId),
-    },
-    buffer
-  );
-}
+  /** Decode Pyth Governance Header */
+  static decode(data: Buffer): PythGovernanceHeader {
+    let deserialized = this.layout.decode(data);
+    if (deserialized.magicNumber !== MAGIC_NUMBER) {
+      throw new Error("Wrong magic number");
+    }
 
-export function verifyHeader(
-  deserialized: Readonly<{
-    magicNumber: number;
-    module: number;
-    action: number;
-    chain: ChainId;
-  }>
-): PythGovernanceHeader {
-  if (deserialized.magicNumber !== MAGIC_NUMBER) {
-    throw new Error("Wrong magic number");
-  }
+    if (!toChainName(deserialized.chain)) {
+      throw new Error("Chain Id not found");
+    }
 
-  if (!toChainName(deserialized.chain)) {
-    throw new Error("Chain Id not found");
+    return new PythGovernanceHeader(
+      toChainName(deserialized.chain),
+      toActionName({
+        actionId: deserialized.action,
+        moduleId: deserialized.module,
+      })
+    );
   }
 
-  let governanceHeader: PythGovernanceHeader = {
-    targetChainId: toChainName(deserialized.chain),
-    action: toActionName({
-      actionId: deserialized.action,
-      moduleId: deserialized.module,
-    }),
-  };
-  return governanceHeader;
+  /** Encode Pyth Governance Header */
+  encode(): Buffer {
+    // The code will crash if the payload is actually bigger than PACKET_DATA_SIZE. But PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload should never be bigger than this anyway
+    const buffer = Buffer.alloc(PACKET_DATA_SIZE);
+    let module: number;
+    let action: number;
+    if (this.action in ExecutorAction) {
+      module = MODULE_EXECUTOR;
+      action = ExecutorAction[this.action as keyof typeof ExecutorAction];
+    } else {
+      module = MODULE_TARGET;
+      action = TargetAction[this.action as keyof typeof TargetAction];
+    }
+    const span = PythGovernanceHeader.layout.encode(
+      {
+        magicNumber: MAGIC_NUMBER,
+        module,
+        action,
+        chain: toChainId(this.targetChainId),
+      },
+      buffer
+    );
+    return buffer.subarray(0, span);
+  }
 }
 
+/** Decode a governance payload */
 export function decodeGovernancePayload(data: Buffer): PythGovernanceAction {
-  const header = decodeHeader(data);
+  const header = PythGovernanceHeader.decode(data);
   switch (header.action) {
     case "ExecutePostedVaa":
       return ExecutePostedVaa.decode(data);