Skip to content

Commit 78917f6

Browse files
authored
Implement PythGovernanceAction (#906)
* blah * rename workflow * grrr * ok progress * ok progress * ok progress * grrr * ok * fix * fix layout
1 parent 52ae0b8 commit 78917f6

File tree

12 files changed

+828
-177
lines changed

12 files changed

+828
-177
lines changed

.github/workflows/xc-admin-frontend-image-push.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
name: Build and Push Cross Chain Admin Frontend
1+
name: xc_admin_frontend Docker Image
22
on:
33
pull_request:
44
push:

governance/xc_admin/packages/xc_admin_common/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
"@types/lodash": "^4.14.191",
3939
"jest": "^29.3.1",
4040
"prettier": "^2.8.1",
41-
"ts-jest": "^29.0.3"
41+
"ts-jest": "^29.0.3",
42+
"fast-check": "^3.10.0"
4243
}
4344
}

governance/xc_admin/packages/xc_admin_common/src/__tests__/GovernancePayload.test.ts

Lines changed: 174 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,39 @@
11
import { PublicKey, SystemProgram } from "@solana/web3.js";
2-
import { PythGovernanceHeader, ExecutePostedVaa } from "..";
2+
import {
3+
PythGovernanceHeader,
4+
ExecutePostedVaa,
5+
MODULES,
6+
MODULE_EXECUTOR,
7+
TargetAction,
8+
ExecutorAction,
9+
ActionName,
10+
PythGovernanceAction,
11+
decodeGovernancePayload,
12+
} from "..";
13+
import * as fc from "fast-check";
14+
import {
15+
ChainId,
16+
ChainName,
17+
CHAINS,
18+
toChainId,
19+
toChainName,
20+
} from "@certusone/wormhole-sdk";
21+
import { Arbitrary, IntArrayConstraints } from "fast-check";
22+
import {
23+
AptosAuthorizeUpgradeContract,
24+
CosmosUpgradeContract,
25+
EvmUpgradeContract,
26+
} from "../governance_payload/UpgradeContract";
27+
import {
28+
AuthorizeGovernanceDataSourceTransfer,
29+
RequestGovernanceDataSourceTransfer,
30+
} from "../governance_payload/GovernanceDataSourceTransfer";
31+
import { SetFee } from "../governance_payload/SetFee";
32+
import { SetValidPeriod } from "../governance_payload/SetValidPeriod";
33+
import {
34+
DataSource,
35+
SetDataSources,
36+
} from "../governance_payload/SetDataSources";
337

438
test("GovernancePayload ser/de", (done) => {
539
jest.setTimeout(60000);
@@ -121,3 +155,142 @@ test("GovernancePayload ser/de", (done) => {
121155

122156
done();
123157
});
158+
159+
/** Fastcheck generator for arbitrary PythGovernanceHeaders */
160+
function governanceHeaderArb(): Arbitrary<PythGovernanceHeader> {
161+
const actions = [
162+
...Object.keys(ExecutorAction),
163+
...Object.keys(TargetAction),
164+
] as ActionName[];
165+
const actionArb = fc.constantFrom(...actions);
166+
const targetChainIdArb = fc.constantFrom(
167+
...(Object.keys(CHAINS) as ChainName[])
168+
);
169+
170+
return actionArb.chain((action) => {
171+
return targetChainIdArb.chain((chainId) => {
172+
return fc.constant(new PythGovernanceHeader(chainId, action));
173+
});
174+
});
175+
}
176+
177+
/** Fastcheck generator for arbitrary Buffers */
178+
function bufferArb(constraints?: IntArrayConstraints): Arbitrary<Buffer> {
179+
return fc.uint8Array(constraints).map((a) => Buffer.from(a));
180+
}
181+
182+
/** Fastcheck generator for a uint of numBits bits. Warning: don't pass numBits > float precision */
183+
function uintArb(numBits: number): Arbitrary<number> {
184+
return fc.bigUintN(numBits).map((x) => Number.parseInt(x.toString()));
185+
}
186+
187+
/** Fastcheck generator for a byte array encoded as a hex string. */
188+
function hexBytesArb(constraints?: IntArrayConstraints): Arbitrary<string> {
189+
return fc.uint8Array(constraints).map((a) => Buffer.from(a).toString("hex"));
190+
}
191+
192+
function dataSourceArb(): Arbitrary<DataSource> {
193+
return fc.record({
194+
emitterChain: uintArb(16),
195+
emitterAddress: hexBytesArb({ minLength: 32, maxLength: 32 }),
196+
});
197+
}
198+
199+
/**
200+
* Fastcheck generator for arbitrary PythGovernanceActions.
201+
*
202+
* Note that this generator doesn't generate ExecutePostedVaa instruction payloads because they're hard to generate.
203+
*/
204+
function governanceActionArb(): Arbitrary<PythGovernanceAction> {
205+
return governanceHeaderArb().chain<PythGovernanceAction>((header) => {
206+
if (header.action === "ExecutePostedVaa") {
207+
// NOTE: the instructions field is hard to generatively test, so we're using the hardcoded
208+
// tests above instead.
209+
return fc.constant(new ExecutePostedVaa(header.targetChainId, []));
210+
} else if (header.action === "UpgradeContract") {
211+
const cosmosArb = fc.bigUintN(64).map((codeId) => {
212+
return new CosmosUpgradeContract(header.targetChainId, codeId);
213+
});
214+
const aptosArb = hexBytesArb({ minLength: 32, maxLength: 32 }).map(
215+
(buffer) => {
216+
return new AptosAuthorizeUpgradeContract(
217+
header.targetChainId,
218+
buffer
219+
);
220+
}
221+
);
222+
const evmArb = hexBytesArb({ minLength: 20, maxLength: 20 }).map(
223+
(address) => {
224+
return new EvmUpgradeContract(header.targetChainId, address);
225+
}
226+
);
227+
228+
return fc.oneof(cosmosArb, aptosArb, evmArb);
229+
} else if (header.action === "AuthorizeGovernanceDataSourceTransfer") {
230+
return bufferArb().map((claimVaa) => {
231+
return new AuthorizeGovernanceDataSourceTransfer(
232+
header.targetChainId,
233+
claimVaa
234+
);
235+
});
236+
} else if (header.action === "SetDataSources") {
237+
return fc.array(dataSourceArb()).map((dataSources) => {
238+
return new SetDataSources(header.targetChainId, dataSources);
239+
});
240+
} else if (header.action === "SetFee") {
241+
return fc
242+
.record({ v: fc.bigUintN(64), e: fc.bigUintN(64) })
243+
.map(({ v, e }) => {
244+
return new SetFee(header.targetChainId, v, e);
245+
});
246+
} else if (header.action === "SetValidPeriod") {
247+
return fc.bigUintN(64).map((period) => {
248+
return new SetValidPeriod(header.targetChainId, period);
249+
});
250+
} else if (header.action === "RequestGovernanceDataSourceTransfer") {
251+
return fc.bigUintN(32).map((index) => {
252+
return new RequestGovernanceDataSourceTransfer(
253+
header.targetChainId,
254+
parseInt(index.toString())
255+
);
256+
});
257+
} else {
258+
throw new Error("Unsupported action type");
259+
}
260+
});
261+
}
262+
263+
test("Header serialization round-trip test", (done) => {
264+
fc.assert(
265+
fc.property(governanceHeaderArb(), (original) => {
266+
const decoded = PythGovernanceHeader.decode(original.encode());
267+
if (decoded === undefined) {
268+
return false;
269+
}
270+
271+
return (
272+
decoded.action === original.action &&
273+
decoded.targetChainId === original.targetChainId
274+
);
275+
})
276+
);
277+
278+
done();
279+
});
280+
281+
test("Governance action serialization round-trip test", (done) => {
282+
fc.assert(
283+
fc.property(governanceActionArb(), (original) => {
284+
const encoded = original.encode();
285+
const decoded = decodeGovernancePayload(encoded);
286+
if (decoded === undefined) {
287+
return false;
288+
}
289+
290+
// TODO: not sure if i love this test.
291+
return decoded.encode().equals(original.encode());
292+
})
293+
);
294+
295+
done();
296+
});
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
import { Layout } from "@solana/buffer-layout";
2+
3+
export class UInt64BE extends Layout<bigint> {
4+
constructor(span: number, property?: string) {
5+
super(span, property);
6+
}
7+
8+
override decode(b: Uint8Array, offset?: number): bigint {
9+
let o = offset ?? 0;
10+
return Buffer.from(b.slice(o, o + this.span)).readBigUInt64BE();
11+
}
12+
13+
override encode(src: bigint, b: Uint8Array, offset?: number): number {
14+
const buffer = Buffer.alloc(this.span);
15+
buffer.writeBigUint64BE(src);
16+
b.set(buffer, offset);
17+
return this.span;
18+
}
19+
}
20+
21+
export class HexBytes extends Layout<string> {
22+
// span is the number of bytes to read
23+
constructor(span: number, property?: string) {
24+
super(span, property);
25+
}
26+
27+
override decode(b: Uint8Array, offset?: number): string {
28+
let o = offset ?? 0;
29+
return Buffer.from(b.slice(o, o + this.span)).toString("hex");
30+
}
31+
32+
override encode(src: string, b: Uint8Array, offset?: number): number {
33+
const buffer = Buffer.alloc(this.span);
34+
buffer.write(src, "hex");
35+
b.set(buffer, offset);
36+
return this.span;
37+
}
38+
}
39+
40+
/** A big-endian u64, returned as a bigint. */
41+
export function u64be(property?: string | undefined): UInt64BE {
42+
return new UInt64BE(8, property);
43+
}
44+
45+
/** An array of numBytes bytes, returned as a hexadecimal string. */
46+
export function hexBytes(
47+
numBytes: number,
48+
property?: string | undefined
49+
): HexBytes {
50+
return new HexBytes(numBytes, property);
51+
}
Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
import {
2+
ActionName,
3+
PythGovernanceAction,
4+
PythGovernanceActionImpl,
5+
PythGovernanceHeader,
6+
} from "./PythGovernanceAction";
7+
import * as BufferLayout from "@solana/buffer-layout";
8+
import { ChainName } from "@certusone/wormhole-sdk";
9+
10+
/**
11+
* Authorize transferring the governance data source from the sender's emitter address to another emitter.
12+
* The receiving emitter address is the sender of claimVaa, which must be a RequestGovernanceDataSourceTransfer message.
13+
*/
14+
export class AuthorizeGovernanceDataSourceTransfer
15+
implements PythGovernanceAction
16+
{
17+
readonly actionName: ActionName;
18+
readonly claimVaa: Buffer;
19+
20+
constructor(readonly targetChainId: ChainName, vaa: Buffer) {
21+
this.actionName = "AuthorizeGovernanceDataSourceTransfer";
22+
this.claimVaa = new Buffer(vaa);
23+
}
24+
25+
static decode(
26+
data: Buffer
27+
): AuthorizeGovernanceDataSourceTransfer | undefined {
28+
const header = PythGovernanceHeader.decode(data);
29+
if (!header || header.action !== "AuthorizeGovernanceDataSourceTransfer") {
30+
return undefined;
31+
}
32+
33+
const payload = data.subarray(PythGovernanceHeader.span, data.length);
34+
35+
return new AuthorizeGovernanceDataSourceTransfer(
36+
header.targetChainId,
37+
payload
38+
);
39+
}
40+
41+
encode(): Buffer {
42+
const headerBuffer = new PythGovernanceHeader(
43+
this.targetChainId,
44+
this.actionName
45+
).encode();
46+
return Buffer.concat([headerBuffer, this.claimVaa]);
47+
}
48+
}
49+
50+
/**
51+
* Request a transfer of the governance data source to the emitter of this message.
52+
*/
53+
export class RequestGovernanceDataSourceTransfer extends PythGovernanceActionImpl {
54+
static layout: BufferLayout.Structure<
55+
Readonly<{ governanceDataSourceIndex: number }>
56+
> = BufferLayout.struct([BufferLayout.u32be()]);
57+
58+
constructor(
59+
targetChainId: ChainName,
60+
readonly governanceDataSourceIndex: number
61+
) {
62+
super(targetChainId, "RequestGovernanceDataSourceTransfer");
63+
}
64+
65+
static decode(data: Buffer): RequestGovernanceDataSourceTransfer | undefined {
66+
const decoded = PythGovernanceActionImpl.decodeWithPayload(
67+
data,
68+
"RequestGovernanceDataSourceTransfer",
69+
RequestGovernanceDataSourceTransfer.layout
70+
);
71+
if (!decoded) return undefined;
72+
73+
return new RequestGovernanceDataSourceTransfer(
74+
decoded[0].targetChainId,
75+
decoded[1].governanceDataSourceIndex
76+
);
77+
}
78+
79+
encode(): Buffer {
80+
return super.encodeWithPayload(RequestGovernanceDataSourceTransfer.layout, {
81+
governanceDataSourceIndex: this.governanceDataSourceIndex,
82+
});
83+
}
84+
}

0 commit comments

Comments
 (0)