Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Clear mempool transactions with inputs being spent immediately when a block arrives #2156

Merged
merged 4 commits into from
Jun 1, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/main/resources/application.conf
Original file line number Diff line number Diff line change
Expand Up @@ -79,8 +79,8 @@ ergo {
mempoolCapacity = 1000

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

@aslesarenko aslesarenko May 22, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please clarify if block arrived, but the interval has not elapsed, does the re-check happen?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

no


# Mempool transaction sorting scheme ("random", "bySize", or "byExecutionCost")
mempoolSorting = "random"
Expand Down
2 changes: 1 addition & 1 deletion src/main/scala/org/ergoplatform/local/CleanupWorker.scala
Original file line number Diff line number Diff line change
Expand Up @@ -66,8 +66,8 @@ class CleanupWorker(nodeViewHolderRef: ActorRef,

val now = System.currentTimeMillis()

val allPoolTxs = mempool.getAllPrioritized
// Check transactions sorted by priority. Parent transaction comes before its children.
val allPoolTxs = mempool.getAllPrioritized
val txsToValidate = allPoolTxs.filter { utx =>
(now - utx.lastCheckedTime) > TimeLimit
}.toList
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -402,7 +402,7 @@ abstract class ErgoNodeViewHolder[State <: ErgoState[State]](settings: ErgoSetti
.flatMap(extractTransactions)
.filter(tx => !appliedTxs.exists(_.id == tx.id))
.map(tx => UnconfirmedTransaction(tx, None))
memPool.remove(appliedTxs).put(rolledBackTxs)
memPool.removeWithDoubleSpends(appliedTxs).put(rolledBackTxs)
}

/**
Expand Down
34 changes: 28 additions & 6 deletions src/main/scala/org/ergoplatform/nodeView/mempool/ErgoMemPool.scala
Original file line number Diff line number Diff line change
Expand Up @@ -106,15 +106,37 @@ class ErgoMemPool private[mempool](private[mempool] val pool: OrderedTxPool,
}

/**
* Remove transaction from the pool
* Remove transaction from the pool along with its double-spends
*/
def remove(tx: ErgoTransaction): ErgoMemPool = {
log.debug(s"Removing transaction ${tx.id} from the mempool")
new ErgoMemPool(pool.remove(tx), updateStatsOnRemoval(tx), sortingOption)
def removeTxAndDoubleSpends(tx: ErgoTransaction): ErgoMemPool = {
def removeTx(mp: ErgoMemPool, tx: ErgoTransaction): ErgoMemPool = {
log.debug(s"Removing transaction ${tx.id} from the mempool")
new ErgoMemPool(mp.pool.remove(tx), mp.updateStatsOnRemoval(tx), sortingOption)
}

val poolWithoutTx = removeTx(this, tx)
val doubleSpentTransactionIds = tx.inputs.flatMap(i =>
poolWithoutTx.pool.inputs.get(i.boxId)
).toSet
val doubleSpentTransactions = doubleSpentTransactionIds.flatMap { txId =>
poolWithoutTx.pool.orderedTransactions.get(txId)
}
doubleSpentTransactions.foldLeft(poolWithoutTx) { case (pool, tx) =>
removeTx(pool, tx.transaction)
}
}

def remove(txs: TraversableOnce[ErgoTransaction]): ErgoMemPool = {
txs.foldLeft(this) { case (acc, tx) => acc.remove(tx) }
/**
* Remove provided transactions and their doublespends from the pool
*/
def removeWithDoubleSpends(txs: TraversableOnce[ErgoTransaction]): ErgoMemPool = {
txs.foldLeft(this) { case (memPool, tx) =>
if (memPool.contains(tx.id)) { // tx could be removed earlier in this loop as double-spend of another tx
memPool.removeTxAndDoubleSpends(tx)
} else {
memPool
}
}
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
package org.ergoplatform.nodeView.mempool

import org.ergoplatform.modifiers.mempool.{ErgoTransaction, UnconfirmedTransaction}
import org.ergoplatform.{ErgoBoxCandidate, Input}
import org.ergoplatform.nodeView.mempool.ErgoMemPoolUtils.{ProcessingOutcome, SortingOption}
import org.ergoplatform.modifiers.mempool.{ErgoTransaction, UnconfirmedTransaction}
import org.ergoplatform.nodeView.state.wrapped.WrappedUtxoState
import org.ergoplatform.settings.ErgoSettings
import org.ergoplatform.utils.ErgoTestHelpers
import org.ergoplatform.{ErgoBoxCandidate, Input}
import org.ergoplatform.utils.{ErgoTestHelpers, RandomWrapper}
import org.scalatest.flatspec.AnyFlatSpec
import org.scalatestplus.scalacheck.ScalaCheckPropertyChecks
import sigma.ast.ErgoTree.ZeroHeader
Expand Down Expand Up @@ -299,11 +299,41 @@ class ErgoMemPoolSpec extends AnyFlatSpec
}
pool.size shouldBe (family_depth + 1) * txs.size
allTxs.foreach { tx =>
pool = pool.remove(tx.transaction)
pool = pool.removeTxAndDoubleSpends(tx.transaction)
}
pool.size shouldBe 0
}

it should "correctly remove doublespents of a transaction from pool" in {
val (us, bh) = createUtxoState(settings)
val genesis = validFullBlock(None, us, bh)
val wus = WrappedUtxoState(us, bh, settings, parameters).applyModifier(genesis)(_ => ()).get
val boxes = wus.takeBoxes(4)

val limit = 10000

val tx1 = validTransactionsFromBoxes(limit, boxes.take(1), new RandomWrapper)
._1.map(tx => UnconfirmedTransaction(tx, None)).head

val tx2 = validTransactionsFromBoxes(limit, boxes.takeRight(2), new RandomWrapper)
._1.map(tx => UnconfirmedTransaction(tx, None)).head

val tx3 = validTransactionsFromBoxes(limit, boxes.take(1), new RandomWrapper)
._1.map(tx => UnconfirmedTransaction(tx, None)).head

tx1.transaction.inputs.head.boxId shouldBe tx3.transaction.inputs.head.boxId

var pool = ErgoMemPool.empty(settings)
Seq(tx2, tx3).foreach { tx =>
pool = pool.put(tx)
}

pool = pool.removeTxAndDoubleSpends(tx1.transaction)
pool.contains(tx1.transaction) shouldBe false
pool.contains(tx2.transaction) shouldBe true
pool.contains(tx3.transaction) shouldBe false
}

it should "return results take / getAll / getAllPrioritized sorted by priority" in {
val feeProp = settings.chainSettings.monetary.feeProposition

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

allTxs.foreach { tx =>
pool = pool.remove(tx.transaction)
pool = pool.removeTxAndDoubleSpends(tx.transaction)
}
pool.size shouldBe 0
pool.stats.takenTxns shouldBe (family_depth + 1) * txs.size
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -84,15 +84,15 @@ trait MempoolTransactionsTest
property("Size of mempool should decrease when removing a present transaction") {
forAll(memPoolGenerator, unconfirmedTxSeqGenerator) { (mp: ErgoMemPool, unconfirmedTxs: Seq[UnconfirmedTransaction]) =>
val m: ErgoMemPool = mp.put(unconfirmedTxs)
val m2: ErgoMemPool = m.remove(unconfirmedTxs.headOption.get.transaction)
val m2: ErgoMemPool = m.removeTxAndDoubleSpends(unconfirmedTxs.headOption.get.transaction)
m2.size shouldBe unconfirmedTxs.size - 1
}
}

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