From 58db641dddfcc23beb54d25c988270ae69e2dd49 Mon Sep 17 00:00:00 2001 From: guibescos <59208140+guibescos@users.noreply.github.com> Date: Mon, 20 Feb 2023 15:58:31 +0100 Subject: [PATCH] [xc-admin] Batch instructions (#612) * Checkpoint * Working * Remove console log * Restore send all * Fix tests --- .../src/__tests__/TransactionSize.test.ts | 138 ++++++++++++++++++ .../packages/xc_admin_common/src/propose.ts | 129 ++++++++++++---- package-lock.json | 4 +- 3 files changed, 238 insertions(+), 33 deletions(-) create mode 100644 governance/xc_admin/packages/xc_admin_common/src/__tests__/TransactionSize.test.ts diff --git a/governance/xc_admin/packages/xc_admin_common/src/__tests__/TransactionSize.test.ts b/governance/xc_admin/packages/xc_admin_common/src/__tests__/TransactionSize.test.ts new file mode 100644 index 00000000..ce2ac21e --- /dev/null +++ b/governance/xc_admin/packages/xc_admin_common/src/__tests__/TransactionSize.test.ts @@ -0,0 +1,138 @@ +import { AnchorProvider, Wallet } from "@project-serum/anchor"; +import { pythOracleProgram } from "@pythnetwork/client"; +import { + getPythClusterApiUrl, + getPythProgramKeyForCluster, + PythCluster, +} from "@pythnetwork/client/lib/cluster"; +import { + Connection, + Keypair, + PACKET_DATA_SIZE, + PublicKey, + Transaction, + TransactionInstruction, +} from "@solana/web3.js"; +import { + batchIntoTransactions, + getSizeOfCompressedU16, + getSizeOfTransaction, + MultisigInstructionProgram, + MultisigParser, +} from ".."; +import { PythMultisigInstruction } from "../multisig_transaction/PythMultisigInstruction"; + +it("Unit test compressed u16 size", async () => { + expect(getSizeOfCompressedU16(127)).toBe(1); + expect(getSizeOfCompressedU16(128)).toBe(2); + expect(getSizeOfCompressedU16(16383)).toBe(2); + expect(getSizeOfCompressedU16(16384)).toBe(3); +}); + +it("Unit test for getSizeOfTransaction", async () => { + jest.setTimeout(60000); + + const cluster: PythCluster = "devnet"; + const pythProgram = pythOracleProgram( + getPythProgramKeyForCluster(cluster), + new AnchorProvider( + new Connection(getPythClusterApiUrl(cluster)), + new Wallet(new Keypair()), + AnchorProvider.defaultOptions() + ) + ); + + const payer = new Keypair(); + const productAccount = PublicKey.unique(); + + const ixsToSend: TransactionInstruction[] = []; + + ixsToSend.push( + await pythProgram.methods + .addProduct({ + asset_type: "Crypto", + base: "ETH", + description: "ETH/USD", + quote_currency: "USD", + symbol: "Crypto.ETH/USD", + generic_symbol: "ETHUSD", + }) + .accounts({ + fundingAccount: payer.publicKey, + productAccount, + tailMappingAccount: PublicKey.unique(), + }) + .instruction() + ); + + ixsToSend.push( + await pythProgram.methods + .addPrice(-8, 1) + .accounts({ + fundingAccount: payer.publicKey, + productAccount, + priceAccount: PublicKey.unique(), + }) + .instruction() + ); + + const transaction = new Transaction(); + for (let ix of ixsToSend) { + transaction.add(ix); + } + + transaction.recentBlockhash = "GqdFtdM7zzWw33YyHtBNwPhyBsdYKcfm9gT47bWnbHvs"; // Mock blockhash from devnet + transaction.feePayer = payer.publicKey; + expect(transaction.serialize({ requireAllSignatures: false }).length).toBe( + getSizeOfTransaction(ixsToSend) + ); +}); + +it("Unit test for getSizeOfTransaction", async () => { + jest.setTimeout(60000); + + const cluster: PythCluster = "devnet"; + const pythProgram = pythOracleProgram( + getPythProgramKeyForCluster(cluster), + new AnchorProvider( + new Connection(getPythClusterApiUrl(cluster)), + new Wallet(new Keypair()), + AnchorProvider.defaultOptions() + ) + ); + const ixsToSend: TransactionInstruction[] = []; + const payer = new Keypair(); + + for (let i = 0; i < 100; i++) { + ixsToSend.push( + await pythProgram.methods + .addPublisher(PublicKey.unique()) + .accounts({ + fundingAccount: payer.publicKey, + priceAccount: PublicKey.unique(), + }) + .instruction() + ); + } + + const txToSend: Transaction[] = batchIntoTransactions( + ixsToSend, + payer.publicKey + ); + expect( + txToSend.map((tx) => tx.instructions.length).reduce((a, b) => a + b) + ).toBe(ixsToSend.length); + expect( + txToSend.every( + (tx) => getSizeOfTransaction(tx.instructions) <= PACKET_DATA_SIZE + ) + ).toBeTruthy(); + + for (let tx of txToSend) { + tx.recentBlockhash = "GqdFtdM7zzWw33YyHtBNwPhyBsdYKcfm9gT47bWnbHvs"; // Mock blockhash from devnet + tx.feePayer = payer.publicKey; + expect(tx.serialize({ requireAllSignatures: false }).length).toBe( + getSizeOfTransaction(tx.instructions) + ); + } +}); diff --git a/governance/xc_admin/packages/xc_admin_common/src/propose.ts b/governance/xc_admin/packages/xc_admin_common/src/propose.ts index 7667a249..492e756c 100644 --- a/governance/xc_admin/packages/xc_admin_common/src/propose.ts +++ b/governance/xc_admin/packages/xc_admin_common/src/propose.ts @@ -6,6 +6,7 @@ import { SYSVAR_RENT_PUBKEY, SYSVAR_CLOCK_PUBKEY, SystemProgram, + PACKET_DATA_SIZE, } from "@solana/web3.js"; import { BN } from "bn.js"; import { AnchorProvider } from "@project-serum/anchor"; @@ -41,8 +42,8 @@ export async function proposeInstructions( wormholeAddress?: PublicKey ): Promise { const msAccount = await squad.getMultisig(vault); - let txToSend: Transaction[] = []; - const createProposal = new Transaction().add( + let ixToSend: TransactionInstruction[] = []; + const createProposal = ixToSend.push( await squad.buildCreateTransaction( msAccount.publicKey, msAccount.authorityIndex, @@ -54,7 +55,6 @@ export async function proposeInstructions( new BN(msAccount.transactionIndex + 1), squad.multisigProgramId )[0]; - txToSend.push(createProposal); if (remote) { if (!wormholeAddress) { @@ -69,47 +69,38 @@ export async function proposeInstructions( i + 1, wormholeAddress ); - txToSend.push( - new Transaction().add( - await squad.buildAddInstruction( - vault, - newProposalAddress, - squadIx.instruction, - i + 1, - squadIx.authorityIndex, - squadIx.authorityBump, - squadIx.authorityType - ) + ixToSend.push( + await squad.buildAddInstruction( + vault, + newProposalAddress, + squadIx.instruction, + i + 1, + squadIx.authorityIndex, + squadIx.authorityBump, + squadIx.authorityType ) ); } } else { for (let i = 0; i < instructions.length; i++) { - txToSend.push( - new Transaction().add( - await squad.buildAddInstruction( - vault, - newProposalAddress, - instructions[i], - i + 1 - ) + ixToSend.push( + await squad.buildAddInstruction( + vault, + newProposalAddress, + instructions[i], + i + 1 ) ); } } - txToSend.push( - new Transaction().add( - await squad.buildActivateTransaction(vault, newProposalAddress) - ) + ixToSend.push( + await squad.buildActivateTransaction(vault, newProposalAddress) ); - txToSend.push( - new Transaction().add( - await squad.buildApproveTransaction(vault, newProposalAddress) - ) - ); + ixToSend.push(await squad.buildApproveTransaction(vault, newProposalAddress)); + const txToSend = batchIntoTransactions(ixToSend, squad.wallet.publicKey); await new AnchorProvider( squad.connection, squad.wallet, @@ -122,6 +113,82 @@ export async function proposeInstructions( return newProposalAddress; } +/** + * Batch instructions into transactions + */ +export function batchIntoTransactions( + instructions: TransactionInstruction[], + feePayer: PublicKey +): Transaction[] { + let i = 0; + const txToSend: Transaction[] = []; + while (i < instructions.length) { + let j = i + 2; + while ( + j < instructions.length && + getSizeOfTransaction(instructions.slice(i, j)) <= PACKET_DATA_SIZE + ) { + j += 1; + } + const tx = new Transaction(); + tx.feePayer = feePayer; + for (let k = i; k < j - 1; k += 1) { + tx.add(instructions[k]); + } + i = j - 1; + txToSend.push(tx); + } + return txToSend; +} + +/** + * Get the size of a transaction that would contain the provided array of instructions + */ +export function getSizeOfTransaction( + instructions: TransactionInstruction[] +): number { + const signers = new Set(); + const accounts = new Set(); + + instructions.map((ix) => { + accounts.add(ix.programId.toBase58()), + ix.keys.map((key) => { + if (key.isSigner) { + signers.add(key.pubkey.toBase58()); + } + accounts.add(key.pubkey.toBase58()); + }); + }); + + const instruction_sizes: number = instructions + .map( + (ix) => + 1 + + getSizeOfCompressedU16(ix.keys.length) + + ix.keys.length + + getSizeOfCompressedU16(ix.data.length) + + ix.data.length + ) + .reduce((a, b) => a + b, 0); + return ( + 1 + + signers.size * 64 + + 3 + + getSizeOfCompressedU16(accounts.size) + + 32 * accounts.size + + 32 + + getSizeOfCompressedU16(instructions.length) + + instruction_sizes + ); +} + +/** + * Get the size of n in bytes when serialized as a CompressedU16 + */ +export function getSizeOfCompressedU16(n: number) { + return 1 + Number(n >= 128) + Number(n >= 16384); +} + /** * Wrap `instruction` in a Wormhole message for remote execution * @param squad Squads client diff --git a/package-lock.json b/package-lock.json index c12cc94f..b8524d7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -85694,8 +85694,8 @@ "@coral-xyz/anchor": "^0.26.0", "@headlessui/react": "^1.7.7", "@pythnetwork/client": "^2.15.0", - "@solana/spl-token": "*", - "@radix-ui/react-tooltip": "*", + "@radix-ui/react-tooltip": "^1.0.3", + "@solana/spl-token": "^0.3.7", "@solana/wallet-adapter-base": "^0.9.20", "@solana/wallet-adapter-react": "^0.15.28", "@solana/wallet-adapter-react-ui": "^0.9.27",