diff --git a/.changeset/thick-radios-appear.md b/.changeset/thick-radios-appear.md new file mode 100644 index 000000000000..577d763a47fd --- /dev/null +++ b/.changeset/thick-radios-appear.md @@ -0,0 +1,5 @@ +--- +"@solana/transaction-messages": patch +--- + +Freeze the instructions and lifetimeConstraint fields within transaction messages diff --git a/packages/transaction-messages/src/__tests__/blockhash-test.ts b/packages/transaction-messages/src/__tests__/blockhash-test.ts index 2f829a884128..c22fe010bad8 100644 --- a/packages/transaction-messages/src/__tests__/blockhash-test.ts +++ b/packages/transaction-messages/src/__tests__/blockhash-test.ts @@ -137,4 +137,11 @@ describe('setTransactionMessageLifetimeUsingBlockhash', () => { ); expect(txWithBlockhashLifetimeConstraint).toBeFrozenObject(); }); + it('freezes the blockhash constraint', () => { + const txWithBlockhashLifetimeConstraint = setTransactionMessageLifetimeUsingBlockhash( + BLOCKHASH_CONSTRAINT_A, + baseTx, + ); + expect(txWithBlockhashLifetimeConstraint.lifetimeConstraint).toBeFrozenObject(); + }); }); diff --git a/packages/transaction-messages/src/__tests__/create-transaction-message-test.ts b/packages/transaction-messages/src/__tests__/create-transaction-message-test.ts index 90cb1f8b464b..70f726cffbb2 100644 --- a/packages/transaction-messages/src/__tests__/create-transaction-message-test.ts +++ b/packages/transaction-messages/src/__tests__/create-transaction-message-test.ts @@ -19,4 +19,8 @@ describe('createTransactionMessage', () => { const tx = createTransactionMessage({ version: 0 }); expect(tx).toBeFrozenObject(); }); + it('freezes the instructions array', () => { + const tx = createTransactionMessage({ version: 0 }); + expect(tx.instructions).toBeFrozenObject(); + }); }); diff --git a/packages/transaction-messages/src/__tests__/decompile-message-test.ts b/packages/transaction-messages/src/__tests__/decompile-message-test.ts index 914f0179e076..654b1d9566fd 100644 --- a/packages/transaction-messages/src/__tests__/decompile-message-test.ts +++ b/packages/transaction-messages/src/__tests__/decompile-message-test.ts @@ -1,3 +1,5 @@ +import '@solana/test-matchers/toBeFrozenObject'; + import { Address } from '@solana/addresses'; import { SOLANA_ERROR__TRANSACTION__FAILED_TO_DECOMPILE_ADDRESS_LOOKUP_TABLE_CONTENTS_MISSING, @@ -40,6 +42,23 @@ describe('decompileTransactionMessage', () => { }); }); + it('freezes the blockhash lifetime constraint', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 0, + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, + }, + instructions: [], + lifetimeToken: blockhash, + staticAccounts: [feePayer], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + expect(transaction.lifetimeConstraint).toBeFrozenObject(); + }); + it('converts a transaction with version legacy', () => { const compiledTransaction: CompiledTransactionMessage = { header: { @@ -140,6 +159,42 @@ describe('decompileTransactionMessage', () => { expect(transaction.instructions).toStrictEqual([expectedInstruction]); }); + it('freezes the instruction accounts', () => { + const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; + + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 2, // 1 passed into instruction + 1 program + numReadonlySignerAccounts: 1, + numSignerAccounts: 3, // fee payer + 2 passed into instruction + }, + instructions: [ + { + accountIndices: [1, 2, 3, 4], + data: new Uint8Array([0, 1, 2, 3, 4]), + programAddressIndex: 5, + }, + ], + lifetimeToken: blockhash, + staticAccounts: [ + // writable signers + feePayer, + 'H4RdPRWYk3pKw2CkNznxQK6J6herjgQke2pzFJW4GC6x' as Address, + // read-only signers + 'G35QeFd4jpXWfRkuRKwn8g4vYrmn8DWJ5v88Kkpd8z1V' as Address, + // writable non-signers + '3LeBzRE9Yna5zi9R8vdT3MiNQYuEp4gJgVyhhwmqfCtd' as Address, + // read-only non-signers + '8kud9bpNvfemXYdTFjs5cZ8fZinBkx8JAnhVmRwJZk5e' as Address, + programAddress, + ], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + expect(transaction.instructions[0].accounts).toBeFrozenObject(); + }); + it('converts a transaction with multiple instructions', () => { const compiledTransaction: CompiledTransactionMessage = { header: { @@ -194,6 +249,46 @@ describe('decompileTransactionMessage', () => { lastValidBlockHeight: 100n, }); }); + + it('freezes the instructions within the transaction', () => { + const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; + + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 1, + // fee payer + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // program address + }, + instructions: [{ programAddressIndex: 1 }], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + expect(transaction.instructions[0]).toBeFrozenObject(); + }); + + it('freezes the instructions array', () => { + const programAddress = 'HZMKVnRrWLyQLwPLTTLKtY7ET4Cf7pQugrTr9eTBrpsf' as Address; + + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 1, + // fee payer + numReadonlySignerAccounts: 0, + numSignerAccounts: 1, // program address + }, + instructions: [{ programAddressIndex: 1 }], + lifetimeToken: blockhash, + staticAccounts: [feePayer, programAddress], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + expect(transaction.instructions).toBeFrozenObject(); + }); }); describe('for a transaction with a durable nonce lifetime', () => { @@ -266,6 +361,42 @@ describe('decompileTransactionMessage', () => { expect(transaction.lifetimeConstraint).toStrictEqual({ nonce }); }); + it('freezes the nonce lifetime constraint', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 2, // recent blockhashes sysvar, system program + numReadonlySignerAccounts: 0, // nonce authority already added as fee payer + numSignerAccounts: 1, // fee payer and nonce authority are the same account + }, + instructions: [ + { + accountIndices: [ + 1, // nonce account address + 3, // recent blockhashes sysvar + 0, // nonce authority address + ], + data: new Uint8Array([4, 0, 0, 0]), + programAddressIndex: 2, + }, + ], + lifetimeToken: nonce, + staticAccounts: [ + // writable signers + nonceAuthorityAddress, + // no read-only signers + // writable non-signers + nonceAccountAddress, + // read-only non-signers + systemProgramAddress, + recentBlockhashesSysvarAddress, + ], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + expect(transaction.lifetimeConstraint).toBeFrozenObject(); + }); + it('converts a transaction with one instruction which is advance nonce (fee payer is not nonce authority)', () => { const compiledTransaction: CompiledTransactionMessage = { header: { @@ -405,6 +536,96 @@ describe('decompileTransactionMessage', () => { expect(transaction.instructions).toStrictEqual(expectedInstructions); expect(transaction.lifetimeConstraint).toStrictEqual({ nonce }); }); + + it('freezes the instructions within the transaction', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 4, // recent blockhashes sysvar, system program, 2 other program addresses + numReadonlySignerAccounts: 0, // nonce authority already added as fee payer + numSignerAccounts: 1, // fee payer and nonce authority are the same account + }, + instructions: [ + { + accountIndices: [ + 1, // nonce account address + 3, // recent blockhashes sysvar + 0, // nonce authority address + ], + data: new Uint8Array([4, 0, 0, 0]), + programAddressIndex: 2, + }, + { + accountIndices: [0, 1], + data: new Uint8Array([1, 2, 3, 4]), + programAddressIndex: 4, + }, + { programAddressIndex: 5 }, + ], + lifetimeToken: nonce, + staticAccounts: [ + // writable signers + nonceAuthorityAddress, + // no read-only signers + // writable non-signers + nonceAccountAddress, + // read-only non-signers + systemProgramAddress, + recentBlockhashesSysvarAddress, + '3hpECiFPtnyxoWqWqcVyfBUDhPKSZXWDduNXFywo8ncP' as Address, + 'Cmqw16pVQvmW1b7Ek1ioQ5Ggf1PaoXi5XxsK9iVSbRKC' as Address, + ], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + expect(transaction.instructions[0]).toBeFrozenObject(); + expect(transaction.instructions[1]).toBeFrozenObject(); + expect(transaction.instructions[2]).toBeFrozenObject(); + }); + + it('freezes the instructions array', () => { + const compiledTransaction: CompiledTransactionMessage = { + header: { + numReadonlyNonSignerAccounts: 4, // recent blockhashes sysvar, system program, 2 other program addresses + numReadonlySignerAccounts: 0, // nonce authority already added as fee payer + numSignerAccounts: 1, // fee payer and nonce authority are the same account + }, + instructions: [ + { + accountIndices: [ + 1, // nonce account address + 3, // recent blockhashes sysvar + 0, // nonce authority address + ], + data: new Uint8Array([4, 0, 0, 0]), + programAddressIndex: 2, + }, + { + accountIndices: [0, 1], + data: new Uint8Array([1, 2, 3, 4]), + programAddressIndex: 4, + }, + { programAddressIndex: 5 }, + ], + lifetimeToken: nonce, + staticAccounts: [ + // writable signers + nonceAuthorityAddress, + // no read-only signers + // writable non-signers + nonceAccountAddress, + // read-only non-signers + systemProgramAddress, + recentBlockhashesSysvarAddress, + '3hpECiFPtnyxoWqWqcVyfBUDhPKSZXWDduNXFywo8ncP' as Address, + 'Cmqw16pVQvmW1b7Ek1ioQ5Ggf1PaoXi5XxsK9iVSbRKC' as Address, + ], + version: 0, + }; + + const transaction = decompileTransactionMessage(compiledTransaction); + expect(transaction.instructions).toBeFrozenObject(); + }); }); describe('for a transaction with address lookup tables', () => { diff --git a/packages/transaction-messages/src/__tests__/durable-nonce-test.ts b/packages/transaction-messages/src/__tests__/durable-nonce-test.ts index 2f65bbcbd5a2..3484cb4c7995 100644 --- a/packages/transaction-messages/src/__tests__/durable-nonce-test.ts +++ b/packages/transaction-messages/src/__tests__/durable-nonce-test.ts @@ -189,7 +189,7 @@ describe('setTransactionMessageLifetimeUsingDurableNonce', () => { ); expect(durableNonceTxWithConstraintA).toHaveProperty('lifetimeConstraint', { nonce: NONCE_CONSTRAINT_A.nonce }); }); - it('appends an `AdvanceNonceAccount` instruction', () => { + it('prepends an `AdvanceNonceAccount` instruction', () => { const durableNonceTxWithConstraintA = setTransactionMessageLifetimeUsingDurableNonce( NONCE_CONSTRAINT_A, baseTx, @@ -199,6 +199,13 @@ describe('setTransactionMessageLifetimeUsingDurableNonce', () => { baseTx.instructions[0], ]); }); + it('freezes the prepended `AdvanceNonceAccount` instruction', () => { + const durableNonceTxWithConstraintA = setTransactionMessageLifetimeUsingDurableNonce( + NONCE_CONSTRAINT_A, + baseTx, + ); + expect(durableNonceTxWithConstraintA.instructions[0]).toBeFrozenObject(); + }); describe('given a transaction with an advance nonce account instruction but no nonce lifetime constraint', () => { it('does not modify an `AdvanceNonceAccount` instruction if the existing one matches the constraint added', () => { const instruction = createMockAdvanceNonceAccountInstruction(NONCE_CONSTRAINT_A); @@ -213,19 +220,39 @@ describe('setTransactionMessageLifetimeUsingDurableNonce', () => { ); expect(durableNonceTxWithConstraintA.instructions).toEqual([instruction, baseTx.instructions[0]]); }); - it('replaces an `AdvanceNonceAccount` instruction if the existing one does not match the constraint added', () => { - const transaction: BaseTransactionMessage = { - ...baseTx, - instructions: [createMockAdvanceNonceAccountInstruction(NONCE_CONSTRAINT_B), baseTx.instructions[0]], - }; - const durableNonceTxWithConstraintA = setTransactionMessageLifetimeUsingDurableNonce( - NONCE_CONSTRAINT_A, - transaction, - ); - expect(durableNonceTxWithConstraintA.instructions).toEqual([ - createMockAdvanceNonceAccountInstruction(NONCE_CONSTRAINT_A), - baseTx.instructions[0], - ]); + describe('when the existing `AdvanceNonceAccount` instruction does not match the constraint added', () => { + it('replaces the existing instruction', () => { + const transaction: BaseTransactionMessage = { + ...baseTx, + instructions: [ + createMockAdvanceNonceAccountInstruction(NONCE_CONSTRAINT_B), + baseTx.instructions[0], + ], + }; + const durableNonceTxWithConstraintA = setTransactionMessageLifetimeUsingDurableNonce( + NONCE_CONSTRAINT_A, + transaction, + ); + expect(durableNonceTxWithConstraintA.instructions).toEqual([ + createMockAdvanceNonceAccountInstruction(NONCE_CONSTRAINT_A), + baseTx.instructions[0], + ]); + }); + + it('freezes the replacement instruction', () => { + const transaction: BaseTransactionMessage = { + ...baseTx, + instructions: [ + createMockAdvanceNonceAccountInstruction(NONCE_CONSTRAINT_B), + baseTx.instructions[0], + ], + }; + const durableNonceTxWithConstraintA = setTransactionMessageLifetimeUsingDurableNonce( + NONCE_CONSTRAINT_A, + transaction, + ); + expect(durableNonceTxWithConstraintA.instructions[0]).toBeFrozenObject(); + }); }); }); describe('given a durable nonce transaction', () => { @@ -260,6 +287,13 @@ describe('setTransactionMessageLifetimeUsingDurableNonce', () => { durableNonceTxWithConstraintA.instructions[1], ]); }); + it('freezes the replacement advance nonce account instruction', () => { + const durableNonceTxWithConstraintB = setTransactionMessageLifetimeUsingDurableNonce( + NONCE_CONSTRAINT_B, + durableNonceTxWithConstraintA, + ); + expect(durableNonceTxWithConstraintB.instructions[0]).toBeFrozenObject(); + }); it('returns the original transaction when trying to set the same durable nonce constraint again', () => { const txWithSameNonceLifetimeConstraint = setTransactionMessageLifetimeUsingDurableNonce( NONCE_CONSTRAINT_A, @@ -272,4 +306,12 @@ describe('setTransactionMessageLifetimeUsingDurableNonce', () => { const durableNonceTx = setTransactionMessageLifetimeUsingDurableNonce(NONCE_CONSTRAINT_A, baseTx); expect(durableNonceTx).toBeFrozenObject(); }); + it('freezes the instructions array', () => { + const durableNonceTx = setTransactionMessageLifetimeUsingDurableNonce(NONCE_CONSTRAINT_A, baseTx); + expect(durableNonceTx.instructions).toBeFrozenObject(); + }); + it('freezes the nonce lifetime', () => { + const durableNonceTx = setTransactionMessageLifetimeUsingDurableNonce(NONCE_CONSTRAINT_A, baseTx); + expect(durableNonceTx.lifetimeConstraint).toBeFrozenObject(); + }); }); diff --git a/packages/transaction-messages/src/__tests__/instructions-test.ts b/packages/transaction-messages/src/__tests__/instructions-test.ts index 7195ede27298..d7a2ac04be18 100644 --- a/packages/transaction-messages/src/__tests__/instructions-test.ts +++ b/packages/transaction-messages/src/__tests__/instructions-test.ts @@ -47,6 +47,10 @@ describe('Transaction instruction helpers', () => { const txWithAddedInstruction = appendTransactionMessageInstruction(exampleInstruction, baseTx); expect(txWithAddedInstruction).toBeFrozenObject(); }); + it('freezes the instructions array', () => { + const txWithAddedInstruction = appendTransactionMessageInstruction(exampleInstruction, baseTx); + expect(txWithAddedInstruction.instructions).toBeFrozenObject(); + }); }); describe('appendTransactionMessageInstructions', () => { it('adds the instructions to the end of the list', () => { @@ -67,6 +71,13 @@ describe('Transaction instruction helpers', () => { ); expect(txWithAddedInstruction).toBeFrozenObject(); }); + it('freezes the instructions array', () => { + const txWithAddedInstruction = appendTransactionMessageInstructions( + [exampleInstruction, secondExampleInstruction], + baseTx, + ); + expect(txWithAddedInstruction.instructions).toBeFrozenObject(); + }); }); describe('prependTransactionMessageInstruction', () => { it('adds the instruction to the beginning of the list', () => { @@ -77,6 +88,10 @@ describe('Transaction instruction helpers', () => { const txWithAddedInstruction = prependTransactionMessageInstruction(exampleInstruction, baseTx); expect(txWithAddedInstruction).toBeFrozenObject(); }); + it('freezes the instructions array', () => { + const txWithAddedInstruction = prependTransactionMessageInstruction(exampleInstruction, baseTx); + expect(txWithAddedInstruction.instructions).toBeFrozenObject(); + }); }); describe('prependTransactionMessageInstructions', () => { it('adds the instructions to the beginning of the list', () => { @@ -97,5 +112,12 @@ describe('Transaction instruction helpers', () => { ); expect(txWithAddedInstruction).toBeFrozenObject(); }); + it('freezes the instructions array', () => { + const txWithAddedInstruction = prependTransactionMessageInstructions( + [exampleInstruction, secondExampleInstruction], + baseTx, + ); + expect(txWithAddedInstruction.instructions).toBeFrozenObject(); + }); }); }); diff --git a/packages/transaction-messages/src/blockhash.ts b/packages/transaction-messages/src/blockhash.ts index 312bca627d8f..5a4bdaf2515c 100644 --- a/packages/transaction-messages/src/blockhash.ts +++ b/packages/transaction-messages/src/blockhash.ts @@ -64,7 +64,7 @@ export function setTransactionMessageLifetimeUsingBlockhash( } const out = { ...transaction, - lifetimeConstraint: blockhashLifetimeConstraint, + lifetimeConstraint: Object.freeze(blockhashLifetimeConstraint), }; Object.freeze(out); return out; diff --git a/packages/transaction-messages/src/create-transaction-message.ts b/packages/transaction-messages/src/create-transaction-message.ts index 9d6f391c35be..5a242e5b3db4 100644 --- a/packages/transaction-messages/src/create-transaction-message.ts +++ b/packages/transaction-messages/src/create-transaction-message.ts @@ -10,10 +10,8 @@ export function createTransactionMessage( export function createTransactionMessage({ version, }: TransactionConfig): TransactionMessage { - const out: TransactionMessage = { - instructions: [], + return Object.freeze({ + instructions: Object.freeze([]), version, - }; - Object.freeze(out); - return out; + }) as TransactionMessage; } diff --git a/packages/transaction-messages/src/decompile-message.ts b/packages/transaction-messages/src/decompile-message.ts index ba6c0f273ac3..1164f5f1e7fa 100644 --- a/packages/transaction-messages/src/decompile-message.ts +++ b/packages/transaction-messages/src/decompile-message.ts @@ -136,11 +136,11 @@ function convertInstruction( const accounts = instruction.accountIndices?.map(accountIndex => accountMetas[accountIndex]); const { data } = instruction; - return { + return Object.freeze({ programAddress, - ...(accounts && accounts.length ? { accounts } : {}), + ...(accounts && accounts.length ? { accounts: Object.freeze(accounts) } : {}), ...(data && data.length ? { data } : {}), - }; + }); } type LifetimeConstraint = diff --git a/packages/transaction-messages/src/durable-nonce.ts b/packages/transaction-messages/src/durable-nonce.ts index cc0a729953a2..8b438573e2ae 100644 --- a/packages/transaction-messages/src/durable-nonce.ts +++ b/packages/transaction-messages/src/durable-nonce.ts @@ -177,26 +177,24 @@ export function setTransactionMessageLifetimeUsingDurableNonce< } else { // we have a different advance nonce instruction as the first instruction, replace it newInstructions = [ - createAdvanceNonceAccountInstruction(nonceAccountAddress, nonceAuthorityAddress), + Object.freeze(createAdvanceNonceAccountInstruction(nonceAccountAddress, nonceAuthorityAddress)), ...transaction.instructions.slice(1), ]; } } else { // we don't have an existing advance nonce instruction as the first instruction, prepend one newInstructions = [ - createAdvanceNonceAccountInstruction(nonceAccountAddress, nonceAuthorityAddress), + Object.freeze(createAdvanceNonceAccountInstruction(nonceAccountAddress, nonceAuthorityAddress)), ...transaction.instructions, ]; } - const out = { + return Object.freeze({ ...transaction, - instructions: newInstructions, - lifetimeConstraint: { + instructions: Object.freeze(newInstructions), + lifetimeConstraint: Object.freeze({ nonce, - }, - } as TransactionMessageWithDurableNonceLifetime & + }), + }) as TransactionMessageWithDurableNonceLifetime & TTransaction; - Object.freeze(out); - return out; } diff --git a/packages/transaction-messages/src/instructions.ts b/packages/transaction-messages/src/instructions.ts index dde6bf3f6b62..d1ded91f9a40 100644 --- a/packages/transaction-messages/src/instructions.ts +++ b/packages/transaction-messages/src/instructions.ts @@ -11,12 +11,10 @@ export function appendTransactionMessageInstructions, transaction: TTransaction, ): TTransaction { - const out = { + return Object.freeze({ ...transaction, - instructions: [...transaction.instructions, ...instructions], - }; - Object.freeze(out); - return out; + instructions: Object.freeze([...transaction.instructions, ...instructions]), + }); } export function prependTransactionMessageInstruction( @@ -30,10 +28,8 @@ export function prependTransactionMessageInstructions, transaction: TTransaction, ): TTransaction { - const out = { + return Object.freeze({ ...transaction, - instructions: [...instructions, ...transaction.instructions], - }; - Object.freeze(out); - return out; + instructions: Object.freeze([...instructions, ...transaction.instructions]), + }); }