Conflicts: Script.js ScriptInterpreter.js Transaction.js test/testdata.js ...conflicts resolved by taking Manuel's changes, and then manually including Matias's changes on those same files. The conflicts resulted from differences in indentation, which is because Matias' changes unindendented all the code that had been but is not now inside a function.
820 lines
23 KiB
JavaScript
820 lines
23 KiB
JavaScript
var imports = require('soop').imports();
|
|
var config = imports.config || require('./config');
|
|
var log = imports.log || require('./util/log');
|
|
var Address = imports.Address || require('./Address');
|
|
var Script = imports.Script || require('./Script');
|
|
var ScriptInterpreter = imports.ScriptInterpreter || require('./ScriptInterpreter');
|
|
var util = imports.util || require('./util/util');
|
|
var bignum = imports.bignum || require('bignum');
|
|
var Put = imports.Put || require('bufferput');
|
|
var Parser = imports.Parser || require('./util/BinaryParser');
|
|
var Step = imports.Step || require('step');
|
|
var buffertools = imports.buffertools || require('buffertools');
|
|
var error = imports.error || require('./util/error');
|
|
|
|
var COINBASE_OP = Buffer.concat([util.NULL_HASH, new Buffer('FFFFFFFF', 'hex')]);
|
|
|
|
function TransactionIn(data) {
|
|
if ("object" !== typeof data) {
|
|
data = {};
|
|
}
|
|
if (data.o) {
|
|
this.o = data.o;
|
|
} else {
|
|
if (data.oTxHash && typeof data.oIndex !== 'undefined' && data.oIndex >= 0) {
|
|
var hash = new Buffer(data.oTxHash, 'hex');
|
|
hash = buffertools.reverse(hash);
|
|
var voutBuf = new Buffer(4);
|
|
voutBuf.writeUInt32LE(data.oIndex, 0);
|
|
this.o = Buffer.concat([hash, voutBuf]);
|
|
}
|
|
}
|
|
this.s = Buffer.isBuffer(data.s) ? data.s :
|
|
Buffer.isBuffer(data.script) ? data.script : util.EMPTY_BUFFER;
|
|
this.q = data.q ? data.q : data.sequence;
|
|
}
|
|
|
|
TransactionIn.prototype.getScript = function getScript() {
|
|
return new Script(this.s);
|
|
};
|
|
|
|
TransactionIn.prototype.isCoinBase = function isCoinBase() {
|
|
return buffertools.compare(this.o, COINBASE_OP) === 0;
|
|
};
|
|
|
|
TransactionIn.prototype.serialize = function serialize() {
|
|
var slen = util.varIntBuf(this.s.length);
|
|
var qbuf = new Buffer(4);
|
|
qbuf.writeUInt32LE(this.q, 0);
|
|
|
|
var ret = Buffer.concat([this.o, slen, this.s, qbuf]);
|
|
return ret;
|
|
};
|
|
|
|
TransactionIn.prototype.getOutpointHash = function getOutpointHash() {
|
|
if ("undefined" !== typeof this.o.outHashCache) {
|
|
return this.o.outHashCache;
|
|
}
|
|
|
|
return this.o.outHashCache = this.o.slice(0, 32);
|
|
};
|
|
|
|
TransactionIn.prototype.getOutpointIndex = function getOutpointIndex() {
|
|
return (this.o[32] ) +
|
|
(this.o[33] << 8) +
|
|
(this.o[34] << 16) +
|
|
(this.o[35] << 24);
|
|
};
|
|
|
|
TransactionIn.prototype.setOutpointIndex = function setOutpointIndex(n) {
|
|
this.o[32] = n & 0xff;
|
|
this.o[33] = n >> 8 & 0xff;
|
|
this.o[34] = n >> 16 & 0xff;
|
|
this.o[35] = n >> 24 & 0xff;
|
|
};
|
|
|
|
|
|
function TransactionOut(data) {
|
|
if ("object" !== typeof data) {
|
|
data = {};
|
|
}
|
|
this.v = data.v ? data.v : data.value;
|
|
this.s = data.s ? data.s : data.script;
|
|
};
|
|
|
|
TransactionOut.prototype.getValue = function getValue() {
|
|
return new Parser(this.v).word64lu();
|
|
};
|
|
|
|
TransactionOut.prototype.getScript = function getScript() {
|
|
return new Script(this.s);
|
|
};
|
|
|
|
TransactionOut.prototype.serialize = function serialize() {
|
|
var slen = util.varIntBuf(this.s.length);
|
|
return Buffer.concat([this.v, slen, this.s]);
|
|
};
|
|
|
|
function Transaction(data) {
|
|
if ("object" !== typeof data) {
|
|
data = {};
|
|
}
|
|
this.hash = data.hash || null;
|
|
this.version = data.version;
|
|
this.lock_time = data.lock_time;
|
|
this.ins = Array.isArray(data.ins) ? data.ins.map(function (data) {
|
|
var txin = new TransactionIn();
|
|
txin.s = data.s;
|
|
txin.q = data.q;
|
|
txin.o = data.o;
|
|
return txin;
|
|
}) : [];
|
|
this.outs = Array.isArray(data.outs) ? data.outs.map(function (data) {
|
|
var txout = new TransactionOut();
|
|
txout.v = data.v;
|
|
txout.s = data.s;
|
|
return txout;
|
|
}) : [];
|
|
if (data.buffer) this._buffer = data.buffer;
|
|
};
|
|
this.class = Transaction;
|
|
Transaction.In = TransactionIn;
|
|
Transaction.Out = TransactionOut;
|
|
|
|
Transaction.prototype.isCoinBase = function () {
|
|
return this.ins.length == 1 && this.ins[0].isCoinBase();
|
|
};
|
|
|
|
Transaction.prototype.isStandard = function isStandard() {
|
|
var i;
|
|
for (i = 0; i < this.ins.length; i++) {
|
|
if (this.ins[i].getScript().getInType() == "Strange") {
|
|
return false;
|
|
}
|
|
}
|
|
for (i = 0; i < this.outs.length; i++) {
|
|
if (this.outs[i].getScript().getOutType() == "Strange") {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
|
|
Transaction.prototype.serialize = function serialize() {
|
|
var bufs = [];
|
|
|
|
var buf = new Buffer(4);
|
|
buf.writeUInt32LE(this.version, 0);
|
|
bufs.push(buf);
|
|
|
|
bufs.push(util.varIntBuf(this.ins.length));
|
|
this.ins.forEach(function (txin) {
|
|
bufs.push(txin.serialize());
|
|
});
|
|
|
|
bufs.push(util.varIntBuf(this.outs.length));
|
|
this.outs.forEach(function (txout) {
|
|
bufs.push(txout.serialize());
|
|
});
|
|
|
|
var buf = new Buffer(4);
|
|
buf.writeUInt32LE(this.lock_time, 0);
|
|
bufs.push(buf);
|
|
|
|
this._buffer = Buffer.concat(bufs);
|
|
return this._buffer;
|
|
};
|
|
|
|
Transaction.prototype.getBuffer = function getBuffer() {
|
|
if (this._buffer) return this._buffer;
|
|
|
|
return this.serialize();
|
|
};
|
|
|
|
Transaction.prototype.calcHash = function calcHash() {
|
|
this.hash = util.twoSha256(this.getBuffer());
|
|
return this.hash;
|
|
};
|
|
|
|
Transaction.prototype.checkHash = function checkHash() {
|
|
if (!this.hash || !this.hash.length) return false;
|
|
|
|
return buffertools.compare(this.calcHash(), this.hash) === 0;
|
|
};
|
|
|
|
Transaction.prototype.getHash = function getHash() {
|
|
if (!this.hash || !this.hash.length) {
|
|
this.hash = this.calcHash();
|
|
}
|
|
return this.hash;
|
|
};
|
|
|
|
// convert encoded list of inputs to easy-to-use JS list-of-lists
|
|
Transaction.prototype.inputs = function inputs() {
|
|
var res = [];
|
|
for (var i = 0; i < this.ins.length; i++) {
|
|
var txin = this.ins[i];
|
|
var outHash = txin.getOutpointHash();
|
|
var outIndex = txin.getOutpointIndex();
|
|
res.push([outHash, outIndex]);
|
|
}
|
|
|
|
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() {
|
|
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(), 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, callback) {
|
|
return ScriptInterpreter.verify(this.ins[n].getScript(),
|
|
scriptPubKey,
|
|
this, n, 0,
|
|
callback);
|
|
};
|
|
|
|
/**
|
|
* Returns an object containing all pubkey hashes affected by this transaction.
|
|
*
|
|
* The return object contains the base64-encoded pubKeyHash values as keys
|
|
* and the original pubKeyHash buffers as values.
|
|
*/
|
|
Transaction.prototype.getAffectedKeys = function getAffectedKeys(txCache) {
|
|
// TODO: Function won't consider results cached if there are no affected
|
|
// accounts.
|
|
if (!(this.affects && this.affects.length)) {
|
|
this.affects = [];
|
|
|
|
// Index any pubkeys affected by the outputs of this transaction
|
|
for (var i = 0, l = this.outs.length; i < l; i++) {
|
|
try {
|
|
var txout = this.outs[i];
|
|
var script = txout.getScript();
|
|
|
|
var outPubKey = script.simpleOutPubKeyHash();
|
|
if (outPubKey) {
|
|
this.affects.push(outPubKey);
|
|
}
|
|
} catch (err) {
|
|
// It's not our job to validate, so we just ignore any errors and issue
|
|
// a very low level log message.
|
|
log.debug("Unable to determine affected pubkeys: " +
|
|
(err.stack ? err.stack : ""+err));
|
|
}
|
|
};
|
|
|
|
// Index any pubkeys affected by the inputs of this transaction
|
|
var txIndex = txCache.txIndex;
|
|
for (var i = 0, l = this.ins.length; i < l; i++) {
|
|
try {
|
|
var txin = this.ins[i];
|
|
|
|
if (txin.isCoinBase()) continue;
|
|
|
|
// In the case of coinbase or IP transactions, the txin doesn't
|
|
// actually contain the pubkey, so we look at the referenced txout
|
|
// instead.
|
|
var outHash = txin.getOutpointHash();
|
|
var outIndex = txin.getOutpointIndex();
|
|
var outHashBase64 = outHash.toString('base64');
|
|
var fromTxOuts = txIndex[outHashBase64];
|
|
|
|
if (!fromTxOuts) {
|
|
throw new Error("Input not found!");
|
|
}
|
|
|
|
var txout = fromTxOuts[outIndex];
|
|
var script = txout.getScript();
|
|
|
|
var outPubKey = script.simpleOutPubKeyHash();
|
|
if (outPubKey) {
|
|
this.affects.push(outPubKey);
|
|
}
|
|
} catch (err) {
|
|
// It's not our job to validate, so we just ignore any errors and issue
|
|
// a very low level log message.
|
|
log.debug("Unable to determine affected pubkeys: " +
|
|
(err.stack ? err.stack : ""+err));
|
|
}
|
|
}
|
|
}
|
|
|
|
var affectedKeys = {};
|
|
|
|
this.affects.forEach(function (pubKeyHash) {
|
|
affectedKeys[pubKeyHash.toString('base64')] = pubKeyHash;
|
|
});
|
|
|
|
return affectedKeys;
|
|
};
|
|
|
|
var OP_CODESEPARATOR = 171;
|
|
|
|
var SIGHASH_ALL = 1;
|
|
var SIGHASH_NONE = 2;
|
|
var SIGHASH_SINGLE = 3;
|
|
var SIGHASH_ANYONECANPAY = 80;
|
|
|
|
Transaction.SIGHASH_ALL=SIGHASH_ALL;
|
|
Transaction.SIGHASH_NONE=SIGHASH_NONE;
|
|
Transaction.SIGHASH_SINGLE=SIGHASH_SINGLE;
|
|
Transaction.SIGHASH_ANYONECANPAY=SIGHASH_ANYONECANPAY;
|
|
|
|
Transaction.prototype.hashForSignature =
|
|
function hashForSignature(script, inIndex, hashType) {
|
|
if (+inIndex !== inIndex ||
|
|
inIndex < 0 || inIndex >= this.ins.length) {
|
|
throw new Error("Input index '"+inIndex+"' invalid or out of bounds "+
|
|
"("+this.ins.length+" inputs)");
|
|
}
|
|
|
|
// Clone transaction
|
|
var txTmp = new Transaction();
|
|
this.ins.forEach(function (txin, i) {
|
|
txTmp.ins.push(new TransactionIn(txin));
|
|
});
|
|
this.outs.forEach(function (txout) {
|
|
txTmp.outs.push(new TransactionOut(txout));
|
|
});
|
|
txTmp.version = this.version;
|
|
txTmp.lock_time = this.lock_time;
|
|
|
|
// In case concatenating two scripts ends up with two codeseparators,
|
|
// or an extra one at the end, this prevents all those possible
|
|
// incompatibilities.
|
|
script.findAndDelete(OP_CODESEPARATOR);
|
|
|
|
// Get mode portion of hashtype
|
|
var hashTypeMode = hashType & 0x1f;
|
|
|
|
// Generate modified transaction data for hash
|
|
var bytes = (new Put());
|
|
bytes.word32le(this.version);
|
|
|
|
// Serialize inputs
|
|
if (hashType & SIGHASH_ANYONECANPAY) {
|
|
// Blank out all inputs except current one, not recommended for open
|
|
// transactions.
|
|
bytes.varint(1);
|
|
bytes.put(this.ins[inIndex].o);
|
|
bytes.varint(script.buffer.length);
|
|
bytes.put(script.buffer);
|
|
bytes.word32le(this.ins[inIndex].q);
|
|
} else {
|
|
bytes.varint(this.ins.length);
|
|
for (var i = 0, l = this.ins.length; i < l; i++) {
|
|
var txin = this.ins[i];
|
|
bytes.put(this.ins[i].o);
|
|
|
|
// Current input's script gets set to the script to be signed, all others
|
|
// get blanked.
|
|
if (inIndex === i) {
|
|
bytes.varint(script.buffer.length);
|
|
bytes.put(script.buffer);
|
|
} else {
|
|
bytes.varint(0);
|
|
}
|
|
|
|
if (hashTypeMode === SIGHASH_NONE && inIndex !== i) {
|
|
bytes.word32le(0);
|
|
} else {
|
|
bytes.word32le(this.ins[i].q);
|
|
}
|
|
}
|
|
}
|
|
|
|
// Serialize outputs
|
|
if (hashTypeMode === SIGHASH_NONE) {
|
|
bytes.varint(0);
|
|
} else {
|
|
var outsLen;
|
|
if (hashTypeMode === SIGHASH_SINGLE) {
|
|
// TODO: Untested
|
|
if (inIndex >= txTmp.outs.length) {
|
|
throw new Error("Transaction.hashForSignature(): SIGHASH_SINGLE " +
|
|
"no corresponding txout found - out of bounds");
|
|
}
|
|
outsLen = inIndex + 1;
|
|
} else {
|
|
outsLen = this.outs.length;
|
|
}
|
|
|
|
// TODO: If hashTypeMode !== SIGHASH_SINGLE, we could memcpy this whole
|
|
// section from the original transaction as is.
|
|
bytes.varint(outsLen);
|
|
for (var i = 0; i < outsLen; i++) {
|
|
if (hashTypeMode === SIGHASH_SINGLE && i !== inIndex) {
|
|
// Zero all outs except the one we want to keep
|
|
bytes.put(util.INT64_MAX);
|
|
bytes.varint(0);
|
|
} else {
|
|
bytes.put(this.outs[i].v);
|
|
bytes.varint(this.outs[i].s.length);
|
|
bytes.put(this.outs[i].s);
|
|
}
|
|
}
|
|
}
|
|
|
|
bytes.word32le(this.lock_time);
|
|
|
|
var buffer = bytes.buffer();
|
|
|
|
// Append hashType
|
|
buffer = Buffer.concat([buffer, new Buffer([parseInt(hashType), 0, 0, 0])]);
|
|
|
|
return util.twoSha256(buffer);
|
|
};
|
|
|
|
/**
|
|
* Returns an object with the same field names as jgarzik's getblock patch.
|
|
*/
|
|
Transaction.prototype.getStandardizedObject = function getStandardizedObject() {
|
|
var tx = {
|
|
hash: util.formatHashFull(this.getHash()),
|
|
version: this.version,
|
|
lock_time: this.lock_time
|
|
};
|
|
|
|
var totalSize = 8; // version + lock_time
|
|
totalSize += util.getVarIntSize(this.ins.length); // tx_in count
|
|
var ins = this.ins.map(function (txin) {
|
|
var txinObj = {
|
|
prev_out: {
|
|
hash: buffertools.reverse(new Buffer(txin.getOutpointHash())).toString('hex'),
|
|
n: txin.getOutpointIndex()
|
|
}
|
|
};
|
|
if (txin.isCoinBase()) {
|
|
txinObj.coinbase = txin.s.toString('hex');
|
|
} else {
|
|
txinObj.scriptSig = new Script(txin.s).getStringContent(false, 0);
|
|
}
|
|
totalSize += 36 + util.getVarIntSize(txin.s.length) +
|
|
txin.s.length + 4; // outpoint + script_len + script + sequence
|
|
return txinObj;
|
|
});
|
|
|
|
totalSize += util.getVarIntSize(this.outs.length);
|
|
var outs = this.outs.map(function (txout) {
|
|
totalSize += util.getVarIntSize(txout.s.length) +
|
|
txout.s.length + 8; // script_len + script + value
|
|
return {
|
|
value: util.formatValue(txout.v),
|
|
scriptPubKey: new Script(txout.s).getStringContent(false, 0)
|
|
};
|
|
});
|
|
|
|
tx.size = totalSize;
|
|
|
|
tx["in"] = ins;
|
|
tx["out"] = outs;
|
|
|
|
return tx;
|
|
};
|
|
|
|
// Add some Mongoose compatibility functions to the plain object
|
|
Transaction.prototype.toObject = function toObject() {
|
|
return this;
|
|
};
|
|
|
|
Transaction.prototype.fromObj = function fromObj(obj) {
|
|
var txobj = {};
|
|
txobj.version = obj.version || 1;
|
|
txobj.lock_time = obj.lock_time || 0;
|
|
txobj.ins = [];
|
|
txobj.outs = [];
|
|
|
|
obj.inputs.forEach(function(inputobj) {
|
|
var txin = new TransactionIn();
|
|
txin.s = util.EMPTY_BUFFER;
|
|
txin.q = 0xffffffff;
|
|
|
|
var hash = new Buffer(inputobj.txid, 'hex');
|
|
hash = buffertools.reverse(hash);
|
|
var vout = parseInt(inputobj.vout);
|
|
var voutBuf = new Buffer(4);
|
|
voutBuf.writeUInt32LE(vout, 0);
|
|
|
|
txin.o = Buffer.concat([hash, voutBuf]);
|
|
|
|
txobj.ins.push(txin);
|
|
});
|
|
|
|
var keys = Object.keys(obj.outputs);
|
|
keys.forEach(function(addrStr) {
|
|
var addr = new Address(addrStr);
|
|
var script = Script.createPubKeyHashOut(addr.payload());
|
|
|
|
var valueNum = bignum(obj.outputs[addrStr]);
|
|
var value = util.bigIntToValue(valueNum);
|
|
|
|
var txout = new TransactionOut();
|
|
txout.v = value;
|
|
txout.s = script.getBuffer();
|
|
|
|
txobj.outs.push(txout);
|
|
});
|
|
|
|
this.lock_time = txobj.lock_time;
|
|
this.version = txobj.version;
|
|
this.ins = txobj.ins;
|
|
this.outs = txobj.outs;
|
|
}
|
|
|
|
Transaction.prototype.parse = function (parser) {
|
|
if (Buffer.isBuffer(parser)) {
|
|
this._buffer = parser;
|
|
parser = new Parser(parser);
|
|
}
|
|
|
|
var i, sLen, startPos = parser.pos;
|
|
|
|
this.version = parser.word32le();
|
|
|
|
var txinCount = parser.varInt();
|
|
|
|
this.ins = [];
|
|
for (j = 0; j < txinCount; j++) {
|
|
var txin = new TransactionIn();
|
|
txin.o = parser.buffer(36); // outpoint
|
|
sLen = parser.varInt(); // script_len
|
|
txin.s = parser.buffer(sLen); // script
|
|
txin.q = parser.word32le(); // sequence
|
|
this.ins.push(txin);
|
|
}
|
|
|
|
var txoutCount = parser.varInt();
|
|
|
|
this.outs = [];
|
|
for (j = 0; j < txoutCount; j++) {
|
|
var txout = new TransactionOut();
|
|
txout.v = parser.buffer(8); // value
|
|
sLen = parser.varInt(); // script_len
|
|
txout.s = parser.buffer(sLen); // script
|
|
this.outs.push(txout);
|
|
}
|
|
|
|
this.lock_time = parser.word32le();
|
|
this.calcHash();
|
|
};
|
|
|
|
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 = [];
|
|
|
|
try {
|
|
cbs.forEach(function (cb) {
|
|
cb.apply(null, args);
|
|
});
|
|
} catch (err) {
|
|
log.err("Callback error after connecting tx inputs: "+
|
|
(err.stack ? err.stack : err.toString()));
|
|
}
|
|
};
|
|
|
|
module.exports = require('soop')(Transaction);
|