wip
This commit is contained in:
parent
80762543e7
commit
a793166eb8
3 changed files with 130 additions and 35 deletions
|
@ -13,9 +13,20 @@ var Transaction = require('./transaction')
|
||||||
|
|
||||||
// inspects a scriptSig w/ optional redeemScript and
|
// inspects a scriptSig w/ optional redeemScript and
|
||||||
// derives any input information required
|
// derives any input information required
|
||||||
function expandInput (scriptSig, redeemScript) {
|
function expandInput (scriptSig, redeemScript, witnessStack) {
|
||||||
var scriptSigChunks = bscript.decompile(scriptSig)
|
var witnessType
|
||||||
var prevOutType = bscript.classifyInput(scriptSigChunks, true)
|
if (witnessStack) {
|
||||||
|
witnessType = bscript.classifyWitness(witnessStack)
|
||||||
|
}
|
||||||
|
|
||||||
|
var prevOutType, scriptSigChunks
|
||||||
|
if (scriptSig.length === 0 && witnessStack) {
|
||||||
|
prevOutType = witnessType
|
||||||
|
} else {
|
||||||
|
scriptSigChunks = bscript.decompile(scriptSig)
|
||||||
|
prevOutType = bscript.classifyInput(scriptSigChunks, true)
|
||||||
|
}
|
||||||
|
|
||||||
var pubKeys, signatures, prevOutScript
|
var pubKeys, signatures, prevOutScript
|
||||||
|
|
||||||
switch (prevOutType) {
|
switch (prevOutType) {
|
||||||
|
@ -31,8 +42,14 @@ function expandInput (scriptSig, redeemScript) {
|
||||||
result.redeemScriptType = result.prevOutType
|
result.redeemScriptType = result.prevOutType
|
||||||
result.prevOutScript = bscript.scriptHash.output.encode(bcrypto.hash160(redeemScript))
|
result.prevOutScript = bscript.scriptHash.output.encode(bcrypto.hash160(redeemScript))
|
||||||
result.prevOutType = scriptTypes.P2SH
|
result.prevOutType = scriptTypes.P2SH
|
||||||
|
result.witness = false
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
case scriptTypes.P2WPKH:
|
||||||
|
pubKeys = witnessStack.slice(1)
|
||||||
|
signatures = witnessStack.slice(0, 1)
|
||||||
|
break
|
||||||
|
|
||||||
case scriptTypes.P2PKH:
|
case scriptTypes.P2PKH:
|
||||||
// if (redeemScript) throw new Error('Nonstandard... P2SH(P2PKH)')
|
// if (redeemScript) throw new Error('Nonstandard... P2SH(P2PKH)')
|
||||||
pubKeys = scriptSigChunks.slice(1)
|
pubKeys = scriptSigChunks.slice(1)
|
||||||
|
@ -59,13 +76,19 @@ function expandInput (scriptSig, redeemScript) {
|
||||||
return chunk === ops.OP_0 ? undefined : chunk
|
return chunk === ops.OP_0 ? undefined : chunk
|
||||||
})
|
})
|
||||||
break
|
break
|
||||||
|
|
||||||
|
case scriptTypes.NONSTANDARD:
|
||||||
|
return { prevOutType: prevOutType, prevOutScript: EMPTY_SCRIPT }
|
||||||
|
|
||||||
|
default: return {}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
pubKeys: pubKeys,
|
pubKeys: pubKeys,
|
||||||
signatures: signatures,
|
signatures: signatures,
|
||||||
prevOutScript: prevOutScript,
|
prevOutScript: prevOutScript,
|
||||||
prevOutType: prevOutType
|
prevOutType: prevOutType,
|
||||||
|
witness: Boolean(witnessStack)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -123,6 +146,15 @@ function expandOutput (script, scriptType, ourPubKey) {
|
||||||
if (pkh1.equals(pkh2)) pubKeys = [ourPubKey]
|
if (pkh1.equals(pkh2)) pubKeys = [ourPubKey]
|
||||||
break
|
break
|
||||||
|
|
||||||
|
// does our hash160(pubKey) match the output scripts?
|
||||||
|
case scriptTypes.P2WPKH:
|
||||||
|
if (!ourPubKey) break
|
||||||
|
|
||||||
|
var wpkh1 = scriptChunks[1]
|
||||||
|
var wpkh2 = bcrypto.hash160(ourPubKey)
|
||||||
|
if (wpkh1.equals(wpkh2)) pubKeys = [ourPubKey]
|
||||||
|
break
|
||||||
|
|
||||||
case scriptTypes.P2PK:
|
case scriptTypes.P2PK:
|
||||||
pubKeys = scriptChunks.slice(0, 1)
|
pubKeys = scriptChunks.slice(0, 1)
|
||||||
break
|
break
|
||||||
|
@ -163,11 +195,15 @@ function prepareInput (input, kpPubKey, redeemScript) {
|
||||||
input.redeemScriptType = expanded.scriptType
|
input.redeemScriptType = expanded.scriptType
|
||||||
input.prevOutScript = input.prevOutScript || bscript.scriptHash.output.encode(redeemScriptHash)
|
input.prevOutScript = input.prevOutScript || bscript.scriptHash.output.encode(redeemScriptHash)
|
||||||
input.prevOutType = scriptTypes.P2SH
|
input.prevOutType = scriptTypes.P2SH
|
||||||
|
input.witness = false
|
||||||
|
|
||||||
// maybe we have some prevOut knowledge
|
// maybe we have some prevOut knowledge
|
||||||
} else if (input.prevOutType) {
|
} else if (input.prevOutType) {
|
||||||
// pay-to-scriptHash is not possible without a redeemScript
|
// embedded scripts are not possible without a redeemScript
|
||||||
if (input.prevOutType === scriptTypes.P2SH) throw new Error('PrevOutScript is P2SH, missing redeemScript')
|
if (input.prevOutType === scriptTypes.P2SH ||
|
||||||
|
input.prevOutType === scriptTypes.P2WSH) {
|
||||||
|
throw new Error('PrevOutScript is ' + input.prevOutType + ', requires redeemScript')
|
||||||
|
}
|
||||||
|
|
||||||
// try to derive missing information using our kpPubKey
|
// try to derive missing information using our kpPubKey
|
||||||
expanded = expandOutput(input.prevOutScript, input.prevOutType, kpPubKey)
|
expanded = expandOutput(input.prevOutScript, input.prevOutType, kpPubKey)
|
||||||
|
@ -175,6 +211,7 @@ function prepareInput (input, kpPubKey, redeemScript) {
|
||||||
|
|
||||||
input.pubKeys = expanded.pubKeys
|
input.pubKeys = expanded.pubKeys
|
||||||
input.signatures = expanded.signatures
|
input.signatures = expanded.signatures
|
||||||
|
input.witness = (input.prevOutScript === scriptTypes.P2WPKH)
|
||||||
|
|
||||||
// no prior knowledge, assume pubKeyHash
|
// no prior knowledge, assume pubKeyHash
|
||||||
} else {
|
} else {
|
||||||
|
@ -182,24 +219,31 @@ function prepareInput (input, kpPubKey, redeemScript) {
|
||||||
input.prevOutType = scriptTypes.P2PKH
|
input.prevOutType = scriptTypes.P2PKH
|
||||||
input.pubKeys = [kpPubKey]
|
input.pubKeys = [kpPubKey]
|
||||||
input.signatures = [undefined]
|
input.signatures = [undefined]
|
||||||
|
input.witness = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var EMPTY_SCRIPT = new Buffer(0)
|
||||||
|
|
||||||
function buildInput (input, allowIncomplete) {
|
function buildInput (input, allowIncomplete) {
|
||||||
var signatures = input.signatures
|
var signatures = input.signatures
|
||||||
var scriptType = input.redeemScriptType || input.prevOutType
|
var scriptType = input.redeemScriptType || input.prevOutType
|
||||||
var scriptSig
|
var stack
|
||||||
|
|
||||||
switch (scriptType) {
|
switch (scriptType) {
|
||||||
|
case scriptTypes.P2WPKH:
|
||||||
|
if (signatures.length < 1 || !signatures[0]) throw new Error('Not enough signatures provided')
|
||||||
|
stack = bscript.witnessPubKeyHash.input.encodeStack(signatures[0], input.pubKeys[0])
|
||||||
|
break
|
||||||
|
|
||||||
case scriptTypes.P2PKH:
|
case scriptTypes.P2PKH:
|
||||||
|
if (signatures.length < 1 || !signatures[0]) throw new Error('Not enough signatures provided')
|
||||||
|
stack = bscript.pubKeyHash.input.encodeStack(signatures[0], input.pubKeys[0])
|
||||||
|
break
|
||||||
|
|
||||||
case scriptTypes.P2PK:
|
case scriptTypes.P2PK:
|
||||||
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')
|
||||||
if (scriptType === scriptTypes.P2PKH) {
|
stack = bscript.pubKey.input.encodeStack(signatures[0])
|
||||||
scriptSig = bscript.pubKeyHash.input.encode(signatures[0], input.pubKeys[0])
|
|
||||||
} else {
|
|
||||||
scriptSig = bscript.pubKey.input.encode(signatures[0])
|
|
||||||
}
|
|
||||||
|
|
||||||
break
|
break
|
||||||
|
|
||||||
// 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
|
||||||
|
@ -213,18 +257,40 @@ function buildInput (input, allowIncomplete) {
|
||||||
signatures = signatures.filter(function (x) { return x !== ops.OP_0 })
|
signatures = signatures.filter(function (x) { return x !== ops.OP_0 })
|
||||||
}
|
}
|
||||||
|
|
||||||
scriptSig = bscript.multisig.input.encode(signatures, allowIncomplete ? undefined : input.redeemScript)
|
stack = bscript.multisig.input.encodeStack(signatures, allowIncomplete ? undefined : input.redeemScript)
|
||||||
break
|
break
|
||||||
|
|
||||||
default: return
|
default: return
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrap as scriptHash if necessary
|
var script, witness
|
||||||
if (input.prevOutType === scriptTypes.P2SH) {
|
|
||||||
scriptSig = bscript.scriptHash.input.encode(scriptSig, input.redeemScript)
|
// encode witness for P2WPKH if necessary
|
||||||
|
if (input.prevOutType === scriptTypes.P2WPKH ||
|
||||||
|
input.redeemScriptType === scriptTypes.P2WPKH) {
|
||||||
|
witness = stack
|
||||||
|
script = EMPTY_SCRIPT
|
||||||
}
|
}
|
||||||
|
|
||||||
return scriptSig
|
// no witness? plain old script!
|
||||||
|
if (witness === undefined) {
|
||||||
|
script = bscript.compilePushOnly(stack)
|
||||||
|
}
|
||||||
|
|
||||||
|
// wrap as scriptHash if necessary
|
||||||
|
if (input.prevOutType === scriptTypes.P2SH) {
|
||||||
|
script = bscript.scriptHash.input.encode(script, input.redeemScript)
|
||||||
|
}
|
||||||
|
|
||||||
|
// falsy is easier
|
||||||
|
if (script === EMPTY_SCRIPT) {
|
||||||
|
script = undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
script: script,
|
||||||
|
witness: witness
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function TransactionBuilder (network, maximumFeeRate) {
|
function TransactionBuilder (network, maximumFeeRate) {
|
||||||
|
@ -276,7 +342,8 @@ TransactionBuilder.fromTransaction = function (transaction, network) {
|
||||||
transaction.ins.forEach(function (txIn) {
|
transaction.ins.forEach(function (txIn) {
|
||||||
txb.__addInputUnsafe(txIn.hash, txIn.index, {
|
txb.__addInputUnsafe(txIn.hash, txIn.index, {
|
||||||
sequence: txIn.sequence,
|
sequence: txIn.sequence,
|
||||||
script: txIn.script
|
script: txIn.script,
|
||||||
|
witness: txIn.witness
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -328,7 +395,7 @@ TransactionBuilder.prototype.__addInputUnsafe = function (txHash, vout, options)
|
||||||
|
|
||||||
// derive what we can from the scriptSig
|
// derive what we can from the scriptSig
|
||||||
if (options.script !== undefined) {
|
if (options.script !== undefined) {
|
||||||
input = expandInput(options.script)
|
input = expandInput(options.script, null, options.witness)
|
||||||
}
|
}
|
||||||
|
|
||||||
// if an input value was given, retain it
|
// if an input value was given, retain it
|
||||||
|
@ -395,16 +462,14 @@ TransactionBuilder.prototype.__build = function (allowIncomplete) {
|
||||||
var scriptType = input.redeemScriptType || input.prevOutType
|
var scriptType = input.redeemScriptType || input.prevOutType
|
||||||
if (!scriptType && !allowIncomplete) throw new Error('Transaction is not complete')
|
if (!scriptType && !allowIncomplete) throw new Error('Transaction is not complete')
|
||||||
|
|
||||||
// build a scriptSig
|
var result = buildInput(input, allowIncomplete)
|
||||||
var scriptSig = buildInput(input, allowIncomplete)
|
|
||||||
|
|
||||||
// skip if no scriptSig exists
|
// skip if no result
|
||||||
if (!scriptSig) {
|
if (!result && allowIncomplete) return
|
||||||
if (!allowIncomplete) throw new Error(scriptType + ' not supported')
|
if (result && result.script) return tx.setInputScript(i, result.script)
|
||||||
return
|
if (result && result.witness) return tx.setWitness(i, result.witness)
|
||||||
}
|
|
||||||
|
|
||||||
tx.setInputScript(i, scriptSig)
|
throw new Error(scriptType + ' not supported')
|
||||||
})
|
})
|
||||||
|
|
||||||
if (!allowIncomplete) {
|
if (!allowIncomplete) {
|
||||||
|
@ -422,10 +487,11 @@ function canSign (input) {
|
||||||
input.pubKeys !== undefined &&
|
input.pubKeys !== undefined &&
|
||||||
input.signatures !== undefined &&
|
input.signatures !== undefined &&
|
||||||
input.signatures.length === input.pubKeys.length &&
|
input.signatures.length === input.pubKeys.length &&
|
||||||
input.pubKeys.length > 0
|
input.pubKeys.length > 0 &&
|
||||||
|
input.witness !== undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashType) {
|
TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashType, witnessValue) {
|
||||||
if (keyPair.network !== this.network) throw new Error('Inconsistent network')
|
if (keyPair.network !== this.network) throw new Error('Inconsistent network')
|
||||||
if (!this.inputs[vin]) throw new Error('No input at index: ' + vin)
|
if (!this.inputs[vin]) throw new Error('No input at index: ' + vin)
|
||||||
hashType = hashType || Transaction.SIGHASH_ALL
|
hashType = hashType || Transaction.SIGHASH_ALL
|
||||||
|
@ -441,14 +507,20 @@ TransactionBuilder.prototype.sign = function (vin, keyPair, redeemScript, hashTy
|
||||||
|
|
||||||
var kpPubKey = keyPair.getPublicKeyBuffer()
|
var kpPubKey = keyPair.getPublicKeyBuffer()
|
||||||
if (!canSign(input)) {
|
if (!canSign(input)) {
|
||||||
prepareInput(input, kpPubKey, redeemScript)
|
prepareInput(input, kpPubKey, redeemScript, witnessValue)
|
||||||
|
|
||||||
if (!canSign(input)) throw Error(input.prevOutType + ' not supported')
|
if (!canSign(input)) throw Error(input.prevOutType + ' not supported')
|
||||||
}
|
}
|
||||||
|
|
||||||
// ready to sign
|
// ready to sign
|
||||||
var hashScript = input.redeemScript || input.prevOutScript
|
var hashScript = input.redeemScript || input.prevOutScript
|
||||||
var signatureHash = this.tx.hashForSignature(vin, hashScript, hashType)
|
|
||||||
|
var signatureHash
|
||||||
|
if (input.witness) {
|
||||||
|
signatureHash = this.tx.hashForWitnessV0(vin, hashScript, witnessValue, hashType)
|
||||||
|
} else {
|
||||||
|
signatureHash = this.tx.hashForSignature(vin, hashScript, hashType)
|
||||||
|
}
|
||||||
|
|
||||||
// enforce in order signing of public keys
|
// enforce in order signing of public keys
|
||||||
var signed = input.pubKeys.some(function (pubKey, i) {
|
var signed = input.pubKeys.some(function (pubKey, i) {
|
||||||
|
|
25
test/fixtures/transaction_builder.json
vendored
25
test/fixtures/transaction_builder.json
vendored
|
@ -377,6 +377,29 @@
|
||||||
"value": 10000
|
"value": 10000
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"description": "Transaction w/ P2WPKH -> P2WPKH",
|
||||||
|
"txHex": "01000000000101ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0000000000ffffffff011027000000000000160014aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5024730440220092de398bfbd808f6a2315c184cebf2f872692853482781eaeb72057706ac31202200a666c0eba2da011d04499b071143124559cad32a063785d713f2e0d4a15945101210279be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f8179800000000",
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
"vout": 0,
|
||||||
|
"prevTxScript": "OP_0 751e76e8199196d454941c45d1b3a323f1433bd6",
|
||||||
|
"signs": [
|
||||||
|
{
|
||||||
|
"keyPair": "KwDiBf89QgGbjEhKnhXJuH7LrciVrZi3qYjgd9M7rFU73sVHnoWn",
|
||||||
|
"value": 10000
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"script": "OP_0 aa4d7985c57e011a8b3dd8e0e5a73aaef41629c5",
|
||||||
|
"value": 10000
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"fromTransaction": [
|
"fromTransaction": [
|
||||||
|
@ -852,7 +875,7 @@
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"exception": "PrevOutScript is P2SH, missing redeemScript",
|
"exception": "PrevOutScript is scripthash, requires redeemScript",
|
||||||
"inputs": [
|
"inputs": [
|
||||||
{
|
{
|
||||||
"txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
"txId": "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff",
|
||||||
|
|
|
@ -283,10 +283,10 @@ describe('TransactionBuilder', function () {
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!sign.throws) {
|
if (!sign.throws) {
|
||||||
txb.sign(index, keyPair, redeemScript, sign.hashType)
|
txb.sign(index, keyPair, redeemScript, sign.hashType, sign.value)
|
||||||
} else {
|
} else {
|
||||||
assert.throws(function () {
|
assert.throws(function () {
|
||||||
txb.sign(index, keyPair, redeemScript, sign.hashType)
|
txb.sign(index, keyPair, redeemScript, sign.hashType, sign.value)
|
||||||
}, new RegExp(f.exception))
|
}, new RegExp(f.exception))
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
Loading…
Reference in a new issue