|
|
@ -211,148 +211,6 @@ Transaction.prototype.inputs = function inputs() { |
|
|
|
return res; |
|
|
|
}; |
|
|
|
|
|
|
|
/** |
|
|
|
* Load and cache transaction inputs. |
|
|
|
* |
|
|
|
* This function will try to load the inputs for a transaction. |
|
|
|
* |
|
|
|
* @param {BlockChain} blockChain A reference to the BlockChain object. |
|
|
|
* @param {TransactionMap|null} txStore Additional transactions to consider. |
|
|
|
* @param {Boolean} wait Whether to keep trying until the dependencies are |
|
|
|
* met (or a timeout occurs.) |
|
|
|
* @param {Function} callback Function to call on completion. |
|
|
|
*/ |
|
|
|
Transaction.prototype.cacheInputs = |
|
|
|
function cacheInputs(blockChain, txStore, wait, callback) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
var txCache = new TransactionInputsCache(this); |
|
|
|
txCache.buffer(blockChain, txStore, wait, callback); |
|
|
|
}; |
|
|
|
|
|
|
|
Transaction.prototype.verify = function verify(txCache, blockChain, callback) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
var txIndex = txCache.txIndex; |
|
|
|
|
|
|
|
var outpoints = []; |
|
|
|
|
|
|
|
var valueIn = bignum(0); |
|
|
|
var valueOut = bignum(0); |
|
|
|
|
|
|
|
function getTxOut(txin, n) { |
|
|
|
var outHash = txin.getOutpointHash(); |
|
|
|
var outIndex = txin.getOutpointIndex(); |
|
|
|
var outHashBase64 = outHash.toString('base64'); |
|
|
|
var fromTxOuts = txIndex[outHashBase64]; |
|
|
|
|
|
|
|
if (!fromTxOuts) { |
|
|
|
throw new MissingSourceError( |
|
|
|
"Source tx " + util.formatHash(outHash) + |
|
|
|
" for inputs " + n + " not found", |
|
|
|
// We store the hash of the missing tx in the error
|
|
|
|
// so that the txStore can watch out for it.
|
|
|
|
outHash.toString('base64') |
|
|
|
); |
|
|
|
} |
|
|
|
|
|
|
|
var txout = fromTxOuts[outIndex]; |
|
|
|
|
|
|
|
if (!txout) { |
|
|
|
throw new Error("Source output index " + outIndex + |
|
|
|
" for input " + n + " out of bounds"); |
|
|
|
} |
|
|
|
|
|
|
|
return txout; |
|
|
|
} |
|
|
|
|
|
|
|
Step( |
|
|
|
function verifyInputs(opts) { |
|
|
|
var group = this.group(); |
|
|
|
|
|
|
|
if (self.isCoinBase()) { |
|
|
|
throw new Error("Coinbase tx are invalid unless part of a block"); |
|
|
|
} |
|
|
|
|
|
|
|
self.ins.forEach(function(txin, n) { |
|
|
|
var txout = getTxOut(txin, n); |
|
|
|
|
|
|
|
// TODO: Verify coinbase maturity
|
|
|
|
|
|
|
|
valueIn = valueIn.add(util.valueToBigInt(txout.v)); |
|
|
|
|
|
|
|
outpoints.push(txin.o); |
|
|
|
|
|
|
|
self.verifyInput(n, txout.getScript(), opts, group()); |
|
|
|
}); |
|
|
|
}, |
|
|
|
|
|
|
|
function verifyInputsResults(err, results) { |
|
|
|
if (err) throw err; |
|
|
|
|
|
|
|
for (var i = 0, l = results.length; i < l; i++) { |
|
|
|
if (!results[i]) { |
|
|
|
var txout = getTxOut(self.ins[i]); |
|
|
|
log.debug('Script evaluated to false'); |
|
|
|
log.debug('|- scriptSig', "" + self.ins[i].getScript()); |
|
|
|
log.debug('`- scriptPubKey', "" + txout.getScript()); |
|
|
|
throw new VerificationError('Script for input ' + i + ' evaluated to false'); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
this(); |
|
|
|
}, |
|
|
|
|
|
|
|
function queryConflicts(err) { |
|
|
|
if (err) throw err; |
|
|
|
|
|
|
|
// Make sure there are no other transactions spending the same outs
|
|
|
|
blockChain.countConflictingTransactions(outpoints, this); |
|
|
|
}, |
|
|
|
function checkConflicts(err, count) { |
|
|
|
if (err) throw err; |
|
|
|
|
|
|
|
self.outs.forEach(function(txout) { |
|
|
|
valueOut = valueOut.add(util.valueToBigInt(txout.v)); |
|
|
|
}); |
|
|
|
|
|
|
|
if (valueIn.cmp(valueOut) < 0) { |
|
|
|
var outValue = util.formatValue(valueOut); |
|
|
|
var inValue = util.formatValue(valueIn); |
|
|
|
throw new Error("Tx output value (BTC " + outValue + ") " + |
|
|
|
"exceeds input value (BTC " + inValue + ")"); |
|
|
|
} |
|
|
|
|
|
|
|
var fees = valueIn.sub(valueOut); |
|
|
|
|
|
|
|
if (count) { |
|
|
|
// Spent output detected, retrieve transaction that spends it
|
|
|
|
blockChain.getConflictingTransactions(outpoints, function(err, results) { |
|
|
|
if (results.length) { |
|
|
|
if (buffertools.compare(results[0].getHash(), self.getHash()) === 0) { |
|
|
|
log.warn("Detected tx re-add (recoverable db corruption): " + util.formatHashAlt(results[0].getHash())); |
|
|
|
// TODO: Needs to return an error for the memory pool case?
|
|
|
|
callback(null, fees); |
|
|
|
} else { |
|
|
|
callback(new Error("At least one referenced output has" + " already been spent in tx " + util.formatHashAlt(results[0].getHash()))); |
|
|
|
} |
|
|
|
} else { |
|
|
|
callback(new Error("Outputs of this transaction are spent, but " + |
|
|
|
"the transaction(s) that spend them are not " + |
|
|
|
"available. This probably means you need to " + |
|
|
|
"reset your database.")); |
|
|
|
} |
|
|
|
}); |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
// Success
|
|
|
|
this(null, fees); |
|
|
|
}, |
|
|
|
callback |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
Transaction.prototype.verifyInput = function verifyInput(n, scriptPubKey, opts, callback) { |
|
|
|
var scriptSig = this.ins[n].getScript(); |
|
|
|
return ScriptInterpreter.verifyFull( |
|
|
@ -1110,132 +968,4 @@ Transaction.createAndSign = function(utxos, outs, keys, opts) { |
|
|
|
return ret; |
|
|
|
}; |
|
|
|
|
|
|
|
var TransactionInputsCache = exports.TransactionInputsCache = |
|
|
|
function TransactionInputsCache(tx) { |
|
|
|
var txList = []; |
|
|
|
var txList64 = []; |
|
|
|
var reqOuts = {}; |
|
|
|
|
|
|
|
// Get list of transactions required for verification
|
|
|
|
tx.ins.forEach(function(txin) { |
|
|
|
if (txin.isCoinBase()) return; |
|
|
|
|
|
|
|
var hash = txin.o.slice(0, 32); |
|
|
|
var hash64 = hash.toString('base64'); |
|
|
|
if (txList64.indexOf(hash64) == -1) { |
|
|
|
txList.push(hash); |
|
|
|
txList64.push(hash64); |
|
|
|
} |
|
|
|
if (!reqOuts[hash64]) { |
|
|
|
reqOuts[hash64] = []; |
|
|
|
} |
|
|
|
reqOuts[hash64][txin.getOutpointIndex()] = true; |
|
|
|
}); |
|
|
|
|
|
|
|
this.tx = tx; |
|
|
|
this.txList = txList; |
|
|
|
this.txList64 = txList64; |
|
|
|
this.txIndex = {}; |
|
|
|
this.requiredOuts = reqOuts; |
|
|
|
this.callbacks = []; |
|
|
|
}; |
|
|
|
|
|
|
|
TransactionInputsCache.prototype.buffer = function buffer(blockChain, txStore, wait, callback) { |
|
|
|
var self = this; |
|
|
|
|
|
|
|
var complete = false; |
|
|
|
|
|
|
|
if ("function" === typeof callback) { |
|
|
|
self.callbacks.push(callback); |
|
|
|
} |
|
|
|
|
|
|
|
var missingTx = {}; |
|
|
|
self.txList64.forEach(function(hash64) { |
|
|
|
missingTx[hash64] = true; |
|
|
|
}); |
|
|
|
|
|
|
|
// A utility function to create the index object from the txs result lists
|
|
|
|
function indexTxs(err, txs) { |
|
|
|
if (err) throw err; |
|
|
|
|
|
|
|
// Index memory transactions
|
|
|
|
txs.forEach(function(tx) { |
|
|
|
var hash64 = tx.getHash().toString('base64'); |
|
|
|
var obj = {}; |
|
|
|
Object.keys(self.requiredOuts[hash64]).forEach(function(o) { |
|
|
|
obj[+o] = tx.outs[+o]; |
|
|
|
}); |
|
|
|
self.txIndex[hash64] = obj; |
|
|
|
delete missingTx[hash64]; |
|
|
|
}); |
|
|
|
|
|
|
|
this(null); |
|
|
|
}; |
|
|
|
|
|
|
|
Step( |
|
|
|
// First find and index memory transactions (if a txStore was provided)
|
|
|
|
function findMemTx() { |
|
|
|
if (txStore) { |
|
|
|
txStore.find(self.txList64, this); |
|
|
|
} else { |
|
|
|
this(null, []); |
|
|
|
} |
|
|
|
}, |
|
|
|
indexTxs, |
|
|
|
// Second find and index persistent transactions
|
|
|
|
function findBlockChainTx(err) { |
|
|
|
if (err) throw err; |
|
|
|
|
|
|
|
// TODO: Major speedup should be possible if we load only the outs and not
|
|
|
|
// whole transactions.
|
|
|
|
var callback = this; |
|
|
|
blockChain.getOutputsByHashes(self.txList, function(err, result) { |
|
|
|
callback(err, result); |
|
|
|
}); |
|
|
|
}, |
|
|
|
indexTxs, |
|
|
|
function saveTxCache(err) { |
|
|
|
if (err) throw err; |
|
|
|
|
|
|
|
var missingTxDbg = ''; |
|
|
|
if (Object.keys(missingTx).length) { |
|
|
|
missingTxDbg = Object.keys(missingTx).map(function(hash64) { |
|
|
|
return util.formatHash(new Buffer(hash64, 'base64')); |
|
|
|
}).join(','); |
|
|
|
} |
|
|
|
|
|
|
|
if (wait && Object.keys(missingTx).length) { |
|
|
|
// TODO: This might no longer be needed now that saveTransactions uses
|
|
|
|
// the safe=true option.
|
|
|
|
setTimeout(function() { |
|
|
|
var missingHashes = Object.keys(missingTx); |
|
|
|
if (missingHashes.length) { |
|
|
|
self.callback(new Error('Missing inputs (timeout while searching): ' + missingTxDbg)); |
|
|
|
} else if (!complete) { |
|
|
|
self.callback(new Error('Callback failed to trigger')); |
|
|
|
} |
|
|
|
}, 10000); |
|
|
|
} else { |
|
|
|
complete = true; |
|
|
|
this(null, self); |
|
|
|
} |
|
|
|
}, |
|
|
|
self.callback.bind(self) |
|
|
|
); |
|
|
|
}; |
|
|
|
|
|
|
|
|
|
|
|
TransactionInputsCache.prototype.callback = function callback(err) { |
|
|
|
var args = Array.prototype.slice.apply(arguments); |
|
|
|
|
|
|
|
// Empty the callback array first (because downstream functions could add new
|
|
|
|
// callbacks or otherwise interfere if were not in a consistent state.)
|
|
|
|
var cbs = this.callbacks; |
|
|
|
this.callbacks = []; |
|
|
|
|
|
|
|
cbs.forEach(function(cb) { |
|
|
|
cb.apply(null, args); |
|
|
|
}); |
|
|
|
}; |
|
|
|
|
|
|
|
module.exports = require('soop')(Transaction); |
|
|
|