From eb1a3ea6d29a224aa3f16249239dd2e862bce042 Mon Sep 17 00:00:00 2001 From: Christopher Jeffrey Date: Sun, 10 Jan 2016 03:23:29 -0800 Subject: [PATCH] transaction improvements. --- lib/bcoin/input.js | 17 ++-- lib/bcoin/output.js | 9 +- lib/bcoin/protocol/parser.js | 2 - lib/bcoin/script.js | 177 +++++++++++++++++++++++------------ lib/bcoin/tx-pool.js | 1 - lib/bcoin/tx.js | 64 +++---------- lib/bcoin/utils.js | 4 +- lib/bcoin/wallet.js | 41 ++++---- test/script-test.js | 2 +- test/wallet-test.js | 4 +- 10 files changed, 167 insertions(+), 154 deletions(-) diff --git a/lib/bcoin/input.js b/lib/bcoin/input.js index 99db8473..554d4beb 100644 --- a/lib/bcoin/input.js +++ b/lib/bcoin/input.js @@ -7,6 +7,7 @@ var bn = require('bn.js'); var bcoin = require('../bcoin'); var utils = bcoin.utils; +var constants = bcoin.protocol.constants; /** * Input @@ -180,6 +181,7 @@ Input.prototype.__defineGetter__('scriptaddr', function() { // Schema and defaults for data object: // { // type: null, +// subtype: null, // side: 'input', // signatures: [], // keys: [], @@ -263,7 +265,7 @@ Input.getData = function getData(input) { } else if (data.type === 'scripthash') { // We work backwards here: scripthash is one of the few cases // where we get more data from the input than the output. - val = Input.getData({ + val = bcoin.input.getData({ out: { hash: input.out.hash, index: input.out.index }, script: input.script, seq: input.seq @@ -313,19 +315,16 @@ Input.getData = function getData(input) { } if (bcoin.script.isScripthashInput(s)) { - signature = sub.slice(1, -1); redeem = sub[sub.length - 1]; hash = utils.ripesha(redeem); address = bcoin.wallet.hash2addr(hash, 'scripthash'); redeem = bcoin.script.decode(redeem); - data = bcoin.output.getData({ - script: redeem, - value: new bn(0) - }); - return utils.merge(data, { + val = bcoin.input.getData(sub.slice(0, -1)); + data = bcoin.output.getData(redeem); + return utils.merge(val, data, { type: 'scripthash', + subtype: data.type, side: 'input', - signatures: signature, redeem: redeem, scripthash: hash, scriptaddress: address, @@ -351,6 +350,7 @@ Input.prototype.inspect = function inspect() { return { type: this.type, + subtype: this.data.subtype, address: this.address, signatures: this.signatures.map(utils.toHex), keys: this.keys.map(utils.toHex), @@ -358,6 +358,7 @@ Input.prototype.inspect = function inspect() { lock: this.lock, value: utils.btc(output.value), script: bcoin.script.format(this.script)[0], + redeem: this.data.redeem ? bcoin.script.format(this.data.redeem)[0] : null, seq: this.seq, output: output }; diff --git a/lib/bcoin/output.js b/lib/bcoin/output.js index 220d5922..c99c6d31 100644 --- a/lib/bcoin/output.js +++ b/lib/bcoin/output.js @@ -147,6 +147,7 @@ Output.prototype.__defineGetter__('scriptaddr', function() { // Schema and defaults for data object: // { // type: null, +// subtype: null, // side: 'output', // signatures: [], // keys: [], @@ -215,14 +216,13 @@ Output.getData = function getData(output) { address = bcoin.wallet.hash2addr(hash); return utils.merge(def, { type: 'pubkeyhash', - side: 'output', hashes: [hash], addresses: [address] }); } - keys = bcoin.script.isMultisig(s); - if (keys) { + if (bcoin.script.isMultisig(s)) { + keys = sub.slice(1, -2); hash = keys.map(function(key) { return utils.ripesha(key); }); @@ -272,9 +272,6 @@ Output.prototype.inspect = function inspect() { keys: this.keys.map(utils.toHex), hashes: this.hashes.map(utils.toHex), addresses: this.addresses, - redeem: this.type === 'scripthash' - ? bcoin.script.format(this.data.redeem)[0] - : null, m: this.m, n: this.n, text: this.text, diff --git a/lib/bcoin/protocol/parser.js b/lib/bcoin/protocol/parser.js index 87bde29d..084abfe2 100644 --- a/lib/bcoin/protocol/parser.js +++ b/lib/bcoin/protocol/parser.js @@ -47,7 +47,6 @@ Parser.prototype.feed = function feed(data) { while (this.pendingTotal >= this.waiting) { // Concat chunks - // chunk = new Array(this.waiting); chunk = new Buffer(this.waiting); i = 0; @@ -55,7 +54,6 @@ Parser.prototype.feed = function feed(data) { len = 0; for (; off < chunk.length; i++) { - // len = utils.copy(this.pending[0], chunk, off); len = this.pending[0].copy(chunk, off, 0, this.pending[0].length); if (len === this.pending[0].length) this.pending.shift(); diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index 206ef01d..8b4d2c71 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -150,6 +150,64 @@ script.encode = function encode(s) { return res; }; +script.verify = function verify(input, output, tx, i, flags) { + var copy, res, redeem; + var stack = []; + + if (!flags) + flags = {}; + + // Execute the input script + script.execute(input, stack, tx, i, flags); + + // Copy the stack for P2SH + if (flags.verifyp2sh !== false) + copy = stack.slice(); + + // Execute the previous output script + res = script.execute(output, stack, tx, i, flags); + + // Verify the script did not fail as well as the stack values + if (!res || stack.length === 0 || new bn(stack.pop()).cmpn(0) === 0) + return false; + + // If the script is P2SH, execute the real output script + if (flags.verifyp2sh !== false && script.isScripthash(output)) { + // P2SH can only have push ops in the scriptSig + if (!script.pushOnly(input)) + return false; + + // Reset the stack + stack = copy; + + // Stack should _never_ be empty at this point + assert(stack.length !== 0); + + // Grab the real redeem script + redeem = stack.pop(); + + if (!Array.isArray(redeem)) + return false; + + redeem = script.decode(redeem); + + // Execute the redeem script + res = script.execute(redeem, stack, tx, i, flags); + + // Verify the script did not fail as well as the stack values + if (!res || stack.length === 0 || new bn(stack.pop()).cmpn(0) === 0) + return false; + } + + // Ensure there is nothing left on the stack + if (flags.cleanstack !== false) { + if (stack.length !== 0) + return false; + } + + return true; +}; + script.subscript = function subscript(s, lastSep) { var i, res; @@ -179,7 +237,7 @@ script.subscript = function subscript(s, lastSep) { return res; }; -script.verify = function verify(hash, sig, pub) { +script.checksig = function checksig(hash, sig, pub) { var k; try { @@ -249,7 +307,7 @@ script.execute = function execute(s, stack, tx, index, flags, recurse) { var n, n1, n2, n3; var res; var key, sig, type, subscript, hash; - var keys, i, j, key, m; + var keys, i, j, m; var succ; var lock, threshold; var evalScript; @@ -700,7 +758,7 @@ script.execute = function execute(s, stack, tx, index, flags, recurse) { subscript = script.subscript(s, lastSep); hash = tx.subscriptHash(index, subscript, type); - res = script.verify(hash, sig.slice(0, -1), key); + res = script.checksig(hash, sig.slice(0, -1), key); if (o === 'checksigverify') { if (!res) return false; @@ -763,13 +821,19 @@ script.execute = function execute(s, stack, tx, index, flags, recurse) { res = false; for (; !res && j < n; j++) - res = script.verify(hash, sig.slice(0, -1), keys[j]); + res = script.checksig(hash, sig.slice(0, -1), keys[j]); if (res) succ++; } // Extra value + if (stack.length < 1) + return false; + if (flags.verifynulldummy !== false) { + if (stack[stack.length - 1].length > 0) + return false; + } stack.pop(); // Too many signatures on stack @@ -866,20 +930,6 @@ script.execute = function execute(s, stack, tx, index, flags, recurse) { return true; }; -script.exec = function exec(input, output, tx, i, flags) { - var stack = []; - var res; - - script.execute(input, stack, tx, i, flags); - - res = script.execute(output, stack, tx, i, flags); - - if (!res || stack.length === 0 || new bn(stack.pop()).cmpn(0) === 0) - return false; - - return true; -}; - script.redeem = function redeem(keys, m, n) { if (keys.length !== n) throw new Error(n + ' keys are required to generate redeem script'); @@ -892,9 +942,9 @@ script.redeem = function redeem(keys, m, n) { keys = utils.sortKeys(keys); - return [ m ].concat( + return [m].concat( keys, - [ n, 'checkmultisig' ] + [n, 'checkmultisig'] ); }; @@ -908,11 +958,8 @@ script.standard = function standard(s) { }; script.isStandard = function isStandard(s) { - var m, n; var type = script.standard(s); - - if (!type) - return false; + var m, n; if (type === 'multisig') { m = new bn(s[0]).toNumber(); @@ -982,7 +1029,7 @@ script.isPubkey = function isPubkey(s, key) { if (key) return utils.isEqual(s[0], key); - return s[0]; + return true; }; script.isPubkeyhash = function isPubkeyhash(s, hash) { @@ -1008,11 +1055,11 @@ script.isPubkeyhash = function isPubkeyhash(s, hash) { if (hash) return utils.isEqual(s[2], hash); - return s[2]; + return true; }; script.isMultisig = function isMultisig(s, pubs) { - var m, n, keys, isArray, total; + var m, n, keys, total; s = script.subscript(s); @@ -1048,15 +1095,11 @@ script.isMultisig = function isMultisig(s, pubs) { keys = s.slice(1, 1 + n); - isArray = keys.every(function(k) { - return Array.isArray(k); - }); - - if (!isArray) + if (!keys.every(Array.isArray)) return false; if (!pubs) - return keys; + return true; total = keys.filter(function(k) { return pubs.some(function(pub) { @@ -1133,7 +1176,7 @@ script.isPubkeyInput = function isPubkeyInput(s, key, tx, i) { // checksig script to see if this is our input. // This will only work if the script verifies. if (key) - return script.exec(s, [key, 'checksig'], tx, i); + return script.verify(s, [key, 'checksig'], tx, i); return true; }; @@ -1153,10 +1196,10 @@ script.isPubkeyhashInput = function isPubkeyhashInput(s, key) { if (key) return utils.isEqual(s[1], key); - return s[1]; + return true; }; -script.isMultisigInput = function isMultisigInput(s, pubs, tx, i) { +script.isMultisigInput = function isMultisigInput(s, keys, tx, i) { var i, res, o; // We need to rule out scripthash @@ -1180,40 +1223,58 @@ script.isMultisigInput = function isMultisigInput(s, pubs, tx, i) { // Execute the script against our pubkeys' // redeem script to see if this is our input. // This will only work if the script verifies. - if (pubs && pubs.length >= 2) { - o = script.redeem(pubs, 2, pubs.length); - return script.exec(s, o, tx, i); + if (keys && keys.length >= 2) { + o = script.redeem(keys, s.length - 1, keys.length); + return script.verify(s, o, tx, i); } return true; }; -script.isScripthashInput = function isScripthashInput(s, redeem) { - var i, res, r, keys; +script.isScripthashInput = function isScripthashInput(s, data) { + var raw, redeem; s = script.subscript(s); - if (s.length < 4) + // Grab the raw redeem script. + raw = s[s.length - 1]; + + // Need at least one data element with + // the redeem script. + if (s.length < 2) return false; - if (!Array.isArray(s[0]) || s[0].length !== 0) + // Last data element should be an array + // for the redeem script. + if (!Array.isArray(raw)) return false; - for (i = 1; i < s.length - 1; i++) { - if (!script.isSig(s[i])) - return false; + // P2SH redeem scripts can be nonstandard: make + // it easier for other functions to parse this. + redeem = script.decode(raw); + redeem = script.subscript(redeem); + if (script.lockTime(redeem)) + redeem = redeem.slice(3); + + // Get the "real" scriptSig + s = s.slice(0, -1); + + // Do some sanity checking on the inputs + if (!script.isPubkeyInput(s) + && !script.isPubkeyhashInput(s) + && !script.isMultisigInput(s)) { + return false; } - r = Array.isArray(s[s.length - 1]) && s[s.length - 1]; - if (r[r.length - 1] !== constants.opcodes.checkmultisig) - return false; + // Check data against last array in case + // a raw redeem script was passed in. + if (data && utils.isEqual(data, raw)) + return true; - if (redeem) - return utils.isEqual(redeem, r); - - keys = script.decode(r).slice(1, -2); - - return keys; + // Test against all other script types + return script.isPubkey(redeem, data) + || script.isPubkeyhash(redeem, data) + || script.isMultisig(redeem, data); }; script.coinbaseBits = function coinbaseBits(s, block) { @@ -1526,7 +1587,7 @@ script.sigopsScripthash = function sigopsScripthash(s) { }; script.args = function args(s) { - var type, pubs, m; + var type, keys, m; s = bcoin.script.subscript(s); @@ -1542,11 +1603,11 @@ script.args = function args(s) { return 2; if (type === 'multisig') { - pubs = bcoin.script.isMultisig(s); + keys = bcoin.script.isMultisig(s); if (!pub) return -1; m = new bn(s[0]).toNumber(); - if (pubs.length < 1 || m < 1) + if (keys.length < 1 || m < 1) return -1; return m + 1; } diff --git a/lib/bcoin/tx-pool.js b/lib/bcoin/tx-pool.js index 58a0602b..8a38fb22 100644 --- a/lib/bcoin/tx-pool.js +++ b/lib/bcoin/tx-pool.js @@ -123,7 +123,6 @@ TXPool.prototype.add = function add(tx, noWrite) { // signature checking code to ownInput for p2sh and p2pk, // we could in theory use ownInput here (and down below) // instead. - // if (this._wallet.ownInput(input.out.tx, input.out.index)) if (input.out.tx) { if (!this._wallet.ownOutput(input.out.tx, input.out.index)) continue; diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index c1621481..10b38d17 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -596,8 +596,11 @@ TX.prototype.verify = function verify(index, force, flags) { if (this.inputs.length === 0) return false; + if (!flags) + flags = {}; + return this.inputs.every(function(input, i) { - var stack, prev, push, res, redeem; + var output; if (index != null && index !== i) return true; @@ -605,41 +608,16 @@ TX.prototype.verify = function verify(index, force, flags) { if (!input.out.tx) return false; + output = input.out.tx.outputs[input.out.index]; + + assert(input.out.tx.outputs.length > input.out.index); assert.equal(input.out.tx.hash('hex'), input.out.hash); // Transaction cannot reference itself if (input.out.tx.hash('hex') === this.hash('hex')) return false; - assert(input.out.tx.outputs.length > input.out.index); - - stack = []; - prev = input.out.tx.outputs[input.out.index].script; - - if (bcoin.script.isScripthash(prev)) { - // P2SH transactions cannot have anything - // other than pushdata ops in the scriptSig. - if (!bcoin.script.pushOnly(input.script)) - return false; - } - - bcoin.script.execute(input.script, stack, this, i, flags); - res = bcoin.script.execute(prev, stack, this, i, flags); - - if (!res || stack.length === 0 || new bn(stack.pop()).cmpn(0) === 0) - return false; - - if (bcoin.script.isScripthash(prev)) { - redeem = input.script[input.script.length - 1]; - if (!Array.isArray(redeem)) - return false; - redeem = bcoin.script.decode(redeem); - res = bcoin.script.execute(redeem, stack, this, i, flags); - if (!res || stack.length === 0 || new bn(stack.pop()).cmpn(0) === 0) - return false; - } - - return true; + return bcoin.script.verify(input.script, output.script, this, i, flags); }, this); }; @@ -660,7 +638,7 @@ TX.prototype.maxSize = function maxSize() { // Add size for signatures and public keys copy.inputs.forEach(function(input, i) { - var s, m, n, script, redeem; + var s, m, n; // Get the previous output's subscript s = input.out.tx.getSubscript(input.out.index); @@ -694,34 +672,16 @@ TX.prototype.maxSize = function maxSize() { } if (bcoin.script.isScripthash(s)) { - script = this.inputs[i].script; - if (script.length) { - redeem = bcoin.script.decode(script[script.length - 1]); - m = redeem[0]; - n = redeem[redeem.length - 2]; - // If using pushdata instead of OP_1-16: - if (Array.isArray(m)) - m = m[0] || 0; - if (Array.isArray(n)) - n = n[0] || 0; - } else { - // May end up in a higher fee if we - // do not have the redeem script available. - m = 15; - n = 15; - } - assert(m >= 1 && m <= n); - assert(n >= 1 && n <= 15); // Multisig // Empty byte size += 1; // Signature + len - size += 74 * m; + size += 74 * 15; // Redeem script // m byte size += 1; // 1 byte length + 65 byte pubkey - size += 66 * n; + size += 66 * 15; // n byte size += 1; // checkmultisig byte @@ -848,7 +808,7 @@ TX.prototype._recalculateFee = function recalculateFee() { output = this.outputs[this.outputs.length - 1]; } - var byteSize = this.maxSize(); + var byteSize = this.render().length; var newFee = Math.ceil(byteSize / 1024) * TX.fee; var currentFee = this.getFee().toNumber(); diff --git a/lib/bcoin/utils.js b/lib/bcoin/utils.js index 1495f421..b0adc520 100644 --- a/lib/bcoin/utils.js +++ b/lib/bcoin/utils.js @@ -366,8 +366,10 @@ utils.array2utf8 = function array2utf8(arr) { }; utils.copy = function copy(src, dst, off, force) { - if (Buffer.isBuffer(src) && Buffer.isBuffer(dst) && !force) + if (Buffer.isBuffer(src) && Buffer.isBuffer(dst)) { + assert(!force); return src.copy(dst, off, 0, src.length); + } var len = src.length; var i = 0; diff --git a/lib/bcoin/wallet.js b/lib/bcoin/wallet.js index 08de94bf..f323686d 100644 --- a/lib/bcoin/wallet.js +++ b/lib/bcoin/wallet.js @@ -405,7 +405,7 @@ Wallet.prototype.ownInput = function ownInput(tx, index) { if (bcoin.script.isPubkeyhashInput(input.script, key)) return true; - // if (bcoin.script.isMultisigInput(input.script, key, tx, i)) + // if (bcoin.script.isMultisigInput(input.script, keys, tx, i)) // return true; if (bcoin.script.isScripthashInput(input.script, redeem)) @@ -449,10 +449,9 @@ Wallet.prototype.fillUnspent = function fillUnspent(tx, changeAddress) { return tx.fillUnspent(this.unspent(), changeAddress); }; -Wallet.prototype.scriptInputs = function scriptInputs(tx, inputs) { +Wallet.prototype.scriptInputs = function scriptInputs(tx) { var pub = this.getFullPublicKey(); - - inputs = inputs || tx.inputs; + var inputs = tx.inputs; inputs = inputs.filter(function(input, i) { if (!input.out.tx && this.tx._all[input.out.hash]) @@ -470,14 +469,13 @@ Wallet.prototype.scriptInputs = function scriptInputs(tx, inputs) { return inputs.length; }; -Wallet.prototype.signInputs = function signInputs(tx, type, inputs) { +Wallet.prototype.signInputs = function signInputs(tx, type) { var key = this.key; + var inputs = tx.inputs; if (!type) type = 'all'; - inputs = inputs || tx.inputs; - inputs = inputs.filter(function(input, i) { if (!input.out.tx && this.tx._all[input.out.hash]) input.out.tx = this.tx._all[input.out.hash]; @@ -493,14 +491,13 @@ Wallet.prototype.signInputs = function signInputs(tx, type, inputs) { return inputs.length; }; -Wallet.prototype.sign = function sign(tx, type, inputs) { +Wallet.prototype.sign = function sign(tx, type) { if (!type) type = 'all'; var pub = this.getFullPublicKey(); var key = this.key; - - inputs = inputs || tx.inputs; + var inputs = tx.inputs; // Add signature script to each input inputs = inputs.filter(function(input, i) { @@ -623,11 +620,7 @@ Wallet.prototype.toJSON = function toJSON(encrypt) { }; Wallet.fromJSON = function fromJSON(json, decrypt) { - var compressed, key, w; - var priv = json.priv; - var pub = json.pub; - var xprivkey = json.xprivkey; - var multisig = json.multisig; + var priv, pub, xprivkey, multisig, compressed, key, w; assert.equal(json.v, 2); assert.equal(json.type, 'wallet'); @@ -638,7 +631,8 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { if (json.encrypted && !decrypt) throw new Error('Cannot decrypt wallet'); - if (priv) { + if (json.priv) { + priv = json.priv; if (json.encrypted) priv = decrypt(priv); @@ -656,20 +650,21 @@ Wallet.fromJSON = function fromJSON(json, decrypt) { compressed = false; } } else { - pub = bcoin.utils.fromBase58(pub); + pub = bcoin.utils.fromBase58(json.pub); compressed = pub[0] !== 0x04; } - if (multisig) { + if (json.multisig) { multisig = { - type: multisig.type, - keys: multisig.keys.map(utils.fromBase58), - m: multisig.m, - n: multisig.n + type: json.multisig.type, + keys: json.multisig.keys.map(utils.fromBase58), + m: json.multisig.m, + n: json.multisig.n }; } - if (xprivkey) { + if (json.xprivkey) { + xprivkey = json.xprivkey; if (json.encrypted) xprivkey = decrypt(xprivkey); priv = bcoin.hd.priv(xprivkey); diff --git a/test/script-test.js b/test/script-test.js index cb036ce5..f6747772 100644 --- a/test/script-test.js +++ b/test/script-test.js @@ -42,7 +42,7 @@ describe('Script', function() { var hex = '6a28590c080112220a1b353930632e6f7267282a5f5e294f7665726c6179404f7261636c65103b1a010c' var encoded = bcoin.utils.toArray(hex, 'hex') var decoded = bcoin.script.decode(encoded); - assert(bcoin.script.isColored(decoded)) + assert(bcoin.script.isNulldata(decoded)) }) it('should handle if statements correctly', function () { diff --git a/test/wallet-test.js b/test/wallet-test.js index 5dbcd2f7..2383f908 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -254,8 +254,8 @@ describe('Wallet', function() { tx.input(unspent1[1]); tx.input(unspent1[2]); tx.input(unspent2[1]); - assert.equal(w1.sign(tx, 'all', tx.inputs.slice(), 0), 2); - assert.equal(w2.sign(tx, 'all', tx.inputs.slice(2), 2), 1); + assert.equal(w1.sign(tx, 'all', tx.inputs.slice()), 2); + assert.equal(w2.sign(tx, 'all', tx.inputs.slice(2)), 1); // Verify assert.equal(tx.verify(), true);