4

Currently to get the size of a transaction I have to do the following

const tx = new Transaction().add(...)
const { blockhash } = await connection.getLatestBlockhash();
tx.recentBlockhash = blockhash;

tx.feePayer = wallet.publicKey;
const signed = await wallet.signTransaction(tx);
signed.partialSign(...); // add if needed
console.log('tx len', signed.serialize().length);

My question: is there a way to estimate the tx size before signing and getting the blockhash?

By looking at the cookbook, I think a starting point is: 32 * # accounts + 64 * # signatures, however, I'm still missing some bytes which I think are due to metadata? How can I get the size of these?

2 Answers 2

5

The blockhash is always the same length (32 bytes) and the signature(s) are always the same length (64 bytes each).

You should be able set the feePayer to new PublicKey(0) and set the recentBlockhash to new PublicKey(0).toBase58() (or "11111111111111111111111111111111"), then serialize it without signing:

const serialized = transaction.serialize({
  verifySignatures: false,
  requireAllSignatures: false,
})

Adding a byte for the number of signatures, I think you should be able to get the total size with:

const size = serialized.length + 1 + (transaction.signatures.length * 64);

The extra 1 byte is to store the number of signatures. https://docs.solana.com/developing/programming-model/transactions#transaction-format has the details.

You should test this against a valid, complete transaction to see if it's off. If it is, it should be by some predictable factor.

5

Many thanks to @Jordan! After digging a bit more I think I got to a decent solution.

Here is the breakdown. For each tx we have:

  • a compact array of signatures, 64 bytes each
  • a header array with: # signatures, # read-only accounts that need signatures, # read-only accounts that don't need signatures, 3 bytes total
  • a compact array of accounts, 32 bytes each
  • a blockhash, 32 bytes
  • a compact array of instructions.

Each instruction has this format:

  • index of program id, 1 byte
  • compact array of account indexes, 1 byte for each index
  • compact array of opaque bytes, 1 byte each

Moreover, whenever there is a compact array, we need to add 1-3 bytes depending on how many elements are there in the array, more info here.

This is the code, I tested it against several txs and seems to work well (legacy transactions only):

import { PublicKey, Transaction } from '@solana/web3.js';

/**
 * @param tx a solana transaction
 * @param feePayer the publicKey of the signer
 * @returns size in bytes of the transaction
 */
const getTxSize = (tx: Transaction, feePayer: PublicKey): number => {
    const feePayerPk = [feePayer.toBase58()];

    const signers = new Set<string>(feePayerPk);
    const accounts = new Set<string>(feePayerPk);

    const ixsSize = tx.instructions.reduce((acc, ix) => {
        ix.keys.forEach(({ pubkey, isSigner }) => {
            const pk = pubkey.toBase58();
            if (isSigner) signers.add(pk);
            accounts.add(pk);
        });

        accounts.add(ix.programId.toBase58());

        const nIndexes = ix.keys.length;
        const opaqueData = ix.data.length;

        return (
            acc +
            1 + // PID index
            compactArraySize(nIndexes, 1) +
            compactArraySize(opaqueData, 1)
        );
    }, 0);

    return (
        compactArraySize(signers.size, 64) + // signatures
        3 + // header
        compactArraySize(accounts.size, 32) + // accounts
        32 + // blockhash
        compactHeader(tx.instructions.length) + // instructions
        ixsSize
    );
};

// COMPACT ARRAY

const LOW_VALUE = 127; // 0x7f
const HIGH_VALUE = 16383; // 0x3fff

/**
 * Compact u16 array header size
 * @param n elements in the compact array
 * @returns size in bytes of array header
 */
const compactHeader = (n: number) => (n <= LOW_VALUE ? 1 : n <= HIGH_VALUE ? 2 : 3);

/**
 * Compact u16 array size
 * @param n elements in the compact array
 * @param size bytes per each element
 * @returns size in bytes of array
 */
const compactArraySize = (n: number, size: number) => compactHeader(n) + n * size;

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.