diff --git a/lib/bcoin/env.js b/lib/bcoin/env.js index 2bad1966..3875e91d 100644 --- a/lib/bcoin/env.js +++ b/lib/bcoin/env.js @@ -70,6 +70,7 @@ if (!utils.isBrowser) * @property {Function} profiler - {@link module:profiler}. * @property {Function} ldb - See {@link module:ldb}. * @property {Function} script - {@link Script} constructor. + * @property {Function} opcode - {@link Opcode} constructor. * @property {Function} stack - {@link Stack} constructor. * @property {Function} witness - {@link Witness} constructor. * @property {Function} input - {@link Input} constructor. @@ -147,6 +148,7 @@ function Environment(options) { this.profiler = require('./profiler'); this.timedata = require('./timedata'); this.script = require('./script'); + this.opcode = this.script.Opcode; this.stack = this.script.Stack; this.witness = this.script.Witness; this.address = require('./address'); diff --git a/lib/bcoin/miner.js b/lib/bcoin/miner.js index 868b918a..4be398ed 100644 --- a/lib/bcoin/miner.js +++ b/lib/bcoin/miner.js @@ -452,8 +452,9 @@ MinerBlock.prototype.updateCommitment = function updateCommitment() { */ MinerBlock.prototype.updateCoinbase = function updateCoinbase() { - this.coinbase.inputs[0].script[1] = bcoin.script.array(this.extraNonce); - this.coinbase.inputs[0].script.refresh(); + var input = this.coinbase.inputs[0]; + input.script.code[1] = bcoin.opcode.fromNumber(this.extraNonce); + input.script.compile(); this.coinbase.outputs[0].value = this.block.getReward(this.network); }; diff --git a/lib/bcoin/script.js b/lib/bcoin/script.js index bf597f09..02425511 100644 --- a/lib/bcoin/script.js +++ b/lib/bcoin/script.js @@ -80,7 +80,7 @@ Witness.prototype.toString = function toString() { */ Witness.prototype.toASM = function toASM(decode) { - return Script.formatASM(this.items, decode); + return Script.formatASM(Script.parseArray(this.items), decode); }; /** @@ -146,8 +146,8 @@ Witness.prototype.isPubkeyInput = function isPubkeyInput() { Witness.prototype.isPubkeyhashInput = function isPubkeyhashInput() { return this.items.length === 2 - && Script.isSignature(this.items[0]) - && Script.isKey(this.items[1]); + && Script.isSignatureEncoding(this.items[0]) + && Script.isKeyEncoding(this.items[1]); }; /** @@ -167,9 +167,7 @@ Witness.prototype.isMultisigInput = function isMultisigInput() { */ Witness.prototype.isScripthashInput = function isScripthashInput() { - if (this.items.length === 0) - return false; - return !this.isPubkeyhashInput(); + return this.items.length > 0 && !this.isPubkeyhashInput(); }; /** @@ -182,6 +180,28 @@ Witness.prototype.isUnknownInput = function isUnknownInput() { return this.getInputType() === 'unknown'; }; +/** + * Test the witness against a bloom filter. + * @param {Bloom} filter + * @returns {Boolean} + */ + +Witness.prototype.test = function test(filter) { + var i, chunk; + + for (i = 0; i < this.items.length; i++) { + chunk = this.items[i]; + + if (chunk.length === 0) + continue; + + if (filter.test(chunk)) + return true; + } + + return false; +}; + /** * Grab and deserialize the redeem script from the witness. * @returns {Script} Redeem script. @@ -202,6 +222,14 @@ Witness.prototype.getRedeem = function getRedeem() { return this.redeem; }; +/** + * Does nothing currently. + */ + +Witness.prototype.compile = function compile() { + // NOP +}; + /** * Find a data element in a witness. * @param {Buffer} data - Data element to match against. @@ -225,6 +253,81 @@ Witness.prototype.toRaw = function toRaw(enc) { return data; }; +Witness.prototype.unshift = function unshift(data) { + return this.items.unshift(Witness.encodeItem(data)); +}; + +Witness.prototype.push = function push(data) { + return this.items.push(Witness.encodeItem(data)); +}; + +Witness.prototype.shift = function shift() { + return this.items.shift(); +}; + +Witness.prototype.pop = function push(data) { + return this.items.pop(); +}; + +Witness.prototype.remove = function remove(i) { + return this.items.splice(i, 1)[0]; +}; + +Witness.prototype.insert = function insert(i, data) { + assert(i <= this.items.length, 'Index out of bounds.'); + this.items.splice(i, 0, Witness.encodeItem(data))[0]; +}; + +Witness.prototype.get = function get(i) { + return this.items[i]; +}; + +Witness.prototype.getNumber = function getNumber(i) { + var item = this.items[i]; + if (!item || item.length > 5) + return; + return Script.num(item, constants.flags.VERIFY_NONE, 5); +}; + +Witness.prototype.getString = function getString(i) { + var item = this.items[i]; + if (!item) + return; + return item.toString('utf8'); +}; + +Witness.prototype.set = function set(i, data) { + assert(i <= this.items.length, 'Index out of bounds.'); + this.items[i] = Witness.encodeItem(data); +}; + +Witness.prototype.__defineGetter__('length', function() { + return this.code.length; +}); + +Witness.prototype.__defineSetter__('length', function(len) { + return this.code.length = len; +}); + +Witness.encodeItem = function encodeItem(data) { + if (typeof data === 'number') { + if (data === opcodes.OP_1NEGATE) + data = -1; + else if (data === opcodes.OP_0) + data = 0; + else if (data >= opcodes.OP_1 && data <= opcodes.OP_16) + data -= 0x50; + else + throw new Error('Non-push opcode in witness.'); + return Script.array(data); + } + if (bn.isBN(data)) + return Script.array(data); + if (typeof data === 'string') + return new Buffer(data, 'utf8'); + return data; +}; + /** * Create a witness from a serialized buffer. * @param {Buffer|String} data - Serialized witness. @@ -842,6 +945,13 @@ function Script(raw) { assert(Array.isArray(this.code)); } +/** + * Convert the script to an array of + * Buffers (pushdatas) and Numbers + * (opcodes). + * @returns {Array} + */ + Script.prototype.toArray = function toArray() { var code = []; var i, op; @@ -854,6 +964,12 @@ Script.prototype.toArray = function toArray() { return code; }; +/** + * Instantiate script from an array + * of buffers and numbers. + * @returns {Script} + */ + Script.fromArray = function fromArray(code) { return new Script(code); }; @@ -906,7 +1022,12 @@ Script.prototype.encode = function encode() { return this.raw; }; -Script.prototype.refresh = function refresh() { +/** + * Re-encode the script internally. Useful if you + * changed something manually in the `code` array. + */ + +Script.prototype.compile = function compile() { this.raw = Script.encode(this.code); }; @@ -2061,10 +2182,13 @@ Script.isCode = function isCode(raw) { for (i = 0; i < code.length; i++) { op = code[i]; + if (op.data) continue; + if (op.value === -1) return false; + if (constants.opcodesByVal[op.value] == null) return false; } @@ -2072,17 +2196,6 @@ Script.isCode = function isCode(raw) { return true; }; -/** - * Concatenate scripts, inserting code separators in between them. - * @param {Script[]} scripts - * @returns {Array} code - */ - -Script.prototype.concat = function concat(scripts) { - scripts.unshift(this); - return Script.concat(scripts); -}; - /** * Create a pay-to-pubkey script. * @param {Buffer} key @@ -2090,6 +2203,7 @@ Script.prototype.concat = function concat(scripts) { */ Script.createPubkey = function createPubkey(key) { + assert(key.length >= 33); return new Script([key, opcodes.OP_CHECKSIG]); }; @@ -2100,6 +2214,7 @@ Script.createPubkey = function createPubkey(key) { */ Script.createPubkeyhash = function createPubkeyhash(hash) { + assert(hash.length === 20); return new Script([ opcodes.OP_DUP, opcodes.OP_HASH160, @@ -2177,7 +2292,6 @@ Script.createNulldata = function createNulldata(flags) { Script.createWitnessProgram = function createWitnessProgram(version, data) { assert(typeof version === 'number' && version >= 0 && version <= 16); - assert(Buffer.isBuffer(data)); assert(data.length >= 2 && data.length <= 32); return new Script([version === 0 ? 0 : version + 0x50, data]); }; @@ -2358,6 +2472,7 @@ Script.prototype.getAddress = function getAddress() { /** * Test whether the output script is pay-to-pubkey. + * @param {Boolean} [sloppy=false] - Allow non-minimal scripts. * @returns {Boolean} */ @@ -2375,6 +2490,7 @@ Script.prototype.isPubkey = function isPubkey(sloppy) { /** * Test whether the output script is pay-to-pubkeyhash. + * @param {Boolean} [sloppy=false] - Allow non-minimal scripts. * @returns {Boolean} */ @@ -2447,8 +2563,8 @@ Script.prototype.isScripthash = function isScripthash() { }; /** - * Test whether the output script is nulldata/opreturn. This will - * fail if the pushdata is greater than 80 bytes. + * Test whether the output script is nulldata/opreturn. + * @param {Boolean} [sloppy=false] - Allow non-minimal scripts. * @returns {Boolean} */ @@ -2586,10 +2702,9 @@ Script.prototype.getWitnessProgram = function getWitnessProgram() { */ Script.prototype.isWitnessPubkeyhash = function isWitnessPubkeyhash() { - if (!this.isWitnessProgram()) - return false; - - return this.raw[0] === opcodes.OP_0 && this.raw.length === 22; + return this.raw.length === 22 + && this.raw[0] === opcodes.OP_0 + && this.raw[1] === 0x14; }; /** @@ -2598,10 +2713,9 @@ Script.prototype.isWitnessPubkeyhash = function isWitnessPubkeyhash() { */ Script.prototype.isWitnessScripthash = function isWitnessScripthash() { - if (!this.isWitnessProgram()) - return false; - - return this.raw[0] === opcodes.OP_0 && this.raw.length === 34; + return this.raw.length === 34 + && this.raw[0] === opcodes.OP_0 + && this.raw[1] === 0x20; }; /** @@ -2675,7 +2789,6 @@ Script.createOutputScript = function createOutputScript(options) { return Script.createPubkey(options.key); if (options.keyHash) { - assert(options.keyHash.length === 20); if (options.version != null) return Script.createWitnessProgram(options.version, options.keyHash); return Script.createPubkeyhash(options.keyHash); @@ -2688,11 +2801,8 @@ Script.createOutputScript = function createOutputScript(options) { } if (options.scriptHash) { - if (options.version != null) { - assert(options.scriptHash.length === 32); + if (options.version != null) return Script.createWitnessProgram(options.version, options.scriptHash); - } - assert(options.scriptHash.length === 20); return Script.createScripthash(options.scriptHash); } @@ -2825,6 +2935,7 @@ Script.prototype.getCoinbaseHeight = function getCoinbaseHeight() { /** * Get coinbase height. + * @param {Buffer} raw - Raw script. * @returns {Number} `-1` if not present. */ @@ -2852,31 +2963,124 @@ Script.getCoinbaseHeight = function getCoinbaseHeight(raw) { */ Script.prototype.getCoinbaseFlags = function getCoinbaseFlags() { - var coinbase = {}; var index = 0; - var nonce; + var height = this.getCoinbaseHeight(); + var extraNonce, flags, text; - coinbase.height = this.getCoinbaseHeight(); - - if (coinbase.height !== -1) + if (height !== -1) index++; - if (this.code[1].data && this.code[1].data.length <= 6) { - coinbase.extraNonce = new bn(this.code[1].data, 'le').toNumber(); - } else { - nonce = this.getSmall(1); - coinbase.extraNonce = nonce === -1 ? -1 : nonce; + if (this.code[1].data && this.code[1].data.length <= 6) + extraNonce = new bn(this.code[1].data, 'le').toNumber(); + else + extraNonce = this.getSmall(1); + + flags = Script.encode(this.code.slice(index)); + + text = flags.toString('utf8'); + text = text.replace(/[\u0000-\u0019\u007f-\u00ff]/g, ''); + + return { + height: height, + extraNonce: extraNonce, + flags: flags, + text: text + }; +}; + +/** + * Test the script against a bloom filter. + * @param {Bloom} filter + * @returns {Boolean} + */ + +Script.prototype.test = function test(filter) { + var i, op; + + for (i = 0; i < this.code.length; i++) { + op = this.code[i]; + + if (op.value === -1) + break; + + if (!op.data || op.data.length === 0) + continue; + + if (filter.test(op.data)) + return true; } - coinbase.flags = Script.encode(this.code.slice(index)); - - coinbase.text = coinbase.flags - .toString('utf8') - .replace(/[\u0000-\u0019\u007f-\u00ff]/g, ''); - - return coinbase; + return false; }; +Script.prototype.unshift = function unshift(data) { + return this.code.unshift(Opcode.from(data)); +}; + +Script.prototype.push = function push(data) { + return this.code.push(Opcode.from(data)); +}; + +Script.prototype.shift = function shift() { + var op = this.code.shift(); + if (!op) + return; + return op.data || op.value; +}; + +Script.prototype.pop = function push(data) { + var op = this.code.pop(); + if (!op) + return; + return op.data || op.value; +}; + +Script.prototype.remove = function remove(i) { + var op = this.code.splice(i, 1)[0]; + if (!op) + return; + return op.data || op.value; +}; + +Script.prototype.insert = function insert(i, data) { + assert(i <= this.code.length, 'Index out of bounds.'); + this.code.splice(i, 0, Opcode.from(data))[0]; +}; + +Script.prototype.get = function get(i) { + var op = this.code[i]; + if (!op) + return; + return op.data || op.value; +}; + +Script.prototype.getNumber = function getNumber(i) { + var op = this.code[i]; + if (!op || !op.data || op.data.length > 5) + return; + return Script.num(op.data, constants.flags.VERIFY_NONE, 5); +}; + +Script.prototype.getString = function getString(i) { + var op = this.code[i]; + if (!op || !op.data) + return; + return op.data.toString('utf8'); +}; + +Script.prototype.set = function set(i, data) { + assert(i <= this.code.length, 'Index out of bounds.'); + this.code[i] = Opcode.from(data); +}; + +Script.prototype.__defineGetter__('length', function() { + return this.code.length; +}); + +Script.prototype.__defineSetter__('length', function(len) { + return this.code.length = len; +}); + /** * Test whether the data element is a ripemd160 hash. * @param {Buffer?} hash @@ -3167,10 +3371,13 @@ Script.isLowDER = function isLowDER(sig) { */ Script.format = function format(code) { - return code.map(function(op) { - var data = op.data; - var value = op.value; - var size; + var out = []; + var i, op, data, value, size; + + for (i = 0; i < code.length; i++) { + op = code[i]; + data = op.data; + value = op.value; if (data) { size = data.length.toString(16); @@ -3182,25 +3389,35 @@ Script.format = function format(code) { value = value.toString(16); if (value.length < 2) value = '0' + value; - return '0x' + value + ' 0x' + data.toString('hex'); + value = '0x' + value + ' 0x' + data.toString('hex'); + out.push(value); + continue; } value = constants.opcodesByVal[value]; - return value + ' 0x' + size + ' 0x' + data.toString('hex'); + value = value + ' 0x' + size + ' 0x' + data.toString('hex'); + out.push(value); + continue; } assert(typeof value === 'number'); - if (constants.opcodesByVal[value]) - return constants.opcodesByVal[value]; + if (constants.opcodesByVal[value]) { + value = constants.opcodesByVal[value]; + out.push(value); + continue; + } value = value.toString(16); if (value.length < 2) value = '0' + value; - return '0x' + value; - }).join(' '); + value = '0x' + value; + out.push(value); + } + + return out.join(' '); }; /** @@ -3267,15 +3484,20 @@ Script.formatASM = function formatASM(code, decode) { Script.prototype.isPushOnly = function isPushOnly() { var i, op; + for (i = 0; i < this.code.length; i++) { op = this.code[i]; + if (op.data) continue; + if (op.value == -1) return false; + if (op.value > opcodes.OP_16) return false; } + return true; }; @@ -3300,18 +3522,21 @@ Script.prototype.getSigops = function getSigops(accurate) { if (op.value === -1) break; - op = op.value; - - if (op === opcodes.OP_CHECKSIG || op === opcodes.OP_CHECKSIGVERIFY) { - total++; - } else if (op === opcodes.OP_CHECKMULTISIG || op === opcodes.OP_CHECKMULTISIGVERIFY) { - if (accurate && lastOp >= opcodes.OP_1 && lastOp <= opcodes.OP_16) - total += lastOp - 0x50; - else - total += constants.script.MAX_MULTISIG_PUBKEYS; + switch (op.value) { + case opcodes.OP_CHECKSIG: + case opcodes.OP_CHECKSIGVERIFY: + total++; + break; + case opcodes.OP_CHECKMULTISIG: + case opcodes.OP_CHECKMULTISIGVERIFY: + if (accurate && lastOp >= opcodes.OP_1 && lastOp <= opcodes.OP_16) + total += lastOp - 0x50; + else + total += constants.script.MAX_MULTISIG_PUBKEYS; + break; } - lastOp = op; + lastOp = op.value; } return total; @@ -3331,10 +3556,13 @@ Script.prototype.getScripthashSigops = function getScripthashSigops(input) { for (i = 0; i < input.code.length; i++) { op = input.code[i]; + if (op.data) continue; + if (op.value === -1) return 0; + if (op.value > opcodes.OP_16) return 0; } @@ -3447,13 +3675,13 @@ Script.fromString = function fromString(code) { assert(op[op.length - 1] === '\'', 'Unknown opcode.'); op = op.slice(1, -1); op = new Buffer(op, 'ascii'); - p.writeBytes(Script.encodeArray([op])); + p.writeBytes(Script.encodePush(op)); continue; } if (/^-?\d+$/.test(op)) { op = new bn(op, 10); op = Script.array(op); - p.writeBytes(Script.encodeArray([op])); + p.writeBytes(Script.fromArray([op]).raw); continue; } assert(op.indexOf('0x') === 0, 'Unknown opcode.'); @@ -3785,78 +4013,14 @@ Script.fromRaw = function fromRaw(data, enc) { /** * Decode a serialized script into script code. * Note that the serialized script must _not_ - * include the varint size before it. Note that - * this will apply hidden `opcode` properties - * to each Buffer if the buffer was created from - * a non-minimal pushdata. + * include the varint size before it. Parse + * errors will output an opcode with a value + * of -1. * - * BCoin parses scripts "differently" because it - * parses them _before they're executed_. This - * lends itself to some interesting edge cases. - * - * If bitcoind comes across a bad push, it - * will return an invalid opcode. The problem - * is bitcoind parses scripts _as_ they are - * executing, which can be slow for us because - * now every function that needs to test the - * script needs to parse the raw data. It's - * also impossible to read a script - * _backwards_ making testing for things like - * multisig outputs even more difficult. - * - * If this function comes accross a bad push - * in its parsing, it simply will _not - * consider the pushdata to be a pushdata_ - * but just another opcode in the code array - * (all of the data after the pushdata op - * will also be considered opcodes rather - * than data). - - * Also note that this function uses reference - * Buffer slices. Larger buffer slices should - * _never_ be passed in here. * @param {Buffer} raw - Serialized script. - * @returns {Array} Script code. + * @returns {Opcode[]} Script code. */ -function Opcode(value, data) { - this.value = value; - this.data = data || null; -} - -Opcode.fromData = function fromData(data) { - if (data.length === 0) - return new Opcode(opcodes.OP_0); - - if (data.length <= 0x4b) { - if (data.length === 1) { - if (data[0] >= 1 && data[0] <= 16) - return new Opcode(data[0] + 0x50); - - if (data[0] === 0x81) - return new Opcode(opcodes.OP_1NEGATE); - } - return new Opcode(data.length, data); - } - - if (data.length <= 0xff) - return new Opcode(opcodes.OP_PUSHDATA1, data); - - if (data.length <= 0xffff) - return new Opcode(opcodes.OP_PUSHDATA2, data); - - if (data.length <= 0xffffffff) - return new Opcode(opcodes.OP_PUSHDATA4, data); - - assert(false, 'Bad pushdata size.'); -}; - -Opcode.fromOp = function fromOp(op) { - return new Opcode(op); -}; - -Script.opcode = Opcode; - Script.decode = function decode(raw) { var p = new BufferReader(raw, true); var code = []; @@ -3919,11 +4083,7 @@ Script.decode = function decode(raw) { /** * Encode and serialize script code. This will _not_ - * include the varint size at the start. This will - * correctly reserialize non-standard pushdata ops - * if the code was originally created from - * {Script.decode}. Otherwise, it will convert the - * code's pushdatas to minimaldata representations. + * include the varint size at the start. * @param {Array} code - Script code. * @returns {Buffer} Serialized script. */ @@ -3934,6 +4094,10 @@ Script.encode = function encode(code, writer) { for (i = 0; i < code.length; i++) { op = code[i]; + + if (op.value === -1) + throw new Error('Cannot reserialize a parse error.'); + if (op.data) { if (op.value <= 0x4b) { p.writeU8(op.data.length); @@ -3951,10 +4115,11 @@ Script.encode = function encode(code, writer) { p.writeU32(op.data.length); p.writeBytes(op.data); } else { - assert(false, 'Bad pushdata op.'); + throw new Error('Unknown pushdata opcode.'); } continue; } + p.writeU8(op.value); } @@ -3964,40 +4129,24 @@ Script.encode = function encode(code, writer) { return p; }; -Script.parseArray = function parseArray(code, writer) { +/** + * Convert an array of Buffers and + * Numbers into an array of Opcodes. + * @param {Array} code + * @returns {Opcode[]} + */ + +Script.parseArray = function parseArray(code) { var out = []; var i, op; - assert(Array.isArray(code)); - if (code[0] instanceof Opcode) return code; for (i = 0; i < code.length; i++) { op = code[i]; if (Buffer.isBuffer(op)) { - if (op.length === 0) { - out.push(new Opcode(opcodes.OP_0)); - } else if (op.length <= 0x4b) { - if (op.length === 1) { - if (op[0] >= 1 && op[0] <= 16) { - out.push(new Opcode(op[0] + 0x50)); - continue; - } else if (op[0] === 0x81) { - out.push(new Opcode(opcodes.OP_1NEGATE)); - continue; - } - } - out.push(new Opcode(op.length, op)); - } else if (op.length <= 0xff) { - out.push(new Opcode(opcodes.OP_PUSHDATA1, op)); - } else if (op.length <= 0xffff) { - out.push(new Opcode(opcodes.OP_PUSHDATA2, op)); - } else if (op.length <= 0xffffffff) { - out.push(new Opcode(opcodes.OP_PUSHDATA4, op)); - } else { - assert(false, 'Bad pushdata op.'); - } + out.push(Opcode.fromData(op)); continue; } assert(typeof op === 'number'); @@ -4009,8 +4158,8 @@ Script.parseArray = function parseArray(code, writer) { /** * Encode a single push op. Note that this - * will _always_ be a PUSHDATA. Used for - * findAndDelete. + * will _always_ be a PUSHDATA. Analagous to + * bitcoind's `CScript() << vector`. * @param {Buffer} data * @returns {Buffer} */ @@ -4033,114 +4182,11 @@ Script.encodePush = function encodePush(data) { p.writeU32(data.length); p.writeBytes(data); } else { - assert(false, 'Bad pushdata op.'); + throw new Error('Pushdata size too large.'); } return p.render(); }; -function ScriptWriter() { - this.ops = []; -} - -ScriptWriter.prototype.writeString = function writeString(str, enc) { - if (typeof str === 'string') - str = new Buffer(str, enc); - - this.writeData(str); -}; - -ScriptWriter.prototype.writeData = function writeData(data, op) { - var p = this.writer; - - if (op != null) - return this.writePush(data, op); - - if (data.length === 0) { - this.ops.push(new Opcode(opcodes.OP_0)); - return; - } - - if (data.length === 1) { - if (data[0] >= 1 && data[0] <= 16) { - this.ops.push(new Opcode(data[0] + 0x50)); - return; - } - if (data[0] === 0x81) { - this.ops.push(new Opcode(opcodes.OP_1NEGATE)); - return; - } - } - - return this.writePush(data, op); -}; - -ScriptWriter.prototype.writePush = function writePush(data, op) { - var p = this.writer; - - if (op != null) { - this.ops.push(new Opcode(op, data)); - return; - } - - if (data.length <= 0x4b) { - this.ops.push(new Opcode(data.length, data)); - return; - } - - if (data.length <= 0xff) { - this.ops.push(new Opcode(opcodes.OP_PUSHDATA1, data)); - return; - } - - if (data.length <= 0xffff) { - this.ops.push(new Opcode(opcodes.OP_PUSHDATA2, data)); - return; - } - - if (data.length <= 0xffffffff) { - this.ops.push(new Opcode(opcodes.OP_PUSHDATA4, data)); - return; - } - - throw new Error('Pushdata too large.'); -}; - -ScriptWriter.prototype.writeScript = function writeScript(script) { - this.writeData(script.raw); -}; - -ScriptWriter.prototype.writeOp = function writeOp(opcode) { - this.ops.push(new Opcode(opcode)); -}; - -ScriptWriter.prototype.writeNumber = function writeNumber(num) { - var data; - - if (bn.isBN(num)) { - if (num.cmpn(16) > 0) { - this.writeData(Script.array(num)); - return; - } - num = num.toNumber(); - } - - if (num >= 1 && num <= 16) { - this.writeOp(num + 0x50); - return; - } - - if (num === -1) { - this.writeOp(opcodes.OP_1NEGATE); - return; - } - - this.writeData(Script.array(num)); -}; - -ScriptWriter.prototype.toScript = function toScript() { - return new Script(this.ops); -}; - /** * Calculate the size (including * the opcode) of a pushdata. @@ -4162,7 +4208,7 @@ Script.sizePush = function sizePush(num) { }; /** - * Test an object to see if it is a Script. + * Test whether an object a Script. * @param {Object} obj * @returns {Boolean} */ @@ -4173,6 +4219,154 @@ Script.isScript = function isScript(obj) { && typeof obj.getSubscript === 'function'; }; +/** + * A simple struct which contains + * an opcode and pushdata buffer. + * @exports Opcode + * @constructor + * @param {Number} value - Opcode. + * @param {Buffer?} data - Pushdata buffer. + * @property {Number} value + * @property {Buffer|null} data + */ + +function Opcode(value, data) { + if (!(this instanceof Opcode)) + return new Opcode(value, data); + + this.value = value; + this.data = data || null; +} + +/** + * Encode the opcode. + * @returns {Buffer} + */ + +Opcode.prototype.toRaw = function toRaw() { + return Script.encode([this]); +}; + +/** + * Instantiate an opcode from a number. + * @param {Number} op + * @returns {Opcode} + */ + +Opcode.fromOp = function fromOp(op) { + return new Opcode(op); +}; + +/** + * Instantiate a pushdata opcode from + * a buffer (will encode minimaldata). + * @param {Buffer} data + * @returns {Opcode} + */ + +Opcode.fromData = function fromData(data) { + if (data.length === 0) + return new Opcode(opcodes.OP_0); + + if (data.length === 1) { + if (data[0] >= 1 && data[0] <= 16) + return new Opcode(data[0] + 0x50); + + if (data[0] === 0x81) + return new Opcode(opcodes.OP_1NEGATE); + } + + return Opcode.fromPush(data); +}; + +/** + * Instantiate a pushdata opcode from a + * buffer (this differs from fromData in + * that it will _always_ be a pushdata op). + * @param {Buffer} data + * @returns {Opcode} + */ + +Opcode.fromPush = function fromPush(data) { + if (data.length <= 0x4b) + return new Opcode(data.length, data); + + if (data.length <= 0xff) + return new Opcode(opcodes.OP_PUSHDATA1, data); + + if (data.length <= 0xffff) + return new Opcode(opcodes.OP_PUSHDATA2, data); + + if (data.length <= 0xffffffff) + return new Opcode(opcodes.OP_PUSHDATA4, data); + + throw new Error('Pushdata size too large.'); +}; + +/** + * Instantiate an opcode from a Number. + * @param {Number|BN} num + * @returns {Opcode} + */ + +Opcode.fromNumber = function fromNumber(num) { + if (bn.isBN(num)) { + if (!(num.cmpn(-1) >= 0 && num.cmpn(16) <= 0)) + return Opcode.fromData(Script.array(num)); + num = num.toNumber(); + } + + if (num >= 1 && num <= 16) + return Opcode.fromOp(num + 0x50); + + if (num === -1) + return Opcode.fromOp(opcodes.OP_1NEGATE); + + return Opcode.fromData(Script.array(num)); +}; + +/** + * Instantiate a pushdata opcode from a string. + * @param {String} data + * @returns {Opcode} + */ + +Opcode.fromString = function fromString(data, enc) { + if (typeof data === 'string') + data = new Buffer(data, enc); + return Opcode.fromPush(data); +}; + +/** + * Instantiate a pushdata opcode from anything. + * @params {String|Buffer|Number|BN|Opcode} data + * @returns {Opcode} + */ + +Opcode.from = function from(data) { + if (data instanceof Opcode) + return data; + if (typeof data === 'number') + return Opcode.fromOp(data); + if (bn.isBN(data)) + return Opcode.fromNumber(data); + if (typeof data === 'string') + return Opcode.fromString(data, 'utf8'); + return Opcode.fromPush(data); +}; + +/** + * Test whether an object an Opcode. + * @param {Object} obj + * @returns {Boolean} + */ + +Opcode.isOpcode = function isOpcode(obj) { + return obj + && typeof obj.value === 'number' + && (Buffer.isBuffer(obj.data) || obj.data === null); +}; + /* * Expose */ @@ -4183,7 +4377,8 @@ exports.opcodes = constants.opcodes; exports.opcodesByVal = constants.opcodesByVal; exports.Script = Script; -exports.Witness = Witness; +exports.Opcode = Opcode; exports.Stack = Stack; +exports.Witness = Witness; module.exports = exports; diff --git a/lib/bcoin/tx.js b/lib/bcoin/tx.js index 018b4ade..134a512b 100644 --- a/lib/bcoin/tx.js +++ b/lib/bcoin/tx.js @@ -1333,7 +1333,7 @@ TX.prototype.hasStandardInputs = function hasStandardInputs(flags) { if (stack.length === 0) return false; - redeem = stack.getRedeem(false); + redeem = stack.getRedeem(); if (!redeem) return false; @@ -1645,22 +1645,6 @@ TX.prototype.isWatched = function isWatched(filter) { if (!filter) return false; - function testScript(code) { - var i, chunk; - - for (i = 0; i < code.length; i++) { - chunk = code[i]; - if (chunk === -1) - break; - if (!Buffer.isBuffer(chunk) || chunk.length === 0) - continue; - if (filter.test(chunk)) - return true; - } - - return false; - } - // 1. Test the tx hash if (filter.test(this.hash())) found = true; @@ -1670,7 +1654,7 @@ TX.prototype.isWatched = function isWatched(filter) { for (i = 0; i < this.outputs.length; i++) { output = this.outputs[i]; // Test the output script - if (testScript(output.script.toArray())) { + if (output.script.test(filter)) { if (filter.update === constants.filterFlags.ALL) { outpoint = bcoin.protocol.framer.outpoint(this.hash(), i); filter.add(outpoint); @@ -1700,11 +1684,11 @@ TX.prototype.isWatched = function isWatched(filter) { return true; // Test the input script - if (testScript(input.script.toArray())) + if (input.script.test(filter)) return true; // Test the witness - // if (testScript(input.witness.items)) + // if (input.witness.test(filter)) // return true; } diff --git a/test/script-test.js b/test/script-test.js index 0bbf5861..14b75433 100644 --- a/test/script-test.js +++ b/test/script-test.js @@ -32,7 +32,7 @@ describe('Script', function() { it('should encode/decode numbers', function() { var script = [0, 0x51, 0x52, 0x60]; - var encoded = bcoin.script.encodeArray(script); + var encoded = bcoin.script.fromArray(script).raw; var decoded = bcoin.script.decode(encoded).map(function(op) { return op.value; }); assert.deepEqual(decoded, script); }); diff --git a/test/wallet-test.js b/test/wallet-test.js index 829f9e35..f72a09c7 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -7,6 +7,8 @@ var network = bcoin.protocol.network; var utils = bcoin.utils; var assert = require('assert'); +var FAKE_SIG = new Buffer([0,0,0,0,0,0,0,0,0]); + var KEY1 = 'xprv9s21ZrQH143K3Aj6xQBymM31Zb4BVc7wxqfUhMZrzewdDVCt' + 'qUP9iWfcHgJofs25xbaUpCps9GDXj83NiWvQCAkWQhVj5J4CorfnpKX94AZ'; @@ -227,8 +229,8 @@ describe('Wallet', function() { w.scriptInputs(fake, function(err) { assert.ifError(err); // Fake signature - fake.inputs[0].script.code[0] = new Buffer([0,0,0,0,0,0,0,0,0]); - fake.inputs[0].script.refresh(); + fake.inputs[0].script.code[0] = bcoin.opcode.fromData(FAKE_SIG); + fake.inputs[0].script.compile(); // balance: 11000 // Fake TX should temporarly change output @@ -696,8 +698,8 @@ describe('Wallet', function() { if (witness) { send.inputs[0].witness.items[2] = new Buffer([]); } else { - send.inputs[0].script.code[2] = 0; - send.inputs[0].script.refresh(); + send.inputs[0].script.code[2] = new bcoin.opcode(0); + send.inputs[0].script.compile(); } assert(!send.verify(flags));