fix: support serialization of partially signed transactions

This commit is contained in:
Michael Vines
2020-09-11 15:04:36 -07:00
committed by mergify[bot]
parent 4bb6c2fffb
commit a59d305e09
4 changed files with 97 additions and 16 deletions

7
web3.js/module.d.ts vendored
View File

@ -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 ===

View File

@ -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 ===

View File

@ -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 (signature === null) {
if (requireAllSignatures) {
return false;
}
} else {
if ( if (
!nacl.sign.detached.verify(signData, signature, publicKey.toBuffer()) !nacl.sign.detached.verify(signData, signature, publicKey.toBuffer())
) { ) {
verified = false; 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');
} }

View File

@ -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);