Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions .changeset/cyan-pumas-talk.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
'@solana/instruction-plans': minor
'@solana/transactions': minor
---

Add version-aware transaction size limits. Version 1 transactions now allow up to 4096 bytes, while legacy and v0 transactions continue to use the existing 1232-byte limit. Two new helper functions are exported from `@solana/transactions`: `getTransactionSizeLimit` for compiled `Transaction` objects, and `getTransactionMessageSizeLimit` for `TransactionMessage` objects.

The existing `TRANSACTION_SIZE_LIMIT`, `TRANSACTION_PACKET_SIZE`, and `TRANSACTION_PACKET_HEADER` constants are now deprecated in favour of `getTransactionSizeLimit` and will be removed in a future major version.
10 changes: 5 additions & 5 deletions packages/instruction-plans/src/instruction-plan.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import {
TransactionMessage,
TransactionMessageWithFeePayer,
} from '@solana/transaction-messages';
import { getTransactionMessageSize, TRANSACTION_SIZE_LIMIT } from '@solana/transactions';
import { getTransactionMessageSize, getTransactionMessageSizeLimit } from '@solana/transactions';

/**
* A set of instructions with constraints on how they can be executed.
Expand Down Expand Up @@ -934,7 +934,7 @@ export function getLinearMessagePackerInstructionPlan({
appendTransactionMessageInstruction(getInstruction(offset, 0), message),
);
const freeSpace =
TRANSACTION_SIZE_LIMIT -
getTransactionMessageSizeLimit(message) -
messageSizeWithBaseInstruction /* Includes the base instruction (length: 0). */ -
1; /* Leeway for shortU16 numbers in transaction headers. */

Expand All @@ -945,7 +945,7 @@ export function getLinearMessagePackerInstructionPlan({
// there is no point packing the base instruction alone.
numBytesRequired: messageSizeWithBaseInstruction - messageSize + 1,
// (-1) Leeway for shortU16 numbers in transaction headers.
numFreeBytes: TRANSACTION_SIZE_LIMIT - messageSize - 1,
numFreeBytes: getTransactionMessageSizeLimit(message) - messageSize - 1,
});
}

Expand Down Expand Up @@ -1006,13 +1006,13 @@ export function getMessagePackerInstructionPlanFromInstructions<TInstruction ext
message = appendTransactionMessageInstruction(instructions[index], message);
const messageSize = getTransactionMessageSize(message);

if (messageSize > TRANSACTION_SIZE_LIMIT) {
if (messageSize > getTransactionMessageSizeLimit(message)) {
if (index === instructionIndex) {
throw new SolanaError(
SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN,
{
numBytesRequired: messageSize - originalMessageSize,
numFreeBytes: TRANSACTION_SIZE_LIMIT - originalMessageSize,
numFreeBytes: getTransactionMessageSizeLimit(message) - originalMessageSize,
},
);
}
Expand Down
12 changes: 6 additions & 6 deletions packages/instruction-plans/src/transaction-planner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ import {
TransactionMessage,
TransactionMessageWithFeePayer,
} from '@solana/transaction-messages';
import { getTransactionMessageSize, TRANSACTION_SIZE_LIMIT } from '@solana/transactions';
import { getTransactionMessageSize, getTransactionMessageSizeLimit } from '@solana/transactions';

import {
InstructionPlan,
Expand Down Expand Up @@ -326,7 +326,7 @@ async function selectAndMutateCandidate(
),
context.abortSignal,
);
if (getTransactionMessageSize(message) <= TRANSACTION_SIZE_LIMIT) {
if (getTransactionMessageSize(message) <= getTransactionMessageSizeLimit(message)) {
candidate.message = message;
return candidate;
}
Expand Down Expand Up @@ -364,11 +364,11 @@ async function createNewMessage(
context.abortSignal,
);
const updatedMessageSize = getTransactionMessageSize(updatedMessage);
if (updatedMessageSize > TRANSACTION_SIZE_LIMIT) {
if (updatedMessageSize > getTransactionMessageSizeLimit(updatedMessage)) {
const newMessageSize = getTransactionMessageSize(newMessage);
throw new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN, {
numBytesRequired: updatedMessageSize - newMessageSize,
numFreeBytes: TRANSACTION_SIZE_LIMIT - newMessageSize,
numFreeBytes: getTransactionMessageSizeLimit(newMessage) - newMessageSize,
});
}
return updatedMessage;
Expand Down Expand Up @@ -409,11 +409,11 @@ function fitEntirePlanInsideMessage(
newMessage = appendTransactionMessageInstructions([instructionPlan.instruction], message);
// eslint-disable-next-line no-case-declarations
const newMessageSize = getTransactionMessageSize(newMessage);
if (newMessageSize > TRANSACTION_SIZE_LIMIT) {
if (newMessageSize > getTransactionMessageSizeLimit(newMessage)) {
const baseMessageSize = getTransactionMessageSize(message);
throw new SolanaError(SOLANA_ERROR__INSTRUCTION_PLANS__MESSAGE_CANNOT_ACCOMMODATE_PLAN, {
numBytesRequired: newMessageSize - baseMessageSize,
numFreeBytes: TRANSACTION_SIZE_LIMIT - baseMessageSize,
numFreeBytes: getTransactionMessageSizeLimit(message) - baseMessageSize,
});
}
return newMessage;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,9 +12,10 @@ import {
import {
assertIsTransactionMessageWithinSizeLimit,
getTransactionMessageSize,
getTransactionMessageSizeLimit,
isTransactionMessageWithinSizeLimit,
} from '../transaction-message-size';
import { TRANSACTION_SIZE_LIMIT } from '../transaction-size';
import { LEGACY_TRANSACTION_SIZE_LIMIT, V1_TRANSACTION_SIZE_LIMIT } from '../transaction-size-limits';

const MOCK_BLOCKHASH = {
blockhash: '11111111111111111111111111111111' as Blockhash,
Expand All @@ -30,13 +31,37 @@ const SMALL_TRANSACTION_MESSAGE = pipe(
const OVERSIZED_TRANSACTION_MESSAGE = pipe(SMALL_TRANSACTION_MESSAGE, m =>
appendTransactionMessageInstruction(
{
data: new Uint8Array(TRANSACTION_SIZE_LIMIT + 1),
data: new Uint8Array(LEGACY_TRANSACTION_SIZE_LIMIT + 1),
programAddress: address('33333333333333333333333333333333333333333333'),
},
m,
),
);

const SMALL_V1_TRANSACTION_MESSAGE = pipe(
// @ts-expect-error v1 not yet included in type for `createTransactionMessage`
createTransactionMessage({ version: 1 }),
m => setTransactionMessageLifetimeUsingBlockhash(MOCK_BLOCKHASH, m),
m => setTransactionMessageFeePayer(address('22222222222222222222222222222222222222222222'), m),
);

describe('getTransactionMessageSizeLimit', () => {
it('returns LEGACY_TRANSACTION_SIZE_LIMIT for a legacy transaction message', () => {
const legacyMessage = pipe(createTransactionMessage({ version: 'legacy' }), m =>
setTransactionMessageFeePayer(address('22222222222222222222222222222222222222222222'), m),
);
expect(getTransactionMessageSizeLimit(legacyMessage)).toBe(LEGACY_TRANSACTION_SIZE_LIMIT);
});

it('returns LEGACY_TRANSACTION_SIZE_LIMIT for a v0 transaction message', () => {
expect(getTransactionMessageSizeLimit(SMALL_TRANSACTION_MESSAGE)).toBe(LEGACY_TRANSACTION_SIZE_LIMIT);
});

it('returns V1_TRANSACTION_SIZE_LIMIT for a v1 transaction message', () => {
expect(getTransactionMessageSizeLimit(SMALL_V1_TRANSACTION_MESSAGE)).toBe(V1_TRANSACTION_SIZE_LIMIT);
});
});

describe('getTransactionMessageSize', () => {
it('gets the size of a compilable transaction message', () => {
expect(getTransactionMessageSize(SMALL_TRANSACTION_MESSAGE)).toBe(136);
Expand All @@ -55,6 +80,32 @@ describe('isTransactionMessageWithinSizeLimit', () => {
it('returns false when the compiled size is above the transaction size limit', () => {
expect(isTransactionMessageWithinSizeLimit(OVERSIZED_TRANSACTION_MESSAGE)).toBe(false);
});

it('returns true for a v1 message whose size exceeds the legacy limit but is within the v1 limit', () => {
const v1MessageOverLegacyLimit = pipe(SMALL_V1_TRANSACTION_MESSAGE, m =>
appendTransactionMessageInstruction(
{
data: new Uint8Array(LEGACY_TRANSACTION_SIZE_LIMIT + 1),
programAddress: address('33333333333333333333333333333333333333333333'),
},
m,
),
);
expect(isTransactionMessageWithinSizeLimit(v1MessageOverLegacyLimit)).toBe(true);
});

it('returns false for a v1 message whose size exceeds the v1 limit', () => {
const v1MessageOverV1Limit = pipe(SMALL_V1_TRANSACTION_MESSAGE, m =>
appendTransactionMessageInstruction(
{
data: new Uint8Array(V1_TRANSACTION_SIZE_LIMIT + 1),
programAddress: address('33333333333333333333333333333333333333333333'),
},
m,
),
);
expect(isTransactionMessageWithinSizeLimit(v1MessageOverV1Limit)).toBe(false);
});
});

describe('assertIsTransactionMessageWithinSizeLimit', () => {
Expand All @@ -66,7 +117,38 @@ describe('assertIsTransactionMessageWithinSizeLimit', () => {
expect(() => assertIsTransactionMessageWithinSizeLimit(OVERSIZED_TRANSACTION_MESSAGE)).toThrow(
new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, {
transactionSize: 1405,
transactionSizeLimit: TRANSACTION_SIZE_LIMIT,
transactionSizeLimit: LEGACY_TRANSACTION_SIZE_LIMIT,
}),
);
});

it('does not throw for a v1 message whose size exceeds the legacy limit but is within the v1 limit', () => {
const v1MessageOverLegacyLimit = pipe(SMALL_V1_TRANSACTION_MESSAGE, m =>
appendTransactionMessageInstruction(
{
data: new Uint8Array(LEGACY_TRANSACTION_SIZE_LIMIT + 1),
programAddress: address('33333333333333333333333333333333333333333333'),
},
m,
),
);
expect(() => assertIsTransactionMessageWithinSizeLimit(v1MessageOverLegacyLimit)).not.toThrow();
});

it('throws for a v1 message whose size exceeds the v1 limit', () => {
const v1MessageOverV1Limit = pipe(SMALL_V1_TRANSACTION_MESSAGE, m =>
appendTransactionMessageInstruction(
{
data: new Uint8Array(V1_TRANSACTION_SIZE_LIMIT + 1),
programAddress: address('33333333333333333333333333333333333333333333'),
},
m,
),
);
expect(() => assertIsTransactionMessageWithinSizeLimit(v1MessageOverV1Limit)).toThrow(
new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, {
transactionSize: 4271,
transactionSizeLimit: V1_TRANSACTION_SIZE_LIMIT,
}),
);
});
Expand Down
60 changes: 57 additions & 3 deletions packages/transactions/src/__tests__/transaction-size-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,8 @@ import {
assertIsTransactionWithinSizeLimit,
getTransactionSize,
isTransactionWithinSizeLimit,
TRANSACTION_SIZE_LIMIT,
} from '../transaction-size';
import { LEGACY_TRANSACTION_SIZE_LIMIT, V1_TRANSACTION_SIZE_LIMIT } from '../transaction-size-limits';

const MOCK_BLOCKHASH = {
blockhash: '11111111111111111111111111111111' as Blockhash,
Expand All @@ -34,7 +34,40 @@ const OVERSIZED_TRANSACTION = compileTransaction(
pipe(SMALL_TRANSACTION_MESSAGE, m =>
appendTransactionMessageInstruction(
{
data: new Uint8Array(TRANSACTION_SIZE_LIMIT + 1),
data: new Uint8Array(LEGACY_TRANSACTION_SIZE_LIMIT + 1),
programAddress: address('33333333333333333333333333333333333333333333'),
},
m,
),
),
);

const SMALL_V1_TRANSACTION_MESSAGE = pipe(
// @ts-expect-error v1 not yet included in type for `createTransactionMessage`
createTransactionMessage({ version: 1 }),
m => setTransactionMessageLifetimeUsingBlockhash(MOCK_BLOCKHASH, m),
m => setTransactionMessageFeePayer(address('22222222222222222222222222222222222222222222'), m),
);

// A v1 transaction whose size exceeds the legacy limit but is within the v1 limit.
const V1_TRANSACTION_OVER_LEGACY_LIMIT = compileTransaction(
pipe(SMALL_V1_TRANSACTION_MESSAGE, m =>
appendTransactionMessageInstruction(
{
data: new Uint8Array(LEGACY_TRANSACTION_SIZE_LIMIT + 1),
programAddress: address('33333333333333333333333333333333333333333333'),
},
m,
),
),
);

// A v1 transaction whose size exceeds the v1 limit.
const V1_TRANSACTION_OVER_V1_LIMIT = compileTransaction(
pipe(SMALL_V1_TRANSACTION_MESSAGE, m =>
appendTransactionMessageInstruction(
{
data: new Uint8Array(V1_TRANSACTION_SIZE_LIMIT + 1),
programAddress: address('33333333333333333333333333333333333333333333'),
},
m,
Expand All @@ -60,6 +93,14 @@ describe('isTransactionWithinSizeLimit', () => {
it('returns false when the transaction size is above the transaction size limit', () => {
expect(isTransactionWithinSizeLimit(OVERSIZED_TRANSACTION)).toBe(false);
});

it('returns true for a v1 transaction whose size exceeds the legacy limit but is within the v1 limit', () => {
expect(isTransactionWithinSizeLimit(V1_TRANSACTION_OVER_LEGACY_LIMIT)).toBe(true);
});

it('returns false for a v1 transaction whose size exceeds the v1 limit', () => {
expect(isTransactionWithinSizeLimit(V1_TRANSACTION_OVER_V1_LIMIT)).toBe(false);
});
});

describe('assertIsTransactionWithinSizeLimit', () => {
Expand All @@ -71,7 +112,20 @@ describe('assertIsTransactionWithinSizeLimit', () => {
expect(() => assertIsTransactionWithinSizeLimit(OVERSIZED_TRANSACTION)).toThrow(
new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, {
transactionSize: 1405,
transactionSizeLimit: TRANSACTION_SIZE_LIMIT,
transactionSizeLimit: LEGACY_TRANSACTION_SIZE_LIMIT,
}),
);
});

it('does not throw for a v1 transaction whose size exceeds the legacy limit but is within the v1 limit', () => {
expect(() => assertIsTransactionWithinSizeLimit(V1_TRANSACTION_OVER_LEGACY_LIMIT)).not.toThrow();
});

it('throws for a v1 transaction whose size exceeds the v1 limit', () => {
expect(() => assertIsTransactionWithinSizeLimit(V1_TRANSACTION_OVER_V1_LIMIT)).toThrow(
new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, {
transactionSize: 4271,
transactionSizeLimit: V1_TRANSACTION_SIZE_LIMIT,
}),
);
});
Expand Down
26 changes: 22 additions & 4 deletions packages/transactions/src/transaction-message-size.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,8 @@ import type {
} from '@solana/transaction-messages';

import { compileTransaction } from './compile-transaction';
import { getTransactionSize, TRANSACTION_SIZE_LIMIT } from './transaction-size';
import { getTransactionSize } from './transaction-size';
import { LEGACY_TRANSACTION_SIZE_LIMIT, V1_TRANSACTION_SIZE_LIMIT } from './transaction-size-limits';

/**
* Gets the compiled transaction size of a given transaction message in bytes.
Expand All @@ -22,6 +23,22 @@ export function getTransactionMessageSize(
return getTransactionSize(compileTransaction(transactionMessage));
}

/**
* Returns the maximum allowed compiled size in bytes for a given transaction message.
*
* This depends on the version of the transaction message.
*
* @example
* ```ts
* const sizeLimit = getTransactionMessageSizeLimit(transactionMessage);
* ```
*/
export function getTransactionMessageSizeLimit(
transactionMessage: TransactionMessage & TransactionMessageWithFeePayer,
): number {
return transactionMessage.version === 1 ? V1_TRANSACTION_SIZE_LIMIT : LEGACY_TRANSACTION_SIZE_LIMIT;
}

/**
* Checks if a transaction message is within the size limit
* when compiled into a transaction.
Expand All @@ -40,7 +57,7 @@ export function isTransactionMessageWithinSizeLimit<
>(
transactionMessage: TTransactionMessage,
): transactionMessage is TransactionMessageWithinSizeLimit & TTransactionMessage {
return getTransactionMessageSize(transactionMessage) <= TRANSACTION_SIZE_LIMIT;
return getTransactionMessageSize(transactionMessage) <= getTransactionMessageSizeLimit(transactionMessage);
}

/**
Expand All @@ -64,10 +81,11 @@ export function assertIsTransactionMessageWithinSizeLimit<
transactionMessage: TTransactionMessage,
): asserts transactionMessage is TransactionMessageWithinSizeLimit & TTransactionMessage {
const transactionSize = getTransactionMessageSize(transactionMessage);
if (transactionSize > TRANSACTION_SIZE_LIMIT) {
const transactionSizeLimit = getTransactionMessageSizeLimit(transactionMessage);
if (transactionSize > transactionSizeLimit) {
throw new SolanaError(SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT, {
transactionSize,
transactionSizeLimit: TRANSACTION_SIZE_LIMIT,
transactionSizeLimit,
});
}
}
15 changes: 15 additions & 0 deletions packages/transactions/src/transaction-size-limits.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* This file defines the size limits for transactions
* It is used by both transaction-size and transaction-message-size
* But intentionally not exported from the package
*/

/**
* The maximum size of a legacy (and v0) transaction in bytes.
*/
export const LEGACY_TRANSACTION_SIZE_LIMIT = 1232;

/**
* The maximum size of a version 1 transaction in bytes.
*/
export const V1_TRANSACTION_SIZE_LIMIT = 4096;
Loading
Loading