[xc-admin] Decoders can't throw (#486)

* Push code

* Format
This commit is contained in:
guibescos 2023-01-12 20:35:19 -06:00 committed by GitHub
parent 411ed32859
commit bab75170f9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 109 additions and 67 deletions

View File

@ -14,8 +14,8 @@ test("GovernancePayload ser/de", (done) => {
buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26]))
).toBeTruthy();
let governanceHeader = PythGovernanceHeader.decode(buffer);
expect(governanceHeader.targetChainId).toBe("pythnet");
expect(governanceHeader.action).toBe("ExecutePostedVaa");
expect(governanceHeader?.targetChainId).toBe("pythnet");
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
// Valid header 2
expectedGovernanceHeader = new PythGovernanceHeader(
@ -37,25 +37,25 @@ test("GovernancePayload ser/de", (done) => {
expect(governanceHeader?.action).toBe("SetFee");
// Wrong magic number
expect(() =>
expect(
PythGovernanceHeader.decode(
Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0])
)
).toThrow("Wrong magic number");
).toBeUndefined();
// Wrong chain
expect(() =>
expect(
PythGovernanceHeader.decode(
Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0])
)
).toThrow("Chain Id not found");
).toBeUndefined();
// Wrong module/action combination
expect(() =>
expect(
PythGovernanceHeader.decode(
Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0])
)
).toThrow("Invalid header, action doesn't match module");
).toBeUndefined();
// Decode executePostVaa with empty instructions
let expectedExecutePostedVaa = new ExecutePostedVaa("pythnet", []);

View File

@ -199,9 +199,6 @@ test("Wormhole multisig instruction parse: send message with governance payload"
.then((instruction) => {
const parsedInstruction = parser.parseInstruction(instruction);
if (parsedInstruction instanceof WormholeMultisigInstruction) {
expect(
parsedInstruction instanceof WormholeMultisigInstruction
).toBeTruthy();
expect(parsedInstruction.program).toBe(
MultisigInstructionProgram.WormholeBridge
);
@ -313,15 +310,13 @@ test("Wormhole multisig instruction parse: send message with governance payload"
);
expect(parsedInstruction.args.consistencyLevel).toBe(0);
if (
parsedInstruction.args.governanceAction instanceof ExecutePostedVaa
) {
expect(parsedInstruction.args.governanceAction.targetChainId).toBe(
if (parsedInstruction.governanceAction instanceof ExecutePostedVaa) {
expect(parsedInstruction.governanceAction.targetChainId).toBe(
"pythnet"
);
(
parsedInstruction.args.governanceAction
parsedInstruction.governanceAction
.instructions as TransactionInstruction[]
).forEach((instruction, i) => {
expect(

View File

@ -1,6 +1,10 @@
import { ChainId, ChainName } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout";
import { PythGovernanceAction, PythGovernanceHeader } from ".";
import {
PythGovernanceAction,
PythGovernanceHeader,
safeLayoutDecode,
} from ".";
import { Layout } from "@solana/buffer-layout";
import {
AccountMeta,
@ -75,11 +79,16 @@ export class ExecutePostedVaa implements PythGovernanceAction {
}
/** Decode ExecutePostedVaa */
static decode(data: Buffer): ExecutePostedVaa {
let header = PythGovernanceHeader.decode(data);
let deserialized = this.layout.decode(
static decode(data: Buffer): ExecutePostedVaa | undefined {
const header = PythGovernanceHeader.decode(data);
if (!header) return undefined;
const deserialized = safeLayoutDecode(
this.layout,
data.subarray(PythGovernanceHeader.span)
);
if (!deserialized) return undefined;
let instructions: TransactionInstruction[] = deserialized.map((ix) => {
let programId: PublicKey = new PublicKey(ix.programId);
let keys: AccountMeta[] = ix.accounts.map((acc) => {

View File

@ -30,7 +30,7 @@ export const TargetAction = {
/** Helper to get the ActionName from a (moduleId, actionId) tuple*/
export function toActionName(
deserialized: Readonly<{ moduleId: number; actionId: number }>
): ActionName {
): ActionName | undefined {
if (deserialized.moduleId == MODULE_EXECUTOR && deserialized.actionId == 0) {
return "ExecutePostedVaa";
} else if (deserialized.moduleId == MODULE_TARGET) {
@ -49,7 +49,7 @@ export function toActionName(
return "RequestGovernanceDataSourceTransfer";
}
}
throw new Error("Invalid header, action doesn't match module");
return undefined;
}
export declare type ActionName =
@ -84,23 +84,28 @@ export class PythGovernanceHeader {
this.action = action;
}
/** Decode Pyth Governance Header */
static decode(data: Buffer): PythGovernanceHeader {
let deserialized = this.layout.decode(data);
if (deserialized.magicNumber !== MAGIC_NUMBER) {
throw new Error("Wrong magic number");
}
static decode(data: Buffer): PythGovernanceHeader | undefined {
const deserialized = safeLayoutDecode(this.layout, data);
if (!toChainName(deserialized.chain)) {
throw new Error("Chain Id not found");
}
if (!deserialized) return undefined;
return new PythGovernanceHeader(
toChainName(deserialized.chain),
toActionName({
actionId: deserialized.action,
moduleId: deserialized.module,
})
);
if (deserialized.magicNumber !== MAGIC_NUMBER) return undefined;
if (!toChainName(deserialized.chain)) return undefined;
const actionName = toActionName({
actionId: deserialized.action,
moduleId: deserialized.module,
});
if (actionName) {
return new PythGovernanceHeader(
toChainName(deserialized.chain),
actionName
);
} else {
return undefined;
}
}
/** Encode Pyth Governance Header */
@ -134,13 +139,28 @@ export const MODULE_EXECUTOR = 0;
export const MODULE_TARGET = 1;
/** Decode a governance payload */
export function decodeGovernancePayload(data: Buffer): PythGovernanceAction {
export function decodeGovernancePayload(
data: Buffer
): PythGovernanceAction | undefined {
const header = PythGovernanceHeader.decode(data);
if (!header) return undefined;
switch (header.action) {
case "ExecutePostedVaa":
return ExecutePostedVaa.decode(data);
default:
throw "Not supported";
return undefined;
}
}
export function safeLayoutDecode<T>(
layout: BufferLayout.Layout<T>,
data: Buffer
): T | undefined {
try {
return layout.decode(data);
} catch {
return undefined;
}
}

View File

@ -1,4 +1,8 @@
import { MultisigInstruction, MultisigInstructionProgram } from ".";
import {
MultisigInstruction,
MultisigInstructionProgram,
UNRECOGNIZED_INSTRUCTION,
} from ".";
import { AnchorAccounts, resolveAccountNames } from "./anchor";
import { pythIdl, pythOracleCoder } from "@pythnetwork/client";
import { TransactionInstruction } from "@solana/web3.js";
@ -35,8 +39,8 @@ export class PythMultisigInstruction implements MultisigInstruction {
);
} else {
return new PythMultisigInstruction(
"Unrecognized instruction",
{},
UNRECOGNIZED_INSTRUCTION,
{ data: instruction.data },
{ named: {}, remaining: instruction.keys }
);
}

View File

@ -2,7 +2,11 @@ import { createReadOnlyWormholeProgramInterface } from "@certusone/wormhole-sdk/
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 {
MultisigInstruction,
MultisigInstructionProgram,
UNRECOGNIZED_INSTRUCTION,
} from ".";
import {
decodeGovernancePayload,
PythGovernanceAction,
@ -14,15 +18,18 @@ export class WormholeMultisigInstruction implements MultisigInstruction {
readonly name: string;
readonly args: { [key: string]: any };
readonly accounts: AnchorAccounts;
readonly governanceAction: PythGovernanceAction | undefined;
constructor(
name: string,
args: { [key: string]: any },
accounts: AnchorAccounts
accounts: AnchorAccounts,
governanceAction: PythGovernanceAction | undefined
) {
this.name = name;
this.args = args;
this.accounts = accounts;
this.governanceAction = governanceAction;
}
static fromTransactionInstruction(
@ -38,32 +45,38 @@ export class WormholeMultisigInstruction implements MultisigInstruction {
).decode(instruction.data);
if (deserializedData) {
let result = new WormholeMultisigInstruction(
deserializedData.name,
deserializedData.data,
resolveAccountNames(
wormholeProgram.idl,
deserializedData.name,
instruction
)
);
if (deserializedData.name === "postMessage") {
const decodedGovernanceAction: PythGovernanceAction | undefined =
decodeGovernancePayload((deserializedData.data as any).payload);
if (result.name === "postMessage") {
try {
const decoded: PythGovernanceAction = decodeGovernancePayload(
result.args.payload
);
result.args.governanceAction = decoded;
} catch {
result.args.governanceAction = {};
}
return new WormholeMultisigInstruction(
deserializedData.name,
deserializedData.data,
resolveAccountNames(
wormholeProgram.idl,
deserializedData.name,
instruction
),
decodedGovernanceAction
);
} else {
return new WormholeMultisigInstruction(
deserializedData.name,
deserializedData.data,
resolveAccountNames(
wormholeProgram.idl,
deserializedData.name,
instruction
),
undefined
);
}
return result;
} else {
return new WormholeMultisigInstruction(
"Unrecognized instruction",
{},
{ named: {}, remaining: instruction.keys }
UNRECOGNIZED_INSTRUCTION,
{ data: instruction.data },
{ named: {}, remaining: instruction.keys },
undefined
);
}
}

View File

@ -15,7 +15,7 @@ export function resolveAccountNames(
): { named: NamedAccounts; remaining: RemainingAccounts } {
const ix = idl.instructions.find((ix) => ix.name == name);
if (!ix) {
throw Error("Instruction name not found");
return { named: {}, remaining: instruction.keys };
}
const named: NamedAccounts = {};
const remaining: RemainingAccounts = [];

View File

@ -7,6 +7,7 @@ import { WORMHOLE_ADDRESS } from "../wormhole";
import { PythMultisigInstruction } from "./PythMultisigInstruction";
import { WormholeMultisigInstruction } from "./WormholeMultisigInstruction";
export const UNRECOGNIZED_INSTRUCTION = "unrecognizedInstruction";
export enum MultisigInstructionProgram {
PythOracle,
WormholeBridge,