rpc: Properly throw on internal I/O errors in GetTransaction #35728

pull maflcko wants to merge 3 commits into bitcoin:master from maflcko:2607-io-error-tx changing 10 files +108 −41
  1. maflcko commented at 12:09 PM on July 15, 2026: member

    Currently the GetTransaction return value is a nullable transaction pointer, where nullptr represents two different outcomes: I/O error and missing tx.

    Reporting a confirmed transaction as missing is wrong, so properly return an error instead.

  2. DrahtBot renamed this:
    rpc: Properly throw on internal I/O errors in GetTransaction
    rpc: Properly throw on internal I/O errors in GetTransaction
    on Jul 15, 2026
  3. DrahtBot added the label RPC/REST/ZMQ on Jul 15, 2026
  4. DrahtBot commented at 12:09 PM on July 15, 2026: contributor

    <!--e57a25ab6845829454e8d69fc972939a-->

    The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.

    <!--006a51241073e994b41acfe9ec718e94-->

    Code Coverage & Benchmarks

    For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35728.

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

    See the guideline and AI policy for information on the review process.

    Type Reviewers
    Concept ACK yuvicc, musaHaruna

    If your review is incorrectly listed, please copy-paste <code>&lt;!--meta-tag:bot-skip--&gt;</code> into the comment that the bot should ignore.

    <!--174a7506f384e20aa4161008e828411d-->

    Conflicts

    Reviewers, this pull request conflicts with the following ones:

    • #35676 (util: Abort in CheckDiskSpace/FlatFileSeq::Open on rare exceptions by maflcko)
    • #35531 (txindex: hash keys and pack positions to reduce disk usage by andrewtoth)
    • #35105 (Refactor: Updated TransactionError to use util::Expected and removed TransactionError:OK by kevkevinpal)
    • #34729 (Reduce log noise by ajtowns)
    • #24230 (indexes: Stop using node internal types and locking cs_main, improve sync logic by ryanofsky)

    If you consider this pull request important, please also help to review the conflicting pull requests. Ideally, start with the one that should be merged first.

    <!--5faf32d7da4f0f540f40219e4f7537a3-->

  5. rpc: Properly throw on internal FindTx IO errors in ProcessPSBT
    Currently, the FindTx return value is a flat bool, where false
    represents two different outcomes: I/O error and missing tx.
    
    One may argue that this is fine in ProcessPSBT, because a -txindex
    lookup error is rare, and an error where the fallbacks to lookup the tx
    are also failing is even rarer. However, a -txindex corruption may also
    point to a larger system corruption and it seems better to notify the
    user properly by throwing an exception.
    
    If I/O errors should be ignored in the future, it would be trivial to
    use value_or(nullptr) here. However, that decision should be up to the
    caller, and not enforced by the low-level function.
    
    This commit only fixes ProcessPSBT. Other RPCs are done in the next
    commit and use the value_or(nullptr) flattening fallback for now.
    cccc39d4b6
  6. maflcko force-pushed on Jul 15, 2026
  7. DrahtBot added the label CI failed on Jul 15, 2026
  8. DrahtBot commented at 12:15 PM on July 15, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task lint: https://github.com/bitcoin/bitcoin/actions/runs/29414186507/job/87348122506</sub> <sub>LLM reason (✨ experimental): CI failed due to a Python lint error from ruff (F401 unused import in test/functional/feature_io_errors.py).</sub>

    <details><summary>Hints</summary>

    Try to run the tests locally, according to the documentation. However, a CI failure may still happen due to a number of reasons, for example:

    • Possibly due to a silent merge conflict (the changes in this pull request being incompatible with the current code in the target branch). If so, make sure to rebase on the latest commit of the target branch.

    • A sanitizer issue, which can only be found by compiling with the sanitizer and running the affected test.

    • An intermittent issue.

    Leave a comment here, if you need help tracking down a confusing failure.

    </details>

  9. maflcko force-pushed on Jul 15, 2026
  10. rpc: Properly throw on internal I/O errors in GetTransaction
    Currently the GetTransaction return value is a nullable transaction
    pointer, where nullptr represents two different outcomes: I/O error and
    missing tx.
    
    Reporting a confirmed transaction as missing is wrong, so properly
    return an error instead.
    222207dcc1
  11. refactor: Use GetTransaction in ProcessPSBT fac83ef8e9
  12. maflcko force-pushed on Jul 15, 2026
  13. DrahtBot removed the label CI failed on Jul 15, 2026
  14. yuvicc commented at 6:02 AM on July 16, 2026: contributor

    Concept ACK Distinguishing “transaction not found” from internal read failures in TxIndex::FindTx/GetTransaction makes sense.

  15. in src/rpc/rawtransaction.cpp:317 in 222207dcc1


    yuvicc commented at 7:04 AM on July 16, 2026:

    Do we ever reach here? In pruned mode when user calls getrawtransaction <txid> <blockhash> for the block that is pruned we return "I/O error reading block data" instead we should return "Block not available" which was done previously?

    functional test to check this behavior

    <details>

        def test_block_without_data_is_not_io_error(self):
            node = self.nodes[0]
            txid = node.getblock(node.getblockhash(1))["tx"][0]
            tip = node.getbestblockhash()
            block = create_block(
                int(tip, 16),
                create_coinbase(node.getblockcount() + 1),
                ntime=node.getblock(tip)["time"] + 1,
            )
            block.solve()
            node.submitheader(block.serialize()[:80].hex())
    
            assert_raises_rpc_error(-1, "Block not available", node.getrawtransaction, txid, 1, block.hash_hex)
    
    

    </details>

  16. musaHaruna commented at 11:51 AM on July 16, 2026: contributor

    Concept ACK on separating expected outcomes ("transaction not found") from unexpected failures (I/O errors).

  17. in test/functional/feature_io_errors.py:47 in fac83ef8e9
      42 | +        msg = "I/O error reading block data"
      43 | +        assert_raises_rpc_error(-32603, msg, node.getrawtransaction, "a" * 64, blockhash=block)
      44 | +        msg = "Can't read block from disk"
      45 | +        assert_raises_rpc_error(-32603, msg, node.gettxoutproof, [tx2["txid"]])
      46 | +
      47 | +        blk_dat_moved.rename(blk_dat)
    


    musaHaruna commented at 11:59 AM on July 16, 2026:
            # Simulate a block file I/O failure by temporarily renaming the block file.
            blk_dat.rename(blk_dat_moved)
    
            try:
                msg = "I/O error while opening block file via txindex"
                assert_raises_rpc_error(-32603, msg, node.gettxoutproof, [txid])
                assert_raises_rpc_error(-32603, msg, node.getrawtransaction, txid)
                if self.is_wallet_compiled():
                    assert_raises_rpc_error(-32603, msg, node.utxoupdatepsbt, psbt)
                msg = "I/O error reading block data"
                assert_raises_rpc_error(-32603, msg, node.getrawtransaction, "a" * 64, blockhash=block)
                msg = "Can't read block from disk"
                assert_raises_rpc_error(-32603, msg, node.gettxoutproof, [tx2["txid"]])
            finally:
                blk_dat_moved.rename(blk_dat)
    

    Some small suggestions:

    Will it be worth it to add a brief comment before renaming blk00000.dat to clarify that the rename is intentionally simulating a block file I/O failure?

    Since the test temporarily renames blk00000.dat to simulate an I/O failure, would it be safer to wrap the assertions in a try...finally block so that the block file is always restored, even if an assertion or unexpected exception causes the test to exit early.


    maflcko commented at 12:07 PM on July 16, 2026:

    Since the test temporarily renames blk00000.dat to simulate an I/O failure, would it be safer to wrap the assertions in a try...finally block so that the block file is always restored, even if an assertion or unexpected exception causes the test to exit early.

    Why would it be safer to rename a file that will be deleted a tick later?


    musaHaruna commented at 1:10 PM on July 16, 2026:

    Apologies not sure if I understand the question, but what I mean by wrapping in try...finally is:

    blk_dat.rename(blk_dat_moved)
    
    msg = "I/O error while opening block file via txindex"
    assert_raises_rpc_error(-32603, msg, node.getrawtransaction, txid)
    
    # Suppose the assertion above fails.
    # Execution never reaches this line.
    blk_dat_moved.rename(blk_dat)
    

    If node.getrawtransaction(txid) does not return the expected RPC error (for example, due to a regression). Now the code is left with blk00000.dat.moved.

    So my thought is with try...finally even if the assertion fails, the finally block executes first, and the node is restored to its original state.

    So my initial comment is more of a question; would it be safer if we wrap the assertations in a try...finally.


github-metadata-mirror

This is a metadata mirror of the GitHub repository bitcoin/bitcoin. This site is not affiliated with GitHub. Content is generated from a GitHub metadata backup.
generated: 2026-07-25 06:50 UTC

This site is hosted by @0xB10C
More mirrored repositories can be found on mirror.b10c.me