[xc-admin] Header becomes class (#484)

* Header becomes class 2

* Revert test

* Cleanup

* Cleanup
This commit is contained in:
guibescos 2023-01-12 13:36:06 -06:00 committed by GitHub
parent 31ab162168
commit 411ed32859
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 129 additions and 159 deletions

View File

@ -1,66 +1,60 @@
import { ChainName } from "@certusone/wormhole-sdk"; import { PublicKey, SystemProgram } from "@solana/web3.js";
import { PACKET_DATA_SIZE, PublicKey, SystemProgram } from "@solana/web3.js"; import { PythGovernanceHeader, ExecutePostedVaa } from "..";
import { ActionName, decodeHeader, encodeHeader, ExecutePostedVaa } from "..";
test("GovernancePayload ser/de", (done) => { test("GovernancePayload ser/de", (done) => {
jest.setTimeout(60000); jest.setTimeout(60000);
// Valid header 1 // Valid header 1
let expectedGovernanceHeader = { let expectedGovernanceHeader = new PythGovernanceHeader(
targetChainId: "pythnet" as ChainName, "pythnet",
action: "ExecutePostedVaa" as ActionName, "ExecutePostedVaa"
}; );
let buffer = Buffer.alloc(PACKET_DATA_SIZE); let buffer = expectedGovernanceHeader.encode();
let span = encodeHeader(expectedGovernanceHeader, buffer);
expect( expect(
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26])) buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 26]))
).toBeTruthy(); ).toBeTruthy();
let governanceHeader = PythGovernanceHeader.decode(buffer);
let governanceHeader = decodeHeader(buffer.subarray(0, span)); expect(governanceHeader.targetChainId).toBe("pythnet");
expect(governanceHeader?.targetChainId).toBe("pythnet"); expect(governanceHeader.action).toBe("ExecutePostedVaa");
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
// Valid header 2 // Valid header 2
expectedGovernanceHeader = { expectedGovernanceHeader = new PythGovernanceHeader(
targetChainId: "unset" as ChainName, "unset",
action: "ExecutePostedVaa" as ActionName, "ExecutePostedVaa"
}; );
buffer = Buffer.alloc(PACKET_DATA_SIZE); buffer = expectedGovernanceHeader.encode();
span = encodeHeader(expectedGovernanceHeader, buffer); expect(buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 0]))).toBeTruthy();
expect( governanceHeader = PythGovernanceHeader.decode(buffer);
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 0]))
).toBeTruthy();
governanceHeader = decodeHeader(buffer.subarray(0, span));
expect(governanceHeader?.targetChainId).toBe("unset"); expect(governanceHeader?.targetChainId).toBe("unset");
expect(governanceHeader?.action).toBe("ExecutePostedVaa"); expect(governanceHeader?.action).toBe("ExecutePostedVaa");
// Valid header 3 // Valid header 3
expectedGovernanceHeader = { expectedGovernanceHeader = new PythGovernanceHeader("solana", "SetFee");
targetChainId: "solana" as ChainName, buffer = expectedGovernanceHeader.encode();
action: "SetFee" as ActionName, expect(buffer.equals(Buffer.from([80, 84, 71, 77, 1, 3, 0, 1]))).toBeTruthy();
}; governanceHeader = PythGovernanceHeader.decode(buffer);
buffer = Buffer.alloc(PACKET_DATA_SIZE);
span = encodeHeader(expectedGovernanceHeader, buffer);
expect(
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 1, 3, 0, 1]))
).toBeTruthy();
governanceHeader = decodeHeader(buffer.subarray(0, span));
expect(governanceHeader?.targetChainId).toBe("solana"); expect(governanceHeader?.targetChainId).toBe("solana");
expect(governanceHeader?.action).toBe("SetFee"); expect(governanceHeader?.action).toBe("SetFee");
// Wrong magic number // Wrong magic number
expect(() => expect(() =>
decodeHeader(Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0])) PythGovernanceHeader.decode(
Buffer.from([0, 0, 0, 0, 0, 0, 0, 26, 0, 0, 0, 0])
)
).toThrow("Wrong magic number"); ).toThrow("Wrong magic number");
// Wrong chain // Wrong chain
expect(() => expect(() =>
decodeHeader(Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0])) PythGovernanceHeader.decode(
Buffer.from([80, 84, 71, 77, 0, 0, 255, 255, 0, 0, 0, 0])
)
).toThrow("Chain Id not found"); ).toThrow("Chain Id not found");
// Wrong module/action combination // Wrong module/action combination
expect(() => expect(() =>
decodeHeader(Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0])) PythGovernanceHeader.decode(
Buffer.from([80, 84, 71, 77, 0, 1, 0, 26, 0, 0, 0, 0])
)
).toThrow("Invalid header, action doesn't match module"); ).toThrow("Invalid header, action doesn't match module");
// Decode executePostVaa with empty instructions // Decode executePostVaa with empty instructions

View File

@ -1,11 +1,6 @@
import { ChainId, ChainName } from "@certusone/wormhole-sdk"; import { ChainId, ChainName } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout"; import * as BufferLayout from "@solana/buffer-layout";
import { import { PythGovernanceAction, PythGovernanceHeader } from ".";
encodeHeader,
governanceHeaderLayout,
PythGovernanceAction,
verifyHeader,
} from ".";
import { Layout } from "@solana/buffer-layout"; import { Layout } from "@solana/buffer-layout";
import { import {
AccountMeta, AccountMeta,
@ -56,30 +51,20 @@ export const accountMetaLayout = BufferLayout.struct<AccountMetadata>([
BufferLayout.u8("isSigner"), BufferLayout.u8("isSigner"),
BufferLayout.u8("isWritable"), BufferLayout.u8("isWritable"),
]); ]);
export const instructionDataLayout = BufferLayout.struct<InstructionData>([ export const instructionDataLayout = BufferLayout.struct<InstructionData>([
BufferLayout.blob(32, "programId"), BufferLayout.blob(32, "programId"),
new Vector<AccountMetadata>(accountMetaLayout, "accounts"), new Vector<AccountMetadata>(accountMetaLayout, "accounts"),
new Vector<number>(BufferLayout.u8(), "data"), new Vector<number>(BufferLayout.u8(), "data"),
]); ]);
export const executePostedVaaLayout: BufferLayout.Structure<
Readonly<{
header: Readonly<{
magicNumber: number;
module: number;
action: number;
chain: ChainId;
}>;
instructions: InstructionData[];
}>
> = BufferLayout.struct([
governanceHeaderLayout(),
new Vector<InstructionData>(instructionDataLayout, "instructions"),
]);
export class ExecutePostedVaa implements PythGovernanceAction { export class ExecutePostedVaa implements PythGovernanceAction {
readonly targetChainId: ChainName; readonly targetChainId: ChainName;
readonly instructions: TransactionInstruction[]; readonly instructions: TransactionInstruction[];
static layout: Vector<InstructionData> = new Vector<InstructionData>(
instructionDataLayout,
"instructions"
);
constructor( constructor(
targetChainId: ChainName, targetChainId: ChainName,
@ -89,37 +74,36 @@ export class ExecutePostedVaa implements PythGovernanceAction {
this.instructions = instructions; this.instructions = instructions;
} }
/** Decode ExecutePostedVaaArgs */ /** Decode ExecutePostedVaa */
static decode(data: Buffer): ExecutePostedVaa { static decode(data: Buffer): ExecutePostedVaa {
let deserialized = executePostedVaaLayout.decode(data); let header = PythGovernanceHeader.decode(data);
let deserialized = this.layout.decode(
let header = verifyHeader(deserialized.header); data.subarray(PythGovernanceHeader.span)
let instructions: TransactionInstruction[] = deserialized.instructions.map(
(ix) => {
let programId: PublicKey = new PublicKey(ix.programId);
let keys: AccountMeta[] = ix.accounts.map((acc) => {
return {
pubkey: new PublicKey(acc.pubkey),
isSigner: Boolean(acc.isSigner),
isWritable: Boolean(acc.isWritable),
};
});
let data: Buffer = Buffer.from(ix.data);
return { programId, keys, data };
}
); );
let instructions: TransactionInstruction[] = deserialized.map((ix) => {
let programId: PublicKey = new PublicKey(ix.programId);
let keys: AccountMeta[] = ix.accounts.map((acc) => {
return {
pubkey: new PublicKey(acc.pubkey),
isSigner: Boolean(acc.isSigner),
isWritable: Boolean(acc.isWritable),
};
});
let data: Buffer = Buffer.from(ix.data);
return { programId, keys, data };
});
return new ExecutePostedVaa(header.targetChainId, instructions); return new ExecutePostedVaa(header.targetChainId, instructions);
} }
/** Encode ExecutePostedVaaArgs */ /** Encode ExecutePostedVaa */
encode(): Buffer { encode(): Buffer {
// PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload will never be bigger than that const headerBuffer = new PythGovernanceHeader(
this.targetChainId,
"ExecutePostedVaa"
).encode();
// The code will crash if the payload is actually bigger than PACKET_DATA_SIZE. But PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload should never be bigger than this anyway
const buffer = Buffer.alloc(PACKET_DATA_SIZE); const buffer = Buffer.alloc(PACKET_DATA_SIZE);
const offset = encodeHeader(
{ action: "ExecutePostedVaa", targetChainId: this.targetChainId },
buffer
);
let instructions: InstructionData[] = this.instructions.map((ix) => { let instructions: InstructionData[] = this.instructions.map((ix) => {
let programId = ix.programId.toBytes(); let programId = ix.programId.toBytes();
let accounts: AccountMetadata[] = ix.keys.map((acc) => { let accounts: AccountMetadata[] = ix.keys.map((acc) => {
@ -133,13 +117,7 @@ export class ExecutePostedVaa implements PythGovernanceAction {
return { programId, accounts, data }; return { programId, accounts, data };
}); });
const span = const span = ExecutePostedVaa.layout.encode(instructions, buffer);
offset + return Buffer.concat([headerBuffer, buffer.subarray(0, span)]);
new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
instructions,
buffer,
offset
);
return buffer.subarray(0, span);
} }
} }

View File

@ -5,6 +5,7 @@ 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 { PACKET_DATA_SIZE } from "@solana/web3.js";
import { ExecutePostedVaa } from "./ExecutePostedVaa"; import { ExecutePostedVaa } from "./ExecutePostedVaa";
export interface PythGovernanceAction { export interface PythGovernanceAction {
@ -12,6 +13,7 @@ export interface PythGovernanceAction {
encode(): Buffer; encode(): Buffer;
} }
/** Each of the actions that can be directed to the Executor Module */
export const ExecutorAction = { export const ExecutorAction = {
ExecutePostedVaa: 0, ExecutePostedVaa: 0,
} as const; } as const;
@ -25,6 +27,7 @@ export const TargetAction = {
RequestGovernanceDataSourceTransfer: 5, RequestGovernanceDataSourceTransfer: 5,
} as const; } as const;
/** Helper to get the ActionName from a (moduleId, actionId) tuple*/
export function toActionName( export function toActionName(
deserialized: Readonly<{ moduleId: number; actionId: number }> deserialized: Readonly<{ moduleId: number; actionId: number }>
): ActionName { ): ActionName {
@ -48,28 +51,23 @@ export function toActionName(
} }
throw new Error("Invalid header, action doesn't match module"); throw new Error("Invalid header, action doesn't match module");
} }
export declare type ActionName = export declare type ActionName =
| keyof typeof ExecutorAction | keyof typeof ExecutorAction
| keyof typeof TargetAction; | keyof typeof TargetAction;
export type PythGovernanceHeader = { /** Governance header that should be in every Pyth crosschain governance message*/
targetChainId: ChainName; export class PythGovernanceHeader {
action: ActionName; readonly targetChainId: ChainName;
}; readonly action: ActionName;
static layout: BufferLayout.Structure<
export const MAGIC_NUMBER = 0x4d475450; Readonly<{
export const MODULE_EXECUTOR = 0; magicNumber: number;
export const MODULE_TARGET = 1; module: number;
action: number;
export function governanceHeaderLayout(): BufferLayout.Structure< chain: ChainId;
Readonly<{ }>
magicNumber: number; > = BufferLayout.struct(
module: number;
action: number;
chain: ChainId;
}>
> {
return BufferLayout.struct(
[ [
BufferLayout.u32("magicNumber"), BufferLayout.u32("magicNumber"),
BufferLayout.u8("module"), BufferLayout.u8("module"),
@ -78,66 +76,66 @@ export function governanceHeaderLayout(): BufferLayout.Structure<
], ],
"header" "header"
); );
} /** Span of the serialized governance header */
static span = 8;
/** Decode Pyth Governance Header and return undefined if the header is invalid */ constructor(targetChainId: ChainName, action: ActionName) {
export function decodeHeader(data: Buffer): PythGovernanceHeader { this.targetChainId = targetChainId;
let deserialized = governanceHeaderLayout().decode(data); this.action = action;
return verifyHeader(deserialized);
}
export function encodeHeader(
src: PythGovernanceHeader,
buffer: Buffer
): number {
let module: number;
let action: number;
if (src.action in ExecutorAction) {
module = MODULE_EXECUTOR;
action = ExecutorAction[src.action as keyof typeof ExecutorAction];
} else {
module = MODULE_TARGET;
action = TargetAction[src.action as keyof typeof TargetAction];
} }
return governanceHeaderLayout().encode( /** Decode Pyth Governance Header */
{ static decode(data: Buffer): PythGovernanceHeader {
magicNumber: MAGIC_NUMBER, let deserialized = this.layout.decode(data);
module, if (deserialized.magicNumber !== MAGIC_NUMBER) {
action, throw new Error("Wrong magic number");
chain: toChainId(src.targetChainId), }
},
buffer
);
}
export function verifyHeader( if (!toChainName(deserialized.chain)) {
deserialized: Readonly<{ throw new Error("Chain Id not found");
magicNumber: number; }
module: number;
action: number; return new PythGovernanceHeader(
chain: ChainId; toChainName(deserialized.chain),
}> toActionName({
): PythGovernanceHeader { actionId: deserialized.action,
if (deserialized.magicNumber !== MAGIC_NUMBER) { moduleId: deserialized.module,
throw new Error("Wrong magic number"); })
);
} }
if (!toChainName(deserialized.chain)) { /** Encode Pyth Governance Header */
throw new Error("Chain Id not found"); encode(): Buffer {
// The code will crash if the payload is actually bigger than PACKET_DATA_SIZE. But PACKET_DATA_SIZE is the maximum transaction size of Solana, so our serialized payload should never be bigger than this anyway
const buffer = Buffer.alloc(PACKET_DATA_SIZE);
let module: number;
let action: number;
if (this.action in ExecutorAction) {
module = MODULE_EXECUTOR;
action = ExecutorAction[this.action as keyof typeof ExecutorAction];
} else {
module = MODULE_TARGET;
action = TargetAction[this.action as keyof typeof TargetAction];
}
const span = PythGovernanceHeader.layout.encode(
{
magicNumber: MAGIC_NUMBER,
module,
action,
chain: toChainId(this.targetChainId),
},
buffer
);
return buffer.subarray(0, span);
} }
let governanceHeader: PythGovernanceHeader = {
targetChainId: toChainName(deserialized.chain),
action: toActionName({
actionId: deserialized.action,
moduleId: deserialized.module,
}),
};
return governanceHeader;
} }
export const MAGIC_NUMBER = 0x4d475450;
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 {
const header = decodeHeader(data); const header = PythGovernanceHeader.decode(data);
switch (header.action) { switch (header.action) {
case "ExecutePostedVaa": case "ExecutePostedVaa":
return ExecutePostedVaa.decode(data); return ExecutePostedVaa.decode(data);