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

pull maflcko wants to merge 5 commits into bitcoin:master from maflcko:2607-io-error-tx changing 6 files +126 −37
  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:

    • #36002 (txindex: allow running in pruned mode by andrewtoth)
    • #35676 (util: Abort in CheckDiskSpace/FlatFileSeq::Open on rare exceptions by maflcko)
    • #35474 (node: move index ownership to NodeContext by w0xlt)
    • #34729 (Reduce log noise by ajtowns)
    • #31252 (rpc: print P2WSH and P2SH redem Script in getrawtransaction and getblock by polespinasa)
    • #29278 (Wallet: Add maxfeerate wallet startup option by ismaelsadeeq)

    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. maflcko force-pushed on Jul 15, 2026
  6. DrahtBot added the label CI failed on Jul 15, 2026
  7. 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>

  8. maflcko force-pushed on Jul 15, 2026
  9. maflcko force-pushed on Jul 15, 2026
  10. DrahtBot removed the label CI failed on Jul 15, 2026
  11. 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.

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


    maflcko commented at 4:30 PM on September 23, 2026:

    Hmm, I think the "I/O error reading block data" is correct, even when pruning.

    The correct way to handle this is to call CheckBlockDataAvailability early, and then throw the "I/O error reading block data" in GetTransaction.

    Waiting for the txindex and the blockindex to "fall through" to return a nullptr makes it impossible to uncover IO errors and makes it impossible to distinguish IO errors with normal missing data.

    The goal of this pull is to fix exactly that.

    (Edit: I've pushed your test draft and my fix)

  13. 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).

  14. in test/functional/feature_io_errors.py:47 in fac83ef8e9 outdated
      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.


    maflcko commented at 4:35 PM on September 23, 2026:

    Apologies not sure if I understand the question

    It was just a rhetorical question with the answer: "I don’t think a finally block is needed here: if an assertion fails, the functional test’s temporary datadir is deleted during teardown, so restoring the renamed file provides no practical benefit."

  15. maflcko marked this as a draft on Aug 7, 2026
  16. DrahtBot added the label Needs rebase on Aug 15, 2026
  17. maflcko marked this as ready for review on Sep 23, 2026
  18. test: Add test for getrawtransaction block read errors faf3bc736c
  19. rpc: Return proper error messages from getrawtransaction
    For the cases where a block hash is passed in.
    
    Previously, the RPC returned with a general "Block not available" error
    for pruned blocks or not yet downloaded blocks.
    
    Fix this by returning the more descriptive error via
    CheckBlockDataAvailability.
    
    Also, IO errors are incorrectly marked as "No such transaction found in
    the provided block."
    
    Fix this by throwing an IO error.
    faea6f9911
  20. test: Add test to check RPCs throw on IO errors fa2d97d01c
  21. maflcko force-pushed on Sep 23, 2026
  22. rpc: Properly throw on internal FindTx IO errors in ProcessPSBT and GetTransaction
    Currently, the FindTx return value is an optional, where nullopt
    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
    catch them here. However, that decision should be up to the
    caller, and not enforced by the low-level function.
    
    Do the same for 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.
    fadc37ca72
  23. refactor: Use GetTransaction in ProcessPSBT fa52cc16d5
  24. maflcko force-pushed on Sep 23, 2026
  25. DrahtBot added the label CI failed on Sep 23, 2026
  26. maflcko commented at 4:41 PM on September 23, 2026: member

    Force pushed to re-create the patch from scratch (addressing all feedback + "rebase")

  27. maflcko removed the label CI failed on Sep 23, 2026
  28. DrahtBot removed the label Needs rebase on Sep 23, 2026

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-09-24 10:51 UTC

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