Răsfoiți Sursa

Check duplicates (#427)

* Check duplicates

* Cleanup

* Multisig/refactor wormhole to avoid many rpc calls (#428)

* Refactor wormhole

* Fix wasm bug

* hasWormholePayload becomes sync
guibescos 2 ani în urmă
părinte
comite
e484f5cbb7

+ 130 - 94
third_party/pyth/multisig-wh-message-builder/src/index.ts

@@ -1,9 +1,4 @@
-import {
-  importCoreWasm,
-  ixFromRust,
-  setDefaultWasm,
-  utils as wormholeUtils,
-} from "@certusone/wormhole-sdk";
+import { ixFromRust, setDefaultWasm } from "@certusone/wormhole-sdk";
 import * as anchor from "@project-serum/anchor";
 import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
 import {
@@ -15,14 +10,24 @@ import {
   TransactionInstruction,
 } from "@solana/web3.js";
 import Squads from "@sqds/mesh";
-import { getIxAuthorityPDA, getIxPDA, getMsPDA } from "@sqds/mesh";
+import { getIxAuthorityPDA } from "@sqds/mesh";
 import { InstructionAccount } from "@sqds/mesh/lib/types";
 import bs58 from "bs58";
 import { program } from "commander";
 import * as fs from "fs";
 import { LedgerNodeWallet } from "./wallet";
 import lodash from "lodash";
-import { getActiveProposals, getProposalInstructions } from "./multisig";
+import {
+  getActiveProposals,
+  getManyProposalsInstructions,
+  getProposalInstructions,
+} from "./multisig";
+import {
+  WormholeNetwork,
+  loadWormholeTools,
+  WormholeTools,
+  parse,
+} from "./wormhole";
 
 setDefaultWasm("node");
 
@@ -40,8 +45,7 @@ setDefaultWasm("node");
 //
 // - "localdevnet" - always means the Tilt devnet
 
-type Cluster = "devnet" | "mainnet" | "localdevnet";
-type WormholeNetwork = "TESTNET" | "MAINNET" | "DEVNET";
+export type Cluster = "devnet" | "mainnet" | "localdevnet";
 
 type Config = {
   wormholeClusterName: WormholeNetwork;
@@ -49,7 +53,7 @@ type Config = {
   vault: PublicKey;
 };
 
-const CONFIG: Record<Cluster, Config> = {
+export const CONFIG: Record<Cluster, Config> = {
   devnet: {
     wormholeClusterName: "TESTNET",
     vault: new PublicKey("6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3"),
@@ -162,6 +166,7 @@ program
     "keys/key.json"
   )
   .option("-p, --payload <hex-string>", "payload to sign", "0xdeadbeef")
+  .option("-s, --skip-duplicate-check", "Skip checking duplicates")
   .action(async (options) => {
     const cluster: Cluster = options.cluster;
     const squad = await getSquadsClient(
@@ -171,11 +176,49 @@ program
       options.ledgerDerivationChange,
       options.wallet
     );
+    const wormholeTools = await loadWormholeTools(cluster, squad.connection);
+
+    if (!options.skipDuplicateCheck) {
+      const activeProposals = await getActiveProposals(
+        squad,
+        CONFIG[cluster].vault
+      );
+      const activeInstructions = await getManyProposalsInstructions(
+        squad,
+        activeProposals
+      );
+
+      const msAccount = await squad.getMultisig(CONFIG[cluster].vault);
+      const emitter = squad.getAuthorityPDA(
+        msAccount.publicKey,
+        msAccount.authorityIndex
+      );
+
+      for (let i = 0; i < activeProposals.length; i++) {
+        if (
+          hasWormholePayload(
+            squad,
+            emitter,
+            activeProposals[i].publicKey,
+            options.payload,
+            activeInstructions[i],
+            wormholeTools
+          )
+        ) {
+          console.log(
+            `❌ Skipping, payload ${options.payload} matches instructions at ${activeProposals[i].publicKey}`
+          );
+          return;
+        }
+      }
+    }
+
     await createWormholeMsgMultisigTx(
       options.cluster,
       squad,
       CONFIG[cluster].vault,
-      options.payload
+      options.payload,
+      wormholeTools
     );
   });
 
@@ -208,13 +251,35 @@ program
       options.ledgerDerivationChange,
       options.wallet
     );
-    await verifyWormholePayload(
-      options.cluster,
+    const wormholeTools = await loadWormholeTools(cluster, squad.connection);
+
+    let onChainInstructions = await getProposalInstructions(
       squad,
-      CONFIG[cluster].vault,
-      new PublicKey(options.txPda),
-      options.payload
+      await squad.getTransaction(new PublicKey(options.txPda))
+    );
+
+    const msAccount = await squad.getMultisig(CONFIG[cluster].vault);
+    const emitter = squad.getAuthorityPDA(
+      msAccount.publicKey,
+      msAccount.authorityIndex
     );
+
+    if (
+      hasWormholePayload(
+        squad,
+        emitter,
+        new PublicKey(options.txPda),
+        options.payload,
+        onChainInstructions,
+        wormholeTools
+      )
+    ) {
+      console.log(
+        "✅ This proposal is verified to be created with the given payload."
+      );
+    } else {
+      console.log("❌ This proposal does not match the given payload.");
+    }
   });
 
 program
@@ -325,7 +390,8 @@ program
       squad,
       CONFIG[cluster].vault,
       new PublicKey(options.txPda),
-      CONFIG[cluster].wormholeRpcEndpoint
+      CONFIG[cluster].wormholeRpcEndpoint,
+      await loadWormholeTools(cluster, squad.connection)
     );
   });
 
@@ -479,8 +545,7 @@ async function getSquadsClient(
       if (solRpcUrl) {
         return Squads.endpoint(solRpcUrl, wallet);
       } else {
-        console.log("rpc:", solRpcUrl);
-        throw `ERROR: solRpcUrl was not specified for localdevnet!`;
+        return Squads.localnet(wallet);
       }
     }
     default: {
@@ -583,26 +648,13 @@ async function setIsActiveIx(
   };
 }
 
-async function getWormholeMessageIx(
-  cluster: Cluster,
+function getWormholeMessageIx(
   payer: PublicKey,
   emitter: PublicKey,
   message: PublicKey,
-  connection: anchor.web3.Connection,
-  payload: string
+  payload: string,
+  wormholeTools: WormholeTools
 ) {
-  const wormholeClusterName: WormholeNetwork =
-    CONFIG[cluster].wormholeClusterName;
-  const wormholeAddress =
-    wormholeUtils.CONTRACTS[wormholeClusterName].solana.core;
-  const { post_message_ix, fee_collector_address, state_address, parse_state } =
-    await importCoreWasm();
-  const feeCollector = new PublicKey(fee_collector_address(wormholeAddress));
-  const bridgeState = new PublicKey(state_address(wormholeAddress));
-  const bridgeAccountInfo = await connection.getAccountInfo(bridgeState);
-  const bridgeStateParsed = parse_state(bridgeAccountInfo!.data);
-  const bridgeFee = bridgeStateParsed.config.fee;
-
   if (payload.startsWith("0x")) {
     payload = payload.substring(2);
   }
@@ -610,12 +662,12 @@ async function getWormholeMessageIx(
   return [
     SystemProgram.transfer({
       fromPubkey: payer,
-      toPubkey: feeCollector,
-      lamports: bridgeFee,
+      toPubkey: wormholeTools.feeCollector,
+      lamports: wormholeTools.bridgeFee,
     }),
     ixFromRust(
-      post_message_ix(
-        wormholeAddress,
+      wormholeTools.post_message_ix(
+        wormholeTools.wormholeAddress.toBase58(),
         payer.toBase58(),
         emitter.toBase58(),
         message.toBase58(),
@@ -631,7 +683,8 @@ async function createWormholeMsgMultisigTx(
   cluster: Cluster,
   squad: Squads,
   vault: PublicKey,
-  payload: string
+  payload: string,
+  wormholeTools: WormholeTools
 ) {
   const msAccount = await squad.getMultisig(vault);
   const emitter = squad.getAuthorityPDA(
@@ -649,13 +702,12 @@ async function createWormholeMsgMultisigTx(
   );
 
   console.log("Creating wormhole instructions...");
-  const wormholeIxs = await getWormholeMessageIx(
-    cluster,
+  const wormholeIxs = getWormholeMessageIx(
     emitter,
     emitter,
     messagePDA,
-    squad.connection,
-    payload
+    payload,
+    wormholeTools
   );
   console.log("Wormhole instructions created.");
 
@@ -678,29 +730,19 @@ async function createWormholeMsgMultisigTx(
   );
 }
 
-async function verifyWormholePayload(
-  cluster: Cluster,
+function hasWormholePayload(
   squad: Squads,
-  vault: PublicKey,
+  emitter: PublicKey,
   txPubkey: PublicKey,
-  payload: string
-) {
-  const msAccount = await squad.getMultisig(vault);
-  const emitter = squad.getAuthorityPDA(
-    msAccount.publicKey,
-    msAccount.authorityIndex
-  );
-  console.log(`Emitter Address: ${emitter.toBase58()}`);
-
-  const tx = await squad.getTransaction(txPubkey);
-  const onChainInstructions = await getProposalInstructions(squad, tx);
-
+  payload: string,
+  onChainInstructions: InstructionAccount[],
+  wormholeTools: WormholeTools
+): boolean {
   if (onChainInstructions.length !== 2) {
-    throw new Error(
-      `Expected 2 instructions in the transaction, found ${
-        tx.instructionIndex + 1
-      }`
+    console.debug(
+      `Expected 2 instructions in the transaction, found ${onChainInstructions.length}`
     );
+    return false;
   }
 
   const [messagePDA] = getIxAuthorityPDA(
@@ -709,56 +751,54 @@ async function verifyWormholePayload(
     squad.multisigProgramId
   );
 
-  const wormholeIxs = await getWormholeMessageIx(
-    cluster,
+  const wormholeIxs = getWormholeMessageIx(
     emitter,
     emitter,
     messagePDA,
-    squad.connection,
-    payload
-  );
-
-  console.log("Checking equality of the 1st instruction...");
-  verifyOnChainInstruction(
-    wormholeIxs[0],
-    onChainInstructions[0] as InstructionAccount
+    payload,
+    wormholeTools
   );
 
-  console.log("Checking equality of the 2nd instruction...");
-  verifyOnChainInstruction(
-    wormholeIxs[1],
-    onChainInstructions[1] as InstructionAccount
-  );
-
-  console.log(
-    "✅ The transaction is verified to be created with the given payload."
+  return (
+    isEqualOnChainInstruction(
+      wormholeIxs[0],
+      onChainInstructions[0] as InstructionAccount
+    ) &&
+    isEqualOnChainInstruction(
+      wormholeIxs[1],
+      onChainInstructions[1] as InstructionAccount
+    )
   );
 }
 
-function verifyOnChainInstruction(
+function isEqualOnChainInstruction(
   instruction: TransactionInstruction,
   onChainInstruction: InstructionAccount
-) {
+): boolean {
   if (!instruction.programId.equals(onChainInstruction.programId)) {
-    throw new Error(
+    console.debug(
       `Program id mismatch: Expected ${instruction.programId.toBase58()}, found ${onChainInstruction.programId.toBase58()}`
     );
+    return false;
   }
 
   if (!lodash.isEqual(instruction.keys, onChainInstruction.keys)) {
-    throw new Error(
+    console.debug(
       `Instruction accounts mismatch. Expected ${instruction.keys}, found ${onChainInstruction.keys}`
     );
+    return false;
   }
 
   const onChainData = onChainInstruction.data as Buffer;
   if (!instruction.data.equals(onChainData)) {
-    throw new Error(
+    console.debug(
       `Instruction data mismatch. Expected ${instruction.data.toString(
         "hex"
       )}, Found ${onChainData.toString("hex")}`
     );
+    return false;
   }
+  return true;
 }
 
 async function executeMultisigTx(
@@ -766,7 +806,8 @@ async function executeMultisigTx(
   squad: Squads,
   vault: PublicKey,
   txPDA: PublicKey,
-  rpcUrl: string
+  rpcUrl: string,
+  wormholeTools: WormholeTools
 ) {
   const msAccount = await squad.getMultisig(vault);
 
@@ -862,7 +903,7 @@ async function executeMultisigTx(
   const { vaaBytes } = await response.json();
   console.log(`VAA (Base64): ${vaaBytes}`);
   console.log(`VAA (Hex): ${Buffer.from(vaaBytes, "base64").toString("hex")}`);
-  const parsedVaa = await parse(vaaBytes);
+  const parsedVaa = parse(vaaBytes, wormholeTools);
   console.log(`Emitter chain: ${parsedVaa.emitter_chain}`);
   console.log(`Nonce: ${parsedVaa.nonce}`);
   console.log(`Payload: ${Buffer.from(parsedVaa.payload).toString("hex")}`);
@@ -939,8 +980,3 @@ async function removeMember(
     squadIxs
   );
 }
-
-async function parse(data: string) {
-  const { parse_vaa } = await importCoreWasm();
-  return parse_vaa(Uint8Array.from(Buffer.from(data, "base64")));
-}

+ 49 - 0
third_party/pyth/multisig-wh-message-builder/src/wormhole.ts

@@ -0,0 +1,49 @@
+import {
+  importCoreWasm,
+  utils as wormholeUtils,
+} from "@certusone/wormhole-sdk";
+import { Connection, PublicKey } from "@solana/web3.js";
+import { Cluster, CONFIG } from ".";
+
+export type WormholeNetwork = "TESTNET" | "MAINNET" | "DEVNET";
+
+export async function loadWormholeTools(
+  cluster: Cluster,
+  connection: Connection
+): Promise<WormholeTools> {
+  const wormholeClusterName: WormholeNetwork =
+    CONFIG[cluster].wormholeClusterName;
+  const wormholeAddress =
+    wormholeUtils.CONTRACTS[wormholeClusterName].solana.core;
+  const {
+    post_message_ix,
+    fee_collector_address,
+    state_address,
+    parse_state,
+    parse_vaa,
+  } = await importCoreWasm();
+  const feeCollector = new PublicKey(fee_collector_address(wormholeAddress));
+  const bridgeState = new PublicKey(state_address(wormholeAddress));
+  const bridgeAccountInfo = await connection.getAccountInfo(bridgeState);
+  const bridgeStateParsed = parse_state(bridgeAccountInfo!.data);
+  const bridgeFee = bridgeStateParsed.config.fee;
+  return {
+    post_message_ix,
+    parse_vaa,
+    bridgeFee,
+    feeCollector,
+    wormholeAddress: new PublicKey(wormholeAddress),
+  };
+}
+
+export type WormholeTools = {
+  post_message_ix: any;
+  parse_vaa: any;
+  bridgeFee: number;
+  wormholeAddress: PublicKey;
+  feeCollector: PublicKey;
+};
+
+export function parse(data: string, wormholeTools: WormholeTools) {
+  return wormholeTools.parse_vaa(Uint8Array.from(Buffer.from(data, "base64")));
+}