Skip to content

Commit ecb0e17

Browse files
authored
[xc-admin] batch wormhole messages (#615)
* Add * Cleanup * Add unit test
1 parent 6f49dfb commit ecb0e17

File tree

2 files changed

+66
-19
lines changed

2 files changed

+66
-19
lines changed

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

Lines changed: 16 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,13 @@ import {
1414
TransactionInstruction,
1515
} from "@solana/web3.js";
1616
import {
17+
batchIntoExecutorPayload,
1718
batchIntoTransactions,
1819
getSizeOfCompressedU16,
20+
getSizeOfExecutorInstructions,
1921
getSizeOfTransaction,
20-
MultisigInstructionProgram,
21-
MultisigParser,
22+
MAX_EXECUTOR_PAYLOAD_SIZE,
2223
} from "..";
23-
import { PythMultisigInstruction } from "../multisig_transaction/PythMultisigInstruction";
2424

2525
it("Unit test compressed u16 size", async () => {
2626
expect(getSizeOfCompressedU16(127)).toBe(1);
@@ -115,10 +115,7 @@ it("Unit test for getSizeOfTransaction", async () => {
115115
);
116116
}
117117

118-
const txToSend: Transaction[] = batchIntoTransactions(
119-
ixsToSend,
120-
payer.publicKey
121-
);
118+
const txToSend: Transaction[] = batchIntoTransactions(ixsToSend);
122119
expect(
123120
txToSend.map((tx) => tx.instructions.length).reduce((a, b) => a + b)
124121
).toBe(ixsToSend.length);
@@ -135,4 +132,16 @@ it("Unit test for getSizeOfTransaction", async () => {
135132
getSizeOfTransaction(tx.instructions)
136133
);
137134
}
135+
136+
const batches: TransactionInstruction[][] =
137+
batchIntoExecutorPayload(ixsToSend);
138+
expect(batches.map((batch) => batch.length).reduce((a, b) => a + b)).toBe(
139+
ixsToSend.length
140+
);
141+
expect(
142+
batches.every(
143+
(batch) =>
144+
getSizeOfExecutorInstructions(batch) <= MAX_EXECUTOR_PAYLOAD_SIZE
145+
)
146+
).toBeTruthy();
138147
});

governance/xc_admin/packages/xc_admin_common/src/propose.ts

Lines changed: 50 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,8 @@ import {
1919
import { ExecutePostedVaa } from "./governance_payload/ExecutePostedVaa";
2020
import { OPS_KEY } from "./multisig";
2121

22+
export const MAX_EXECUTOR_PAYLOAD_SIZE = PACKET_DATA_SIZE - 687; // Bigger payloads won't fit in one addInstruction call when adding to the proposal
23+
2224
type SquadInstruction = {
2325
instruction: TransactionInstruction;
2426
authorityIndex?: number;
@@ -43,7 +45,7 @@ export async function proposeInstructions(
4345
): Promise<PublicKey> {
4446
const msAccount = await squad.getMultisig(vault);
4547
let ixToSend: TransactionInstruction[] = [];
46-
const createProposal = ixToSend.push(
48+
ixToSend.push(
4749
await squad.buildCreateTransaction(
4850
msAccount.publicKey,
4951
msAccount.authorityIndex,
@@ -60,12 +62,14 @@ export async function proposeInstructions(
6062
if (!wormholeAddress) {
6163
throw new Error("Need wormhole address");
6264
}
63-
for (let i = 0; i < instructions.length; i++) {
65+
66+
const batches = batchIntoExecutorPayload(instructions);
67+
for (const [i, batch] of batches.entries()) {
6468
const squadIx = await wrapAsRemoteInstruction(
6569
squad,
6670
vault,
6771
newProposalAddress,
68-
instructions[i],
72+
batch,
6973
i + 1,
7074
wormholeAddress
7175
);
@@ -100,7 +104,7 @@ export async function proposeInstructions(
100104

101105
ixToSend.push(await squad.buildApproveTransaction(vault, newProposalAddress));
102106

103-
const txToSend = batchIntoTransactions(ixToSend, squad.wallet.publicKey);
107+
const txToSend = batchIntoTransactions(ixToSend);
104108
await new AnchorProvider(
105109
squad.connection,
106110
squad.wallet,
@@ -113,12 +117,38 @@ export async function proposeInstructions(
113117
return newProposalAddress;
114118
}
115119

120+
/**
121+
* Batch instructions into batches for inclusion in a remote executor payload
122+
*/
123+
export function batchIntoExecutorPayload(
124+
instructions: TransactionInstruction[]
125+
): TransactionInstruction[][] {
126+
let i = 0;
127+
const batches: TransactionInstruction[][] = [];
128+
while (i < instructions.length) {
129+
let j = i + 2;
130+
while (
131+
j < instructions.length &&
132+
getSizeOfExecutorInstructions(instructions.slice(i, j)) <=
133+
MAX_EXECUTOR_PAYLOAD_SIZE
134+
) {
135+
j += 1;
136+
}
137+
const batch: TransactionInstruction[] = [];
138+
for (let k = i; k < j - 1; k += 1) {
139+
batch.push(instructions[k]);
140+
}
141+
i = j - 1;
142+
batches.push(batch);
143+
}
144+
return batches;
145+
}
146+
116147
/**
117148
* Batch instructions into transactions
118149
*/
119150
export function batchIntoTransactions(
120-
instructions: TransactionInstruction[],
121-
feePayer: PublicKey
151+
instructions: TransactionInstruction[]
122152
): Transaction[] {
123153
let i = 0;
124154
const txToSend: Transaction[] = [];
@@ -131,7 +161,6 @@ export function batchIntoTransactions(
131161
j += 1;
132162
}
133163
const tx = new Transaction();
134-
tx.feePayer = feePayer;
135164
for (let k = i; k < j - 1; k += 1) {
136165
tx.add(instructions[k]);
137166
}
@@ -141,6 +170,16 @@ export function batchIntoTransactions(
141170
return txToSend;
142171
}
143172

173+
/** Get the size of instructions when serialized as in a remote executor payload */
174+
export function getSizeOfExecutorInstructions(
175+
instructions: TransactionInstruction[]
176+
) {
177+
return instructions
178+
.map((ix) => {
179+
return 32 + 4 + ix.keys.length * 34 + 4 + ix.data.length;
180+
})
181+
.reduce((a, b) => a + b);
182+
}
144183
/**
145184
* Get the size of a transaction that would contain the provided array of instructions
146185
*/
@@ -170,6 +209,7 @@ export function getSizeOfTransaction(
170209
ix.data.length
171210
)
172211
.reduce((a, b) => a + b, 0);
212+
173213
return (
174214
1 +
175215
signers.size * 64 +
@@ -194,7 +234,7 @@ export function getSizeOfCompressedU16(n: number) {
194234
* @param squad Squads client
195235
* @param vault vault public key (the id of the multisig where these instructions should be proposed)
196236
* @param proposalAddress address of the proposal
197-
* @param instruction instruction to be wrapped in a Wormhole message
237+
* @param instructions instructions to be wrapped in a Wormhole message
198238
* @param instructionIndex index of the instruction within the proposal
199239
* @param wormholeAddress address of the Wormhole bridge
200240
* @returns an instruction to be proposed
@@ -203,7 +243,7 @@ export async function wrapAsRemoteInstruction(
203243
squad: Squads,
204244
vault: PublicKey,
205245
proposalAddress: PublicKey,
206-
instruction: TransactionInstruction,
246+
instructions: TransactionInstruction[],
207247
instructionIndex: number,
208248
wormholeAddress: PublicKey
209249
): Promise<SquadInstruction> {
@@ -225,9 +265,7 @@ export async function wrapAsRemoteInstruction(
225265
provider
226266
);
227267

228-
const buffer: Buffer = new ExecutePostedVaa("pythnet", [
229-
instruction,
230-
]).encode();
268+
const buffer: Buffer = new ExecutePostedVaa("pythnet", instructions).encode();
231269

232270
const accounts = getPostMessageAccounts(wormholeAddress, emitter, messagePDA);
233271

0 commit comments

Comments
 (0)