Skip to content

Commit cb11699

Browse files
authored
Do not allow decoding transactions with an unsupported version (#871)
* Do not allow decoding transactions with an unsupported version * Add changeset * Restrict the type of version in VersionedCompiledTransactionMessage
1 parent f591dea commit cb11699

File tree

9 files changed

+102
-11
lines changed

9 files changed

+102
-11
lines changed

.changeset/fast-pumas-sin.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
'@solana/transaction-messages': patch
3+
'@solana/transactions': patch
4+
'@solana/errors': patch
5+
---
6+
7+
Do not allow decoding transactions with an unsupported version

packages/errors/src/codes.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -213,6 +213,7 @@ export const SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH = 5663017;
213213
export const SOLANA_ERROR__TRANSACTION__FAILED_TO_ESTIMATE_COMPUTE_LIMIT = 5663018;
214214
export const SOLANA_ERROR__TRANSACTION__FAILED_WHEN_SIMULATING_TO_ESTIMATE_COMPUTE_LIMIT = 5663019;
215215
export const SOLANA_ERROR__TRANSACTION__EXCEEDS_SIZE_LIMIT = 5663020;
216+
export const SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED = 5663021;
216217

217218
// Transaction errors.
218219
// Reserve error codes starting with [7050000-7050999] for the Rust enum `TransactionError`.
@@ -531,6 +532,7 @@ export type SolanaErrorCode =
531532
| typeof SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE
532533
| typeof SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH
533534
| typeof SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING
535+
| typeof SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED
534536
| typeof SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE
535537
| typeof SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_BORROW_OUTSTANDING
536538
| typeof SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_IN_USE

packages/errors/src/context.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,7 @@ import {
162162
SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE,
163163
SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH,
164164
SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING,
165+
SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED,
165166
SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE,
166167
SOLANA_ERROR__TRANSACTION_ERROR__DUPLICATE_INSTRUCTION,
167168
SOLANA_ERROR__TRANSACTION_ERROR__INSUFFICIENT_FUNDS_FOR_RENT,
@@ -638,6 +639,9 @@ export type SolanaErrorContext = DefaultUnspecifiedErrorContextToUndefined<
638639
[SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING]: {
639640
addresses: string[];
640641
};
642+
[SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED]: {
643+
unsupportedVersion: number;
644+
};
641645
[SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE]: {
642646
actualVersion: number;
643647
};

packages/errors/src/messages.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ import {
205205
SOLANA_ERROR__TRANSACTION__INVOKED_PROGRAMS_MUST_NOT_BE_WRITABLE,
206206
SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH,
207207
SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING,
208+
SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED,
208209
SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE,
209210
SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_BORROW_OUTSTANDING,
210211
SOLANA_ERROR__TRANSACTION_ERROR__ACCOUNT_IN_USE,
@@ -648,4 +649,6 @@ export const SolanaErrorMessages: Readonly<{
648649
[SOLANA_ERROR__TRANSACTION__SIGNATURES_MISSING]: 'Transaction is missing signatures for addresses: $addresses.',
649650
[SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE]:
650651
'Transaction version must be in the range [0, 127]. `$actualVersion` given',
652+
[SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED]:
653+
'This version of Kit does not support decoding transactions with version $unsupportedVersion. The current max supported version is 0.',
651654
};

packages/transaction-messages/src/codecs/__tests__/transaction-version-test.ts

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Decoder, Encoder } from '@solana/codecs-core';
2+
import { SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, SolanaError } from '@solana/errors';
23

34
import { TransactionVersion } from '../../transaction-message';
45
import {
@@ -10,6 +11,7 @@ import {
1011
const VERSION_FLAG_MASK = 0x80;
1112
const VERSION_TEST_CASES = // Versions 0–127
1213
[...Array(128).keys()].map(version => [version | VERSION_FLAG_MASK, version as TransactionVersion] as const);
14+
const UNSUPPORTED_VERSION_TEST_CASES = VERSION_TEST_CASES.slice(1); // versions 1-127
1315

1416
describe.each([getTransactionVersionCodec, getTransactionVersionEncoder])(
1517
'Transaction version encoder',
@@ -21,8 +23,15 @@ describe.each([getTransactionVersionCodec, getTransactionVersionEncoder])(
2123
it('serializes no data when the version is `legacy`', () => {
2224
expect(transactionVersion.encode('legacy')).toEqual(new Uint8Array());
2325
});
24-
it.each(VERSION_TEST_CASES)('serializes to `%s` when the version is `%s`', (expected, version) => {
25-
expect(transactionVersion.encode(version)).toEqual(new Uint8Array([expected]));
26+
it('serializes to `0x80` when the version is `0`', () => {
27+
expect(transactionVersion.encode(0)).toEqual(new Uint8Array([0x80]));
28+
});
29+
it.each(UNSUPPORTED_VERSION_TEST_CASES)('fatals for unsupported version `%s`', (_byte, version) => {
30+
expect(() => transactionVersion.encode(version)).toThrow(
31+
new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, {
32+
unsupportedVersion: version,
33+
}),
34+
);
2635
});
2736
it.each([-1 as TransactionVersion, 128 as TransactionVersion])(
2837
'throws when passed the out-of-range version `%s`',
@@ -40,9 +49,6 @@ describe.each([getTransactionVersionCodec, getTransactionVersionDecoder])(
4049
beforeEach(() => {
4150
transactionVersion = serializerFactory();
4251
});
43-
it.each(VERSION_TEST_CASES)('deserializes `%s` to the version `%s`', (byte, expected) => {
44-
expect(transactionVersion.decode(new Uint8Array([byte]))).toEqual(expected);
45-
});
4652
it('deserializes to `legacy` when missing the version flag', () => {
4753
expect(
4854
transactionVersion.decode(
@@ -51,5 +57,19 @@ describe.each([getTransactionVersionCodec, getTransactionVersionDecoder])(
5157
),
5258
).toBe('legacy');
5359
});
60+
it('deserializes to 0 for a version 0 transaction', () => {
61+
expect(
62+
transactionVersion.decode(
63+
new Uint8Array([0 | VERSION_FLAG_MASK]), // version 0 with the version flag
64+
),
65+
).toBe(0);
66+
});
67+
it.each(UNSUPPORTED_VERSION_TEST_CASES)('fatals for unsupported version `%s`', (byte, version) => {
68+
expect(() => transactionVersion.decode(new Uint8Array([byte]))).toThrow(
69+
new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, {
70+
unsupportedVersion: version,
71+
}),
72+
);
73+
});
5474
},
5575
);

packages/transaction-messages/src/codecs/transaction-version.ts

Lines changed: 19 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,13 @@ import {
66
VariableSizeDecoder,
77
VariableSizeEncoder,
88
} from '@solana/codecs-core';
9-
import { SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE, SolanaError } from '@solana/errors';
9+
import {
10+
SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED,
11+
SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_OUT_OF_RANGE,
12+
SolanaError,
13+
} from '@solana/errors';
1014

11-
import { TransactionVersion } from '../transaction-message';
15+
import { MAX_SUPPORTED_TRANSACTION_VERSION, TransactionVersion } from '../transaction-message';
1216

1317
const VERSION_FLAG_MASK = 0x80;
1418

@@ -31,6 +35,12 @@ export function getTransactionVersionEncoder(): VariableSizeEncoder<TransactionV
3135
actualVersion: value,
3236
});
3337
}
38+
39+
if (value > MAX_SUPPORTED_TRANSACTION_VERSION) {
40+
throw new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, {
41+
unsupportedVersion: value,
42+
});
43+
}
3444
bytes.set([value | VERSION_FLAG_MASK], offset);
3545
return offset + 1;
3646
},
@@ -53,8 +63,13 @@ export function getTransactionVersionDecoder(): VariableSizeDecoder<TransactionV
5363
// No version flag set; it's a legacy (unversioned) transaction.
5464
return ['legacy', offset];
5565
} else {
56-
const version = (firstByte ^ VERSION_FLAG_MASK) as TransactionVersion;
57-
return [version, offset + 1];
66+
const version = firstByte ^ VERSION_FLAG_MASK;
67+
if (version > MAX_SUPPORTED_TRANSACTION_VERSION) {
68+
throw new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, {
69+
unsupportedVersion: version,
70+
});
71+
}
72+
return [version as TransactionVersion, offset + 1];
5873
}
5974
},
6075
});

packages/transaction-messages/src/compile/message.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -48,7 +48,7 @@ type VersionedCompiledTransactionMessage = BaseCompiledTransactionMessage &
4848
Readonly<{
4949
/** A list of address tables and the accounts that this transaction loads from them */
5050
addressTableLookups?: ReturnType<typeof getCompiledAddressTableLookups>;
51-
version: number;
51+
version: 0;
5252
}>;
5353

5454
/**

packages/transaction-messages/src/transaction-message.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ export type BaseTransactionMessage<
88
version: TVersion;
99
}>;
1010

11+
export const MAX_SUPPORTED_TRANSACTION_VERSION = 0;
12+
1113
type LegacyInstruction<TProgramAddress extends string = string> = Instruction<TProgramAddress, readonly AccountMeta[]>;
1214
type LegacyTransactionMessage = BaseTransactionMessage<'legacy', LegacyInstruction>;
1315
type V0TransactionMessage = BaseTransactionMessage<0, Instruction>;

packages/transactions/src/codecs/__tests__/transaction-codec-test.ts

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@ import '@solana/test-matchers/toBeFrozenObject';
22

33
import { Address } from '@solana/addresses';
44
import { ReadonlyUint8Array, VariableSizeDecoder, VariableSizeEncoder } from '@solana/codecs-core';
5-
import { SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH, SolanaError } from '@solana/errors';
5+
import {
6+
SOLANA_ERROR__TRANSACTION__MESSAGE_SIGNATURES_MISMATCH,
7+
SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED,
8+
SolanaError,
9+
} from '@solana/errors';
610
import { SignatureBytes } from '@solana/keys';
711

812
import { Transaction, TransactionMessageBytes } from '../../transaction';
@@ -167,6 +171,40 @@ describe.each([getTransactionDecoder, getTransactionCodec])('Transaction decoder
167171
const decoded = decoder.decode(encodedTransaction);
168172
expect(decoded.signatures).toBeFrozenObject();
169173
});
174+
175+
it('should fatal for unsupported transaction version 1', () => {
176+
const signature = new Uint8Array(64).fill(0) as ReadonlyUint8Array as SignatureBytes;
177+
const messageBytes = new Uint8Array([
178+
/** VERSION HEADER */
179+
129, // 1 + version mask
180+
181+
/** MESSAGE HEADER */
182+
1, // numSignerAccounts
183+
0, // numReadonlySignerAccount
184+
1, // numReadonlyNonSignerAccounts
185+
186+
/** STATIC ADDRESSES */
187+
2, // Number of static accounts
188+
...addressBytes,
189+
...new Uint8Array(64).fill(12),
190+
191+
/** REST OF TRANSACTION MESSAGE (arbitrary) */
192+
...new Uint8Array(100).fill(1),
193+
]) as ReadonlyUint8Array as TransactionMessageBytes;
194+
195+
const encodedTransaction = new Uint8Array([
196+
/** SIGNATURES */
197+
1, // num signatures
198+
...signature,
199+
200+
/** MESSAGE */
201+
...messageBytes,
202+
]);
203+
204+
expect(() => decoder.decode(encodedTransaction)).toThrow(
205+
new SolanaError(SOLANA_ERROR__TRANSACTION__VERSION_NUMBER_NOT_SUPPORTED, { unsupportedVersion: 1 }),
206+
);
207+
});
170208
});
171209

172210
describe('for a transaction with multiple signatures', () => {

0 commit comments

Comments
 (0)