From 867e8522760f021014feaa4e8793c4011f402376 Mon Sep 17 00:00:00 2001 From: Loris Leiva Date: Thu, 1 May 2025 12:06:58 +0100 Subject: [PATCH] Add transaction size helpers --- .changeset/legal-pillows-vanish.md | 6 ++ packages/errors/src/codes.ts | 2 + packages/errors/src/context.ts | 8 ++ packages/errors/src/messages.ts | 3 + .../transaction-message-size-test.ts | 73 ++++++++++++++++ .../src/__tests__/transaction-size-test.ts | 78 +++++++++++++++++ .../transaction-message-size-typetest.ts | 22 +++++ .../transaction-size-typetest.ts | 21 +++++ packages/transactions/src/index.ts | 2 + .../src/transaction-message-size.ts | 70 +++++++++++++++ packages/transactions/src/transaction-size.ts | 86 +++++++++++++++++++ 11 files changed, 371 insertions(+) create mode 100644 .changeset/legal-pillows-vanish.md create mode 100644 packages/transactions/src/__tests__/transaction-message-size-test.ts create mode 100644 packages/transactions/src/__tests__/transaction-size-test.ts create mode 100644 packages/transactions/src/__typetests__/transaction-message-size-typetest.ts create mode 100644 packages/transactions/src/__typetests__/transaction-size-typetest.ts create mode 100644 packages/transactions/src/transaction-message-size.ts create mode 100644 packages/transactions/src/transaction-size.ts diff --git a/.changeset/legal-pillows-vanish.md b/.changeset/legal-pillows-vanish.md new file mode 100644 index 000000000..5ababac6f --- /dev/null +++ b/.changeset/legal-pillows-vanish.md @@ -0,0 +1,6 @@ +--- +'@solana/transactions': patch +'@solana/errors': patch +--- + +Add a variety of types, constants and functions to help with transaction sizes and their limits diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 92d1019fe..e6a4dbb1f 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -208,6 +208,7 @@ export const SOLANA_ERROR__TRANSACTION__CANNOT_ENCODE_WITH_EMPTY_SIGNATURES = 56 export const SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH = 5663017; export const SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT = 5663018; export const SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT = 5663019; +export const SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT = 5663020; // Transaction errors. // Reserve error codes starting with [7050000-7050999] for the Rust enum `TransactionError`. @@ -490,6 +491,7 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__TRANSACTION__ADDRESS_MISSING | typeof SOLANA_ERROR__TRANSACTION__ADDRESSES_CANNOT_SIGN_TRANSACTION | typeof SOLANA_ERROR__TRANSACTION__CANNOT_ENCODE_WITH_EMPTY_SIGNATURES + | typeof SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT | typeof SOLANA_ERROR__TRANSACTION__EXPECTED_BLOCKHASH_LIFETIME | typeof SOLANA_ERROR__TRANSACTION__EXPECTED_NONCE_LIFETIME | typeof SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 59609ec04..574ca4394 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -147,6 +147,7 @@ import { SOLANA_ERROR__TIMESTAMP_OUT_OF_RANGE, SOLANA_ERROR__TRANSACTION__ADDRESS_MISSING, SOLANA_ERROR__TRANSACTION__ADDRESSES_CANNOT_SIGN_TRANSACTION, + SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING, SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_INDEX_OUT_OF_RANGE, SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_INSTRUCTION_PROGRAM_ADDRESS_NOT_FOUND, @@ -175,6 +176,9 @@ interface ReadonlyUint8Array extends Omit setTransactionMessageLifetimeUsingBlockhash(MOCK_BLOCKHASH, m), + m => setTransactionMessageFeePayer(address('22222222222222222222222222222222222222222222'), m), +); + +const OVERSIZED_TRANSACTION_MESSAGE = pipe(SMALL_TRANSACTION_MESSAGE, m => + appendTransactionMessageInstruction( + { + data: new Uint8Array(TRANSACTION_SIZE_LIMIT + 1), + programAddress: address('33333333333333333333333333333333333333333333'), + }, + m, + ), +); + +describe('getTransactionMessageSize', () => { + it('gets the size of a compilable transaction message', () => { + expect(getTransactionMessageSize(SMALL_TRANSACTION_MESSAGE)).toBe(136); + }); + + it('gets the size of an oversized compilable transaction message', () => { + expect(getTransactionMessageSize(OVERSIZED_TRANSACTION_MESSAGE)).toBe(1405); + }); +}); + +describe('isTransactionMessageWithinSizeLimit', () => { + it('returns true when the compiled size is under the transaction size limit', () => { + expect(isTransactionMessageWithinSizeLimit(SMALL_TRANSACTION_MESSAGE)).toBe(true); + }); + + it('returns false when the compiled size is above the transaction size limit', () => { + expect(isTransactionMessageWithinSizeLimit(OVERSIZED_TRANSACTION_MESSAGE)).toBe(false); + }); +}); + +describe('assertIsTransactionMessageWithinSizeLimit', () => { + it('does not throw when the compiled size is under the transaction size limit', () => { + expect(() => assertIsTransactionMessageWithinSizeLimit(SMALL_TRANSACTION_MESSAGE)).not.toThrow(); + }); + + it('throws when the compiled size is above the transaction size limit', () => { + expect(() => assertIsTransactionMessageWithinSizeLimit(OVERSIZED_TRANSACTION_MESSAGE)).toThrow( + new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, { + transactionSize: 1405, + transactionSizeLimit: TRANSACTION_SIZE_LIMIT, + }), + ); + }); +}); diff --git a/packages/transactions/src/__tests__/transaction-size-test.ts b/packages/transactions/src/__tests__/transaction-size-test.ts new file mode 100644 index 000000000..3c61a1ee3 --- /dev/null +++ b/packages/transactions/src/__tests__/transaction-size-test.ts @@ -0,0 +1,78 @@ +import { address } from '@solana/addresses'; +import { SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, SolanaError } from '@solana/errors'; +import { pipe } from '@solana/functional'; +import type { Blockhash } from '@solana/rpc-types'; +import { + appendTransactionMessageInstruction, + createTransactionMessage, + setTransactionMessageFeePayer, + setTransactionMessageLifetimeUsingBlockhash, +} from '@solana/transaction-messages'; + +import { compileTransaction } from '../compile-transaction'; +import { + assertIsTransactionWithinSizeLimit, + getTransactionSize, + isTransactionWithinSizeLimit, + TRANSACTION_SIZE_LIMIT, +} from '../transaction-size'; + +const MOCK_BLOCKHASH = { + blockhash: '11111111111111111111111111111111' as Blockhash, + lastValidBlockHeight: 0n, +}; + +const SMALL_TRANSACTION_MESSAGE = pipe( + createTransactionMessage({ version: 0 }), + m => setTransactionMessageLifetimeUsingBlockhash(MOCK_BLOCKHASH, m), + m => setTransactionMessageFeePayer(address('22222222222222222222222222222222222222222222'), m), +); + +const SMALL_TRANSACTION = compileTransaction(SMALL_TRANSACTION_MESSAGE); + +const OVERSIZED_TRANSACTION = compileTransaction( + pipe(SMALL_TRANSACTION_MESSAGE, m => + appendTransactionMessageInstruction( + { + data: new Uint8Array(TRANSACTION_SIZE_LIMIT + 1), + programAddress: address('33333333333333333333333333333333333333333333'), + }, + m, + ), + ), +); + +describe('getTransactionSize', () => { + it('gets the size of a transaction', () => { + expect(getTransactionSize(SMALL_TRANSACTION)).toBe(136); + }); + + it('gets the size of an oversized transaction', () => { + expect(getTransactionSize(OVERSIZED_TRANSACTION)).toBe(1405); + }); +}); + +describe('isTransactionWithinSizeLimit', () => { + it('returns true when the transaction size is under the transaction size limit', () => { + expect(isTransactionWithinSizeLimit(SMALL_TRANSACTION)).toBe(true); + }); + + it('returns false when the transaction size is above the transaction size limit', () => { + expect(isTransactionWithinSizeLimit(OVERSIZED_TRANSACTION)).toBe(false); + }); +}); + +describe('assertIsTransactionWithinSizeLimit', () => { + it('does not throw when the transaction size is under the transaction size limit', () => { + expect(() => assertIsTransactionWithinSizeLimit(SMALL_TRANSACTION)).not.toThrow(); + }); + + it('throws when the transaction size is above the transaction size limit', () => { + expect(() => assertIsTransactionWithinSizeLimit(OVERSIZED_TRANSACTION)).toThrow( + new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, { + transactionSize: 1405, + transactionSizeLimit: TRANSACTION_SIZE_LIMIT, + }), + ); + }); +}); diff --git a/packages/transactions/src/__typetests__/transaction-message-size-typetest.ts b/packages/transactions/src/__typetests__/transaction-message-size-typetest.ts new file mode 100644 index 000000000..722e479d4 --- /dev/null +++ b/packages/transactions/src/__typetests__/transaction-message-size-typetest.ts @@ -0,0 +1,22 @@ +import { CompilableTransactionMessage } from '@solana/transaction-messages'; + +import { + assertIsTransactionMessageWithinSizeLimit, + isTransactionMessageWithinSizeLimit, + TransactionMessageWithinSizeLimit, +} from '../transaction-message-size'; + +// isTransactionMessageWithinSizeLimit +{ + const transactionMessage = null as unknown as CompilableTransactionMessage & { some: 1 }; + if (isTransactionMessageWithinSizeLimit(transactionMessage)) { + transactionMessage satisfies CompilableTransactionMessage & TransactionMessageWithinSizeLimit & { some: 1 }; + } +} + +// assertIsTransactionMessageWithinSizeLimit +{ + const transactionMessage = null as unknown as CompilableTransactionMessage & { some: 1 }; + assertIsTransactionMessageWithinSizeLimit(transactionMessage); + transactionMessage satisfies CompilableTransactionMessage & TransactionMessageWithinSizeLimit & { some: 1 }; +} diff --git a/packages/transactions/src/__typetests__/transaction-size-typetest.ts b/packages/transactions/src/__typetests__/transaction-size-typetest.ts new file mode 100644 index 000000000..0fcb22377 --- /dev/null +++ b/packages/transactions/src/__typetests__/transaction-size-typetest.ts @@ -0,0 +1,21 @@ +import { Transaction } from '../transaction'; +import { + assertIsTransactionWithinSizeLimit, + isTransactionWithinSizeLimit, + TransactionWithinSizeLimit, +} from '../transaction-size'; + +// isTransactionWithinSizeLimit +{ + const transaction = null as unknown as Transaction & { some: 1 }; + if (isTransactionWithinSizeLimit(transaction)) { + transaction satisfies Transaction & TransactionWithinSizeLimit & { some: 1 }; + } +} + +// assertIsTransactionWithinSizeLimit +{ + const transaction = null as unknown as Transaction & { some: 1 }; + assertIsTransactionWithinSizeLimit(transaction); + transaction satisfies Transaction & TransactionWithinSizeLimit & { some: 1 }; +} diff --git a/packages/transactions/src/index.ts b/packages/transactions/src/index.ts index 5c10e1792..970358bc7 100644 --- a/packages/transactions/src/index.ts +++ b/packages/transactions/src/index.ts @@ -13,4 +13,6 @@ export * from './lifetime'; export * from './compile-transaction'; export * from './signatures'; export * from './wire-transaction'; +export * from './transaction-message-size'; +export * from './transaction-size'; export * from './transaction'; diff --git a/packages/transactions/src/transaction-message-size.ts b/packages/transactions/src/transaction-message-size.ts new file mode 100644 index 000000000..297151cdb --- /dev/null +++ b/packages/transactions/src/transaction-message-size.ts @@ -0,0 +1,70 @@ +import { SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, SolanaError } from '@solana/errors'; +import type { NominalType } from '@solana/nominal-types'; +import type { CompilableTransactionMessage } from '@solana/transaction-messages'; + +import { compileTransaction } from './compile-transaction'; +import { getTransactionSize, TRANSACTION_SIZE_LIMIT } from './transaction-size'; + +/** + * Gets the compiled transaction size of a given transaction message in bytes. + * + * @example + * ```ts + * const transactionSize = getTransactionMessageSize(transactionMessage); + * ``` + */ +export function getTransactionMessageSize(transactionMessage: CompilableTransactionMessage): number { + return getTransactionSize(compileTransaction(transactionMessage)); +} + +/** + * A type guard that checks if a transaction message is within the size limit + * when compiled into a transaction. + */ +export type TransactionMessageWithinSizeLimit = NominalType<'transactionSize', 'withinLimit'>; + +/** + * Checks if a transaction message is within the size limit + * when compiled into a transaction. + * + * @typeParam TTransactionMessage - The type of the given transaction message. + * + * @example + * ```ts + * if (isTransactionMessageWithinSizeLimit(transactionMessage)) { + * transactionMessage satisfies TransactionMessageWithinSizeLimit; + * } + * ``` + */ +export function isTransactionMessageWithinSizeLimit( + transactionMessage: TTransactionMessage, +): transactionMessage is TransactionMessageWithinSizeLimit & TTransactionMessage { + return getTransactionMessageSize(transactionMessage) <= TRANSACTION_SIZE_LIMIT; +} + +/** + * Asserts that a given transaction message is within the size limit + * when compiled into a transaction. + * + * Throws a {@link SolanaError} of code {@link SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT} + * if the transaction message exceeds the size limit. + * + * @typeParam TTransactionMessage - The type of the given transaction message. + * + * @example + * ```ts + * assertIsTransactionMessageWithinSizeLimit(transactionMessage); + * transactionMessage satisfies TransactionMessageWithinSizeLimit; + * ``` + */ +export function assertIsTransactionMessageWithinSizeLimit( + transactionMessage: TTransactionMessage, +): asserts transactionMessage is TransactionMessageWithinSizeLimit & TTransactionMessage { + const transactionSize = getTransactionMessageSize(transactionMessage); + if (transactionSize > TRANSACTION_SIZE_LIMIT) { + throw new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, { + transactionSize, + transactionSizeLimit: TRANSACTION_SIZE_LIMIT, + }); + } +} diff --git a/packages/transactions/src/transaction-size.ts b/packages/transactions/src/transaction-size.ts new file mode 100644 index 000000000..85159ea83 --- /dev/null +++ b/packages/transactions/src/transaction-size.ts @@ -0,0 +1,86 @@ +import { SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, SolanaError } from '@solana/errors'; +import type { NominalType } from '@solana/nominal-types'; + +import { getTransactionEncoder } from './codecs'; +import { Transaction } from './transaction'; + +/** + * The maximum size of a transaction packet in bytes. + */ +export const TRANSACTION_PACKET_SIZE = 1280; + +/** + * The size of the transaction packet header in bytes. + * This includes the IPv6 header and the fragment header. + */ +export const TRANSACTION_PACKET_HEADER = + 40 /* 40 bytes is the size of the IPv6 header. */ + 8; /* 8 bytes is the size of the fragment header. */ + +/** + * The maximum size of a transaction in bytes. + * + * Note that this excludes the transaction packet header. + * In other words, this is how much content we can fit in a transaction packet. + */ +export const TRANSACTION_SIZE_LIMIT = TRANSACTION_PACKET_SIZE - TRANSACTION_PACKET_HEADER; + +/** + * Gets the size of a given transaction in bytes. + * + * @example + * ```ts + * const transactionSize = getTransactionSize(transaction); + * ``` + */ +export function getTransactionSize(transaction: Transaction): number { + return getTransactionEncoder().getSizeFromValue(transaction); +} + +/** + * A type guard that checks if a transaction is within the size limit. + */ +export type TransactionWithinSizeLimit = NominalType<'transactionSize', 'withinLimit'>; + +/** + * Checks if a transaction is within the size limit. + * + * @typeParam TTransaction - The type of the given transaction. + * + * @example + * ```ts + * if (isTransactionWithinSizeLimit(transaction)) { + * transaction satisfies TransactionWithinSizeLimit; + * } + * ``` + */ +export function isTransactionWithinSizeLimit( + transaction: TTransaction, +): transaction is TransactionWithinSizeLimit & TTransaction { + return getTransactionSize(transaction) <= TRANSACTION_SIZE_LIMIT; +} + +/** + * Asserts that a given transaction is within the size limit. + * + * Throws a {@link SolanaError} of code {@link SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT} + * if the transaction exceeds the size limit. + * + * @typeParam TTransaction - The type of the given transaction. + * + * @example + * ```ts + * assertIsTransactionWithinSizeLimit(transaction); + * transaction satisfies TransactionWithinSizeLimit; + * ``` + */ +export function assertIsTransactionWithinSizeLimit( + transaction: TTransaction, +): asserts transaction is TransactionWithinSizeLimit & TTransaction { + const transactionSize = getTransactionSize(transaction); + if (transactionSize > TRANSACTION_SIZE_LIMIT) { + throw new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, { + transactionSize, + transactionSizeLimit: TRANSACTION_SIZE_LIMIT, + }); + } +}