From b9b9bc9f76be0d901d06adbc1f3fc0bfd8e456d2 Mon Sep 17 00:00:00 2001 From: Marcelo Salhab Brogliato Date: Wed, 1 Oct 2025 14:23:45 -0500 Subject: [PATCH] feat(verification): Limit the number of conflicts in mempool --- hathor/transaction/exceptions.py | 8 ++++ hathor/verification/transaction_verifier.py | 16 +++++++ tests/tx/test_verification_mempool.py | 46 +++++++++++++++++++++ 3 files changed, 70 insertions(+) diff --git a/hathor/transaction/exceptions.py b/hathor/transaction/exceptions.py index 3ccd40e41..e2e8b6136 100644 --- a/hathor/transaction/exceptions.py +++ b/hathor/transaction/exceptions.py @@ -114,6 +114,14 @@ class ConflictWithConfirmedTxError(TxValidationError): """Input has a conflict with a confirmed transaction.""" +class TooManyWithinConflicts(TxValidationError): + """Input has too many within conflicts already.""" + + +class TooManyBetweenConflicts(TxValidationError): + """Input has too many between conflicts already.""" + + class TooManyOutputs(TxValidationError): """More than 256 outputs""" diff --git a/hathor/verification/transaction_verifier.py b/hathor/verification/transaction_verifier.py index 78f8752a1..fb21865e8 100644 --- a/hathor/verification/transaction_verifier.py +++ b/hathor/verification/transaction_verifier.py @@ -40,9 +40,11 @@ ScriptError, TimestampError, TooFewInputs, + TooManyBetweenConflicts, TooManyInputs, TooManySigOps, TooManyTokens, + TooManyWithinConflicts, UnusedTokensError, WeightError, ) @@ -57,6 +59,8 @@ cpu = get_cpu_profiler() MAX_TOKENS_LENGTH: int = 16 +MAX_WITHIN_CONFLICTS: int = 8 +MAX_BETWEEN_CONFLICTS: int = 8 class TransactionVerifier: @@ -356,12 +360,14 @@ def verify_conflict(self, tx: Transaction, params: VerificationParams) -> None: if not params.reject_conflicts_with_confirmed_txs: return + between_counter = 0 for txin in tx.inputs: spent_tx = tx.get_spent_tx(txin) spent_tx_meta = spent_tx.get_metadata() if txin.index not in spent_tx_meta.spent_outputs: continue spent_by_list = spent_tx_meta.spent_outputs[txin.index] + within_counter = 0 for h in spent_by_list: if h == tx.hash: # Skip tx itself. @@ -370,6 +376,16 @@ def verify_conflict(self, tx: Transaction, params: VerificationParams) -> None: if conflict_tx.get_metadata().first_block is not None: # only mempool conflicts are allowed raise ConflictWithConfirmedTxError('transaction has a conflict with a confirmed transaction') + if within_counter == 0: + # Only increment once per input. + between_counter += 1 + within_counter += 1 + + if within_counter >= MAX_WITHIN_CONFLICTS: + raise TooManyWithinConflicts + + if between_counter > MAX_BETWEEN_CONFLICTS: + raise TooManyBetweenConflicts @dataclass(kw_only=True, slots=True) diff --git a/tests/tx/test_verification_mempool.py b/tests/tx/test_verification_mempool.py index d0fae0325..a59454bb2 100644 --- a/tests/tx/test_verification_mempool.py +++ b/tests/tx/test_verification_mempool.py @@ -22,12 +22,15 @@ ConflictWithConfirmedTxError, InvalidToken, TimestampError, + TooManyBetweenConflicts, TooManyTokens, + TooManyWithinConflicts, UnusedTokensError, ) from hathor.transaction.nc_execution_state import NCExecutionState from hathor.transaction.token_creation_tx import TokenCreationTransaction from hathor.verification.nano_header_verifier import MAX_SEQNUM_DIFF_MEMPOOL +from hathor.verification.transaction_verifier import MAX_BETWEEN_CONFLICTS, MAX_WITHIN_CONFLICTS from hathor.verification.vertex_verifier import MAX_PAST_TIMESTAMP_ALLOWED from tests import unittest from tests.dag_builder.builder import TestDAGBuilder @@ -176,6 +179,49 @@ def test_conflict_with_confirmed_tx(self) -> None: self.manager.vertex_handler.on_new_mempool_transaction(tx3) assert isinstance(e.exception.__cause__, ConflictWithConfirmedTxError) + def test_too_many_between_conflicts(self) -> None: + lines = [f'tx0.out[{i}] <<< txN tx{i + 1}' for i in range(0, MAX_BETWEEN_CONFLICTS + 1)] + orders = [f'tx{i + 1} < txN' for i in range(0, MAX_BETWEEN_CONFLICTS + 1)] + newline = '\n' + artifacts = self.dag_builder.build_from_str(f''' + blockchain genesis b[1..30] + b10 < dummy + + {newline.join(lines)} + {newline.join(orders)} + ''') + artifacts.propagate_with(self.manager, up_to_before='txN') + txN = artifacts.get_typed_vertex('txN', Transaction) + + # need to fix the timestamp to pass the old vertices mempool verification + txN.timestamp = int(self.manager.reactor.seconds()) + self.dag_builder._exporter._vertex_resolver(txN) + + with self.assertRaises(InvalidNewTransaction) as e: + self.manager.vertex_handler.on_new_mempool_transaction(txN) + assert isinstance(e.exception.__cause__, TooManyBetweenConflicts) + + def test_too_many_within_conflicts(self) -> None: + tx_list = [f'tx{i + 1}' for i in range(0, MAX_WITHIN_CONFLICTS + 1)] + artifacts = self.dag_builder.build_from_str(f''' + blockchain genesis b[1..30] + b10 < dummy + + tx0.out[0] <<< {' '.join(tx_list)} + + {' < '.join(tx_list)} + ''') + artifacts.propagate_with(self.manager, up_to_before=tx_list[-1]) + txN = artifacts.get_typed_vertex(tx_list[-1], Transaction) + + # need to fix the timestamp to pass the old vertices mempool verification + txN.timestamp = int(self.manager.reactor.seconds()) + self.dag_builder._exporter._vertex_resolver(txN) + + with self.assertRaises(InvalidNewTransaction) as e: + self.manager.vertex_handler.on_new_mempool_transaction(txN) + assert isinstance(e.exception.__cause__, TooManyWithinConflicts) + @inlineCallbacks def test_checkpoints(self) -> Generator: artifacts = self.dag_builder.build_from_str('''