public void MempoolRemoveTest() { TestMemPoolEntryHelper entry = new TestMemPoolEntryHelper(); // Parent transaction with three children, // and three grand-children: Transaction txParent = new Transaction(); txParent.AddInput(new TxIn()); txParent.Inputs[0].ScriptSig = new Script(OpcodeType.OP_11); for (int i = 0; i < 3; i++) { txParent.AddOutput(new TxOut(new Money(33000L), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); } Transaction[] txChild = new Transaction[3]; for (int i = 0; i < 3; i++) { txChild[i] = new Transaction(); txChild[i].AddInput(new TxIn(new OutPoint(txParent, i), new Script(OpcodeType.OP_11))); txChild[i].AddOutput(new TxOut(new Money(11000L), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); } Transaction[] txGrandChild = new Transaction[3]; for (int i = 0; i < 3; i++) { txGrandChild[i] = new Transaction(); txGrandChild[i].AddInput(new TxIn(new OutPoint(txChild[i], 0), new Script(OpcodeType.OP_11))); txGrandChild[i].AddOutput(new TxOut(new Money(11000L), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); } var settings = NodeSettings.Default(); TxMempool testPool = new TxMempool(DateTimeProvider.Default, new BlockPolicyEstimator(new MempoolSettings(settings), settings.LoggerFactory, settings), settings.LoggerFactory, settings); // Nothing in pool, remove should do nothing: var poolSize = testPool.Size; testPool.RemoveRecursive(txParent); Assert.Equal(testPool.Size, poolSize); // Just the parent: testPool.AddUnchecked(txParent.GetHash(), entry.FromTx(txParent)); poolSize = testPool.Size; testPool.RemoveRecursive(txParent); Assert.Equal(testPool.Size, poolSize - 1); // Parent, children, grandchildren: testPool.AddUnchecked(txParent.GetHash(), entry.FromTx(txParent)); for (int i = 0; i < 3; i++) { testPool.AddUnchecked(txChild[i].GetHash(), entry.FromTx(txChild[i])); testPool.AddUnchecked(txGrandChild[i].GetHash(), entry.FromTx(txGrandChild[i])); } // Remove Child[0], GrandChild[0] should be removed: poolSize = testPool.Size; testPool.RemoveRecursive(txChild[0]); Assert.Equal(testPool.Size, poolSize - 2); // ... make sure grandchild and child are gone: poolSize = testPool.Size; testPool.RemoveRecursive(txGrandChild[0]); Assert.Equal(testPool.Size, poolSize); poolSize = testPool.Size; testPool.RemoveRecursive(txChild[0]); Assert.Equal(testPool.Size, poolSize); // Remove parent, all children/grandchildren should go: poolSize = testPool.Size; testPool.RemoveRecursive(txParent); Assert.Equal(testPool.Size, poolSize - 5); Assert.Equal(0, testPool.Size); // Add children and grandchildren, but NOT the parent (simulate the parent being in a block) for (int i = 0; i < 3; i++) { testPool.AddUnchecked(txChild[i].GetHash(), entry.FromTx(txChild[i])); testPool.AddUnchecked(txGrandChild[i].GetHash(), entry.FromTx(txGrandChild[i])); } // Now remove the parent, as might happen if a block-re-org occurs but the parent cannot be // put into the mempool (maybe because it is non-standard): poolSize = testPool.Size; testPool.RemoveRecursive(txParent); Assert.Equal(poolSize - 6, testPool.Size); Assert.Equal(0, testPool.Size); }
public void MempoolSizeLimitTest() { var settings = NodeSettings.Default(); var dateTimeSet = new DateTimeProviderSet(); var pool = new TxMempool(dateTimeSet, new BlockPolicyEstimator(new MempoolSettings(settings), settings.LoggerFactory, settings), settings.LoggerFactory, settings); var entry = new TestMemPoolEntryHelper(); entry.Priority(10.0); Transaction tx1 = new Transaction(); tx1.AddInput(new TxIn(new Script(OpcodeType.OP_1))); tx1.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_1, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx1.GetHash(), entry.Fee(10000L).FromTx(tx1, pool)); Transaction tx2 = new Transaction(); tx2.AddInput(new TxIn(new Script(OpcodeType.OP_2))); tx2.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_2, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx2.GetHash(), entry.Fee(5000L).FromTx(tx2, pool)); pool.TrimToSize(pool.DynamicMemoryUsage()); // should do nothing Assert.True(pool.Exists(tx1.GetHash())); Assert.True(pool.Exists(tx2.GetHash())); pool.TrimToSize(pool.DynamicMemoryUsage() * 3 / 4); // should remove the lower-feerate transaction Assert.True(pool.Exists(tx1.GetHash())); Assert.True(!pool.Exists(tx2.GetHash())); pool.AddUnchecked(tx2.GetHash(), entry.FromTx(tx2, pool)); Transaction tx3 = new Transaction(); tx3.AddInput(new TxIn(new OutPoint(tx2.GetHash(), 0), new Script(OpcodeType.OP_2))); tx3.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_3, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx3.GetHash(), entry.Fee(20000L).FromTx(tx3, pool)); pool.TrimToSize(pool.DynamicMemoryUsage() * 3 / 4); // tx3 should pay for tx2 (CPFP) Assert.True(!pool.Exists(tx1.GetHash())); Assert.True(pool.Exists(tx2.GetHash())); Assert.True(pool.Exists(tx3.GetHash())); pool.TrimToSize(tx1.GetVirtualSize()); // mempool is limited to tx1's size in memory usage, so nothing fits Assert.True(!pool.Exists(tx1.GetHash())); Assert.True(!pool.Exists(tx2.GetHash())); Assert.True(!pool.Exists(tx3.GetHash())); FeeRate maxFeeRateRemoved = new FeeRate(25000, tx3.GetVirtualSize() + tx2.GetVirtualSize()); Assert.Equal(pool.GetMinFee(1).FeePerK, maxFeeRateRemoved.FeePerK + 1000); Transaction tx4 = new Transaction(); tx4.AddInput(new TxIn(new Script(OpcodeType.OP_4))); tx4.AddInput(new TxIn(new Script(OpcodeType.OP_4))); tx4.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_4, OpcodeType.OP_EQUAL))); tx4.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_4, OpcodeType.OP_EQUAL))); Transaction tx5 = new Transaction(); tx5.AddInput(new TxIn(new OutPoint(tx4.GetHash(), 0), new Script(OpcodeType.OP_4))); tx5.AddInput(new TxIn(new Script(OpcodeType.OP_5))); tx5.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_5, OpcodeType.OP_EQUAL))); tx5.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_5, OpcodeType.OP_EQUAL))); Transaction tx6 = new Transaction(); tx6.AddInput(new TxIn(new OutPoint(tx4.GetHash(), 0), new Script(OpcodeType.OP_4))); tx6.AddInput(new TxIn(new Script(OpcodeType.OP_6))); tx6.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_6, OpcodeType.OP_EQUAL))); tx6.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_6, OpcodeType.OP_EQUAL))); Transaction tx7 = new Transaction(); tx7.AddInput(new TxIn(new OutPoint(tx5.GetHash(), 0), new Script(OpcodeType.OP_5))); tx7.AddInput(new TxIn(new OutPoint(tx6.GetHash(), 0), new Script(OpcodeType.OP_6))); tx7.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_7, OpcodeType.OP_EQUAL))); tx7.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_7, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx4.GetHash(), entry.Fee(7000L).FromTx(tx4, pool)); pool.AddUnchecked(tx5.GetHash(), entry.Fee(1000L).FromTx(tx5, pool)); pool.AddUnchecked(tx6.GetHash(), entry.Fee(1100L).FromTx(tx6, pool)); pool.AddUnchecked(tx7.GetHash(), entry.Fee(9000L).FromTx(tx7, pool)); // we only require this remove, at max, 2 txn, because its not clear what we're really optimizing for aside from that pool.TrimToSize(pool.DynamicMemoryUsage() - 1); Assert.True(pool.Exists(tx4.GetHash())); Assert.True(pool.Exists(tx6.GetHash())); Assert.True(!pool.Exists(tx7.GetHash())); if (!pool.Exists(tx5.GetHash())) { pool.AddUnchecked(tx5.GetHash(), entry.Fee(1000L).FromTx(tx5, pool)); } pool.AddUnchecked(tx7.GetHash(), entry.Fee(9000L).FromTx(tx7, pool)); pool.TrimToSize(pool.DynamicMemoryUsage() / 2); // should maximize mempool size by only removing 5/7 Assert.True(pool.Exists(tx4.GetHash())); Assert.True(!pool.Exists(tx5.GetHash())); Assert.True(pool.Exists(tx6.GetHash())); Assert.True(!pool.Exists(tx7.GetHash())); pool.AddUnchecked(tx5.GetHash(), entry.Fee(1000L).FromTx(tx5, pool)); pool.AddUnchecked(tx7.GetHash(), entry.Fee(9000L).FromTx(tx7, pool)); List <Transaction> vtx = new List <Transaction>(); dateTimeSet.time = 42 + TxMempool.RollingFeeHalflife; Assert.Equal(pool.GetMinFee(1).FeePerK.Satoshi, maxFeeRateRemoved.FeePerK.Satoshi + 1000); // ... we should keep the same min fee until we get a block pool.RemoveForBlock(vtx, 1); dateTimeSet.time = 42 + 2 * +TxMempool.RollingFeeHalflife; Assert.Equal(pool.GetMinFee(1).FeePerK.Satoshi, (maxFeeRateRemoved.FeePerK.Satoshi + 1000) / 2); // ... then feerate should drop 1/2 each halflife dateTimeSet.time = 42 + 2 * TxMempool.RollingFeeHalflife + TxMempool.RollingFeeHalflife / 2; Assert.Equal(pool.GetMinFee(pool.DynamicMemoryUsage() * 5 / 2).FeePerK.Satoshi, (maxFeeRateRemoved.FeePerK.Satoshi + 1000) / 4); // ... with a 1/2 halflife when mempool is < 1/2 its target size dateTimeSet.time = 42 + 2 * TxMempool.RollingFeeHalflife + TxMempool.RollingFeeHalflife / 2 + TxMempool.RollingFeeHalflife / 4; Assert.Equal(pool.GetMinFee(pool.DynamicMemoryUsage() * 9 / 2).FeePerK.Satoshi, (maxFeeRateRemoved.FeePerK.Satoshi + 1000) / 8); // ... with a 1/4 halflife when mempool is < 1/4 its target size dateTimeSet.time = 42 + 7 * TxMempool.RollingFeeHalflife + TxMempool.RollingFeeHalflife / 2 + TxMempool.RollingFeeHalflife / 4; Assert.Equal(1000, pool.GetMinFee(1).FeePerK.Satoshi); // ... but feerate should never drop below 1000 dateTimeSet.time = 42 + 8 * TxMempool.RollingFeeHalflife + TxMempool.RollingFeeHalflife / 2 + TxMempool.RollingFeeHalflife / 4; Assert.Equal(0, pool.GetMinFee(1).FeePerK); // ... unless it has gone all the way to 0 (after getting past 1000/2) }
public void MempoolIndexingTest() { var settings = NodeSettings.Default(); var pool = new TxMempool(DateTimeProvider.Default, new BlockPolicyEstimator(new MempoolSettings(settings), settings.LoggerFactory, settings), settings.LoggerFactory, settings); var entry = new TestMemPoolEntryHelper(); /* 3rd highest fee */ Transaction tx1 = new Transaction(); tx1.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx1.GetHash(), entry.Fee(new Money(10000L)).Priority(10.0).FromTx(tx1)); /* highest fee */ Transaction tx2 = new Transaction(); tx2.AddOutput(new TxOut(new Money(2 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx2.GetHash(), entry.Fee(new Money(20000L)).Priority(9.0).FromTx(tx2)); /* lowest fee */ Transaction tx3 = new Transaction(); tx3.AddOutput(new TxOut(new Money(5 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx3.GetHash(), entry.Fee(new Money(0L)).Priority(100.0).FromTx(tx3)); /* 2nd highest fee */ Transaction tx4 = new Transaction(); tx4.AddOutput(new TxOut(new Money(6 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx4.GetHash(), entry.Fee(new Money(15000L)).Priority(1.0).FromTx(tx4)); /* equal fee rate to tx1, but newer */ Transaction tx5 = new Transaction(); tx5.AddOutput(new TxOut(new Money(11 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx5.GetHash(), entry.Fee(new Money(10000L)).Priority(10.0).Time(1).FromTx(tx5)); // assert size Assert.Equal(5, pool.Size); List <string> sortedOrder = new List <string>(5); sortedOrder.Insert(0, tx3.GetHash().ToString()); // 0 sortedOrder.Insert(1, tx5.GetHash().ToString()); // 10000 sortedOrder.Insert(2, tx1.GetHash().ToString()); // 10000 sortedOrder.Insert(3, tx4.GetHash().ToString()); // 15000 sortedOrder.Insert(4, tx2.GetHash().ToString()); // 20000 this.CheckSort(pool, pool.MapTx.DescendantScore.ToList(), sortedOrder); /* low fee but with high fee child */ /* tx6 -> tx7 -> tx8, tx9 -> tx10 */ Transaction tx6 = new Transaction(); tx6.AddOutput(new TxOut(new Money(20 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx6.GetHash(), entry.Fee(new Money(0L)).FromTx(tx6)); // assert size Assert.Equal(6, pool.Size); // Check that at this point, tx6 is sorted low sortedOrder.Insert(0, tx6.GetHash().ToString()); this.CheckSort(pool, pool.MapTx.DescendantScore.ToList(), sortedOrder); TxMempool.SetEntries setAncestors = new TxMempool.SetEntries(); setAncestors.Add(pool.MapTx.TryGet(tx6.GetHash())); Transaction tx7 = new Transaction(); tx7.AddInput(new TxIn(new OutPoint(tx6.GetHash(), 0), new Script(OpcodeType.OP_11))); tx7.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); tx7.AddOutput(new TxOut(new Money(1 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); TxMempool.SetEntries setAncestorsCalculated = new TxMempool.SetEntries(); string dummy; Assert.True(pool.CalculateMemPoolAncestors(entry.Fee(2000000L).FromTx(tx7), setAncestorsCalculated, 100, 1000000, 1000, 1000000, out dummy)); Assert.True(setAncestorsCalculated.Equals(setAncestors)); pool.AddUnchecked(tx7.GetHash(), entry.FromTx(tx7), setAncestors); Assert.Equal(7, pool.Size); // Now tx6 should be sorted higher (high fee child): tx7, tx6, tx2, ... sortedOrder.RemoveAt(0); sortedOrder.Add(tx6.GetHash().ToString()); sortedOrder.Add(tx7.GetHash().ToString()); this.CheckSort(pool, pool.MapTx.DescendantScore.ToList(), sortedOrder); /* low fee child of tx7 */ Transaction tx8 = new Transaction(); tx8.AddInput(new TxIn(new OutPoint(tx7.GetHash(), 0), new Script(OpcodeType.OP_11))); tx8.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); setAncestors.Add(pool.MapTx.TryGet(tx7.GetHash())); pool.AddUnchecked(tx8.GetHash(), entry.Fee(0L).Time(2).FromTx(tx8), setAncestors); // Now tx8 should be sorted low, but tx6/tx both high sortedOrder.Insert(0, tx8.GetHash().ToString()); this.CheckSort(pool, pool.MapTx.DescendantScore.ToList(), sortedOrder); /* low fee child of tx7 */ Transaction tx9 = new Transaction(); tx9.AddInput(new TxIn(new OutPoint(tx7.GetHash(), 1), new Script(OpcodeType.OP_11))); tx9.AddOutput(new TxOut(new Money(1 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); pool.AddUnchecked(tx9.GetHash(), entry.Fee(0L).Time(3).FromTx(tx9), setAncestors); // tx9 should be sorted low Assert.Equal(9, pool.Size); sortedOrder.Insert(0, tx9.GetHash().ToString()); this.CheckSort(pool, pool.MapTx.DescendantScore.ToList(), sortedOrder); List <string> snapshotOrder = sortedOrder.ToList(); setAncestors.Add(pool.MapTx.TryGet(tx8.GetHash())); setAncestors.Add(pool.MapTx.TryGet(tx9.GetHash())); /* tx10 depends on tx8 and tx9 and has a high fee*/ Transaction tx10 = new Transaction(); tx10.AddInput(new TxIn(new OutPoint(tx8.GetHash(), 0), new Script(OpcodeType.OP_11))); tx10.AddInput(new TxIn(new OutPoint(tx9.GetHash(), 0), new Script(OpcodeType.OP_11))); tx10.AddOutput(new TxOut(new Money(10 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); setAncestorsCalculated.Clear(); Assert.True(pool.CalculateMemPoolAncestors(entry.Fee(200000L).Time(4).FromTx(tx10), setAncestorsCalculated, 100, 1000000, 1000, 1000000, out dummy)); Assert.True(setAncestorsCalculated.Equals(setAncestors)); pool.AddUnchecked(tx10.GetHash(), entry.FromTx(tx10), setAncestors); /** * tx8 and tx9 should both now be sorted higher * Final order after tx10 is added: * * tx3 = 0 (1) * tx5 = 10000 (1) * tx1 = 10000 (1) * tx4 = 15000 (1) * tx2 = 20000 (1) * tx9 = 200k (2 txs) * tx8 = 200k (2 txs) * tx10 = 200k (1 tx) * tx6 = 2.2M (5 txs) * tx7 = 2.2M (4 txs) */ sortedOrder.RemoveRange(0, 2); // take out tx9, tx8 from the beginning sortedOrder.Insert(5, tx9.GetHash().ToString()); sortedOrder.Insert(6, tx8.GetHash().ToString()); sortedOrder.Insert(7, tx10.GetHash().ToString()); // tx10 is just before tx6 this.CheckSort(pool, pool.MapTx.DescendantScore.ToList(), sortedOrder); // there should be 10 transactions in the mempool Assert.Equal(10, pool.Size); // Now try removing tx10 and verify the sort order returns to normal pool.RemoveRecursive(pool.MapTx.TryGet(tx10.GetHash()).Transaction); this.CheckSort(pool, pool.MapTx.DescendantScore.ToList(), snapshotOrder); pool.RemoveRecursive(pool.MapTx.TryGet(tx9.GetHash()).Transaction); pool.RemoveRecursive(pool.MapTx.TryGet(tx8.GetHash()).Transaction); /* Now check the sort on the mining score index. * Final order should be: * * tx7 (2M) * tx2 (20k) * tx4 (15000) * tx1/tx5 (10000) * tx3/6 (0) * (Ties resolved by hash) */ sortedOrder.Clear(); sortedOrder.Add(tx7.GetHash().ToString()); sortedOrder.Add(tx2.GetHash().ToString()); sortedOrder.Add(tx4.GetHash().ToString()); if (tx1.GetHash() < tx5.GetHash()) { sortedOrder.Add(tx5.GetHash().ToString()); sortedOrder.Add(tx1.GetHash().ToString()); } else { sortedOrder.Add(tx1.GetHash().ToString()); sortedOrder.Add(tx5.GetHash().ToString()); } if (tx3.GetHash() < tx6.GetHash()) { sortedOrder.Add(tx6.GetHash().ToString()); sortedOrder.Add(tx3.GetHash().ToString()); } else { sortedOrder.Add(tx3.GetHash().ToString()); sortedOrder.Add(tx6.GetHash().ToString()); } this.CheckSort(pool, pool.MapTx.MiningScore.ToList(), sortedOrder); }