Skip to content

security: block save and transaction confirmation are not atomic #2156

@createkr

Description

@createkr

Component: node/rustchain_block_producer.pyBlockProducer.save_block() / node/rustchain_tx_handler.pyTransactionPool.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

  1. 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.

  2. 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.

  3. 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

  1. Create a block with 2 transactions from the same sender where the combined amount exceeds the sender's balance.
  2. The first confirm_transaction() succeeds and drains the balance.
  3. The second confirm_transaction() fails the balance check and returns False.
  4. save_block() ignores the False return value, commits the block.
  5. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions