Skip to content

Commit e484f5c

Browse files
authored
Check duplicates (#427)
* Check duplicates * Cleanup * Multisig/refactor wormhole to avoid many rpc calls (#428) * Refactor wormhole * Fix wasm bug * hasWormholePayload becomes sync
1 parent 09f8af7 commit e484f5c

File tree

2 files changed

+179
-94
lines changed

2 files changed

+179
-94
lines changed

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

Lines changed: 130 additions & 94 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,4 @@
1-
import {
2-
importCoreWasm,
3-
ixFromRust,
4-
setDefaultWasm,
5-
utils as wormholeUtils,
6-
} from "@certusone/wormhole-sdk";
1+
import { ixFromRust, setDefaultWasm } from "@certusone/wormhole-sdk";
72
import * as anchor from "@project-serum/anchor";
83
import NodeWallet from "@project-serum/anchor/dist/cjs/nodewallet";
94
import {
@@ -15,14 +10,24 @@ import {
1510
TransactionInstruction,
1611
} from "@solana/web3.js";
1712
import Squads from "@sqds/mesh";
18-
import { getIxAuthorityPDA, getIxPDA, getMsPDA } from "@sqds/mesh";
13+
import { getIxAuthorityPDA } from "@sqds/mesh";
1914
import { InstructionAccount } from "@sqds/mesh/lib/types";
2015
import bs58 from "bs58";
2116
import { program } from "commander";
2217
import * as fs from "fs";
2318
import { LedgerNodeWallet } from "./wallet";
2419
import lodash from "lodash";
25-
import { getActiveProposals, getProposalInstructions } from "./multisig";
20+
import {
21+
getActiveProposals,
22+
getManyProposalsInstructions,
23+
getProposalInstructions,
24+
} from "./multisig";
25+
import {
26+
WormholeNetwork,
27+
loadWormholeTools,
28+
WormholeTools,
29+
parse,
30+
} from "./wormhole";
2631

2732
setDefaultWasm("node");
2833

@@ -40,16 +45,15 @@ setDefaultWasm("node");
4045
//
4146
// - "localdevnet" - always means the Tilt devnet
4247

43-
type Cluster = "devnet" | "mainnet" | "localdevnet";
44-
type WormholeNetwork = "TESTNET" | "MAINNET" | "DEVNET";
48+
export type Cluster = "devnet" | "mainnet" | "localdevnet";
4549

4650
type Config = {
4751
wormholeClusterName: WormholeNetwork;
4852
wormholeRpcEndpoint: string;
4953
vault: PublicKey;
5054
};
5155

52-
const CONFIG: Record<Cluster, Config> = {
56+
export const CONFIG: Record<Cluster, Config> = {
5357
devnet: {
5458
wormholeClusterName: "TESTNET",
5559
vault: new PublicKey("6baWtW1zTUVMSJHJQVxDUXWzqrQeYBr6mu31j3bTKwY3"),
@@ -162,6 +166,7 @@ program
162166
"keys/key.json"
163167
)
164168
.option("-p, --payload <hex-string>", "payload to sign", "0xdeadbeef")
169+
.option("-s, --skip-duplicate-check", "Skip checking duplicates")
165170
.action(async (options) => {
166171
const cluster: Cluster = options.cluster;
167172
const squad = await getSquadsClient(
@@ -171,11 +176,49 @@ program
171176
options.ledgerDerivationChange,
172177
options.wallet
173178
);
179+
const wormholeTools = await loadWormholeTools(cluster, squad.connection);
180+
181+
if (!options.skipDuplicateCheck) {
182+
const activeProposals = await getActiveProposals(
183+
squad,
184+
CONFIG[cluster].vault
185+
);
186+
const activeInstructions = await getManyProposalsInstructions(
187+
squad,
188+
activeProposals
189+
);
190+
191+
const msAccount = await squad.getMultisig(CONFIG[cluster].vault);
192+
const emitter = squad.getAuthorityPDA(
193+
msAccount.publicKey,
194+
msAccount.authorityIndex
195+
);
196+
197+
for (let i = 0; i < activeProposals.length; i++) {
198+
if (
199+
hasWormholePayload(
200+
squad,
201+
emitter,
202+
activeProposals[i].publicKey,
203+
options.payload,
204+
activeInstructions[i],
205+
wormholeTools
206+
)
207+
) {
208+
console.log(
209+
`❌ Skipping, payload ${options.payload} matches instructions at ${activeProposals[i].publicKey}`
210+
);
211+
return;
212+
}
213+
}
214+
}
215+
174216
await createWormholeMsgMultisigTx(
175217
options.cluster,
176218
squad,
177219
CONFIG[cluster].vault,
178-
options.payload
220+
options.payload,
221+
wormholeTools
179222
);
180223
});
181224

@@ -208,13 +251,35 @@ program
208251
options.ledgerDerivationChange,
209252
options.wallet
210253
);
211-
await verifyWormholePayload(
212-
options.cluster,
254+
const wormholeTools = await loadWormholeTools(cluster, squad.connection);
255+
256+
let onChainInstructions = await getProposalInstructions(
213257
squad,
214-
CONFIG[cluster].vault,
215-
new PublicKey(options.txPda),
216-
options.payload
258+
await squad.getTransaction(new PublicKey(options.txPda))
259+
);
260+
261+
const msAccount = await squad.getMultisig(CONFIG[cluster].vault);
262+
const emitter = squad.getAuthorityPDA(
263+
msAccount.publicKey,
264+
msAccount.authorityIndex
217265
);
266+
267+
if (
268+
hasWormholePayload(
269+
squad,
270+
emitter,
271+
new PublicKey(options.txPda),
272+
options.payload,
273+
onChainInstructions,
274+
wormholeTools
275+
)
276+
) {
277+
console.log(
278+
"✅ This proposal is verified to be created with the given payload."
279+
);
280+
} else {
281+
console.log("❌ This proposal does not match the given payload.");
282+
}
218283
});
219284

220285
program
@@ -325,7 +390,8 @@ program
325390
squad,
326391
CONFIG[cluster].vault,
327392
new PublicKey(options.txPda),
328-
CONFIG[cluster].wormholeRpcEndpoint
393+
CONFIG[cluster].wormholeRpcEndpoint,
394+
await loadWormholeTools(cluster, squad.connection)
329395
);
330396
});
331397

@@ -479,8 +545,7 @@ async function getSquadsClient(
479545
if (solRpcUrl) {
480546
return Squads.endpoint(solRpcUrl, wallet);
481547
} else {
482-
console.log("rpc:", solRpcUrl);
483-
throw `ERROR: solRpcUrl was not specified for localdevnet!`;
548+
return Squads.localnet(wallet);
484549
}
485550
}
486551
default: {
@@ -583,39 +648,26 @@ async function setIsActiveIx(
583648
};
584649
}
585650

586-
async function getWormholeMessageIx(
587-
cluster: Cluster,
651+
function getWormholeMessageIx(
588652
payer: PublicKey,
589653
emitter: PublicKey,
590654
message: PublicKey,
591-
connection: anchor.web3.Connection,
592-
payload: string
655+
payload: string,
656+
wormholeTools: WormholeTools
593657
) {
594-
const wormholeClusterName: WormholeNetwork =
595-
CONFIG[cluster].wormholeClusterName;
596-
const wormholeAddress =
597-
wormholeUtils.CONTRACTS[wormholeClusterName].solana.core;
598-
const { post_message_ix, fee_collector_address, state_address, parse_state } =
599-
await importCoreWasm();
600-
const feeCollector = new PublicKey(fee_collector_address(wormholeAddress));
601-
const bridgeState = new PublicKey(state_address(wormholeAddress));
602-
const bridgeAccountInfo = await connection.getAccountInfo(bridgeState);
603-
const bridgeStateParsed = parse_state(bridgeAccountInfo!.data);
604-
const bridgeFee = bridgeStateParsed.config.fee;
605-
606658
if (payload.startsWith("0x")) {
607659
payload = payload.substring(2);
608660
}
609661

610662
return [
611663
SystemProgram.transfer({
612664
fromPubkey: payer,
613-
toPubkey: feeCollector,
614-
lamports: bridgeFee,
665+
toPubkey: wormholeTools.feeCollector,
666+
lamports: wormholeTools.bridgeFee,
615667
}),
616668
ixFromRust(
617-
post_message_ix(
618-
wormholeAddress,
669+
wormholeTools.post_message_ix(
670+
wormholeTools.wormholeAddress.toBase58(),
619671
payer.toBase58(),
620672
emitter.toBase58(),
621673
message.toBase58(),
@@ -631,7 +683,8 @@ async function createWormholeMsgMultisigTx(
631683
cluster: Cluster,
632684
squad: Squads,
633685
vault: PublicKey,
634-
payload: string
686+
payload: string,
687+
wormholeTools: WormholeTools
635688
) {
636689
const msAccount = await squad.getMultisig(vault);
637690
const emitter = squad.getAuthorityPDA(
@@ -649,13 +702,12 @@ async function createWormholeMsgMultisigTx(
649702
);
650703

651704
console.log("Creating wormhole instructions...");
652-
const wormholeIxs = await getWormholeMessageIx(
653-
cluster,
705+
const wormholeIxs = getWormholeMessageIx(
654706
emitter,
655707
emitter,
656708
messagePDA,
657-
squad.connection,
658-
payload
709+
payload,
710+
wormholeTools
659711
);
660712
console.log("Wormhole instructions created.");
661713

@@ -678,29 +730,19 @@ async function createWormholeMsgMultisigTx(
678730
);
679731
}
680732

681-
async function verifyWormholePayload(
682-
cluster: Cluster,
733+
function hasWormholePayload(
683734
squad: Squads,
684-
vault: PublicKey,
735+
emitter: PublicKey,
685736
txPubkey: PublicKey,
686-
payload: string
687-
) {
688-
const msAccount = await squad.getMultisig(vault);
689-
const emitter = squad.getAuthorityPDA(
690-
msAccount.publicKey,
691-
msAccount.authorityIndex
692-
);
693-
console.log(`Emitter Address: ${emitter.toBase58()}`);
694-
695-
const tx = await squad.getTransaction(txPubkey);
696-
const onChainInstructions = await getProposalInstructions(squad, tx);
697-
737+
payload: string,
738+
onChainInstructions: InstructionAccount[],
739+
wormholeTools: WormholeTools
740+
): boolean {
698741
if (onChainInstructions.length !== 2) {
699-
throw new Error(
700-
`Expected 2 instructions in the transaction, found ${
701-
tx.instructionIndex + 1
702-
}`
742+
console.debug(
743+
`Expected 2 instructions in the transaction, found ${onChainInstructions.length}`
703744
);
745+
return false;
704746
}
705747

706748
const [messagePDA] = getIxAuthorityPDA(
@@ -709,64 +751,63 @@ async function verifyWormholePayload(
709751
squad.multisigProgramId
710752
);
711753

712-
const wormholeIxs = await getWormholeMessageIx(
713-
cluster,
754+
const wormholeIxs = getWormholeMessageIx(
714755
emitter,
715756
emitter,
716757
messagePDA,
717-
squad.connection,
718-
payload
719-
);
720-
721-
console.log("Checking equality of the 1st instruction...");
722-
verifyOnChainInstruction(
723-
wormholeIxs[0],
724-
onChainInstructions[0] as InstructionAccount
758+
payload,
759+
wormholeTools
725760
);
726761

727-
console.log("Checking equality of the 2nd instruction...");
728-
verifyOnChainInstruction(
729-
wormholeIxs[1],
730-
onChainInstructions[1] as InstructionAccount
731-
);
732-
733-
console.log(
734-
"✅ The transaction is verified to be created with the given payload."
762+
return (
763+
isEqualOnChainInstruction(
764+
wormholeIxs[0],
765+
onChainInstructions[0] as InstructionAccount
766+
) &&
767+
isEqualOnChainInstruction(
768+
wormholeIxs[1],
769+
onChainInstructions[1] as InstructionAccount
770+
)
735771
);
736772
}
737773

738-
function verifyOnChainInstruction(
774+
function isEqualOnChainInstruction(
739775
instruction: TransactionInstruction,
740776
onChainInstruction: InstructionAccount
741-
) {
777+
): boolean {
742778
if (!instruction.programId.equals(onChainInstruction.programId)) {
743-
throw new Error(
779+
console.debug(
744780
`Program id mismatch: Expected ${instruction.programId.toBase58()}, found ${onChainInstruction.programId.toBase58()}`
745781
);
782+
return false;
746783
}
747784

748785
if (!lodash.isEqual(instruction.keys, onChainInstruction.keys)) {
749-
throw new Error(
786+
console.debug(
750787
`Instruction accounts mismatch. Expected ${instruction.keys}, found ${onChainInstruction.keys}`
751788
);
789+
return false;
752790
}
753791

754792
const onChainData = onChainInstruction.data as Buffer;
755793
if (!instruction.data.equals(onChainData)) {
756-
throw new Error(
794+
console.debug(
757795
`Instruction data mismatch. Expected ${instruction.data.toString(
758796
"hex"
759797
)}, Found ${onChainData.toString("hex")}`
760798
);
799+
return false;
761800
}
801+
return true;
762802
}
763803

764804
async function executeMultisigTx(
765805
cluster: string,
766806
squad: Squads,
767807
vault: PublicKey,
768808
txPDA: PublicKey,
769-
rpcUrl: string
809+
rpcUrl: string,
810+
wormholeTools: WormholeTools
770811
) {
771812
const msAccount = await squad.getMultisig(vault);
772813

@@ -862,7 +903,7 @@ async function executeMultisigTx(
862903
const { vaaBytes } = await response.json();
863904
console.log(`VAA (Base64): ${vaaBytes}`);
864905
console.log(`VAA (Hex): ${Buffer.from(vaaBytes, "base64").toString("hex")}`);
865-
const parsedVaa = await parse(vaaBytes);
906+
const parsedVaa = parse(vaaBytes, wormholeTools);
866907
console.log(`Emitter chain: ${parsedVaa.emitter_chain}`);
867908
console.log(`Nonce: ${parsedVaa.nonce}`);
868909
console.log(`Payload: ${Buffer.from(parsedVaa.payload).toString("hex")}`);
@@ -939,8 +980,3 @@ async function removeMember(
939980
squadIxs
940981
);
941982
}
942-
943-
async function parse(data: string) {
944-
const { parse_vaa } = await importCoreWasm();
945-
return parse_vaa(Uint8Array.from(Buffer.from(data, "base64")));
946-
}

0 commit comments

Comments
 (0)