From 6da02f1da178b2b165582d2489c88ac5248a3ef3 Mon Sep 17 00:00:00 2001 From: Nicolas Schapeler Date: Tue, 20 Dec 2022 03:01:46 +0100 Subject: [PATCH] feat: add tests for partially signed tx verification --- web3.js/test/transaction.test.ts | 197 +++++++++++++++++++++---------- 1 file changed, 135 insertions(+), 62 deletions(-) diff --git a/web3.js/test/transaction.test.ts b/web3.js/test/transaction.test.ts index d9639a028b1d67..6d653e0ed6c04e 100644 --- a/web3.js/test/transaction.test.ts +++ b/web3.js/test/transaction.test.ts @@ -1,23 +1,23 @@ import bs58 from 'bs58'; -import {Buffer} from 'buffer'; -import {expect} from 'chai'; +import { Buffer } from 'buffer'; +import { expect } from 'chai'; -import {Connection} from '../src/connection'; -import {Keypair} from '../src/keypair'; -import {PublicKey} from '../src/publickey'; +import { Connection } from '../src/connection'; +import { Keypair } from '../src/keypair'; +import { PublicKey } from '../src/publickey'; import { Transaction, TransactionInstruction, TransactionMessage, VersionedTransaction, } from '../src/transaction'; -import {StakeProgram, SystemProgram} from '../src/programs'; -import {Message} from '../src/message'; +import { StakeProgram, SystemProgram } from '../src/programs'; +import { Message } from '../src/message'; import invariant from '../src/utils/assert'; -import {toBuffer} from '../src/utils/to-buffer'; -import {helpers} from './mocks/rpc-http'; -import {url} from './url'; -import {sign} from '../src/utils/ed25519'; +import { toBuffer } from '../src/utils/to-buffer'; +import { helpers } from './mocks/rpc-http'; +import { url } from './url'; +import { sign } from '../src/utils/ed25519'; describe('Transaction', () => { describe('compileMessage', () => { @@ -61,19 +61,19 @@ describe('Transaction', () => { }).add({ keys: [ // Regular accounts - {pubkey: accountRegular9, isSigner: false, isWritable: false}, - {pubkey: accountRegular8, isSigner: false, isWritable: false}, + { pubkey: accountRegular9, isSigner: false, isWritable: false }, + { pubkey: accountRegular8, isSigner: false, isWritable: false }, // Writable accounts - {pubkey: accountWritable7, isSigner: false, isWritable: true}, - {pubkey: accountWritable6, isSigner: false, isWritable: true}, + { pubkey: accountWritable7, isSigner: false, isWritable: true }, + { pubkey: accountWritable6, isSigner: false, isWritable: true }, // Signers - {pubkey: accountSigner5, isSigner: true, isWritable: false}, - {pubkey: accountSigner4, isSigner: true, isWritable: false}, + { pubkey: accountSigner5, isSigner: true, isWritable: false }, + { pubkey: accountSigner4, isSigner: true, isWritable: false }, // Writable Signers - {pubkey: accountWritableSigner3, isSigner: true, isWritable: true}, - {pubkey: accountWritableSigner2, isSigner: true, isWritable: true}, + { pubkey: accountWritableSigner3, isSigner: true, isWritable: true }, + { pubkey: accountWritableSigner2, isSigner: true, isWritable: true }, // Payer. - {pubkey: payer, isSigner: true, isWritable: true}, + { pubkey: payer, isSigner: true, isWritable: true }, ], programId, }); @@ -126,19 +126,19 @@ describe('Transaction', () => { }).add({ keys: [ // Should sort last. - {pubkey: account5, isSigner: false, isWritable: false}, - {pubkey: account5, isSigner: false, isWritable: false}, + { pubkey: account5, isSigner: false, isWritable: false }, + { pubkey: account5, isSigner: false, isWritable: false }, // Should be considered writeable. - {pubkey: account4, isSigner: false, isWritable: false}, - {pubkey: account4, isSigner: false, isWritable: true}, + { pubkey: account4, isSigner: false, isWritable: false }, + { pubkey: account4, isSigner: false, isWritable: true }, // Should be considered a signer. - {pubkey: account3, isSigner: false, isWritable: false}, - {pubkey: account3, isSigner: true, isWritable: false}, + { pubkey: account3, isSigner: false, isWritable: false }, + { pubkey: account3, isSigner: true, isWritable: false }, // Should be considered a writable signer. - {pubkey: account2, isSigner: false, isWritable: true}, - {pubkey: account2, isSigner: true, isWritable: false}, + { pubkey: account2, isSigner: false, isWritable: true }, + { pubkey: account2, isSigner: true, isWritable: false }, // Payer. - {pubkey: payer, isSigner: true, isWritable: true}, + { pubkey: payer, isSigner: true, isWritable: true }, ], programId, }); @@ -169,8 +169,8 @@ describe('Transaction', () => { lastValidBlockHeight: 9999, }).add({ keys: [ - {pubkey: other.publicKey, isSigner: true, isWritable: true}, - {pubkey: payer.publicKey, isSigner: true, isWritable: true}, + { pubkey: other.publicKey, isSigner: true, isWritable: true }, + { pubkey: payer.publicKey, isSigner: true, isWritable: true }, ], programId, }); @@ -223,7 +223,7 @@ describe('Transaction', () => { blockhash: recentBlockhash, lastValidBlockHeight: 9999, }).add({ - keys: [{pubkey: payer.publicKey, isSigner: true, isWritable: false}], + keys: [{ pubkey: payer.publicKey, isSigner: true, isWritable: false }], programId, }); @@ -358,7 +358,7 @@ describe('Transaction', () => { const accountFrom = Keypair.generate(); const accountTo = Keypair.generate(); - const latestBlockhash = await helpers.latestBlockhash({connection}); + const latestBlockhash = await helpers.latestBlockhash({ connection }); const transaction = new Transaction({ feePayer: accountFrom.publicKey, @@ -406,7 +406,7 @@ describe('Transaction', () => { expect(() => partialTransaction.serialize()).to.throw(); expect(() => - partialTransaction.serialize({requireAllSignatures: false}), + partialTransaction.serialize({ requireAllSignatures: false }), ).not.to.throw(); partialTransaction.partialSign(account2); @@ -421,7 +421,7 @@ describe('Transaction', () => { invariant(partialTransaction.signatures[0].signature); partialTransaction.signatures[0].signature.fill(1); expect(() => - partialTransaction.serialize({requireAllSignatures: false}), + partialTransaction.serialize({ requireAllSignatures: false }), ).to.throw(); expect(() => partialTransaction.serialize({ @@ -444,9 +444,9 @@ describe('Transaction', () => { lastValidBlockHeight: 9999, }).add({ keys: [ - {pubkey: duplicate1.publicKey, isSigner: true, isWritable: true}, - {pubkey: payer.publicKey, isSigner: false, isWritable: true}, - {pubkey: duplicate2.publicKey, isSigner: true, isWritable: false}, + { pubkey: duplicate1.publicKey, isSigner: true, isWritable: true }, + { pubkey: payer.publicKey, isSigner: false, isWritable: true }, + { pubkey: duplicate2.publicKey, isSigner: true, isWritable: false }, ], programId, }); @@ -475,9 +475,9 @@ describe('Transaction', () => { lastValidBlockHeight: 9999, }).add({ keys: [ - {pubkey: duplicate1.publicKey, isSigner: true, isWritable: true}, - {pubkey: payer.publicKey, isSigner: false, isWritable: true}, - {pubkey: duplicate2.publicKey, isSigner: true, isWritable: false}, + { pubkey: duplicate1.publicKey, isSigner: true, isWritable: true }, + { pubkey: payer.publicKey, isSigner: false, isWritable: true }, + { pubkey: duplicate2.publicKey, isSigner: true, isWritable: false }, ], programId, }); @@ -566,7 +566,7 @@ describe('Transaction', () => { }), }; - const transferTransaction = new Transaction({nonceInfo}).add( + const transferTransaction = new Transaction({ nonceInfo }).add( SystemProgram.transfer({ fromPubkey: account1.publicKey, toPubkey: account2.publicKey, @@ -580,7 +580,7 @@ describe('Transaction', () => { const stakeAccount = Keypair.generate(); const voteAccount = Keypair.generate(); - const stakeTransaction = new Transaction({nonceInfo}).add( + const stakeTransaction = new Transaction({ nonceInfo }).add( StakeProgram.delegate({ stakePubkey: stakeAccount.publicKey, authorizedPubkey: account1.publicKey, @@ -715,7 +715,7 @@ describe('Transaction', () => { authorizedPubkey: nonceAuthority, }), }; - const transaction = new Transaction({nonceInfo}); + const transaction = new Transaction({ nonceInfo }); expect(transaction.recentBlockhash).to.be.undefined; expect(transaction.lastValidBlockHeight).to.be.undefined; expect(transaction.nonceInfo).to.equal(nonceInfo); @@ -785,7 +785,7 @@ describe('Transaction', () => { expectedTransaction.serialize(); }).to.throw('Transaction fee payer required'); expect(() => { - expectedTransaction.serialize({verifySignatures: false}); + expectedTransaction.serialize({ verifySignatures: false }); }).to.throw('Transaction fee payer required'); expect(() => { expectedTransaction.serializeMessage(); @@ -799,7 +799,7 @@ describe('Transaction', () => { }).to.throw('Signature verification failed'); // Serializing without signatures is allowed if sigverify disabled. - expectedTransaction.serialize({verifySignatures: false}); + expectedTransaction.serialize({ verifySignatures: false }); // Serializing the message is allowed when signature array has null signatures expectedTransaction.serializeMessage(); @@ -814,20 +814,20 @@ describe('Transaction', () => { }).to.throw('Signature verification failed'); // Serializing without signatures is allowed if sigverify disabled. - expectedTransaction.serialize({verifySignatures: false}); + expectedTransaction.serialize({ verifySignatures: false }); // Serializing the message is allowed when signature array has null signatures expectedTransaction.serializeMessage(); const expectedSerializationWithNoSignatures = Buffer.from( 'AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAAAABAAEDE5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9' + - 'Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAAAAAAAAAAAAAA' + - 'AAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwC' + - 'AAAAMQAAAAAAAAA=', + 'AAAAAAAAAAAAAAAAAAABAAEDE5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9' + + 'Q5/Mtmcn8onFxt47xKj+XdXXd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAAAAAAAAAAAAAA' + + 'AAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0ROug7bEsbx0xxuDkqEvwUusBAgIAAQwC' + + 'AAAAMQAAAAAAAAA=', 'base64', ); - expect(expectedTransaction.serialize({requireAllSignatures: false})).to.eql( + expect(expectedTransaction.serialize({ requireAllSignatures: false })).to.eql( expectedSerializationWithNoSignatures, ); @@ -836,15 +836,88 @@ describe('Transaction', () => { expect(expectedTransaction.signatures).to.have.length(1); const expectedSerialization = Buffer.from( 'AVuErQHaXv0SG0/PchunfxHKt8wMRfMZzqV0tkC5qO6owYxWU2v871AoWywGoFQr4z+q/7mE8lIufNl/' + - 'kxj+nQ0BAAEDE5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9Q5/Mtmcn8onFxt47xKj+XdXX' + - 'd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0' + - 'ROug7bEsbx0xxuDkqEvwUusBAgIAAQwCAAAAMQAAAAAAAAA=', + 'kxj+nQ0BAAEDE5j2LG0aRXxRumpLXz29L2n8qTIWIY3ImX5Ba9F9k8r9Q5/Mtmcn8onFxt47xKj+XdXX' + + 'd3C8j/FcPu7csUrz/AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAxJrndgN4IFTxep3s6kO0' + + 'ROug7bEsbx0xxuDkqEvwUusBAgIAAQwCAAAAMQAAAAAAAAA=', 'base64', ); expect(expectedTransaction.serialize()).to.eql(expectedSerialization); expect(expectedTransaction.signatures).to.have.length(1); }); + it('partially signed transaction', () => { + 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, + }); + const expectedTransaction = new Transaction({ + blockhash: recentBlockhash, + lastValidBlockHeight: 9999, + }).add(transfer); + + // To have 2 required signers we add a feepayer + expectedTransaction.feePayer = feePayer.publicKey; + + 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; + + // 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; + + // Add all required sigs + 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; + + // 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); @@ -961,7 +1034,7 @@ describe('Transaction', () => { programId: Keypair.generate().publicKey, }), ); - const t1 = Transaction.from(t0.serialize({requireAllSignatures: false})); + const t1 = Transaction.from(t0.serialize({ requireAllSignatures: false })); t1.partialSign(signer); t1.serialize(); }); @@ -971,12 +1044,12 @@ describe('VersionedTransaction', () => { it('deserializes versioned transactions', () => { const serializedVersionedTx = Buffer.from( 'AdTIDASR42TgVuXKkd7mJKk373J3LPVp85eyKMVcrboo9KTY8/vm6N/Cv0NiHqk2I8iYw6VX5ZaBKG8z' + - '9l1XjwiAAQACA+6qNbqfjaIENwt9GzEK/ENiB/ijGwluzBUmQ9xlTAMcCaS0ctnyxTcXXlJr7u2qtnaM' + - 'gIAO2/c7RBD0ipHWUcEDBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAJbI7VNs6MzREUlnzRaJ' + - 'pBKP8QQoDn2dWQvD0KIgHFDiAwIACQAgoQcAAAAAAAIABQEAAAQAATYPBwAKBDIBAyQWIw0oCxIdCA4i' + - 'JzQRKwUZHxceHCohMBUJJiwpMxAaGC0TLhQxGyAMBiU2NS8VDgAAAADuAgAAAAAAAAIAAAAAAAAAAdGCT' + - 'Qiq5yw3+3m1sPoRNj0GtUNNs0FIMocxzt3zuoSZHQABAwQFBwgLDA8RFBcYGhwdHh8iIyUnKiwtLi8yF' + - 'wIGCQoNDhASExUWGRsgISQmKCkrMDEz', + '9l1XjwiAAQACA+6qNbqfjaIENwt9GzEK/ENiB/ijGwluzBUmQ9xlTAMcCaS0ctnyxTcXXlJr7u2qtnaM' + + 'gIAO2/c7RBD0ipHWUcEDBkZv5SEXMv/srbpyw5vnvIzlu8X3EmssQ5s6QAAAAJbI7VNs6MzREUlnzRaJ' + + 'pBKP8QQoDn2dWQvD0KIgHFDiAwIACQAgoQcAAAAAAAIABQEAAAQAATYPBwAKBDIBAyQWIw0oCxIdCA4i' + + 'JzQRKwUZHxceHCohMBUJJiwpMxAaGC0TLhQxGyAMBiU2NS8VDgAAAADuAgAAAAAAAAIAAAAAAAAAAdGCT' + + 'Qiq5yw3+3m1sPoRNj0GtUNNs0FIMocxzt3zuoSZHQABAwQFBwgLDA8RFBcYGhwdHh8iIyUnKiwtLi8yF' + + 'wIGCQoNDhASExUWGRsgISQmKCkrMDEz', 'base64', );