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-->
For details see: https://corecheck.dev/bitcoin/bitcoin/pulls/35728.
<!--021abf342d371248e50ceaed478a90ca-->
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><!--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-->
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.
<!--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>
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.
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>
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.