Add signature verify method
This commit is contained in:
parent
02ba6c78d1
commit
d0d94c7f06
4 changed files with 170 additions and 13 deletions
40
src/psbt.js
40
src/psbt.js
|
@ -5,6 +5,7 @@ const utils_1 = require('bip174/src/lib/utils');
|
||||||
const address_1 = require('./address');
|
const address_1 = require('./address');
|
||||||
const bufferutils_1 = require('./bufferutils');
|
const bufferutils_1 = require('./bufferutils');
|
||||||
const crypto_1 = require('./crypto');
|
const crypto_1 = require('./crypto');
|
||||||
|
const ecpair_1 = require('./ecpair');
|
||||||
const networks_1 = require('./networks');
|
const networks_1 = require('./networks');
|
||||||
const payments = require('./payments');
|
const payments = require('./payments');
|
||||||
const bscript = require('./script');
|
const bscript = require('./script');
|
||||||
|
@ -304,6 +305,39 @@ class Psbt extends bip174_1.Psbt {
|
||||||
this.clearFinalizedInput(inputIndex);
|
this.clearFinalizedInput(inputIndex);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
validateSignatures(inputIndex, pubkey) {
|
||||||
|
const input = this.inputs[inputIndex];
|
||||||
|
const partialSig = (input || {}).partialSig;
|
||||||
|
if (!input || !partialSig || partialSig.length < 1)
|
||||||
|
throw new Error('No signatures to validate');
|
||||||
|
const mySigs = pubkey
|
||||||
|
? partialSig.filter(sig => sig.pubkey.equals(pubkey))
|
||||||
|
: partialSig;
|
||||||
|
if (mySigs.length < 1) throw new Error('No signatures for this pubkey');
|
||||||
|
const results = [];
|
||||||
|
let hashCache;
|
||||||
|
let scriptCache;
|
||||||
|
let sighashCache;
|
||||||
|
for (const pSig of mySigs) {
|
||||||
|
const sig = bscript.signature.decode(pSig.signature);
|
||||||
|
const { hash, script } =
|
||||||
|
sighashCache !== sig.hashType
|
||||||
|
? getHashForSig(
|
||||||
|
inputIndex,
|
||||||
|
Object.assign({}, input, { sighashType: sig.hashType }),
|
||||||
|
this.__TX,
|
||||||
|
this.__CACHE,
|
||||||
|
)
|
||||||
|
: { hash: hashCache, script: scriptCache };
|
||||||
|
sighashCache = sig.hashType;
|
||||||
|
hashCache = hash;
|
||||||
|
scriptCache = script;
|
||||||
|
checkScriptForPubkey(pSig.pubkey, script, 'verify');
|
||||||
|
const keypair = ecpair_1.fromPublicKey(pSig.pubkey);
|
||||||
|
results.push(keypair.verify(hash, sig.signature));
|
||||||
|
}
|
||||||
|
return results.every(res => res === true);
|
||||||
|
}
|
||||||
signInput(inputIndex, keyPair) {
|
signInput(inputIndex, keyPair) {
|
||||||
if (!keyPair || !keyPair.publicKey)
|
if (!keyPair || !keyPair.publicKey)
|
||||||
throw new Error('Need Signer to sign input');
|
throw new Error('Need Signer to sign input');
|
||||||
|
@ -391,7 +425,7 @@ function getHashAndSighashType(inputs, inputIndex, pubkey, unsignedTx, cache) {
|
||||||
unsignedTx,
|
unsignedTx,
|
||||||
cache,
|
cache,
|
||||||
);
|
);
|
||||||
checkScriptForPubkey(pubkey, script);
|
checkScriptForPubkey(pubkey, script, 'sign');
|
||||||
return {
|
return {
|
||||||
hash,
|
hash,
|
||||||
sighashType,
|
sighashType,
|
||||||
|
@ -494,7 +528,7 @@ function canFinalize(input, script, scriptType) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
function checkScriptForPubkey(pubkey, script) {
|
function checkScriptForPubkey(pubkey, script, action) {
|
||||||
const pubkeyHash = crypto_1.hash160(pubkey);
|
const pubkeyHash = crypto_1.hash160(pubkey);
|
||||||
const decompiled = bscript.decompile(script);
|
const decompiled = bscript.decompile(script);
|
||||||
if (decompiled === null) throw new Error('Unknown script error');
|
if (decompiled === null) throw new Error('Unknown script error');
|
||||||
|
@ -504,7 +538,7 @@ function checkScriptForPubkey(pubkey, script) {
|
||||||
});
|
});
|
||||||
if (!hasKey) {
|
if (!hasKey) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Can not sign for this input with the key ${pubkey.toString('hex')}`,
|
`Can not ${action} for this input with the key ${pubkey.toString('hex')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -7,6 +7,65 @@ const regtest = regtestUtils.network
|
||||||
// See bottom of file for some helper functions used to make the payment objects needed.
|
// See bottom of file for some helper functions used to make the payment objects needed.
|
||||||
|
|
||||||
describe('bitcoinjs-lib (transactions with psbt)', () => {
|
describe('bitcoinjs-lib (transactions with psbt)', () => {
|
||||||
|
it('can create a 1-to-1 Transaction', () => {
|
||||||
|
const alice = bitcoin.ECPair.fromWIF('L2uPYXe17xSTqbCjZvL2DsyXPCbXspvcu5mHLDYUgzdUbZGSKrSr')
|
||||||
|
const psbt = new bitcoin.Psbt()
|
||||||
|
psbt.setVersion(2) // These are defaults. This line is not needed.
|
||||||
|
psbt.setLocktime(0) // These are defaults. This line is not needed.
|
||||||
|
psbt.addInput({
|
||||||
|
// if hash is string, txid, if hash is Buffer, is reversed compared to txid
|
||||||
|
hash: '7d067b4a697a09d2c3cff7d4d9506c9955e93bff41bf82d439da7d030382bc3e',
|
||||||
|
index: 0,
|
||||||
|
sequence: 0xffffffff, // These are defaults. This line is not needed.
|
||||||
|
|
||||||
|
// non-segwit inputs now require passing the whole previous tx as Buffer
|
||||||
|
nonWitnessUtxo: Buffer.from(
|
||||||
|
'0200000001f9f34e95b9d5c8abcd20fc5bd4a825d1517be62f0f775e5f36da944d9' +
|
||||||
|
'452e550000000006b483045022100c86e9a111afc90f64b4904bd609e9eaed80d48' +
|
||||||
|
'ca17c162b1aca0a788ac3526f002207bb79b60d4fc6526329bf18a77135dc566020' +
|
||||||
|
'9e761da46e1c2f1152ec013215801210211755115eabf846720f5cb18f248666fec' +
|
||||||
|
'631e5e1e66009ce3710ceea5b1ad13ffffffff01' +
|
||||||
|
// value in satoshis (Int64LE) = 0x015f90 = 90000
|
||||||
|
'905f010000000000' +
|
||||||
|
// scriptPubkey length
|
||||||
|
'19' +
|
||||||
|
// scriptPubkey
|
||||||
|
'76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac' +
|
||||||
|
// locktime
|
||||||
|
'00000000',
|
||||||
|
'hex',
|
||||||
|
),
|
||||||
|
|
||||||
|
// // If this input was segwit, instead of nonWitnessUtxo, you would add
|
||||||
|
// // a witnessUtxo as follows. The scriptPubkey and the value only are needed.
|
||||||
|
// witnessUtxo: {
|
||||||
|
// script: Buffer.from(
|
||||||
|
// '76a9148bbc95d2709c71607c60ee3f097c1217482f518d88ac',
|
||||||
|
// 'hex',
|
||||||
|
// ),
|
||||||
|
// value: 90000,
|
||||||
|
// },
|
||||||
|
|
||||||
|
// Not featured here: redeemScript. A Buffer of the redeemScript
|
||||||
|
})
|
||||||
|
psbt.addOutput({
|
||||||
|
address: '1KRMKfeZcmosxALVYESdPNez1AP1mEtywp',
|
||||||
|
value: 80000
|
||||||
|
})
|
||||||
|
psbt.signInput(0, alice)
|
||||||
|
psbt.validateSignatures(0)
|
||||||
|
psbt.finalizeAllInputs()
|
||||||
|
assert.strictEqual(
|
||||||
|
psbt.extractTransaction().toHex(),
|
||||||
|
'02000000013ebc8203037dda39d482bf41ff3be955996c50d9d4f7cfc3d2097a694a7' +
|
||||||
|
'b067d000000006b483045022100931b6db94aed25d5486884d83fc37160f37f3368c0' +
|
||||||
|
'd7f48c757112abefec983802205fda64cff98c849577026eb2ce916a50ea70626a766' +
|
||||||
|
'9f8596dd89b720a26b4d501210365db9da3f8a260078a7e8f8b708a1161468fb2323f' +
|
||||||
|
'fda5ec16b261ec1056f455ffffffff0180380100000000001976a914ca0d36044e0dc' +
|
||||||
|
'08a22724efa6f6a07b0ec4c79aa88ac00000000',
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
it('can create (and broadcast via 3PBP) a typical Transaction', async () => {
|
it('can create (and broadcast via 3PBP) a typical Transaction', async () => {
|
||||||
// these are { payment: Payment; keys: ECPair[] }
|
// these are { payment: Payment; keys: ECPair[] }
|
||||||
const alice1 = createPayment('p2pkh')
|
const alice1 = createPayment('p2pkh')
|
||||||
|
@ -64,6 +123,12 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
|
||||||
// final1.combine(final2) would give the exact same result
|
// final1.combine(final2) would give the exact same result
|
||||||
psbt.combine(final1, final2)
|
psbt.combine(final1, final2)
|
||||||
|
|
||||||
|
// Finalizer wants to check all signatures are valid before finalizing.
|
||||||
|
// If the finalizer wants to check for specific pubkeys, the second arg
|
||||||
|
// can be passed. See the first multisig example below.
|
||||||
|
assert.strictEqual(psbt.validateSignatures(0), true)
|
||||||
|
assert.strictEqual(psbt.validateSignatures(1), true)
|
||||||
|
|
||||||
// This step it new. Since we separate the signing operation and
|
// This step it new. Since we separate the signing operation and
|
||||||
// the creation of the scriptSig and witness stack, we are able to
|
// the creation of the scriptSig and witness stack, we are able to
|
||||||
psbt.finalizeAllInputs()
|
psbt.finalizeAllInputs()
|
||||||
|
@ -94,6 +159,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
|
||||||
})
|
})
|
||||||
.signInput(0, alice1.keys[0])
|
.signInput(0, alice1.keys[0])
|
||||||
|
|
||||||
|
assert.strictEqual(psbt.validateSignatures(0), true)
|
||||||
psbt.finalizeAllInputs()
|
psbt.finalizeAllInputs()
|
||||||
|
|
||||||
// build and broadcast to the RegTest network
|
// build and broadcast to the RegTest network
|
||||||
|
@ -122,6 +188,11 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
|
||||||
.signInput(0, multisig.keys[0])
|
.signInput(0, multisig.keys[0])
|
||||||
.signInput(0, multisig.keys[2])
|
.signInput(0, multisig.keys[2])
|
||||||
|
|
||||||
|
assert.strictEqual(psbt.validateSignatures(0), true)
|
||||||
|
assert.strictEqual(psbt.validateSignatures(0, multisig.keys[0].publicKey), true)
|
||||||
|
assert.throws(() => {
|
||||||
|
psbt.validateSignatures(0, multisig.keys[3].publicKey)
|
||||||
|
}, new RegExp('No signatures for this pubkey'))
|
||||||
psbt.finalizeAllInputs()
|
psbt.finalizeAllInputs()
|
||||||
|
|
||||||
const tx = psbt.extractTransaction()
|
const tx = psbt.extractTransaction()
|
||||||
|
@ -158,6 +229,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
|
||||||
})
|
})
|
||||||
.signInput(0, p2sh.keys[0])
|
.signInput(0, p2sh.keys[0])
|
||||||
|
|
||||||
|
assert.strictEqual(psbt.validateSignatures(0), true)
|
||||||
psbt.finalizeAllInputs()
|
psbt.finalizeAllInputs()
|
||||||
|
|
||||||
const tx = psbt.extractTransaction()
|
const tx = psbt.extractTransaction()
|
||||||
|
@ -196,6 +268,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
|
||||||
})
|
})
|
||||||
.signInput(0, p2wpkh.keys[0])
|
.signInput(0, p2wpkh.keys[0])
|
||||||
|
|
||||||
|
assert.strictEqual(psbt.validateSignatures(0), true)
|
||||||
psbt.finalizeAllInputs()
|
psbt.finalizeAllInputs()
|
||||||
|
|
||||||
const tx = psbt.extractTransaction()
|
const tx = psbt.extractTransaction()
|
||||||
|
@ -232,6 +305,7 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
|
||||||
})
|
})
|
||||||
.signInput(0, p2wsh.keys[0])
|
.signInput(0, p2wsh.keys[0])
|
||||||
|
|
||||||
|
assert.strictEqual(psbt.validateSignatures(0), true)
|
||||||
psbt.finalizeAllInputs()
|
psbt.finalizeAllInputs()
|
||||||
|
|
||||||
const tx = psbt.extractTransaction()
|
const tx = psbt.extractTransaction()
|
||||||
|
@ -271,6 +345,11 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
|
||||||
.signInput(0, p2sh.keys[2])
|
.signInput(0, p2sh.keys[2])
|
||||||
.signInput(0, p2sh.keys[3])
|
.signInput(0, p2sh.keys[3])
|
||||||
|
|
||||||
|
assert.strictEqual(psbt.validateSignatures(0), true)
|
||||||
|
assert.strictEqual(psbt.validateSignatures(0, p2sh.keys[3].publicKey), true)
|
||||||
|
assert.throws(() => {
|
||||||
|
psbt.validateSignatures(0, p2sh.keys[1].publicKey)
|
||||||
|
}, new RegExp('No signatures for this pubkey'))
|
||||||
psbt.finalizeAllInputs()
|
psbt.finalizeAllInputs()
|
||||||
|
|
||||||
const tx = psbt.extractTransaction()
|
const tx = psbt.extractTransaction()
|
||||||
|
@ -287,7 +366,8 @@ describe('bitcoinjs-lib (transactions with psbt)', () => {
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
function createPayment(_type) {
|
function createPayment(_type, network) {
|
||||||
|
network = network || regtest
|
||||||
const splitType = _type.split('-').reverse();
|
const splitType = _type.split('-').reverse();
|
||||||
const isMultisig = splitType[0].slice(0, 4) === 'p2ms';
|
const isMultisig = splitType[0].slice(0, 4) === 'p2ms';
|
||||||
const keys = [];
|
const keys = [];
|
||||||
|
@ -297,11 +377,11 @@ function createPayment(_type) {
|
||||||
m = parseInt(match[1])
|
m = parseInt(match[1])
|
||||||
let n = parseInt(match[2])
|
let n = parseInt(match[2])
|
||||||
while (n > 1) {
|
while (n > 1) {
|
||||||
keys.push(bitcoin.ECPair.makeRandom({ network: regtest }));
|
keys.push(bitcoin.ECPair.makeRandom({ network }));
|
||||||
n--
|
n--
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
keys.push(bitcoin.ECPair.makeRandom({ network: regtest }));
|
keys.push(bitcoin.ECPair.makeRandom({ network }));
|
||||||
|
|
||||||
let payment;
|
let payment;
|
||||||
splitType.forEach(type => {
|
splitType.forEach(type => {
|
||||||
|
@ -309,17 +389,17 @@ function createPayment(_type) {
|
||||||
payment = bitcoin.payments.p2ms({
|
payment = bitcoin.payments.p2ms({
|
||||||
m,
|
m,
|
||||||
pubkeys: keys.map(key => key.publicKey).sort(),
|
pubkeys: keys.map(key => key.publicKey).sort(),
|
||||||
network: regtest,
|
network,
|
||||||
});
|
});
|
||||||
} else if (['p2sh', 'p2wsh'].indexOf(type) > -1) {
|
} else if (['p2sh', 'p2wsh'].indexOf(type) > -1) {
|
||||||
payment = bitcoin.payments[type]({
|
payment = bitcoin.payments[type]({
|
||||||
redeem: payment,
|
redeem: payment,
|
||||||
network: regtest,
|
network,
|
||||||
});
|
});
|
||||||
} else {
|
} else {
|
||||||
payment = bitcoin.payments[type]({
|
payment = bitcoin.payments[type]({
|
||||||
pubkey: keys[0].publicKey,
|
pubkey: keys[0].publicKey,
|
||||||
network: regtest,
|
network,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
|
@ -10,7 +10,11 @@ import { checkForInput } from 'bip174/src/lib/utils';
|
||||||
import { toOutputScript } from './address';
|
import { toOutputScript } from './address';
|
||||||
import { reverseBuffer } from './bufferutils';
|
import { reverseBuffer } from './bufferutils';
|
||||||
import { hash160 } from './crypto';
|
import { hash160 } from './crypto';
|
||||||
import { Signer, SignerAsync } from './ecpair';
|
import {
|
||||||
|
fromPublicKey as ecPairFromPublicKey,
|
||||||
|
Signer,
|
||||||
|
SignerAsync,
|
||||||
|
} from './ecpair';
|
||||||
import { bitcoin as btcNetwork, Network } from './networks';
|
import { bitcoin as btcNetwork, Network } from './networks';
|
||||||
import * as payments from './payments';
|
import * as payments from './payments';
|
||||||
import * as bscript from './script';
|
import * as bscript from './script';
|
||||||
|
@ -367,6 +371,40 @@ export class Psbt extends PsbtBase {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
validateSignatures(inputIndex: number, pubkey?: Buffer): boolean {
|
||||||
|
const input = this.inputs[inputIndex];
|
||||||
|
const partialSig = (input || {}).partialSig;
|
||||||
|
if (!input || !partialSig || partialSig.length < 1)
|
||||||
|
throw new Error('No signatures to validate');
|
||||||
|
const mySigs = pubkey
|
||||||
|
? partialSig.filter(sig => sig.pubkey.equals(pubkey))
|
||||||
|
: partialSig;
|
||||||
|
if (mySigs.length < 1) throw new Error('No signatures for this pubkey');
|
||||||
|
const results: boolean[] = [];
|
||||||
|
let hashCache: Buffer;
|
||||||
|
let scriptCache: Buffer;
|
||||||
|
let sighashCache: number;
|
||||||
|
for (const pSig of mySigs) {
|
||||||
|
const sig = bscript.signature.decode(pSig.signature);
|
||||||
|
const { hash, script } =
|
||||||
|
sighashCache! !== sig.hashType
|
||||||
|
? getHashForSig(
|
||||||
|
inputIndex,
|
||||||
|
Object.assign({}, input, { sighashType: sig.hashType }),
|
||||||
|
this.__TX,
|
||||||
|
this.__CACHE,
|
||||||
|
)
|
||||||
|
: { hash: hashCache!, script: scriptCache! };
|
||||||
|
sighashCache = sig.hashType;
|
||||||
|
hashCache = hash;
|
||||||
|
scriptCache = script;
|
||||||
|
checkScriptForPubkey(pSig.pubkey, script, 'verify');
|
||||||
|
const keypair = ecPairFromPublicKey(pSig.pubkey);
|
||||||
|
results.push(keypair.verify(hash, sig.signature));
|
||||||
|
}
|
||||||
|
return results.every(res => res === true);
|
||||||
|
}
|
||||||
|
|
||||||
signInput(inputIndex: number, keyPair: Signer): this {
|
signInput(inputIndex: number, keyPair: Signer): this {
|
||||||
if (!keyPair || !keyPair.publicKey)
|
if (!keyPair || !keyPair.publicKey)
|
||||||
throw new Error('Need Signer to sign input');
|
throw new Error('Need Signer to sign input');
|
||||||
|
@ -507,7 +545,7 @@ function getHashAndSighashType(
|
||||||
unsignedTx,
|
unsignedTx,
|
||||||
cache,
|
cache,
|
||||||
);
|
);
|
||||||
checkScriptForPubkey(pubkey, script);
|
checkScriptForPubkey(pubkey, script, 'sign');
|
||||||
return {
|
return {
|
||||||
hash,
|
hash,
|
||||||
sighashType,
|
sighashType,
|
||||||
|
@ -628,7 +666,11 @@ function canFinalize(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function checkScriptForPubkey(pubkey: Buffer, script: Buffer): void {
|
function checkScriptForPubkey(
|
||||||
|
pubkey: Buffer,
|
||||||
|
script: Buffer,
|
||||||
|
action: string,
|
||||||
|
): void {
|
||||||
const pubkeyHash = hash160(pubkey);
|
const pubkeyHash = hash160(pubkey);
|
||||||
|
|
||||||
const decompiled = bscript.decompile(script);
|
const decompiled = bscript.decompile(script);
|
||||||
|
@ -641,7 +683,7 @@ function checkScriptForPubkey(pubkey: Buffer, script: Buffer): void {
|
||||||
|
|
||||||
if (!hasKey) {
|
if (!hasKey) {
|
||||||
throw new Error(
|
throw new Error(
|
||||||
`Can not sign for this input with the key ${pubkey.toString('hex')}`,
|
`Can not ${action} for this input with the key ${pubkey.toString('hex')}`,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
1
types/psbt.d.ts
vendored
1
types/psbt.d.ts
vendored
|
@ -29,6 +29,7 @@ export declare class Psbt extends PsbtBase {
|
||||||
inputResults: boolean[];
|
inputResults: boolean[];
|
||||||
};
|
};
|
||||||
finalizeInput(inputIndex: number): boolean;
|
finalizeInput(inputIndex: number): boolean;
|
||||||
|
validateSignatures(inputIndex: number, pubkey?: Buffer): boolean;
|
||||||
signInput(inputIndex: number, keyPair: Signer): this;
|
signInput(inputIndex: number, keyPair: Signer): this;
|
||||||
signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise<void>;
|
signInputAsync(inputIndex: number, keyPair: SignerAsync): Promise<void>;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue