From 0254f1ed5ea106fbaad7fb3c08bb6c6ac2786ead Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 4 Aug 2026 22:16:13 -0400 Subject: [PATCH 01/29] Regenerate artifacts (sorts). --- builds/gnu/Makefile.am | 86 +++++++++++++++++++------------------- include/bitcoin/server.hpp | 28 ++++++------- 2 files changed, 57 insertions(+), 57 deletions(-) diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index fe918ebb..653c30b3 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -61,13 +61,6 @@ src_libbitcoin_server_la_SOURCES = \ ${srcdir}/../../src/protocols/protocol_html.cpp \ ${srcdir}/../../src/protocols/protocol_http.cpp \ ${srcdir}/../../src/protocols/admin/protocol_admin.cpp \ - ${srcdir}/../../src/protocols/native/protocol_native.cpp \ - ${srcdir}/../../src/protocols/native/protocol_native_address.cpp \ - ${srcdir}/../../src/protocols/native/protocol_native_block.cpp \ - ${srcdir}/../../src/protocols/native/protocol_native_configuration.cpp \ - ${srcdir}/../../src/protocols/native/protocol_native_input.cpp \ - ${srcdir}/../../src/protocols/native/protocol_native_output.cpp \ - ${srcdir}/../../src/protocols/native/protocol_native_tx.cpp \ ${srcdir}/../../src/protocols/bitcoind/protocol_bitcoind_rest.cpp \ ${srcdir}/../../src/protocols/bitcoind/protocol_bitcoind_rpc.cpp \ ${srcdir}/../../src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp \ @@ -86,6 +79,13 @@ src_libbitcoin_server_la_SOURCES = \ ${srcdir}/../../src/protocols/electrum/protocol_electrum_subscribe.cpp \ ${srcdir}/../../src/protocols/electrum/protocol_electrum_transactions.cpp \ ${srcdir}/../../src/protocols/electrum/protocol_electrum_version.cpp \ + ${srcdir}/../../src/protocols/native/protocol_native.cpp \ + ${srcdir}/../../src/protocols/native/protocol_native_address.cpp \ + ${srcdir}/../../src/protocols/native/protocol_native_block.cpp \ + ${srcdir}/../../src/protocols/native/protocol_native_configuration.cpp \ + ${srcdir}/../../src/protocols/native/protocol_native_input.cpp \ + ${srcdir}/../../src/protocols/native/protocol_native_output.cpp \ + ${srcdir}/../../src/protocols/native/protocol_native_tx.cpp \ ${srcdir}/../../src/protocols/stratum_v1/protocol_stratum_v1.cpp include_bitcoindir = \ @@ -106,21 +106,6 @@ include_bitcoin_server_HEADERS = \ ${srcdir}/../../include/bitcoin/server/settings.hpp \ ${srcdir}/../../include/bitcoin/server/version.hpp -include_bitcoin_server_impl_protocolsdir = \ - ${includedir}/bitcoin/server/impl/protocols - -include_bitcoin_server_impl_protocols_HEADERS = \ - ${srcdir}/../../include/bitcoin/server/impl/protocols/protocol_native.ipp - -include_bitcoin_server_sessionsdir = \ - ${includedir}/bitcoin/server/sessions - -include_bitcoin_server_sessions_HEADERS = \ - ${srcdir}/../../include/bitcoin/server/sessions/session.hpp \ - ${srcdir}/../../include/bitcoin/server/sessions/session_handshake.hpp \ - ${srcdir}/../../include/bitcoin/server/sessions/session_server.hpp \ - ${srcdir}/../../include/bitcoin/server/sessions/sessions.hpp - include_bitcoin_server_channelsdir = \ ${includedir}/bitcoin/server/channels @@ -133,6 +118,27 @@ include_bitcoin_server_channels_HEADERS = \ ${srcdir}/../../include/bitcoin/server/channels/channel_stratum_v2.hpp \ ${srcdir}/../../include/bitcoin/server/channels/channels.hpp +include_bitcoin_server_impl_protocolsdir = \ + ${includedir}/bitcoin/server/impl/protocols + +include_bitcoin_server_impl_protocols_HEADERS = \ + ${srcdir}/../../include/bitcoin/server/impl/protocols/protocol_native.ipp + +include_bitcoin_server_interfacesdir = \ + ${includedir}/bitcoin/server/interfaces + +include_bitcoin_server_interfaces_HEADERS = \ + ${srcdir}/../../include/bitcoin/server/interfaces/admin.hpp \ + ${srcdir}/../../include/bitcoin/server/interfaces/bitcoind_rest.hpp \ + ${srcdir}/../../include/bitcoin/server/interfaces/bitcoind_rpc.hpp \ + ${srcdir}/../../include/bitcoin/server/interfaces/btcd.hpp \ + ${srcdir}/../../include/bitcoin/server/interfaces/electrum.hpp \ + ${srcdir}/../../include/bitcoin/server/interfaces/interfaces.hpp \ + ${srcdir}/../../include/bitcoin/server/interfaces/native.hpp \ + ${srcdir}/../../include/bitcoin/server/interfaces/stratum_v1.hpp \ + ${srcdir}/../../include/bitcoin/server/interfaces/stratum_v2.hpp \ + ${srcdir}/../../include/bitcoin/server/interfaces/types.hpp + include_bitcoin_server_parsersdir = \ ${includedir}/bitcoin/server/parsers @@ -164,20 +170,14 @@ include_bitcoin_server_protocols_HEADERS = \ ${srcdir}/../../include/bitcoin/server/protocols/protocol_stratum_v2.hpp \ ${srcdir}/../../include/bitcoin/server/protocols/protocols.hpp -include_bitcoin_server_interfacesdir = \ - ${includedir}/bitcoin/server/interfaces +include_bitcoin_server_sessionsdir = \ + ${includedir}/bitcoin/server/sessions -include_bitcoin_server_interfaces_HEADERS = \ - ${srcdir}/../../include/bitcoin/server/interfaces/admin.hpp \ - ${srcdir}/../../include/bitcoin/server/interfaces/bitcoind_rest.hpp \ - ${srcdir}/../../include/bitcoin/server/interfaces/bitcoind_rpc.hpp \ - ${srcdir}/../../include/bitcoin/server/interfaces/btcd.hpp \ - ${srcdir}/../../include/bitcoin/server/interfaces/electrum.hpp \ - ${srcdir}/../../include/bitcoin/server/interfaces/interfaces.hpp \ - ${srcdir}/../../include/bitcoin/server/interfaces/native.hpp \ - ${srcdir}/../../include/bitcoin/server/interfaces/stratum_v1.hpp \ - ${srcdir}/../../include/bitcoin/server/interfaces/stratum_v2.hpp \ - ${srcdir}/../../include/bitcoin/server/interfaces/types.hpp +include_bitcoin_server_sessions_HEADERS = \ + ${srcdir}/../../include/bitcoin/server/sessions/session.hpp \ + ${srcdir}/../../include/bitcoin/server/sessions/session_handshake.hpp \ + ${srcdir}/../../include/bitcoin/server/sessions/session_server.hpp \ + ${srcdir}/../../include/bitcoin/server/sessions/sessions.hpp # Tests. #============================================================================== @@ -209,6 +209,7 @@ test_libbitcoin_server_test_SOURCES = \ ${srcdir}/../../test/main.cpp \ ${srcdir}/../../test/settings.cpp \ ${srcdir}/../../test/test.cpp \ + ${srcdir}/../../test/mocks/blocks.cpp \ ${srcdir}/../../test/parsers/admin_query.cpp \ ${srcdir}/../../test/parsers/admin_target.cpp \ ${srcdir}/../../test/parsers/bitcoind_target.cpp \ @@ -217,13 +218,6 @@ test_libbitcoin_server_test_SOURCES = \ ${srcdir}/../../test/parsers/native_target.cpp \ ${srcdir}/../../test/protocols/admin/admin_diagnostics.cpp \ ${srcdir}/../../test/protocols/admin/admin_setup_fixture.cpp \ - ${srcdir}/../../test/protocols/native/native_address.cpp \ - ${srcdir}/../../test/protocols/native/native_block.cpp \ - ${srcdir}/../../test/protocols/native/native_configuration.cpp \ - ${srcdir}/../../test/protocols/native/native_input.cpp \ - ${srcdir}/../../test/protocols/native/native_output.cpp \ - ${srcdir}/../../test/protocols/native/native_setup_fixture.cpp \ - ${srcdir}/../../test/protocols/native/native_tx.cpp \ ${srcdir}/../../test/protocols/bitcoind/bitcoind_json.cpp \ ${srcdir}/../../test/protocols/bitcoind/bitcoind_rest.cpp \ ${srcdir}/../../test/protocols/bitcoind/bitcoind_rpc.cpp \ @@ -243,7 +237,13 @@ test_libbitcoin_server_test_SOURCES = \ ${srcdir}/../../test/protocols/electrum/electrum_subscribe.cpp \ ${srcdir}/../../test/protocols/electrum/electrum_transactions.cpp \ ${srcdir}/../../test/protocols/electrum/electrum_version.cpp \ - ${srcdir}/../../test/mocks/blocks.cpp + ${srcdir}/../../test/protocols/native/native_address.cpp \ + ${srcdir}/../../test/protocols/native/native_block.cpp \ + ${srcdir}/../../test/protocols/native/native_configuration.cpp \ + ${srcdir}/../../test/protocols/native/native_input.cpp \ + ${srcdir}/../../test/protocols/native/native_output.cpp \ + ${srcdir}/../../test/protocols/native/native_setup_fixture.cpp \ + ${srcdir}/../../test/protocols/native/native_tx.cpp TESTS = test_runner.sh diff --git a/include/bitcoin/server.hpp b/include/bitcoin/server.hpp index 47a08daf..5b39999f 100644 --- a/include/bitcoin/server.hpp +++ b/include/bitcoin/server.hpp @@ -22,10 +22,6 @@ #include #include #include -#include -#include -#include -#include #include #include #include @@ -33,6 +29,16 @@ #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include @@ -54,15 +60,9 @@ #include #include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include +#include +#include +#include +#include #endif From 251b342370905aa6202be8f237b77ae260e1b16f Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 4 Aug 2026 22:45:32 -0400 Subject: [PATCH 02/29] btcd: reduce comment density to standard. --- docs/btcd-endpoint.md | 377 ------------------ endpoints/README.md | 4 +- endpoints/test_btcd_rpc.py | 12 +- .../bitcoin/server/channels/channel_btcd.hpp | 38 +- include/bitcoin/server/interfaces/btcd.hpp | 14 +- .../protocols/protocol_bitcoind_rpc.hpp | 19 +- .../server/protocols/protocol_btcd_rpc.hpp | 134 ++----- include/bitcoin/server/sessions/sessions.hpp | 15 +- .../bitcoind/protocol_bitcoind_rpc.cpp | 42 +- src/protocols/btcd/protocol_btcd_rpc.cpp | 107 ++--- .../btcd/protocol_btcd_rpc_filter.cpp | 17 +- .../btcd/protocol_btcd_rpc_utility.cpp | 33 +- test/protocols/bitcoind/bitcoind_rpc.cpp | 9 +- test/protocols/btcd/btcd_rpc.cpp | 49 +-- test/protocols/btcd/btcd_setup_fixture.hpp | 10 +- 15 files changed, 128 insertions(+), 752 deletions(-) delete mode 100644 docs/btcd-endpoint.md diff --git a/docs/btcd-endpoint.md b/docs/btcd-endpoint.md deleted file mode 100644 index 94daa8f5..00000000 --- a/docs/btcd-endpoint.md +++ /dev/null @@ -1,377 +0,0 @@ -# btcd-Compatible Endpoint - -This document specifies the `btcd` JSON-RPC/WebSocket compatibility endpoint for -libbitcoin-server, modeled on the existing `bitcoind` endpoint (see -`src/protocols/bitcoind/`, added in #795) and on -[btcd's JSON-RPC API](https://btcd.readthedocs.io/en/latest/json_rpc_api.html). - -## Background - -btcd is a Bitcoin full node written in Go that intentionally separates wallet and -chain services into independent processes: unlike Core/bitcoind, a direct -connection to btcd exposes only chain RPCs, never wallet methods. That matches -libbitcoin-server's own scope (no wallet), which is what makes btcd a good second -compatibility target after bitcoind. - -btcd exposes two transports on the same port (default `8334`): - -- **HTTP POST**, one JSON-RPC request/response per connection (same shape as the - existing `bitcoind` endpoint). -- **WebSocket** (`/ws`), btcd's preferred transport: a single connection carries - many requests plus asynchronous push notifications (new blocks, filtered - transactions, rescan progress). - -Real btcd authenticates via TLS + HTTP Basic Auth, with two credential tiers: -`rpcuser`/`rpcpass` (full access) and `rpclimituser`/`rpclimitpass` (restricted to -a fixed allowlist of chain-read methods). **Decision: this endpoint does not build -a bespoke two-tier scheme.** It reuses `network::settings::http_server`'s -credential model as-is (`btcd.credential = username:password[:method,...]`, same -config shape as `bitcoind.*`), which happens to already support scoping a -credential to a method allowlist generically; `channel_btcd::permitted()` enforces -that scope over both http and ws. Sensitive methods (`stop`, mining/peer -management) are stubbed to `not_implemented` unconditionally regardless of scope, -so this is revisited only if a real `stop` path is implemented later. - -## Architecture - -``` -network::protocol_http -└── server::protocol_bitcoind_rpc - └── server::protocol_btcd_rpc (new) - -network::channel_http -└── server::channel_http - └── server::channel_btcd (new) - -server::session_btcd = session_server (new) -``` - -`protocol_btcd_rpc` inherits `protocol_bitcoind_rpc`, which already implements the -chain-read/rawtx handlers both APIs share (`getbestblockhash`, `getblock`, -`getblockhash`, `getblockheader`, `getblockcount`, `gettxout`, `getrawtransaction`, -`sendrawtransaction`, ...). It adds a second dispatcher for `interface::btcd` -(btcd-only methods) and overrides `network::protocol_http::dispatch_websocket()` — -the extension point `protocol_http` already exposes for handling an HTTP→WS -upgrade — to route btcd's WS-only notification/subscription methods. - -`channel_btcd` extends `channel_http` with an `authenticated_` flag. Nothing is -composed or re-sent per ws message -- ws data frames structurally cannot carry -a per-message Authorization header at all. Instead, authorization for the -whole connection is established exactly **once**, by whichever happens first: -real Basic Auth on the one-time ws upgrade request (a normal HTTP request, -with real headers, checked the same way a plain http request is), or the -in-band `authenticate` command, called once as a substitute for that header. -Either way the result is a cached credential digest (`authenticated_`/ -`authenticated_digest_`) consulted on every later call via `permitted()`, not -re-verified against anything sent with that call. Plain HTTP POST traffic -(the inherited bitcoind-style chain methods) is unaffected by any of this -- -it keeps real per-request Basic Auth headers, checked on every request, same -as bitcoind's own endpoint always did. - -**A `session_handshake` variant of this was -tried and reverted.** The idea was to model btcd's auth on -`protocol_electrum_version`'s handshake pattern: a dedicated protocol object that -gates attachment of the real one. It doesn't fit. `session_handshake` calls -`channel->resume()` immediately after the handshake protocol attaches, on the -assumption that the handshake can only complete by reading a real message -(true for electrum's `server.version`, true for p2p's version message) — but -btcd's handshake, when no credential is configured, completes without reading -anything at all. That created a real async gap between "handshake decided -it's done" and "`protocol_btcd_rpc` has actually subscribed its handlers" — -harmless for a websocket client (the upgrade round-trip masks it), but a -plain HTTP client can land a request in that gap before anything is -listening, and did, reliably, in testing. Real btcd has no such gap because -it has no handshake protocol at all: `session_server` (used here) never calls -`resume()` until *after* `attach_protocols()` has synchronously subscribed -the real handlers — the same shape bitcoind already uses, and the reason -bitcoind never had this problem. `authenticate` is just an ordinary -extension method on `protocol_btcd_rpc`, exactly matching real btcd, where it -is described as "disallowed when basic auth has already been established" — -a one-time alternative to a Basic Auth header (see the note above on how -authorization is cached, not composed, for ws traffic), not a mandatory step -every connection passes through. - -Notification/subscription state (watched blocks, watched scripts/outpoints) -follows the pattern already established in `protocol_electrum` -(`src/protocols/electrum/protocol_electrum.cpp`): a small map guarded by a -dedicated notification strand, populated by `node::chase` event callbacks, fed to -clients via `do_`/`complete_*` handlers. Model the btcd notifier on that -shape rather than inventing a new one. - -New files: - -- `include/bitcoin/server/channels/channel_btcd.hpp` -- `include/bitcoin/server/protocols/protocol_btcd_rpc.hpp` -- `include/bitcoin/server/interfaces/btcd.hpp` (method table, see - `interfaces/bitcoind_rpc.hpp` for the pattern) -- `src/protocols/btcd/protocol_btcd_rpc.cpp` -- `src/protocols/btcd/protocol_btcd_rpc_subscribe.cpp` -- `test/protocols/btcd/btcd_rpc.cpp` - -Modified: `channels.hpp`, `protocols.hpp`, `sessions.hpp`, `settings.hpp`, -`server_node.hpp`/`.cpp`, `parser.cpp` (all following the `bitcoind` wiring as a -template). - -## Method Scope - -### Inherited from `protocol_bitcoind_rpc` - -`getbestblockhash`, `getblock`, `getblockhash`, `getblockheader`, `getblockcount`, -`gettxout`, `getrawtransaction`, `sendrawtransaction`. No btcd-specific work -needed; response shapes already match Core/btcd conventions closely enough as-is. -**Bridged into the ws dispatcher**: `dispatch_websocket` tries -the btcd-only extension dispatcher first, then falls back to -`protocol_bitcoind_rpc::dispatch_rpc` when that dispatcher reports -`unexpected_method` — a pure method-name lookup miss, never conflated with an -argument-mismatch error, so the fallback cannot mask a real handler fault. -`send_rpc` (the shared sender all inherited chain handlers funnel through) -branches on `websocket()`: the post path is unchanged (still derives headers -from the cached `http::request`), while the ws path sends a minimal response -with no header enrichment — ws frames are synthesized with no real headers to -echo in the first place (`channel_http::create_request()`), matching -`protocol_btcd_rpc`'s own ws senders. A ws-connected client (required for -`authenticate`/`session`/`notifyblocks`) can now also call `getblockcount` -etc. on that same connection, as a real lnd/btcwallet client needs. - -**`getblockchaininfo`** (shared with `bitcoind`, not btcd-only) also reports a -`bip9_softforks.taproot` entry unconditionally — real btcd includes this too -(taproot is a permanent, long-since-locked-in consensus rule, no live -signaling left to track). Added specifically because lnd's -`chainreg.backendSupportsTaproot` requires this key's presence before it will -treat *any* btcd/bitcoind backend as usable. - -### btcd-only methods — implemented - -- **`authenticate`**, **`session`** — ordinary extension methods, not a - handshake stage (see [Architecture](#architecture)); `session` returns a - real per-channel id, `btcwallet` uses it to detect reconnect-to-a-fresh- - server and re-arm filters. -- **`notifyblocks`/`stopnotifyblocks`** — subscribes to - `node::chase::organized`/`reorganized`, pushes `blockconnected`/ - `blockdisconnected` (positional `[hash, height, time]`, verified against - `btcsuite/btcd/btcjson/chainsvrwsntfns.go`). -- **`getcurrentnet`** — the network-match check lnd/btcwallet performs once at - connect; returns `network_settings().identifier`, the same p2p handshake - magic real btcd returns. -- **`loadtxfilter`** + filtered block notifications - (`filteredblockconnected`/`filteredblockdisconnected`) + **`rescanblocks`** — - a per-connection in-memory watch-list (base58 p2kh/p2sh via - `wallet::payment_address`, bech32/bech32m p2wpkh/p2wsh/p2tr via - `wallet::witness_address` + `chain::script::output_pattern()`, plus - outpoints), matched by direct script inspection of each newly-connected (or, - for `rescanblocks`, explicitly named historical) block's own transactions — - not the persisted address index (`address_enabled`/`get_history`), which is - the wrong tool for "does this one block match this small watch-list" and - would needlessly require the address index to be built at all. An output - that matches a watched address has its own outpoint auto-added to the - watched set (matching real btcd's `wsClientFilter`), so a later spend of it - also matches without an explicit re-subscribe. This is the mechanism both - `btcwallet`'s `chain.RPCClient` and lnd's own `chainntnfs/btcdnotify` - depend on (see [lnd Compatibility](#lnd-compatibility)). -- **`getbestblock`** — a distinct btcd extension from `getbestblockhash`, - returning `{hash, height}` of the chain tip together. Found to be required - by `btcwallet`'s own separate rpcclient connection (not lnd's own - `chain.RPCClient`, which uses `getbestblockhash`) during wallet chain-sync - bootstrap, via a real lnd integration test. -- **`rescan`** (deprecated upstream, superseded by `rescanblocks`) — implemented - only for its empty-addresses/empty-outpoints case: real btcd's own - `handleRescan` skips scanning entirely and reports immediate completion - (`rescanfinished` notification carrying the current chain tip) when given - nothing to watch. This is the exact call `btcwallet`'s own rpcclient makes - purely to bootstrap its initial sync starting point — found via the same - real lnd integration test. A non-empty address/outpoint list remains - `not_implemented` (see [Permanently stubbed](#permanently-stubbed-not_implemented)). -- Every btcd-only method above is also reachable over plain HTTP POST, not - only the websocket connection — real lnd/btcwallet clients issue - capability-check calls (e.g. `getinfo`) this way immediately on connect, - before establishing the persistent ws connection that subscriptions need. - Found missing (a real, silent connection-killing bug) via a live lnd - integration test; fixed by adding `protocol_bitcoind_rpc:: - dispatch_extension`, the post-side mirror of the ws-side fallback described - above. -- **`getdifficulty`, `getinfo`, `getnettotals`, `getnetworkhashps`, - `createrawtransaction`, `decoderawtransaction`, `decodescript`, - `validateaddress`, `help`** — no specific named consumer; these round out - compatibility for generic btcd-speaking tooling (block explorers, - mining-pool stats, monitoring dashboards, tx-building/debug scripts), not - lnd. - -Notes on scope/fidelity (all verified against real btcd's `btcjson` -result/command structs before implementing): - -- `createrawtransaction`/`decoderawtransaction`/`decodescript` reuse existing - serializers rather than re-deriving them: `bitcoind(tx)` - (`system::chain::json::transaction`) already produces the bare - decoderawtransaction shape (`inject_tx_context` — block-context fields — - is a separate, additive call, skipped here since a standalone hex tx has no - block context); `chain::script::to_string()` already produces the "asm" - disassembly. There is no existing script-type classifier - (pubkeyhash/scripthash/multisig/etc.) or reverse address encoder anywhere - in the codebase, so `decodescript`'s `type`/`address`/`p2sh` fields and - `validateaddress` are new, built from `chain::script::output_pattern()` and - `wallet::payment_address`/`witness_address` (the same dual-parse approach - as `loadtxfilter`'s, kept as a separate small helper rather than forcing - a shared abstraction onto that already-tested code). -- `getnetworkhashps` approximates from the requested height's difficulty and - a fixed 600s target spacing, not a true windowed average over the `blocks` - parameter — there is no existing big-integer cumulative-work-over-a-range - helper to build a real windowed estimate from, and this method has no named - consumer, so the simpler approximation was chosen over adding one. -- `getnettotals`'s byte counters are not tracked by this node and are - reported as zero, matching the same honesty convention the inherited - bitcoind `getnetworkinfo` handler already uses for untracked - peer-dependent fields (rather than `not_implemented`). -- `help` lists method names only; no per-command argument usage text. - -### Permanently stubbed (`not_implemented`) - -- **`stop`** — no secure remote-shutdown path exists in libbitcoin-server; always - returns `not_implemented` regardless of auth tier or config. -- **`notifynewtransactions`**, and the deprecated WS methods `notifyreceived`, - `stopnotifyreceived`, `notifyspent`, `stopnotifyspent` (all superseded - upstream by `loadtxfilter`/`rescanblocks`) — no mempool exists in v4, revisit in - v5. -- **`rescan`'s non-empty-address/outpoint case** — a real historical block scan - (chunked `rescanprogress` notifications, per-tx `recvtx`/`redeemingtx` - notifications — a different wire shape than `filteredblockconnected`'s - per-block shape —, reorg recovery) that no observed real caller needs; - `loadtxfilter`/`rescanblocks` already cover actual address/outpoint watching - for lnd. The empty-case bootstrap call is implemented (see above). -- **`submitblock`** — not in current scope. -- Mining (`getgenerate`, `gethashespersec`, `getmininginfo`, `setgenerate`) and - peer-management (`addnode`, `getaddednodeinfo`, `getconnectioncount`, - `getpeerinfo`) — out of scope; libbitcoin-server is not a miner and peer - management is a node-config concern, not an RPC surface here. - -## lnd Compatibility - -btcd's own websocket API is one of lnd's three supported chain backends (the -others being `bitcoind` via ZMQ, and `neutrino`). Hooking an `lnd` node up to -libbitcoin-server through this endpoint means satisfying the calls -`btcwallet`'s `chain.RPCClient` (`btcsuite/btcwallet/chain/btcd.go`) and lnd's own -`chainntnfs/btcdnotify` package make against a btcd-style server. - -**Verified end-to-end against a real lnd 0.21.1-beta binary**: pointed -directly at a libbitcoin-server instance running this endpoint -(`--bitcoin.node=btcd`), lnd reaches `Chain backend is fully synced!` — -wallet opened/unlocked, chain-sync bootstrap completed, headers walked to -the live chain tip. Every gap below was found this way, not by reading -source alone; each is confirmed fixed by a subsequent clean run one step -further than the last. - -**Plain RPC** (all either inherited from bitcoind or btcd extensions): -`getbestblockhash`/`getblockcount` (chain tip), `getblockhash`, `getblockheader` -(chain-view / `IsCurrent` checks), `getblock` (verbose, block scanning), -`getrawtransaction`, `sendrawtransaction` (broadcasting funding/commitment/ -sweep/justice transactions), `gettxout` (UTXO-existence checks), `getcurrentnet` -(network-match validation performed once at connect), `getbestblock` -(`btcwallet`'s own separate rpcclient connection, used during its own -chain-sync bootstrap — distinct from `getbestblockhash`). - -**WebSocket notification surface.** Both `btcwallet`'s RPCClient -and lnd's `btcdnotify` depend on: - -- `NotifyBlocks` → `OnBlockConnected`/`OnBlockDisconnected` — implemented. -- A working `session` on connect — `btcwallet` uses the session id to detect a - reconnect to a fresh server instance, which must trigger re-registration of all - filters/rescans. Implemented for real (not stubbed), for exactly this reason. -- **`loadtxfilter`** (address/outpoint watch-list registration) plus - block-filtered notification delivery (the modern wire methods - `filteredblockconnected`/`filteredblockdisconnected`, which drive lnd's - `OnFilteredBlockConnected` client callback — verified against - `btcsuite/btcd/btcjson/chainsvrwsntfns.go` and `rpcclient/notify.go`; the - deprecated per-tx `OnRecvTx`/`OnRedeemingTx` callbacks are fed by a - different, older wire pair (`recvtx`/`redeemingtx`) that modern - `btcwallet`/`lnd` don't use) — implemented. This is the mechanism - both the wallet and lnd's own notifier use to detect channel-funding - confirmations and on-chain spends of channel outputs (breach/force-close - detection); not optional legacy behavior. -- **`rescanblocks`** — used at wallet startup (birthday scan) and after any - downtime to replay historical blocks against the loaded filter — - implemented, same match logic as live filtered notifications, - applied to explicitly named blocks instead of the newly-connected one. -- **`rescan`** (older, deprecated upstream) — `btcwallet`'s own rpcclient - still calls this once, with an empty address/outpoint list, purely to - bootstrap its initial sync starting point; implemented for that case (see - [Method Scope](#method-scope)). A general historical address/outpoint scan - remains stubbed; no observed real caller needs it. - -**Hard blocker: mempool.** Both sources confirm unconfirmed-transaction awareness -is an exercised, not merely optional, code path: `relevanttxaccepted` (the modern -wire method, driving lnd's `OnRelevantTxAccepted` callback; armed by -`notifynewtransactions`, already permanently stubbed — see above) fires for a -mempool-resident transaction matching the loaded filter, and -lnd's notifier has a dedicated mempool-spend lookup path -(`LookupInputMempoolSpend`) used for fast breach/force-close reaction ahead of -confirmation. **libbitcoin-server v4 has no mempool**, so `relevanttxaccepted` -cannot be implemented now — the confirmed-only fallback (`filteredblockconnected`) -covers the same address/outpoint watch-list, just one block -later than a mempool-aware node would, so it means lnd would lose -unconfirmed-spend awareness and any zero-conf-style UX. Final safety in Lightning -still rests on confirmation + CSV/CLTV timeout margins, so this is a -responsiveness/UX degradation rather than a correctness break, but it's a real, -user-visible limitation worth stating plainly rather than glossing over. Revisit -once a v5 mempool exists. - -**Not needed for lnd**: fee estimation — lnd's default fee estimator for the -`btcd` backend is a web API (`chainfee.WebAPIEstimator`), not an RPC call to the -node, so `estimatesmartfee`-equivalent support is not on the critical path. - -## Configuration - -New `[btcd]` section, mirroring `[bitcoind]`'s shape in `parser.cpp`/ -`settings.hpp` (`bind`, `safe`, `cert_auth`, `cert_path`, `key_path`, `key_pass`, -`credential`, `connections`, `inactivity_minutes`, -`expiration_minutes`, `minimum_buffer`, `maximum_request`, `host`, `origin`, -`allow_opaque_origin`). No dedicated `stop`-gating config key: `stop` has no -conditional path at all right now (always `not_implemented`, unconditionally), -so a flag controlling it would be dead configuration. If a real guarded `stop` -path is ever implemented, add the gating key then. - -Default port: `8334` (matches upstream btcd). - -## Code Conventions (apply from the outset, per #795 review feedback) - -The `bitcoind` PR (#795) went through several review rounds with @evoskuil before -merging; apply these proactively on the btcd PR to avoid repeating the same -cycle: - -- No inline (header-implemented) non-template method bodies — put them in - `.cpp` files; keeps build/rebuild times down for a developer-facing lib. -- Protocol-specific json/serialization helpers are protected static methods on - the base protocol class (inherited by subclasses), not free functions in the - `server` namespace. Split into a `_json.cpp` for organization if it grows. -- Pass `hash_cptr` and other small shared_ptr objects as `const&`, never by - value — a copy needlessly bumps the refcount and hits a shared lock. -- Prefer `to_shared(std::move(x))` over manual move-then-construct. -- Use `std::from_chars` (C++17) directly; add an explicit leading-zero guard if - canonical parsing matters (`from_chars` alone accepts `"01"`). -- Arithmetic: `add1()`, `floored_subtract()` — never raw `-`, the store is - concurrent and `top >= height` is not assured — `possible_sign_cast<>`, - `possible_wide_cast<>`, `is_lesser()` over raw casts, - `ceilinged_add()`/`is_overflow()` for additions that could overflow, - `std::next()` instead of pointer arithmetic. -- Cache constants that are invariant for the process lifetime (e.g. genesis - hash lookups) as function-local `static`; use `constexpr base16_hash("...")` - instead of runtime `encode_hash`/string comparisons in hot paths. -- Use the query layer's existing fluent methods instead of re-deriving values: - `get_confirmed_headers` + `get_wire_header` instead of hand-looping - `chain::header` objects, `get_tx_height(height, link)` alone instead of extra - containment checks, the stored context field (single `get_context` query) for - median-time-past instead of recomputing over 11 headers. -- Stream into one pre-sized buffer (`stream::out::fast` + a typed writer) - instead of looping and copying per item. Don't mask a per-item store failure - inside such a loop as "not found" — that's `database::error::integrity` - (`send_internal_server_error`), a different failure class. -- Naming: `send_text()`/`send_json()`/`to_data()`/`to_text()` — "data" means - byte array, "text" means base16 string; avoid "hex"/"bin" abbreviations. -- Pure type→json serialization belongs in **libbitcoin-system** as a - `chain/json/` serializer (see `system/chain/json/block.hpp`+`.cpp`+ - test), not hand-rolled long-term in the protocol layer. File a system-repo PR - if a new btcd-only json shape is needed, rather than keeping it local. -- Tests: Boost.Test only for Unit/Component/Functional tiers, no external - dependencies (no Python) — that's `test/protocols/btcd/`. Data-driven loops - only over genuine external vectors; no line-wrapping; precompute `constexpr` - hashes/expectations at file scope. The Python suite under `endpoints/` is a - separate, optional **acceptance** tier (run against a live compiled node), - not a merge requirement. diff --git a/endpoints/README.md b/endpoints/README.md index 84355dee..9a3ceb81 100644 --- a/endpoints/README.md +++ b/endpoints/README.md @@ -272,9 +272,7 @@ pytest test_bitcoind_rpc.py -k "getblock" Tests the btcd-compatible endpoint: JSON-RPC 1.0 over a persistent websocket connection (session/notification/filter/admin extension methods) plus plain http post (chain methods inherited from `protocol_bitcoind_rpc`, same shape -as `bitcoind`). See `docs/btcd-endpoint.md` for the full design, method -scope, and the lnd/btcwallet compatibility analysis (verified end-to-end -against a real lnd binary). +as `bitcoind`). Unlike the other endpoint suites, this one is explicitly split into two kinds of test: diff --git a/endpoints/test_btcd_rpc.py b/endpoints/test_btcd_rpc.py index bc73e7e1..bac51d40 100644 --- a/endpoints/test_btcd_rpc.py +++ b/endpoints/test_btcd_rpc.py @@ -4,8 +4,7 @@ btcd speaks JSON-RPC 1.0 over a persistent websocket connection (preferred, required for the session/notification extension methods) or plain HTTP POST (same request/response shape as the bitcoind endpoint, for the chain methods -inherited from protocol_bitcoind_rpc). See docs/btcd-endpoint.md for the full -design and phased scope. +inherited from protocol_bitcoind_rpc). This suite is split into what's real today and what's a development target: @@ -627,7 +626,7 @@ def test_btcd_and_chain_method_share_one_websocket_connection(conn): # ═══════════════════════════════════════════════════════════════════════════════ -# WIRED BUT NOT YET IMPLEMENTED (blocked on a v5 mempool, see docs/btcd-endpoint.md) +# WIRED BUT NOT YET IMPLEMENTED (blocked on a v5 mempool) # ═══════════════════════════════════════════════════════════════════════════════ # These methods are present in interfaces/btcd.hpp's method table today, but # every handler unconditionally returns not_implemented. Each xfail here is a @@ -675,11 +674,10 @@ def test_stop_always_not_implemented(conn): # ═══════════════════════════════════════════════════════════════════════════════ -# PHASE C: generic btcd-tooling compatibility (implemented, no lnd consumer) +# Generic btcd-tooling compatibility (implemented) # ═══════════════════════════════════════════════════════════════════════════════ -# See docs/btcd-endpoint.md for scope/fidelity notes (getnetworkhashps is an -# approximation, getnettotals's byte counters are untracked zeros, help lists -# method names only). +# getnetworkhashps is an approximation, getnettotals's byte counters are +# untracked zeros, help lists method names only. def test_getdifficulty_returns_number(conn): response = conn.send_rpc("getdifficulty") diff --git a/include/bitcoin/server/channels/channel_btcd.hpp b/include/bitcoin/server/channels/channel_btcd.hpp index f5801670..72cddcaa 100644 --- a/include/bitcoin/server/channels/channel_btcd.hpp +++ b/include/bitcoin/server/channels/channel_btcd.hpp @@ -27,6 +27,7 @@ namespace libbitcoin { namespace server { +/// Channel for btcd channels (http/ws json-rpc). class BCS_API channel_btcd : public server::channel, public network::channel_http, @@ -45,16 +46,14 @@ class BCS_API channel_btcd { } - /// True once the client has authenticated over the ws 'authenticate' - /// handshake (or basic auth was not configured, requiring none). + /// True once the in-band 'authenticate' call has succeeded. inline bool authenticated() const NOEXCEPT { return authenticated_; } - /// Latch the credential digest that satisfied the in-band 'authenticate' - /// call (see protocol_btcd_rpc::handle_authenticate), so that permitted() - /// can later apply that credential's method scoping to ws traffic. + /// Latch the credential digest satisfying 'authenticate', applying its + /// method scoping to subsequent ws traffic (see permitted). inline void set_authenticated(const system::hash_digest& digest) NOEXCEPT { authenticated_ = true; @@ -62,16 +61,8 @@ class BCS_API channel_btcd } /// True if the credential that authorized this channel permits the rpc - /// method. Plain http traffic defers entirely to channel_http's own - /// header-latched digest (unchanged from bitcoind). For ws traffic, real - /// btcd recognizes two alternative ways a connection can be authorized -- - /// basic auth already established on the ws upgrade request (also - /// channel_http's header-latched digest, since the upgrade request has - /// real headers even though subsequent ws data frames don't), or the - /// in-band 'authenticate' call (digest latched above); its own docs - /// describe 'authenticate' as "disallowed when basic auth has already - /// been established" -- i.e. an alternative to it, not an additional - /// requirement on top of it. + /// method. A ws connection is authorized by basic auth on the upgrade + /// request or by the in-band 'authenticate' call (btcd allows either). inline bool permitted(const std::string& method) const NOEXCEPT override { if (!websocket()) @@ -89,8 +80,7 @@ class BCS_API channel_btcd protected: using value_type = network::http::body::value_type; - /// Preselect the json-rpc request parser for websocket frames, so that - /// each btcd ws message is parsed the same way as an http post body. + /// Overridden to change default websocket reader to json-rpc request. inline value_type websocket_body() const NOEXCEPT override { // There is no forwarding constructor so assign and move. @@ -99,16 +89,10 @@ class BCS_API channel_btcd return value; } - /// channel_http's authorized() only latches basic-auth from the ws - /// upgrade request (the upgrade request has real headers); subsequent ws - /// data frames are synthesized and structurally cannot carry a - /// per-frame Authorization header. Deferring to the base check here - /// would therefore reject every ws frame once a credential is - /// configured, unless basic auth happened to be presented on the - /// upgrade itself -- permitted() (above) is the real per-method gate for - /// ws traffic, so this base check must never reject a ws frame on its - /// own. The plain http post path (e.g. inherited bitcoind-compatible - /// chain methods) keeps the normal per-request enforcement. + /// ws data frames cannot carry an Authorization header, so the base + /// check would reject them all once a credential is configured -- + /// permitted() is the per-method gate for ws traffic. The plain http + /// post path keeps normal per-request enforcement. inline bool authorized() const NOEXCEPT override { return websocket() ? true : network::channel_http::authorized(); diff --git a/include/bitcoin/server/interfaces/btcd.hpp b/include/bitcoin/server/interfaces/btcd.hpp index 3995849a..09b236be 100644 --- a/include/bitcoin/server/interfaces/btcd.hpp +++ b/include/bitcoin/server/interfaces/btcd.hpp @@ -1,7 +1,7 @@ -/* +/** * Copyright (c) 2011-2026 libbitcoin developers * - * This file is part of libbitcoin-server. + * This file is part of libbitcoin. * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as published by @@ -31,7 +31,7 @@ struct btcd_methods /// BTCD protocol (unversioned, json-rpc v1.0 over ws/s). static constexpr std::tuple methods { - /// Handshake (disallowed when basic auth has been established). + /// Authentication (one-time, in-band alternative to basic auth). method<"authenticate", string_t, string_t>{ "username", "password" }, /// Admin. @@ -59,10 +59,10 @@ struct btcd_methods method<"stopnotifyspent", value_t>{ "outpoints" }, method<"rescan", string_t, value_t, value_t, optional<""_t>>{ "beginblock", "addresses", "outpoints", "endblock" }, - /// Admin (network magic, checked once by btcwallet/lnd at connect). + /// Admin (network magic). method<"getcurrentnet">{}, - /// Generic btcd-tooling compatibility (no lnd consumer). + /// Generic btcd-tooling compatibility. method<"getdifficulty">{}, method<"getinfo">{}, method<"getnettotals">{}, @@ -73,9 +73,7 @@ struct btcd_methods method<"validateaddress", string_t>{ "address" }, method<"help", optional<""_t>>{ "command" }, - /// btcd extension: {hash, height} of the best chain tip, used by - /// btcwallet's own rpcclient connection (distinct from lnd's own - /// chain.RPCClient) during wallet chain-sync bootstrap. + /// btcd extension: {hash, height} of the best chain tip. method<"getbestblock">{} }; diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp index 5330ae51..eb331e00 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp @@ -62,19 +62,12 @@ class BCS_API protocol_bitcoind_rpc const post::cptr& post) NOEXCEPT override; /// Dispatch a chain-rpc message with no post-http context (websocket). - /// Caches id/version (as handle_receive_post does via set_rpc_request) - /// and notifies rpc_dispatcher_ directly; returns error::unexpected_method - /// if the message names a method this dispatcher does not recognize. - /// Callers (e.g. protocol_btcd_rpc::dispatch_websocket) use this to reach - /// the standard chain handlers (getblockcount etc.) over websocket. + /// Returns error::unexpected_method on a method-name lookup miss. code dispatch_rpc(const network::rpc::request_t& message) NOEXCEPT; - /// Extension point for a derived class's own additional dispatcher (e.g. - /// protocol_btcd_rpc's btcd_dispatcher_), tried by handle_receive_post - /// when rpc_dispatcher_ reports unexpected_method -- the post-side - /// mirror of dispatch_rpc above (ws falling back to the chain - /// dispatcher). Default: no extension dispatcher, always - /// unexpected_method. + /// Extension point for a derived class's own additional dispatcher, + /// tried by handle_receive_post when rpc_dispatcher_ reports + /// unexpected_method. Default: always unexpected_method. virtual code dispatch_extension( const network::rpc::request_t& message) NOEXCEPT; @@ -128,9 +121,7 @@ class BCS_API protocol_bitcoind_rpc double maxfeerate) NOEXCEPT; /// Serialize an object (chain::header, chain::transaction, ...) to a - /// base16 string. Template, so defined here (not *_json.cpp) -- shared - /// with derived classes (e.g. protocol_btcd_rpc's filtered-notification - /// tx serialization), not just this class's own handlers. + /// base16 string. template static std::string to_text(const Object& object, size_t size, Args&&... args) NOEXCEPT diff --git a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp index 8ba7cac6..3460a519 100644 --- a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp @@ -31,41 +31,13 @@ namespace libbitcoin { namespace server { -/// btcd-compatible json-rpc-v1 over websocket. Inherits the standard chain -/// handlers (getblockcount, getblock, etc.) from protocol_bitcoind_rpc, which -/// remain reachable both via the inherited http post path (unchanged) and, -/// bridged, over this class's ws dispatcher (dispatch_websocket falls back to -/// protocol_bitcoind_rpc::dispatch_rpc when the btcd-only dispatcher reports -/// unexpected_method). Adds the btcd-only extension methods (session/ -/// notification/filter/admin) over websocket via a second dispatcher -/// (dispatch_websocket, below). -/// -/// Attaches immediately on connect (session_btcd = session_server< -/// protocol_btcd_rpc>, no separate handshake protocol/stage) -- matching real -/// btcd's own model, which has no handshake either: HTTP Basic Auth is -/// checked synchronously per plain-http request (channel_http::authorized(), -/// unchanged from bitcoind, and re-checked on every request since ordinary -/// HTTP headers accompany each one). Over the persistent ws connection, -/// nothing is re-sent or re-checked per-message -- ws data frames -/// structurally cannot carry a per-message Authorization header at all, so -/// there is no way to compose one even if this wanted to. Instead, -/// authorization for the whole connection is established exactly once, by -/// whichever of two events happens first: real Basic Auth presented on the -/// one-time ws upgrade request (which does have real headers, latched via -/// the same channel_http mechanism as a plain http request), or the in-band -/// 'authenticate' extension method below, called once as a substitute for -/// that header. Either way the result (a credential digest) is cached on -/// channel_btcd for the connection's lifetime (see channel_btcd:: -/// set_authenticated/permitted) and consulted, not re-verified, on every -/// subsequent call. See docs/btcd-endpoint.md -/// for why an earlier session_handshake -/// design (modeled on electrum/p2p's version negotiation) was reverted: those -/// protocols' handshakes always wait for a real message to complete, but -/// btcd's has no credential-required case to wait for -- modeling it on that -/// pattern introduced an async attach gap real btcd never has, which a plain -/// http client (no upgrade round-trip to mask the gap, unlike ws) could land -/// in before this class ever subscribed its handlers. -/// +/// btcd-compatible json-rpc-v1 over http/ws. Inherits the standard chain +/// handlers (getblockcount etc.) from protocol_bitcoind_rpc and adds the +/// btcd-only extension methods via a second dispatcher. Both dispatchers are +/// bridged in both directions, so all methods are reachable over both +/// transports. Connection authorization is established once, by basic auth +/// on the ws upgrade request or by the in-band 'authenticate' method (see +/// channel_btcd::permitted). class BCS_API protocol_btcd_rpc : public server::protocol_bitcoind_rpc, protected network::tracker @@ -99,53 +71,32 @@ class BCS_API protocol_btcd_rpc protected: /// Dispatch btcd ws frames: tries the btcd-only dispatcher first, then - /// falls back to the inherited chain dispatcher (dispatch_rpc) so - /// standard methods (getblockcount etc.) are also reachable over ws, not - /// only via the inherited handle_receive_post's http post path. + /// falls back to the inherited chain dispatcher (dispatch_rpc). void dispatch_websocket( const network::http::request& request) NOEXCEPT override; - /// Post-side mirror of the above: lets handle_receive_post reach - /// btcd-only methods (e.g. getinfo) over plain http post too. Real - /// clients are not guaranteed to call these exclusively over an - /// upgraded ws connection -- confirmed via a live lnd integration test, - /// whose chain.RPCClient issues getblockchaininfo/getinfo as post-based - /// capability checks immediately on connect, before any subscription - /// traffic that actually requires ws (notifyblocks/loadtxfilter/etc). + /// Post-side mirror of the above: lets handle_receive_post reach the + /// btcd-only methods over plain http post too (real clients issue e.g. + /// getinfo as post-based capability checks before any ws traffic). code dispatch_extension( const network::rpc::request_t& message) NOEXCEPT override; - /// Handlers (session/handshake). 'authenticate' is a real, ordinary - /// extension method here (not a separate handshake stage, see class - /// comment): it hashes the given credential the same way - /// config::credential does, checks it via options_.authorized(), and on - /// success latches the digest onto channel_btcd so permitted()'s method - /// scoping can apply to subsequent ws calls. interface::btcd's method - /// table has no notion of "not yet authenticated", so this always runs - /// regardless of current state -- matches real btcd's own tolerance of a - /// repeat authenticate call (only disallowed once basic auth has already - /// been established on the ws upgrade, not enforced here since that - /// case is harmless to allow). A failed attempt ends the session - /// (dispatch_websocket sends the error before stopping), matching real - /// btcd's documented behavior of closing the connection on invalid - /// credentials rather than leaving it open for a retry. + /// Handlers (authentication/admin). A failed authenticate ends the + /// session, once the error response has reached the client (btcd closes + /// the connection on invalid credentials). bool handle_authenticate(const code& ec, btcd_interface::authenticate, const std::string& username, const std::string& password) NOEXCEPT; bool handle_session(const code& ec, btcd_interface::session) NOEXCEPT; - /// Handler (network magic; btcwallet/lnd check this once at connect to - /// confirm they're talking to the expected network). + /// Handler (network magic). bool handle_get_current_net(const code& ec, btcd_interface::get_current_net) NOEXCEPT; - /// Handler (chain tip; btcwallet's own rpcclient connection calls this - /// during wallet chain-sync bootstrap, distinct from getbestblockhash - /// which lnd's own chain.RPCClient uses). + /// Handler (chain tip {hash, height}). bool handle_get_best_block(const code& ec, btcd_interface::get_best_block) NOEXCEPT; - /// Handlers (generic btcd-tooling compatibility, no lnd consumer -- see - /// docs/btcd-endpoint.md). + /// Handlers (generic btcd-tooling compatibility). bool handle_get_difficulty(const code& ec, btcd_interface::get_difficulty) NOEXCEPT; bool handle_get_info(const code& ec, btcd_interface::get_info) NOEXCEPT; @@ -169,13 +120,8 @@ class BCS_API protocol_btcd_rpc bool handle_help(const code& ec, btcd_interface::help, const std::string& command) NOEXCEPT; - /// Address string (base58 p2kh/p2sh or bech32/bech32m p2wpkh/p2wsh/p2tr) - /// to output script, for createrawtransaction. A separate, standalone - /// helper from parse_filter_addresses (which classifies into this - /// class's watch-list hash buckets, not a reusable script) -- kept - /// duplicated rather than forcing a shared abstraction onto already- - /// tested filter-parsing code for a small amount of address-string - /// parsing logic. + /// Address string (base58 or bech32/bech32m) to output script, for + /// createrawtransaction. code parse_output_script(const std::string& text, system::chain::script& out) NOEXCEPT; @@ -191,15 +137,9 @@ class BCS_API protocol_btcd_rpc bool handle_stop_notify_new_transactions(const code& ec, btcd_interface::stop_notify_new_transactions) NOEXCEPT; - /// Handlers (address/outpoint filtering). loadtxfilter never itself - /// triggers notifications (matching real btcd) -- notifyblocks remains - /// the only thing that arms delivery; once armed, do_block_connected/ - /// do_block_disconnected send filteredblockconnected/disconnected - /// alongside the existing unfiltered blockconnected/disconnected, - /// unconditionally (an empty/never-loaded filter just yields an empty - /// subscribedtxs array, matching real btcd's own behavior). rescanblocks - /// replays the same match logic against explicitly named historical - /// blocks using the already-loaded filter. + /// Handlers (address/outpoint filtering). notifyblocks arms delivery; + /// loadtxfilter only populates the filter (as btcd). rescanblocks + /// replays the match logic against explicitly named historical blocks. bool handle_load_tx_filter(const code& ec, btcd_interface::load_tx_filter, bool reload, const network::rpc::value_t& addresses, @@ -209,20 +149,16 @@ class BCS_API protocol_btcd_rpc const network::rpc::value_t& blockhashes) NOEXCEPT; /// Filter parsing (loadtxfilter). Merges into the existing filter unless - /// reload clears it first. Returns error::invalid_argument if any address - /// fails to parse as either a base58 (p2kh/p2sh) or bech32/bech32m - /// (p2wpkh/p2wsh/p2tr) address, or any outpoint is malformed. + /// reload clears it first. code parse_filter_addresses(bool reload, const network::rpc::value_t& addresses) NOEXCEPT; code parse_filter_outpoints(bool reload, const network::rpc::value_t& outpoints) NOEXCEPT; /// Match a block's transactions against the loaded filter: an output - /// paying a watched address, or an input spending a watched outpoint. - /// A matched address-output's own outpoint is added to the watched set - /// (auto-tracking a future spend of it), matching real btcd's - /// wsClientFilter behavior. Returns the hex-encoded (witness) matched - /// transactions, in block order; empty if none matched. + /// paying a watched address, or an input spending a watched outpoint. A + /// matched output's own outpoint is added to the watched set (as btcd). + /// Returns the matched transactions as base16, in block order. network::rpc::array_t match_filtered_transactions( const system::chain::block& block) NOEXCEPT; @@ -253,12 +189,9 @@ class BCS_API protocol_btcd_rpc void do_block_connected(node::header_t link) NOEXCEPT; void do_block_disconnected(node::header_t link) NOEXCEPT; - /// Senders (btcd ws envelope, distinct id/version cache from the http - /// json-rpc-v2 senders inherited from protocol_bitcoind_rpc). The - /// close_reason overloads complete via handle_complete's (ec, reason) - /// idiom: the channel only stops (if reason is truthy) once the write - /// has actually completed, so a failed authenticate's error reaches the - /// client before the connection closes. + /// Senders (btcd envelope, distinct id/version cache from the inherited + /// senders). close_reason (if truthy) stops the channel only once the + /// write has completed, so the error reaches the client first. void send_btcd_result(network::rpc::value_option&& result, size_t size_hint) NOEXCEPT; void send_btcd_error(const code& ec) NOEXCEPT; @@ -291,13 +224,8 @@ class BCS_API protocol_btcd_rpc network::rpc::version btcd_version_{}; network::rpc::id_option btcd_id_{}; - // Tx filter (loadtxfilter), protected by strand -- populated by - // handle_load_tx_filter, read/mutated by match_filtered_transactions - // (do_block_connected/do_block_disconnected, handle_rescan_blocks), all - // of which run stranded (ws dispatch and POST_BTCD-posted chase handlers - // alike). p2kh_/p2wpkh_ share one hash set (both 20-byte hash160 payloads - // but under different script templates, kept separate so a p2kh watch - // never matches a p2wpkh output or vice versa); p2sh_/p2wsh_ likewise. + // Tx filter (loadtxfilter). Hash sets are kept per script template so a + // p2kh watch never matches a p2wpkh output or vice versa. std::unordered_set pay_key_hashes_{}; std::unordered_set pay_script_hashes_{}; std::set pay_witness_key_hash_programs_{}; diff --git a/include/bitcoin/server/sessions/sessions.hpp b/include/bitcoin/server/sessions/sessions.hpp index 5c96c7ca..b1980396 100644 --- a/include/bitcoin/server/sessions/sessions.hpp +++ b/include/bitcoin/server/sessions/sessions.hpp @@ -32,20 +32,12 @@ namespace server { using session_admin = session_server; using session_native = session_server; using session_bitcoind = session_server; +using session_btcd = session_server; using session_stratum_v1 = session_server; using session_stratum_v2 = session_server; using session_electrum = session_handshake; -// No session_handshake here: unlike electrum/p2p, btcd has no negotiation to -// wait for -- real btcd authenticates via plain HTTP Basic Auth (checked -// synchronously per request, same as bitcoind) or an in-band ws -// 'authenticate' method that is just an ordinary extension method, not a -// separate handshake stage (see protocol_btcd_rpc's class comment and -// docs/btcd-endpoint.md for why session_handshake was tried and reverted). -using session_btcd = session_server; - } // namespace server } // namespace libbitcoin @@ -83,12 +75,9 @@ server::session → node::session ╞══ session_admin = server::session_server ╞══ session_native = server::session_server ╞══ session_bitcoind = server::session_server + ╞══ session_btcd = server::session_server ╞══ session_stratum_v1 = server::session_server ╞══ session_stratum_v2 = server::session_server - ╞══ session_btcd = server::session_server - │ (no handshake stage -- real btcd has no negotiation to wait - │ for; auth is a synchronous per-request/per-connection check, - │ same shape as bitcoind, not a separate protocol) └── server::session_handshake<...Protocols> ╘══ session_electrum = server::session_handshake< protocol_electrum_version, protocol_electrum> diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index 0ef5eeb5..79856446 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -157,10 +157,8 @@ void protocol_bitcoind_rpc::handle_receive_post(const code& ec, return; } - // Dispatch the request to subscribers, falling back to a derived - // class's own extension dispatcher (e.g. protocol_btcd_rpc's btcd-only - // methods) if this dispatcher doesn't recognize the method -- the post- - // side mirror of dispatch_websocket's ws-side fallback to dispatch_rpc. + // Dispatch the request to subscribers, falling back to a derived class's + // own extension dispatcher if this one doesn't recognize the method. auto code = rpc_dispatcher_.notify(message); if (code == network::error::unexpected_method) code = dispatch_extension(message); @@ -168,11 +166,7 @@ void protocol_bitcoind_rpc::handle_receive_post(const code& ec, if (!code) return; - // Unknown to every dispatcher tried: reply with a json-rpc error and - // keep the connection alive for a subsequent request on the same - // keep-alive connection (matches dispatch_websocket's handling of the - // same case, and ordinary http semantics -- an error response body is - // not itself a reason to drop the TCP connection). + // Unknown method: reply with an error and keep the connection alive. if (code == network::error::unexpected_method) send_error(code); else @@ -185,11 +179,8 @@ code protocol_bitcoind_rpc::dispatch_extension(const request_t&) NOEXCEPT return network::error::unexpected_method; } -// Dispatch a chain-rpc message with no post-http context (websocket). Mirrors -// handle_receive_post's id/version caching (set_rpc_request) and dispatcher -// notify, without the post-specific host/origin/body checks -- those are -// meaningless for a ws frame (channel_btcd::permitted() and the ws upgrade -// itself already gate access) and are handled by the caller. +// Dispatch a chain-rpc message with no post-http context (websocket) -- the +// post-specific host/origin/body checks are meaningless for a ws frame. code protocol_bitcoind_rpc::dispatch_rpc(const request_t& message) NOEXCEPT { BC_ASSERT(stranded()); @@ -318,18 +309,10 @@ bool protocol_bitcoind_rpc::handle_get_block_chain_info(const code& ec, { "pruned", network_settings().pruned_node() }, { "warnings", std::string{} }, - // Taproot is a permanent, long-since-locked-in consensus rule (no - // live BIP9 signaling to track), so this is reported unconditionally - // rather than computed -- present specifically because real - // clients check for it: lnd's chainreg.backendSupportsTaproot - // (chainreg/taproot_check.go) requires a "taproot" entry here - // (mainnet activation height 709632, BIP9 bit 2) before it will - // treat *any* btcd/bitcoind backend as usable, checking only for - // this key's presence, not its field values. Confirmed via lnd's - // own source that "bitcoind versions before 0.19 and also btcd use - // the SoftForks fields" -- this is the real btcd-compatible path, - // not a workaround; getdeploymentinfo (lnd's fallback if this is - // absent) has no equivalent in real btcd at all. + // Taproot is a permanent consensus rule (no live signaling to + // track), so this is reported unconditionally rather than computed. + // lnd's backendSupportsTaproot requires the key's presence (not its + // field values) before treating any btcd/bitcoind backend as usable. { "bip9_softforks", object_t { { "taproot", object_t @@ -781,11 +764,8 @@ void protocol_bitcoind_rpc::send_rpc(response_t&& model, using namespace http; static const auto json = from_media_type(media_type::application_json); - // Websocket frames carry no cached post request (dispatch_rpc does not - // set one, unlike handle_receive_post) and have no per-message headers - // to echo (add_common_headers/add_access_control_headers is a post-only - // concept) -- send a minimal response, matching protocol_btcd_rpc's own - // ws senders. + // Websocket frames carry no cached post request and no per-message + // headers to echo -- send a minimal response. if (websocket()) { id_.reset(); diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd_rpc.cpp index 04f30fed..854ed169 100644 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc.cpp @@ -96,20 +96,8 @@ void protocol_btcd_rpc::stopping(const code& ec) NOEXCEPT // Websocket dispatch. // ---------------------------------------------------------------------------- -// btcd extension methods (session/notify/filter/admin, including -// authenticate) arrive as ws frames here, and are tried first. Standard -// chain methods (getblockcount etc.), inherited from protocol_bitcoind_rpc, -// remain reachable via plain http post on the same endpoint (unchanged, via -// the inherited handle_receive_post) and are also bridged into this ws -// dispatcher via dispatch_rpc, once the btcd-only dispatcher reports -// unexpected_method. -// -// A credential may be scoped to a subset of methods (see config::credential -// / channel_btcd::permitted()), so each call is checked against that scope -- -// except 'authenticate' itself, which must always reach handle_authenticate -// regardless of current auth state: permitted() requires authentication to -// already be established, so gating 'authenticate' on it would make -// authenticating impossible in the first place. +// 'authenticate' bypasses permitted(), which requires authentication to +// already be established -- gating it would make authenticating impossible. void protocol_btcd_rpc::dispatch_websocket( const network::http::request& request) NOEXCEPT @@ -137,11 +125,9 @@ void protocol_btcd_rpc::dispatch_websocket( return; } - // Try the btcd-only extension methods first, then fall back to the - // standard chain handlers inherited from protocol_bitcoind_rpc (dispatch_ - // rpc), so a single ws connection can reach both. dispatcher::notify only - // returns unexpected_method for a method-name lookup miss (never for an - // argument mismatch), so this fallback cannot mask a real handler error. + // Fall back to the inherited chain dispatcher on a method-name lookup + // miss (notify never returns unexpected_method for an argument mismatch, + // so the fallback cannot mask a real handler error). auto code = btcd_dispatcher_.notify(message); if (code == network::error::unexpected_method) code = dispatch_rpc(message); @@ -149,9 +135,7 @@ void protocol_btcd_rpc::dispatch_websocket( if (!code) return; - // Unknown method: reply with a json-rpc error and keep the ws connection - // alive (matches real btcd behavior, and the same guard already applied - // to protocol_bitcoind_rpc::handle_receive_post for its own dispatcher). + // Unknown method: reply with an error and keep the connection alive. if (code == network::error::unexpected_method) send_btcd_error(code); else @@ -159,10 +143,7 @@ void protocol_btcd_rpc::dispatch_websocket( } // Post-side mirror of the above: reached from the inherited -// handle_receive_post when rpc_dispatcher_ reports unexpected_method, so -// btcd-only methods are also callable over plain http post (see class -// comment -- real lnd/btcwallet issue capability-check calls, e.g. -// getinfo, this way before any ws-only subscription traffic). +// handle_receive_post, so btcd-only methods are also callable over post. code protocol_btcd_rpc::dispatch_extension( const network::rpc::request_t& message) NOEXCEPT { @@ -172,7 +153,7 @@ code protocol_btcd_rpc::dispatch_extension( return btcd_dispatcher_.notify(message); } -// Handlers (session/handshake). +// Handlers (authentication/admin). // ---------------------------------------------------------------------------- bool protocol_btcd_rpc::handle_authenticate(const code& ec, @@ -182,10 +163,7 @@ bool protocol_btcd_rpc::handle_authenticate(const code& ec, if (stopped(ec)) return false; - // No credential configured: nothing to check against, so a no-op - // success (matches real btcd's own no-auth-required mode -- there is no - // "wrong" answer when authorization was never required in the first - // place). + // No credential configured: nothing to check against, no-op success. if (!options_.authorize()) { send_btcd_result({}, 4); @@ -204,9 +182,8 @@ bool protocol_btcd_rpc::handle_authenticate(const code& ec, return true; } - // A failed authenticate ends the session (matches real btcd: invalid - // credentials close the connection rather than leave it open for a - // retry) -- but only once the error has actually reached the client. + // A failed authenticate ends the session (as btcd), once the error has + // reached the client. const code unauthorized{ network::error::unauthorized }; send_btcd_error(unauthorized, two * unauthorized.message().size(), unauthorized); @@ -220,7 +197,7 @@ bool protocol_btcd_rpc::handle_session(const code& ec, return false; // The channel identifier is unique per connection for the process - // lifetime, satisfying btcd's reconnect-detection use of session ids. + // lifetime, satisfying btcwallet's reconnect-detection use of session ids. object_t result{}; result.emplace("id", value_t{ possible_sign_cast(identifier()) }); send_btcd_result(value_t{ std::move(result) }, 32); @@ -233,10 +210,8 @@ bool protocol_btcd_rpc::handle_get_current_net(const code& ec, if (stopped(ec)) return false; - // network_settings().identifier is the p2p handshake magic (e.g. - // 3652501241 / 0xd9b4bef9 for mainnet) -- the same value real btcd - // returns from getcurrentnet, verified once by btcwallet/lnd at connect - // to confirm they're talking to the expected network. + // The p2p handshake magic, the same value btcd returns (checked once by + // btcwallet/lnd at connect to confirm the expected network). send_btcd_result( value_t{ possible_sign_cast(network_settings().identifier) }, 20); @@ -249,10 +224,7 @@ bool protocol_btcd_rpc::handle_get_best_block(const code& ec, if (stopped(ec)) return false; - // btcd extension distinct from getbestblockhash: returns hash and height - // together. Found to be required by btcwallet's own rpcclient connection - // (separate from lnd's own chain.RPCClient) during wallet chain-sync - // bootstrap -- a real lnd integration test hangs here without it. + // Required by btcwallet during wallet chain-sync bootstrap. const auto& query = archive(); object_t result{}; result.emplace("hash", value_t{ encode_hash(query.get_top_confirmed_hash()) }); @@ -365,17 +337,11 @@ bool protocol_btcd_rpc::handle_rescan(const code& ec, { if (stopped(ec)) return false; - // Minimal implementation of this deprecated method: real btcd's own - // handleRescan (rpcwebsocket.go) skips scanning entirely and reports - // immediate completion when given no addresses/outpoints to watch (" - // Skipping rescan as client has no addrs/utxos") -- exactly the call - // btcwallet's own rpcclient makes purely to bootstrap its initial - // sync starting point, confirmed via a real lnd integration test. A - // non-empty address/outpoint list would require a real historical - // block scan (recvtx/redeemingtx notifications, chunked progress, - // reorg recovery) -- deliberately left not_implemented; no observed - // real caller needs it (loadtxfilter/rescanblocks already cover actual - // address/outpoint watching for lnd). + // Deprecated method, implemented only for the empty addrs/outpoints + // case (btcd skips scanning and reports immediate completion) -- the + // call btcwallet makes to bootstrap its initial sync starting point. A + // non-empty list would require a real historical scan; no observed + // caller needs it (loadtxfilter/rescanblocks cover actual watching). hash_digest begin_hash{}; if (!decode_hash(begin_hash, beginblock)) { @@ -402,9 +368,7 @@ bool protocol_btcd_rpc::handle_rescan(const code& ec, } // Nothing to watch: report the current chain tip as already-finished, - // ignoring beginblock (real btcd does the same -- the finished - // notification always carries chain.BestSnapshot(), not the requested - // beginblock, in this branch). + // ignoring beginblock (as btcd). const auto top = query.get_top_confirmed(); const auto header = query.get_header(query.to_confirmed(top)); if (!header) @@ -413,11 +377,7 @@ bool protocol_btcd_rpc::handle_rescan(const code& ec, return true; } - // Reply to the rescan call itself before the separate, unprompted - // notification -- unlike do_block_connected's pushes (which never race - // a pending reply), this handler owns both writes in the same call, so - // ordering them ordinary-reply-first-then-push keeps a single request/ - // response exchange recognizable as such on the wire. + // Reply to the rescan call itself before the unprompted notification. send_btcd_result({}, 4); array_t params{}; @@ -482,9 +442,7 @@ void protocol_btcd_rpc::do_block_connected(node::header_t link_value) NOEXCEPT if (!header) return; - // btcd 'blockconnected' notification: [hash, height, time] (positional, - // matching btcjson.BlockConnectedNtfn -- the unfiltered notification - // fired by notifyblocks, distinct from loadtxfilter's filtered variant). + // btcd 'blockconnected' notification: [hash, height, time]. array_t params{}; params.emplace_back(value_t{ encode_hash(query.get_header_key(link)) }); params.emplace_back(value_t{ possible_sign_cast(height) }); @@ -493,12 +451,8 @@ void protocol_btcd_rpc::do_block_connected(node::header_t link_value) NOEXCEPT send_btcd_notification("blockconnected", std::move(params), 256); - // btcd 'filteredblockconnected': [height, header, subscribedtxs] - // (matching btcjson.FilteredBlockConnectedNtfn). Sent unconditionally - // alongside blockconnected, same as real btcd (verified against - // rpcwebsocket.go's notificationHandler: both notifyBlockConnected and - // notifyFilteredBlockConnected fire for every notifyblocks client - // regardless of whether a filter was ever loaded) -- an empty/never- + // btcd 'filteredblockconnected': [height, header, subscribedtxs]. Sent + // unconditionally alongside blockconnected (as btcd) -- an empty/never- // loaded filter just yields an empty subscribedtxs array. constexpr auto witness = true; const auto block = query.get_block(link, witness); @@ -543,8 +497,7 @@ void protocol_btcd_rpc::do_block_disconnected( send_btcd_notification("blockdisconnected", std::move(params), 256); - // btcd 'filteredblockdisconnected': [height, header] (no subscribedtxs - // field -- matching btcjson.FilteredBlockDisconnectedNtfn). + // btcd 'filteredblockdisconnected': [height, header]. array_t filtered_params{}; filtered_params.emplace_back(value_t{ possible_sign_cast(height) }); @@ -615,8 +568,7 @@ void protocol_btcd_rpc::send_btcd_notification(const std::string& method, message.body() = std::move(notification); message.prepare_payload(); - // NOTIFY does not restart the reader (the ws reader is already active, - // independent of this unprompted push). + // NOTIFY does not restart the (already active) ws reader. NOTIFY(std::move(message), handle_complete, _1, error::success); } @@ -639,9 +591,8 @@ void protocol_btcd_rpc::send_btcd_rpc(response_t&& model, size_t size_hint, }; message.prepare_payload(); - // handle_complete only stops (if close_reason is truthy) once this write - // has actually completed -- SEND still restarts the ws reader on success, - // matching the base behavior for every other response this class sends. + // handle_complete stops (if close_reason is truthy) only once the write + // has completed; SEND restarts the ws reader on success. SEND(std::move(message), handle_complete, _1, close_reason); } diff --git a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp index b5d78a5e..fbd05779 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp @@ -37,10 +37,6 @@ BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) // Filter parsing (loadtxfilter). // ---------------------------------------------------------------------------- -// Addresses/outpoints arrive as generic value_t (not pre-typed by the -// dispatcher, unlike single-valued params), so each is unpacked here via the -// same std::holds_alternative/std::get idiom the dispatcher itself uses -// internally (see network::rpc::dispatcher::get_array/get_object). code protocol_btcd_rpc::parse_filter_addresses(bool reload, const value_t& addresses) NOEXCEPT @@ -222,15 +218,10 @@ bool protocol_btcd_rpc::handle_rescan_blocks(const code& ec, // Filter matching. // ---------------------------------------------------------------------------- -// Manual per-block script inspection, not the persisted address index -// (get_history/address_enabled etc.) -- that index is for whole-chain -// historical lookups by address, the wrong tool for "does this one just- -// connected (or explicitly named, for rescanblocks) block match this small -// in-memory watch-list", and would require the address index to be enabled -// at all. Mirrors real btcd's own per-client wsClientFilter (rpcwebsocket.go: -// notifyForTx/subscribedClients), including auto-tracking: an output that -// matches a watched address has its own outpoint added to filter_outpoints_, -// so a later spend of it also matches without an explicit re-subscribe. +// Manual per-block script inspection, not the persisted address index -- +// that index is for whole-chain lookups, the wrong tool for matching one +// block against a small in-memory watch-list, and would require the index +// to be enabled. Mirrors btcd's per-client wsClientFilter. array_t protocol_btcd_rpc::match_filtered_transactions( const chain::block& block) NOEXCEPT diff --git a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp b/src/protocols/btcd/protocol_btcd_rpc_utility.cpp index 548b8013..cee982e3 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc_utility.cpp @@ -37,7 +37,7 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) -// Generic btcd-tooling compatibility (no lnd consumer). +// Generic btcd-tooling compatibility. // ---------------------------------------------------------------------------- bool protocol_btcd_rpc::handle_get_difficulty(const code& ec, @@ -74,10 +74,8 @@ bool protocol_btcd_rpc::handle_get_info(const code& ec, return true; } - // Peer-dependent fields (connections) and errors are reported as - // empty/zero -- libbitcoin-server is not a peer-introspection service, - // matching the same honesty convention already used by the inherited - // bitcoind getnetworkinfo handler. + // Untracked peer-dependent fields are reported as empty/zero, matching + // the inherited getnetworkinfo handler's convention. send_btcd_result(object_t { { "version", 0 }, @@ -100,9 +98,7 @@ bool protocol_btcd_rpc::handle_get_net_totals(const code& ec, if (stopped(ec)) return false; - // Byte counters are not tracked by this node; reported as zero rather - // than not_implemented (matches getnetworkinfo's own convention for - // untracked peer-dependent fields). timemillis is real (current time). + // Untracked byte counters are reported as zero; timemillis is real. send_btcd_result(object_t { { "totalbytesrecv", 0 }, @@ -119,10 +115,7 @@ bool protocol_btcd_rpc::handle_get_network_hash_ps(const code& ec, return false; // Approximated from the requested height's difficulty and the standard - // 600s target spacing, not a windowed average over 'blocks' -- there is - // no existing big-integer cumulative-work-over-a-range helper in this - // codebase to build a real windowed estimate from, and this is generic- - // tooling scope (no lnd consumer), not consensus-critical. + // 600s target spacing, not a windowed average over 'blocks'. const auto& query = archive(); const auto top = query.get_top_confirmed(); const auto target = (height < 0) ? top : @@ -145,7 +138,6 @@ bool protocol_btcd_rpc::handle_get_network_hash_ps(const code& ec, // Raw transaction/script utilities. // ---------------------------------------------------------------------------- -// A separate, standalone helper from parse_filter_addresses (see header). code protocol_btcd_rpc::parse_output_script(const std::string& text, chain::script& out) NOEXCEPT { @@ -261,9 +253,8 @@ bool protocol_btcd_rpc::handle_decode_raw_transaction(const code& ec, return true; } - // bitcoind(tx) alone (no inject_tx_context) is exactly the bare - // decoderawtransaction shape -- no block-context fields to add for a - // standalone, possibly-unbroadcast transaction. + // bitcoind(tx) alone (no inject_tx_context) is the bare + // decoderawtransaction shape -- a standalone tx has no block context. send_btcd_result(value_from(bitcoind(tx)), two * tx.serialized_size(witness)); return true; @@ -335,9 +326,7 @@ bool protocol_btcd_rpc::handle_decode_script(const code& ec, result.emplace("address", value_t{ address.encoded() }); } - // The p2sh-wrapping address of this exact script (i.e. the address that - // pays a redeem script equal to this one), regardless of this script's - // own pattern -- matches real btcd's "p2sh" field. + // The p2sh-wrapping address of this exact script (btcd's "p2sh" field). const wallet::payment_address wrapped{ script, p2sh_ }; if (wrapped) result.emplace("p2sh", value_t{ wrapped.encoded() }); @@ -392,11 +381,7 @@ bool protocol_btcd_rpc::handle_help(const code& ec, btcd_interface::help, if (stopped(ec)) return false; - // Minimal, generic-tooling-only implementation: no specific consumer - // (see docs/btcd-endpoint.md), so this lists method names rather than - // building full per-command argument usage text. Chain methods - // (getblockcount etc., inherited from protocol_bitcoind_rpc) are - // documented by bitcoind's own docs, unchanged. + // Lists method names only, no per-command argument usage text. send_btcd_result(std::string{ "authenticate session getcurrentnet getbestblock notifyblocks " "stopnotifyblocks loadtxfilter rescanblocks getdifficulty getinfo " diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 61850ee8..632bb41f 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -126,13 +126,8 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__ten_block_store__expected) BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__bip9_softforks_taproot__present) { - // lnd's chainreg.backendSupportsTaproot (chainreg/taproot_check.go) - // requires this exact key to treat any btcd/bitcoind backend as usable - // -- it checks only for the "taproot" key's presence, not its field - // values (verified against lnd's own source, real lnd integration - // test, 2026-07-30). Real btcd's own getblockchaininfo response - // includes this too (per lnd's own source comment), so this is shared, - // correct behavior for both endpoints, not a btcd-only workaround. + // lnd's backendSupportsTaproot requires this key's presence to treat + // any btcd/bitcoind backend as usable. const auto response = rpc("getblockchaininfo"); const auto& result = response.at("result"); BOOST_REQUIRE(result.as_object().contains("bip9_softforks")); diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index 548871a2..960d43b5 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -67,9 +67,7 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__session__returns_id) BOOST_AUTO_TEST_CASE(btcd_rpc__getcurrentnet__mainnet_magic) { - // btcd_setup_fixture configures system::chain::selection::mainnet, whose - // network_settings().identifier (p2p handshake magic) is 3652501241 - // (0xd9b4bef9) -- the same value real btcd returns from getcurrentnet. + // The fixture configures mainnet, magic 3652501241 (0xd9b4bef9). const auto response = rpc("getcurrentnet"); BOOST_REQUIRE(!has_error(response)); BOOST_REQUIRE(has_result(response)); @@ -78,9 +76,6 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__getcurrentnet__mainnet_magic) BOOST_AUTO_TEST_CASE(btcd_rpc__getbestblock__ten_block_store__block9) { - // btcd extension distinct from getbestblockhash: {hash, height} together. - // Required by btcwallet's own rpcclient connection (separate from lnd's - // own chain.RPCClient) during wallet chain-sync bootstrap. const auto response = rpc("getbestblock"); BOOST_REQUIRE(!has_error(response)); BOOST_REQUIRE(has_result(response)); @@ -92,14 +87,8 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__getbestblock__ten_block_store__block9) BOOST_AUTO_TEST_CASE(btcd_rpc__btcd_only_method__reachable_over_http_post) { - // A real lnd integration test found that btcwallet's chain.RPCClient - // issues capability-check calls (getblockchaininfo, then getinfo) over - // plain http post immediately on connect, before any subscription - // traffic that requires ws. getinfo is btcd-only (not in the inherited - // rpc_dispatcher_), so this exercises protocol_bitcoind_rpc:: - // dispatch_extension (protocol_btcd_rpc's override tries - // btcd_dispatcher_) -- the post-side mirror of dispatch_websocket's - // existing ws-side fallback to the inherited chain dispatcher. + // getinfo is btcd-only, so this exercises dispatch_extension (real + // clients issue post-based capability checks before any ws traffic). const auto response = http_rpc("getinfo"); BOOST_REQUIRE(!has_error(response)); BOOST_REQUIRE(has_result(response)); @@ -125,16 +114,9 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyblocks__returns_null_result) // Standard chain methods (bridged into the ws dispatcher) // ---------------------------------------------------------------------------- -// Inherited from protocol_bitcoind_rpc, reachable both via plain http post on -// the same endpoint (see test/protocols/bitcoind) and, bridged, over this ws -// connection (dispatch_websocket falls back to dispatch_rpc on -// unexpected_method from the btcd-only dispatcher). BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__ten_block_store__nine) { - // btcd_ten_block_setup_fixture populates a ten-block confirmed store - // (genesis + 9), so top confirmed height is 9 (matches the equivalent - // bitcoind_rpc__getblockcount__ten_block_store__nine test). const auto response = rpc("getblockcount"); BOOST_REQUIRE(!has_error(response)); BOOST_REQUIRE(has_result(response)); @@ -144,11 +126,9 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__ten_block_store__nine) // Address/outpoint filtering (loadtxfilter, filteredblockconnected/ // disconnected, rescanblocks). // ---------------------------------------------------------------------------- -// blocks 1-9 (btcd_ten_block_setup_fixture) are real early mainnet blocks, -// each a single p2pk coinbase with no inter-block spends (first real tx in -// mainnet history isn't until block 170), so none of them can exercise -// loadtxfilter's actual address matching -- block10() below (a controlled -// p2kh output chained after block9) is built for that purpose. +// blocks 1-9 are p2pk coinbases with no inter-block spends, so they cannot +// exercise address matching -- block10() (a controlled p2kh output chained +// after block9) is built for that purpose. namespace { @@ -194,10 +174,8 @@ const chain::block& block10() NOEXCEPT 0 }; - // query.set()/push_confirmed() are raw persistence calls (no - // organize-time consensus validation), so an internally- - // consistent merkle root is not required for this fixture -- - // chain::block::generate_merkle_root() is private besides. + // Raw persistence (no organize-time validation), so a consistent + // merkle root is not required. return chain::block { chain::header @@ -322,12 +300,6 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__unknown_beginblock__not_found) BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__no_addrs_no_outpoints__finishes_immediately) { - // Minimal implementation of this deprecated method: matches real btcd's - // own "skip scanning, report immediate completion" branch for an empty - // addr/outpoint list -- the exact call btcwallet's own rpcclient makes - // to bootstrap its initial sync starting point (found via a real lnd - // integration test), distinct from rescanblocks/loadtxfilter's actual - // address-watching path. const auto response = rpc("rescan", "[\"" + block9 + "\",[],[],\"\"]"); BOOST_REQUIRE(!has_error(response)); @@ -342,15 +314,12 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__no_addrs_no_outpoints__finishes_immediate BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__with_addresses__not_implemented) { - // A real historical address/outpoint scan (recvtx/redeemingtx - // notifications, chunked progress, reorg recovery) is deliberately not - // implemented -- no observed real caller needs it. const auto response = rpc("rescan", "[\"" + block9 + "\",[\"" + filter_test_address_text() + "\"],[],\"\"]"); BOOST_REQUIRE(has_error(response)); } -// Generic btcd-tooling compatibility (no lnd consumer) +// Generic btcd-tooling compatibility // ---------------------------------------------------------------------------- BOOST_AUTO_TEST_CASE(btcd_rpc__getdifficulty__returns_number) diff --git a/test/protocols/btcd/btcd_setup_fixture.hpp b/test/protocols/btcd/btcd_setup_fixture.hpp index bc9d080b..f2f47113 100644 --- a/test/protocols/btcd/btcd_setup_fixture.hpp +++ b/test/protocols/btcd/btcd_setup_fixture.hpp @@ -31,10 +31,8 @@ struct btcd_setup_fixture DELETE_COPY_MOVE(btcd_setup_fixture); // username/password configure a single btcd.credential entry (no auth - // required if username is empty), matching the one-tier credential model. - // methods, if non-empty, scopes that credential to a comma-separated - // method list (config::credential's own "user:pass:method,..." syntax); - // empty (the default) leaves the credential unscoped (all methods). + // required if username is empty); methods, if non-empty, scopes it to a + // comma-separated method list ("user:pass:method,..." syntax). explicit btcd_setup_fixture(const initializer& setup, std::string_view username={}, std::string_view password={}, std::string_view methods={}); @@ -47,9 +45,7 @@ struct btcd_setup_fixture std::string_view params = "[]"); // JSON-RPC 2.0 over plain HTTP POST to "/" (a separate connection from - // the ws one rpc() uses) -- same transport the inherited chain methods - // use, and the one a real btcd/lnd client's capability-check calls - // (e.g. getinfo) go over before any ws-only subscription traffic. + // the ws one rpc() uses). boost::json::value http_rpc(std::string_view method, std::string_view params = "[]"); From d3ef458e65605b1bbec33bc3c0c92616f2799373 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Tue, 4 Aug 2026 23:41:29 -0400 Subject: [PATCH 03/29] btcd: test style and completeness. --- test/protocols/btcd/btcd_rpc.cpp | 457 ++++++++------------- test/protocols/btcd/btcd_setup_fixture.cpp | 52 ++- test/protocols/btcd/btcd_setup_fixture.hpp | 51 ++- 3 files changed, 257 insertions(+), 303 deletions(-) diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index 960d43b5..03f2c353 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -20,96 +20,95 @@ #include "btcd_setup_fixture.hpp" using namespace system; +static const code not_found{ server::error::not_found }; +static const code not_implemented{ server::error::not_implemented }; +static const code invalid_argument{ server::error::invalid_argument }; +static const code unauthorized{ network::error::unauthorized }; +static const code unexpected_method{ network::error::unexpected_method }; + +// mock_block10 chains onto block9 and pays found_address from its second +// transaction only (its other outputs pay distinct key/script hashes), so a +// filter watching found_address matches exactly one of its transactions. +static const std::string found_address{ "1BaMPFdqMUQ46BV8iRcwbVfsam57oBLMM" }; +static const std::string bogus_address{ "not-an-address" }; namespace { -bool has_error(const boost::json::value& response) NOEXCEPT -{ - return response.is_object() && response.as_object().contains("error") && - !response.at("error").is_null(); -} - -bool has_result(const boost::json::value& response) NOEXCEPT -{ - return response.is_object() && response.as_object().contains("result") && - !response.at("result").is_null(); -} +const auto block9 = encode_hash(test::block9_hash); std::string as_text(const boost::json::value& value) NOEXCEPT { return { value.as_string().c_str() }; } -const auto block9 = encode_hash(test::block9_hash); +std::string coinbase_txid(const chain::block& block) NOEXCEPT +{ + return encode_hash(block.transactions_ptr()->front()->hash(false)); +} } // namespace -BOOST_FIXTURE_TEST_SUITE(btcd_rpc_tests, btcd_ten_block_setup_fixture) +BOOST_FIXTURE_TEST_SUITE(btcd_tests, btcd_ten_block_setup_fixture) // session management // ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__no_auth_configured__null_result) +BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__no_credential_configured__null_result) { - // Without basic auth configured, authenticate is a no-op success. + // Without a credential configured, authenticate is a no-op success. const auto response = rpc("authenticate", R"(["user","pass"])"); - BOOST_REQUIRE(!has_error(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); } -BOOST_AUTO_TEST_CASE(btcd_rpc__session__returns_id) +BOOST_AUTO_TEST_CASE(btcd_rpc__session__default__returns_id) { const auto response = rpc("session"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - BOOST_REQUIRE(response.at("result").is_object()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); BOOST_REQUIRE(response.at("result").as_object().contains("id")); } -BOOST_AUTO_TEST_CASE(btcd_rpc__getcurrentnet__mainnet_magic) +BOOST_AUTO_TEST_CASE(btcd_rpc__getcurrentnet__mainnet__magic) { // The fixture configures mainnet, magic 3652501241 (0xd9b4bef9). const auto response = rpc("getcurrentnet"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 3652501241); } BOOST_AUTO_TEST_CASE(btcd_rpc__getbestblock__ten_block_store__block9) { const auto response = rpc("getbestblock"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); const auto& result = response.at("result"); BOOST_REQUIRE_EQUAL(as_text(result.at("hash")), block9); BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); } -BOOST_AUTO_TEST_CASE(btcd_rpc__btcd_only_method__reachable_over_http_post) +BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__http_post__dispatched) { // getinfo is btcd-only, so this exercises dispatch_extension (real // clients issue post-based capability checks before any ws traffic). const auto response = http_rpc("getinfo"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - BOOST_REQUIRE(response.at("result").as_object().contains("blocks")); + REQUIRE_NO_THROW_TRUE(response.at("result").as_object().contains("blocks")); } // block subscription // ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(btcd_rpc__notifyblocks__returns_null_result) +BOOST_AUTO_TEST_CASE(btcd_rpc__notifyblocks__default__null_result) { const auto response = rpc("notifyblocks"); - BOOST_REQUIRE(!has_error(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); } -BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyblocks__returns_null_result) +BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyblocks__subscribed__null_result) { // Subscribe first so stop is meaningful. - rpc("notifyblocks"); + const auto subscribed = rpc("notifyblocks"); + REQUIRE_NO_THROW_TRUE(subscribed.at("result").is_null()); + const auto response = rpc("stopnotifyblocks"); - BOOST_REQUIRE(!has_error(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); } // Standard chain methods (bridged into the ws dispatcher) @@ -118,8 +117,6 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyblocks__returns_null_result) BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__ten_block_store__nine) { const auto response = rpc("getblockcount"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); } @@ -127,163 +124,81 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__ten_block_store__nine) // disconnected, rescanblocks). // ---------------------------------------------------------------------------- // blocks 1-9 are p2pk coinbases with no inter-block spends, so they cannot -// exercise address matching -- block10() (a controlled p2kh output chained -// after block9) is built for that purpose. - -namespace { - -const short_hash& filter_test_hash() NOEXCEPT -{ - static const short_hash hash{ base16_array( - "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a") }; - return hash; -} - -const wallet::payment_address& filter_test_address() NOEXCEPT -{ - static const wallet::payment_address address{ filter_test_hash() }; - return address; -} - -const std::string& filter_test_address_text() NOEXCEPT -{ - static const std::string text{ filter_test_address().encoded() }; - return text; -} - -const chain::block& block10() NOEXCEPT -{ - static const chain::block instance - { - [&]() NOEXCEPT - { - using namespace wallet; - const chain::transaction coinbase - { - 1, - chain::inputs{ chain::input - { - chain::point{}, chain::script{}, 0xffffffff - } }, - chain::outputs{ chain::output - { - 50'0000'0000, filter_test_address().output_script( - payment_address::mainnet_p2kh, - payment_address::mainnet_p2sh) - } }, - 0 - }; - - // Raw persistence (no organize-time validation), so a consistent - // merkle root is not required. - return chain::block - { - chain::header - { - 1, test::block9_hash, null_hash, - test::block9.header().timestamp() + 600, - test::block9.header().bits(), 0 - }, - chain::transactions{ coinbase } - }; - }() - }; - - return instance; -} - -} // namespace +// exercise address matching -- mock_block10 is added where a match is required. -BOOST_AUTO_TEST_CASE(btcd_rpc__loadtxfilter__valid_address__acknowledges) +BOOST_AUTO_TEST_CASE(btcd_rpc__loadtxfilter__valid_address__null_result) { - const auto response = rpc("loadtxfilter", - "[true,[\"" + filter_test_address_text() + "\"],[]]"); - BOOST_REQUIRE(!has_error(response)); + const auto response = rpc("loadtxfilter", (boost_format(R"([true,["%1%"],[]])") % found_address).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); } BOOST_AUTO_TEST_CASE(btcd_rpc__loadtxfilter__invalid_address__invalid_argument) { - const auto response = rpc("loadtxfilter", - R"([true,["not-an-address"],[]])"); - BOOST_REQUIRE(has_error(response)); + const auto result = rpc_error("loadtxfilter", (boost_format(R"([true,["%1%"],[]])") % bogus_address).str()); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); } -BOOST_AUTO_TEST_CASE(btcd_rpc__loadtxfilter__valid_outpoint__acknowledges) +BOOST_AUTO_TEST_CASE(btcd_rpc__loadtxfilter__valid_outpoint__null_result) { - const auto txid = encode_hash( - test::block1.transactions_ptr()->front()->hash(false)); - const auto response = rpc("loadtxfilter", "[true,[],[{\"hash\":\"" + - txid + "\",\"index\":0}]]"); - BOOST_REQUIRE(!has_error(response)); + const auto request = R"([true,[],[{"hash":"%1%","index":0}]])"; + const auto response = rpc("loadtxfilter", (boost_format(request) % coinbase_txid(test::block1)).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); } BOOST_AUTO_TEST_CASE(btcd_rpc__loadtxfilter__malformed_outpoint__invalid_argument) { - const auto response = rpc("loadtxfilter", R"([true,[],[{"hash":"00"}]])"); - BOOST_REQUIRE(has_error(response)); + const auto result = rpc_error("loadtxfilter", R"([true,[],[{"hash":"00"}]])"); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); } BOOST_AUTO_TEST_CASE(btcd_rpc__rescanblocks__unknown_hash__not_found) { // 'blockhashes' is one positional arg that is itself an array, so the // wire params need double-wrapping: [[...]], not [...]. - const auto response = rpc("rescanblocks", "[[\"" + encode_hash(null_hash) + - "\"]]"); - BOOST_REQUIRE(has_error(response)); + const auto result = rpc_error("rescanblocks", (boost_format(R"([["%1%"]])") % encode_hash(null_hash)).str()); + BOOST_REQUIRE_EQUAL(result, not_found.value()); } BOOST_AUTO_TEST_CASE(btcd_rpc__rescanblocks__no_filter_match__empty_result) { - rpc("loadtxfilter", "[true,[\"" + filter_test_address_text() + "\"],[]]"); + rpc("loadtxfilter", (boost_format(R"([true,["%1%"],[]])") % found_address).str()); - const auto response = rpc("rescanblocks", - "[[\"" + encode_hash(test::block1_hash) + "\"]]"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - BOOST_REQUIRE(response.at("result").as_array().empty()); + const auto response = rpc("rescanblocks", (boost_format(R"([["%1%"]])") % encode_hash(test::block1_hash)).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").as_array().empty()); } -BOOST_AUTO_TEST_CASE(btcd_rpc__rescanblocks__address_match__returns_matched_tx) +BOOST_AUTO_TEST_CASE(btcd_rpc__rescanblocks__address_match__matched_transaction) { - BOOST_REQUIRE(query_.set(block10(), database::context{ 0, 10, 0 }, false, - false)); - BOOST_REQUIRE(query_.push_confirmed(query_.to_header(block10().hash()), - true)); + const auto block10 = encode_hash(test::mock_block10.hash()); + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); - rpc("loadtxfilter", "[true,[\"" + filter_test_address_text() + "\"],[]]"); + rpc("loadtxfilter", (boost_format(R"([true,["%1%"],[]])") % found_address).str()); - const auto response = rpc("rescanblocks", - "[[\"" + encode_hash(block10().hash()) + "\"]]"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); + const auto response = rpc("rescanblocks", (boost_format(R"([["%1%"]])") % block10).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_array()); const auto& result = response.at("result").as_array(); BOOST_REQUIRE_EQUAL(result.size(), 1u); - BOOST_REQUIRE_EQUAL(result.front().at("hash").as_string(), - encode_hash(block10().hash())); - BOOST_REQUIRE_EQUAL(result.front().at("transactions").as_array().size(), - 1u); + BOOST_REQUIRE_EQUAL(as_text(result.front().at("hash")), block10); + BOOST_REQUIRE_EQUAL(result.front().at("transactions").as_array().size(), 1u); } BOOST_AUTO_TEST_CASE(btcd_rpc__filteredblockconnected__address_match__delivered) { rpc("notifyblocks"); - rpc("loadtxfilter", "[true,[\"" + filter_test_address_text() + "\"],[]]"); + rpc("loadtxfilter", (boost_format(R"([true,["%1%"],[]])") % found_address).str()); - BOOST_REQUIRE(query_.set(block10(), database::context{ 0, 10, 0 }, false, - false)); - BOOST_REQUIRE(query_.push_confirmed(query_.to_header(block10().hash()), - true)); + BOOST_REQUIRE(query_.set(test::mock_block10, database::context{ 0, 10, 0 }, false, false)); + BOOST_REQUIRE(query_.push_confirmed(query_.to_header(test::mock_block10.hash()), true)); notify(node::chase::organized, { 10_u32 }); const auto blockconnected = receive_notification(); - BOOST_REQUIRE_EQUAL(blockconnected.at("method").as_string(), - "blockconnected"); + BOOST_REQUIRE_EQUAL(as_text(blockconnected.at("method")), "blockconnected"); const auto filtered = receive_notification(); - BOOST_REQUIRE_EQUAL(filtered.at("method").as_string(), - "filteredblockconnected"); + BOOST_REQUIRE_EQUAL(as_text(filtered.at("method")), "filteredblockconnected"); const auto& params = filtered.at("params").as_array(); BOOST_REQUIRE_EQUAL(params.size(), 3u); @@ -293,84 +208,75 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__filteredblockconnected__address_match__delivered) BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__unknown_beginblock__not_found) { - const auto response = rpc("rescan", - "[\"" + encode_hash(null_hash) + "\",[],[],\"\"]"); - BOOST_REQUIRE(has_error(response)); + const auto result = rpc_error("rescan", (boost_format(R"(["%1%",[],[],""])") % encode_hash(null_hash)).str()); + BOOST_REQUIRE_EQUAL(result, not_found.value()); } -BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__no_addrs_no_outpoints__finishes_immediately) +BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__no_addresses_or_outpoints__rescan_finished) { - const auto response = rpc("rescan", "[\"" + block9 + "\",[],[],\"\"]"); - BOOST_REQUIRE(!has_error(response)); + const auto response = rpc("rescan", (boost_format(R"(["%1%",[],[],""])") % block9).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); const auto finished = receive_notification(); - BOOST_REQUIRE_EQUAL(finished.at("method").as_string(), "rescanfinished"); + BOOST_REQUIRE_EQUAL(as_text(finished.at("method")), "rescanfinished"); const auto& params = finished.at("params").as_array(); BOOST_REQUIRE_EQUAL(params.size(), 3u); - BOOST_REQUIRE_EQUAL(params[0].as_string(), block9); + BOOST_REQUIRE_EQUAL(as_text(params[0]), block9); BOOST_REQUIRE_EQUAL(params[1].as_int64(), 9); } BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__with_addresses__not_implemented) { - const auto response = rpc("rescan", - "[\"" + block9 + "\",[\"" + filter_test_address_text() + "\"],[],\"\"]"); - BOOST_REQUIRE(has_error(response)); + const auto request = R"(["%1%",["%2%"],[],""])"; + const auto result = rpc_error("rescan", (boost_format(request) % block9 % found_address).str()); + BOOST_REQUIRE_EQUAL(result, not_implemented.value()); } // Generic btcd-tooling compatibility // ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(btcd_rpc__getdifficulty__returns_number) +BOOST_AUTO_TEST_CASE(btcd_rpc__getdifficulty__ten_block_store__number) { const auto response = rpc("getdifficulty"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - BOOST_REQUIRE(response.at("result").is_double()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_double()); } BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__ten_block_store__nine) { const auto response = rpc("getinfo"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + const auto& result = response.at("result").as_object(); BOOST_REQUIRE_EQUAL(result.at("blocks").as_int64(), 9); BOOST_REQUIRE_EQUAL(result.at("testnet").as_bool(), false); } -BOOST_AUTO_TEST_CASE(btcd_rpc__getnettotals__returns_object) +BOOST_AUTO_TEST_CASE(btcd_rpc__getnettotals__untracked_counters__zero) { const auto response = rpc("getnettotals"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + const auto& result = response.at("result").as_object(); - BOOST_REQUIRE(result.contains("totalbytesrecv")); - BOOST_REQUIRE(result.contains("totalbytessent")); + BOOST_REQUIRE_EQUAL(result.at("totalbytesrecv").as_int64(), 0); + BOOST_REQUIRE_EQUAL(result.at("totalbytessent").as_int64(), 0); BOOST_REQUIRE(result.at("timemillis").as_int64() > 0); } -BOOST_AUTO_TEST_CASE(btcd_rpc__getnetworkhashps__returns_number) +BOOST_AUTO_TEST_CASE(btcd_rpc__getnetworkhashps__ten_block_store__number) { const auto response = rpc("getnetworkhashps"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - BOOST_REQUIRE(response.at("result").is_double()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_double()); } -BOOST_AUTO_TEST_CASE(btcd_rpc__createrawtransaction__well_formed_hex) +BOOST_AUTO_TEST_CASE(btcd_rpc__createrawtransaction__one_input_one_output__decodes) { - const auto txid = encode_hash( - test::block1.transactions_ptr()->front()->hash(false)); - const auto response = rpc("createrawtransaction", - "[[{\"txid\":\"" + txid + "\",\"vout\":0}],{\"" + - filter_test_address_text() + "\":0.01}]"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); + const auto request = R"([[{"txid":"%1%","vout":0}],{"%2%":0.01}])"; + const auto response = rpc("createrawtransaction", (boost_format(request) % coinbase_txid(test::block1) % found_address).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); data_chunk data{}; - BOOST_REQUIRE(decode_base16(data, response.at("result").as_string())); + BOOST_REQUIRE(decode_base16(data, as_text(response.at("result")))); const chain::transaction tx{ data, false }; BOOST_REQUIRE(tx.is_valid()); @@ -378,101 +284,104 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__createrawtransaction__well_formed_hex) BOOST_REQUIRE_EQUAL(tx.outputs_ptr()->size(), 1u); } -BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__block1_coinbase) +BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__block1_coinbase__expected_txid) { const auto& coinbase = *test::block1.transactions_ptr()->front(); - const auto hex = encode_base16(coinbase.to_data(false)); - - const auto response = rpc("decoderawtransaction", "[\"" + hex + "\"]"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - - const auto& result = response.at("result").as_object(); - BOOST_REQUIRE_EQUAL(result.at("txid").as_string(), - encode_hash(coinbase.hash(false))); + const auto request = R"(["%1%"])"; + const auto response = rpc("decoderawtransaction", (boost_format(request) % encode_base16(coinbase.to_data(false))).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + BOOST_REQUIRE_EQUAL(as_text(response.at("result").at("txid")), encode_hash(coinbase.hash(false))); } BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__malformed_hex__invalid_argument) { - const auto response = rpc("decoderawtransaction", R"(["not-hex"])"); - BOOST_REQUIRE(has_error(response)); + const auto result = rpc_error("decoderawtransaction", R"(["not-hex"])"); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); } -BOOST_AUTO_TEST_CASE(btcd_rpc__decodescript__pubkey_script__typed_correctly) +BOOST_AUTO_TEST_CASE(btcd_rpc__decodescript__pay_public_key__pubkey) { // block1's coinbase output is a p2pk script (early mainnet convention). - const auto& script = - test::block1.transactions_ptr()->front()->outputs_ptr()->front()-> - script(); - const auto hex = encode_base16(script.to_data(false)); - - const auto response = rpc("decodescript", "[\"" + hex + "\"]"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); + const auto& script = test::block1.transactions_ptr()->front()->outputs_ptr()->front()->script(); + const auto request = R"(["%1%"])"; + const auto response = rpc("decodescript", (boost_format(request) % encode_base16(script.to_data(false))).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); const auto& result = response.at("result").as_object(); - BOOST_REQUIRE_EQUAL(result.at("type").as_string(), "pubkey"); + BOOST_REQUIRE_EQUAL(as_text(result.at("type")), "pubkey"); BOOST_REQUIRE(result.contains("asm")); BOOST_REQUIRE(result.contains("p2sh")); } BOOST_AUTO_TEST_CASE(btcd_rpc__validateaddress__valid_address__isvalid_true) { - const auto response = rpc("validateaddress", - "[\"" + filter_test_address_text() + "\"]"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); + const auto request = R"(["%1%"])"; + const auto response = rpc("validateaddress", (boost_format(request) % found_address).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); const auto& result = response.at("result").as_object(); BOOST_REQUIRE_EQUAL(result.at("isvalid").as_bool(), true); - BOOST_REQUIRE_EQUAL(result.at("address").as_string(), - filter_test_address_text()); + BOOST_REQUIRE_EQUAL(as_text(result.at("address")), found_address); BOOST_REQUIRE_EQUAL(result.at("iswitness").as_bool(), false); } BOOST_AUTO_TEST_CASE(btcd_rpc__validateaddress__invalid_address__isvalid_false) { - const auto response = rpc("validateaddress", R"(["not-an-address"])"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - BOOST_REQUIRE_EQUAL( - response.at("result").as_object().at("isvalid").as_bool(), false); + const auto request = R"(["%1%"])"; + const auto response = rpc("validateaddress", (boost_format(request) % bogus_address).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + BOOST_REQUIRE_EQUAL(response.at("result").at("isvalid").as_bool(), false); } -BOOST_AUTO_TEST_CASE(btcd_rpc__help__returns_method_list) +BOOST_AUTO_TEST_CASE(btcd_rpc__help__default__method_list) { const auto response = rpc("help"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - BOOST_REQUIRE(response.at("result").as_string().find("getcurrentnet") != - std::string::npos); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_NE(as_text(response.at("result")).find("getcurrentnet"), std::string::npos); } // not implemented stubs // ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(btcd_rpc__not_implemented__error) +BOOST_AUTO_TEST_CASE(btcd_rpc__stop__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("stop"), not_implemented.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__notifynewtransactions__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("notifynewtransactions", "[false]"), not_implemented.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifynewtransactions__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("stopnotifynewtransactions"), not_implemented.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__notifyreceived__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("notifyreceived", "[[]]"), not_implemented.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyreceived__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("stopnotifyreceived", "[[]]"), not_implemented.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__notifyspent__default__not_implemented) { - const std::vector> methods - { - { "stop", "[]" }, - { "notifynewtransactions", "[false]" }, - { "stopnotifynewtransactions", "[]" }, - { "notifyreceived", "[[]]" }, - { "stopnotifyreceived", "[[]]" }, - { "notifyspent", "[[]]" }, - { "stopnotifyspent", "[[]]" } - }; + BOOST_REQUIRE_EQUAL(rpc_error("notifyspent", "[[]]"), not_implemented.value()); +} - for (const auto& method: methods) - BOOST_REQUIRE_MESSAGE(has_error(rpc(method.first, method.second)), - method.first); +BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyspent__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("stopnotifyspent", "[[]]"), not_implemented.value()); } // response envelope // ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(btcd_rpc__response__id_matches_request) +BOOST_AUTO_TEST_CASE(btcd_rpc__response__sequential_requests__id_matches_request) { // request_id_ increments each call: first call uses id=0, second id=1. const auto r0 = rpc("session"); @@ -481,15 +390,13 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__response__id_matches_request) BOOST_REQUIRE_EQUAL(r1.at("id").as_int64(), 1); } -BOOST_AUTO_TEST_CASE(btcd_rpc__unknown_method__error_keeps_connection_alive) +BOOST_AUTO_TEST_CASE(btcd_rpc__unknown_method__default__unexpected_method) { - // Unrecognized method returns a json-rpc error rather than dropping the - // ws connection (matches real btcd, verified by the follow-up call). - const auto unknown = rpc("nosuchmethod"); - BOOST_REQUIRE(has_error(unknown)); + BOOST_REQUIRE_EQUAL(rpc_error("nosuchmethod"), unexpected_method.value()); + // The connection survives an unknown method (as btcd). const auto follow_up = rpc("session"); - BOOST_REQUIRE(!has_error(follow_up)); + REQUIRE_NO_THROW_TRUE(follow_up.at("result").is_object()); } BOOST_AUTO_TEST_SUITE_END() @@ -499,69 +406,63 @@ BOOST_AUTO_TEST_SUITE_END() // call, enforced over ws by protocol_btcd_rpc::dispatch_websocket. // ---------------------------------------------------------------------------- -BOOST_FIXTURE_TEST_SUITE(btcd_scoped_credential_tests, - btcd_scoped_credential_setup_fixture) +BOOST_FIXTURE_TEST_SUITE(btcd_scoped_credential_tests, btcd_scoped_credential_setup_fixture) -BOOST_AUTO_TEST_CASE(btcd_scoped_credential__listed_method__permitted) +BOOST_AUTO_TEST_CASE(btcd_scoped_credential__session__listed_method__permitted) { - const auto auth = rpc("authenticate", - R"([")" BTCD_TEST_USERNAME R"(", ")" BTCD_TEST_PASSWORD R"("])"); - BOOST_REQUIRE(!has_error(auth)); + BOOST_REQUIRE(authenticate()); - // BTCD_TEST_SCOPED_METHOD ("session") is the credential's only permitted - // method. + // BTCD_TEST_SCOPED_METHOD ("session") is the credential's only method. const auto session = rpc("session"); - BOOST_REQUIRE(!has_error(session)); + REQUIRE_NO_THROW_TRUE(session.at("result").is_object()); } -BOOST_AUTO_TEST_CASE(btcd_scoped_credential__unlisted_method__rejected) +BOOST_AUTO_TEST_CASE(btcd_scoped_credential__notifyblocks__unlisted_method__unauthorized) { - const auto auth = rpc("authenticate", - R"([")" BTCD_TEST_USERNAME R"(", ")" BTCD_TEST_PASSWORD R"("])"); - BOOST_REQUIRE(!has_error(auth)); + BOOST_REQUIRE(authenticate()); - // notifyblocks is a real, implemented handler, not scoped by this - // credential -- confirms rejection is permitted()'s doing, not the - // method being unimplemented. - const auto response = rpc("notifyblocks"); - BOOST_REQUIRE(has_error(response)); + // notifyblocks is implemented, so rejection is permitted()'s doing. + BOOST_REQUIRE_EQUAL(rpc_error("notifyblocks"), unauthorized.value()); } BOOST_AUTO_TEST_SUITE_END() // authenticate (protocol_btcd_rpc::handle_authenticate): exercises the -// credential-configured branch, not covered by btcd_rpc_tests above (whose +// credential-configured branch, not covered by btcd_tests above (whose // fixture leaves btcd.credential unset, so every one of those connections // takes the no-op path). // ---------------------------------------------------------------------------- BOOST_FIXTURE_TEST_SUITE(btcd_auth_tests, btcd_credentialed_setup_fixture) -BOOST_AUTO_TEST_CASE(btcd_auth__correct_credentials__succeeds_then_session_works) +BOOST_AUTO_TEST_CASE(btcd_auth__authenticate__correct_credentials__null_result) { - const auto auth = rpc("authenticate", - R"([")" BTCD_TEST_USERNAME R"(", ")" BTCD_TEST_PASSWORD R"("])"); - BOOST_REQUIRE(!has_error(auth)); + const auto request = R"(["%1%","%2%"])"; + const auto response = rpc("authenticate", (boost_format(request) % BTCD_TEST_USERNAME % BTCD_TEST_PASSWORD).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} - // Connection survives a successful handshake; other methods now work. +BOOST_AUTO_TEST_CASE(btcd_auth__session__authenticated__permitted) +{ + BOOST_REQUIRE(authenticate()); + + // Connection survives, and other methods now work. const auto session = rpc("session"); - BOOST_REQUIRE(!has_error(session)); + REQUIRE_NO_THROW_TRUE(session.at("result").is_object()); } -BOOST_AUTO_TEST_CASE(btcd_auth__wrong_password__rejected) +BOOST_AUTO_TEST_CASE(btcd_auth__authenticate__wrong_password__unauthorized) { - const auto response = rpc("authenticate", - R"([")" BTCD_TEST_USERNAME R"(", "definitely-wrong"])"); - BOOST_REQUIRE(has_error(response)); + const auto request = R"(["%1%","definitely-wrong"])"; + const auto result = rpc_error("authenticate", (boost_format(request) % BTCD_TEST_USERNAME).str()); + BOOST_REQUIRE_EQUAL(result, unauthorized.value()); } -BOOST_AUTO_TEST_CASE(btcd_auth__other_method_before_authenticating__rejected) +BOOST_AUTO_TEST_CASE(btcd_auth__session__not_authenticated__unauthorized) { - // 'session' instead of 'authenticate' as the first message: the - // handshake requires authenticate specifically when a credential is - // configured. - const auto response = rpc("session"); - BOOST_REQUIRE(has_error(response)); + // 'session' as the first message, with a credential configured: + // permitted() rejects until authenticate has latched a digest. + BOOST_REQUIRE_EQUAL(rpc_error("session"), unauthorized.value()); } BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/btcd/btcd_setup_fixture.cpp b/test/protocols/btcd/btcd_setup_fixture.cpp index 135e1a6b..a19e944c 100644 --- a/test/protocols/btcd/btcd_setup_fixture.cpp +++ b/test/protocols/btcd/btcd_setup_fixture.cpp @@ -28,8 +28,7 @@ using namespace boost::beast; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) btcd_setup_fixture::btcd_setup_fixture(const initializer& setup, - std::string_view username, std::string_view password, - std::string_view methods) + const configurator& configure) : config_ { system::chain::selection::mainnet, @@ -55,19 +54,21 @@ btcd_setup_fixture::btcd_setup_fixture(const initializer& setup, auto& btcd = config_.server.btcd; btcd.binds = { { BTCD_ENDPOINT } }; + // 2: the ws connection (websocket_/socket_) plus the plain http one // (http_socket_) used by http_rpc(). btcd.connections = 2; - if (!username.empty()) - btcd.credentials.emplace_back(std::string{ username } + ":" + - std::string{ password } + - (methods.empty() ? "" : ":" + std::string{ methods })); + btcd.inactivity_minutes = 1; database_settings.interval_depth = 2; node_settings.delay_inbound = false; node_settings.minimum_fee_rate = 99.0; network_settings.inbound.connections = 0; network_settings.outbound.connections = 0; + // Apply test-specific configuration overrides. + if (configure) + configure(config_); + // Create and populate the store. auto ec = store_.create([](auto, auto) {}); BOOST_REQUIRE_MESSAGE(!ec, ec.message()); @@ -99,10 +100,14 @@ btcd_setup_fixture::~btcd_setup_fixture() network::boost_code ec{}; websocket_.close(websocket::close_code::normal, ec); - // Expected and harmless during fixture teardown. + // Expected and harmless during fixture teardown: + // beast::websocket::error::closed : normal (graceful handshake). + // asio::error::operation_aborted : hard (invalid request test). + // asio::error::connection_reset : peer gone (failed authenticate test). if (ec && ec != boost::beast::websocket::error::closed && - ec != boost::asio::error::operation_aborted) + ec != boost::asio::error::operation_aborted && + ec != boost::asio::error::connection_reset) { BOOST_WARN_MESSAGE(false, ec.message()); } @@ -134,6 +139,37 @@ boost::json::value btcd_setup_fixture::rpc(std::string_view method, return test::parse_json(buffers_to_string(buffer.data())); } +int64_t btcd_setup_fixture::rpc_error(std::string_view method, + std::string_view params) +{ + try + { + return rpc(method, params).at("error").as_object().at("code") + .as_int64(); + } + catch (const boost::system::system_error&) + { + return -1; + } +} + +bool btcd_setup_fixture::authenticate(const std::string& username, + const std::string& password) +{ + const auto request = R"(["%1%","%2%"])"; + const auto response = rpc("authenticate", + (boost_format(request) % username % password).str()); + + try + { + return response.at("error").is_null(); + } + catch (const boost::system::system_error&) + { + return false; + } +} + boost::json::value btcd_setup_fixture::receive_notification() { flat_buffer buffer{}; diff --git a/test/protocols/btcd/btcd_setup_fixture.hpp b/test/protocols/btcd/btcd_setup_fixture.hpp index f2f47113..7711019b 100644 --- a/test/protocols/btcd/btcd_setup_fixture.hpp +++ b/test/protocols/btcd/btcd_setup_fixture.hpp @@ -23,27 +23,36 @@ #include "../../mocks/blocks.hpp" #define BTCD_ENDPOINT "127.0.0.1:65004" +#define BTCD_TEST_USERNAME "user" +#define BTCD_TEST_PASSWORD "pass" +#define BTCD_TEST_SCOPED_METHOD "session" struct btcd_setup_fixture { using initializer = std::function; + using configurator = std::function; DELETE_COPY_MOVE(btcd_setup_fixture); - // username/password configure a single btcd.credential entry (no auth - // required if username is empty); methods, if non-empty, scopes it to a - // comma-separated method list ("user:pass:method,..." syntax). explicit btcd_setup_fixture(const initializer& setup, - std::string_view username={}, std::string_view password={}, - std::string_view methods={}); + const configurator& configure={}); ~btcd_setup_fixture(); // JSON-RPC 1.0 over websocket. params must be a json array (json-rpc-v1 - // does not support 'null' or omitted params; use "[]" for no arguments). + // does not support 'null' or omitted params, use "[]" for no arguments). // Returns the parsed json-rpc response object (result/error). boost::json::value rpc(std::string_view method, std::string_view params = "[]"); + // As rpc(), returning the response's error code (-1 if not an error). + int64_t rpc_error(std::string_view method, + std::string_view params = "[]"); + + // Authorize the connection (btcd's in-band credential method), for tests + // that require it as a precondition -- mirrors electrum's handshake(). + bool authenticate(const std::string& username=BTCD_TEST_USERNAME, + const std::string& password=BTCD_TEST_PASSWORD); + // JSON-RPC 2.0 over plain HTTP POST to "/" (a separate connection from // the ws one rpc() uses). boost::json::value http_rpc(std::string_view method, @@ -73,8 +82,8 @@ struct btcd_setup_fixture tcp_stream socket_{ io_.get_executor() }; websocket_stream websocket_{ socket_ }; tcp_stream http_socket_{ io_.get_executor() }; - int request_id_{}; int http_request_id_{}; + int request_id_{}; }; struct btcd_ten_block_setup_fixture @@ -89,12 +98,9 @@ struct btcd_ten_block_setup_fixture } }; -#define BTCD_TEST_USERNAME "user" -#define BTCD_TEST_PASSWORD "pass" - -// Configured with a credential but does not authenticate -- for tests that -// exercise the in-band 'authenticate' call itself (success, wrong-password -// rejection, calling another method before authenticating). +// Configured with an unscoped credential -- for tests that exercise the +// in-band 'authenticate' call itself (success, wrong-password rejection, +// calling another method before authenticating). struct btcd_credentialed_setup_fixture : btcd_setup_fixture { @@ -102,13 +108,17 @@ struct btcd_credentialed_setup_fixture : btcd_setup_fixture([](test::query_t& query) { return test::setup_ten_block_store(query); - }, BTCD_TEST_USERNAME, BTCD_TEST_PASSWORD) + }, [](configuration& config) + { + config.server.btcd.credentials = + { + { BTCD_TEST_USERNAME ":" BTCD_TEST_PASSWORD } + }; + }) { } }; -#define BTCD_TEST_SCOPED_METHOD "session" - // Configured with a credential scoped to a single method -- for tests that // verify channel_btcd::permitted() enforces per-method credential scoping // over ws once authenticated. @@ -119,7 +129,14 @@ struct btcd_scoped_credential_setup_fixture : btcd_setup_fixture([](test::query_t& query) { return test::setup_ten_block_store(query); - }, BTCD_TEST_USERNAME, BTCD_TEST_PASSWORD, BTCD_TEST_SCOPED_METHOD) + }, [](configuration& config) + { + config.server.btcd.credentials = + { + { BTCD_TEST_USERNAME ":" BTCD_TEST_PASSWORD ":" + BTCD_TEST_SCOPED_METHOD } + }; + }) { } }; From 0754f1c27334c5abd76f8b014639e651c526bd64 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 01:03:53 -0400 Subject: [PATCH 04/29] Adapt to network change. --- src/protocols/admin/protocol_admin.cpp | 8 ++++++++ src/protocols/native/protocol_native.cpp | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/src/protocols/admin/protocol_admin.cpp b/src/protocols/admin/protocol_admin.cpp index 53f5bea4..256ee805 100644 --- a/src/protocols/admin/protocol_admin.cpp +++ b/src/protocols/admin/protocol_admin.cpp @@ -110,6 +110,14 @@ void protocol_admin::dispatch_websocket(const http::request& request) NOEXCEPT { BC_ASSERT(stranded()); + // ws frames carry no headers, so ws authorization is enforced here, not + // by the channel (established by basic auth on the upgrade request). + if (!authorized()) + { + stop(network::error::unauthorized); + return; + } + // Admin websocket interface supports only string requests. if (!request.body().contains()) { diff --git a/src/protocols/native/protocol_native.cpp b/src/protocols/native/protocol_native.cpp index c9ec20aa..2d5dd4b8 100644 --- a/src/protocols/native/protocol_native.cpp +++ b/src/protocols/native/protocol_native.cpp @@ -151,6 +151,14 @@ void protocol_native::dispatch_websocket(const http::request& request) NOEXCEPT { BC_ASSERT(stranded()); + // ws frames carry no headers, so ws authorization is enforced here, not + // by the channel (established by basic auth on the upgrade request). + if (!authorized()) + { + stop(network::error::unauthorized); + return; + } + // Native websocket interface supports only string requests. if (!request.body().contains()) { From f3df68b546ea4dd745345c88bb2f692df06535e5 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 01:04:13 -0400 Subject: [PATCH 05/29] btcd: drop channel_btcd. --- builds/gnu/Makefile.am | 1 - .../libbitcoin-server.vcxproj | 1 - .../libbitcoin-server.vcxproj.filters | 3 - .../libbitcoin-server.vcxproj | 1 - .../libbitcoin-server.vcxproj.filters | 3 - include/bitcoin/server.hpp | 1 - .../bitcoin/server/channels/channel_btcd.hpp | 113 ------------------ include/bitcoin/server/channels/channels.hpp | 7 +- 8 files changed, 2 insertions(+), 128 deletions(-) delete mode 100644 include/bitcoin/server/channels/channel_btcd.hpp diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index 653c30b3..bfb6c265 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -111,7 +111,6 @@ include_bitcoin_server_channelsdir = \ include_bitcoin_server_channels_HEADERS = \ ${srcdir}/../../include/bitcoin/server/channels/channel.hpp \ - ${srcdir}/../../include/bitcoin/server/channels/channel_btcd.hpp \ ${srcdir}/../../include/bitcoin/server/channels/channel_electrum.hpp \ ${srcdir}/../../include/bitcoin/server/channels/channel_http.hpp \ ${srcdir}/../../include/bitcoin/server/channels/channel_stratum_v1.hpp \ diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj index c865858b..793a95ee 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj @@ -166,7 +166,6 @@ - diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters index 4304d63a..16570f39 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -200,9 +200,6 @@ include\bitcoin\server\channels - - include\bitcoin\server\channels - include\bitcoin\server\channels diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj index 1aeb9b5a..84642d51 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj @@ -166,7 +166,6 @@ - diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters index 4304d63a..16570f39 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -200,9 +200,6 @@ include\bitcoin\server\channels - - include\bitcoin\server\channels - include\bitcoin\server\channels diff --git a/include/bitcoin/server.hpp b/include/bitcoin/server.hpp index 5b39999f..dc8ea732 100644 --- a/include/bitcoin/server.hpp +++ b/include/bitcoin/server.hpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include diff --git a/include/bitcoin/server/channels/channel_btcd.hpp b/include/bitcoin/server/channels/channel_btcd.hpp deleted file mode 100644 index 72cddcaa..00000000 --- a/include/bitcoin/server/channels/channel_btcd.hpp +++ /dev/null @@ -1,113 +0,0 @@ -/** - * Copyright (c) 2011-2026 libbitcoin developers - * - * This file is part of libbitcoin. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ -#ifndef LIBBITCOIN_SERVER_CHANNELS_CHANNEL_BTCD_HPP -#define LIBBITCOIN_SERVER_CHANNELS_CHANNEL_BTCD_HPP - -#include -#include -#include -#include - -namespace libbitcoin { -namespace server { - -/// Channel for btcd channels (http/ws json-rpc). -class BCS_API channel_btcd - : public server::channel, - public network::channel_http, - protected network::tracker -{ -public: - typedef std::shared_ptr ptr; - - inline channel_btcd(const network::logger& log, - const network::socket::ptr& socket, uint64_t identifier, - const node::configuration& config, const options_t& options) NOEXCEPT - : server::channel(log, socket, identifier, config), - network::channel_http(log, socket, identifier, config.network, options), - network::tracker(log), - options_(options) - { - } - - /// True once the in-band 'authenticate' call has succeeded. - inline bool authenticated() const NOEXCEPT - { - return authenticated_; - } - - /// Latch the credential digest satisfying 'authenticate', applying its - /// method scoping to subsequent ws traffic (see permitted). - inline void set_authenticated(const system::hash_digest& digest) NOEXCEPT - { - authenticated_ = true; - authenticated_digest_ = digest; - } - - /// True if the credential that authorized this channel permits the rpc - /// method. A ws connection is authorized by basic auth on the upgrade - /// request or by the in-band 'authenticate' call (btcd allows either). - inline bool permitted(const std::string& method) const NOEXCEPT override - { - if (!websocket()) - return network::channel_http::permitted(method); - - if (!options_.authorize()) - return true; - - if (network::channel_http::permitted(method)) - return true; - - return authenticated_ && options_.permitted(authenticated_digest_, method); - } - -protected: - using value_type = network::http::body::value_type; - - /// Overridden to change default websocket reader to json-rpc request. - inline value_type websocket_body() const NOEXCEPT override - { - // There is no forwarding constructor so assign and move. - network::http::body::value_type value{}; - value = network::rpc::request{}; - return value; - } - - /// ws data frames cannot carry an Authorization header, so the base - /// check would reject them all once a credential is configured -- - /// permitted() is the per-method gate for ws traffic. The plain http - /// post path keeps normal per-request enforcement. - inline bool authorized() const NOEXCEPT override - { - return websocket() ? true : network::channel_http::authorized(); - } - -private: - // This is thread safe. - const options_t& options_; - - // These are protected by strand. - bool authenticated_{}; - system::hash_digest authenticated_digest_{}; -}; - -} // namespace server -} // namespace libbitcoin - -#endif diff --git a/include/bitcoin/server/channels/channels.hpp b/include/bitcoin/server/channels/channels.hpp index 8698a76f..6488a15c 100644 --- a/include/bitcoin/server/channels/channels.hpp +++ b/include/bitcoin/server/channels/channels.hpp @@ -20,7 +20,6 @@ #define LIBBITCOIN_SERVER_CHANNELS_CHANNELS_HPP #include -#include #include #include #include @@ -39,8 +38,7 @@ network::channel │ └── [server::channel_stratum_v1] │ └── [server::channel_electrum] ├── channel_http -│ ├── [server::channel_http] -│ └── [server::channel_btcd] +│ └── [server::channel_http] └── channel_peer └── [node::channel_peer] @@ -52,7 +50,6 @@ server::channel → node::channel ├── channel_stratum_v2 → network::channel ├── channel_stratum_v1 → network::channel_rpc ├── channel_electrum → network::channel_rpc -├── channel_http → network::channel_http -├── channel_btcd → network::channel_http +└── channel_http → network::channel_http */ From e40f9bf21201ae5f7d4e98df4f37a56f2d0a80fc Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 01:06:38 -0400 Subject: [PATCH 06/29] btcd: build authorize on channel_http. --- .../server/protocols/protocol_btcd_rpc.hpp | 11 ++-- src/protocols/btcd/protocol_btcd_rpc.cpp | 54 +++++++++++-------- test/protocols/btcd/btcd_rpc.cpp | 25 ++++++--- test/protocols/btcd/btcd_setup_fixture.hpp | 2 +- 4 files changed, 54 insertions(+), 38 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp index 3460a519..feb6c8d3 100644 --- a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp @@ -35,17 +35,14 @@ namespace server { /// handlers (getblockcount etc.) from protocol_bitcoind_rpc and adds the /// btcd-only extension methods via a second dispatcher. Both dispatchers are /// bridged in both directions, so all methods are reachable over both -/// transports. Connection authorization is established once, by basic auth -/// on the ws upgrade request or by the in-band 'authenticate' method (see -/// channel_btcd::permitted). +/// transports. ws authorization is established once per connection, by basic +/// auth on the ws upgrade request or by the in-band 'authenticate' method, +/// and latched on the channel (see dispatch_websocket). class BCS_API protocol_btcd_rpc : public server::protocol_bitcoind_rpc, protected network::tracker { public: - // Replace channel_t so session_server creates channel_btcd instances. - using channel_t = channel_btcd; - typedef std::shared_ptr ptr; using btcd_interface = interface::btcd; using btcd_dispatcher = network::rpc::dispatcher; @@ -56,7 +53,6 @@ class BCS_API protocol_btcd_rpc : server::protocol_bitcoind_rpc(session, channel, options), network::tracker(session->log), options_(options), - btcd_channel_(std::dynamic_pointer_cast(channel)), p2kh_(session->system_settings().forks.difficult ? system::wallet::payment_address::mainnet_p2kh : system::wallet::payment_address::testnet_p2kh), @@ -215,7 +211,6 @@ class BCS_API protocol_btcd_rpc // These are thread safe. std::atomic_bool subscribed_blocks_{}; const options_t& options_; - const channel_btcd::ptr btcd_channel_; const uint8_t p2kh_; const uint8_t p2sh_; diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd_rpc.cpp index 854ed169..71e1a490 100644 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc.cpp @@ -96,30 +96,50 @@ void protocol_btcd_rpc::stopping(const code& ec) NOEXCEPT // Websocket dispatch. // ---------------------------------------------------------------------------- -// 'authenticate' bypasses permitted(), which requires authentication to -// already be established -- gating it would make authenticating impossible. +// ws frames carry no headers, so ws authorization is enforced here, not by +// the channel. It is established at most once per connection, by basic auth +// on the upgrade request or by exactly one authenticate call -- so a call is +// invalid if authorized and the method is authenticate, or not authorized +// and the method is not authenticate (as btcd). void protocol_btcd_rpc::dispatch_websocket( const network::http::request& request) NOEXCEPT { BC_ASSERT(stranded()); - // channel_btcd::websocket_body() preselects the json-rpc request parser. - if (!request.body().contains()) + // btcd websocket frames are json-rpc text (channel default reader). + if (!request.body().contains()) { send_btcd_error(error::invalid_argument); return; } - const auto& body = request.body().get(); - const auto& message = body.message; + request_t message{}; + try + { + message = value_to(parse( + request.body().get())); + } + catch (const std::exception&) + { + send_btcd_error(error::invalid_argument); + return; + } // Cache request context for response building (version + id). btcd_version_ = message.jsonrpc; btcd_id_ = message.id; - if (message.method != btcd_interface::authenticate::name && - !permitted(message.method)) + const auto authenticate = + (message.method == btcd_interface::authenticate::name); + + if (authenticate == authorized()) + { + send_btcd_error(network::error::unauthorized); + return; + } + + if (!authenticate && !permitted(message.method)) { send_btcd_error(network::error::unauthorized); return; @@ -163,21 +183,11 @@ bool protocol_btcd_rpc::handle_authenticate(const code& ec, if (stopped(ec)) return false; - // No credential configured: nothing to check against, no-op success. - if (!options_.authorize()) - { - send_btcd_result({}, 4); - return true; - } - - // Same digest scheme as config::credential (settings::http_server has no - // direct username:password lookup, only digest-keyed authorized()). - const auto digest = sha256_hash( - "Basic " + encode_base64(username + ":" + password)); - - if (options_.authorized(digest)) + // Reachable only when not authorized (see dispatch_websocket). The + // channel latches the digest only if it matches a configured credential. + set_authorized(network::config::credential::to_digest(username, password)); + if (authorized()) { - btcd_channel_->set_authenticated(digest); send_btcd_result({}, 4); return true; } diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index 03f2c353..fefbf828 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -53,11 +53,12 @@ BOOST_FIXTURE_TEST_SUITE(btcd_tests, btcd_ten_block_setup_fixture) // session management // ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__no_credential_configured__null_result) +BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__no_credential_configured__unauthorized) { - // Without a credential configured, authenticate is a no-op success. - const auto response = rpc("authenticate", R"(["user","pass"])"); - REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); + // Without a credential configured the connection is authorized from the + // start, and authenticate is invalid once authorization is established. + const auto result = rpc_error("authenticate", R"(["user","pass"])"); + BOOST_REQUIRE_EQUAL(result, unauthorized.value()); } BOOST_AUTO_TEST_CASE(btcd_rpc__session__default__returns_id) @@ -401,7 +402,7 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__unknown_method__default__unexpected_method) BOOST_AUTO_TEST_SUITE_END() -// Method-scoped credentials (channel_btcd::permitted()): a credential's +// Method-scoped credentials (channel_http::permitted()): a credential's // optional 'user:pass:method,...' suffix restricts which methods it may // call, enforced over ws by protocol_btcd_rpc::dispatch_websocket. // ---------------------------------------------------------------------------- @@ -460,9 +461,19 @@ BOOST_AUTO_TEST_CASE(btcd_auth__authenticate__wrong_password__unauthorized) BOOST_AUTO_TEST_CASE(btcd_auth__session__not_authenticated__unauthorized) { - // 'session' as the first message, with a credential configured: - // permitted() rejects until authenticate has latched a digest. + // 'session' as the first message, with a credential configured: any + // method but authenticate is invalid until authorization is established. BOOST_REQUIRE_EQUAL(rpc_error("session"), unauthorized.value()); } +BOOST_AUTO_TEST_CASE(btcd_auth__authenticate__already_authenticated__unauthorized) +{ + BOOST_REQUIRE(authenticate()); + + // authenticate is invalid once authorization is established (as btcd). + const auto request = R"(["%1%","%2%"])"; + const auto result = rpc_error("authenticate", (boost_format(request) % BTCD_TEST_USERNAME % BTCD_TEST_PASSWORD).str()); + BOOST_REQUIRE_EQUAL(result, unauthorized.value()); +} + BOOST_AUTO_TEST_SUITE_END() diff --git a/test/protocols/btcd/btcd_setup_fixture.hpp b/test/protocols/btcd/btcd_setup_fixture.hpp index 7711019b..c4055887 100644 --- a/test/protocols/btcd/btcd_setup_fixture.hpp +++ b/test/protocols/btcd/btcd_setup_fixture.hpp @@ -120,7 +120,7 @@ struct btcd_credentialed_setup_fixture }; // Configured with a credential scoped to a single method -- for tests that -// verify channel_btcd::permitted() enforces per-method credential scoping +// verify channel_http::permitted() enforces per-method credential scoping // over ws once authenticated. struct btcd_scoped_credential_setup_fixture : btcd_setup_fixture From 33e4f213e4524f666d330e4ec5903db34c6786cc Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 01:08:36 -0400 Subject: [PATCH 07/29] Delint (unused var). --- src/protocols/btcd/protocol_btcd_rpc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd_rpc.cpp index 71e1a490..dc27f04e 100644 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc.cpp @@ -402,7 +402,7 @@ bool protocol_btcd_rpc::handle_rescan(const code& ec, // Chase events. // ---------------------------------------------------------------------------- -bool protocol_btcd_rpc::handle_chase(const code& ec, node::chase event_, +bool protocol_btcd_rpc::handle_chase(const code&, node::chase event_, node::event_value value) NOEXCEPT { // Do not pass ec to stopped, it is not a call status. From 471110ff92222fe30639e05b234c15c16aeacc00 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 01:30:03 -0400 Subject: [PATCH 08/29] Remove nonsense. --- endpoints/README.md | 12 ++++++------ endpoints/test_bitcoind_rest.py | 2 +- endpoints/test_bitcoind_rpc.py | 2 +- endpoints/test_btcd_rpc.py | 18 +++++++++--------- .../server/protocols/protocol_bitcoind_rpc.hpp | 2 +- .../server/protocols/protocol_btcd_rpc.hpp | 4 ++-- .../bitcoind/protocol_bitcoind_rpc.cpp | 2 +- src/protocols/btcd/protocol_btcd_rpc.cpp | 4 ++-- test/protocols/btcd/btcd_rpc.cpp | 2 +- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/endpoints/README.md b/endpoints/README.md index 9a3ceb81..8294f9a1 100644 --- a/endpoints/README.md +++ b/endpoints/README.md @@ -124,7 +124,7 @@ pytest test_bitcoind_rpc.py \ --bitcoind-rpc-port=8332 ``` -**With authentication (for Bitcoin Core):** +**With authentication (for bitcoind):** ```bash pytest test_bitcoind_rpc.py \ @@ -241,7 +241,7 @@ pytest test_native.py -k "not address" ### test_bitcoind_rpc.py — bitcoind RPC Compatibility -Tests JSON-RPC 2.0 interface compatible with Bitcoin Core RPC. +Tests JSON-RPC 2.0 interface compatible with the bitcoind RPC interface. **Coverage:** - ✅ Blockchain methods (17 methods) @@ -271,7 +271,7 @@ pytest test_bitcoind_rpc.py -k "getblock" Tests the btcd-compatible endpoint: JSON-RPC 1.0 over a persistent websocket connection (session/notification/filter/admin extension methods) plus plain -http post (chain methods inherited from `protocol_bitcoind_rpc`, same shape +http post (bitcoind interface methods inherited from `protocol_bitcoind_rpc`, same shape as `bitcoind`). Unlike the other endpoint suites, this one is explicitly split into two @@ -291,7 +291,7 @@ kinds of test: - ✅ Block subscription — `notifyblocks`/`stopnotifyblocks` (ack), a real `blockconnected` push-notification test (positional `[hash, height, time]`, verified against upstream btcd's actual wire format) -- ✅ Chain methods, both over http post and over the persistent ws +- ✅ bitcoind interface methods, both over http post and over the persistent ws connection (`getbestblockhash`, `getblockcount`, `getblockhash`, `getblockheader`, `gettxout`, `getrawtransaction`) - ✅ `loadtxfilter`/`rescanblocks` (address/outpoint watch-list, filtered @@ -591,7 +591,7 @@ SKIP: No response (possibly unsupported method) ``` Some public servers (electrs in particular) do not implement all Electrum methods and simply do not respond. The test skips cleanly. This is not a test failure. -**6. Authentication errors (Bitcoin Core)** +**6. Authentication errors (bitcoind)** ``` RuntimeError: RPC connection error: 401 Unauthorized ``` @@ -724,7 +724,7 @@ When adding new tests: - [pytest Documentation](https://docs.pytest.org/) - [libbitcoin-server Documentation](../../docs/) -- [Bitcoin Core RPC Documentation](https://developer.bitcoin.org/reference/rpc/) +- [bitcoind RPC documentation](https://developer.bitcoin.org/reference/rpc/) - [Electrum Protocol Documentation](https://electrumx.readthedocs.io/en/latest/protocol.html) - [Electrum Protocol Changelog](https://electrumx.readthedocs.io/en/latest/protocol-changes.html) - [libbitcoin-server GitHub](https://github.com/libbitcoin/libbitcoin-server) diff --git a/endpoints/test_bitcoind_rest.py b/endpoints/test_bitcoind_rest.py index c82475bb..fcfbff5f 100644 --- a/endpoints/test_bitcoind_rest.py +++ b/endpoints/test_bitcoind_rest.py @@ -1,7 +1,7 @@ """ Tests for libbitcoin-server bitcoind REST compatibility interface. -Covers the Bitcoin Core REST endpoints served under /rest/: chaininfo, block, +Covers the bitcoind REST endpoints served under /rest/: chaininfo, block, block/notxdetails, block/spent (libbitcoin extension), blockhashbyheight, headers, blockpart (libbitcoin extension), and the basic (neutrino) filters. diff --git a/endpoints/test_bitcoind_rpc.py b/endpoints/test_bitcoind_rpc.py index 70f5e7c9..68923927 100644 --- a/endpoints/test_bitcoind_rpc.py +++ b/endpoints/test_bitcoind_rpc.py @@ -1,7 +1,7 @@ """ Tests for libbitcoin-server bitcoind RPC compatibility interface. -Tests the JSON-RPC 2.0 interface that provides Bitcoin Core compatible +Tests the JSON-RPC 2.0 interface that provides bitcoind compatible RPC methods for blockchain queries. Run with: diff --git a/endpoints/test_btcd_rpc.py b/endpoints/test_btcd_rpc.py index bac51d40..84587670 100644 --- a/endpoints/test_btcd_rpc.py +++ b/endpoints/test_btcd_rpc.py @@ -3,7 +3,7 @@ btcd speaks JSON-RPC 1.0 over a persistent websocket connection (preferred, required for the session/notification extension methods) or plain HTTP POST -(same request/response shape as the bitcoind endpoint, for the chain methods +(same request/response shape as the bitcoind endpoint, for the bitcoind interface methods inherited from protocol_bitcoind_rpc). This suite is split into what's real today and what's a development target: @@ -258,7 +258,7 @@ def read_notification(self, timeout_s: float) -> Optional[dict]: def http_rpc(config: dict, method: str, params: Optional[list] = None) -> dict: """Plain http post json-rpc call to the same btcd endpoint (no ws upgrade). - The chain methods inherited from protocol_bitcoind_rpc are reachable this + The bitcoind interface methods inherited from protocol_bitcoind_rpc are reachable this way today even though they aren't yet bridged into the ws dispatcher. """ payload = { @@ -568,7 +568,7 @@ def test_filteredblockconnected_notification(conn, btcd_config): # ═══════════════════════════════════════════════════════════════════════════════ -# STANDARD CHAIN METHODS (bridged into the ws dispatcher) +# BITCOIND INTERFACE METHODS (bridged into the ws dispatcher) # ═══════════════════════════════════════════════════════════════════════════════ # Inherited from protocol_bitcoind_rpc. Reachable both over plain http post # (unchanged from bitcoind) and over the same ws connection used @@ -579,7 +579,7 @@ def test_filteredblockconnected_notification(conn, btcd_config): # to ws, so authenticate/session/notifyblocks and getblockcount etc. must all # work on the one connection. -CHAIN_METHODS = [ +BITCOIND_METHODS = [ ("getbestblockhash", []), ("getblockcount", []), ("getblockhash", [ReferenceData.GENESIS_HEIGHT]), @@ -589,7 +589,7 @@ def test_filteredblockconnected_notification(conn, btcd_config): ] -@pytest.mark.parametrize("method,params", CHAIN_METHODS) +@pytest.mark.parametrize("method,params", BITCOIND_METHODS) def test_chain_method_over_http_post(btcd_config, method, params): """Positive control: the chain logic itself works, reachable over plain http post to the same endpoint (unchanged from bitcoind).""" @@ -599,9 +599,9 @@ def test_chain_method_over_http_post(btcd_config, method, params): assert "result" in data -@pytest.mark.parametrize("method,params", CHAIN_METHODS) +@pytest.mark.parametrize("method,params", BITCOIND_METHODS) def test_chain_method_over_websocket(conn, method, params): - """Standard chain methods now also work over the same ws connection used + """bitcoind interface methods also work over the same ws connection used for session/notifyblocks/etc (see protocol_btcd_rpc::dispatch_websocket / protocol_bitcoind_rpc::dispatch_rpc).""" response = conn.send_rpc(method, params) @@ -611,7 +611,7 @@ def test_chain_method_over_websocket(conn, method, params): def test_btcd_and_chain_method_share_one_websocket_connection(conn): """The actual point of the ws bridge: a single persistent ws connection -- the kind a real lnd/btcwallet client opens once and keeps -- can reach both a - btcd-only extension method (session) and a standard chain method + btcd-only extension method (session) and a bitcoind interface method (getblockcount) without reconnecting or falling back to plain http post. """ session = conn.send_rpc("session") @@ -704,7 +704,7 @@ def test_btcd_only_method_reachable_over_http_post(btcd_config): protocol_bitcoind_rpc::dispatch_extension (an override point, protocol_btcd_rpc's implementation tries btcd_dispatcher_) -- this is the post-side mirror of dispatch_websocket's existing ws-side fallback - to the inherited chain dispatcher.""" + to the inherited bitcoind dispatcher.""" data = http_rpc(btcd_config, "getinfo") assert data.get("error") is None assert isinstance(data.get("result"), dict) diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp index eb331e00..dc4ad97c 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp @@ -61,7 +61,7 @@ class BCS_API protocol_bitcoind_rpc void handle_receive_post(const code& ec, const post::cptr& post) NOEXCEPT override; - /// Dispatch a chain-rpc message with no post-http context (websocket). + /// Dispatch an rpc message with no post-http context (websocket). /// Returns error::unexpected_method on a method-name lookup miss. code dispatch_rpc(const network::rpc::request_t& message) NOEXCEPT; diff --git a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp index feb6c8d3..4874fcda 100644 --- a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp @@ -31,7 +31,7 @@ namespace libbitcoin { namespace server { -/// btcd-compatible json-rpc-v1 over http/ws. Inherits the standard chain +/// btcd-compatible json-rpc-v1 over http/ws. Inherits the bitcoind interface /// handlers (getblockcount etc.) from protocol_bitcoind_rpc and adds the /// btcd-only extension methods via a second dispatcher. Both dispatchers are /// bridged in both directions, so all methods are reachable over both @@ -67,7 +67,7 @@ class BCS_API protocol_btcd_rpc protected: /// Dispatch btcd ws frames: tries the btcd-only dispatcher first, then - /// falls back to the inherited chain dispatcher (dispatch_rpc). + /// falls back to the inherited bitcoind dispatcher (dispatch_rpc). void dispatch_websocket( const network::http::request& request) NOEXCEPT override; diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index 79856446..3b0672ad 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -179,7 +179,7 @@ code protocol_bitcoind_rpc::dispatch_extension(const request_t&) NOEXCEPT return network::error::unexpected_method; } -// Dispatch a chain-rpc message with no post-http context (websocket) -- the +// Dispatch an rpc message with no post-http context (websocket) -- the // post-specific host/origin/body checks are meaningless for a ws frame. code protocol_bitcoind_rpc::dispatch_rpc(const request_t& message) NOEXCEPT { diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd_rpc.cpp index dc27f04e..3aedb608 100644 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc.cpp @@ -82,7 +82,7 @@ void protocol_btcd_rpc::start() NOEXCEPT SUBSCRIBE_BTCD(handle_stop_notify_spent, _1, _2, _3); SUBSCRIBE_BTCD(handle_rescan, _1, _2, _3, _4, _5, _6); - // Base registers all standard chain rpc handlers and starts the listener. + // Base registers all bitcoind interface handlers and starts the listener. protocol_bitcoind_rpc::start(); } @@ -145,7 +145,7 @@ void protocol_btcd_rpc::dispatch_websocket( return; } - // Fall back to the inherited chain dispatcher on a method-name lookup + // Fall back to the inherited bitcoind dispatcher on a method-name lookup // miss (notify never returns unexpected_method for an argument mismatch, // so the fallback cannot mask a real handler error). auto code = btcd_dispatcher_.notify(message); diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index fefbf828..4d70bd7d 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -112,7 +112,7 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyblocks__subscribed__null_result) REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); } -// Standard chain methods (bridged into the ws dispatcher) +// bitcoind interface methods (bridged into the ws dispatcher) // ---------------------------------------------------------------------------- BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__ten_block_store__nine) From 536c388ba0e3a94f6c8e8ebd29f775ba7be2f4b9 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 01:55:52 -0400 Subject: [PATCH 09/29] Add websocket support to bitcoind service. --- .../protocols/protocol_bitcoind_rpc.hpp | 33 +++--- .../bitcoind/protocol_bitcoind_rpc.cpp | 108 ++++++++++++++---- 2 files changed, 103 insertions(+), 38 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp index dc4ad97c..643e6357 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp @@ -55,21 +55,13 @@ class BCS_API protocol_bitcoind_rpc using post = network::http::method::post; using options = network::http::method::options; - /// Dispatch. + /// Dispatch (the post and websocket transports of the interface). void handle_receive_options(const code& ec, const network::http::method::options::cptr& options) NOEXCEPT override; void handle_receive_post(const code& ec, const post::cptr& post) NOEXCEPT override; - - /// Dispatch an rpc message with no post-http context (websocket). - /// Returns error::unexpected_method on a method-name lookup miss. - code dispatch_rpc(const network::rpc::request_t& message) NOEXCEPT; - - /// Extension point for a derived class's own additional dispatcher, - /// tried by handle_receive_post when rpc_dispatcher_ reports - /// unexpected_method. Default: always unexpected_method. - virtual code dispatch_extension( - const network::rpc::request_t& message) NOEXCEPT; + void dispatch_websocket( + const network::http::request& request) NOEXCEPT override; /// Handlers. bool handle_get_best_block_hash(const code& ec, @@ -146,15 +138,25 @@ class BCS_API protocol_bitcoind_rpc const system::chain::header& header) NOEXCEPT; static std::string chain_name(const node::query& query) NOEXCEPT; - /// Senders. + /// Senders. close_reason (if truthy) stops the channel only once the + /// write has completed, so the error reaches the client first. void send_error(const code& ec) NOEXCEPT; void send_error(const code& ec, size_t size_hint) NOEXCEPT; + void send_error(const code& ec, size_t size_hint, + const code& close_reason) NOEXCEPT; void send_error(const code& ec, network::rpc::value_option&& error, size_t size_hint) NOEXCEPT; void send_text(std::string&& hexidecimal) NOEXCEPT; void send_result(network::rpc::value_option&& result, size_t size_hint) NOEXCEPT; + /// Cache rpc response context for serialization (requires strand). The + /// websocket overload has no http request to echo headers from. + void set_rpc_request(network::rpc::version version, + const network::rpc::id_option& id, + const network::http::request_cptr& request) NOEXCEPT; + void set_rpc_request(const network::rpc::request_t& message) NOEXCEPT; + private: template inline void subscribe(Method&& method, Args&&... args) NOEXCEPT @@ -165,11 +167,8 @@ class BCS_API protocol_bitcoind_rpc // Senders. void send_rpc(network::rpc::response_t&& model, size_t size_hint) NOEXCEPT; - - // Cache request for serialization (requires strand). - void set_rpc_request(network::rpc::version version, - const network::rpc::id_option& id, - const network::http::request_cptr& request) NOEXCEPT; + void send_rpc(network::rpc::response_t&& model, size_t size_hint, + const code& close_reason) NOEXCEPT; // Validate a transaction given next block context. bool get_pool_context(system::chain::context& pool) const NOEXCEPT; diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index 3b0672ad..0aaf03cf 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -157,12 +157,8 @@ void protocol_bitcoind_rpc::handle_receive_post(const code& ec, return; } - // Dispatch the request to subscribers, falling back to a derived class's - // own extension dispatcher if this one doesn't recognize the method. - auto code = rpc_dispatcher_.notify(message); - if (code == network::error::unexpected_method) - code = dispatch_extension(message); - + // Dispatch the request to the interface dispatcher. + const auto code = rpc_dispatcher_.notify(message); if (!code) return; @@ -173,20 +169,60 @@ void protocol_bitcoind_rpc::handle_receive_post(const code& ec, stop(code); } -// Default: no extension dispatcher (see protocol_btcd_rpc for an override). -code protocol_bitcoind_rpc::dispatch_extension(const request_t&) NOEXCEPT -{ - return network::error::unexpected_method; -} - -// Dispatch an rpc message with no post-http context (websocket) -- the -// post-specific host/origin/body checks are meaningless for a ws frame. -code protocol_bitcoind_rpc::dispatch_rpc(const request_t& message) NOEXCEPT +// The websocket transport of the interface: parse the ws text frame as a +// json-rpc request and dispatch as an http post would. +void protocol_bitcoind_rpc::dispatch_websocket( + const network::http::request& request) NOEXCEPT { BC_ASSERT(stranded()); - id_ = message.id; - version_ = message.jsonrpc; - return rpc_dispatcher_.notify(message); + + // ws frames carry no headers, so ws authorization is enforced here, not + // by the channel (established by basic auth on the upgrade request). + if (!authorized()) + { + stop(network::error::unauthorized); + return; + } + + // Websocket frames are json-rpc text (channel default reader). + if (!request.body().contains()) + { + stop(error::invalid_argument); + return; + } + + request_t message{}; + try + { + message = value_to(parse( + request.body().get())); + } + catch (const std::exception&) + { + stop(error::invalid_argument); + return; + } + + // Cache request context for response building (version + id). + set_rpc_request(message); + + // The credential may be restricted to a subset of interface methods. + if (!permitted(message.method)) + { + send_error(error::unauthorized); + return; + } + + // Dispatch the request to the interface dispatcher. + const auto code = rpc_dispatcher_.notify(message); + if (!code) + return; + + // Unknown method: reply with an error and keep the connection alive. + if (code == network::error::unexpected_method) + send_error(code); + else + stop(code); } // Handlers. @@ -721,6 +757,22 @@ void protocol_bitcoind_rpc::send_error(const code& ec, send_error(ec, {}, size_hint); } +void protocol_bitcoind_rpc::send_error(const code& ec, size_t size_hint, + const code& close_reason) NOEXCEPT +{ + BC_ASSERT(stranded()); + send_rpc( + { + .jsonrpc = version_, + .id = id_, + .error = result_t + { + .code = ec.value(), + .message = ec.message() + } + }, size_hint, close_reason); +} + void protocol_bitcoind_rpc::send_error(const code& ec, value_option&& error, size_t size_hint) NOEXCEPT { @@ -759,6 +811,12 @@ void protocol_bitcoind_rpc::send_result(value_option&& result, // private void protocol_bitcoind_rpc::send_rpc(response_t&& model, size_t size_hint) NOEXCEPT +{ + send_rpc(std::move(model), size_hint, error::success); +} + +void protocol_bitcoind_rpc::send_rpc(response_t&& model, size_t size_hint, + const code& close_reason) NOEXCEPT { BC_ASSERT(stranded()); using namespace http; @@ -777,7 +835,7 @@ void protocol_bitcoind_rpc::send_rpc(response_t&& model, { .size_hint = size_hint }, std::move(model), }; message.prepare_payload(); - SEND(std::move(message), handle_complete, _1, error::success); + SEND(std::move(message), handle_complete, _1, close_reason); return; } @@ -791,10 +849,10 @@ void protocol_bitcoind_rpc::send_rpc(response_t&& model, { .size_hint = size_hint }, std::move(model), }; message.prepare_payload(); - SEND(std::move(message), handle_complete, _1, error::success); + SEND(std::move(message), handle_complete, _1, close_reason); } -// private +// protected void protocol_bitcoind_rpc::set_rpc_request(version version, const id_option& id, const http::request_cptr& request) NOEXCEPT { @@ -804,6 +862,14 @@ void protocol_bitcoind_rpc::set_rpc_request(version version, set_request(request); } +// protected +void protocol_bitcoind_rpc::set_rpc_request(const request_t& message) NOEXCEPT +{ + BC_ASSERT(stranded()); + id_ = message.id; + version_ = message.jsonrpc; +} + // private http::request_cptr protocol_bitcoind_rpc::reset_rpc_request() NOEXCEPT { From 955e09e0d1c24633cd4a436112447e3d6f4ab9bd Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 02:14:11 -0400 Subject: [PATCH 10/29] btcd: normalize inheritance and dispatch design. --- endpoints/test_btcd_rpc.py | 17 +- include/bitcoin/server/interfaces/btcd.hpp | 2 +- .../server/protocols/protocol_btcd_rpc.hpp | 46 ++-- src/protocols/btcd/protocol_btcd_rpc.cpp | 221 ++++++++---------- .../btcd/protocol_btcd_rpc_filter.cpp | 16 +- .../btcd/protocol_btcd_rpc_utility.cpp | 46 ++-- test/protocols/btcd/btcd_rpc.cpp | 10 +- 7 files changed, 157 insertions(+), 201 deletions(-) diff --git a/endpoints/test_btcd_rpc.py b/endpoints/test_btcd_rpc.py index 84587670..70e7d72c 100644 --- a/endpoints/test_btcd_rpc.py +++ b/endpoints/test_btcd_rpc.py @@ -692,19 +692,10 @@ def test_getinfo_returns_object(conn): def test_btcd_only_method_reachable_over_http_post(btcd_config): - """A real lnd integration test found that btcwallet's chain.RPCClient - issues capability-check calls (getblockchaininfo, then getinfo) over - plain http post immediately on connect, before any subscription traffic - that requires ws. getblockchaininfo (inherited from protocol_bitcoind_rpc) - already worked over post; getinfo (a btcd-only method, previously only - reachable via the ws dispatcher) did not -- protocol_bitcoind_rpc:: - handle_receive_post had no fallback to a derived class's own dispatcher, - and unconditionally dropped the connection on any unrecognized method, - so lnd's client retried forever without ever succeeding. Fixed via - protocol_bitcoind_rpc::dispatch_extension (an override point, - protocol_btcd_rpc's implementation tries btcd_dispatcher_) -- this is - the post-side mirror of dispatch_websocket's existing ws-side fallback - to the inherited bitcoind dispatcher.""" + """Real clients (btcwallet's chain.RPCClient) issue capability-check + calls (getblockchaininfo, then getinfo) over plain http post immediately + on connect, before any subscription traffic that requires ws. getinfo is + btcd-only, so this exercises the btcd post transport.""" data = http_rpc(btcd_config, "getinfo") assert data.get("error") is None assert isinstance(data.get("result"), dict) diff --git a/include/bitcoin/server/interfaces/btcd.hpp b/include/bitcoin/server/interfaces/btcd.hpp index 09b236be..835a6785 100644 --- a/include/bitcoin/server/interfaces/btcd.hpp +++ b/include/bitcoin/server/interfaces/btcd.hpp @@ -28,7 +28,7 @@ namespace interface { struct btcd_methods { - /// BTCD protocol (unversioned, json-rpc v1.0 over ws/s). + /// BTCD protocol (unversioned, json-rpc v1.0 over http/ws). static constexpr std::tuple methods { /// Authentication (one-time, in-band alternative to basic auth). diff --git a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp index 4874fcda..6d169e9a 100644 --- a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp @@ -31,13 +31,12 @@ namespace libbitcoin { namespace server { -/// btcd-compatible json-rpc-v1 over http/ws. Inherits the bitcoind interface -/// handlers (getblockcount etc.) from protocol_bitcoind_rpc and adds the -/// btcd-only extension methods via a second dispatcher. Both dispatchers are -/// bridged in both directions, so all methods are reachable over both -/// transports. ws authorization is established once per connection, by basic -/// auth on the ws upgrade request or by the in-band 'authenticate' method, -/// and latched on the channel (see dispatch_websocket). +/// btcd-compatible json-rpc-v1 over http/ws. Adds the btcd extension +/// interface to the inherited bitcoind interface, each verb handler trying +/// its own dispatcher and deferring to the base transport handler on a miss. +/// ws authorization is established once per connection, by basic auth on the +/// ws upgrade request or by the in-band 'authenticate' method, and latched +/// on the channel (see dispatch_websocket). class BCS_API protocol_btcd_rpc : public server::protocol_bitcoind_rpc, protected network::tracker @@ -66,17 +65,14 @@ class BCS_API protocol_btcd_rpc void stopping(const code& ec) NOEXCEPT override; protected: - /// Dispatch btcd ws frames: tries the btcd-only dispatcher first, then - /// falls back to the inherited bitcoind dispatcher (dispatch_rpc). + /// Dispatch (the post and websocket transports of the btcd extension + /// interface). Each defers to the base transport handler on a method + /// name miss (the interfaces have disjoint method names). + void handle_receive_post(const code& ec, + const post::cptr& post) NOEXCEPT override; void dispatch_websocket( const network::http::request& request) NOEXCEPT override; - /// Post-side mirror of the above: lets handle_receive_post reach the - /// btcd-only methods over plain http post too (real clients issue e.g. - /// getinfo as post-based capability checks before any ws traffic). - code dispatch_extension( - const network::rpc::request_t& message) NOEXCEPT override; - /// Handlers (authentication/admin). A failed authenticate ends the /// session, once the error response has reached the client (btcd closes /// the connection on invalid credentials). @@ -185,16 +181,8 @@ class BCS_API protocol_btcd_rpc void do_block_connected(node::header_t link) NOEXCEPT; void do_block_disconnected(node::header_t link) NOEXCEPT; - /// Senders (btcd envelope, distinct id/version cache from the inherited - /// senders). close_reason (if truthy) stops the channel only once the - /// write has completed, so the error reaches the client first. - void send_btcd_result(network::rpc::value_option&& result, - size_t size_hint) NOEXCEPT; - void send_btcd_error(const code& ec) NOEXCEPT; - void send_btcd_error(const code& ec, size_t size_hint) NOEXCEPT; - void send_btcd_error(const code& ec, size_t size_hint, - const code& close_reason) NOEXCEPT; - void send_btcd_notification(const std::string& method, + /// Sender (btcd server push, json-rpc-v1 request shape with no id). + void send_notification(const std::string& method, network::rpc::array_t&& params, size_t size_hint) NOEXCEPT; private: @@ -204,20 +192,14 @@ class BCS_API protocol_btcd_rpc btcd_dispatcher_.subscribe(BIND_SHARED(method, args)); } - // Senders. - void send_btcd_rpc(network::rpc::response_t&& model, size_t size_hint, - const code& close_reason) NOEXCEPT; - // These are thread safe. std::atomic_bool subscribed_blocks_{}; const options_t& options_; const uint8_t p2kh_; const uint8_t p2sh_; - // These are protected by strand. + // This is protected by strand. btcd_dispatcher btcd_dispatcher_{}; - network::rpc::version btcd_version_{}; - network::rpc::id_option btcd_id_{}; // Tx filter (loadtxfilter). Hash sets are kept per script template so a // p2kh watch never matches a p2wpkh output or vice versa. diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd_rpc.cpp index 3aedb608..b0e452ac 100644 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc.cpp @@ -82,7 +82,7 @@ void protocol_btcd_rpc::start() NOEXCEPT SUBSCRIBE_BTCD(handle_stop_notify_spent, _1, _2, _3); SUBSCRIBE_BTCD(handle_rescan, _1, _2, _3, _4, _5, _6); - // Base registers all bitcoind interface handlers and starts the listener. + // Base registers the bitcoind interface handlers and starts the listener. protocol_bitcoind_rpc::start(); } @@ -94,23 +94,77 @@ void protocol_btcd_rpc::stopping(const code& ec) NOEXCEPT protocol_bitcoind_rpc::stopping(ec); } -// Websocket dispatch. +// Dispatch (the post and websocket transports of the btcd interface). // ---------------------------------------------------------------------------- -// ws frames carry no headers, so ws authorization is enforced here, not by -// the channel. It is established at most once per connection, by basic auth -// on the upgrade request or by exactly one authenticate call -- so a call is -// invalid if authorized and the method is authenticate, or not authorized +// ws authorization is established at most once per connection, by basic auth +// on the upgrade request or by exactly one authenticate call -- so a ws call +// is invalid if authorized and the method is authenticate, or not authorized // and the method is not authenticate (as btcd). +void protocol_btcd_rpc::handle_receive_post(const code& ec, + const post::cptr& post) NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (stopped(ec)) + return; + + // Enforce http host header (if any hosts are configured). + if (!is_allowed_host(*post, post->version())) + { + send_bad_host(*post); + return; + } + + // Enforce http origin policy (if any origins are configured). + if (!is_allowed_origin(*post, post->version())) + { + send_forbidden(*post); + return; + } + + // Endpoint accepts only json-rpc posts. + if (!post->body().contains()) + { + send_bad_request(*post); + return; + } + + const auto& message = post->body().get().message; + set_rpc_request(message.jsonrpc, message.id, post); + + // The credential may be restricted to a subset of interface methods. + if (!permitted(message.method)) + { + send_error(error::unauthorized); + return; + } + + // authenticate is websocket-only (as btcd): excluded from the post + // surface, deferring to the base miss (method not found). + const auto code = (message.method == btcd_interface::authenticate::name) ? + network::error::unexpected_method : btcd_dispatcher_.notify(message); + + // Defer to the base post transport on a method name miss. + if (code == network::error::unexpected_method) + { + protocol_bitcoind_rpc::handle_receive_post(ec, post); + return; + } + + if (code) + stop(code); +} + void protocol_btcd_rpc::dispatch_websocket( const network::http::request& request) NOEXCEPT { BC_ASSERT(stranded()); - // btcd websocket frames are json-rpc text (channel default reader). + // Websocket frames are json-rpc text (channel default reader). if (!request.body().contains()) { - send_btcd_error(error::invalid_argument); + stop(error::invalid_argument); return; } @@ -122,57 +176,41 @@ void protocol_btcd_rpc::dispatch_websocket( } catch (const std::exception&) { - send_btcd_error(error::invalid_argument); + stop(error::invalid_argument); return; } // Cache request context for response building (version + id). - btcd_version_ = message.jsonrpc; - btcd_id_ = message.id; + set_rpc_request(message); const auto authenticate = (message.method == btcd_interface::authenticate::name); if (authenticate == authorized()) { - send_btcd_error(network::error::unauthorized); + send_error(network::error::unauthorized); return; } if (!authenticate && !permitted(message.method)) { - send_btcd_error(network::error::unauthorized); + send_error(network::error::unauthorized); return; } - // Fall back to the inherited bitcoind dispatcher on a method-name lookup - // miss (notify never returns unexpected_method for an argument mismatch, - // so the fallback cannot mask a real handler error). - auto code = btcd_dispatcher_.notify(message); - if (code == network::error::unexpected_method) - code = dispatch_rpc(message); + const auto code = btcd_dispatcher_.notify(message); - if (!code) + // Defer to the base websocket transport on a method name miss. + if (code == network::error::unexpected_method) + { + protocol_bitcoind_rpc::dispatch_websocket(request); return; + } - // Unknown method: reply with an error and keep the connection alive. - if (code == network::error::unexpected_method) - send_btcd_error(code); - else + if (code) stop(code); } -// Post-side mirror of the above: reached from the inherited -// handle_receive_post, so btcd-only methods are also callable over post. -code protocol_btcd_rpc::dispatch_extension( - const network::rpc::request_t& message) NOEXCEPT -{ - BC_ASSERT(stranded()); - btcd_version_ = message.jsonrpc; - btcd_id_ = message.id; - return btcd_dispatcher_.notify(message); -} - // Handlers (authentication/admin). // ---------------------------------------------------------------------------- @@ -188,14 +226,14 @@ bool protocol_btcd_rpc::handle_authenticate(const code& ec, set_authorized(network::config::credential::to_digest(username, password)); if (authorized()) { - send_btcd_result({}, 4); + send_result({}, 4); return true; } // A failed authenticate ends the session (as btcd), once the error has // reached the client. const code unauthorized{ network::error::unauthorized }; - send_btcd_error(unauthorized, two * unauthorized.message().size(), + send_error(unauthorized, two * unauthorized.message().size(), unauthorized); return true; } @@ -210,7 +248,7 @@ bool protocol_btcd_rpc::handle_session(const code& ec, // lifetime, satisfying btcwallet's reconnect-detection use of session ids. object_t result{}; result.emplace("id", value_t{ possible_sign_cast(identifier()) }); - send_btcd_result(value_t{ std::move(result) }, 32); + send_result(value_t{ std::move(result) }, 32); return true; } @@ -222,7 +260,7 @@ bool protocol_btcd_rpc::handle_get_current_net(const code& ec, // The p2p handshake magic, the same value btcd returns (checked once by // btcwallet/lnd at connect to confirm the expected network). - send_btcd_result( + send_result( value_t{ possible_sign_cast(network_settings().identifier) }, 20); return true; @@ -240,7 +278,7 @@ bool protocol_btcd_rpc::handle_get_best_block(const code& ec, result.emplace("hash", value_t{ encode_hash(query.get_top_confirmed_hash()) }); result.emplace("height", value_t{ possible_sign_cast(query.get_top_confirmed()) }); - send_btcd_result(value_t{ std::move(result) }, 96); + send_result(value_t{ std::move(result) }, 96); return true; } @@ -254,7 +292,7 @@ bool protocol_btcd_rpc::handle_notify_blocks(const code& ec, return false; subscribed_blocks_.store(true, std::memory_order_relaxed); - send_btcd_result({}, 4); + send_result({}, 4); return true; } @@ -265,7 +303,7 @@ bool protocol_btcd_rpc::handle_stop_notify_blocks(const code& ec, return false; subscribed_blocks_.store(false, std::memory_order_relaxed); - send_btcd_result({}, 4); + send_result({}, 4); return true; } @@ -276,7 +314,7 @@ bool protocol_btcd_rpc::handle_notify_new_transactions(const code& ec, btcd_interface::notify_new_transactions, bool) NOEXCEPT { if (stopped(ec)) return false; - send_btcd_error(error::not_implemented); + send_error(error::not_implemented); return true; } @@ -284,7 +322,7 @@ bool protocol_btcd_rpc::handle_stop_notify_new_transactions(const code& ec, btcd_interface::stop_notify_new_transactions) NOEXCEPT { if (stopped(ec)) return false; - send_btcd_error(error::not_implemented); + send_error(error::not_implemented); return true; } @@ -301,7 +339,7 @@ bool protocol_btcd_rpc::handle_stop(const code& ec, // No secure remote-shutdown path exists; always rejected regardless of // configuration or auth state. - send_btcd_error(error::not_implemented); + send_error(error::not_implemented); return true; } @@ -312,7 +350,7 @@ bool protocol_btcd_rpc::handle_notify_received(const code& ec, btcd_interface::notify_received, const value_t&) NOEXCEPT { if (stopped(ec)) return false; - send_btcd_error(error::not_implemented); + send_error(error::not_implemented); return true; } @@ -320,7 +358,7 @@ bool protocol_btcd_rpc::handle_stop_notify_received(const code& ec, btcd_interface::stop_notify_received, const value_t&) NOEXCEPT { if (stopped(ec)) return false; - send_btcd_error(error::not_implemented); + send_error(error::not_implemented); return true; } @@ -328,7 +366,7 @@ bool protocol_btcd_rpc::handle_notify_spent(const code& ec, btcd_interface::notify_spent, const value_t&) NOEXCEPT { if (stopped(ec)) return false; - send_btcd_error(error::not_implemented); + send_error(error::not_implemented); return true; } @@ -336,7 +374,7 @@ bool protocol_btcd_rpc::handle_stop_notify_spent(const code& ec, btcd_interface::stop_notify_spent, const value_t&) NOEXCEPT { if (stopped(ec)) return false; - send_btcd_error(error::not_implemented); + send_error(error::not_implemented); return true; } @@ -355,14 +393,14 @@ bool protocol_btcd_rpc::handle_rescan(const code& ec, hash_digest begin_hash{}; if (!decode_hash(begin_hash, beginblock)) { - send_btcd_error(error::not_found, two * beginblock.size()); + send_error(error::not_found, two * beginblock.size()); return true; } const auto& query = archive(); if (query.to_header(begin_hash).is_terminal()) { - send_btcd_error(error::not_found, two * beginblock.size()); + send_error(error::not_found, two * beginblock.size()); return true; } @@ -373,7 +411,7 @@ bool protocol_btcd_rpc::handle_rescan(const code& ec, if (has_addresses || has_outpoints) { - send_btcd_error(error::not_implemented); + send_error(error::not_implemented); return true; } @@ -383,19 +421,19 @@ bool protocol_btcd_rpc::handle_rescan(const code& ec, const auto header = query.get_header(query.to_confirmed(top)); if (!header) { - send_btcd_error(error::not_found); + send_error(error::not_found); return true; } // Reply to the rescan call itself before the unprompted notification. - send_btcd_result({}, 4); + send_result({}, 4); array_t params{}; params.emplace_back(value_t{ encode_hash(query.get_top_confirmed_hash()) }); params.emplace_back(value_t{ possible_sign_cast(top) }); params.emplace_back(value_t{ possible_sign_cast(header->timestamp()) }); - send_btcd_notification("rescanfinished", std::move(params), 256); + send_notification("rescanfinished", std::move(params), 256); return true; } @@ -459,7 +497,7 @@ void protocol_btcd_rpc::do_block_connected(node::header_t link_value) NOEXCEPT params.emplace_back(value_t{ possible_sign_cast( header->timestamp()) }); - send_btcd_notification("blockconnected", std::move(params), 256); + send_notification("blockconnected", std::move(params), 256); // btcd 'filteredblockconnected': [height, header, subscribedtxs]. Sent // unconditionally alongside blockconnected (as btcd) -- an empty/never- @@ -476,7 +514,7 @@ void protocol_btcd_rpc::do_block_connected(node::header_t link_value) NOEXCEPT to_text(*header, chain::header::serialized_size()) }); filtered_params.emplace_back(value_t{ match_filtered_transactions(*block) }); - send_btcd_notification("filteredblockconnected", + send_notification("filteredblockconnected", std::move(filtered_params), 256); } @@ -505,7 +543,7 @@ void protocol_btcd_rpc::do_block_disconnected( params.emplace_back(value_t{ possible_sign_cast( header->timestamp()) }); - send_btcd_notification("blockdisconnected", std::move(params), 256); + send_notification("blockdisconnected", std::move(params), 256); // btcd 'filteredblockdisconnected': [height, header]. array_t filtered_params{}; @@ -514,53 +552,14 @@ void protocol_btcd_rpc::do_block_disconnected( filtered_params.emplace_back(value_t{ to_text(*header, chain::header::serialized_size()) }); - send_btcd_notification("filteredblockdisconnected", + send_notification("filteredblockdisconnected", std::move(filtered_params), 256); } // Senders. // ---------------------------------------------------------------------------- -void protocol_btcd_rpc::send_btcd_result(value_option&& result, - size_t size_hint) NOEXCEPT -{ - BC_ASSERT(stranded()); - send_btcd_rpc( - { - .jsonrpc = btcd_version_, - .id = btcd_id_, - .result = std::move(result) - }, size_hint, error::success); -} - -void protocol_btcd_rpc::send_btcd_error(const code& ec) NOEXCEPT -{ - send_btcd_error(ec, two * ec.message().size()); -} - -void protocol_btcd_rpc::send_btcd_error(const code& ec, - size_t size_hint) NOEXCEPT -{ - send_btcd_error(ec, size_hint, error::success); -} - -void protocol_btcd_rpc::send_btcd_error(const code& ec, size_t size_hint, - const code& close_reason) NOEXCEPT -{ - BC_ASSERT(stranded()); - send_btcd_rpc( - { - .jsonrpc = btcd_version_, - .id = btcd_id_, - .error = result_t - { - .code = ec.value(), - .message = ec.message() - } - }, size_hint, close_reason); -} - -void protocol_btcd_rpc::send_btcd_notification(const std::string& method, +void protocol_btcd_rpc::send_notification(const std::string& method, array_t&& params, size_t size_hint) NOEXCEPT { BC_ASSERT(stranded()); @@ -582,30 +581,6 @@ void protocol_btcd_rpc::send_btcd_notification(const std::string& method, NOTIFY(std::move(message), handle_complete, _1, error::success); } -// private -void protocol_btcd_rpc::send_btcd_rpc(response_t&& model, size_t size_hint, - const code& close_reason) NOEXCEPT -{ - BC_ASSERT(stranded()); - using namespace network::http; - static const auto json = from_media_type(media_type::application_json); - - btcd_id_.reset(); - btcd_version_ = version::undefined; - - http::response message{ status::ok, 11 }; - message.set(field::content_type, json); - message.body() = rpc::response - { - { .size_hint = size_hint }, std::move(model), - }; - message.prepare_payload(); - - // handle_complete stops (if close_reason is truthy) only once the write - // has completed; SEND restarts the ws reader on success. - SEND(std::move(message), handle_complete, _1, close_reason); -} - BC_POP_WARNING() BC_POP_WARNING() BC_POP_WARNING() diff --git a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp index fbd05779..52edcb6a 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp @@ -148,17 +148,17 @@ bool protocol_btcd_rpc::handle_load_tx_filter(const code& ec, if (const auto fault = parse_filter_addresses(reload, addresses)) { - send_btcd_error(fault); + send_error(fault); return true; } if (const auto fault = parse_filter_outpoints(reload, outpoints)) { - send_btcd_error(fault); + send_error(fault); return true; } - send_btcd_result({}, 4); + send_result({}, 4); return true; } @@ -170,7 +170,7 @@ bool protocol_btcd_rpc::handle_rescan_blocks(const code& ec, if (!std::holds_alternative(blockhashes.value())) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } @@ -182,7 +182,7 @@ bool protocol_btcd_rpc::handle_rescan_blocks(const code& ec, { if (!std::holds_alternative(item.value())) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } @@ -190,14 +190,14 @@ bool protocol_btcd_rpc::handle_rescan_blocks(const code& ec, hash_digest hash{}; if (!decode_hash(hash, text)) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } const auto block = query.get_block(query.to_header(hash), witness); if (!block) { - send_btcd_error(error::not_found); + send_error(error::not_found); return true; } @@ -212,7 +212,7 @@ bool protocol_btcd_rpc::handle_rescan_blocks(const code& ec, } } - send_btcd_result(value_t{ std::move(discovered) }, 256); + send_result(value_t{ std::move(discovered) }, 256); return true; } diff --git a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp b/src/protocols/btcd/protocol_btcd_rpc_utility.cpp index cee982e3..e064b93d 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc_utility.cpp @@ -51,11 +51,11 @@ bool protocol_btcd_rpc::handle_get_difficulty(const code& ec, query.get_top_confirmed())); if (!header) { - send_btcd_error(database::error::integrity); + send_error(database::error::integrity); return true; } - send_btcd_result(header->difficulty(), 20); + send_result(header->difficulty(), 20); return true; } @@ -70,13 +70,13 @@ bool protocol_btcd_rpc::handle_get_info(const code& ec, const auto header = query.get_header(query.to_confirmed(top)); if (!header) { - send_btcd_error(database::error::integrity); + send_error(database::error::integrity); return true; } // Untracked peer-dependent fields are reported as empty/zero, matching // the inherited getnetworkinfo handler's convention. - send_btcd_result(object_t + send_result(object_t { { "version", 0 }, { "protocolversion", 70016 }, @@ -99,7 +99,7 @@ bool protocol_btcd_rpc::handle_get_net_totals(const code& ec, return false; // Untracked byte counters are reported as zero; timemillis is real. - send_btcd_result(object_t + send_result(object_t { { "totalbytesrecv", 0 }, { "totalbytessent", 0 }, @@ -124,13 +124,13 @@ bool protocol_btcd_rpc::handle_get_network_hash_ps(const code& ec, const auto header = query.get_header(query.to_confirmed(target)); if (!header) { - send_btcd_error(database::error::integrity); + send_error(database::error::integrity); return true; } constexpr auto average_seconds_per_block = 600.0; constexpr auto two_to_the_32 = 4294967296.0; - send_btcd_result( + send_result( header->difficulty() * two_to_the_32 / average_seconds_per_block, 20); return true; } @@ -171,7 +171,7 @@ bool protocol_btcd_rpc::handle_create_raw_transaction(const code& ec, { if (!std::holds_alternative(item.value())) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } @@ -182,7 +182,7 @@ bool protocol_btcd_rpc::handle_create_raw_transaction(const code& ec, !std::holds_alternative(txid_it->second.value()) || !std::holds_alternative(vout_it->second.value())) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } @@ -191,7 +191,7 @@ bool protocol_btcd_rpc::handle_create_raw_transaction(const code& ec, if (!decode_hash(hash, std::get(txid_it->second.value())) || !to_integer(vout, std::get(vout_it->second.value()))) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } @@ -205,14 +205,14 @@ bool protocol_btcd_rpc::handle_create_raw_transaction(const code& ec, { if (!std::holds_alternative(pair.second.value())) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } chain::script script{}; if (const auto fault = parse_output_script(pair.first, script)) { - send_btcd_error(fault); + send_error(fault); return true; } @@ -227,7 +227,7 @@ bool protocol_btcd_rpc::handle_create_raw_transaction(const code& ec, // Unsigned, so no witness data yet -- plain (non-witness) serialization. constexpr auto witness = false; - send_btcd_result(to_text(tx, tx.serialized_size(witness), witness), 400); + send_result(to_text(tx, tx.serialized_size(witness), witness), 400); return true; } @@ -241,7 +241,7 @@ bool protocol_btcd_rpc::handle_decode_raw_transaction(const code& ec, data_chunk data{}; if (!decode_base16(data, hexstring)) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } @@ -249,13 +249,13 @@ bool protocol_btcd_rpc::handle_decode_raw_transaction(const code& ec, const chain::transaction tx{ data, witness }; if (!tx.is_valid()) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } // bitcoind(tx) alone (no inject_tx_context) is the bare // decoderawtransaction shape -- a standalone tx has no block context. - send_btcd_result(value_from(bitcoind(tx)), + send_result(value_from(bitcoind(tx)), two * tx.serialized_size(witness)); return true; } @@ -269,7 +269,7 @@ bool protocol_btcd_rpc::handle_decode_script(const code& ec, data_chunk data{}; if (!decode_base16(data, hex)) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } @@ -277,7 +277,7 @@ bool protocol_btcd_rpc::handle_decode_script(const code& ec, const chain::script script{ data, false }; if (!script.is_valid()) { - send_btcd_error(error::invalid_argument); + send_error(error::invalid_argument); return true; } @@ -331,7 +331,7 @@ bool protocol_btcd_rpc::handle_decode_script(const code& ec, if (wrapped) result.emplace("p2sh", value_t{ wrapped.encoded() }); - send_btcd_result(value_t{ std::move(result) }, 256); + send_result(value_t{ std::move(result) }, 256); return true; } @@ -344,7 +344,7 @@ bool protocol_btcd_rpc::handle_validate_address(const code& ec, const wallet::payment_address base58(address); if (base58) { - send_btcd_result(object_t + send_result(object_t { { "isvalid", true }, { "address", value_t{ base58.encoded() } }, @@ -357,7 +357,7 @@ bool protocol_btcd_rpc::handle_validate_address(const code& ec, const wallet::witness_address segwit(address); if (segwit) { - send_btcd_result(object_t + send_result(object_t { { "isvalid", true }, { "address", value_t{ segwit.encoded() } }, @@ -371,7 +371,7 @@ bool protocol_btcd_rpc::handle_validate_address(const code& ec, return true; } - send_btcd_result(object_t{ { "isvalid", false } }, 32); + send_result(object_t{ { "isvalid", false } }, 32); return true; } @@ -382,7 +382,7 @@ bool protocol_btcd_rpc::handle_help(const code& ec, btcd_interface::help, return false; // Lists method names only, no per-command argument usage text. - send_btcd_result(std::string{ + send_result(std::string{ "authenticate session getcurrentnet getbestblock notifyblocks " "stopnotifyblocks loadtxfilter rescanblocks getdifficulty getinfo " "getnettotals getnetworkhashps createrawtransaction " diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index 4d70bd7d..a025118e 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -87,12 +87,20 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__getbestblock__ten_block_store__block9) BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__http_post__dispatched) { - // getinfo is btcd-only, so this exercises dispatch_extension (real + // getinfo is btcd-only, so this exercises the btcd post transport (real // clients issue post-based capability checks before any ws traffic). const auto response = http_rpc("getinfo"); REQUIRE_NO_THROW_TRUE(response.at("result").as_object().contains("blocks")); } +BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__http_post__unexpected_method) +{ + // authenticate is websocket-only (as btcd): not part of the post surface. + const auto response = http_rpc("authenticate", R"(["user","pass"])"); + REQUIRE_NO_THROW_TRUE(response.at("error").is_object()); + BOOST_REQUIRE_EQUAL(response.at("error").at("code").as_int64(), unexpected_method.value()); +} + // block subscription // ---------------------------------------------------------------------------- From 7b0ba9516c9bb8c08d2951d4e7c529b0ee409e09 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 02:15:11 -0400 Subject: [PATCH 11/29] Add bitcoind websockets tests. --- test/protocols/bitcoind/bitcoind_rpc.cpp | 60 +++++++++++++++++++ .../bitcoind/bitcoind_setup_fixture.cpp | 50 +++++++++++++++- .../bitcoind/bitcoind_setup_fixture.hpp | 13 +++- 3 files changed, 121 insertions(+), 2 deletions(-) diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 632bb41f..36755f3c 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -304,6 +304,66 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__batch__empty__dropped) REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } +// websocket transport +// ---------------------------------------------------------------------------- +// The same interface dispatched over ws (dispatch_websocket): each frame is +// a json-rpc text message. + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockcount__websocket__nine) +{ + BOOST_REQUIRE(!ws_upgrade()); + + const auto response = ws_rpc("getblockcount"); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getbestblockhash__websocket__block9) +{ + BOOST_REQUIRE(!ws_upgrade()); + + const auto response = ws_rpc("getbestblockhash"); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), block9); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockhash__websocket__block5) +{ + BOOST_REQUIRE(!ws_upgrade()); + + const auto response = ws_rpc("getblockhash", "[5]"); + BOOST_REQUIRE_EQUAL(as_text(response.at("result")), block5); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__websocket__expected) +{ + BOOST_REQUIRE(!ws_upgrade()); + + const auto response = ws_rpc("getblockchaininfo"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result"); + BOOST_REQUIRE_EQUAL(as_text(result.at("chain")), "main"); + BOOST_REQUIRE_EQUAL(result.at("blocks").as_int64(), 9); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__unknown_method__websocket__error_keeps_connection) +{ + BOOST_REQUIRE(!ws_upgrade()); + + const auto unknown = ws_rpc("nosuchmethod"); + REQUIRE_NO_THROW_TRUE(unknown.at("error").is_object()); + + const auto response = ws_rpc("getblockcount"); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); +} + +BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) +{ + BOOST_REQUIRE(!ws_upgrade()); + + const auto response = ws_rpc("getblockcount"); + BOOST_REQUIRE_EQUAL(response.at("id").as_int64(), 0); +} + BOOST_AUTO_TEST_SUITE_END() // segwit (witness store: genesis + two blocks carrying witness transactions) diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp index 233fff8e..482e7411 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.cpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.cpp @@ -78,7 +78,26 @@ bitcoind_setup_fixture::bitcoind_setup_fixture(const initializer& setup) bitcoind_setup_fixture::~bitcoind_setup_fixture() { - socket_.close(); + if (websocket_.has_value()) + { + network::boost_code ec{}; + websocket_.value().close(websocket::close_code::normal, ec); + + // Expected and harmless during fixture teardown: + // beast::websocket::error::closed : normal (graceful handshake). + // asio::error::operation_aborted : hard (invalid request test). + if (ec && + ec != boost::beast::websocket::error::closed && + ec != boost::asio::error::operation_aborted) + { + BOOST_WARN_MESSAGE(false, ec.message()); + } + } + else + { + socket_.close(); + } + server_.close(); const auto ec = store_.close([](auto, auto){}); BOOST_WARN_MESSAGE(!ec, ec.message()); @@ -139,6 +158,35 @@ boost::json::value bitcoind_setup_fixture::rpc_body(std::string_view body) test::parse_json(response.body()); } +network::boost_code bitcoind_setup_fixture::ws_upgrade() +{ + network::boost_code ec{}; + BOOST_CHECK(!websocket_.has_value()); + + websocket_.emplace(socket_); + websocket_.value().text(true); + websocket_.value().handshake("localhost", "/", ec); + return ec; +} + +boost::json::value bitcoind_setup_fixture::ws_rpc(std::string_view method, + std::string_view params) +{ + std::ostringstream body{}; + body << R"({"jsonrpc":"2.0","id":0,"method":")" << method + << R"(","params":)" << params << "}"; + + network::boost_code ec{}; + BOOST_CHECK(websocket_.has_value()); + websocket_.value().write(net::buffer(body.str()), ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + + flat_buffer buffer{}; + websocket_.value().read(buffer, ec); + BOOST_CHECK_MESSAGE(!ec, ec.message()); + return test::parse_json(buffers_to_string(buffer.data())); +} + bitcoind_setup_fixture::status bitcoind_setup_fixture::rest_status(std::string_view target) { diff --git a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp index 62ff935e..a2cc5bba 100644 --- a/test/protocols/bitcoind/bitcoind_setup_fixture.hpp +++ b/test/protocols/bitcoind/bitcoind_setup_fixture.hpp @@ -41,6 +41,13 @@ struct bitcoind_setup_fixture // the parsed json response, or {"dropped":true} if the channel dropped. boost::json::value rpc_body(std::string_view body); + // Upgrade the connection to websocket (no further http requests). + network::boost_code ws_upgrade(); + + // As rpc(), over the upgraded websocket connection. + boost::json::value ws_rpc(std::string_view method, + std::string_view params="[]"); + // bitcoind REST over HTTP GET (target under "/rest/..."). status rest_status(std::string_view target); boost::json::value rest_json(std::string_view target); @@ -59,10 +66,14 @@ struct bitcoind_setup_fixture static string_request create_post(std::string_view target, std::string_view body); + using tcp_stream = boost::beast::tcp_stream; + using websocket_stream = boost::beast::websocket::stream; + network::logger log_; server::server_node server_; boost::asio::io_context io{}; - boost::beast::tcp_stream socket_{ io.get_executor() }; + tcp_stream socket_{ io.get_executor() }; + std::optional websocket_{}; }; struct bitcoind_ten_block_setup_fixture From 82877e32d5781284116c2a94b4a10eb806bd4b48 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 09:50:33 -0400 Subject: [PATCH 12/29] Generalize channel_http for templated body selection. --- .../bitcoin/server/channels/channel_http.hpp | 13 ++++++++----- include/bitcoin/server/channels/channels.hpp | 4 ++-- .../server/protocols/protocol_bitcoind_rpc.hpp | 4 ++-- .../bitcoin/server/protocols/protocol_html.hpp | 2 +- .../bitcoind/protocol_bitcoind_rpc.cpp | 16 +++------------- src/protocols/btcd/protocol_btcd_rpc.cpp | 17 +++-------------- 6 files changed, 19 insertions(+), 37 deletions(-) diff --git a/include/bitcoin/server/channels/channel_http.hpp b/include/bitcoin/server/channels/channel_http.hpp index aab39f39..2fe2cac6 100644 --- a/include/bitcoin/server/channels/channel_http.hpp +++ b/include/bitcoin/server/channels/channel_http.hpp @@ -27,10 +27,13 @@ namespace libbitcoin { namespace server { +/// Channel for http services, websocket frames read as Body (a json-rpc +/// service instantiates with network::rpc::request). +template class BCS_API channel_http : public server::channel, public network::channel_http, - protected network::tracker + protected network::tracker> { public: typedef std::shared_ptr ptr; @@ -40,19 +43,19 @@ class BCS_API channel_http const node::configuration& config, const options_t& options) NOEXCEPT : server::channel(log, socket, identifier, config), network::channel_http(log, socket, identifier, config.network, options), - network::tracker(log) + network::tracker>(log) { } protected: using value_type = network::http::body::value_type; - /// Overridden to change default websocket reader to string. + /// Overridden to set the websocket reader body type. inline value_type websocket_body() const NOEXCEPT override { // There is no forwarding constructor so assign and move. - network::http::body::value_type value{}; - value = network::http::string_value{}; + value_type value{}; + value = Body{}; return value; } }; diff --git a/include/bitcoin/server/channels/channels.hpp b/include/bitcoin/server/channels/channels.hpp index 6488a15c..a151318f 100644 --- a/include/bitcoin/server/channels/channels.hpp +++ b/include/bitcoin/server/channels/channels.hpp @@ -38,7 +38,7 @@ network::channel │ └── [server::channel_stratum_v1] │ └── [server::channel_electrum] ├── channel_http -│ └── [server::channel_http] +│ └── [server::channel_http] └── channel_peer └── [node::channel_peer] @@ -50,6 +50,6 @@ server::channel → node::channel ├── channel_stratum_v2 → network::channel ├── channel_stratum_v1 → network::channel_rpc ├── channel_electrum → network::channel_rpc -└── channel_http → network::channel_http +└── channel_http → network::channel_http */ diff --git a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp index 643e6357..da3eb2a2 100644 --- a/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp @@ -33,8 +33,8 @@ class BCS_API protocol_bitcoind_rpc protected network::tracker { public: - // Replace base class channel_t (network::channel_http). - using channel_t = channel_http; + // Replace base class channel_t (json-rpc websocket reader body). + using channel_t = channel_http; typedef std::shared_ptr ptr; using rpc_interface = interface::bitcoind_rpc; diff --git a/include/bitcoin/server/protocols/protocol_html.hpp b/include/bitcoin/server/protocols/protocol_html.hpp index 507c9049..6701cd0d 100644 --- a/include/bitcoin/server/protocols/protocol_html.hpp +++ b/include/bitcoin/server/protocols/protocol_html.hpp @@ -34,7 +34,7 @@ class BCS_API protocol_html { public: using options_t = server::settings::html_server; - using channel_t = channel_http; + using channel_t = channel_http<>; protected: inline protocol_html(const auto& session, diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index 0aaf03cf..ae414f1c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -184,24 +184,14 @@ void protocol_bitcoind_rpc::dispatch_websocket( return; } - // Websocket frames are json-rpc text (channel default reader). - if (!request.body().contains()) + // Websocket frames are parsed as json-rpc requests (websocket_rpc). + if (!request.body().contains()) { stop(error::invalid_argument); return; } - request_t message{}; - try - { - message = value_to(parse( - request.body().get())); - } - catch (const std::exception&) - { - stop(error::invalid_argument); - return; - } + const auto& message = request.body().get().message; // Cache request context for response building (version + id). set_rpc_request(message); diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd_rpc.cpp index b0e452ac..6145478d 100644 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc.cpp @@ -38,7 +38,6 @@ using namespace system; using namespace network; using namespace network::rpc; using namespace std::placeholders; -using namespace boost::json; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) @@ -161,24 +160,14 @@ void protocol_btcd_rpc::dispatch_websocket( { BC_ASSERT(stranded()); - // Websocket frames are json-rpc text (channel default reader). - if (!request.body().contains()) + // Websocket frames are parsed as json-rpc requests (websocket_rpc). + if (!request.body().contains()) { stop(error::invalid_argument); return; } - request_t message{}; - try - { - message = value_to(parse( - request.body().get())); - } - catch (const std::exception&) - { - stop(error::invalid_argument); - return; - } + const auto& message = request.body().get().message; // Cache request context for response building (version + id). set_rpc_request(message); From 7d4ef25f9e757d019ad00ee59e339da463fc31f7 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 10:31:35 -0400 Subject: [PATCH 13/29] Derive bitcoind service emissions from config. --- include/bitcoin/server/settings.hpp | 13 ++++- src/parser.cpp | 10 ++++ .../bitcoind/protocol_bitcoind_rpc.cpp | 57 ++++++++++--------- test/protocols/bitcoind/bitcoind_rpc.cpp | 4 +- 4 files changed, 56 insertions(+), 28 deletions(-) diff --git a/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp index e2743848..e62d97d7 100644 --- a/include/bitcoin/server/settings.hpp +++ b/include/bitcoin/server/settings.hpp @@ -152,6 +152,17 @@ class BCS_API settings virtual bool enabled() const NOEXCEPT; }; + struct bitcoind_server + : public network::settings::http_server + { + using base = network::settings::http_server; + using base::base; + + /// Arbitrary version identity returned by getnetworkinfo. + system::config::version version{}; + std::string subversion{ "/libbitcoin:server/" }; + }; + // html_server precludes copy. DELETE_COPY(settings); @@ -165,7 +176,7 @@ class BCS_API settings server::settings::html_server native; /// bitcoind compat interface (http/s, stateless json-rpc-v2) - network::settings::http_server bitcoind{ "bitcoind" }; + bitcoind_server bitcoind{ "bitcoind" }; /// btcd compat interface (http/s + websocket, json-rpc-v1) network::settings::http_server btcd{ "btcd" }; diff --git a/src/parser.cpp b/src/parser.cpp index 7f88ccc5..04addd90 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1120,6 +1120,16 @@ options_metadata parser::load_settings() THROWS value(&configured.server.bitcoind.server), "The server name (http header), defaults to '" BC_HTTP_SERVER_NAME "'." ) + ( + "bitcoind.version", + value(&configured.server.bitcoind.version), + "The version identity (getnetworkinfo), defaults to '0.0'." + ) + ( + "bitcoind.subversion", + value(&configured.server.bitcoind.subversion), + "The subversion identity (getnetworkinfo), defaults to '/libbitcoin:server/'." + ) ( "bitcoind.host", value(&configured.server.bitcoind.hosts), diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index ae414f1c..1c9b6cb7 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -184,7 +184,7 @@ void protocol_bitcoind_rpc::dispatch_websocket( return; } - // Websocket frames are parsed as json-rpc requests (websocket_rpc). + // Websocket frames are parsed as json-rpc requests (channel body type). if (!request.body().contains()) { stop(error::invalid_argument); @@ -317,6 +317,26 @@ bool protocol_bitcoind_rpc::handle_get_block_chain_info(const code& ec, std::min(1.0, static_cast(blocks) / static_cast(headers)); + // Softfork activation is configured, not assumable. Taproot is reported + // when configured active, with its configured activation height -- lnd's + // backendSupportsTaproot requires the key's presence (not its field + // values) before treating any btcd/bitcoind backend as usable. + const auto& settings = system_settings(); + object_t soft_forks{}; + if (settings.forks.bip341 && settings.forks.bip342) + { + soft_forks.emplace("taproot", object_t + { + { "status", std::string{ "active" } }, + { "bit", 2 }, + { "startTime", -1 }, + { "timeout", -1 }, + { "since", possible_sign_cast( + settings.bip9_bit2_active_checkpoint.height()) }, + { "min_activation_height", 0 } + }); + } + // TODO: blocks/headers is a misnomer (off-by-one), intended? using namespace chain; send_result(object_t @@ -334,25 +354,7 @@ bool protocol_bitcoind_rpc::handle_get_block_chain_info(const code& ec, { "initialblockdownload", !is_current_chain(true) }, { "pruned", network_settings().pruned_node() }, { "warnings", std::string{} }, - - // Taproot is a permanent consensus rule (no live signaling to - // track), so this is reported unconditionally rather than computed. - // lnd's backendSupportsTaproot requires the key's presence (not its - // field values) before treating any btcd/bitcoind backend as usable. - { "bip9_softforks", object_t - { - { "taproot", object_t - { - { "status", std::string{ "active" } }, - { "bit", 2 }, - { "startTime", -1 }, - { "timeout", -1 }, - { "since", 709632 }, - { "min_activation_height", 0 } - } - } - } - } + { "bip9_softforks", std::move(soft_forks) } }, 512); return true; } @@ -609,22 +611,25 @@ bool protocol_bitcoind_rpc::handle_get_network_info(const code& ec, if (stopped(ec)) return false; - // TODO: get most of these values from either config or network/node props. + // bitcoind's numeric version encoding (10'000 major, 100 minor, patch). + const auto& settings = server_settings().bitcoind; + const auto& segments = settings.version.segments(); + const auto version = 10'000 * segments[0] + 100 * segments[1] + segments[2]; // libbitcoin-server is a node, not a wallet/peer-introspection service; // peer-dependent fields (connections, addresses) are reported as empty. send_result(object_t { - { "version", 0 }, - { "subversion", std::string{ "/libbitcoin:server/" } }, - { "protocolversion", 70016 }, + { "version", version }, + { "subversion", settings.subversion }, + { "protocolversion", network_settings().protocol_maximum }, { "localrelay", true }, { "timeoffset", 0 }, { "connections", 0 }, { "networkactive", true }, { "networks", array_t{} }, - { "relayfee", 0.00001 }, - { "incrementalfee", 0.00001 }, + { "relayfee", node_settings().minimum_fee_rate }, + { "incrementalfee", node_settings().minimum_bump_rate }, { "localaddresses", array_t{} }, { "warnings", std::string{} } }, 256); diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 36755f3c..c621419b 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -127,11 +127,13 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__ten_block_store__expected) BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockchaininfo__bip9_softforks_taproot__present) { // lnd's backendSupportsTaproot requires this key's presence to treat - // any btcd/bitcoind backend as usable. + // any btcd/bitcoind backend as usable. The activation height is the + // configured (mainnet) bip9 bit2 checkpoint. const auto response = rpc("getblockchaininfo"); const auto& result = response.at("result"); BOOST_REQUIRE(result.as_object().contains("bip9_softforks")); BOOST_REQUIRE(result.at("bip9_softforks").as_object().contains("taproot")); + BOOST_REQUIRE_EQUAL(result.at("bip9_softforks").at("taproot").at("since").as_int64(), 709632); } BOOST_AUTO_TEST_CASE(bitcoind_rpc__gettxout__unspent_coinbase__output) From 40372faf3f1e4c1c5bb7478b6238153d54c03576 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 10:31:35 -0400 Subject: [PATCH 14/29] btcd: derive service emissions from config. --- include/bitcoin/server/settings.hpp | 12 ++++++++++- src/parser.cpp | 5 +++++ src/protocols/btcd/protocol_btcd_rpc.cpp | 2 +- .../btcd/protocol_btcd_rpc_utility.cpp | 21 ++++++++++++------- 4 files changed, 30 insertions(+), 10 deletions(-) diff --git a/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp index e62d97d7..d94dd24f 100644 --- a/include/bitcoin/server/settings.hpp +++ b/include/bitcoin/server/settings.hpp @@ -163,6 +163,16 @@ class BCS_API settings std::string subversion{ "/libbitcoin:server/" }; }; + struct btcd_server + : public network::settings::http_server + { + using base = network::settings::http_server; + using base::base; + + /// Arbitrary version identity returned by getinfo. + system::config::version version{}; + }; + // html_server precludes copy. DELETE_COPY(settings); @@ -179,7 +189,7 @@ class BCS_API settings bitcoind_server bitcoind{ "bitcoind" }; /// btcd compat interface (http/s + websocket, json-rpc-v1) - network::settings::http_server btcd{ "btcd" }; + btcd_server btcd{ "btcd" }; /// electrum compat interface (tcp/s, json-rpc-v2) electrum_server electrum{ "electrum" }; diff --git a/src/parser.cpp b/src/parser.cpp index 04addd90..4030731c 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1212,6 +1212,11 @@ options_metadata parser::load_settings() THROWS value(&configured.server.btcd.server), "The server name (http header), defaults to '" BC_HTTP_SERVER_NAME "'." ) + ( + "btcd.version", + value(&configured.server.btcd.version), + "The version identity (getinfo), defaults to '0.0'." + ) ( "btcd.host", value(&configured.server.btcd.hosts), diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd_rpc.cpp index 6145478d..779c8df6 100644 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc.cpp @@ -160,7 +160,7 @@ void protocol_btcd_rpc::dispatch_websocket( { BC_ASSERT(stranded()); - // Websocket frames are parsed as json-rpc requests (websocket_rpc). + // Websocket frames are parsed as json-rpc requests (channel body type). if (!request.body().contains()) { stop(error::invalid_argument); diff --git a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp b/src/protocols/btcd/protocol_btcd_rpc_utility.cpp index e064b93d..9e15e347 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc_utility.cpp @@ -74,19 +74,25 @@ bool protocol_btcd_rpc::handle_get_info(const code& ec, return true; } + // btcd's numeric version encoding (1'000'000 major, 10'000 minor, 100 + // patch). + const auto& segments = server_settings().btcd.version.segments(); + const auto version = 1'000'000 * segments[0] + 10'000 * segments[1] + + 100 * segments[2]; + // Untracked peer-dependent fields are reported as empty/zero, matching // the inherited getnetworkinfo handler's convention. send_result(object_t { - { "version", 0 }, - { "protocolversion", 70016 }, + { "version", version }, + { "protocolversion", network_settings().protocol_maximum }, { "blocks", possible_sign_cast(top) }, { "timeoffset", 0 }, { "connections", 0 }, { "proxy", std::string{} }, { "difficulty", header->difficulty() }, { "testnet", chain_name(query) != "main" }, - { "relayfee", 0.00001 }, + { "relayfee", node_settings().minimum_fee_rate }, { "errors", std::string{} } }, 256); return true; @@ -114,8 +120,8 @@ bool protocol_btcd_rpc::handle_get_network_hash_ps(const code& ec, if (stopped(ec)) return false; - // Approximated from the requested height's difficulty and the standard - // 600s target spacing, not a windowed average over 'blocks'. + // Approximated from the requested height's difficulty and the configured + // target spacing, not a windowed average over 'blocks'. const auto& query = archive(); const auto top = query.get_top_confirmed(); const auto target = (height < 0) ? top : @@ -128,10 +134,9 @@ bool protocol_btcd_rpc::handle_get_network_hash_ps(const code& ec, return true; } - constexpr auto average_seconds_per_block = 600.0; constexpr auto two_to_the_32 = 4294967296.0; - send_result( - header->difficulty() * two_to_the_32 / average_seconds_per_block, 20); + const auto spacing = system_settings().block_spacing_seconds; + send_result(header->difficulty() * two_to_the_32 / spacing, 20); return true; } From 5060cc162306abd4577dc1100fce876ae1a71e31 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 11:07:23 -0400 Subject: [PATCH 15/29] btcd: add maximum_filters setting (DoS). --- .../server/protocols/protocol_btcd_rpc.hpp | 4 ++++ include/bitcoin/server/settings.hpp | 3 +++ src/parser.cpp | 5 ++++ .../btcd/protocol_btcd_rpc_filter.cpp | 24 +++++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp index 6d169e9a..28182604 100644 --- a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp @@ -140,6 +140,10 @@ class BCS_API protocol_btcd_rpc btcd_interface::rescan_blocks, const network::rpc::value_t& blockhashes) NOEXCEPT; + /// Cumulative number of filter entries, bounded by maximum_filters. + size_t filters() const NOEXCEPT; + bool filtered() const NOEXCEPT; + /// Filter parsing (loadtxfilter). Merges into the existing filter unless /// reload clears it first. code parse_filter_addresses(bool reload, diff --git a/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp index d94dd24f..9e82eb81 100644 --- a/include/bitcoin/server/settings.hpp +++ b/include/bitcoin/server/settings.hpp @@ -171,6 +171,9 @@ class BCS_API settings /// Arbitrary version identity returned by getinfo. system::config::version version{}; + + /// Maximum cumulative number of loadtxfilter entries per channel. + uint32_t maximum_filters{ 1'000'000 }; }; // html_server precludes copy. diff --git a/src/parser.cpp b/src/parser.cpp index 4030731c..0675a214 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1217,6 +1217,11 @@ options_metadata parser::load_settings() THROWS value(&configured.server.btcd.version), "The version identity (getinfo), defaults to '0.0'." ) + ( + "btcd.maximum_filters", + value(&configured.server.btcd.maximum_filters), + "The maximum number of loadtxfilter entries, defaults to '1000000'." + ) ( "btcd.host", value(&configured.server.btcd.hosts), diff --git a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp index 52edcb6a..d12e7d1d 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp @@ -35,6 +35,27 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) +// Filter bounds. +// ---------------------------------------------------------------------------- +// The filter is per channel and grows from client calls (loadtxfilter) and +// from chain data (match_filtered_transactions auto-tracks the outpoint of a +// matched output, as btcd). Both are bounded by maximum_filters. + +size_t protocol_btcd_rpc::filters() const NOEXCEPT +{ + BC_ASSERT(stranded()); + return pay_key_hashes_.size() + pay_script_hashes_.size() + + pay_witness_key_hash_programs_.size() + + pay_witness_script_hash_programs_.size() + + pay_witness_taproot_programs_.size() + filter_outpoints_.size(); +} + +bool protocol_btcd_rpc::filtered() const NOEXCEPT +{ + BC_ASSERT(stranded()); + return filters() >= options_.maximum_filters; +} + // Filter parsing (loadtxfilter). // ---------------------------------------------------------------------------- @@ -57,6 +78,9 @@ code protocol_btcd_rpc::parse_filter_addresses(bool reload, for (const auto& item: std::get(addresses.value())) { + if (filtered()) + return error::oversubscribed; + if (!std::holds_alternative(item.value())) return error::invalid_argument; From fc0a7f9fdcf4feeebc0e6e30985ef01d50e23f39 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 12:07:31 -0400 Subject: [PATCH 16/29] btcd: replace O(N) scans with O(1) indexation, effective concurrency. --- .../server/protocols/protocol_btcd_rpc.hpp | 120 +++- include/bitcoin/server/settings.hpp | 5 +- src/parser.cpp | 7 +- src/protocols/btcd/protocol_btcd_rpc.cpp | 104 +--- .../btcd/protocol_btcd_rpc_filter.cpp | 584 ++++++++++++------ test/protocols/btcd/btcd_rpc.cpp | 48 ++ test/protocols/btcd/btcd_setup_fixture.cpp | 5 +- test/protocols/btcd/btcd_setup_fixture.hpp | 37 +- 8 files changed, 596 insertions(+), 314 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp index 28182604..eb1cd9e1 100644 --- a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp @@ -20,9 +20,10 @@ #define LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_BTCD_RPC_HPP #include +#include #include #include -#include +#include #include #include #include @@ -52,12 +53,14 @@ class BCS_API protocol_btcd_rpc : server::protocol_bitcoind_rpc(session, channel, options), network::tracker(session->log), options_(options), + turbo_(session->database_settings().turbo), p2kh_(session->system_settings().forks.difficult ? system::wallet::payment_address::mainnet_p2kh : system::wallet::payment_address::testnet_p2kh), p2sh_(session->system_settings().forks.difficult ? system::wallet::payment_address::mainnet_p2sh : - system::wallet::payment_address::testnet_p2sh) + system::wallet::payment_address::testnet_p2sh), + notification_strand_(channel->service().get_executor()) { } @@ -132,6 +135,7 @@ class BCS_API protocol_btcd_rpc /// Handlers (address/outpoint filtering). notifyblocks arms delivery; /// loadtxfilter only populates the filter (as btcd). rescanblocks /// replays the match logic against explicitly named historical blocks. + /// Address watching requires the address index (not_implemented). bool handle_load_tx_filter(const code& ec, btcd_interface::load_tx_filter, bool reload, const network::rpc::value_t& addresses, @@ -140,24 +144,6 @@ class BCS_API protocol_btcd_rpc btcd_interface::rescan_blocks, const network::rpc::value_t& blockhashes) NOEXCEPT; - /// Cumulative number of filter entries, bounded by maximum_filters. - size_t filters() const NOEXCEPT; - bool filtered() const NOEXCEPT; - - /// Filter parsing (loadtxfilter). Merges into the existing filter unless - /// reload clears it first. - code parse_filter_addresses(bool reload, - const network::rpc::value_t& addresses) NOEXCEPT; - code parse_filter_outpoints(bool reload, - const network::rpc::value_t& outpoints) NOEXCEPT; - - /// Match a block's transactions against the loaded filter: an output - /// paying a watched address, or an input spending a watched outpoint. A - /// matched output's own outpoint is added to the watched set (as btcd). - /// Returns the matched transactions as base16, in block order. - network::rpc::array_t match_filtered_transactions( - const system::chain::block& block) NOEXCEPT; - /// Handler (admin, permanently not_implemented). bool handle_stop(const code& ec, btcd_interface::stop) NOEXCEPT; @@ -182,13 +168,79 @@ class BCS_API protocol_btcd_rpc /// Chase event subscription (block connect/disconnect for notifyblocks). bool handle_chase(const code& ec, node::chase event_, node::event_value value) NOEXCEPT; - void do_block_connected(node::header_t link) NOEXCEPT; - void do_block_disconnected(node::header_t link) NOEXCEPT; /// Sender (btcd server push, json-rpc-v1 request shape with no id). void send_notification(const std::string& method, network::rpc::array_t&& params, size_t size_hint) NOEXCEPT; +protected: + using point = system::chain::point; + using hash_digest = system::hash_digest; + using history = database::history; + using histories = database::histories; + using cursor_t = database::height_link; + + /// Watch of an address (by index key), matched via cursored history. + struct address_watch final + { + cursor_t cursor{}; + }; + + /// Watch of an outpoint, matched via spender differencing. + struct outpoint_watch final + { + database::history outpoint{}; + database::histories spenders{}; + }; + + /// Filter (loadtxfilter). + /// ----------------------------------------------------------------------- + + code parse_filter_keys(const network::rpc::value_t& addresses, + std::vector& out) NOEXCEPT; + code parse_filter_points(const network::rpc::value_t& outpoints, + std::vector& out) NOEXCEPT; + + void do_load_tx_filter(bool reload, const std::vector& keys, + const std::vector& points) NOEXCEPT; + void complete_load_tx_filter(const code& ec) NOEXCEPT; + + void do_rescan_blocks(const std::vector& hashes) NOEXCEPT; + void do_rescan_watches(const std::vector& hashes, + const std::vector& keys, + const std::vector& points) NOEXCEPT; + void complete_rescan_blocks(const code& ec, + const network::rpc::array_t& discovered) NOEXCEPT; + + /// Notification event handlers. + /// ----------------------------------------------------------------------- + + void do_connected(node::header_t link) NOEXCEPT; + void do_disconnected(node::header_t link) NOEXCEPT; + void notify_connected(const std::string& hash, size_t height, + uint32_t time, const std::string& header, + const network::rpc::array_t& txs) NOEXCEPT; + void notify_disconnected(const std::string& hash, size_t height, + uint32_t time, const std::string& header) NOEXCEPT; + + /// Utilities (notification strand). + /// ----------------------------------------------------------------------- + + /// Matched transactions by height, ordered by block position. + using matched_txs = std::map; + using matches = std::map; + + /// Accumulate watched-address matches at the given heights. + code match_addresses(matches& out, address_watch& sub, + const hash_digest& key, const std::set& heights) NOEXCEPT; + + /// Accumulate new watched-outpoint spender matches at the given heights. + void match_outpoints(matches& out, outpoint_watch& sub, + const point& prevout, const std::set& heights) NOEXCEPT; + + /// Serialize one block's matched transactions (base16, position order). + network::rpc::array_t serialize_matches(const matched_txs& txs) NOEXCEPT; + private: template inline void btcd_subscribe(Method&& method, Args&&... args) NOEXCEPT @@ -196,23 +248,31 @@ class BCS_API protocol_btcd_rpc btcd_dispatcher_.subscribe(BIND_SHARED(method, args)); } + // Post to notification strand. + template + inline auto notify(Method&& method, Args&&... args) NOEXCEPT + { + return boost::asio::post(notification_strand_, + BIND_SAFE(BIND_SHARED(method, args))); + } + // These are thread safe. std::atomic_bool subscribed_blocks_{}; + std::atomic_bool stopping_{}; const options_t& options_; + const bool turbo_; const uint8_t p2kh_; const uint8_t p2sh_; // This is protected by strand. btcd_dispatcher btcd_dispatcher_{}; - // Tx filter (loadtxfilter). Hash sets are kept per script template so a - // p2kh watch never matches a p2wpkh output or vice versa. - std::unordered_set pay_key_hashes_{}; - std::unordered_set pay_script_hashes_{}; - std::set pay_witness_key_hash_programs_{}; - std::set pay_witness_script_hash_programs_{}; - std::set pay_witness_taproot_programs_{}; - std::unordered_set filter_outpoints_{}; + // This is thread safe, uses network threadpool. + network::asio::strand notification_strand_; + + // These are protected by notification strand. + std::map address_watches_{}; + std::map outpoint_watches_{}; }; } // namespace server diff --git a/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp index 9e82eb81..b7a09c59 100644 --- a/include/bitcoin/server/settings.hpp +++ b/include/bitcoin/server/settings.hpp @@ -172,8 +172,11 @@ class BCS_API settings /// Arbitrary version identity returned by getinfo. system::config::version version{}; - /// Maximum cumulative number of loadtxfilter entries per channel. + /// Maximum cumulative number of loadtxfilter watches per channel. uint32_t maximum_filters{ 1'000'000 }; + + /// Maximum number of address history entries upon one index walk. + uint32_t maximum_history{ 1'000'000 }; }; // html_server precludes copy. diff --git a/src/parser.cpp b/src/parser.cpp index 0675a214..730e6c7d 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -1220,7 +1220,12 @@ options_metadata parser::load_settings() THROWS ( "btcd.maximum_filters", value(&configured.server.btcd.maximum_filters), - "The maximum number of loadtxfilter entries, defaults to '1000000'." + "The maximum number of loadtxfilter watches, defaults to '1000000'." + ) + ( + "btcd.maximum_history", + value(&configured.server.btcd.maximum_history), + "The maximum number of address history entries, defaults to '1000000'." ) ( "btcd.host", diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd_rpc.cpp index 779c8df6..6475864c 100644 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc.cpp @@ -29,11 +29,6 @@ namespace server { #define SUBSCRIBE_BTCD(method, ...) \ btcd_subscribe(&CLASS::method, __VA_ARGS__) -// protocol_bitcoind_rpc declares 'using post = network::http::method::post', -// which shadows network::protocol::post. Qualify explicitly. -#define POST_BTCD(method, ...) \ - this->network::protocol::template post(&CLASS::method, __VA_ARGS__) - using namespace system; using namespace network; using namespace network::rpc; @@ -85,9 +80,11 @@ void protocol_btcd_rpc::start() NOEXCEPT protocol_bitcoind_rpc::start(); } +// Events unsubscription is asynchronous, race is ok. void protocol_btcd_rpc::stopping(const code& ec) NOEXCEPT { BC_ASSERT(stranded()); + stopping_.store(true); btcd_dispatcher_.stop(ec); unsubscribe_chase(); protocol_bitcoind_rpc::stopping(ec); @@ -160,7 +157,7 @@ void protocol_btcd_rpc::dispatch_websocket( { BC_ASSERT(stranded()); - // Websocket frames are parsed as json-rpc requests (channel body type). + // Websocket frames are parsed as json-rpc requests (websocket_rpc). if (!request.body().contains()) { stop(error::invalid_argument); @@ -428,6 +425,8 @@ bool protocol_btcd_rpc::handle_rescan(const code& ec, // Chase events. // ---------------------------------------------------------------------------- +// Block events are matched against the watch-list on the notification strand +// (see protocol_btcd_rpc_filter.cpp) and notified back on the channel strand. bool protocol_btcd_rpc::handle_chase(const code&, node::chase event_, node::event_value value) NOEXCEPT @@ -442,7 +441,7 @@ bool protocol_btcd_rpc::handle_chase(const code&, node::chase event_, if (subscribed_blocks_.load(std::memory_order_relaxed)) { BC_ASSERT(std::holds_alternative(value)); - POST_BTCD(do_block_connected, std::get(value)); + POST_NOTIFY(do_connected, std::get(value)); } break; @@ -450,7 +449,7 @@ bool protocol_btcd_rpc::handle_chase(const code&, node::chase event_, if (subscribed_blocks_.load(std::memory_order_relaxed)) { BC_ASSERT(std::holds_alternative(value)); - POST_BTCD(do_block_disconnected, std::get(value)); + POST_NOTIFY(do_disconnected, std::get(value)); } break; @@ -461,90 +460,6 @@ bool protocol_btcd_rpc::handle_chase(const code&, node::chase event_, return !stopped(); } -void protocol_btcd_rpc::do_block_connected(node::header_t link_value) NOEXCEPT -{ - BC_ASSERT(stranded()); - - if (stopped() || !subscribed_blocks_.load(std::memory_order_relaxed)) - return; - - const database::header_link link{ link_value }; - const auto& query = archive(); - - size_t height{}; - if (!query.get_height(height, link)) - return; - - const auto header = query.get_header(link); - if (!header) - return; - - // btcd 'blockconnected' notification: [hash, height, time]. - array_t params{}; - params.emplace_back(value_t{ encode_hash(query.get_header_key(link)) }); - params.emplace_back(value_t{ possible_sign_cast(height) }); - params.emplace_back(value_t{ possible_sign_cast( - header->timestamp()) }); - - send_notification("blockconnected", std::move(params), 256); - - // btcd 'filteredblockconnected': [height, header, subscribedtxs]. Sent - // unconditionally alongside blockconnected (as btcd) -- an empty/never- - // loaded filter just yields an empty subscribedtxs array. - constexpr auto witness = true; - const auto block = query.get_block(link, witness); - if (!block) - return; - - array_t filtered_params{}; - filtered_params.emplace_back(value_t{ - possible_sign_cast(height) }); - filtered_params.emplace_back(value_t{ - to_text(*header, chain::header::serialized_size()) }); - filtered_params.emplace_back(value_t{ match_filtered_transactions(*block) }); - - send_notification("filteredblockconnected", - std::move(filtered_params), 256); -} - -void protocol_btcd_rpc::do_block_disconnected( - node::header_t link_value) NOEXCEPT -{ - BC_ASSERT(stranded()); - - if (stopped() || !subscribed_blocks_.load(std::memory_order_relaxed)) - return; - - const database::header_link link{ link_value }; - const auto& query = archive(); - - size_t height{}; - if (!query.get_height(height, link)) - return; - - const auto header = query.get_header(link); - if (!header) - return; - - array_t params{}; - params.emplace_back(value_t{ encode_hash(query.get_header_key(link)) }); - params.emplace_back(value_t{ possible_sign_cast(height) }); - params.emplace_back(value_t{ possible_sign_cast( - header->timestamp()) }); - - send_notification("blockdisconnected", std::move(params), 256); - - // btcd 'filteredblockdisconnected': [height, header]. - array_t filtered_params{}; - filtered_params.emplace_back(value_t{ - possible_sign_cast(height) }); - filtered_params.emplace_back(value_t{ - to_text(*header, chain::header::serialized_size()) }); - - send_notification("filteredblockdisconnected", - std::move(filtered_params), 256); -} - // Senders. // ---------------------------------------------------------------------------- @@ -566,8 +481,11 @@ void protocol_btcd_rpc::send_notification(const std::string& method, message.body() = std::move(notification); message.prepare_payload(); + // The notification strand poster (notify, see POST_NOTIFY) shadows + // network::protocol_http::notify. Qualify explicitly (as POST_BTCD). // NOTIFY does not restart the (already active) ws reader. - NOTIFY(std::move(message), handle_complete, _1, error::success); + this->network::protocol_http::template notify(std::move(message), + &CLASS::handle_complete, _1, error::success); } BC_POP_WARNING() diff --git a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp index d12e7d1d..be53d0ab 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp @@ -27,6 +27,11 @@ namespace server { #define CLASS protocol_btcd_rpc +// protocol_bitcoind_rpc declares 'using post = network::http::method::post', +// which shadows network::protocol::post. Qualify explicitly. +#define POST_BTCD(method, ...) \ + this->network::protocol::template post(&CLASS::method, __VA_ARGS__) + using namespace system; using namespace network; using namespace network::rpc; @@ -35,103 +40,42 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) -// Filter bounds. -// ---------------------------------------------------------------------------- -// The filter is per channel and grows from client calls (loadtxfilter) and -// from chain data (match_filtered_transactions auto-tracks the outpoint of a -// matched output, as btcd). Both are bounded by maximum_filters. - -size_t protocol_btcd_rpc::filters() const NOEXCEPT -{ - BC_ASSERT(stranded()); - return pay_key_hashes_.size() + pay_script_hashes_.size() + - pay_witness_key_hash_programs_.size() + - pay_witness_script_hash_programs_.size() + - pay_witness_taproot_programs_.size() + filter_outpoints_.size(); -} - -bool protocol_btcd_rpc::filtered() const NOEXCEPT -{ - BC_ASSERT(stranded()); - return filters() >= options_.maximum_filters; -} - -// Filter parsing (loadtxfilter). +// Filter (loadtxfilter). // ---------------------------------------------------------------------------- +// The watch-list is matched via the address index and spender lookups (the +// electrum subscription model), never by block scanning. Watches are keyed +// by index key (sha256 of the output script), matched by cursored history +// delta, so each connected block costs one bounded index walk per watch. -code protocol_btcd_rpc::parse_filter_addresses(bool reload, - const value_t& addresses) NOEXCEPT +code protocol_btcd_rpc::parse_filter_keys(const value_t& addresses, + std::vector& out) NOEXCEPT { BC_ASSERT(stranded()); - if (reload) - { - pay_key_hashes_.clear(); - pay_script_hashes_.clear(); - pay_witness_key_hash_programs_.clear(); - pay_witness_script_hash_programs_.clear(); - pay_witness_taproot_programs_.clear(); - } - if (!std::holds_alternative(addresses.value())) return error::invalid_argument; for (const auto& item: std::get(addresses.value())) { - if (filtered()) - return error::oversubscribed; - if (!std::holds_alternative(item.value())) return error::invalid_argument; - const auto& text = std::get(item.value()); + chain::script script{}; + if (const auto ec = parse_output_script( + std::get(item.value()), script)) + return ec; - // Try base58 (p2kh/p2sh) first. - const wallet::payment_address base58(text); - if (base58) - { - if (base58.prefix() == p2kh_) - pay_key_hashes_.insert(base58.hash()); - else if (base58.prefix() == p2sh_) - pay_script_hashes_.insert(base58.hash()); - else - return error::invalid_argument; - - continue; - } - - // Otherwise bech32/bech32m (p2wpkh/p2wsh/p2tr). - const wallet::witness_address segwit(text); - if (!segwit) - return error::invalid_argument; - - switch (segwit.identifier()) - { - case wallet::witness_address::program_type::version0_p2kh: - pay_witness_key_hash_programs_.insert(segwit.program()); - break; - case wallet::witness_address::program_type::version0_p2sh: - pay_witness_script_hash_programs_.insert(segwit.program()); - break; - case wallet::witness_address::program_type::version1_taproot: - pay_witness_taproot_programs_.insert(segwit.program()); - break; - default: - return error::invalid_argument; - } + out.push_back(script.hash()); } return error::success; } -code protocol_btcd_rpc::parse_filter_outpoints(bool reload, - const value_t& outpoints) NOEXCEPT +code protocol_btcd_rpc::parse_filter_points(const value_t& outpoints, + std::vector& out) NOEXCEPT { BC_ASSERT(stranded()); - if (reload) - filter_outpoints_.clear(); - if (!std::holds_alternative(outpoints.value())) return error::invalid_argument; @@ -154,7 +98,7 @@ code protocol_btcd_rpc::parse_filter_outpoints(bool reload, !to_integer(index, std::get(index_it->second.value()))) return error::invalid_argument; - filter_outpoints_.emplace(hash, index); + out.emplace_back(hash, index); } return error::success; @@ -170,22 +114,117 @@ bool protocol_btcd_rpc::handle_load_tx_filter(const code& ec, if (stopped(ec)) return false; - if (const auto fault = parse_filter_addresses(reload, addresses)) + std::vector keys{}; + if (const auto fault = parse_filter_keys(addresses, keys)) { send_error(fault); return true; } - if (const auto fault = parse_filter_outpoints(reload, outpoints)) + std::vector points{}; + if (const auto fault = parse_filter_points(outpoints, points)) { send_error(fault); return true; } - send_result({}, 4); + if (!keys.empty() && !archive().address_enabled()) + { + send_error(error::not_implemented); + return true; + } + + monitor(true); + POST_NOTIFY(do_load_tx_filter, reload, std::move(keys), + std::move(points)); return true; } +void protocol_btcd_rpc::do_load_tx_filter(bool reload, + const std::vector& keys, + const std::vector& points) NOEXCEPT +{ + BC_ASSERT(notification_strand_.running_in_this_thread()); + + if (reload) + { + address_watches_.clear(); + outpoint_watches_.clear(); + } + + code ec{ error::success }; + const auto& query = archive(); + const auto maximum = server_settings().btcd.maximum_filters; + const auto limit = server_settings().btcd.maximum_history; + + for (const auto& key: keys) + { + if (stopping_) + return; + + if (address_watches_.size() + outpoint_watches_.size() >= maximum) + { + ec = error::subscription_limit; + break; + } + + // Prime the cursor to present, so matching reports new blocks only. + const auto at = address_watches_.try_emplace(key, address_watch{}); + if (at.second) + { + histories discard{}; + const auto fault = query.get_history(stopping_, + at.first->second.cursor, discard, key, limit, turbo_); + if (fault == database::error::query_canceled) + return; + } + } + + for (const auto& prevout: points) + { + if (stopping_) + return; + + if (ec) + break; + + if (address_watches_.size() + outpoint_watches_.size() >= maximum) + { + ec = error::subscription_limit; + break; + } + + // Prime the spender set to present, so matching reports new only. + const auto at = outpoint_watches_.try_emplace(prevout, + outpoint_watch{}); + if (at.second) + { + auto& sub = at.first->second; + sub.outpoint = query.get_tx_history(query.to_tx(prevout.hash())); + sub.spenders = query.get_spenders_history(prevout); + } + } + + POST_BTCD(complete_load_tx_filter, ec); +} + +void protocol_btcd_rpc::complete_load_tx_filter(const code& ec) NOEXCEPT +{ + BC_ASSERT(stranded()); + + monitor(false); + if (stopped()) + return; + + if (ec) + { + send_error(ec); + return; + } + + send_result({}, 4); +} + bool protocol_btcd_rpc::handle_rescan_blocks(const code& ec, btcd_interface::rescan_blocks, const value_t& blockhashes) NOEXCEPT { @@ -198,145 +237,320 @@ bool protocol_btcd_rpc::handle_rescan_blocks(const code& ec, return true; } - constexpr auto witness = true; - const auto& query = archive(); - array_t discovered{}; - + std::vector hashes{}; for (const auto& item: std::get(blockhashes.value())) { - if (!std::holds_alternative(item.value())) - { - send_error(error::invalid_argument); - return true; - } - - const auto& text = std::get(item.value()); hash_digest hash{}; - if (!decode_hash(hash, text)) + if (!std::holds_alternative(item.value()) || + !decode_hash(hash, std::get(item.value()))) { send_error(error::invalid_argument); return true; } - const auto block = query.get_block(query.to_header(hash), witness); - if (!block) + hashes.push_back(hash); + } + + if (!archive().address_enabled()) + { + send_error(error::not_implemented); + return true; + } + + monitor(true); + POST_NOTIFY(do_rescan_blocks, std::move(hashes)); + return true; +} + +// Snapshot the watch-list on the notification strand, so the query itself +// runs parallel (as electrum's one-shot queries) without serializing against +// this channel's notifications. +void protocol_btcd_rpc::do_rescan_blocks( + const std::vector& hashes) NOEXCEPT +{ + BC_ASSERT(notification_strand_.running_in_this_thread()); + + std::vector keys{}; + keys.reserve(address_watches_.size()); + for (const auto& watch: address_watches_) + keys.push_back(watch.first); + + std::vector points{}; + points.reserve(outpoint_watches_.size()); + for (const auto& watch: outpoint_watches_) + points.push_back(watch.first); + + PARALLEL(do_rescan_watches, hashes, std::move(keys), std::move(points)); +} + +void protocol_btcd_rpc::do_rescan_watches( + const std::vector& hashes, + const std::vector& keys, + const std::vector& points) NOEXCEPT +{ + BC_ASSERT(!stranded()); + + // Resolve named blocks to heights (result retains request order). + std::set heights{}; + std::vector> named{}; + const auto& query = archive(); + for (const auto& hash: hashes) + { + if (stopping_) + return; + + size_t height{}; + if (!query.get_height(height, query.to_header(hash))) { - send_error(error::not_found); - return true; + POST_BTCD(complete_rescan_blocks, error::not_found, array_t{}); + return; } - auto matched = match_filtered_transactions(*block); - if (!matched.empty()) + heights.insert(height); + named.emplace_back(hash, height); + } + + // Match the snapshot against the named heights (full replay). + matches matched{}; + for (const auto& key: keys) + { + if (stopping_) + return; + + address_watch replay{}; + const auto fault = match_addresses(matched, replay, key, heights); + if (fault == database::error::query_canceled) + return; + } + + for (const auto& prevout: points) + { + if (stopping_) + return; + + outpoint_watch replay{}; + match_outpoints(matched, replay, prevout, heights); + } + + array_t discovered{}; + for (const auto& [hash, height]: named) + { + if (stopping_) + return; + + const auto at = matched.find(height); + if (at != matched.end() && !at->second.empty()) { discovered.emplace_back(value_t{ object_t { - { "hash", value_t{ text } }, - { "transactions", value_t{ std::move(matched) } } + { "hash", value_t{ encode_hash(hash) } }, + { "transactions", value_t{ serialize_matches(at->second) } } } }); } } - send_result(value_t{ std::move(discovered) }, 256); - return true; + POST_BTCD(complete_rescan_blocks, error::success, std::move(discovered)); } -// Filter matching. +void protocol_btcd_rpc::complete_rescan_blocks(const code& ec, + const array_t& discovered) NOEXCEPT +{ + BC_ASSERT(stranded()); + + monitor(false); + if (stopped()) + return; + + if (ec) + { + send_error(ec); + return; + } + + send_result(value_t{ discovered }, 256); +} + +// Notification event handlers. // ---------------------------------------------------------------------------- -// Manual per-block script inspection, not the persisted address index -- -// that index is for whole-chain lookups, the wrong tool for matching one -// block against a small in-memory watch-list, and would require the index -// to be enabled. Mirrors btcd's per-client wsClientFilter. -array_t protocol_btcd_rpc::match_filtered_transactions( - const chain::block& block) NOEXCEPT +void protocol_btcd_rpc::do_connected(node::header_t link_value) NOEXCEPT +{ + BC_ASSERT(notification_strand_.running_in_this_thread()); + + const database::header_link link{ link_value }; + const auto& query = archive(); + + size_t height{}; + if (!query.get_height(height, link)) + return; + + const auto header = query.get_header(link); + if (!header) + return; + + // Match the watch-list against the connected block (cursored delta). + // Cursors advance here, so this stays on the notification strand. + matches matched{}; + const std::set heights{ height }; + for (auto& [key, sub]: address_watches_) + { + if (stopping_) + return; + + const auto fault = match_addresses(matched, sub, key, heights); + if (fault == database::error::query_canceled) + return; + } + + for (auto& [prevout, sub]: outpoint_watches_) + { + if (stopping_) + return; + + match_outpoints(matched, sub, prevout, heights); + } + + array_t txs{}; + const auto at = matched.find(height); + if (at != matched.end()) + txs = serialize_matches(at->second); + + POST_BTCD(notify_connected, encode_hash(query.get_header_key(link)), + height, header->timestamp(), + to_text(*header, chain::header::serialized_size()), std::move(txs)); +} + +void protocol_btcd_rpc::do_disconnected(node::header_t link_value) NOEXCEPT +{ + BC_ASSERT(notification_strand_.running_in_this_thread()); + + // Reset watch cursors, disconnected blocks invalidate the walks. + for (auto& [key, sub]: address_watches_) + sub.cursor = {}; + + const database::header_link link{ link_value }; + const auto& query = archive(); + + size_t height{}; + if (!query.get_height(height, link)) + return; + + const auto header = query.get_header(link); + if (!header) + return; + + POST_BTCD(notify_disconnected, encode_hash(query.get_header_key(link)), + height, header->timestamp(), + to_text(*header, chain::header::serialized_size())); +} + +// btcd 'blockconnected' [hash, height, time] and 'filteredblockconnected' +// [height, header, subscribedtxs]. filtered is sent unconditionally +// alongside (as btcd) -- an empty filter just yields an empty array. +void protocol_btcd_rpc::notify_connected(const std::string& hash, + size_t height, uint32_t time, const std::string& header, + const array_t& txs) NOEXCEPT { BC_ASSERT(stranded()); - constexpr auto witness = true; - array_t matched{}; - for (const auto& tx: *block.transactions_ptr()) + if (stopped() || !subscribed_blocks_.load(std::memory_order_relaxed)) + return; + + send_notification("blockconnected", array_t { - auto hit = false; + value_t{ hash }, + value_t{ possible_sign_cast(height) }, + value_t{ possible_sign_cast(time) } + }, 256); - for (const auto& input: *tx->inputs_ptr()) - if (filter_outpoints_.contains(input->point())) - hit = true; + send_notification("filteredblockconnected", array_t + { + value_t{ possible_sign_cast(height) }, + value_t{ header }, + value_t{ txs } + }, 256); +} - const auto hash = tx->hash(false); - const auto& outputs = *tx->outputs_ptr(); - for (size_t index = 0; index < outputs.size(); ++index) - { - const auto& script = outputs[index]->script(); - const auto point_index = possible_narrow_cast(index); +// btcd 'blockdisconnected' [hash, height, time] and +// 'filteredblockdisconnected' [height, header]. +void protocol_btcd_rpc::notify_disconnected(const std::string& hash, + size_t height, uint32_t time, const std::string& header) NOEXCEPT +{ + BC_ASSERT(stranded()); - switch (script.output_pattern()) - { - case chain::script_pattern::pay_key_hash: - { - const auto address = wallet::payment_address:: - extract_output(script, p2kh_, p2sh_); - if (address && pay_key_hashes_.contains(address.hash())) - { - hit = true; - filter_outpoints_.emplace(hash, point_index); - } - break; - } - case chain::script_pattern::pay_script_hash: - { - const auto address = wallet::payment_address:: - extract_output(script, p2kh_, p2sh_); - if (address && pay_script_hashes_.contains(address.hash())) - { - hit = true; - filter_outpoints_.emplace(hash, point_index); - } - break; - } - case chain::script_pattern::pay_witness_key_hash: - { - const auto& program = script.witness_program(); - if (program && pay_witness_key_hash_programs_.contains( - *program)) - { - hit = true; - filter_outpoints_.emplace(hash, point_index); - } - break; - } - case chain::script_pattern::pay_witness_script_hash: - { - const auto& program = script.witness_program(); - if (program && pay_witness_script_hash_programs_.contains( - *program)) - { - hit = true; - filter_outpoints_.emplace(hash, point_index); - } - break; - } - case chain::script_pattern::pay_witness_v1_taproot: - { - const auto& program = script.witness_program(); - if (program && pay_witness_taproot_programs_.contains( - *program)) - { - hit = true; - filter_outpoints_.emplace(hash, point_index); - } - break; - } - default: - break; - } - } + if (stopped() || !subscribed_blocks_.load(std::memory_order_relaxed)) + return; + + send_notification("blockdisconnected", array_t + { + value_t{ hash }, + value_t{ possible_sign_cast(height) }, + value_t{ possible_sign_cast(time) } + }, 256); + + send_notification("filteredblockdisconnected", array_t + { + value_t{ possible_sign_cast(height) }, + value_t{ header } + }, 256); +} + +// Utilities (notification strand). +// ---------------------------------------------------------------------------- + +// Called from the notification strand (live, cursors advance) and parallel +// (rescan, against a snapshot), so neither strand is asserted here. +code protocol_btcd_rpc::match_addresses(matches& out, address_watch& sub, + const hash_digest& key, const std::set& heights) NOEXCEPT +{ + histories delta{}; + const auto& query = archive(); + const auto limit = server_settings().btcd.maximum_history; + const auto ec = query.get_history(stopping_, sub.cursor, delta, key, + limit, turbo_); + if (ec) + return ec; + + for (const auto& entry: delta) + if (entry.confirmed() && heights.contains(entry.tx.height())) + out[entry.tx.height()].emplace(entry.position, entry.tx.hash()); + + return error::success; +} + +void protocol_btcd_rpc::match_outpoints(matches& out, outpoint_watch& sub, + const point& prevout, const std::set& heights) NOEXCEPT +{ + outpoint_watch next{}; + const auto& query = archive(); + next.outpoint = query.get_tx_history(query.to_tx(prevout.hash())); + next.spenders = query.get_spenders_history(prevout); + + for (const auto& spender: difference(next.spenders, sub.spenders)) + if (spender.confirmed() && heights.contains(spender.tx.height())) + out[spender.tx.height()].emplace(spender.position, + spender.tx.hash()); + + sub = std::move(next); +} + +array_t protocol_btcd_rpc::serialize_matches(const matched_txs& txs) NOEXCEPT +{ + constexpr auto witness = true; + array_t out{}; + const auto& query = archive(); + for (const auto& [position, hash]: txs) + { + if (stopping_) + return out; - if (hit) - matched.emplace_back(value_t{ to_text(*tx, + const auto tx = query.get_transaction(query.to_tx(hash), witness); + if (tx) + out.emplace_back(value_t{ to_text(*tx, tx->serialized_size(witness), witness) }); } - return matched; + return out; } BC_POP_WARNING() diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index a025118e..a17c2181 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -23,6 +23,7 @@ using namespace system; static const code not_found{ server::error::not_found }; static const code not_implemented{ server::error::not_implemented }; static const code invalid_argument{ server::error::invalid_argument }; +static const code subscription_limit{ server::error::subscription_limit }; static const code unauthorized{ network::error::unauthorized }; static const code unexpected_method{ network::error::unexpected_method }; @@ -30,6 +31,7 @@ static const code unexpected_method{ network::error::unexpected_method }; // transaction only (its other outputs pay distinct key/script hashes), so a // filter watching found_address matches exactly one of its transactions. static const std::string found_address{ "1BaMPFdqMUQ46BV8iRcwbVfsam57oBLMM" }; +static const std::string other_address{ "1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn" }; static const std::string bogus_address{ "not-an-address" }; namespace { @@ -410,6 +412,52 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__unknown_method__default__unexpected_method) BOOST_AUTO_TEST_SUITE_END() +// Filter limit (btcd.maximum_filters): loadtxfilter watches are bounded per +// channel (DoS guard). +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(btcd_limited_filter_tests, btcd_limited_filter_setup_fixture) + +BOOST_AUTO_TEST_CASE(btcd_limited_filter__loadtxfilter__over_limit__subscription_limit) +{ + // The fixture allows one watch; the second address exceeds it. + const auto request = R"([true,["%1%","%2%"],[]])"; + const auto result = rpc_error("loadtxfilter", (boost_format(request) % found_address % other_address).str()); + BOOST_REQUIRE_EQUAL(result, subscription_limit.value()); +} + +BOOST_AUTO_TEST_SUITE_END() + +// Address index requirement: address watching and rescan are index queries, +// not_implemented when the index is disabled. +// ---------------------------------------------------------------------------- + +BOOST_FIXTURE_TEST_SUITE(btcd_no_index_tests, btcd_no_index_setup_fixture) + +BOOST_AUTO_TEST_CASE(btcd_no_index__loadtxfilter__address__not_implemented) +{ + const auto request = R"([true,["%1%"],[]])"; + const auto result = rpc_error("loadtxfilter", (boost_format(request) % found_address).str()); + BOOST_REQUIRE_EQUAL(result, not_implemented.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_no_index__loadtxfilter__outpoint_only__null_result) +{ + // Outpoint watching is spender lookup, not an address index query. + const auto request = R"([true,[],[{"hash":"%1%","index":0}]])"; + const auto response = rpc("loadtxfilter", (boost_format(request) % coinbase_txid(test::block1)).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} + +BOOST_AUTO_TEST_CASE(btcd_no_index__rescanblocks__any__not_implemented) +{ + const auto request = R"([["%1%"]])"; + const auto result = rpc_error("rescanblocks", (boost_format(request) % encode_hash(test::block1_hash)).str()); + BOOST_REQUIRE_EQUAL(result, not_implemented.value()); +} + +BOOST_AUTO_TEST_SUITE_END() + // Method-scoped credentials (channel_http::permitted()): a credential's // optional 'user:pass:method,...' suffix restricts which methods it may // call, enforced over ws by protocol_btcd_rpc::dispatch_websocket. diff --git a/test/protocols/btcd/btcd_setup_fixture.cpp b/test/protocols/btcd/btcd_setup_fixture.cpp index a19e944c..874f7a5a 100644 --- a/test/protocols/btcd/btcd_setup_fixture.cpp +++ b/test/protocols/btcd/btcd_setup_fixture.cpp @@ -28,7 +28,7 @@ using namespace boost::beast; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) btcd_setup_fixture::btcd_setup_fixture(const initializer& setup, - const configurator& configure) + bool address_index, const configurator& configure) : config_ { system::chain::selection::mainnet, @@ -39,6 +39,9 @@ btcd_setup_fixture::btcd_setup_fixture(const initializer& setup, { [&]() NOEXCEPT -> const database::settings& { + if (!address_index) + config_.database.address.buckets = 0; + config_.database.path = TEST_DIRECTORY; return config_.database; }() diff --git a/test/protocols/btcd/btcd_setup_fixture.hpp b/test/protocols/btcd/btcd_setup_fixture.hpp index c4055887..ecb3dc85 100644 --- a/test/protocols/btcd/btcd_setup_fixture.hpp +++ b/test/protocols/btcd/btcd_setup_fixture.hpp @@ -35,7 +35,7 @@ struct btcd_setup_fixture DELETE_COPY_MOVE(btcd_setup_fixture); explicit btcd_setup_fixture(const initializer& setup, - const configurator& configure={}); + bool address_index=true, const configurator& configure={}); ~btcd_setup_fixture(); // JSON-RPC 1.0 over websocket. params must be a json array (json-rpc-v1 @@ -108,7 +108,7 @@ struct btcd_credentialed_setup_fixture : btcd_setup_fixture([](test::query_t& query) { return test::setup_ten_block_store(query); - }, [](configuration& config) + }, true, [](configuration& config) { config.server.btcd.credentials = { @@ -129,7 +129,7 @@ struct btcd_scoped_credential_setup_fixture : btcd_setup_fixture([](test::query_t& query) { return test::setup_ten_block_store(query); - }, [](configuration& config) + }, true, [](configuration& config) { config.server.btcd.credentials = { @@ -141,4 +141,35 @@ struct btcd_scoped_credential_setup_fixture } }; +// Configured with a single-watch filter limit -- for tests that verify +// loadtxfilter enforces maximum_filters. +struct btcd_limited_filter_setup_fixture + : btcd_setup_fixture +{ + inline btcd_limited_filter_setup_fixture() + : btcd_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, true, [](configuration& config) + { + config.server.btcd.maximum_filters = 1; + }) + { + } +}; + +// Configured without the address index -- for tests that verify address +// watching and rescan report not_implemented. +struct btcd_no_index_setup_fixture + : btcd_setup_fixture +{ + inline btcd_no_index_setup_fixture() + : btcd_setup_fixture([](test::query_t& query) + { + return test::setup_ten_block_store(query); + }, false) + { + } +}; + #endif From b7e948d047fbd6810972d4ed7a35313193199743 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 12:53:33 -0400 Subject: [PATCH 17/29] Drop redundant size_t casts from bitcoind emissions. --- src/protocols/bitcoind/protocol_bitcoind_rpc.cpp | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index 1c9b6cb7..b2ee2692 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -314,8 +314,7 @@ bool protocol_bitcoind_rpc::handle_get_block_chain_info(const code& ec, // verificationprogress is approximated as confirmed/candidate height, the // best available estimate of the chain tip during sync (1.0 once current). const auto progress = is_zero(headers) ? 1.0 : - std::min(1.0, static_cast(blocks) / - static_cast(headers)); + std::min(1.0, to_floating(blocks) / to_floating(headers)); // Softfork activation is configured, not assumable. Taproot is reported // when configured active, with its configured activation height -- lnd's @@ -331,8 +330,7 @@ bool protocol_bitcoind_rpc::handle_get_block_chain_info(const code& ec, { "bit", 2 }, { "startTime", -1 }, { "timeout", -1 }, - { "since", possible_sign_cast( - settings.bip9_bit2_active_checkpoint.height()) }, + { "since", settings.bip9_bit2_active_checkpoint.height() }, { "min_activation_height", 0 } }); } @@ -342,8 +340,8 @@ bool protocol_bitcoind_rpc::handle_get_block_chain_info(const code& ec, send_result(object_t { { "chain", chain_name(query) }, - { "blocks", possible_wide_cast(blocks) }, - { "headers", possible_wide_cast(headers) }, + { "blocks", blocks }, + { "headers", headers }, { "bestblockhash", encode_hash(query.get_header_key(link)) }, { "bits", encode_base16(to_big_endian(header->bits())) }, { "target", encode_hash(from_uintx(compact::expand(header->bits()))) }, @@ -366,7 +364,7 @@ bool protocol_bitcoind_rpc::handle_get_block_count(const code& ec, return false; const auto top = archive().get_top_confirmed(); - send_result(possible_wide_cast(top), 20); + send_result(top, 20); return true; } From 8d1545cc519abb3458b0a48ca17766cb43ea3705 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 13:05:53 -0400 Subject: [PATCH 18/29] btcd: fix type safety issues, excessive/incorrect casting. --- src/protocols/btcd/protocol_btcd_rpc.cpp | 27 ++++++------ .../btcd/protocol_btcd_rpc_filter.cpp | 36 ++++++++-------- .../btcd/protocol_btcd_rpc_utility.cpp | 41 +++++++++++-------- 3 files changed, 54 insertions(+), 50 deletions(-) diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd_rpc.cpp index 6475864c..0e37540b 100644 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc.cpp @@ -233,8 +233,8 @@ bool protocol_btcd_rpc::handle_session(const code& ec, // The channel identifier is unique per connection for the process // lifetime, satisfying btcwallet's reconnect-detection use of session ids. object_t result{}; - result.emplace("id", value_t{ possible_sign_cast(identifier()) }); - send_result(value_t{ std::move(result) }, 32); + result.emplace("id", identifier()); + send_result(std::move(result), 32); return true; } @@ -246,9 +246,7 @@ bool protocol_btcd_rpc::handle_get_current_net(const code& ec, // The p2p handshake magic, the same value btcd returns (checked once by // btcwallet/lnd at connect to confirm the expected network). - send_result( - value_t{ possible_sign_cast(network_settings().identifier) }, - 20); + send_result(network_settings().identifier, 20); return true; } @@ -261,10 +259,9 @@ bool protocol_btcd_rpc::handle_get_best_block(const code& ec, // Required by btcwallet during wallet chain-sync bootstrap. const auto& query = archive(); object_t result{}; - result.emplace("hash", value_t{ encode_hash(query.get_top_confirmed_hash()) }); - result.emplace("height", value_t{ - possible_sign_cast(query.get_top_confirmed()) }); - send_result(value_t{ std::move(result) }, 96); + result.emplace("hash", encode_hash(query.get_top_confirmed_hash())); + result.emplace("height", query.get_top_confirmed()); + send_result(std::move(result), 96); return true; } @@ -414,12 +411,12 @@ bool protocol_btcd_rpc::handle_rescan(const code& ec, // Reply to the rescan call itself before the unprompted notification. send_result({}, 4); - array_t params{}; - params.emplace_back(value_t{ encode_hash(query.get_top_confirmed_hash()) }); - params.emplace_back(value_t{ possible_sign_cast(top) }); - params.emplace_back(value_t{ - possible_sign_cast(header->timestamp()) }); - send_notification("rescanfinished", std::move(params), 256); + send_notification("rescanfinished", array_t + { + encode_hash(query.get_top_confirmed_hash()), + top, + header->timestamp() + }, 256); return true; } diff --git a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp index be53d0ab..5e37781a 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp @@ -341,11 +341,11 @@ void protocol_btcd_rpc::do_rescan_watches( const auto at = matched.find(height); if (at != matched.end() && !at->second.empty()) { - discovered.emplace_back(value_t{ object_t + discovered.emplace_back(object_t { - { "hash", value_t{ encode_hash(hash) } }, - { "transactions", value_t{ serialize_matches(at->second) } } - } }); + { "hash", encode_hash(hash) }, + { "transactions", serialize_matches(at->second) } + }); } } @@ -367,7 +367,7 @@ void protocol_btcd_rpc::complete_rescan_blocks(const code& ec, return; } - send_result(value_t{ discovered }, 256); + send_result(discovered, 256); } // Notification event handlers. @@ -458,16 +458,16 @@ void protocol_btcd_rpc::notify_connected(const std::string& hash, send_notification("blockconnected", array_t { - value_t{ hash }, - value_t{ possible_sign_cast(height) }, - value_t{ possible_sign_cast(time) } + hash, + height, + time }, 256); send_notification("filteredblockconnected", array_t { - value_t{ possible_sign_cast(height) }, - value_t{ header }, - value_t{ txs } + height, + header, + txs }, 256); } @@ -483,15 +483,15 @@ void protocol_btcd_rpc::notify_disconnected(const std::string& hash, send_notification("blockdisconnected", array_t { - value_t{ hash }, - value_t{ possible_sign_cast(height) }, - value_t{ possible_sign_cast(time) } + hash, + height, + time }, 256); send_notification("filteredblockdisconnected", array_t { - value_t{ possible_sign_cast(height) }, - value_t{ header } + height, + header }, 256); } @@ -546,8 +546,8 @@ array_t protocol_btcd_rpc::serialize_matches(const matched_txs& txs) NOEXCEPT const auto tx = query.get_transaction(query.to_tx(hash), witness); if (tx) - out.emplace_back(value_t{ to_text(*tx, - tx->serialized_size(witness), witness) }); + out.emplace_back(to_text(*tx, + tx->serialized_size(witness), witness)); } return out; diff --git a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp b/src/protocols/btcd/protocol_btcd_rpc_utility.cpp index 9e15e347..ee17bcd7 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_rpc_utility.cpp @@ -18,7 +18,7 @@ */ #include -#include +#include #include #include #include @@ -86,7 +86,7 @@ bool protocol_btcd_rpc::handle_get_info(const code& ec, { { "version", version }, { "protocolversion", network_settings().protocol_maximum }, - { "blocks", possible_sign_cast(top) }, + { "blocks", top }, { "timeoffset", 0 }, { "connections", 0 }, { "proxy", std::string{} }, @@ -109,7 +109,7 @@ bool protocol_btcd_rpc::handle_get_net_totals(const code& ec, { { "totalbytesrecv", 0 }, { "totalbytessent", 0 }, - { "timemillis", possible_sign_cast(zulu_time()) * 1000 } + { "timemillis", possible_wide_cast(zulu_time()) * 1'000 } }, 64); return true; } @@ -124,8 +124,8 @@ bool protocol_btcd_rpc::handle_get_network_hash_ps(const code& ec, // target spacing, not a windowed average over 'blocks'. const auto& query = archive(); const auto top = query.get_top_confirmed(); - const auto target = (height < 0) ? top : - std::min(static_cast(height), top); + const auto target = is_negative(height) ? top : + std::min(possible_sign_cast(height), top); const auto header = query.get_header(query.to_confirmed(target)); if (!header) @@ -221,9 +221,17 @@ bool protocol_btcd_rpc::handle_create_raw_transaction(const code& ec, return true; } + // Scale to satoshis and validate, the value is client-provided and + // the product may exceed the integer domain (not whole, as scaling + // a decimal fraction carries floating point representation error). + uint64_t satoshi{}; const auto btc = std::get(pair.second.value()); - const auto satoshi = static_cast( - std::llround(btc * chain::satoshi_per_bitcoin)); + if (!to_integer(satoshi, btc * chain::satoshi_per_bitcoin, false)) + { + send_error(error::invalid_argument); + return true; + } + outs.emplace_back(satoshi, std::move(script)); } @@ -288,7 +296,7 @@ bool protocol_btcd_rpc::handle_decode_script(const code& ec, using namespace chain; object_t result{}; - result.emplace("asm", value_t{ script.to_string(flags::all_rules, true) }); + result.emplace("asm", script.to_string(flags::all_rules, true)); std::string kind{ "nonstandard" }; switch (script.output_pattern()) @@ -320,7 +328,7 @@ bool protocol_btcd_rpc::handle_decode_script(const code& ec, default: break; } - result.emplace("type", value_t{ kind }); + result.emplace("type", kind); if (script.output_pattern() == script_pattern::pay_key_hash || script.output_pattern() == script_pattern::pay_script_hash) @@ -328,15 +336,15 @@ bool protocol_btcd_rpc::handle_decode_script(const code& ec, const auto address = wallet::payment_address::extract_output(script, p2kh_, p2sh_); if (address) - result.emplace("address", value_t{ address.encoded() }); + result.emplace("address", address.encoded()); } // The p2sh-wrapping address of this exact script (btcd's "p2sh" field). const wallet::payment_address wrapped{ script, p2sh_ }; if (wrapped) - result.emplace("p2sh", value_t{ wrapped.encoded() }); + result.emplace("p2sh", wrapped.encoded()); - send_result(value_t{ std::move(result) }, 256); + send_result(std::move(result), 256); return true; } @@ -352,7 +360,7 @@ bool protocol_btcd_rpc::handle_validate_address(const code& ec, send_result(object_t { { "isvalid", true }, - { "address", value_t{ base58.encoded() } }, + { "address", base58.encoded() }, { "isscript", base58.prefix() == p2sh_ }, { "iswitness", false } }, 128); @@ -365,13 +373,12 @@ bool protocol_btcd_rpc::handle_validate_address(const code& ec, send_result(object_t { { "isvalid", true }, - { "address", value_t{ segwit.encoded() } }, + { "address", segwit.encoded() }, { "isscript", segwit.identifier() == wallet::witness_address::program_type::version0_p2sh }, { "iswitness", true }, - { "witness_version", value_t{ - possible_sign_cast(segwit.version()) } }, - { "witness_program", value_t{ encode_base16(segwit.program()) } } + { "witness_version", segwit.version() }, + { "witness_program", encode_base16(segwit.program()) } }, 128); return true; } From 185180ebb0cf9764381e2ed048b972acaaaa4222 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 15:18:53 -0400 Subject: [PATCH 19/29] btcd: rename protocol_btcd_rpc to protocol_btcd. --- builds/gnu/Makefile.am | 8 ++-- .../libbitcoin-server.vcxproj | 8 ++-- .../libbitcoin-server.vcxproj.filters | 8 ++-- .../libbitcoin-server.vcxproj | 8 ++-- .../libbitcoin-server.vcxproj.filters | 8 ++-- include/bitcoin/server.hpp | 2 +- ...rotocol_btcd_rpc.hpp => protocol_btcd.hpp} | 14 +++--- .../bitcoin/server/protocols/protocols.hpp | 6 +-- include/bitcoin/server/sessions/sessions.hpp | 4 +- ...rotocol_btcd_rpc.cpp => protocol_btcd.cpp} | 48 +++++++++---------- ...pc_filter.cpp => protocol_btcd_filter.cpp} | 36 +++++++------- ..._utility.cpp => protocol_btcd_utility.cpp} | 24 +++++----- test/protocols/btcd/btcd_rpc.cpp | 4 +- 13 files changed, 89 insertions(+), 89 deletions(-) rename include/bitcoin/server/protocols/{protocol_btcd_rpc.hpp => protocol_btcd.hpp} (96%) rename src/protocols/btcd/{protocol_btcd_rpc.cpp => protocol_btcd.cpp} (90%) rename src/protocols/btcd/{protocol_btcd_rpc_filter.cpp => protocol_btcd_filter.cpp} (92%) rename src/protocols/btcd/{protocol_btcd_rpc_utility.cpp => protocol_btcd_utility.cpp} (93%) diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index bfb6c265..31cfbf0e 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -64,9 +64,9 @@ src_libbitcoin_server_la_SOURCES = \ ${srcdir}/../../src/protocols/bitcoind/protocol_bitcoind_rest.cpp \ ${srcdir}/../../src/protocols/bitcoind/protocol_bitcoind_rpc.cpp \ ${srcdir}/../../src/protocols/bitcoind/protocol_bitcoind_rpc_json.cpp \ - ${srcdir}/../../src/protocols/btcd/protocol_btcd_rpc.cpp \ - ${srcdir}/../../src/protocols/btcd/protocol_btcd_rpc_filter.cpp \ - ${srcdir}/../../src/protocols/btcd/protocol_btcd_rpc_utility.cpp \ + ${srcdir}/../../src/protocols/btcd/protocol_btcd.cpp \ + ${srcdir}/../../src/protocols/btcd/protocol_btcd_filter.cpp \ + ${srcdir}/../../src/protocols/btcd/protocol_btcd_utility.cpp \ ${srcdir}/../../src/protocols/electrum/protocol_electrum.cpp \ ${srcdir}/../../src/protocols/electrum/protocol_electrum_addresses.cpp \ ${srcdir}/../../src/protocols/electrum/protocol_electrum_fees.cpp \ @@ -158,7 +158,7 @@ include_bitcoin_server_protocols_HEADERS = \ ${srcdir}/../../include/bitcoin/server/protocols/protocol_admin.hpp \ ${srcdir}/../../include/bitcoin/server/protocols/protocol_bitcoind_rest.hpp \ ${srcdir}/../../include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp \ - ${srcdir}/../../include/bitcoin/server/protocols/protocol_btcd_rpc.hpp \ + ${srcdir}/../../include/bitcoin/server/protocols/protocol_btcd.hpp \ ${srcdir}/../../include/bitcoin/server/protocols/protocol_electrum.hpp \ ${srcdir}/../../include/bitcoin/server/protocols/protocol_electrum_version.hpp \ ${srcdir}/../../include/bitcoin/server/protocols/protocol_html.hpp \ diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj index 793a95ee..fa8aaf94 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj @@ -134,9 +134,9 @@ - - - + + + @@ -196,7 +196,7 @@ - + diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters index 16570f39..cf6833e9 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -108,13 +108,13 @@ src\protocols\bitcoind - + src\protocols\btcd - + src\protocols\btcd - + src\protocols\btcd @@ -290,7 +290,7 @@ include\bitcoin\server\protocols - + include\bitcoin\server\protocols diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj index 84642d51..2e1568b9 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj @@ -134,9 +134,9 @@ - - - + + + @@ -196,7 +196,7 @@ - + diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters index 16570f39..cf6833e9 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -108,13 +108,13 @@ src\protocols\bitcoind - + src\protocols\btcd - + src\protocols\btcd - + src\protocols\btcd @@ -290,7 +290,7 @@ include\bitcoin\server\protocols - + include\bitcoin\server\protocols diff --git a/include/bitcoin/server.hpp b/include/bitcoin/server.hpp index dc8ea732..8b575b50 100644 --- a/include/bitcoin/server.hpp +++ b/include/bitcoin/server.hpp @@ -49,7 +49,7 @@ #include #include #include -#include +#include #include #include #include diff --git a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp b/include/bitcoin/server/protocols/protocol_btcd.hpp similarity index 96% rename from include/bitcoin/server/protocols/protocol_btcd_rpc.hpp rename to include/bitcoin/server/protocols/protocol_btcd.hpp index eb1cd9e1..7451f553 100644 --- a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd.hpp @@ -16,8 +16,8 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -#ifndef LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_BTCD_RPC_HPP -#define LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_BTCD_RPC_HPP +#ifndef LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_BTCD_HPP +#define LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_BTCD_HPP #include #include @@ -38,20 +38,20 @@ namespace server { /// ws authorization is established once per connection, by basic auth on the /// ws upgrade request or by the in-band 'authenticate' method, and latched /// on the channel (see dispatch_websocket). -class BCS_API protocol_btcd_rpc +class BCS_API protocol_btcd : public server::protocol_bitcoind_rpc, - protected network::tracker + protected network::tracker { public: - typedef std::shared_ptr ptr; + typedef std::shared_ptr ptr; using btcd_interface = interface::btcd; using btcd_dispatcher = network::rpc::dispatcher; - inline protocol_btcd_rpc(const auto& session, + inline protocol_btcd(const auto& session, const network::channel::ptr& channel, const options_t& options) NOEXCEPT : server::protocol_bitcoind_rpc(session, channel, options), - network::tracker(session->log), + network::tracker(session->log), options_(options), turbo_(session->database_settings().turbo), p2kh_(session->system_settings().forks.difficult ? diff --git a/include/bitcoin/server/protocols/protocols.hpp b/include/bitcoin/server/protocols/protocols.hpp index 0f4d6bc9..1929f7b9 100644 --- a/include/bitcoin/server/protocols/protocols.hpp +++ b/include/bitcoin/server/protocols/protocols.hpp @@ -22,7 +22,7 @@ #include #include #include -#include +#include #include #include #include @@ -50,7 +50,7 @@ network::protocol │ └── [server::protocol_rpc] ├── protocol_http │ ├── [server::protocol_bitcoind_rpc] -│ │ └── [server::protocol_btcd_rpc] +│ │ └── [server::protocol_btcd] │ └── [server::protocol_http] └── protocol_peer ├── [node::protocol_peer] @@ -96,6 +96,6 @@ server::protocol → node::protocol │ └── protocol_native └── protocol_bitcoind_rpc ├── protocol_bitcoind_rest - └── protocol_btcd_rpc + └── protocol_btcd */ diff --git a/include/bitcoin/server/sessions/sessions.hpp b/include/bitcoin/server/sessions/sessions.hpp index b1980396..78d2890f 100644 --- a/include/bitcoin/server/sessions/sessions.hpp +++ b/include/bitcoin/server/sessions/sessions.hpp @@ -32,7 +32,7 @@ namespace server { using session_admin = session_server; using session_native = session_server; using session_bitcoind = session_server; -using session_btcd = session_server; +using session_btcd = session_server; using session_stratum_v1 = session_server; using session_stratum_v2 = session_server; using session_electrum = session_handshake ╞══ session_native = server::session_server ╞══ session_bitcoind = server::session_server - ╞══ session_btcd = server::session_server + ╞══ session_btcd = server::session_server ╞══ session_stratum_v1 = server::session_server ╞══ session_stratum_v2 = server::session_server └── server::session_handshake<...Protocols> diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd.cpp similarity index 90% rename from src/protocols/btcd/protocol_btcd_rpc.cpp rename to src/protocols/btcd/protocol_btcd.cpp index 0e37540b..28815e29 100644 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ b/src/protocols/btcd/protocol_btcd.cpp @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -#include +#include #include #include @@ -25,7 +25,7 @@ namespace libbitcoin { namespace server { -#define CLASS protocol_btcd_rpc +#define CLASS protocol_btcd #define SUBSCRIBE_BTCD(method, ...) \ btcd_subscribe(&CLASS::method, __VA_ARGS__) @@ -41,7 +41,7 @@ BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) // Start. // ---------------------------------------------------------------------------- -void protocol_btcd_rpc::start() NOEXCEPT +void protocol_btcd::start() NOEXCEPT { BC_ASSERT(stranded()); @@ -81,7 +81,7 @@ void protocol_btcd_rpc::start() NOEXCEPT } // Events unsubscription is asynchronous, race is ok. -void protocol_btcd_rpc::stopping(const code& ec) NOEXCEPT +void protocol_btcd::stopping(const code& ec) NOEXCEPT { BC_ASSERT(stranded()); stopping_.store(true); @@ -97,7 +97,7 @@ void protocol_btcd_rpc::stopping(const code& ec) NOEXCEPT // is invalid if authorized and the method is authenticate, or not authorized // and the method is not authenticate (as btcd). -void protocol_btcd_rpc::handle_receive_post(const code& ec, +void protocol_btcd::handle_receive_post(const code& ec, const post::cptr& post) NOEXCEPT { BC_ASSERT(stranded()); @@ -152,7 +152,7 @@ void protocol_btcd_rpc::handle_receive_post(const code& ec, stop(code); } -void protocol_btcd_rpc::dispatch_websocket( +void protocol_btcd::dispatch_websocket( const network::http::request& request) NOEXCEPT { BC_ASSERT(stranded()); @@ -200,7 +200,7 @@ void protocol_btcd_rpc::dispatch_websocket( // Handlers (authentication/admin). // ---------------------------------------------------------------------------- -bool protocol_btcd_rpc::handle_authenticate(const code& ec, +bool protocol_btcd::handle_authenticate(const code& ec, btcd_interface::authenticate, const std::string& username, const std::string& password) NOEXCEPT { @@ -224,7 +224,7 @@ bool protocol_btcd_rpc::handle_authenticate(const code& ec, return true; } -bool protocol_btcd_rpc::handle_session(const code& ec, +bool protocol_btcd::handle_session(const code& ec, btcd_interface::session) NOEXCEPT { if (stopped(ec)) @@ -238,7 +238,7 @@ bool protocol_btcd_rpc::handle_session(const code& ec, return true; } -bool protocol_btcd_rpc::handle_get_current_net(const code& ec, +bool protocol_btcd::handle_get_current_net(const code& ec, btcd_interface::get_current_net) NOEXCEPT { if (stopped(ec)) @@ -250,7 +250,7 @@ bool protocol_btcd_rpc::handle_get_current_net(const code& ec, return true; } -bool protocol_btcd_rpc::handle_get_best_block(const code& ec, +bool protocol_btcd::handle_get_best_block(const code& ec, btcd_interface::get_best_block) NOEXCEPT { if (stopped(ec)) @@ -268,7 +268,7 @@ bool protocol_btcd_rpc::handle_get_best_block(const code& ec, // Handlers (block subscription). // ---------------------------------------------------------------------------- -bool protocol_btcd_rpc::handle_notify_blocks(const code& ec, +bool protocol_btcd::handle_notify_blocks(const code& ec, btcd_interface::notify_blocks) NOEXCEPT { if (stopped(ec)) @@ -279,7 +279,7 @@ bool protocol_btcd_rpc::handle_notify_blocks(const code& ec, return true; } -bool protocol_btcd_rpc::handle_stop_notify_blocks(const code& ec, +bool protocol_btcd::handle_stop_notify_blocks(const code& ec, btcd_interface::stop_notify_blocks) NOEXCEPT { if (stopped(ec)) @@ -293,7 +293,7 @@ bool protocol_btcd_rpc::handle_stop_notify_blocks(const code& ec, // Handlers (mempool subscription, not_implemented pending v5 mempool). // ---------------------------------------------------------------------------- -bool protocol_btcd_rpc::handle_notify_new_transactions(const code& ec, +bool protocol_btcd::handle_notify_new_transactions(const code& ec, btcd_interface::notify_new_transactions, bool) NOEXCEPT { if (stopped(ec)) return false; @@ -301,7 +301,7 @@ bool protocol_btcd_rpc::handle_notify_new_transactions(const code& ec, return true; } -bool protocol_btcd_rpc::handle_stop_notify_new_transactions(const code& ec, +bool protocol_btcd::handle_stop_notify_new_transactions(const code& ec, btcd_interface::stop_notify_new_transactions) NOEXCEPT { if (stopped(ec)) return false; @@ -309,13 +309,13 @@ bool protocol_btcd_rpc::handle_stop_notify_new_transactions(const code& ec, return true; } -// Handlers (address/outpoint filtering): see protocol_btcd_rpc_filter.cpp. +// Handlers (address/outpoint filtering): see protocol_btcd_filter.cpp. // ---------------------------------------------------------------------------- // Handler (admin, permanently not_implemented). // ---------------------------------------------------------------------------- -bool protocol_btcd_rpc::handle_stop(const code& ec, +bool protocol_btcd::handle_stop(const code& ec, btcd_interface::stop) NOEXCEPT { if (stopped(ec)) return false; @@ -329,7 +329,7 @@ bool protocol_btcd_rpc::handle_stop(const code& ec, // Handlers (deprecated, permanently not_implemented). // ---------------------------------------------------------------------------- -bool protocol_btcd_rpc::handle_notify_received(const code& ec, +bool protocol_btcd::handle_notify_received(const code& ec, btcd_interface::notify_received, const value_t&) NOEXCEPT { if (stopped(ec)) return false; @@ -337,7 +337,7 @@ bool protocol_btcd_rpc::handle_notify_received(const code& ec, return true; } -bool protocol_btcd_rpc::handle_stop_notify_received(const code& ec, +bool protocol_btcd::handle_stop_notify_received(const code& ec, btcd_interface::stop_notify_received, const value_t&) NOEXCEPT { if (stopped(ec)) return false; @@ -345,7 +345,7 @@ bool protocol_btcd_rpc::handle_stop_notify_received(const code& ec, return true; } -bool protocol_btcd_rpc::handle_notify_spent(const code& ec, +bool protocol_btcd::handle_notify_spent(const code& ec, btcd_interface::notify_spent, const value_t&) NOEXCEPT { if (stopped(ec)) return false; @@ -353,7 +353,7 @@ bool protocol_btcd_rpc::handle_notify_spent(const code& ec, return true; } -bool protocol_btcd_rpc::handle_stop_notify_spent(const code& ec, +bool protocol_btcd::handle_stop_notify_spent(const code& ec, btcd_interface::stop_notify_spent, const value_t&) NOEXCEPT { if (stopped(ec)) return false; @@ -361,7 +361,7 @@ bool protocol_btcd_rpc::handle_stop_notify_spent(const code& ec, return true; } -bool protocol_btcd_rpc::handle_rescan(const code& ec, +bool protocol_btcd::handle_rescan(const code& ec, btcd_interface::rescan, const std::string& beginblock, const value_t& addresses, const value_t& outpoints, const std::string&) NOEXCEPT @@ -423,9 +423,9 @@ bool protocol_btcd_rpc::handle_rescan(const code& ec, // Chase events. // ---------------------------------------------------------------------------- // Block events are matched against the watch-list on the notification strand -// (see protocol_btcd_rpc_filter.cpp) and notified back on the channel strand. +// (see protocol_btcd_filter.cpp) and notified back on the channel strand. -bool protocol_btcd_rpc::handle_chase(const code&, node::chase event_, +bool protocol_btcd::handle_chase(const code&, node::chase event_, node::event_value value) NOEXCEPT { // Do not pass ec to stopped, it is not a call status. @@ -460,7 +460,7 @@ bool protocol_btcd_rpc::handle_chase(const code&, node::chase event_, // Senders. // ---------------------------------------------------------------------------- -void protocol_btcd_rpc::send_notification(const std::string& method, +void protocol_btcd::send_notification(const std::string& method, array_t&& params, size_t size_hint) NOEXCEPT { BC_ASSERT(stranded()); diff --git a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp b/src/protocols/btcd/protocol_btcd_filter.cpp similarity index 92% rename from src/protocols/btcd/protocol_btcd_rpc_filter.cpp rename to src/protocols/btcd/protocol_btcd_filter.cpp index 5e37781a..b6eb66a7 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp +++ b/src/protocols/btcd/protocol_btcd_filter.cpp @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -#include +#include #include #include @@ -25,7 +25,7 @@ namespace libbitcoin { namespace server { -#define CLASS protocol_btcd_rpc +#define CLASS protocol_btcd // protocol_bitcoind_rpc declares 'using post = network::http::method::post', // which shadows network::protocol::post. Qualify explicitly. @@ -47,7 +47,7 @@ BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) // by index key (sha256 of the output script), matched by cursored history // delta, so each connected block costs one bounded index walk per watch. -code protocol_btcd_rpc::parse_filter_keys(const value_t& addresses, +code protocol_btcd::parse_filter_keys(const value_t& addresses, std::vector& out) NOEXCEPT { BC_ASSERT(stranded()); @@ -71,7 +71,7 @@ code protocol_btcd_rpc::parse_filter_keys(const value_t& addresses, return error::success; } -code protocol_btcd_rpc::parse_filter_points(const value_t& outpoints, +code protocol_btcd::parse_filter_points(const value_t& outpoints, std::vector& out) NOEXCEPT { BC_ASSERT(stranded()); @@ -107,7 +107,7 @@ code protocol_btcd_rpc::parse_filter_points(const value_t& outpoints, // Handlers (address/outpoint filtering). // ---------------------------------------------------------------------------- -bool protocol_btcd_rpc::handle_load_tx_filter(const code& ec, +bool protocol_btcd::handle_load_tx_filter(const code& ec, btcd_interface::load_tx_filter, bool reload, const value_t& addresses, const value_t& outpoints) NOEXCEPT { @@ -140,7 +140,7 @@ bool protocol_btcd_rpc::handle_load_tx_filter(const code& ec, return true; } -void protocol_btcd_rpc::do_load_tx_filter(bool reload, +void protocol_btcd::do_load_tx_filter(bool reload, const std::vector& keys, const std::vector& points) NOEXCEPT { @@ -208,7 +208,7 @@ void protocol_btcd_rpc::do_load_tx_filter(bool reload, POST_BTCD(complete_load_tx_filter, ec); } -void protocol_btcd_rpc::complete_load_tx_filter(const code& ec) NOEXCEPT +void protocol_btcd::complete_load_tx_filter(const code& ec) NOEXCEPT { BC_ASSERT(stranded()); @@ -225,7 +225,7 @@ void protocol_btcd_rpc::complete_load_tx_filter(const code& ec) NOEXCEPT send_result({}, 4); } -bool protocol_btcd_rpc::handle_rescan_blocks(const code& ec, +bool protocol_btcd::handle_rescan_blocks(const code& ec, btcd_interface::rescan_blocks, const value_t& blockhashes) NOEXCEPT { if (stopped(ec)) @@ -265,7 +265,7 @@ bool protocol_btcd_rpc::handle_rescan_blocks(const code& ec, // Snapshot the watch-list on the notification strand, so the query itself // runs parallel (as electrum's one-shot queries) without serializing against // this channel's notifications. -void protocol_btcd_rpc::do_rescan_blocks( +void protocol_btcd::do_rescan_blocks( const std::vector& hashes) NOEXCEPT { BC_ASSERT(notification_strand_.running_in_this_thread()); @@ -283,7 +283,7 @@ void protocol_btcd_rpc::do_rescan_blocks( PARALLEL(do_rescan_watches, hashes, std::move(keys), std::move(points)); } -void protocol_btcd_rpc::do_rescan_watches( +void protocol_btcd::do_rescan_watches( const std::vector& hashes, const std::vector& keys, const std::vector& points) NOEXCEPT @@ -352,7 +352,7 @@ void protocol_btcd_rpc::do_rescan_watches( POST_BTCD(complete_rescan_blocks, error::success, std::move(discovered)); } -void protocol_btcd_rpc::complete_rescan_blocks(const code& ec, +void protocol_btcd::complete_rescan_blocks(const code& ec, const array_t& discovered) NOEXCEPT { BC_ASSERT(stranded()); @@ -373,7 +373,7 @@ void protocol_btcd_rpc::complete_rescan_blocks(const code& ec, // Notification event handlers. // ---------------------------------------------------------------------------- -void protocol_btcd_rpc::do_connected(node::header_t link_value) NOEXCEPT +void protocol_btcd::do_connected(node::header_t link_value) NOEXCEPT { BC_ASSERT(notification_strand_.running_in_this_thread()); @@ -420,7 +420,7 @@ void protocol_btcd_rpc::do_connected(node::header_t link_value) NOEXCEPT to_text(*header, chain::header::serialized_size()), std::move(txs)); } -void protocol_btcd_rpc::do_disconnected(node::header_t link_value) NOEXCEPT +void protocol_btcd::do_disconnected(node::header_t link_value) NOEXCEPT { BC_ASSERT(notification_strand_.running_in_this_thread()); @@ -447,7 +447,7 @@ void protocol_btcd_rpc::do_disconnected(node::header_t link_value) NOEXCEPT // btcd 'blockconnected' [hash, height, time] and 'filteredblockconnected' // [height, header, subscribedtxs]. filtered is sent unconditionally // alongside (as btcd) -- an empty filter just yields an empty array. -void protocol_btcd_rpc::notify_connected(const std::string& hash, +void protocol_btcd::notify_connected(const std::string& hash, size_t height, uint32_t time, const std::string& header, const array_t& txs) NOEXCEPT { @@ -473,7 +473,7 @@ void protocol_btcd_rpc::notify_connected(const std::string& hash, // btcd 'blockdisconnected' [hash, height, time] and // 'filteredblockdisconnected' [height, header]. -void protocol_btcd_rpc::notify_disconnected(const std::string& hash, +void protocol_btcd::notify_disconnected(const std::string& hash, size_t height, uint32_t time, const std::string& header) NOEXCEPT { BC_ASSERT(stranded()); @@ -500,7 +500,7 @@ void protocol_btcd_rpc::notify_disconnected(const std::string& hash, // Called from the notification strand (live, cursors advance) and parallel // (rescan, against a snapshot), so neither strand is asserted here. -code protocol_btcd_rpc::match_addresses(matches& out, address_watch& sub, +code protocol_btcd::match_addresses(matches& out, address_watch& sub, const hash_digest& key, const std::set& heights) NOEXCEPT { histories delta{}; @@ -518,7 +518,7 @@ code protocol_btcd_rpc::match_addresses(matches& out, address_watch& sub, return error::success; } -void protocol_btcd_rpc::match_outpoints(matches& out, outpoint_watch& sub, +void protocol_btcd::match_outpoints(matches& out, outpoint_watch& sub, const point& prevout, const std::set& heights) NOEXCEPT { outpoint_watch next{}; @@ -534,7 +534,7 @@ void protocol_btcd_rpc::match_outpoints(matches& out, outpoint_watch& sub, sub = std::move(next); } -array_t protocol_btcd_rpc::serialize_matches(const matched_txs& txs) NOEXCEPT +array_t protocol_btcd::serialize_matches(const matched_txs& txs) NOEXCEPT { constexpr auto witness = true; array_t out{}; diff --git a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp b/src/protocols/btcd/protocol_btcd_utility.cpp similarity index 93% rename from src/protocols/btcd/protocol_btcd_rpc_utility.cpp rename to src/protocols/btcd/protocol_btcd_utility.cpp index ee17bcd7..816fe5c9 100644 --- a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_utility.cpp @@ -16,7 +16,7 @@ * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ -#include +#include #include #include @@ -26,7 +26,7 @@ namespace libbitcoin { namespace server { -#define CLASS protocol_btcd_rpc +#define CLASS protocol_btcd using namespace system; using namespace network; @@ -40,7 +40,7 @@ BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) // Generic btcd-tooling compatibility. // ---------------------------------------------------------------------------- -bool protocol_btcd_rpc::handle_get_difficulty(const code& ec, +bool protocol_btcd::handle_get_difficulty(const code& ec, btcd_interface::get_difficulty) NOEXCEPT { if (stopped(ec)) @@ -59,7 +59,7 @@ bool protocol_btcd_rpc::handle_get_difficulty(const code& ec, return true; } -bool protocol_btcd_rpc::handle_get_info(const code& ec, +bool protocol_btcd::handle_get_info(const code& ec, btcd_interface::get_info) NOEXCEPT { if (stopped(ec)) @@ -98,7 +98,7 @@ bool protocol_btcd_rpc::handle_get_info(const code& ec, return true; } -bool protocol_btcd_rpc::handle_get_net_totals(const code& ec, +bool protocol_btcd::handle_get_net_totals(const code& ec, btcd_interface::get_net_totals) NOEXCEPT { if (stopped(ec)) @@ -114,7 +114,7 @@ bool protocol_btcd_rpc::handle_get_net_totals(const code& ec, return true; } -bool protocol_btcd_rpc::handle_get_network_hash_ps(const code& ec, +bool protocol_btcd::handle_get_network_hash_ps(const code& ec, btcd_interface::get_network_hash_ps, uint32_t, int32_t height) NOEXCEPT { if (stopped(ec)) @@ -143,7 +143,7 @@ bool protocol_btcd_rpc::handle_get_network_hash_ps(const code& ec, // Raw transaction/script utilities. // ---------------------------------------------------------------------------- -code protocol_btcd_rpc::parse_output_script(const std::string& text, +code protocol_btcd::parse_output_script(const std::string& text, chain::script& out) NOEXCEPT { const wallet::payment_address base58(text); @@ -163,7 +163,7 @@ code protocol_btcd_rpc::parse_output_script(const std::string& text, return error::invalid_argument; } -bool protocol_btcd_rpc::handle_create_raw_transaction(const code& ec, +bool protocol_btcd::handle_create_raw_transaction(const code& ec, btcd_interface::create_raw_transaction, const array_t& inputs, const object_t& outputs, uint32_t locktime) NOEXCEPT { @@ -244,7 +244,7 @@ bool protocol_btcd_rpc::handle_create_raw_transaction(const code& ec, return true; } -bool protocol_btcd_rpc::handle_decode_raw_transaction(const code& ec, +bool protocol_btcd::handle_decode_raw_transaction(const code& ec, btcd_interface::decode_raw_transaction, const std::string& hexstring) NOEXCEPT { @@ -273,7 +273,7 @@ bool protocol_btcd_rpc::handle_decode_raw_transaction(const code& ec, return true; } -bool protocol_btcd_rpc::handle_decode_script(const code& ec, +bool protocol_btcd::handle_decode_script(const code& ec, btcd_interface::decode_script, const std::string& hex) NOEXCEPT { if (stopped(ec)) @@ -348,7 +348,7 @@ bool protocol_btcd_rpc::handle_decode_script(const code& ec, return true; } -bool protocol_btcd_rpc::handle_validate_address(const code& ec, +bool protocol_btcd::handle_validate_address(const code& ec, btcd_interface::validate_address, const std::string& address) NOEXCEPT { if (stopped(ec)) @@ -387,7 +387,7 @@ bool protocol_btcd_rpc::handle_validate_address(const code& ec, return true; } -bool protocol_btcd_rpc::handle_help(const code& ec, btcd_interface::help, +bool protocol_btcd::handle_help(const code& ec, btcd_interface::help, const std::string&) NOEXCEPT { if (stopped(ec)) diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index a17c2181..e2982e7c 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -460,7 +460,7 @@ BOOST_AUTO_TEST_SUITE_END() // Method-scoped credentials (channel_http::permitted()): a credential's // optional 'user:pass:method,...' suffix restricts which methods it may -// call, enforced over ws by protocol_btcd_rpc::dispatch_websocket. +// call, enforced over ws by protocol_btcd::dispatch_websocket. // ---------------------------------------------------------------------------- BOOST_FIXTURE_TEST_SUITE(btcd_scoped_credential_tests, btcd_scoped_credential_setup_fixture) @@ -484,7 +484,7 @@ BOOST_AUTO_TEST_CASE(btcd_scoped_credential__notifyblocks__unlisted_method__unau BOOST_AUTO_TEST_SUITE_END() -// authenticate (protocol_btcd_rpc::handle_authenticate): exercises the +// authenticate (protocol_btcd::handle_authenticate): exercises the // credential-configured branch, not covered by btcd_tests above (whose // fixture leaves btcd.credential unset, so every one of those connections // takes the no-op path). From 07131bb75d73423e313eda7f9a999670b350bbe7 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 17:36:14 -0400 Subject: [PATCH 20/29] btcd: reorder interface methods, strip narrative comments. --- endpoints/test_bitcoind_rpc.py | 6 +- endpoints/test_btcd_rpc.py | 18 +- endpoints/test_electrum.py | 22 +- endpoints/test_native.py | 4 +- include/bitcoin/server/interfaces/btcd.hpp | 103 +++--- .../server/protocols/protocol_btcd.hpp | 103 ++---- .../bitcoind/protocol_bitcoind_rpc.cpp | 2 +- src/protocols/btcd/protocol_btcd.cpp | 107 +++--- src/protocols/btcd/protocol_btcd_filter.cpp | 81 ++--- src/protocols/btcd/protocol_btcd_utility.cpp | 86 +++-- test/protocols/btcd/btcd_rpc.cpp | 318 +++++++++--------- 11 files changed, 406 insertions(+), 444 deletions(-) diff --git a/endpoints/test_bitcoind_rpc.py b/endpoints/test_bitcoind_rpc.py index 68923927..6d070047 100644 --- a/endpoints/test_bitcoind_rpc.py +++ b/endpoints/test_bitcoind_rpc.py @@ -172,7 +172,7 @@ def raw_rpc( # ═══════════════════════════════════════════════════════════════════════════════ def test_getbestblockhash(bitcoind_rpc_config): - """Test getbestblockhash - returns hash of the best (tip) block""" + """Test getbestblockhash - returns hash of the top block""" response = send_rpc(bitcoind_rpc_config, "getbestblockhash") assert "error" not in response or response["error"] is None @@ -222,7 +222,7 @@ def test_getblockchaininfo(bitcoind_rpc_config): assert "chain" in result # chainwork and size_on_disk are intentionally omitted by the implementation: # chainwork needs a cumulative-work index (cf. getchainwork, not implemented) - # and size_on_disk needs store-size accounting. The remaining Core fields are + # and size_on_disk needs store-size accounting. The remaining bitcoind fields are # returned. assert "difficulty" in result assert "headers" in result @@ -500,7 +500,7 @@ def test_getrawtransaction_segwit(bitcoind_rpc_config): assert result["hash"] != result["txid"] # Segwit weight accounting: vsize == ceil(weight / 4). # (Note: libbitcoin reports "size" as the stripped, non-witness size, whereas - # Core reports the total witnessed size, so size/vsize ordering is not + # bitcoind reports the total witnessed size, so size/vsize ordering is not # asserted here.) if "weight" in result and "vsize" in result: assert result["vsize"] == (result["weight"] + 3) // 4 diff --git a/endpoints/test_btcd_rpc.py b/endpoints/test_btcd_rpc.py index 70e7d72c..49bd9c96 100644 --- a/endpoints/test_btcd_rpc.py +++ b/endpoints/test_btcd_rpc.py @@ -2,9 +2,9 @@ Tests for libbitcoin-server btcd JSON-RPC/websocket compatibility interface. btcd speaks JSON-RPC 1.0 over a persistent websocket connection (preferred, -required for the session/notification extension methods) or plain HTTP POST -(same request/response shape as the bitcoind endpoint, for the bitcoind interface methods -inherited from protocol_bitcoind_rpc). +required for the session/notification methods) or plain HTTP POST +(same request/response shape as the bitcoind endpoint, for the bitcoind +interface methods inherited from protocol_bitcoind_rpc). This suite is split into what's real today and what's a development target: @@ -289,7 +289,7 @@ def conn(btcd_config: dict) -> BtcdConnection: the server has credentials configured. Every method other than 'authenticate' itself is rejected over ws until - the connection has authenticated (see protocol_btcd_rpc::dispatch_ + the connection has authenticated (see protocol_btcd::dispatch_ websocket) -- a real client always does this handshake first, so this fixture does it here rather than in every single test. @@ -500,7 +500,7 @@ def test_rescan_no_addrs_no_outpoints_finishes_immediately(conn): bootstrap its initial sync starting point (found via a real lnd integration test), distinct from rescanblocks/loadtxfilter's actual address-watching path. The finished notification always carries the - current chain tip, not the requested beginblock. + current chain top, not the requested beginblock. """ response = conn.send_rpc("rescan", [ReferenceData.GENESIS_HASH, [], [], ""]) assert response.get("error") is None @@ -572,7 +572,7 @@ def test_filteredblockconnected_notification(conn, btcd_config): # ═══════════════════════════════════════════════════════════════════════════════ # Inherited from protocol_bitcoind_rpc. Reachable both over plain http post # (unchanged from bitcoind) and over the same ws connection used -# for session/notifyblocks/etc: protocol_btcd_rpc::dispatch_websocket falls +# for session/notifyblocks/etc: protocol_btcd::dispatch_websocket falls # back to protocol_bitcoind_rpc::dispatch_rpc when the btcd-only dispatcher # reports "unexpected method". This is what a real lnd/btcwallet client # needs -- it can't open a second plain-http connection once it has upgraded @@ -602,16 +602,16 @@ def test_chain_method_over_http_post(btcd_config, method, params): @pytest.mark.parametrize("method,params", BITCOIND_METHODS) def test_chain_method_over_websocket(conn, method, params): """bitcoind interface methods also work over the same ws connection used - for session/notifyblocks/etc (see protocol_btcd_rpc::dispatch_websocket / + for session/notifyblocks/etc (see protocol_btcd::dispatch_websocket / protocol_bitcoind_rpc::dispatch_rpc).""" response = conn.send_rpc(method, params) assert "result" in response -def test_btcd_and_chain_method_share_one_websocket_connection(conn): +def test_btcd_and_bitcoind_method_share_one_websocket_connection(conn): """The actual point of the ws bridge: a single persistent ws connection -- the kind a real lnd/btcwallet client opens once and keeps -- can reach both a - btcd-only extension method (session) and a bitcoind interface method + btcd interface method (session) and a bitcoind interface method (getblockcount) without reconnecting or falling back to plain http post. """ session = conn.send_rpc("session") diff --git a/endpoints/test_electrum.py b/endpoints/test_electrum.py index 1f2d2cb3..17aa3ac8 100644 --- a/endpoints/test_electrum.py +++ b/endpoints/test_electrum.py @@ -560,7 +560,7 @@ def test_blockchain_block_headers_height_900000_with_cp(): assert isinstance(h, str) and len(h) == 64 -def test_blockchain_block_headers_20000_at_tip_with_cp(): +def test_blockchain_block_headers_20000_at_top_with_cp(): """ Request 20,000 headers at a well-confirmed range with a checkpoint proof. @@ -570,7 +570,7 @@ def test_blockchain_block_headers_20000_at_tip_with_cp(): the response is valid and internally consistent, not a specific count. """ start, count = 900000, 20000 - cp = start + count - 1 # = 919999, well below current tip + cp = start + count - 1 # = 919999, well below current top result = send_rpc("blockchain.block.headers", [start, count, cp]) assert isinstance(result, dict) @@ -589,9 +589,9 @@ def test_blockchain_block_headers_20000_at_tip_with_cp(): assert isinstance(h, str) and len(h) == 64 -def test_blockchain_block_headers_max_at_tip_with_cp(): +def test_blockchain_block_headers_max_at_top_with_cp(): """ - Request as many headers as possible ending at the current chain tip. + Request as many headers as possible ending at the current chain top. Uses count=20160 (max in the Electrum spec). The server will cap the actual returned count at its own configured maximum. The test verifies @@ -600,17 +600,17 @@ def test_blockchain_block_headers_max_at_tip_with_cp(): """ count = 20160 - tip = send_rpc("blockchain.headers.subscribe") + top = send_rpc("blockchain.headers.subscribe") # height key differs by version: 'height' (v1.3+) or 'block_height' (v1.0-v1.2) - tip_height = ( - tip.get("height") or tip.get("block_height") - if isinstance(tip, dict) else None + top_height = ( + top.get("height") or top.get("block_height") + if isinstance(top, dict) else None ) - if tip_height is None: + if top_height is None: pytest.skip("blockchain.headers.subscribe did not return height") - start = tip_height - count + 1 - cp = tip_height + start = top_height - count + 1 + cp = top_height result = send_rpc("blockchain.block.headers", [start, count, cp]) diff --git a/endpoints/test_native.py b/endpoints/test_native.py index c7a9629b..6223578b 100644 --- a/endpoints/test_native.py +++ b/endpoints/test_native.py @@ -82,13 +82,13 @@ def test_configuration(native_config): def test_top(native_config): - """Test /v1/top endpoint - chain tip information.""" + """Test /v1/top endpoint - chain top information.""" data = get_json(native_config["base_url"], "top") if isinstance(data, int): assert data >= 100_000, "Chain height should be at least 100k" else: assert isinstance(data, dict) - height = data.get("height") or data.get("chain_height") or data.get("tip_height") + height = data.get("height") assert isinstance(height, int) assert height >= 100_000, "Chain height should be at least 100k" diff --git a/include/bitcoin/server/interfaces/btcd.hpp b/include/bitcoin/server/interfaces/btcd.hpp index 835a6785..e8dd33b4 100644 --- a/include/bitcoin/server/interfaces/btcd.hpp +++ b/include/bitcoin/server/interfaces/btcd.hpp @@ -31,50 +31,42 @@ struct btcd_methods /// BTCD protocol (unversioned, json-rpc v1.0 over http/ws). static constexpr std::tuple methods { - /// Authentication (one-time, in-band alternative to basic auth). + /// Administrative (stop not implemented). method<"authenticate", string_t, string_t>{ "username", "password" }, - - /// Admin. + method<"help", optional<""_t>>{ "command" }, method<"session">{}, + method<"stop">{}, - /// Block subscription. + /// Getters. + method<"getbestblock">{}, + method<"getcurrentnet">{}, + method<"getdifficulty">{}, + method<"getinfo">{}, + method<"getnettotals">{}, + method<"getnetworkhashps", optional<120_u32>, optional<-1_i32>>{ "blocks", "height" }, + + /// Tools. + method<"createrawtransaction", array_t, object_t, optional<0_u32>>{ "inputs", "outputs", "locktime" }, + method<"decoderawtransaction", string_t>{ "hexstring" }, + method<"decodescript", string_t>{ "hex" }, + method<"validateaddress", string_t>{ "address" }, + + /// Subscription. method<"notifyblocks">{}, method<"stopnotifyblocks">{}, - - /// Tx subscription. method<"notifynewtransactions", optional>{ "verbose" }, method<"stopnotifynewtransactions">{}, - /// Address/outpoint filtering. + /// Filters. method<"loadtxfilter", boolean_t, value_t, value_t>{ "reload", "addresses", "outpoints" }, method<"rescanblocks", value_t>{ "blockhashes" }, - /// Admin (not_implemented; no secure remote-shutdown path exists). - method<"stop">{}, - - /// Deprecated (address/outpoint filtering). + /// Deprecated. method<"notifyreceived", value_t>{ "addresses" }, method<"stopnotifyreceived", value_t>{ "addresses" }, method<"notifyspent", value_t>{ "outpoints" }, method<"stopnotifyspent", value_t>{ "outpoints" }, - method<"rescan", string_t, value_t, value_t, optional<""_t>>{ "beginblock", "addresses", "outpoints", "endblock" }, - - /// Admin (network magic). - method<"getcurrentnet">{}, - - /// Generic btcd-tooling compatibility. - method<"getdifficulty">{}, - method<"getinfo">{}, - method<"getnettotals">{}, - method<"getnetworkhashps", optional<120_u32>, optional<-1_i32>>{ "blocks", "height" }, - method<"createrawtransaction", array_t, object_t, optional<0_u32>>{ "inputs", "outputs", "locktime" }, - method<"decoderawtransaction", string_t>{ "hexstring" }, - method<"decodescript", string_t>{ "hex" }, - method<"validateaddress", string_t>{ "address" }, - method<"help", optional<""_t>>{ "command" }, - - /// btcd extension: {hash, height} of the best chain tip. - method<"getbestblock">{} + method<"rescan", string_t, value_t, value_t, optional<""_t>>{ "beginblock", "addresses", "outpoints", "endblock" } }; template @@ -85,30 +77,35 @@ struct btcd_methods // Derive this from above in c++26 using reflection. using authenticate = at<0>; - using session = at<1>; - using notify_blocks = at<2>; - using stop_notify_blocks = at<3>; - using notify_new_transactions = at<4>; - using stop_notify_new_transactions = at<5>; - using load_tx_filter = at<6>; - using rescan_blocks = at<7>; - using stop = at<8>; - using notify_received = at<9>; - using stop_notify_received = at<10>; - using notify_spent = at<11>; - using stop_notify_spent = at<12>; - using rescan = at<13>; - using get_current_net = at<14>; - using get_difficulty = at<15>; - using get_info = at<16>; - using get_net_totals = at<17>; - using get_network_hash_ps = at<18>; - using create_raw_transaction = at<19>; - using decode_raw_transaction = at<20>; - using decode_script = at<21>; - using validate_address = at<22>; - using help = at<23>; - using get_best_block = at<24>; + using help = at<1>; + using session = at<2>; + using stop = at<3>; + + using get_best_block = at<4>; + using get_current_net = at<5>; + using get_difficulty = at<6>; + using get_info = at<7>; + using get_net_totals = at<8>; + using get_network_hash_ps = at<9>; + + using create_raw_transaction = at<10>; + using decode_raw_transaction = at<11>; + using decode_script = at<12>; + using validate_address = at<13>; + + using notify_blocks = at<14>; + using stop_notify_blocks = at<15>; + using notify_new_transactions = at<16>; + using stop_notify_new_transactions = at<17>; + + using load_tx_filter = at<18>; + using rescan_blocks = at<19>; + + using notify_received = at<20>; + using stop_notify_received = at<21>; + using notify_spent = at<22>; + using stop_notify_spent = at<23>; + using rescan = at<24>; }; } // namespace interface diff --git a/include/bitcoin/server/protocols/protocol_btcd.hpp b/include/bitcoin/server/protocols/protocol_btcd.hpp index 7451f553..fc4ad9aa 100644 --- a/include/bitcoin/server/protocols/protocol_btcd.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd.hpp @@ -23,7 +23,6 @@ #include #include #include -#include #include #include #include @@ -32,12 +31,7 @@ namespace libbitcoin { namespace server { -/// btcd-compatible json-rpc-v1 over http/ws. Adds the btcd extension -/// interface to the inherited bitcoind interface, each verb handler trying -/// its own dispatcher and deferring to the base transport handler on a miss. -/// ws authorization is established once per connection, by basic auth on the -/// ws upgrade request or by the in-band 'authenticate' method, and latched -/// on the channel (see dispatch_websocket). +/// btcd interface, added to the inherited bitcoind interface. class BCS_API protocol_btcd : public server::protocol_bitcoind_rpc, protected network::tracker @@ -68,30 +62,25 @@ class BCS_API protocol_btcd void stopping(const code& ec) NOEXCEPT override; protected: - /// Dispatch (the post and websocket transports of the btcd extension - /// interface). Each defers to the base transport handler on a method - /// name miss (the interfaces have disjoint method names). + /// Dispatch. void handle_receive_post(const code& ec, const post::cptr& post) NOEXCEPT override; void dispatch_websocket( const network::http::request& request) NOEXCEPT override; - /// Handlers (authentication/admin). A failed authenticate ends the - /// session, once the error response has reached the client (btcd closes - /// the connection on invalid credentials). + /// Handlers (administrative). bool handle_authenticate(const code& ec, btcd_interface::authenticate, const std::string& username, const std::string& password) NOEXCEPT; + bool handle_help(const code& ec, btcd_interface::help, + const std::string& command) NOEXCEPT; bool handle_session(const code& ec, btcd_interface::session) NOEXCEPT; + bool handle_stop(const code& ec, btcd_interface::stop) NOEXCEPT; - /// Handler (network magic). - bool handle_get_current_net(const code& ec, - btcd_interface::get_current_net) NOEXCEPT; - - /// Handler (chain tip {hash, height}). + /// Handlers (getters). bool handle_get_best_block(const code& ec, btcd_interface::get_best_block) NOEXCEPT; - - /// Handlers (generic btcd-tooling compatibility). + bool handle_get_current_net(const code& ec, + btcd_interface::get_current_net) NOEXCEPT; bool handle_get_difficulty(const code& ec, btcd_interface::get_difficulty) NOEXCEPT; bool handle_get_info(const code& ec, btcd_interface::get_info) NOEXCEPT; @@ -100,6 +89,8 @@ class BCS_API protocol_btcd bool handle_get_network_hash_ps(const code& ec, btcd_interface::get_network_hash_ps, uint32_t blocks, int32_t height) NOEXCEPT; + + /// Handlers (tools). bool handle_create_raw_transaction(const code& ec, btcd_interface::create_raw_transaction, const network::rpc::array_t& inputs, @@ -112,30 +103,18 @@ class BCS_API protocol_btcd bool handle_validate_address(const code& ec, btcd_interface::validate_address, const std::string& address) NOEXCEPT; - bool handle_help(const code& ec, btcd_interface::help, - const std::string& command) NOEXCEPT; - /// Address string (base58 or bech32/bech32m) to output script, for - /// createrawtransaction. - code parse_output_script(const std::string& text, - system::chain::script& out) NOEXCEPT; - - /// Handlers (block subscription). + /// Handlers (subscription, mempool pending v5). bool handle_notify_blocks(const code& ec, btcd_interface::notify_blocks) NOEXCEPT; bool handle_stop_notify_blocks(const code& ec, btcd_interface::stop_notify_blocks) NOEXCEPT; - - /// Handlers (mempool subscription, not_implemented pending v5 mempool). bool handle_notify_new_transactions(const code& ec, btcd_interface::notify_new_transactions, bool verbose) NOEXCEPT; bool handle_stop_notify_new_transactions(const code& ec, btcd_interface::stop_notify_new_transactions) NOEXCEPT; - /// Handlers (address/outpoint filtering). notifyblocks arms delivery; - /// loadtxfilter only populates the filter (as btcd). rescanblocks - /// replays the match logic against explicitly named historical blocks. - /// Address watching requires the address index (not_implemented). + /// Handlers (filters, address watch requires the address index). bool handle_load_tx_filter(const code& ec, btcd_interface::load_tx_filter, bool reload, const network::rpc::value_t& addresses, @@ -144,10 +123,7 @@ class BCS_API protocol_btcd btcd_interface::rescan_blocks, const network::rpc::value_t& blockhashes) NOEXCEPT; - /// Handler (admin, permanently not_implemented). - bool handle_stop(const code& ec, btcd_interface::stop) NOEXCEPT; - - /// Handlers (deprecated, permanently not_implemented). + /// Handlers (deprecated, not_implemented). bool handle_notify_received(const code& ec, btcd_interface::notify_received, const network::rpc::value_t& addresses) NOEXCEPT; @@ -165,11 +141,15 @@ class BCS_API protocol_btcd const network::rpc::value_t& outpoints, const std::string& endblock) NOEXCEPT; - /// Chase event subscription (block connect/disconnect for notifyblocks). + /// Event handlers. bool handle_chase(const code& ec, node::chase event_, node::event_value value) NOEXCEPT; - /// Sender (btcd server push, json-rpc-v1 request shape with no id). + /// Address to output script. + code parse_output_script(const std::string& text, + system::chain::script& out) NOEXCEPT; + + /// Sender (server push, no id). void send_notification(const std::string& method, network::rpc::array_t&& params, size_t size_hint) NOEXCEPT; @@ -180,35 +160,32 @@ class BCS_API protocol_btcd using histories = database::histories; using cursor_t = database::height_link; - /// Watch of an address (by index key), matched via cursored history. struct address_watch final { cursor_t cursor{}; }; - /// Watch of an outpoint, matched via spender differencing. struct outpoint_watch final { database::history outpoint{}; database::histories spenders{}; }; - /// Filter (loadtxfilter). + /// Completion handlers (for long-running or other async queries). /// ----------------------------------------------------------------------- - code parse_filter_keys(const network::rpc::value_t& addresses, - std::vector& out) NOEXCEPT; - code parse_filter_points(const network::rpc::value_t& outpoints, - std::vector& out) NOEXCEPT; + code parse_filter_keys(system::hashes& out, + const network::rpc::value_t& addresses) NOEXCEPT; + code parse_filter_points(system::chain::points& out, + const network::rpc::value_t& outpoints) NOEXCEPT; - void do_load_tx_filter(bool reload, const std::vector& keys, - const std::vector& points) NOEXCEPT; + void do_load_tx_filter(bool reload, const system::hashes& keys, + const system::chain::points& points) NOEXCEPT; void complete_load_tx_filter(const code& ec) NOEXCEPT; - void do_rescan_blocks(const std::vector& hashes) NOEXCEPT; - void do_rescan_watches(const std::vector& hashes, - const std::vector& keys, - const std::vector& points) NOEXCEPT; + void do_rescan_blocks(const system::hashes& hashes) NOEXCEPT; + void do_rescan_watches(const system::hashes& hashes, + const system::hashes& keys, system::chain::points& points) NOEXCEPT; void complete_rescan_blocks(const code& ec, const network::rpc::array_t& discovered) NOEXCEPT; @@ -223,22 +200,16 @@ class BCS_API protocol_btcd void notify_disconnected(const std::string& hash, size_t height, uint32_t time, const std::string& header) NOEXCEPT; - /// Utilities (notification strand). + /// Utilities. /// ----------------------------------------------------------------------- - - /// Matched transactions by height, ordered by block position. using matched_txs = std::map; using matches = std::map; + using sizes = std::set; - /// Accumulate watched-address matches at the given heights. code match_addresses(matches& out, address_watch& sub, - const hash_digest& key, const std::set& heights) NOEXCEPT; - - /// Accumulate new watched-outpoint spender matches at the given heights. + const hash_digest& key, const sizes& heights) NOEXCEPT; void match_outpoints(matches& out, outpoint_watch& sub, - const point& prevout, const std::set& heights) NOEXCEPT; - - /// Serialize one block's matched transactions (base16, position order). + const point& prevout, const sizes& heights) NOEXCEPT; network::rpc::array_t serialize_matches(const matched_txs& txs) NOEXCEPT; private: @@ -257,12 +228,12 @@ class BCS_API protocol_btcd } // These are thread safe. - std::atomic_bool subscribed_blocks_{}; - std::atomic_bool stopping_{}; const options_t& options_; const bool turbo_; const uint8_t p2kh_; const uint8_t p2sh_; + std::atomic_bool stopping_{}; + std::atomic_bool subscribed_blocks_{}; // This is protected by strand. btcd_dispatcher btcd_dispatcher_{}; @@ -271,8 +242,8 @@ class BCS_API protocol_btcd network::asio::strand notification_strand_; // These are protected by notification strand. - std::map address_watches_{}; std::map outpoint_watches_{}; + std::map address_watches_{}; }; } // namespace server diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index b2ee2692..cdaa226c 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -312,7 +312,7 @@ bool protocol_bitcoind_rpc::handle_get_block_chain_info(const code& ec, // TODO: make utility method and move explanation there. // verificationprogress is approximated as confirmed/candidate height, the - // best available estimate of the chain tip during sync (1.0 once current). + // best available estimate of the chain top during sync (1.0 once current). const auto progress = is_zero(headers) ? 1.0 : std::min(1.0, to_floating(blocks) / to_floating(headers)); diff --git a/src/protocols/btcd/protocol_btcd.cpp b/src/protocols/btcd/protocol_btcd.cpp index 28815e29..f96978f0 100644 --- a/src/protocols/btcd/protocol_btcd.cpp +++ b/src/protocols/btcd/protocol_btcd.cpp @@ -50,26 +50,37 @@ void protocol_btcd::start() NOEXCEPT subscribe_chase(BIND(handle_chase, _1, _2, _3)); + // Administrative methods. SUBSCRIBE_BTCD(handle_authenticate, _1, _2, _3, _4); + SUBSCRIBE_BTCD(handle_help, _1, _2, _3); SUBSCRIBE_BTCD(handle_session, _1, _2); - SUBSCRIBE_BTCD(handle_get_current_net, _1, _2); + SUBSCRIBE_BTCD(handle_stop, _1, _2); + + // Getter methods. SUBSCRIBE_BTCD(handle_get_best_block, _1, _2); + SUBSCRIBE_BTCD(handle_get_current_net, _1, _2); SUBSCRIBE_BTCD(handle_get_difficulty, _1, _2); SUBSCRIBE_BTCD(handle_get_info, _1, _2); SUBSCRIBE_BTCD(handle_get_net_totals, _1, _2); SUBSCRIBE_BTCD(handle_get_network_hash_ps, _1, _2, _3, _4); + + // Tool methods. SUBSCRIBE_BTCD(handle_create_raw_transaction, _1, _2, _3, _4, _5); SUBSCRIBE_BTCD(handle_decode_raw_transaction, _1, _2, _3); SUBSCRIBE_BTCD(handle_decode_script, _1, _2, _3); SUBSCRIBE_BTCD(handle_validate_address, _1, _2, _3); - SUBSCRIBE_BTCD(handle_help, _1, _2, _3); + + // Subscription methods. SUBSCRIBE_BTCD(handle_notify_blocks, _1, _2); SUBSCRIBE_BTCD(handle_stop_notify_blocks, _1, _2); SUBSCRIBE_BTCD(handle_notify_new_transactions, _1, _2, _3); SUBSCRIBE_BTCD(handle_stop_notify_new_transactions, _1, _2); + + // Filter methods. SUBSCRIBE_BTCD(handle_load_tx_filter, _1, _2, _3, _4, _5); SUBSCRIBE_BTCD(handle_rescan_blocks, _1, _2, _3); - SUBSCRIBE_BTCD(handle_stop, _1, _2); + + // Deprecated methods. SUBSCRIBE_BTCD(handle_notify_received, _1, _2, _3); SUBSCRIBE_BTCD(handle_stop_notify_received, _1, _2, _3); SUBSCRIBE_BTCD(handle_notify_spent, _1, _2, _3); @@ -90,12 +101,8 @@ void protocol_btcd::stopping(const code& ec) NOEXCEPT protocol_bitcoind_rpc::stopping(ec); } -// Dispatch (the post and websocket transports of the btcd interface). +// Dispatch. // ---------------------------------------------------------------------------- -// ws authorization is established at most once per connection, by basic auth -// on the upgrade request or by exactly one authenticate call -- so a ws call -// is invalid if authorized and the method is authenticate, or not authorized -// and the method is not authenticate (as btcd). void protocol_btcd::handle_receive_post(const code& ec, const post::cptr& post) NOEXCEPT @@ -197,7 +204,7 @@ void protocol_btcd::dispatch_websocket( stop(code); } -// Handlers (authentication/admin). +// Handlers (administrative). // ---------------------------------------------------------------------------- bool protocol_btcd::handle_authenticate(const code& ec, @@ -224,6 +231,22 @@ bool protocol_btcd::handle_authenticate(const code& ec, return true; } +bool protocol_btcd::handle_help(const code& ec, btcd_interface::help, + const std::string&) NOEXCEPT +{ + if (stopped(ec)) + return false; + + // Lists method names only, no per-command argument usage text. + send_result(std::string{ + "authenticate help session stop getbestblock getcurrentnet " + "getdifficulty getinfo getnettotals getnetworkhashps " + "createrawtransaction decoderawtransaction decodescript " + "validateaddress notifyblocks stopnotifyblocks loadtxfilter " + "rescanblocks" }, 256); + return true; +} + bool protocol_btcd::handle_session(const code& ec, btcd_interface::session) NOEXCEPT { @@ -238,34 +261,24 @@ bool protocol_btcd::handle_session(const code& ec, return true; } -bool protocol_btcd::handle_get_current_net(const code& ec, - btcd_interface::get_current_net) NOEXCEPT +bool protocol_btcd::handle_stop(const code& ec, + btcd_interface::stop) NOEXCEPT { - if (stopped(ec)) - return false; + if (stopped(ec)) return false; - // The p2p handshake magic, the same value btcd returns (checked once by - // btcwallet/lnd at connect to confirm the expected network). - send_result(network_settings().identifier, 20); + // No secure remote-shutdown path exists; always rejected regardless of + // configuration or auth state. + send_error(error::not_implemented); return true; } -bool protocol_btcd::handle_get_best_block(const code& ec, - btcd_interface::get_best_block) NOEXCEPT -{ - if (stopped(ec)) - return false; +// Handlers (getters): see protocol_btcd_utility.cpp. +// ---------------------------------------------------------------------------- - // Required by btcwallet during wallet chain-sync bootstrap. - const auto& query = archive(); - object_t result{}; - result.emplace("hash", encode_hash(query.get_top_confirmed_hash())); - result.emplace("height", query.get_top_confirmed()); - send_result(std::move(result), 96); - return true; -} +// Handlers (tools): see protocol_btcd_utility.cpp. +// ---------------------------------------------------------------------------- -// Handlers (block subscription). +// Handlers (subscription, mempool not_implemented pending v5 mempool). // ---------------------------------------------------------------------------- bool protocol_btcd::handle_notify_blocks(const code& ec, @@ -290,9 +303,6 @@ bool protocol_btcd::handle_stop_notify_blocks(const code& ec, return true; } -// Handlers (mempool subscription, not_implemented pending v5 mempool). -// ---------------------------------------------------------------------------- - bool protocol_btcd::handle_notify_new_transactions(const code& ec, btcd_interface::notify_new_transactions, bool) NOEXCEPT { @@ -309,23 +319,9 @@ bool protocol_btcd::handle_stop_notify_new_transactions(const code& ec, return true; } -// Handlers (address/outpoint filtering): see protocol_btcd_filter.cpp. +// Handlers (filters): see protocol_btcd_filter.cpp. // ---------------------------------------------------------------------------- -// Handler (admin, permanently not_implemented). -// ---------------------------------------------------------------------------- - -bool protocol_btcd::handle_stop(const code& ec, - btcd_interface::stop) NOEXCEPT -{ - if (stopped(ec)) return false; - - // No secure remote-shutdown path exists; always rejected regardless of - // configuration or auth state. - send_error(error::not_implemented); - return true; -} - // Handlers (deprecated, permanently not_implemented). // ---------------------------------------------------------------------------- @@ -368,11 +364,8 @@ bool protocol_btcd::handle_rescan(const code& ec, { if (stopped(ec)) return false; - // Deprecated method, implemented only for the empty addrs/outpoints - // case (btcd skips scanning and reports immediate completion) -- the - // call btcwallet makes to bootstrap its initial sync starting point. A - // non-empty list would require a real historical scan; no observed - // caller needs it (loadtxfilter/rescanblocks cover actual watching). + // Implemented only for the empty addrs/outpoints case (as btcd), the + // call btcwallet makes to bootstrap its sync starting point. hash_digest begin_hash{}; if (!decode_hash(begin_hash, beginblock)) { @@ -398,7 +391,7 @@ bool protocol_btcd::handle_rescan(const code& ec, return true; } - // Nothing to watch: report the current chain tip as already-finished, + // Nothing to watch: report the current chain top as already-finished, // ignoring beginblock (as btcd). const auto top = query.get_top_confirmed(); const auto header = query.get_header(query.to_confirmed(top)); @@ -422,8 +415,6 @@ bool protocol_btcd::handle_rescan(const code& ec, // Chase events. // ---------------------------------------------------------------------------- -// Block events are matched against the watch-list on the notification strand -// (see protocol_btcd_filter.cpp) and notified back on the channel strand. bool protocol_btcd::handle_chase(const code&, node::chase event_, node::event_value value) NOEXCEPT @@ -478,9 +469,7 @@ void protocol_btcd::send_notification(const std::string& method, message.body() = std::move(notification); message.prepare_payload(); - // The notification strand poster (notify, see POST_NOTIFY) shadows - // network::protocol_http::notify. Qualify explicitly (as POST_BTCD). - // NOTIFY does not restart the (already active) ws reader. + // The notification strand poster shadows notify, qualify (as POST_BTCD). this->network::protocol_http::template notify(std::move(message), &CLASS::handle_complete, _1, error::success); } diff --git a/src/protocols/btcd/protocol_btcd_filter.cpp b/src/protocols/btcd/protocol_btcd_filter.cpp index b6eb66a7..e8cd86da 100644 --- a/src/protocols/btcd/protocol_btcd_filter.cpp +++ b/src/protocols/btcd/protocol_btcd_filter.cpp @@ -40,15 +40,11 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) -// Filter (loadtxfilter). +// Filter parsing (loadtxfilter). // ---------------------------------------------------------------------------- -// The watch-list is matched via the address index and spender lookups (the -// electrum subscription model), never by block scanning. Watches are keyed -// by index key (sha256 of the output script), matched by cursored history -// delta, so each connected block costs one bounded index walk per watch. -code protocol_btcd::parse_filter_keys(const value_t& addresses, - std::vector& out) NOEXCEPT +code protocol_btcd::parse_filter_keys(hashes& out, + const value_t& addresses) NOEXCEPT { BC_ASSERT(stranded()); @@ -71,8 +67,8 @@ code protocol_btcd::parse_filter_keys(const value_t& addresses, return error::success; } -code protocol_btcd::parse_filter_points(const value_t& outpoints, - std::vector& out) NOEXCEPT +code protocol_btcd::parse_filter_points(chain::points& out, + const value_t& outpoints) NOEXCEPT { BC_ASSERT(stranded()); @@ -92,8 +88,8 @@ code protocol_btcd::parse_filter_points(const value_t& outpoints, !std::holds_alternative(index_it->second.value())) return error::invalid_argument; - hash_digest hash{}; uint32_t index{}; + hash_digest hash{}; if (!decode_hash(hash, std::get(hash_it->second.value())) || !to_integer(index, std::get(index_it->second.value()))) return error::invalid_argument; @@ -104,7 +100,7 @@ code protocol_btcd::parse_filter_points(const value_t& outpoints, return error::success; } -// Handlers (address/outpoint filtering). +// Handlers (filters). // ---------------------------------------------------------------------------- bool protocol_btcd::handle_load_tx_filter(const code& ec, @@ -114,15 +110,15 @@ bool protocol_btcd::handle_load_tx_filter(const code& ec, if (stopped(ec)) return false; - std::vector keys{}; - if (const auto fault = parse_filter_keys(addresses, keys)) + hashes keys{}; + if (const auto fault = parse_filter_keys(keys, addresses)) { send_error(fault); return true; } - std::vector points{}; - if (const auto fault = parse_filter_points(outpoints, points)) + chain::points points{}; + if (const auto fault = parse_filter_points(points, outpoints)) { send_error(fault); return true; @@ -140,9 +136,8 @@ bool protocol_btcd::handle_load_tx_filter(const code& ec, return true; } -void protocol_btcd::do_load_tx_filter(bool reload, - const std::vector& keys, - const std::vector& points) NOEXCEPT +void protocol_btcd::do_load_tx_filter(bool reload, const hashes& keys, + const chain::points& points) NOEXCEPT { BC_ASSERT(notification_strand_.running_in_this_thread()); @@ -237,7 +232,7 @@ bool protocol_btcd::handle_rescan_blocks(const code& ec, return true; } - std::vector hashes{}; + hashes block_hashes{}; for (const auto& item: std::get(blockhashes.value())) { hash_digest hash{}; @@ -248,7 +243,7 @@ bool protocol_btcd::handle_rescan_blocks(const code& ec, return true; } - hashes.push_back(hash); + block_hashes.push_back(hash); } if (!archive().address_enabled()) @@ -258,43 +253,39 @@ bool protocol_btcd::handle_rescan_blocks(const code& ec, } monitor(true); - POST_NOTIFY(do_rescan_blocks, std::move(hashes)); + POST_NOTIFY(do_rescan_blocks, std::move(block_hashes)); return true; } -// Snapshot the watch-list on the notification strand, so the query itself -// runs parallel (as electrum's one-shot queries) without serializing against -// this channel's notifications. -void protocol_btcd::do_rescan_blocks( - const std::vector& hashes) NOEXCEPT +// Snapshot the watch-list, so the query runs parallel (not on the strand). +void protocol_btcd::do_rescan_blocks(const hashes& block_hashes) NOEXCEPT { BC_ASSERT(notification_strand_.running_in_this_thread()); - std::vector keys{}; + hashes keys{}; keys.reserve(address_watches_.size()); for (const auto& watch: address_watches_) keys.push_back(watch.first); - std::vector points{}; + chain::points points{}; points.reserve(outpoint_watches_.size()); for (const auto& watch: outpoint_watches_) points.push_back(watch.first); - PARALLEL(do_rescan_watches, hashes, std::move(keys), std::move(points)); + PARALLEL(do_rescan_watches, block_hashes, std::move(keys), + std::move(points)); } -void protocol_btcd::do_rescan_watches( - const std::vector& hashes, - const std::vector& keys, - const std::vector& points) NOEXCEPT +void protocol_btcd::do_rescan_watches(const hashes& block_hashes, + const hashes& keys, chain::points& points) NOEXCEPT { BC_ASSERT(!stranded()); // Resolve named blocks to heights (result retains request order). - std::set heights{}; + sizes heights{}; std::vector> named{}; const auto& query = archive(); - for (const auto& hash: hashes) + for (const auto& hash: block_hashes) { if (stopping_) return; @@ -391,7 +382,7 @@ void protocol_btcd::do_connected(node::header_t link_value) NOEXCEPT // Match the watch-list against the connected block (cursored delta). // Cursors advance here, so this stays on the notification strand. matches matched{}; - const std::set heights{ height }; + const sizes heights{ height }; for (auto& [key, sub]: address_watches_) { if (stopping_) @@ -444,9 +435,8 @@ void protocol_btcd::do_disconnected(node::header_t link_value) NOEXCEPT to_text(*header, chain::header::serialized_size())); } -// btcd 'blockconnected' [hash, height, time] and 'filteredblockconnected' -// [height, header, subscribedtxs]. filtered is sent unconditionally -// alongside (as btcd) -- an empty filter just yields an empty array. +// blockconnected [hash, height, time] and filteredblockconnected [height, +// header, subscribedtxs], the latter sent unconditionally (as btcd). void protocol_btcd::notify_connected(const std::string& hash, size_t height, uint32_t time, const std::string& header, const array_t& txs) NOEXCEPT @@ -471,8 +461,8 @@ void protocol_btcd::notify_connected(const std::string& hash, }, 256); } -// btcd 'blockdisconnected' [hash, height, time] and -// 'filteredblockdisconnected' [height, header]. +// blockdisconnected [hash, height, time] and filteredblockdisconnected +// [height, header]. void protocol_btcd::notify_disconnected(const std::string& hash, size_t height, uint32_t time, const std::string& header) NOEXCEPT { @@ -495,13 +485,12 @@ void protocol_btcd::notify_disconnected(const std::string& hash, }, 256); } -// Utilities (notification strand). +// Utilities. // ---------------------------------------------------------------------------- -// Called from the notification strand (live, cursors advance) and parallel -// (rescan, against a snapshot), so neither strand is asserted here. +// Called from notification strand (live) and parallel (rescan), so unasserted. code protocol_btcd::match_addresses(matches& out, address_watch& sub, - const hash_digest& key, const std::set& heights) NOEXCEPT + const hash_digest& key, const sizes& heights) NOEXCEPT { histories delta{}; const auto& query = archive(); @@ -519,7 +508,7 @@ code protocol_btcd::match_addresses(matches& out, address_watch& sub, } void protocol_btcd::match_outpoints(matches& out, outpoint_watch& sub, - const point& prevout, const std::set& heights) NOEXCEPT + const point& prevout, const sizes& heights) NOEXCEPT { outpoint_watch next{}; const auto& query = archive(); diff --git a/src/protocols/btcd/protocol_btcd_utility.cpp b/src/protocols/btcd/protocol_btcd_utility.cpp index 816fe5c9..d8d8b07b 100644 --- a/src/protocols/btcd/protocol_btcd_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_utility.cpp @@ -37,9 +37,36 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) -// Generic btcd-tooling compatibility. +// Handlers (getters). // ---------------------------------------------------------------------------- +bool protocol_btcd::handle_get_best_block(const code& ec, + btcd_interface::get_best_block) NOEXCEPT +{ + if (stopped(ec)) + return false; + + // Required by btcwallet during wallet chain-sync bootstrap. + const auto& query = archive(); + object_t result{}; + result.emplace("hash", encode_hash(query.get_top_confirmed_hash())); + result.emplace("height", query.get_top_confirmed()); + send_result(std::move(result), 96); + return true; +} + +bool protocol_btcd::handle_get_current_net(const code& ec, + btcd_interface::get_current_net) NOEXCEPT +{ + if (stopped(ec)) + return false; + + // The p2p handshake magic, the same value btcd returns (checked once by + // btcwallet/lnd at connect to confirm the expected network). + send_result(network_settings().identifier, 20); + return true; +} + bool protocol_btcd::handle_get_difficulty(const code& ec, btcd_interface::get_difficulty) NOEXCEPT { @@ -140,29 +167,9 @@ bool protocol_btcd::handle_get_network_hash_ps(const code& ec, return true; } -// Raw transaction/script utilities. +// Handlers (tools). // ---------------------------------------------------------------------------- -code protocol_btcd::parse_output_script(const std::string& text, - chain::script& out) NOEXCEPT -{ - const wallet::payment_address base58(text); - if (base58) - { - out = base58.output_script(p2kh_, p2sh_); - return error::success; - } - - const wallet::witness_address segwit(text); - if (segwit) - { - out = segwit.script(); - return error::success; - } - - return error::invalid_argument; -} - bool protocol_btcd::handle_create_raw_transaction(const code& ec, btcd_interface::create_raw_transaction, const array_t& inputs, const object_t& outputs, uint32_t locktime) NOEXCEPT @@ -221,9 +228,8 @@ bool protocol_btcd::handle_create_raw_transaction(const code& ec, return true; } - // Scale to satoshis and validate, the value is client-provided and - // the product may exceed the integer domain (not whole, as scaling - // a decimal fraction carries floating point representation error). + // Scale to satoshis and validate the domain (not whole, as scaling + // carries floating point representation error). uint64_t satoshi{}; const auto btc = std::get(pair.second.value()); if (!to_integer(satoshi, btc * chain::satoshi_per_bitcoin, false)) @@ -387,19 +393,27 @@ bool protocol_btcd::handle_validate_address(const code& ec, return true; } -bool protocol_btcd::handle_help(const code& ec, btcd_interface::help, - const std::string&) NOEXCEPT +// Utilities. +// ---------------------------------------------------------------------------- + +code protocol_btcd::parse_output_script(const std::string& text, + chain::script& out) NOEXCEPT { - if (stopped(ec)) - return false; + const wallet::payment_address base58(text); + if (base58) + { + out = base58.output_script(p2kh_, p2sh_); + return error::success; + } - // Lists method names only, no per-command argument usage text. - send_result(std::string{ - "authenticate session getcurrentnet getbestblock notifyblocks " - "stopnotifyblocks loadtxfilter rescanblocks getdifficulty getinfo " - "getnettotals getnetworkhashps createrawtransaction " - "decoderawtransaction decodescript validateaddress help stop" }, 256); - return true; + const wallet::witness_address segwit(text); + if (segwit) + { + out = segwit.script(); + return error::success; + } + + return error::invalid_argument; } BC_POP_WARNING() diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index e2982e7c..082879a1 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -52,7 +52,7 @@ std::string coinbase_txid(const chain::block& block) NOEXCEPT BOOST_FIXTURE_TEST_SUITE(btcd_tests, btcd_ten_block_setup_fixture) -// session management +// administrative // ---------------------------------------------------------------------------- BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__no_credential_configured__unauthorized) @@ -63,6 +63,21 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__no_credential_configured__unauthori BOOST_REQUIRE_EQUAL(result, unauthorized.value()); } +BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__http_post__unexpected_method) +{ + // authenticate is websocket-only (as btcd): not part of the post surface. + const auto response = http_rpc("authenticate", R"(["user","pass"])"); + REQUIRE_NO_THROW_TRUE(response.at("error").is_object()); + BOOST_REQUIRE_EQUAL(response.at("error").at("code").as_int64(), unexpected_method.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__help__default__method_list) +{ + const auto response = rpc("help"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + BOOST_REQUIRE_NE(as_text(response.at("result")).find("getcurrentnet"), std::string::npos); +} + BOOST_AUTO_TEST_CASE(btcd_rpc__session__default__returns_id) { const auto response = rpc("session"); @@ -70,13 +85,14 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__session__default__returns_id) BOOST_REQUIRE(response.at("result").as_object().contains("id")); } -BOOST_AUTO_TEST_CASE(btcd_rpc__getcurrentnet__mainnet__magic) +BOOST_AUTO_TEST_CASE(btcd_rpc__stop__default__not_implemented) { - // The fixture configures mainnet, magic 3652501241 (0xd9b4bef9). - const auto response = rpc("getcurrentnet"); - BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 3652501241); + BOOST_REQUIRE_EQUAL(rpc_error("stop"), not_implemented.value()); } +// getters +// ---------------------------------------------------------------------------- + BOOST_AUTO_TEST_CASE(btcd_rpc__getbestblock__ten_block_store__block9) { const auto response = rpc("getbestblock"); @@ -87,6 +103,29 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__getbestblock__ten_block_store__block9) BOOST_REQUIRE_EQUAL(result.at("height").as_int64(), 9); } +BOOST_AUTO_TEST_CASE(btcd_rpc__getcurrentnet__mainnet__magic) +{ + // The fixture configures mainnet, magic 3652501241 (0xd9b4bef9). + const auto response = rpc("getcurrentnet"); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 3652501241); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__getdifficulty__ten_block_store__number) +{ + const auto response = rpc("getdifficulty"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_double()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__ten_block_store__nine) +{ + const auto response = rpc("getinfo"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + BOOST_REQUIRE_EQUAL(result.at("blocks").as_int64(), 9); + BOOST_REQUIRE_EQUAL(result.at("testnet").as_bool(), false); +} + BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__http_post__dispatched) { // getinfo is btcd-only, so this exercises the btcd post transport (real @@ -95,15 +134,91 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__http_post__dispatched) REQUIRE_NO_THROW_TRUE(response.at("result").as_object().contains("blocks")); } -BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__http_post__unexpected_method) +BOOST_AUTO_TEST_CASE(btcd_rpc__getnettotals__untracked_counters__zero) { - // authenticate is websocket-only (as btcd): not part of the post surface. - const auto response = http_rpc("authenticate", R"(["user","pass"])"); - REQUIRE_NO_THROW_TRUE(response.at("error").is_object()); - BOOST_REQUIRE_EQUAL(response.at("error").at("code").as_int64(), unexpected_method.value()); + const auto response = rpc("getnettotals"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + BOOST_REQUIRE_EQUAL(result.at("totalbytesrecv").as_int64(), 0); + BOOST_REQUIRE_EQUAL(result.at("totalbytessent").as_int64(), 0); + BOOST_REQUIRE(result.at("timemillis").as_int64() > 0); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__getnetworkhashps__ten_block_store__number) +{ + const auto response = rpc("getnetworkhashps"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_double()); } -// block subscription +// tools +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(btcd_rpc__createrawtransaction__one_input_one_output__decodes) +{ + const auto request = R"([[{"txid":"%1%","vout":0}],{"%2%":0.01}])"; + const auto response = rpc("createrawtransaction", (boost_format(request) % coinbase_txid(test::block1) % found_address).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); + + data_chunk data{}; + BOOST_REQUIRE(decode_base16(data, as_text(response.at("result")))); + + const chain::transaction tx{ data, false }; + BOOST_REQUIRE(tx.is_valid()); + BOOST_REQUIRE_EQUAL(tx.inputs_ptr()->size(), 1u); + BOOST_REQUIRE_EQUAL(tx.outputs_ptr()->size(), 1u); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__block1_coinbase__expected_txid) +{ + const auto& coinbase = *test::block1.transactions_ptr()->front(); + const auto request = R"(["%1%"])"; + const auto response = rpc("decoderawtransaction", (boost_format(request) % encode_base16(coinbase.to_data(false))).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + BOOST_REQUIRE_EQUAL(as_text(response.at("result").at("txid")), encode_hash(coinbase.hash(false))); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__malformed_hex__invalid_argument) +{ + const auto result = rpc_error("decoderawtransaction", R"(["not-hex"])"); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__decodescript__pay_public_key__pubkey) +{ + // block1's coinbase output is a p2pk script (early mainnet convention). + const auto& script = test::block1.transactions_ptr()->front()->outputs_ptr()->front()->script(); + const auto request = R"(["%1%"])"; + const auto response = rpc("decodescript", (boost_format(request) % encode_base16(script.to_data(false))).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + BOOST_REQUIRE_EQUAL(as_text(result.at("type")), "pubkey"); + BOOST_REQUIRE(result.contains("asm")); + BOOST_REQUIRE(result.contains("p2sh")); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__validateaddress__valid_address__isvalid_true) +{ + const auto request = R"(["%1%"])"; + const auto response = rpc("validateaddress", (boost_format(request) % found_address).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + + const auto& result = response.at("result").as_object(); + BOOST_REQUIRE_EQUAL(result.at("isvalid").as_bool(), true); + BOOST_REQUIRE_EQUAL(as_text(result.at("address")), found_address); + BOOST_REQUIRE_EQUAL(result.at("iswitness").as_bool(), false); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__validateaddress__invalid_address__isvalid_false) +{ + const auto request = R"(["%1%"])"; + const auto response = rpc("validateaddress", (boost_format(request) % bogus_address).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); + BOOST_REQUIRE_EQUAL(response.at("result").at("isvalid").as_bool(), false); +} + +// subscription // ---------------------------------------------------------------------------- BOOST_AUTO_TEST_CASE(btcd_rpc__notifyblocks__default__null_result) @@ -122,17 +237,17 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyblocks__subscribed__null_result) REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); } -// bitcoind interface methods (bridged into the ws dispatcher) -// ---------------------------------------------------------------------------- +BOOST_AUTO_TEST_CASE(btcd_rpc__notifynewtransactions__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("notifynewtransactions", "[false]"), not_implemented.value()); +} -BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__ten_block_store__nine) +BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifynewtransactions__default__not_implemented) { - const auto response = rpc("getblockcount"); - BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); + BOOST_REQUIRE_EQUAL(rpc_error("stopnotifynewtransactions"), not_implemented.value()); } -// Address/outpoint filtering (loadtxfilter, filteredblockconnected/ -// disconnected, rescanblocks). +// filters // ---------------------------------------------------------------------------- // blocks 1-9 are p2pk coinbases with no inter-block spends, so they cannot // exercise address matching -- mock_block10 is added where a match is required. @@ -217,6 +332,29 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__filteredblockconnected__address_match__delivered) BOOST_REQUIRE_EQUAL(params[2].as_array().size(), 1u); } +// deprecated +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(btcd_rpc__notifyreceived__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("notifyreceived", "[[]]"), not_implemented.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyreceived__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("stopnotifyreceived", "[[]]"), not_implemented.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__notifyspent__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("notifyspent", "[[]]"), not_implemented.value()); +} + +BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyspent__default__not_implemented) +{ + BOOST_REQUIRE_EQUAL(rpc_error("stopnotifyspent", "[[]]"), not_implemented.value()); +} + BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__unknown_beginblock__not_found) { const auto result = rpc_error("rescan", (boost_format(R"(["%1%",[],[],""])") % encode_hash(null_hash)).str()); @@ -244,149 +382,13 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__with_addresses__not_implemented) BOOST_REQUIRE_EQUAL(result, not_implemented.value()); } -// Generic btcd-tooling compatibility -// ---------------------------------------------------------------------------- - -BOOST_AUTO_TEST_CASE(btcd_rpc__getdifficulty__ten_block_store__number) -{ - const auto response = rpc("getdifficulty"); - REQUIRE_NO_THROW_TRUE(response.at("result").is_double()); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__ten_block_store__nine) -{ - const auto response = rpc("getinfo"); - REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); - - const auto& result = response.at("result").as_object(); - BOOST_REQUIRE_EQUAL(result.at("blocks").as_int64(), 9); - BOOST_REQUIRE_EQUAL(result.at("testnet").as_bool(), false); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__getnettotals__untracked_counters__zero) -{ - const auto response = rpc("getnettotals"); - REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); - - const auto& result = response.at("result").as_object(); - BOOST_REQUIRE_EQUAL(result.at("totalbytesrecv").as_int64(), 0); - BOOST_REQUIRE_EQUAL(result.at("totalbytessent").as_int64(), 0); - BOOST_REQUIRE(result.at("timemillis").as_int64() > 0); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__getnetworkhashps__ten_block_store__number) -{ - const auto response = rpc("getnetworkhashps"); - REQUIRE_NO_THROW_TRUE(response.at("result").is_double()); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__createrawtransaction__one_input_one_output__decodes) -{ - const auto request = R"([[{"txid":"%1%","vout":0}],{"%2%":0.01}])"; - const auto response = rpc("createrawtransaction", (boost_format(request) % coinbase_txid(test::block1) % found_address).str()); - REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); - - data_chunk data{}; - BOOST_REQUIRE(decode_base16(data, as_text(response.at("result")))); - - const chain::transaction tx{ data, false }; - BOOST_REQUIRE(tx.is_valid()); - BOOST_REQUIRE_EQUAL(tx.inputs_ptr()->size(), 1u); - BOOST_REQUIRE_EQUAL(tx.outputs_ptr()->size(), 1u); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__block1_coinbase__expected_txid) -{ - const auto& coinbase = *test::block1.transactions_ptr()->front(); - const auto request = R"(["%1%"])"; - const auto response = rpc("decoderawtransaction", (boost_format(request) % encode_base16(coinbase.to_data(false))).str()); - REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); - BOOST_REQUIRE_EQUAL(as_text(response.at("result").at("txid")), encode_hash(coinbase.hash(false))); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__malformed_hex__invalid_argument) -{ - const auto result = rpc_error("decoderawtransaction", R"(["not-hex"])"); - BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__decodescript__pay_public_key__pubkey) -{ - // block1's coinbase output is a p2pk script (early mainnet convention). - const auto& script = test::block1.transactions_ptr()->front()->outputs_ptr()->front()->script(); - const auto request = R"(["%1%"])"; - const auto response = rpc("decodescript", (boost_format(request) % encode_base16(script.to_data(false))).str()); - REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); - - const auto& result = response.at("result").as_object(); - BOOST_REQUIRE_EQUAL(as_text(result.at("type")), "pubkey"); - BOOST_REQUIRE(result.contains("asm")); - BOOST_REQUIRE(result.contains("p2sh")); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__validateaddress__valid_address__isvalid_true) -{ - const auto request = R"(["%1%"])"; - const auto response = rpc("validateaddress", (boost_format(request) % found_address).str()); - REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); - - const auto& result = response.at("result").as_object(); - BOOST_REQUIRE_EQUAL(result.at("isvalid").as_bool(), true); - BOOST_REQUIRE_EQUAL(as_text(result.at("address")), found_address); - BOOST_REQUIRE_EQUAL(result.at("iswitness").as_bool(), false); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__validateaddress__invalid_address__isvalid_false) -{ - const auto request = R"(["%1%"])"; - const auto response = rpc("validateaddress", (boost_format(request) % bogus_address).str()); - REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); - BOOST_REQUIRE_EQUAL(response.at("result").at("isvalid").as_bool(), false); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__help__default__method_list) -{ - const auto response = rpc("help"); - REQUIRE_NO_THROW_TRUE(response.at("result").is_string()); - BOOST_REQUIRE_NE(as_text(response.at("result")).find("getcurrentnet"), std::string::npos); -} - -// not implemented stubs +// bitcoind interface methods (bridged into the ws dispatcher) // ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(btcd_rpc__stop__default__not_implemented) -{ - BOOST_REQUIRE_EQUAL(rpc_error("stop"), not_implemented.value()); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__notifynewtransactions__default__not_implemented) -{ - BOOST_REQUIRE_EQUAL(rpc_error("notifynewtransactions", "[false]"), not_implemented.value()); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifynewtransactions__default__not_implemented) -{ - BOOST_REQUIRE_EQUAL(rpc_error("stopnotifynewtransactions"), not_implemented.value()); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__notifyreceived__default__not_implemented) -{ - BOOST_REQUIRE_EQUAL(rpc_error("notifyreceived", "[[]]"), not_implemented.value()); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyreceived__default__not_implemented) -{ - BOOST_REQUIRE_EQUAL(rpc_error("stopnotifyreceived", "[[]]"), not_implemented.value()); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__notifyspent__default__not_implemented) -{ - BOOST_REQUIRE_EQUAL(rpc_error("notifyspent", "[[]]"), not_implemented.value()); -} - -BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyspent__default__not_implemented) +BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__ten_block_store__nine) { - BOOST_REQUIRE_EQUAL(rpc_error("stopnotifyspent", "[[]]"), not_implemented.value()); + const auto response = rpc("getblockcount"); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); } // response envelope From 236b0bfc22887c6b920b25907c6aa5a35ef5d155 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 17:36:14 -0400 Subject: [PATCH 21/29] Add [wallet] settings for address encoding. --- .../server/protocols/protocol_btcd.hpp | 10 ++-- .../server/protocols/protocol_electrum.hpp | 8 +-- include/bitcoin/server/settings.hpp | 16 ++++++ src/parser.cpp | 17 ++++++ src/protocols/btcd/protocol_btcd_utility.cpp | 5 +- src/settings.cpp | 53 +++++++++++++++++-- 6 files changed, 91 insertions(+), 18 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_btcd.hpp b/include/bitcoin/server/protocols/protocol_btcd.hpp index fc4ad9aa..b0bf47b3 100644 --- a/include/bitcoin/server/protocols/protocol_btcd.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd.hpp @@ -48,12 +48,9 @@ class BCS_API protocol_btcd network::tracker(session->log), options_(options), turbo_(session->database_settings().turbo), - p2kh_(session->system_settings().forks.difficult ? - system::wallet::payment_address::mainnet_p2kh : - system::wallet::payment_address::testnet_p2kh), - p2sh_(session->system_settings().forks.difficult ? - system::wallet::payment_address::mainnet_p2sh : - system::wallet::payment_address::testnet_p2sh), + p2kh_(session->server_settings().wallet.p2kh_prefix), + p2sh_(session->server_settings().wallet.p2sh_prefix), + witness_(session->server_settings().wallet.witness_prefix), notification_strand_(channel->service().get_executor()) { } @@ -232,6 +229,7 @@ class BCS_API protocol_btcd const bool turbo_; const uint8_t p2kh_; const uint8_t p2sh_; + const std::string witness_; std::atomic_bool stopping_{}; std::atomic_bool subscribed_blocks_{}; diff --git a/include/bitcoin/server/protocols/protocol_electrum.hpp b/include/bitcoin/server/protocols/protocol_electrum.hpp index 990bf212..23e7d683 100644 --- a/include/bitcoin/server/protocols/protocol_electrum.hpp +++ b/include/bitcoin/server/protocols/protocol_electrum.hpp @@ -44,12 +44,8 @@ class BCS_API protocol_electrum : protocol_rpc(session, channel, options), options_(options), turbo_(session->database_settings().turbo), - p2kh_(session->system_settings().forks.difficult ? - system::wallet::payment_address::mainnet_p2kh : - system::wallet::payment_address::testnet_p2kh), - p2sh_(session->system_settings().forks.difficult ? - system::wallet::payment_address::mainnet_p2sh : - system::wallet::payment_address::testnet_p2sh), + p2kh_(session->server_settings().wallet.p2kh_prefix), + p2sh_(session->server_settings().wallet.p2sh_prefix), channel_(std::dynamic_pointer_cast(channel)), notification_strand_(channel_->service().get_executor()), network::tracker(session->log) diff --git a/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp index b7a09c59..3d6e7c1e 100644 --- a/include/bitcoin/server/settings.hpp +++ b/include/bitcoin/server/settings.hpp @@ -152,6 +152,19 @@ class BCS_API settings virtual bool enabled() const NOEXCEPT; }; + /// Address encoding, implied by coin and network (not by forks). + struct wallet_settings + { + DEFAULT_COPY_MOVE_DESTRUCT(wallet_settings); + + wallet_settings() NOEXCEPT; + wallet_settings(system::chain::selection context) NOEXCEPT; + + system::config::byte p2kh_prefix; + system::config::byte p2sh_prefix; + std::string witness_prefix; + }; + struct bitcoind_server : public network::settings::http_server { @@ -185,6 +198,9 @@ class BCS_API settings settings(system::chain::selection context, const embedded_pages& native, const embedded_pages& admin) NOEXCEPT; + /// address encoding (coin/network identity) + wallet_settings wallet; + /// admin web interface, isolated (http/s, stateless html) server::settings::html_server admin; diff --git a/src/parser.cpp b/src/parser.cpp index 730e6c7d..2e8fc631 100644 --- a/src/parser.cpp +++ b/src/parser.cpp @@ -860,6 +860,23 @@ options_metadata parser::load_settings() THROWS "The socks5 proxy endpoint (port required)." ) + /* [wallet] */ + ( + "wallet.p2kh_prefix", + value(&configured.server.wallet.p2kh_prefix), + "The pay-to-public-key-hash address prefix, defaults to '0' (use '111' for testnet)." + ) + ( + "wallet.p2sh_prefix", + value(&configured.server.wallet.p2sh_prefix), + "The pay-to-script-hash address prefix, defaults to '5' (use '196' for testnet)." + ) + ( + "wallet.witness_prefix", + value(&configured.server.wallet.witness_prefix), + "The witness address prefix, defaults to 'bc' (use 'tb' for testnet)." + ) + /* [admin] */ ( "admin.bind", diff --git a/src/protocols/btcd/protocol_btcd_utility.cpp b/src/protocols/btcd/protocol_btcd_utility.cpp index d8d8b07b..e0530353 100644 --- a/src/protocols/btcd/protocol_btcd_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_utility.cpp @@ -374,7 +374,7 @@ bool protocol_btcd::handle_validate_address(const code& ec, } const wallet::witness_address segwit(address); - if (segwit) + if (segwit && segwit.prefix() == witness_) { send_result(object_t { @@ -406,8 +406,9 @@ code protocol_btcd::parse_output_script(const std::string& text, return error::success; } + // The parse accepts any prefix, so the configured one is a check. const wallet::witness_address segwit(text); - if (segwit) + if (segwit && segwit.prefix() == witness_) { out = segwit.script(); return error::success; diff --git a/src/settings.cpp b/src/settings.cpp index 39323a22..69b7d0d4 100644 --- a/src/settings.cpp +++ b/src/settings.cpp @@ -81,12 +81,57 @@ std::filesystem::path settings::events_file() const NOEXCEPT namespace server { +using namespace system::wallet; + +// witness_address defines mainnet/testnet prefixes only (bip173). +constexpr auto regtest_witness_prefix = "bcrt"; + // settings::settings -settings::settings(system::chain::selection, const embedded_pages& native, - const embedded_pages& admin) NOEXCEPT - : native("native", native), - admin("admin", admin) +settings::settings(system::chain::selection context, + const embedded_pages& native, const embedded_pages& admin) NOEXCEPT + : wallet(context), + native("native", native), + admin("admin", admin) +{ +} + +// settings::wallet_settings +settings::wallet_settings::wallet_settings() NOEXCEPT + : wallet_settings(system::chain::selection::mainnet) +{ +} + +settings::wallet_settings::wallet_settings( + system::chain::selection context) NOEXCEPT + : p2kh_prefix(payment_address::mainnet_p2kh), + p2sh_prefix(payment_address::mainnet_p2sh), + witness_prefix(witness_address::mainnet) { + // Testnet and regtest share base58 versions, regtest differs in bech32. + switch (context) + { + case system::chain::selection::testnet3: + case system::chain::selection::testnet4: + { + p2kh_prefix = payment_address::testnet_p2kh; + p2sh_prefix = payment_address::testnet_p2sh; + witness_prefix = witness_address::testnet; + break; + } + case system::chain::selection::regtest: + { + p2kh_prefix = payment_address::testnet_p2kh; + p2sh_prefix = payment_address::testnet_p2sh; + witness_prefix = regtest_witness_prefix; + break; + } + case system::chain::selection::mainnet: + case system::chain::selection::none: + default: + { + break; + } + } } // settings::embedded_pages From 349797b06682a93538681252031a8c5a97cbad3f Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 17:36:15 -0400 Subject: [PATCH 22/29] btcd: move filter parsing to parsers. --- builds/gnu/Makefile.am | 2 + .../libbitcoin-server.vcxproj | 2 + .../libbitcoin-server.vcxproj.filters | 6 + .../libbitcoin-server.vcxproj | 2 + .../libbitcoin-server.vcxproj.filters | 6 + include/bitcoin/server.hpp | 1 + .../bitcoin/server/parsers/btcd_filter.hpp | 46 ++++++++ include/bitcoin/server/parsers/parsers.hpp | 1 + .../server/protocols/protocol_btcd.hpp | 9 -- src/parsers/btcd_filter.cpp | 109 ++++++++++++++++++ src/protocols/btcd/protocol_btcd_filter.cpp | 66 +---------- src/protocols/btcd/protocol_btcd_utility.cpp | 28 +---- 12 files changed, 182 insertions(+), 96 deletions(-) create mode 100644 include/bitcoin/server/parsers/btcd_filter.hpp create mode 100644 src/parsers/btcd_filter.cpp diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index 31cfbf0e..3a98f875 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -55,6 +55,7 @@ src_libbitcoin_server_la_SOURCES = \ ${srcdir}/../../src/parsers/admin_query.cpp \ ${srcdir}/../../src/parsers/admin_target.cpp \ ${srcdir}/../../src/parsers/bitcoind_target.cpp \ + ${srcdir}/../../src/parsers/btcd_filter.cpp \ ${srcdir}/../../src/parsers/electrum_version.cpp \ ${srcdir}/../../src/parsers/native_query.cpp \ ${srcdir}/../../src/parsers/native_target.cpp \ @@ -145,6 +146,7 @@ include_bitcoin_server_parsers_HEADERS = \ ${srcdir}/../../include/bitcoin/server/parsers/admin_query.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/admin_target.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/bitcoind_target.hpp \ + ${srcdir}/../../include/bitcoin/server/parsers/btcd_filter.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/electrum_version.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/native_query.hpp \ ${srcdir}/../../include/bitcoin/server/parsers/native_target.hpp \ diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj index fa8aaf94..44738f13 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj @@ -127,6 +127,7 @@ + @@ -188,6 +189,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters index cf6833e9..d98c5445 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -87,6 +87,9 @@ src\parsers + + src\parsers + src\parsers @@ -266,6 +269,9 @@ include\bitcoin\server\parsers + + include\bitcoin\server\parsers + include\bitcoin\server\parsers diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj index 2e1568b9..318a680f 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj @@ -127,6 +127,7 @@ + @@ -188,6 +189,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters index cf6833e9..d98c5445 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters @@ -87,6 +87,9 @@ src\parsers + + src\parsers + src\parsers @@ -266,6 +269,9 @@ include\bitcoin\server\parsers + + include\bitcoin\server\parsers + include\bitcoin\server\parsers diff --git a/include/bitcoin/server.hpp b/include/bitcoin/server.hpp index 8b575b50..44e38fc4 100644 --- a/include/bitcoin/server.hpp +++ b/include/bitcoin/server.hpp @@ -41,6 +41,7 @@ #include #include #include +#include #include #include #include diff --git a/include/bitcoin/server/parsers/btcd_filter.hpp b/include/bitcoin/server/parsers/btcd_filter.hpp new file mode 100644 index 00000000..35cb115f --- /dev/null +++ b/include/bitcoin/server/parsers/btcd_filter.hpp @@ -0,0 +1,46 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#ifndef LIBBITCOIN_SERVER_PARSERS_BTCD_FILTER_HPP +#define LIBBITCOIN_SERVER_PARSERS_BTCD_FILTER_HPP + +#include + +namespace libbitcoin { +namespace server { +namespace btcd { + +/// Parse an address to its output script, prefixes are validated. +BCS_API code output_script(system::chain::script& out, + const std::string& text, uint8_t p2kh, uint8_t p2sh, + const std::string& witness) NOEXCEPT; + +/// Parse loadtxfilter addresses to their output script hashes. +BCS_API code filter_keys(system::hashes& out, + const network::rpc::value_t& addresses, uint8_t p2kh, uint8_t p2sh, + const std::string& witness) NOEXCEPT; + +/// Parse loadtxfilter outpoints to their points. +BCS_API code filter_points(system::chain::points& out, + const network::rpc::value_t& outpoints) NOEXCEPT; + +} // namespace btcd +} // namespace server +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/server/parsers/parsers.hpp b/include/bitcoin/server/parsers/parsers.hpp index a76d840d..40ee10ae 100644 --- a/include/bitcoin/server/parsers/parsers.hpp +++ b/include/bitcoin/server/parsers/parsers.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include diff --git a/include/bitcoin/server/protocols/protocol_btcd.hpp b/include/bitcoin/server/protocols/protocol_btcd.hpp index b0bf47b3..2273ab10 100644 --- a/include/bitcoin/server/protocols/protocol_btcd.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd.hpp @@ -142,10 +142,6 @@ class BCS_API protocol_btcd bool handle_chase(const code& ec, node::chase event_, node::event_value value) NOEXCEPT; - /// Address to output script. - code parse_output_script(const std::string& text, - system::chain::script& out) NOEXCEPT; - /// Sender (server push, no id). void send_notification(const std::string& method, network::rpc::array_t&& params, size_t size_hint) NOEXCEPT; @@ -171,11 +167,6 @@ class BCS_API protocol_btcd /// Completion handlers (for long-running or other async queries). /// ----------------------------------------------------------------------- - code parse_filter_keys(system::hashes& out, - const network::rpc::value_t& addresses) NOEXCEPT; - code parse_filter_points(system::chain::points& out, - const network::rpc::value_t& outpoints) NOEXCEPT; - void do_load_tx_filter(bool reload, const system::hashes& keys, const system::chain::points& points) NOEXCEPT; void complete_load_tx_filter(const code& ec) NOEXCEPT; diff --git a/src/parsers/btcd_filter.cpp b/src/parsers/btcd_filter.cpp new file mode 100644 index 00000000..4a9d5cee --- /dev/null +++ b/src/parsers/btcd_filter.cpp @@ -0,0 +1,109 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include + +#include + +namespace libbitcoin { +namespace server { +namespace btcd { + +using namespace system; +using namespace network::rpc; + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) + +code output_script(chain::script& out, const std::string& text, uint8_t p2kh, + uint8_t p2sh, const std::string& witness) NOEXCEPT +{ + const wallet::payment_address base58(text); + if (base58) + { + out = base58.output_script(p2kh, p2sh); + return error::success; + } + + // The parse accepts any prefix, so the configured one is a check. + const wallet::witness_address segwit(text); + if (segwit && segwit.prefix() == witness) + { + out = segwit.script(); + return error::success; + } + + return error::invalid_argument; +} + +code filter_keys(hashes& out, const value_t& addresses, uint8_t p2kh, + uint8_t p2sh, const std::string& witness) NOEXCEPT +{ + if (!std::holds_alternative(addresses.value())) + return error::invalid_argument; + + for (const auto& item: std::get(addresses.value())) + { + if (!std::holds_alternative(item.value())) + return error::invalid_argument; + + chain::script script{}; + if (const auto ec = output_script(script, + std::get(item.value()), p2kh, p2sh, witness)) + return ec; + + out.push_back(script.hash()); + } + + return error::success; +} + +code filter_points(chain::points& out, const value_t& outpoints) NOEXCEPT +{ + if (!std::holds_alternative(outpoints.value())) + return error::invalid_argument; + + for (const auto& item: std::get(outpoints.value())) + { + if (!std::holds_alternative(item.value())) + return error::invalid_argument; + + const auto& fields = std::get(item.value()); + const auto hash_it = fields.find("hash"); + const auto index_it = fields.find("index"); + if (hash_it == fields.end() || index_it == fields.end() || + !std::holds_alternative(hash_it->second.value()) || + !std::holds_alternative(index_it->second.value())) + return error::invalid_argument; + + uint32_t index{}; + hash_digest hash{}; + if (!decode_hash(hash, std::get(hash_it->second.value())) || + !to_integer(index, std::get(index_it->second.value()))) + return error::invalid_argument; + + out.emplace_back(hash, index); + } + + return error::success; +} + +BC_POP_WARNING() + +} // namespace btcd +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/btcd/protocol_btcd_filter.cpp b/src/protocols/btcd/protocol_btcd_filter.cpp index e8cd86da..964240a1 100644 --- a/src/protocols/btcd/protocol_btcd_filter.cpp +++ b/src/protocols/btcd/protocol_btcd_filter.cpp @@ -21,6 +21,7 @@ #include #include #include +#include namespace libbitcoin { namespace server { @@ -40,66 +41,6 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) -// Filter parsing (loadtxfilter). -// ---------------------------------------------------------------------------- - -code protocol_btcd::parse_filter_keys(hashes& out, - const value_t& addresses) NOEXCEPT -{ - BC_ASSERT(stranded()); - - if (!std::holds_alternative(addresses.value())) - return error::invalid_argument; - - for (const auto& item: std::get(addresses.value())) - { - if (!std::holds_alternative(item.value())) - return error::invalid_argument; - - chain::script script{}; - if (const auto ec = parse_output_script( - std::get(item.value()), script)) - return ec; - - out.push_back(script.hash()); - } - - return error::success; -} - -code protocol_btcd::parse_filter_points(chain::points& out, - const value_t& outpoints) NOEXCEPT -{ - BC_ASSERT(stranded()); - - if (!std::holds_alternative(outpoints.value())) - return error::invalid_argument; - - for (const auto& item: std::get(outpoints.value())) - { - if (!std::holds_alternative(item.value())) - return error::invalid_argument; - - const auto& fields = std::get(item.value()); - const auto hash_it = fields.find("hash"); - const auto index_it = fields.find("index"); - if (hash_it == fields.end() || index_it == fields.end() || - !std::holds_alternative(hash_it->second.value()) || - !std::holds_alternative(index_it->second.value())) - return error::invalid_argument; - - uint32_t index{}; - hash_digest hash{}; - if (!decode_hash(hash, std::get(hash_it->second.value())) || - !to_integer(index, std::get(index_it->second.value()))) - return error::invalid_argument; - - out.emplace_back(hash, index); - } - - return error::success; -} - // Handlers (filters). // ---------------------------------------------------------------------------- @@ -111,14 +52,15 @@ bool protocol_btcd::handle_load_tx_filter(const code& ec, return false; hashes keys{}; - if (const auto fault = parse_filter_keys(keys, addresses)) + if (const auto fault = btcd::filter_keys(keys, addresses, p2kh_, p2sh_, + witness_)) { send_error(fault); return true; } chain::points points{}; - if (const auto fault = parse_filter_points(points, outpoints)) + if (const auto fault = btcd::filter_points(points, outpoints)) { send_error(fault); return true; diff --git a/src/protocols/btcd/protocol_btcd_utility.cpp b/src/protocols/btcd/protocol_btcd_utility.cpp index e0530353..91796855 100644 --- a/src/protocols/btcd/protocol_btcd_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_utility.cpp @@ -22,6 +22,7 @@ #include #include #include +#include namespace libbitcoin { namespace server { @@ -222,7 +223,8 @@ bool protocol_btcd::handle_create_raw_transaction(const code& ec, } chain::script script{}; - if (const auto fault = parse_output_script(pair.first, script)) + if (const auto fault = btcd::output_script(script, pair.first, p2kh_, + p2sh_, witness_)) { send_error(fault); return true; @@ -393,30 +395,6 @@ bool protocol_btcd::handle_validate_address(const code& ec, return true; } -// Utilities. -// ---------------------------------------------------------------------------- - -code protocol_btcd::parse_output_script(const std::string& text, - chain::script& out) NOEXCEPT -{ - const wallet::payment_address base58(text); - if (base58) - { - out = base58.output_script(p2kh_, p2sh_); - return error::success; - } - - // The parse accepts any prefix, so the configured one is a check. - const wallet::witness_address segwit(text); - if (segwit && segwit.prefix() == witness_) - { - out = segwit.script(); - return error::success; - } - - return error::invalid_argument; -} - BC_POP_WARNING() BC_POP_WARNING() BC_POP_WARNING() From 2b4c7cab494e66c441c6890c75495df6c6429360 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 18:12:09 -0400 Subject: [PATCH 23/29] btcd: style, comments. --- src/parsers/btcd_filter.cpp | 12 +++++------- src/protocols/btcd/protocol_btcd_utility.cpp | 12 ++++++------ test/protocols/bitcoind/bitcoind_rpc.cpp | 8 +++----- 3 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/parsers/btcd_filter.cpp b/src/parsers/btcd_filter.cpp index 4a9d5cee..b3d57098 100644 --- a/src/parsers/btcd_filter.cpp +++ b/src/parsers/btcd_filter.cpp @@ -32,18 +32,16 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) code output_script(chain::script& out, const std::string& text, uint8_t p2kh, uint8_t p2sh, const std::string& witness) NOEXCEPT { - const wallet::payment_address base58(text); - if (base58) + if (const wallet::payment_address payment{ text }; payment) { - out = base58.output_script(p2kh, p2sh); + out = payment.output_script(p2kh, p2sh); return error::success; } - // The parse accepts any prefix, so the configured one is a check. - const wallet::witness_address segwit(text); - if (segwit && segwit.prefix() == witness) + if (const wallet::witness_address payment{ text }; + payment && payment.prefix() == witness) { - out = segwit.script(); + out = payment.script(); return error::success; } diff --git a/src/protocols/btcd/protocol_btcd_utility.cpp b/src/protocols/btcd/protocol_btcd_utility.cpp index 91796855..004e9890 100644 --- a/src/protocols/btcd/protocol_btcd_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_utility.cpp @@ -375,18 +375,18 @@ bool protocol_btcd::handle_validate_address(const code& ec, return true; } - const wallet::witness_address segwit(address); - if (segwit && segwit.prefix() == witness_) + const wallet::witness_address witness(address); + if (witness && witness.prefix() == witness_) { send_result(object_t { { "isvalid", true }, - { "address", segwit.encoded() }, - { "isscript", segwit.identifier() == + { "address", witness.encoded() }, + { "isscript", witness.identifier() == wallet::witness_address::program_type::version0_p2sh }, { "iswitness", true }, - { "witness_version", segwit.version() }, - { "witness_program", encode_base16(segwit.program()) } + { "witness_version", witness.version() }, + { "witness_program", encode_base16(witness.program()) } }, 128); return true; } diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index c621419b..c1b5f3cc 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -199,7 +199,7 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__getnetworkinfo__fields) BOOST_REQUIRE(result.at("networks").is_array()); } -// not implemented (structured not_implemented error) +// not implemented // ---------------------------------------------------------------------------- BOOST_AUTO_TEST_CASE(bitcoind_rpc__not_implemented__error) @@ -306,10 +306,8 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__batch__empty__dropped) REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } -// websocket transport +// websocket // ---------------------------------------------------------------------------- -// The same interface dispatched over ws (dispatch_websocket): each frame is -// a json-rpc text message. BOOST_AUTO_TEST_CASE(bitcoind_rpc__getblockcount__websocket__nine) { @@ -368,7 +366,7 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__response__websocket__id_matches_request) BOOST_AUTO_TEST_SUITE_END() -// segwit (witness store: genesis + two blocks carrying witness transactions) +// witness // ---------------------------------------------------------------------------- BOOST_FIXTURE_TEST_SUITE(bitcoind_witness_tests, bitcoind_witness_setup_fixture) From 412da53c2a1c6de9f15346f1e44b9b00df91213b Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 20:17:17 -0400 Subject: [PATCH 24/29] btcd: style, comments. --- .../server/protocols/protocol_btcd.hpp | 4 +- src/parsers/btcd_filter.cpp | 22 ++++--- src/protocols/btcd/protocol_btcd.cpp | 58 ++++++++----------- 3 files changed, 39 insertions(+), 45 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_btcd.hpp b/include/bitcoin/server/protocols/protocol_btcd.hpp index 2273ab10..e5dbac9e 100644 --- a/include/bitcoin/server/protocols/protocol_btcd.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd.hpp @@ -101,7 +101,7 @@ class BCS_API protocol_btcd btcd_interface::validate_address, const std::string& address) NOEXCEPT; - /// Handlers (subscription, mempool pending v5). + /// Handlers (subscription). bool handle_notify_blocks(const code& ec, btcd_interface::notify_blocks) NOEXCEPT; bool handle_stop_notify_blocks(const code& ec, @@ -111,7 +111,7 @@ class BCS_API protocol_btcd bool handle_stop_notify_new_transactions(const code& ec, btcd_interface::stop_notify_new_transactions) NOEXCEPT; - /// Handlers (filters, address watch requires the address index). + /// Handlers (filters). bool handle_load_tx_filter(const code& ec, btcd_interface::load_tx_filter, bool reload, const network::rpc::value_t& addresses, diff --git a/src/parsers/btcd_filter.cpp b/src/parsers/btcd_filter.cpp index b3d57098..0c6040ce 100644 --- a/src/parsers/btcd_filter.cpp +++ b/src/parsers/btcd_filter.cpp @@ -18,6 +18,7 @@ */ #include +#include #include namespace libbitcoin { @@ -25,20 +26,23 @@ namespace server { namespace btcd { using namespace system; +using namespace system::chain; using namespace network::rpc; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) -code output_script(chain::script& out, const std::string& text, uint8_t p2kh, +code output_script(script& out, const std::string& text, uint8_t p2kh, uint8_t p2sh, const std::string& witness) NOEXCEPT { - if (const wallet::payment_address payment{ text }; payment) + using namespace wallet; + + if (const payment_address payment{ text }; payment) { out = payment.output_script(p2kh, p2sh); return error::success; } - if (const wallet::witness_address payment{ text }; + if (const witness_address payment{ text }; payment && payment.prefix() == witness) { out = payment.script(); @@ -54,14 +58,14 @@ code filter_keys(hashes& out, const value_t& addresses, uint8_t p2kh, if (!std::holds_alternative(addresses.value())) return error::invalid_argument; + script script{}; for (const auto& item: std::get(addresses.value())) { if (!std::holds_alternative(item.value())) return error::invalid_argument; - chain::script script{}; - if (const auto ec = output_script(script, - std::get(item.value()), p2kh, p2sh, witness)) + const auto value = std::get(item.value()); + if (const auto ec = output_script(script, value, p2kh, p2sh, witness)) return ec; out.push_back(script.hash()); @@ -70,11 +74,13 @@ code filter_keys(hashes& out, const value_t& addresses, uint8_t p2kh, return error::success; } -code filter_points(chain::points& out, const value_t& outpoints) NOEXCEPT +code filter_points(points& out, const value_t& outpoints) NOEXCEPT { if (!std::holds_alternative(outpoints.value())) return error::invalid_argument; + uint32_t index{}; + hash_digest hash{}; for (const auto& item: std::get(outpoints.value())) { if (!std::holds_alternative(item.value())) @@ -88,8 +94,6 @@ code filter_points(chain::points& out, const value_t& outpoints) NOEXCEPT !std::holds_alternative(index_it->second.value())) return error::invalid_argument; - uint32_t index{}; - hash_digest hash{}; if (!decode_hash(hash, std::get(hash_it->second.value())) || !to_integer(index, std::get(index_it->second.value()))) return error::invalid_argument; diff --git a/src/protocols/btcd/protocol_btcd.cpp b/src/protocols/btcd/protocol_btcd.cpp index f96978f0..c89e6867 100644 --- a/src/protocols/btcd/protocol_btcd.cpp +++ b/src/protocols/btcd/protocol_btcd.cpp @@ -33,6 +33,7 @@ using namespace system; using namespace network; using namespace network::rpc; using namespace std::placeholders; +constexpr auto relaxed = std::memory_order_relaxed; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) @@ -214,20 +215,20 @@ bool protocol_btcd::handle_authenticate(const code& ec, if (stopped(ec)) return false; - // Reachable only when not authorized (see dispatch_websocket). The - // channel latches the digest only if it matches a configured credential. + // Reachable only when not authorized (see dispatch_websocket). set_authorized(network::config::credential::to_digest(username, password)); + if (authorized()) { send_result({}, 4); return true; } - // A failed authenticate ends the session (as btcd), once the error has - // reached the client. const code unauthorized{ network::error::unauthorized }; - send_error(unauthorized, two * unauthorized.message().size(), - unauthorized); + const auto size = two * unauthorized.message().size(); + + // Failed authentication ends session once error has reached caller. + send_error(unauthorized, size, unauthorized); return true; } @@ -253,8 +254,6 @@ bool protocol_btcd::handle_session(const code& ec, if (stopped(ec)) return false; - // The channel identifier is unique per connection for the process - // lifetime, satisfying btcwallet's reconnect-detection use of session ids. object_t result{}; result.emplace("id", identifier()); send_result(std::move(result), 32); @@ -264,21 +263,15 @@ bool protocol_btcd::handle_session(const code& ec, bool protocol_btcd::handle_stop(const code& ec, btcd_interface::stop) NOEXCEPT { - if (stopped(ec)) return false; + if (stopped(ec)) + return false; - // No secure remote-shutdown path exists; always rejected regardless of - // configuration or auth state. + // The server/node cannot stop itself. send_error(error::not_implemented); return true; } -// Handlers (getters): see protocol_btcd_utility.cpp. -// ---------------------------------------------------------------------------- - -// Handlers (tools): see protocol_btcd_utility.cpp. -// ---------------------------------------------------------------------------- - -// Handlers (subscription, mempool not_implemented pending v5 mempool). +// Handlers (subscription). // ---------------------------------------------------------------------------- bool protocol_btcd::handle_notify_blocks(const code& ec, @@ -287,7 +280,7 @@ bool protocol_btcd::handle_notify_blocks(const code& ec, if (stopped(ec)) return false; - subscribed_blocks_.store(true, std::memory_order_relaxed); + subscribed_blocks_.store(true, relaxed); send_result({}, 4); return true; } @@ -298,7 +291,7 @@ bool protocol_btcd::handle_stop_notify_blocks(const code& ec, if (stopped(ec)) return false; - subscribed_blocks_.store(false, std::memory_order_relaxed); + subscribed_blocks_.store(false, relaxed); send_result({}, 4); return true; } @@ -319,10 +312,7 @@ bool protocol_btcd::handle_stop_notify_new_transactions(const code& ec, return true; } -// Handlers (filters): see protocol_btcd_filter.cpp. -// ---------------------------------------------------------------------------- - -// Handlers (deprecated, permanently not_implemented). +// Handlers (deprecated, not_implemented). // ---------------------------------------------------------------------------- bool protocol_btcd::handle_notify_received(const code& ec, @@ -364,8 +354,8 @@ bool protocol_btcd::handle_rescan(const code& ec, { if (stopped(ec)) return false; - // Implemented only for the empty addrs/outpoints case (as btcd), the - // call btcwallet makes to bootstrap its sync starting point. + // Implemented only for the empty addresses/outpoints case (as btcd). + // This is the call btcwallet makes to bootstrap its sync starting point. hash_digest begin_hash{}; if (!decode_hash(begin_hash, beginblock)) { @@ -391,8 +381,6 @@ bool protocol_btcd::handle_rescan(const code& ec, return true; } - // Nothing to watch: report the current chain top as already-finished, - // ignoring beginblock (as btcd). const auto top = query.get_top_confirmed(); const auto header = query.get_header(query.to_confirmed(top)); if (!header) @@ -401,9 +389,7 @@ bool protocol_btcd::handle_rescan(const code& ec, return true; } - // Reply to the rescan call itself before the unprompted notification. send_result({}, 4); - send_notification("rescanfinished", array_t { encode_hash(query.get_top_confirmed_hash()), @@ -426,23 +412,27 @@ bool protocol_btcd::handle_chase(const code&, node::chase event_, switch (event_) { case node::chase::organized: - if (subscribed_blocks_.load(std::memory_order_relaxed)) + { + if (subscribed_blocks_.load(relaxed)) { BC_ASSERT(std::holds_alternative(value)); POST_NOTIFY(do_connected, std::get(value)); } break; - + } case node::chase::reorganized: - if (subscribed_blocks_.load(std::memory_order_relaxed)) + { + if (subscribed_blocks_.load(relaxed)) { BC_ASSERT(std::holds_alternative(value)); POST_NOTIFY(do_disconnected, std::get(value)); } break; - + } default: + { break; + } } return !stopped(); From b5fd28a2f73af3cc1f5d4c5daae8f309286ba328 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 20:17:25 -0400 Subject: [PATCH 25/29] btcd: derive help method list from interface metadata. --- include/bitcoin/server/interfaces/btcd.hpp | 21 +++++++++++++-------- include/bitcoin/server/interfaces/types.hpp | 7 +++++++ src/protocols/btcd/protocol_btcd.cpp | 8 ++------ 3 files changed, 22 insertions(+), 14 deletions(-) diff --git a/include/bitcoin/server/interfaces/btcd.hpp b/include/bitcoin/server/interfaces/btcd.hpp index e8dd33b4..6bac48f2 100644 --- a/include/bitcoin/server/interfaces/btcd.hpp +++ b/include/bitcoin/server/interfaces/btcd.hpp @@ -31,11 +31,11 @@ struct btcd_methods /// BTCD protocol (unversioned, json-rpc v1.0 over http/ws). static constexpr std::tuple methods { - /// Administrative (stop not implemented). + /// Administrative. method<"authenticate", string_t, string_t>{ "username", "password" }, method<"help", optional<""_t>>{ "command" }, method<"session">{}, - method<"stop">{}, + method<"stop">{ unimplemented }, /// Getters. method<"getbestblock">{}, @@ -54,21 +54,26 @@ struct btcd_methods /// Subscription. method<"notifyblocks">{}, method<"stopnotifyblocks">{}, - method<"notifynewtransactions", optional>{ "verbose" }, - method<"stopnotifynewtransactions">{}, + method<"notifynewtransactions", optional>{ unimplemented, "verbose" }, + method<"stopnotifynewtransactions">{ unimplemented }, /// Filters. method<"loadtxfilter", boolean_t, value_t, value_t>{ "reload", "addresses", "outpoints" }, method<"rescanblocks", value_t>{ "blockhashes" }, /// Deprecated. - method<"notifyreceived", value_t>{ "addresses" }, - method<"stopnotifyreceived", value_t>{ "addresses" }, - method<"notifyspent", value_t>{ "outpoints" }, - method<"stopnotifyspent", value_t>{ "outpoints" }, + method<"notifyreceived", value_t>{ unimplemented, "addresses" }, + method<"stopnotifyreceived", value_t>{ unimplemented, "addresses" }, + method<"notifyspent", value_t>{ unimplemented, "outpoints" }, + method<"stopnotifyspent", value_t>{ unimplemented, "outpoints" }, method<"rescan", string_t, value_t, value_t, optional<""_t>>{ "beginblock", "addresses", "outpoints", "endblock" } }; + /// Method names as reported by help. + static constexpr auto name_data = method_names(); + static constexpr std::string_view names{ name_data.data(), + name_data.size() }; + template using subscriber = network::subscriber; diff --git a/include/bitcoin/server/interfaces/types.hpp b/include/bitcoin/server/interfaces/types.hpp index 25cb1327..5628473c 100644 --- a/include/bitcoin/server/interfaces/types.hpp +++ b/include/bitcoin/server/interfaces/types.hpp @@ -31,6 +31,11 @@ template using method = network::rpc::method; template using method_at = network::rpc::method_at; +template +constexpr auto method_names() NOEXCEPT +{ + return network::rpc::method_names(); +} template using publish = network::rpc::publish; @@ -52,6 +57,8 @@ namespace empty { constexpr auto array = network::rpc::empty::array; }; namespace empty { constexpr auto object = network::rpc::empty::object; }; namespace empty { constexpr auto value = network::rpc::empty::value; }; +constexpr auto unimplemented = network::rpc::unimplemented; + } // namespace interface } // namespace server } // namespace libbitcoin diff --git a/src/protocols/btcd/protocol_btcd.cpp b/src/protocols/btcd/protocol_btcd.cpp index c89e6867..339e9879 100644 --- a/src/protocols/btcd/protocol_btcd.cpp +++ b/src/protocols/btcd/protocol_btcd.cpp @@ -239,12 +239,8 @@ bool protocol_btcd::handle_help(const code& ec, btcd_interface::help, return false; // Lists method names only, no per-command argument usage text. - send_result(std::string{ - "authenticate help session stop getbestblock getcurrentnet " - "getdifficulty getinfo getnettotals getnetworkhashps " - "createrawtransaction decoderawtransaction decodescript " - "validateaddress notifyblocks stopnotifyblocks loadtxfilter " - "rescanblocks" }, 256); + send_result(std::string{ btcd_interface::names }, + two * btcd_interface::names.size()); return true; } From eb671d3fae2518c35cbd6271ac50dff4e1ae6309 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 20:46:55 -0400 Subject: [PATCH 26/29] btcd: test unimplemented method list. --- builds/gnu/Makefile.am | 1 + .../libbitcoin-server-test.vcxproj | 1 + .../libbitcoin-server-test.vcxproj.filters | 22 +++-- .../libbitcoin-server-test.vcxproj | 1 + .../libbitcoin-server-test.vcxproj.filters | 22 +++-- test/interfaces/btcd.cpp | 80 +++++++++++++++++++ 6 files changed, 111 insertions(+), 16 deletions(-) create mode 100644 test/interfaces/btcd.cpp diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index 3a98f875..37eff919 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -210,6 +210,7 @@ test_libbitcoin_server_test_SOURCES = \ ${srcdir}/../../test/main.cpp \ ${srcdir}/../../test/settings.cpp \ ${srcdir}/../../test/test.cpp \ + ${srcdir}/../../test/interfaces/btcd.cpp \ ${srcdir}/../../test/mocks/blocks.cpp \ ${srcdir}/../../test/parsers/admin_query.cpp \ ${srcdir}/../../test/parsers/admin_target.cpp \ diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj index af97a969..6c88926e 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -120,6 +120,7 @@ + diff --git a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters index 5264f999..d60a9e5c 100644 --- a/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters +++ b/builds/msvc/vs2022/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters @@ -10,30 +10,33 @@ {66A0E586-2E3A-448F-0000-000000000000} - + {66A0E586-2E3A-448F-0000-000000000001} - + {66A0E586-2E3A-448F-0000-000000000002} - + {66A0E586-2E3A-448F-0000-000000000003} - + {66A0E586-2E3A-448F-0000-000000000004} - + {66A0E586-2E3A-448F-0000-000000000005} - + {66A0E586-2E3A-448F-0000-000000000006} - + {66A0E586-2E3A-448F-0000-000000000007} - + {66A0E586-2E3A-448F-0000-000000000008} + + {66A0E586-2E3A-448F-0000-000000000009} + @@ -42,6 +45,9 @@ src + + src\interfaces + src diff --git a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj index b2ae5c35..68c1ede7 100644 --- a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj @@ -120,6 +120,7 @@ + diff --git a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters index 5264f999..d60a9e5c 100644 --- a/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters +++ b/builds/msvc/vs2026/libbitcoin-server-test/libbitcoin-server-test.vcxproj.filters @@ -10,30 +10,33 @@ {66A0E586-2E3A-448F-0000-000000000000} - + {66A0E586-2E3A-448F-0000-000000000001} - + {66A0E586-2E3A-448F-0000-000000000002} - + {66A0E586-2E3A-448F-0000-000000000003} - + {66A0E586-2E3A-448F-0000-000000000004} - + {66A0E586-2E3A-448F-0000-000000000005} - + {66A0E586-2E3A-448F-0000-000000000006} - + {66A0E586-2E3A-448F-0000-000000000007} - + {66A0E586-2E3A-448F-0000-000000000008} + + {66A0E586-2E3A-448F-0000-000000000009} + @@ -42,6 +45,9 @@ src + + src\interfaces + src diff --git a/test/interfaces/btcd.cpp b/test/interfaces/btcd.cpp new file mode 100644 index 00000000..55175478 --- /dev/null +++ b/test/interfaces/btcd.cpp @@ -0,0 +1,80 @@ +/** + * Copyright (c) 2011-2026 libbitcoin developers + * + * This file is part of libbitcoin. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ +#include "../test.hpp" + +using namespace interface; + +// setup +// ----------------------------------------------------------------------------- + +// Keyed by name, so that assertions are independent of interface order. A +// misspelled name matches neither, failing both btcd_served and btcd_unserved. +constexpr bool btcd_declared(const std::string_view& name, bool implemented) NOEXCEPT +{ + auto result = false; + std::apply([&](const auto&... items) NOEXCEPT + { + ((result = result || (items.name == name && + items.implemented() == implemented)), ...); + }, btcd_methods::methods); + + return result; +} + +constexpr bool btcd_served(const std::string_view& name) NOEXCEPT +{ + return btcd_declared(name, true); +} + +constexpr bool btcd_unserved(const std::string_view& name) NOEXCEPT +{ + return btcd_declared(name, false); +} + +// btcd_methods::implemented +// ----------------------------------------------------------------------------- + +// These are dispatchable but answer not_implemented (see protocol_btcd). +static_assert(btcd_unserved("stop")); +static_assert(btcd_unserved("notifynewtransactions")); +static_assert(btcd_unserved("stopnotifynewtransactions")); +static_assert(btcd_unserved("notifyreceived")); +static_assert(btcd_unserved("stopnotifyreceived")); +static_assert(btcd_unserved("notifyspent")); +static_assert(btcd_unserved("stopnotifyspent")); + +// rescan is served for the empty addresses/outpoints form (btcwallet sync +// bootstrap), so it is published despite its group. +static_assert(btcd_served("rescan")); + +// A name absent from the interface satisfies neither. +static_assert(!btcd_served("getblockcount") && !btcd_unserved("getblockcount")); + +// btcd_methods::names +// ----------------------------------------------------------------------------- + +// The published list is the served subset, in interface order. +static_assert(btcd_methods::names == + "authenticate help session " + "getbestblock getcurrentnet getdifficulty getinfo getnettotals " + "getnetworkhashps " + "createrawtransaction decoderawtransaction decodescript validateaddress " + "notifyblocks stopnotifyblocks " + "loadtxfilter rescanblocks " + "rescan"); From 995b1301e009ec4986a7a27072842cfc11413e2b Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 22:59:00 -0400 Subject: [PATCH 27/29] Comments. --- src/protocols/bitcoind/protocol_bitcoind_rpc.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index cdaa226c..5145bfaf 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -614,8 +614,6 @@ bool protocol_bitcoind_rpc::handle_get_network_info(const code& ec, const auto& segments = settings.version.segments(); const auto version = 10'000 * segments[0] + 100 * segments[1] + segments[2]; - // libbitcoin-server is a node, not a wallet/peer-introspection service; - // peer-dependent fields (connections, addresses) are reported as empty. send_result(object_t { { "version", version }, @@ -815,8 +813,6 @@ void protocol_bitcoind_rpc::send_rpc(response_t&& model, size_t size_hint, using namespace http; static const auto json = from_media_type(media_type::application_json); - // Websocket frames carry no cached post request and no per-message - // headers to echo -- send a minimal response. if (websocket()) { id_.reset(); From fe2c772f10e962a5c3cfd22fd8ab6991705d8962 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 23:54:44 -0400 Subject: [PATCH 28/29] btcd: style, comments, reduce copies. --- .../server/protocols/protocol_btcd.hpp | 18 +- src/protocols/btcd/protocol_btcd.cpp | 21 +-- src/protocols/btcd/protocol_btcd_filter.cpp | 133 +++++++------ src/protocols/btcd/protocol_btcd_utility.cpp | 176 +++++++++--------- 4 files changed, 168 insertions(+), 180 deletions(-) diff --git a/include/bitcoin/server/protocols/protocol_btcd.hpp b/include/bitcoin/server/protocols/protocol_btcd.hpp index e5dbac9e..5c2adde5 100644 --- a/include/bitcoin/server/protocols/protocol_btcd.hpp +++ b/include/bitcoin/server/protocols/protocol_btcd.hpp @@ -149,6 +149,9 @@ class BCS_API protocol_btcd protected: using point = system::chain::point; using hash_digest = system::hash_digest; + using header_cptr = system::chain::header::cptr; + using hashes_ptr = std::shared_ptr; + using array_ptr = std::shared_ptr; using history = database::history; using histories = database::histories; using cursor_t = database::height_link; @@ -171,22 +174,21 @@ class BCS_API protocol_btcd const system::chain::points& points) NOEXCEPT; void complete_load_tx_filter(const code& ec) NOEXCEPT; - void do_rescan_blocks(const system::hashes& hashes) NOEXCEPT; - void do_rescan_watches(const system::hashes& hashes, + void do_rescan_blocks(const hashes_ptr& hashes) NOEXCEPT; + void do_rescan_watches(const hashes_ptr& hashes, const system::hashes& keys, system::chain::points& points) NOEXCEPT; void complete_rescan_blocks(const code& ec, - const network::rpc::array_t& discovered) NOEXCEPT; + const array_ptr& discovered) NOEXCEPT; /// Notification event handlers. /// ----------------------------------------------------------------------- void do_connected(node::header_t link) NOEXCEPT; void do_disconnected(node::header_t link) NOEXCEPT; - void notify_connected(const std::string& hash, size_t height, - uint32_t time, const std::string& header, - const network::rpc::array_t& txs) NOEXCEPT; - void notify_disconnected(const std::string& hash, size_t height, - uint32_t time, const std::string& header) NOEXCEPT; + void notify_connected(const header_cptr& header, size_t height, + const array_ptr& txs) NOEXCEPT; + void notify_disconnected(const header_cptr& header, + size_t height) NOEXCEPT; /// Utilities. /// ----------------------------------------------------------------------- diff --git a/src/protocols/btcd/protocol_btcd.cpp b/src/protocols/btcd/protocol_btcd.cpp index 339e9879..d5739f7b 100644 --- a/src/protocols/btcd/protocol_btcd.cpp +++ b/src/protocols/btcd/protocol_btcd.cpp @@ -224,10 +224,9 @@ bool protocol_btcd::handle_authenticate(const code& ec, return true; } + // Failed authentication ends session once error has reached caller. const code unauthorized{ network::error::unauthorized }; const auto size = two * unauthorized.message().size(); - - // Failed authentication ends session once error has reached caller. send_error(unauthorized, size, unauthorized); return true; } @@ -239,8 +238,8 @@ bool protocol_btcd::handle_help(const code& ec, btcd_interface::help, return false; // Lists method names only, no per-command argument usage text. - send_result(std::string{ btcd_interface::names }, - two * btcd_interface::names.size()); + const auto size = two * btcd_interface::names.size(); + send_result(std::string{ btcd_interface::names }, size); return true; } @@ -343,6 +342,8 @@ bool protocol_btcd::handle_stop_notify_spent(const code& ec, return true; } +// Implemented only for the empty addresses/outpoints case (as btcd). +// This is the call btcwallet makes to bootstrap its sync starting point. bool protocol_btcd::handle_rescan(const code& ec, btcd_interface::rescan, const std::string& beginblock, const value_t& addresses, const value_t& outpoints, @@ -350,8 +351,6 @@ bool protocol_btcd::handle_rescan(const code& ec, { if (stopped(ec)) return false; - // Implemented only for the empty addresses/outpoints case (as btcd). - // This is the call btcwallet makes to bootstrap its sync starting point. hash_digest begin_hash{}; if (!decode_hash(begin_hash, beginblock)) { @@ -378,7 +377,8 @@ bool protocol_btcd::handle_rescan(const code& ec, } const auto top = query.get_top_confirmed(); - const auto header = query.get_header(query.to_confirmed(top)); + const auto confirmed = query.to_confirmed(top); + const auto header = query.get_header(confirmed); if (!header) { send_error(error::not_found); @@ -388,7 +388,7 @@ bool protocol_btcd::handle_rescan(const code& ec, send_result({}, 4); send_notification("rescanfinished", array_t { - encode_hash(query.get_top_confirmed_hash()), + encode_hash(header->hash()), top, header->timestamp() }, 256); @@ -441,10 +441,9 @@ void protocol_btcd::send_notification(const std::string& method, array_t&& params, size_t size_hint) NOEXCEPT { BC_ASSERT(stranded()); + using namespace network::http; static const auto json = from_media_type(media_type::application_json); - - // Server-push notification: json-rpc-v1 request shape, no id. rpc::request notification{ { .size_hint = size_hint } }; notification.message.jsonrpc = version::v1; notification.message.method = method; @@ -455,7 +454,7 @@ void protocol_btcd::send_notification(const std::string& method, message.body() = std::move(notification); message.prepare_payload(); - // The notification strand poster shadows notify, qualify (as POST_BTCD). + // The notification strand poster shadows notify. this->network::protocol_http::template notify(std::move(message), &CLASS::handle_complete, _1, error::success); } diff --git a/src/protocols/btcd/protocol_btcd_filter.cpp b/src/protocols/btcd/protocol_btcd_filter.cpp index 964240a1..f9da6056 100644 --- a/src/protocols/btcd/protocol_btcd_filter.cpp +++ b/src/protocols/btcd/protocol_btcd_filter.cpp @@ -18,6 +18,7 @@ */ #include +#include #include #include #include @@ -36,6 +37,7 @@ namespace server { using namespace system; using namespace network; using namespace network::rpc; +constexpr auto relaxed = std::memory_order_relaxed; BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) @@ -73,8 +75,7 @@ bool protocol_btcd::handle_load_tx_filter(const code& ec, } monitor(true); - POST_NOTIFY(do_load_tx_filter, reload, std::move(keys), - std::move(points)); + POST_NOTIFY(do_load_tx_filter, reload, std::move(keys), std::move(points)); return true; } @@ -89,6 +90,7 @@ void protocol_btcd::do_load_tx_filter(bool reload, const hashes& keys, outpoint_watches_.clear(); } + histories discard{}; code ec{ error::success }; const auto& query = archive(); const auto maximum = server_settings().btcd.maximum_filters; @@ -99,7 +101,8 @@ void protocol_btcd::do_load_tx_filter(bool reload, const hashes& keys, if (stopping_) return; - if (address_watches_.size() + outpoint_watches_.size() >= maximum) + if (ceilinged_add(address_watches_.size(), outpoint_watches_.size()) >= + maximum) { ec = error::subscription_limit; break; @@ -109,7 +112,6 @@ void protocol_btcd::do_load_tx_filter(bool reload, const hashes& keys, const auto at = address_watches_.try_emplace(key, address_watch{}); if (at.second) { - histories discard{}; const auto fault = query.get_history(stopping_, at.first->second.cursor, discard, key, limit, turbo_); if (fault == database::error::query_canceled) @@ -125,15 +127,15 @@ void protocol_btcd::do_load_tx_filter(bool reload, const hashes& keys, if (ec) break; - if (address_watches_.size() + outpoint_watches_.size() >= maximum) + if (ceilinged_add(address_watches_.size(), outpoint_watches_.size()) >= + maximum) { ec = error::subscription_limit; break; } // Prime the spender set to present, so matching reports new only. - const auto at = outpoint_watches_.try_emplace(prevout, - outpoint_watch{}); + const auto at = outpoint_watches_.try_emplace(prevout, outpoint_watch{}); if (at.second) { auto& sub = at.first->second; @@ -174,10 +176,10 @@ bool protocol_btcd::handle_rescan_blocks(const code& ec, return true; } + hash_digest hash{}; hashes block_hashes{}; for (const auto& item: std::get(blockhashes.value())) { - hash_digest hash{}; if (!std::holds_alternative(item.value()) || !decode_hash(hash, std::get(item.value()))) { @@ -195,12 +197,13 @@ bool protocol_btcd::handle_rescan_blocks(const code& ec, } monitor(true); - POST_NOTIFY(do_rescan_blocks, std::move(block_hashes)); + POST_NOTIFY(do_rescan_blocks, emplace_shared( + std::move(block_hashes))); return true; } // Snapshot the watch-list, so the query runs parallel (not on the strand). -void protocol_btcd::do_rescan_blocks(const hashes& block_hashes) NOEXCEPT +void protocol_btcd::do_rescan_blocks(const hashes_ptr& block_hashes) NOEXCEPT { BC_ASSERT(notification_strand_.running_in_this_thread()); @@ -218,16 +221,17 @@ void protocol_btcd::do_rescan_blocks(const hashes& block_hashes) NOEXCEPT std::move(points)); } -void protocol_btcd::do_rescan_watches(const hashes& block_hashes, +void protocol_btcd::do_rescan_watches(const hashes_ptr& block_hashes, const hashes& keys, chain::points& points) NOEXCEPT { BC_ASSERT(!stranded()); - // Resolve named blocks to heights (result retains request order). sizes heights{}; - std::vector> named{}; const auto& query = archive(); - for (const auto& hash: block_hashes) + std::vector> named{}; + + // Resolve named blocks to heights (result retains request order). + for (auto& hash: *block_hashes) { if (stopping_) return; @@ -235,12 +239,13 @@ void protocol_btcd::do_rescan_watches(const hashes& block_hashes, size_t height{}; if (!query.get_height(height, query.to_header(hash))) { - POST_BTCD(complete_rescan_blocks, error::not_found, array_t{}); + POST_BTCD(complete_rescan_blocks, error::not_found, + to_shared()); return; } heights.insert(height); - named.emplace_back(hash, height); + named.emplace_back(std::move(hash), height); } // Match the snapshot against the named heights (full replay). @@ -282,11 +287,12 @@ void protocol_btcd::do_rescan_watches(const hashes& block_hashes, } } - POST_BTCD(complete_rescan_blocks, error::success, std::move(discovered)); + POST_BTCD(complete_rescan_blocks, error::success, + emplace_shared(std::move(discovered))); } void protocol_btcd::complete_rescan_blocks(const code& ec, - const array_t& discovered) NOEXCEPT + const array_ptr& discovered) NOEXCEPT { BC_ASSERT(stranded()); @@ -300,7 +306,7 @@ void protocol_btcd::complete_rescan_blocks(const code& ec, return; } - send_result(discovered, 256); + send_result(std::move(*discovered), 256); } // Notification event handlers. @@ -310,8 +316,8 @@ void protocol_btcd::do_connected(node::header_t link_value) NOEXCEPT { BC_ASSERT(notification_strand_.running_in_this_thread()); - const database::header_link link{ link_value }; const auto& query = archive(); + const database::header_link link{ link_value }; size_t height{}; if (!query.get_height(height, link)) @@ -348,9 +354,8 @@ void protocol_btcd::do_connected(node::header_t link_value) NOEXCEPT if (at != matched.end()) txs = serialize_matches(at->second); - POST_BTCD(notify_connected, encode_hash(query.get_header_key(link)), - height, header->timestamp(), - to_text(*header, chain::header::serialized_size()), std::move(txs)); + POST_BTCD(notify_connected, header, height, + emplace_shared(std::move(txs))); } void protocol_btcd::do_disconnected(node::header_t link_value) NOEXCEPT @@ -372,74 +377,63 @@ void protocol_btcd::do_disconnected(node::header_t link_value) NOEXCEPT if (!header) return; - POST_BTCD(notify_disconnected, encode_hash(query.get_header_key(link)), - height, header->timestamp(), - to_text(*header, chain::header::serialized_size())); + POST_BTCD(notify_disconnected, header, height); } -// blockconnected [hash, height, time] and filteredblockconnected [height, -// header, subscribedtxs], the latter sent unconditionally (as btcd). -void protocol_btcd::notify_connected(const std::string& hash, - size_t height, uint32_t time, const std::string& header, - const array_t& txs) NOEXCEPT +void protocol_btcd::notify_connected(const header_cptr& header, + size_t height, const array_ptr& txs) NOEXCEPT { BC_ASSERT(stranded()); - if (stopped() || !subscribed_blocks_.load(std::memory_order_relaxed)) + if (stopped() || !subscribed_blocks_.load(relaxed)) return; - send_notification("blockconnected", array_t - { - hash, - height, - time - }, 256); - - send_notification("filteredblockconnected", array_t - { - height, - header, - txs - }, 256); + // Elements are moved, as a braced initializer list always copies. + array_t connected{}; + connected.emplace_back(encode_hash(header->get_hash())); + connected.emplace_back(height); + connected.emplace_back(header->timestamp()); + send_notification("blockconnected", std::move(connected), 256); + + array_t filtered{}; + filtered.emplace_back(height); + filtered.emplace_back(to_text(*header, chain::header::serialized_size())); + filtered.emplace_back(std::move(*txs)); + send_notification("filteredblockconnected", std::move(filtered), 256); } -// blockdisconnected [hash, height, time] and filteredblockdisconnected -// [height, header]. -void protocol_btcd::notify_disconnected(const std::string& hash, - size_t height, uint32_t time, const std::string& header) NOEXCEPT +void protocol_btcd::notify_disconnected(const header_cptr& header, + size_t height) NOEXCEPT { BC_ASSERT(stranded()); - if (stopped() || !subscribed_blocks_.load(std::memory_order_relaxed)) + if (stopped() || !subscribed_blocks_.load(relaxed)) return; - send_notification("blockdisconnected", array_t - { - hash, - height, - time - }, 256); + array_t disconnected{}; + disconnected.emplace_back(encode_hash(header->get_hash())); + disconnected.emplace_back(height); + disconnected.emplace_back(header->timestamp()); + send_notification("blockdisconnected", std::move(disconnected), 256); - send_notification("filteredblockdisconnected", array_t - { - height, - header - }, 256); + array_t filtered{}; + filtered.emplace_back(height); + filtered.emplace_back(to_text(*header, chain::header::serialized_size())); + send_notification("filteredblockdisconnected", std::move(filtered), 256); } // Utilities. // ---------------------------------------------------------------------------- -// Called from notification strand (live) and parallel (rescan), so unasserted. +// Called from notification strand (live) and parallel (rescan). code protocol_btcd::match_addresses(matches& out, address_watch& sub, const hash_digest& key, const sizes& heights) NOEXCEPT { histories delta{}; const auto& query = archive(); const auto limit = server_settings().btcd.maximum_history; - const auto ec = query.get_history(stopping_, sub.cursor, delta, key, - limit, turbo_); - if (ec) + if (const auto ec = query.get_history(stopping_, sub.cursor, delta, key, + limit, turbo_); ec) return ec; for (const auto& entry: delta) @@ -449,6 +443,7 @@ code protocol_btcd::match_addresses(matches& out, address_watch& sub, return error::success; } +// Called from notification strand (live) and parallel (rescan). void protocol_btcd::match_outpoints(matches& out, outpoint_watch& sub, const point& prevout, const sizes& heights) NOEXCEPT { @@ -467,18 +462,16 @@ void protocol_btcd::match_outpoints(matches& out, outpoint_watch& sub, array_t protocol_btcd::serialize_matches(const matched_txs& txs) NOEXCEPT { - constexpr auto witness = true; array_t out{}; + constexpr auto witness = true; const auto& query = archive(); for (const auto& [position, hash]: txs) { if (stopping_) return out; - const auto tx = query.get_transaction(query.to_tx(hash), witness); - if (tx) - out.emplace_back(to_text(*tx, - tx->serialized_size(witness), witness)); + if (const auto tx = query.get_transaction(query.to_tx(hash), witness); tx) + out.emplace_back(to_text(*tx, tx->serialized_size(witness), witness)); } return out; diff --git a/src/protocols/btcd/protocol_btcd_utility.cpp b/src/protocols/btcd/protocol_btcd_utility.cpp index 004e9890..db1a2a7e 100644 --- a/src/protocols/btcd/protocol_btcd_utility.cpp +++ b/src/protocols/btcd/protocol_btcd_utility.cpp @@ -19,6 +19,7 @@ #include #include +#include #include #include #include @@ -38,32 +39,61 @@ BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) +// Utilities. +// ---------------------------------------------------------------------------- + +static std::string to_script_type(chain::script_pattern pattern) NOEXCEPT +{ + using namespace chain; + switch (pattern) + { + case script_pattern::pay_key_hash: + return "pubkeyhash"; + case script_pattern::pay_script_hash: + return "scripthash"; + case script_pattern::pay_multisig: + return "multisig"; + case script_pattern::pay_public_key: + return "pubkey"; + case script_pattern::pay_null_data: + return "nulldata"; + case script_pattern::pay_witness_key_hash: + return "witness_v0_keyhash"; + case script_pattern::pay_witness_script_hash: + return "witness_v0_scripthash"; + case script_pattern::pay_witness_v1_taproot: + return "witness_v1_taproot"; + default: + return "nonstandard"; + } +} + // Handlers (getters). // ---------------------------------------------------------------------------- +// Required by btcwallet during wallet chain-sync bootstrap. bool protocol_btcd::handle_get_best_block(const code& ec, btcd_interface::get_best_block) NOEXCEPT { if (stopped(ec)) return false; - // Required by btcwallet during wallet chain-sync bootstrap. const auto& query = archive(); - object_t result{}; - result.emplace("hash", encode_hash(query.get_top_confirmed_hash())); - result.emplace("height", query.get_top_confirmed()); - send_result(std::move(result), 96); + send_result(object_t + { + { "hash", encode_hash(query.get_top_confirmed_hash()) }, + { "height", query.get_top_confirmed() } + }, 96); return true; } +// p2p magic (checked once by btcwallet/lnd to confirm network). bool protocol_btcd::handle_get_current_net(const code& ec, btcd_interface::get_current_net) NOEXCEPT { if (stopped(ec)) return false; - // The p2p handshake magic, the same value btcd returns (checked once by - // btcwallet/lnd at connect to confirm the expected network). send_result(network_settings().identifier, 20); return true; } @@ -75,8 +105,8 @@ bool protocol_btcd::handle_get_difficulty(const code& ec, return false; const auto& query = archive(); - const auto header = query.get_header(query.to_confirmed( - query.get_top_confirmed())); + const auto top = query.get_top_confirmed(); + const auto header = query.get_header(query.to_confirmed(top)); if (!header) { send_error(database::error::integrity); @@ -102,14 +132,11 @@ bool protocol_btcd::handle_get_info(const code& ec, return true; } - // btcd's numeric version encoding (1'000'000 major, 10'000 minor, 100 - // patch). + // btcd's numeric version encoding (1'000'000 major, 10'000 minor, 100 patch). const auto& segments = server_settings().btcd.version.segments(); const auto version = 1'000'000 * segments[0] + 10'000 * segments[1] + 100 * segments[2]; - // Untracked peer-dependent fields are reported as empty/zero, matching - // the inherited getnetworkinfo handler's convention. send_result(object_t { { "version", version }, @@ -132,7 +159,6 @@ bool protocol_btcd::handle_get_net_totals(const code& ec, if (stopped(ec)) return false; - // Untracked byte counters are reported as zero; timemillis is real. send_result(object_t { { "totalbytesrecv", 0 }, @@ -148,12 +174,10 @@ bool protocol_btcd::handle_get_network_hash_ps(const code& ec, if (stopped(ec)) return false; - // Approximated from the requested height's difficulty and the configured - // target spacing, not a windowed average over 'blocks'. const auto& query = archive(); const auto top = query.get_top_confirmed(); const auto target = is_negative(height) ? top : - std::min(possible_sign_cast(height), top); + std::min(sign_cast(height), top); const auto header = query.get_header(query.to_confirmed(target)); if (!header) @@ -162,9 +186,9 @@ bool protocol_btcd::handle_get_network_hash_ps(const code& ec, return true; } - constexpr auto two_to_the_32 = 4294967296.0; - const auto spacing = system_settings().block_spacing_seconds; - send_result(header->difficulty() * two_to_the_32 / spacing, 20); + const auto period = system_settings().block_spacing_seconds; + const auto span = to_floating(power2(32u)); + send_result(header->difficulty() * span / period, 20); return true; } @@ -178,8 +202,15 @@ bool protocol_btcd::handle_create_raw_transaction(const code& ec, if (stopped(ec)) return false; - chain::inputs ins{}; - ins.reserve(inputs.size()); + using namespace chain; + uint32_t vout{}; + hash_digest hash{}; + + // The transaction owns shared inputs/outputs, so these are populated + // directly, avoiding the intermediate vectors that to_shareds() copies. + const auto ins = std::make_shared(); + ins->reserve(inputs.size()); + for (const auto& item: inputs) { if (!std::holds_alternative(item.value())) @@ -199,8 +230,6 @@ bool protocol_btcd::handle_create_raw_transaction(const code& ec, return true; } - hash_digest hash{}; - uint32_t vout{}; if (!decode_hash(hash, std::get(txid_it->second.value())) || !to_integer(vout, std::get(vout_it->second.value()))) { @@ -208,12 +237,14 @@ bool protocol_btcd::handle_create_raw_transaction(const code& ec, return true; } - ins.emplace_back(chain::point{ hash, vout }, chain::script{}, - 0xffffffff_u32); + ins->push_back(to_shared(point{ hash, vout }, script{}, + max_input_sequence)); } - chain::outputs outs{}; - outs.reserve(outputs.size()); + script script{}; + uint64_t satoshi{}; + const auto outs = std::make_shared(); + outs->reserve(outputs.size()); for (const auto& pair: outputs) { if (!std::holds_alternative(pair.second.value())) @@ -222,7 +253,6 @@ bool protocol_btcd::handle_create_raw_transaction(const code& ec, return true; } - chain::script script{}; if (const auto fault = btcd::output_script(script, pair.first, p2kh_, p2sh_, witness_)) { @@ -230,24 +260,18 @@ bool protocol_btcd::handle_create_raw_transaction(const code& ec, return true; } - // Scale to satoshis and validate the domain (not whole, as scaling - // carries floating point representation error). - uint64_t satoshi{}; const auto btc = std::get(pair.second.value()); - if (!to_integer(satoshi, btc * chain::satoshi_per_bitcoin, false)) + if (!to_integer(satoshi, btc * satoshi_per_bitcoin, false)) { send_error(error::invalid_argument); return true; } - outs.emplace_back(satoshi, std::move(script)); + outs->push_back(to_shared(satoshi, std::move(script))); } - const chain::transaction tx{ 1, std::move(ins), std::move(outs), - locktime }; - - // Unsigned, so no witness data yet -- plain (non-witness) serialization. constexpr auto witness = false; + const transaction tx{ 1, ins, outs, locktime }; send_result(to_text(tx, tx.serialized_size(witness), witness), 400); return true; } @@ -274,10 +298,7 @@ bool protocol_btcd::handle_decode_raw_transaction(const code& ec, return true; } - // bitcoind(tx) alone (no inject_tx_context) is the bare - // decoderawtransaction shape -- a standalone tx has no block context. - send_result(value_from(bitcoind(tx)), - two * tx.serialized_size(witness)); + send_result(value_from(bitcoind(tx)), two * tx.serialized_size(witness)); return true; } @@ -294,63 +315,33 @@ bool protocol_btcd::handle_decode_script(const code& ec, return true; } - // false: raw script bytes, no serialized length prefix. - const chain::script script{ data, false }; + using namespace chain; + constexpr auto prefix = false; + const script script{ data, prefix }; if (!script.is_valid()) { send_error(error::invalid_argument); return true; } - using namespace chain; + using namespace wallet; + const auto pattern = script.output_pattern(); + object_t result{}; result.emplace("asm", script.to_string(flags::all_rules, true)); + result.emplace("type", to_script_type(pattern)); - std::string kind{ "nonstandard" }; - switch (script.output_pattern()) + if (pattern == script_pattern::pay_key_hash || + pattern == script_pattern::pay_script_hash) { - case script_pattern::pay_key_hash: - kind = "pubkeyhash"; - break; - case script_pattern::pay_script_hash: - kind = "scripthash"; - break; - case script_pattern::pay_multisig: - kind = "multisig"; - break; - case script_pattern::pay_public_key: - kind = "pubkey"; - break; - case script_pattern::pay_null_data: - kind = "nulldata"; - break; - case script_pattern::pay_witness_key_hash: - kind = "witness_v0_keyhash"; - break; - case script_pattern::pay_witness_script_hash: - kind = "witness_v0_scripthash"; - break; - case script_pattern::pay_witness_v1_taproot: - kind = "witness_v1_taproot"; - break; - default: - break; + const auto pay = payment_address::extract_output(script, p2kh_, p2sh_); + if (pay) + result.emplace("address", pay.encoded()); } - result.emplace("type", kind); - if (script.output_pattern() == script_pattern::pay_key_hash || - script.output_pattern() == script_pattern::pay_script_hash) - { - const auto address = wallet::payment_address::extract_output(script, - p2kh_, p2sh_); - if (address) - result.emplace("address", address.encoded()); - } - - // The p2sh-wrapping address of this exact script (btcd's "p2sh" field). - const wallet::payment_address wrapped{ script, p2sh_ }; - if (wrapped) - result.emplace("p2sh", wrapped.encoded()); + const payment_address pay{ script, p2sh_ }; + if (pay) + result.emplace("p2sh", pay.encoded()); send_result(std::move(result), 256); return true; @@ -362,7 +353,8 @@ bool protocol_btcd::handle_validate_address(const code& ec, if (stopped(ec)) return false; - const wallet::payment_address base58(address); + using namespace wallet; + const payment_address base58(address); if (base58) { send_result(object_t @@ -375,15 +367,17 @@ bool protocol_btcd::handle_validate_address(const code& ec, return true; } - const wallet::witness_address witness(address); + const witness_address witness(address); if (witness && witness.prefix() == witness_) { + const auto version0_p2sh = (witness.identifier() == + witness_address::program_type::version0_p2sh); + send_result(object_t { { "isvalid", true }, { "address", witness.encoded() }, - { "isscript", witness.identifier() == - wallet::witness_address::program_type::version0_p2sh }, + { "isscript", version0_p2sh }, { "iswitness", true }, { "witness_version", witness.version() }, { "witness_program", encode_base16(witness.program()) } From 374047c95a2abf6a64e1a000d1cbb763c82e1216 Mon Sep 17 00:00:00 2001 From: Eric Voskuil Date: Wed, 5 Aug 2026 23:54:52 -0400 Subject: [PATCH 29/29] Style. --- src/protocols/bitcoind/protocol_bitcoind_rpc.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index 5145bfaf..c505f987 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -539,7 +539,7 @@ bool protocol_bitcoind_rpc::handle_get_tx_out(const code& ec, size_t height{}; const auto strong = query.get_tx_height(height, tx_link); - const auto depth = strong ? add1(floored_subtract(top, height)) : 0_size; + const auto depth = strong ? add1(floored_subtract(top, height)) : zero; const auto coins = to_floating(output->value()) / chain::satoshi_per_bitcoin;