diff --git a/bench/tx.js b/bench/tx.js index 932c2570d..b6baa7cef 100644 --- a/bench/tx.js +++ b/bench/tx.js @@ -27,10 +27,8 @@ const tx10 = common.readTX('tx10'); const [tx, view] = tx5.getTX(); const end = bench('sigops'); - let sigops = 0; - for (let i = 0; i < 100000; i++) - sigops += tx.getSigopsCost(view); + tx.getSigopsCost(view); end(100000); } @@ -161,10 +159,10 @@ for (let i = 0; i < 100; i++) { hash: encoding.NULL_HASH, index: 0 }, - script: [ - Buffer.allocUnsafe(9), - random.randomBytes(33) - ] + script: new Script() + .pushData(Buffer.allocUnsafe(9)) + .pushData(random.randomBytes(33)) + .compile() }); mtx.addOutput({ address: Address.fromHash(random.randomBytes(20)), diff --git a/docs/Scripting.md b/docs/Scripting.md index 0e70ef613..dc44083dd 100644 --- a/docs/Scripting.md +++ b/docs/Scripting.md @@ -1,37 +1,38 @@ Scripts are array-like objects with some helper functions. ``` js -var bcoin = require('bcoin'); -var assert = require('assert'); -var ScriptNum = bcoin.scriptnum; -var opcodes = bcoin.script.opcodes; +const bcoin = require('bcoin'); +const assert = require('assert'); +const Script = bcoin.script; +const Witness = bcoin.witness; +const Stack = bcoin.stack; -var output = new bcoin.script(); -output.push(opcodes.OP_DROP); -output.push(opcodes.OP_ADD); -output.push(new ScriptNum(7)); -output.push(opcodes.OP_NUMEQUAL); +const output = new Script(); +output.pushSym('OP_DROP'); +output.pushSym('OP_ADD'); +output.pushInt(7); +output.pushSym('OP_NUMEQUAL'); // Compile the script to its binary representation // (you must do this if you change something!). -output.compile(); assert(output.getSmall(2) === 7); // compiled as OP_7 +output.compile(); -var input = new bcoin.script(); -input.set(0, 'hello world'); // add some metadata -input.push(new ScriptNum(2)); -input.push(new ScriptNum(5)); +const input = new Script(); +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(); // A stack is another array-like object which contains // only Buffers (whereas scripts contain Opcode objects). -var stack = new bcoin.stack(); +const stack = new Stack(); input.execute(stack); output.execute(stack); // Verify the script was successful in its execution: assert(stack.length === 1); -assert(bcoin.script.bool(stack.pop()) === true); +assert(stack.getBool(-1) === true); ``` Using a witness would be similar, but witnesses do not get executed, they @@ -39,11 +40,11 @@ simply _become_ the stack. The witness object itself is very similar to the Stack object (an array-like object containing Buffers). ``` js -var witness = new bcoin.witness(); -witness.push(new ScriptNum(2)); -witness.push(new ScriptNum(5)); -witness.push('hello world'); +const witness = new Witness(); +witness.pushInt(2); +witness.pushInt(5); +witness.pushString('hello world'); -var stack = witness.toStack(); +const stack = witness.toStack(); output.execute(stack); ``` diff --git a/lib/coins/compress.js b/lib/coins/compress.js index 536844602..001649420 100644 --- a/lib/coins/compress.js +++ b/lib/coins/compress.js @@ -38,29 +38,29 @@ function compressScript(script, bw) { // P2PKH -> 0 | key-hash // Saves 5 bytes. - if (script.isPubkeyhash(true)) { - const hash = script.code[2].data; + const pkh = script.getPubkeyhash(true); + if (pkh) { bw.writeU8(0); - bw.writeBytes(hash); + bw.writeBytes(pkh); return bw; } // P2SH -> 1 | script-hash // Saves 3 bytes. - if (script.isScripthash()) { - const hash = script.code[1].data; + const sh = script.getScripthash(); + if (sh) { bw.writeU8(1); - bw.writeBytes(hash); + bw.writeBytes(sh); return bw; } // P2PK -> 2-5 | compressed-key // Only works if the key is valid. // Saves up to 35 bytes. - if (script.isPubkey(true)) { - const data = script.code[0].data; - if (publicKeyVerify(data)) { - const key = compressKey(data); + const pk = script.getPubkey(true); + if (pk) { + if (publicKeyVerify(pk)) { + const key = compressKey(pk); bw.writeBytes(key); return bw; } @@ -135,9 +135,9 @@ function sizeScript(script) { if (script.isScripthash()) return 21; - if (script.isPubkey(true)) { - const key = script.code[0].data; - if (publicKeyVerify(key)) + const pk = script.getPubkey(true); + if (pk) { + if (publicKeyVerify(pk)) return 33; } diff --git a/lib/http/rpc.js b/lib/http/rpc.js index ff1e5090f..3d3a4c6b5 100644 --- a/lib/http/rpc.js +++ b/lib/http/rpc.js @@ -1408,10 +1408,11 @@ RPC.prototype._createTemplate = async function _createTemplate(maxVersion, coinb // instead of a coinbasevalue. if (coinbase) { const tx = attempt.toCoinbase(); + const input = tx.inputs[0]; // Pop off the nonces. - tx.inputs[0].script.code.pop(); - tx.inputs[0].script.compile(); + input.script.pop(); + input.script.compile(); if (attempt.witness) { // We don't include the commitment @@ -1420,12 +1421,11 @@ RPC.prototype._createTemplate = async function _createTemplate(maxVersion, coinb assert(output.script.isCommitment()); // Also not including the witness nonce. - tx.inputs[0].witness.length = 0; - tx.inputs[0].witness.compile(); - - tx.refresh(); + input.witness.clear(); } + tx.refresh(); + json.coinbasetxn = { data: tx.toRaw().toString('hex'), txid: tx.txid(), @@ -2334,13 +2334,14 @@ RPC.prototype._addBlock = async function _addBlock(block) { // Fix eloipool bug (witness nonce is not present). if (state.hasWitness() && block.getCommitmentHash()) { const tx = block.txs[0]; + const input = tx.inputs[0]; if (!tx.hasWitness()) { this.logger.warning('Submitted block had no witness nonce.'); this.logger.debug(tx); // Recreate witness nonce (all zeroes). - tx.inputs[0].witness.set(0, encoding.ZERO_HASH); - tx.inputs[0].witness.compile(); + input.witness.push(encoding.ZERO_HASH); + input.witness.compile(); tx.refresh(); block.refresh(); diff --git a/lib/mining/template.js b/lib/mining/template.js index 744abd749..a1a3477bb 100644 --- a/lib/mining/template.js +++ b/lib/mining/template.js @@ -22,7 +22,6 @@ const policy = require('../protocol/policy'); const encoding = require('../utils/encoding'); const CoinView = require('../coins/coinview'); const Script = require('../script/script'); -const ScriptNum = require('../script/scriptnum'); const common = require('./common'); const DUMMY = Buffer.alloc(0); @@ -241,24 +240,23 @@ BlockTemplate.prototype.createCoinbase = function createCoinbase(hash) { const input = new Input(); // Height (required in v2+ blocks) - const height = ScriptNum.fromNumber(this.height); - input.script.push(height); + input.script.pushInt(this.height); // Coinbase flags. - input.script.push(encoding.ZERO_HASH160); + input.script.pushData(encoding.ZERO_HASH160); // Smaller nonce for good measure. - input.script.push(util.nonce(4)); + input.script.pushData(util.nonce(4)); // Extra nonce: incremented when // the nonce overflows. - input.script.push(encoding.ZERO_U64); + input.script.pushData(encoding.ZERO_U64); input.script.compile(); // Set up the witness nonce. if (this.witness) { - input.witness.set(0, encoding.ZERO_HASH); + input.witness.push(encoding.ZERO_HASH); input.witness.compile(); } @@ -281,7 +279,9 @@ BlockTemplate.prototype.createCoinbase = function createCoinbase(hash) { } // Padding for the CB height (constant size). - const padding = 5 - input.script.code[0].getSize(); + const op = input.script.get(0); + assert(op); + const padding = 5 - op.getSize(); assert(padding >= 0); // Reserved size. @@ -306,11 +306,10 @@ BlockTemplate.prototype.createCoinbase = function createCoinbase(hash) { } // Setup coinbase flags (variable size). - input.script.set(1, this.coinbaseFlags); + input.script.setData(1, this.coinbaseFlags); input.script.compile(); // Setup output script (variable size). - output.script.clear(); output.script.fromAddress(this.address); cb.refresh(); diff --git a/lib/primitives/address.js b/lib/primitives/address.js index 8ba984a27..8d0ea10c0 100644 --- a/lib/primitives/address.js +++ b/lib/primitives/address.js @@ -426,45 +426,39 @@ Address.fromBech32 = function fromBech32(data, network) { */ Address.prototype.fromScript = function fromScript(script) { - if (script.isPubkey()) { - this.hash = digest.hash160(script.get(0)); + const pk = script.getPubkey(); + + if (pk) { + this.hash = digest.hash160(pk); this.type = Address.types.PUBKEYHASH; this.version = -1; return this; } - if (script.isPubkeyhash()) { - this.hash = script.get(2); + const pkh = script.getPubkeyhash(); + + if (pkh) { + this.hash = pkh; this.type = Address.types.PUBKEYHASH; this.version = -1; return this; } - if (script.isScripthash()) { - this.hash = script.get(1); + const sh = script.getScripthash(); + + if (sh) { + this.hash = sh; this.type = Address.types.SCRIPTHASH; this.version = -1; return this; } - if (script.isWitnessPubkeyhash()) { - this.hash = script.get(1); - this.type = Address.types.WITNESS; - this.version = 0; - return this; - } - - if (script.isWitnessScripthash()) { - this.hash = script.get(1); - this.type = Address.types.WITNESS; - this.version = 0; - return this; - } + const program = script.getProgram(); - if (script.isWitnessMasthash()) { - this.hash = script.get(1); + if (program && !program.isMalformed()) { + this.hash = program.data; this.type = Address.types.WITNESS; - this.version = 1; + this.version = program.version; return this; } @@ -486,17 +480,21 @@ Address.prototype.fromScript = function fromScript(script) { */ Address.prototype.fromWitness = function fromWitness(witness) { + const [, pk] = witness.getPubkeyhashInput(); + // We're pretty much screwed here // since we can't get the version. - if (witness.isPubkeyhashInput()) { - this.hash = digest.hash160(witness.get(1)); + if (pk) { + this.hash = digest.hash160(pk); this.type = Address.types.WITNESS; this.version = 0; return this; } - if (witness.isScripthashInput()) { - this.hash = digest.sha256(witness.get(witness.length - 1)); + const redeem = witness.getScripthashInput(); + + if (redeem) { + this.hash = digest.sha256(redeem); this.type = Address.types.WITNESS; this.version = 0; return this; @@ -512,15 +510,19 @@ Address.prototype.fromWitness = function fromWitness(witness) { */ Address.prototype.fromInputScript = function fromInputScript(script) { - if (script.isPubkeyhashInput()) { - this.hash = digest.hash160(script.get(1)); + const [, pk] = script.getPubkeyhashInput(); + + if (pk) { + this.hash = digest.hash160(pk); this.type = Address.types.PUBKEYHASH; this.version = -1; return this; } - if (script.isScripthashInput()) { - this.hash = digest.hash160(script.get(script.length - 1)); + const redeem = script.getScripthashInput(); + + if (redeem) { + this.hash = digest.hash160(redeem); this.type = Address.types.SCRIPTHASH; this.version = -1; return this; diff --git a/lib/primitives/block.js b/lib/primitives/block.js index 09bda2e21..7e20bcb46 100644 --- a/lib/primitives/block.js +++ b/lib/primitives/block.js @@ -384,7 +384,7 @@ Block.prototype.getCommitmentHash = function getCommitmentHash(enc) { for (let i = coinbase.outputs.length - 1; i >= 0; i--) { const output = coinbase.outputs[i]; if (output.script.isCommitment()) { - hash = output.script.getCommitmentHash(); + hash = output.script.getCommitment(); break; } } diff --git a/lib/primitives/input.js b/lib/primitives/input.js index a4dc2b647..1a272533c 100644 --- a/lib/primitives/input.js +++ b/lib/primitives/input.js @@ -89,6 +89,28 @@ Input.prototype.clone = function clone() { return input; }; +/** + * Test equality against another input. + * @param {Input} input + * @returns {Boolean} + */ + +Input.prototype.equals = function equals(input) { + assert(Input.isInput(input)); + return this.prevout.equals(input.prevout); +}; + +/** + * Compare against another input (BIP69). + * @param {Input} input + * @returns {Number} + */ + +Input.prototype.compare = function compare(input) { + assert(Input.isInput(input)); + return this.prevout.compare(input.prevout); +}; + /** * Get the previous output script type as a string. * Will "guess" based on the input script and/or @@ -488,11 +510,7 @@ Input.fromTX = function fromTX(tx, index) { */ Input.isInput = function isInput(obj) { - return obj - && typeof obj.prevout === 'object' - && typeof obj.script === 'object' - && typeof obj.witness === 'object' - && typeof obj.getAddress === 'function'; + return obj instanceof Input; }; /* diff --git a/lib/primitives/mtx.js b/lib/primitives/mtx.js index 58711b1d3..286fb4432 100644 --- a/lib/primitives/mtx.js +++ b/lib/primitives/mtx.js @@ -21,7 +21,7 @@ const encoding = require('../utils/encoding'); const consensus = require('../protocol/consensus'); const policy = require('../protocol/policy'); const Amount = require('../btc/amount'); -const opcodes = Script.opcodes; +const Stack = require('../script/stack'); /** * A mutable transaction object. @@ -436,20 +436,22 @@ MTX.prototype.scriptInput = function scriptInput(index, coin, ring) { // Don't bother with any below calculation // if the output is already templated. - if (input.script.length !== 0 - || input.witness.length !== 0) { + if (input.script.raw.length !== 0 + || input.witness.items.length !== 0) { return true; } // Get the previous output's script - let prev = coin.script; + const prev = coin.script; // This is easily the hardest part about // building a transaction with segwit: // figuring out where the redeem script // and witness redeem scripts go. - if (prev.isScripthash()) { - const redeem = ring.getRedeem(prev.get(1)); + const sh = prev.getScripthash(); + + if (sh) { + const redeem = ring.getRedeem(sh); if (!redeem) return false; @@ -457,33 +459,37 @@ MTX.prototype.scriptInput = function scriptInput(index, coin, ring) { // Witness program nested in regular P2SH. if (redeem.isProgram()) { // P2WSH nested within pay-to-scripthash. - if (redeem.isWitnessScripthash()) { - prev = ring.getRedeem(redeem.get(1)); + const wsh = redeem.getWitnessScripthash(); + if (wsh) { + const wredeem = ring.getRedeem(wsh); - if (!prev) + if (!wredeem) return false; - if (!this.scriptVector(prev, input.witness, ring)) + const witness = this.scriptVector(wredeem, ring); + + if (!witness) return false; - input.witness.push(prev.toRaw()); - input.witness.compile(); + witness.push(wredeem.toRaw()); - input.script.push(redeem.toRaw()); - input.script.compile(); + input.witness.fromStack(witness); + input.script.fromItems([redeem.toRaw()]); return true; } // P2WPKH nested within pay-to-scripthash. - if (redeem.isWitnessPubkeyhash()) { - prev = Script.fromPubkeyhash(ring.getKeyHash()); + const wpkh = redeem.getWitnessPubkeyhash(); + if (wpkh) { + const pkh = Script.fromPubkeyhash(wpkh); + const witness = this.scriptVector(pkh, ring); - if (!this.scriptVector(prev, input.witness, ring)) + if (!witness) return false; - input.script.push(redeem.toRaw()); - input.script.compile(); + input.witness.fromStack(witness); + input.script.fromItems([redeem.toRaw()]); return true; } @@ -493,11 +499,14 @@ MTX.prototype.scriptInput = function scriptInput(index, coin, ring) { } // Regular P2SH. - if (!this.scriptVector(redeem, input.script, ring)) + const vector = this.scriptVector(redeem, ring); + + if (!vector) return false; - input.script.push(redeem.toRaw()); - input.script.compile(); + vector.push(redeem.toRaw()); + + input.script.fromStack(vector); return true; } @@ -505,29 +514,35 @@ MTX.prototype.scriptInput = function scriptInput(index, coin, ring) { // Witness program. if (prev.isProgram()) { // Bare P2WSH. - if (prev.isWitnessScripthash()) { - const redeem = ring.getRedeem(prev.get(1)); + const wsh = prev.getWitnessScripthash(); + if (wsh) { + const wredeem = ring.getRedeem(wsh); - if (!redeem) + if (!wredeem) return false; - if (!this.scriptVector(redeem, input.witness, ring)) + const vector = this.scriptVector(wredeem, ring); + + if (!vector) return false; - input.witness.push(redeem.toRaw()); - input.witness.compile(); + vector.push(wredeem.toRaw()); + + input.witness.fromStack(vector); return true; } // Bare P2WPKH. - if (prev.isWitnessPubkeyhash()) { - prev = Script.fromPubkeyhash(prev.get(1)); + const wpkh = prev.getWitnessPubkeyhash(); + if (wpkh) { + const pkh = Script.fromPubkeyhash(wpkh); + const vector = this.scriptVector(pkh, ring); - if (!this.scriptVector(prev, input.witness, ring)) + if (!vector) return false; - input.witness.compile(); + input.witness.fromStack(vector); return true; } @@ -537,61 +552,73 @@ MTX.prototype.scriptInput = function scriptInput(index, coin, ring) { } // Wow, a normal output! Praise be to Jengus and Gord. - return this.scriptVector(prev, input.script, ring); + const vector = this.scriptVector(prev, ring); + + if (!vector) + return false; + + input.script.fromStack(vector); + + return true; }; /** * Build script for a single vector * based on a previous script. * @param {Script} prev - * @param {Witness|Script} vector * @param {Buffer} ring * @return {Boolean} */ -MTX.prototype.scriptVector = function scriptVector(prev, vector, ring) { +MTX.prototype.scriptVector = function scriptVector(prev, ring) { // P2PK - if (prev.isPubkey()) { - if (!prev.get(0).equals(ring.publicKey)) - return false; + const pk = prev.getPubkey(); + if (pk) { + if (!pk.equals(ring.publicKey)) + return null; - vector.set(0, opcodes.OP_0); + const stack = new Stack(); - return true; + stack.pushInt(0); + + return stack; } // P2PKH - if (prev.isPubkeyhash()) { - if (!prev.get(2).equals(ring.getKeyHash())) - return false; + const pkh = prev.getPubkeyhash(); + if (pkh) { + if (!pkh.equals(ring.getKeyHash())) + return null; - vector.set(0, opcodes.OP_0); - vector.set(1, ring.publicKey); + const stack = new Stack(); - return true; + stack.pushInt(0); + stack.pushData(ring.publicKey); + + return stack; } // Multisig - if (prev.isMultisig()) { + const [, n] = prev.getMultisig(); + if (n !== -1) { if (prev.indexOf(ring.publicKey) === -1) - return false; + return null; // Technically we should create m signature slots, // but we create n signature slots so we can order // the signatures properly. - vector.set(0, opcodes.OP_0); + const stack = new Stack(); - // Grab `n` value (number of keys). - const n = prev.getSmall(prev.length - 2); + stack.pushInt(0); // Fill script with `n` signature slots. for (let i = 0; i < n; i++) - vector.set(i + 1, opcodes.OP_0); + stack.pushInt(0); - return true; + return stack; } - return false; + return null; }; /** @@ -605,11 +632,11 @@ MTX.prototype.scriptVector = function scriptVector(prev, vector, ring) { * @returns {Promise} */ -MTX.prototype.signInputAsync = function signInputAsync(index, coin, ring, type, pool) { +MTX.prototype.signInputAsync = async function signInputAsync(index, coin, ring, type, pool) { if (!pool) return this.signInput(index, coin, ring, type); - return pool.signInput(this, index, coin, ring, type, pool); + return await pool.signInput(this, index, coin, ring, type, pool); }; /** @@ -656,32 +683,51 @@ MTX.prototype.signInput = function signInput(index, coin, ring, type) { vector = input.witness; redeem = true; version = 1; - } else if (prev.isWitnessPubkeyhash()) { - prev = Script.fromPubkeyhash(prev.get(1)); - vector = input.witness; - redeem = false; - version = 1; + } else { + const wpkh = prev.getWitnessPubkeyhash(); + if (wpkh) { + prev = Script.fromPubkeyhash(wpkh); + vector = input.witness; + redeem = false; + version = 1; + } } // Create our signature. const sig = this.signature(index, prev, value, key, type, version); if (redeem) { - const redeem = vector.pop(); - const result = this.signVector(prev, vector, sig, ring); - vector.push(redeem); - vector.compile(); - return result; + const stack = vector.toStack(); + const redeem = stack.pop(); + + const result = this.signVector(prev, stack, sig, ring); + + if (!result) + return false; + + result.push(redeem); + + vector.fromStack(result); + + return true; } - return this.signVector(prev, vector, sig, ring); + const stack = vector.toStack(); + const result = this.signVector(prev, stack, sig, ring); + + if (!result) + return false; + + vector.fromStack(result); + + return true; }; /** * Add a signature to a vector * based on a previous script. * @param {Script} prev - * @param {Witness|Script} vector + * @param {Stack} vector * @param {Buffer} sig * @param {KeyRing} ring * @return {Boolean} @@ -689,79 +735,83 @@ MTX.prototype.signInput = function signInput(index, coin, ring, type) { MTX.prototype.signVector = function signVector(prev, vector, sig, ring) { // P2PK - if (prev.isPubkey()) { + const pk = prev.getPubkey(); + if (pk) { // Make sure the pubkey is ours. - if (!ring.publicKey.equals(prev.get(0))) - return false; + if (!ring.publicKey.equals(pk)) + return null; - // Already signed. - if (Script.isSignature(vector.get(0))) - return true; - - if (vector.getSmall(0) !== 0) + if (vector.length === 0) throw new Error('Input has not been templated.'); + // Already signed. + if (vector.get(0).length > 0) + return vector; + vector.set(0, sig); - vector.compile(); - return true; + return vector; } // P2PKH - if (prev.isPubkeyhash()) { + const pkh = prev.getPubkeyhash(); + if (pkh) { // Make sure the pubkey hash is ours. - if (!ring.getKeyHash().equals(prev.get(2))) - return false; + if (!ring.getKeyHash().equals(pkh)) + return null; - // Already signed. - if (Script.isSignature(vector.get(0))) - return true; + if (vector.length !== 2) + throw new Error('Input has not been templated.'); - if (!Script.isKey(vector.get(1))) + if (vector.get(1).length === 0) throw new Error('Input has not been templated.'); + // Already signed. + if (vector.get(0).length > 0) + return vector; + vector.set(0, sig); - vector.compile(); - return true; + return vector; } // Multisig - if (prev.isMultisig()) { - if (vector.getSmall(0) !== 0) + const [m, n] = prev.getMultisig(); + if (m !== -1) { + if (vector.length < 2) throw new Error('Input has not been templated.'); - // Grab `n` value (number of keys). - const n = prev.getSmall(prev.length - 2); + if (vector.get(0).length !== 0) + throw new Error('Input has not been templated.'); // Too many signature slots. Abort. if (vector.length - 1 > n) - return false; + throw new Error('Input has not been templated.'); // Count the number of current signatures. let total = 0; for (let i = 1; i < vector.length; i++) { - if (Script.isSignature(vector.get(i))) + const item = vector.get(i); + if (item.length > 0) total++; } - // Grab `m` value (number of sigs required). - const m = prev.getSmall(0); - // Signatures are already finalized. if (total === m && vector.length - 1 === m) - return true; + return vector; // Add some signature slots for us to use if // there was for some reason not enough. while (vector.length - 1 < n) - vector.push(opcodes.OP_0); + vector.pushInt(0); // Grab the redeem script's keys to figure // out where our key should go. const keys = []; - for (let i = 1; i < prev.length - 2; i++) - keys.push(prev.get(i)); + for (const op of prev.code) { + if (op.data) + keys.push(op.data); + } // Find the key index so we can place // the signature in the same index. @@ -771,7 +821,7 @@ MTX.prototype.signVector = function signVector(prev, vector, sig, ring) { // script. We tried to sign a transaction // that is not redeemable by us. if (keyIndex === -1) - return false; + return null; // Offset key index by one to turn it into // "sig index". Accounts for OP_0 byte at @@ -782,7 +832,7 @@ MTX.prototype.signVector = function signVector(prev, vector, sig, ring) { // and increment the total number of // signatures. if (keyIndex < vector.length && total < m) { - if (vector.getSmall(keyIndex) === 0) { + if (vector.get(keyIndex).length === 0) { vector.set(keyIndex, sig); total++; } @@ -792,7 +842,8 @@ MTX.prototype.signVector = function signVector(prev, vector, sig, ring) { if (total >= m) { // Remove empty slots left over. for (let i = vector.length - 1; i >= 1; i--) { - if (vector.getSmall(i) === 0) + const item = vector.get(i); + if (item.length === 0) vector.remove(i); } @@ -808,15 +859,10 @@ MTX.prototype.signVector = function signVector(prev, vector, sig, ring) { assert(vector.length - 1 === m); } - vector.compile(); - - if (total !== m) - return false; - - return true; + return vector; } - return false; + return null; }; /** @@ -873,46 +919,57 @@ MTX.prototype.isInputSigned = function isInputSigned(index, coin) { return false; vector = input.witness; redeem = true; - } else if (prev.isWitnessPubkeyhash()) { - prev = Script.fromPubkeyhash(prev.get(1)); - vector = input.witness; - redeem = false; + } else { + const wpkh = prev.getWitnessPubkeyhash(); + if (wpkh) { + prev = Script.fromPubkeyhash(wpkh); + vector = input.witness; + redeem = false; + } } - if (redeem) { - const redeem = vector.pop(); - const result = this.isVectorSigned(prev, vector); - vector.push(redeem); - return result; - } + const stack = vector.toStack(); + + if (redeem) + stack.pop(); - return this.isVectorSigned(prev, vector); + return this.isVectorSigned(prev, stack); }; /** * Test whether a vector is fully-signed. * @param {Script} prev - * @param {Script|Witness} vector + * @param {Stack} vector * @returns {Boolean} */ MTX.prototype.isVectorSigned = function isVectorSigned(prev, vector) { if (prev.isPubkey()) { - if (!Script.isSignature(vector.get(0))) + if (vector.length !== 1) + return false; + + if (vector.get(0).length === 0) return false; + return true; } if (prev.isPubkeyhash()) { - if (!Script.isSignature(vector.get(0))) + if (vector.length !== 2) + return false; + + if (vector.get(0).length === 0) return false; + + if (vector.get(1).length === 0) + return false; + return true; } - if (prev.isMultisig()) { - // Grab `m` value (number of required sigs). - const m = prev.getSmall(0); + const [m] = prev.getMultisig(); + if (m !== -1) { // Ensure we have the correct number // of required signatures. if (vector.length - 1 !== m) @@ -920,7 +977,8 @@ MTX.prototype.isVectorSigned = function isVectorSigned(prev, vector) { // Ensure all members are signatures. for (let i = 1; i < vector.length; i++) { - if (!Script.isSignature(vector.get(i))) + const item = vector.get(i); + if (item.length === 0) return false; } @@ -1020,11 +1078,11 @@ MTX.prototype.sign = function sign(ring, type) { * @returns {Promise} */ -MTX.prototype.signAsync = function signAsync(ring, type, pool) { +MTX.prototype.signAsync = async function signAsync(ring, type, pool) { if (!pool) return this.sign(ring, type); - return pool.sign(this, ring, type); + return await pool.sign(this, ring, type); }; /** @@ -1035,6 +1093,7 @@ MTX.prototype.signAsync = function signAsync(ring, type, pool) { MTX.prototype.estimateSize = async function estimateSize(estimate) { const scale = consensus.WITNESS_SCALE_FACTOR; + let total = 0; // Calculate the size, minus the input scripts. @@ -1083,13 +1142,14 @@ MTX.prototype.estimateSize = async function estimateSize(estimate) { continue; } - if (prev.isMultisig()) { + const [m] = prev.getMultisig(); + if (m !== -1) { let size = 0; // Bare Multisig // OP_0 size += 1; // OP_PUSHDATA0 [signature] ... - size += (1 + 73) * prev.getSmall(0); + size += (1 + 73) * m; // varint len size += encoding.sizeVarint(size); total += size; @@ -1427,10 +1487,7 @@ MTX.fromTX = function fromTX(tx) { */ MTX.isMTX = function isMTX(obj) { - return obj - && Array.isArray(obj.inputs) - && typeof obj.locktime === 'number' - && typeof obj.scriptInput === 'function'; + return obj instanceof MTX; }; /** @@ -1849,23 +1906,11 @@ function sortValue(a, b) { } function sortInputs(a, b) { - const ahash = a.prevout.txid(); - const bhash = b.prevout.txid(); - const cmp = util.strcmp(ahash, bhash); - - if (cmp !== 0) - return cmp; - - return a.prevout.index - b.prevout.index; + return a.compare(b); } function sortOutputs(a, b) { - const cmp = a.value - b.value; - - if (cmp !== 0) - return cmp; - - return a.script.raw.compare(b.script.raw); + return a.compare(b); } /* diff --git a/lib/primitives/outpoint.js b/lib/primitives/outpoint.js index 24720a9a6..d8cfd2a4e 100644 --- a/lib/primitives/outpoint.js +++ b/lib/primitives/outpoint.js @@ -6,8 +6,8 @@ 'use strict'; -const util = require('../utils/util'); const assert = require('assert'); +const util = require('../utils/util'); const StaticWriter = require('../utils/writer'); const BufferReader = require('../utils/reader'); const encoding = require('../utils/encoding'); @@ -62,6 +62,47 @@ Outpoint.fromOptions = function fromOptions(options) { return new Outpoint().fromOptions(options); }; +/** + * Clone the outpoint. + * @returns {Outpoint} + */ + +Outpoint.prototype.clone = function clone() { + const outpoint = new Outpoint(); + outpoint.hash = this.value; + outpoint.index = this.index; + return outpoint; +}; + +/** + * Test equality against another outpoint. + * @param {Outpoint} prevout + * @returns {Boolean} + */ + +Outpoint.prototype.equals = function equals(prevout) { + assert(Outpoint.isOutpoint(prevout)); + return this.hash === prevout.hash + && this.index === prevout.index; +}; + +/** + * Compare against another outpoint (BIP69). + * @param {Outpoint} prevout + * @returns {Number} + */ + +Outpoint.prototype.compare = function compare(prevout) { + assert(Outpoint.isOutpoint(prevout)); + + const cmp = util.strcmp(this.txid(), prevout.txid()); + + if (cmp !== 0) + return cmp; + + return this.index - prevout.index; +}; + /** * Test whether the outpoint is null (hash of zeroes * with max-u32 index). Used to detect coinbases. @@ -293,10 +334,7 @@ Outpoint.prototype.inspect = function inspect() { */ Outpoint.isOutpoint = function isOutpoint(obj) { - return obj - && typeof obj.hash === 'string' - && typeof obj.index === 'number' - && typeof obj.toKey === 'function'; + return obj instanceof Outpoint; }; /* diff --git a/lib/primitives/output.js b/lib/primitives/output.js index f5d11fe3d..4d21f035a 100644 --- a/lib/primitives/output.js +++ b/lib/primitives/output.js @@ -118,6 +118,35 @@ Output.prototype.clone = function clone() { return output; }; +/** + * Test equality against another output. + * @param {Output} output + * @returns {Boolean} + */ + +Output.prototype.equals = function equals(output) { + assert(Output.isOutput(output)); + return this.value === output.value + && this.script.equals(output.script); +}; + +/** + * Compare against another output (BIP69). + * @param {Output} output + * @returns {Number} + */ + +Output.prototype.compare = function compare(output) { + assert(Output.isOutput(output)); + + const cmp = this.value - output.value; + + if (cmp !== 0) + return cmp; + + return this.script.compare(output.script); +}; + /** * Get the script type as a string. * @returns {ScriptType} type @@ -339,10 +368,7 @@ Output.fromRaw = function fromRaw(data, enc) { */ Output.isOutput = function isOutput(obj) { - return obj - && typeof obj.value === 'number' - && typeof obj.script === 'object' - && typeof obj.getAddress === 'function'; + return obj instanceof Output; }; /* diff --git a/lib/primitives/tx.js b/lib/primitives/tx.js index fc3bce78b..c0841175c 100644 --- a/lib/primitives/tx.js +++ b/lib/primitives/tx.js @@ -24,7 +24,7 @@ const InvItem = require('./invitem'); const Bloom = require('../utils/bloom'); const consensus = require('../protocol/consensus'); const policy = require('../protocol/policy'); -const {ScriptError} = require('../script/common'); +const ScriptError = require('../script/scripterror'); const hashType = Script.hashType; /** @@ -60,6 +60,7 @@ function TX(options) { this._raw = null; this._size = -1; this._witness = -1; + this._sigops = -1; this._hashPrevouts = null; this._hashSequence = null; @@ -156,6 +157,7 @@ TX.prototype.refresh = function refresh() { this._raw = null; this._size = -1; this._witness = -1; + this._sigops = -1; this._hashPrevouts = null; this._hashSequence = null; @@ -1295,6 +1297,9 @@ TX.prototype.verifySequence = function verifySequence(index, locktime) { */ TX.prototype.getLegacySigops = function getLegacySigops() { + if (this._sigops !== -1) + return this._sigops; + let total = 0; for (const input of this.inputs) @@ -1303,6 +1308,9 @@ TX.prototype.getLegacySigops = function getLegacySigops() { for (const output of this.outputs) total += output.script.getSigops(false); + if (!this.mutable) + this._sigops = total; + return total; }; @@ -1313,11 +1321,11 @@ TX.prototype.getLegacySigops = function getLegacySigops() { */ TX.prototype.getScripthashSigops = function getScripthashSigops(view) { - let total = 0; - if (this.isCoinbase()) return 0; + let total = 0; + for (const input of this.inputs) { const coin = view.getOutputFor(input); @@ -1333,6 +1341,30 @@ TX.prototype.getScripthashSigops = function getScripthashSigops(view) { return total; }; +/** + * Calculate accurate sigop count, taking into account redeem scripts. + * @param {CoinView} view + * @returns {Number} sigop count + */ + +TX.prototype.getWitnessSigops = function getWitnessSigops(view) { + if (this.isCoinbase()) + return 0; + + let total = 0; + + for (const input of this.inputs) { + const coin = view.getOutputFor(input); + + if (!coin) + continue; + + total += coin.script.getWitnessSigops(input.script, input.witness); + } + + return total; +}; + /** * Calculate sigops cost, taking into account witness programs. * @param {CoinView} view @@ -1341,29 +1373,18 @@ TX.prototype.getScripthashSigops = function getScripthashSigops(view) { */ TX.prototype.getSigopsCost = function getSigopsCost(view, flags) { - const scale = consensus.WITNESS_SCALE_FACTOR; - let cost = this.getLegacySigops() * scale; - if (flags == null) flags = Script.flags.STANDARD_VERIFY_FLAGS; - if (this.isCoinbase()) - return cost; + const scale = consensus.WITNESS_SCALE_FACTOR; + + let cost = this.getLegacySigops() * scale; if (flags & Script.flags.VERIFY_P2SH) cost += this.getScripthashSigops(view) * scale; - if (!(flags & Script.flags.VERIFY_WITNESS)) - return cost; - - for (const input of this.inputs) { - const coin = view.getOutputFor(input); - - if (!coin) - continue; - - cost += coin.script.getWitnessSigops(input.script, input.witness); - } + if (flags & Script.flags.VERIFY_WITNESS) + cost += this.getWitnessSigops(view); return cost; }; @@ -1627,7 +1648,7 @@ TX.prototype.hasStandardWitness = function hasStandardWitness(view) { } if (prev.isPubkeyhash()) { - if (input.witness.length - 1 !== 2) + if (input.witness.items.length - 1 !== 2) return false; if (witness.items[0].length > 73) @@ -2513,10 +2534,7 @@ TX.isWitness = function isWitness(br) { */ TX.isTX = function isTX(obj) { - return obj - && Array.isArray(obj.inputs) - && typeof obj.locktime === 'number' - && typeof obj.witnessHash === 'function'; + return obj instanceof TX; }; /* diff --git a/lib/script/common.js b/lib/script/common.js index 7fa84a4ad..b0722dc89 100644 --- a/lib/script/common.js +++ b/lib/script/common.js @@ -14,6 +14,7 @@ const assert = require('assert'); const util = require('../utils/util'); const secp256k1 = require('../crypto/secp256k1'); +const ScriptNum = require('./scriptnum'); /** * Script opcodes. @@ -163,6 +164,32 @@ exports.opcodes = { exports.opcodesByVal = util.reverse(exports.opcodes); +/** + * Small ints (1 indexed, 1==0). + * @const {Buffer[]} + */ + +exports.small = [ + Buffer.from([0x81]), + Buffer.from([]), + Buffer.from([0x01]), + Buffer.from([0x02]), + Buffer.from([0x03]), + Buffer.from([0x04]), + Buffer.from([0x05]), + Buffer.from([0x06]), + Buffer.from([0x07]), + Buffer.from([0x08]), + Buffer.from([0x09]), + Buffer.from([0x0a]), + Buffer.from([0x0b]), + Buffer.from([0x0c]), + Buffer.from([0x0d]), + Buffer.from([0x0e]), + Buffer.from([0x0f]), + Buffer.from([0x10]) +]; + /** * Script and locktime flags. See {@link VerifyFlags}. * @enum {Number} @@ -293,27 +320,6 @@ exports.types = { exports.typesByVal = util.reverse(exports.types); -/** - * False stack return value. - * @const {Buffer} - */ - -exports.STACK_FALSE = Buffer.from([]); - -/** - * True stack return value. - * @const {Buffer} - */ - -exports.STACK_TRUE = Buffer.from([0x01]); - -/** - * -1 stack return value. - * @const {Buffer} - */ - -exports.STACK_NEGATE = Buffer.from([0x81]); - /** * Test a signature to see whether it contains a valid sighash type. * @param {Buffer} sig @@ -347,56 +353,6 @@ 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 ripemd160 hash. - * @param {Buffer?} hash - * @returns {Boolean} - */ - -exports.isHash = function isHash(hash) { - return Buffer.isBuffer(hash) && hash.length === 20; -}; - -/** - * Test whether the data element is a public key. Note that - * this does not verify the format of the key, only the length. - * @param {Buffer?} key - * @returns {Boolean} - */ - -exports.isKey = function isKey(key) { - return Buffer.isBuffer(key) && key.length >= 33 && key.length <= 65; -}; - -/** - * Test whether the data element is a signature. Note that - * this does not verify the format of the signature, only the length. - * @param {Buffer?} sig - * @returns {Boolean} - */ - -exports.isSignature = function isSignature(sig) { - return Buffer.isBuffer(sig) && sig.length >= 9 && sig.length <= 73; -}; - /** * Test whether the data element is a valid key. * @param {Buffer} key @@ -532,224 +488,31 @@ exports.isSignatureEncoding = function isSignatureEncoding(sig) { }; /** - * Cast a big number or Buffer to a bool. - * @see CastToBool - * @param {Buffer} value - * @returns {Boolean} - */ - -exports.toBool = function toBool(value) { - assert(Buffer.isBuffer(value)); - - for (let i = 0; i < value.length; i++) { - if (value[i] !== 0) { - // Cannot be negative zero - if (i === value.length - 1 && value[i] === 0x80) - return false; - return true; - } - } - - return false; -}; - -/** - * Format script code into a human readable-string. - * @param {Array} code - * @returns {String} Human-readable string. - */ - -exports.formatStack = function formatStack(items) { - const out = []; - - for (const item of items) - out.push(item.toString('hex')); - - return out.join(' '); -}; - -/** - * Format script code into a human readable-string. - * @param {Array} code - * @returns {String} Human-readable string. - */ - -exports.formatCode = function formatCode(code) { - const out = []; - - for (const op of code) { - // Bad push - if (op.value === -1) { - out.push('OP_INVALIDOPCODE'); - break; - } - - if (op.data) { - const symbol = exports.opcodesByVal[op.value]; - - // Direct push - if (!symbol) { - let size = op.value.toString(16); - if (size.length < 2) - size = '0' + size; - out.push(`0x${size} 0x${op.data.toString('hex')}`); - continue; - } - - // Pushdatas - let size = op.data.length.toString(16); - - while (size.length % 2 !== 0) - size = '0' + size; - - out.push(`${symbol} 0x${size} 0x${op.data.toString('hex')}`); - - continue; - } - - // Opcodes - let symbol = exports.opcodesByVal[op.value]; - if (symbol) { - out.push(symbol); - continue; - } - - // Unknown opcodes - symbol = op.value.toString(16); - - if (symbol.length < 2) - symbol = '0' + symbol; - - out.push(`0x${symbol}`); - } - - return out.join(' '); -}; - -/** - * Format script code into bitcoind asm format. - * @param {Array} code + * Format stack item into bitcoind asm format. + * @param {Buffer} item * @param {Boolean?} decode - Attempt to decode hash types. * @returns {String} Human-readable string. */ -exports.formatItem = function formatItem(data, decode) { - if (data.length <= 4) { - data = exports.num(data, exports.flags.VERIFY_NONE); - return data.toString(10); +exports.toASM = function toASM(item, decode) { + if (item.length <= 4) { + const num = ScriptNum.decode(item, false, 4); + return num.toString(10); } - if (decode) { - let symbol = ''; - if (exports.isSignatureEncoding(data)) { - const type = data[data.length - 1]; + if (decode && exports.isSignatureEncoding(item)) { + const type = item[item.length - 1]; - symbol = exports.hashTypeByVal[type & 0x1f] || ''; + let symbol = exports.hashTypeByVal[type & 0x1f] || ''; - if (symbol) { - if (type & exports.hashType.ANYONECANPAY) - symbol += '|ANYONECANPAY'; - symbol = `[${symbol}]`; - } - - data = data.slice(0, -1); - } - return data.toString('hex') + symbol; - } - - return data.toString('hex'); -}; - -/** - * Format script code into bitcoind asm format. - * @param {Array} code - * @param {Boolean?} decode - Attempt to decode hash types. - * @returns {String} Human-readable string. - */ - -exports.formatASM = function formatASM(code, decode) { - if (code.length > 0) { - if (code[0].value === exports.opcodes.OP_RETURN) - decode = false; - } - - const out = []; - - for (const op of code) { - if (op.value === -1) { - out.push('[error]'); - break; - } - - if (op.data) { - const data = exports.formatItem(op.data, decode); - out.push(data); - continue; + if (symbol) { + if (type & exports.hashType.ANYONECANPAY) + symbol += '|ANYONECANPAY'; + symbol = `[${symbol}]`; } - const symbol = exports.opcodesByVal[op.value] || 'OP_UNKNOWN'; - - out.push(symbol); - } - - return out.join(' '); -}; - -/** - * Format script code into bitcoind asm format. - * @param {Array} code - * @param {Boolean?} decode - Attempt to decode hash types. - * @returns {String} Human-readable string. - */ - -exports.formatStackASM = function formatStackASM(items, decode) { - const out = []; - - for (const item of items) { - const data = exports.formatItem(item, decode); - out.push(data); + return item.slice(0, -1).toString('hex') + symbol; } - return out.join(' '); + return item.toString('hex'); }; - -/** - * An error thrown from the scripting system, - * potentially pertaining to Script execution. - * @alias module:script.ScriptError - * @constructor - * @extends Error - * @param {String} code - Error code. - * @param {Opcode} op - Opcode. - * @param {Number?} ip - Instruction pointer. - * @property {String} message - Error message. - * @property {String} code - Original code passed in. - * @property {Number} op - Opcode. - * @property {Number} ip - Instruction pointer. - */ - -exports.ScriptError = function ScriptError(code, op, ip) { - if (!(this instanceof ScriptError)) - return new ScriptError(code, op, ip); - - Error.call(this); - - this.type = 'ScriptError'; - this.code = code; - this.message = code; - this.op = -1; - this.ip = -1; - - if (typeof op === 'string') { - this.message = op; - } else if (op) { - this.message = `${code} (op=${op.toSymbol()}, ip=${ip})`; - this.op = op.value; - this.ip = ip; - } - - if (Error.captureStackTrace) - Error.captureStackTrace(this, ScriptError); -}; - -Object.setPrototypeOf(exports.ScriptError.prototype, Error.prototype); diff --git a/lib/script/index.js b/lib/script/index.js index 351b9d836..c3d9386b5 100644 --- a/lib/script/index.js +++ b/lib/script/index.js @@ -14,6 +14,7 @@ exports.common = require('./common'); exports.Opcode = require('./opcode'); exports.Program = require('./program'); exports.Script = require('./script'); +exports.ScriptError = require('./scripterror'); exports.ScriptNum = require('./scriptnum'); exports.sigcache = require('./sigcache'); exports.Stack = require('./stack'); diff --git a/lib/script/opcode.js b/lib/script/opcode.js index dcf76b888..246dd980b 100644 --- a/lib/script/opcode.js +++ b/lib/script/opcode.js @@ -101,6 +101,194 @@ Opcode.prototype.isBranch = function isBranch() { return this.value >= opcodes.OP_IF && this.value <= opcodes.OP_ENDIF; }; +/** + * Test opcode equality. + * @param {Opcode} op + * @returns {Boolean} + */ + +Opcode.prototype.equals = function equals(op) { + assert(Opcode.isOpcode(op)); + + if (this.value !== op.value) + return false; + + if (!this.data) { + assert(!op.data); + return true; + } + + assert(op.data); + + return this.data.equals(op.data); +}; + +/** + * Convert Opcode to opcode value. + * @returns {Number} + */ + +Opcode.prototype.toOp = function toOp() { + return this.value; +}; + +/** + * Covert opcode to data push. + * @returns {Buffer|null} + */ + +Opcode.prototype.toData = function toData() { + return this.data; +}; + +/** + * Covert opcode to data length. + * @returns {Number} + */ + +Opcode.prototype.toLength = function toLength() { + return this.data ? this.data.length : -1; +}; + +/** + * Covert and _cast_ opcode to data push. + * @returns {Buffer|null} + */ + +Opcode.prototype.toPush = function toPush() { + if (this.data) + return this.data; + + if (this.value === opcodes.OP_1NEGATE) + return common.small[-1 + 1]; + + if (this.value === opcodes.OP_0) + return common.small[0 + 1]; + + if (this.value >= opcodes.OP_1 && this.value <= opcodes.OP_16) + return common.small[this.value - 0x50 + 1]; + + return null; +}; + +/** + * Get string for opcode. + * @param {String?} enc + * @returns {Buffer|null} + */ + +Opcode.prototype.toString = function toString(enc) { + const data = this.toPush(); + + if (!data) + return null; + + return data.toString(enc || 'utf8'); +}; + +/** + * Convert opcode to small integer. + * @returns {Number} + */ + +Opcode.prototype.toSmall = function toSmall() { + if (this.value === opcodes.OP_0) + return 0; + + if (this.value >= opcodes.OP_1 && this.value <= opcodes.OP_16) + return this.value - 0x50; + + return -1; +}; + +/** + * Convert opcode to script number. + * @returns {ScriptNum|null} + */ + +Opcode.prototype.toNum = function toNum(minimal, limit) { + const smi = this.toSmall(); + + if (smi !== -1) + return ScriptNum.fromInt(smi); + + if (!this.data) + return null; + + return ScriptNum.decode(this.data, minimal, limit); +}; + +/** + * Convert opcode to integer. + * @returns {Number} + */ + +Opcode.prototype.toInt = function toInt() { + const num = this.toNum(); + + if (!num) + return -1; + + return num.getInt(); +}; + +/** + * Convert opcode to boolean. + * @returns {Boolean} + */ + +Opcode.prototype.toBool = function toBool() { + const smi = this.toSmall(); + + if (smi === -1) + return false; + + return smi === 1; +}; + +/** + * Convert opcode to its symbolic representation. + * @returns {String} + */ + +Opcode.prototype.toSymbol = function toSymbol() { + let op = this.value; + + if (op === -1) + op = 0xff; + + let symbol = common.opcodesByVal[op]; + + if (symbol == null) + symbol = `0x${util.hex8(op)}`; + + return symbol; +}; + +/** + * Calculate opcode size. + * @returns {Number} + */ + +Opcode.prototype.getSize = function getSize() { + if (!this.data) + return 1; + + if (this.value <= 0x4b) + return 1 + this.data.length; + + switch (this.value) { + case opcodes.OP_PUSHDATA1: + return 2 + this.data.length; + case opcodes.OP_PUSHDATA2: + return 3 + this.data.length; + case opcodes.OP_PUSHDATA4: + return 5 + this.data.length; + default: + throw new Error('Unknown pushdata opcode.'); + } +}; + /** * Encode the opcode to a buffer writer. * @param {BufferWriter} bw @@ -156,119 +344,59 @@ Opcode.prototype.toRaw = function toRaw() { }; /** - * Calculate opcode size. - * @returns {Number} + * Convert the opcode to a bitcoind test string. + * @returns {String} Human-readable script code. */ -Opcode.prototype.getSize = function getSize() { - if (!this.data) - return 1; - - if (this.value <= 0x4b) - return 1 + this.data.length; - - switch (this.value) { - case opcodes.OP_PUSHDATA1: - return 2 + this.data.length; - case opcodes.OP_PUSHDATA2: - return 3 + this.data.length; - case opcodes.OP_PUSHDATA4: - return 5 + this.data.length; - default: - throw new Error('Unknown pushdata opcode.'); - } -}; +Opcode.prototype.toFormat = function toFormat() { + // Bad push + if (this.value === -1) + return 'OP_INVALIDOPCODE'; -/** - * Instantiate opcode from buffer reader. - * @param {BufferReader} br - * @returns {Opcode} - */ + if (this.data) { + const symbol = common.opcodesByVal[this.value]; + const data = this.data.toString('hex'); -Opcode.fromReader = function fromReader(br) { - const value = br.readU8(); - - const cached = opCache[value]; + // Direct push + if (!symbol) { + const size = util.hex8(this.value); + return `0x${size} 0x${data}`; + } - if (cached) - return cached; + // Pushdatas + let size = this.data.length.toString(16); - const op = new Opcode(value, null); + while (size.length % 2 !== 0) + size = '0' + size; - if (value >= 0x01 && value <= 0x4b) { - if (br.left() < value) { - op.value = -1; - br.seek(br.left()); - return op; - } - op.value = value; - op.data = br.readBytes(value); - return op; + return `${symbol} 0x${size} 0x${data}`; } - let size; + // Opcodes + const symbol = common.opcodesByVal[this.value]; + if (symbol) + return symbol; - switch (value) { - case opcodes.OP_PUSHDATA1: - if (br.left() < 1) { - op.value = -1; - break; - } - size = br.readU8(); - if (br.left() < size) { - op.value = -1; - br.seek(br.left()); - break; - } - op.value = value; - op.data = br.readBytes(size); - break; - case opcodes.OP_PUSHDATA2: - if (br.left() < 2) { - op.value = -1; - br.seek(br.left()); - break; - } - size = br.readU16(); - if (br.left() < size) { - op.value = -1; - br.seek(br.left()); - break; - } - op.value = value; - op.data = br.readBytes(size); - break; - case opcodes.OP_PUSHDATA4: - if (br.left() < 4) { - op.value = -1; - br.seek(br.left()); - break; - } - size = br.readU32(); - if (br.left() < size) { - op.value = -1; - br.seek(br.left()); - break; - } - op.value = value; - op.data = br.readBytes(size); - break; - default: - op.value = value; - break; - } + // Unknown opcodes + const value = util.hex8(this.value); - return op; + return `0x${value}`; }; /** - * Instantiate opcode from serialized data. - * @param {Buffer} data - * @returns {Opcode} + * Format the opcode as bitcoind asm. + * @param {Boolean?} decode - Attempt to decode hash types. + * @returns {String} Human-readable script. */ -Opcode.fromRaw = function fromRaw(data) { - return Opcode.fromReader(new BufferReader(data)); +Opcode.prototype.toASM = function toASM(decode) { + if (this.value === -1) + return '[error]'; + + if (this.data) + return common.toASM(this.data, decode); + + return common.opcodesByVal[this.value] || 'OP_UNKNOWN'; }; /** @@ -278,6 +406,8 @@ Opcode.fromRaw = function fromRaw(data) { */ Opcode.fromOp = function fromOp(op) { + assert(typeof op === 'number'); + const cached = opCache[op]; if (cached) @@ -294,6 +424,8 @@ Opcode.fromOp = function fromOp(op) { */ Opcode.fromData = function fromData(data) { + assert(Buffer.isBuffer(data)); + if (data.length === 0) return Opcode.fromOp(opcodes.OP_0); @@ -317,6 +449,8 @@ Opcode.fromData = function fromData(data) { */ Opcode.fromPush = function fromPush(data) { + assert(Buffer.isBuffer(data)); + if (data.length <= 0x4b) return new Opcode(data.length, data); @@ -333,24 +467,16 @@ Opcode.fromPush = function fromPush(data) { }; /** - * Instantiate an opcode from a Number. - * @param {Number|ScriptNum|BN} num - * @returns {Opcode} - */ - -Opcode.fromNumber = function fromNumber(num) { - return Opcode.fromData(ScriptNum.encode(num)); -}; - -/** - * Instantiate an opcode from a Number. - * @param {Boolean} value + * Instantiate a pushdata opcode from a string. + * @param {String} str + * @param {String} [enc=utf8] * @returns {Opcode} */ -Opcode.fromBool = function fromBool(value) { - assert(typeof value === 'boolean'); - return Opcode.fromSmall(value ? 1 : 0); +Opcode.fromString = function fromString(str, enc) { + assert(typeof str === 'string'); + const data = Buffer.from(str, enc || 'utf8'); + return Opcode.fromData(data); }; /** @@ -365,44 +491,46 @@ Opcode.fromSmall = function fromSmall(num) { }; /** - * Instantiate a pushdata opcode from a string. - * @param {String} data + * Instantiate an opcode from a ScriptNum. + * @param {ScriptNumber} num * @returns {Opcode} */ -Opcode.fromString = function fromString(data, enc) { - if (typeof data === 'string') - data = Buffer.from(data, enc); - - return Opcode.fromData(data); +Opcode.fromNum = function fromNum(num) { + assert(ScriptNum.isScriptNum(num)); + return Opcode.fromData(num.encode()); }; /** - * Instantiate a pushdata opcode from anything. - * @param {String|Buffer|Number|ScriptNum|Opcode} data + * Instantiate an opcode from a Number. + * @param {Number} num * @returns {Opcode} */ -Opcode.from = function from(data) { - if (data instanceof Opcode) - return data; +Opcode.fromInt = function fromInt(num) { + assert(util.isInt(num)); - if (typeof data === 'number') - return Opcode.fromOp(data); + if (num === -1) + return Opcode.fromOp(opcodes.OP_1NEGATE); - if (Buffer.isBuffer(data)) - return Opcode.fromData(data); + if (num === 0) + return Opcode.fromOp(opcodes.OP_0); - if (typeof data === 'string') - return Opcode.fromString(data, 'utf8'); + if (num >= 1 && num <= 16) + return Opcode.fromOp(num + 0x50); - if (typeof data === 'boolean') - return Opcode.fromBool(data); + return Opcode.fromNum(ScriptNum.fromNumber(num)); +}; - if (ScriptNum.isEncodable(data)) - return Opcode.fromNumber(data); +/** + * Instantiate an opcode from a Number. + * @param {Boolean} value + * @returns {Opcode} + */ - throw new Error('Bad data for opcode.'); +Opcode.fromBool = function fromBool(value) { + assert(typeof value === 'boolean'); + return Opcode.fromSmall(value ? 1 : 0); }; /** @@ -423,29 +551,116 @@ Opcode.fromSymbol = function fromSymbol(name) { if (!util.startsWith(name, 'OP_')) name = `OP_${name}`; - const op = common.opcodes[name]; - assert(op != null, 'Unknown opcode.'); + let op = common.opcodes[name]; + + if (op == null) { + assert(util.startsWith(name, 'OP_0X'), 'Unknown opcode.'); + assert(name.length === 7, 'Unknown opcode.'); + + op = parseInt(name.substring(5), 16); + + assert(util.isU8(op), 'Unknown opcode.'); + } return Opcode.fromOp(op); }; /** - * Convert opcode to its symbolic representation. - * @returns {String} + * Instantiate opcode from buffer reader. + * @param {BufferReader} br + * @returns {Opcode} */ -Opcode.prototype.toSymbol = function toSymbol() { - let op = this.value; +Opcode.fromReader = function fromReader(br) { + const value = br.readU8(); - if (op === -1) - op = 0xff; + const cached = opCache[value]; - let symbol = common.opcodesByVal[op]; + if (cached) + return cached; - if (symbol == null) - symbol = util.hex8(op); + const op = new Opcode(value, null); - return symbol; + if (value >= 0x01 && value <= 0x4b) { + if (br.left() < value) { + op.value = -1; + br.seek(br.left()); + return op; + } + op.data = br.readBytes(value); + return op; + } + + switch (value) { + case opcodes.OP_PUSHDATA1: { + if (br.left() < 1) { + op.value = -1; + break; + } + + const size = br.readU8(); + + if (br.left() < size) { + op.value = -1; + br.seek(br.left()); + break; + } + + op.data = br.readBytes(size); + + break; + } + case opcodes.OP_PUSHDATA2: { + if (br.left() < 2) { + op.value = -1; + br.seek(br.left()); + break; + } + + const size = br.readU16(); + + if (br.left() < size) { + op.value = -1; + br.seek(br.left()); + break; + } + + op.data = br.readBytes(size); + + break; + } + case opcodes.OP_PUSHDATA4: { + if (br.left() < 4) { + op.value = -1; + br.seek(br.left()); + break; + } + + const size = br.readU32(); + + if (br.left() < size) { + op.value = -1; + br.seek(br.left()); + break; + } + + op.data = br.readBytes(size); + + break; + } + } + + return op; +}; + +/** + * Instantiate opcode from serialized data. + * @param {Buffer} data + * @returns {Opcode} + */ + +Opcode.fromRaw = function fromRaw(data) { + return Opcode.fromReader(new BufferReader(data)); }; /** @@ -455,9 +670,7 @@ Opcode.prototype.toSymbol = function toSymbol() { */ Opcode.isOpcode = function isOpcode(obj) { - return obj - && typeof obj.value === 'number' - && (Buffer.isBuffer(obj.data) || obj.data === null); + return obj instanceof Opcode; }; /* diff --git a/lib/script/script.js b/lib/script/script.js index 443ae915d..e36340e11 100644 --- a/lib/script/script.js +++ b/lib/script/script.js @@ -19,6 +19,7 @@ const StaticWriter = require('../utils/staticwriter'); const Program = require('./program'); const Opcode = require('./opcode'); const Stack = require('./stack'); +const ScriptError = require('./scripterror'); const ScriptNum = require('./scriptnum'); const common = require('./common'); const encoding = require('../utils/encoding'); @@ -26,8 +27,7 @@ const secp256k1 = require('../crypto/secp256k1'); const Address = require('../primitives/address'); const opcodes = common.opcodes; const scriptTypes = common.types; -const ScriptError = common.ScriptError; -const STACK_FALSE = common.STACK_FALSE; +const EMPTY_BUFFER = Buffer.alloc(0); /** * Represents a input or output script. @@ -44,7 +44,7 @@ function Script(options) { if (!(this instanceof Script)) return new Script(options); - this.raw = STACK_FALSE; + this.raw = EMPTY_BUFFER; this.code = []; if (options) @@ -140,7 +140,7 @@ Script.prototype.fromOptions = function fromOptions(options) { if (options.code) { if (!options.raw) - return this.fromCode(options.code); + return this.fromArray(options.code); assert(Array.isArray(options.code), 'Code must be an array.'); this.code = options.code; } @@ -158,6 +158,33 @@ Script.fromOptions = function fromOptions(options) { return new Script().fromOptions(options); }; +/** + * Instantiate a value-only iterator. + * @returns {ScriptIterator} + */ + +Script.prototype.values = function values() { + return this.code.values(); +}; + +/** + * Instantiate a key and value iterator. + * @returns {ScriptIterator} + */ + +Script.prototype.entries = function entries() { + return this.code.entries(); +}; + +/** + * Instantiate a value-only iterator. + * @returns {ScriptIterator} + */ + +Script.prototype[Symbol.iterator] = function() { + return this.code[Symbol.iterator](); +}; + /** * Convert the script to an array of * Buffers (pushdatas) and Numbers @@ -166,12 +193,7 @@ Script.fromOptions = function fromOptions(options) { */ Script.prototype.toArray = function toArray() { - const code = []; - - for (const op of this.code) - code.push(op.data || op.value); - - return code; + return this.code.slice(); }; /** @@ -185,28 +207,12 @@ Script.prototype.toArray = function toArray() { Script.prototype.fromArray = function fromArray(code) { assert(Array.isArray(code)); - if (code.length === 0) - return this; - - if (code[0] instanceof Opcode) - return this.fromCode(code); - - for (const op of code) { - if (Buffer.isBuffer(op)) { - this.code.push(Opcode.fromData(op)); - continue; - } - if (typeof op === 'string') { - this.code.push(Opcode.fromSymbol(op)); - continue; - } - assert(typeof op === 'number'); - this.code.push(Opcode.fromOp(op)); - } + this.clear(); - this.compile(); + for (const op of code) + this.push(op); - return this; + return this.compile(); }; /** @@ -221,42 +227,81 @@ Script.fromArray = function fromArray(code) { }; /** - * Return an array of opcodes. - * @returns {Opcode[]} + * Convert script to stack items. + * @returns {Buffer[]} */ -Script.prototype.toCode = function toCode() { - return this.code.slice(); +Script.prototype.toItems = function toItems() { + const items = []; + + for (const op of this.code) { + const data = op.toPush(); + + if (!data) + throw new Error('Non-push opcode in script.'); + + items.push(data); + } + + return items; }; /** - * Inject properties from an array of opcodes. - * @param {Opcode[]} code + * Inject data from stack items. * @private + * @param {Buffer[]} items + * @returns {Script} */ -Script.prototype.fromCode = function fromCode(code) { - assert(Array.isArray(code)); +Script.prototype.fromItems = function fromItems(items) { + assert(Array.isArray(items)); - if (code.length === 0) - return this; + this.clear(); - assert(code[0] instanceof Opcode); + for (const item of items) + this.pushData(item); - this.code = code; - this.compile(); + return this.compile(); +}; - return this; +/** + * Instantiate script from stack items. + * @param {Buffer[]} items + * @returns {Script} + */ + +Script.fromItems = function fromItems(items) { + return new Script().fromItems(items); +}; + +/** + * Convert script to stack. + * @returns {Stack} + */ + +Script.prototype.toStack = function toStack() { + return new Stack(this.toItems()); +}; + +/** + * Inject data from stack. + * @private + * @param {Stack} stack + * @returns {Script} + */ + +Script.prototype.fromStack = function fromStack(stack) { + return this.fromItems(stack.items); }; /** - * Instantiate script from an array of opcodes. - * @param {Opcode[]} code + * Instantiate script from stack. + * @param {Stack} stack * @returns {Script} */ -Script.fromCode = function fromCode(code) { - return new Script().fromCode(code); +Script.fromStack = function fromStack(stack) { + return new Script().fromStack(stack); }; /** @@ -277,8 +322,41 @@ Script.prototype.clone = function clone() { */ Script.prototype.inject = function inject(script) { - this.code = script.code.slice(); this.raw = script.raw; + this.code = script.code.slice(); + return this; +}; + +/** + * Test equality against script. + * @param {Script} script + * @returns {Boolean} + */ + +Script.prototype.equals = function equals(script) { + assert(Script.isScript(script)); + return this.raw.equals(script.raw); +}; + +/** + * Compare against another script. + * @param {Script} script + * @returns {Number} + */ + +Script.prototype.compare = function compare(script) { + assert(Script.isScript(script)); + return this.raw.compare(script.raw); +}; + +/** + * Clear the script. + * @returns {Script} + */ + +Script.prototype.clear = function clear() { + this.raw = EMPTY_BUFFER; + this.code.length = 0; return this; }; @@ -297,7 +375,12 @@ Script.prototype.inspect = function inspect() { */ Script.prototype.toString = function toString() { - return common.formatCode(this.code); + const out = []; + + for (const op of this.code) + out.push(op.toFormat()); + + return out.join(' '); }; /** @@ -307,30 +390,32 @@ Script.prototype.toString = function toString() { */ Script.prototype.toASM = function toASM(decode) { - return common.formatASM(this.code, decode); -}; - -/** - * Calculate size of code to be compiled. - * @returns {Number} - */ + if (this.isNulldata()) + decode = false; -Script.prototype.getCodeSize = function getCodeSize() { - let size = 0; + const out = []; for (const op of this.code) - size += op.getSize(); + out.push(op.toASM(decode)); - return size; + 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() { - const size = this.getCodeSize(); + 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) @@ -393,26 +478,26 @@ Script.fromJSON = function fromJSON(json) { /** * Get the script's "subscript" starting at a separator. - * @param {Number?} lastSep - The last separator to sign/verify beyond. + * @param {Number} index - The last separator to sign/verify beyond. * @returns {Script} Subscript. */ -Script.prototype.getSubscript = function getSubscript(lastSep) { - if (lastSep === 0) +Script.prototype.getSubscript = function getSubscript(index) { + if (index === 0) return this.clone(); - const code = []; + const script = new Script(); - for (let i = lastSep; i < this.code.length; i++) { + for (let i = index; i < this.code.length; i++) { const op = this.code[i]; if (op.value === -1) break; - code.push(op); + script.code.push(op); } - return Script.fromCode(code); + return script.compile(); }; /** @@ -444,17 +529,17 @@ Script.prototype.removeSeparators = function removeSeparators() { // Uncommon case: someone actually // has a code separator. Go through // and remove them all. - const code = []; + const script = new Script(); for (const op of this.code) { if (op.value === -1) break; if (op.value !== opcodes.OP_CODESEPARATOR) - code.push(op); + script.code.push(op); } - return Script.fromCode(code); + return script.compile(); }; /** @@ -475,7 +560,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (version == null) version = 0; - if (this.getSize() > consensus.MAX_SCRIPT_SIZE) + if (this.raw.length > consensus.MAX_SCRIPT_SIZE) throw new ScriptError('SCRIPT_SIZE'); const state = []; @@ -495,18 +580,8 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (op.value === -1) throw new ScriptError('BAD_OPCODE', op, ip); - if (op.data) { - if (op.data.length > consensus.MAX_SCRIPT_PUSH) - throw new ScriptError('PUSH_SIZE', op, ip); - - if (negate === 0) { - if (minimal && !op.isMinimal()) - throw new ScriptError('MINIMALDATA', op, ip); - stack.push(op.data); - } - - continue; - } + if (op.data && op.data.length > consensus.MAX_SCRIPT_PUSH) + throw new ScriptError('PUSH_SIZE', op, ip); if (op.value > opcodes.OP_16 && ++opCount > consensus.MAX_SCRIPT_OPS) throw new ScriptError('OP_COUNT', op, ip); @@ -514,8 +589,20 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (op.isDisabled()) throw new ScriptError('DISABLED_OPCODE', op, ip); - if (negate !== 0 && !op.isBranch()) + if (negate && !op.isBranch()) + continue; + + if (op.data) { + if (minimal && !op.isMinimal()) + throw new ScriptError('MINIMALDATA', op, ip); + + stack.push(op.data); + + if (stack.length + alt.length > consensus.MAX_SCRIPT_STACK) + throw new ScriptError('STACK_SIZE'); + continue; + } switch (op.value) { case opcodes.OP_0: { @@ -562,7 +649,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - let locktime = stack.num(-1, minimal, 5); + let locktime = stack.getNum(-1, minimal, 5); if (locktime.isNeg()) throw new ScriptError('NEGATIVE_LOCKTIME', op, ip); @@ -588,7 +675,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - let locktime = stack.num(-1, minimal, 5); + let locktime = stack.getNum(-1, minimal, 5); if (locktime.isNeg()) throw new ScriptError('NEGATIVE_LOCKTIME', op, ip); @@ -616,12 +703,12 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers case opcodes.OP_NOTIF: { let val = false; - if (negate === 0) { + if (!negate) { if (stack.length < 1) throw new ScriptError('UNBALANCED_CONDITIONAL', op, ip); if (version === 1 && (flags & Script.flags.VERIFY_MINIMALIF)) { - const item = stack.top(-1); + const item = stack.get(-1); if (item.length > 1) throw new ScriptError('MINIMALIF'); @@ -630,7 +717,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers throw new ScriptError('MINIMALIF'); } - val = stack.bool(-1); + val = stack.getBool(-1); if (op.value === opcodes.OP_NOTIF) val = !val; @@ -641,7 +728,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers state.push(val); if (!val) - negate++; + negate += 1; break; } @@ -652,9 +739,9 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers state[state.length - 1] = !state[state.length - 1]; if (!state[state.length - 1]) - negate++; + negate += 1; else - negate--; + negate -= 1; break; } @@ -663,7 +750,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers throw new ScriptError('UNBALANCED_CONDITIONAL', op, ip); if (!state.pop()) - negate--; + negate -= 1; break; } @@ -671,7 +758,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - if (!stack.bool(-1)) + if (!stack.getBool(-1)) throw new ScriptError('VERIFY', op, ip); stack.pop(); @@ -707,8 +794,8 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const v1 = stack.top(-2); - const v2 = stack.top(-1); + const v1 = stack.get(-2); + const v2 = stack.get(-1); stack.push(v1); stack.push(v2); @@ -718,9 +805,9 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 3) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const v1 = stack.top(-3); - const v2 = stack.top(-2); - const v3 = stack.top(-1); + const v1 = stack.get(-3); + const v2 = stack.get(-2); + const v3 = stack.get(-1); stack.push(v1); stack.push(v2); @@ -731,8 +818,8 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 4) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const v1 = stack.top(-4); - const v2 = stack.top(-3); + const v1 = stack.get(-4); + const v2 = stack.get(-3); stack.push(v1); stack.push(v2); @@ -742,8 +829,8 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 6) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const v1 = stack.top(-6); - const v2 = stack.top(-5); + const v1 = stack.get(-6); + const v2 = stack.get(-5); stack.erase(-6, -4); stack.push(v1); @@ -762,8 +849,8 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - if (stack.bool(-1)) { - const val = stack.top(-1); + if (stack.getBool(-1)) { + const val = stack.get(-1); stack.push(val); } @@ -784,7 +871,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length === 0) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - stack.push(stack.top(-1)); + stack.push(stack.get(-1)); break; } case opcodes.OP_NIP: { @@ -798,7 +885,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - stack.push(stack.top(-2)); + stack.push(stack.get(-2)); break; } case opcodes.OP_PICK: @@ -806,13 +893,13 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const num = stack.int(-1, minimal); + const num = stack.getInt(-1, minimal); stack.pop(); if (num < 0 || num >= stack.length) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const val = stack.top(-num - 1); + const val = stack.get(-num - 1); if (op.value === opcodes.OP_ROLL) stack.remove(-num - 1); @@ -839,14 +926,14 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - stack.insert(-2, stack.top(-1)); + stack.insert(-2, stack.get(-1)); break; } case opcodes.OP_SIZE: { if (stack.length < 1) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - stack.pushInt(stack.top(-1).length); + stack.pushInt(stack.get(-1).length); break; } case opcodes.OP_EQUAL: @@ -854,8 +941,8 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const v1 = stack.top(-2); - const v2 = stack.top(-1); + const v1 = stack.get(-2); + const v2 = stack.get(-1); const res = v1.equals(v2); @@ -881,7 +968,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 1) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - let num = stack.num(-1, minimal); + let num = stack.getNum(-1, minimal); let cmp; switch (op.value) { @@ -931,8 +1018,8 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const n1 = stack.num(-2, minimal); - const n2 = stack.num(-1, minimal); + const n1 = stack.getNum(-2, minimal); + const n2 = stack.getNum(-1, minimal); let num, cmp; switch (op.value) { @@ -994,7 +1081,7 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers stack.pushNum(num); if (op.value === opcodes.OP_NUMEQUALVERIFY) { - if (!stack.bool(-1)) + if (!stack.getBool(-1)) throw new ScriptError('NUMEQUALVERIFY', op, ip); stack.pop(); } @@ -1005,9 +1092,9 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 3) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const n1 = stack.num(-3, minimal); - const n2 = stack.num(-2, minimal); - const n3 = stack.num(-1, minimal); + const n1 = stack.getNum(-3, minimal); + const n2 = stack.getNum(-2, minimal); + const n3 = stack.getNum(-1, minimal); const val = n2.lte(n1) && n1.lt(n3); @@ -1065,13 +1152,13 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < 2) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - const sig = stack.top(-2); - const key = stack.top(-1); + const sig = stack.get(-2); + const key = stack.get(-1); const subscript = this.getSubscript(lastSep); if (version === 0) - subscript.removeData(sig); + subscript.findAndDelete(sig); validateSignature(sig, flags); validateKey(key, flags, version); @@ -1111,10 +1198,11 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (stack.length < i) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - let n = stack.int(-i, minimal); - let ikey2 = n + 2; + let n = stack.getInt(-i, minimal); + let okey = n + 2; + let ikey, isig; - if (!(n >= 0 && n <= consensus.MAX_MULTISIG_PUBKEYS)) + if (n < 0 || n > consensus.MAX_MULTISIG_PUBKEYS) throw new ScriptError('PUBKEY_COUNT', op, ip); opCount += n; @@ -1122,20 +1210,20 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers if (opCount > consensus.MAX_SCRIPT_OPS) throw new ScriptError('OP_COUNT', op, ip); - i++; - let ikey = i; + i += 1; + ikey = i; i += n; if (stack.length < i) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); - let m = stack.int(-i, minimal); + let m = stack.getInt(-i, minimal); - if (!(m >= 0 && m <= n)) + if (m < 0 || m > n) throw new ScriptError('SIG_COUNT', op, ip); - i++; - let isig = i; + i += 1; + isig = i; i += m; if (stack.length < i) @@ -1144,15 +1232,15 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers const subscript = this.getSubscript(lastSep); for (let j = 0; j < m; j++) { - const sig = stack.top(-isig - j); + const sig = stack.get(-isig - j); if (version === 0) - subscript.removeData(sig); + subscript.findAndDelete(sig); } let res = true; while (res && m > 0) { - const sig = stack.top(-isig); - const key = stack.top(-ikey); + const sig = stack.get(-isig); + const key = stack.get(-ikey); validateSignature(sig, flags); validateKey(key, flags, version); @@ -1168,33 +1256,37 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers ); if (checksig(hash, sig, key)) { - isig++; - m--; + isig += 1; + m -= 1; } } - ikey++; - n--; + ikey += 1; + n -= 1; if (m > n) res = false; } - while (i-- > 1) { + while (i > 1) { if (!res && (flags & Script.flags.VERIFY_NULLFAIL)) { - if (ikey2 === 0 && stack.top(-1).length !== 0) + if (okey === 0 && stack.get(-1).length !== 0) throw new ScriptError('NULLFAIL', op, ip); } - if (ikey2 > 0) - ikey2--; + + if (okey > 0) + okey -= 1; + stack.pop(); + + i -= 1; } if (stack.length < 1) throw new ScriptError('INVALID_STACK_OPERATION', op, ip); if (flags & Script.flags.VERIFY_NULLDUMMY) { - if (stack.top(-1).length !== 0) + if (stack.get(-1).length !== 0) throw new ScriptError('SIG_NULLDUMMY', op, ip); } @@ -1214,10 +1306,10 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers throw new ScriptError('BAD_OPCODE', op, ip); } } - } - if (stack.length + alt.length > consensus.MAX_SCRIPT_STACK) - throw new ScriptError('STACK_SIZE'); + if (stack.length + alt.length > consensus.MAX_SCRIPT_STACK) + throw new ScriptError('STACK_SIZE'); + } if (state.length !== 0) throw new ScriptError('UNBALANCED_CONDITIONAL'); @@ -1238,43 +1330,47 @@ Script.prototype.execute = function execute(stack, flags, tx, index, value, vers * @returns {Number} Total. */ -Script.prototype.removeData = function removeData(data) { - const index = []; +Script.prototype.findAndDelete = function findAndDelete(data) { + const target = Opcode.fromPush(data); - // We need to go forward first. We can't go - // backwards (this is consensus code and we - // need to be aware of bad pushes). - for (let i = 0; i < this.code.length; i++) { - const op = this.code[i]; + if (this.raw.length < target.getSize()) + return 0; + + let found = false; + + for (const op of this.code) { + if (op.value === -1) + break; - if (op.value === -1) { - // Can't reserialize - // a parse error. - if (index.length > 0) - index.push(i); + if (op.equals(target)) { + found = true; break; } + } - if (!op.data) - continue; + if (!found) + return 0; - if (!op.isMinimal()) - continue; + const code = []; - if (op.data.equals(data)) - index.push(i); - } + let total = 0; - if (index.length === 0) - return 0; + for (const op of this.code) { + if (op.value === -1) + break; + + if (op.equals(target)) { + total += 1; + continue; + } - // Go backwards and splice out the data. - for (let i = index.length - 1; i >= 0; i--) - this.code.splice(index[i], 1); + code.push(op); + } + this.code = code; this.compile(); - return index.length; + return total; }; /** @@ -1308,9 +1404,6 @@ Script.prototype.indexOf = function indexOf(data) { Script.prototype.isCode = function isCode() { for (const op of this.code) { - if (op.data) - continue; - if (op.value === -1) return false; @@ -1328,7 +1421,7 @@ Script.prototype.isCode = function isCode() { */ Script.prototype.fromPubkey = function fromPubkey(key) { - assert(Buffer.isBuffer(key) && key.length >= 33 && key.length <= 65); + assert(Buffer.isBuffer(key) && (key.length === 33 || key.length === 65)); this.raw = Buffer.allocUnsafe(1 + key.length + 1); this.raw[0] = key.length; @@ -1337,7 +1430,8 @@ Script.prototype.fromPubkey = function fromPubkey(key) { key = this.raw.slice(1, 1 + key.length); - this.code.push(new Opcode(key.length, key)); + this.code.length = 0; + this.code.push(Opcode.fromData(key)); this.code.push(Opcode.fromOp(opcodes.OP_CHECKSIG)); return this; @@ -1372,9 +1466,10 @@ Script.prototype.fromPubkeyhash = function fromPubkeyhash(hash) { 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(new Opcode(0x14, hash)); + this.code.push(Opcode.fromPush(hash)); this.code.push(Opcode.fromOp(opcodes.OP_EQUALVERIFY)); this.code.push(Opcode.fromOp(opcodes.OP_CHECKSIG)); @@ -1406,19 +1501,17 @@ Script.prototype.fromMultisig = function fromMultisig(m, n, keys) { assert(m >= 1 && m <= n); assert(n >= 1 && n <= 15); - keys = sortKeys(keys); - - this.push(Opcode.fromSmall(m)); + this.clear(); - for (const key of keys) - this.push(key); + this.pushSmall(m); - this.push(Opcode.fromSmall(n)); - this.push(opcodes.OP_CHECKMULTISIG); + for (const key of sortKeys(keys)) + this.pushData(key); - this.compile(); + this.pushSmall(n); + this.pushOp(opcodes.OP_CHECKMULTISIG); - return this; + return this.compile(); }; /** @@ -1450,8 +1543,9 @@ Script.prototype.fromScripthash = function fromScripthash(hash) { hash = this.raw.slice(2, 22); + this.code.length = 0; this.code.push(Opcode.fromOp(opcodes.OP_HASH160)); - this.code.push(new Opcode(0x14, hash)); + this.code.push(Opcode.fromPush(hash)); this.code.push(Opcode.fromOp(opcodes.OP_EQUAL)); return this; @@ -1476,10 +1570,12 @@ Script.fromScripthash = function fromScripthash(hash) { Script.prototype.fromNulldata = function fromNulldata(flags) { assert(Buffer.isBuffer(flags)); assert(flags.length <= policy.MAX_OP_RETURN, 'Nulldata too large.'); - this.push(opcodes.OP_RETURN); - this.push(flags); - this.compile(); - return this; + + this.clear(); + this.pushOp(opcodes.OP_RETURN); + this.pushData(flags); + + return this.compile(); }; /** @@ -1503,17 +1599,16 @@ Script.prototype.fromProgram = function fromProgram(version, data) { assert(util.isU8(version) && version >= 0 && version <= 16); assert(Buffer.isBuffer(data) && data.length >= 2 && data.length <= 40); - const op = Opcode.fromSmall(version); - this.raw = Buffer.allocUnsafe(2 + data.length); - this.raw[0] = op.value; + this.raw[0] = version === 0 ? 0 : version + 0x50; this.raw[1] = data.length; data.copy(this.raw, 2); data = this.raw.slice(2, 2 + data.length); - this.code.push(op); - this.code.push(new Opcode(data.length, data)); + this.code.length = 0; + this.code.push(Opcode.fromSmall(version)); + this.code.push(Opcode.fromPush(data)); return this; }; @@ -1576,15 +1671,14 @@ Script.prototype.fromCommitment = function fromCommitment(hash, flags) { bw.writeU32BE(0xaa21a9ed); bw.writeHash(hash); - this.push(opcodes.OP_RETURN); - this.push(bw.render()); + this.clear(); + this.pushOp(opcodes.OP_RETURN); + this.pushData(bw.render()); if (flags) - this.push(flags); + this.pushData(flags); - this.compile(); - - return this; + return this.compile(); }; /** @@ -1604,18 +1698,22 @@ Script.fromCommitment = function fromCommitment(hash, flags) { */ Script.prototype.getRedeem = function getRedeem() { - if (this.code.length === 0) - return null; + let data = null; - if (!this.isPushOnly()) - return null; + for (const op of this.code) { + if (op.value === -1) + return null; + + if (op.value > opcodes.OP_16) + return null; - const redeem = this.code[this.code.length - 1]; + data = op.data; + } - if (!redeem.data) + if (!data) return null; - return Script.fromRaw(redeem.data); + return Script.fromRaw(data); }; /** @@ -1666,30 +1764,22 @@ Script.prototype.isUnknown = function isUnknown() { */ Script.prototype.isStandard = function isStandard() { - const type = this.getType(); + const [m, n] = this.getMultisig(); - switch (type) { - case scriptTypes.MULTISIG: { - const m = this.getSmall(0); - const n = this.getSmall(this.code.length - 2); - - if (n < 1 || n > 3) - return false; + if (m !== -1) { + if (n < 1 || n > 3) + return false; - if (m < 1 || m > n) - return false; + if (m < 1 || m > n) + return false; - return true; - } - case scriptTypes.NULLDATA: { - if (this.raw.length > policy.MAX_OP_RETURN_BYTES) - return false; - return true; - } - default: { - return type !== scriptTypes.NONSTANDARD; - } + return true; } + + if (this.isNulldata()) + return this.raw.length <= policy.MAX_OP_RETURN_BYTES; + + return this.getType() !== scriptTypes.NONSTANDARD; }; /** @@ -1768,15 +1858,34 @@ Script.prototype.sha256 = function sha256(enc) { Script.prototype.isPubkey = function isPubkey(minimal) { if (minimal) { return this.raw.length >= 35 - && this.raw[0] >= 33 && this.raw[0] <= 65 + && (this.raw[0] === 33 || this.raw[0] === 65) && this.raw[0] + 2 === this.raw.length && this.raw[this.raw.length - 1] === opcodes.OP_CHECKSIG; } - return this.code.length === 2 - && this.code[0].data - && common.isKey(this.code[0].data) - && this.code[1].value === opcodes.OP_CHECKSIG; + if (this.code.length !== 2) + return false; + + const size = this.getLength(0); + + return (size === 33 || size === 65) + && this.getOp(1) === opcodes.OP_CHECKSIG; +}; + +/** + * Get P2PK key if present. + * @param {Boolean} [minimal=false] - Minimaldata only. + * @returns {Buffer|null} + */ + +Script.prototype.getPubkey = function getPubkey(minimal) { + if (!this.isPubkey(minimal)) + return null; + + if (minimal) + return this.raw.slice(1, 1 + this.raw[0]); + + return this.getData(0); }; /** @@ -1786,7 +1895,7 @@ Script.prototype.isPubkey = function isPubkey(minimal) { */ Script.prototype.isPubkeyhash = function isPubkeyhash(minimal) { - if (minimal) { + if (minimal || this.raw.length === 25) { return this.raw.length === 25 && this.raw[0] === opcodes.OP_DUP && this.raw[1] === opcodes.OP_HASH160 @@ -1795,12 +1904,30 @@ Script.prototype.isPubkeyhash = function isPubkeyhash(minimal) { && this.raw[24] === opcodes.OP_CHECKSIG; } - return this.code.length === 5 - && this.code[0].value === opcodes.OP_DUP - && this.code[1].value === opcodes.OP_HASH160 - && common.isHash(this.code[2].data) - && this.code[3].value === opcodes.OP_EQUALVERIFY - && this.code[4].value === opcodes.OP_CHECKSIG; + if (this.code.length !== 5) + 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; +}; + +/** + * Get P2PKH hash if present. + * @param {Boolean} [minimal=false] - Minimaldata only. + * @returns {Buffer|null} + */ + +Script.prototype.getPubkeyhash = function getPubkeyhash(minimal) { + if (!this.isPubkeyhash(minimal)) + return null; + + if (minimal) + return this.raw.slice(3, 23); + + return this.getData(2); }; /** @@ -1810,29 +1937,30 @@ Script.prototype.isPubkeyhash = function isPubkeyhash(minimal) { */ Script.prototype.isMultisig = function isMultisig(minimal) { - if (this.raw.length < 41) + if (this.code.length < 4 || this.code.length > 19) return false; - if (this.raw[this.raw.length - 1] !== opcodes.OP_CHECKMULTISIG) + if (this.getOp(-1) !== opcodes.OP_CHECKMULTISIG) return false; - const n = common.getSmall(this.raw[this.raw.length - 2]); + const m = this.getSmall(0); - if (n < 1) + if (m < 1) return false; - const m = common.getSmall(this.raw[0]); + const n = this.getSmall(-2); - if (!(m >= 1 && m <= n)) + if (n < 1 || m > n) return false; - if (n + 3 !== this.code.length) + if (this.code.length !== n + 3) return false; for (let i = 1; i < n + 1; i++) { const op = this.code[i]; + const size = op.toLength(); - if (!common.isKey(op.data)) + if (size !== 33 && size !== 65) return false; if (minimal && !op.isMinimal()) @@ -1842,6 +1970,19 @@ Script.prototype.isMultisig = function isMultisig(minimal) { return true; }; +/** + * Get multisig m and n values if present. + * @param {Boolean} [minimal=false] - Minimaldata only. + * @returns {Array} [m, n] + */ + +Script.prototype.getMultisig = function getMultisig(minimal) { + if (!this.isMultisig(minimal)) + return [-1, -1]; + + return [this.getSmall(0), this.getSmall(-2)]; +}; + /** * Test whether the output script is pay-to-scripthash. Note that * bitcoin itself requires scripthashes to be in strict minimaldata @@ -1857,6 +1998,18 @@ Script.prototype.isScripthash = function isScripthash() { && this.raw[22] === opcodes.OP_EQUAL; }; +/** + * Get P2SH hash if present. + * @returns {Buffer|null} + */ + +Script.prototype.getScripthash = function getScripthash() { + if (!this.isScripthash()) + return null; + + return this.raw.slice(2, 22); +}; + /** * Test whether the output script is nulldata/opreturn. * @param {Boolean} [minimal=false] - Minimaldata only. @@ -1864,51 +2017,60 @@ Script.prototype.isScripthash = function isScripthash() { */ Script.prototype.isNulldata = function isNulldata(minimal) { - if (this.raw.length === 0) + if (this.code.length === 0) return false; - if (this.raw[0] !== opcodes.OP_RETURN) + if (this.getOp(0) !== opcodes.OP_RETURN) return false; - if (this.raw.length === 1) + if (this.code.length === 1) return true; if (minimal) { if (this.raw.length > policy.MAX_OP_RETURN_BYTES) return false; - - if (this.raw.length === 2) - return common.getSmall(this.raw[1]) !== -1; - - if (this.raw[1] >= 0x01 && this.raw[1] <= 0x4b) - return this.raw[1] + 2 === this.raw.length; - - if (this.raw[1] === opcodes.OP_PUSHDATA1) - return this.raw[2] > 75 && this.raw[2] + 3 === this.raw.length; - - return false; } for (let i = 1; i < this.code.length; i++) { const op = this.code[i]; - if (op.data) - continue; - if (op.value === -1) return false; if (op.value > opcodes.OP_16) return false; + + if (minimal && !op.isMinimal()) + return false; } return true; }; /** - * Test whether the output script is a segregated witness - * commitment. - * @returns {Boolean} + * Get OP_RETURN data if present. + * @param {Boolean} [minimal=false] - Minimaldata only. + * @returns {Buffer|null} + */ + +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(); + if (data) + return data; + } + + return EMPTY_BUFFER; +}; + +/** + * Test whether the output script is a segregated witness + * commitment. + * @returns {Boolean} */ Script.prototype.isCommitment = function isCommitment() { @@ -1923,7 +2085,7 @@ Script.prototype.isCommitment = function isCommitment() { * @returns {Buffer|null} */ -Script.prototype.getCommitmentHash = function getCommitmentHash() { +Script.prototype.getCommitment = function getCommitment() { if (!this.isCommitment()) return null; @@ -1938,11 +2100,11 @@ Script.prototype.getCommitmentHash = function getCommitmentHash() { */ Script.prototype.isProgram = function isProgram() { - if (!(this.raw.length >= 4 && this.raw.length <= 42)) + if (this.raw.length < 4 || this.raw.length > 42) return false; if (this.raw[0] !== opcodes.OP_0 - && !(this.raw[0] >= opcodes.OP_1 && this.raw[0] <= opcodes.OP_16)) { + && (this.raw[0] < opcodes.OP_1 || this.raw[0] > opcodes.OP_16)) { return false; } @@ -1957,12 +2119,12 @@ Script.prototype.isProgram = function isProgram() { * @returns {Program|null} */ -Script.prototype.toProgram = function toProgram() { +Script.prototype.getProgram = function getProgram() { if (!this.isProgram()) return null; - const version = common.getSmall(this.raw[0]); - const data = this.raw.slice(2); + const version = this.getSmall(0); + const data = this.getData(1); return new Program(version, data); }; @@ -1970,20 +2132,22 @@ Script.prototype.toProgram = function toProgram() { /** * Get the script to the equivalent witness * program (mimics bitcoind's scriptForWitness). - * @returns {Program|null} + * @returns {Script|null} */ Script.prototype.forWitness = function forWitness() { if (this.isProgram()) - return this; + return this.clone(); - if (this.isPubkey()) { - const hash = digest.hash160(this.get(0)); + const pk = this.getPubkey(); + if (pk) { + const hash = digest.hash160(pk); return Script.fromProgram(0, hash); } - if (this.isPubkeyhash()) - return Script.fromProgram(0, this.get(2)); + const pkh = this.getPubkeyhash(); + if (pkh) + return Script.fromProgram(0, pkh); return Script.fromProgram(0, this.sha256()); }; @@ -2000,6 +2164,18 @@ Script.prototype.isWitnessPubkeyhash = function isWitnessPubkeyhash() { && this.raw[1] === 0x14; }; +/** + * Get P2WPKH hash if present. + * @returns {Buffer|null} + */ + +Script.prototype.getWitnessPubkeyhash = function getWitnessPubkeyhash() { + if (!this.isWitnessPubkeyhash()) + return null; + + return this.getData(1); +}; + /** * Test whether the output script is * a pay-to-witness-scripthash program. @@ -2012,6 +2188,18 @@ Script.prototype.isWitnessScripthash = function isWitnessScripthash() { && this.raw[1] === 0x20; }; +/** + * Get P2WSH hash if present. + * @returns {Buffer|null} + */ + +Script.prototype.getWitnessScripthash = function getWitnessScripthash() { + if (!this.isWitnessScripthash()) + return null; + + return this.getData(1); +}; + /** * Test whether the output script * is a pay-to-mast program. @@ -2024,6 +2212,18 @@ Script.prototype.isWitnessMasthash = function isWitnessMasthash() { && this.raw[1] === 0x20; }; +/** + * Get P2WMH hash if present. + * @returns {Buffer|null} + */ + +Script.prototype.getWitnessMasthash = function getWitnessMasthash() { + if (!this.isWitnessMasthash()) + return null; + + return this.getData(1); +}; + /** * Test whether the output script is unspendable. * @returns {Boolean} @@ -2075,18 +2275,24 @@ Script.prototype.isUnknownInput = function isUnknownInput() { */ Script.prototype.isPubkeyInput = function isPubkeyInput() { - if (this.raw.length < 10) + if (this.code.length !== 1) return false; - if (this.raw.length > 78) - return false; + const size = this.getLength(0); - if (this.raw[0] > opcodes.OP_PUSHDATA4) - return false; + return size >= 9 && size <= 73; +}; + +/** + * Get P2PK signature if present. + * @returns {Buffer|null} + */ - return this.code.length === 1 - && this.code[1].data - && common.isSignature(this.code[0].data); +Script.prototype.getPubkeyInput = function getPubkeyInput() { + if (!this.isPubkeyInput()) + return null; + + return this.getData(0); }; /** @@ -2096,20 +2302,26 @@ Script.prototype.isPubkeyInput = function isPubkeyInput() { */ Script.prototype.isPubkeyhashInput = function isPubkeyhashInput() { - if (this.raw.length < 44) + if (this.code.length !== 2) return false; - if (this.raw.length > 148) - return false; + const sig = this.getLength(0); + const key = this.getLength(1); - if (this.raw[0] > opcodes.OP_PUSHDATA4) - return false; + return sig >= 9 && sig <= 73 + && (key === 33 || key === 65); +}; - return this.code.length === 2 - && this.code[0].data - && common.isSignature(this.code[0].data) - && this.code[1].data - && common.isKey(this.code[1].data); +/** + * Get P2PKH signature and key if present. + * @returns {Array} [sig, key] + */ + +Script.prototype.getPubkeyhashInput = function getPubkeyhashInput() { + if (!this.isPubkeyhashInput()) + return [null, null]; + + return [this.getData(0), this.getData(1)]; }; /** @@ -2119,13 +2331,13 @@ Script.prototype.isPubkeyhashInput = function isPubkeyhashInput() { */ Script.prototype.isMultisigInput = function isMultisigInput() { - if (this.raw.length < 20) + if (this.code.length < 2) return false; - if (this.raw[0] !== opcodes.OP_0) + if (this.getOp(0) !== opcodes.OP_0) return false; - if (this.raw[1] > opcodes.OP_PUSHDATA4) + if (this.getOp(1) > opcodes.OP_PUSHDATA4) return false; // We need to rule out scripthash @@ -2133,22 +2345,32 @@ Script.prototype.isMultisigInput = function isMultisigInput() { if (this.isScripthashInput()) return false; - if (this.code.length < 3) - return false; - for (let i = 1; i < this.code.length; i++) { - const op = this.code[i]; - - if (!op.data) - return false; - - if (!common.isSignature(op.data)) + const size = this.getLength(i); + if (size < 9 || size > 73) return false; } return true; }; +/** + * Get multisig signatures if present. + * @returns {Buffer[]|null} + */ + +Script.prototype.getMultisigInput = function getMultisigInput() { + if (!this.isMultisigInput()) + return null; + + const sigs = []; + + for (let i = 1; i < this.code.length; i++) + sigs.push(this.getData(i)); + + return sigs; +}; + /** * "Guess" whether the input script is pay-to-scripthash. * This method is not 100% reliable. @@ -2156,15 +2378,15 @@ Script.prototype.isMultisigInput = function isMultisigInput() { */ Script.prototype.isScripthashInput = function isScripthashInput() { - if (this.raw.length < 2) + if (this.code.length < 2) return false; // Grab the raw redeem script. - const op = this.code[this.code.length - 1]; + const raw = this.getData(-1); // Last data element should be an array // for the redeem script. - if (!op.data) + if (!raw) return false; // Testing for scripthash inputs requires @@ -2176,23 +2398,41 @@ Script.prototype.isScripthashInput = function isScripthashInput() { // key, and we ensure that it is at least // a script that does not use undefined // opcodes. - if (op.data.length === 0) + if (raw.length === 0) return false; - if (common.isSignatureEncoding(op.data)) + if (common.isSignatureEncoding(raw)) return false; - if (common.isKeyEncoding(op.data)) + if (common.isKeyEncoding(raw)) return false; - const redeem = Script.fromRaw(op.data); + const redeem = Script.fromRaw(raw); if (!redeem.isCode()) return false; + if (redeem.isUnspendable()) + return false; + + if (!this.isPushOnly()) + return false; + return true; }; +/** + * Get P2SH redeem script if present. + * @returns {Buffer|null} + */ + +Script.prototype.getScripthashInput = function getScripthashInput() { + if (!this.isScripthashInput()) + return null; + + return this.getData(-1); +}; + /** * Get coinbase height. * @returns {Number} `-1` if not present. @@ -2212,23 +2452,27 @@ Script.getCoinbaseHeight = function getCoinbaseHeight(raw) { if (raw.length === 0) return -1; + // First opcode. + const value = raw[0]; + // Small ints are allowed. - const smi = common.getSmall(raw[0]); + if (value === 0) + return 0; - if (smi !== -1) - return smi; + if (value >= opcodes.OP_1 && value <= opcodes.OP_16) + return value - 0x50; // No more than 6 bytes (we can't // handle 7 byte JS numbers and // height 281 trillion is far away). - if (raw[0] > 0x06) + if (value > 0x06) return -1; // No bad pushes allowed. - if (raw.length < 1 + raw[0]) + if (raw.length < 1 + value) return -1; - const data = raw.slice(1, 1 + raw[0]); + const data = raw.slice(1, 1 + value); // Deserialize the height. let height; @@ -2238,8 +2482,12 @@ Script.getCoinbaseHeight = function getCoinbaseHeight(raw) { return -1; } + // Cannot be negative. + if (height.isNeg()) + return -1; + // Reserialize the height. - const op = Opcode.fromNumber(height); + const op = Opcode.fromNum(height); // Should have been OP_0-OP_16. if (!op.data) @@ -2275,323 +2523,552 @@ Script.prototype.test = function test(filter) { }; /** - * Unshift an item onto the `code` array. - * @param {Number|String|ScriptNum|Buffer} data - * @returns {Number} Length. + * Test the script to see if it contains only push ops. + * Push ops are: OP_1NEGATE, OP_0-OP_16 and all PUSHDATAs. + * @returns {Boolean} */ -Script.prototype.unshift = function unshift(data) { - return this.code.unshift(Opcode.from(data)); +Script.prototype.isPushOnly = function isPushOnly() { + for (const op of this.code) { + if (op.value === -1) + return false; + + if (op.value > opcodes.OP_16) + return false; + } + + return true; }; /** - * Push an item onto the `code` array. - * @param {Number|String|ScriptNum|Buffer} data - * @returns {Number} Length. + * Count the sigops in the script. + * @param {Boolean} accurate - Whether to enable accurate counting. This will + * take into account the `n` value for OP_CHECKMULTISIG(VERIFY). + * @returns {Number} sigop count */ -Script.prototype.push = function push(data) { - return this.code.push(Opcode.from(data)); +Script.prototype.getSigops = function getSigops(accurate) { + let total = 0; + let lastOp = -1; + + for (const op of this.code) { + if (op.value === -1) + break; + + switch (op.value) { + case opcodes.OP_CHECKSIG: + case opcodes.OP_CHECKSIGVERIFY: + total += 1; + break; + case opcodes.OP_CHECKMULTISIG: + case opcodes.OP_CHECKMULTISIGVERIFY: + if (accurate && lastOp >= opcodes.OP_1 && lastOp <= opcodes.OP_16) + total += lastOp - 0x50; + else + total += consensus.MAX_MULTISIG_PUBKEYS; + break; + } + + lastOp = op.value; + } + + return total; }; /** - * Shift an item off of the `code` array. - * @returns {Buffer} + * Count the sigops in the script, taking into account redeem scripts. + * @param {Script} input - Input script, needed for access to redeem script. + * @returns {Number} sigop count */ -Script.prototype.shift = function shift() { - const op = this.code.shift(); +Script.prototype.getScripthashSigops = function getScripthashSigops(input) { + if (!this.isScripthash()) + return this.getSigops(true); - if (!op) - return null; + const redeem = input.getRedeem(); + + if (!redeem) + return 0; - return op.data || op.value; + return redeem.getSigops(true); }; /** - * Pop an item off of the `code` array. - * @returns {Buffer} + * Count the sigops in a script, taking into account witness programs. + * @param {Script} input + * @param {Witness} witness + * @returns {Number} sigop count */ -Script.prototype.pop = function pop(data) { - const op = this.code.pop(); +Script.prototype.getWitnessSigops = function getWitnessSigops(input, witness) { + let program = this.getProgram(); - if (!op) - return null; + if (!program) { + if (this.isScripthash()) { + const redeem = input.getRedeem(); + if (redeem) + program = redeem.getProgram(); + } + } + + if (!program) + return 0; + + if (program.version === 0) { + if (program.data.length === 20) + return 1; + + if (program.data.length === 32 && witness.items.length > 0) { + const redeem = witness.getRedeem(); + return redeem.getSigops(true); + } + } - return op.data || op.value; + return 0; }; -/** - * Remove an item from the `code` array. - * @param {Number} index - * @returns {Buffer|Number} +/* + * Mutation */ -Script.prototype.remove = function remove(i) { - const op = this.code.splice(i, 1)[0]; +Script.prototype.get = function get(index) { + if (index < 0) + index += this.code.length; - if (!op) + if (index < 0 || index >= this.code.length) return null; - return op.data || op.value; + return this.code[index]; }; -/** - * Insert an item into the `code` array. - * @param {Number} index - * @param {Number|String|ScriptNum|Buffer} data - */ +Script.prototype.pop = function pop() { + const op = this.code.pop(); + return op || null; +}; -Script.prototype.insert = function insert(i, data) { - assert(i <= this.code.length, 'Index out of bounds.'); - this.code.splice(i, 0, Opcode.from(data)); +Script.prototype.shift = function shift() { + const op = this.code.shift(); + return op || null; }; -/** - * Get an item from the `code` array. - * @param {Number} index - * @returns {Buffer} - */ +Script.prototype.remove = function remove(index) { + if (index < 0) + index += this.code.length; + + if (index < 0 || index >= this.code.length) + return null; -Script.prototype.get = function get(i) { - const op = this.code[i]; + const items = this.code.splice(index, 1); - if (!op) + if (items.length === 0) return null; - return op.data || op.value; + return items[0]; }; -/** - * Get a small integer from an opcode (OP_0-OP_16). - * @param {Number} index - * @returns {Number} - */ +Script.prototype.set = function set(index, op) { + if (index < 0) + index += this.code.length; -Script.prototype.getSmall = function getSmall(i) { - const op = this.code[i]; + assert(Opcode.isOpcode(op)); + assert(index >= 0 && index <= this.code.length); - if (!op) - return -1; + this.code[index] = op; - return common.getSmall(op.value); + return this; }; -/** - * Get a number from the `code` array (5-byte limit). - * @params {Number} index - * @returns {ScriptNum} - */ +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.getNumber = function getNumber(i) { - const small = this.getSmall(i); - const op = this.code[i]; +Script.prototype.insert = function insert(index, op) { + if (index < 0) + index += this.code.length; - if (small !== -1) - return ScriptNum.fromInt(small); + assert(Opcode.isOpcode(op)); + assert(index >= 0 && index <= this.code.length); - if (!op || !op.data || op.data.length > 5) - return null; + this.code.splice(index, 0, op); - return ScriptNum.decode(op.data, false, 5); + return this; }; -/** - * Get a string from the `code` array (utf8). - * @params {Number} index - * @returns {String} +/* + * Op */ -Script.prototype.getString = function getString(i) { - const op = this.code[i]; +Script.prototype.getOp = function getOp(index) { + const op = this.get(index); + return op ? op.value : -1; +}; - if (!op || !op.data) - return null; +Script.prototype.popOp = function popOp() { + const op = this.pop(); + return op ? op.value : -1; +}; - return op.data.toString('utf8'); +Script.prototype.shiftOp = function shiftOp() { + const op = this.shift(); + return op ? op.value : -1; }; -/** - * Clear the script code. - */ +Script.prototype.removeOp = function removeOp(index) { + const op = this.remove(index); + return op ? op.value : -1; +}; -Script.prototype.clear = function clear() { - this.code.length = 0; +Script.prototype.setOp = function setOp(index, value) { + return this.set(index, Opcode.fromOp(value)); }; -/** - * Set an item in the `code` array. - * @param {Number} index - * @param {Buffer|Number|String|ScriptNum} data +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.set = function set(i, data) { - assert(i <= this.code.length, 'Index out of bounds.'); - this.code[i] = Opcode.from(data); +Script.prototype.getData = function getData(index) { + const op = this.get(index); + return op ? op.data : null; }; -/** - * Test whether the data element is a public key. Note that - * this does not verify the format of the key, only the length. - * @param {Buffer?} key - * @returns {Boolean} +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.isKey = function isKey(key) { - return common.isKey(key); +Script.prototype.getLength = function getLength(index) { + const op = this.get(index); + return op ? op.toLength() : -1; }; -/** - * Test whether the data element is a signature. Note that - * this does not verify the format of the signature, only the length. - * @param {Buffer?} sig - * @returns {Boolean} +/* + * Push */ -Script.isSignature = function isSignature(sig) { - return common.isSignature(sig); +Script.prototype.getPush = function getPush(index) { + const op = this.get(index); + return op ? op.toPush() : null; }; -/** - * Test the script to see if it contains only push ops. - * Push ops are: OP_1NEGATE, OP_0-OP_16 and all PUSHDATAs. - * @returns {Boolean} +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.isPushOnly = function isPushOnly() { - for (const op of this.code) { - if (op.data) - continue; +Script.prototype.getString = function getString(index, enc) { + const op = this.get(index); + return op ? op.toString(enc) : null; +}; - if (op.value === -1) - return false; +Script.prototype.popString = function popString(enc) { + const op = this.pop(); + return op ? op.toString(enc) : null; +}; - if (op.value > opcodes.OP_16) - return false; - } +Script.prototype.shiftString = function shiftString(enc) { + const op = this.shift(); + return op ? op.toString(enc) : null; +}; - return true; +Script.prototype.removeString = function removeString(index, enc) { + const op = this.remove(index); + return op ? op.toString(enc) : null; }; -/** - * Count the sigops in the script. - * @param {Boolean} accurate - Whether to enable accurate counting. This will - * take into account the `n` value for OP_CHECKMULTISIG(VERIFY). - * @returns {Number} sigop count +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.getSigops = function getSigops(accurate) { - let total = 0; - let lastOp = -1; +Script.prototype.getSmall = function getSmall(index) { + const op = this.get(index); + return op ? op.toSmall() : -1; +}; - for (const op of this.code) { - if (op.data) - continue; +Script.prototype.popSmall = function popSmall() { + const op = this.pop(); + return op ? op.toSmall() : -1; +}; - if (op.value === -1) - break; +Script.prototype.shiftSmall = function shiftSmall() { + const op = this.shift(); + return op ? op.toSmall() : -1; +}; - 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 += consensus.MAX_MULTISIG_PUBKEYS; - break; - } +Script.prototype.removeSmall = function removeSmall(index) { + const op = this.remove(index); + return op ? op.toSmall() : -1; +}; - lastOp = op.value; - } +Script.prototype.setSmall = function setSmall(index, num) { + return this.set(index, Opcode.fromSmall(num)); +}; - return total; +Script.prototype.pushSmall = function pushSmall(num) { + return this.push(Opcode.fromSmall(num)); }; -/** - * Count the sigops in the script, taking into account redeem scripts. - * @param {Script} input - Input script, needed for access to redeem script. - * @returns {Number} sigop count +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.getScripthashSigops = function getScripthashSigops(input) { - if (!this.isScripthash()) - return this.getSigops(true); +Script.prototype.getNum = function getNum(index, minimal, limit) { + const op = this.get(index); + return op ? op.toNum(minimal, limit) : null; +}; - if (input.code.length === 0) - return 0; +Script.prototype.popNum = function popNum(minimal, limit) { + const op = this.pop(); + return op ? op.toNum(minimal, limit) : null; +}; - let op; +Script.prototype.shiftNum = function shiftNum(minimal, limit) { + const op = this.shift(); + return op ? op.toNum(minimal, limit) : null; +}; - for (op of input.code) { - if (op.data) - continue; +Script.prototype.removeNum = function removeNum(index, minimal, limit) { + const op = this.remove(index); + return op ? op.toNum(minimal, limit) : null; +}; - if (op.value === -1) - return 0; +Script.prototype.setNum = function setNum(index, num) { + return this.set(index, Opcode.fromNum(num)); +}; - if (op.value > opcodes.OP_16) - return 0; - } +Script.prototype.pushNum = function pushNum(num) { + return this.push(Opcode.fromNum(num)); +}; - if (!op.data) - return 0; +Script.prototype.unshiftNum = function unshiftNum(num) { + return this.unshift(Opcode.fromNum(num)); +}; - const redeem = new Script(op.data); +Script.prototype.insertNum = function insertNum(index, num) { + return this.insert(index, Opcode.fromNum(num)); +}; - return redeem.getSigops(true); +/* + * Int + */ + +Script.prototype.getInt = function getInt(index, minimal, limit) { + const op = this.get(index); + return op ? op.toInt(minimal, limit) : -1; }; -/** - * Count the sigops for a program. - * @param {Program} program - * @param {Witness} witness - * @returns {Number} sigop count +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.witnessSigops = function witnessSigops(program, witness) { - if (program.version === 0) { - if (program.data.length === 20) - return 1; +Script.prototype.getBool = function getBool(index) { + const op = this.get(index); + return op ? op.toBool() : false; +}; - if (program.data.length === 32 && witness.items.length > 0) { - const redeem = witness.getRedeem(); - return redeem.getSigops(true); - } - } +Script.prototype.popBool = function popBool() { + const op = this.pop(); + return op ? op.toBool() : false; +}; - return 0; +Script.prototype.shiftBool = function shiftBool() { + const op = this.shift(); + return op ? op.toBool() : false; }; -/** - * Count the sigops in a script, taking into account witness programs. - * @param {Script} input - * @param {Witness} witness - * @returns {Number} sigop count +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.getWitnessSigops = function getWitnessSigops(input, witness) { - if (this.isProgram()) - return Script.witnessSigops(this.toProgram(), witness); - - // This is a unique situation in terms of consensus - // rules. We can just grab the redeem script without - // "parsing" (i.e. checking for pushdata parse errors) - // the script. This is because isPushOnly is called - // which checks for parse errors and will return - // false if one is found. Even the bitcoind code - // does not check the return value of GetOp. - if (this.isScripthash() && input.isPushOnly()) { - const redeem = input.getRedeem(); - if (redeem && redeem.isProgram()) - return Script.witnessSigops(redeem.toProgram(), witness); - } +Script.prototype.getSym = function getSym(index) { + const op = this.get(index); + return op ? op.toSymbol() : null; +}; - return 0; +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)); }; /** @@ -2634,7 +3111,7 @@ Script.prototype.fromString = function fromString(code) { if (/^-?\d+$/.test(item)) { const num = ScriptNum.fromString(item, 10); - const op = Opcode.fromNumber(num); + const op = Opcode.fromNum(num); bw.writeBytes(op.toRaw()); continue; } @@ -2705,7 +3182,7 @@ Script.verify = function verify(input, witness, output, tx, index, value, flags) output.execute(stack, flags, tx, index, value, 0); // Verify the stack values. - if (stack.length === 0 || !stack.bool(-1)) + if (stack.length === 0 || !stack.getBool(-1)) throw new ScriptError('EVAL_FALSE'); let hadWitness = false; @@ -2745,7 +3222,7 @@ Script.verify = function verify(input, witness, output, tx, index, value, flags) redeem.execute(stack, flags, tx, index, value, 0); // Verify the the stack values. - if (stack.length === 0 || !stack.bool(-1)) + if (stack.length === 0 || !stack.getBool(-1)) throw new ScriptError('EVAL_FALSE'); if ((flags & Script.flags.VERIFY_WITNESS) && redeem.isProgram()) { @@ -2792,7 +3269,7 @@ Script.verify = function verify(input, witness, output, tx, index, value, flags) */ Script.verifyProgram = function verifyProgram(witness, output, flags, tx, index, value) { - const program = output.toProgram(); + const program = output.getProgram(); assert(program, 'verifyProgram called on non-witness-program.'); assert((flags & Script.flags.VERIFY_WITNESS) !== 0); @@ -2810,7 +3287,7 @@ Script.verifyProgram = function verifyProgram(witness, output, flags, tx, index, if (!digest.sha256(witnessScript).equals(program.data)) throw new ScriptError('WITNESS_PROGRAM_MISMATCH'); - redeem = new Script(witnessScript); + redeem = Script.fromRaw(witnessScript); } else if (program.data.length === 20) { if (stack.length !== 2) throw new ScriptError('WITNESS_PROGRAM_MISMATCH'); @@ -2846,7 +3323,7 @@ Script.verifyProgram = function verifyProgram(witness, output, flags, tx, index, redeem.execute(stack, flags, tx, index, value, 1); // Verify the stack values. - if (stack.length !== 1 || !stack.bool(-1)) + if (stack.length !== 1 || !stack.getBool(-1)) throw new ScriptError('EVAL_FALSE'); }; @@ -2869,7 +3346,7 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, index if (stack.length < 4) throw new ScriptError('INVALID_MAST_STACK'); - const metadata = stack.top(-1); + const metadata = stack.get(-1); if (metadata.length < 1 || metadata.length > 5) throw new ScriptError('INVALID_MAST_STACK'); @@ -2900,7 +3377,7 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, index let mastRoot = new BufferWriter(); mastRoot.writeU32(version); - const pathdata = stack.top(-2); + const pathdata = stack.get(-2); if (pathdata.length & 0x1f) throw new ScriptError('INVALID_MAST_STACK'); @@ -2921,7 +3398,7 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, index for (let j = 0; j < depth; j++) path.push(pathdata.slice(j * 32, j * 32 + 32)); - const posdata = stack.top(-3); + const posdata = stack.get(-3); if (posdata.length > 4) throw new ScriptError('INVALID_MAST_STACK'); @@ -2947,7 +3424,7 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, index scripts.writeBytes(output.raw); for (let j = 0; j < subscripts; j++) { - const script = stack.top(-(4 + j)); + const script = stack.get(-(4 + j)); if (version === 0) { if ((scripts.written + script.length) > consensus.MAX_SCRIPT_SIZE) throw new ScriptError('SCRIPT_SIZE'); @@ -2974,7 +3451,7 @@ Script.verifyMast = function verifyMast(program, stack, output, flags, tx, index } scripts = scripts.render(); - output = new Script(scripts); + output = Script.fromRaw(scripts); output.execute(stack, flags, tx, index, value, 1); if (stack.length !== 0) @@ -3040,9 +3517,7 @@ Script.fromRaw = function fromRaw(data, enc) { */ Script.isScript = function isScript(obj) { - return obj - && Buffer.isBuffer(obj.raw) - && typeof obj.getSubscript === 'function'; + return obj instanceof Script; }; /* diff --git a/lib/script/scripterror.js b/lib/script/scripterror.js new file mode 100644 index 000000000..d910c5241 --- /dev/null +++ b/lib/script/scripterror.js @@ -0,0 +1,50 @@ +/*! + * scripterror.js - script error for bcoin + * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +/** + * An error thrown from the scripting system, + * potentially pertaining to Script execution. + * @alias module:script.ScriptError + * @constructor + * @extends Error + * @param {String} code - Error code. + * @param {Opcode} op - Opcode. + * @param {Number?} ip - Instruction pointer. + * @property {String} message - Error message. + * @property {String} code - Original code passed in. + * @property {Number} op - Opcode. + * @property {Number} ip - Instruction pointer. + */ + +function ScriptError(code, op, ip) { + if (!(this instanceof ScriptError)) + return new ScriptError(code, op, ip); + + Error.call(this); + + this.type = 'ScriptError'; + this.code = code; + this.message = code; + this.op = -1; + this.ip = -1; + + if (typeof op === 'string') { + this.message = op; + } else if (op) { + this.message = `${code} (op=${op.toSymbol()}, ip=${ip})`; + this.op = op.value; + this.ip = ip; + } + + if (Error.captureStackTrace) + Error.captureStackTrace(this, ScriptError); +}; + +Object.setPrototypeOf(ScriptError.prototype, Error.prototype); + +module.exports = ScriptError; diff --git a/lib/script/scriptnum.js b/lib/script/scriptnum.js index 3a39c3962..eec0fdf64 100644 --- a/lib/script/scriptnum.js +++ b/lib/script/scriptnum.js @@ -8,7 +8,7 @@ const assert = require('assert'); const {I64} = require('../utils/int64'); -const {ScriptError} = require('./common'); +const ScriptError = require('./scripterror'); /* * Constants diff --git a/lib/script/stack.js b/lib/script/stack.js index 4a66bf126..49e708902 100644 --- a/lib/script/stack.js +++ b/lib/script/stack.js @@ -10,10 +10,6 @@ const assert = require('assert'); const common = require('./common'); const ScriptNum = require('./scriptnum'); -const ScriptError = common.ScriptError; -const STACK_FALSE = common.STACK_FALSE; -const STACK_TRUE = common.STACK_TRUE; -const STACK_NEGATE = common.STACK_NEGATE; /** * Represents the stack of a Script during execution. @@ -45,6 +41,33 @@ Object.defineProperty(Stack.prototype, 'length', { } }); +/** + * Instantiate a key and value iterator. + * @returns {StackIterator} + */ + +Stack.prototype[Symbol.iterator] = function iterator() { + return this.items[Symbol.iterator](); +}; + +/** + * Instantiate a value-only iterator. + * @returns {StackIterator} + */ + +Stack.prototype.values = function values() { + return this.items.values(); +}; + +/** + * Instantiate a key and value iterator. + * @returns {StackIterator} + */ + +Stack.prototype.entries = function entries() { + return this.items.entries(); +}; + /** * Inspect the stack. * @returns {String} Human-readable stack. @@ -60,7 +83,12 @@ Stack.prototype.inspect = function inspect() { */ Stack.prototype.toString = function toString() { - return common.formatStack(this.items); + const out = []; + + for (const item of this.items) + out.push(item.toString('hex')); + + return out.join(' '); }; /** @@ -70,7 +98,12 @@ Stack.prototype.toString = function toString() { */ Stack.prototype.toASM = function toASM(decode) { - return common.formatStackASM(this.items, decode); + const out = []; + + for (const item of this.items) + out.push(common.toASM(item, decode)); + + return out.join(' '); }; /** @@ -83,114 +116,136 @@ Stack.prototype.clone = function clone() { }; /** - * Push item onto stack. - * @see Array#push - * @param {Buffer} item - * @returns {Number} Stack size. + * Clear the stack. + * @returns {Stack} */ -Stack.prototype.push = function push(item) { - assert(Buffer.isBuffer(item)); - return this.items.push(item); +Stack.prototype.clear = function clear() { + this.items.length = 0; + return this; }; /** - * Push boolean onto stack. - * @see Array#push - * @param {Boolean} value - * @returns {Number} Stack size. + * Get a stack item by index. + * @param {Number} index + * @returns {Buffer|null} */ -Stack.prototype.pushBool = function pushBool(value) { - assert(typeof value === 'boolean'); - return this.items.push(value ? STACK_TRUE : STACK_FALSE); +Stack.prototype.get = function get(index) { + if (index < 0) + index += this.items.length; + + if (index < 0 || index >= this.items.length) + return null; + + return this.items[index]; }; /** - * Push script number onto stack. - * @see Array#push - * @param {ScriptNum} num - * @returns {Number} Stack size. + * Pop a stack item. + * @see Array#pop + * @returns {Buffer|null} */ -Stack.prototype.pushNum = function pushNum(num) { - assert(ScriptNum.isScriptNum(num)); - return this.items.push(num.encode()); +Stack.prototype.pop = function pop() { + const item = this.items.pop(); + return item || null; }; /** - * Push integer onto stack. - * @see Array#push - * @param {Number} value - * @returns {Number} Stack size. + * Shift a stack item. + * @see Array#shift + * @returns {Buffer|null} */ -Stack.prototype.pushInt = function pushInt(value) { - assert(typeof value === 'number'); - - if (value >= -1 && value <= 16) { - switch (value) { - case -1: - return this.items.push(STACK_NEGATE); - case 0: - return this.items.push(STACK_FALSE); - case 1: - return this.items.push(STACK_TRUE); - } - const item = Buffer.allocUnsafe(1); - item[0] = value; - return this.items.push(item); - } +Stack.prototype.shift = function shift() { + const item = this.items.shift(); + return item || null; +}; + +/** + * Remove an item. + * @param {Number} index + * @returns {Buffer} + */ + +Stack.prototype.remove = function remove(index) { + if (index < 0) + index += this.items.length; + + if (index < 0 || index >= this.items.length) + return null; + + const items = this.items.splice(index, 1); - const num = ScriptNum.fromNumber(value); + if (items.length === 0) + return null; - return this.items.push(num.encode()); + return items[0]; }; /** - * Unshift item from stack. - * @see Array#unshift + * Set stack item at index. + * @param {Number} index + * @param {Buffer} value + * @returns {Buffer} + */ + +Stack.prototype.set = function set(index, item) { + if (index < 0) + index += this.items.length; + + assert(Buffer.isBuffer(item)); + assert(index >= 0 && index <= this.items.length); + + this.items[index] = item; + + return this; +}; + +/** + * Push item onto stack. + * @see Array#push * @param {Buffer} item - * @returns {Number} + * @returns {Number} Stack size. */ -Stack.prototype.unshift = function unshift(item) { +Stack.prototype.push = function push(item) { assert(Buffer.isBuffer(item)); - return this.items.unshift(item); + this.items.push(item); + return this; }; /** - * Slice out part of the stack items. - * @param {Number} start - * @param {Number} end - * @see Array#slice - * @returns {Stack} + * Unshift item from stack. + * @see Array#unshift + * @param {Buffer} item + * @returns {Number} */ -Stack.prototype.slice = function slice(start, end) { - this.items = this.items.slice(start, end); +Stack.prototype.unshift = function unshift(item) { + assert(Buffer.isBuffer(item)); + this.items.unshift(item); return this; }; /** - * Splice stack items. - * @see Array#splice + * Insert an item. * @param {Number} index - * @param {Number} remove - * @param {Buffer?} insert - * @returns {Buffer[]} + * @param {Buffer} item + * @returns {Buffer} */ -Stack.prototype.splice = function splice(i, remove, insert) { - if (i < 0) - i = this.items.length + i; +Stack.prototype.insert = function insert(index, item) { + if (index < 0) + index += this.items.length; - if (insert === undefined) - return this.items.splice(i, remove); + assert(Buffer.isBuffer(item)); + assert(index >= 0 && index <= this.items.length); - assert(Buffer.isBuffer(insert)); + this.items.splice(index, 0, item); - return this.items.splice(i, remove, insert); + return this; }; /** @@ -211,184 +266,228 @@ Stack.prototype.erase = function erase(start, end) { }; /** - * Insert an item. - * @param {Number} index - * @param {Buffer} item - * @returns {Buffer} + * Swap stack values. + * @param {Number} i1 - Index 1. + * @param {Number} i2 - Index 2. */ -Stack.prototype.insert = function insert(i, item) { - if (i < 0) - i = this.items.length + i; +Stack.prototype.swap = function swap(i1, i2) { + if (i1 < 0) + i1 = this.items.length + i1; - assert(Buffer.isBuffer(item)); + if (i2 < 0) + i2 = this.items.length + i2; + + const v1 = this.items[i1]; + const v2 = this.items[i2]; - this.items.splice(i, 0, item); + this.items[i1] = v2; + this.items[i2] = v1; }; -/** - * Remove an item. - * @param {Number} index - * @returns {Buffer} +/* + * Data */ -Stack.prototype.remove = function remove(i) { - if (i < 0) - i = this.items.length + i; +Stack.prototype.getData = function getData(index) { + return this.get(index); +}; - if (i >= this.items.length) - return undefined; +Stack.prototype.popData = function popData() { + return this.pop(); +}; - return this.items.splice(i, 1)[0]; +Stack.prototype.shiftData = function shiftData() { + return this.shift(); }; -/** - * Pop a stack item. - * @see Array#pop - * @returns {Buffer|null} - */ +Stack.prototype.removeData = function removeData(index) { + return this.remove(index); +}; -Stack.prototype.pop = function pop() { - return this.items.pop(); +Stack.prototype.setData = function setData(index, data) { + return this.set(index, data); }; -/** - * Shift a stack item. - * @see Array#shift - * @returns {Buffer|null} +Stack.prototype.pushData = function pushData(data) { + return this.push(data); +}; + +Stack.prototype.unshiftData = function unshiftData(data) { + return this.unshift(data); +}; + +Stack.prototype.insertData = function insertData(index, data) { + return this.insert(index, data); +}; + +/* + * Length */ -Stack.prototype.shift = function shift() { - return this.items.shift(); +Stack.prototype.getLength = function getLength(index) { + const item = this.get(index); + return item ? item.length : -1; }; -/** - * Get a stack item by index. - * @param {Number} index - * @returns {Buffer|null} +/* + * String */ -Stack.prototype.get = function get(i) { - if (i < 0) - i = this.items.length + i; +Stack.prototype.getString = function getString(index, enc) { + const item = this.get(index); + return item ? Stack.toString(item, enc) : null; +}; - return this.items[i]; +Stack.prototype.popString = function popString(enc) { + const item = this.pop(); + return item ? Stack.toString(item, enc) : null; }; -/** - * Get a stack item by index - * and decode as a boolean. - * @param {Number} index - * @returns {Boolean} - * @throws on invalid stack operation - */ +Stack.prototype.shiftString = function shiftString(enc) { + const item = this.shift(); + return item ? Stack.toString(item, enc) : null; +}; -Stack.prototype.bool = function bool(i) { - if (i < 0) - i = this.items.length + i; +Stack.prototype.removeString = function removeString(index, enc) { + const item = this.remove(index); + return item ? Stack.toString(item, enc) : null; +}; - if (i < 0 || i >= this.items.length) - throw new ScriptError('INVALID_STACK_OPERATION', -1, -1); +Stack.prototype.setString = function setString(index, str, enc) { + return this.set(index, Stack.fromString(str, enc)); +}; - return common.toBool(this.items[i]); +Stack.prototype.pushString = function pushString(str, enc) { + return this.push(Stack.fromString(str, enc)); }; -/** - * Get a stack item by index - * and decode as a scriptnum. - * @param {Number} index - * @param {Boolean?} minimal - * @param {Number?} limit - * @returns {ScriptNum} - * @throws on invalid stack operation +Stack.prototype.unshiftString = function unshiftString(str, enc) { + return this.unshift(Stack.fromString(str, enc)); +}; + +Stack.prototype.insertString = function insertString(index, str, enc) { + return this.insert(index, Stack.fromString(str, enc)); +}; + +/* + * Num */ -Stack.prototype.num = function num(i, minimal, limit) { - if (i < 0) - i = this.items.length + i; +Stack.prototype.getNum = function getNum(index, minimal, limit) { + const item = this.get(index); + return item ? Stack.toNum(item, minimal, limit) : null; +}; - if (i < 0 || i >= this.items.length) - throw new ScriptError('INVALID_STACK_OPERATION', -1, -1); +Stack.prototype.popNum = function popNum(minimal, limit) { + const item = this.pop(); + return item ? Stack.toNum(item, minimal, limit) : null; +}; - return ScriptNum.decode(this.items[i], minimal, limit); +Stack.prototype.shiftNum = function shiftNum(minimal, limit) { + const item = this.shift(); + return item ? Stack.toNum(item, minimal, limit) : null; }; -/** - * Get a stack item by index - * and decode as an integer. - * @param {Number} index - * @param {Boolean?} minimal - * @returns {Number} - * @throws on invalid stack operation - */ +Stack.prototype.removeNum = function removeNum(index, minimal, limit) { + const item = this.remove(index); + return item ? Stack.toNum(item, minimal, limit) : null; +}; -Stack.prototype.int = function int(i, minimal) { - if (i < 0) - i = this.items.length + i; +Stack.prototype.setNum = function setNum(index, num) { + return this.set(index, Stack.fromNum(num)); +}; - if (i < 0 || i >= this.items.length) - throw new ScriptError('INVALID_STACK_OPERATION', -1, -1); +Stack.prototype.pushNum = function pushNum(num) { + return this.push(Stack.fromNum(num)); +}; - return ScriptNum.decode(this.items[i], minimal).getInt(); +Stack.prototype.unshiftNum = function unshiftNum(num) { + return this.unshift(Stack.fromNum(num)); }; -/** - * Get a stack item relative to - * the top of the stack. - * @example - * stack.top(-1); - * @param {Number} index - * @returns {Buffer|null} +Stack.prototype.insertNum = function insertNum(index, num) { + return this.insert(index, Stack.fromNum(num)); +}; + +/* + * Int */ -Stack.prototype.top = function top(i) { - return this.items[this.items.length + i]; +Stack.prototype.getInt = function getInt(index, minimal, limit) { + const item = this.get(index); + return item ? Stack.toInt(item, minimal, limit) : -1; }; -/** - * Clear the stack. - */ +Stack.prototype.popInt = function popInt(minimal, limit) { + const item = this.pop(); + return item ? Stack.toInt(item, minimal, limit) : -1; +}; -Stack.prototype.clear = function clear() { - this.items.length = 0; +Stack.prototype.shiftInt = function shiftInt(minimal, limit) { + const item = this.shift(); + return item ? Stack.toInt(item, minimal, limit) : -1; }; -/** - * Set stack item at index. - * @param {Number} index - * @param {Buffer} value - * @returns {Buffer} - */ +Stack.prototype.removeInt = function removeInt(index, minimal, limit) { + const item = this.remove(index); + return item ? Stack.toInt(item, minimal, limit) : -1; +}; -Stack.prototype.set = function set(i, value) { - if (i < 0) - i = this.items.length + i; +Stack.prototype.setInt = function setInt(index, num) { + return this.set(index, Stack.fromInt(num)); +}; - assert(Buffer.isBuffer(value)); +Stack.prototype.pushInt = function pushInt(num) { + return this.push(Stack.fromInt(num)); +}; - this.items[i] = value; +Stack.prototype.unshiftInt = function unshiftInt(num) { + return this.unshift(Stack.fromInt(num)); +}; - return value; +Stack.prototype.insertInt = function insertInt(index, num) { + return this.insert(index, Stack.fromInt(num)); }; -/** - * Swap stack values. - * @param {Number} i1 - Index 1. - * @param {Number} i2 - Index 2. +/* + * Bool */ -Stack.prototype.swap = function swap(i1, i2) { - if (i1 < 0) - i1 = this.items.length + i1; +Stack.prototype.getBool = function getBool(index) { + const item = this.get(index); + return item ? Stack.toBool(item) : false; +}; - if (i2 < 0) - i2 = this.items.length + i2; +Stack.prototype.popBool = function popBool() { + const item = this.pop(); + return item ? Stack.toBool(item) : false; +}; - const v1 = this.items[i1]; - const v2 = this.items[i2]; +Stack.prototype.shiftBool = function shiftBool() { + const item = this.shift(); + return item ? Stack.toBool(item) : false; +}; - this.items[i1] = v2; - this.items[i2] = v1; +Stack.prototype.removeBool = function removeBool(index) { + const item = this.remove(index); + return item ? Stack.toBool(item) : false; +}; + +Stack.prototype.setBool = function setBool(index, value) { + return this.set(index, Stack.fromBool(value)); +}; + +Stack.prototype.pushBool = function pushBool(value) { + return this.push(Stack.fromBool(value)); +}; + +Stack.prototype.unshiftBool = function unshiftBool(value) { + return this.unshift(Stack.fromBool(value)); +}; + +Stack.prototype.insertBool = function insertBool(index, value) { + return this.insert(index, Stack.fromBool(value)); }; /** @@ -398,7 +497,73 @@ Stack.prototype.swap = function swap(i1, i2) { */ Stack.isStack = function isStack(obj) { - return obj && Array.isArray(obj.items) && typeof obj.swap === 'function'; + return obj instanceof Stack; +}; + +/* + * Encoding + */ + +Stack.toString = function toString(item, enc) { + assert(Buffer.isBuffer(item)); + return item.toString(enc || 'utf8'); +}; + +Stack.fromString = function fromString(str, enc) { + assert(typeof str === 'string'); + return Buffer.from(str, enc || 'utf8'); +}; + +Stack.toNum = function toNum(item, minimal, limit) { + assert(Buffer.isBuffer(item)); + return ScriptNum.decode(item, minimal, limit); +}; + +Stack.fromNum = function fromNum(num) { + assert(ScriptNum.isScriptNum(num)); + return num.encode(); +}; + +Stack.toInt = function toInt(item, minimal, limit) { + assert(Buffer.isBuffer(item)); + + const num = Stack.toNum(item, minimal, limit); + + if (!num) + return -1; + + return num.getInt(); +}; + +Stack.fromInt = function fromInt(int) { + assert(typeof int === 'number'); + + if (int >= -1 && int <= 16) + return common.small[int + 1]; + + const num = ScriptNum.fromNumber(int); + + return Stack.fromNum(num); +}; + +Stack.toBool = function toBool(item) { + assert(Buffer.isBuffer(item)); + + for (let i = 0; i < item.length; i++) { + if (item[i] !== 0) { + // Cannot be negative zero + if (i === item.length - 1 && item[i] === 0x80) + return false; + return true; + } + } + + return false; +}; + +Stack.fromBool = function fromBool(value) { + assert(typeof value === 'boolean'); + return Stack.fromInt(value ? 1 : 0); }; /* diff --git a/lib/script/witness.js b/lib/script/witness.js index 073e61fc5..001f9ef34 100644 --- a/lib/script/witness.js +++ b/lib/script/witness.js @@ -8,21 +8,15 @@ 'use strict'; const assert = require('assert'); -const ScriptNum = require('./scriptnum'); const util = require('../utils/util'); const Script = require('./script'); const common = require('./common'); const encoding = require('../utils/encoding'); -const Opcode = require('./opcode'); const BufferReader = require('../utils/reader'); const StaticWriter = require('../utils/staticwriter'); const Address = require('../primitives/address'); const Stack = require('./stack'); -const opcodes = common.opcodes; const scriptTypes = common.types; -const STACK_TRUE = common.STACK_TRUE; -const STACK_FALSE = common.STACK_FALSE; -const STACK_NEGATE = common.STACK_NEGATE; /** * Refers to the witness field of segregated witness transactions. @@ -39,25 +33,13 @@ function Witness(options) { if (!(this instanceof Witness)) return new Witness(options); - this.items = []; + Stack.call(this, []); if (options) this.fromOptions(options); } -/* - * Expose length setter and getter. - */ - -Object.defineProperty(Witness.prototype, 'length', { - get() { - return this.items.length; - }, - set(length) { - this.items.length = length; - return this.items.length; - } -}); +Object.setPrototypeOf(Witness.prototype, Stack.prototype); /** * Inject properties from options object. @@ -119,31 +101,72 @@ Witness.fromArray = function fromArray(items) { }; /** - * Inspect a Witness object. - * @returns {String} Human-readable script. + * Convert witness to an array of buffers. + * @returns {Buffer[]} */ -Witness.prototype.inspect = function inspect() { - return ``; +Witness.prototype.toItems = function toItems() { + return this.items.slice(); }; /** - * Convert a Witness object to a String. - * @returns {String} Human-readable script. + * Inject properties from an array of buffers. + * @private + * @param {Buffer[]} items + */ + +Witness.prototype.fromItems = function fromItems(items) { + assert(Array.isArray(items)); + this.items = items; + return this; +}; + +/** + * Insantiate witness from an array of buffers. + * @param {Buffer[]} items + * @returns {Witness} */ -Witness.prototype.toString = function toString() { - return common.formatStack(this.items); +Witness.fromItems = function fromItems(items) { + return new Witness().fromItems(items); }; /** - * Format the witness object as bitcoind asm. - * @param {Boolean?} decode - Attempt to decode hash types. + * Convert witness to a stack. + * @returns {Stack} + */ + +Witness.prototype.toStack = function toStack() { + return new Stack(this.toArray()); +}; + +/** + * Inject properties from a stack. + * @private + * @param {Stack} stack + */ + +Witness.prototype.fromStack = function fromStack(stack) { + return this.fromArray(stack.items); +}; + +/** + * Insantiate witness from a stack. + * @param {Stack} stack + * @returns {Witness} + */ + +Witness.fromStack = function fromStack(stack) { + return new Witness().fromStack(stack); +}; + +/** + * Inspect a Witness object. * @returns {String} Human-readable script. */ -Witness.prototype.toASM = function toASM(decode) { - return common.formatStackASM(this.items, decode); +Witness.prototype.inspect = function inspect() { + return ``; }; /** @@ -169,14 +192,12 @@ Witness.prototype.inject = function inject(witness) { }; /** - * Convert the Witness to a Stack object. - * This is usually done before executing - * a witness program. - * @returns {Stack} + * Compile witness (NOP). + * @returns {Witness} */ -Witness.prototype.toStack = function toStack() { - return new Stack(this.items.slice()); +Witness.prototype.compile = function compile() { + return this; }; /** @@ -215,6 +236,16 @@ Witness.prototype.isPubkeyInput = function isPubkeyInput() { return false; }; +/** + * Get P2PK signature if present. + * Always returns null. + * @returns {Buffer|null} + */ + +Witness.prototype.getPubkeyInput = function getPubkeyInput() { + return null; +}; + /** * "Guess" whether the witness is a pubkeyhash input. * This method is not 100% reliable. @@ -227,6 +258,17 @@ Witness.prototype.isPubkeyhashInput = function isPubkeyhashInput() { && common.isKeyEncoding(this.items[1]); }; +/** + * Get P2PKH signature and key if present. + * @returns {Array} [sig, key] + */ + +Witness.prototype.getPubkeyhashInput = function getPubkeyhashInput() { + if (!this.isPubkeyhashInput()) + return [null, null]; + return [this.items[0], this.items[1]]; +}; + /** * "Test" whether the witness is a multisig input. * Always returns false. @@ -237,6 +279,16 @@ Witness.prototype.isMultisigInput = function isMultisigInput() { return false; }; +/** + * Get multisig signatures key if present. + * Always returns null. + * @returns {Buffer[]|null} + */ + +Witness.prototype.getMultisigInput = function getMultisigInput() { + return null; +}; + /** * "Guess" whether the witness is a scripthash input. * This method is not 100% reliable. @@ -247,6 +299,17 @@ Witness.prototype.isScripthashInput = function isScripthashInput() { return this.items.length > 0 && !this.isPubkeyhashInput(); }; +/** + * Get P2SH redeem script if present. + * @returns {Buffer|null} + */ + +Witness.prototype.getScripthashInput = function getScripthashInput() { + if (!this.isScripthashInput()) + return null; + return this.items[this.items.length - 1]; +}; + /** * "Guess" whether the witness is an unknown/non-standard type. * This method is not 100% reliable. @@ -292,14 +355,6 @@ Witness.prototype.getRedeem = function getRedeem() { return Script.fromRaw(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. @@ -390,179 +445,6 @@ Witness.fromJSON = function fromJSON(json) { return new Witness().fromJSON(json); }; -/** - * Unshift an item onto the witness vector. - * @param {Number|String|Buffer|ScriptNum} data - * @returns {Number} - */ - -Witness.prototype.unshift = function unshift(data) { - return this.items.unshift(Witness.encodeItem(data)); -}; - -/** - * Push an item onto the witness vector. - * @param {Number|String|Buffer|ScriptNum} data - * @returns {Number} - */ - -Witness.prototype.push = function push(data) { - return this.items.push(Witness.encodeItem(data)); -}; - -/** - * Shift an item off the witness vector. - * @returns {Buffer} - */ - -Witness.prototype.shift = function shift() { - return this.items.shift(); -}; - -/** - * Shift an item off the witness vector. - * @returns {Buffer} - */ - -Witness.prototype.pop = function pop(data) { - return this.items.pop(); -}; - -/** - * Remove an item from the witness vector. - * @param {Number} index - * @returns {Buffer} - */ - -Witness.prototype.remove = function remove(i) { - return this.items.splice(i, 1)[0]; -}; - -/** - * Insert an item into the witness vector. - * @param {Number} index - * @param {Number|String|Buffer|ScriptNum} data - */ - -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]; -}; - -/** - * Get an item from the witness vector. - * @param {Number} index - * @returns {Buffer} - */ - -Witness.prototype.get = function get(i) { - return this.items[i]; -}; - -/** - * Get a small int (0-16) from the witness vector. - * @param {Number} index - * @returns {Number} `-1` on non-existent. - */ - -Witness.prototype.getSmall = function getSmall(i) { - const item = this.items[i]; - if (!item || item.length > 1) - return -1; - if (item.length === 0) - return 0; - if (!(item[0] >= 1 && item[1] <= 16)) - return -1; - return item[0]; -}; - -/** - * Get a number from the witness vector. - * @param {Number} index - * @returns {ScriptNum} - */ - -Witness.prototype.getNumber = function getNumber(i) { - const item = this.items[i]; - - if (!item || item.length > 5) - return null; - - return ScriptNum.decode(item, false, 5); -}; - -/** - * Get a string from the witness vector. - * @param {Number} index - * @returns {String} - */ - -Witness.prototype.getString = function getString(i) { - const item = this.items[i]; - - if (!item) - return null; - - return item.toString('utf8'); -}; - -/** - * Clear the witness items. - */ - -Witness.prototype.clear = function clear() { - this.items.length = 0; -}; - -/** - * Set an item in the witness vector. - * @param {Number} index - * @param {Number|String|Buffer|ScriptNum} data - */ - -Witness.prototype.set = function set(i, data) { - assert(i <= this.items.length, 'Index out of bounds.'); - this.items[i] = Witness.encodeItem(data); -}; - -/** - * Encode a witness item. - * @param {Number|String|Buffer|ScriptNum} data - * @returns {Buffer} - */ - -Witness.encodeItem = function encodeItem(data) { - if (data instanceof Opcode) - data = data.data || data.value; - - if (Buffer.isBuffer(data)) - return data; - - if (typeof data === 'number') { - if (data === opcodes.OP_1NEGATE) - return STACK_NEGATE; - - if (data === opcodes.OP_0) - return STACK_FALSE; - - if (data >= opcodes.OP_1 && data <= opcodes.OP_16) - return Buffer.from([data - 0x50]); - - throw new Error('Non-push opcode in witness.'); - } - - if (typeof data === 'string') - return Buffer.from(data, 'utf8'); - - if (typeof data === 'boolean') - return data ? STACK_TRUE : STACK_FALSE; - - if (ScriptNum.isEncodable(data)) - return ScriptNum.encode(data); - - throw new Error('Not a witness item.'); -}; - /** * Inject properties from buffer reader. * @private @@ -655,9 +537,7 @@ Witness.fromString = function fromString(items) { */ Witness.isWitness = function isWitness(obj) { - return obj - && Array.isArray(obj.items) - && typeof obj.toStack === 'function'; + return obj instanceof Witness; }; /* diff --git a/lib/utils/enforce.js b/lib/utils/enforce.js new file mode 100644 index 000000000..34b363bce --- /dev/null +++ b/lib/utils/enforce.js @@ -0,0 +1,165 @@ +/*! + * enforce.js - type enforcement for bcoin + * Copyright (c) 2014-2017, Christopher Jeffrey (MIT License). + * https://github.com/bcoin-org/bcoin + */ + +'use strict'; + +const util = require('./util'); + +function enforce(value, name, type, func) { + if (!value) { + if (!func) + func = enforce; + + if (name && !type) + throwError(name, func); + + if (!name) + name = 'value'; + + throwError(`'${name}' must be a(n) ${type}.`, func); + } +} + +function throwError(msg, func) { + const error = new TypeError(msg); + if (Error.captureStackTrace && func) + Error.captureStackTrace(error, func); + throw error; +} + +enforce.none = function none(value, name) { + enforce(value == null, name, 'object', none); +}; + +enforce.nul = function nul(value, name) { + enforce(value === null, name, 'object', nul); +}; + +enforce.undef = function undef(value, name) { + enforce(value === undefined, name, 'object', undef); +}; + +enforce.str = function str(value, name) { + enforce(typeof value === 'string', name, 'string', str); +}; + +enforce.bool = function bool(value, name) { + enforce(typeof value === 'boolean', name, 'boolean', bool); +}; + +enforce.num = function num(value, name) { + enforce(util.isNumber(value), name, 'number', num); +}; + +enforce.obj = function obj(v, name) { + enforce(v && typeof v === 'object' && !Array.isArray(v), name, 'object', obj); +}; + +enforce.array = function array(value, name) { + enforce(Array.isArray(value), name, 'object', array); +}; + +enforce.func = function func(value, name) { + enforce(typeof value === 'function', name, 'function', func); +}; + +enforce.error = function error(value, name) { + enforce(value instanceof Error, name, 'object', error); +}; + +enforce.regexp = function regexp(value, name) { + enforce(value && typeof value.exec === 'function' , name, 'object', regexp); +}; + +enforce.buf = function buf(value, name) { + enforce(Buffer.isBuffer(value), name, 'buffer', buf); +}; + +enforce.len = function len(value, length, name) { + if ((typeof value !== 'string' && !value) || value.length !== length) { + if (!name) + name = 'value'; + throwError(`'${name}' must have a length of ${length}.`, len); + } +}; + +enforce.instance = function instance(obj, parent, name) { + if (!(obj instanceof parent)) { + if (!name) + name = 'value'; + throwError(`'${name}' must be an instance of ${parent.name}.`, instance); + } +}; + +enforce.uint = function uint(value, name) { + enforce(util.isUInt(value), name, 'uint', uint); +}; + +enforce.int = function int(value, name) { + enforce(util.isInt(value), name, 'int', int); +}; + +enforce.u8 = function u8(value, name) { + enforce(util.isU8(value), name, 'uint8', u8); +}; + +enforce.u16 = function u16(value, name) { + enforce(util.isU16(value), name, 'uint16', u16); +}; + +enforce.u32 = function u32(value, name) { + enforce(util.isU32(value), name, 'uint32', u32); +}; + +enforce.u64 = function u64(value, name) { + enforce(util.isU64(value), name, 'uint64', u64); +}; + +enforce.i8 = function i8(value, name) { + enforce(util.isI8(value), name, 'int8', i8); +}; + +enforce.i16 = function i16(value, name) { + enforce(util.isI16(value), name, 'int16', i16); +}; + +enforce.i32 = function i32(value, name) { + enforce(util.isI32(value), name, 'int32', i32); +}; + +enforce.i64 = function i64(value, name) { + enforce(util.isI64(value), name, 'int64', i64); +}; + +enforce.ufloat = function ufloat(value, name) { + enforce(util.isUfloat(value), name, 'positive float', ufloat); +}; + +enforce.float = function float(value, name) { + enforce(util.isFloat(value), name, 'float', float); +}; + +enforce.ascii = function ascii(value, name) { + enforce(util.isAscii(value), name, 'ascii string', ascii); +}; + +enforce.hex = function hex(value, name) { + enforce(util.isHex(value), name, 'hex string', hex); +}; + +enforce.hex160 = function hex160(value, name) { + enforce(util.isHex160(value), name, '160 bit hex string', hex160); +}; + +enforce.hex256 = function hex256(value, name) { + enforce(util.isHex256(value), name, '256 bit hex string', hex256); +}; + +enforce.base58 = function base58(value, name) { + enforce(util.isBase58(value), name, 'base58 string', base58); +}; + +module.exports = enforce; diff --git a/lib/utils/index.js b/lib/utils/index.js index e36f5a181..bac0d298c 100644 --- a/lib/utils/index.js +++ b/lib/utils/index.js @@ -19,6 +19,7 @@ exports.bech32 = require('./bech32'); exports.Bloom = require('./bloom'); exports.co = require('./co'); exports.encoding = require('./encoding'); +exports.enforce = require('./enforce'); exports.fs = require('./fs'); exports.GCSFilter = require('./gcs'); exports.Heap = require('./heap'); diff --git a/lib/utils/util.js b/lib/utils/util.js index a39024c1c..ffe898ef1 100644 --- a/lib/utils/util.js +++ b/lib/utils/util.js @@ -281,6 +281,7 @@ util.inspectify = function inspectify(obj, color) { return obj; inspectOptions.colors = color !== false; + return nodeUtil.inspect(obj, inspectOptions); }; diff --git a/lib/wallet/wallet.js b/lib/wallet/wallet.js index 06daedcdf..4257e1328 100644 --- a/lib/wallet/wallet.js +++ b/lib/wallet/wallet.js @@ -57,7 +57,7 @@ const Mnemonic = HD.Mnemonic; * @param {Number?} options.m - `m` value for multisig. * @param {Number?} options.n - `n` value for multisig. * @param {String?} options.id - Wallet ID (used for storage) - * @param {String?} options.mnemonic - mnemonic phrase to use to instantiate an + * @param {String?} options.mnemonic - mnemonic phrase to use to instantiate an * hd private key for wallet * (default=account key "address"). */ @@ -1643,6 +1643,7 @@ Wallet.prototype.increaseFee = async function increaseFee(hash, rate, passphrase throw new Error('Not all coins available.'); const oldFee = tx.getFee(view); + let fee = tx.getMinFee(null, rate); if (fee > MTX.Selector.MAX_FEE) @@ -1655,10 +1656,8 @@ Wallet.prototype.increaseFee = async function increaseFee(hash, rate, passphrase mtx.view = view; for (const input of mtx.inputs) { - input.script.length = 0; - input.script.compile(); - input.witness.length = 0; - input.witness.compile(); + input.script.clear(); + input.witness.clear(); } let change; diff --git a/lib/workers/packets.js b/lib/workers/packets.js index 9b9abf75f..814663e50 100644 --- a/lib/workers/packets.js +++ b/lib/workers/packets.js @@ -20,7 +20,7 @@ const MTX = require('../primitives/mtx'); const TX = require('../primitives/tx'); const KeyRing = require('../primitives/keyring'); const CoinView = require('../coins/coinview'); -const {ScriptError} = require('../script/common'); +const ScriptError = require('../script/scripterror'); /* * Constants diff --git a/scripts/fuzz.js b/scripts/fuzz.js index 03c72717b..357826f6d 100644 --- a/scripts/fuzz.js +++ b/scripts/fuzz.js @@ -9,10 +9,10 @@ const Output = require('../lib/primitives/output'); const Outpoint = require('../lib/primitives/outpoint'); const TX = require('../lib/primitives/tx'); const random = require('../lib/crypto/random'); +const flags = Script.flags; -const MANDATORY = Script.flags.MANDATORY_VERIFY_FLAGS - | Script.flags.VERIFY_WITNESS; -const STANDARD = Script.flags.STANDARD_VERIFY_FLAGS; +const MANDATORY = flags.MANDATORY_VERIFY_FLAGS | flags.VERIFY_WITNESS; +const STANDARD = flags.STANDARD_VERIFY_FLAGS; function randomOutpoint() { const hash = random.randomBytes(32).toString('hex'); @@ -36,14 +36,13 @@ function randomTX() { const tx = new TX(); const inputs = util.random(1, 5); const outputs = util.random(0, 5); - let i; tx.version = util.random(0, 0xffffffff); - for (i = 0; i < inputs; i++) + for (let i = 0; i < inputs; i++) tx.inputs.push(randomInput()); - for (i = 0; i < outputs; i++) + for (let i = 0; i < outputs; i++) tx.inputs.push(randomOutput()); if (util.random(0, 5) === 0) @@ -57,10 +56,9 @@ function randomTX() { function randomWitness(redeem) { const size = util.random(1, 100); const witness = new Witness(); - let i, len; - for (i = 0; i < size; i++) { - len = util.random(0, 100); + for (let i = 0; i < size; i++) { + const len = util.random(0, 100); witness.push(random.randomBytes(len)); } @@ -75,19 +73,16 @@ function randomWitness(redeem) { function randomInputScript(redeem) { const size = util.random(1, 100); const script = new Script(); - let i, len; - for (i = 0; i < size; i++) { - len = util.random(0, 100); - script.push(random.randomBytes(len)); + for (let i = 0; i < size; i++) { + const len = util.random(0, 100); + script.pushData(random.randomBytes(len)); } if (redeem) - script.push(redeem); + script.pushData(redeem); - script.compile(); - - return script; + return script.compile(); } function randomOutputScript() { @@ -96,14 +91,10 @@ function randomOutputScript() { } function isPushOnly(script) { - let i, op; - if (script.isPushOnly()) return true; - for (i = 0; i < script.code.length; i++) { - op = script.code[i]; - + for (const op of script.code) { if (op.value === Script.opcodes.NOP) continue; @@ -132,10 +123,9 @@ function randomMultisig() { const n = util.random(1, 16); const m = util.random(1, n); const keys = []; - let i, len; - for (i = 0; i < n; i++) { - len = util.random(0, 2) === 0 ? 33 : 65; + for (let i = 0; i < n; i++) { + const len = util.random(0, 2) === 0 ? 33 : 65; keys.push(random.randomBytes(len)); } @@ -247,7 +237,7 @@ function randomWitnessNestedContext() { const redeem = randomRedeem(); const program = Script.fromProgram(0, redeem.sha256()); return { - input: new Script([program.toRaw()]), + input: Script.fromItems([program.toRaw()]), witness: randomWitness(redeem.toRaw()), output: Script.fromScripthash(program.hash160()), redeem: redeem @@ -275,7 +265,6 @@ function randomContext() { function fuzzSimple(flags) { let tx = randomTX(); let total = -1; - let stack, input, output; for (;;) { if (++total % 1000 === 0) @@ -284,8 +273,8 @@ function fuzzSimple(flags) { if (total % 500 === 0) tx = randomTX(); - stack = new Stack(); - input = randomInputScript(); + const stack = new Stack(); + const input = randomInputScript(); try { input.execute(stack, flags, tx, 0, 0, 0); @@ -295,7 +284,7 @@ function fuzzSimple(flags) { throw e; } - output = randomOutputScript(); + const output = randomOutputScript(); try { output.execute(stack, flags, tx, 0, 0, 0); @@ -308,7 +297,7 @@ function fuzzSimple(flags) { if (stack.length === 0) continue; - if (!stack.bool(-1)) + if (!stack.getBool(-1)) continue; if (isPushOnly(output)) @@ -332,7 +321,6 @@ function fuzzSimple(flags) { function fuzzVerify(flags) { let tx = randomTX(); let total = -1; - let input, output, witness; for (;;) { if (++total % 1000 === 0) @@ -341,9 +329,9 @@ function fuzzVerify(flags) { if (total % 500 === 0) tx = randomTX(); - input = randomInputScript(); - witness = randomWitness(); - output = randomOutputScript(); + const input = randomInputScript(); + const witness = randomWitness(); + const output = randomOutputScript(); try { Script.verify( @@ -382,7 +370,6 @@ function fuzzVerify(flags) { function fuzzLess(flags) { let tx = randomTX(); let total = -1; - let ctx; for (;;) { if (++total % 1000 === 0) @@ -391,7 +378,7 @@ function fuzzLess(flags) { if (total % 500 === 0) tx = randomTX(); - ctx = randomContext(); + const ctx = randomContext(); try { Script.verify( diff --git a/scripts/gen.js b/scripts/gen.js index 0d35288f1..e3fe3d3d4 100644 --- a/scripts/gen.js +++ b/scripts/gen.js @@ -6,13 +6,10 @@ const encoding = require('../lib/utils/encoding'); const TX = require('../lib/primitives/tx'); const Block = require('../lib/primitives/block'); const Script = require('../lib/script/script'); -const Opcode = require('../lib/script/opcode'); -const ScriptNum = require('../lib/script/scriptnum'); -const opcodes = Script.opcodes; function createGenesisBlock(options) { let flags = options.flags; - let script = options.script; + let key = options.key; let reward = options.reward; if (!flags) { @@ -21,13 +18,11 @@ function createGenesisBlock(options) { 'ascii'); } - if (!script) { - script = Script.fromArray([ - Buffer.from('04678afdb0fe5548271967f1a67130b7105cd6a828e039' - + '09a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c3' - + '84df7ba0b8d578a4c702b6bf11d5f', 'hex'), - opcodes.OP_CHECKSIG - ]); + if (!key) { + key = Buffer.from('' + + '04678afdb0fe5548271967f1a67130b7105cd6a828e039' + + '09a67962e0ea1f61deb649f6bc3f4cef38c4f35504e51ec112de5c3' + + '84df7ba0b8d578a4c702b6bf11d5f', 'hex'); } if (!reward) @@ -40,16 +35,16 @@ function createGenesisBlock(options) { hash: encoding.NULL_HASH, index: 0xffffffff }, - script: [ - Opcode.fromNumber(new ScriptNum(486604799)), - Opcode.fromPush(Buffer.from([4])), - Opcode.fromData(flags) - ], + script: Script() + .pushInt(486604799) + .pushPush(Buffer.from([4])) + .pushData(flags) + .compile(), sequence: 0xffffffff }], outputs: [{ value: reward, - script: script + script: Script.fromPubkey(key) }], locktime: 0 }); diff --git a/test/chain-test.js b/test/chain-test.js index 693c0e86c..c3785303b 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -4,7 +4,6 @@ 'use strict'; const assert = require('./util/assert'); -const ScriptNum = require('../lib/script/scriptnum'); const consensus = require('../lib/protocol/consensus'); const encoding = require('../lib/utils/encoding'); const Coin = require('../lib/primitives/coin'); @@ -17,6 +16,7 @@ const MemWallet = require('./util/memwallet'); const Network = require('../lib/protocol/network'); const Output = require('../lib/primitives/output'); const common = require('../lib/blockchain/common'); +const Opcode = require('../lib/script/opcode'); const opcodes = Script.opcodes; const network = Network.get('regtest'); @@ -79,8 +79,8 @@ async function mineCSV(fund) { spend.addOutput({ script: [ - ScriptNum.encode(1), - Script.opcodes.OP_CHECKSEQUENCEVERIFY + Opcode.fromInt(1), + Opcode.fromSymbol('checksequenceverify') ], value: 10000 }); @@ -419,8 +419,8 @@ describe('Chain', function() { spend.addOutput({ script: [ - ScriptNum.encode(2), - Script.opcodes.OP_CHECKSEQUENCEVERIFY + Opcode.fromInt(2), + Opcode.fromSymbol('checksequenceverify') ], value: 10000 }); @@ -444,8 +444,8 @@ describe('Chain', function() { spend.addOutput({ script: [ - ScriptNum.encode(1), - Script.opcodes.OP_CHECKSEQUENCEVERIFY + Opcode.fromInt(1), + Opcode.fromSymbol('checksequenceverify') ], value: 1 * 1e8 }); @@ -479,8 +479,8 @@ describe('Chain', function() { spend.addOutput({ script: [ - ScriptNum.encode(2), - Script.opcodes.OP_CHECKSEQUENCEVERIFY + Opcode.fromInt(2), + Opcode.fromSymbol('checksequenceverify') ], value: 1 * 1e8 }); @@ -547,7 +547,6 @@ describe('Chain', function() { const tx = block.txs[0]; const input = tx.inputs[0]; input.witness.set(0, Buffer.allocUnsafe(33)); - input.witness.compile(); block.refresh(true); assert.strictEqual(await addBlock(block), 'bad-witness-nonce-size'); }); @@ -557,7 +556,6 @@ describe('Chain', function() { const tx = block.txs[0]; const input = tx.inputs[0]; input.witness.set(0, encoding.ONE_HASH); - input.witness.compile(); block.refresh(true); assert.strictEqual(await addBlock(block), 'bad-witness-merkle-match'); }); @@ -570,9 +568,9 @@ describe('Chain', function() { assert(output.script.isCommitment()); - const commit = Buffer.from(output.script.get(1)); + const commit = Buffer.from(output.script.getData(1)); commit.fill(0, 10); - output.script.set(1, commit); + output.script.setData(1, commit); output.script.compile(); block.refresh(true); @@ -787,13 +785,14 @@ describe('Chain', function() { const flags = common.flags.DEFAULT_FLAGS & ~common.flags.VERIFY_POW; const redeem = new Script(); - redeem.push(new ScriptNum(20)); + redeem.pushInt(20); for (let i = 0; i < 20; i++) - redeem.push(encoding.ZERO_KEY); + redeem.pushData(encoding.ZERO_KEY); + + redeem.pushInt(20); + redeem.pushOp(opcodes.OP_CHECKMULTISIG); - redeem.push(new ScriptNum(20)); - redeem.push(opcodes.OP_CHECKMULTISIG); redeem.compile(); const script = Script.fromScripthash(redeem.hash160()); @@ -828,13 +827,15 @@ describe('Chain', function() { const job = await cpu.createJob(); const script = new Script(); - script.push(new ScriptNum(20)); + + script.pushInt(20); for (let i = 0; i < 20; i++) - script.push(encoding.ZERO_KEY); + script.pushData(encoding.ZERO_KEY); + + script.pushInt(20); + script.pushOp(opcodes.OP_CHECKMULTISIG); - script.push(new ScriptNum(20)); - script.push(opcodes.OP_CHECKMULTISIG); script.compile(); for (let i = start; i <= end; i++) { @@ -848,7 +849,7 @@ describe('Chain', function() { for (let j = 2; j < cb.outputs.length; j++) { mtx.addTX(cb, j); - mtx.inputs[j - 2].script = new Script([script.toRaw()]); + mtx.inputs[j - 2].script.fromItems([script.toRaw()]); } mtx.addOutput(witWallet.getAddress(), 1); diff --git a/test/mempool-test.js b/test/mempool-test.js index 0fb934fb2..635673371 100644 --- a/test/mempool-test.js +++ b/test/mempool-test.js @@ -81,7 +81,7 @@ describe('Mempool', function() { const sig = t1.signature(0, script, 70000, key.privateKey, ALL, 0); - t1.inputs[0].script = new Script([sig]); + t1.inputs[0].script = Script.fromItems([sig]); // balance: 51000 wallet.sign(t1); @@ -126,8 +126,9 @@ describe('Mempool', function() { wallet.template(fake); // Fake signature - fake.inputs[0].script.set(0, encoding.ZERO_SIG); - fake.inputs[0].script.compile(); + const input = fake.inputs[0]; + input.script.setData(0, encoding.ZERO_SIG); + input.script.compile(); // balance: 11000 { @@ -188,7 +189,7 @@ describe('Mempool', function() { chain.tip.height = 200; const sig = tx.signature(0, prev, 70000, key.privateKey, ALL, 0); - tx.inputs[0].script = new Script([sig]); + tx.inputs[0].script = Script.fromItems([sig]); await mempool.addTX(tx.toTX()); chain.tip.height = 0; @@ -209,7 +210,7 @@ describe('Mempool', function() { chain.tip.height = 200 - 1; const sig = tx.signature(0, prev, 70000, key.privateKey, ALL, 0); - tx.inputs[0].script = new Script([sig]); + tx.inputs[0].script = Script.fromItems([sig]); let err; try { @@ -268,7 +269,7 @@ describe('Mempool', function() { tx.addCoin(dummyInput(prev, prevHash)); const sig = tx.signature(0, prev, 70000, key.privateKey, ALL, 0); - tx.inputs[0].script = new Script([sig]); + tx.inputs[0].script = Script.fromItems([sig]); tx.inputs[0].witness.push(Buffer.alloc(0)); let err; diff --git a/test/node-test.js b/test/node-test.js index 6e1eb42e1..a7dc5dea4 100644 --- a/test/node-test.js +++ b/test/node-test.js @@ -4,11 +4,11 @@ 'use strict'; const assert = require('./util/assert'); -const ScriptNum = require('../lib/script/scriptnum'); const consensus = require('../lib/protocol/consensus'); const co = require('../lib/utils/co'); const Coin = require('../lib/primitives/coin'); const Script = require('../lib/script/script'); +const Opcode = require('../lib/script/opcode'); const FullNode = require('../lib/node/fullnode'); const MTX = require('../lib/primitives/mtx'); const TX = require('../lib/primitives/tx'); @@ -63,8 +63,8 @@ async function mineCSV(fund) { spend.addOutput({ script: [ - ScriptNum.encode(1), - Script.opcodes.OP_CHECKSEQUENCEVERIFY + Opcode.fromInt(1), + Opcode.fromSymbol('checksequenceverify') ], value: 10 * 1e8 }); @@ -348,8 +348,8 @@ describe('Node', function() { spend.addOutput({ script: [ - ScriptNum.encode(2), - Script.opcodes.OP_CHECKSEQUENCEVERIFY + Opcode.fromInt(2), + Opcode.fromSymbol('checksequenceverify') ], value: 10 * 1e8 }); @@ -373,8 +373,8 @@ describe('Node', function() { spend.addOutput({ script: [ - ScriptNum.encode(1), - Script.opcodes.OP_CHECKSEQUENCEVERIFY + Opcode.fromInt(1), + Opcode.fromSymbol('checksequenceverify') ], value: 10 * 1e8 }); @@ -418,8 +418,8 @@ describe('Node', function() { spend.addOutput({ script: [ - ScriptNum.encode(2), - Script.opcodes.OP_CHECKSEQUENCEVERIFY + Opcode.fromInt(2), + Opcode.fromSymbol('checksequenceverify') ], value: 10 * 1e8 }); diff --git a/test/script-test.js b/test/script-test.js index 9885ad029..dc47e8d00 100644 --- a/test/script-test.js +++ b/test/script-test.js @@ -7,10 +7,10 @@ const assert = require('./util/assert'); const Script = require('../lib/script/script'); const Witness = require('../lib/script/witness'); const Stack = require('../lib/script/stack'); +const Opcode = require('../lib/script/opcode'); const TX = require('../lib/primitives/tx'); const util = require('../lib/utils/util'); const encoding = require('../lib/utils/encoding'); -const opcodes = Script.opcodes; const scripts = require('./data/script-tests.json'); @@ -18,7 +18,7 @@ function isSuccess(stack) { if (stack.length === 0) return false; - if (!stack.bool(-1)) + if (!stack.getBool(-1)) return false; return true; @@ -67,34 +67,6 @@ function parseScriptTest(data) { } describe('Script', function() { - it('should encode/decode script', () => { - const src = Buffer.from('' - + '20' - + '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f' - + '20' - + '101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f' - + 'ac', - 'hex'); - - const decoded = Script.fromRaw(src); - assert.strictEqual(decoded.code.length, 3); - assert.strictEqual(decoded.code[0].data.toString('hex'), - '000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f'); - assert.strictEqual(decoded.code[1].data.toString('hex'), - '101112131415161718191a1b1c1d1e1f202122232425262728292a2b2c2d2e2f'); - assert.strictEqual(decoded.code[2].value, opcodes.OP_CHECKSIG); - - const dst = decoded.toRaw(); - assert.bufferEqual(dst, src); - }); - - it('should encode/decode numbers', () => { - const script = [0, 0x51, 0x52, 0x60]; - const encoded = Script.fromArray(script).raw; - const decoded = Script(encoded).toArray(); - assert.deepStrictEqual(decoded, script); - }); - it('should recognize a P2SH output', () => { const hex = 'a91419a7d869032368fd1f1e26e5e73a4ad0e474960e87'; const decoded = Script.fromRaw(hex, 'hex'); @@ -110,17 +82,20 @@ describe('Script', function() { it('should handle if statements correctly', () => { { - const input = new Script([opcodes.OP_1, opcodes.OP_2]); + const input = new Script([ + Opcode.fromInt(1), + Opcode.fromInt(2) + ]); const output = new Script([ - opcodes.OP_2, - opcodes.OP_EQUAL, - opcodes.OP_IF, - opcodes.OP_3, - opcodes.OP_ELSE, - opcodes.OP_4, - opcodes.OP_ENDIF, - opcodes.OP_5 + Opcode.fromInt(2), + Opcode.fromSymbol('equal'), + Opcode.fromSymbol('if'), + Opcode.fromInt(3), + Opcode.fromSymbol('else'), + Opcode.fromInt(4), + Opcode.fromSymbol('endif'), + Opcode.fromInt(5) ]); const stack = new Stack(); @@ -132,17 +107,20 @@ describe('Script', function() { } { - const input = new Script([opcodes.OP_1, opcodes.OP_2]); + const input = new Script([ + Opcode.fromInt(1), + Opcode.fromInt(2) + ]); const output = new Script([ - opcodes.OP_9, - opcodes.OP_EQUAL, - opcodes.OP_IF, - opcodes.OP_3, - opcodes.OP_ELSE, - opcodes.OP_4, - opcodes.OP_ENDIF, - opcodes.OP_5 + Opcode.fromInt(9), + Opcode.fromSymbol('equal'), + Opcode.fromSymbol('if'), + Opcode.fromInt(3), + Opcode.fromSymbol('else'), + Opcode.fromInt(4), + Opcode.fromSymbol('endif'), + Opcode.fromInt(5) ]); const stack = new Stack(); @@ -154,15 +132,18 @@ describe('Script', function() { } { - const input = new Script([opcodes.OP_1, opcodes.OP_2]); + const input = new Script([ + Opcode.fromInt(1), + Opcode.fromInt(2) + ]); const output = new Script([ - opcodes.OP_2, - opcodes.OP_EQUAL, - opcodes.OP_IF, - opcodes.OP_3, - opcodes.OP_ENDIF, - opcodes.OP_5 + Opcode.fromInt(2), + Opcode.fromSymbol('equal'), + Opcode.fromSymbol('if'), + Opcode.fromInt(3), + Opcode.fromSymbol('endif'), + Opcode.fromInt(5) ]); const stack = new Stack(); @@ -174,15 +155,18 @@ describe('Script', function() { } { - const input = new Script([opcodes.OP_1, opcodes.OP_2]); + const input = new Script([ + Opcode.fromInt(1), + Opcode.fromInt(2) + ]); const output = new Script([ - opcodes.OP_9, - opcodes.OP_EQUAL, - opcodes.OP_IF, - opcodes.OP_3, - opcodes.OP_ENDIF, - opcodes.OP_5 + Opcode.fromInt(9), + Opcode.fromSymbol('equal'), + Opcode.fromSymbol('if'), + Opcode.fromInt(3), + Opcode.fromSymbol('endif'), + Opcode.fromInt(5) ]); const stack = new Stack(); @@ -194,15 +178,18 @@ describe('Script', function() { } { - const input = new Script([opcodes.OP_1, opcodes.OP_2]); + const input = new Script([ + Opcode.fromInt(1), + Opcode.fromInt(2) + ]); const output = new Script([ - opcodes.OP_9, - opcodes.OP_EQUAL, - opcodes.OP_NOTIF, - opcodes.OP_3, - opcodes.OP_ENDIF, - opcodes.OP_5 + Opcode.fromInt(9), + Opcode.fromSymbol('equal'), + Opcode.fromSymbol('notif'), + Opcode.fromInt(3), + Opcode.fromSymbol('endif'), + Opcode.fromInt(5) ]); const stack = new Stack(); @@ -216,15 +203,15 @@ describe('Script', function() { it('should handle CScriptNums correctly', () => { const input = new Script([ - Buffer.from('ffffff7f', 'hex'), - opcodes.OP_NEGATE, - opcodes.OP_DUP, - opcodes.OP_ADD + Opcode.fromString('ffffff7f', 'hex'), + Opcode.fromSymbol('negate'), + Opcode.fromSymbol('dup'), + Opcode.fromSymbol('add') ]); const output = new Script([ - Buffer.from('feffffff80', 'hex'), - opcodes.OP_EQUAL + Opcode.fromString('feffffff80', 'hex'), + Opcode.fromSymbol('equal') ]); const stack = new Stack(); @@ -237,15 +224,15 @@ describe('Script', function() { it('should handle CScriptNums correctly', () => { const input = new Script([ - opcodes.OP_11, - opcodes.OP_10, - opcodes.OP_1, - opcodes.OP_ADD + Opcode.fromInt(11), + Opcode.fromInt(10), + Opcode.fromInt(1), + Opcode.fromSymbol('add') ]); const output = new Script([ - opcodes.OP_NUMNOTEQUAL, - opcodes.OP_NOT + Opcode.fromSymbol('numnotequal'), + Opcode.fromSymbol('not') ]); const stack = new Stack(); @@ -258,19 +245,19 @@ describe('Script', function() { it('should handle OP_ROLL correctly', () => { const input = new Script([ - Buffer.from([0x16]), - Buffer.from([0x15]), - Buffer.from([0x14]) + Opcode.fromInt(0x16), + Opcode.fromInt(0x15), + Opcode.fromInt(0x14) ]); const output = new Script([ - opcodes.OP_0, - opcodes.OP_ROLL, - Buffer.from([0x14]), - opcodes.OP_EQUALVERIFY, - opcodes.OP_DEPTH, - opcodes.OP_2, - opcodes.OP_EQUAL + Opcode.fromInt(0), + Opcode.fromSymbol('roll'), + Opcode.fromInt(0x14), + Opcode.fromSymbol('equalverify'), + Opcode.fromSymbol('depth'), + Opcode.fromInt(2), + Opcode.fromSymbol('equal') ]); const stack = new Stack(); @@ -302,7 +289,10 @@ describe('Script', function() { hash: encoding.NULL_HASH, index: 0xffffffff }, - script: [opcodes.OP_0, opcodes.OP_0], + script: [ + Opcode.fromOp(0), + Opcode.fromOp(0) + ], witness: [], sequence: 0xffffffff }], diff --git a/test/tx-test.js b/test/tx-test.js index 25c08c8ae..1e27f85ed 100644 --- a/test/tx-test.js +++ b/test/tx-test.js @@ -13,11 +13,11 @@ const Output = require('../lib/primitives/output'); const Outpoint = require('../lib/primitives/outpoint'); const Script = require('../lib/script/script'); const Witness = require('../lib/script/witness'); +const Opcode = require('../lib/script/opcode'); const Input = require('../lib/primitives/input'); const CoinView = require('../lib/coins/coinview'); const KeyRing = require('../lib/primitives/keyring'); const common = require('./util/common'); -const opcodes = Script.opcodes; const validTests = require('./data/tx-valid.json'); const invalidTests = require('./data/tx-invalid.json'); @@ -624,8 +624,8 @@ describe('TX', function() { const output = Script.fromMultisig(1, 2, [pub, pub]); const input = new Script([ - opcodes.OP_0, - opcodes.OP_0 + Opcode.fromOp(0), + Opcode.fromOp(0) ]); const witness = new Witness(); @@ -646,9 +646,9 @@ describe('TX', function() { const output = Script.fromScripthash(redeem.hash160()); const input = new Script([ - opcodes.OP_0, - opcodes.OP_0, - redeem.toRaw() + Opcode.fromOp(0), + Opcode.fromOp(0), + Opcode.fromData(redeem.toRaw()) ]); const witness = new Witness(); @@ -707,7 +707,7 @@ describe('TX', function() { const output = Script.fromScripthash(redeem.hash160()); const input = new Script([ - redeem.toRaw() + Opcode.fromData(redeem.toRaw()) ]); const witness = new Witness([ @@ -754,7 +754,7 @@ describe('TX', function() { const output = Script.fromScripthash(redeem.hash160()); const input = new Script([ - redeem.toRaw() + Opcode.fromData(redeem.toRaw()) ]); const witness = new Witness([ diff --git a/test/wallet-test.js b/test/wallet-test.js index b05b35338..1f7caf5a2 100644 --- a/test/wallet-test.js +++ b/test/wallet-test.js @@ -27,7 +27,7 @@ const KEY2 = 'xprv9s21ZrQH143K3mqiSThzPtWAabQ22Pjp3uSNnZ53A5bQ4udp' + 'faKekc2m4AChLYH1XDzANhrSdxHYWUeTWjYJwFwWFyHkTMnMeAcW4JyRCZa'; const workers = new WorkerPool({ - enabled: true + enabled: false }); const wdb = new WalletDB({ @@ -222,8 +222,9 @@ async function testP2SH(witness, nesting) { assert(bob.account.change.getAddress().equals(change2)); assert(carol.account.change.getAddress().equals(change2)); - tx.inputs[0][vector].set(2, encoding.ZERO_SIG); - tx.inputs[0][vector].compile(); + const input = tx.inputs[0]; + input[vector].setData(2, encoding.ZERO_SIG); + input[vector].compile(); assert(!tx.verify(view, flags)); assert.strictEqual(tx.getFee(view), 10000); @@ -373,8 +374,9 @@ describe('Wallet', function() { // Script inputs but do not sign await alice.template(fake); // Fake signature - fake.inputs[0].script.set(0, encoding.ZERO_SIG); - fake.inputs[0].script.compile(); + const input = fake.inputs[0]; + input.script.setData(0, encoding.ZERO_SIG); + input.script.compile(); // balance: 11000 // Fake TX should temporarily change output.