From a26d539a6cd7d4df4e894b83bfc3cedd3c7e7503 Mon Sep 17 00:00:00 2001 From: kewde Date: Wed, 11 Jan 2023 21:08:29 +0100 Subject: [PATCH 1/3] fix: backport https://github.com/solana-labs/solana/pull/23720 --- src/connection.ts | 2 + src/publickey.ts | 4 ++ src/transaction.ts | 85 ++++++++++++++++++++++++++++++++++++++++ test/transaction.test.ts | 13 ++++-- 4 files changed, 100 insertions(+), 4 deletions(-) diff --git a/src/connection.ts b/src/connection.ts index c823b7685c9..73de684e3c2 100644 --- a/src/connection.ts +++ b/src/connection.ts @@ -3518,6 +3518,8 @@ export class Connection { transaction = transactionOrMessage; } else { transaction = Transaction.populate(transactionOrMessage); + // HACK: this function relies on mutating the populated transaction + transaction._message = transaction._json = undefined; } if (transaction.nonceInfo && signers) { diff --git a/src/publickey.ts b/src/publickey.ts index a92f3d06c4f..9a23b4cd6f5 100644 --- a/src/publickey.ts +++ b/src/publickey.ts @@ -87,6 +87,10 @@ export class PublicKey extends Struct { return bs58.encode(this.toBytes()); } + toJSON(): string { + return this.toBase58(); + } + /** * Return the byte array representation of the public key */ diff --git a/src/transaction.ts b/src/transaction.ts index 5466464f5a9..081173934d3 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -65,6 +65,19 @@ export type SerializeConfig = { verifySignatures?: boolean; }; +/** + * @internal + */ +export interface TransactionInstructionJSON { + keys: { + pubkey: string; + isSigner: boolean; + isWritable: boolean; + }[]; + programId: string; + data: number[]; +} + /** * Transaction Instruction class */ @@ -92,6 +105,21 @@ export class TransactionInstruction { this.data = opts.data; } } + + /** + * @internal + */ + toJSON(): TransactionInstructionJSON { + return { + keys: this.keys.map(({pubkey, isSigner, isWritable}) => ({ + pubkey: pubkey.toJSON(), + isSigner, + isWritable, + })), + programId: this.programId.toJSON(), + data: [...this.data], + }; + } } /** @@ -127,6 +155,20 @@ export type NonceInformation = { nonceInstruction: TransactionInstruction; }; +/** + * @internal + */ +export interface TransactionJSON { + recentBlockhash: string | null; + feePayer: string | null; + nonceInfo: { + nonce: string; + nonceInstruction: TransactionInstructionJSON; + } | null; + instructions: TransactionInstructionJSON[]; + signatures: {publicKey: string; signature: number[] | null}[]; +} + /** * Transaction class */ @@ -168,6 +210,16 @@ export class Transaction { */ nonceInfo?: NonceInformation; + /** + * @internal + */ + _message?: Message; + + /** + * @internal + */ + _json?: TransactionJSON; + /** * Construct an empty Transaction */ @@ -175,6 +227,27 @@ export class Transaction { opts && Object.assign(this, opts); } + /** + * @internal + */ + toJSON(): TransactionJSON { + return { + recentBlockhash: this.recentBlockhash || null, + feePayer: this.feePayer ? this.feePayer.toJSON() : null, + nonceInfo: this.nonceInfo + ? { + nonce: this.nonceInfo.nonce, + nonceInstruction: this.nonceInfo.nonceInstruction.toJSON(), + } + : null, + instructions: this.instructions.map(instruction => instruction.toJSON()), + signatures: this.signatures.map(({publicKey, signature}) => ({ + publicKey: publicKey.toJSON(), + signature: signature ? [...signature] : null, + })), + }; + } + /** * Add one or more instructions to this Transaction */ @@ -203,6 +276,15 @@ export class Transaction { * Compile transaction data */ compileMessage(): Message { + if (this._message) { + if (JSON.stringify(this.toJSON()) !== JSON.stringify(this._json)) { + throw new Error( + 'Transaction mutated after being populated from Message', + ); + } + return this._message; + } + const {nonceInfo} = this; if (nonceInfo && this.instructions[0] != nonceInfo.nonceInstruction) { this.recentBlockhash = nonceInfo.nonce; @@ -708,6 +790,9 @@ export class Transaction { ); }); + transaction._message = message; + transaction._json = transaction.toJSON(); + return transaction; } } diff --git a/test/transaction.test.ts b/test/transaction.test.ts index 77d78805bab..3e633dda749 100644 --- a/test/transaction.test.ts +++ b/test/transaction.test.ts @@ -341,14 +341,14 @@ describe('Transaction', () => { }).add(transfer); expectedTransaction.sign(sender); - const wireTransaction = Buffer.from( + const serializedTransaction = Buffer.from( 'AVuErQHaXv0SG0/PchunfxHKt8wMRfMZzqV0tkC5qO6owYxWU2v871AoWywGoFQr4z+q/7mE8lIufNl/kxj+nQ0BAAEDE5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwCAAAAMQAAAAAAAAA=', 'base64', ); - const tx = Transaction.from(wireTransaction); + const deserializedTransaction = Transaction.from(serializedTransaction); - expect(tx).to.eql(expectedTransaction); - expect(wireTransaction).to.eql(expectedTransaction.serialize()); + expect(expectedTransaction.serialize()).to.eql(serializedTransaction); + expect(deserializedTransaction.serialize()).to.eql(serializedTransaction); }); it('populate transaction', () => { @@ -385,6 +385,11 @@ describe('Transaction', () => { expect(transaction.instructions).to.have.length(1); expect(transaction.signatures).to.have.length(2); expect(transaction.recentBlockhash).to.eq(recentBlockhash); + + transaction.feePayer = new PublicKey(6); + expect(() => transaction.compileMessage()).to.throw( + 'Transaction mutated after being populated from Message', + ); }); it('serialize unsigned transaction', () => { From 58a06bb22183d4fb30423cc63b3f4227d3c52bf3 Mon Sep 17 00:00:00 2001 From: kewde Date: Wed, 11 Jan 2023 21:13:17 +0100 Subject: [PATCH 2/3] fix: backport https://github.com/solana-labs/solana/pull/24475 --- src/transaction.ts | 11 +++-- test/transaction.test.ts | 86 +++++++++++++++++++++++++++++++++++++++- 2 files changed, 89 insertions(+), 8 deletions(-) diff --git a/src/transaction.ts b/src/transaction.ts index 081173934d3..f14ce3991b9 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -166,7 +166,7 @@ export interface TransactionJSON { nonceInstruction: TransactionInstructionJSON; } | null; instructions: TransactionInstructionJSON[]; - signatures: {publicKey: string; signature: number[] | null}[]; + signers: string[]; } /** @@ -241,10 +241,9 @@ export class Transaction { } : null, instructions: this.instructions.map(instruction => instruction.toJSON()), - signatures: this.signatures.map(({publicKey, signature}) => ({ - publicKey: publicKey.toJSON(), - signature: signature ? [...signature] : null, - })), + signers: this.signatures.map(({publicKey}) => { + return publicKey.toJSON(); + }), }; } @@ -279,7 +278,7 @@ export class Transaction { if (this._message) { if (JSON.stringify(this.toJSON()) !== JSON.stringify(this._json)) { throw new Error( - 'Transaction mutated after being populated from Message', + 'Transaction message mutated after being populated from Message', ); } return this._message; diff --git a/test/transaction.test.ts b/test/transaction.test.ts index 3e633dda749..762e097d19f 100644 --- a/test/transaction.test.ts +++ b/test/transaction.test.ts @@ -5,7 +5,7 @@ import {expect} from 'chai'; import {Keypair} from '../src/keypair'; import {PublicKey} from '../src/publickey'; -import {Transaction} from '../src/transaction'; +import {Transaction, TransactionInstruction} from '../src/transaction'; import {StakeProgram} from '../src/stake-program'; import {SystemProgram} from '../src/system-program'; import {Message} from '../src/message'; @@ -388,7 +388,7 @@ describe('Transaction', () => { transaction.feePayer = new PublicKey(6); expect(() => transaction.compileMessage()).to.throw( - 'Transaction mutated after being populated from Message', + 'Transaction message mutated after being populated from Message', ); }); @@ -510,4 +510,86 @@ describe('Transaction', () => { tx.addSignature(from.publicKey, toBuffer(signature)); expect(tx.verifySignatures()).to.be.true; }); + + it('can serialize, deserialize, and reserialize with a partial signer', () => { + const signer = Keypair.generate(); + const acc0Writable = Keypair.generate(); + const acc1Writable = Keypair.generate(); + const acc2Writable = Keypair.generate(); + const t0 = new Transaction({ + recentBlockhash: 'HZaTsZuhN1aaz9WuuimCFMyH7wJ5xiyMUHFCnZSMyguH', + feePayer: signer.publicKey, + }); + t0.add( + new TransactionInstruction({ + keys: [ + { + pubkey: signer.publicKey, + isWritable: true, + isSigner: true, + }, + { + pubkey: acc0Writable.publicKey, + isWritable: true, + isSigner: false, + }, + ], + programId: Keypair.generate().publicKey, + }), + ); + t0.add( + new TransactionInstruction({ + keys: [ + { + pubkey: acc1Writable.publicKey, + isWritable: false, + isSigner: false, + }, + ], + programId: Keypair.generate().publicKey, + }), + ); + t0.add( + new TransactionInstruction({ + keys: [ + { + pubkey: acc2Writable.publicKey, + isWritable: true, + isSigner: false, + }, + ], + programId: Keypair.generate().publicKey, + }), + ); + t0.add( + new TransactionInstruction({ + keys: [ + { + pubkey: signer.publicKey, + isWritable: true, + isSigner: true, + }, + { + pubkey: acc0Writable.publicKey, + isWritable: false, + isSigner: false, + }, + { + pubkey: acc2Writable.publicKey, + isWritable: false, + isSigner: false, + }, + { + pubkey: acc1Writable.publicKey, + isWritable: true, + isSigner: false, + }, + ], + programId: Keypair.generate().publicKey, + }), + ); + const t1 = Transaction.from(t0.serialize({requireAllSignatures: false})); + t1.partialSign(signer); + t1.serialize(); + }); }); From 98637fe9aef9017876baeebac299d8be1a023fc2 Mon Sep 17 00:00:00 2001 From: kewde Date: Wed, 11 Jan 2023 21:16:11 +0100 Subject: [PATCH 3/3] fix: backport https://github.com/solana-labs/solana/pull/25141 --- src/transaction.ts | 10 ++++---- test/transaction.test.ts | 51 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 51 insertions(+), 10 deletions(-) diff --git a/src/transaction.ts b/src/transaction.ts index f14ce3991b9..0ff94188bea 100644 --- a/src/transaction.ts +++ b/src/transaction.ts @@ -275,12 +275,10 @@ export class Transaction { * Compile transaction data */ compileMessage(): Message { - if (this._message) { - if (JSON.stringify(this.toJSON()) !== JSON.stringify(this._json)) { - throw new Error( - 'Transaction message mutated after being populated from Message', - ); - } + if ( + this._message && + JSON.stringify(this.toJSON()) === JSON.stringify(this._json) + ) { return this._message; } diff --git a/test/transaction.test.ts b/test/transaction.test.ts index 762e097d19f..af803b11f1c 100644 --- a/test/transaction.test.ts +++ b/test/transaction.test.ts @@ -385,11 +385,54 @@ describe('Transaction', () => { expect(transaction.instructions).to.have.length(1); expect(transaction.signatures).to.have.length(2); expect(transaction.recentBlockhash).to.eq(recentBlockhash); + }); - transaction.feePayer = new PublicKey(6); - expect(() => transaction.compileMessage()).to.throw( - 'Transaction message mutated after being populated from Message', - ); + it('populate then compile transaction', () => { + const recentBlockhash = new PublicKey(1).toString(); + const message = new Message({ + accountKeys: [ + new PublicKey(1).toString(), + new PublicKey(2).toString(), + new PublicKey(3).toString(), + new PublicKey(4).toString(), + new PublicKey(5).toString(), + ], + header: { + numReadonlySignedAccounts: 0, + numReadonlyUnsignedAccounts: 3, + numRequiredSignatures: 2, + }, + instructions: [ + { + accounts: [1, 2, 3], + data: bs58.encode(Buffer.alloc(5).fill(9)), + programIdIndex: 2, + }, + ], + recentBlockhash, + }); + + const signatures = [ + bs58.encode(Buffer.alloc(64).fill(1)), + bs58.encode(Buffer.alloc(64).fill(2)), + ]; + + const transaction = Transaction.populate(message, signatures); + const compiledMessage = transaction.compileMessage(); + expect(compiledMessage).to.eql(message); + + // show that without caching the message, the populated message + // might not be the same when re-compiled + transaction._message = undefined; + const compiledMessage2 = transaction.compileMessage(); + expect(compiledMessage2).not.to.eql(message); + + // show that even if message is cached, transaction may still + // be modified + transaction._message = message; + transaction.recentBlockhash = new PublicKey(100).toString(); + const compiledMessage3 = transaction.compileMessage(); + expect(compiledMessage3).not.to.eql(message); }); it('serialize unsigned transaction', () => {