diff --git a/lib/bcoin.js b/lib/bcoin.js index 55951f69..473b0af4 100644 --- a/lib/bcoin.js +++ b/lib/bcoin.js @@ -65,6 +65,7 @@ bcoin.input = require('./bcoin/input'); bcoin.output = require('./bcoin/output'); bcoin.coin = require('./bcoin/coin'); bcoin.tx = require('./bcoin/tx'); +bcoin.mtx = require('./bcoin/mtx'); bcoin.txpool = require('./bcoin/tx-pool'); bcoin.block = require('./bcoin/block'); bcoin.ramdisk = require('./bcoin/ramdisk'); diff --git a/lib/bcoin/block.js b/lib/bcoin/block.js index 72401260..ba15431d 100644 --- a/lib/bcoin/block.js +++ b/lib/bcoin/block.js @@ -41,7 +41,6 @@ function Block(data, subtype) { this._raw = data._raw || null; this._size = data._size || 0; - this.network = data.network || false; this.relayedBy = data.relayedBy || '0.0.0.0'; this._chain = data.chain; @@ -78,17 +77,11 @@ function Block(data, subtype) { }); } - this.verify(); + if (!this._raw) + this._raw = this.render(); - if (this.subtype === 'block' && !this.valid) { - this.txs = this.txs.map(function(tx) { - tx.block = null; - if (tx.ts === self.ts) - tx.ts = 0; - tx.height = -1; - return tx; - }); - } + if (!this._size) + this._size = this._raw.length; } Block.prototype.hash = function hash(enc) { @@ -99,7 +92,7 @@ Block.prototype.hash = function hash(enc) { }; Block.prototype.abbr = function abbr() { - if (this.network && this._raw) + if (this._raw) return this._raw.slice(0, 80); var res = new Buffer(80); @@ -125,7 +118,7 @@ Block.verify = function verify(data, subtype) { }; Block.prototype.render = function render() { - if (this.network && this._raw) + if (this._raw) return this._raw; return bcoin.protocol.framer.block(this, this.subtype); }; @@ -459,7 +452,6 @@ Block.prototype.toJSON = function toJSON() { type: 'block', subtype: this.subtype, height: this.height, - network: this.network, relayedBy: this.relayedBy, hash: utils.revHex(this.hash('hex')), version: this.version, @@ -507,7 +499,6 @@ Block.prototype.toCompact = function toCompact() { prevBlock: this.prevBlock, ts: this.ts, height: this.height, - network: this.network, relayedBy: this.relayedBy, block: utils.toHex(this.render()) }; @@ -530,7 +521,6 @@ Block._fromCompact = function _fromCompact(json) { data = parser.parseBlock(raw); data.height = json.height; - data.network = json.network; data.relayedBy = json.relayedBy; return data; diff --git a/lib/bcoin/input.js b/lib/bcoin/input.js index 9cd5ae4b..6fbb35c2 100644 --- a/lib/bcoin/input.js +++ b/lib/bcoin/input.js @@ -22,7 +22,7 @@ function Input(options) { assert(typeof options.script !== 'string'); - prevout = options.prevout || options.out; + prevout = options.prevout; this.prevout = { hash: prevout.hash, diff --git a/lib/bcoin/mtx.js b/lib/bcoin/mtx.js new file mode 100644 index 00000000..bcda7d92 --- /dev/null +++ b/lib/bcoin/mtx.js @@ -0,0 +1,1200 @@ +/** + * mtx.js - mutable transaction object for bcoin + * Copyright (c) 2014-2015, Fedor Indutny (MIT License) + * https://github.com/indutny/bcoin + */ + +var bn = require('bn.js'); + +var bcoin = require('../bcoin'); +var utils = bcoin.utils; +var assert = utils.assert; +var constants = bcoin.protocol.constants; + +/** + * MTX + */ + +function MTX(options) { + if (!(this instanceof MTX)) + return new MTX(options); + + if (!options) + options = {}; + + this.options = options; + + this.type = 'mtx'; + this.version = options.version || 1; + this.inputs = []; + this.outputs = []; + this.locktime = 0; + this.ts = 0; + this.block = null; + + this._hash = null; + this._raw = null; + this._size = 0; + this._offset = 0; + + this.height = -1; + this.relayedBy = '0.0.0.0'; + + this._chain = options.chain; + + if (options.inputs) { + assert(this.inputs.length === 0); + options.inputs.forEach(function(input) { + this.addInput(input); + }, this); + } + + if (options.outputs) { + assert(this.outputs.length === 0); + options.outputs.forEach(function(output) { + this.addOutput(output); + }, this); + } + + this.changeIndex = options.changeIndex != null ? options.changeIndex : -1; + this.ps = this.ts === 0 ? utils.now() : 0; +} + +utils.inherits(MTX, bcoin.tx); + +MTX.prototype.clone = function clone() { + var tx = new MTX(this); + + tx.inputs = tx.inputs.map(function(input) { + input.script = input.script.slice(); + return input; + }); + + tx.outputs = tx.outputs.map(function(output) { + output.script = output.script.slice(); + return output; + }); + + return tx; +}; + +MTX.prototype.hash = function hash(enc) { + var hash = utils.dsha256(this.render()); + return enc === 'hex' ? utils.toHex(hash) : hash; +}; + +MTX.prototype.render = function render() { + return bcoin.protocol.framer.tx(this); +}; + +MTX.prototype.getSize = function getSize() { + return this.render().length; +}; + +MTX.prototype.addInput = function addInput(options, index) { + var input, i; + + if (options instanceof MTX) + options = bcoin.coin(options, index); + + if (options instanceof bcoin.coin) { + options = { + prevout: { hash: options.hash, index: options.index }, + output: options + }; + } + + assert(options.prevout); + + // i = this._inputIndex(options.prevout.hash, options.prevout.index); + // assert(i === -1); + + input = bcoin.input(options); + + this.inputs.push(input); + + return this; +}; + +MTX.prototype.scriptInput = function scriptInput(index, publicKey, redeem) { + var input, s, n, i; + + if (typeof index !== 'number') + index = this.inputs.indexOf(index); + + // Get the input + input = this.inputs[index]; + assert(input); + + // Already has a script template (at least) + // if (input.script.length) + // return; + + // We should have previous outputs by now. + assert(input.output); + + // Get the previous output's subscript + s = input.output.script; + + // P2SH + if (bcoin.script.isScripthash(s)) { + if (!redeem) + return false; + s = bcoin.script.decode(redeem); + } else { + redeem = null; + } + + if (bcoin.script.isPubkey(s)) { + // P2PK + if (!utils.isEqual(s[0], publicKey)) + return false; + // Already has a script template (at least) + if (input.script.length) + return true; + input.script = [0]; + } else if (bcoin.script.isPubkeyhash(s)) { + // P2PKH + if (!utils.isEqual(s[2], bcoin.address.hash160(publicKey))) + return false; + // Already has a script template (at least) + if (input.script.length) + return true; + input.script = [0, publicKey]; + } else if (bcoin.script.isMultisig(s)) { + // Multisig + for (i = 0; i < s.length; i++) { + if (utils.isEqual(s[i], publicKey)) + break; + } + + if (i === s.length) + return false; + + // Already has a script template (at least) + if (input.script.length) + return true; + + // Technically we should create m signature slots, + // but we create n signature slots so we can order + // the signatures properly. + input.script = [0]; + + // Grab `n` value (number of keys). + n = s[s.length - 2]; + + // Fill script with `n` signature slots. + for (i = 0; i < n; i++) + input.script[i + 1] = 0; + } else { + for (i = 0; i < s.length; i++) { + if (utils.isEqual(s[i], publicKey)) + break; + } + + if (i === s.length) + return false; + + // Already has a script template (at least) + if (input.script.length) + return true; + + // Likely a non-standard scripthash multisig + // input. Determine n value by counting keys. + // Also, only allow nonstandard types for + // scripthash. + if (redeem) { + input.script = [0]; + // Fill script with `n` signature slots. + for (i = 0; i < s.length; i++) { + if (bcoin.script.isKey(s[i])) + input.script.push(0); + } + } + } + + // P2SH requires the redeem script after signatures + if (redeem) + input.script.push(redeem); + + return true; +}; + +MTX.prototype.createSignature = function createSignature(index, key, type) { + var input, s, hash, signature; + + if (typeof index !== 'number') + index = this.inputs.indexOf(index); + + if (type == null) + type = 'all'; + + if (typeof type === 'string') + type = constants.hashType[type]; + + // Get the input + input = this.inputs[index]; + assert(input); + + // We should have previous outputs by now. + assert(input.output); + + // Get the previous output's subscript + s = input.output.script; + + // We need to grab the redeem script when + // signing p2sh transactions. + if (bcoin.script.isScripthash(s)) + s = bcoin.script.getRedeem(input.script); + + // Get the hash of the current tx, minus the other + // inputs, plus the sighash type. + hash = this.signatureHash(index, s, type); + + // Sign the transaction with our one input + signature = bcoin.script.sign(hash, key, type); + + // Something is broken if this doesn't work: + assert(bcoin.script.checksig(hash, signature, key)); + + return signature; +}; + +// Sign the now-built scriptSigs +MTX.prototype.signInput = function signInput(index, key, type) { + var input, s, signature, ki, signatures, i; + var len, m, n, keys, publicKey, keyHash; + + if (typeof index !== 'number') + index = this.inputs.indexOf(index); + + // Get the input + input = this.inputs[index]; + assert(input); + + // We should have previous outputs by now. + assert(input.output); + + // Create our signature. + signature = this.createSignature(index, key, type); + + // Get the previous output's subscript + s = input.output.script; + + // Script length, needed for multisig + len = input.script.length; + + // We need to grab the redeem script when + // signing p2sh transactions. + if (bcoin.script.isScripthash(s)) { + s = bcoin.script.getRedeem(input.script); + // Decrement `len` to avoid the redeem script + len--; + } + + // Get pubkey. + publicKey = key.getPublicKey(); + + // Add signatures. + if (bcoin.script.isPubkey(s)) { + // P2PK + + // Already signed. + if (bcoin.script.isSignature(input.script[0])) + return true; + + // Make sure the pubkey is ours. + if (!utils.isEqual(publicKey, s[0])) + return false; + + input.script[0] = signature; + + return true; + } + + if (bcoin.script.isPubkeyhash(s)) { + // P2PKH + + // Already signed. + if (bcoin.script.isSignature(input.script[0])) + return true; + + // Make sure the pubkey hash is ours. + keyHash = bcoin.address.hash160(publicKey); + if (!utils.isEqual(keyHash, s[2])) + return false; + + input.script[0] = signature; + + return true; + } + + if (bcoin.script.isMultisig(s)) { + // Multisig + + // Grab the redeem script's keys to figure + // out where our key should go. + keys = s.slice(1, -2); + + // Grab `m` value (number of sigs required). + m = s[0]; + + // Grab `n` value (number of keys). + n = s[s.length - 2]; + } else { + // Only allow non-standard signing for + // scripthash. + if (len !== input.script.length - 1) + return false; + + keys = []; + + for (i = 0; i < s.length; i++) { + if (bcoin.script.isKey(s[i])) + keys.push(s[i]); + } + + n = keys.length; + m = n; + } + + // Something is very wrong here. Abort. + if (len - 1 > n) + return false; + + // Count the number of current signatures. + signatures = 0; + for (i = 1; i < len; i++) { + if (bcoin.script.isSignature(input.script[i])) + signatures++; + } + + // Signatures are already finalized. + if (signatures === m && len - 1 === m) + return true; + + // This can happen in a case where another + // implementation adds signatures willy-nilly + // or by `m`. Add some signature slots for + // us to use. + while (len - 1 < n) { + input.script.splice(len, 0, 0); + len++; + } + + // Find the key index so we can place + // the signature in the same index. + for (ki = 0; ki < keys.length; ki++) { + if (utils.isEqual(publicKey, keys[ki])) + break; + } + + // Our public key is not in the prev_out + // script. We tried to sign a transaction + // that is not redeemable by us. + if (ki === keys.length) + return false; + + // Offset key index by one to turn it into + // "sig index". Accounts for OP_0 byte at + // the start. + ki++; + + // Add our signature to the correct slot + // and increment the total number of + // signatures. + if (ki < len && signatures < m) { + if (input.script[ki] === 0) { + input.script[ki] = signature; + signatures++; + } + } + + // All signatures added. Finalize. + if (signatures >= m) { + // Remove empty slots left over. + for (i = len - 1; i >= 1; i--) { + if (input.script[i] === 0) { + input.script.splice(i, 1); + len--; + } + } + + // Remove signatures which are not required. + // This should never happen except when dealing + // with implementations that potentially handle + // signature slots differently. + while (signatures > m) { + input.script.splice(len - 1, 1); + signatures--; + len--; + } + + // Sanity checks. + assert.equal(signatures, m); + assert.equal(len - 1, m); + } + + return signatures === m; +}; + +MTX.prototype.sign = function sign(index, key, redeem, type) { + var publicKey = key.getPublicKey(); + var input; + + if (index && typeof index === 'object') + index = this.inputs.indexOf(index); + + input = this.inputs[index]; + assert(input); + + // Build script for input + if (!this.scriptInput(index, publicKey, redeem)) + return false; + + // Sign input + if (!this.signInput(index, key, type)) + return false; + + return true; +}; + +MTX.prototype.isSigned = function isSigned(index, required) { + var i, input, s, len, m, j, total; + + if (this._signed) + return true; + + if (index && typeof index === 'object') + index = this.inputs.indexOf(index); + + if (index != null) + assert(this.inputs[index]); + + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + + if (index != null && i !== index) + continue; + + // We can't check for signatures unless + // we have the previous output. + assert(input.output); + + // Get the prevout's subscript + s = input.output.script; + + // Script length, needed for multisig + len = input.script.length; + + // Grab the redeem script if P2SH + if (bcoin.script.isScripthash(s)) { + s = bcoin.script.getRedeem(input.script); + // Decrement `len` to avoid the redeem script + len--; + } + + // Check for signatures. + // P2PK + if (bcoin.script.isPubkey(s)) { + if (!bcoin.script.isSignature(input.script[0])) + return false; + continue; + } + + // P2PK + if (bcoin.script.isPubkeyhash(s)) { + if (!bcoin.script.isSignature(input.script[0])) + return false; + continue; + } + + // Multisig + if (bcoin.script.isMultisig(s)) { + // Grab `m` value (number of required sigs). + m = s[0]; + if (Buffer.isBuffer(m)) + m = m[0] || 0; + + // Ensure all members are signatures. + for (j = 1; j < len; j++) { + if (!bcoin.script.isSignature(input.script[j])) + return false; + } + + // Ensure we have the correct number + // of required signatures. + if (len - 1 !== m) + return false; + + continue; + } + + if (required == null) + continue; + + // Unknown + total = 0; + for (j = 0; j < input.script.length; j++) { + if (bcoin.script.isSignatureEncoding(input.script[j])) + total++; + } + + if (total !== required) + return false; + } + + return this._signed = true; +}; + +MTX.prototype.addOutput = function addOutput(obj, value) { + var options, output; + + if ((obj instanceof bcoin.wallet) || (obj instanceof bcoin.address)) + obj = obj.getAddress(); + + if (typeof obj === 'string') { + options = { + address: obj, + value: value + }; + } else { + options = obj; + } + + output = bcoin.output(options); + + this.outputs.push(output); + + this.scriptOutput(this.outputs.length - 1, options); + + return this; +}; + +MTX.prototype.scriptOutput = function scriptOutput(index, options) { + var output, script, keys, m, n, hash, flags; + + if (options instanceof bcoin.output) + return; + + if (typeof index !== 'number') + index = this.outputs.indexOf(index); + + output = this.outputs[index]; + assert(output); + + script = output.script; + + if (options.keys) { + // Bare Multisig Transaction + // https://github.com/bitcoin/bips/blob/master/bip-0010.mediawiki + // https://github.com/bitcoin/bips/blob/master/bip-0011.mediawiki + // https://github.com/bitcoin/bips/blob/master/bip-0019.mediawiki + // m [key1] [key2] ... n checkmultisig + keys = options.keys.map(utils.ensureBuffer); + + m = options.m; + n = options.n || keys.length; + + if (!(m >= 1 && m <= n)) + return; + + if (!(n >= 1 && n <= (options.scriptHash ? 15 : 3))) + return; + + script = bcoin.script.createMultisig(keys, m, n); + } else if (bcoin.address.getType(options.address) === 'scripthash') { + // P2SH Transaction + // https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki + // hash160 [20-byte-redeemscript-hash] equal + script = bcoin.script.createScripthash( + bcoin.address.toHash(options.address, 'scripthash') + ); + } else if (options.address) { + // P2PKH Transaction + // dup hash160 [pubkey-hash] equalverify checksig + script = bcoin.script.createPubkeyhash( + bcoin.address.toHash(options.address, 'pubkeyhash') + ); + } else if (options.key) { + // P2PK Transaction + // [pubkey] checksig + script = [ + utils.ensureBuffer(options.key), + 'checksig' + ]; + } else if (options.flags) { + // Nulldata Transaction + // return [data] + flags = options.flags; + if (typeof flags === 'string') + flags = new Buffer(flags, 'ascii'); + assert(Buffer.isBuffer(flags)); + assert(flags.length <= constants.script.maxOpReturn); + script = bcoin.script.createNulldata(flags); + } + + // P2SH Transaction + // hash160 [hash] eq + if (options.scriptHash) { + if (options.locktime != null) { + script = [ + bcoin.script.array(options.locktime), + 'checklocktimeverify', + 'drop' + ].concat(script); + } + hash = utils.ripesha(bcoin.script.encode(script)); + script = bcoin.script.createScripthash(hash); + } + + output.script = script; +}; + +MTX.prototype.maxSize = function maxSize(maxM, maxN) { + var copy = this.clone(); + var i, j, input, total, size, s, m, n; + + // Create copy with 0-script inputs + for (i = 0; i < copy.inputs.length; i++) + copy.inputs[i].script = []; + + total = copy.render(true).length; + + // Add size for signatures and public keys + for (i = 0; i < copy.inputs.length; i++) { + input = copy.inputs[i]; + size = 0; + + assert(input.output); + + // Get the previous output's subscript + s = input.output.script; + + // If we have access to the redeem script, + // we can use it to calculate size much easier. + if (this.inputs[i].script.length && bcoin.script.isScripthash(s)) { + s = bcoin.script.getRedeem(this.inputs[i].script); + // Need to add the redeem script size + // here since it will be ignored by + // the isMultisig clause. + // OP_PUSHDATA2 [redeem] + size += 3 + bcoin.script.getSize(s); + } + + if (bcoin.script.isPubkey(s)) { + // P2PK + // OP_PUSHDATA0 [signature] + size += 1 + 73; + } else if (bcoin.script.isPubkeyhash(s)) { + // P2PKH + // OP_PUSHDATA0 [signature] + size += 1 + 73; + // OP_PUSHDATA0 [key] + size += 1 + 33; + } else if (bcoin.script.isMultisig(s)) { + // Bare Multisig + // Get the previous m value: + m = s[0]; + // OP_0 + size += 1; + // OP_PUSHDATA0 [signature] ... + size += (1 + 73) * m; + } else if (bcoin.script.isScripthash(s)) { + // P2SH Multisig + // This technically won't work well for other + // kinds of P2SH. It will also over-estimate + // the fee by a lot (at least 10000 satoshis + // since we don't have access to the m and n + // values), which will be recalculated later. + // If fee turns out to be smaller later, we + // simply add more of the fee to the change + // output. + // m value + m = maxM || 15; + // n value + n = maxN || 15; + // OP_0 + size += 1; + // OP_PUSHDATA0 [signature] ... + size += (1 + 73) * m; + // OP_PUSHDATA2 [redeem] + size += 3; + // m value + size += 1; + // OP_PUSHDATA0 [key] ... + size += (1 + 33) * n; + // n value + size += 1; + // OP_CHECKMULTISIG + size += 1; + } else { + // OP_PUSHDATA0 [signature] + for (j = 0; j < s.length; j++) { + if (bcoin.script.isKey(s[j])) + size += 1 + 73; + } + } + + // Byte for varint size of input script + size += utils.sizeIntv(size); + + total += size; + } + + return total; +}; + +MTX.prototype.selectCoins = function selectCoins(unspent, options) { + var self = this; + var tx = this.clone(); + var outputValue = tx.getOutputValue(); + var totalkb = 1; + var chosen = []; + var lastAdded = 0; + var minFee = constants.tx.minFee; + var dustThreshold = constants.tx.dustThreshold; + var i, size, newkb, change; + var fee; + + assert(tx.inputs.length === 0); + + if (!options || typeof options !== 'object') { + options = { + changeAddress: arguments[1], + fee: arguments[2] + }; + } + + if (!options.selection || options.selection === 'age') { + // Oldest unspents first + unspent = unspent.slice().sort(function(a, b) { + return a.height - b.height; + }); + } else if (options.selection === 'random' || options.selection === 'all') { + // Random unspents + unspent = unspent.slice().sort(function(a, b) { + return Math.random() > 0.5 ? 1 : -1; + }); + } + + function total() { + if (options.subtractFee) + return outputValue; + return outputValue.add(fee); + } + + function isFull() { + return tx.getInputValue().cmp(total()) >= 0; + } + + function addCoins() { + var i, index; + + for (i = lastAdded; i < unspent.length; i++) { + // Add new inputs until MTX will have enough + // funds to cover both minimum post cost + // and fee. + tx.addInput(unspent[i]); + chosen.push(unspent[i]); + lastAdded++; + + if (options.wallet) + options.wallet.scriptInputs(tx, index); + + if (options.selection === 'all') + continue; + + // Stop once we're full. + if (isFull()) + break; + } + } + + if (options.fee) { + fee = options.fee; + + // Transfer `total` funds maximum. + addCoins(); + } else { + fee = new bn(minFee); + + // Transfer `total` funds maximum. + addCoins(); + + // Add dummy output (for `change`) to + // calculate maximum MTX size. + tx.addOutput({ + address: options.changeAddress, + value: new bn(0) + }); + + // Change fee value if it is more than 1024 + // bytes (10000 satoshi for every 1024 bytes). + do { + // Calculate max possible size after signing. + size = tx.maxSize(options.m, options.n); + + // if (newkb == null && tx.isFree(size)) { + // fee = new bn(0); + // break; + // } + + newkb = Math.ceil(size / 1024) - totalkb; + fee.iaddn(newkb * minFee); + totalkb += newkb; + + // Failed to get enough funds, add more inputs. + if (!isFull()) + addCoins(); + } while (!isFull() && lastAdded < unspent.length); + } + + if (!isFull()) { + // Still failing to get enough funds. + chosen = null; + } else { + // How much money is left after filling outputs. + change = tx.getInputValue().sub(total()); + + // Attempt to subtract fee. + if (options.subtractFee) { + for (i = 0; i < tx.outputs.length; i++) { + if (tx.outputs[i].value.cmp(fee.addn(dustThreshold)) >= 0) { + tx.outputs[i].value.isub(fee); + break; + } + } + // Could not subtract fee + if (i === tx.outputs.length) + chosen = null; + } + } + + // Return necessary inputs and change. + return { + coins: chosen, + change: change, + fee: fee, + total: total(), + kb: totalkb + }; +}; + +MTX.prototype.fill = function fill(unspent, options) { + var result, err; + + if (!options || typeof options !== 'object') { + options = { + changeAddress: arguments[1], + fee: arguments[2] + }; + } + + assert(unspent); + assert(options.changeAddress); + + result = this.selectCoins(unspent, options); + + if (!result.coins) { + err = new Error('Could not fill transaction'); + err.requiredFunds = result.total; + throw err; + } + + result.coins.forEach(function(coin) { + this.addInput(coin); + }, this); + + if (result.change.cmpn(constants.tx.dustThreshold) < 0) { + // Do nothing. Change is added to fee. + assert.equal( + this.getFee().toNumber(), + result.fee.add(result.change).toNumber() + ); + this.changeIndex = -1; + } else { + this.addOutput({ + address: options.changeAddress, + value: result.change + }); + + this.changeIndex = this.outputs.length - 1; + + assert.equal(this.getFee().toNumber(), result.fee.toNumber()); + } + + return result; +}; + +// https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki +MTX.prototype.sortMembers = function sortMembers() { + var changeOutput; + + if (this.changeIndex !== -1) { + changeOutput = this.outputs[this.changeIndex]; + assert(changeOutput); + } + + this.inputs = this.inputs.slice().sort(function(a, b) { + var h1 = new Buffer(a.prevout.hash, 'hex'); + var h2 = new Buffer(b.prevout.hash, 'hex'); + + var res = utils.cmp(h1, h2); + if (res !== 0) + return res; + + return a.prevout.index - b.prevout.index; + }); + + this.outputs = this.outputs.slice().sort(function(a, b) { + var res = a.value.cmp(b.value); + if (res !== 0) + return res; + + a = bcoin.script.encode(a.script); + b = bcoin.script.encode(b.script); + + return utils.cmp(a, b); + }); + + if (this.changeIndex !== -1) { + this.changeIndex = this.outputs.indexOf(changeOutput); + assert(this.changeIndex !== -1); + } +}; + +MTX.prototype.getTargetLocktime = function getTargetLocktime() { + var bestValue = 0; + var i, locktime, bestType; + + for (i = 0; i < this.inputs.length; i++) { + locktime = this.inputs[i].getLocktime(); + + if (!locktime) + continue; + + // Incompatible types + if (bestType && bestType !== locktime.type) + return; + + bestType = locktime.type; + + if (locktime.value < bestValue) + continue; + + bestValue = locktime.value; + } + + return { + type: bestType || 'height', + value: bestValue + }; +}; + +MTX.prototype.avoidFeeSniping = function avoidFeeSniping(height) { + if (height == null) { + if (!this.chain) + return; + + height = this.chain.height; + } + + if (height === -1) + height = 0; + + this.setLocktime(height); + + if ((Math.random() * 10 | 0) === 0) + this.setLocktime(Math.max(0, this.locktime - (Math.random() * 100 | 0))); +}; + +MTX.prototype.setLocktime = function setLocktime(locktime) { + var i, input; + + this.locktime = locktime; + + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + if (input.sequence === 0xffffffff) + input.sequence = 0; + } +}; + +MTX.prototype.increaseFee = function increaseFee(unspent, address, fee) { + var i, input, result; + + this.inputs.length = 0; + + if (this.changeIndex !== -1) + this.outputs.splice(this.changeIndex, 1); + + if (!fee) + fee = this.getFee().add(new bn(10000)); + + result = this.fill(unspent, address, fee); + + for (i = 0; i < this.inputs.length; i++) { + input = this.inputs[i]; + input.sequence = 0xffffffff - 1; + } +}; + +MTX.prototype.toCompact = function toCompact(coins) { + return { + type: 'tx', + block: this.block, + height: this.height, + ts: this.ts, + ps: this.ps, + relayedBy: this.relayedBy, + changeIndex: this.changeIndex, + coins: coins ? this.inputs.map(function(input) { + return input.output ? input.output.toRaw('hex') : null; + }) : null, + tx: utils.toHex(this.render()) + }; +}; + +MTX._fromCompact = function _fromCompact(json) { + var raw, data, tx; + + assert.equal(json.type, 'tx'); + + raw = new Buffer(json.tx, 'hex'); + data = new bcoin.protocol.parser().parseTX(raw); + + data.height = json.height; + data.block = json.block; + data.ts = json.ts; + data.ps = json.ps; + data.relayedBy = json.relayedBy; + data.changeIndex = json.changeIndex; + + if (json.coins) { + json.coins.forEach(function(output, i) { + if (!output) + return; + + data.inputs[i].output = bcoin.coin._fromRaw(output, 'hex'); + }); + } + + return data; +}; + +MTX.fromCompact = function fromCompact(json) { + return new MTX(MTX._fromCompact(json)); +}; + +MTX.prototype.toJSON = function toJSON() { + return { + type: 'mtx', + hash: utils.revHex(this.hash('hex')), + height: this.height, + block: this.block ? utils.revHex(this.block) : null, + ts: this.ts, + ps: this.ps, + relayedBy: this.relayedBy, + changeIndex: this.changeIndex, + version: this.version, + inputs: this.inputs.map(function(input) { + return input.toJSON(); + }), + outputs: this.outputs.map(function(output) { + return output.toJSON(); + }), + locktime: this.locktime + }; +}; + +MTX._fromJSON = function fromJSON(json) { + return { + block: json.block ? utils.revHex(json.block) : null, + height: json.height, + ts: json.ts, + ps: json.ps, + relayedBy: json.relayedBy, + changeIndex: json.changeIndex, + version: json.version, + inputs: json.inputs.map(function(input) { + return bcoin.input._fromJSON(input); + }), + outputs: json.outputs.map(function(output) { + return bcoin.output._fromJSON(output); + }), + locktime: json.locktime + }; +}; + +MTX.fromJSON = function fromJSON(json) { + return new MTX(MTX._fromJSON(json)); +}; + +MTX.prototype.toRaw = function toRaw(enc) { + var data = this.render(); + + if (enc === 'hex') + data = utils.toHex(data); + + return data; +}; + +MTX._fromRaw = function _fromRaw(data, enc) { + var parser = new bcoin.protocol.parser(); + + if (enc === 'hex') + data = new Buffer(data, 'hex'); + + return parser.parseTX(data); +}; + +MTX.fromRaw = function fromRaw(data, enc) { + return new MTX(MTX._fromRaw(data, enc)); +}; + +MTX.fromTX = function fromTX(tx) { + var mtx = new bcoin.tx({ + ts: tx.ts, + block: tx.block, + height: tx.height, + version: tx.version, + inputs: tx.inputs.map(function(input) { + input.script = input.script.slice(); + return input; + }), + outputs: tx.outputs.map(function(output) { + output.script = output.script.slice(); + return output; + }), + locktime: tx.locktime + }); + mtx.ps = tx.ps; + return mtx; +}; + +MTX.prototype.toTX = function toTX() { + var tx = new bcoin.tx({ + ts: this.ts, + block: this.block, + height: this.height, + version: this.version, + inputs: this.inputs.map(function(input) { + input.script = input.script.slice(); + return input; + }), + outputs: this.outputs.map(function(output) { + output.script = output.script.slice(); + return output; + }), + locktime: this.locktime + }); + return tx; +}; + +/** + * Expose + */ + +module.exports = MTX; + diff --git a/lib/bcoin/tx-pool.js b/lib/bcoin/tx-pool.js index d9520257..d723d7e9 100644 --- a/lib/bcoin/tx-pool.js +++ b/lib/bcoin/tx-pool.js @@ -67,6 +67,9 @@ TXPool.prototype.add = function add(tx, noWrite) { if (!this._wallet.ownInput(tx) && !this._wallet.ownOutput(tx)) return false; + if (tx instanceof bcoin.tx) + tx = bcoin.mtx.fromTX(tx); + // Ignore stale pending transactions if (tx.ts === 0 && tx.ps + 2 * 24 * 3600 < utils.now()) { this._removeTX(tx, noWrite); @@ -77,7 +80,6 @@ TXPool.prototype.add = function add(tx, noWrite) { if (this._all[hash]) { // Transaction was confirmed, update it in storage if (tx.ts !== 0 && this._all[hash].ts === 0) { - this._all[hash].ps = 0; this._all[hash].ts = tx.ts; this._all[hash].block = tx.block; this._all[hash].height = tx.height; diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 4e88b00c..c5636006 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -31,16 +31,11 @@ function TX(data, block) { this.block = data.block || null; this._hash = null; - // Legacy - if (data.lock != null) - this.locktime = data.lock; - this._raw = data._raw || null; this._size = data._size || 0; this._offset = data._offset || 0; this.height = data.height != null ? data.height : -1; - this.network = data.network || false; this.relayedBy = data.relayedBy || '0.0.0.0'; this._chain = data.chain; @@ -68,65 +63,54 @@ function TX(data, block) { } } - this.changeIndex = data.changeIndex != null ? data.changeIndex : -1; + if (!this._raw) + this._raw = this.render(); - // ps = Pending Since - this.ps = this.ts === 0 ? utils.now() : 0; + if (!this._size) + this._size = this._raw.length; } TX.prototype.setBlock = function setBlock(block) { - this.network = true; this.relayedBy = block.relayedBy; this.ts = block.ts; this.block = block.hash('hex'); this.height = block.height; - this.ps = 0; }; -// Legacy -TX.prototype.__defineSetter__('lock', function(locktime) { - return this.locktime = locktime; -}); - -TX.prototype.__defineGetter__('lock', function() { - return this.locktime; -}); - -TX.prototype.__defineSetter__('lockTime', function(locktime) { - return this.locktime = locktime; -}); - -TX.prototype.__defineGetter__('lockTime', function() { - return this.locktime; -}); - TX.prototype.clone = function clone() { - return new TX(this); + var tx = new TX(this); + + tx.inputs = tx.inputs.map(function(input) { + input.script = input.script.slice(); + return input; + }); + + tx.outputs = tx.outputs.map(function(output) { + output.script = output.script.slice(); + return output; + }); + + delete tx._raw; + delete tx._size; + + return tx; }; -TX.prototype.isStatic = function isStatic() { - return this.ts !== 0 || this.network; -}; - -TX.prototype.hash = function hash(enc, force) { +TX.prototype.hash = function hash(enc) { var hash; - if (!force && this._hash) + if (this._hash) return enc === 'hex' ? utils.toHex(this._hash) : this._hash; - if (!force && this.isStatic() && this._raw) - hash = utils.dsha256(this._raw); - else - hash = utils.dsha256(this.render(true)); + hash = utils.dsha256(this._raw); - if (this.isStatic()) - this._hash = hash; + this._hash = hash; return enc === 'hex' ? utils.toHex(hash) : hash; }; -TX.prototype.render = function render(force) { - if (!force && this.isStatic() && this._raw) +TX.prototype.render = function render() { + if (this._raw) return this._raw; return bcoin.protocol.framer.tx(this); }; @@ -135,35 +119,14 @@ TX.prototype.getSize = function getSize() { return this._size || this.render().length; }; -TX.prototype.size = TX.prototype.getSize; +TX.prototype.addInput = function addInput(input) { + assert(input.prevout); -TX.prototype.addInput = function _addInput(options, index) { - var input, i; - - if (options instanceof TX) - options = bcoin.coin(options, index); - - if (options instanceof bcoin.coin) { - options = { - prevout: { hash: options.hash, index: options.index }, - output: options - }; - } - - assert(options.prevout); - - // i = this._inputIndex(options.prevout.hash, options.prevout.index); - // assert(i === -1); - - input = bcoin.input(options); + input = bcoin.input(input); this.inputs.push(input); - - return this; }; -TX.prototype.input = TX.prototype.addInput; - TX.prototype._inputIndex = function _inputIndex(hash, index) { var i, ex; @@ -176,545 +139,10 @@ TX.prototype._inputIndex = function _inputIndex(hash, index) { return -1; }; -TX.prototype.scriptInput = function scriptInput(index, publicKey, redeem) { - var input, s, n, i; - - if (typeof index !== 'number') - index = this.inputs.indexOf(index); - - // Get the input - input = this.inputs[index]; - assert(input); - - // Already has a script template (at least) - // if (input.script.length) - // return; - - // We should have previous outputs by now. - assert(input.output); - - // Get the previous output's subscript - s = input.output.script; - - // P2SH - if (bcoin.script.isScripthash(s)) { - if (!redeem) - return false; - s = bcoin.script.decode(redeem); - } else { - redeem = null; - } - - if (bcoin.script.isPubkey(s)) { - // P2PK - if (!utils.isEqual(s[0], publicKey)) - return false; - // Already has a script template (at least) - if (input.script.length) - return true; - input.script = [0]; - } else if (bcoin.script.isPubkeyhash(s)) { - // P2PKH - if (!utils.isEqual(s[2], bcoin.address.hash160(publicKey))) - return false; - // Already has a script template (at least) - if (input.script.length) - return true; - input.script = [0, publicKey]; - } else if (bcoin.script.isMultisig(s)) { - // Multisig - for (i = 0; i < s.length; i++) { - if (utils.isEqual(s[i], publicKey)) - break; - } - - if (i === s.length) - return false; - - // Already has a script template (at least) - if (input.script.length) - return true; - - // Technically we should create m signature slots, - // but we create n signature slots so we can order - // the signatures properly. - input.script = [0]; - - // Grab `n` value (number of keys). - n = s[s.length - 2]; - - // Fill script with `n` signature slots. - for (i = 0; i < n; i++) - input.script[i + 1] = 0; - } else { - for (i = 0; i < s.length; i++) { - if (utils.isEqual(s[i], publicKey)) - break; - } - - if (i === s.length) - return false; - - // Already has a script template (at least) - if (input.script.length) - return true; - - // Likely a non-standard scripthash multisig - // input. Determine n value by counting keys. - // Also, only allow nonstandard types for - // scripthash. - if (redeem) { - input.script = [0]; - // Fill script with `n` signature slots. - for (i = 0; i < s.length; i++) { - if (bcoin.script.isKey(s[i])) - input.script.push(0); - } - } - } - - // P2SH requires the redeem script after signatures - if (redeem) - input.script.push(redeem); - - return true; -}; - -TX.prototype.createSignature = function createSignature(index, key, type) { - var input, s, hash, signature; - - if (typeof index !== 'number') - index = this.inputs.indexOf(index); - - if (type == null) - type = 'all'; - - if (typeof type === 'string') - type = constants.hashType[type]; - - // Get the input - input = this.inputs[index]; - assert(input); - - // We should have previous outputs by now. - assert(input.output); - - // Get the previous output's subscript - s = input.output.script; - - // We need to grab the redeem script when - // signing p2sh transactions. - if (bcoin.script.isScripthash(s)) - s = bcoin.script.getRedeem(input.script); - - // Get the hash of the current tx, minus the other - // inputs, plus the sighash type. - hash = this.signatureHash(index, s, type); - - // Sign the transaction with our one input - signature = bcoin.script.sign(hash, key, type); - - // Something is broken if this doesn't work: - assert(bcoin.script.checksig(hash, signature, key)); - - return signature; -}; - -// Legacy -TX.prototype.signature = TX.prototype.createSignature; - -// Sign the now-built scriptSigs -TX.prototype.signInput = function signInput(index, key, type) { - var input, s, signature, ki, signatures, i; - var len, m, n, keys, publicKey, keyHash; - - if (typeof index !== 'number') - index = this.inputs.indexOf(index); - - // Get the input - input = this.inputs[index]; - assert(input); - - // We should have previous outputs by now. - assert(input.output); - - // Create our signature. - signature = this.createSignature(index, key, type); - - // Get the previous output's subscript - s = input.output.script; - - // Script length, needed for multisig - len = input.script.length; - - // We need to grab the redeem script when - // signing p2sh transactions. - if (bcoin.script.isScripthash(s)) { - s = bcoin.script.getRedeem(input.script); - // Decrement `len` to avoid the redeem script - len--; - } - - // Get pubkey. - publicKey = key.getPublicKey(); - - // Add signatures. - if (bcoin.script.isPubkey(s)) { - // P2PK - - // Already signed. - if (bcoin.script.isSignature(input.script[0])) - return true; - - // Make sure the pubkey is ours. - if (!utils.isEqual(publicKey, s[0])) - return false; - - input.script[0] = signature; - - return true; - } - - if (bcoin.script.isPubkeyhash(s)) { - // P2PKH - - // Already signed. - if (bcoin.script.isSignature(input.script[0])) - return true; - - // Make sure the pubkey hash is ours. - keyHash = bcoin.address.hash160(publicKey); - if (!utils.isEqual(keyHash, s[2])) - return false; - - input.script[0] = signature; - - return true; - } - - if (bcoin.script.isMultisig(s)) { - // Multisig - - // Grab the redeem script's keys to figure - // out where our key should go. - keys = s.slice(1, -2); - - // Grab `m` value (number of sigs required). - m = s[0]; - - // Grab `n` value (number of keys). - n = s[s.length - 2]; - } else { - // Only allow non-standard signing for - // scripthash. - if (len !== input.script.length - 1) - return false; - - keys = []; - - for (i = 0; i < s.length; i++) { - if (bcoin.script.isKey(s[i])) - keys.push(s[i]); - } - - n = keys.length; - m = n; - } - - // Something is very wrong here. Abort. - if (len - 1 > n) - return false; - - // Count the number of current signatures. - signatures = 0; - for (i = 1; i < len; i++) { - if (bcoin.script.isSignature(input.script[i])) - signatures++; - } - - // Signatures are already finalized. - if (signatures === m && len - 1 === m) - return true; - - // This can happen in a case where another - // implementation adds signatures willy-nilly - // or by `m`. Add some signature slots for - // us to use. - while (len - 1 < n) { - input.script.splice(len, 0, 0); - len++; - } - - // Find the key index so we can place - // the signature in the same index. - for (ki = 0; ki < keys.length; ki++) { - if (utils.isEqual(publicKey, keys[ki])) - break; - } - - // Our public key is not in the prev_out - // script. We tried to sign a transaction - // that is not redeemable by us. - if (ki === keys.length) - return false; - - // Offset key index by one to turn it into - // "sig index". Accounts for OP_0 byte at - // the start. - ki++; - - // Add our signature to the correct slot - // and increment the total number of - // signatures. - if (ki < len && signatures < m) { - if (input.script[ki] === 0) { - input.script[ki] = signature; - signatures++; - } - } - - // All signatures added. Finalize. - if (signatures >= m) { - // Remove empty slots left over. - for (i = len - 1; i >= 1; i--) { - if (input.script[i] === 0) { - input.script.splice(i, 1); - len--; - } - } - - // Remove signatures which are not required. - // This should never happen except when dealing - // with implementations that potentially handle - // signature slots differently. - while (signatures > m) { - input.script.splice(len - 1, 1); - signatures--; - len--; - } - - // Sanity checks. - assert.equal(signatures, m); - assert.equal(len - 1, m); - } - - return signatures === m; -}; - -TX.prototype.sign = function sign(index, key, redeem, type) { - var publicKey = key.getPublicKey(); - var input; - - if (index && typeof index === 'object') - index = this.inputs.indexOf(index); - - input = this.inputs[index]; - assert(input); - - // Build script for input - if (!this.scriptInput(index, publicKey, redeem)) - return false; - - // Sign input - if (!this.signInput(index, key, type)) - return false; - - return true; -}; - -TX.prototype.isSigned = function isSigned(index, required) { - var i, input, s, len, m, j, total; - - if (this._signed) - return true; - - if (index && typeof index === 'object') - index = this.inputs.indexOf(index); - - if (index != null) - assert(this.inputs[index]); - - for (i = 0; i < this.inputs.length; i++) { - input = this.inputs[i]; - - if (index != null && i !== index) - continue; - - // We can't check for signatures unless - // we have the previous output. - assert(input.output); - - // Get the prevout's subscript - s = input.output.script; - - // Script length, needed for multisig - len = input.script.length; - - // Grab the redeem script if P2SH - if (bcoin.script.isScripthash(s)) { - s = bcoin.script.getRedeem(input.script); - // Decrement `len` to avoid the redeem script - len--; - } - - // Check for signatures. - // P2PK - if (bcoin.script.isPubkey(s)) { - if (!bcoin.script.isSignature(input.script[0])) - return false; - continue; - } - - // P2PK - if (bcoin.script.isPubkeyhash(s)) { - if (!bcoin.script.isSignature(input.script[0])) - return false; - continue; - } - - // Multisig - if (bcoin.script.isMultisig(s)) { - // Grab `m` value (number of required sigs). - m = s[0]; - if (Buffer.isBuffer(m)) - m = m[0] || 0; - - // Ensure all members are signatures. - for (j = 1; j < len; j++) { - if (!bcoin.script.isSignature(input.script[j])) - return false; - } - - // Ensure we have the correct number - // of required signatures. - if (len - 1 !== m) - return false; - - continue; - } - - if (required == null) - continue; - - // Unknown - total = 0; - for (j = 0; j < input.script.length; j++) { - if (bcoin.script.isSignatureEncoding(input.script[j])) - total++; - } - - if (total !== required) - return false; - } - - return this._signed = true; -}; - -TX.prototype.addOutput = function addOutput(obj, value) { - var options, output; - - if ((obj instanceof bcoin.wallet) || (obj instanceof bcoin.address)) - obj = obj.getAddress(); - - if (typeof obj === 'string') { - options = { - address: obj, - value: value - }; - } else { - options = obj; - } - - output = bcoin.output(options); +TX.prototype.addOutput = function addOutput(output) { + output = bcoin.output(output); this.outputs.push(output); - - this.scriptOutput(this.outputs.length - 1, options); - - return this; -}; - -TX.prototype.out = TX.prototype.addOutput; -TX.prototype.output = TX.prototype.addOutput; - -TX.prototype.scriptOutput = function scriptOutput(index, options) { - var output, script, keys, m, n, hash, flags; - - if (options instanceof bcoin.output) - return; - - if (typeof index !== 'number') - index = this.outputs.indexOf(index); - - output = this.outputs[index]; - assert(output); - - script = output.script; - - if (options.keys) { - // Bare Multisig Transaction - // https://github.com/bitcoin/bips/blob/master/bip-0010.mediawiki - // https://github.com/bitcoin/bips/blob/master/bip-0011.mediawiki - // https://github.com/bitcoin/bips/blob/master/bip-0019.mediawiki - // m [key1] [key2] ... n checkmultisig - keys = options.keys.map(utils.ensureBuffer); - - m = options.m; - n = options.n || keys.length; - - if (!(m >= 1 && m <= n)) - return; - - if (!(n >= 1 && n <= (options.scriptHash ? 15 : 3))) - return; - - script = bcoin.script.createMultisig(keys, m, n); - } else if (bcoin.address.getType(options.address) === 'scripthash') { - // P2SH Transaction - // https://github.com/bitcoin/bips/blob/master/bip-0016.mediawiki - // hash160 [20-byte-redeemscript-hash] equal - script = bcoin.script.createScripthash( - bcoin.address.toHash(options.address, 'scripthash') - ); - } else if (options.address) { - // P2PKH Transaction - // dup hash160 [pubkey-hash] equalverify checksig - script = bcoin.script.createPubkeyhash( - bcoin.address.toHash(options.address, 'pubkeyhash') - ); - } else if (options.key) { - // P2PK Transaction - // [pubkey] checksig - script = [ - utils.ensureBuffer(options.key), - 'checksig' - ]; - } else if (options.flags) { - // Nulldata Transaction - // return [data] - flags = options.flags; - if (typeof flags === 'string') - flags = new Buffer(flags, 'ascii'); - assert(Buffer.isBuffer(flags)); - assert(flags.length <= constants.script.maxOpReturn); - script = bcoin.script.createNulldata(flags); - } - - // P2SH Transaction - // hash160 [hash] eq - if (options.scriptHash) { - if (options.locktime != null) { - script = [ - bcoin.script.array(options.locktime), - 'checklocktimeverify', - 'drop' - ].concat(script); - } - hash = utils.ripesha(bcoin.script.encode(script)); - script = bcoin.script.createScripthash(hash); - } - - output.script = script; }; TX.prototype.getSubscript = function getSubscript(index) { @@ -795,7 +223,7 @@ TX.prototype.signatureHash = function signatureHash(index, s, type) { copy.inputs.length = 1; } - copy = copy.render(true); + copy = copy.render(); msg = new Buffer(copy.length + 4); utils.copy(copy, msg, 0); @@ -819,7 +247,7 @@ TX.prototype.tbsHash = function tbsHash(enc, force) { copy.inputs[i].script = []; } - this._tbsHash = utils.dsha256(copy.render(true)); + this._tbsHash = utils.dsha256(copy.render()); } return enc === 'hex' @@ -861,323 +289,6 @@ TX.prototype.isCoinbase = function isCoinbase() { return this.inputs.length === 1 && +this.inputs[0].prevout.hash === 0; }; -TX.prototype.maxSize = function maxSize(maxM, maxN) { - var copy = this.clone(); - var i, j, input, total, size, s, m, n; - - // Create copy with 0-script inputs - for (i = 0; i < copy.inputs.length; i++) - copy.inputs[i].script = []; - - total = copy.render(true).length; - - // Add size for signatures and public keys - for (i = 0; i < copy.inputs.length; i++) { - input = copy.inputs[i]; - size = 0; - - assert(input.output); - - // Get the previous output's subscript - s = input.output.script; - - // If we have access to the redeem script, - // we can use it to calculate size much easier. - if (this.inputs[i].script.length && bcoin.script.isScripthash(s)) { - s = bcoin.script.getRedeem(this.inputs[i].script); - // Need to add the redeem script size - // here since it will be ignored by - // the isMultisig clause. - // OP_PUSHDATA2 [redeem] - size += 3 + bcoin.script.getSize(s); - } - - if (bcoin.script.isPubkey(s)) { - // P2PK - // OP_PUSHDATA0 [signature] - size += 1 + 73; - } else if (bcoin.script.isPubkeyhash(s)) { - // P2PKH - // OP_PUSHDATA0 [signature] - size += 1 + 73; - // OP_PUSHDATA0 [key] - size += 1 + 33; - } else if (bcoin.script.isMultisig(s)) { - // Bare Multisig - // Get the previous m value: - m = s[0]; - // OP_0 - size += 1; - // OP_PUSHDATA0 [signature] ... - size += (1 + 73) * m; - } else if (bcoin.script.isScripthash(s)) { - // P2SH Multisig - // This technically won't work well for other - // kinds of P2SH. It will also over-estimate - // the fee by a lot (at least 10000 satoshis - // since we don't have access to the m and n - // values), which will be recalculated later. - // If fee turns out to be smaller later, we - // simply add more of the fee to the change - // output. - // m value - m = maxM || 15; - // n value - n = maxN || 15; - // OP_0 - size += 1; - // OP_PUSHDATA0 [signature] ... - size += (1 + 73) * m; - // OP_PUSHDATA2 [redeem] - size += 3; - // m value - size += 1; - // OP_PUSHDATA0 [key] ... - size += (1 + 33) * n; - // n value - size += 1; - // OP_CHECKMULTISIG - size += 1; - } else { - // OP_PUSHDATA0 [signature] - for (j = 0; j < s.length; j++) { - if (bcoin.script.isKey(s[j])) - size += 1 + 73; - } - } - - // Byte for varint size of input script - size += utils.sizeIntv(size); - - total += size; - } - - return total; -}; - -TX.prototype.selectCoins = function selectCoins(unspent, options) { - var self = this; - var tx = this.clone(); - var outputValue = tx.getOutputValue(); - var totalkb = 1; - var chosen = []; - var lastAdded = 0; - var minFee = constants.tx.minFee; - var dustThreshold = constants.tx.dustThreshold; - var i, size, newkb, change; - var fee; - - assert(tx.inputs.length === 0); - - if (!options || typeof options !== 'object') { - options = { - changeAddress: arguments[1], - fee: arguments[2] - }; - } - - if (!options.selection || options.selection === 'age') { - // Oldest unspents first - unspent = unspent.slice().sort(function(a, b) { - return a.height - b.height; - }); - } else if (options.selection === 'random' || options.selection === 'all') { - // Random unspents - unspent = unspent.slice().sort(function(a, b) { - return Math.random() > 0.5 ? 1 : -1; - }); - } - - function total() { - if (options.subtractFee) - return outputValue; - return outputValue.add(fee); - } - - function isFull() { - return tx.getInputValue().cmp(total()) >= 0; - } - - function addCoins() { - var i, index; - - for (i = lastAdded; i < unspent.length; i++) { - // Add new inputs until TX will have enough - // funds to cover both minimum post cost - // and fee. - tx.addInput(unspent[i]); - chosen.push(unspent[i]); - lastAdded++; - - if (options.wallet) - options.wallet.scriptInputs(tx, index); - - if (options.selection === 'all') - continue; - - // Stop once we're full. - if (isFull()) - break; - } - } - - if (options.fee) { - fee = options.fee; - - // Transfer `total` funds maximum. - addCoins(); - } else { - fee = new bn(minFee); - - // Transfer `total` funds maximum. - addCoins(); - - // Add dummy output (for `change`) to - // calculate maximum TX size. - tx.addOutput({ - address: options.changeAddress, - value: new bn(0) - }); - - // Change fee value if it is more than 1024 - // bytes (10000 satoshi for every 1024 bytes). - do { - // Calculate max possible size after signing. - size = tx.maxSize(options.m, options.n); - - // if (newkb == null && tx.isFree(size)) { - // fee = new bn(0); - // break; - // } - - newkb = Math.ceil(size / 1024) - totalkb; - fee.iaddn(newkb * minFee); - totalkb += newkb; - - // Failed to get enough funds, add more inputs. - if (!isFull()) - addCoins(); - } while (!isFull() && lastAdded < unspent.length); - } - - if (!isFull()) { - // Still failing to get enough funds. - chosen = null; - } else { - // How much money is left after filling outputs. - change = tx.getInputValue().sub(total()); - - // Attempt to subtract fee. - if (options.subtractFee) { - for (i = 0; i < tx.outputs.length; i++) { - if (tx.outputs[i].value.cmp(fee.addn(dustThreshold)) >= 0) { - tx.outputs[i].value.isub(fee); - break; - } - } - // Could not subtract fee - if (i === tx.outputs.length) - chosen = null; - } - } - - // Return necessary inputs and change. - return { - coins: chosen, - change: change, - fee: fee, - total: total(), - kb: totalkb - }; -}; - -TX.prototype.fill = function fill(unspent, options) { - var result, err; - - if (!options || typeof options !== 'object') { - options = { - changeAddress: arguments[1], - fee: arguments[2] - }; - } - - assert(unspent); - assert(options.changeAddress); - - result = this.selectCoins(unspent, options); - - if (!result.coins) { - err = new Error('Could not fill transaction'); - err.requiredFunds = result.total; - throw err; - } - - result.coins.forEach(function(coin) { - this.addInput(coin); - }, this); - - if (result.change.cmpn(constants.tx.dustThreshold) < 0) { - // Do nothing. Change is added to fee. - assert.equal( - this.getFee().toNumber(), - result.fee.add(result.change).toNumber() - ); - this.changeIndex = -1; - } else { - this.addOutput({ - address: options.changeAddress, - value: result.change - }); - - this.changeIndex = this.outputs.length - 1; - - assert.equal(this.getFee().toNumber(), result.fee.toNumber()); - } - - return result; -}; - -// https://github.com/bitcoin/bips/blob/master/bip-0069.mediawiki -TX.prototype.sortMembers = function sortMembers() { - var changeOutput; - - if (this.changeIndex !== -1) { - changeOutput = this.outputs[this.changeIndex]; - assert(changeOutput); - } - - this.inputs = this.inputs.slice().sort(function(a, b) { - var h1 = new Buffer(a.prevout.hash, 'hex'); - var h2 = new Buffer(b.prevout.hash, 'hex'); - - var res = utils.cmp(h1, h2); - if (res !== 0) - return res; - - return a.prevout.index - b.prevout.index; - }); - - this.outputs = this.outputs.slice().sort(function(a, b) { - var res = a.value.cmp(b.value); - if (res !== 0) - return res; - - a = bcoin.script.encode(a.script); - b = bcoin.script.encode(b.script); - - return utils.cmp(a, b); - }); - - if (this.changeIndex !== -1) { - this.changeIndex = this.outputs.indexOf(changeOutput); - assert(this.changeIndex !== -1); - } -}; - -// Legacy -TX.prototype.fillUnspent = TX.prototype.fill; -TX.prototype.fillInputs = TX.prototype.fill; - TX.prototype.getFee = function getFee() { if (!this.hasPrevout()) return new bn(0); @@ -1219,37 +330,6 @@ TX.prototype.getFunds = function getFunds(side) { return this.getOutputValue(); }; -// Legacy -TX.prototype.funds = TX.prototype.getFunds; - -TX.prototype.getTargetLocktime = function getTargetLocktime() { - var bestValue = 0; - var i, locktime, bestType; - - for (i = 0; i < this.inputs.length; i++) { - locktime = this.inputs[i].getLocktime(); - - if (!locktime) - continue; - - // Incompatible types - if (bestType && bestType !== locktime.type) - return; - - bestType = locktime.type; - - if (locktime.value < bestValue) - continue; - - bestValue = locktime.value; - } - - return { - type: bestType || 'height', - value: bestValue - }; -}; - TX.prototype.testInputs = function testInputs(addressTable, index, collect) { var inputs = []; var i, input; @@ -1334,56 +414,6 @@ TX.prototype.testOutputs = function testOutputs(addressTable, index, collect) { return outputs; }; -TX.prototype.avoidFeeSniping = function avoidFeeSniping(height) { - if (height == null) { - if (!this.chain) - return; - - height = this.chain.height; - } - - if (height === -1) - height = 0; - - this.setLocktime(height); - - if ((Math.random() * 10 | 0) === 0) - this.setLocktime(Math.max(0, this.locktime - (Math.random() * 100 | 0))); -}; - -TX.prototype.setLocktime = function setLocktime(locktime) { - var i, input; - - this.locktime = locktime; - - for (i = 0; i < this.inputs.length; i++) { - input = this.inputs[i]; - if (input.sequence === 0xffffffff) - input.sequence = 0; - } -}; - -TX.prototype.increaseFee = function increaseFee(unspent, address, fee) { - var i, input, result; - - this.inputs = []; - - if (this.changeIndex !== -1) - this.outputs.splice(this.changeIndex, 1); - - if (!fee) - fee = this.getFee().add(new bn(10000)); - - result = this.fill(unspent, address, fee); - - for (i = 0; i < this.inputs.length; i++) { - input = this.inputs[i]; - input.sequence = 0xffffffff - 1; - } - - return !!result.inputs; -}; - TX.prototype.hasPrevout = function hasPrevout() { if (this.inputs.length === 0) return false; @@ -1594,6 +624,10 @@ TX.prototype.isStandardInputs = function isStandardInputs(flags) { return true; }; +TX.prototype.maxSize = function maxSize() { + return this.getSize(); +}; + TX.prototype.getPriority = function getPriority(size) { var sum, i, input, age, height; @@ -1724,10 +758,8 @@ TX.prototype.__defineGetter__('priority', function() { }); TX.prototype.inspect = function inspect() { - var copy = bcoin.tx(this); + var copy = this.clone(); copy.__proto__ = null; - if (this.block) - copy.block = this.block; delete copy._raw; delete copy._chain; copy.hash = this.hash('hex'); @@ -1747,10 +779,7 @@ TX.prototype.toCompact = function toCompact(coins) { block: this.block, height: this.height, ts: this.ts, - ps: this.ps, - network: this.network, relayedBy: this.relayedBy, - changeIndex: this.changeIndex, coins: coins ? this.inputs.map(function(input) { return input.output ? input.output.toRaw('hex') : null; }) : null, @@ -1769,10 +798,7 @@ TX._fromCompact = function _fromCompact(json) { data.height = json.height; data.block = json.block; data.ts = json.ts; - data.ps = json.ps; - data.network = json.network; data.relayedBy = json.relayedBy; - data.changeIndex = json.changeIndex; if (json.coins) { json.coins.forEach(function(output, i) { @@ -1797,10 +823,7 @@ TX.prototype.toJSON = function toJSON() { height: this.height, block: this.block ? utils.revHex(this.block) : null, ts: this.ts, - ps: this.ps, - network: this.network, relayedBy: this.relayedBy, - changeIndex: this.changeIndex, version: this.version, inputs: this.inputs.map(function(input) { return input.toJSON(); @@ -1817,10 +840,7 @@ TX._fromJSON = function fromJSON(json) { block: json.block ? utils.revHex(json.block) : null, height: json.height, ts: json.ts, - ps: json.ps, - network: json.network, relayedBy: json.relayedBy, - changeIndex: json.changeIndex, version: json.version, inputs: json.inputs.map(function(input) { return bcoin.input._fromJSON(input); diff --git a/test/wallet-test.js b/test/wallet-test.js index 6a003d9b..53077275 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -23,7 +23,7 @@ describe('Wallet', function() { var w = bcoin.wallet(); // Input transcation - var src = bcoin.tx({ + var src = bcoin.mtx({ outputs: [{ value: 5460 * 2, address: w.getAddress() @@ -37,9 +37,9 @@ describe('Wallet', function() { return acc.iadd(out.value); }, new bn(0)).toString(10), 5460 * 2); - var tx = bcoin.tx() - .input(src, 0) - .out(w.getAddress(), 5460); + var tx = bcoin.mtx() + .addInput(src, 0) + .addOutput(w.getAddress(), 5460); w.sign(tx); assert(tx.verify()); @@ -56,7 +56,7 @@ describe('Wallet', function() { w.addKey(k2); // Input transcation - var src = bcoin.tx({ + var src = bcoin.mtx({ outputs: [{ value: 5460 * 2, m: 1, @@ -71,9 +71,9 @@ describe('Wallet', function() { return acc.iadd(out.value); }, new bn(0)).toString(10), 5460 * 2); - var tx = bcoin.tx() - .input(src, 0) - .out(w.getAddress(), 5460); + var tx = bcoin.mtx() + .addInput(src, 0) + .addOutput(w.getAddress(), 5460); var maxSize = tx.maxSize(); w.sign(tx); @@ -86,31 +86,31 @@ describe('Wallet', function() { var f = bcoin.wallet(); // Coinbase - var t1 = bcoin.tx().out(w, 50000).out(w, 1000); + var t1 = bcoin.mtx().addOutput(w, 50000).addOutput(w, 1000); // balance: 51000 w.sign(t1); - var t2 = bcoin.tx().input(t1, 0) // 50000 - .out(w, 24000) - .out(w, 24000); + var t2 = bcoin.mtx().addInput(t1, 0) // 50000 + .addOutput(w, 24000) + .addOutput(w, 24000); // balance: 49000 w.sign(t2); - var t3 = bcoin.tx().input(t1, 1) // 1000 - .input(t2, 0) // 24000 - .out(w, 23000); + var t3 = bcoin.mtx().addInput(t1, 1) // 1000 + .addInput(t2, 0) // 24000 + .addOutput(w, 23000); // balance: 47000 w.sign(t3); - var t4 = bcoin.tx().input(t2, 1) // 24000 - .input(t3, 0) // 23000 - .out(w, 11000) - .out(w, 11000); + var t4 = bcoin.mtx().addInput(t2, 1) // 24000 + .addInput(t3, 0) // 23000 + .addOutput(w, 11000) + .addOutput(w, 11000); // balance: 22000 w.sign(t4); - var f1 = bcoin.tx().input(t4, 1) // 11000 - .out(f, 10000); + var f1 = bcoin.mtx().addInput(t4, 1) // 11000 + .addOutput(f, 10000); // balance: 11000 w.sign(f1); - var fake = bcoin.tx().input(t1, 1) // 1000 (already redeemed) - .out(w, 500); + var fake = bcoin.mtx().addInput(t1, 1) // 1000 (already redeemed) + .addOutput(w, 500); // Script inputs but do not sign w.scriptInputs(fake); // Fake signature @@ -154,25 +154,25 @@ describe('Wallet', function() { var w2 = bcoin.wallet(); // Coinbase - var t1 = bcoin.tx().out(w1, 5460).out(w1, 5460).out(w1, 5460).out(w1, 5460); + var t1 = bcoin.mtx().addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460); // Fake TX should temporarly change output w1.addTX(t1); // Create new transaction - var t2 = bcoin.tx().out(w2, 5460); + var t2 = bcoin.mtx().addOutput(w2, 5460); assert(w1.fill(t2)); w1.sign(t2); assert(t2.verify()); - assert.equal(t2.funds('in').toString(10), 16380); + assert.equal(t2.getInputValue().toString(10), 16380); // If change < dust and is added to outputs: - // assert.equal(t2.funds('out').toString(10), 6380); + // assert.equal(t2.getOutputValue().toString(10), 6380); // If change < dust and is added to fee: - assert.equal(t2.funds('out').toString(10), 5460); + assert.equal(t2.getOutputValue().toString(10), 5460); // Create new transaction - var t3 = bcoin.tx().out(w2, 15000); + var t3 = bcoin.mtx().addOutput(w2, 15000); try { w1.fill(t3); } catch (e) { @@ -190,33 +190,33 @@ describe('Wallet', function() { var to = bcoin.wallet(); // Coinbase - var t1 = bcoin.tx().out(w1, 5460).out(w1, 5460).out(w1, 5460).out(w1, 5460); + var t1 = bcoin.mtx().addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460).addOutput(w1, 5460); // Fake TX should temporarly change output w1.addTX(t1); // Coinbase - var t2 = bcoin.tx().out(w2, 5460).out(w2, 5460).out(w2, 5460).out(w2, 5460); + var t2 = bcoin.mtx().addOutput(w2, 5460).addOutput(w2, 5460).addOutput(w2, 5460).addOutput(w2, 5460); // Fake TX should temporarly change output w2.addTX(t2); // Create our tx with an output - var tx = bcoin.tx(); - tx.out(to, 5460); + var tx = bcoin.mtx(); + tx.addOutput(to, 5460); - var cost = tx.funds('out'); + var cost = tx.getOutputValue(); var total = cost.add(new bn(constants.tx.minFee)); var unspent1 = w1.unspent(); var unspent2 = w2.unspent(); // Add dummy output (for `left`) to calculate maximum TX size - tx.out(w1, new bn(0)); + tx.addOutput(w1, new bn(0)); // Add our unspent inputs to sign - tx.input(unspent1[0]); - tx.input(unspent1[1]); - tx.input(unspent2[0]); + tx.addInput(unspent1[0]); + tx.addInput(unspent1[1]); + tx.addInput(unspent2[0]); - var left = tx.funds('in').sub(total); + var left = tx.getInputValue().sub(total); if (left.cmpn(constants.tx.dustThreshold) < 0) { tx.outputs[tx.outputs.length - 2].value.iadd(left); left = new bn(0); @@ -235,9 +235,9 @@ describe('Wallet', function() { // Sign transaction using `inputs` and `off` params. tx.inputs.length = 0; - tx.input(unspent1[1]); - tx.input(unspent1[2]); - tx.input(unspent2[1]); + tx.addInput(unspent1[1]); + tx.addInput(unspent1[2]); + tx.addInput(unspent2[1]); assert.equal(w1.sign(tx, 'all'), 2); assert.equal(w2.sign(tx, 'all'), 1); @@ -288,8 +288,8 @@ describe('Wallet', function() { assert.equal(w3.getAddress(), addr); // Add a shared unspent transaction to our wallets - var utx = bcoin.tx(); - utx.output({ address: addr, value: 5460 * 10 }); + var utx = bcoin.mtx(); + utx.addOutput({ address: addr, value: 5460 * 10 }); // Simulate a confirmation utx.ps = 0; @@ -312,8 +312,8 @@ describe('Wallet', function() { assert.equal(w3.getAddress(), addr); // Create a tx requiring 2 signatures - var send = bcoin.tx(); - send.output({ address: receive.getAddress(), value: 5460 }); + var send = bcoin.mtx(); + send.addOutput({ address: receive.getAddress(), value: 5460 }); assert(!send.verify()); var result = w1.fill(send, { m: w1.m, n: w1.n }); assert(result);