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>; |     signatures?: Array<SignaturePubkeyPair>; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   export type SerializeConfig = { | ||||||
|  |     requireAllSignatures?: boolean; | ||||||
|  |     verifySignatures?: boolean; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   export class Transaction { |   export class Transaction { | ||||||
|     signatures: Array<SignaturePubkeyPair>; |     signatures: Array<SignaturePubkeyPair>; | ||||||
|     signature?: Buffer; |     signature?: Buffer; | ||||||
| @@ -640,7 +645,7 @@ declare module '@solana/web3.js' { | |||||||
|     addSignature(pubkey: PublicKey, signature: Buffer): void; |     addSignature(pubkey: PublicKey, signature: Buffer): void; | ||||||
|     setSigners(...signer: Array<PublicKey>): void; |     setSigners(...signer: Array<PublicKey>): void; | ||||||
|     verifySignatures(): boolean; |     verifySignatures(): boolean; | ||||||
|     serialize(): Buffer; |     serialize(config?: SerializeConfig): Buffer; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // === src/stake-program.js === |   // === src/stake-program.js === | ||||||
|   | |||||||
| @@ -621,6 +621,11 @@ declare module '@solana/web3.js' { | |||||||
|     signatures?: Array<SignaturePubkeyPair>, |     signatures?: Array<SignaturePubkeyPair>, | ||||||
|   |}; |   |}; | ||||||
|  |  | ||||||
|  |   declare export type SerializeConfig = { | ||||||
|  |     requireAllSignatures?: boolean, | ||||||
|  |     verifySignatures?: boolean, | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   declare export class Transaction { |   declare export class Transaction { | ||||||
|     signatures: Array<SignaturePubkeyPair>; |     signatures: Array<SignaturePubkeyPair>; | ||||||
|     signature: ?Buffer; |     signature: ?Buffer; | ||||||
| @@ -644,7 +649,7 @@ declare module '@solana/web3.js' { | |||||||
|     addSignature(pubkey: PublicKey, signature: Buffer): void; |     addSignature(pubkey: PublicKey, signature: Buffer): void; | ||||||
|     setSigners(...signers: Array<PublicKey>): void; |     setSigners(...signers: Array<PublicKey>): void; | ||||||
|     verifySignatures(): boolean; |     verifySignatures(): boolean; | ||||||
|     serialize(): Buffer; |     serialize(config?: SerializeConfig): Buffer; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // === src/stake-program.js === |   // === src/stake-program.js === | ||||||
|   | |||||||
| @@ -62,6 +62,18 @@ export type TransactionInstructionCtorFields = {| | |||||||
|   data?: Buffer, |   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 |  * Transaction Instruction class | ||||||
|  */ |  */ | ||||||
| @@ -462,37 +474,49 @@ export class Transaction { | |||||||
|    * Verify signatures of a complete, signed Transaction |    * Verify signatures of a complete, signed Transaction | ||||||
|    */ |    */ | ||||||
|   verifySignatures(): boolean { |   verifySignatures(): boolean { | ||||||
|     return this._verifySignatures(this.serializeMessage()); |     return this._verifySignatures(this.serializeMessage(), true); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * @private |    * @private | ||||||
|    */ |    */ | ||||||
|   _verifySignatures(signData: Buffer): boolean { |   _verifySignatures(signData: Buffer, requireAllSignatures: boolean): boolean { | ||||||
|     let verified = true; |  | ||||||
|     for (const {signature, publicKey} of this.signatures) { |     for (const {signature, publicKey} of this.signatures) { | ||||||
|       if ( |       if (signature === null) { | ||||||
|         !nacl.sign.detached.verify(signData, signature, publicKey.toBuffer()) |         if (requireAllSignatures) { | ||||||
|       ) { |           return false; | ||||||
|         verified = false; |         } | ||||||
|  |       } else { | ||||||
|  |         if ( | ||||||
|  |           !nacl.sign.detached.verify(signData, signature, publicKey.toBuffer()) | ||||||
|  |         ) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|       } |       } | ||||||
|     } |     } | ||||||
|     return verified; |     return true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   /** |   /** | ||||||
|    * Serialize the Transaction in the wire format. |    * 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; |     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'); |       throw new Error('Transaction has not been signed'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const signData = this.serializeMessage(); |     const signData = this.serializeMessage(); | ||||||
|     if (!this._verifySignatures(signData)) { |     if ( | ||||||
|  |       verifySignatures && | ||||||
|  |       !this._verifySignatures(signData, requireAllSignatures) | ||||||
|  |     ) { | ||||||
|       throw new Error('Transaction has not been signed correctly'); |       throw new Error('Transaction has not been signed correctly'); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -136,9 +136,41 @@ test('partialSign', () => { | |||||||
|   partialTransaction.setSigners(account1.publicKey, account2.publicKey); |   partialTransaction.setSigners(account1.publicKey, account2.publicKey); | ||||||
|   expect(partialTransaction.signatures[0].signature).toBeNull(); |   expect(partialTransaction.signatures[0].signature).toBeNull(); | ||||||
|   expect(partialTransaction.signatures[1].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); |   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', () => { | describe('dedupe', () => { | ||||||
| @@ -392,6 +424,9 @@ test('serialize unsigned transaction', () => { | |||||||
|   expect(() => { |   expect(() => { | ||||||
|     expectedTransaction.serialize(); |     expectedTransaction.serialize(); | ||||||
|   }).toThrow(Error); |   }).toThrow(Error); | ||||||
|  |   expect(() => { | ||||||
|  |     expectedTransaction.serialize({verifySignatures: false}); | ||||||
|  |   }).toThrow(Error); | ||||||
|   expect(() => { |   expect(() => { | ||||||
|     expectedTransaction.serializeMessage(); |     expectedTransaction.serializeMessage(); | ||||||
|   }).toThrow('Transaction feePayer required'); |   }).toThrow('Transaction feePayer required'); | ||||||
| @@ -407,6 +442,18 @@ test('serialize unsigned transaction', () => { | |||||||
|   // Serializing the message is allowed when signature array has null signatures |   // Serializing the message is allowed when signature array has null signatures | ||||||
|   expectedTransaction.serializeMessage(); |   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 |   // Properly signed transaction succeeds | ||||||
|   expectedTransaction.partialSign(sender); |   expectedTransaction.partialSign(sender); | ||||||
|   expect(expectedTransaction.signatures.length).toBe(1); |   expect(expectedTransaction.signatures.length).toBe(1); | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user