Component: node/rustchain_block_producer.py — BlockProducer.save_block() / node/rustchain_tx_handler.py — TransactionPool.confirm_transaction()
Severity: Medium
Summary: save_block() and confirm_transaction() use separate SQLite connections, so the block insert and all transaction confirmations are not a single atomic unit. A crash or confirmation failure during save_block() can leave the database in an inconsistent state — the block is saved but some (or none) of its transactions are confirmed, and those transactions remain in the pending pool indefinitely.
Root Cause
BlockProducer.save_block() opens its own sqlite3.connect(self.db_path) and inserts the block row. It then loops over block.body.transactions calling self.tx_pool.confirm_transaction(tx_hash, height, hash) for each one. confirm_transaction() opens a separate sqlite3.connect(self.db_path) via _get_connection(), performs its own balance updates, and commits independently.
The return value of confirm_transaction() is also never checked — if a confirmation fails (e.g., insufficient balance because a prior tx in the same block already drained it), the loop continues and the block is saved anyway.
Impact
-
Partial block confirmation — If the node crashes between the block INSERT and the confirmation loop, or mid-loop, the block exists in blocks but some transactions remain in pending_transactions. Those transactions can be re-included in a future block, potentially double-crediting the receiver.
-
Silent confirmation failures — When confirm_transaction() returns False (e.g., balance check fails), save_block() ignores the failure and commits the block. The block's transaction count in body_json says N transactions, but fewer were actually confirmed.
-
Balance inconsistency — The receiver balance update happens inside confirm_transaction(). If some confirmations succeed and the block save rolls back, receiver balances are credited but the block that justified them doesn't exist.
Reproduction
- Create a block with 2 transactions from the same sender where the combined amount exceeds the sender's balance.
- The first
confirm_transaction() succeeds and drains the balance.
- The second
confirm_transaction() fails the balance check and returns False.
save_block() ignores the False return value, commits the block.
- The second transaction remains in
pending_transactions forever (or until expiration cleanup).
Distinction from Prior Submissions
Recommended Fix
Pass the block producer's database connection into confirm_transaction() so that the block insert and all confirmations share a single transaction boundary. If any confirmation fails, the entire block save is rolled back.
Component:
node/rustchain_block_producer.py—BlockProducer.save_block()/node/rustchain_tx_handler.py—TransactionPool.confirm_transaction()Severity: Medium
Summary:
save_block()andconfirm_transaction()use separate SQLite connections, so the block insert and all transaction confirmations are not a single atomic unit. A crash or confirmation failure duringsave_block()can leave the database in an inconsistent state — the block is saved but some (or none) of its transactions are confirmed, and those transactions remain in the pending pool indefinitely.Root Cause
BlockProducer.save_block()opens its ownsqlite3.connect(self.db_path)and inserts the block row. It then loops overblock.body.transactionscallingself.tx_pool.confirm_transaction(tx_hash, height, hash)for each one.confirm_transaction()opens a separatesqlite3.connect(self.db_path)via_get_connection(), performs its own balance updates, and commits independently.The return value of
confirm_transaction()is also never checked — if a confirmation fails (e.g., insufficient balance because a prior tx in the same block already drained it), the loop continues and the block is saved anyway.Impact
Partial block confirmation — If the node crashes between the block INSERT and the confirmation loop, or mid-loop, the block exists in
blocksbut some transactions remain inpending_transactions. Those transactions can be re-included in a future block, potentially double-crediting the receiver.Silent confirmation failures — When
confirm_transaction()returnsFalse(e.g., balance check fails),save_block()ignores the failure and commits the block. The block's transaction count inbody_jsonsays N transactions, but fewer were actually confirmed.Balance inconsistency — The receiver balance update happens inside
confirm_transaction(). If some confirmations succeed and the block save rolls back, receiver balances are credited but the block that justified them doesn't exist.Reproduction
confirm_transaction()succeeds and drains the balance.confirm_transaction()fails the balance check and returnsFalse.save_block()ignores theFalsereturn value, commits the block.pending_transactionsforever (or until expiration cleanup).Distinction from Prior Submissions
utxo_db.pyapply_transaction()rollback in the UTXO model. This finding is about the account modelsave_block↔confirm_transactionboundary — different tables (blocks,pending_transactions,transaction_history,balances), different code path, different failure mode.Recommended Fix
Pass the block producer's database connection into
confirm_transaction()so that the block insert and all confirmations share a single transaction boundary. If any confirmation fails, the entire block save is rolled back.