'use strict'; const assert = require('assert'); const util = require('../lib/utils/util'); const encoding = require('../lib/utils/encoding'); const random = require('../lib/crypto/random'); const consensus = require('../lib/protocol/consensus'); const TX = require('../lib/primitives/tx'); const Coin = require('../lib/primitives/coin'); const Output = require('../lib/primitives/output'); const Script = require('../lib/script/script'); const Witness = require('../lib/script/witness'); const Input = require('../lib/primitives/input'); const CoinView = require('../lib/coins/coinview'); const KeyRing = require('../lib/primitives/keyring'); const parseTX = require('./util/common').parseTX; const opcodes = Script.opcodes; const valid = require('./data/tx_valid.json'); const invalid = require('./data/tx_invalid.json'); const sighash = require('./data/sighash.json'); const tx1 = parseTX('data/tx1.hex'); const tx2 = parseTX('data/tx2.hex'); const tx3 = parseTX('data/tx3.hex'); const tx4 = parseTX('data/tx4.hex'); const wtx = parseTX('data/wtx.hex'); const coolest = parseTX('data/coolest-tx-ever-sent.hex'); const MAX_SAFE_INTEGER = Number.MAX_SAFE_INTEGER; const MAX_SAFE_ADDITION = 0xfffffffffffff; function clearCache(tx, noCache) { if (!noCache) { assert.equal(tx.hash('hex'), tx.clone().hash('hex')); return; } tx.refresh(); } function parseTest(data) { let [coins, tx, names] = data; let view = new CoinView(); let flags = 0; let coin; if (!names) names = ''; tx = TX.fromRaw(tx, 'hex'); names = names.trim().split(/,\s*/); for (let name of names) { name = `VERIFY_${name}`; assert(Script.flags[name] != null, 'Unknown flag.'); flags |= Script.flags[name]; } for (let [hash, index, script, value] of coins) { hash = util.revHex(hash); script = Script.fromString(script); value = parseInt(value || '0', 10); if (index === -1) continue; coin = new Coin({ version: 1, height: -1, coinbase: false, hash: hash, index: index, script: script, value: value }); view.addCoin(coin); } coin = view.getOutput(tx.inputs[0]); return { tx: tx, flags: flags, view: view, comments: coin ? util.inspectify(coin.script, false) : 'coinbase', data: data }; } function sigopContext(scriptSig, witness, scriptPubkey) { let view = new CoinView(); let input, output, fund, spend; input = new Input(); output = new Output(); output.value = 1; output.script = scriptPubkey; fund = new TX(); fund.version = 1; fund.inputs.push(input); fund.outputs.push(output); fund.refresh(); input = new Input(); input.prevout.hash = fund.hash('hex'); input.prevout.index = 0; input.script = scriptSig; input.witness = witness; output = new Output(); output.value = 1; spend = new TX(); spend.version = 1; spend.inputs.push(input); spend.outputs.push(output); spend.refresh(); view.addTX(fund, 0); return { fund: fund, spend: spend, view: view }; } describe('TX', function() { let raw = '010000000125393c67cd4f581456dd0805fa8e9db3abdf90dbe1d4b53e28' + '6490f35d22b6f2010000006b483045022100f4fa5ced20d2dbd2f905809d' + '79ebe34e03496ef2a48a04d0a9a1db436a211dd202203243d086398feb4a' + 'c21b3b79884079036cd5f3707ba153b383eabefa656512dd0121022ebabe' + 'fede28804b331608d8ef11e1d65b5a920720db8a644f046d156b3a73c0ff' + 'ffffff0254150000000000001976a9140740345f114e1a1f37ac1cc442b4' + '32b91628237e88ace7d27b00000000001976a91495ad422bb5911c2c9fe6' + 'ce4f82a13c85f03d9b2e88ac00000000'; let inp = '01000000052fa236559f51f343f0905ea627a955f421a198541d928798b8' + '186980273942ec010000006b483045022100ae27626778eba264d56883f5' + 'edc1a49897bf209e98f21c870a55d13bec916e1802204b66f4e3235143d1' + '1aef327d9454754cd1f28807c3bf9996c107900df9d19ea60121022ebabe' + 'fede28804b331608d8ef11e1d65b5a920720db8a644f046d156b3a73c0ff' + 'ffffffe2136f72e4a25e300137b98b402cda91db5c6db6373ba81c722ae1' + 'a85315b591000000006b483045022100f84293ea9bfb6d150f3a72d8b5ce' + 'b294a77b31442bf9d4ab2058f046a9b65a9f022075935dc0a6a628df26eb' + 'b7215634fd33b65f4da105665595028837680b87ea360121039708df1967' + '09c5041dc9a26457a0cfa303076329f389687bdc9709d5862fd664ffffff' + 'fff6e67655a42a2f955ec8610940c983042516c32298e57684b3c29fcade' + '7e637a000000006a47304402203bbfb53c3011d742f3f942db18a44d8c3d' + 'd111990ee7cc42959383dd7a3e8e8d02207f0f5ed3e165d9db81ac69d36c' + '60a1a4a482f22cb0048dafefa5e704e84dd18e0121039708df196709c504' + '1dc9a26457a0cfa303076329f389687bdc9709d5862fd664ffffffff9a02' + 'e72123a149570c11696d3c798593785e95b8a3c3fc49ae1d07d809d94d5a' + '000000006b483045022100ad0e6f5f73221aa4eda9ad82c7074882298bcf' + '668f34ae81126df0213b2961850220020ba23622d75fb8f95199063b804f' + '62ba103545af4e16b5be0b6dc0cb51aac60121039708df196709c5041dc9' + 'a26457a0cfa303076329f389687bdc9709d5862fd664ffffffffd7db5a38' + '72589ca8aa3cd5ebb0f22dbb3956f8d691e15dc010fe1093c045c3de0000' + '00006b48304502210082b91a67da1f02dcb0d00e63b67f10af8ba9639b16' + '5f9ff974862a9d4900e27c022069e4a58f591eb3fc7d7d0b176d64d59e90' + 'aef0c601b3c84382abad92f6973e630121039708df196709c5041dc9a264' + '57a0cfa303076329f389687bdc9709d5862fd664ffffffff025415000000' + '0000001976a9140740345f114e1a1f37ac1cc442b432b91628237e88ac4b' + '0f7c00000000001976a91495ad422bb5911c2c9fe6ce4f82a13c85f03d9b' + '2e88ac00000000'; [false, true].forEach((noCache) => { let suffix = noCache ? 'without cache' : 'with cache'; it(`should decode/encode with parser/framer ${suffix}`, () => { let tx = TX.fromRaw(raw, 'hex'); clearCache(tx, noCache); assert.equal(tx.toRaw().toString('hex'), raw); }); it(`should be verifiable ${suffix}`, () => { let tx = TX.fromRaw(raw, 'hex'); let p = TX.fromRaw(inp, 'hex'); let view = new CoinView(); view.addTX(p, -1); clearCache(tx, noCache); clearCache(p, noCache); assert(tx.verify(view)); }); it(`should verify non-minimal output ${suffix}`, () => { clearCache(tx1.tx, noCache); assert(tx1.tx.verify(tx1.view, Script.flags.VERIFY_P2SH)); }); it(`should verify tx.version == 0 ${suffix}`, () => { clearCache(tx2.tx, noCache); assert(tx2.tx.verify(tx2.view, Script.flags.VERIFY_P2SH)); }); it(`should verify sighash_single bug w/ findanddelete ${suffix}`, () => { clearCache(tx3.tx, noCache); assert(tx3.tx.verify(tx3.view, Script.flags.VERIFY_P2SH)); }); it(`should verify high S value with only DERSIG enabled ${suffix}`, () => { let coin = tx4.view.getOutput(tx4.tx.inputs[0]); let flags = Script.flags.VERIFY_P2SH | Script.flags.VERIFY_DERSIG; clearCache(tx4.tx, noCache); assert(tx4.tx.verifyInput(0, coin, flags)); }); it(`should verify the coolest tx ever sent ${suffix}`, () => { clearCache(coolest.tx, noCache); assert(coolest.tx.verify(coolest.view, Script.flags.VERIFY_NONE)); }); it(`should parse witness tx properly ${suffix}`, () => { let raw1, raw2, wtx2; clearCache(wtx.tx, noCache); assert.equal(wtx.tx.inputs.length, 5); assert.equal(wtx.tx.outputs.length, 1980); assert(wtx.tx.hasWitness()); assert.notEqual(wtx.tx.hash('hex'), wtx.tx.witnessHash('hex')); assert.equal(wtx.tx.witnessHash('hex'), '088c919cd8408005f255c411f786928385688a9e8fdb2db4c9bc3578ce8c94cf'); assert.equal(wtx.tx.getSize(), 62138); assert.equal(wtx.tx.getVirtualSize(), 61813); assert.equal(wtx.tx.getWeight(), 247250); raw1 = wtx.tx.toRaw(); clearCache(wtx.tx, true); raw2 = wtx.tx.toRaw(); assert.deepEqual(raw1, raw2); wtx2 = TX.fromRaw(raw2); clearCache(wtx2, noCache); assert.equal(wtx.tx.hash('hex'), wtx2.hash('hex')); assert.equal(wtx.tx.witnessHash('hex'), wtx2.witnessHash('hex')); }); [[valid, true], [invalid, false]].forEach((test) => { let [arr, valid] = test; let comment = ''; arr.forEach((json, i) => { let data, tx, view, flags, comments; if (json.length === 1) { comment += ' ' + json[0]; return; } data = parseTest(json); if (!data) { comment = ''; return; } tx = data.tx; view = data.view; flags = data.flags; comments = comment.trim(); if (!comments) comments = data.comments; comment = ''; if (valid) { if (comments.indexOf('Coinbase') === 0) { it(`should handle valid coinbase ${suffix}: ${comments}`, () => { clearCache(tx, noCache); assert.ok(tx.isSane()); }); return; } it(`should handle valid tx test ${suffix}: ${comments}`, () => { clearCache(tx, noCache); assert.ok(tx.verify(view, flags)); }); } else { if (comments === 'Duplicate inputs') { it(`should handle duplicate input test ${suffix}: ${comments}`, () => { clearCache(tx, noCache); assert.ok(tx.verify(view, flags)); assert.ok(!tx.isSane()); }); return; } if (comments === 'Negative output') { it(`should handle invalid tx (negative) ${suffix}: ${comments}`, () => { clearCache(tx, noCache); assert.ok(tx.verify(view, flags)); assert.ok(!tx.isSane()); }); return; } if (comments.indexOf('Coinbase') === 0) { it(`should handle invalid coinbase ${suffix}: ${comments}`, () => { clearCache(tx, noCache); assert.ok(!tx.isSane()); }); return; } it(`should handle invalid tx test ${suffix}: ${comments}`, () => { clearCache(tx, noCache); assert.ok(!tx.verify(view, flags)); }); } }); }); sighash.forEach((data) => { let [tx, script, index, type, hash] = data; let expected, hex; if (data.length === 1) return; tx = TX.fromRaw(tx, 'hex'); script = Script.fromRaw(script, 'hex'); expected = util.revHex(hash); hex = type & 3; if (type & 0x80) hex |= 0x80; hex = hex.toString(16); if (hex.length % 2 !== 0) hex = '0' + hex; clearCache(tx, noCache); it(`should get sighash of ${hash} (${hex}) ${suffix}`, () => { let subscript = script.getSubscript(0).removeSeparators(); let hash = tx.signatureHash(index, subscript, 0, type, 0); assert.equal(hash.toString('hex'), expected); }); }); }); function createInput(value, view) { let hash = random.randomBytes(32).toString('hex'); let output = new Output(); output.value = value; view.addOutput(hash, 0, output); return { prevout: { hash: hash, index: 0 } }; } it('should fail on >51 bit coin values', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [createInput(consensus.MAX_MONEY + 1, view)], outputs: [{ script: [], value: consensus.MAX_MONEY }], locktime: 0 }); assert.ok(tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); it('should handle 51 bit coin values', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [createInput(consensus.MAX_MONEY, view)], outputs: [{ script: [], value: consensus.MAX_MONEY }], locktime: 0 }); assert.ok(tx.isSane()); assert.ok(tx.verifyInputs(view, 0)); }); it('should fail on >51 bit output values', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [createInput(consensus.MAX_MONEY, view)], outputs: [{ script: [], value: consensus.MAX_MONEY + 1 }], locktime: 0 }); assert.ok(!tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); it('should handle 51 bit output values', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [createInput(consensus.MAX_MONEY, view)], outputs: [{ script: [], value: consensus.MAX_MONEY }], locktime: 0 }); assert.ok(tx.isSane()); assert.ok(tx.verifyInputs(view, 0)); }); it('should fail on >51 bit fees', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [createInput(consensus.MAX_MONEY + 1, view)], outputs: [{ script: [], value: 0 }], locktime: 0 }); assert.ok(tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); it('should fail on >51 bit values from multiple', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [ createInput(Math.floor(consensus.MAX_MONEY / 2), view), createInput(Math.floor(consensus.MAX_MONEY / 2), view), createInput(Math.floor(consensus.MAX_MONEY / 2), view) ], outputs: [{ script: [], value: consensus.MAX_MONEY }], locktime: 0 }); assert.ok(tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); it('should fail on >51 bit output values from multiple', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [createInput(consensus.MAX_MONEY, view)], outputs: [ { script: [], value: Math.floor(consensus.MAX_MONEY / 2) }, { script: [], value: Math.floor(consensus.MAX_MONEY / 2) }, { script: [], value: Math.floor(consensus.MAX_MONEY / 2) } ], locktime: 0 }); assert.ok(!tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); it('should fail on >51 bit fees from multiple', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [ createInput(Math.floor(consensus.MAX_MONEY / 2), view), createInput(Math.floor(consensus.MAX_MONEY / 2), view), createInput(Math.floor(consensus.MAX_MONEY / 2), view) ], outputs: [{ script: [], value: 0 }], locktime: 0 }); assert.ok(tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); it('should fail to parse >53 bit values', () => { let view = new CoinView(); let tx, raw; tx = new TX({ version: 1, flag: 1, inputs: [ createInput(Math.floor(consensus.MAX_MONEY / 2), view) ], outputs: [{ script: [], value: 0xdeadbeef }], locktime: 0 }); raw = tx.toRaw(); assert(encoding.readU64(raw, 47) === 0xdeadbeef); raw[54] = 0x7f; assert.throws(() => { TX.fromRaw(raw); }); tx.outputs[0].value = 0; tx.refresh(); raw = tx.toRaw(); assert(encoding.readU64(raw, 47) === 0x00); raw[54] = 0x80; assert.throws(() => { TX.fromRaw(raw); }); }); it('should fail on 53 bit coin values', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [createInput(MAX_SAFE_INTEGER, view)], outputs: [{ script: [], value: consensus.MAX_MONEY }], locktime: 0 }); assert.ok(tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); it('should fail on 53 bit output values', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [createInput(consensus.MAX_MONEY, view)], outputs: [{ script: [], value: MAX_SAFE_INTEGER }], locktime: 0 }); assert.ok(!tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); it('should fail on 53 bit fees', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [createInput(MAX_SAFE_INTEGER, view)], outputs: [{ script: [], value: 0 }], locktime: 0 }); assert.ok(tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); [MAX_SAFE_ADDITION, MAX_SAFE_INTEGER].forEach((MAX) => { it('should fail on >53 bit values from multiple', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [ createInput(MAX, view), createInput(MAX, view), createInput(MAX, view) ], outputs: [{ script: [], value: consensus.MAX_MONEY }], locktime: 0 }); assert.ok(tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); it('should fail on >53 bit output values from multiple', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [createInput(consensus.MAX_MONEY, view)], outputs: [ { script: [], value: MAX }, { script: [], value: MAX }, { script: [], value: MAX } ], locktime: 0 }); assert.ok(!tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); it('should fail on >53 bit fees from multiple', () => { let view = new CoinView(); let tx = new TX({ version: 1, flag: 1, inputs: [ createInput(MAX, view), createInput(MAX, view), createInput(MAX, view) ], outputs: [{ script: [], value: 0 }], locktime: 0 }); assert.ok(tx.isSane()); assert.ok(!tx.verifyInputs(view, 0)); }); }); it('should count sigops for multisig', () => { let flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; let key = KeyRing.generate(); let pub = key.publicKey; let ctx, output, input, witness; output = Script.fromMultisig(1, 2, [pub, pub]); input = new Script([ opcodes.OP_0, opcodes.OP_0 ]); witness = new Witness(); ctx = sigopContext(input, witness, output); assert.equal(ctx.spend.getSigopsCost(ctx.view, flags), 0); assert.equal(ctx.fund.getSigopsCost(ctx.view, flags), consensus.MAX_MULTISIG_PUBKEYS * consensus.WITNESS_SCALE_FACTOR); }); it('should count sigops for p2sh multisig', () => { let flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; let key = KeyRing.generate(); let pub = key.publicKey; let ctx, redeem, output, input, witness; redeem = Script.fromMultisig(1, 2, [pub, pub]); output = Script.fromScripthash(redeem.hash160()); input = new Script([ opcodes.OP_0, opcodes.OP_0, redeem.toRaw() ]); witness = new Witness(); ctx = sigopContext(input, witness, output); assert.equal(ctx.spend.getSigopsCost(ctx.view, flags), 2 * consensus.WITNESS_SCALE_FACTOR); }); it('should count sigops for p2wpkh', () => { let flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; let key = KeyRing.generate(); let ctx, output, input, witness; output = Script.fromProgram(0, key.getKeyHash()); input = new Script(); witness = new Witness([ Buffer.from([0]), Buffer.from([0]) ]); ctx = sigopContext(input, witness, output); assert.equal(ctx.spend.getSigopsCost(ctx.view, flags), 1); assert.equal( ctx.spend.getSigopsCost(ctx.view, flags & ~Script.flags.VERIFY_WITNESS), 0); output = Script.fromProgram(1, key.getKeyHash()); ctx = sigopContext(input, witness, output); assert.equal(ctx.spend.getSigopsCost(ctx.view, flags), 0); output = Script.fromProgram(0, key.getKeyHash()); ctx = sigopContext(input, witness, output); ctx.spend.inputs[0].prevout.hash = encoding.NULL_HASH; ctx.spend.inputs[0].prevout.index = 0xffffffff; ctx.spend.refresh(); assert.equal(ctx.spend.getSigopsCost(ctx.view, flags), 0); }); it('should count sigops for nested p2wpkh', () => { let flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; let key = KeyRing.generate(); let ctx, redeem, output, input, witness; redeem = Script.fromProgram(0, key.getKeyHash()); output = Script.fromScripthash(redeem.hash160()); input = new Script([ redeem.toRaw() ]); witness = new Witness([ Buffer.from([0]), Buffer.from([0]) ]); ctx = sigopContext(input, witness, output); assert.equal(ctx.spend.getSigopsCost(ctx.view, flags), 1); }); it('should count sigops for p2wsh', () => { let flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; let key = KeyRing.generate(); let pub = key.publicKey; let ctx, redeem, output, input, witness; redeem = Script.fromMultisig(1, 2, [pub, pub]); output = Script.fromProgram(0, redeem.sha256()); input = new Script(); witness = new Witness([ Buffer.from([0]), Buffer.from([0]), redeem.toRaw() ]); ctx = sigopContext(input, witness, output); assert.equal(ctx.spend.getSigopsCost(ctx.view, flags), 2); assert.equal( ctx.spend.getSigopsCost(ctx.view, flags & ~Script.flags.VERIFY_WITNESS), 0); }); it('should count sigops for nested p2wsh', () => { let flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; let key = KeyRing.generate(); let pub = key.publicKey; let ctx, wscript, redeem, output, input, witness; wscript = Script.fromMultisig(1, 2, [pub, pub]); redeem = Script.fromProgram(0, wscript.sha256()); output = Script.fromScripthash(redeem.hash160()); input = new Script([ redeem.toRaw() ]); witness = new Witness([ Buffer.from([0]), Buffer.from([0]), wscript.toRaw() ]); ctx = sigopContext(input, witness, output); assert.equal(ctx.spend.getSigopsCost(ctx.view, flags), 2); }); });