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
2 changes: 1 addition & 1 deletion TESTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,7 @@ A cell that names a crate-local test describes the witness that exists today. It
| `sigop_adjusted_size_budget_and_reopen` | Mempool (crate) | One empty `ActiveMempool`: sigop-adjusted vsize (boundary, min relay, full-pool floor, RBF, package and 1p1c), the raw-weight cluster limit, the shared block sigop budget, and sigop cost plus bytes-per-sigop and reserve overlays across reopen and compact. Each beat restores an empty pool. |
| `mempool_accept_life` | Mempool (crate) | One `ActiveMempool` against one chain view that blocks move. Orphan parks and re-announce, dry run not parked, parent promotes the child; missing vout, invalid parent, and block-spent coin reject without parking. Full RBF and no return, the staged commit failing closed on a conflict that landed after prepare, pure RBFR unpinning a child, replaced txs out of the cluster count; a ~30 kvB single tx under the vsize cap and the ten-way merge over it. Package order, CPFP, child fail restoring the RBF victim. A block evicts double-spent txs with descendants; a reorg readmits the parent and evicts the BIP68 and coinbase-maturity spends. Raised `-minrelaytxfee`: 1p1c needs a paying child, an unrelated tx does not ride the waiver, child fail takes a promoted spender down. Full pool: a protected lone worst chunk evicts nothing and leaves the floor, the next arrival evicts the CPFP pair together. |
| `rpc_regtest_from_genesis` | RPC (crate) | One regtest hub from genesis through the first submits. At genesis: `size_on_disk` is the store walk, IBD comes from the hub and not the stale atomic, buried deployments, `generateblock submit=false` connects nothing, and the priority, mocktime, mockscheduler, submitheader decode, and not-found refuses. The first block pays a p2wpkh address: display-order hashes and txids, raw `getblock` and header, a headers-only child at progress 0.5. Mocktime stamps `generate` and makes a far block `time-too-new`. Coinbase-only blocks: verbosity 1 without a seqsigwit zip, `getnetworkhashps` over chainwork, the empty template and proposal needles, a `time-too-old` header, an invalid parent body that marks its branch, and the structure rejects named on this hub in [`docs/consensus-tests.md`](docs/consensus-tests.md) (S1–S3, S6's RPC `bad-txnmrklroot`, S8's missing and mismatched witness commitment, S9–S11, S13, S17, S18), including S1–S3 on an equal-work sibling. A resubmit of a rejected sibling is `duplicate-invalid`; a valid sibling stays `inconclusive`. |
| `rpc_regtest_mature_chain_ops` | RPC (crate) | One regtest hub mined to height 130. The `nblocks=0` window, GBT fee and sigops (bare, P2SH, P2WSH), sigop-adjusted mempool vsize (`getmempoolentry`, package retry, `blockmintxfee`; weight and the Esplora/Electrum histogram stay raw) and a big-sigops cluster under the block budget, a non-DER spend with Core `reject-details`, deprioritise, `getblocktemplate` order (higher rate, then higher fee, then the lesser txid, and a negative fee delta omitted), `generateblock` reject shapes then parent-first mining, a premature coinbase, proposal spend/value/final needles against the chain, a repeated txid and a second coinbase, default and explicit `maxfeerate`, `testmempoolaccept` known vs mempool vs archived, invalidate and reconsider, and a parked sibling (held `getblock`, `preciousblock`). Last, a mainnet view of the same hub refuses the regtest-only methods and still takes `submitblock`. |
| `rpc_regtest_mature_chain_ops` | RPC (crate) | One regtest hub mined to height 130. The `nblocks=0` window, GBT fee and sigops (bare, P2SH, P2WSH), sigop-adjusted mempool vsize (`getmempoolentry`, package retry, `blockmintxfee`; weight and the Esplora/Electrum histogram stay raw) and a big-sigops cluster under the block budget, a non-DER spend with Core `reject-details`, deprioritise, `getblocktemplate` order (higher rate, then higher fee, then the lesser txid, and a negative fee delta omitted), `generateblock` reject shapes then parent-first mining (the child's mempool spend hides the parent output from `gettxout`), a premature coinbase, proposal spend/value/final needles against the chain, a repeated txid and a second coinbase, default and explicit `maxfeerate`, `testmempoolaccept` known vs mempool vs archived, invalidate and reconsider, and a parked sibling (held `getblock`, `preciousblock`). Last, a mainnet view of the same hub refuses the regtest-only methods and still takes `submitblock`. |
| `block_cache_and_mempool_hub_surface` | Net | BlockCache locator/eviction + MempoolHub accept/remove/reorg on mature chain. Eviction uses body depth 16. |
| `store_error_and_corrupt_paths` | Store | Error/corrupt surfaces |
| `store_table_header_and_idx_corrupt` | Store | Table header/head corrupt open |
Expand Down
4 changes: 2 additions & 2 deletions changelog.d/gettxout-unspendable.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@ 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.
the UTXO set, and rbitcoin already refuses to spend them. An unspent
mempool output is still listed. One spent by another mempool tx is null.
6 changes: 6 additions & 0 deletions changelog.d/rpc-gettxout-mempool-spent.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
Fixed

- **`gettxout` hides an output of a mempool transaction that another mempool
transaction spends.** The confirmed branch already did. A wallet that
picked coins with `gettxout` (default `include_mempool`) saw its own
unconfirmed change as still spendable.
5 changes: 5 additions & 0 deletions crates/rbitcoin-rpc/src/methods/chain.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1070,6 +1070,11 @@ pub fn gettxout(ctx: &RpcContext, params: &RpcParams) -> Result<Value, Value> {
if let Some(mp) = ctx.mempool.as_ref() {
let tid = Txid::from_byte_array(want);
if let Some(tx) = mp.get_tx(&tid) {
// Another mempool tx spending this output takes it out of the
// coins view, as the confirmed path does below.
if mp.spends_outpoint(&bitcoin::OutPoint { txid: tid, vout: n }) {
return Ok(Value::Null);
}
if let Some(out) = tx.output.get(n as usize) {
return Ok(json!({
"bestblock": getbestblockhash(ctx)?,
Expand Down
2 changes: 1 addition & 1 deletion crates/rbitcoin-rpc/src/methods/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ pub const NAMED_HELP: &[(&str, &str)] = &[
(
"gettxout",
"gettxout txid n (include_mempool)\n\
Connected Class A + mempool. Default include_mempool hides confirmed outs spent by a live mempool tx.",
Connected Class A + mempool. Default include_mempool hides an out spent by a live mempool tx, confirmed or not.",
),
(
"sendrawtransaction",
Expand Down
16 changes: 16 additions & 0 deletions crates/rbitcoin-rpc/tests/regtest_chain_ops_journey.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2297,6 +2297,22 @@ fn chain_ops_generateblock_and_parent_first(ctx: &RpcContext, cbs: &mut TrueCoin
);

let child_id = dispatch(ctx, "sendrawtransaction", vec![json!(tx_hex(&child))]).unwrap();
// Core's mempool coins view: the parent output is spent by the child, so
// it is not a UTXO. Without the mempool it was never a confirmed coin.
for include_mempool in [true, false] {
let out = dispatch(
ctx,
"gettxout",
vec![parent_id.clone(), json!(0), json!(include_mempool)],
)
.unwrap();
assert!(
out.is_null(),
"include_mempool={include_mempool}: mempool-spent parent output: {out}"
);
}
let unconfirmed = dispatch(ctx, "gettxout", vec![child_id.clone(), json!(0)]).unwrap();
assert_eq!(unconfirmed["confirmations"], 0, "{unconfirmed}");
dispatch(ctx, "generate", vec![json!(1)]).unwrap();
let tip = best_hash(ctx);
let mined = dispatch(ctx, "getblock", vec![tip.clone(), json!(1)]).unwrap();
Expand Down
2 changes: 1 addition & 1 deletion docs/rpc.md
Original file line number Diff line number Diff line change
Expand Up @@ -158,7 +158,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. `scriptPubKey` is the `getrawtransaction` object (`hex`, `asm`, Core `type`, `address` for a standard script, and `desc`). 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. |
| `gettxout` | All networks. Connected Class A + mempool. `scriptPubKey` is the `getrawtransaction` object (`hex`, `asm`, Core `type`, `address` for a standard script, and `desc`). 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 output spent by another mempool tx is `null`; an unspent mempool output is listed. 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