Skip to content

wip: use IPC for various things#1

Open
Sjors wants to merge 21 commits into
masterfrom
2026/05/ipc
Open

wip: use IPC for various things#1
Sjors wants to merge 21 commits into
masterfrom
2026/05/ipc

Conversation

@Sjors
Copy link
Copy Markdown
Owner

@Sjors Sjors commented May 12, 2026

Depends on:

Heavily vibe coded and it's possible that not all uses of IPC are actually beneficial.

Sjors added 6 commits May 12, 2026 18:01
Introduce an experimental backend that talks to a multiprocess
`bitcoin-node` over a Unix socket using libmultiprocess Cap'n Proto
framing, as exposed by Bitcoin Core PR #29409 (interfaces::Chain).

Adds:

  - a new optional `daemon_ipc_socket` config / CLI option
  - a new `src/ipc.rs` module with an `IpcChain` handle that owns a
    dedicated worker thread running a current-thread tokio runtime +
    `LocalSet` (the generated capnp clients are not `Send`)
  - capnp / capnp-rpc / tokio / tokio-util / futures dependencies plus
    a path dep on the local `bitcoin-capnp-types` crate

When `daemon_ipc_socket` is set, `Daemon::connect` boots the
`IpcChain` worker and `Daemon::for_blocks` fetches each block via
`Chain.findBlock(wantData=true)` instead of using the P2P getblock
loop. The serialized form returned by libmultiprocess is the same
wire encoding electrs already consumes, so the downstream parsing
and indexing pipeline is unchanged.

When the option is absent (the default), the P2P path is preserved
verbatim, so this is a strict superset of existing behaviour.
Mirrors `tests/run.sh` but launches the multiprocess `bitcoin node`
binary with `-ipcbind=unix` and points electrs at the resulting
`<datadir>/regtest/node.sock` via `--daemon-ipc-socket`.

The path to the multiprocess binary is configurable via
`$BITCOIN_MULTIPROCESS_BIN`; if unset, the script looks for `bitcoin`
on `PATH`. There is no hardcoded fallback.
- New `integration-ipc` job clones bitcoin/bitcoin#29409, builds the
  multiprocess `bitcoin` multi-call binary with capnproto enabled,
  installs Electrum + electrs, and invokes `tests/run-ipc.sh`.
- The existing `build` and `integration` jobs now also clone the
  `bitcoin-capnp-types` chain-interface branch into a sibling
  directory to satisfy the `path = "../capnp-types"` Cargo dep
  introduced in this branch. Mirrored in both Dockerfiles.
Replaces a JSON-RPC `getblockchaininfo` call with the equivalent
`Chain.havePruned` capnp method whenever the IPC backend is
configured. The RPC fallback is retained for the non-IPC path so this
remains a strict no-op for current deployments.

This is the first migration of an existing electrs RPC call onto the
PR #29409 chain interface; it is small but exercises the pattern
end-to-end (config-conditional dispatch + integration test coverage).
When the IPC chain interface is available, fetch the block bytes via
`Chain.findBlock` (already used for block download in for_blocks) and
extract txids from the decoded block instead of issuing a JSON-RPC
`getblock` call. This mirrors the call pattern of for_blocks and
removes one more dependency on the JSON-RPC backend.

The non-IPC path keeps the original `get_block_info` RPC call so this
is a no-op for current deployments. Verified with both tests/run.sh
(=== PASSED ===) and tests/run-ipc.sh (=== PASSED (IPC) ===).
…C is set

Adds an `IpcChain::broadcast_transaction` wrapper around the
`Chain.broadcastTransaction` capnp method and uses it from
`Daemon::broadcast` when the IPC backend is configured. The method
matches `sendrawtransaction`'s defaults: `MEMPOOL_AND_BROADCAST_TO_ALL`
(broadcastMethod = 0) and `DEFAULT_MAX_RAW_TX_FEE = 0.10 BTC`. On
failure the error string from the node is propagated.

The non-IPC path keeps the original `send_raw_transaction` RPC call,
so this is a no-op for current deployments. Verified end-to-end: the
`payto & broadcast` step of tests/run-ipc.sh exercises the new path
and reports === PASSED (IPC) ===.
@Sjors Sjors changed the title wip: use IPC to fetch blocks wip: use IPC for various things May 12, 2026
Sjors added 5 commits May 12, 2026 18:45
Add IpcChain::find_locator_fork (encodes a CBlockLocator on the wire) and
IpcChain::get_new_headers, which walks forward from the fork point using
getBlockHash + findBlock(wantData=true) and parses the first 80 bytes of
each block as a header. Cap the response at 2000 headers per call to
mirror the P2P getheaders semantics.

When --daemon-ipc-socket is set, Daemon::get_new_headers now goes through
this IPC path instead of the P2P getheaders exchange in src/p2p.rs.
Behaviour without IPC is unchanged.

The chain interface does not expose a header-only fetch, so this
downloads each full block twice (once for the header, again during the
subsequent for_blocks indexing pass). Acceptable for an experimental
backend over a local unix socket.
…IfTipChanged

Restructure the IPC worker to spawn each Job onto the LocalSet so a
long-running notifier task can run concurrently with regular RPC calls.
Ctx now derives Clone (its two capnp clients are Rc-backed and cheap to
clone) and is passed by value into each Job.

Add IpcChain::start_block_notifier, which kicks off a worker-side loop
that snapshots the current tip via getHeight + getBlockHash, blocks on
Chain.waitForNotificationsIfTipChanged, and signals on a bounded(1)
crossbeam channel each time the tip changes.

Daemon::connect now starts this notifier when an IPC socket is
configured and stashes the receiver on the Daemon. Daemon::new_block_notification
returns the IPC receiver instead of the P2P new-block channel when IPC
is available, eliminating the remaining P2P consumer for new-block
notifications. Behaviour without IPC is unchanged.
Two unrelated cleanup-noise fixes in the integration scripts:

- tests/run.sh: `bitcoin-cli stop` was sometimes racing the node's own
  exit and leaving a 'Could not connect to the server' line on stdout.
  Make the stop best-effort and fall back to killing the PID.

- tests/run-ipc.sh: when the multiprocess node has IPC clients still
  attached at shutdown, the RPC `stop` request hangs indefinitely
  waiting for them to disconnect. The IPC client lives in electrs,
  which has already stopped above but whose capnp connection drop is
  not flushed back to the node. Skip the polite stop and just kill
  the node — this is a regtest fixture.
Now that header sync, block fetch, and new-block notifications all go through the Chain interface when --daemon-ipc-socket is set, there is no consumer left for the P2P connection. Make Daemon.p2p an Option and skip Connection::connect (and the network_active enforcement) on the IPC path. The non-IPC path is unchanged.
The Chain interface exposes no header-only fetch, so get_new_headers has
to download each full block via findBlock(wantData=true) just to keep
the first 80 bytes. Without a cache, the for_blocks pass that
immediately follows refetches every one of those blocks over IPC --
doubling the byte count of initial sync, and making it a worse choice
than P2P for any chain longer than a handful of blocks.

Add a small bounded cache (256 MiB worth of payload, best-effort inserts
on overflow) on IpcChain. get_new_headers stashes each block it fetches;
get_block consults the cache and removes on hit, so for_blocks drains it
in iteration order. Cache size is naturally bounded by the in-flight
headers (capped at 100 per get_new_headers call -- much smaller than the
P2P getheaders 2000, since each "header" here costs a full block, and
mainnet blocks of up to ~4 MiB would otherwise blow the cache budget on
a single batch) and falls back gracefully to direct fetch on overflow.

A header-only chain method on the Bitcoin Core side would obviate this
entirely; noted as a follow-up in EXPERIMENT.md.
Sjors added 3 commits May 12, 2026 20:13
Drop the `branches: [master]` filter on the `push` trigger so CI runs
on every push, not just pushes to master. This makes the workflow fire
for branch pushes on forks (e.g. Sjors/electrs experimental branches),
matching the way most contributors iterate before opening a PR.

PR triggers are unaffected.
…Fee} on IPC

Adds two thin wrappers on `IpcChain`:

- `estimate_smart_fee_sat_per_kvb(nblocks)` calls
  `Chain.estimateSmartFee` (economical mode, no FeeCalculation), and
- `relay_min_fee_sat_per_kvb()` calls `Chain.relayMinFee`.

Both decode the serialized `CFeeRate` reply (FeeFrac { int64_t fee;
int32_t size; } -> 12 bytes LE, per Bitcoin Core SERIALIZE_METHODS)
into satoshis per kilo-vbyte via a new `decode_fee_rate_kvb` helper.
A `CFeeRate{}` (size == 0) reply maps to `Ok(None)` so the smart-fee
path can mirror the JSON-RPC -32603 "no estimate" behaviour.

`Daemon::estimate_fee` and `Daemon::get_relay_fee` now prefer the IPC
path when `ipc` is set and fall back to the bitcoind JSON-RPC client
otherwise.

This drops electrs's dependence on `getnetworkinfo.relayfee` and
`estimatesmartfee` for IPC deployments. Verified the `*_returns_default`
and `*_returns_decodable_blob` Chain integration tests in
bitcoin-capnp-types pass against the same multiprocess node electrs
talks to.
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.

1 participant