Skip to content

rpc: answers Core clients rely on (captaind / bark on rbitcoin) - #1191

Draft
mvuk wants to merge 5 commits into
reardencode:masterfrom
mvuk:rpc/core-client-compat
Draft

mvuk wants to merge 5 commits into
reardencode:masterfrom
mvuk:rpc/core-client-compat

Conversation

@mvuk

@mvuk mvuk commented Oct 24, 2026 •

Copy link
Copy Markdown

Summary

RPC answers that Core clients rely on, found by running Bark's captaind and bark integration suite against this node with no Esplora or electrs (#1165). No relay or consensus policy changes.

  • --rpc-client-version N (conf rpc_client_version=). Default getnetworkinfo.version stays 190000; captaind refuses to boot below 310000, so an operator running it sets --rpc-client-version=310000. Only the integer changes. (Was rpc: let operators raise getnetworkinfo.version #1159.)
  • gettxout hides an output of a mempool transaction that another mempool transaction spends. The confirmed branch already checked spends_outpoint; the mempool branch did not. Core's mempool coins view drops it. A wallet that picks coins with gettxout(include_mempool=true) double-spent its own unconfirmed change and got insufficient fee from the replacement check.
  • getblock verbosity 1 sends size, strippedsize, weight for a stale block (confirmations: -1). They were only present at verbosity 2 off the active chain. BDK's bdk_bitcoind_rpc::Emitter reads verbose getblock while it walks a reorg and failed on the missing field.
  • getmempoolentry and verbose getrawmempool report bip125-replaceable: true when the transaction or an unconfirmed ancestor has an input sequence at or below 0xfffffffd, as Core v30 still does. bitcoincore-rpc's GetMempoolEntryResult requires the field.
  • Regtest generate* / generateblock run one at a time. Each call selected pool transactions and then connected its block with no lock across the two. Two calls at once could both select a transaction; the second block carried one the first had just confirmed and failed bad-txns-BIP30. Core does both under cs_main.

Each fix has a regression test that fails on master and passes here. Part of #1164 / #1165. The v3 / TRUC / anchor-package changes are #1192.

Test plan

Matthew Vuk and others added 5 commits October 24, 2026 00:21
Default stays 190000 so existing clients keep the Core 0.19 response
shape. --rpc-client-version N (conf rpc_client_version, range
1..=1000000) reports a higher integer without adding methods. Bark
captaind requires 310000 because it reads getmempoolentry chunk fields
this node already returns.
An output of a mempool transaction came back from gettxout even when a
second mempool transaction already spent it. Core's mempool coins view
drops it, the same as the confirmed branch here already does. A wallet
that picks coins with gettxout(include_mempool=true) double-spent its
own unconfirmed change and got "insufficient fee" from the RBF check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A block off the active chain (confirmations -1) only carried size,
strippedsize and weight at verbosity 2. Core sends them from verbosity
1 for any block. BDK's bitcoind emitter reads verbose getblock while it
walks a reorg and fails on the missing field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Core's getmempoolentry and verbose getrawmempool carry
bip125-replaceable: true when the tx or an unconfirmed ancestor has an
input sequence at or below 0xfffffffd. rust-bitcoin's
GetMempoolEntryResult requires the field, so clients built on
bitcoincore-rpc fail to parse an rbitcoin entry without it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
generate, generatetoaddress, generatetodescriptor and generateblock
selected pool transactions and then connected their block with no lock
across the two. Two calls at once could both select a transaction; the
second block then carried one the first had just confirmed and was
rejected as bad-txns-BIP30. Core assembles and connects under cs_main.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

@coreyphillips coreyphillips left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two independent reviews.

needs changing before merge

  • CHANGELOG.md edited on a feature branch (CHANGELOG.md:16-18)

worth doing, does not block

  • Mining lock comment misstates Core, and the lock duplicates the hub's (crates/rbitcoin-rpc/src/methods/mine.rs:37-47)
  • docs/rpc.md still describes the old gettxout and getmempoolentry output (docs/rpc.md:147). The gettxout row at docs/rpc.md:167 still says only a confirmed output spent by a mempool tx is null, and that "A mempool tx's outputs are all listed, as in Core". After this PR, a mempool output that another mempool tx spends is also null. The getmempoolentry / getrawmempool row at :147 lists the entry fields but not bip125-replaceable. That row also says fullrbf is true (Libre always replaces), so it should say the new field reports BIP 125 signaling (on the tx or an unconfirmed ancestor), not whether this node would accept a replacement. rpc.md owns these facts per docs/README.md.

nits

  • Test doc comment now attaches to the assert_bip125 helper (crates/rbitcoin-rpc/tests/methods/mod.rs:1236-1243)
  • signals_bip125 clones every ancestor body (crates/rbitcoin-rpc/src/methods/mempool.rs:144-162)

Comment thread CHANGELOG.md
Comment on lines +16 to +18
- **`getnetworkinfo.version` stays 190000 by default.** `--rpc-client-version N`
(conf `rpc_client_version=`, range 1–1000000) raises only that integer.
Response shapes do not change. Bark captaind needs 310000.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

needs changing before merge: CHANGELOG.md edited on a feature branch

Commit 62a331a adds the --rpc-client-version bullet straight into ## [Unreleased] in CHANGELOG.md. The other four commits correctly add changelog.d/ fragments. AGENTS.md lists "Do not edit CHANGELOG.md on a feature branch" as a hard stop. docs/releases.md:48 says feature pulls do not edit it and that unreleased notes are one changelog.d/<topic>.md per pull, so that open branches do not collide on the same line. I confirmed it with git diff origin/master...HEAD -- CHANGELOG.md. The fix is to revert CHANGELOG.md and add something like changelog.d/rpc-client-version.md with category Added, since this is a new flag.

Comment on lines +37 to +47
/// One regtest mining call at a time, from pool selection through the block
/// connect. Core assembles and connects under cs_main; without this, a second
/// call can select a transaction the first just confirmed and mine it again
/// (`bad-txns-BIP30`).
static REGTEST_MINING: std::sync::Mutex<()> = std::sync::Mutex::new(());

fn regtest_mining_lock() -> std::sync::MutexGuard<'static, ()> {
REGTEST_MINING
.lock()
.unwrap_or_else(std::sync::PoisonError::into_inner)
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

worth doing, does not block: Mining lock comment misstates Core, and the lock duplicates the hub's

The comment at mine.rs:37-40 and the changelog fragment say Core assembles and connects under cs_main. It does not. In Core, generateBlocks calls createNewBlock, which takes cs_main and mempool.cs together, and only afterwards calls ProcessNewBlock. generateblock releases cs_main before GenerateBlock. Taking both locks together keeps each template's tip and transaction set consistent, so a losing concurrent call in Core produces a stale sibling block, not a bad-txns-BIP30 failure. The actual rbitcoin bug is that select_generate_txs runs outside the hub's existing generate_lock (chain.rs:2401), which already serializes tip read, mining and accept. This PR adds a static REGTEST_MINING mutex around that one. Because it is process-global rather than per hub, an in-process multi-hub harness now serializes mining across unrelated nodes. It also does not cover a peer block or submitblock that confirms the selected transaction between selection and the tip read. That race already existed before this PR. Moving selection inside the hub's generate_lock would leave one lock and fix the root cause. If that is out of scope, please at least correct the comment and changelog wording. The lock does fix the generate-vs-generate case: concurrent_generate_mines_each_pool_tx_once passed for me.

Comment on lines 1236 to +1243
/// Graph fields come from the cluster, not stub 1/0. Local sendraw is
/// unbroadcast until a peer getdata completes.
/// Core `bip125-replaceable` on each mempool entry.
fn assert_bip125(entries: &[&Value], want: bool) {
for e in entries {
assert_eq!(e["bip125-replaceable"], want, "{e}");
}
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: Test doc comment now attaches to the assert_bip125 helper

assert_bip125 and verbose_child were inserted between the doc comment of mempool_graph_fields_follow_cluster_and_unbroadcast ("Graph fields come from the cluster...") and the test itself. That comment now documents the helper, and the test has none.

Comment on lines +144 to +162
pub fn signals_bip125(mp: &MempoolHub, txid: &Txid) -> bool {
let mut seen = std::collections::HashSet::new();
let mut stack = vec![*txid];
while let Some(t) = stack.pop() {
if !seen.insert(t) {
continue;
}
let Some(tx) = mp.get_tx(&t) else {
continue;
};
if tx.input.iter().any(|i| i.sequence.is_rbf()) {
return true;
}
if let Some((depends, _)) = mp.depends_spentby(&t) {
stack.extend(depends);
}
}
false
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: signals_bip125 clones every ancestor body

For each ancestor visited, the walk calls MempoolHub::get_tx, which takes a read lock and clones the whole transaction, then calls depends_spentby, which takes a second lock. Verbose getrawmempool does this for every entry. The walk is bounded by the 64-tx cluster cap, so this is cost, not a hazard. A single read guard over the graph that checks input sequences without cloning would avoid it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants