|
|
@ -7,16 +7,95 @@ var ECSignature = require('./ecsignature') |
|
|
|
var Script = require('./script') |
|
|
|
var Transaction = require('./transaction') |
|
|
|
|
|
|
|
function isCoinbase(txHash) { |
|
|
|
return Array.prototype.every.call(txHash, function(x) { |
|
|
|
return x === 0 |
|
|
|
}) |
|
|
|
} |
|
|
|
|
|
|
|
function extractInput(txIn) { |
|
|
|
var redeemScript |
|
|
|
var scriptSig = txIn.script |
|
|
|
var prevOutScript |
|
|
|
var prevOutType = scripts.classifyInput(scriptSig, true) |
|
|
|
var scriptType |
|
|
|
|
|
|
|
// Re-classify if scriptHash
|
|
|
|
if (prevOutType === 'scripthash') { |
|
|
|
redeemScript = Script.fromBuffer(scriptSig.chunks.slice(-1)[0]) |
|
|
|
prevOutScript = scripts.scriptHashOutput(redeemScript.getHash()) |
|
|
|
|
|
|
|
scriptSig = Script.fromChunks(scriptSig.chunks.slice(0, -1)) |
|
|
|
scriptType = scripts.classifyInput(scriptSig, true) |
|
|
|
|
|
|
|
} else { |
|
|
|
scriptType = prevOutType |
|
|
|
} |
|
|
|
|
|
|
|
// Extract hashType, pubKeys and signatures
|
|
|
|
var hashType, parsed, pubKeys, signatures |
|
|
|
|
|
|
|
switch (scriptType) { |
|
|
|
case 'pubkeyhash': { |
|
|
|
parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0]) |
|
|
|
hashType = parsed.hashType |
|
|
|
pubKeys = [ECPubKey.fromBuffer(scriptSig.chunks[1])] |
|
|
|
signatures = [parsed.signature] |
|
|
|
prevOutScript = pubKeys[0].getAddress().toOutputScript() |
|
|
|
|
|
|
|
break |
|
|
|
} |
|
|
|
|
|
|
|
case 'pubkey': { |
|
|
|
parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0]) |
|
|
|
hashType = parsed.hashType |
|
|
|
signatures = [parsed.signature] |
|
|
|
|
|
|
|
if (redeemScript) { |
|
|
|
pubKeys = [ECPubKey.fromBuffer(redeemScript.chunks[0])] |
|
|
|
} |
|
|
|
|
|
|
|
break |
|
|
|
} |
|
|
|
|
|
|
|
case 'multisig': { |
|
|
|
signatures = scriptSig.chunks.slice(1).map(function(chunk) { |
|
|
|
if (chunk === ops.OP_0) return chunk |
|
|
|
|
|
|
|
var parsed = ECSignature.parseScriptSignature(chunk) |
|
|
|
hashType = parsed.hashType |
|
|
|
|
|
|
|
return parsed.signature |
|
|
|
}) |
|
|
|
|
|
|
|
if (redeemScript) { |
|
|
|
pubKeys = redeemScript.chunks.slice(1, -2).map(ECPubKey.fromBuffer) |
|
|
|
} |
|
|
|
|
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
return { |
|
|
|
hashType: hashType, |
|
|
|
prevOutScript: prevOutScript, |
|
|
|
prevOutType: prevOutType, |
|
|
|
pubKeys: pubKeys, |
|
|
|
redeemScript: redeemScript, |
|
|
|
scriptType: scriptType, |
|
|
|
signatures: signatures |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
function TransactionBuilder() { |
|
|
|
this.prevOutMap = {} |
|
|
|
this.prevTxMap = {} |
|
|
|
this.prevOutScripts = {} |
|
|
|
this.prevOutTypes = {} |
|
|
|
|
|
|
|
this.signatures = [] |
|
|
|
this.inputs = [] |
|
|
|
this.tx = new Transaction() |
|
|
|
} |
|
|
|
|
|
|
|
// Static constructors
|
|
|
|
TransactionBuilder.fromTransaction = function(transaction) { |
|
|
|
var txb = new TransactionBuilder() |
|
|
|
|
|
|
@ -35,78 +114,19 @@ TransactionBuilder.fromTransaction = function(transaction) { |
|
|
|
}) |
|
|
|
|
|
|
|
// Extract/add signatures
|
|
|
|
transaction.ins.forEach(function(txIn, i) { |
|
|
|
txb.inputs = transaction.ins.map(function(txIn) { |
|
|
|
// TODO: remove me after testcase added
|
|
|
|
assert(!isCoinbase(txIn.hash), 'coinbase inputs not supported') |
|
|
|
|
|
|
|
// Ignore empty scripts
|
|
|
|
if (txIn.script.buffer.length === 0) return |
|
|
|
|
|
|
|
assert(!Array.prototype.every.call(txIn.hash, function(x) { |
|
|
|
return x === 0 |
|
|
|
}), 'coinbase inputs not supported') |
|
|
|
|
|
|
|
var redeemScript |
|
|
|
var scriptSig = txIn.script |
|
|
|
var scriptType = scripts.classifyInput(scriptSig, true) |
|
|
|
|
|
|
|
// Re-classify if P2SH
|
|
|
|
if (scriptType === 'scripthash') { |
|
|
|
redeemScript = Script.fromBuffer(scriptSig.chunks.slice(-1)[0]) |
|
|
|
scriptSig = Script.fromChunks(scriptSig.chunks.slice(0, -1)) |
|
|
|
|
|
|
|
scriptType = scripts.classifyInput(scriptSig, true) |
|
|
|
assert.equal(scripts.classifyOutput(redeemScript), scriptType, 'Non-matching scriptSig and scriptPubKey in input') |
|
|
|
} |
|
|
|
|
|
|
|
// Extract hashType, pubKeys and signatures
|
|
|
|
var hashType, parsed, pubKeys, signatures |
|
|
|
|
|
|
|
switch (scriptType) { |
|
|
|
case 'pubkeyhash': |
|
|
|
parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0]) |
|
|
|
var pubKey = ECPubKey.fromBuffer(scriptSig.chunks[1]) |
|
|
|
|
|
|
|
hashType = parsed.hashType |
|
|
|
pubKeys = [pubKey] |
|
|
|
signatures = [parsed.signature] |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
case 'multisig': |
|
|
|
parsed = scriptSig.chunks.slice(1).filter(function(chunk) { |
|
|
|
return chunk !== ops.OP_0 |
|
|
|
}).map(ECSignature.parseScriptSignature) |
|
|
|
|
|
|
|
hashType = parsed[0].hashType |
|
|
|
pubKeys = [] |
|
|
|
signatures = parsed.map(function(p) { return p.signature }) |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
case 'pubkey': |
|
|
|
parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0]) |
|
|
|
|
|
|
|
hashType = parsed.hashType |
|
|
|
pubKeys = [] |
|
|
|
signatures = [parsed.signature] |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
default: |
|
|
|
assert(false, scriptType + ' inputs not supported') |
|
|
|
} |
|
|
|
|
|
|
|
txb.signatures[i] = { |
|
|
|
hashType: hashType, |
|
|
|
pubKeys: pubKeys, |
|
|
|
redeemScript: redeemScript, |
|
|
|
scriptType: scriptType, |
|
|
|
signatures: signatures |
|
|
|
} |
|
|
|
return extractInput(txIn) |
|
|
|
}) |
|
|
|
|
|
|
|
return txb |
|
|
|
} |
|
|
|
|
|
|
|
// Operations
|
|
|
|
TransactionBuilder.prototype.addInput = function(prevTx, index, sequence, prevOutScript) { |
|
|
|
var prevOutHash |
|
|
|
|
|
|
@ -125,151 +145,220 @@ TransactionBuilder.prototype.addInput = function(prevTx, index, sequence, prevOu |
|
|
|
|
|
|
|
} |
|
|
|
|
|
|
|
var prevOutType |
|
|
|
if (prevOutScript !== undefined) { |
|
|
|
prevOutType = scripts.classifyOutput(prevOutScript) |
|
|
|
var input = {} |
|
|
|
if (prevOutScript) { |
|
|
|
var prevOutType = scripts.classifyOutput(prevOutScript) |
|
|
|
|
|
|
|
// if we can, extract pubKey information
|
|
|
|
switch (prevOutType) { |
|
|
|
case 'multisig': |
|
|
|
input.pubKeys = prevOutScript.chunks.slice(1, -2).map(ECPubKey.fromBuffer) |
|
|
|
break |
|
|
|
|
|
|
|
case 'pubkey': |
|
|
|
input.pubKeys = prevOutScript.chunks.slice(0, 1).map(ECPubKey.fromBuffer) |
|
|
|
break |
|
|
|
} |
|
|
|
|
|
|
|
if (prevOutType !== 'scripthash') { |
|
|
|
input.scriptType = prevOutType |
|
|
|
} |
|
|
|
|
|
|
|
assert.notEqual(prevOutType, 'nonstandard', 'PrevOutScript not supported (nonstandard)') |
|
|
|
input.prevOutScript = prevOutScript |
|
|
|
input.prevOutType = prevOutType |
|
|
|
} |
|
|
|
|
|
|
|
assert(this.signatures.every(function(input) { |
|
|
|
return input.hashType & Transaction.SIGHASH_ANYONECANPAY |
|
|
|
assert(this.inputs.every(function(input2) { |
|
|
|
if (input2.hashType === undefined) return true |
|
|
|
|
|
|
|
return input2.hashType & Transaction.SIGHASH_ANYONECANPAY |
|
|
|
}), 'No, this would invalidate signatures') |
|
|
|
|
|
|
|
var prevOut = prevOutHash.toString('hex') + ':' + index |
|
|
|
assert(!(prevOut in this.prevOutMap), 'Transaction is already an input') |
|
|
|
assert(!(prevOut in this.prevTxMap), 'Transaction is already an input') |
|
|
|
|
|
|
|
var vout = this.tx.addInput(prevOutHash, index, sequence) |
|
|
|
this.prevOutMap[prevOut] = true |
|
|
|
this.prevOutScripts[vout] = prevOutScript |
|
|
|
this.prevOutTypes[vout] = prevOutType |
|
|
|
var vin = this.tx.addInput(prevOutHash, index, sequence) |
|
|
|
this.inputs[vin] = input |
|
|
|
this.prevTxMap[prevOut] = vin |
|
|
|
|
|
|
|
return vout |
|
|
|
return vin |
|
|
|
} |
|
|
|
|
|
|
|
TransactionBuilder.prototype.addOutput = function(scriptPubKey, value) { |
|
|
|
assert(this.signatures.every(function(signature) { |
|
|
|
return (signature.hashType & 0x1f) === Transaction.SIGHASH_SINGLE |
|
|
|
assert(this.inputs.every(function(input) { |
|
|
|
if (input.hashType === undefined) return true |
|
|
|
|
|
|
|
return (input.hashType & 0x1f) === Transaction.SIGHASH_SINGLE |
|
|
|
}), 'No, this would invalidate signatures') |
|
|
|
|
|
|
|
return this.tx.addOutput(scriptPubKey, value) |
|
|
|
} |
|
|
|
|
|
|
|
TransactionBuilder.prototype.build = function() { |
|
|
|
return this.__build(false) |
|
|
|
} |
|
|
|
TransactionBuilder.prototype.build = function() { return this.__build(false) } |
|
|
|
TransactionBuilder.prototype.buildIncomplete = function() { return this.__build(true) } |
|
|
|
|
|
|
|
TransactionBuilder.prototype.buildIncomplete = function() { |
|
|
|
return this.__build(true) |
|
|
|
} |
|
|
|
var canSignTypes = { 'pubkeyhash': true, 'multisig': true, 'pubkey': true } |
|
|
|
|
|
|
|
TransactionBuilder.prototype.__build = function(allowIncomplete) { |
|
|
|
if (!allowIncomplete) { |
|
|
|
assert(this.tx.ins.length > 0, 'Transaction has no inputs') |
|
|
|
assert(this.tx.outs.length > 0, 'Transaction has no outputs') |
|
|
|
assert(this.signatures.length > 0, 'Transaction has no signatures') |
|
|
|
assert.equal(this.signatures.length, this.tx.ins.length, 'Transaction is missing signatures') |
|
|
|
} |
|
|
|
|
|
|
|
var tx = this.tx.clone() |
|
|
|
|
|
|
|
// Create script signatures from signature meta-data
|
|
|
|
this.signatures.forEach(function(input, index) { |
|
|
|
var scriptSig |
|
|
|
this.inputs.forEach(function(input, index) { |
|
|
|
var scriptType = input.scriptType |
|
|
|
var scriptSig |
|
|
|
|
|
|
|
var signatures = input.signatures.map(function(signature) { |
|
|
|
return signature.toScriptSignature(input.hashType) |
|
|
|
}) |
|
|
|
|
|
|
|
switch (scriptType) { |
|
|
|
case 'pubkeyhash': |
|
|
|
var signature = signatures[0] |
|
|
|
var pubKey = input.pubKeys[0] |
|
|
|
scriptSig = scripts.pubKeyHashInput(signature, pubKey) |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
case 'multisig': |
|
|
|
var redeemScript = allowIncomplete ? undefined : input.redeemScript |
|
|
|
scriptSig = scripts.multisigInput(signatures, redeemScript) |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
case 'pubkey': |
|
|
|
var signature = signatures[0] |
|
|
|
scriptSig = scripts.pubKeyInput(signature) |
|
|
|
|
|
|
|
break |
|
|
|
|
|
|
|
default: |
|
|
|
assert(false, scriptType + ' not supported') |
|
|
|
if (!allowIncomplete) { |
|
|
|
assert(!!scriptType, 'Transaction is not complete') |
|
|
|
assert(scriptType in canSignTypes, scriptType + ' not supported') |
|
|
|
assert(input.signatures, 'Transaction is missing signatures') |
|
|
|
} |
|
|
|
|
|
|
|
if (input.redeemScript) { |
|
|
|
scriptSig = scripts.scriptHashInput(scriptSig, input.redeemScript) |
|
|
|
if (input.signatures) { |
|
|
|
switch (scriptType) { |
|
|
|
case 'pubkeyhash': |
|
|
|
var pkhSignature = input.signatures[0].toScriptSignature(input.hashType) |
|
|
|
scriptSig = scripts.pubKeyHashInput(pkhSignature, input.pubKeys[0]) |
|
|
|
break |
|
|
|
|
|
|
|
case 'multisig': |
|
|
|
// Array.prototype.map is sparse-compatible
|
|
|
|
var msSignatures = input.signatures.map(function(signature) { |
|
|
|
return signature.toScriptSignature(input.hashType) |
|
|
|
}) |
|
|
|
|
|
|
|
// fill in blanks with OP_0
|
|
|
|
for (var i = 0; i < msSignatures.length; ++i) { |
|
|
|
if (msSignatures[i]) continue |
|
|
|
|
|
|
|
msSignatures[i] = ops.OP_0 |
|
|
|
} |
|
|
|
|
|
|
|
var redeemScript = allowIncomplete ? undefined : input.redeemScript |
|
|
|
scriptSig = scripts.multisigInput(msSignatures, redeemScript) |
|
|
|
break |
|
|
|
|
|
|
|
case 'pubkey': |
|
|
|
var pkSignature = input.signatures[0].toScriptSignature(input.hashType) |
|
|
|
scriptSig = scripts.pubKeyInput(pkSignature) |
|
|
|
break |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
tx.setInputScript(index, scriptSig) |
|
|
|
// did we build a scriptSig?
|
|
|
|
if (scriptSig) { |
|
|
|
// wrap as scriptHash if necessary
|
|
|
|
if (input.prevOutType === 'scripthash') { |
|
|
|
scriptSig = scripts.scriptHashInput(scriptSig, input.redeemScript) |
|
|
|
} |
|
|
|
|
|
|
|
tx.setInputScript(index, scriptSig) |
|
|
|
} |
|
|
|
}) |
|
|
|
|
|
|
|
return tx |
|
|
|
} |
|
|
|
|
|
|
|
TransactionBuilder.prototype.sign = function(index, privKey, redeemScript, hashType) { |
|
|
|
assert(this.tx.ins.length >= index, 'No input at index: ' + index) |
|
|
|
assert(index in this.inputs, 'No input at index: ' + index) |
|
|
|
hashType = hashType || Transaction.SIGHASH_ALL |
|
|
|
|
|
|
|
var prevOutScript = this.prevOutScripts[index] |
|
|
|
var prevOutType = this.prevOutTypes[index] |
|
|
|
|
|
|
|
var scriptType, hash |
|
|
|
if (redeemScript) { |
|
|
|
prevOutScript = prevOutScript || scripts.scriptHashOutput(redeemScript.getHash()) |
|
|
|
prevOutType = prevOutType || 'scripthash' |
|
|
|
|
|
|
|
assert.equal(prevOutType, 'scripthash', 'PrevOutScript must be P2SH') |
|
|
|
|
|
|
|
scriptType = scripts.classifyOutput(redeemScript) |
|
|
|
|
|
|
|
assert.notEqual(scriptType, 'scripthash', 'RedeemScript can\'t be P2SH') |
|
|
|
assert.notEqual(scriptType, 'nonstandard', 'RedeemScript not supported (nonstandard)') |
|
|
|
var input = this.inputs[index] |
|
|
|
var canSign = input.hashType && |
|
|
|
input.prevOutScript && |
|
|
|
input.prevOutType && |
|
|
|
input.pubKeys && |
|
|
|
input.scriptType && |
|
|
|
input.signatures |
|
|
|
|
|
|
|
// are we almost ready to sign?
|
|
|
|
if (canSign) { |
|
|
|
// if redeemScript was provided, enforce consistency
|
|
|
|
if (redeemScript) { |
|
|
|
assert.deepEqual(input.redeemScript, redeemScript, 'Inconsistent redeemScript') |
|
|
|
} |
|
|
|
|
|
|
|
hash = this.tx.hashForSignature(index, redeemScript, hashType) |
|
|
|
assert.equal(input.hashType, hashType, 'Inconsistent hashType') |
|
|
|
|
|
|
|
// no? prepare
|
|
|
|
} else { |
|
|
|
prevOutScript = prevOutScript || privKey.pub.getAddress().toOutputScript() |
|
|
|
prevOutType = prevOutType || 'pubkeyhash' |
|
|
|
|
|
|
|
assert.notEqual(prevOutType, 'scripthash', 'PrevOutScript is P2SH, missing redeemScript') |
|
|
|
|
|
|
|
scriptType = prevOutType |
|
|
|
if (redeemScript) { |
|
|
|
// if we have a prevOutScript, enforce scriptHash equality to the redeemScript
|
|
|
|
if (input.prevOutScript) { |
|
|
|
assert.equal(input.prevOutType, 'scripthash', 'PrevOutScript must be P2SH') |
|
|
|
|
|
|
|
var scriptHash = input.prevOutScript.chunks[1] |
|
|
|
assert.deepEqual(scriptHash, redeemScript.getHash(), 'RedeemScript does not match ' + scriptHash.toString('hex')) |
|
|
|
} |
|
|
|
|
|
|
|
var scriptType = scripts.classifyOutput(redeemScript) |
|
|
|
assert(scriptType in canSignTypes, 'RedeemScript not supported (' + scriptType + ')') |
|
|
|
|
|
|
|
var pubKeys = [] |
|
|
|
switch (scriptType) { |
|
|
|
case 'multisig': |
|
|
|
pubKeys = redeemScript.chunks.slice(1, -2).map(ECPubKey.fromBuffer) |
|
|
|
break |
|
|
|
|
|
|
|
case 'pubkeyhash': |
|
|
|
var pkh1 = redeemScript.chunks[2] |
|
|
|
var pkh2 = privKey.pub.getAddress().hash |
|
|
|
|
|
|
|
assert.deepEqual(pkh1, pkh2, 'privateKey cannot sign for this input') |
|
|
|
pubKeys = [privKey.pub] |
|
|
|
break |
|
|
|
|
|
|
|
case 'pubkey': |
|
|
|
pubKeys = redeemScript.chunks.slice(0, 1).map(ECPubKey.fromBuffer) |
|
|
|
break |
|
|
|
} |
|
|
|
|
|
|
|
if (!input.prevOutScript) { |
|
|
|
input.prevOutScript = scripts.scriptHashOutput(redeemScript.getHash()) |
|
|
|
input.prevOutType = 'scripthash' |
|
|
|
} |
|
|
|
|
|
|
|
input.pubKeys = pubKeys |
|
|
|
input.redeemScript = redeemScript |
|
|
|
input.scriptType = scriptType |
|
|
|
|
|
|
|
} else { |
|
|
|
assert.notEqual(input.prevOutType, 'scripthash', 'PrevOutScript is P2SH, missing redeemScript') |
|
|
|
|
|
|
|
// can we sign this?
|
|
|
|
if (input.scriptType) { |
|
|
|
assert(input.pubKeys, input.scriptType + ' not supported') |
|
|
|
|
|
|
|
// we know nothin' Jon Snow, assume pubKeyHash
|
|
|
|
} else { |
|
|
|
input.prevOutScript = privKey.pub.getAddress().toOutputScript() |
|
|
|
input.prevOutType = 'pubkeyhash' |
|
|
|
input.pubKeys = [privKey.pub] |
|
|
|
input.scriptType = input.prevOutType |
|
|
|
|
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
hash = this.tx.hashForSignature(index, prevOutScript, hashType) |
|
|
|
input.hashType = hashType |
|
|
|
input.signatures = input.signatures || [] |
|
|
|
} |
|
|
|
|
|
|
|
this.prevOutScripts[index] = prevOutScript |
|
|
|
this.prevOutTypes[index] = prevOutType |
|
|
|
|
|
|
|
if (!(index in this.signatures)) { |
|
|
|
this.signatures[index] = { |
|
|
|
hashType: hashType, |
|
|
|
pubKeys: [], |
|
|
|
redeemScript: redeemScript, |
|
|
|
scriptType: scriptType, |
|
|
|
signatures: [] |
|
|
|
} |
|
|
|
} else { |
|
|
|
assert.equal(scriptType, 'multisig', scriptType + ' doesn\'t support multiple signatures') |
|
|
|
} |
|
|
|
// enforce in order signing of public keys
|
|
|
|
assert(input.pubKeys.some(function(pubKey, i) { |
|
|
|
if (!privKey.pub.Q.equals(pubKey.Q)) return false |
|
|
|
|
|
|
|
var input = this.signatures[index] |
|
|
|
assert.equal(input.hashType, hashType, 'Inconsistent hashType') |
|
|
|
assert.deepEqual(input.redeemScript, redeemScript, 'Inconsistent redeemScript') |
|
|
|
assert(!input.signatures[i], 'Signature already exists') |
|
|
|
var signatureScript = input.redeemScript || input.prevOutScript |
|
|
|
var signatureHash = this.tx.hashForSignature(index, signatureScript, hashType) |
|
|
|
var signature = privKey.sign(signatureHash) |
|
|
|
input.signatures[i] = signature |
|
|
|
|
|
|
|
var signature = privKey.sign(hash) |
|
|
|
input.pubKeys.push(privKey.pub) |
|
|
|
input.signatures.push(signature) |
|
|
|
return true |
|
|
|
}, this), 'privateKey cannot sign for this input') |
|
|
|
} |
|
|
|
|
|
|
|
module.exports = TransactionBuilder |
|
|
|