diff --git a/bench/tx.js b/bench/tx.js index b6baa7ce..b16e8902 100644 --- a/bench/tx.js +++ b/bench/tx.js @@ -159,7 +159,7 @@ for (let i = 0; i < 100; i++) { hash: encoding.NULL_HASH, index: 0 }, - script: new Script() + script: Script.write() .pushData(Buffer.allocUnsafe(9)) .pushData(random.randomBytes(33)) .compile() diff --git a/docs/Scripting.md b/docs/Scripting.md index dc44083d..6961893f 100644 --- a/docs/Scripting.md +++ b/docs/Scripting.md @@ -7,7 +7,7 @@ const Script = bcoin.script; const Witness = bcoin.witness; const Stack = bcoin.stack; -const output = new Script(); +let output = Script.write(); output.pushSym('OP_DROP'); output.pushSym('OP_ADD'); output.pushInt(7); @@ -15,15 +15,15 @@ output.pushSym('OP_NUMEQUAL'); // Compile the script to its binary representation // (you must do this if you change something!). assert(output.getSmall(2) === 7); // compiled as OP_7 -output.compile(); +output = output.compile(); -const input = new Script(); +let input = Script.write(); input.setString(0, 'hello world'); // add some metadata input.pushInt(2); input.pushInt(5); input.push(input.shift()); assert(input.getString(2) === 'hello world'); -input.compile(); +input = input.compile(); // A stack is another array-like object which contains // only Buffers (whereas scripts contain Opcode objects). diff --git a/lib/http/rpc.js b/lib/http/rpc.js index 76c19c5a..32f4aba7 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -1409,10 +1409,11 @@ RPC.prototype._createTemplate = async function _createTemplate(maxVersion, coinb if (coinbase) { const tx = attempt.toCoinbase(); const input = tx.inputs[0]; + const script = input.script.write(); // Pop off the nonces. - input.script.pop(); - input.script.compile(); + script.pop(); + script.compile(); if (attempt.witness) { // We don't include the commitment @@ -1863,7 +1864,7 @@ RPC.prototype.signRawTransaction = async function signRawTransaction(args, help) const redeem = Script.fromRaw(redeemRaw); - for (const op of redeem.code) { + for (const op of redeem.values()) { if (!op.data) continue; @@ -2341,7 +2342,6 @@ RPC.prototype._addBlock = async function _addBlock(block) { // Recreate witness nonce (all zeroes). input.witness.push(encoding.ZERO_HASH); - input.witness.compile(); tx.refresh(); block.refresh(); diff --git a/lib/mempool/mempoolentry.js b/lib/mempool/mempoolentry.js index 2ca6f637..b38f722b 100644 --- a/lib/mempool/mempoolentry.js +++ b/lib/mempool/mempoolentry.js @@ -226,6 +226,7 @@ MempoolEntry.prototype.getDescRate = function getDescRate() { MempoolEntry.prototype.memUsage = function memUsage() { const tx = this.tx; + let total = 0; total += 176; // mempool entry @@ -247,13 +248,6 @@ MempoolEntry.prototype.memUsage = function memUsage() { total += 40; // script total += 80; // script raw buffer - total += 32; // script code array - total += input.script.code.length * 40; // opcodes - - for (const op of input.script.code) { - if (op.data) - total += 80; // op buffers - } total += 96; // witness total += 32; // witness items @@ -262,18 +256,9 @@ MempoolEntry.prototype.memUsage = function memUsage() { total += 32; // output array - for (const output of tx.outputs) { - total += 104; // output - total += 40; // script - total += 80; // script raw buffer - total += 32; // script code array - total += output.script.code.length * 40; // opcodes - - for (const op of output.script.code) { - if (op.data) - total += 80; // op buffers - } - } + total += tx.outputs.length * 104; // output + total += tx.outputs.length * 40; // script + total += tx.outputs.length * 80; // script raw buffer return total; }; diff --git a/lib/mining/template.js b/lib/mining/template.js index a1a3477b..b93a23c5 100644 --- a/lib/mining/template.js +++ b/lib/mining/template.js @@ -238,27 +238,26 @@ BlockTemplate.prototype.createCoinbase = function createCoinbase(hash) { // Coinbase input. const input = new Input(); + const script = input.script.write(); // Height (required in v2+ blocks) - input.script.pushInt(this.height); + script.pushInt(this.height); // Coinbase flags. - input.script.pushData(encoding.ZERO_HASH160); + script.pushData(encoding.ZERO_HASH160); // Smaller nonce for good measure. - input.script.pushData(util.nonce(4)); + script.pushData(util.nonce(4)); // Extra nonce: incremented when // the nonce overflows. - input.script.pushData(encoding.ZERO_U64); + script.pushData(encoding.ZERO_U64); - input.script.compile(); + script.compile(); // Set up the witness nonce. - if (this.witness) { + if (this.witness) input.witness.push(encoding.ZERO_HASH); - input.witness.compile(); - } cb.inputs.push(input); @@ -279,7 +278,7 @@ BlockTemplate.prototype.createCoinbase = function createCoinbase(hash) { } // Padding for the CB height (constant size). - const op = input.script.get(0); + const op = input.script.read().get(); assert(op); const padding = 5 - op.getSize(); assert(padding >= 0); @@ -306,8 +305,9 @@ BlockTemplate.prototype.createCoinbase = function createCoinbase(hash) { } // Setup coinbase flags (variable size). - input.script.setData(1, this.coinbaseFlags); - input.script.compile(); + input.script.write() + .setData(1, this.coinbaseFlags) + .compile(); // Setup output script (variable size). output.script.fromAddress(this.address); @@ -436,7 +436,6 @@ BlockTemplate.prototype.getCoinbase = function getCoinbase(nonce1, nonce2) { if (this.witness) { const input = tx.inputs[0]; input.witness.push(encoding.ZERO_HASH); - input.witness.compile(); tx.refresh(); } diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 6ae12cf4..33fbb567 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -808,7 +808,7 @@ MTX.prototype.signVector = function signVector(prev, vector, sig, ring) { // Grab the redeem script's keys to figure // out where our key should go. const keys = []; - for (const op of prev.code) { + for (const op of prev.values()) { if (op.data) keys.push(op.data); } diff --git a/lib/script/common.js b/lib/script/common.js index 4313a329..0a68da66 100644 --- a/lib/script/common.js +++ b/lib/script/common.js @@ -354,6 +354,24 @@ exports.isLowDER = function isLowDER(sig) { return secp256k1.isLowS(sig.slice(0, -1)); }; +/** + * Get a small integer from an opcode (OP_0-OP_16). + * @param {Number} index + * @returns {Number} + */ + +exports.getSmall = function getSmall(op) { + assert(typeof op === 'number'); + + if (op === exports.opcodes.OP_0) + return 0; + + if (op >= exports.opcodes.OP_1 && op <= exports.opcodes.OP_16) + return op - 0x50; + + return -1; +}; + /** * Test whether the data element is a valid key. * @param {Buffer} key diff --git a/lib/script/opcode.js b/lib/script/opcode.js index a46366e1..02bdf7c9 100644 --- a/lib/script/opcode.js +++ b/lib/script/opcode.js @@ -572,6 +572,158 @@ Opcode.fromSymbol = function fromSymbol(name) { return Opcode.fromOp(value); }; +/** + * Instantiate opcode from buffer reader. + * Only reads opcode values. + * @param {BufferReader} br + * @returns {Number} + */ + +Opcode.readOp = function readOp(br) { + const value = br.readU8(); + + if (opCache[value]) + return value; + + switch (value) { + case opcodes.OP_PUSHDATA1: { + if (br.left() < 1) + return -1; + + const size = br.readU8(); + + if (br.left() < size) { + br.seek(br.left()); + return -1; + } + + br.seek(size); + + return value; + } + case opcodes.OP_PUSHDATA2: { + if (br.left() < 2) { + br.seek(br.left()); + return -1; + } + + const size = br.readU16(); + + if (br.left() < size) { + br.seek(br.left()); + return -1; + } + + br.seek(size); + + return value; + } + case opcodes.OP_PUSHDATA4: { + if (br.left() < 4) { + br.seek(br.left()); + return -1; + } + + const size = br.readU32(); + + if (br.left() < size) { + br.seek(br.left()); + return -1; + } + + br.seek(size); + + return value; + } + default: { + if (br.left() < value) { + br.seek(br.left()); + return -1; + } + + br.seek(value); + + return value; + } + } +}; + +/** + * Instantiate opcode from buffer reader. + * Reads pushdata size only. + * @param {BufferReader} br + * @returns {Number} + */ + +Opcode.readLength = function readLength(br) { + const value = br.readU8(); + + if (opCache[value]) + return -1; + + switch (value) { + case opcodes.OP_PUSHDATA1: { + if (br.left() < 1) + return -1; + + const size = br.readU8(); + + if (br.left() < size) { + br.seek(br.left()); + return -1; + } + + br.seek(size); + + return size; + } + case opcodes.OP_PUSHDATA2: { + if (br.left() < 2) { + br.seek(br.left()); + return -1; + } + + const size = br.readU16(); + + if (br.left() < size) { + br.seek(br.left()); + return -1; + } + + br.seek(size); + + return size; + } + case opcodes.OP_PUSHDATA4: { + if (br.left() < 4) { + br.seek(br.left()); + return -1; + } + + const size = br.readU32(); + + if (br.left() < size) { + br.seek(br.left()); + return -1; + } + + br.seek(size); + + return size; + } + default: { + if (br.left() < value) { + br.seek(br.left()); + return -1; + } + + br.seek(value); + + return value; + } + } +}; + /** * Instantiate opcode from buffer reader. * @param {BufferReader} br diff --git a/lib/script/script.js b/lib/script/script.js index 751ea81e..509c7b24 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -45,7 +45,6 @@ function Script(options) { return new Script(options); this.raw = EMPTY_BUFFER; - this.code = []; if (options) this.fromOptions(options); @@ -102,20 +101,6 @@ Script.types = common.types; Script.typesByVal = common.typesByVal; -/* - * Expose length setter and getter. - */ - -Object.defineProperty(Script.prototype, 'length', { - get() { - return this.code.length; - }, - set(length) { - this.code.length = length; - return this.code.length; - } -}); - /** * Inject properties from options object. * @private @@ -131,19 +116,8 @@ Script.prototype.fromOptions = function fromOptions(options) { if (Array.isArray(options)) return this.fromArray(options); - if (options.raw) { - if (!options.code) - return this.fromRaw(options.raw); - assert(Buffer.isBuffer(options.raw), 'Raw must be a Buffer.'); - this.raw = options.raw; - } - - if (options.code) { - if (!options.raw) - return this.fromArray(options.code); - assert(Array.isArray(options.code), 'Code must be an array.'); - this.code = options.code; - } + if (options.raw) + return this.fromRaw(options.raw); return this; }; @@ -158,13 +132,40 @@ Script.fromOptions = function fromOptions(options) { return new Script().fromOptions(options); }; +/** + * Instantiate a script reader. + * @returns {ScriptReader} + */ + +Script.prototype.read = function read() { + return new ScriptReader(this); +}; + +/** + * Instantiate a script writer. + * @returns {ScriptWriter} + */ + +Script.prototype.write = function write() { + return new ScriptWriter(this); +}; + +/** + * Instantiate a value-only iterator. + * @returns {ScriptIterator} + */ + +Script.prototype[Symbol.iterator] = function iterator() { + return new ScriptIterator(this, 0); +}; + /** * Instantiate a value-only iterator. * @returns {ScriptIterator} */ Script.prototype.values = function values() { - return this.code.values(); + return new ScriptIterator(this, 0); }; /** @@ -173,16 +174,16 @@ Script.prototype.values = function values() { */ Script.prototype.entries = function entries() { - return this.code.entries(); + return new ScriptIterator(this, 1); }; /** - * Instantiate a value-only iterator. + * Instantiate a value-only iterator (returns op values). * @returns {ScriptIterator} */ -Script.prototype[Symbol.iterator] = function() { - return this.code[Symbol.iterator](); +Script.prototype.ops = function ops() { + return new ScriptIterator(this, 2); }; /** @@ -193,7 +194,12 @@ Script.prototype[Symbol.iterator] = function() { */ Script.prototype.toArray = function toArray() { - return this.code.slice(); + const ops = []; + + for (const op of this.values()) + ops.push(op); + + return ops; }; /** @@ -207,12 +213,17 @@ Script.prototype.toArray = function toArray() { Script.prototype.fromArray = function fromArray(code) { assert(Array.isArray(code)); - this.clear(); + if (code.length === 0) + return this; + + const script = new ScriptWriter(); for (const op of code) - this.push(op); + script.push(op); - return this.compile(); + this.raw = script.render(); + + return this; }; /** @@ -232,10 +243,11 @@ Script.fromArray = function fromArray(code) { */ Script.prototype.toItems = function toItems() { + const script = this.read(); const items = []; - for (const op of this.code) { - const data = op.toPush(); + while (script.left()) { + const data = script.getPush(); if (!data) throw new Error('Non-push opcode in script.'); @@ -256,12 +268,14 @@ Script.prototype.toItems = function toItems() { Script.prototype.fromItems = function fromItems(items) { assert(Array.isArray(items)); - this.clear(); + const script = new ScriptWriter(); for (const item of items) - this.pushData(item); + script.pushData(item); - return this.compile(); + this.raw = script.render(); + + return this; }; /** @@ -323,7 +337,6 @@ Script.prototype.clone = function clone() { Script.prototype.inject = function inject(script) { this.raw = script.raw; - this.code = script.code.slice(); return this; }; @@ -356,7 +369,6 @@ Script.prototype.compare = function compare(script) { Script.prototype.clear = function clear() { this.raw = EMPTY_BUFFER; - this.code.length = 0; return this; }; @@ -377,7 +389,7 @@ Script.prototype.inspect = function inspect() { Script.prototype.toString = function toString() { const out = []; - for (const op of this.code) + for (const op of this.values()) out.push(op.toFormat()); return out.join(' '); @@ -395,37 +407,12 @@ Script.prototype.toASM = function toASM(decode) { const out = []; - for (const op of this.code) + for (const op of this.values()) out.push(op.toASM(decode)); return out.join(' '); }; -/** - * Re-encode the script internally. Useful if you - * changed something manually in the `code` array. - * @returns {Script} - */ - -Script.prototype.compile = function compile() { - if (this.code.length === 0) - return this.clear(); - - let size = 0; - - for (const op of this.code) - size += op.getSize(); - - const bw = new StaticWriter(size); - - for (const op of this.code) - op.toWriter(bw); - - this.raw = bw.render(); - - return this; -}; - /** * Write the script to a buffer writer. * @param {BufferWriter} bw @@ -486,18 +473,7 @@ Script.prototype.getSubscript = function getSubscript(index) { if (index === 0) return this.clone(); - const script = new Script(); - - for (let i = index; i < this.code.length; i++) { - const op = this.code[i]; - - if (op.value === -1) - break; - - script.code.push(op); - } - - return script.compile(); + return Script.fromRaw(this.raw.slice(index)); }; /** @@ -513,11 +489,11 @@ Script.prototype.removeSeparators = function removeSeparators() { // Optimizing for the common case: // Check for any separators first. - for (const op of this.code) { - if (op.value === -1) + for (const op of this.ops()) { + if (op === -1) break; - if (op.value === opcodes.OP_CODESEPARATOR) { + if (op === opcodes.OP_CODESEPARATOR) { found = true; break; } @@ -529,9 +505,9 @@ Script.prototype.removeSeparators = function removeSeparators() { // Uncommon case: someone actually // has a code separator. Go through // and remove them all. - const script = new Script(); + const script = new ScriptWriter(); - for (const op of this.code) { + for (const op of this.values()) { if (op.value === -1) break; @@ -574,8 +550,10 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (flags & Script.flags.VERIFY_MINIMALDATA) minimal = true; - for (let ip = 0; ip < this.code.length; ip++) { - const op = this.code[ip]; + const script = this.read(); + + while (script.left()) { + const [ip, op] = script.read(); if (op.value === -1) throw new ScriptError('BAD_OPCODE', op, ip); @@ -1144,7 +1122,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers break; } case opcodes.OP_CODESEPARATOR: { - lastSep = ip + 1; + lastSep = script.offset(); break; } case opcodes.OP_CHECKSIG: @@ -1341,7 +1319,7 @@ Script.prototype.findAndDelete = function findAndDelete(data) { let found = false; - for (const op of this.code) { + for (const op of this.values()) { if (op.value === -1) break; @@ -1354,11 +1332,11 @@ Script.prototype.findAndDelete = function findAndDelete(data) { if (!found) return 0; - const code = []; + const script = new ScriptWriter(); let total = 0; - for (const op of this.code) { + for (const op of this.values()) { if (op.value === -1) break; @@ -1367,11 +1345,10 @@ Script.prototype.findAndDelete = function findAndDelete(data) { continue; } - code.push(op); + script.code.push(op); } - this.code = code; - this.compile(); + this.raw = script.render(); return total; }; @@ -1383,9 +1360,7 @@ Script.prototype.findAndDelete = function findAndDelete(data) { */ Script.prototype.indexOf = function indexOf(data) { - for (let i = 0; i < this.code.length; i++) { - const op = this.code[i]; - + for (const [i, op] of this.entries()) { if (op.value === -1) break; @@ -1406,14 +1381,26 @@ Script.prototype.indexOf = function indexOf(data) { */ Script.prototype.isCode = function isCode() { - for (const op of this.code) { - if (op.value === -1) + for (const op of this.ops()) { + if (op === -1) return false; - if (op.isDisabled()) - return false; - - switch (op.value) { + switch (op) { + case opcodes.OP_CAT: + case opcodes.OP_SUBSTR: + case opcodes.OP_LEFT: + case opcodes.OP_RIGHT: + case opcodes.OP_INVERT: + case opcodes.OP_AND: + case opcodes.OP_OR: + case opcodes.OP_XOR: + case opcodes.OP_2MUL: + case opcodes.OP_2DIV: + case opcodes.OP_MUL: + case opcodes.OP_DIV: + case opcodes.OP_MOD: + case opcodes.OP_LSHIFT: + case opcodes.OP_RSHIFT: case opcodes.OP_RESERVED: case opcodes.OP_NOP: case opcodes.OP_VER: @@ -1425,7 +1412,7 @@ Script.prototype.isCode = function isCode() { return false; } - if (op.value > opcodes.OP_CHECKSEQUENCEVERIFY) + if (op > opcodes.OP_CHECKSEQUENCEVERIFY) return false; } @@ -1446,12 +1433,6 @@ Script.prototype.fromPubkey = function fromPubkey(key) { key.copy(this.raw, 1); this.raw[1 + key.length] = opcodes.OP_CHECKSIG; - key = this.raw.slice(1, 1 + key.length); - - this.code.length = 0; - this.code.push(Opcode.fromPush(key)); - this.code.push(Opcode.fromOp(opcodes.OP_CHECKSIG)); - return this; }; @@ -1482,15 +1463,6 @@ Script.prototype.fromPubkeyhash = function fromPubkeyhash(hash) { this.raw[23] = opcodes.OP_EQUALVERIFY; this.raw[24] = opcodes.OP_CHECKSIG; - hash = this.raw.slice(3, 23); - - this.code.length = 0; - this.code.push(Opcode.fromOp(opcodes.OP_DUP)); - this.code.push(Opcode.fromOp(opcodes.OP_HASH160)); - this.code.push(Opcode.fromPush(hash)); - this.code.push(Opcode.fromOp(opcodes.OP_EQUALVERIFY)); - this.code.push(Opcode.fromOp(opcodes.OP_CHECKSIG)); - return this; }; @@ -1519,17 +1491,19 @@ Script.prototype.fromMultisig = function fromMultisig(m, n, keys) { assert(m >= 1 && m <= n); assert(n >= 1 && n <= 15); - this.clear(); + const script = new ScriptWriter(); - this.pushSmall(m); + script.pushSmall(m); for (const key of sortKeys(keys)) - this.pushData(key); + script.pushData(key); - this.pushSmall(n); - this.pushOp(opcodes.OP_CHECKMULTISIG); + script.pushSmall(n); + script.pushOp(opcodes.OP_CHECKMULTISIG); - return this.compile(); + this.raw = script.render(); + + return this; }; /** @@ -1559,13 +1533,6 @@ Script.prototype.fromScripthash = function fromScripthash(hash) { hash.copy(this.raw, 2); this.raw[22] = opcodes.OP_EQUAL; - hash = this.raw.slice(2, 22); - - this.code.length = 0; - this.code.push(Opcode.fromOp(opcodes.OP_HASH160)); - this.code.push(Opcode.fromPush(hash)); - this.code.push(Opcode.fromOp(opcodes.OP_EQUAL)); - return this; }; @@ -1589,11 +1556,14 @@ Script.prototype.fromNulldata = function fromNulldata(flags) { assert(Buffer.isBuffer(flags)); assert(flags.length <= policy.MAX_OP_RETURN, 'Nulldata too large.'); - this.clear(); - this.pushOp(opcodes.OP_RETURN); - this.pushData(flags); + const script = new ScriptWriter(); - return this.compile(); + script.pushOp(opcodes.OP_RETURN); + script.pushData(flags); + + this.raw = script.render(); + + return this; }; /** @@ -1622,12 +1592,6 @@ Script.prototype.fromProgram = function fromProgram(version, data) { this.raw[1] = data.length; data.copy(this.raw, 2); - data = this.raw.slice(2, 2 + data.length); - - this.code.length = 0; - this.code.push(Opcode.fromSmall(version)); - this.code.push(Opcode.fromPush(data)); - return this; }; @@ -1689,14 +1653,17 @@ Script.prototype.fromCommitment = function fromCommitment(hash, flags) { bw.writeU32BE(0xaa21a9ed); bw.writeHash(hash); - this.clear(); - this.pushOp(opcodes.OP_RETURN); - this.pushData(bw.render()); + const script = new ScriptWriter(); + + script.pushOp(opcodes.OP_RETURN); + script.pushData(bw.render()); if (flags) - this.pushData(flags); + script.pushData(flags); - return this.compile(); + this.raw = script.render(); + + return this; }; /** @@ -1718,7 +1685,7 @@ Script.fromCommitment = function fromCommitment(hash, flags) { Script.prototype.getRedeem = function getRedeem() { let data = null; - for (const op of this.code) { + for (const op of this.values()) { if (op.value === -1) return null; @@ -1881,13 +1848,18 @@ Script.prototype.isPubkey = function isPubkey(minimal) { && this.raw[this.raw.length - 1] === opcodes.OP_CHECKSIG; } - if (this.code.length !== 2) + if (this.raw.length < 35 || this.raw.length > 71) return false; - const size = this.getLength(0); + if (this.raw[this.raw.length - 1] !== opcodes.OP_CHECKSIG) + return false; + + const script = this.read(); + const size = script.getLength(); return (size === 33 || size === 65) - && this.getOp(1) === opcodes.OP_CHECKSIG; + && script.getOp() === opcodes.OP_CHECKSIG + && script.left() === 0; }; /** @@ -1903,7 +1875,7 @@ Script.prototype.getPubkey = function getPubkey(minimal) { if (minimal) return this.raw.slice(1, 1 + this.raw[0]); - return this.getData(0); + return this.read().getData(); }; /** @@ -1922,14 +1894,23 @@ Script.prototype.isPubkeyhash = function isPubkeyhash(minimal) { && this.raw[24] === opcodes.OP_CHECKSIG; } - if (this.code.length !== 5) + if (this.raw.length < 25 || this.raw.length > 29) return false; - return this.getOp(0) === opcodes.OP_DUP - && this.getOp(1) === opcodes.OP_HASH160 - && this.getLength(2) === 20 - && this.getOp(3) === opcodes.OP_EQUALVERIFY - && this.getOp(4) === opcodes.OP_CHECKSIG; + if (this.raw[0] !== opcodes.OP_DUP) + return false; + + if (this.raw[this.raw.length - 1] !== opcodes.OP_CHECKSIG) + return false; + + const script = this.read(); + + return script.getOp() === opcodes.OP_DUP + && script.getOp() === opcodes.OP_HASH160 + && script.getLength() === 20 + && script.getOp() === opcodes.OP_EQUALVERIFY + && script.getOp() === opcodes.OP_CHECKSIG + && script.left() === 0; }; /** @@ -1945,7 +1926,7 @@ Script.prototype.getPubkeyhash = function getPubkeyhash(minimal) { if (minimal) return this.raw.slice(3, 23); - return this.getData(2); + return this.read().seek(2).getData(); }; /** @@ -1955,27 +1936,31 @@ Script.prototype.getPubkeyhash = function getPubkeyhash(minimal) { */ Script.prototype.isMultisig = function isMultisig(minimal) { - if (this.code.length < 4 || this.code.length > 19) + if (this.raw.length < 41 || this.raw.length > 1123) return false; - if (this.getOp(-1) !== opcodes.OP_CHECKMULTISIG) + if (this.raw[this.raw.length - 1] !== opcodes.OP_CHECKMULTISIG) return false; - const m = this.getSmall(0); + const m = common.getSmall(this.raw[0]); if (m < 1) return false; - const n = this.getSmall(-2); + const n = common.getSmall(this.raw[this.raw.length - 2]); if (n < 1 || m > n) return false; - if (this.code.length !== n + 3) - return false; + const script = this.read(); + script.seek(1); + + for (let i = 0; i < n; i++) { + const op = script.get(); + + if (!op) + return false; - for (let i = 1; i < n + 1; i++) { - const op = this.code[i]; const size = op.toLength(); if (size !== 33 && size !== 65) @@ -1985,7 +1970,9 @@ Script.prototype.isMultisig = function isMultisig(minimal) { return false; } - return true; + return script.getSmall() === n + && script.getOp() === opcodes.OP_CHECKMULTISIG + && script.left() === 0; }; /** @@ -1998,7 +1985,10 @@ Script.prototype.getMultisig = function getMultisig(minimal) { if (!this.isMultisig(minimal)) return [-1, -1]; - return [this.getSmall(0), this.getSmall(-2)]; + const m = common.getSmall(this.raw[0]); + const n = common.getSmall(this.raw[this.raw.length - 2]); + + return [m, n]; }; /** @@ -2025,7 +2015,7 @@ Script.prototype.getScripthash = function getScripthash() { if (!this.isScripthash()) return null; - return this.getData(1); + return this.raw.slice(2, 22); }; /** @@ -2035,13 +2025,13 @@ Script.prototype.getScripthash = function getScripthash() { */ Script.prototype.isNulldata = function isNulldata(minimal) { - if (this.code.length === 0) + if (this.raw.length === 0) return false; - if (this.getOp(0) !== opcodes.OP_RETURN) + if (this.raw[0] !== opcodes.OP_RETURN) return false; - if (this.code.length === 1) + if (this.raw.length === 1) return true; if (minimal) { @@ -2049,8 +2039,12 @@ Script.prototype.isNulldata = function isNulldata(minimal) { return false; } - for (let i = 1; i < this.code.length; i++) { - const op = this.code[i]; + const script = this.read(); + script.seek(1); + + while (script.left()) { + const op = script.get(); + assert(op); if (op.value === -1) return false; @@ -2075,9 +2069,11 @@ Script.prototype.getNulldata = function getNulldata(minimal) { if (!this.isNulldata(minimal)) return null; - for (let i = 1; i < this.code.length; i++) { - const op = this.code[i]; - const data = op.toPush(); + const script = this.read(); + script.seek(1); + + while (script.left()) { + const data = script.getPush(); if (data) return data; } @@ -2141,8 +2137,8 @@ Script.prototype.getProgram = function getProgram() { if (!this.isProgram()) return null; - const version = this.getSmall(0); - const data = this.getData(1); + const version = common.getSmall(this.raw[0]); + const data = this.raw.slice(2); return new Program(version, data); }; @@ -2191,7 +2187,7 @@ Script.prototype.getWitnessPubkeyhash = function getWitnessPubkeyhash() { if (!this.isWitnessPubkeyhash()) return null; - return this.getData(1); + return this.raw.slice(2, 22); }; /** @@ -2215,7 +2211,7 @@ Script.prototype.getWitnessScripthash = function getWitnessScripthash() { if (!this.isWitnessScripthash()) return null; - return this.getData(1); + return this.raw.slice(2, 34); }; /** @@ -2238,8 +2234,7 @@ Script.prototype.isWitnessMasthash = function isWitnessMasthash() { Script.prototype.getWitnessMasthash = function getWitnessMasthash() { if (!this.isWitnessMasthash()) return null; - - return this.getData(1); + return this.raw.slice(2, 34); }; /** @@ -2293,12 +2288,13 @@ Script.prototype.isUnknownInput = function isUnknownInput() { */ Script.prototype.isPubkeyInput = function isPubkeyInput() { - if (this.code.length !== 1) + if (this.raw.length < 10 || this.raw.length > 78) return false; - const size = this.getLength(0); + const script = this.read(); + const size = script.getLength(); - return size >= 9 && size <= 73; + return size >= 9 && size <= 73 && script.left() === 0; }; /** @@ -2310,7 +2306,7 @@ Script.prototype.getPubkeyInput = function getPubkeyInput() { if (!this.isPubkeyInput()) return null; - return this.getData(0); + return this.read().getData(); }; /** @@ -2320,14 +2316,16 @@ Script.prototype.getPubkeyInput = function getPubkeyInput() { */ Script.prototype.isPubkeyhashInput = function isPubkeyhashInput() { - if (this.code.length !== 2) + if (this.raw.length < 44 || this.raw.length > 148) return false; - const sig = this.getLength(0); - const key = this.getLength(1); + const script = this.read(); + const sig = script.getLength(); + const key = script.getLength(); return sig >= 9 && sig <= 73 - && (key === 33 || key === 65); + && (key === 33 || key === 65) + && script.left() === 0; }; /** @@ -2339,7 +2337,9 @@ Script.prototype.getPubkeyhashInput = function getPubkeyhashInput() { if (!this.isPubkeyhashInput()) return [null, null]; - return [this.getData(0), this.getData(1)]; + const script = this.read(); + + return [script.getData(), script.getData()]; }; /** @@ -2349,13 +2349,13 @@ Script.prototype.getPubkeyhashInput = function getPubkeyhashInput() { */ Script.prototype.isMultisigInput = function isMultisigInput() { - if (this.code.length < 2) + if (this.raw.length < 20) return false; - if (this.getOp(0) !== opcodes.OP_0) + if (this.raw[0] !== opcodes.OP_0) return false; - if (this.getOp(1) > opcodes.OP_PUSHDATA4) + if (this.raw[1] > opcodes.OP_PUSHDATA4) return false; // We need to rule out scripthash @@ -2363,8 +2363,11 @@ Script.prototype.isMultisigInput = function isMultisigInput() { if (this.isScripthashInput()) return false; - for (let i = 1; i < this.code.length; i++) { - const size = this.getLength(i); + const script = this.read(); + script.seek(1); + + while (script.left()) { + const size = script.getLength(); if (size < 9 || size > 73) return false; } @@ -2381,10 +2384,13 @@ Script.prototype.getMultisigInput = function getMultisigInput() { if (!this.isMultisigInput()) return null; + const script = this.read(); + script.seek(1); + const sigs = []; - for (let i = 1; i < this.code.length; i++) - sigs.push(this.getData(i)); + while (script.left()) + sigs.push(script.getData()); return sigs; }; @@ -2396,15 +2402,15 @@ Script.prototype.getMultisigInput = function getMultisigInput() { */ Script.prototype.isScripthashInput = function isScripthashInput() { - if (this.code.length < 2) + if (this.raw.length < 2) return false; // Grab the raw redeem script. - const raw = this.getData(-1); + const redeem = this.getRedeem(); // Last data element should be an array // for the redeem script. - if (!raw) + if (!redeem) return false; // Testing for scripthash inputs requires @@ -2416,26 +2422,21 @@ Script.prototype.isScripthashInput = function isScripthashInput() { // key, and we ensure that it is at least // a script that does not use undefined // opcodes. - if (raw.length === 0) + if (redeem.raw.length === 0) return false; - if (common.isSignatureEncoding(raw)) + if (common.isSignatureEncoding(redeem.raw)) return false; - if (common.isKeyEncoding(raw)) + if (common.isKeyEncoding(redeem.raw)) return false; - const redeem = Script.fromRaw(raw); - if (!redeem.isCode()) return false; if (redeem.isUnspendable()) return false; - if (!this.isPushOnly()) - return false; - return true; }; @@ -2448,7 +2449,14 @@ Script.prototype.getScripthashInput = function getScripthashInput() { if (!this.isScripthashInput()) return null; - return this.getData(-1); + const script = this.read(); + + let data = null; + + while (script.left()) + data = script.getData(); + + return data; }; /** @@ -2498,7 +2506,7 @@ Script.getCoinbaseHeight = function getCoinbaseHeight(raw) { */ Script.prototype.test = function test(filter) { - for (const op of this.code) { + for (const op of this.values()) { if (op.value === -1) break; @@ -2519,11 +2527,11 @@ Script.prototype.test = function test(filter) { */ Script.prototype.isPushOnly = function isPushOnly() { - for (const op of this.code) { - if (op.value === -1) + for (const op of this.ops()) { + if (op === -1) return false; - if (op.value > opcodes.OP_16) + if (op > opcodes.OP_16) return false; } @@ -2541,11 +2549,11 @@ Script.prototype.getSigops = function getSigops(accurate) { let total = 0; let lastOp = -1; - for (const op of this.code) { - if (op.value === -1) + for (const op of this.ops()) { + if (op === -1) break; - switch (op.value) { + switch (op) { case opcodes.OP_CHECKSIG: case opcodes.OP_CHECKSIGVERIFY: total += 1; @@ -2559,7 +2567,7 @@ Script.prototype.getSigops = function getSigops(accurate) { break; } - lastOp = op.value; + lastOp = op; } return total; @@ -2617,450 +2625,6 @@ Script.prototype.getWitnessSigops = function getWitnessSigops(input, witness) { return 0; }; -/* - * Mutation - */ - -Script.prototype.get = function get(index) { - if (index < 0) - index += this.code.length; - - if (index < 0 || index >= this.code.length) - return null; - - return this.code[index]; -}; - -Script.prototype.pop = function pop() { - const op = this.code.pop(); - return op || null; -}; - -Script.prototype.shift = function shift() { - const op = this.code.shift(); - return op || null; -}; - -Script.prototype.remove = function remove(index) { - if (index < 0) - index += this.code.length; - - if (index < 0 || index >= this.code.length) - return null; - - const items = this.code.splice(index, 1); - - if (items.length === 0) - return null; - - return items[0]; -}; - -Script.prototype.set = function set(index, op) { - if (index < 0) - index += this.code.length; - - assert(Opcode.isOpcode(op)); - assert(index >= 0 && index <= this.code.length); - - this.code[index] = op; - - return this; -}; - -Script.prototype.push = function push(op) { - assert(Opcode.isOpcode(op)); - this.code.push(op); - return this; -}; - -Script.prototype.unshift = function unshift(op) { - assert(Opcode.isOpcode(op)); - this.code.unshift(op); - return this; -}; - -Script.prototype.insert = function insert(index, op) { - if (index < 0) - index += this.code.length; - - assert(Opcode.isOpcode(op)); - assert(index >= 0 && index <= this.code.length); - - this.code.splice(index, 0, op); - - return this; -}; - -/* - * Op - */ - -Script.prototype.getOp = function getOp(index) { - const op = this.get(index); - return op ? op.value : -1; -}; - -Script.prototype.popOp = function popOp() { - const op = this.pop(); - return op ? op.value : -1; -}; - -Script.prototype.shiftOp = function shiftOp() { - const op = this.shift(); - return op ? op.value : -1; -}; - -Script.prototype.removeOp = function removeOp(index) { - const op = this.remove(index); - return op ? op.value : -1; -}; - -Script.prototype.setOp = function setOp(index, value) { - return this.set(index, Opcode.fromOp(value)); -}; - -Script.prototype.pushOp = function pushOp(value) { - return this.push(Opcode.fromOp(value)); -}; - -Script.prototype.unshiftOp = function unshiftOp(value) { - return this.unshift(Opcode.fromOp(value)); -}; - -Script.prototype.insertOp = function insertOp(index, value) { - return this.insert(index, Opcode.fromOp(value)); -}; - -/* - * Data - */ - -Script.prototype.getData = function getData(index) { - const op = this.get(index); - return op ? op.data : null; -}; - -Script.prototype.popData = function popData() { - const op = this.pop(); - return op ? op.data : null; -}; - -Script.prototype.shiftData = function shiftData() { - const op = this.shift(); - return op ? op.data : null; -}; - -Script.prototype.removeData = function removeData(index) { - const op = this.remove(index); - return op ? op.data : null; -}; - -Script.prototype.setData = function setData(index, data) { - return this.set(index, Opcode.fromData(data)); -}; - -Script.prototype.pushData = function pushData(data) { - return this.push(Opcode.fromData(data)); -}; - -Script.prototype.unshiftData = function unshiftData(data) { - return this.unshift(Opcode.fromData(data)); -}; - -Script.prototype.insertData = function insertData(index, data) { - return this.insert(index, Opcode.fromData(data)); -}; - -/* - * Length - */ - -Script.prototype.getLength = function getLength(index) { - const op = this.get(index); - return op ? op.toLength() : -1; -}; - -/* - * Push - */ - -Script.prototype.getPush = function getPush(index) { - const op = this.get(index); - return op ? op.toPush() : null; -}; - -Script.prototype.popPush = function popPush() { - const op = this.pop(); - return op ? op.toPush() : null; -}; - -Script.prototype.shiftPush = function shiftPush() { - const op = this.shift(); - return op ? op.toPush() : null; -}; - -Script.prototype.removePush = function removePush(index) { - const op = this.remove(index); - return op ? op.toPush() : null; -}; - -Script.prototype.setPush = function setPush(index, data) { - return this.set(index, Opcode.fromPush(data)); -}; - -Script.prototype.pushPush = function pushPush(data) { - return this.push(Opcode.fromPush(data)); -}; - -Script.prototype.unshiftPush = function unshiftPush(data) { - return this.unshift(Opcode.fromPush(data)); -}; - -Script.prototype.insertPush = function insertPush(index, data) { - return this.insert(index, Opcode.fromPush(data)); -}; - -/* - * String - */ - -Script.prototype.getString = function getString(index, enc) { - const op = this.get(index); - return op ? op.toString(enc) : null; -}; - -Script.prototype.popString = function popString(enc) { - const op = this.pop(); - return op ? op.toString(enc) : null; -}; - -Script.prototype.shiftString = function shiftString(enc) { - const op = this.shift(); - return op ? op.toString(enc) : null; -}; - -Script.prototype.removeString = function removeString(index, enc) { - const op = this.remove(index); - return op ? op.toString(enc) : null; -}; - -Script.prototype.setString = function setString(index, str, enc) { - return this.set(index, Opcode.fromString(str, enc)); -}; - -Script.prototype.pushString = function pushString(str, enc) { - return this.push(Opcode.fromString(str, enc)); -}; - -Script.prototype.unshiftString = function unshiftString(str, enc) { - return this.unshift(Opcode.fromString(str, enc)); -}; - -Script.prototype.insertString = function insertString(index, str, enc) { - return this.insert(index, Opcode.fromString(str, enc)); -}; - -/* - * Small - */ - -Script.prototype.getSmall = function getSmall(index) { - const op = this.get(index); - return op ? op.toSmall() : -1; -}; - -Script.prototype.popSmall = function popSmall() { - const op = this.pop(); - return op ? op.toSmall() : -1; -}; - -Script.prototype.shiftSmall = function shiftSmall() { - const op = this.shift(); - return op ? op.toSmall() : -1; -}; - -Script.prototype.removeSmall = function removeSmall(index) { - const op = this.remove(index); - return op ? op.toSmall() : -1; -}; - -Script.prototype.setSmall = function setSmall(index, num) { - return this.set(index, Opcode.fromSmall(num)); -}; - -Script.prototype.pushSmall = function pushSmall(num) { - return this.push(Opcode.fromSmall(num)); -}; - -Script.prototype.unshiftSmall = function unshiftSmall(num) { - return this.unshift(Opcode.fromSmall(num)); -}; - -Script.prototype.insertSmall = function insertSmall(index, num) { - return this.insert(index, Opcode.fromSmall(num)); -}; - -/* - * Num - */ - -Script.prototype.getNum = function getNum(index, minimal, limit) { - const op = this.get(index); - return op ? op.toNum(minimal, limit) : null; -}; - -Script.prototype.popNum = function popNum(minimal, limit) { - const op = this.pop(); - return op ? op.toNum(minimal, limit) : null; -}; - -Script.prototype.shiftNum = function shiftNum(minimal, limit) { - const op = this.shift(); - return op ? op.toNum(minimal, limit) : null; -}; - -Script.prototype.removeNum = function removeNum(index, minimal, limit) { - const op = this.remove(index); - return op ? op.toNum(minimal, limit) : null; -}; - -Script.prototype.setNum = function setNum(index, num) { - return this.set(index, Opcode.fromNum(num)); -}; - -Script.prototype.pushNum = function pushNum(num) { - return this.push(Opcode.fromNum(num)); -}; - -Script.prototype.unshiftNum = function unshiftNum(num) { - return this.unshift(Opcode.fromNum(num)); -}; - -Script.prototype.insertNum = function insertNum(index, num) { - return this.insert(index, Opcode.fromNum(num)); -}; - -/* - * Int - */ - -Script.prototype.getInt = function getInt(index, minimal, limit) { - const op = this.get(index); - return op ? op.toInt(minimal, limit) : -1; -}; - -Script.prototype.popInt = function popInt(minimal, limit) { - const op = this.pop(); - return op ? op.toInt(minimal, limit) : -1; -}; - -Script.prototype.shiftInt = function shiftInt(minimal, limit) { - const op = this.shift(); - return op ? op.toInt(minimal, limit) : -1; -}; - -Script.prototype.removeInt = function removeInt(index, minimal, limit) { - const op = this.remove(index); - return op ? op.toInt(minimal, limit) : -1; -}; - -Script.prototype.setInt = function setInt(index, num) { - return this.set(index, Opcode.fromInt(num)); -}; - -Script.prototype.pushInt = function pushInt(num) { - return this.push(Opcode.fromInt(num)); -}; - -Script.prototype.unshiftInt = function unshiftInt(num) { - return this.unshift(Opcode.fromInt(num)); -}; - -Script.prototype.insertInt = function insertInt(index, num) { - return this.insert(index, Opcode.fromInt(num)); -}; - -/* - * Bool - */ - -Script.prototype.getBool = function getBool(index) { - const op = this.get(index); - return op ? op.toBool() : false; -}; - -Script.prototype.popBool = function popBool() { - const op = this.pop(); - return op ? op.toBool() : false; -}; - -Script.prototype.shiftBool = function shiftBool() { - const op = this.shift(); - return op ? op.toBool() : false; -}; - -Script.prototype.removeBool = function removeBool(index) { - const op = this.remove(index); - return op ? op.toBool() : false; -}; - -Script.prototype.setBool = function setBool(index, value) { - return this.set(index, Opcode.fromBool(value)); -}; - -Script.prototype.pushBool = function pushBool(value) { - return this.push(Opcode.fromBool(value)); -}; - -Script.prototype.unshiftBool = function unshiftBool(value) { - return this.unshift(Opcode.fromBool(value)); -}; - -Script.prototype.insertBool = function insertBool(index, value) { - return this.insert(index, Opcode.fromBool(value)); -}; - -/* - * Symbol - */ - -Script.prototype.getSym = function getSym(index) { - const op = this.get(index); - return op ? op.toSymbol() : null; -}; - -Script.prototype.popSym = function popSym() { - const op = this.pop(); - return op ? op.toSymbol() : null; -}; - -Script.prototype.shiftSym = function shiftSym() { - const op = this.shift(); - return op ? op.toSymbol() : null; -}; - -Script.prototype.removeSym = function removeSym(index) { - const op = this.remove(index); - return op ? op.toSymbol() : null; -}; - -Script.prototype.setSym = function setSym(index, symbol) { - return this.set(index, Opcode.fromSymbol(symbol)); -}; - -Script.prototype.pushSym = function pushSym(symbol) { - return this.push(Opcode.fromSymbol(symbol)); -}; - -Script.prototype.unshiftSym = function unshiftSym(symbol) { - return this.unshift(Opcode.fromSymbol(symbol)); -}; - -Script.prototype.insertSym = function insertSym(index, symbol) { - return this.insert(index, Opcode.fromSymbol(symbol)); -}; - /** * Inject properties from bitcoind test string. * @private @@ -3466,13 +3030,8 @@ Script.prototype.fromReader = function fromReader(br) { */ Script.prototype.fromRaw = function fromRaw(data) { - const br = new BufferReader(data, true); - + assert(Buffer.isBuffer(data)); this.raw = data; - - while (br.left()) - this.code.push(Opcode.fromReader(br)); - return this; }; @@ -3600,8 +3159,683 @@ function checksig(msg, sig, key) { return secp256k1.verify(msg, sig.slice(0, -1), key); } +/** + * ScriptReader + * @constructor + * @ignore + */ + +function ScriptReader(script) { + assert(Script.isScript(script)); + this.br = new BufferReader(script.raw, true); + this.index = -1; +} + +ScriptReader.prototype.reset = function reset() { + this.br.offset = 0; + this.index = -1; + return this; +}; + +ScriptReader.prototype.offset = function offset() { + return this.br.offset; +}; + +ScriptReader.prototype.seek = function seek(offset) { + let index = this.index + offset; + + if (index < -1) + index = -1; + + if (index < this.index) + this.reset(); + + while (this.index !== index) { + if (this.getOp() === -1) + break; + } + + return this; +}; + +ScriptReader.prototype.read = function read() { + const op = this.get(); + return [this.index, op]; +}; + +ScriptReader.prototype.get = function get() { + if (this.br.left() === 0) + return null; + + this.index += 1; + + return Opcode.fromReader(this.br); +}; + +ScriptReader.prototype.getOp = function getOp() { + if (this.br.left() === 0) + return -1; + + this.index += 1; + + return Opcode.readOp(this.br); +}; + +ScriptReader.prototype.getData = function getData() { + const op = this.get(); + + if (!op) + return null; + + return op.data; +}; + +ScriptReader.prototype.getLength = function getLength() { + if (this.br.left() === 0) + return -1; + + this.index += 1; + + return Opcode.readLength(this.br); +}; + +ScriptReader.prototype.getPush = function getPush() { + const op = this.get(); + + if (!op) + return null; + + return op.toPush(); +}; + +ScriptReader.prototype.getString = function getString(enc) { + const op = this.get(); + + if (!op) + return null; + + return op.toString(enc); +}; + +ScriptReader.prototype.getSmall = function getSmall() { + const op = this.get(); + + if (!op) + return -1; + + return op.toSmall(); +}; + +ScriptReader.prototype.getNum = function getNum(minimal, limit) { + const op = this.get(); + + if (!op) + return null; + + return op.toNum(minimal, limit); +}; + +ScriptReader.prototype.getInt = function getInt(minimal, limit) { + const op = this.get(); + + if (!op) + return -1; + + return op.toInt(minimal, limit); +}; + +ScriptReader.prototype.getBool = function getBool() { + const op = this.get(); + + if (!op) + return false; + + return op.toBool(); +}; + +ScriptReader.prototype.left = function left() { + return this.br.left(); +}; + +/** + * ScriptWriter + * @constructor + * @ignore + */ + +function ScriptWriter(script) { + if (!(this instanceof ScriptWriter)) + return new ScriptWriter(script); + + this.script = script || new Script(); + this.code = this.script.toArray(); +} + +Object.defineProperty(ScriptWriter.prototype, 'length', { + get() { + return this.code.length; + }, + set(length) { + this.code.length = length; + return this.code.length; + } +}); + +ScriptWriter.prototype.get = function get(index) { + if (index < 0) + index += this.code.length; + + if (index < 0 || index >= this.code.length) + return null; + + return this.code[index]; +}; + +ScriptWriter.prototype.pop = function pop() { + const op = this.code.pop(); + return op || null; +}; + +ScriptWriter.prototype.shift = function shift() { + const op = this.code.shift(); + return op || null; +}; + +ScriptWriter.prototype.remove = function remove(index) { + if (index < 0) + index += this.code.length; + + if (index < 0 || index >= this.code.length) + return null; + + const items = this.code.splice(index, 1); + + if (items.length === 0) + return null; + + return items[0]; +}; + +ScriptWriter.prototype.set = function set(index, op) { + if (index < 0) + index += this.code.length; + + assert(Opcode.isOpcode(op)); + assert(index >= 0 && index <= this.code.length); + + this.code[index] = op; + + return this; +}; + +ScriptWriter.prototype.push = function push(op) { + assert(Opcode.isOpcode(op)); + this.code.push(op); + return this; +}; + +ScriptWriter.prototype.unshift = function unshift(op) { + assert(Opcode.isOpcode(op)); + this.code.unshift(op); + return this; +}; + +ScriptWriter.prototype.insert = function insert(index, op) { + if (index < 0) + index += this.code.length; + + assert(Opcode.isOpcode(op)); + assert(index >= 0 && index <= this.code.length); + + this.code.splice(index, 0, op); + + return this; +}; + +/* + * Op + */ + +ScriptWriter.prototype.getOp = function getOp(index) { + const op = this.get(index); + return op ? op.value : -1; +}; + +ScriptWriter.prototype.popOp = function popOp() { + const op = this.pop(); + return op ? op.value : -1; +}; + +ScriptWriter.prototype.shiftOp = function shiftOp() { + const op = this.shift(); + return op ? op.value : -1; +}; + +ScriptWriter.prototype.removeOp = function removeOp(index) { + const op = this.remove(index); + return op ? op.value : -1; +}; + +ScriptWriter.prototype.setOp = function setOp(index, value) { + return this.set(index, Opcode.fromOp(value)); +}; + +ScriptWriter.prototype.pushOp = function pushOp(value) { + return this.push(Opcode.fromOp(value)); +}; + +ScriptWriter.prototype.unshiftOp = function unshiftOp(value) { + return this.unshift(Opcode.fromOp(value)); +}; + +ScriptWriter.prototype.insertOp = function insertOp(index, value) { + return this.insert(index, Opcode.fromOp(value)); +}; + +/* + * Data + */ + +ScriptWriter.prototype.getData = function getData(index) { + const op = this.get(index); + return op ? op.data : null; +}; + +ScriptWriter.prototype.popData = function popData() { + const op = this.pop(); + return op ? op.data : null; +}; + +ScriptWriter.prototype.shiftData = function shiftData() { + const op = this.shift(); + return op ? op.data : null; +}; + +ScriptWriter.prototype.removeData = function removeData(index) { + const op = this.remove(index); + return op ? op.data : null; +}; + +ScriptWriter.prototype.setData = function setData(index, data) { + return this.set(index, Opcode.fromData(data)); +}; + +ScriptWriter.prototype.pushData = function pushData(data) { + return this.push(Opcode.fromData(data)); +}; + +ScriptWriter.prototype.unshiftData = function unshiftData(data) { + return this.unshift(Opcode.fromData(data)); +}; + +ScriptWriter.prototype.insertData = function insertData(index, data) { + return this.insert(index, Opcode.fromData(data)); +}; + +/* + * Length + */ + +ScriptWriter.prototype.getLength = function getLength(index) { + const op = this.get(index); + return op ? op.toLength() : -1; +}; + +/* + * Push + */ + +ScriptWriter.prototype.getPush = function getPush(index) { + const op = this.get(index); + return op ? op.toPush() : null; +}; + +ScriptWriter.prototype.popPush = function popPush() { + const op = this.pop(); + return op ? op.toPush() : null; +}; + +ScriptWriter.prototype.shiftPush = function shiftPush() { + const op = this.shift(); + return op ? op.toPush() : null; +}; + +ScriptWriter.prototype.removePush = function removePush(index) { + const op = this.remove(index); + return op ? op.toPush() : null; +}; + +ScriptWriter.prototype.setPush = function setPush(index, data) { + return this.set(index, Opcode.fromPush(data)); +}; + +ScriptWriter.prototype.pushPush = function pushPush(data) { + return this.push(Opcode.fromPush(data)); +}; + +ScriptWriter.prototype.unshiftPush = function unshiftPush(data) { + return this.unshift(Opcode.fromPush(data)); +}; + +ScriptWriter.prototype.insertPush = function insertPush(index, data) { + return this.insert(index, Opcode.fromPush(data)); +}; + +/* + * String + */ + +ScriptWriter.prototype.getString = function getString(index, enc) { + const op = this.get(index); + return op ? op.toString(enc) : null; +}; + +ScriptWriter.prototype.popString = function popString(enc) { + const op = this.pop(); + return op ? op.toString(enc) : null; +}; + +ScriptWriter.prototype.shiftString = function shiftString(enc) { + const op = this.shift(); + return op ? op.toString(enc) : null; +}; + +ScriptWriter.prototype.removeString = function removeString(index, enc) { + const op = this.remove(index); + return op ? op.toString(enc) : null; +}; + +ScriptWriter.prototype.setString = function setString(index, str, enc) { + return this.set(index, Opcode.fromString(str, enc)); +}; + +ScriptWriter.prototype.pushString = function pushString(str, enc) { + return this.push(Opcode.fromString(str, enc)); +}; + +ScriptWriter.prototype.unshiftString = function unshiftString(str, enc) { + return this.unshift(Opcode.fromString(str, enc)); +}; + +ScriptWriter.prototype.insertString = function insertString(index, str, enc) { + return this.insert(index, Opcode.fromString(str, enc)); +}; + +/* + * Small + */ + +ScriptWriter.prototype.getSmall = function getSmall(index) { + const op = this.get(index); + return op ? op.toSmall() : -1; +}; + +ScriptWriter.prototype.popSmall = function popSmall() { + const op = this.pop(); + return op ? op.toSmall() : -1; +}; + +ScriptWriter.prototype.shiftSmall = function shiftSmall() { + const op = this.shift(); + return op ? op.toSmall() : -1; +}; + +ScriptWriter.prototype.removeSmall = function removeSmall(index) { + const op = this.remove(index); + return op ? op.toSmall() : -1; +}; + +ScriptWriter.prototype.setSmall = function setSmall(index, num) { + return this.set(index, Opcode.fromSmall(num)); +}; + +ScriptWriter.prototype.pushSmall = function pushSmall(num) { + return this.push(Opcode.fromSmall(num)); +}; + +ScriptWriter.prototype.unshiftSmall = function unshiftSmall(num) { + return this.unshift(Opcode.fromSmall(num)); +}; + +ScriptWriter.prototype.insertSmall = function insertSmall(index, num) { + return this.insert(index, Opcode.fromSmall(num)); +}; + +/* + * Num + */ + +ScriptWriter.prototype.getNum = function getNum(index, minimal, limit) { + const op = this.get(index); + return op ? op.toNum(minimal, limit) : null; +}; + +ScriptWriter.prototype.popNum = function popNum(minimal, limit) { + const op = this.pop(); + return op ? op.toNum(minimal, limit) : null; +}; + +ScriptWriter.prototype.shiftNum = function shiftNum(minimal, limit) { + const op = this.shift(); + return op ? op.toNum(minimal, limit) : null; +}; + +ScriptWriter.prototype.removeNum = function removeNum(index, minimal, limit) { + const op = this.remove(index); + return op ? op.toNum(minimal, limit) : null; +}; + +ScriptWriter.prototype.setNum = function setNum(index, num) { + return this.set(index, Opcode.fromNum(num)); +}; + +ScriptWriter.prototype.pushNum = function pushNum(num) { + return this.push(Opcode.fromNum(num)); +}; + +ScriptWriter.prototype.unshiftNum = function unshiftNum(num) { + return this.unshift(Opcode.fromNum(num)); +}; + +ScriptWriter.prototype.insertNum = function insertNum(index, num) { + return this.insert(index, Opcode.fromNum(num)); +}; + +/* + * Int + */ + +ScriptWriter.prototype.getInt = function getInt(index, minimal, limit) { + const op = this.get(index); + return op ? op.toInt(minimal, limit) : -1; +}; + +ScriptWriter.prototype.popInt = function popInt(minimal, limit) { + const op = this.pop(); + return op ? op.toInt(minimal, limit) : -1; +}; + +ScriptWriter.prototype.shiftInt = function shiftInt(minimal, limit) { + const op = this.shift(); + return op ? op.toInt(minimal, limit) : -1; +}; + +ScriptWriter.prototype.removeInt = function removeInt(index, minimal, limit) { + const op = this.remove(index); + return op ? op.toInt(minimal, limit) : -1; +}; + +ScriptWriter.prototype.setInt = function setInt(index, num) { + return this.set(index, Opcode.fromInt(num)); +}; + +ScriptWriter.prototype.pushInt = function pushInt(num) { + return this.push(Opcode.fromInt(num)); +}; + +ScriptWriter.prototype.unshiftInt = function unshiftInt(num) { + return this.unshift(Opcode.fromInt(num)); +}; + +ScriptWriter.prototype.insertInt = function insertInt(index, num) { + return this.insert(index, Opcode.fromInt(num)); +}; + +/* + * Bool + */ + +ScriptWriter.prototype.getBool = function getBool(index) { + const op = this.get(index); + return op ? op.toBool() : false; +}; + +ScriptWriter.prototype.popBool = function popBool() { + const op = this.pop(); + return op ? op.toBool() : false; +}; + +ScriptWriter.prototype.shiftBool = function shiftBool() { + const op = this.shift(); + return op ? op.toBool() : false; +}; + +ScriptWriter.prototype.removeBool = function removeBool(index) { + const op = this.remove(index); + return op ? op.toBool() : false; +}; + +ScriptWriter.prototype.setBool = function setBool(index, value) { + return this.set(index, Opcode.fromBool(value)); +}; + +ScriptWriter.prototype.pushBool = function pushBool(value) { + return this.push(Opcode.fromBool(value)); +}; + +ScriptWriter.prototype.unshiftBool = function unshiftBool(value) { + return this.unshift(Opcode.fromBool(value)); +}; + +ScriptWriter.prototype.insertBool = function insertBool(index, value) { + return this.insert(index, Opcode.fromBool(value)); +}; + +/* + * Symbol + */ + +ScriptWriter.prototype.getSym = function getSym(index) { + const op = this.get(index); + return op ? op.toSymbol() : null; +}; + +ScriptWriter.prototype.popSym = function popSym() { + const op = this.pop(); + return op ? op.toSymbol() : null; +}; + +ScriptWriter.prototype.shiftSym = function shiftSym() { + const op = this.shift(); + return op ? op.toSymbol() : null; +}; + +ScriptWriter.prototype.removeSym = function removeSym(index) { + const op = this.remove(index); + return op ? op.toSymbol() : null; +}; + +ScriptWriter.prototype.setSym = function setSym(index, symbol) { + return this.set(index, Opcode.fromSymbol(symbol)); +}; + +ScriptWriter.prototype.pushSym = function pushSym(symbol) { + return this.push(Opcode.fromSymbol(symbol)); +}; + +ScriptWriter.prototype.unshiftSym = function unshiftSym(symbol) { + return this.unshift(Opcode.fromSymbol(symbol)); +}; + +ScriptWriter.prototype.insertSym = function insertSym(index, symbol) { + return this.insert(index, Opcode.fromSymbol(symbol)); +}; + +/* + * Render + */ + +ScriptWriter.prototype.render = function render() { + let size = 0; + + for (const op of this.code) + size += op.getSize(); + + const bw = new StaticWriter(size); + + for (const op of this.code) + op.toWriter(bw); + + return bw.render(); +}; + +ScriptWriter.prototype.compile = function compile() { + this.script.raw = this.render(); + return this.script; +}; + +/** + * ScriptIterator + * @constructor + * @ignore + */ + +function ScriptIterator(script, type) { + this.br = new BufferReader(script.raw, true); + this.type = type; + this.index = 0; +} + +ScriptIterator.prototype[Symbol.iterator] = function iterator() { + return this; +}; + +ScriptIterator.prototype.next = function next() { + let value = undefined; + + if (this.br.left() === 0) + return { value, done: true }; + + switch (this.type) { + case 0: + value = Opcode.fromReader(this.br); + break; + case 1: + value = [this.index, Opcode.fromReader(this.br)]; + this.index += 1; + break; + case 2: + value = Opcode.readOp(this.br); + break; + } + + return { value, done: false }; +}; + /* * Expose */ -module.exports = Script; +exports = Script; +exports.Reader = ScriptReader; +exports.Writer = ScriptWriter; + +exports.read = function read(script) { + return new ScriptReader(script); +}; + +exports.write = function write(script) { + return new ScriptWriter(script); +}; + +module.exports = exports; diff --git a/lib/script/witness.js b/lib/script/witness.js index 001f9ef3..15b50456 100644 --- a/lib/script/witness.js +++ b/lib/script/witness.js @@ -191,15 +191,6 @@ Witness.prototype.inject = function inject(witness) { return this; }; -/** - * Compile witness (NOP). - * @returns {Witness} - */ - -Witness.prototype.compile = function compile() { - return this; -}; - /** * "Guess" the type of the witness. * This method is not 100% reliable. diff --git a/lib/utils/gcs.js b/lib/utils/gcs.js index a0881190..3e801e8c 100644 --- a/lib/utils/gcs.js +++ b/lib/utils/gcs.js @@ -516,7 +516,7 @@ function siphash24(data, key) { } function getPushes(items, script) { - for (const op of script.code) { + for (const op of script.values()) { if (!op.data || op.data.length === 0) continue; diff --git a/scripts/fuzz.js b/scripts/fuzz.js index 87835e2c..0e22c6da 100644 --- a/scripts/fuzz.js +++ b/scripts/fuzz.js @@ -119,14 +119,12 @@ function randomWitness(redeem) { if (redeem) witness.push(redeem); - witness.compile(); - return witness; } function randomInputScript(redeem) { const size = util.random(1, 100); - const script = new Script(); + const script = Script.write(); for (let i = 0; i < size; i++) { const len = util.random(0, 100); @@ -148,7 +146,7 @@ function isPushOnly(script) { if (script.isPushOnly()) return true; - for (const op of script.code) { + for (const op of script.values()) { if (op.value === Script.opcodes.NOP) continue; diff --git a/scripts/gen.js b/scripts/gen.js index e3fe3d3d..cc293a98 100644 --- a/scripts/gen.js +++ b/scripts/gen.js @@ -35,7 +35,7 @@ function createGenesisBlock(options) { hash: encoding.NULL_HASH, index: 0xffffffff }, - script: Script() + script: Script.write() .pushInt(486604799) .pushPush(Buffer.from([4])) .pushData(flags) diff --git a/test/chain-test.js b/test/chain-test.js index c3785303..965cdc91 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -568,10 +568,11 @@ describe('Chain', function() { assert(output.script.isCommitment()); - const commit = Buffer.from(output.script.getData(1)); + const script = output.script.write(); + const commit = Buffer.from(script.getData(1)); commit.fill(0, 10); - output.script.setData(1, commit); - output.script.compile(); + script.setData(1, commit); + script.compile(); block.refresh(true); block.merkleRoot = block.createMerkleRoot('hex'); @@ -784,16 +785,16 @@ describe('Chain', function() { it('should mine 111 multisig blocks', async () => { const flags = common.flags.DEFAULT_FLAGS & ~common.flags.VERIFY_POW; - const redeem = new Script(); - redeem.pushInt(20); + const sw = Script.write(); + sw.pushInt(20); for (let i = 0; i < 20; i++) - redeem.pushData(encoding.ZERO_KEY); + sw.pushData(encoding.ZERO_KEY); - redeem.pushInt(20); - redeem.pushOp(opcodes.OP_CHECKMULTISIG); + sw.pushInt(20); + sw.pushOp(opcodes.OP_CHECKMULTISIG); - redeem.compile(); + const redeem = sw.compile(); const script = Script.fromScripthash(redeem.hash160()); @@ -826,17 +827,17 @@ describe('Chain', function() { const end = chain.height - 100; const job = await cpu.createJob(); - const script = new Script(); + const sw = Script.write(); - script.pushInt(20); + sw.pushInt(20); for (let i = 0; i < 20; i++) - script.pushData(encoding.ZERO_KEY); + sw.pushData(encoding.ZERO_KEY); - script.pushInt(20); - script.pushOp(opcodes.OP_CHECKMULTISIG); + sw.pushInt(20); + sw.pushOp(opcodes.OP_CHECKMULTISIG); - script.compile(); + const script = sw.compile(); for (let i = start; i <= end; i++) { const block = await chain.db.getBlock(i); diff --git a/test/mempool-test.js b/test/mempool-test.js index 63567337..634ad70b 100644 --- a/test/mempool-test.js +++ b/test/mempool-test.js @@ -127,8 +127,9 @@ describe('Mempool', function() { // Fake signature const input = fake.inputs[0]; - input.script.setData(0, encoding.ZERO_SIG); - input.script.compile(); + const sw = input.script.write(); + sw.setData(0, encoding.ZERO_SIG); + sw.compile(); // balance: 11000 { diff --git a/test/wallet-test.js b/test/wallet-test.js index 9a7daaee..2ee1d407 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -223,8 +223,9 @@ async function testP2SH(witness, nesting) { assert(carol.account.change.getAddress().equals(change2)); const input = tx.inputs[0]; - input[vector].setData(2, encoding.ZERO_SIG); - input[vector].compile(); + const stack = input[vector].toStack(); + stack.set(2, encoding.ZERO_SIG); + input[vector].fromStack(stack); assert(!tx.verify(view, flags)); assert.strictEqual(tx.getFee(view), 10000); @@ -375,8 +376,9 @@ describe('Wallet', function() { await alice.template(fake); // Fake signature const input = fake.inputs[0]; - input.script.setData(0, encoding.ZERO_SIG); - input.script.compile(); + const script = input.script.write(); + script.setData(0, encoding.ZERO_SIG); + script.compile(); // balance: 11000 // Fake TX should temporarily change output.