[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 { PACKET_DATA_SIZE, PublicKey, SystemProgram } from "@solana/web3.js";
import { ActionName, decodeHeader, encodeHeader, ExecutePostedVaa } from "..";
import { PublicKey, SystemProgram } from "@solana/web3.js";
import { PythGovernanceHeader, ExecutePostedVaa } from "..";
test("GovernancePayload ser/de", (done) => {
jest.setTimeout(60000);
// Valid header 1
let expectedGovernanceHeader = {
targetChainId: "pythnet" as ChainName,
action: "ExecutePostedVaa" as ActionName,
};
let buffer = Buffer.alloc(PACKET_DATA_SIZE);
let span = encodeHeader(expectedGovernanceHeader, buffer);
let expectedGovernanceHeader = new PythGovernanceHeader(
"pythnet",
"ExecutePostedVaa"
);
let buffer = expectedGovernanceHeader.encode();
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();
let governanceHeader = decodeHeader(buffer.subarray(0, span));
expect(governanceHeader?.targetChainId).toBe("pythnet");
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
let governanceHeader = PythGovernanceHeader.decode(buffer);
expect(governanceHeader.targetChainId).toBe("pythnet");
expect(governanceHeader.action).toBe("ExecutePostedVaa");
// Valid header 2
expectedGovernanceHeader = {
targetChainId: "unset" as ChainName,
action: "ExecutePostedVaa" as ActionName,
};
buffer = Buffer.alloc(PACKET_DATA_SIZE);
span = encodeHeader(expectedGovernanceHeader, buffer);
expect(
buffer.subarray(0, span).equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 0]))
).toBeTruthy();
governanceHeader = decodeHeader(buffer.subarray(0, span));
expectedGovernanceHeader = new PythGovernanceHeader(
"unset",
"ExecutePostedVaa"
);
buffer = expectedGovernanceHeader.encode();
expect(buffer.equals(Buffer.from([80, 84, 71, 77, 0, 0, 0, 0]))).toBeTruthy();
governanceHeader = PythGovernanceHeader.decode(buffer);
expect(governanceHeader?.targetChainId).toBe("unset");
expect(governanceHeader?.action).toBe("ExecutePostedVaa");
// Valid header 3
expectedGovernanceHeader = {
targetChainId: "solana" as ChainName,
action: "SetFee" as ActionName,
};
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));
expectedGovernanceHeader = new PythGovernanceHeader("solana", "SetFee");
buffer = expectedGovernanceHeader.encode();
expect(buffer.equals(Buffer.from([80, 84, 71, 77, 1, 3, 0, 1]))).toBeTruthy();
governanceHeader = PythGovernanceHeader.decode(buffer);
expect(governanceHeader?.targetChainId).toBe("solana");
expect(governanceHeader?.action).toBe("SetFee");
// Wrong magic number
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");
// Wrong chain
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");
// Wrong module/action combination
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");
// Decode executePostVaa with empty instructions

View File

@ -1,11 +1,6 @@
import { ChainId, ChainName } from "@certusone/wormhole-sdk";
import * as BufferLayout from "@solana/buffer-layout";
import {
encodeHeader,
governanceHeaderLayout,
PythGovernanceAction,
verifyHeader,
} from ".";
import { PythGovernanceAction, PythGovernanceHeader } from ".";
import { Layout } from "@solana/buffer-layout";
import {
AccountMeta,
@ -56,30 +51,20 @@ export const accountMetaLayout = BufferLayout.struct<AccountMetadata>([
BufferLayout.u8("isSigner"),
BufferLayout.u8("isWritable"),
]);
export const instructionDataLayout = BufferLayout.struct<InstructionData>([
BufferLayout.blob(32, "programId"),
new Vector<AccountMetadata>(accountMetaLayout, "accounts"),
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 {
readonly targetChainId: ChainName;
readonly instructions: TransactionInstruction[];
static layout: Vector<InstructionData> = new Vector<InstructionData>(
instructionDataLayout,
"instructions"
);
constructor(
targetChainId: ChainName,
@ -89,14 +74,13 @@ export class ExecutePostedVaa implements PythGovernanceAction {
this.instructions = instructions;
}
/** Decode ExecutePostedVaaArgs */
/** Decode ExecutePostedVaa */
static decode(data: Buffer): ExecutePostedVaa {
let deserialized = executePostedVaaLayout.decode(data);
let header = verifyHeader(deserialized.header);
let instructions: TransactionInstruction[] = deserialized.instructions.map(
(ix) => {
let header = PythGovernanceHeader.decode(data);
let deserialized = this.layout.decode(
data.subarray(PythGovernanceHeader.span)
);
let instructions: TransactionInstruction[] = deserialized.map((ix) => {
let programId: PublicKey = new PublicKey(ix.programId);
let keys: AccountMeta[] = ix.accounts.map((acc) => {
return {
@ -107,19 +91,19 @@ export class ExecutePostedVaa implements PythGovernanceAction {
});
let data: Buffer = Buffer.from(ix.data);
return { programId, keys, data };
}
);
});
return new ExecutePostedVaa(header.targetChainId, instructions);
}
/** Encode ExecutePostedVaaArgs */
/** Encode ExecutePostedVaa */
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 offset = encodeHeader(
{ action: "ExecutePostedVaa", targetChainId: this.targetChainId },
buffer
);
let instructions: InstructionData[] = this.instructions.map((ix) => {
let programId = ix.programId.toBytes();
let accounts: AccountMetadata[] = ix.keys.map((acc) => {
@ -133,13 +117,7 @@ export class ExecutePostedVaa implements PythGovernanceAction {
return { programId, accounts, data };
});
const span =
offset +
new Vector<InstructionData>(instructionDataLayout, "instructions").encode(
instructions,
buffer,
offset
);
return buffer.subarray(0, span);
const span = ExecutePostedVaa.layout.encode(instructions, buffer);
return Buffer.concat([headerBuffer, buffer.subarray(0, span)]);
}
}

View File

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