From ef13e0d3c826aefc08ce953f02776f8ba45e3277 Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 14 Sep 2020 16:06:57 -0400 Subject: [PATCH 1/2] test: cover mempool eviction when full --- test/mempool-test.js | 145 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 145 insertions(+) diff --git a/test/mempool-test.js b/test/mempool-test.js index 170429302b..5bccec0aa1 100644 --- a/test/mempool-test.js +++ b/test/mempool-test.js @@ -1084,4 +1084,149 @@ describe('Mempool', function() { assert(!mempool.getClaim(claim.hash())); }); }); + + describe('Mempool eviction', function () { + // Computed in advance with MempoolEntry.memUsage() + const txMemUsage = 1728; + // Should allow 9 transactions in mempool. + // The 10th transaction will push the mempool size over 100% of the limit. + // Mempool will then remove two transactions to get under 90% limit. + const maxSize = txMemUsage * 10 - 1; + // 1 hour + const expiryTime = 60 * 60; + + const workers = new WorkerPool({ + enabled: true, + size: 2 + }); + + const chain = new Chain({ + memory: true, + workers, + network: 'regtest' + }); + + const mempool = new Mempool({ + chain, + workers, + memory: true, + maxSize, + expiryTime + }); + + before(async () => { + await mempool.open(); + await chain.open(); + await workers.open(); + }); + + after(async () => { + await workers.close(); + await chain.close(); + await mempool.close(); + }); + + // Number of coins available in + // chaincoins (100k satoshi per coin). + const N = 100; + const chaincoins = new MemWallet(); + const wallet = new MemWallet(); + + async function getMockBlock(chain, txs = [], cb = true) { + if (cb) { + const raddr = KeyRing.generate().getAddress(); + const mtx = new MTX(); + mtx.addInput(new Input()); + mtx.addOutput(raddr, 0); + mtx.locktime = chain.height + 1; + + txs = [mtx.toTX(), ...txs]; + } + + const view = new CoinView(); + for (const tx of txs) { + view.addTX(tx, -1); + } + + const now = Math.floor(Date.now() / 1000); + const time = chain.tip.time <= now ? chain.tip.time + 1 : now; + + const block = new Block(); + block.txs = txs; + block.prevBlock = chain.tip.hash; + block.time = time; + block.bits = await chain.getTarget(block.time, chain.tip); + + // Ensure mockblocks are unique (required for reorg testing) + block.merkleRoot = block.createMerkleRoot(); + block.witnessRoot = block.createWitnessRoot(); + block.treeRoot = chain.db.treeRoot(); + + return [block, view]; + } + + it('should create coins in chain', async () => { + const mtx = new MTX(); + mtx.locktime = chain.height + 1; + mtx.addInput(new Input()); + + for (let i = 0; i < N; i++) { + const addr = chaincoins.createReceive().getAddress(); + mtx.addOutput(addr, 100000); + } + + const cb = mtx.toTX(); + const [block] = await getMockBlock(chain, [cb], false); + const entry = await chain.add(block, VERIFY_BODY); + + await mempool._addBlock(entry, block.txs); + + // Add 100 blocks so we don't get + // premature spend of coinbase. + for (let i = 0; i < 100; i++) { + const [block] = await getMockBlock(chain); + const entry = await chain.add(block, VERIFY_BODY); + + await mempool._addBlock(entry, block.txs); + } + + chaincoins.addTX(cb); + }); + + it('should limit mempool size', async () => { + let expectedSize = 0; + + for (let i = 0; i < N; i++) { + // Spend a different coin each time to avoid exceeding max ancestors. + const coin = chaincoins.getCoins()[i]; + const addr = wallet.createReceive().getAddress(); + + const mtx = new MTX(); + mtx.addCoin(coin); + // Increment fee with each TX so oldest TX gets evicted first. + // Otherwise the new TX might be the one that gets evicted, + // resulting in a "mempool full" error instead. + mtx.addOutput(addr, 90000 - (10 * i)); + chaincoins.sign(mtx); + const tx = mtx.toTX(); + + expectedSize += txMemUsage; + + if (expectedSize < maxSize) { + await mempool.addTX(tx); + } else { + assert(i >= 9); + let evicted = false; + mempool.once('remove entry', () => { + evicted = true; + // We've exceeded the max size by 1 TX + // Mempool will remove 2 TXs to get below 90% limit. + expectedSize -= txMemUsage * 2; + }); + await mempool.addTX(tx); + assert(evicted); + } + } + }); + }); }); From df8547b8501cbb3dd55609ef5b4b0d9ac65ad31d Mon Sep 17 00:00:00 2001 From: Matthew Zipkin Date: Mon, 14 Sep 2020 16:38:58 -0400 Subject: [PATCH 2/2] mempool: evict expired transactions regardless of mempool size --- lib/mempool/mempool.js | 3 --- test/mempool-test.js | 58 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 3 deletions(-) diff --git a/lib/mempool/mempool.js b/lib/mempool/mempool.js index bb80d8a401..00379815d0 100644 --- a/lib/mempool/mempool.js +++ b/lib/mempool/mempool.js @@ -584,9 +584,6 @@ class Mempool extends EventEmitter { limitSize(added) { const maxSize = this.options.maxSize; - if (this.size <= maxSize) - return false; - const threshold = maxSize - (maxSize / 10); const expiryTime = this.options.expiryTime; diff --git a/test/mempool-test.js b/test/mempool-test.js index 5bccec0aa1..79130a3414 100644 --- a/test/mempool-test.js +++ b/test/mempool-test.js @@ -1228,5 +1228,63 @@ describe('Mempool', function() { } } }); + + it('should evict old transactions', async () => { + // Clear mempool. Note that TXs in last test were not + // added to the wallet: we can re-spend those coins. + await mempool.reset(); + + let now = 0; + const original = util.now; + try { + util.now = () => { + return now; + }; + + // After we cross the expiry threshold, one TX at a time + // will start to expire, starting with the oldest. + const sent = []; + let evicted = 0; + mempool.on('remove entry', (entry) => { + const expected = sent.shift(); + assert.bufferEqual(entry.tx.hash(), expected); + evicted++; + }); + + for (let i = 0; i < N; i++) { + // Spend a different coin each time to avoid exceeding max ancestors. + const coin = chaincoins.getCoins()[i]; + const addr = wallet.createReceive().getAddress(); + + const mtx = new MTX(); + mtx.addCoin(coin); + mtx.addOutput(addr, 90000); + chaincoins.sign(mtx); + const tx = mtx.toTX(); + + sent.push(tx.hash()); + + // mempool size is not a factor + assert(mempool.size + (txMemUsage * 2) < maxSize); + + await mempool.addTX(tx); + + // Time travel forward ten minutes + now += 60 * 10; + + // The first 6 TXs are added without incident. + // After that, a virtual hour will have passed, and + // each new TX will trigger the eviction of one old TX. + if (i < 6) { + assert(mempool.map.size === i + 1); + } else { + assert(mempool.map.size === 6); + assert.strictEqual(evicted, (i + 1) - 6); + } + } + } finally { + util.now = original; + } + }); }); });