From 7f0357b5cfe099f00264aa66f6baf98eb3e0807a Mon Sep 17 00:00:00 2001 From: Nodari Chkuaselidze Date: Wed, 24 May 2023 18:25:52 +0400 Subject: [PATCH 1/2] mempool: invalidate claims when the claim period ends. --- lib/mempool/mempool.js | 7 ++ test/mempool-invalidation-test.js | 165 ++++++++++++++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 test/mempool-invalidation-test.js diff --git a/lib/mempool/mempool.js b/lib/mempool/mempool.js index 8b8426c2b..e2d705a1e 100644 --- a/lib/mempool/mempool.js +++ b/lib/mempool/mempool.js @@ -202,6 +202,7 @@ class Mempool extends EventEmitter { */ async _addBlock(block, txs, view) { + const nextHeight = block.height + 1; const entries = []; const cb = txs[0]; @@ -284,6 +285,12 @@ class Mempool extends EventEmitter { this.untrackClaim(entry); } + // At the chain.tip.height of claimPeriod - 1, we no longer + // accept new claims as they will be invalid in the next block. + // We also need to clean them up from the mempool. + if (nextHeight === this.network.names.claimPeriod) + this.dropClaims(); + // Remove all GooSig based airdrops from the mempool // when the block one before the height that disables // GooSig is added to the mempool to prevent the diff --git a/test/mempool-invalidation-test.js b/test/mempool-invalidation-test.js new file mode 100644 index 000000000..9e45da134 --- /dev/null +++ b/test/mempool-invalidation-test.js @@ -0,0 +1,165 @@ +'use strict'; + +const assert = require('bsert'); +const {BufferMap} = require('buffer-map'); +const Network = require('../lib/protocol/network'); +const FullNode = require('../lib/node/fullnode'); +const ownership = require('../lib/covenants/ownership'); +const rules = require('../lib/covenants/rules'); +const {forEvent} = require('./util/common'); + +const network = Network.get('regtest'); +const { + treeInterval, + claimPeriod +} = network.names; + +const ACTUAL_CLAIM_PERIOD = claimPeriod; + +describe('Mempool Invalidation', function() { + const NAMES = [ + // roots + 'nl', + + // top 100 + 'paypal', + + // custom + 'cloudflare', + + // other + 'steamdb' + ]; + + describe('Claim Invalidation (Integration)', function() { + this.timeout(50000); + + let node, wallet; + + // copy names + const TEST_CLAIMS = NAMES.slice(); + + before(async () => { + node = new FullNode({ + memory: true, + network: network.type, + plugins: [require('../lib/wallet/plugin')] + }); + + await node.ensure(); + await node.open(); + + // Ignore claim validation + ownership.ignore = true; + + const walletPlugin = node.require('walletdb'); + const wdb = walletPlugin.wdb; + wallet = await wdb.get('primary'); + + const addr = await wallet.receiveAddress('default'); + node.miner.addAddress(addr.toString()); + + // first interval maturity + // second interval mine claim + network.names.claimPeriod = treeInterval * 3; + + // third interval last block should invalidate. + }); + + after(async () => { + network.names.claimPeriod = ACTUAL_CLAIM_PERIOD; + + await node.close(); + }); + + it('should mine an interval', async () => { + for (let i = 0; i < treeInterval; i++) + await mineBlock(node); + }); + + it('should mine claims before claimPeriod timeout', async () => { + const name = TEST_CLAIMS.shift(); + + const claim = await wallet.makeFakeClaim(name); + let block; + + await node.mempool.insertClaim(claim); + assert.strictEqual(node.mempool.claims.size, 1); + + // retain claim in mempool. + block = await mineBlock(node, { ignoreClaims: true }); + assert.strictEqual(node.mempool.claims.size, 1); + assert.strictEqual(block.txs[0].outputs.length, 1); + + // Now we can mine it. + block = await mineBlock(node); + assert.strictEqual(node.mempool.claims.size, 0); + assert.strictEqual(block.txs[0].outputs.length, 2); + assert.strictEqual(block.txs[0].outputs[1].covenant.type, rules.types.CLAIM); + }); + + it('should invalidate claim after claimPeriod timeout', async () => { + const name = TEST_CLAIMS.shift(); + const claim = await wallet.makeFakeClaim(name); + + let block = null; + + // Mempool treats txs in it as if they were mined in the next block, + // so we need next block to still be valid. + while (node.chain.tip.height < network.names.claimPeriod - 2) + await mineBlock(node); + + await node.mempool.insertClaim(claim); + block = await mineBlock(node, { ignoreClaims: true }); + + // Should invalidate the claim, because next block can't have claims. + assert.strictEqual(node.mempool.claims.size, 0); + assert.strictEqual(block.txs[0].outputs.length, 1); + + // Should fail to insert claim, as they can't be mined. + let err; + try { + err = await node.mempool.insertClaim(claim); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.type, 'VerifyError'); + assert.strictEqual(err.reason, 'invalid-covenant'); + + block = await mineBlock(node); + assert.strictEqual(node.mempool.claims.size, 0); + assert.strictEqual(block.txs[0].outputs.length, 1); + }); + }); +}); + +async function mineBlock(node, opts = {}) { + assert(node); + const chain = node.chain; + const miner = node.miner; + + const ignoreClaims = opts.ignoreClaims || false; + + const forBlock = forEvent(node, 'block', 1, 2000); + + let backupClaims = null; + + if (ignoreClaims) { + backupClaims = node.mempool.claims; + node.mempool.claims = new BufferMap(); + } + const job = await miner.cpu.createJob(chain.tip); + + job.refresh(); + + if (ignoreClaims) + node.mempool.claims = backupClaims; + + const block = await job.mineAsync(); + await chain.add(block); + await forBlock; + + return block; +} From f857df5eed104f75d4d513e48aaa9de724839ff8 Mon Sep 17 00:00:00 2001 From: Nodari Chkuaselidze Date: Thu, 25 May 2023 16:01:43 +0400 Subject: [PATCH 2/2] test: add reorg test to the mempool claim invalidation. --- lib/mempool/mempool.js | 2 +- test/mempool-invalidation-test.js | 147 ++++++++++++++++++++++++++++-- 2 files changed, 138 insertions(+), 11 deletions(-) diff --git a/lib/mempool/mempool.js b/lib/mempool/mempool.js index e2d705a1e..26c05dcd7 100644 --- a/lib/mempool/mempool.js +++ b/lib/mempool/mempool.js @@ -295,7 +295,7 @@ class Mempool extends EventEmitter { // when the block one before the height that disables // GooSig is added to the mempool to prevent the // mining of invalid blocks. - if (block.height + 1 === this.network.goosigStop) { + if (nextHeight === this.network.goosigStop) { for (const [hash, entry] of this.airdrops.entries()) { const airdrop = this.getAirdrop(hash); const key = airdrop.getKey(); diff --git a/test/mempool-invalidation-test.js b/test/mempool-invalidation-test.js index 9e45da134..eaafb312d 100644 --- a/test/mempool-invalidation-test.js +++ b/test/mempool-invalidation-test.js @@ -87,12 +87,12 @@ describe('Mempool Invalidation', function() { assert.strictEqual(node.mempool.claims.size, 1); // retain claim in mempool. - block = await mineBlock(node, { ignoreClaims: true }); + [block] = await mineBlock(node, { ignoreClaims: true }); assert.strictEqual(node.mempool.claims.size, 1); assert.strictEqual(block.txs[0].outputs.length, 1); // Now we can mine it. - block = await mineBlock(node); + [block] = await mineBlock(node); assert.strictEqual(node.mempool.claims.size, 0); assert.strictEqual(block.txs[0].outputs.length, 2); assert.strictEqual(block.txs[0].outputs[1].covenant.type, rules.types.CLAIM); @@ -110,7 +110,7 @@ describe('Mempool Invalidation', function() { await mineBlock(node); await node.mempool.insertClaim(claim); - block = await mineBlock(node, { ignoreClaims: true }); + [block] = await mineBlock(node, { ignoreClaims: true }); // Should invalidate the claim, because next block can't have claims. assert.strictEqual(node.mempool.claims.size, 0); @@ -128,7 +128,127 @@ describe('Mempool Invalidation', function() { assert.strictEqual(err.type, 'VerifyError'); assert.strictEqual(err.reason, 'invalid-covenant'); - block = await mineBlock(node); + [block] = await mineBlock(node); + assert.strictEqual(node.mempool.claims.size, 0); + assert.strictEqual(block.txs[0].outputs.length, 1); + }); + }); + + describe('Claim Invalidation on reorg (Integration)', function() { + this.timeout(50000); + + let node, wallet; + + // copy names + const TEST_CLAIMS = NAMES.slice(); + + before(async () => { + node = new FullNode({ + memory: true, + network: network.type, + plugins: [require('../lib/wallet/plugin')] + }); + + await node.ensure(); + await node.open(); + + // Ignore claim validation + ownership.ignore = true; + + const walletPlugin = node.require('walletdb'); + const wdb = walletPlugin.wdb; + wallet = await wdb.get('primary'); + + const addr = await wallet.receiveAddress('default'); + node.miner.addAddress(addr.toString()); + + // first interval maturity + // second interval mine claim + network.names.claimPeriod = treeInterval * 3; + + // third interval last block should invalidate. + }); + + after(async () => { + network.names.claimPeriod = ACTUAL_CLAIM_PERIOD; + + await node.close(); + }); + + it('should mine an interval', async () => { + for (let i = 0; i < treeInterval; i++) + await mineBlock(node); + }); + + it('should mine claims before claimPeriod timeout', async () => { + const name = TEST_CLAIMS.shift(); + + const claim = await wallet.makeFakeClaim(name); + let block; + + await node.mempool.insertClaim(claim); + assert.strictEqual(node.mempool.claims.size, 1); + + // retain claim in mempool. + [block] = await mineBlock(node, { ignoreClaims: true }); + assert.strictEqual(node.mempool.claims.size, 1); + assert.strictEqual(block.txs[0].outputs.length, 1); + + // Now we can mine it. + [block] = await mineBlock(node); + assert.strictEqual(node.mempool.claims.size, 0); + assert.strictEqual(block.txs[0].outputs.length, 2); + assert.strictEqual(block.txs[0].outputs[1].covenant.type, rules.types.CLAIM); + }); + + it('should invalidate claim after claimPeriod timeout', async () => { + const name = TEST_CLAIMS.shift(); + const claim = await wallet.makeFakeClaim(name); + + let block, entry; + + // Mempool treats txs in it as if they were mined in the next block, + // so we need next block to still be valid. + while (node.chain.tip.height < network.names.claimPeriod - 2) + await mineBlock(node); + + await node.mempool.insertClaim(claim); + // here we experience a reorg into the claim period. + const tip = node.chain.tip; + const prev = await node.chain.getPrevious(tip); + + [block, entry] = await mineBlock(node, { + ignoreClaims: true, + tip: prev, + blockWait: false + }); + + assert.strictEqual(node.mempool.claims.size, 1); + assert.strictEqual(block.txs[0].outputs.length, 1); + + // Now reorg. + [block, entry] = await mineBlock(node, { + ignoreClaims: true, + tip: entry + }); + + // Should invalidate the claim, because next block can't have claims. + assert.strictEqual(node.mempool.claims.size, 0); + assert.strictEqual(block.txs[0].outputs.length, 1); + + // Should fail to insert claim, as they can't be mined. + let err; + try { + err = await node.mempool.insertClaim(claim); + } catch (e) { + err = e; + } + + assert(err); + assert.strictEqual(err.type, 'VerifyError'); + assert.strictEqual(err.reason, 'invalid-covenant'); + + [block] = await mineBlock(node); assert.strictEqual(node.mempool.claims.size, 0); assert.strictEqual(block.txs[0].outputs.length, 1); }); @@ -140,9 +260,14 @@ async function mineBlock(node, opts = {}) { const chain = node.chain; const miner = node.miner; - const ignoreClaims = opts.ignoreClaims || false; + const ignoreClaims = opts.ignoreClaims ?? false; + const tip = opts.tip || chain.tip; + const blockWait = opts.blockWait ?? true; + + let forBlock = null; - const forBlock = forEvent(node, 'block', 1, 2000); + if (blockWait) + forBlock = forEvent(node, 'block', 1, 2000); let backupClaims = null; @@ -150,7 +275,7 @@ async function mineBlock(node, opts = {}) { backupClaims = node.mempool.claims; node.mempool.claims = new BufferMap(); } - const job = await miner.cpu.createJob(chain.tip); + const job = await miner.cpu.createJob(tip); job.refresh(); @@ -158,8 +283,10 @@ async function mineBlock(node, opts = {}) { node.mempool.claims = backupClaims; const block = await job.mineAsync(); - await chain.add(block); - await forBlock; + const entry = await chain.add(block); + + if (blockWait) + await forBlock; - return block; + return [block, entry]; }