Add HD signer methods

This commit is contained in:
junderw 2019-07-19 11:42:45 +09:00
parent 1326e0cc42
commit 4366b621d7
No known key found for this signature in database
GPG key ID: B256185D3A971908
5 changed files with 478 additions and 0 deletions

View file

@ -278,6 +278,86 @@ class Psbt {
}
return results.every(res => res === true);
}
signHD(hdKeyPair, sighashTypes = [transaction_1.Transaction.SIGHASH_ALL]) {
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
throw new Error('Need HDSigner to sign input');
}
const results = [];
for (const i of range(this.data.inputs.length)) {
try {
this.signInputHD(i, hdKeyPair, sighashTypes);
results.push(true);
} catch (err) {
results.push(false);
}
}
if (results.every(v => v === false)) {
throw new Error('No inputs were signed');
}
return this;
}
signHDAsync(
hdKeyPair,
sighashTypes = [transaction_1.Transaction.SIGHASH_ALL],
) {
return new Promise((resolve, reject) => {
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
return reject(new Error('Need HDSigner to sign input'));
}
const results = [];
const promises = [];
for (const i of range(this.data.inputs.length)) {
promises.push(
this.signInputHDAsync(i, hdKeyPair, sighashTypes).then(
() => {
results.push(true);
},
() => {
results.push(false);
},
),
);
}
return Promise.all(promises).then(() => {
if (results.every(v => v === false)) {
return reject(new Error('No inputs were signed'));
}
resolve();
});
});
}
signInputHD(
inputIndex,
hdKeyPair,
sighashTypes = [transaction_1.Transaction.SIGHASH_ALL],
) {
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
throw new Error('Need HDSigner to sign input');
}
const signers = getSignersFromHD(inputIndex, this.data.inputs, hdKeyPair);
signers.forEach(signer => this.signInput(inputIndex, signer, sighashTypes));
return this;
}
signInputHDAsync(
inputIndex,
hdKeyPair,
sighashTypes = [transaction_1.Transaction.SIGHASH_ALL],
) {
return new Promise((resolve, reject) => {
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
return reject(new Error('Need HDSigner to sign input'));
}
const signers = getSignersFromHD(inputIndex, this.data.inputs, hdKeyPair);
const promises = signers.map(signer =>
this.signInputAsync(inputIndex, signer, sighashTypes),
);
return Promise.all(promises)
.then(() => {
resolve();
})
.catch(reject);
});
}
sign(keyPair, sighashTypes = [transaction_1.Transaction.SIGHASH_ALL]) {
if (!keyPair || !keyPair.publicKey)
throw new Error('Need Signer to sign input');
@ -853,6 +933,34 @@ function getScriptFromInput(inputIndex, input, cache) {
}
return res;
}
function getSignersFromHD(inputIndex, inputs, hdKeyPair) {
const input = utils_1.checkForInput(inputs, inputIndex);
if (!input.bip32Derivation || input.bip32Derivation.length === 0) {
throw new Error('Need bip32Derivation to sign with HD');
}
const myDerivations = input.bip32Derivation
.map(bipDv => {
if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) {
return bipDv;
} else {
return;
}
})
.filter(v => !!v);
if (myDerivations.length === 0) {
throw new Error(
'Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint',
);
}
const signers = myDerivations.map(bipDv => {
const node = hdKeyPair.derivePath(bipDv.path);
if (!bipDv.pubkey.equals(node.publicKey)) {
throw new Error('pubkey did not match bip32Derivation');
}
return node;
});
return signers;
}
function getSortedSigs(script, partialSig) {
const p2ms = payments.p2ms({ output: script });
// for each pubkey in order of p2ms script

View file

@ -448,6 +448,52 @@
}
]
},
"signInputHD": {
"checks": [
{
"description": "checks the bip32Derivation exists",
"shouldSign": {
"psbt": "cHNidP8BADMBAAAAARtEptsZNydT9Bh9A5ptwIZz87yH8NXwzr1bjJorAZEAAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFJWXNY2Vp7E5pNOey64rnhhgUlohiKwAAAAAIgYDn85vSg5rlR25fd4MOU2ANsFoO+q828zuOI/5b8tj89kYBCppsiwAAIAAAACAAAAAgAAAAAAAAAAAAAA=",
"inputToCheck": 0,
"xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev"
},
"shouldThrow": {
"errorMessage": "Need bip32Derivation to sign with HD",
"psbt": "cHNidP8BADMBAAAAAXVa+rWvBGNyifYXEMlTten9+qC0xuHcAMxQYrQTwX1dAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFC8spHIOpiw9giaEPd5RGkMYvXRHiKwAAAAAAAA=",
"inputToCheck": 0,
"xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev"
}
},
{
"description": "checks the bip32Derivation exists",
"shouldSign": {
"psbt": "cHNidP8BADMBAAAAARtEptsZNydT9Bh9A5ptwIZz87yH8NXwzr1bjJorAZEAAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFJWXNY2Vp7E5pNOey64rnhhgUlohiKwAAAAAIgYDn85vSg5rlR25fd4MOU2ANsFoO+q828zuOI/5b8tj89kYBCppsiwAAIAAAACAAAAAgAAAAAAAAAAAAAA=",
"inputToCheck": 0,
"xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev"
},
"shouldThrow": {
"errorMessage": "Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint",
"psbt": "cHNidP8BADMBAAAAARtEptsZNydT9Bh9A5ptwIZz87yH8NXwzr1bjJorAZEAAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFJWXNY2Vp7E5pNOey64rnhhgUlohiKwAAAAAIgYD/85vSg5rlR25fd4MOU2ANsFoO+q828zuOI/5b8tj89kY/////ywAAIAAAACAAAAAgAAAAAAAAAAAAAA=",
"inputToCheck": 0,
"xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev"
}
},
{
"description": "checks the bip32Derivation exists",
"shouldSign": {
"psbt": "cHNidP8BADMBAAAAARtEptsZNydT9Bh9A5ptwIZz87yH8NXwzr1bjJorAZEAAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFJWXNY2Vp7E5pNOey64rnhhgUlohiKwAAAAAIgYDn85vSg5rlR25fd4MOU2ANsFoO+q828zuOI/5b8tj89kYBCppsiwAAIAAAACAAAAAgAAAAAAAAAAAAAA=",
"inputToCheck": 0,
"xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev"
},
"shouldThrow": {
"errorMessage": "pubkey did not match bip32Derivation",
"psbt": "cHNidP8BADMBAAAAARtEptsZNydT9Bh9A5ptwIZz87yH8NXwzr1bjJorAZEAAAAAAAD/////AAAAAAAAAQDAAgAAAAH//////////////////////////////////////////wAAAABrSDBFAiEAjtCPUj0vx3I5HFQKAUWHN0vCnT17jd41/omb4nobq/sCIAilSeQVi4mqykgBbs+Wz6PyqdMThi2gT463v4kPWk6cASECaSihTgej6zyYUQLWkPnBx68mOUGCIuXcWbZDMArbhWH/////AQDh9QUAAAAAGXapFJWXNY2Vp7E5pNOey64rnhhgUlohiKwAAAAAIgYD/85vSg5rlR25fd4MOU2ANsFoO+q828zuOI/5b8tj89kYBCppsiwAAIAAAACAAAAAgAAAAAAAAAAAAAA=",
"inputToCheck": 0,
"xprv": "xprv9s21ZrQH143K2XNCa3o3tii6nbyJAET6GjTfzcF6roTjAMzLUBe8nt7QHNYqKah8JBv8V67MTWBCqPptRr6khjTSvCUVru78KHW13Viwnev"
}
}
]
},
"finalizeAllInputs": [
{
"type": "P2PK",

View file

@ -1,6 +1,7 @@
const { describe, it } = require('mocha')
const assert = require('assert')
const bip32 = require('bip32')
const ECPair = require('../src/ecpair')
const Psbt = require('..').Psbt
const NETWORKS = require('../src/networks')
@ -278,6 +279,130 @@ describe(`Psbt`, () => {
})
})
describe('signInputHDAsync', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, async () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotReject(async () => {
await psbtThatShouldsign.signInputHDAsync(
f.shouldSign.inputToCheck,
bip32.fromBase58(f.shouldSign.xprv),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.rejects(async () => {
await psbtThatShouldThrow.signInputHDAsync(
f.shouldThrow.inputToCheck,
bip32.fromBase58(f.shouldThrow.xprv),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp(f.shouldThrow.errorMessage))
assert.rejects(async () => {
await psbtThatShouldThrow.signInputHDAsync(
f.shouldThrow.inputToCheck,
)
}, new RegExp('Need HDSigner to sign input'))
}
})
})
})
describe('signInputHD', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotThrow(() => {
psbtThatShouldsign.signInputHD(
f.shouldSign.inputToCheck,
bip32.fromBase58(f.shouldSign.xprv),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.throws(() => {
psbtThatShouldThrow.signInputHD(
f.shouldThrow.inputToCheck,
bip32.fromBase58(f.shouldThrow.xprv),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp(f.shouldThrow.errorMessage))
assert.throws(() => {
psbtThatShouldThrow.signInputHD(
f.shouldThrow.inputToCheck,
)
}, new RegExp('Need HDSigner to sign input'))
}
})
})
})
describe('signHDAsync', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, async () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotReject(async () => {
await psbtThatShouldsign.signHDAsync(
bip32.fromBase58(f.shouldSign.xprv),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.rejects(async () => {
await psbtThatShouldThrow.signHDAsync(
bip32.fromBase58(f.shouldThrow.xprv),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp('No inputs were signed'))
assert.rejects(async () => {
await psbtThatShouldThrow.signHDAsync()
}, new RegExp('Need HDSigner to sign input'))
}
})
})
})
describe('signHD', () => {
fixtures.signInputHD.checks.forEach(f => {
it(f.description, () => {
if (f.shouldSign) {
const psbtThatShouldsign = Psbt.fromBase64(f.shouldSign.psbt)
assert.doesNotThrow(() => {
psbtThatShouldsign.signHD(
bip32.fromBase58(f.shouldSign.xprv),
f.shouldSign.sighashTypes || undefined,
)
})
}
if (f.shouldThrow) {
const psbtThatShouldThrow = Psbt.fromBase64(f.shouldThrow.psbt)
assert.throws(() => {
psbtThatShouldThrow.signHD(
bip32.fromBase58(f.shouldThrow.xprv),
f.shouldThrow.sighashTypes || undefined,
)
}, new RegExp('No inputs were signed'))
assert.throws(() => {
psbtThatShouldThrow.signHD()
}, new RegExp('Need HDSigner to sign input'))
}
})
})
})
describe('finalizeAllInputs', () => {
fixtures.finalizeAllInputs.forEach(f => {
it(`Finalizes inputs of type "${f.type}"`, () => {

View file

@ -332,6 +332,107 @@ export class Psbt {
return results.every(res => res === true);
}
signHD(
hdKeyPair: HDSigner,
sighashTypes: number[] = [Transaction.SIGHASH_ALL],
): this {
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
throw new Error('Need HDSigner to sign input');
}
const results: boolean[] = [];
for (const i of range(this.data.inputs.length)) {
try {
this.signInputHD(i, hdKeyPair, sighashTypes);
results.push(true);
} catch (err) {
results.push(false);
}
}
if (results.every(v => v === false)) {
throw new Error('No inputs were signed');
}
return this;
}
signHDAsync(
hdKeyPair: HDSigner | HDSignerAsync,
sighashTypes: number[] = [Transaction.SIGHASH_ALL],
): Promise<void> {
return new Promise(
(resolve, reject): any => {
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
return reject(new Error('Need HDSigner to sign input'));
}
const results: boolean[] = [];
const promises: Array<Promise<void>> = [];
for (const i of range(this.data.inputs.length)) {
promises.push(
this.signInputHDAsync(i, hdKeyPair, sighashTypes).then(
() => {
results.push(true);
},
() => {
results.push(false);
},
),
);
}
return Promise.all(promises).then(() => {
if (results.every(v => v === false)) {
return reject(new Error('No inputs were signed'));
}
resolve();
});
},
);
}
signInputHD(
inputIndex: number,
hdKeyPair: HDSigner,
sighashTypes: number[] = [Transaction.SIGHASH_ALL],
): this {
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
throw new Error('Need HDSigner to sign input');
}
const signers = getSignersFromHD(
inputIndex,
this.data.inputs,
hdKeyPair,
) as Signer[];
signers.forEach(signer => this.signInput(inputIndex, signer, sighashTypes));
return this;
}
signInputHDAsync(
inputIndex: number,
hdKeyPair: HDSigner | HDSignerAsync,
sighashTypes: number[] = [Transaction.SIGHASH_ALL],
): Promise<void> {
return new Promise(
(resolve, reject): any => {
if (!hdKeyPair || !hdKeyPair.publicKey || !hdKeyPair.fingerprint) {
return reject(new Error('Need HDSigner to sign input'));
}
const signers = getSignersFromHD(
inputIndex,
this.data.inputs,
hdKeyPair,
);
const promises = signers.map(signer =>
this.signInputAsync(inputIndex, signer, sighashTypes),
);
return Promise.all(promises)
.then(() => {
resolve();
})
.catch(reject);
},
);
}
sign(
keyPair: Signer,
sighashTypes: number[] = [Transaction.SIGHASH_ALL],
@ -525,6 +626,38 @@ interface PsbtOpts {
maximumFeeRate: number;
}
interface HDSignerBase {
/**
* DER format compressed publicKey buffer
*/
publicKey: Buffer;
/**
* The first 4 bytes of the sha256-ripemd160 of the publicKey
*/
fingerprint: Buffer;
}
interface HDSigner extends HDSignerBase {
/**
* The path string must match /^m(\/\d+'?)+$/
* ex. m/44'/0'/0'/1/23 levels with ' must be hard derivations
*/
derivePath(path: string): HDSigner;
/**
* Input hash (the "message digest") for the signature algorithm
* Return a 64 byte signature (32 byte r and 32 byte s in that order)
*/
sign(hash: Buffer): Buffer;
}
/**
* Same as above but with async sign method
*/
interface HDSignerAsync extends HDSignerBase {
derivePath(path: string): HDSignerAsync;
sign(hash: Buffer): Promise<Buffer>;
}
/**
* This function is needed to pass to the bip174 base class's fromBuffer.
* It takes the "transaction buffer" portion of the psbt buffer and returns a
@ -1042,6 +1175,39 @@ function getScriptFromInput(
return res;
}
function getSignersFromHD(
inputIndex: number,
inputs: PsbtInput[],
hdKeyPair: HDSigner | HDSignerAsync,
): Array<Signer | SignerAsync> {
const input = checkForInput(inputs, inputIndex);
if (!input.bip32Derivation || input.bip32Derivation.length === 0) {
throw new Error('Need bip32Derivation to sign with HD');
}
const myDerivations = input.bip32Derivation
.map(bipDv => {
if (bipDv.masterFingerprint.equals(hdKeyPair.fingerprint)) {
return bipDv;
} else {
return;
}
})
.filter(v => !!v);
if (myDerivations.length === 0) {
throw new Error(
'Need one bip32Derivation masterFingerprint to match the HDSigner fingerprint',
);
}
const signers: Array<Signer | SignerAsync> = myDerivations.map(bipDv => {
const node = hdKeyPair.derivePath(bipDv!.path);
if (!bipDv!.pubkey.equals(node.publicKey)) {
throw new Error('pubkey did not match bip32Derivation');
}
return node;
});
return signers;
}
function getSortedSigs(script: Buffer, partialSig: PartialSig[]): Buffer[] {
const p2ms = payments.p2ms({ output: script });
// for each pubkey in order of p2ms script

33
types/psbt.d.ts vendored
View file

@ -61,6 +61,10 @@ export declare class Psbt {
finalizeInput(inputIndex: number): this;
validateAllSignatures(): boolean;
validateSignatures(inputIndex: number, pubkey?: Buffer): boolean;
signHD(hdKeyPair: HDSigner, sighashTypes?: number[]): this;
signHDAsync(hdKeyPair: HDSigner | HDSignerAsync, sighashTypes?: number[]): Promise<void>;
signInputHD(inputIndex: number, hdKeyPair: HDSigner, sighashTypes?: number[]): this;
signInputHDAsync(inputIndex: number, hdKeyPair: HDSigner | HDSignerAsync, sighashTypes?: number[]): Promise<void>;
sign(keyPair: Signer, sighashTypes?: number[]): this;
signAsync(keyPair: Signer | SignerAsync, sighashTypes?: number[]): Promise<void>;
signInput(inputIndex: number, keyPair: Signer, sighashTypes?: number[]): this;
@ -80,4 +84,33 @@ interface PsbtOptsOptional {
network?: Network;
maximumFeeRate?: number;
}
interface HDSignerBase {
/**
* DER format compressed publicKey buffer
*/
publicKey: Buffer;
/**
* The first 4 bytes of the sha256-ripemd160 of the publicKey
*/
fingerprint: Buffer;
}
interface HDSigner extends HDSignerBase {
/**
* The path string must match /^m(\/\d+'?)+$/
* ex. m/44'/0'/0'/1/23 levels with ' must be hard derivations
*/
derivePath(path: string): HDSigner;
/**
* Input hash (the "message digest") for the signature algorithm
* Return a 64 byte signature (32 byte r and 32 byte s in that order)
*/
sign(hash: Buffer): Buffer;
}
/**
* Same as above but with async sign method
*/
interface HDSignerAsync extends HDSignerBase {
derivePath(path: string): HDSignerAsync;
sign(hash: Buffer): Promise<Buffer>;
}
export {};