Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions changelog.d/gettxout-unspendable.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
Fixed

- **`gettxout` returns null for a confirmed unspendable output.** An
`OP_RETURN` output (including the witness commitment) or a script over
10,000 bytes was reported as an unspent coin. Core never adds these to
the UTXO set, and rbitcoin already refuses to spend them. A mempool
transaction's outputs are still listed, as in Core.
4 changes: 4 additions & 0 deletions crates/rbitcoin-rpc/src/methods/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1009,6 +1009,10 @@ pub(crate) fn gettxout(ctx: &RpcContext, params: &RpcParams) -> Result<Value, Va
.query
.tx_output_at_fk(fk, n)
.map_err(|e| rpc_error(ERR_MISC, e.to_string()))?;
// Core never adds an unspendable output to the coins view.
if rbitcoin_consensus::policy::is_unspendable(&out.script) {
return Ok(Value::Null);
}
let height = ctx
.query
.store()
Expand Down
48 changes: 48 additions & 0 deletions crates/rbitcoin-rpc/src/methods_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1656,6 +1656,54 @@ fn getrawtransaction_verbose_mid_disconnect_has_no_block_fields() {
let _ = std::fs::remove_dir_all(&dir);
}

/// Core never adds an unspendable output to the UTXO set, so `gettxout` is
/// null for a confirmed `OP_RETURN` or a script over 10,000 bytes. A script
/// of exactly 10,000 bytes is still a coin. REST `getutxos` follows.
#[test]
fn gettxout_unspendable_confirmed_output_is_null() {
use bitcoin::TxOut;
let (ctx, dir, _hub) = ctx_from_mature_pad(300_000_000);
Comment thread
niteshbalusu11 marked this conversation as resolved.
let (_, mut spend) = mature_coinbase_spend_hex(&ctx, 50_0000_0000 - 101_000);
spend.output.push(TxOut {
value: Amount::from_sat(1_000),
script_pubkey: ScriptBuf::from_bytes(vec![0x6a, 0x01, 0x07]),
});
let mut big = vec![0x51];
big.resize(10_001, 0x61);
spend.output.push(TxOut {
value: Amount::ZERO,
script_pubkey: ScriptBuf::from_bytes(big),
});
spend.output.push(TxOut {
value: Amount::ZERO,
script_pubkey: ScriptBuf::from_bytes(vec![0x51; 10_000]),
});
let hex = rbitcoin_primitives::hex_encode(bitcoin::consensus::encode::serialize(&spend));
let (addr, _) = p2wpkh_regtest();
dispatch(&ctx, "generateblock", vec![json!(addr), json!([hex])]).unwrap();
let txid = hash_hex_display(&spend.compute_txid().to_byte_array());
let coin = dispatch(&ctx, "gettxout", vec![json!(txid), json!(0)]).unwrap();
assert!(!coin.is_null(), "a spendable output is a coin");
let boundary = dispatch(&ctx, "gettxout", vec![json!(txid), json!(3)]).unwrap();
assert_eq!(
boundary["scriptPubKey"]["hex"].as_str().map(str::len),
Some(20_000),
"a 10,000-byte script is still a coin: {boundary}"
);
for n in [1, 2] {
let out = dispatch(&ctx, "gettxout", vec![json!(txid), json!(n)]).unwrap();
assert!(out.is_null(), "unspendable vout {n} is not a coin: {out}");
}
let (status, body) = rest_text(
&ctx,
&format!("/rest/getutxos/{txid}-0/{txid}-1/{txid}-2/{txid}-3.json"),
"",
);
assert_eq!(status, axum::http::StatusCode::OK, "{body}");
assert!(body.contains("\"bitmap\":\"1001\""), "{body}");
let _ = std::fs::remove_dir_all(&dir);
}

#[test]
fn gettxout_leftover_is_connected_not_unconfirmed() {
let (ctx, dir, _hub) = ctx_from_mature_pad(300_000_000);
Expand Down
2 changes: 1 addition & 1 deletion docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -156,7 +156,7 @@ still wait for durable SH when shindex is on.
| `gettxspendingprevout` | All networks. Live mempool spender of each `{txid,vout}`. |
| `submitblock` | All networks. Like Core, a coinbase with a witness commitment and no witness first gets the 32-byte zero reserved value once segwit is active. The parent's height comes from the connected chain, a header tip, or a walk of at most 10,000 stored headers. A parent past that walk is treated as before segwit, so the block is left as sent. Then the same `ChainHub::accept_received_block` as a P2P `block` message: tip-extend, or hold by hash + most-work `accept_branch`. A consensus reject is a BIP22 reason string. A local store fault, including one reading the parent or tip header or a spent output, is RPC error `-25` (Core `state.IsError()`). A cancelled connect is `inconclusive`. Neither caches the block as invalid. |
| `getblockfilter` | All networks. Requires `--block-filter-index`. `filtertype` `basic` only (anything else is `-5`). Returns `filter` and `header` hex. A sealed best-chain height uses the index. One block whose parent filter header is already sealed (genesis uses the zero prev-header) is built from the stored body. A stale branch is rebuilt back to that sealed fork point. An unsealed best-chain gap is “Filter not found. Block filters are still in the process of being indexed.” Unknown hash is `-5` “Block not found”. Flag off is “Index is not enabled”. REST `/rest/blockfilter/` stays watermark-only (“still in the process of being indexed” past the watermark). |
| `gettxout` | All networks. Connected Class A + mempool. Default `include_mempool=true` returns `null` for a confirmed out spent by a live mempool tx. `include_mempool=false` still returns the confirmed coin. A leftover still live in the hub (IBD / `-blocksonly`) uses the connected path, not `confirmations: 0`. A disconnected archive row is `null` (not tip+1 confirmations). A vout at or past the output count is `null`. The genesis coinbase output is `null` (Core never adds it to the coins view); REST `getutxos` follows. |
| `gettxout` | All networks. Connected Class A + mempool. Default `include_mempool=true` returns `null` for a confirmed out spent by a live mempool tx. `include_mempool=false` still returns the confirmed coin. A leftover still live in the hub (IBD / `-blocksonly`) uses the connected path, not `confirmations: 0`. A disconnected archive row is `null` (not tip+1 confirmations). A vout at or past the output count is `null`. The genesis coinbase output is `null` (Core never adds it to the coins view). So is a confirmed unspendable output: a leading `OP_RETURN` (including the witness commitment) or a script over 10,000 bytes. A mempool tx's outputs are all listed, as in Core. REST `getutxos` follows. |
| `getindexinfo` | All networks. Reports `txindex` synced at tip — we reconstruct by txid from Class A (no separate index flag). |
| `getchaintips` | All networks. Active + archive `valid-fork` + held `valid-headers` + header-only (`submitheader` / P2P headers). Invalid body after a known header marks that branch `invalid`. |
| `getdeploymentinfo` | All networks. Buried deployments from `ChainParams` including `--test-activation-height`. `active` follows Core `DeploymentActiveAfter` (true for the *next* block). No BIP9 / testdummy. |
Expand Down
Loading