diff --git a/Stratis.Bitcoin.Tests/MemoryPoolTests.cs b/Stratis.Bitcoin.Tests/MemoryPoolTests.cs index 08f0b36d0f2..203b39005a1 100644 --- a/Stratis.Bitcoin.Tests/MemoryPoolTests.cs +++ b/Stratis.Bitcoin.Tests/MemoryPoolTests.cs @@ -35,12 +35,12 @@ public TxMemPoolEntry FromTx(Transaction tx, TxMemPool pool = null) } // Change the default value - TestMemPoolEntryHelper Fee(Money _fee) { nFee = _fee; return this; } - TestMemPoolEntryHelper Time(long _time) { nTime = _time; return this; } - TestMemPoolEntryHelper Priority(double _priority) { dPriority = _priority; return this; } - TestMemPoolEntryHelper Height(int _height) { nHeight = _height; return this; } - TestMemPoolEntryHelper SpendsCoinbase(bool _flag) { spendsCoinbase = _flag; return this; } - TestMemPoolEntryHelper SigOpsCost(long _sigopsCost) { sigOpCost = _sigopsCost; return this; } + public TestMemPoolEntryHelper Fee(Money _fee) { nFee = _fee; return this; } + public TestMemPoolEntryHelper Time(long _time) { nTime = _time; return this; } + public TestMemPoolEntryHelper Priority(double _priority) { dPriority = _priority; return this; } + public TestMemPoolEntryHelper Height(int _height) { nHeight = _height; return this; } + public TestMemPoolEntryHelper SpendsCoinbase(bool _flag) { spendsCoinbase = _flag; return this; } + public TestMemPoolEntryHelper SigOpsCost(long _sigopsCost) { sigOpCost = _sigopsCost; return this; } } @@ -129,5 +129,204 @@ public void MempoolRemoveTest() Assert.Equal(testPool.Size, poolSize - 6); Assert.Equal(testPool.Size, 0); } - } + + private void CheckSort(TxMemPool pool, List sortedSource, List sortedOrder) + { + Assert.Equal(pool.Size, sortedOrder.Count()); + int count = 0; + using (var it = sortedSource.GetEnumerator()) + for (; it.MoveNext(); ++count) + { + Assert.Equal(it.Current.TransactionHash.ToString(), sortedOrder[count]); + } + } + + [Fact] + public void MempoolIndexingTest() + { + + //Transaction xxx = new Transaction(); + //xxx.AddOutput(new TxOut(new Money(000 * Money.COIN), new Script(OpcodeType.OP_11, OpcodeType.OP_EQUAL))); + //pool.AddUnchecked(xxx.GetHash(), entry.Fee(new Money(000L)).Priority(000.0).FromTx(xxx)); + + var pool = new TxMemPool(new FeeRate(0)); + 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(pool.Size, 5); + + List sortedOrder = new List(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 + 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(pool.Size, 6); + + // Check that at this point, tx6 is sorted low + sortedOrder.Insert(0, tx6.GetHash().ToString()); + 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.Equal(pool.CalculateMemPoolAncestors(entry.Fee(2000000L).FromTx(tx7), setAncestorsCalculated, 100, 1000000, 1000, 1000000, out dummy), true); + Assert.True(setAncestorsCalculated.Equals(setAncestors)); + + pool.AddUnchecked(tx7.GetHash(), entry.FromTx(tx7), setAncestors); + Assert.Equal(pool.Size, 7); + + // 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()); + 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()); + 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(pool.Size, 9); + + sortedOrder.Insert(0, tx9.GetHash().ToString()); + CheckSort(pool, pool.MapTx.DescendantScore.ToList(), sortedOrder); + + List 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.Equal(pool.CalculateMemPoolAncestors(entry.Fee(200000L).Time(4).FromTx(tx10), setAncestorsCalculated, 100, 1000000, 1000, 1000000, out dummy), true); + 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 + CheckSort(pool, pool.MapTx.DescendantScore.ToList(), sortedOrder); + + // there should be 10 transactions in the mempool + Assert.Equal(pool.Size, 10); + + // Now try removing tx10 and verify the sort order returns to normal + pool.RemoveRecursive(pool.MapTx.TryGet(tx10.GetHash()).Transaction); + CheckSort(pool, pool.MapTx.DescendantScore.ToList(), snapshotOrder); + + //pool.removeRecursive(pool.MapTx.find(tx9.GetHash())->GetTx()); + //pool.removeRecursive(pool.MapTx.find(tx8.GetHash())->GetTx()); + ///* 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.push_back(tx7.GetHash().ToString()); + //sortedOrder.push_back(tx2.GetHash().ToString()); + //sortedOrder.push_back(tx4.GetHash().ToString()); + //if (tx1.GetHash() < tx5.GetHash()) + //{ + // sortedOrder.push_back(tx5.GetHash().ToString()); + // sortedOrder.push_back(tx1.GetHash().ToString()); + //} + //else + //{ + // sortedOrder.push_back(tx1.GetHash().ToString()); + // sortedOrder.push_back(tx5.GetHash().ToString()); + //} + //if (tx3.GetHash() < tx6.GetHash()) + //{ + // sortedOrder.push_back(tx6.GetHash().ToString()); + // sortedOrder.push_back(tx3.GetHash().ToString()); + //} + //else + //{ + // sortedOrder.push_back(tx3.GetHash().ToString()); + // sortedOrder.push_back(tx6.GetHash().ToString()); + //} + //CheckSort(pool, sortedOrder); + } + } } diff --git a/Stratis.Bitcoin/MemoryPool/TxMemPool.cs b/Stratis.Bitcoin/MemoryPool/TxMemPool.cs index c47dc41615b..d66a8c16f89 100644 --- a/Stratis.Bitcoin/MemoryPool/TxMemPool.cs +++ b/Stratis.Bitcoin/MemoryPool/TxMemPool.cs @@ -176,11 +176,11 @@ public int Compare(TxMemPoolEntry a, TxMemPoolEntry b) if (f1 == f2) { if (a.Time >= b.Time) - return 1; - return -1; + return -1; + return 1; } - if (f1 < f2) + if (f1 <= f2) return -1; return 1; } @@ -275,11 +275,27 @@ public class TxLinks public SetEntries Children; }; - public class SetEntries : SortedSet + public class SetEntries : SortedSet, IEquatable, IEqualityComparer + { public SetEntries() : base(new CompareIteratorByHash()) { } + + public bool Equals(SetEntries other) + { + return this.SequenceEqual(other, this); + } + + public bool Equals(TxMemPoolEntry x, TxMemPoolEntry y) + { + return x.TransactionHash == y.TransactionHash; + } + + public int GetHashCode(TxMemPoolEntry obj) + { + return obj?.TransactionHash?.GetHashCode() ?? 0; + } } public class TxlinksMap : SortedList @@ -301,7 +317,7 @@ public class NextTxPair public Transaction Transaction; } - IndexedTransactionSet mapTx = new IndexedTransactionSet(); + public IndexedTransactionSet MapTx = new IndexedTransactionSet(); TxlinksMap mapLinks = new TxlinksMap(); List mapNextTx = new List(); private Dictionary mapDeltas = new Dictionary(); @@ -329,7 +345,7 @@ public TxMemPool(FeeRate minReasonableRelayFee) private void InnerClear() { mapLinks.Clear(); - mapTx.Clear(); + MapTx.Clear(); mapNextTx.Clear(); totalTxSize = 0; cachedInnerUsage = 0; @@ -385,13 +401,13 @@ public bool AddUnchecked(uint256 hash, TxMemPoolEntry entry, bool validFeeEstima } - bool AddUnchecked(uint256 hash, TxMemPoolEntry entry, SetEntries setAncestors, bool validFeeEstimate) + public bool AddUnchecked(uint256 hash, TxMemPoolEntry entry, SetEntries setAncestors, bool validFeeEstimate = true) { // Add to memory pool without checking anything. // Used by main.cpp AcceptToMemoryPool(), which DOES do // all the appropriate checks. //LOCK(cs); - mapTx.Add(entry); + MapTx.Add(entry); mapLinks.Add(entry, new TxLinks {Parents = new SetEntries(), Children = new SetEntries()}); // Update transaction for any feeDelta created by PrioritiseTransaction @@ -429,7 +445,7 @@ bool AddUnchecked(uint256 hash, TxMemPoolEntry entry, SetEntries setAncestors, b // Update ancestors with information about this tx foreach (var phash in setParentTransactions) { - var pit = mapTx.TryGet(phash); + var pit = MapTx.TryGet(phash); if (pit != null) UpdateParent(entry, pit, true); } @@ -489,7 +505,7 @@ private SetEntries GetMemPoolParents(TxMemPoolEntry entry) if(entry == null) throw new ArgumentNullException(nameof(entry)); - Check.Assert(mapTx.ContainsKey(entry.TransactionHash)); + Check.Assert(MapTx.ContainsKey(entry.TransactionHash)); var it = mapLinks.TryGet(entry); Check.Assert(it != null); return it.Parents; @@ -500,7 +516,7 @@ private SetEntries GetMemPoolChildren(TxMemPoolEntry entry) if (entry == null) throw new ArgumentNullException(nameof(entry)); - Check.Assert(mapTx.ContainsKey(entry.TransactionHash)); + Check.Assert(MapTx.ContainsKey(entry.TransactionHash)); var it = mapLinks.TryGet(entry); Check.Assert(it != null); return it.Children; @@ -544,7 +560,7 @@ private void UpdateParent(TxMemPoolEntry entry, TxMemPoolEntry parent, bool add) * fSearchForParents = whether to search a tx's vin for in-mempool parents, or * look up parents from mapLinks. Must be true for entries not in the mempool */ - private bool CalculateMemPoolAncestors(TxMemPoolEntry entry, SetEntries setAncestors, long limitAncestorCount, + public bool CalculateMemPoolAncestors(TxMemPoolEntry entry, SetEntries setAncestors, long limitAncestorCount, long limitAncestorSize, long limitDescendantCount, long limitDescendantSize, out string errString, bool fSearchForParents = true) { @@ -559,7 +575,7 @@ private bool CalculateMemPoolAncestors(TxMemPoolEntry entry, SetEntries setAnces // iterate mapTx to find parents. foreach (var txInput in tx.Inputs) { - var piter = mapTx.TryGet(txInput.PrevOut.Hash); + var piter = MapTx.TryGet(txInput.PrevOut.Hash); if (piter != null) { parentHashes.Add(piter); @@ -641,13 +657,13 @@ public bool HasNoInputsOf(Transaction tx) public bool Exists(uint256 hash) { //LOCK(cs); - return mapTx.ContainsKey(hash); + return MapTx.ContainsKey(hash); } public long Size { //LOCK(cs); - get { return this.mapTx.Count; } + get { return this.MapTx.Count; } } public void RemoveRecursive(Transaction origTx) @@ -658,7 +674,7 @@ public void RemoveRecursive(Transaction origTx) //LOCK(cs); SetEntries txToRemove = new SetEntries(); - var origit = mapTx.TryGet(origHahs); + var origit = MapTx.TryGet(origHahs); if (origit != null) { txToRemove.Add(origit); @@ -674,7 +690,7 @@ public void RemoveRecursive(Transaction origTx) var it = mapNextTx.FirstOrDefault(w => w.OutPoint == new OutPoint(origHahs, i)); if (it == null) continue; - var nextit = mapTx.TryGet(it.Transaction.GetHash()); + var nextit = MapTx.TryGet(it.Transaction.GetHash()); Check.Assert(nextit != null); txToRemove.Add(nextit); } @@ -742,7 +758,7 @@ private void RemoveUnchecked(TxMemPoolEntry it) cachedInnerUsage -= 1; //it->DynamicMemoryUsage(); cachedInnerUsage -= mapLinks[it]?.Parents.Count ?? 0 + mapLinks[it]?.Children.Count ?? 0; mapLinks.Remove(it); - mapTx.Remove(it); + MapTx.Remove(it); nTransactionsUpdated++; //minerPolicyEstimator->removeTx(hash); } diff --git a/Stratis.Bitcoin/MemoryPool/TxMemPoolEntry.cs b/Stratis.Bitcoin/MemoryPool/TxMemPoolEntry.cs index 5ad21fccd80..0a019482669 100644 --- a/Stratis.Bitcoin/MemoryPool/TxMemPoolEntry.cs +++ b/Stratis.Bitcoin/MemoryPool/TxMemPoolEntry.cs @@ -201,5 +201,10 @@ public void UpdateLockPoints(LockPoints lp) } public volatile uint vTxHashesIdx; //!< Index in mempool's vTxHashes + + public override string ToString() + { + return $"{this.TransactionHash} - {base.ToString()}"; + } } }