fix: support serialization of partially signed transactions
This commit is contained in:
		
				
					committed by
					
						![mergify[bot]](/avatar/e3df20cd7a67969c41a65f03bea54961?size=40) mergify[bot]
						mergify[bot]
					
				
			
			
				
	
			
			
			
						parent
						
							4bb6c2fffb
						
					
				
				
					commit
					a59d305e09
				
			
							
								
								
									
										7
									
								
								web3.js/module.d.ts
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										7
									
								
								web3.js/module.d.ts
									
									
									
									
										vendored
									
									
								
							| @@ -617,6 +617,11 @@ declare module '@solana/web3.js' { | ||||
|     signatures?: Array<SignaturePubkeyPair>; | ||||
|   }; | ||||
|  | ||||
|   export type SerializeConfig = { | ||||
|     requireAllSignatures?: boolean; | ||||
|     verifySignatures?: boolean; | ||||
|   }; | ||||
|  | ||||
|   export class Transaction { | ||||
|     signatures: Array<SignaturePubkeyPair>; | ||||
|     signature?: Buffer; | ||||
| @@ -640,7 +645,7 @@ declare module '@solana/web3.js' { | ||||
|     addSignature(pubkey: PublicKey, signature: Buffer): void; | ||||
|     setSigners(...signer: Array<PublicKey>): void; | ||||
|     verifySignatures(): boolean; | ||||
|     serialize(): Buffer; | ||||
|     serialize(config?: SerializeConfig): Buffer; | ||||
|   } | ||||
|  | ||||
|   // === src/stake-program.js === | ||||
|   | ||||
| @@ -621,6 +621,11 @@ declare module '@solana/web3.js' { | ||||
|     signatures?: Array<SignaturePubkeyPair>, | ||||
|   |}; | ||||
|  | ||||
|   declare export type SerializeConfig = { | ||||
|     requireAllSignatures?: boolean, | ||||
|     verifySignatures?: boolean, | ||||
|   }; | ||||
|  | ||||
|   declare export class Transaction { | ||||
|     signatures: Array<SignaturePubkeyPair>; | ||||
|     signature: ?Buffer; | ||||
| @@ -644,7 +649,7 @@ declare module '@solana/web3.js' { | ||||
|     addSignature(pubkey: PublicKey, signature: Buffer): void; | ||||
|     setSigners(...signers: Array<PublicKey>): void; | ||||
|     verifySignatures(): boolean; | ||||
|     serialize(): Buffer; | ||||
|     serialize(config?: SerializeConfig): Buffer; | ||||
|   } | ||||
|  | ||||
|   // === src/stake-program.js === | ||||
|   | ||||
| @@ -62,6 +62,18 @@ export type TransactionInstructionCtorFields = {| | ||||
|   data?: Buffer, | ||||
| |}; | ||||
|  | ||||
| /** | ||||
|  * Configuration object for Transaction.serialize() | ||||
|  * | ||||
|  * @typedef {Object} SerializeConfig | ||||
|  * @property {boolean|undefined} requireAllSignatures Require all transaction signatures be present (default: true) | ||||
|  * @property {boolean|undefined} verifySignatures Verify provided signatures (default: true) | ||||
|  */ | ||||
| export type SerializeConfig = { | ||||
|   requireAllSignatures?: boolean, | ||||
|   verifySignatures?: boolean, | ||||
| }; | ||||
|  | ||||
| /** | ||||
|  * Transaction Instruction class | ||||
|  */ | ||||
| @@ -462,37 +474,49 @@ export class Transaction { | ||||
|    * Verify signatures of a complete, signed Transaction | ||||
|    */ | ||||
|   verifySignatures(): boolean { | ||||
|     return this._verifySignatures(this.serializeMessage()); | ||||
|     return this._verifySignatures(this.serializeMessage(), true); | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * @private | ||||
|    */ | ||||
|   _verifySignatures(signData: Buffer): boolean { | ||||
|     let verified = true; | ||||
|   _verifySignatures(signData: Buffer, requireAllSignatures: boolean): boolean { | ||||
|     for (const {signature, publicKey} of this.signatures) { | ||||
|       if ( | ||||
|         !nacl.sign.detached.verify(signData, signature, publicKey.toBuffer()) | ||||
|       ) { | ||||
|         verified = false; | ||||
|       if (signature === null) { | ||||
|         if (requireAllSignatures) { | ||||
|           return false; | ||||
|         } | ||||
|       } else { | ||||
|         if ( | ||||
|           !nacl.sign.detached.verify(signData, signature, publicKey.toBuffer()) | ||||
|         ) { | ||||
|           return false; | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     return verified; | ||||
|     return true; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Serialize the Transaction in the wire format. | ||||
|    * | ||||
|    * The Transaction must have a valid `signature` before invoking this method | ||||
|    */ | ||||
|   serialize(): Buffer { | ||||
|   serialize(config?: SerializeConfig): Buffer { | ||||
|     const {signatures} = this; | ||||
|     if (!signatures || signatures.length === 0) { | ||||
|  | ||||
|     const {requireAllSignatures, verifySignatures} = Object.assign( | ||||
|       {requireAllSignatures: true, verifySignatures: true}, | ||||
|       config, | ||||
|     ); | ||||
|  | ||||
|     if (requireAllSignatures && signatures.length === 0) { | ||||
|       throw new Error('Transaction has not been signed'); | ||||
|     } | ||||
|  | ||||
|     const signData = this.serializeMessage(); | ||||
|     if (!this._verifySignatures(signData)) { | ||||
|     if ( | ||||
|       verifySignatures && | ||||
|       !this._verifySignatures(signData, requireAllSignatures) | ||||
|     ) { | ||||
|       throw new Error('Transaction has not been signed correctly'); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -136,9 +136,41 @@ test('partialSign', () => { | ||||
|   partialTransaction.setSigners(account1.publicKey, account2.publicKey); | ||||
|   expect(partialTransaction.signatures[0].signature).toBeNull(); | ||||
|   expect(partialTransaction.signatures[1].signature).toBeNull(); | ||||
|   partialTransaction.partialSign(account1, account2); | ||||
|  | ||||
|   partialTransaction.partialSign(account1); | ||||
|   expect(partialTransaction.signatures[0].signature).not.toBeNull(); | ||||
|   expect(partialTransaction.signatures[1].signature).toBeNull(); | ||||
|  | ||||
|   expect(() => partialTransaction.serialize()).toThrow(); | ||||
|   expect(() => | ||||
|     partialTransaction.serialize({requireAllSignatures: false}), | ||||
|   ).not.toThrow(); | ||||
|  | ||||
|   partialTransaction.partialSign(account2); | ||||
|  | ||||
|   expect(partialTransaction.signatures[0].signature).not.toBeNull(); | ||||
|   expect(partialTransaction.signatures[1].signature).not.toBeNull(); | ||||
|  | ||||
|   expect(() => partialTransaction.serialize()).not.toThrow(); | ||||
|  | ||||
|   expect(partialTransaction).toEqual(transaction); | ||||
|  | ||||
|   if ( | ||||
|     partialTransaction.signatures[0].signature != null /* <-- pacify flow */ | ||||
|   ) { | ||||
|     partialTransaction.signatures[0].signature[0] = 0; | ||||
|     expect(() => | ||||
|       partialTransaction.serialize({requireAllSignatures: false}), | ||||
|     ).toThrow(); | ||||
|     expect(() => | ||||
|       partialTransaction.serialize({ | ||||
|         verifySignatures: false, | ||||
|         requireAllSignatures: false, | ||||
|       }), | ||||
|     ).not.toThrow(); | ||||
|   } else { | ||||
|     throw new Error('unreachable'); | ||||
|   } | ||||
| }); | ||||
|  | ||||
| describe('dedupe', () => { | ||||
| @@ -392,6 +424,9 @@ test('serialize unsigned transaction', () => { | ||||
|   expect(() => { | ||||
|     expectedTransaction.serialize(); | ||||
|   }).toThrow(Error); | ||||
|   expect(() => { | ||||
|     expectedTransaction.serialize({verifySignatures: false}); | ||||
|   }).toThrow(Error); | ||||
|   expect(() => { | ||||
|     expectedTransaction.serializeMessage(); | ||||
|   }).toThrow('Transaction feePayer required'); | ||||
| @@ -407,6 +442,18 @@ test('serialize unsigned transaction', () => { | ||||
|   // 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=', | ||||
|     'base64', | ||||
|   ); | ||||
|   expect( | ||||
|     expectedTransaction.serialize({requireAllSignatures: false}), | ||||
|   ).toStrictEqual(expectedSerializationWithNoSignatures); | ||||
|  | ||||
|   // Properly signed transaction succeeds | ||||
|   expectedTransaction.partialSign(sender); | ||||
|   expect(expectedTransaction.signatures.length).toBe(1); | ||||
|   | ||||
		Reference in New Issue
	
	Block a user