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.
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.
<!--e57a25ab6845829454e8d69fc972939a-->
The following sections might be updated with supplementary metadata relevant to reviewers and maintainers.
<!--006a51241073e994b41acfe9ec718e94-->
<!--021abf342d371248e50ceaed478a90ca-->
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><!--meta-tag:bot-skip--></code> into the comment that the bot should ignore.
<!--174a7506f384e20aa4161008e828411d-->
Reviewers, this pull request conflicts with the following ones:
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-->
<!--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>
Concept ACK
Distinguishing “transaction not found” from internal read failures in TxIndex::FindTx/GetTransaction makes sense.
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>
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)
Concept ACK on separating expected outcomes ("transaction not found") from unexpected failures (I/O errors).
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)
# 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.
Since the test temporarily renames
blk00000.datto simulate an I/O failure, would it be safer to wrap the assertions in atry...finallyblock 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?
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.
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."
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.
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.
<!--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>
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())};
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
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.
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?",
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};
thx, done
162 | +{ 163 | + LogWarning("%s", err); 164 | + throw std::runtime_error{err}; 165 | +} 166 | + 167 | std::optional<TxIndexResult> TxIndex::FindTx(const Txid& tx_hash) const
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;
};
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.
Concept ACK
test_getrawtransaction_block_data_errors showing default errors when the block is not available and when the block cannot be readgetrawtransaction no longer checks the block availability inline, task delegated to CheckBlockDataAvailability, now I/O errors can be detected in GetTransaction and thrownfeature_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 fileFindTx 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)ProcessPSBT now using GetTransaction instead of a more verbose tx lookupChanges 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.
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.
I wonder if throwing immediately here could prevent callers from trying other lookups that might still succeed for example in
ProcessPSBT, we first callGetTransactionwhich checks the mempool and txindex, if the tx isnt found we useFindCoinsto look in the utxo setBut if
FindTxthrows, 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.
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?