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 +123 −39
  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-->

    External sites

    <!--021abf342d371248e50ceaed478a90ca-->

    Reviews

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

    Type Reviewers
    Concept ACK yuvicc, musaHaruna, enirox001

    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 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/36002.svg"></sub> (txindex: allow running in pruned mode by andrewtoth)
    • #31252 <sub><img src="https://drahtbot.space/ack_count/bitcoin/bitcoin/31252.svg"></sub> (rpc: print P2WSH and P2SH redem Script in getrawtransaction and getblock by polespinasa)

    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. maflcko force-pushed on Sep 23, 2026
  19. maflcko force-pushed on Sep 23, 2026
  20. DrahtBot added the label CI failed on Sep 23, 2026
  21. maflcko commented at 4:41 PM on September 23, 2026: member

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

    <!-- • Found two correctness issues and several wording problems. - [P2] src/index/txindex.cpp:187: iterator failures still become nullopt. A LevelDB iterator I/O error makes Valid() false, while malformed key data makes GetKey() false. Both terminate the loop and can still produce “transaction not found.” Thus fadc37c does not fully establish the contract claimed by its message. - [P2] src/index/txindex.cpp:197: the two explicit continue branches also preserve false negatives. A missing block-index entry or missing BLOCK_HAVE_DATA is described as corruption/incomplete persistence, but if no later candidate succeeds, FindTx returns nullopt. A better pattern is to remember the inconsistency, continue searching for hash-prefix collisions/duplicate transactions, and throw if no valid match is ultimately found.

  22. maflcko removed the label CI failed on Sep 23, 2026
  23. DrahtBot removed the label Needs rebase on Sep 23, 2026
  24. DrahtBot added the label Needs rebase on Sep 26, 2026
  25. maflcko force-pushed on Oct 6, 2026
  26. test: Add test for getrawtransaction block read errors fa5ad60c32
  27. 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.
    fa5b2f13da
  28. test: Add test to check RPCs throw on IO errors fa8a5b033b
  29. maflcko force-pushed on Oct 12, 2026
  30. maflcko force-pushed on Oct 12, 2026
  31. DrahtBot added the label CI failed on Oct 12, 2026
  32. DrahtBot commented at 10:30 AM on October 12, 2026: contributor

    <!--85328a0da195eb286784d51f73fa0af9-->

    🚧 At least one of the CI tasks failed. <sub>Task iwyu: https://github.com/bitcoin/bitcoin/actions/runs/38228957902/job/114743378818</sub> <sub>LLM reason (✨ experimental): CI failed because IWYU removed the unused <atomic> include from src/index/txindex.cpp, leaving an uncommitted diff that the check rejects.</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>

  33. maflcko force-pushed on Oct 12, 2026
  34. DrahtBot removed the label Needs rebase on Oct 12, 2026
  35. DrahtBot removed the label CI failed on Oct 12, 2026
  36. in src/node/transaction.cpp:165 in fa5b2f13da outdated
     166 | -                if (tx->GetHash() == hash) {
     167 | -                    hashBlock = block_index->GetBlockHash();
     168 | -                    return tx;
     169 | -                }
     170 | +        if (!blockman.ReadBlock(block, *block_index)) {
     171 | +            throw std::runtime_error{strprintf("I/O error reading block data for block %s", block_index->GetBlockHash().ToString())};
    


    enirox001 commented at 11:42 PM on October 13, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/35728/changes/fa5b2f13da03d295ab624190f0d393b13da5b929: rpc: Return proper error messages from getrawtransaction

    nit: the header file should be updated to mention that this can throw now

    index bf92869d3e..e7ee5ee1a4 100644
    --- a/src/node/transaction.h
    +++ b/src/node/transaction.h
    @@ -71,6 +71,7 @@ inline constexpr CAmount DEFAULT_MAX_BURN_AMOUNT{0};
      * [@param](/bitcoin-bitcoin/contributor/param/)[in]  blockman        Used to access and read blocks from disk
      * [@param](/bitcoin-bitcoin/contributor/param/)[out] hashBlock       The block hash, if the tx was found via -txindex or block_index
      * [@returns](/bitcoin-bitcoin/contributor/returns/)                    The tx if found, otherwise nullptr
    + * [@throws](/bitcoin-bitcoin/contributor/throws/) a std::runtime_error if block data cannot be read.
      */
     CTransactionRef GetTransaction(const CBlockIndex* block_index, const CTxMemPool* mempool, const Txid& hash, const BlockManager& blockman, uint256& hashBlock);
     } // namespace node
    

    maflcko commented at 1:51 PM on October 14, 2026:

    This is already true on current master: It calls m_db->Read(...), which can throw dbwrapper_error, which is also a runtime_error. So my preference would be to leave this as-is for now. If you really want, I can add a unrelated first commit to document this, but I think it is fine.

  37. in src/index/txindex.cpp:199 in 3333a91dbf
     198 | -                continue;
     199 | +                WarnAndThrowErr(strprintf("%s: Block index entry %s not found for txid %s. Caused by data corruption or possibly an unclean shutdown?",
     200 | +                                          GetName(), candidate_block_hash.ToString(), tx_hash.ToString()));
     201 | +            }
     202 | +            if (!(block_index->nStatus & BLOCK_HAVE_DATA)) {
     203 | +                WarnAndThrowErr(strprintf("%s: Block index entry %s unexpectedly unavailable for txid %s. Caused by data corruption or possibly an unclean shutdown?",
    


    enirox001 commented at 1:36 AM on October 14, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/35728/changes/3333a91dbf1eac659cd7d027bc891a80241e8f66: rpc: Properly throw on internal FindTx IO errors in ProcessPSBT and GetTransaction

    nit: Here I think what is missing is the block data and not the index

    index d608e733f3..9ec5c2e857 100644
    --- a/src/index/txindex.cpp
    +++ b/src/index/txindex.cpp
    @@ -196,7 +196,7 @@ std::optional<TxIndexResult> TxIndex::FindTx(const Txid& tx_hash) const
                                               GetName(), candidate_block_hash.ToString(), tx_hash.ToString()));
                 }
                 if (!(block_index->nStatus & BLOCK_HAVE_DATA)) {
    -                WarnAndThrowErr(strprintf("%s: Block index entry %s unexpectedly unavailable for txid %s. Caused by data corruption or possibly an unclean shutdown?",
    +                WarnAndThrowErr(strprintf("%s: Block data %s unexpectedly unavailable for txid %s. Caused by data corruption or possibly an unclean shutdown?",
                                               GetName(), candidate_block_hash.ToString(), tx_hash.ToString()));
                 }
                 const FlatFilePos tx_position{block_index->nFile, block_index->nDataPos + key.pos.tx_offset_in_block};
    

    maflcko commented at 1:51 PM on October 14, 2026:

    thx, done

  38. in src/index/txindex.cpp:167 in 3333a91dbf outdated
     162 | +{
     163 | +    LogWarning("%s", err);
     164 | +    throw std::runtime_error{err};
     165 | +}
     166 | +
     167 |  std::optional<TxIndexResult> TxIndex::FindTx(const Txid& tx_hash) const
    


    enirox001 commented at 1:44 AM on October 14, 2026:

    In commit https://github.com/bitcoin/bitcoin/pull/35728/changes/3333a91dbf1eac659cd7d027bc891a80241e8f66#r4265873624: rpc: Properly throw on internal FindTx IO errors in ProcessPSBT and GetTransaction

    nit: the header documentation for this method should be updated to mention it can now throw

    index 23fb17d431..c72251273b 100644
    --- a/src/index/txindex.h
    +++ b/src/index/txindex.h
    @@ -63,6 +63,7 @@ public:
         ///
         /// [@param](/bitcoin-bitcoin/contributor/param/)[in]   tx_hash  The hash of the transaction to be returned.
         /// [@return](/bitcoin-bitcoin/contributor/return/)  the transaction and containing block hash, or nullopt if it is not found
    +    /// [@throws](/bitcoin-bitcoin/contributor/throws/) a std::runtime_error If an I/O or transaction index error occurs.
         std::optional<TxIndexResult> FindTx(const Txid& tx_hash) const;
     };
    

    maflcko commented at 1:51 PM on October 14, 2026:

    This is already true on current master: It calls m_db->Read(...), which can throw dbwrapper_error, which is also a runtime_error. So my preference would be to leave this as-is for now. If you really want, I can add a unrelated first commit to document this, but I think it is fine.

  39. enirox001 commented at 2:07 AM on October 14, 2026: contributor

    Concept ACK

    • fa5ad60c32 introduced test_getrawtransaction_block_data_errors showing default errors when the block is not available and when the block cannot be read
    • fa5b2f13da getrawtransaction no longer checks the block availability inline, task delegated to CheckBlockDataAvailability, now I/O errors can be detected in GetTransaction and thrown
    • fa8a5b033b introduced feature_io_errors.py to test I/O errors, creates a psbt and makes the block file unavailable, showing default behavior when rpc commands try to read this file
    • 3333a91dbf FindTx still returns nullopt for a missing tx, but on the I/O and index, errors are handled here, we now log the warning and throw instead of continuing (updated test to use the change)
    • faa4bb35ae In ProcessPSBT now using GetTransaction instead of a more verbose tx lookup

    Changes look good to me, left a few nits, I do have a question though;

    I wonder if throwing immediately here could prevent callers from trying other lookups that might still succeed for example in ProcessPSBT, we first call GetTransaction which checks the mempool and txindex, if the tx isnt found we use FindCoins to look in the utxo set

    But if FindTx throws, we never reach that last check, even though the utxo set might still have the input information.

  40. 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.
    fa5c96a112
  41. refactor: Use GetTransaction in ProcessPSBT fa6a70716a
  42. maflcko force-pushed on Oct 14, 2026
  43. maflcko commented at 1:51 PM on October 14, 2026: member

    I wonder if throwing immediately here could prevent callers from trying other lookups that might still succeed for example in ProcessPSBT, we first call GetTransaction which checks the mempool and txindex, if the tx isnt found we use FindCoins to look in the utxo set

    But if FindTx throws, we never reach that last check, even though the utxo set might still have the input information.

    Correct. But this is intentional: I'd say if someone is running with a corrupt index (or an index with stale data from an unclean shutdown, it would be good to notify them about it as early as possible. This way, they can either disable the index, or recreate it by deleting the index folder. Somehow adding extra fallback code means they'll still run into the issue, just later.

  44. enirox001 commented at 2:56 PM on October 14, 2026: contributor

    Correct. But this is intentional: I'd say if someone is running with a corrupt index (or an index with stale data from an unclean shutdown, it would be good to notify them about it as early as possible. This way, they can either disable the index, or recreate it by deleting the index folder. Somehow adding extra fallback code means they'll still run into the issue, just later.

    Sounds good, letting the user know early on that the index is corrupted is a good idea, Would a short release note make sense for this, since some rpcs now report index or read errors where they previously continued with other lookups or reported a missing tx?


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-10-14 18:51 UTC

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