xc-admin/MultisigParser for wormhole instructions (#470)

* Checkpoint

* Refactor

* Unrecognized instruction -> unrecognized program

* Shorten tests

* Rename test

* Rename WormholeInstruction to WormholeMultisigInstruction

* Refactor into constructor and builder method

* Rename file
This commit is contained in:
guibescos 2023-01-11 12:50:22 -06:00 committed by GitHub
parent 7c2e02ea0b
commit d1f5e5955a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 557 additions and 1 deletions

View File

@ -3749,7 +3749,8 @@
}, },
"node_modules/@pythnetwork/client": { "node_modules/@pythnetwork/client": {
"version": "2.9.0", "version": "2.9.0",
"license": "Apache-2.0", "resolved": "https://registry.npmjs.org/@pythnetwork/client/-/client-2.9.0.tgz",
"integrity": "sha512-2CyDmTwPWW+JCQgRKUpwMR/31oiLWH6I3GA0h2ZXIcbt/hWxcr5TXyKlWuyi+l+jh73WWh88gq8NXLoIgRcvkA==",
"dependencies": { "dependencies": {
"buffer": "^6.0.1" "buffer": "^6.0.1"
}, },
@ -12758,6 +12759,7 @@
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@certusone/wormhole-sdk": "^0.9.8", "@certusone/wormhole-sdk": "^0.9.8",
"@pythnetwork/client": "^2.9.0",
"@solana/buffer-layout": "^4.0.1", "@solana/buffer-layout": "^4.0.1",
"@solana/web3.js": "^1.73.0", "@solana/web3.js": "^1.73.0",
"@sqds/mesh": "^1.0.6", "@sqds/mesh": "^1.0.6",
@ -15330,6 +15332,8 @@
}, },
"@pythnetwork/client": { "@pythnetwork/client": {
"version": "2.9.0", "version": "2.9.0",
"resolved": "https://registry.npmjs.org/@pythnetwork/client/-/client-2.9.0.tgz",
"integrity": "sha512-2CyDmTwPWW+JCQgRKUpwMR/31oiLWH6I3GA0h2ZXIcbt/hWxcr5TXyKlWuyi+l+jh73WWh88gq8NXLoIgRcvkA==",
"requires": { "requires": {
"buffer": "^6.0.1" "buffer": "^6.0.1"
}, },
@ -21188,6 +21192,7 @@
"version": "file:packages/xc-admin-common", "version": "file:packages/xc-admin-common",
"requires": { "requires": {
"@certusone/wormhole-sdk": "^0.9.8", "@certusone/wormhole-sdk": "^0.9.8",
"@pythnetwork/client": "*",
"@solana/buffer-layout": "^4.0.1", "@solana/buffer-layout": "^4.0.1",
"@solana/web3.js": "^1.73.0", "@solana/web3.js": "^1.73.0",
"@sqds/mesh": "^1.0.6", "@sqds/mesh": "^1.0.6",

View File

@ -20,6 +20,7 @@
}, },
"dependencies": { "dependencies": {
"@certusone/wormhole-sdk": "^0.9.8", "@certusone/wormhole-sdk": "^0.9.8",
"@pythnetwork/client": "^2.9.0",
"@solana/buffer-layout": "^4.0.1", "@solana/buffer-layout": "^4.0.1",
"@solana/web3.js": "^1.73.0", "@solana/web3.js": "^1.73.0",
"@sqds/mesh": "^1.0.6", "@sqds/mesh": "^1.0.6",

View File

@ -0,0 +1,365 @@
import { ChainName } from "@certusone/wormhole-sdk";
import { createWormholeProgramInterface } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
import { AnchorProvider, Wallet } from "@project-serum/anchor";
import {
getPythClusterApiUrl,
PythCluster,
} from "@pythnetwork/client/lib/cluster";
import {
Connection,
Keypair,
PublicKey,
SystemProgram,
TransactionInstruction,
} from "@solana/web3.js";
import {
MultisigInstructionProgram,
MultisigParser,
WORMHOLE_ADDRESS,
} from "..";
import {
encodeExecutePostedVaa,
ExecutePostedVaaArgs,
} from "../governance_payload/ExecutePostedVaa";
import { WormholeMultisigInstruction } from "../multisig_transaction/WormholeMultisigInstruction";
test("Wormhole multisig instruction parse: send message without governance payload", (done) => {
jest.setTimeout(60000);
const cluster: PythCluster = "devnet";
const wormholeProgram = createWormholeProgramInterface(
WORMHOLE_ADDRESS[cluster]!,
new AnchorProvider(
new Connection(getPythClusterApiUrl(cluster)),
new Wallet(new Keypair()),
AnchorProvider.defaultOptions()
)
);
const parser = new MultisigParser(cluster);
wormholeProgram.methods
.postMessage(1, Buffer.from([0]), 1)
.accounts({
bridge: PublicKey.unique(),
message: PublicKey.unique(),
emitter: PublicKey.unique(),
sequence: PublicKey.unique(),
feeCollector: PublicKey.unique(),
clock: PublicKey.unique(),
})
.instruction()
.then((instruction) => {
const parsedInstruction = parser.parseInstruction(instruction);
expect(
parsedInstruction instanceof WormholeMultisigInstruction
).toBeTruthy();
if (parsedInstruction instanceof WormholeMultisigInstruction) {
expect(parsedInstruction.program).toBe(
MultisigInstructionProgram.WormholeBridge
);
expect(parsedInstruction.name).toBe("postMessage");
expect(
parsedInstruction.accounts.named["bridge"].pubkey.equals(
instruction.keys[0].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["bridge"].isSigner).toBe(
instruction.keys[0].isSigner
);
expect(parsedInstruction.accounts.named["bridge"].isWritable).toBe(
instruction.keys[0].isWritable
);
expect(
parsedInstruction.accounts.named["message"].pubkey.equals(
instruction.keys[1].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["message"].isSigner).toBe(
instruction.keys[1].isSigner
);
expect(parsedInstruction.accounts.named["message"].isWritable).toBe(
instruction.keys[1].isWritable
);
expect(
parsedInstruction.accounts.named["emitter"].pubkey.equals(
instruction.keys[2].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["emitter"].isSigner).toBe(
instruction.keys[2].isSigner
);
expect(parsedInstruction.accounts.named["emitter"].isWritable).toBe(
instruction.keys[2].isWritable
);
expect(
parsedInstruction.accounts.named["sequence"].pubkey.equals(
instruction.keys[3].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["sequence"].isSigner).toBe(
instruction.keys[3].isSigner
);
expect(parsedInstruction.accounts.named["sequence"].isWritable).toBe(
instruction.keys[3].isWritable
);
expect(
parsedInstruction.accounts.named["payer"].pubkey.equals(
instruction.keys[4].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["payer"].isSigner).toBe(
instruction.keys[4].isSigner
);
expect(parsedInstruction.accounts.named["payer"].isWritable).toBe(
instruction.keys[4].isWritable
);
expect(
parsedInstruction.accounts.named["feeCollector"].pubkey.equals(
instruction.keys[5].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["feeCollector"].isSigner).toBe(
instruction.keys[5].isSigner
);
expect(
parsedInstruction.accounts.named["feeCollector"].isWritable
).toBe(instruction.keys[5].isWritable);
expect(
parsedInstruction.accounts.named["clock"].pubkey.equals(
instruction.keys[6].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["clock"].isSigner).toBe(
instruction.keys[6].isSigner
);
expect(parsedInstruction.accounts.named["clock"].isWritable).toBe(
instruction.keys[6].isWritable
);
expect(
parsedInstruction.accounts.named["rent"].pubkey.equals(
instruction.keys[7].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["rent"].isSigner).toBe(
instruction.keys[7].isSigner
);
expect(parsedInstruction.accounts.named["rent"].isWritable).toBe(
instruction.keys[7].isWritable
);
expect(
parsedInstruction.accounts.named["systemProgram"].pubkey.equals(
instruction.keys[8].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["systemProgram"].isSigner).toBe(
instruction.keys[8].isSigner
);
expect(
parsedInstruction.accounts.named["systemProgram"].isWritable
).toBe(instruction.keys[8].isWritable);
expect(parsedInstruction.accounts.remaining.length).toBe(0);
expect(parsedInstruction.args.nonce).toBe(1);
expect(parsedInstruction.args.payload.equals(Buffer.from([0])));
expect(parsedInstruction.args.consistencyLevel).toBe(1);
expect(parsedInstruction.args.targetChain).toBeUndefined();
done();
} else {
done("Not instance of WormholeInstruction");
}
});
});
test("Wormhole multisig instruction parse: send message with governance payload", (done) => {
jest.setTimeout(60000);
const cluster: PythCluster = "devnet";
const wormholeProgram = createWormholeProgramInterface(
WORMHOLE_ADDRESS[cluster]!,
new AnchorProvider(
new Connection(getPythClusterApiUrl(cluster)),
new Wallet(new Keypair()),
AnchorProvider.defaultOptions()
)
);
const parser = new MultisigParser(cluster);
const executePostedVaaArgs: ExecutePostedVaaArgs = {
targetChainId: "pythnet" as ChainName,
instructions: [
SystemProgram.transfer({
fromPubkey: PublicKey.unique(),
toPubkey: PublicKey.unique(),
lamports: 890880,
}),
],
};
wormholeProgram.methods
.postMessage(0, encodeExecutePostedVaa(executePostedVaaArgs), 0)
.accounts({
bridge: PublicKey.unique(),
message: PublicKey.unique(),
emitter: PublicKey.unique(),
sequence: PublicKey.unique(),
feeCollector: PublicKey.unique(),
clock: PublicKey.unique(),
})
.instruction()
.then((instruction) => {
const parsedInstruction = parser.parseInstruction(instruction);
if (parsedInstruction instanceof WormholeMultisigInstruction) {
expect(
parsedInstruction instanceof WormholeMultisigInstruction
).toBeTruthy();
expect(parsedInstruction.program).toBe(
MultisigInstructionProgram.WormholeBridge
);
expect(parsedInstruction.name).toBe("postMessage");
expect(
parsedInstruction.accounts.named["bridge"].pubkey.equals(
instruction.keys[0].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["bridge"].isSigner).toBe(
instruction.keys[0].isSigner
);
expect(parsedInstruction.accounts.named["bridge"].isWritable).toBe(
instruction.keys[0].isWritable
);
expect(
parsedInstruction.accounts.named["message"].pubkey.equals(
instruction.keys[1].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["message"].isSigner).toBe(
instruction.keys[1].isSigner
);
expect(parsedInstruction.accounts.named["message"].isWritable).toBe(
instruction.keys[1].isWritable
);
expect(
parsedInstruction.accounts.named["emitter"].pubkey.equals(
instruction.keys[2].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["emitter"].isSigner).toBe(
instruction.keys[2].isSigner
);
expect(parsedInstruction.accounts.named["emitter"].isWritable).toBe(
instruction.keys[2].isWritable
);
expect(
parsedInstruction.accounts.named["sequence"].pubkey.equals(
instruction.keys[3].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["sequence"].isSigner).toBe(
instruction.keys[3].isSigner
);
expect(parsedInstruction.accounts.named["sequence"].isWritable).toBe(
instruction.keys[3].isWritable
);
expect(
parsedInstruction.accounts.named["payer"].pubkey.equals(
instruction.keys[4].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["payer"].isSigner).toBe(
instruction.keys[4].isSigner
);
expect(parsedInstruction.accounts.named["payer"].isWritable).toBe(
instruction.keys[4].isWritable
);
expect(
parsedInstruction.accounts.named["feeCollector"].pubkey.equals(
instruction.keys[5].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["feeCollector"].isSigner).toBe(
instruction.keys[5].isSigner
);
expect(
parsedInstruction.accounts.named["feeCollector"].isWritable
).toBe(instruction.keys[5].isWritable);
expect(
parsedInstruction.accounts.named["clock"].pubkey.equals(
instruction.keys[6].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["clock"].isSigner).toBe(
instruction.keys[6].isSigner
);
expect(parsedInstruction.accounts.named["clock"].isWritable).toBe(
instruction.keys[6].isWritable
);
expect(
parsedInstruction.accounts.named["rent"].pubkey.equals(
instruction.keys[7].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["rent"].isSigner).toBe(
instruction.keys[7].isSigner
);
expect(parsedInstruction.accounts.named["rent"].isWritable).toBe(
instruction.keys[7].isWritable
);
expect(
parsedInstruction.accounts.named["systemProgram"].pubkey.equals(
instruction.keys[8].pubkey
)
).toBeTruthy();
expect(parsedInstruction.accounts.named["systemProgram"].isSigner).toBe(
instruction.keys[8].isSigner
);
expect(
parsedInstruction.accounts.named["systemProgram"].isWritable
).toBe(instruction.keys[8].isWritable);
expect(parsedInstruction.accounts.remaining.length).toBe(0);
expect(parsedInstruction.args.nonce).toBe(0);
expect(
parsedInstruction.args.payload.equals(
encodeExecutePostedVaa(executePostedVaaArgs)
)
);
expect(parsedInstruction.args.consistencyLevel).toBe(0);
expect(parsedInstruction.args.governanceName).toBe("ExecutePostedVaa");
expect(parsedInstruction.args.governanceArgs.targetChainId).toBe(
"pythnet"
);
(
parsedInstruction.args.governanceArgs
.instructions as TransactionInstruction[]
).forEach((instruction, i) => {
expect(
instruction.programId.equals(
executePostedVaaArgs.instructions[i].programId
)
);
expect(
instruction.data.equals(executePostedVaaArgs.instructions[i].data)
);
instruction.keys.forEach((account, j) => {
expect(
account.pubkey.equals(
executePostedVaaArgs.instructions[i].keys[j].pubkey
)
).toBeTruthy();
expect(account.isSigner).toBe(
executePostedVaaArgs.instructions[i].keys[j].isSigner
);
expect(account.isWritable).toBe(
executePostedVaaArgs.instructions[i].keys[j].isWritable
);
});
});
done();
} else {
done("Not instance of WormholeInstruction");
}
});
});

View File

@ -5,6 +5,10 @@ import {
toChainName, toChainName,
} from "@certusone/wormhole-sdk"; } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout"; import * as BufferLayout from "@solana/buffer-layout";
import {
decodeExecutePostedVaa,
ExecutePostedVaaArgs,
} from "./ExecutePostedVaa";
export const ExecutorAction = { export const ExecutorAction = {
ExecutePostedVaa: 0, ExecutePostedVaa: 0,
@ -130,4 +134,17 @@ export function verifyHeader(
return governanceHeader; return governanceHeader;
} }
export function decodeGovernancePayload(data: Buffer): {
name: string;
args: ExecutePostedVaaArgs;
} {
const header = decodeHeader(data);
switch (header.action) {
case "ExecutePostedVaa":
return { name: "ExecutePostedVaa", args: decodeExecutePostedVaa(data) };
default:
throw "Not supported";
}
}
export { decodeExecutePostedVaa } from "./ExecutePostedVaa"; export { decodeExecutePostedVaa } from "./ExecutePostedVaa";

View File

@ -1,3 +1,5 @@
export * from "./multisig"; export * from "./multisig";
export * from "./propose"; export * from "./propose";
export * from "./governance_payload"; export * from "./governance_payload";
export * from "./wormhole";
export * from "./multisig_transaction";

View File

@ -0,0 +1,67 @@
import { createReadOnlyWormholeProgramInterface } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole";
import { WormholeInstructionCoder } from "@certusone/wormhole-sdk/lib/cjs/solana/wormhole/coder/instruction";
import { getPythClusterApiUrl } from "@pythnetwork/client/lib/cluster";
import { Connection, TransactionInstruction } from "@solana/web3.js";
import { MultisigInstruction, MultisigInstructionProgram } from ".";
import { decodeGovernancePayload } from "../governance_payload";
import { AnchorAccounts, resolveAccountNames } from "./anchor";
export class WormholeMultisigInstruction implements MultisigInstruction {
readonly program = MultisigInstructionProgram.WormholeBridge;
readonly name: string;
readonly args: { [key: string]: any };
readonly accounts: AnchorAccounts;
constructor(
name: string,
args: { [key: string]: any },
accounts: AnchorAccounts
) {
this.name = name;
this.args = args;
this.accounts = accounts;
}
static fromTransactionInstruction(
instruction: TransactionInstruction
): WormholeMultisigInstruction {
const wormholeProgram = createReadOnlyWormholeProgramInterface(
instruction.programId,
new Connection(getPythClusterApiUrl("devnet")) // Hack to get a decoder, this connection won't actually be used
);
const deserializedData = (
wormholeProgram.coder.instruction as WormholeInstructionCoder
).decode(instruction.data);
if (deserializedData) {
let result = new WormholeMultisigInstruction(
deserializedData.name,
deserializedData.data,
resolveAccountNames(
wormholeProgram.idl,
deserializedData.name,
instruction
)
);
if (result.name === "postMessage") {
try {
const decoded = decodeGovernancePayload(result.args.payload);
result.args.governanceName = decoded.name;
result.args.governanceArgs = decoded.args;
} catch {
result.args.governanceName = "Unrecognized governance message";
result.args.governanceArgs = {};
}
}
return result;
} else {
return new WormholeMultisigInstruction(
"Unrecognized instruction",
{},
{ named: {}, remaining: instruction.keys }
);
}
}
}

View File

@ -0,0 +1,30 @@
import { Idl } from "@coral-xyz/anchor";
import { AccountMeta, TransactionInstruction } from "@solana/web3.js";
type NamedAccounts = Record<string, AccountMeta>;
type RemainingAccounts = AccountMeta[];
export type AnchorAccounts = {
named: NamedAccounts;
remaining: RemainingAccounts;
};
export function resolveAccountNames(
idl: Idl,
name: string,
instruction: TransactionInstruction
): { named: NamedAccounts; remaining: RemainingAccounts } {
const ix = idl.instructions.find((ix) => ix.name == name);
if (!ix) {
throw Error("Instruction name not found");
}
const named: NamedAccounts = {};
const remaining: RemainingAccounts = [];
instruction.keys.map((account, idx) => {
if (idx < ix.accounts.length) {
named[ix.accounts[idx].name] = account;
} else {
remaining.push(account);
}
});
return { named, remaining };
}

View File

@ -0,0 +1,59 @@
import {
getPythProgramKeyForCluster,
PythCluster,
} from "@pythnetwork/client/lib/cluster";
import { PublicKey, TransactionInstruction } from "@solana/web3.js";
import { WORMHOLE_ADDRESS } from "../wormhole";
import { WormholeMultisigInstruction } from "./WormholeMultisigInstruction";
export enum MultisigInstructionProgram {
PythOracle,
WormholeBridge,
UnrecognizedProgram,
}
export interface MultisigInstruction {
readonly program: MultisigInstructionProgram;
}
export class UnrecognizedProgram implements MultisigInstruction {
readonly program = MultisigInstructionProgram.UnrecognizedProgram;
readonly instruction: TransactionInstruction;
constructor(instruction: TransactionInstruction) {
this.instruction = instruction;
}
static fromTransactionInstruction(
instruction: TransactionInstruction
): UnrecognizedProgram {
return new UnrecognizedProgram(instruction);
}
}
export class PythMultisigInstruction implements MultisigInstruction {
readonly program = MultisigInstructionProgram.PythOracle;
}
export class MultisigParser {
readonly pythOracleAddress: PublicKey;
readonly wormholeBridgeAddress: PublicKey | undefined;
constructor(cluster: PythCluster) {
this.pythOracleAddress = getPythProgramKeyForCluster(cluster);
this.wormholeBridgeAddress = WORMHOLE_ADDRESS[cluster];
}
parseInstruction(instruction: TransactionInstruction): MultisigInstruction {
if (
this.wormholeBridgeAddress &&
instruction.programId.equals(this.wormholeBridgeAddress)
) {
return WormholeMultisigInstruction.fromTransactionInstruction(
instruction
);
} else {
return UnrecognizedProgram.fromTransactionInstruction(instruction);
}
}
}

View File

@ -0,0 +1,10 @@
import { PythCluster } from "@pythnetwork/client/lib/cluster";
import { PublicKey } from "@solana/web3.js";
export const WORMHOLE_ADDRESS: Record<PythCluster, PublicKey | undefined> = {
"mainnet-beta": new PublicKey("worm2ZoG2kUd4vFXhvjh93UUH596ayRfgQ2MgjNMTth"),
pythtest: new PublicKey("EUrRARh92Cdc54xrDn6qzaqjA77NRrCcfbr8kPwoTL4z"),
devnet: new PublicKey("3u8hJUVTA4jH1wYAyUur7FFZVQ8H635K3tSHHF4ssjQ5"),
pythnet: new PublicKey("H3fxXJ86ADW2PNuDDmZJg6mzTtPxkYCpNuQUTgmJ7AjU"),
testnet: undefined,
};