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