Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Transaction Builder #244

Merged
merged 16 commits into from
Aug 18, 2014
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ module.exports = {
Script: require('./script'),
scripts: require('./scripts'),
Transaction: require('./transaction'),
TransactionBuilder: require('./transaction_builder'),
networks: require('./networks'),
Wallet: require('./wallet')
}
38 changes: 26 additions & 12 deletions src/transaction.js
Original file line number Diff line number Diff line change
Expand Up @@ -162,7 +162,17 @@ Transaction.prototype.toHex = function() {
* hashType, serializes and finally hashes the result. This hash can then be
* used to sign the transaction input in question.
*/
Transaction.prototype.hashForSignature = function(prevOutScript, inIndex, hashType) {
Transaction.prototype.hashForSignature = function(inIndex, prevOutScript, hashType) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we flipping this?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consistency with all other input related function signatures.
On Jul 19, 2014 6:12 AM, "Kyle Drake" notifications@github.com wrote:

In src/transaction.js:

@@ -162,7 +159,7 @@ Transaction.prototype.toHex = function() {

  • hashType, serializes and finally hashes the result. This hash can then be
  • used to sign the transaction input in question.
    */
    -Transaction.prototype.hashForSignature = function(prevOutScript, inIndex, hashType) {
    +Transaction.prototype.hashForSignature = function(inIndex, prevOutScript, hashType) {

Why are we flipping this?


Reply to this email directly or view it on GitHub
https://github.com/bitcoinjs/bitcoinjs-lib/pull/244/files#r15131198.

// FIXME: remove in 2.x.y
if (arguments[0] instanceof Script) {
console.warn('hashForSignature(prevOutScript, inIndex, ...) has been deprecated. Use hashForSignature(inIndex, prevOutScript, ...)')

// swap the arguments (must be stored in tmp, arguments is special)
var tmp = arguments[0]
inIndex = arguments[1]
prevOutScript = tmp
}

assert(inIndex >= 0, 'Invalid vin index')
assert(inIndex < this.ins.length, 'Invalid vin index')
assert(prevOutScript instanceof Script, 'Invalid Script object')
Expand Down Expand Up @@ -296,35 +306,39 @@ Transaction.fromHex = function(hex) {
return Transaction.fromBuffer(new Buffer(hex, 'hex'))
}

/**
* Signs a pubKeyHash output at some index with the given key
*/
Transaction.prototype.setInputScript = function(index, script) {
this.ins[index].script = script
}

// FIXME: remove in 2.x.y
Transaction.prototype.sign = function(index, privKey, hashType) {
console.warn("Transaction.prototype.sign is deprecated. Use TransactionBuilder instead.")

var prevOutScript = privKey.pub.getAddress().toOutputScript()
var signature = this.signInput(index, prevOutScript, privKey, hashType)

// FIXME: Assumed prior TX was pay-to-pubkey-hash
var scriptSig = scripts.pubKeyHashInput(signature, privKey.pub)
this.setInputScript(index, scriptSig)
}

// FIXME: remove in 2.x.y
Transaction.prototype.signInput = function(index, prevOutScript, privKey, hashType) {
console.warn("Transaction.prototype.signInput is deprecated. Use TransactionBuilder instead.")

hashType = hashType || Transaction.SIGHASH_ALL

var hash = this.hashForSignature(prevOutScript, index, hashType)
var hash = this.hashForSignature(index, prevOutScript, hashType)
var signature = privKey.sign(hash)

return signature.toScriptSignature(hashType)
}

Transaction.prototype.setInputScript = function(index, script) {
this.ins[index].script = script
}

// FIXME: could be validateInput(index, prevTxOut, pub)
// FIXME: remove in 2.x.y
Transaction.prototype.validateInput = function(index, prevOutScript, pubKey, buffer) {
console.warn("Transaction.prototype.validateInput is deprecated. Use TransactionBuilder instead.")

var parsed = ECSignature.parseScriptSignature(buffer)
var hash = this.hashForSignature(prevOutScript, index, parsed.hashType)
var hash = this.hashForSignature(index, prevOutScript, parsed.hashType)

return pubKey.verify(hash, parsed.signature)
}
Expand Down
260 changes: 260 additions & 0 deletions src/transaction_builder.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,260 @@
var assert = require('assert')
var scripts = require('./scripts')

var ECPubKey = require('./ecpubkey')
var ECSignature = require('./ecsignature')
var Script = require('./script')
var Transaction = require('./transaction')

function TransactionBuilder() {
this.prevOutMap = {}
this.prevOutScripts = {}
this.prevOutTypes = {}

this.signatures = []
this.tx = new Transaction()
}

// Static constructors
TransactionBuilder.fromTransaction = function(transaction) {
var txb = new TransactionBuilder()

// Extract/add inputs
transaction.ins.forEach(function(txin) {
txb.addInput(txin.hash, txin.index, txin.sequence)
})

// Extract/add outputs
transaction.outs.forEach(function(txout) {
txb.addOutput(txout.script, txout.value)
})

// Extract/add signatures
transaction.ins.forEach(function(txin) {
// Ignore empty scripts
if (txin.script.buffer.length === 0) return

var redeemScript
var scriptSig = txin.script
var scriptType = scripts.classifyInput(scriptSig)

// 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)
assert.equal(scripts.classifyOutput(redeemScript), scriptType, 'Non-matching scriptSig and scriptPubKey in input')
}

// Extract hashType, pubKeys and signatures
var hashType, pubKeys, signatures

switch (scriptType) {
case 'pubkeyhash':
var parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0])
var pubKey = ECPubKey.fromBuffer(scriptSig.chunks[1])

hashType = parsed.hashType
pubKeys = [pubKey]
signatures = [parsed.signature]

break

case 'multisig':
var scriptSigs = scriptSig.chunks.slice(1) // ignore OP_0
var parsed = scriptSigs.map(function(scriptSig) {
return ECSignature.parseScriptSignature(scriptSig)
})

hashType = parsed[0].hashType
pubKeys = []
signatures = parsed.map(function(p) { return p.signature })

break

case 'pubkey':
var parsed = ECSignature.parseScriptSignature(scriptSig.chunks[0])

hashType = parsed.hashType
pubKeys = []
signatures = [parsed.signature]

break

default:
assert(false, scriptType + ' not supported')
}

txb.signatures[txin.index] = {
hashType: hashType,
pubKeys: pubKeys,
redeemScript: redeemScript,
scriptType: scriptType,
signatures: signatures
}
})

return txb
}

// Operations
TransactionBuilder.prototype.addInput = function(prevTx, index, sequence, prevOutScript) {
var prevOutHash

if (typeof prevTx === 'string') {
prevOutHash = new Buffer(prevTx, 'hex')

// TxId hex is big-endian, we want little-endian hash
Array.prototype.reverse.call(prevOutHash)

} else if (prevTx instanceof Transaction) {
prevOutHash = prevTx.getHash()
prevOutScript = prevTx.outs[index].script

} else {
prevOutHash = prevTx

}

var prevOutType
if (prevOutScript !== undefined) {
prevOutType = scripts.classifyOutput(prevOutScript)

assert.notEqual(prevOutType, 'nonstandard', 'PrevOutScript not supported (nonstandard)')
}

assert(this.signatures.every(function(input) {
return input.hashType & Transaction.SIGHASH_ANYONECANPAY
}), 'No, this would invalidate signatures')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No? what's the question here? :P

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A good point, the situation here is that someone is attempting to add an input to a Transaction that already has signatures.
Those signatures (as specified by their hashType) are enforcing that the Transaction may not be altered without invalidating those signatures; and therefore invaliding the TransactionBuilder object.


var prevOut = prevOutHash.toString('hex') + ':' + index
assert(!(prevOut in this.prevOutMap), 'Transaction is already an input')
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again, is this assert necessary? The existing will just be overwritten. Could be useful when the overwriting behavior is intended.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@weilu a transaction output may only be spent once.
The internal Transaction in TransactionBuilder would be invalidated (see Transaction.prototype.addInput) if it were to be added twice.

See: https://github.com/bitcoinjs/bitcoinjs-lib/pull/244/files#diff-52af0f8b5da51627aa8d01193636ba73R51

These checks are enforced to avoid an invalid Transaction, not about avoiding internal state overwrites.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I understand that a transaction output can only be spent once. I missed that tx.addInput doesn't prevent the same hash & index combination being added more than once. Then my question becomes who's responsibility should it be? Here in TransactionBuilder or over at Transaction?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

At this point Transaction is almost just a basic data structure.
I'm undecided if that data structure should enforce its own semantics... but it would make sense given addInput is basically just a glorified tx.inputs.push({ ... }) at the moment.


var vout = this.tx.addInput(prevOutHash, index, sequence)
this.prevOutMap[prevOut] = true
this.prevOutScripts[vout] = prevOutScript
this.prevOutTypes[vout] = prevOutType

return vout
}

TransactionBuilder.prototype.addOutput = function(scriptPubKey, value) {
assert(this.signatures.every(function(signature) {
return (signature.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.buildIncomplete = function() {
return this.__build(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
var scriptType = input.scriptType

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 (input.redeemScript) {
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)
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)')

hash = this.tx.hashForSignature(index, redeemScript, hashType)

} else {
prevOutScript = prevOutScript || privKey.pub.getAddress().toOutputScript()
scriptType = prevOutType || 'pubkeyhash'

assert.notEqual(scriptType, 'scripthash', 'PrevOutScript requires redeemScript')

hash = this.tx.hashForSignature(index, prevOutScript, hashType)
}

if (!(index in this.signatures)) {
this.signatures[index] = {
hashType: hashType,
pubKeys: [],
redeemScript: redeemScript,
scriptType: scriptType,
signatures: []
}
}

var input = this.signatures[index]
assert.equal(input.hashType, hashType, 'Inconsistent hashType')
assert.deepEqual(input.redeemScript, redeemScript, 'Inconsistent redeemScript')

var signature = privKey.sign(hash)
input.pubKeys.push(privKey.pub)
input.signatures.push(signature)
}

module.exports = TransactionBuilder
Loading