From fd9def2878df0c1ccbf1c92aa5eadebe973529d5 Mon Sep 17 00:00:00 2001 From: siddhant Date: Sun, 22 Mar 2026 19:41:11 +0530 Subject: [PATCH 1/4] minimization in mempool --- minichain/mempool.py | 76 +++++++++++++------------------------------- 1 file changed, 22 insertions(+), 54 deletions(-) diff --git a/minichain/mempool.py b/minichain/mempool.py index cc3b4f7..92f7b69 100644 --- a/minichain/mempool.py +++ b/minichain/mempool.py @@ -3,82 +3,50 @@ logger = logging.getLogger(__name__) - class Mempool: - TRANSACTIONS_PER_BLOCK = 100 - - def __init__(self, max_size=1000, transactions_per_block=TRANSACTIONS_PER_BLOCK): - self._pending_txs = [] - self._seen_tx_ids = set() + def __init__(self, max_size=1000, transactions_per_block=100): + self._pool = {} + self._size = 0 self._lock = threading.Lock() self.max_size = max_size self.transactions_per_block = transactions_per_block - def _get_tx_id(self, tx): - return tx.tx_id - def add_transaction(self, tx): - """ - Adds a transaction to the pool if: - - Signature is valid - - Transaction is not a duplicate - - Mempool is not full - """ - tx_id = self._get_tx_id(tx) - if not tx.verify(): logger.warning("Mempool: Invalid signature rejected") return False with self._lock: - if tx_id in self._seen_tx_ids: - logger.warning("Mempool: Duplicate transaction rejected %s", tx_id) - return False - - replacement_index = None - for index, pending_tx in enumerate(self._pending_txs): - if pending_tx.sender == tx.sender and pending_tx.nonce == tx.nonce: - replacement_index = index - break + pool = self._pool.setdefault(tx.sender, {}) + existing = pool.get(tx.nonce) - if replacement_index is None and len(self._pending_txs) >= self.max_size: + if existing and existing.tx_id == tx.tx_id: + logger.warning("Mempool: Duplicate transaction rejected %s", tx.tx_id) + return False + if not existing and self._size >= self.max_size: logger.warning("Mempool: Full, rejecting transaction") return False - if replacement_index is not None: - old_tx = self._pending_txs[replacement_index] - self._seen_tx_ids.discard(self._get_tx_id(old_tx)) - self._pending_txs[replacement_index] = tx - else: - self._pending_txs.append(tx) - - self._seen_tx_ids.add(tx_id) + self._size += 0 if existing else 1 + pool[tx.nonce] = tx return True def get_transactions_for_block(self): - """ - Returns transactions in deterministic sorted queue order. - This is read-only; transactions are removed only after block acceptance. - """ with self._lock: - selected = list(self._pending_txs) - selected.sort(key=lambda tx: (tx.timestamp, tx.sender, tx.nonce)) - return selected[: self.transactions_per_block] + txs = [t for pool in self._pool.values() for t in pool.values()] + txs.sort(key=lambda t: (t.timestamp, t.sender, t.nonce)) + return txs[: self.transactions_per_block] def remove_transactions(self, transactions): with self._lock: - remove_ids = {self._get_tx_id(tx) for tx in transactions} - remove_sender_nonces = {(tx.sender, tx.nonce) for tx in transactions} - if not remove_ids: - return - self._pending_txs = [ - tx - for tx in self._pending_txs - if self._get_tx_id(tx) not in remove_ids - and (tx.sender, tx.nonce) not in remove_sender_nonces - ] - self._seen_tx_ids = {self._get_tx_id(tx) for tx in self._pending_txs} + for tx in transactions: + pool = self._pool.get(tx.sender) + if pool and tx.nonce in pool: + del pool[tx.nonce] + self._size -= 1 + if not pool: + del self._pool[tx.sender] def __len__(self): with self._lock: - return len(self._pending_txs) + return self._size From 5a27e82b0cf667a5cd068feddc89b05193594fed Mon Sep 17 00:00:00 2001 From: siddhant Date: Mon, 23 Mar 2026 22:11:08 +0530 Subject: [PATCH 2/4] mempool hardening --- minichain/mempool.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/minichain/mempool.py b/minichain/mempool.py index 92f7b69..cf1c98c 100644 --- a/minichain/mempool.py +++ b/minichain/mempool.py @@ -17,8 +17,7 @@ def add_transaction(self, tx): return False with self._lock: - pool = self._pool.setdefault(tx.sender, {}) - existing = pool.get(tx.nonce) + existing = self._pool.get(tx.sender, {}).get(tx.nonce) if existing and existing.tx_id == tx.tx_id: logger.warning("Mempool: Duplicate transaction rejected %s", tx.tx_id) @@ -28,14 +27,16 @@ def add_transaction(self, tx): return False self._size += 0 if existing else 1 + pool = self._pool.setdefault(tx.sender, {}) pool[tx.nonce] = tx return True def get_transactions_for_block(self): with self._lock: txs = [t for pool in self._pool.values() for t in pool.values()] - txs.sort(key=lambda t: (t.timestamp, t.sender, t.nonce)) - return txs[: self.transactions_per_block] + + txs.sort(key=lambda t: (t.timestamp, t.sender, t.nonce)) + return txs[: self.transactions_per_block] def remove_transactions(self, transactions): with self._lock: From 71d44e65c5e0b1f21b92994a15bad4c8d8aa03ca Mon Sep 17 00:00:00 2001 From: siddhant Date: Mon, 23 Mar 2026 23:49:58 +0530 Subject: [PATCH 3/4] mempool hardening --- minichain/mempool.py | 43 +++++++++++++++++++++++++------- tests/test_protocol_hardening.py | 2 +- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/minichain/mempool.py b/minichain/mempool.py index cf1c98c..4f9fc85 100644 --- a/minichain/mempool.py +++ b/minichain/mempool.py @@ -19,24 +19,49 @@ def add_transaction(self, tx): with self._lock: existing = self._pool.get(tx.sender, {}).get(tx.nonce) - if existing and existing.tx_id == tx.tx_id: - logger.warning("Mempool: Duplicate transaction rejected %s", tx.tx_id) - return False + if existing: + if existing.tx_id == tx.tx_id: + logger.warning("Mempool: Duplicate transaction rejected %s", tx.tx_id) + return False + # Fix: Guard against older replacements (e.g. rejected block restore) + # Only allow overwrite if it's a genuinely newer replacement + if tx.timestamp <= existing.timestamp: + logger.warning("Mempool: Ignoring older replacement %s", tx.tx_id) + return False + if not existing and self._size >= self.max_size: logger.warning("Mempool: Full, rejecting transaction") return False - self._size += 0 if existing else 1 - pool = self._pool.setdefault(tx.sender, {}) - pool[tx.nonce] = tx + self._size += 1 + self._pool.setdefault(tx.sender, {})[tx.nonce] = tx return True def get_transactions_for_block(self): with self._lock: - txs = [t for pool in self._pool.values() for t in pool.values()] + snapshot = {s: list(pool.values()) for s, pool in self._pool.items()} + + for txs in snapshot.values(): + txs.sort(key=lambda t: t.nonce) + + selected = [] + while len(selected) < self.transactions_per_block: + best_tx = None + best_sender = None - txs.sort(key=lambda t: (t.timestamp, t.sender, t.nonce)) - return txs[: self.transactions_per_block] + for sender, txs in snapshot.items(): + if txs: + if best_tx is None or txs[0].timestamp < best_tx.timestamp: + best_tx = txs[0] + best_sender = sender + + if not best_tx: + break + + selected.append(best_tx) + snapshot[best_sender].pop(0) + + return selected def remove_transactions(self, transactions): with self._lock: diff --git a/tests/test_protocol_hardening.py b/tests/test_protocol_hardening.py index 60aee4a..6b169e7 100644 --- a/tests/test_protocol_hardening.py +++ b/tests/test_protocol_hardening.py @@ -45,7 +45,7 @@ def _signed_tx(self, nonce, amount=1, timestamp=None) -> Transaction: def test_transactions_for_block_are_sorted_and_capped(self): mempool = Mempool() for nonce in range(mempool.transactions_per_block + 5): - self.assertTrue(mempool.add_transaction(self._signed_tx(nonce, timestamp=5000 - nonce))) + self.assertTrue(mempool.add_transaction(self._signed_tx(nonce, timestamp=5000 + nonce))) selected = mempool.get_transactions_for_block() From 98c352684b8be1c2914dcac5cba5b321607771a4 Mon Sep 17 00:00:00 2001 From: siddhant Date: Tue, 24 Mar 2026 00:18:05 +0530 Subject: [PATCH 4/4] fix logic --- minichain/mempool.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/minichain/mempool.py b/minichain/mempool.py index 4f9fc85..4b71e08 100644 --- a/minichain/mempool.py +++ b/minichain/mempool.py @@ -29,11 +29,11 @@ def add_transaction(self, tx): logger.warning("Mempool: Ignoring older replacement %s", tx.tx_id) return False - if not existing and self._size >= self.max_size: - logger.warning("Mempool: Full, rejecting transaction") - return False - - self._size += 1 + else: + if self._size >= self.max_size: + logger.warning("Mempool: Full, rejecting transaction") + return False + self._size += 1 self._pool.setdefault(tx.sender, {})[tx.nonce] = tx return True @@ -51,7 +51,7 @@ def get_transactions_for_block(self): for sender, txs in snapshot.items(): if txs: - if best_tx is None or txs[0].timestamp < best_tx.timestamp: + if best_tx is None or (txs[0].timestamp, sender, txs[0].nonce) < (best_tx.timestamp, best_sender, best_tx.nonce): best_tx = txs[0] best_sender = sender