diff --git a/.changeset/fast-pumas-sin.md b/.changeset/fast-pumas-sin.md new file mode 100644 index 000000000..ab211cea6 --- /dev/null +++ b/.changeset/fast-pumas-sin.md @@ -0,0 +1,7 @@ +--- +'@solana/transaction-messages': patch +'@solana/transactions': patch +'@solana/errors': patch +--- + +Do not allow decoding transactions with an unsupported version diff --git a/packages/errors/src/codes.ts b/packages/errors/src/codes.ts index 59c25691c..6422caa6c 100644 --- a/packages/errors/src/codes.ts +++ b/packages/errors/src/codes.ts @@ -213,6 +213,7 @@ 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; +export const SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED = 5663021; // Transaction errors. // Reserve error codes starting with [7050000-7050999] for the Rust enum `TransactionError`. @@ -531,6 +532,7 @@ export type SolanaErrorCode = | typeof SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE | typeof SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH | typeof SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING + | typeof SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED | typeof SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE | typeof SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_BORROW_OUTSTANDING | typeof SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_IN_USE diff --git a/packages/errors/src/context.ts b/packages/errors/src/context.ts index 8328d66a7..faf9e7637 100644 --- a/packages/errors/src/context.ts +++ b/packages/errors/src/context.ts @@ -162,6 +162,7 @@ import { SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE, SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, + SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE, SOLANA_ERROR__TRANSACTION_ERROR__DUPLICATE_INSTRUCTION, SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_RENT, @@ -638,6 +639,9 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined< [SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING]: { addresses: string[]; }; + [SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED]: { + unsupportedVersion: number; + }; [SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE]: { actualVersion: number; }; diff --git a/packages/errors/src/messages.ts b/packages/errors/src/messages.ts index d0df75c7b..1168bb590 100644 --- a/packages/errors/src/messages.ts +++ b/packages/errors/src/messages.ts @@ -205,6 +205,7 @@ import { SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE, SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH, SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING, + SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE, SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_BORROW_OUTSTANDING, SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_IN_USE, @@ -648,4 +649,6 @@ export const SolanaErrorMessages: Readonly<{ [SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING]: 'Transaction is missing signatures for addresses: $addresses.', [SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE]: 'Transaction version must be in the range [0, 127]. `$actualVersion` given', + [SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED]: + 'This version of Kit does not support decoding transactions with version $unsupportedVersion. The current max supported version is 0.', }; diff --git a/packages/transaction-messages/src/codecs/__tests__/transaction-version-test.ts b/packages/transaction-messages/src/codecs/__tests__/transaction-version-test.ts index 72ecc7f3c..599e9de66 100644 --- a/packages/transaction-messages/src/codecs/__tests__/transaction-version-test.ts +++ b/packages/transaction-messages/src/codecs/__tests__/transaction-version-test.ts @@ -1,4 +1,5 @@ import { Decoder, Encoder } from '@solana/codecs-core'; +import { SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, SolanaError } from '@solana/errors'; import { TransactionVersion } from '../../transaction-message'; import { @@ -10,6 +11,7 @@ import { const VERSION_FLAG_MASK = 0x80; const VERSION_TEST_CASES = // Versions 0–127 [...Array(128).keys()].map(version => [version | VERSION_FLAG_MASK, version as TransactionVersion] as const); +const UNSUPPORTED_VERSION_TEST_CASES = VERSION_TEST_CASES.slice(1); // versions 1-127 describe.each([getTransactionVersionCodec, getTransactionVersionEncoder])( 'Transaction version encoder', @@ -21,8 +23,15 @@ describe.each([getTransactionVersionCodec, getTransactionVersionEncoder])( it('serializes no data when the version is `legacy`', () => { expect(transactionVersion.encode('legacy')).toEqual(new Uint8Array()); }); - it.each(VERSION_TEST_CASES)('serializes to `%s` when the version is `%s`', (expected, version) => { - expect(transactionVersion.encode(version)).toEqual(new Uint8Array([expected])); + it('serializes to `0x80` when the version is `0`', () => { + expect(transactionVersion.encode(0)).toEqual(new Uint8Array([0x80])); + }); + it.each(UNSUPPORTED_VERSION_TEST_CASES)('fatals for unsupported version `%s`', (_byte, version) => { + expect(() => transactionVersion.encode(version)).toThrow( + new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, { + unsupportedVersion: version, + }), + ); }); it.each([-1 as TransactionVersion, 128 as TransactionVersion])( 'throws when passed the out-of-range version `%s`', @@ -40,9 +49,6 @@ describe.each([getTransactionVersionCodec, getTransactionVersionDecoder])( beforeEach(() => { transactionVersion = serializerFactory(); }); - it.each(VERSION_TEST_CASES)('deserializes `%s` to the version `%s`', (byte, expected) => { - expect(transactionVersion.decode(new Uint8Array([byte]))).toEqual(expected); - }); it('deserializes to `legacy` when missing the version flag', () => { expect( transactionVersion.decode( @@ -51,5 +57,19 @@ describe.each([getTransactionVersionCodec, getTransactionVersionDecoder])( ), ).toBe('legacy'); }); + it('deserializes to 0 for a version 0 transaction', () => { + expect( + transactionVersion.decode( + new Uint8Array([0 | VERSION_FLAG_MASK]), // version 0 with the version flag + ), + ).toBe(0); + }); + it.each(UNSUPPORTED_VERSION_TEST_CASES)('fatals for unsupported version `%s`', (byte, version) => { + expect(() => transactionVersion.decode(new Uint8Array([byte]))).toThrow( + new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, { + unsupportedVersion: version, + }), + ); + }); }, ); diff --git a/packages/transaction-messages/src/codecs/transaction-version.ts b/packages/transaction-messages/src/codecs/transaction-version.ts index 7b190970e..68a5f3b4e 100644 --- a/packages/transaction-messages/src/codecs/transaction-version.ts +++ b/packages/transaction-messages/src/codecs/transaction-version.ts @@ -6,9 +6,13 @@ import { VariableSizeDecoder, VariableSizeEncoder, } from '@solana/codecs-core'; -import { SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE, SolanaError } from '@solana/errors'; +import { + SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, + SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE, + SolanaError, +} from '@solana/errors'; -import { TransactionVersion } from '../transaction-message'; +import { MAX_SUPPORTED_TRANSACTION_VERSION, TransactionVersion } from '../transaction-message'; const VERSION_FLAG_MASK = 0x80; @@ -31,6 +35,12 @@ export function getTransactionVersionEncoder(): VariableSizeEncoder MAX_SUPPORTED_TRANSACTION_VERSION) { + throw new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, { + unsupportedVersion: value, + }); + } bytes.set([value | VERSION_FLAG_MASK], offset); return offset + 1; }, @@ -53,8 +63,13 @@ export function getTransactionVersionDecoder(): VariableSizeDecoder MAX_SUPPORTED_TRANSACTION_VERSION) { + throw new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, { + unsupportedVersion: version, + }); + } + return [version as TransactionVersion, offset + 1]; } }, }); diff --git a/packages/transaction-messages/src/compile/message.ts b/packages/transaction-messages/src/compile/message.ts index 21d30df84..e3a3fdda9 100644 --- a/packages/transaction-messages/src/compile/message.ts +++ b/packages/transaction-messages/src/compile/message.ts @@ -48,7 +48,7 @@ type VersionedCompiledTransactionMessage = BaseCompiledTransactionMessage & Readonly<{ /** A list of address tables and the accounts that this transaction loads from them */ addressTableLookups?: ReturnType; - version: number; + version: 0; }>; /** diff --git a/packages/transaction-messages/src/transaction-message.ts b/packages/transaction-messages/src/transaction-message.ts index 9e1fe9194..39733ed52 100644 --- a/packages/transaction-messages/src/transaction-message.ts +++ b/packages/transaction-messages/src/transaction-message.ts @@ -8,6 +8,8 @@ export type BaseTransactionMessage< version: TVersion; }>; +export const MAX_SUPPORTED_TRANSACTION_VERSION = 0; + type LegacyInstruction = Instruction; type LegacyTransactionMessage = BaseTransactionMessage<'legacy', LegacyInstruction>; type V0TransactionMessage = BaseTransactionMessage<0, Instruction>; diff --git a/packages/transactions/src/codecs/__tests__/transaction-codec-test.ts b/packages/transactions/src/codecs/__tests__/transaction-codec-test.ts index c32c1a75d..e8c7d4f3d 100644 --- a/packages/transactions/src/codecs/__tests__/transaction-codec-test.ts +++ b/packages/transactions/src/codecs/__tests__/transaction-codec-test.ts @@ -2,7 +2,11 @@ import '@solana/test-matchers/toBeFrozenObject'; import { Address } from '@solana/addresses'; import { ReadonlyUint8Array, VariableSizeDecoder, VariableSizeEncoder } from '@solana/codecs-core'; -import { SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH, SolanaError } from '@solana/errors'; +import { + SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH, + SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, + SolanaError, +} from '@solana/errors'; import { SignatureBytes } from '@solana/keys'; import { Transaction, TransactionMessageBytes } from '../../transaction'; @@ -167,6 +171,40 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction decoder const decoded = decoder.decode(encodedTransaction); expect(decoded.signatures).toBeFrozenObject(); }); + + it('should fatal for unsupported transaction version 1', () => { + const signature = new Uint8Array(64).fill(0) as ReadonlyUint8Array as SignatureBytes; + const messageBytes = new Uint8Array([ + /** VERSION HEADER */ + 129, // 1 + version mask + + /** MESSAGE HEADER */ + 1, // numSignerAccounts + 0, // numReadonlySignerAccount + 1, // numReadonlyNonSignerAccounts + + /** STATIC ADDRESSES */ + 2, // Number of static accounts + ...addressBytes, + ...new Uint8Array(64).fill(12), + + /** REST OF TRANSACTION MESSAGE (arbitrary) */ + ...new Uint8Array(100).fill(1), + ]) as ReadonlyUint8Array as TransactionMessageBytes; + + const encodedTransaction = new Uint8Array([ + /** SIGNATURES */ + 1, // num signatures + ...signature, + + /** MESSAGE */ + ...messageBytes, + ]); + + expect(() => decoder.decode(encodedTransaction)).toThrow( + new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, { unsupportedVersion: 1 }), + ); + }); }); describe('for a transaction with multiple signatures', () => {