Skip to content

Commit 87f3fb3

Browse files
authored
Merge pull request #2156 from ergoplatform/i2155
Clear mempool transactions with inputs being spent immediately when a block arrives
2 parents 653584d + f5f5144 commit 87f3fb3

File tree

6 files changed

+69
-17
lines changed

6 files changed

+69
-17
lines changed

src/main/resources/application.conf

+2-2
Original file line numberDiff line numberDiff line change
@@ -79,8 +79,8 @@ ergo {
7979
mempoolCapacity = 1000
8080

8181
# Interval for mempool transaction re-check. We check transaction when it is entering the mempool, and then
82-
# re-check it every interval value
83-
mempoolCleanupDuration = 20m
82+
# re-check it every interval value (but only on new block arrival)
83+
mempoolCleanupDuration = 30s
8484

8585
# Mempool transaction sorting scheme ("random", "bySize", or "byExecutionCost")
8686
mempoolSorting = "random"

src/main/scala/org/ergoplatform/local/CleanupWorker.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -66,8 +66,8 @@ class CleanupWorker(nodeViewHolderRef: ActorRef,
6666

6767
val now = System.currentTimeMillis()
6868

69-
val allPoolTxs = mempool.getAllPrioritized
7069
// Check transactions sorted by priority. Parent transaction comes before its children.
70+
val allPoolTxs = mempool.getAllPrioritized
7171
val txsToValidate = allPoolTxs.filter { utx =>
7272
(now - utx.lastCheckedTime) > TimeLimit
7373
}.toList

src/main/scala/org/ergoplatform/nodeView/ErgoNodeViewHolder.scala

+1-1
Original file line numberDiff line numberDiff line change
@@ -402,7 +402,7 @@ abstract class ErgoNodeViewHolder[State <: ErgoState[State]](settings: ErgoSetti
402402
.flatMap(extractTransactions)
403403
.filter(tx => !appliedTxs.exists(_.id == tx.id))
404404
.map(tx => UnconfirmedTransaction(tx, None))
405-
memPool.remove(appliedTxs).put(rolledBackTxs)
405+
memPool.removeWithDoubleSpends(appliedTxs).put(rolledBackTxs)
406406
}
407407

408408
/**

src/main/scala/org/ergoplatform/nodeView/mempool/ErgoMemPool.scala

+28-6
Original file line numberDiff line numberDiff line change
@@ -106,15 +106,37 @@ class ErgoMemPool private[mempool](private[mempool] val pool: OrderedTxPool,
106106
}
107107

108108
/**
109-
* Remove transaction from the pool
109+
* Remove transaction from the pool along with its double-spends
110110
*/
111-
def remove(tx: ErgoTransaction): ErgoMemPool = {
112-
log.debug(s"Removing transaction ${tx.id} from the mempool")
113-
new ErgoMemPool(pool.remove(tx), updateStatsOnRemoval(tx), sortingOption)
111+
def removeTxAndDoubleSpends(tx: ErgoTransaction): ErgoMemPool = {
112+
def removeTx(mp: ErgoMemPool, tx: ErgoTransaction): ErgoMemPool = {
113+
log.debug(s"Removing transaction ${tx.id} from the mempool")
114+
new ErgoMemPool(mp.pool.remove(tx), mp.updateStatsOnRemoval(tx), sortingOption)
115+
}
116+
117+
val poolWithoutTx = removeTx(this, tx)
118+
val doubleSpentTransactionIds = tx.inputs.flatMap(i =>
119+
poolWithoutTx.pool.inputs.get(i.boxId)
120+
).toSet
121+
val doubleSpentTransactions = doubleSpentTransactionIds.flatMap { txId =>
122+
poolWithoutTx.pool.orderedTransactions.get(txId)
123+
}
124+
doubleSpentTransactions.foldLeft(poolWithoutTx) { case (pool, tx) =>
125+
removeTx(pool, tx.transaction)
126+
}
114127
}
115128

116-
def remove(txs: TraversableOnce[ErgoTransaction]): ErgoMemPool = {
117-
txs.foldLeft(this) { case (acc, tx) => acc.remove(tx) }
129+
/**
130+
* Remove provided transactions and their doublespends from the pool
131+
*/
132+
def removeWithDoubleSpends(txs: TraversableOnce[ErgoTransaction]): ErgoMemPool = {
133+
txs.foldLeft(this) { case (memPool, tx) =>
134+
if (memPool.contains(tx.id)) { // tx could be removed earlier in this loop as double-spend of another tx
135+
memPool.removeTxAndDoubleSpends(tx)
136+
} else {
137+
memPool
138+
}
139+
}
118140
}
119141

120142
/**

src/test/scala/org/ergoplatform/nodeView/mempool/ErgoMemPoolSpec.scala

+35-5
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,11 @@
11
package org.ergoplatform.nodeView.mempool
22

3-
import org.ergoplatform.modifiers.mempool.{ErgoTransaction, UnconfirmedTransaction}
3+
import org.ergoplatform.{ErgoBoxCandidate, Input}
44
import org.ergoplatform.nodeView.mempool.ErgoMemPoolUtils.{ProcessingOutcome, SortingOption}
5+
import org.ergoplatform.modifiers.mempool.{ErgoTransaction, UnconfirmedTransaction}
56
import org.ergoplatform.nodeView.state.wrapped.WrappedUtxoState
67
import org.ergoplatform.settings.ErgoSettings
7-
import org.ergoplatform.utils.ErgoTestHelpers
8-
import org.ergoplatform.{ErgoBoxCandidate, Input}
8+
import org.ergoplatform.utils.{ErgoTestHelpers, RandomWrapper}
99
import org.scalatest.flatspec.AnyFlatSpec
1010
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
1111
import sigma.ast.ErgoTree.ZeroHeader
@@ -299,11 +299,41 @@ class ErgoMemPoolSpec extends AnyFlatSpec
299299
}
300300
pool.size shouldBe (family_depth + 1) * txs.size
301301
allTxs.foreach { tx =>
302-
pool = pool.remove(tx.transaction)
302+
pool = pool.removeTxAndDoubleSpends(tx.transaction)
303303
}
304304
pool.size shouldBe 0
305305
}
306306

307+
it should "correctly remove doublespents of a transaction from pool" in {
308+
val (us, bh) = createUtxoState(settings)
309+
val genesis = validFullBlock(None, us, bh)
310+
val wus = WrappedUtxoState(us, bh, settings, parameters).applyModifier(genesis)(_ => ()).get
311+
val boxes = wus.takeBoxes(4)
312+
313+
val limit = 10000
314+
315+
val tx1 = validTransactionsFromBoxes(limit, boxes.take(1), new RandomWrapper)
316+
._1.map(tx => UnconfirmedTransaction(tx, None)).head
317+
318+
val tx2 = validTransactionsFromBoxes(limit, boxes.takeRight(2), new RandomWrapper)
319+
._1.map(tx => UnconfirmedTransaction(tx, None)).head
320+
321+
val tx3 = validTransactionsFromBoxes(limit, boxes.take(1), new RandomWrapper)
322+
._1.map(tx => UnconfirmedTransaction(tx, None)).head
323+
324+
tx1.transaction.inputs.head.boxId shouldBe tx3.transaction.inputs.head.boxId
325+
326+
var pool = ErgoMemPool.empty(settings)
327+
Seq(tx2, tx3).foreach { tx =>
328+
pool = pool.put(tx)
329+
}
330+
331+
pool = pool.removeTxAndDoubleSpends(tx1.transaction)
332+
pool.contains(tx1.transaction) shouldBe false
333+
pool.contains(tx2.transaction) shouldBe true
334+
pool.contains(tx3.transaction) shouldBe false
335+
}
336+
307337
it should "return results take / getAll / getAllPrioritized sorted by priority" in {
308338
val feeProp = settings.chainSettings.monetary.feeProposition
309339

@@ -378,7 +408,7 @@ class ErgoMemPoolSpec extends AnyFlatSpec
378408
pool.stats.snapTakenTxns shouldBe MemPoolStatistics(System.currentTimeMillis(),0,System.currentTimeMillis()).snapTakenTxns
379409

380410
allTxs.foreach { tx =>
381-
pool = pool.remove(tx.transaction)
411+
pool = pool.removeTxAndDoubleSpends(tx.transaction)
382412
}
383413
pool.size shouldBe 0
384414
pool.stats.takenTxns shouldBe (family_depth + 1) * txs.size

src/test/scala/scorex/testkit/properties/mempool/MempoolTransactionsTest.scala

+2-2
Original file line numberDiff line numberDiff line change
@@ -84,15 +84,15 @@ trait MempoolTransactionsTest
8484
property("Size of mempool should decrease when removing a present transaction") {
8585
forAll(memPoolGenerator, unconfirmedTxSeqGenerator) { (mp: ErgoMemPool, unconfirmedTxs: Seq[UnconfirmedTransaction]) =>
8686
val m: ErgoMemPool = mp.put(unconfirmedTxs)
87-
val m2: ErgoMemPool = m.remove(unconfirmedTxs.headOption.get.transaction)
87+
val m2: ErgoMemPool = m.removeTxAndDoubleSpends(unconfirmedTxs.headOption.get.transaction)
8888
m2.size shouldBe unconfirmedTxs.size - 1
8989
}
9090
}
9191

9292
property("Size of mempool should not decrease when removing a non-present transaction") {
9393
forAll(memPoolGenerator, unconfirmedTxSeqGenerator, unconfirmedTxGenerator) { (mp: ErgoMemPool, unconfirmedTxs: Seq[UnconfirmedTransaction], unconfirmedTx: UnconfirmedTransaction) =>
9494
val m: ErgoMemPool = mp.put(unconfirmedTxs)
95-
val m2: ErgoMemPool = m.remove(unconfirmedTx.transaction)
95+
val m2: ErgoMemPool = m.removeTxAndDoubleSpends(unconfirmedTx.transaction)
9696
m2.size shouldBe unconfirmedTxs.size
9797
}
9898
}

0 commit comments

Comments
 (0)