Check duplicates (#427)

* Check duplicates

* Cleanup

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

* Refactor wormhole

* Fix wasm bug

* hasWormholePayload becomes sync
This commit is contained in:
guibescos 2022-12-15 01:38:13 +08:00 committed by GitHub
parent 09f8af74ed
commit e484f5cbb7
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 179 additions and 94 deletions

View File

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

View File

@ -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")));
}