Merge pull request #1416 from bitcoinjs/signTxb
Migrate to stricter type checks during sign
This commit is contained in:
commit
8bbe7c7178
15 changed files with 1969 additions and 747 deletions
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
400
test/fixtures/transaction_builder.json
vendored
400
test/fixtures/transaction_builder.json
vendored
File diff suppressed because it is too large
Load diff
|
@ -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())
|
||||
|
|
|
@ -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)
|
||||
})
|
||||
})
|
||||
|
|
|
@ -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
|
@ -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 {
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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
17
types/ecpair.d.ts
vendored
|
@ -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
2
types/index.d.ts
vendored
|
@ -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';
|
||||
|
|
14
types/transaction_builder.d.ts
vendored
14
types/transaction_builder.d.ts
vendored
|
@ -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
1
types/types.d.ts
vendored
|
@ -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;
|
||||
|
|
Loading…
Reference in a new issue