diff --git a/web3.js/src/transaction/legacy.ts b/web3.js/src/transaction/legacy.ts index f8871fa0678297..998d6a568d475b 100644 --- a/web3.js/src/transaction/legacy.ts +++ b/web3.js/src/transaction/legacy.ts @@ -726,10 +726,15 @@ export class Transaction { } /** - * Verify signatures of a complete, signed Transaction - */ - verifySignatures(): boolean { - return this._verifySignatures(this.serializeMessage(), true); + * Verify signatures of a Transaction + * Optional parameter specifies if we're expecting a fully signed Transaction or a partially signed one. + * If no boolean is provided, we expect a fully signed Transaction by default. + */ + verifySignatures(requireAllSignatures?: boolean): boolean { + return this._verifySignatures( + this.serializeMessage(), + requireAllSignatures === undefined ? true : requireAllSignatures, + ); } /** diff --git a/web3.js/test/transaction.test.ts b/web3.js/test/transaction.test.ts index d9639a028b1d67..cd9574bb2d8f83 100644 --- a/web3.js/test/transaction.test.ts +++ b/web3.js/test/transaction.test.ts @@ -845,6 +845,127 @@ describe('Transaction', () => { expect(expectedTransaction.signatures).to.have.length(1); }); + describe('partially signed transaction signature verification tests', () => { + const sender = Keypair.fromSeed(Uint8Array.from(Array(32).fill(8))); // Arbitrary known account + const feePayer = Keypair.fromSeed(Uint8Array.from(Array(32).fill(9))); // Arbitrary known account + const fakeKey = Keypair.fromSeed(Uint8Array.from(Array(32).fill(10))); // Arbitrary known account + const recentBlockhash = 'EETubP5AKHgjPAhzPAFcb8BAY1hMH639CWCFTqi3hq1k'; // Arbitrary known recentBlockhash + const recipient = new PublicKey( + 'J3dxNj7nDRRqRRXuEMynDG57DkZK4jYRuv3Garmb1i99', + ); // Arbitrary known public key + const transfer = SystemProgram.transfer({ + fromPubkey: sender.publicKey, + toPubkey: recipient, + lamports: 49, + }); + let expectedTransaction: Transaction; + beforeEach(() => { + expectedTransaction = new Transaction({ + blockhash: recentBlockhash, + lastValidBlockHeight: 9999, + }).add(transfer); + // To have 2 required signers we add a feepayer + expectedTransaction.feePayer = feePayer.publicKey; + }); + + it('verifies for no sigs', () => { + expect(expectedTransaction.signatures).to.have.length(0); + + // No extra param should require all sigs, should be false for no sigs + expect(expectedTransaction.verifySignatures()).to.be.false; + + // True should require all sigs, should be false for no sigs + expect(expectedTransaction.verifySignatures(true)).to.be.false; + + // False should verify only the available sigs, should be true for no sigs + expect(expectedTransaction.verifySignatures(false)).to.be.true; + }); + + it('verifies for one sig', () => { + // Add one required sig + expectedTransaction.partialSign(sender); + + expect( + expectedTransaction.signatures.filter(sig => sig.signature !== null), + ).to.have.length(1); + + // No extra param should require all sigs, should be false for one missing sig + expect(expectedTransaction.verifySignatures()).to.be.false; + + // True should require all sigs, should be false one missing sigs + expect(expectedTransaction.verifySignatures(true)).to.be.false; + + // False should verify only the available sigs, should be true one valid sig + expect(expectedTransaction.verifySignatures(false)).to.be.true; + }); + + it('verifies for all sigs', () => { + // Add all required sigs + expectedTransaction.partialSign(sender); + expectedTransaction.partialSign(feePayer); + + expect( + expectedTransaction.signatures.filter(sig => sig.signature !== null), + ).to.have.length(2); + + // No extra param should require all sigs, should be true for no missing sig + expect(expectedTransaction.verifySignatures()).to.be.true; + + // True should require all sigs, should be true for no missing sig + expect(expectedTransaction.verifySignatures(true)).to.be.true; + + // False should verify only the available sigs, should be true for no missing sig + expect(expectedTransaction.verifySignatures(false)).to.be.true; + }); + + it('throws for wrong sig with only one sig present', () => { + // Add one required sigs + expectedTransaction.partialSign(feePayer); + + // Add a wrong signature + expectedTransaction.signatures[0].publicKey = fakeKey.publicKey; + + // No extra param should require all sigs, should throw for wrong sig + expect(() => expectedTransaction.verifySignatures()).to.throw( + 'unknown signer: ' + fakeKey.publicKey.toBase58(), + ); + + // True should require all sigs, should throw for wrong sig + expect(() => expectedTransaction.verifySignatures(true)).to.throw( + 'unknown signer: ' + fakeKey.publicKey.toBase58(), + ); + + // False should verify only the available sigs, should throw for wrong sig + expect(() => expectedTransaction.verifySignatures(false)).to.throw( + 'unknown signer: ' + fakeKey.publicKey.toBase58(), + ); + }); + + it('throws for wrong sig with all sigs present', () => { + // Add all required sigs + expectedTransaction.partialSign(sender); + expectedTransaction.partialSign(feePayer); + + // Add a wrong signature + expectedTransaction.signatures[0].publicKey = fakeKey.publicKey; + + // No extra param should require all sigs, should throw for wrong sig + expect(() => expectedTransaction.verifySignatures()).to.throw( + 'unknown signer: ' + fakeKey.publicKey.toBase58(), + ); + + // True should require all sigs, should throw for wrong sig + expect(() => expectedTransaction.verifySignatures(true)).to.throw( + 'unknown signer: ' + fakeKey.publicKey.toBase58(), + ); + + // False should verify only the available sigs, should throw for wrong sig + expect(() => expectedTransaction.verifySignatures(false)).to.throw( + 'unknown signer: ' + fakeKey.publicKey.toBase58(), + ); + }); + }); + it('deprecated - externally signed stake delegate', () => { const authority = Keypair.fromSeed(Uint8Array.from(Array(32).fill(1))); const stake = new PublicKey(2);