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>;
};
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 ===

View File

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

View File

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

View File

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