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:
parent
09f8af74ed
commit
e484f5cbb7
|
@ -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
|
||||
payload,
|
||||
wormholeTools
|
||||
);
|
||||
|
||||
console.log("Checking equality of the 1st instruction...");
|
||||
verifyOnChainInstruction(
|
||||
return (
|
||||
isEqualOnChainInstruction(
|
||||
wormholeIxs[0],
|
||||
onChainInstructions[0] as InstructionAccount
|
||||
);
|
||||
|
||||
console.log("Checking equality of the 2nd instruction...");
|
||||
verifyOnChainInstruction(
|
||||
) &&
|
||||
isEqualOnChainInstruction(
|
||||
wormholeIxs[1],
|
||||
onChainInstructions[1] as InstructionAccount
|
||||
);
|
||||
|
||||
console.log(
|
||||
"✅ The transaction is verified to be created with the given payload."
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
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")));
|
||||
}
|
||||
|
|
|
@ -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")));
|
||||
}
|
Loading…
Reference in New Issue