diff --git a/test/chain-test.js b/test/chain-test.js index b6f866ef..47f2837c 100644 --- a/test/chain-test.js +++ b/test/chain-test.js @@ -3,6 +3,7 @@ var assert = require('assert'); var BN = require('bn.js'); var consensus = require('../lib/protocol/consensus'); +var encoding = require('../lib/utils/encoding'); var co = require('../lib/utils/co'); var Coin = require('../lib/primitives/coin'); var Script = require('../lib/script/script'); @@ -10,29 +11,47 @@ var Chain = require('../lib/blockchain/chain'); var Miner = require('../lib/mining/miner'); var MTX = require('../lib/primitives/mtx'); var MemWallet = require('./util/memwallet'); +var Network = require('../lib/protocol/network'); +var Output = require('../lib/primitives/output'); var util = require('../lib/utils/util'); +var opcodes = Script.opcodes; describe('Chain', function() { - var chain = new Chain({ db: 'memory', network: 'regtest' }); - var miner = new Miner({ chain: chain }); - var wallet = new MemWallet({ network: 'regtest' }); - var tip1, tip2, cb1, cb2, mineBlock; + var network = Network.get('regtest'); + var chain = new Chain({ db: 'memory', network: network }); + var miner = new Miner({ chain: chain, version: 4 }); + var wallet = new MemWallet({ network: network }); + var wwallet = new MemWallet({ network: network, witness: true }); + var tip1, tip2, cb1, cb2, addBlock, mineCSV; - this.timeout(5000); + this.timeout(45000); - mineBlock = co(function* mineBlock(tip, tx) { - var attempt = yield miner.createBlock(tip); + addBlock = co(function* addBlock(attempt) { + var block = yield attempt.mineAsync(); + try { + yield chain.add(block); + } catch (e) { + assert(e.type === 'VerifyError'); + return e.reason; + } + return 'OK'; + }); + + mineCSV = co(function* mineCSV(tx) { + var attempt = yield miner.createBlock(); var rtx; - if (!tx) - return yield attempt.mineAsync(); - rtx = new MTX(); - rtx.addTX(tx, 0); + rtx.addOutput({ + script: [ + Script.array(new BN(1)), + Script.opcodes.OP_CHECKSEQUENCEVERIFY + ], + value: 10000 + }); - rtx.addOutput(wallet.getReceive(), 25 * 1e8); - rtx.addOutput(wallet.getChange(), 5 * 1e8); + rtx.addTX(tx, 0); rtx.setLocktime(chain.height); @@ -52,40 +71,57 @@ describe('Chain', function() { }); it('should open chain and miner', co(function* () { - consensus.COINBASE_MATURITY = 0; yield chain.open(); yield miner.open(); })); - it('should open wallet', co(function* () { + it('should add addrs to miner', co(function* () { miner.addresses.length = 0; miner.addAddress(wallet.getReceive()); })); - it('should mine a block', co(function* () { - var block = yield miner.mineBlock(); - assert(block); - yield chain.add(block); + it('should mine a 200 blocks', co(function* () { + var i, block; + + for (i = 0; i < 200; i++) { + block = yield miner.mineBlock(); + assert(block); + yield chain.add(block); + } + + assert.equal(chain.height, 200); })); it('should mine competing chains', co(function* () { - var i, block1, block2; + var i, mtx, at1, at2, blk1, blk2, hash1, hash2; for (i = 0; i < 10; i++) { - block1 = yield mineBlock(tip1, cb1); - cb1 = block1.txs[0]; + at1 = yield miner.createBlock(tip1); + at2 = yield miner.createBlock(tip2); - block2 = yield mineBlock(tip2, cb2); - cb2 = block2.txs[0]; + mtx = yield wallet.create({ + outputs: [{ + address: wallet.getAddress(), + value: 10 * 1e8 + }] + }); - yield chain.add(block1); + at1.addTX(mtx.toTX(), mtx.view); + at2.addTX(mtx.toTX(), mtx.view); - yield chain.add(block2); + blk1 = yield at1.mineAsync(); + blk2 = yield at2.mineAsync(); - assert(chain.tip.hash === block1.hash('hex')); + hash1 = blk1.hash('hex'); + hash2 = blk2.hash('hex'); - tip1 = yield chain.db.getEntry(block1.hash('hex')); - tip2 = yield chain.db.getEntry(block2.hash('hex')); + yield chain.add(blk1); + yield chain.add(blk2); + + assert(chain.tip.hash === hash1); + + tip1 = yield chain.db.getEntry(hash1); + tip2 = yield chain.db.getEntry(hash2); assert(tip1); assert(tip2); @@ -97,22 +133,20 @@ describe('Chain', function() { })); it('should have correct chain value', function() { - assert.equal(chain.db.state.value, 55000000000); - assert.equal(chain.db.state.coin, 20); - assert.equal(chain.db.state.tx, 21); + assert.equal(chain.db.state.value, 897500000000); + assert.equal(chain.db.state.coin, 220); + assert.equal(chain.db.state.tx, 221); }); - it('should have correct balance', co(function* () { - assert.equal(wallet.balance, 550 * 1e8); - //assert.equal(wallet.unconfirmed, 550 * 1e8); - //assert.equal(wallet.confirmed, 550 * 1e8); + it('should have correct wallet balance', co(function* () { + assert.equal(wallet.balance, 897500000000); })); it('should handle a reorg', co(function* () { - var entry, block, forked; + var forked = false; + var entry, block; - // assert.equal(wallet.height, chain.height); - assert.equal(chain.height, 11); + assert.equal(chain.height, 210); entry = yield chain.db.getEntry(tip2.hash); assert(entry); @@ -121,7 +155,6 @@ describe('Chain', function() { block = yield miner.mineBlock(entry); assert(block); - forked = false; chain.once('reorganize', function() { forked = true; }); @@ -134,15 +167,13 @@ describe('Chain', function() { })); it('should have correct chain value', function() { - assert.equal(chain.db.state.value, 60000000000); - assert.equal(chain.db.state.coin, 21); - assert.equal(chain.db.state.tx, 22); + assert.equal(chain.db.state.value, 900000000000); + assert.equal(chain.db.state.coin, 221); + assert.equal(chain.db.state.tx, 222); }); - it('should have correct balance', co(function* () { - assert.equal(wallet.balance, 600 * 1e8); - // assert.equal(wallet.unconfirmed, 1100 * 1e8); - // assert.equal(wallet.confirmed, 600 * 1e8); + it('should have correct wallet balance', co(function* () { + assert.equal(wallet.balance, 900000000000); })); it('should check main chain', co(function* () { @@ -151,12 +182,14 @@ describe('Chain', function() { })); it('should mine a block after a reorg', co(function* () { - var block = yield mineBlock(null, cb2); - var entry, result; + var block = yield miner.mineBlock(); + var hash, entry, result; yield chain.add(block); - entry = yield chain.db.getEntry(block.hash('hex')); + hash = block.hash('hex'); + entry = yield chain.db.getEntry(hash); + assert(entry); assert(chain.tip.hash === entry.hash); @@ -165,9 +198,29 @@ describe('Chain', function() { })); it('should prevent double spend on new chain', co(function* () { - var block = yield mineBlock(null, cb2); - var tip = chain.tip; - var err; + var attempt = yield miner.createBlock(); + var mtx, block, tip, err; + + mtx = yield wallet.create({ + outputs: [{ + address: wallet.getAddress(), + value: 10 * 1e8 + }] + }); + + attempt.addTX(mtx.toTX(), mtx.view); + + block = yield attempt.mineAsync(); + tip = yield chain.add(block); + + attempt = yield miner.createBlock(); + + assert(mtx.outputs.length > 1); + mtx.outputs.pop(); + + attempt.addTX(mtx.toTX(), mtx.view); + + block = yield attempt.mineAsync(); try { yield chain.add(block); @@ -181,9 +234,21 @@ describe('Chain', function() { })); it('should fail to mine a block with coins on an alternate chain', co(function* () { - var block = yield mineBlock(null, cb1); var tip = chain.tip; - var err; + var block = yield chain.db.getBlock(tip1.hash); + var cb = block.txs[0]; + var mtx = new MTX(); + var attempt, block, err; + + mtx.addTX(cb, 0); + mtx.addOutput(wallet.getAddress(), 10 * 1e8); + + wallet.sign(mtx); + + attempt = yield miner.createBlock(); + attempt.addTX(mtx.toTX(), mtx.view); + + block = yield attempt.mineAsync(); try { yield chain.add(block); @@ -197,43 +262,50 @@ describe('Chain', function() { })); it('should have correct chain value', function() { - assert.equal(chain.db.state.value, 65000000000); - assert.equal(chain.db.state.coin, 23); - assert.equal(chain.db.state.tx, 24); + assert.equal(chain.db.state.value, 905000000000); + assert.equal(chain.db.state.coin, 224); + assert.equal(chain.db.state.tx, 225); }); it('should get coin', co(function* () { - var block, tx, output, coin; + var mtx, attempt, block, tx, output, coin; - block = yield mineBlock(); - yield chain.add(block); + mtx = yield wallet.send({ + outputs: [ + { + address: wallet.getAddress(), + value: 1e8 + }, + { + address: wallet.getAddress(), + value: 1e8 + }, + { + address: wallet.getAddress(), + value: 1e8 + } + ] + }); - block = yield mineBlock(null, block.txs[0]); + attempt = yield miner.createBlock(); + attempt.addTX(mtx.toTX(), mtx.view); + + block = yield attempt.mineAsync(); yield chain.add(block); tx = block.txs[1]; - output = Coin.fromTX(tx, 1, chain.height); + output = Coin.fromTX(tx, 2, chain.height); - coin = yield chain.db.getCoin(tx.hash('hex'), 1); + coin = yield chain.db.getCoin(tx.hash('hex'), 2); assert.deepEqual(coin.toRaw(), output.toRaw()); })); - it('should get balance', co(function* () { - var txs; - - assert.equal(wallet.balance, 750 * 1e8); - // assert.equal(wallet.unconfirmed, 1250 * 1e8); - // assert.equal(wallet.confirmed, 750 * 1e8); - - assert(wallet.receiveDepth >= 7); - assert(wallet.changeDepth >= 6); - - // assert.equal(wallet.height, chain.height); - - // txs = wallet.getHistory(); - // assert.equal(txs.length, 45); - assert.equal(wallet.txs, 26); + it('should have correct wallet balance', co(function* () { + assert.equal(wallet.balance, 907500000000); + assert.equal(wallet.receiveDepth, 15); + assert.equal(wallet.changeDepth, 14); + assert.equal(wallet.txs, 226); })); it('should get tips and remove chains', co(function* () { @@ -258,41 +330,46 @@ describe('Chain', function() { return Promise.resolve(); }); - assert.equal(total, 26); + assert.equal(total, 226); })); it('should activate csv', co(function* () { - var deployments = chain.network.deployments; + var deployments = network.deployments; var i, block, prev, state, cache; + miner.options.version = -1; + + assert.equal(chain.height, 214); + prev = yield chain.tip.getPrevious(); state = yield chain.getState(prev, deployments.csv); - assert(state === 0); + assert.equal(state, 1); for (i = 0; i < 417; i++) { block = yield miner.mineBlock(); yield chain.add(block); switch (chain.height) { - case 144: - prev = yield chain.tip.getPrevious(); - state = yield chain.getState(prev, deployments.csv); - assert(state === 1); - break; case 288: prev = yield chain.tip.getPrevious(); state = yield chain.getState(prev, deployments.csv); - assert(state === 2); + assert.equal(state, 1); break; case 432: prev = yield chain.tip.getPrevious(); state = yield chain.getState(prev, deployments.csv); - assert(state === 3); + assert.equal(state, 2); + break; + case 576: + prev = yield chain.tip.getPrevious(); + state = yield chain.getState(prev, deployments.csv); + assert.equal(state, 3); break; } } - assert(chain.height === 432); + assert.equal(chain.height, 631); assert(chain.state.hasCSV()); + assert(chain.state.hasWitness()); cache = yield chain.db.getStateCache(); assert.deepEqual(cache, chain.db.stateCache); @@ -300,56 +377,38 @@ describe('Chain', function() { assert(yield chain.db.verifyDeployments()); })); - var mineCSV = co(function* mineCSV(tx) { - var attempt = yield miner.createBlock(); - var redeemer; - - redeemer = new MTX(); - - redeemer.addOutput({ - script: [ - Script.array(new BN(1)), - Script.opcodes.OP_CHECKSEQUENCEVERIFY - ], - value: 10 * 1e8 - }); - - redeemer.addTX(tx, 0); - - redeemer.setLocktime(chain.height); - - wallet.sign(redeemer); - - attempt.addTX(redeemer.toTX(), redeemer.view); - - return yield attempt.mineAsync(); - }); + it('should have activated segwit', co(function* () { + var deployments = network.deployments; + var prev = yield chain.tip.getPrevious(); + var state = yield chain.getState(prev, deployments.segwit); + assert.equal(state, 3); + })); it('should test csv', co(function* () { - var tx = (yield chain.db.getBlock(chain.height)).txs[0]; + var tx = (yield chain.db.getBlock(chain.height - 100)).txs[0]; var block = yield mineCSV(tx); - var csv, attempt, redeemer; + var csv, attempt, rtx; yield chain.add(block); csv = block.txs[1]; - redeemer = new MTX(); + rtx = new MTX(); - redeemer.addOutput({ + rtx.addOutput({ script: [ Script.array(new BN(2)), Script.opcodes.OP_CHECKSEQUENCEVERIFY ], - value: 10 * 1e8 + value: 10000 }); - redeemer.addTX(csv, 0); - redeemer.setSequence(0, 1, false); + rtx.addTX(csv, 0); + rtx.setSequence(0, 1, false); attempt = yield miner.createBlock(); - attempt.addTX(redeemer.toTX(), redeemer.view); + attempt.addTX(rtx.toTX(), rtx.view); block = yield attempt.mineAsync(); @@ -357,12 +416,11 @@ describe('Chain', function() { })); it('should fail csv with bad sequence', co(function* () { - var csv = (yield chain.db.getBlock(chain.height)).txs[1]; - var block, attempt, redeemer, err; + var csv = (yield chain.db.getBlock(chain.height - 100)).txs[0]; + var rtx = new MTX(); + var block, attempt, err; - redeemer = new MTX(); - - redeemer.addOutput({ + rtx.addOutput({ script: [ Script.array(new BN(1)), Script.opcodes.OP_CHECKSEQUENCEVERIFY @@ -370,12 +428,12 @@ describe('Chain', function() { value: 10 * 1e8 }); - redeemer.addTX(csv, 0); - redeemer.setSequence(0, 1, false); + rtx.addTX(csv, 0); + rtx.setSequence(0, 1, false); attempt = yield miner.createBlock(); - attempt.addTX(redeemer.toTX(), redeemer.view); + attempt.addTX(rtx.toTX(), rtx.view); block = yield attempt.mineAsync(); @@ -396,30 +454,30 @@ describe('Chain', function() { })); it('should fail csv lock checks', co(function* () { - var tx = (yield chain.db.getBlock(chain.height)).txs[0]; + var tx = (yield chain.db.getBlock(chain.height - 100)).txs[0]; var block = yield mineCSV(tx); - var csv, attempt, redeemer, err; + var csv, attempt, rtx, err; yield chain.add(block); csv = block.txs[1]; - redeemer = new MTX(); + rtx = new MTX(); - redeemer.addOutput({ + rtx.addOutput({ script: [ Script.array(new BN(2)), Script.opcodes.OP_CHECKSEQUENCEVERIFY ], - value: 10 * 1e8 + value: 1 * 1e8 }); - redeemer.addTX(csv, 0); - redeemer.setSequence(0, 2, false); + rtx.addTX(csv, 0); + rtx.setSequence(0, 2, false); attempt = yield miner.createBlock(); - attempt.addTX(redeemer.toTX(), redeemer.view); + attempt.addTX(rtx.toTX(), rtx.view); block = yield attempt.mineAsync(); @@ -434,11 +492,376 @@ describe('Chain', function() { })); it('should have correct wallet balance', co(function* () { - assert.equal(wallet.balance, 1289250000000); + assert.equal(wallet.balance, 1412499980000); + })); + + it('should fail to connect bad bits', co(function* () { + var attempt = yield miner.createBlock(); + attempt.block.bits = 553713663; + assert.equal(yield addBlock(attempt), 'bad-diffbits'); + })); + + it('should fail to connect bad MTP', co(function* () { + var mtp = yield chain.tip.getMedianTimeAsync(); + var attempt = yield miner.createBlock(); + attempt.block.ts = mtp - 1; + assert.equal(yield addBlock(attempt), 'time-too-old'); + })); + + it('should fail to connect bad time', co(function* () { + var mtp = yield chain.tip.getMedianTimeAsync(); + var attempt = yield miner.createBlock(); + var now = network.now() + 3 * 60 * 60; + attempt.block.ts = now; + assert.equal(yield addBlock(attempt), 'time-too-new'); + })); + + it('should fail to connect bad locktime', co(function* () { + var attempt = yield miner.createBlock(); + var tx = yield wallet.send({ locktime: 100000 }); + attempt.block.txs.push(tx.toTX()); + attempt.refresh(); + assert.equal(yield addBlock(attempt), 'bad-txns-nonfinal'); + })); + + it('should fail to connect bad cb height', co(function* () { + var bip34height = network.block.bip34height; + var attempt = yield miner.createBlock(); + var tx = attempt.block.txs[0]; + var input = tx.inputs[0]; + + input.script.set(0, new BN(10)); + input.script.compile(); + attempt.refresh(); + + try { + network.block.bip34height = 0; + assert.equal(yield addBlock(attempt), 'bad-cb-height'); + } finally { + network.block.bip34height = bip34height; + } + })); + + it('should fail to connect bad witness nonce size', co(function* () { + var attempt = yield miner.createBlock(); + var tx = attempt.block.txs[0]; + var input = tx.inputs[0]; + input.witness.set(0, new Buffer(33)); + input.witness.compile(); + assert.equal(yield addBlock(attempt), 'bad-witness-merkle-size'); + })); + + it('should fail to connect bad witness nonce', co(function* () { + var attempt = yield miner.createBlock(); + var tx = attempt.block.txs[0]; + var input = tx.inputs[0]; + input.witness.set(0, encoding.ONE_HASH); + input.witness.compile(); + assert.equal(yield addBlock(attempt), 'bad-witness-merkle-match'); + })); + + it('should fail to connect bad witness commitment', co(function* () { + var attempt = yield miner.createBlock(); + var tx = attempt.block.txs[0]; + var output = tx.outputs[1]; + var commit; + + assert(output.script.isCommitment()); + + commit = util.copy(output.script.get(1)); + commit.fill(0, 10); + output.script.set(1, commit); + output.script.compile(); + + attempt.updateMerkle(); + assert.equal(yield addBlock(attempt), 'bad-witness-merkle-match'); + })); + + it('should fail to connect unexpected witness', co(function* () { + var attempt = yield miner.createBlock(); + var tx = attempt.block.txs[0]; + var output = tx.outputs[1]; + assert(output.script.isCommitment()); + tx.outputs.pop(); + attempt.updateMerkle(); + assert.equal(yield addBlock(attempt), 'unexpected-witness'); + })); + + it('should add wit addrs to miner', co(function* () { + miner.addresses.length = 0; + miner.addAddress(wwallet.getReceive()); + assert.equal(wwallet.getReceive().getType(), 'witnesspubkeyhash'); + })); + + it('should mine 2000 witness blocks', co(function* () { + var i, block; + + for (i = 0; i < 2001; i++) { + block = yield miner.mineBlock(); + assert(block); + yield chain.add(block); + } + + assert.equal(chain.height, 2636); + })); + + it('should mine a witness tx', co(function* () { + var block = yield chain.db.getBlock(chain.height - 2000); + var cb = block.txs[0]; + var mtx = new MTX(); + var attempt, block; + + mtx.addTX(cb, 0); + mtx.addOutput(wwallet.getAddress(), 1000); + + wwallet.sign(mtx); + + attempt = yield miner.createBlock(); + attempt.addTX(mtx.toTX(), mtx.view); + + block = yield attempt.mineAsync(); + + yield chain.add(block); + })); + + it('should mine fail to mine too much weight', co(function* () { + var start = chain.height - 2000; + var end = chain.height - 200; + var attempt = yield miner.createBlock(); + var mtx = new MTX(); + var i, j, block, cb, block; + + for (i = start; i <= end; i++) { + block = yield chain.db.getBlock(i); + cb = block.txs[0]; + + mtx = new MTX(); + mtx.addTX(cb, 0); + + for (j = 0; j < 16; j++) + mtx.addOutput(wwallet.getAddress(), 1); + + wwallet.sign(mtx); + + attempt.block.txs.push(mtx.toTX()); + } + + attempt.refresh(); + + assert.equal(yield addBlock(attempt), 'bad-blk-weight'); + })); + + it('should mine fail to mine too much size', co(function* () { + var start = chain.height - 2000; + var end = chain.height - 200; + var attempt = yield miner.createBlock(); + var mtx = new MTX(); + var i, j, block, cb, block; + + for (i = start; i <= end; i++) { + block = yield chain.db.getBlock(i); + cb = block.txs[0]; + + mtx = new MTX(); + mtx.addTX(cb, 0); + + for (j = 0; j < 20; j++) + mtx.addOutput(wwallet.getAddress(), 1); + + wwallet.sign(mtx); + + attempt.block.txs.push(mtx.toTX()); + } + + attempt.refresh(); + + assert.equal(yield addBlock(attempt), 'bad-blk-length'); + })); + + it('should mine a big block', co(function* () { + var start = chain.height - 2000; + var end = chain.height - 200; + var attempt = yield miner.createBlock(); + var mtx = new MTX(); + var i, j, block, cb, block; + + for (i = start; i <= end; i++) { + block = yield chain.db.getBlock(i); + cb = block.txs[0]; + + mtx = new MTX(); + mtx.addTX(cb, 0); + + for (j = 0; j < 15; j++) + mtx.addOutput(wwallet.getAddress(), 1); + + wwallet.sign(mtx); + + attempt.block.txs.push(mtx.toTX()); + } + + attempt.refresh(); + + assert.equal(yield addBlock(attempt), 'OK'); + })); + + it('should fail to mine bad versions', co(function* () { + var i, attempt; + + for (i = 0; i <= 3; i++) { + attempt = yield miner.createBlock(); + attempt.block.version = i; + assert.equal(yield addBlock(attempt), 'bad-version'); + } + })); + + it('should fail to mine bad amount', co(function* () { + var attempt = yield miner.createBlock(); + var i; + + attempt.block.txs[0].outputs[0].value += 1; + attempt.updateMerkle(); + assert.equal(yield addBlock(attempt), 'bad-cb-amount'); + })); + + it('should fail to mine premature cb spend', co(function* () { + var attempt = yield miner.createBlock(); + var block = yield chain.db.getBlock(chain.height - 98); + var cb = block.txs[0]; + var mtx = new MTX(); + var i; + + mtx.addTX(cb, 0); + mtx.addOutput(wwallet.getAddress(), 1); + + wwallet.sign(mtx); + + attempt.addTX(mtx.toTX(), mtx.view); + + assert.equal(yield addBlock(attempt), + 'bad-txns-premature-spend-of-coinbase'); + })); + + it('should fail to mine vout belowout', co(function* () { + var attempt = yield miner.createBlock(); + var block = yield chain.db.getBlock(chain.height - 99); + var cb = block.txs[0]; + var mtx = new MTX(); + var i; + + mtx.addTX(cb, 0); + mtx.addOutput(wwallet.getAddress(), 1e8); + + wwallet.sign(mtx); + + attempt.block.txs.push(mtx.toTX()); + attempt.refresh(); + + assert.equal(yield addBlock(attempt), + 'bad-txns-in-belowout'); + })); + + it('should fail to mine in out toolarge', co(function* () { + var attempt = yield miner.createBlock(); + var block = yield chain.db.getBlock(chain.height - 99); + var cb = block.txs[0]; + var mtx = new MTX(); + var i; + + mtx.addTX(cb, 0); + mtx.addOutput(wwallet.getAddress(), Math.floor(consensus.MAX_MONEY / 2)); + mtx.addOutput(wwallet.getAddress(), Math.floor(consensus.MAX_MONEY / 2)); + mtx.addOutput(wwallet.getAddress(), Math.floor(consensus.MAX_MONEY / 2)); + + wwallet.sign(mtx); + + attempt.block.txs.push(mtx.toTX()); + attempt.refresh(); + + assert.equal(yield addBlock(attempt), + 'bad-txns-txouttotal-toolarge'); + })); + + it('should mine 111 multisig blocks', co(function* () { + var i, j, script, attempt, cb, output, val, block; + + script = new Script(); + script.push(new BN(20)); + + for (i = 0; i < 20; i++) + script.push(encoding.ZERO_KEY); + + script.push(new BN(20)); + script.push(opcodes.OP_CHECKMULTISIG); + script.compile(); + + script = Script.fromScripthash(script.hash160()); + + for (i = 0; i < 111; i++) { + attempt = yield miner.createBlock(); + cb = attempt.block.txs[0]; + val = cb.outputs[0].value; + + cb.outputs[0].value = 0; + + for (j = 0; j < Math.min(100, val); j++) { + output = new Output(); + output.script = script.clone(); + output.value = 1; + + cb.outputs.push(output); + } + + attempt.updateMerkle(); + + block = yield attempt.mineAsync(); + yield chain.add(block); + } + + assert.equal(chain.height, 2749); + })); + + it('should mine fail to mine too many sigops', co(function* () { + var start = chain.height - 110; + var end = chain.height - 100; + var attempt = yield miner.createBlock(); + var i, j, mtx, script, block, cb, block; + + script = new Script(); + script.push(new BN(20)); + + for (i = 0; i < 20; i++) + script.push(encoding.ZERO_KEY); + + script.push(new BN(20)); + script.push(opcodes.OP_CHECKMULTISIG); + script.compile(); + + for (i = start; i <= end; i++) { + block = yield chain.db.getBlock(i); + cb = block.txs[0]; + + if (cb.outputs.length === 2) + continue; + + mtx = new MTX(); + + for (j = 2; j < cb.outputs.length; j++) { + mtx.addTX(cb, j); + mtx.inputs[j - 2].script = new Script([script.toRaw()]); + } + + mtx.addOutput(wwallet.getAddress(), 1); + + attempt.block.txs.push(mtx.toTX()); + } + + attempt.refresh(); + + assert.equal(yield addBlock(attempt), 'bad-blk-sigops'); })); it('should cleanup', co(function* () { - consensus.COINBASE_MATURITY = 100; yield miner.close(); yield chain.close(); })); diff --git a/test/tx-test.js b/test/tx-test.js index 0d2ff8cb..6a5de6fb 100644 --- a/test/tx-test.js +++ b/test/tx-test.js @@ -12,7 +12,11 @@ var Block = require('../lib/primitives/block'); var Coin = require('../lib/primitives/coin'); var Output = require('../lib/primitives/output'); var Script = require('../lib/script/script'); +var Witness = require('../lib/script/witness'); +var Input = require('../lib/primitives/input'); var CoinView = require('../lib/coins/coinview'); +var KeyRing = require('../lib/primitives/keyring'); +var opcodes = Script.opcodes; var valid = require('./data/tx_valid.json'); var invalid = require('./data/tx_invalid.json'); @@ -102,6 +106,43 @@ function parseTest(data) { }; } +function buildTX(spk, ss, wit, view) { + var input, output, fund, spend, tx; + + input = new Input(); + output = new Output(); + output.value = 1; + output.script = spk; + + 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 = ss; + input.witness = wit; + + 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 + }; +} + describe('TX', function() { var raw = '010000000125393c67cd4f581456dd0805fa8e9db3abdf90dbe1d4b53e28' + '6490f35d22b6f2010000006b483045022100f4fa5ced20d2dbd2f905809d' + @@ -687,4 +728,165 @@ describe('TX', function() { assert.equal(block.getReward(view, 0), -1); }); }); + + it('should count sigops for multisig', function() { + var flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; + var view = new CoinView(); + var key = KeyRing.generate(); + var pub = key.publicKey; + var txs, spk, ss, wit; + + spk = Script.fromMultisig(1, 2, [pub, pub]); + + ss = new Script([ + opcodes.OP_0, + opcodes.OP_0 + ]); + + wit = new Witness(); + + txs = buildTX(spk, ss, wit, view); + + assert.equal(txs.spend.getSigopsCost(view, flags), 0); + assert.equal(txs.fund.getSigopsCost(view, flags), + consensus.MAX_MULTISIG_PUBKEYS * consensus.WITNESS_SCALE_FACTOR); + }); + + it('should count sigops for p2sh multisig', function() { + var flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; + var view = new CoinView(); + var key = KeyRing.generate(); + var pub = key.publicKey; + var txs, redeem, spk, ss, wit; + + redeem = Script.fromMultisig(1, 2, [pub, pub]); + spk = Script.fromScripthash(redeem.hash160()); + + ss = new Script([ + opcodes.OP_0, + opcodes.OP_0, + redeem.toRaw() + ]); + + wit = new Witness(); + + txs = buildTX(spk, ss, wit, view); + + assert.equal(txs.spend.getSigopsCost(view, flags), + 2 * consensus.WITNESS_SCALE_FACTOR); + }); + + it('should count sigops for p2wpkh', function() { + var flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; + var view = new CoinView(); + var key = KeyRing.generate(); + var pub = key.publicKey; + var txs, p2pk, spk, ss, wit; + + spk = Script.fromProgram(0, key.getKeyHash()); + + ss = new Script(); + + wit = new Witness([ + new Buffer([0]), + new Buffer([0]) + ]); + + txs = buildTX(spk, ss, wit, view); + + assert.equal(txs.spend.getSigopsCost(view, flags), 1); + assert.equal( + txs.spend.getSigopsCost(view, flags & ~Script.flags.VERIFY_WITNESS), + 0); + + spk = Script.fromProgram(1, key.getKeyHash()); + txs = buildTX(spk, ss, wit, view); + + assert.equal(txs.spend.getSigopsCost(view, flags), 0); + + spk = Script.fromProgram(0, key.getKeyHash()); + txs = buildTX(spk, ss, wit, view); + + txs.spend.inputs[0].prevout.hash = encoding.NULL_HASH; + txs.spend.inputs[0].prevout.index = 0xffffffff; + txs.spend.refresh(); + + assert.equal(txs.spend.getSigopsCost(view, flags), 0); + }); + + it('should count sigops for nested p2wpkh', function() { + var flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; + var view = new CoinView(); + var key = KeyRing.generate(); + var pub = key.publicKey; + var txs, p2pk, spk, ss, wit; + + p2pk = Script.fromProgram(0, key.getKeyHash()); + spk = Script.fromScripthash(p2pk.hash160()); + + ss = new Script([ + p2pk.toRaw() + ]); + + wit = new Witness([ + new Buffer([0]), + new Buffer([0]) + ]); + + txs = buildTX(spk, ss, wit, view); + + assert.equal(txs.spend.getSigopsCost(view, flags), 1); + }); + + it('should count sigops for p2wsh', function() { + var flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; + var view = new CoinView(); + var key = KeyRing.generate(); + var pub = key.publicKey; + var txs, ws, spk, ss, wit; + + ws = Script.fromMultisig(1, 2, [pub, pub]); + spk = Script.fromProgram(0, ws.sha256()); + + ss = new Script(); + + wit = new Witness([ + new Buffer([0]), + new Buffer([0]), + ws.toRaw() + ]); + + txs = buildTX(spk, ss, wit, view); + + assert.equal(txs.spend.getSigopsCost(view, flags), 2); + assert.equal( + txs.spend.getSigopsCost(view, flags & ~Script.flags.VERIFY_WITNESS), + 0); + }); + + it('should count sigops for nested p2wsh', function() { + var flags = Script.flags.VERIFY_WITNESS | Script.flags.VERIFY_P2SH; + var view = new CoinView(); + var key = KeyRing.generate(); + var pub = key.publicKey; + var txs, ws, p2sh, spk, ss, wit; + + ws = Script.fromMultisig(1, 2, [pub, pub]); + p2sh = Script.fromProgram(0, ws.sha256()); + spk = Script.fromScripthash(p2sh.hash160()); + + ss = new Script([ + p2sh.toRaw() + ]); + + wit = new Witness([ + new Buffer([0]), + new Buffer([0]), + ws.toRaw() + ]); + + txs = buildTX(spk, ss, wit, view); + + assert.equal(txs.spend.getSigopsCost(view, flags), 2); + }); }); diff --git a/test/util/memwallet.js b/test/util/memwallet.js index 63edef18..5d756f38 100644 --- a/test/util/memwallet.js +++ b/test/util/memwallet.js @@ -15,6 +15,7 @@ var Bloom = require('../../lib/utils/bloom'); var KeyRing = require('../../lib/primitives/keyring'); var Outpoint = require('../../lib/primitives/outpoint'); var Coin = require('../../lib/primitives/coin'); +var co = require('../../lib/utils/co'); function MemWallet(options) { if (!(this instanceof MemWallet)) @@ -23,13 +24,15 @@ function MemWallet(options) { this.network = Network.primary; this.master = null; this.key = null; + this.witness = false; this.account = 0; this.receiveDepth = 1; this.changeDepth = 1; this.receive = null; this.change = null; + this.map = {}; this.coins = {}; - this.undo = {}; + this.spent = {}; this.paths = {}; this.balance = 0; this.txs = 0; @@ -57,6 +60,11 @@ MemWallet.prototype.fromOptions = function fromOptions(options) { this.key = options.key; } + if (options.witness != null) { + assert(typeof options.witness === 'boolean'); + this.witness = options.witness; + } + if (options.account != null) { assert(typeof options.account === 'number'); this.account = options.account; @@ -128,10 +136,13 @@ MemWallet.prototype.derivePath = function derivePath(path) { MemWallet.prototype.deriveKey = function deriveKey(branch, index) { var key = this.master.deriveAccount44(this.account); key = key.derive(branch).derive(index); - return new KeyRing({ + key = new KeyRing({ network: this.network, - privateKey: key.privateKey + privateKey: key.privateKey, + witness: this.witness }); + key.witness = this.witness; + return key; }; MemWallet.prototype.getKey = function getKey(hash) { @@ -150,7 +161,7 @@ MemWallet.prototype.getCoin = function getCoin(key) { }; MemWallet.prototype.getUndo = function getUndo(key) { - return this.undo[key]; + return this.spent[key]; }; MemWallet.prototype.addCoin = function addCoin(coin) { @@ -159,7 +170,7 @@ MemWallet.prototype.addCoin = function addCoin(coin) { this.filter.add(op.toRaw()); - delete this.undo[key]; + delete this.spent[key]; this.coins[key] = coin; this.balance += coin.value; @@ -171,7 +182,7 @@ MemWallet.prototype.removeCoin = function removeCoin(key) { if (!coin) return; - this.undo[key] = coin; + this.spent[key] = coin; this.balance -= coin.value; delete this.coins[key]; @@ -228,12 +239,16 @@ MemWallet.prototype.removeBlock = function removeBlock(entry, txs) { }; MemWallet.prototype.addTX = function addTX(tx, height) { + var hash = tx.hash('hex'); var result = false; var i, op, path, addr, coin, input, output; if (height == null) height = -1; + if (this.map[hash]) + return true; + for (i = 0; i < tx.inputs.length; i++) { input = tx.inputs[i]; op = input.prevout.toKey(); @@ -266,8 +281,10 @@ MemWallet.prototype.addTX = function addTX(tx, height) { this.syncKey(path); } - if (result) + if (result) { this.txs++; + this.map[hash] = true; + } return result; }; @@ -277,6 +294,9 @@ MemWallet.prototype.removeTX = function removeTX(tx, height) { var result = false; var i, op, coin, input, output; + if (!this.map[hash]) + return false; + for (i = 0; i < tx.outputs.length; i++) { output = tx.outputs[i]; op = Outpoint(hash, i).toKey(); @@ -306,6 +326,8 @@ MemWallet.prototype.removeTX = function removeTX(tx, height) { if (result) this.txs--; + delete this.map[hash]; + return result; }; @@ -368,33 +390,32 @@ MemWallet.prototype.sign = function sign(mtx) { mtx.sign(keys); }; -MemWallet.prototype.send = function send(options) { - var self = this; +MemWallet.prototype.create = co(function* create(options) { var mtx = new MTX(options); var tx; - this.fund(mtx, options).then(function() { - assert(mtx.getFee() <= MTX.Selector.MAX_FEE, 'TX exceeds MAX_FEE.'); + yield this.fund(mtx, options); - mtx.sortMembers(); + assert(mtx.getFee() <= MTX.Selector.MAX_FEE, 'TX exceeds MAX_FEE.'); - if (options.locktime != null) - mtx.setLocktime(options.locktime); + mtx.sortMembers(); - self.sign(mtx); + if (options.locktime != null) + mtx.setLocktime(options.locktime); - if (!mtx.isSigned()) - throw new Error('Cannot sign tx.'); + this.sign(mtx); - tx = mtx.toTX(); + if (!mtx.isSigned()) + throw new Error('Cannot sign tx.'); - self.addTX(tx); - }).catch(function(err) { - throw err; - }); + return mtx; +}); - return tx; -}; +MemWallet.prototype.send = co(function* send(options) { + var mtx = yield this.create(options); + this.addTX(mtx.toTX()); + return mtx; +}); function Path(hash, branch, index) { this.hash = hash;