From f06ab50238b77c8ae3e440b27a0ceba3ba044ccc Mon Sep 17 00:00:00 2001 From: waterWang Date: Fri, 7 Aug 2026 03:54:20 +0800 Subject: [PATCH 1/2] fix: wrap mempool_clear_expired() in BEGIN IMMEDIATE to prevent concurrent mempool corruption (fixes #8176) [fj4WqyCCw3C5ShR1RfB7MoBPTpkRrBFYP1uT35g3MvT] --- node/utxo_db.py | 44 +++++++++++++++++++++++++++++--------------- 1 file changed, 29 insertions(+), 15 deletions(-) diff --git a/node/utxo_db.py b/node/utxo_db.py index 6cd2d719e..dd80cdd75 100644 --- a/node/utxo_db.py +++ b/node/utxo_db.py @@ -1535,9 +1535,16 @@ def mempool_get_block_candidates(self, max_count: int = 100) -> List[dict]: conn.close() def mempool_clear_expired(self) -> int: - """Remove expired transactions from mempool. Returns count removed.""" + """Remove expired transactions from mempool. Returns count removed. + + Uses BEGIN IMMEDIATE to ensure the SELECT-then-DELETE sequence is + atomic. Without it, a concurrent mempool_add() or apply_transaction() + can interleave between the SELECT and the DELETEs, causing mempool + state corruption / double-spend (B2, issue #8176). + """ conn = self._conn() try: + conn.execute("BEGIN IMMEDIATE") now = int(time.time()) try: expired = conn.execute( @@ -1546,22 +1553,29 @@ def mempool_clear_expired(self) -> int: ).fetchall() except sqlite3.OperationalError as exc: if "no such table" in str(exc).lower(): + conn.execute("ROLLBACK") return 0 + conn.execute("ROLLBACK") raise - else: - count = 0 - for row in expired: - conn.execute( - "DELETE FROM utxo_mempool_inputs WHERE tx_id = ?", - (row['tx_id'],), - ) - conn.execute( - "DELETE FROM utxo_mempool WHERE tx_id = ?", - (row['tx_id'],), - ) - count += 1 - conn.commit() - return count + count = 0 + for row in expired: + conn.execute( + "DELETE FROM utxo_mempool_inputs WHERE tx_id = ?", + (row['tx_id'],), + ) + conn.execute( + "DELETE FROM utxo_mempool WHERE tx_id = ?", + (row['tx_id'],), + ) + count += 1 + conn.commit() + return count + except Exception: + try: + conn.execute("ROLLBACK") + except Exception: + pass + raise finally: conn.close() From 1fb989b651045d285bd772d8f4141562a514f0d5 Mon Sep 17 00:00:00 2001 From: waterWang Date: Fri, 7 Aug 2026 03:55:07 +0800 Subject: [PATCH 2/2] fix: require X-P2P-Key auth on /p2p/gossip POST endpoint (fixes #8177) [fj4WqyCCw3C5ShR1RfB7MoBPTpkRrBFYP1uT35g3MvT] --- node/rustchain_p2p_gossip.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/node/rustchain_p2p_gossip.py b/node/rustchain_p2p_gossip.py index 757babe51..f64546bba 100644 --- a/node/rustchain_p2p_gossip.py +++ b/node/rustchain_p2p_gossip.py @@ -1823,6 +1823,10 @@ def receive_gossip(): if not _gossip_rate_check(remote_ip): return jsonify({"error": "rate_limited", "limit": f"{GOSSIP_RATE_LIMIT}/{GOSSIP_RATE_WINDOW_S}s"}), 429 + auth_error = _require_p2p_read_auth() + if auth_error: + return auth_error + if ( request.content_length is not None and request.content_length > MAX_GOSSIP_REQUEST_BYTES