TransactionBuilder: remove hashTypes inconsistency issues, resolves #642
This commit is contained in:
parent
30ba4d620b
commit
3de754a9a2
2 changed files with 46 additions and 87 deletions
|
@ -16,7 +16,7 @@ var Transaction = require('./transaction')
|
||||||
function expandInput (scriptSig, redeemScript) {
|
function expandInput (scriptSig, redeemScript) {
|
||||||
var scriptSigChunks = bscript.decompile(scriptSig)
|
var scriptSigChunks = bscript.decompile(scriptSig)
|
||||||
var prevOutType = bscript.classifyInput(scriptSigChunks, true)
|
var prevOutType = bscript.classifyInput(scriptSigChunks, true)
|
||||||
var hashType, pubKeys, signatures, prevOutScript
|
var pubKeys, signatures, prevOutScript
|
||||||
|
|
||||||
switch (prevOutType) {
|
switch (prevOutType) {
|
||||||
case 'scripthash':
|
case 'scripthash':
|
||||||
|
@ -35,10 +35,8 @@ function expandInput (scriptSig, redeemScript) {
|
||||||
|
|
||||||
case 'pubkeyhash':
|
case 'pubkeyhash':
|
||||||
// if (redeemScript) throw new Error('Nonstandard... P2SH(P2PKH)')
|
// if (redeemScript) throw new Error('Nonstandard... P2SH(P2PKH)')
|
||||||
var s = ECSignature.parseScriptSignature(scriptSigChunks[0])
|
|
||||||
hashType = s.hashType
|
|
||||||
pubKeys = scriptSigChunks.slice(1)
|
pubKeys = scriptSigChunks.slice(1)
|
||||||
signatures = [s.signature]
|
signatures = scriptSigChunks.slice(0, 1)
|
||||||
|
|
||||||
if (redeemScript) break
|
if (redeemScript) break
|
||||||
prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(pubKeys[0]))
|
prevOutScript = bscript.pubKeyHashOutput(bcrypto.hash160(pubKeys[0]))
|
||||||
|
@ -49,9 +47,7 @@ function expandInput (scriptSig, redeemScript) {
|
||||||
pubKeys = bscript.decompile(redeemScript).slice(0, 1)
|
pubKeys = bscript.decompile(redeemScript).slice(0, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
var ss = ECSignature.parseScriptSignature(scriptSigChunks[0])
|
signatures = scriptSigChunks.slice(0, 1)
|
||||||
hashType = ss.hashType
|
|
||||||
signatures = [ss.signature]
|
|
||||||
break
|
break
|
||||||
|
|
||||||
case 'multisig':
|
case 'multisig':
|
||||||
|
@ -60,23 +56,12 @@ function expandInput (scriptSig, redeemScript) {
|
||||||
}
|
}
|
||||||
|
|
||||||
signatures = scriptSigChunks.slice(1).map(function (chunk) {
|
signatures = scriptSigChunks.slice(1).map(function (chunk) {
|
||||||
if (chunk === ops.OP_0) return undefined
|
return chunk === ops.OP_0 ? undefined : chunk
|
||||||
|
|
||||||
var sss = ECSignature.parseScriptSignature(chunk)
|
|
||||||
|
|
||||||
if (hashType !== undefined) {
|
|
||||||
if (sss.hashType !== hashType) throw new Error('Inconsistent hashType')
|
|
||||||
} else {
|
|
||||||
hashType = sss.hashType
|
|
||||||
}
|
|
||||||
|
|
||||||
return sss.signature
|
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
hashType: hashType,
|
|
||||||
pubKeys: pubKeys,
|
pubKeys: pubKeys,
|
||||||
signatures: signatures,
|
signatures: signatures,
|
||||||
prevOutScript: prevOutScript,
|
prevOutScript: prevOutScript,
|
||||||
|
@ -90,8 +75,6 @@ function fixMultisigOrder (input, transaction, vin) {
|
||||||
if (input.pubKeys.length === input.signatures.length) return
|
if (input.pubKeys.length === input.signatures.length) return
|
||||||
|
|
||||||
var unmatched = input.signatures.concat()
|
var unmatched = input.signatures.concat()
|
||||||
var hashType = input.hashType || Transaction.SIGHASH_ALL
|
|
||||||
var hash = transaction.hashForSignature(vin, input.redeemScript, hashType)
|
|
||||||
|
|
||||||
input.signatures = input.pubKeys.map(function (pubKey, y) {
|
input.signatures = input.pubKeys.map(function (pubKey, y) {
|
||||||
var keyPair = ECPair.fromPublicKeyBuffer(pubKey)
|
var keyPair = ECPair.fromPublicKeyBuffer(pubKey)
|
||||||
|
@ -102,8 +85,12 @@ function fixMultisigOrder (input, transaction, vin) {
|
||||||
// skip if undefined || OP_0
|
// skip if undefined || OP_0
|
||||||
if (!signature) return false
|
if (!signature) return false
|
||||||
|
|
||||||
|
// TODO: avoid O(n) hashForSignature
|
||||||
|
var parsed = ECSignature.parseScriptSignature(signature)
|
||||||
|
var hash = transaction.hashForSignature(vin, input.redeemScript, parsed.hashType)
|
||||||
|
|
||||||
// skip if signature does not match pubKey
|
// skip if signature does not match pubKey
|
||||||
if (!keyPair.verify(hash, signature)) return false
|
if (!keyPair.verify(hash, parsed.signature)) return false
|
||||||
|
|
||||||
// remove matched signature from unmatched
|
// remove matched signature from unmatched
|
||||||
unmatched[i] = undefined
|
unmatched[i] = undefined
|
||||||
|
@ -154,7 +141,7 @@ function expandOutput (script, scriptType, ourPubKey) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function prepareInput (input, kpPubKey, redeemScript, hashType) {
|
function prepareInput (input, kpPubKey, redeemScript) {
|
||||||
if (redeemScript) {
|
if (redeemScript) {
|
||||||
var redeemScriptHash = bcrypto.hash160(redeemScript)
|
var redeemScriptHash = bcrypto.hash160(redeemScript)
|
||||||
|
|
||||||
|
@ -196,8 +183,6 @@ function prepareInput (input, kpPubKey, redeemScript, hashType) {
|
||||||
input.pubKeys = [kpPubKey]
|
input.pubKeys = [kpPubKey]
|
||||||
input.signatures = [undefined]
|
input.signatures = [undefined]
|
||||||
}
|
}
|
||||||
|
|
||||||
input.hashType = hashType
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildInput (input, allowIncomplete) {
|
function buildInput (input, allowIncomplete) {
|
||||||
|
@ -209,12 +194,10 @@ function buildInput (input, allowIncomplete) {
|
||||||
case 'pubkeyhash':
|
case 'pubkeyhash':
|
||||||
case 'pubkey':
|
case 'pubkey':
|
||||||
if (signatures.length < 1 || !signatures[0]) throw new Error('Not enough signatures provided')
|
if (signatures.length < 1 || !signatures[0]) throw new Error('Not enough signatures provided')
|
||||||
|
|
||||||
var pkSignature = signatures[0].toScriptSignature(input.hashType)
|
|
||||||
if (scriptType === 'pubkeyhash') {
|
if (scriptType === 'pubkeyhash') {
|
||||||
scriptSig = bscript.pubKeyHashInput(pkSignature, input.pubKeys[0])
|
scriptSig = bscript.pubKeyHashInput(signatures[0], input.pubKeys[0])
|
||||||
} else {
|
} else {
|
||||||
scriptSig = bscript.pubKeyInput(pkSignature)
|
scriptSig = bscript.pubKeyInput(signatures[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
break
|
break
|
||||||
|
@ -222,7 +205,7 @@ function buildInput (input, allowIncomplete) {
|
||||||
// ref https://github.com/bitcoin/bitcoin/blob/d612837814020ae832499d18e6ee5eb919a87907/src/script/sign.cpp#L232
|
// ref https://github.com/bitcoin/bitcoin/blob/d612837814020ae832499d18e6ee5eb919a87907/src/script/sign.cpp#L232
|
||||||
case 'multisig':
|
case 'multisig':
|
||||||
signatures = signatures.map(function (signature) {
|
signatures = signatures.map(function (signature) {
|
||||||
return (signature && signature.toScriptSignature(input.hashType)) || ops.OP_0
|
return signature || ops.OP_0
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!allowIncomplete) {
|
if (!allowIncomplete) {
|
||||||
|
@ -408,8 +391,7 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) {
|
||||||
}
|
}
|
||||||
|
|
||||||
function canSign (input) {
|
function canSign (input) {
|
||||||
return input.hashType !== undefined &&
|
return input.prevOutScript !== undefined &&
|
||||||
input.prevOutScript !== undefined &&
|
|
||||||
input.pubKeys !== undefined &&
|
input.pubKeys !== undefined &&
|
||||||
input.signatures !== undefined &&
|
input.signatures !== undefined &&
|
||||||
input.signatures.length === input.pubKeys.length &&
|
input.signatures.length === input.pubKeys.length &&
|
||||||
|
@ -430,15 +412,9 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy
|
||||||
throw new Error('Inconsistent redeemScript')
|
throw new Error('Inconsistent redeemScript')
|
||||||
}
|
}
|
||||||
|
|
||||||
// if hashType was previously provided, enforce consistency
|
|
||||||
if (input.hashType !== undefined &&
|
|
||||||
input.hashType !== hashType) {
|
|
||||||
throw new Error('Inconsistent hashType')
|
|
||||||
}
|
|
||||||
|
|
||||||
var kpPubKey = keyPair.getPublicKeyBuffer()
|
var kpPubKey = keyPair.getPublicKeyBuffer()
|
||||||
if (!canSign(input)) {
|
if (!canSign(input)) {
|
||||||
prepareInput(input, kpPubKey, redeemScript, hashType)
|
prepareInput(input, kpPubKey, redeemScript)
|
||||||
|
|
||||||
if (!canSign(input)) throw Error(input.prevOutType + ' not supported')
|
if (!canSign(input)) throw Error(input.prevOutType + ' not supported')
|
||||||
}
|
}
|
||||||
|
@ -452,21 +428,30 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy
|
||||||
if (!kpPubKey.equals(pubKey)) return false
|
if (!kpPubKey.equals(pubKey)) return false
|
||||||
if (input.signatures[i]) throw new Error('Signature already exists')
|
if (input.signatures[i]) throw new Error('Signature already exists')
|
||||||
|
|
||||||
input.signatures[i] = keyPair.sign(signatureHash)
|
input.signatures[i] = keyPair.sign(signatureHash).toScriptSignature(hashType)
|
||||||
return true
|
return true
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!signed) throw new Error('Key pair cannot sign for this input')
|
if (!signed) throw new Error('Key pair cannot sign for this input')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function signatureHashType (buffer) {
|
||||||
|
return buffer.readUInt8(buffer.length - 1)
|
||||||
|
}
|
||||||
|
|
||||||
TransactionBuilder.prototype.__canModifyInputs = function () {
|
TransactionBuilder.prototype.__canModifyInputs = function () {
|
||||||
return this.inputs.every(function (otherInput) {
|
return this.inputs.every(function (input) {
|
||||||
// no signature
|
// any signatures?
|
||||||
if (otherInput.hashType === undefined) return true
|
if (input.signatures === undefined) return true
|
||||||
|
|
||||||
|
return input.signatures.every(function (signature) {
|
||||||
|
if (!signature) return true
|
||||||
|
var hashType = signatureHashType(signature)
|
||||||
|
|
||||||
// if SIGHASH_ANYONECANPAY is set, signatures would not
|
// if SIGHASH_ANYONECANPAY is set, signatures would not
|
||||||
// be invalidated by more inputs
|
// be invalidated by more inputs
|
||||||
return otherInput.hashType & Transaction.SIGHASH_ANYONECANPAY
|
return hashType & Transaction.SIGHASH_ANYONECANPAY
|
||||||
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -474,11 +459,14 @@ TransactionBuilder.prototype.__canModifyOutputs = function () {
|
||||||
var nInputs = this.tx.ins.length
|
var nInputs = this.tx.ins.length
|
||||||
var nOutputs = this.tx.outs.length
|
var nOutputs = this.tx.outs.length
|
||||||
|
|
||||||
return this.inputs.every(function (input, i) {
|
return this.inputs.every(function (input) {
|
||||||
// any signatures?
|
if (input.signatures === undefined) return true
|
||||||
if (input.hashType === undefined) return true
|
|
||||||
|
|
||||||
var hashTypeMod = input.hashType & 0x1f
|
return input.signatures.every(function (signature) {
|
||||||
|
if (!signature) return true
|
||||||
|
var hashType = signatureHashType(signature)
|
||||||
|
|
||||||
|
var hashTypeMod = hashType & 0x1f
|
||||||
if (hashTypeMod === Transaction.SIGHASH_NONE) return true
|
if (hashTypeMod === Transaction.SIGHASH_NONE) return true
|
||||||
if (hashTypeMod === Transaction.SIGHASH_SINGLE) {
|
if (hashTypeMod === Transaction.SIGHASH_SINGLE) {
|
||||||
// if SIGHASH_SINGLE is set, and nInputs > nOutputs
|
// if SIGHASH_SINGLE is set, and nInputs > nOutputs
|
||||||
|
@ -486,8 +474,7 @@ TransactionBuilder.prototype.__canModifyOutputs = function () {
|
||||||
// of more outputs
|
// of more outputs
|
||||||
return nInputs <= nOutputs
|
return nInputs <= nOutputs
|
||||||
}
|
}
|
||||||
|
})
|
||||||
return false
|
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
28
test/fixtures/transaction_builder.json
vendored
28
test/fixtures/transaction_builder.json
vendored
|
@ -834,34 +834,6 @@
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"exception": "Inconsistent hashType",
|
|
||||||
"network": "testnet",
|
|
||||||
"inputs": [
|
|
||||||
{
|
|
||||||
"txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
|
||||||
"vout": 0,
|
|
||||||
"signs": [
|
|
||||||
{
|
|
||||||
"redeemScript": "OP_2 0479be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798483ada7726a3c4655da4fbfc0e1108a8fd17b448a68554199c47d08ffb10d4b8 04c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee51ae168fea63dc339a3c58419466ceaeef7f632653266d0e1236431a950cfe52a OP_2 OP_CHECKMULTISIG",
|
|
||||||
"keyPair": "91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgwmaKkrx",
|
|
||||||
"hashType": 4
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"keyPair": "91avARGdfge8E4tZfYLoxeJ5sGBdNJQH4kvjJoQFacbgww7vXtT",
|
|
||||||
"hashType": 2,
|
|
||||||
"throws": true
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"outputs": [
|
|
||||||
{
|
|
||||||
"script": "OP_DUP OP_HASH160 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5 OP_EQUALVERIFY OP_CHECKSIG",
|
|
||||||
"value": 1000
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"exception": "RedeemScript not supported \"OP_HASH160 7f67f0521934a57d3039f77f9f32cf313f3ac74b OP_EQUAL\"",
|
"exception": "RedeemScript not supported \"OP_HASH160 7f67f0521934a57d3039f77f9f32cf313f3ac74b OP_EQUAL\"",
|
||||||
"inputs": [
|
"inputs": [
|
||||||
|
|
Loading…
Reference in a new issue