Merge pull request #1416 from bitcoinjs/signTxb

Migrate to stricter type checks during sign
This commit is contained in:
Jonathan Underwood 2019-06-20 23:02:15 +09:00 committed by GitHub
commit 8bbe7c7178
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
15 changed files with 1969 additions and 747 deletions

View file

@ -13,6 +13,33 @@ const transaction_1 = require('./transaction');
const types = require('./types');
const typeforce = require('typeforce');
const SCRIPT_TYPES = classify.types;
const PREVOUT_TYPES = new Set([
// Raw
'p2pkh',
'p2pk',
'p2wpkh',
'p2ms',
// P2SH wrapped
'p2sh-p2pkh',
'p2sh-p2pk',
'p2sh-p2wpkh',
'p2sh-p2ms',
// P2WSH wrapped
'p2wsh-p2pkh',
'p2wsh-p2pk',
'p2wsh-p2ms',
// P2SH-P2WSH wrapper
'p2sh-p2wsh-p2pkh',
'p2sh-p2wsh-p2pk',
'p2sh-p2wsh-p2ms',
]);
function tfMessage(type, value, message) {
try {
typeforce(type, value);
} catch (err) {
throw new Error(message);
}
}
function txIsString(tx) {
return typeof tx === 'string' || tx instanceof String;
}
@ -118,74 +145,29 @@ class TransactionBuilder {
buildIncomplete() {
return this.__build(true);
}
sign(vin, keyPair, redeemScript, hashType, witnessValue, witnessScript) {
// TODO: remove keyPair.network matching in 4.0.0
if (keyPair.network && keyPair.network !== this.network)
throw new TypeError('Inconsistent network');
if (!this.__INPUTS[vin]) throw new Error('No input at index: ' + vin);
hashType = hashType || transaction_1.Transaction.SIGHASH_ALL;
if (this.__needsOutputs(hashType))
throw new Error('Transaction needs outputs');
const input = this.__INPUTS[vin];
// if redeemScript was previously provided, enforce consistency
if (
input.redeemScript !== undefined &&
redeemScript &&
!input.redeemScript.equals(redeemScript)
) {
throw new Error('Inconsistent redeemScript');
}
const ourPubKey = keyPair.publicKey || keyPair.getPublicKey();
if (!canSign(input)) {
if (witnessValue !== undefined) {
if (input.value !== undefined && input.value !== witnessValue)
throw new Error('Input did not match witnessValue');
typeforce(types.Satoshi, witnessValue);
input.value = witnessValue;
}
if (!canSign(input)) {
const prepared = prepareInput(
input,
ourPubKey,
redeemScript,
witnessScript,
);
// updates inline
Object.assign(input, prepared);
}
if (!canSign(input)) throw Error(input.prevOutType + ' not supported');
}
// ready to sign
let signatureHash;
if (input.hasWitness) {
signatureHash = this.__TX.hashForWitnessV0(
vin,
input.signScript,
input.value,
sign(
signParams,
keyPair,
redeemScript,
hashType,
witnessValue,
witnessScript,
) {
trySign(
getSigningData(
this.network,
this.__INPUTS,
this.__needsOutputs.bind(this),
this.__TX,
signParams,
keyPair,
redeemScript,
hashType,
);
} else {
signatureHash = this.__TX.hashForSignature(
vin,
input.signScript,
hashType,
);
}
// enforce in order signing of public keys
const signed = input.pubkeys.some((pubKey, i) => {
if (!ourPubKey.equals(pubKey)) return false;
if (input.signatures[i]) throw new Error('Signature already exists');
// TODO: add tests
if (ourPubKey.length !== 33 && input.hasWitness) {
throw new Error(
'BIP143 rejects uncompressed public keys in P2WPKH or P2WSH',
);
}
const signature = keyPair.sign(signatureHash, this.__USE_LOW_R);
input.signatures[i] = bscript.signature.encode(signature, hashType);
return true;
});
if (!signed) throw new Error('Key pair cannot sign for this input');
witnessValue,
witnessScript,
this.__USE_LOW_R,
),
);
}
__addInputUnsafe(txHash, vout, options) {
if (transaction_1.Transaction.isCoinbaseHash(txHash)) {
@ -746,3 +728,331 @@ function canSign(input) {
function signatureHashType(buffer) {
return buffer.readUInt8(buffer.length - 1);
}
function checkSignArgs(inputs, signParams) {
if (!PREVOUT_TYPES.has(signParams.prevOutScriptType)) {
throw new TypeError(
`Unknown prevOutScriptType "${signParams.prevOutScriptType}"`,
);
}
tfMessage(
typeforce.Number,
signParams.vin,
`sign must include vin parameter as Number (input index)`,
);
tfMessage(
types.Signer,
signParams.keyPair,
`sign must include keyPair parameter as Signer interface`,
);
tfMessage(
typeforce.maybe(typeforce.Number),
signParams.hashType,
`sign hashType parameter must be a number`,
);
const prevOutType = (inputs[signParams.vin] || []).prevOutType;
const posType = signParams.prevOutScriptType;
switch (posType) {
case 'p2pkh':
if (prevOutType && prevOutType !== 'pubkeyhash') {
throw new TypeError(
`input #${signParams.vin} is not of type p2pkh: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.redeemScript,
`${posType} requires NO redeemScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.witnessValue,
`${posType} requires NO witnessValue`,
);
break;
case 'p2pk':
if (prevOutType && prevOutType !== 'pubkey') {
throw new TypeError(
`input #${signParams.vin} is not of type p2pk: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.redeemScript,
`${posType} requires NO redeemScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.witnessValue,
`${posType} requires NO witnessValue`,
);
break;
case 'p2wpkh':
if (prevOutType && prevOutType !== 'witnesspubkeyhash') {
throw new TypeError(
`input #${signParams.vin} is not of type p2wpkh: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.redeemScript,
`${posType} requires NO redeemScript`,
);
tfMessage(
types.Satoshi,
signParams.witnessValue,
`${posType} requires witnessValue`,
);
break;
case 'p2ms':
if (prevOutType && prevOutType !== 'multisig') {
throw new TypeError(
`input #${signParams.vin} is not of type p2ms: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.redeemScript,
`${posType} requires NO redeemScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.witnessValue,
`${posType} requires NO witnessValue`,
);
break;
case 'p2sh-p2wpkh':
if (prevOutType && prevOutType !== 'scripthash') {
throw new TypeError(
`input #${signParams.vin} is not of type p2sh-p2wpkh: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.Buffer,
signParams.redeemScript,
`${posType} requires redeemScript`,
);
tfMessage(
types.Satoshi,
signParams.witnessValue,
`${posType} requires witnessValue`,
);
break;
case 'p2sh-p2ms':
case 'p2sh-p2pk':
case 'p2sh-p2pkh':
if (prevOutType && prevOutType !== 'scripthash') {
throw new TypeError(
`input #${signParams.vin} is not of type ${posType}: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.Buffer,
signParams.redeemScript,
`${posType} requires redeemScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.witnessValue,
`${posType} requires NO witnessValue`,
);
break;
case 'p2wsh-p2ms':
case 'p2wsh-p2pk':
case 'p2wsh-p2pkh':
if (prevOutType && prevOutType !== 'witnessscripthash') {
throw new TypeError(
`input #${signParams.vin} is not of type ${posType}: ${prevOutType}`,
);
}
tfMessage(
typeforce.Buffer,
signParams.witnessScript,
`${posType} requires witnessScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.redeemScript,
`${posType} requires NO redeemScript`,
);
tfMessage(
types.Satoshi,
signParams.witnessValue,
`${posType} requires witnessValue`,
);
break;
case 'p2sh-p2wsh-p2ms':
case 'p2sh-p2wsh-p2pk':
case 'p2sh-p2wsh-p2pkh':
if (prevOutType && prevOutType !== 'scripthash') {
throw new TypeError(
`input #${signParams.vin} is not of type ${posType}: ${prevOutType}`,
);
}
tfMessage(
typeforce.Buffer,
signParams.witnessScript,
`${posType} requires witnessScript`,
);
tfMessage(
typeforce.Buffer,
signParams.redeemScript,
`${posType} requires witnessScript`,
);
tfMessage(
types.Satoshi,
signParams.witnessValue,
`${posType} requires witnessScript`,
);
break;
}
}
function trySign({
input,
ourPubKey,
keyPair,
signatureHash,
hashType,
useLowR,
}) {
// enforce in order signing of public keys
let signed = false;
for (const [i, pubKey] of input.pubkeys.entries()) {
if (!ourPubKey.equals(pubKey)) continue;
if (input.signatures[i]) throw new Error('Signature already exists');
// TODO: add tests
if (ourPubKey.length !== 33 && input.hasWitness) {
throw new Error(
'BIP143 rejects uncompressed public keys in P2WPKH or P2WSH',
);
}
const signature = keyPair.sign(signatureHash, useLowR);
input.signatures[i] = bscript.signature.encode(signature, hashType);
signed = true;
}
if (!signed) throw new Error('Key pair cannot sign for this input');
}
function getSigningData(
network,
inputs,
needsOutputs,
tx,
signParams,
keyPair,
redeemScript,
hashType,
witnessValue,
witnessScript,
useLowR,
) {
let vin;
if (typeof signParams === 'number') {
console.warn(
'DEPRECATED: TransactionBuilder sign method arguments ' +
'will change in v6, please use the TxbSignArg interface',
);
vin = signParams;
} else if (typeof signParams === 'object') {
checkSignArgs(inputs, signParams);
({
vin,
keyPair,
redeemScript,
hashType,
witnessValue,
witnessScript,
} = signParams);
} else {
throw new TypeError(
'TransactionBuilder sign first arg must be TxbSignArg or number',
);
}
if (keyPair === undefined) {
throw new Error('sign requires keypair');
}
// TODO: remove keyPair.network matching in 4.0.0
if (keyPair.network && keyPair.network !== network)
throw new TypeError('Inconsistent network');
if (!inputs[vin]) throw new Error('No input at index: ' + vin);
hashType = hashType || transaction_1.Transaction.SIGHASH_ALL;
if (needsOutputs(hashType)) throw new Error('Transaction needs outputs');
const input = inputs[vin];
// if redeemScript was previously provided, enforce consistency
if (
input.redeemScript !== undefined &&
redeemScript &&
!input.redeemScript.equals(redeemScript)
) {
throw new Error('Inconsistent redeemScript');
}
const ourPubKey =
keyPair.publicKey || (keyPair.getPublicKey && keyPair.getPublicKey());
if (!canSign(input)) {
if (witnessValue !== undefined) {
if (input.value !== undefined && input.value !== witnessValue)
throw new Error('Input did not match witnessValue');
typeforce(types.Satoshi, witnessValue);
input.value = witnessValue;
}
if (!canSign(input)) {
const prepared = prepareInput(
input,
ourPubKey,
redeemScript,
witnessScript,
);
// updates inline
Object.assign(input, prepared);
}
if (!canSign(input)) throw Error(input.prevOutType + ' not supported');
}
// ready to sign
let signatureHash;
if (input.hasWitness) {
signatureHash = tx.hashForWitnessV0(
vin,
input.signScript,
input.value,
hashType,
);
} else {
signatureHash = tx.hashForSignature(vin, input.signScript, hashType);
}
return {
input,
ourPubKey,
keyPair,
signatureHash,
hashType,
useLowR: !!useLowR,
};
}

View file

@ -13,6 +13,14 @@ exports.BIP32Path = BIP32Path;
BIP32Path.toJSON = () => {
return 'BIP32 derivation path';
};
function Signer(obj) {
return (
(typeforce.Buffer(obj.publicKey) ||
typeof obj.getPublicKey === 'function') &&
typeof obj.sign === 'function'
);
}
exports.Signer = Signer;
const SATOSHI_MAX = 21 * 1e14;
function Satoshi(value) {
return typeforce.UInt53(value) && value <= SATOSHI_MAX;

File diff suppressed because it is too large Load diff

View file

@ -92,7 +92,11 @@ async function faucetComplex (output, value) {
const txvb = new bitcoin.TransactionBuilder(NETWORK)
txvb.addInput(unspent.txId, unspent.vout, null, p2pkh.output)
txvb.addOutput(output, value)
txvb.sign(0, keyPair)
txvb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair
})
const txv = txvb.build()
await broadcast(txv.toHex())

View file

@ -15,12 +15,29 @@ async function buildAndSign (depends, prevOutput, redeemScript, witnessScript) {
txb.addInput(unspent.txId, unspent.vout, null, prevOutput)
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4)
const posType = depends.prevOutScriptType
const needsValue = !!witnessScript || posType.slice(-6) === 'p2wpkh'
if (depends.signatures) {
keyPairs.forEach(keyPair => {
txb.sign(0, keyPair, redeemScript, null, unspent.value, witnessScript)
txb.sign({
prevOutScriptType: posType,
vin: 0,
keyPair,
redeemScript,
witnessValue: needsValue ? unspent.value : undefined,
witnessScript,
})
})
} else if (depends.signature) {
txb.sign(0, keyPairs[0], redeemScript, null, unspent.value, witnessScript)
txb.sign({
prevOutScriptType: posType,
vin: 0,
keyPair: keyPairs[0],
redeemScript,
witnessValue: needsValue ? unspent.value : undefined,
witnessScript,
})
}
return regtestUtils.broadcast(txb.build().toHex())
@ -41,12 +58,14 @@ async function buildAndSign (depends, prevOutput, redeemScript, witnessScript) {
describe('bitcoinjs-lib (payments - ' + k + ')', () => {
it('can broadcast as an output, and be spent as an input', async () => {
await buildAndSign(depends, output, null, null)
Object.assign(depends, { prevOutScriptType: k })
await buildAndSign(depends, output, undefined, undefined)
})
it('can (as P2SH(' + k + ')) broadcast as an output, and be spent as an input', async () => {
const p2sh = bitcoin.payments.p2sh({ redeem: { output }, network: NETWORK })
await buildAndSign(depends, p2sh.output, p2sh.redeem.output, null)
Object.assign(depends, { prevOutScriptType: 'p2sh-' + k })
await buildAndSign(depends, p2sh.output, p2sh.redeem.output, undefined)
})
// NOTE: P2WPKH cannot be wrapped in P2WSH, consensus fail
@ -54,13 +73,15 @@ async function buildAndSign (depends, prevOutput, redeemScript, witnessScript) {
it('can (as P2WSH(' + k + ')) broadcast as an output, and be spent as an input', async () => {
const p2wsh = bitcoin.payments.p2wsh({ redeem: { output }, network: NETWORK })
await buildAndSign(depends, p2wsh.output, null, p2wsh.redeem.output)
Object.assign(depends, { prevOutScriptType: 'p2wsh-' + k })
await buildAndSign(depends, p2wsh.output, undefined, p2wsh.redeem.output)
})
it('can (as P2SH(P2WSH(' + k + '))) broadcast as an output, and be spent as an input', async () => {
const p2wsh = bitcoin.payments.p2wsh({ redeem: { output }, network: NETWORK })
const p2sh = bitcoin.payments.p2sh({ redeem: { output: p2wsh.output }, network: NETWORK })
Object.assign(depends, { prevOutScriptType: 'p2sh-p2wsh-' + k })
await buildAndSign(depends, p2sh.output, p2sh.redeem.output, p2wsh.redeem.output)
})
})

View file

@ -18,7 +18,11 @@ describe('bitcoinjs-lib (transactions)', () => {
txb.addOutput('1cMh228HTCiwS8ZsaakH8A8wze1JR5ZsP', 12000)
// (in)15000 - (out)12000 = (fee)3000, this is the miner fee
txb.sign(0, alice)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair: alice
})
// prepare for broadcast to the Bitcoin network, see "can broadcast a Transaction" below
assert.strictEqual(txb.build().toHex(), '01000000019d344070eac3fe6e394a16d06d7704a7d5c0a10eb2a2c16bc98842b7cc20d561000000006b48304502210088828c0bdfcdca68d8ae0caeb6ec62cd3fd5f9b2191848edae33feb533df35d302202e0beadd35e17e7f83a733f5277028a9b453d525553e3f5d2d7a7aa8010a81d60121029f50f51d63b345039a290c94bffd3180c99ed659ff6ea6b1242bca47eb93b59fffffffff01e02e0000000000001976a91406afd46bcdfd22ef94ac122aa11f241244a37ecc88ac00000000')
@ -36,8 +40,16 @@ describe('bitcoinjs-lib (transactions)', () => {
txb.addOutput('1JtK9CQw1syfWj1WtFMWomrYdV3W2tWBF9', 170000)
// (in)(200000 + 300000) - (out)(180000 + 170000) = (fee)150000, this is the miner fee
txb.sign(1, bob) // Bob signs his input, which was the second input (1th)
txb.sign(0, alice) // Alice signs her input, which was the first input (0th)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 1,
keyPair: bob
}) // Bob signs his input, which was the second input (1th)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair: alice
}) // Alice signs her input, which was the first input (0th)
// prepare for broadcast to the Bitcoin network, see "can broadcast a Transaction" below
assert.strictEqual(txb.build().toHex(), '01000000024c94e48a870b85f41228d33cf25213dfcc8dd796e7211ed6b1f9a014809dbbb5060000006a473044022041450c258ce7cac7da97316bf2ea1ce66d88967c4df94f3e91f4c2a30f5d08cb02203674d516e6bb2b0afd084c3551614bd9cec3c2945231245e891b145f2d6951f0012103e05ce435e462ec503143305feb6c00e06a3ad52fbf939e85c65f3a765bb7baacffffffff3077d9de049574c3af9bc9c09a7c9db80f2d94caaf63988c9166249b955e867d000000006b483045022100aeb5f1332c79c446d3f906e4499b2e678500580a3f90329edf1ba502eec9402e022072c8b863f8c8d6c26f4c691ac9a6610aa4200edc697306648ee844cfbc089d7a012103df7940ee7cddd2f97763f67e1fb13488da3fbdd7f9c68ec5ef0864074745a289ffffffff0220bf0200000000001976a9147dd65592d0ab2fe0d0257d571abf032cd9db93dc88ac10980200000000001976a914c42e7ef92fdb603af844d064faad95db9bcdfd3d88ac00000000')
@ -65,8 +77,16 @@ describe('bitcoinjs-lib (transactions)', () => {
// (in)(5e4 + 7e4) - (out)(8e4 + 1e4) = (fee)3e4 = 30000, this is the miner fee
// Alice signs each input with the respective private keys
txb.sign(0, alice1)
txb.sign(1, alice2)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair: alice1
})
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 1,
keyPair: alice2
})
// build and broadcast our RegTest network
await regtestUtils.broadcast(txb.build().toHex())
@ -85,7 +105,11 @@ describe('bitcoinjs-lib (transactions)', () => {
txb.addInput(unspent.txId, unspent.vout)
txb.addOutput(embed.output, 1000)
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e5)
txb.sign(0, keyPair)
txb.sign({
prevOutScriptType: 'p2pkh',
vin: 0,
keyPair,
})
// build and broadcast to the RegTest network
await regtestUtils.broadcast(txb.build().toHex())
@ -108,8 +132,18 @@ describe('bitcoinjs-lib (transactions)', () => {
txb.addInput(unspent.txId, unspent.vout)
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 1e4)
txb.sign(0, keyPairs[0], p2sh.redeem.output)
txb.sign(0, keyPairs[2], p2sh.redeem.output)
txb.sign({
prevOutScriptType: 'p2sh-p2ms',
vin: 0,
keyPair: keyPairs[0],
redeemScript: p2sh.redeem.output,
})
txb.sign({
prevOutScriptType: 'p2sh-p2ms',
vin: 0,
keyPair: keyPairs[2],
redeemScript: p2sh.redeem.output,
})
const tx = txb.build()
// build and broadcast to the Bitcoin RegTest network
@ -133,7 +167,13 @@ describe('bitcoinjs-lib (transactions)', () => {
const txb = new bitcoin.TransactionBuilder(regtest)
txb.addInput(unspent.txId, unspent.vout)
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4)
txb.sign(0, keyPair, p2sh.redeem.output, null, unspent.value)
txb.sign({
prevOutScriptType: 'p2sh-p2wpkh',
vin: 0,
keyPair: keyPair,
redeemScript: p2sh.redeem.output,
witnessValue: unspent.value,
})
const tx = txb.build()
@ -158,7 +198,12 @@ describe('bitcoinjs-lib (transactions)', () => {
const txb = new bitcoin.TransactionBuilder(regtest)
txb.addInput(unspent.txId, unspent.vout, null, p2wpkh.output) // NOTE: provide the prevOutScript!
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4)
txb.sign(0, keyPair, null, null, unspent.value) // NOTE: no redeem script
txb.sign({
prevOutScriptType: 'p2wpkh',
vin: 0,
keyPair: keyPair,
witnessValue: unspent.value,
}) // NOTE: no redeem script
const tx = txb.build()
// build and broadcast (the P2WPKH transaction) to the Bitcoin RegTest network
@ -183,7 +228,13 @@ describe('bitcoinjs-lib (transactions)', () => {
const txb = new bitcoin.TransactionBuilder(regtest)
txb.addInput(unspent.txId, unspent.vout, null, p2wsh.output) // NOTE: provide the prevOutScript!
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 2e4)
txb.sign(0, keyPair, null, null, 5e4, p2wsh.redeem.output) // NOTE: provide a witnessScript!
txb.sign({
prevOutScriptType: 'p2wsh-p2pk',
vin: 0,
keyPair: keyPair,
witnessValue: 5e4,
witnessScript: p2wsh.redeem.output,
}) // NOTE: provide a witnessScript!
const tx = txb.build()
// build and broadcast (the P2WSH transaction) to the Bitcoin RegTest network
@ -215,9 +266,30 @@ describe('bitcoinjs-lib (transactions)', () => {
const txb = new bitcoin.TransactionBuilder(regtest)
txb.addInput(unspent.txId, unspent.vout, null, p2sh.output)
txb.addOutput(regtestUtils.RANDOM_ADDRESS, 3e4)
txb.sign(0, keyPairs[0], p2sh.redeem.output, null, unspent.value, p2wsh.redeem.output)
txb.sign(0, keyPairs[2], p2sh.redeem.output, null, unspent.value, p2wsh.redeem.output)
txb.sign(0, keyPairs[3], p2sh.redeem.output, null, unspent.value, p2wsh.redeem.output)
txb.sign({
prevOutScriptType: 'p2sh-p2wsh-p2ms',
vin: 0,
keyPair: keyPairs[0],
redeemScript: p2sh.redeem.output,
witnessValue: unspent.value,
witnessScript: p2wsh.redeem.output,
})
txb.sign({
prevOutScriptType: 'p2sh-p2wsh-p2ms',
vin: 0,
keyPair: keyPairs[2],
redeemScript: p2sh.redeem.output,
witnessValue: unspent.value,
witnessScript: p2wsh.redeem.output,
})
txb.sign({
prevOutScriptType: 'p2sh-p2wsh-p2ms',
vin: 0,
keyPair: keyPairs[3],
redeemScript: p2sh.redeem.output,
witnessValue: unspent.value,
witnessScript: p2wsh.redeem.output,
})
const tx = txb.build()

File diff suppressed because it is too large Load diff

View file

@ -19,15 +19,26 @@ interface ECPairOptions {
rng?(arg0: number): Buffer;
}
export interface ECPairInterface {
export interface Signer {
publicKey: Buffer;
network?: Network;
sign(hash: Buffer, lowR?: boolean): Buffer;
getPublicKey?(): Buffer;
}
export interface SignerAsync {
publicKey: Buffer;
network?: Network;
sign(hash: Buffer, lowR?: boolean): Promise<Buffer>;
getPublicKey?(): Buffer;
}
export interface ECPairInterface extends Signer {
compressed: boolean;
network: Network;
publicKey: Buffer;
privateKey?: Buffer;
toWIF(): string;
sign(hash: Buffer, lowR?: boolean): Buffer;
verify(hash: Buffer, signature: Buffer): boolean;
getPublicKey?(): Buffer;
}
class ECPair implements ECPairInterface {

View file

@ -14,7 +14,7 @@ export { Transaction } from './transaction';
export { TransactionBuilder } from './transaction_builder';
export { BIP32Interface } from 'bip32';
export { ECPairInterface } from './ecpair';
export { ECPairInterface, Signer, SignerAsync } from './ecpair';
export { Network } from './networks';
export { Payment, PaymentOpts, Stack, StackElement } from './payments';
export { OpCode } from './script';

View file

@ -2,7 +2,7 @@ import * as baddress from './address';
import { reverseBuffer } from './bufferutils';
import * as classify from './classify';
import * as bcrypto from './crypto';
import { ECPairInterface } from './ecpair';
import { Signer } from './ecpair';
import * as ECPair from './ecpair';
import { Network } from './networks';
import * as networks from './networks';
@ -16,6 +16,27 @@ const typeforce = require('typeforce');
const SCRIPT_TYPES = classify.types;
const PREVOUT_TYPES: Set<string> = new Set([
// Raw
'p2pkh',
'p2pk',
'p2wpkh',
'p2ms',
// P2SH wrapped
'p2sh-p2pkh',
'p2sh-p2pk',
'p2sh-p2wpkh',
'p2sh-p2ms',
// P2WSH wrapped
'p2wsh-p2pkh',
'p2wsh-p2pk',
'p2wsh-p2ms',
// P2SH-P2WSH wrapper
'p2sh-p2wsh-p2pkh',
'p2sh-p2wsh-p2pk',
'p2sh-p2wsh-p2ms',
]);
type MaybeBuffer = Buffer | undefined;
type TxbSignatures = Buffer[] | MaybeBuffer[];
type TxbPubkeys = MaybeBuffer[];
@ -50,6 +71,24 @@ interface TxbOutput {
maxSignatures?: number;
}
interface TxbSignArg {
prevOutScriptType: string;
vin: number;
keyPair: Signer;
redeemScript?: Buffer;
hashType?: number;
witnessValue?: number;
witnessScript?: Buffer;
}
function tfMessage(type: any, value: any, message: string): void {
try {
typeforce(type, value);
} catch (err) {
throw new Error(message);
}
}
function txIsString(tx: Buffer | string | Transaction): tx is string {
return typeof tx === 'string' || tx instanceof String;
}
@ -197,92 +236,28 @@ export class TransactionBuilder {
}
sign(
vin: number,
keyPair: ECPairInterface,
signParams: number | TxbSignArg,
keyPair?: Signer,
redeemScript?: Buffer,
hashType?: number,
witnessValue?: number,
witnessScript?: Buffer,
): void {
// TODO: remove keyPair.network matching in 4.0.0
if (keyPair.network && keyPair.network !== this.network)
throw new TypeError('Inconsistent network');
if (!this.__INPUTS[vin]) throw new Error('No input at index: ' + vin);
hashType = hashType || Transaction.SIGHASH_ALL;
if (this.__needsOutputs(hashType))
throw new Error('Transaction needs outputs');
const input = this.__INPUTS[vin];
// if redeemScript was previously provided, enforce consistency
if (
input.redeemScript !== undefined &&
redeemScript &&
!input.redeemScript.equals(redeemScript)
) {
throw new Error('Inconsistent redeemScript');
}
const ourPubKey = keyPair.publicKey || keyPair.getPublicKey!();
if (!canSign(input)) {
if (witnessValue !== undefined) {
if (input.value !== undefined && input.value !== witnessValue)
throw new Error('Input did not match witnessValue');
typeforce(types.Satoshi, witnessValue);
input.value = witnessValue;
}
if (!canSign(input)) {
const prepared = prepareInput(
input,
ourPubKey,
redeemScript,
witnessScript,
);
// updates inline
Object.assign(input, prepared);
}
if (!canSign(input)) throw Error(input.prevOutType + ' not supported');
}
// ready to sign
let signatureHash: Buffer;
if (input.hasWitness) {
signatureHash = this.__TX.hashForWitnessV0(
vin,
input.signScript as Buffer,
input.value as number,
trySign(
getSigningData(
this.network,
this.__INPUTS,
this.__needsOutputs.bind(this),
this.__TX,
signParams,
keyPair,
redeemScript,
hashType,
);
} else {
signatureHash = this.__TX.hashForSignature(
vin,
input.signScript as Buffer,
hashType,
);
}
// enforce in order signing of public keys
const signed = input.pubkeys!.some((pubKey, i) => {
if (!ourPubKey.equals(pubKey!)) return false;
if (input.signatures![i]) throw new Error('Signature already exists');
// TODO: add tests
if (ourPubKey.length !== 33 && input.hasWitness) {
throw new Error(
'BIP143 rejects uncompressed public keys in P2WPKH or P2WSH',
);
}
const signature = keyPair.sign(signatureHash, this.__USE_LOW_R);
input.signatures![i] = bscript.signature.encode(signature, hashType!);
return true;
});
if (!signed) throw new Error('Key pair cannot sign for this input');
witnessValue,
witnessScript,
this.__USE_LOW_R,
),
);
}
private __addInputUnsafe(
@ -976,3 +951,361 @@ function canSign(input: TxbInput): boolean {
function signatureHashType(buffer: Buffer): number {
return buffer.readUInt8(buffer.length - 1);
}
function checkSignArgs(inputs: TxbInput[], signParams: TxbSignArg): void {
if (!PREVOUT_TYPES.has(signParams.prevOutScriptType)) {
throw new TypeError(
`Unknown prevOutScriptType "${signParams.prevOutScriptType}"`,
);
}
tfMessage(
typeforce.Number,
signParams.vin,
`sign must include vin parameter as Number (input index)`,
);
tfMessage(
types.Signer,
signParams.keyPair,
`sign must include keyPair parameter as Signer interface`,
);
tfMessage(
typeforce.maybe(typeforce.Number),
signParams.hashType,
`sign hashType parameter must be a number`,
);
const prevOutType = (inputs[signParams.vin] || []).prevOutType;
const posType = signParams.prevOutScriptType;
switch (posType) {
case 'p2pkh':
if (prevOutType && prevOutType !== 'pubkeyhash') {
throw new TypeError(
`input #${signParams.vin} is not of type p2pkh: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.redeemScript,
`${posType} requires NO redeemScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.witnessValue,
`${posType} requires NO witnessValue`,
);
break;
case 'p2pk':
if (prevOutType && prevOutType !== 'pubkey') {
throw new TypeError(
`input #${signParams.vin} is not of type p2pk: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.redeemScript,
`${posType} requires NO redeemScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.witnessValue,
`${posType} requires NO witnessValue`,
);
break;
case 'p2wpkh':
if (prevOutType && prevOutType !== 'witnesspubkeyhash') {
throw new TypeError(
`input #${signParams.vin} is not of type p2wpkh: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.redeemScript,
`${posType} requires NO redeemScript`,
);
tfMessage(
types.Satoshi,
signParams.witnessValue,
`${posType} requires witnessValue`,
);
break;
case 'p2ms':
if (prevOutType && prevOutType !== 'multisig') {
throw new TypeError(
`input #${signParams.vin} is not of type p2ms: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.redeemScript,
`${posType} requires NO redeemScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.witnessValue,
`${posType} requires NO witnessValue`,
);
break;
case 'p2sh-p2wpkh':
if (prevOutType && prevOutType !== 'scripthash') {
throw new TypeError(
`input #${signParams.vin} is not of type p2sh-p2wpkh: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.Buffer,
signParams.redeemScript,
`${posType} requires redeemScript`,
);
tfMessage(
types.Satoshi,
signParams.witnessValue,
`${posType} requires witnessValue`,
);
break;
case 'p2sh-p2ms':
case 'p2sh-p2pk':
case 'p2sh-p2pkh':
if (prevOutType && prevOutType !== 'scripthash') {
throw new TypeError(
`input #${signParams.vin} is not of type ${posType}: ${prevOutType}`,
);
}
tfMessage(
typeforce.value(undefined),
signParams.witnessScript,
`${posType} requires NO witnessScript`,
);
tfMessage(
typeforce.Buffer,
signParams.redeemScript,
`${posType} requires redeemScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.witnessValue,
`${posType} requires NO witnessValue`,
);
break;
case 'p2wsh-p2ms':
case 'p2wsh-p2pk':
case 'p2wsh-p2pkh':
if (prevOutType && prevOutType !== 'witnessscripthash') {
throw new TypeError(
`input #${signParams.vin} is not of type ${posType}: ${prevOutType}`,
);
}
tfMessage(
typeforce.Buffer,
signParams.witnessScript,
`${posType} requires witnessScript`,
);
tfMessage(
typeforce.value(undefined),
signParams.redeemScript,
`${posType} requires NO redeemScript`,
);
tfMessage(
types.Satoshi,
signParams.witnessValue,
`${posType} requires witnessValue`,
);
break;
case 'p2sh-p2wsh-p2ms':
case 'p2sh-p2wsh-p2pk':
case 'p2sh-p2wsh-p2pkh':
if (prevOutType && prevOutType !== 'scripthash') {
throw new TypeError(
`input #${signParams.vin} is not of type ${posType}: ${prevOutType}`,
);
}
tfMessage(
typeforce.Buffer,
signParams.witnessScript,
`${posType} requires witnessScript`,
);
tfMessage(
typeforce.Buffer,
signParams.redeemScript,
`${posType} requires witnessScript`,
);
tfMessage(
types.Satoshi,
signParams.witnessValue,
`${posType} requires witnessScript`,
);
break;
}
}
function trySign({
input,
ourPubKey,
keyPair,
signatureHash,
hashType,
useLowR,
}: SigningData): void {
// enforce in order signing of public keys
let signed = false;
for (const [i, pubKey] of input.pubkeys!.entries()) {
if (!ourPubKey.equals(pubKey!)) continue;
if (input.signatures![i]) throw new Error('Signature already exists');
// TODO: add tests
if (ourPubKey.length !== 33 && input.hasWitness) {
throw new Error(
'BIP143 rejects uncompressed public keys in P2WPKH or P2WSH',
);
}
const signature = keyPair.sign(signatureHash, useLowR);
input.signatures![i] = bscript.signature.encode(signature, hashType);
signed = true;
}
if (!signed) throw new Error('Key pair cannot sign for this input');
}
interface SigningData {
input: TxbInput;
ourPubKey: Buffer;
keyPair: Signer;
signatureHash: Buffer;
hashType: number;
useLowR: boolean;
}
type HashTypeCheck = (hashType: number) => boolean;
function getSigningData(
network: Network,
inputs: TxbInput[],
needsOutputs: HashTypeCheck,
tx: Transaction,
signParams: number | TxbSignArg,
keyPair?: Signer,
redeemScript?: Buffer,
hashType?: number,
witnessValue?: number,
witnessScript?: Buffer,
useLowR?: boolean,
): SigningData {
let vin: number;
if (typeof signParams === 'number') {
console.warn(
'DEPRECATED: TransactionBuilder sign method arguments ' +
'will change in v6, please use the TxbSignArg interface',
);
vin = signParams;
} else if (typeof signParams === 'object') {
checkSignArgs(inputs, signParams);
({
vin,
keyPair,
redeemScript,
hashType,
witnessValue,
witnessScript,
} = signParams);
} else {
throw new TypeError(
'TransactionBuilder sign first arg must be TxbSignArg or number',
);
}
if (keyPair === undefined) {
throw new Error('sign requires keypair');
}
// TODO: remove keyPair.network matching in 4.0.0
if (keyPair.network && keyPair.network !== network)
throw new TypeError('Inconsistent network');
if (!inputs[vin]) throw new Error('No input at index: ' + vin);
hashType = hashType || Transaction.SIGHASH_ALL;
if (needsOutputs(hashType)) throw new Error('Transaction needs outputs');
const input = inputs[vin];
// if redeemScript was previously provided, enforce consistency
if (
input.redeemScript !== undefined &&
redeemScript &&
!input.redeemScript.equals(redeemScript)
) {
throw new Error('Inconsistent redeemScript');
}
const ourPubKey =
keyPair.publicKey || (keyPair.getPublicKey && keyPair.getPublicKey());
if (!canSign(input)) {
if (witnessValue !== undefined) {
if (input.value !== undefined && input.value !== witnessValue)
throw new Error('Input did not match witnessValue');
typeforce(types.Satoshi, witnessValue);
input.value = witnessValue;
}
if (!canSign(input)) {
const prepared = prepareInput(
input,
ourPubKey,
redeemScript,
witnessScript,
);
// updates inline
Object.assign(input, prepared);
}
if (!canSign(input)) throw Error(input.prevOutType + ' not supported');
}
// ready to sign
let signatureHash: Buffer;
if (input.hasWitness) {
signatureHash = tx.hashForWitnessV0(
vin,
input.signScript as Buffer,
input.value as number,
hashType,
);
} else {
signatureHash = tx.hashForSignature(
vin,
input.signScript as Buffer,
hashType,
);
}
return {
input,
ourPubKey,
keyPair,
signatureHash,
hashType,
useLowR: !!useLowR,
};
}

View file

@ -12,6 +12,14 @@ BIP32Path.toJSON = (): string => {
return 'BIP32 derivation path';
};
export function Signer(obj: any): boolean {
return (
(typeforce.Buffer(obj.publicKey) ||
typeof obj.getPublicKey === 'function') &&
typeof obj.sign === 'function'
);
}
const SATOSHI_MAX: number = 21 * 1e14;
export function Satoshi(value: number): boolean {
return typeforce.UInt53(value) && value <= SATOSHI_MAX;

17
types/ecpair.d.ts vendored
View file

@ -5,15 +5,24 @@ interface ECPairOptions {
network?: Network;
rng?(arg0: number): Buffer;
}
export interface ECPairInterface {
export interface Signer {
publicKey: Buffer;
network?: Network;
sign(hash: Buffer, lowR?: boolean): Buffer;
getPublicKey?(): Buffer;
}
export interface SignerAsync {
publicKey: Buffer;
network?: Network;
sign(hash: Buffer, lowR?: boolean): Promise<Buffer>;
getPublicKey?(): Buffer;
}
export interface ECPairInterface extends Signer {
compressed: boolean;
network: Network;
publicKey: Buffer;
privateKey?: Buffer;
toWIF(): string;
sign(hash: Buffer, lowR?: boolean): Buffer;
verify(hash: Buffer, signature: Buffer): boolean;
getPublicKey?(): Buffer;
}
declare class ECPair implements ECPairInterface {
private __D?;

2
types/index.d.ts vendored
View file

@ -11,7 +11,7 @@ export { OPS as opcodes } from './script';
export { Transaction } from './transaction';
export { TransactionBuilder } from './transaction_builder';
export { BIP32Interface } from 'bip32';
export { ECPairInterface } from './ecpair';
export { ECPairInterface, Signer, SignerAsync } from './ecpair';
export { Network } from './networks';
export { Payment, PaymentOpts, Stack, StackElement } from './payments';
export { OpCode } from './script';

View file

@ -1,7 +1,16 @@
/// <reference types="node" />
import { ECPairInterface } from './ecpair';
import { Signer } from './ecpair';
import { Network } from './networks';
import { Transaction } from './transaction';
interface TxbSignArg {
prevOutScriptType: string;
vin: number;
keyPair: Signer;
redeemScript?: Buffer;
hashType?: number;
witnessValue?: number;
witnessScript?: Buffer;
}
export declare class TransactionBuilder {
network: Network;
maximumFeeRate: number;
@ -18,7 +27,7 @@ export declare class TransactionBuilder {
addOutput(scriptPubKey: string | Buffer, value: number): number;
build(): Transaction;
buildIncomplete(): Transaction;
sign(vin: number, keyPair: ECPairInterface, redeemScript?: Buffer, hashType?: number, witnessValue?: number, witnessScript?: Buffer): void;
sign(signParams: number | TxbSignArg, keyPair?: Signer, redeemScript?: Buffer, hashType?: number, witnessValue?: number, witnessScript?: Buffer): void;
private __addInputUnsafe;
private __build;
private __canModifyInputs;
@ -26,3 +35,4 @@ export declare class TransactionBuilder {
private __canModifyOutputs;
private __overMaximumFees;
}
export {};

1
types/types.d.ts vendored
View file

@ -3,6 +3,7 @@ export declare function BIP32Path(value: string): boolean;
export declare namespace BIP32Path {
var toJSON: () => string;
}
export declare function Signer(obj: any): boolean;
export declare function Satoshi(value: number): boolean;
export declare const ECPoint: any;
export declare const Network: any;