diff --git a/builds/gnu/Makefile.am b/builds/gnu/Makefile.am index fe918ebb..37eff919 100644 --- a/builds/gnu/Makefile.am +++ b/builds/gnu/Makefile.am @@ -55,25 +55,19 @@ 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 \ ${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 \ - ${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 \ @@ -86,6 +80,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,33 +107,38 @@ 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 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 \ ${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 @@ -140,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 \ @@ -153,7 +160,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 \ @@ -164,20 +171,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 +210,8 @@ 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 \ ${srcdir}/../../test/parsers/bitcoind_target.cpp \ @@ -217,13 +220,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 +239,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/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/vs2022/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj index c865858b..44738f13 100644 --- a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj @@ -127,6 +127,7 @@ + @@ -134,9 +135,9 @@ - - - + + + @@ -166,7 +167,6 @@ - @@ -189,6 +189,7 @@ + @@ -197,7 +198,7 @@ - + diff --git a/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2022/libbitcoin-server/libbitcoin-server.vcxproj.filters index 4304d63a..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 @@ -108,13 +111,13 @@ src\protocols\bitcoind - + src\protocols\btcd - + src\protocols\btcd - + src\protocols\btcd @@ -200,9 +203,6 @@ include\bitcoin\server\channels - - include\bitcoin\server\channels - include\bitcoin\server\channels @@ -269,6 +269,9 @@ include\bitcoin\server\parsers + + include\bitcoin\server\parsers + include\bitcoin\server\parsers @@ -293,7 +296,7 @@ include\bitcoin\server\protocols - + include\bitcoin\server\protocols 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/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj index 1aeb9b5a..318a680f 100644 --- a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj +++ b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj @@ -127,6 +127,7 @@ + @@ -134,9 +135,9 @@ - - - + + + @@ -166,7 +167,6 @@ - @@ -189,6 +189,7 @@ + @@ -197,7 +198,7 @@ - + diff --git a/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters b/builds/msvc/vs2026/libbitcoin-server/libbitcoin-server.vcxproj.filters index 4304d63a..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 @@ -108,13 +111,13 @@ src\protocols\bitcoind - + src\protocols\btcd - + src\protocols\btcd - + src\protocols\btcd @@ -200,9 +203,6 @@ include\bitcoin\server\channels - - include\bitcoin\server\channels - include\bitcoin\server\channels @@ -269,6 +269,9 @@ include\bitcoin\server\parsers + + include\bitcoin\server\parsers + include\bitcoin\server\parsers @@ -293,7 +296,7 @@ include\bitcoin\server\protocols - + include\bitcoin\server\protocols 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..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,10 +271,8 @@ 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). +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 kinds of test: @@ -293,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 @@ -593,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 ``` @@ -726,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..6d070047 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: @@ -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 bc73e7e1..49bd9c96 100644 --- a/endpoints/test_btcd_rpc.py +++ b/endpoints/test_btcd_rpc.py @@ -2,10 +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 chain methods -inherited from protocol_bitcoind_rpc). See docs/btcd-endpoint.md for the full -design and phased scope. +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: @@ -259,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 = { @@ -290,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. @@ -501,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 @@ -569,18 +568,18 @@ 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 -# 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 # 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]), @@ -590,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).""" @@ -600,19 +599,19 @@ 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 - for session/notifyblocks/etc (see protocol_btcd_rpc::dispatch_websocket / + """bitcoind interface methods also work over the same ws connection used + 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 standard chain 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") @@ -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") @@ -694,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 chain 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/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.hpp b/include/bitcoin/server.hpp index 47a08daf..44e38fc4 100644 --- a/include/bitcoin/server.hpp +++ b/include/bitcoin/server.hpp @@ -22,20 +22,26 @@ #include #include #include -#include -#include -#include -#include #include -#include #include #include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include #include #include #include +#include #include #include #include @@ -44,7 +50,7 @@ #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 diff --git a/include/bitcoin/server/channels/channel_btcd.hpp b/include/bitcoin/server/channels/channel_btcd.hpp deleted file mode 100644 index f5801670..00000000 --- a/include/bitcoin/server/channels/channel_btcd.hpp +++ /dev/null @@ -1,129 +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 { - -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 client has authenticated over the ws 'authenticate' - /// handshake (or basic auth was not configured, requiring none). - 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. - 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. 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. - 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; - - /// 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. - 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; - } - - /// 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. - 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/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 8698a76f..a151318f 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 */ diff --git a/include/bitcoin/server/interfaces/btcd.hpp b/include/bitcoin/server/interfaces/btcd.hpp index 3995849a..6bac48f2 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 @@ -28,57 +28,52 @@ 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 { - /// Handshake (disallowed when basic auth has been established). + /// Administrative. method<"authenticate", string_t, string_t>{ "username", "password" }, - - /// Admin. + method<"help", optional<""_t>>{ "command" }, method<"session">{}, + method<"stop">{ unimplemented }, - /// Block subscription. - method<"notifyblocks">{}, - method<"stopnotifyblocks">{}, - - /// Tx subscription. - method<"notifynewtransactions", optional>{ "verbose" }, - method<"stopnotifynewtransactions">{}, - - /// Address/outpoint filtering. - 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). - 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, checked once by btcwallet/lnd at connect). + /// Getters. + method<"getbestblock">{}, method<"getcurrentnet">{}, - - /// Generic btcd-tooling compatibility (no lnd consumer). 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" }, - 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. - method<"getbestblock">{} + /// Subscription. + method<"notifyblocks">{}, + method<"stopnotifyblocks">{}, + 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>{ 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; @@ -87,30 +82,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/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/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_bitcoind_rpc.hpp b/include/bitcoin/server/protocols/protocol_bitcoind_rpc.hpp index 5330ae51..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; @@ -55,28 +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 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. - 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. - 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, @@ -128,9 +113,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 @@ -155,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 @@ -174,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/include/bitcoin/server/protocols/protocol_btcd.hpp b/include/bitcoin/server/protocols/protocol_btcd.hpp new file mode 100644 index 00000000..5c2adde5 --- /dev/null +++ b/include/bitcoin/server/protocols/protocol_btcd.hpp @@ -0,0 +1,243 @@ +/** + * 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_PROTOCOLS_PROTOCOL_BTCD_HPP +#define LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_BTCD_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace libbitcoin { +namespace server { + +/// btcd interface, added to the inherited bitcoind interface. +class BCS_API protocol_btcd + : public server::protocol_bitcoind_rpc, + protected network::tracker +{ +public: + typedef std::shared_ptr ptr; + using btcd_interface = interface::btcd; + using btcd_dispatcher = network::rpc::dispatcher; + + 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), + options_(options), + turbo_(session->database_settings().turbo), + 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()) + { + } + + void start() NOEXCEPT override; + void stopping(const code& ec) NOEXCEPT override; + +protected: + /// 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 (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; + + /// Handlers (getters). + bool handle_get_best_block(const code& ec, + btcd_interface::get_best_block) NOEXCEPT; + 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; + bool handle_get_net_totals(const code& ec, + btcd_interface::get_net_totals) NOEXCEPT; + 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, + const network::rpc::object_t& outputs, uint32_t locktime) NOEXCEPT; + bool handle_decode_raw_transaction(const code& ec, + btcd_interface::decode_raw_transaction, + const std::string& hexstring) NOEXCEPT; + bool handle_decode_script(const code& ec, + btcd_interface::decode_script, const std::string& hex) NOEXCEPT; + bool handle_validate_address(const code& ec, + btcd_interface::validate_address, + const std::string& address) NOEXCEPT; + + /// Handlers (subscription). + 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; + 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 (filters). + bool handle_load_tx_filter(const code& ec, + btcd_interface::load_tx_filter, bool reload, + const network::rpc::value_t& addresses, + const network::rpc::value_t& outpoints) NOEXCEPT; + bool handle_rescan_blocks(const code& ec, + btcd_interface::rescan_blocks, + const network::rpc::value_t& blockhashes) NOEXCEPT; + + /// Handlers (deprecated, not_implemented). + bool handle_notify_received(const code& ec, + btcd_interface::notify_received, + const network::rpc::value_t& addresses) NOEXCEPT; + bool handle_stop_notify_received(const code& ec, + btcd_interface::stop_notify_received, + const network::rpc::value_t& addresses) NOEXCEPT; + bool handle_notify_spent(const code& ec, + btcd_interface::notify_spent, + const network::rpc::value_t& outpoints) NOEXCEPT; + bool handle_stop_notify_spent(const code& ec, + btcd_interface::stop_notify_spent, + const network::rpc::value_t& outpoints) NOEXCEPT; + bool handle_rescan(const code& ec, btcd_interface::rescan, + const std::string& beginblock, const network::rpc::value_t& addresses, + const network::rpc::value_t& outpoints, + const std::string& endblock) NOEXCEPT; + + /// Event handlers. + bool handle_chase(const code& ec, node::chase event_, + node::event_value value) NOEXCEPT; + + /// Sender (server push, 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 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; + + struct address_watch final + { + cursor_t cursor{}; + }; + + struct outpoint_watch final + { + database::history outpoint{}; + database::histories spenders{}; + }; + + /// Completion handlers (for long-running or other async queries). + /// ----------------------------------------------------------------------- + + 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 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 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 header_cptr& header, size_t height, + const array_ptr& txs) NOEXCEPT; + void notify_disconnected(const header_cptr& header, + size_t height) NOEXCEPT; + + /// Utilities. + /// ----------------------------------------------------------------------- + using matched_txs = std::map; + using matches = std::map; + using sizes = std::set; + + code match_addresses(matches& out, address_watch& sub, + const hash_digest& key, const sizes& heights) NOEXCEPT; + void match_outpoints(matches& out, outpoint_watch& sub, + const point& prevout, const sizes& heights) NOEXCEPT; + network::rpc::array_t serialize_matches(const matched_txs& txs) NOEXCEPT; + +private: + template + inline void btcd_subscribe(Method&& method, Args&&... args) NOEXCEPT + { + 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. + const options_t& options_; + const bool turbo_; + const uint8_t p2kh_; + const uint8_t p2sh_; + const std::string witness_; + std::atomic_bool stopping_{}; + std::atomic_bool subscribed_blocks_{}; + + // This is protected by strand. + btcd_dispatcher btcd_dispatcher_{}; + + // This is thread safe, uses network threadpool. + network::asio::strand notification_strand_; + + // These are protected by notification strand. + std::map outpoint_watches_{}; + std::map address_watches_{}; +}; + +} // namespace server +} // namespace libbitcoin + +#endif diff --git a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp b/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp deleted file mode 100644 index 8ba7cac6..00000000 --- a/include/bitcoin/server/protocols/protocol_btcd_rpc.hpp +++ /dev/null @@ -1,312 +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_PROTOCOLS_PROTOCOL_BTCD_RPC_HPP -#define LIBBITCOIN_SERVER_PROTOCOLS_PROTOCOL_BTCD_RPC_HPP - -#include -#include -#include -#include -#include -#include -#include -#include - -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. -/// -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; - - inline protocol_btcd_rpc(const auto& session, - const network::channel::ptr& channel, - const options_t& options) NOEXCEPT - : 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), - p2sh_(session->system_settings().forks.difficult ? - system::wallet::payment_address::mainnet_p2sh : - system::wallet::payment_address::testnet_p2sh) - { - } - - void start() NOEXCEPT override; - void stopping(const code& ec) NOEXCEPT override; - -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. - 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). - 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. - 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). - 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). - 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). - bool handle_get_difficulty(const code& ec, - btcd_interface::get_difficulty) NOEXCEPT; - bool handle_get_info(const code& ec, btcd_interface::get_info) NOEXCEPT; - bool handle_get_net_totals(const code& ec, - btcd_interface::get_net_totals) NOEXCEPT; - bool handle_get_network_hash_ps(const code& ec, - btcd_interface::get_network_hash_ps, uint32_t blocks, - int32_t height) NOEXCEPT; - bool handle_create_raw_transaction(const code& ec, - btcd_interface::create_raw_transaction, - const network::rpc::array_t& inputs, - const network::rpc::object_t& outputs, uint32_t locktime) NOEXCEPT; - bool handle_decode_raw_transaction(const code& ec, - btcd_interface::decode_raw_transaction, - const std::string& hexstring) NOEXCEPT; - bool handle_decode_script(const code& ec, - btcd_interface::decode_script, const std::string& hex) NOEXCEPT; - 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 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. - code parse_output_script(const std::string& text, - system::chain::script& out) NOEXCEPT; - - /// Handlers (block subscription). - 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). 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. - bool handle_load_tx_filter(const code& ec, - btcd_interface::load_tx_filter, bool reload, - const network::rpc::value_t& addresses, - const network::rpc::value_t& outpoints) NOEXCEPT; - bool handle_rescan_blocks(const code& ec, - btcd_interface::rescan_blocks, - 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. - 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. - 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; - - /// Handlers (deprecated, permanently not_implemented). - bool handle_notify_received(const code& ec, - btcd_interface::notify_received, - const network::rpc::value_t& addresses) NOEXCEPT; - bool handle_stop_notify_received(const code& ec, - btcd_interface::stop_notify_received, - const network::rpc::value_t& addresses) NOEXCEPT; - bool handle_notify_spent(const code& ec, - btcd_interface::notify_spent, - const network::rpc::value_t& outpoints) NOEXCEPT; - bool handle_stop_notify_spent(const code& ec, - btcd_interface::stop_notify_spent, - const network::rpc::value_t& outpoints) NOEXCEPT; - bool handle_rescan(const code& ec, btcd_interface::rescan, - const std::string& beginblock, const network::rpc::value_t& addresses, - const network::rpc::value_t& outpoints, - const std::string& endblock) NOEXCEPT; - - /// 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; - - /// 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. - 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, - network::rpc::array_t&& params, size_t size_hint) NOEXCEPT; - -private: - template - inline void btcd_subscribe(Method&& method, Args&&... args) NOEXCEPT - { - 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 channel_btcd::ptr btcd_channel_; - const uint8_t p2kh_; - const uint8_t p2sh_; - - // These are protected by strand. - btcd_dispatcher btcd_dispatcher_{}; - 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. - 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_{}; -}; - -} // namespace server -} // namespace libbitcoin - -#endif 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/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/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 5c96c7ca..78d2890f 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/include/bitcoin/server/settings.hpp b/include/bitcoin/server/settings.hpp index e2743848..3d6e7c1e 100644 --- a/include/bitcoin/server/settings.hpp +++ b/include/bitcoin/server/settings.hpp @@ -152,12 +152,55 @@ 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 + { + using base = network::settings::http_server; + using base::base; + + /// Arbitrary version identity returned by getnetworkinfo. + system::config::version version{}; + 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{}; + + /// 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. DELETE_COPY(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; @@ -165,10 +208,10 @@ 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" }; + 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 7f88ccc5..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", @@ -1120,6 +1137,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), @@ -1202,6 +1229,21 @@ 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.maximum_filters", + value(&configured.server.btcd.maximum_filters), + "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", value(&configured.server.btcd.hosts), diff --git a/src/parsers/btcd_filter.cpp b/src/parsers/btcd_filter.cpp new file mode 100644 index 00000000..0c6040ce --- /dev/null +++ b/src/parsers/btcd_filter.cpp @@ -0,0 +1,111 @@ +/** + * 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 +#include + +namespace libbitcoin { +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(script& out, const std::string& text, uint8_t p2kh, + uint8_t p2sh, const std::string& witness) NOEXCEPT +{ + using namespace wallet; + + if (const payment_address payment{ text }; payment) + { + out = payment.output_script(p2kh, p2sh); + return error::success; + } + + if (const witness_address payment{ text }; + payment && payment.prefix() == witness) + { + out = payment.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; + + script script{}; + for (const auto& item: std::get(addresses.value())) + { + if (!std::holds_alternative(item.value())) + return error::invalid_argument; + + 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()); + } + + return error::success; +} + +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())) + 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; + + 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/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/bitcoind/protocol_bitcoind_rpc.cpp b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp index 0ef5eeb5..c505f987 100644 --- a/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp +++ b/src/protocols/bitcoind/protocol_bitcoind_rpc.cpp @@ -157,45 +157,62 @@ 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. - 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; - // 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 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 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. -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 parsed as json-rpc requests (channel body type). + if (!request.body().contains()) + { + stop(error::invalid_argument); + return; + } + + const auto& message = request.body().get().message; + + // 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. @@ -295,18 +312,36 @@ 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, 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 + // 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", 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 { { "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()))) }, @@ -317,33 +352,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, 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. - { "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; } @@ -355,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; } @@ -530,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; @@ -600,22 +609,23 @@ 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); @@ -738,6 +748,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 { @@ -776,16 +802,17 @@ 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; 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. if (websocket()) { id_.reset(); @@ -797,7 +824,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; } @@ -811,10 +838,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 { @@ -824,6 +851,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 { diff --git a/src/protocols/btcd/protocol_btcd.cpp b/src/protocols/btcd/protocol_btcd.cpp new file mode 100644 index 00000000..d5739f7b --- /dev/null +++ b/src/protocols/btcd/protocol_btcd.cpp @@ -0,0 +1,467 @@ +/** + * 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 +#include +#include + +namespace libbitcoin { +namespace server { + +#define CLASS protocol_btcd +#define SUBSCRIBE_BTCD(method, ...) \ + btcd_subscribe(&CLASS::method, __VA_ARGS__) + +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) +BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) + +// Start. +// ---------------------------------------------------------------------------- + +void protocol_btcd::start() NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (started()) + return; + + 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_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); + + // 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); + + // 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); + SUBSCRIBE_BTCD(handle_stop_notify_spent, _1, _2, _3); + SUBSCRIBE_BTCD(handle_rescan, _1, _2, _3, _4, _5, _6); + + // Base registers the bitcoind interface handlers and starts the listener. + protocol_bitcoind_rpc::start(); +} + +// Events unsubscription is asynchronous, race is ok. +void protocol_btcd::stopping(const code& ec) NOEXCEPT +{ + BC_ASSERT(stranded()); + stopping_.store(true); + btcd_dispatcher_.stop(ec); + unsubscribe_chase(); + protocol_bitcoind_rpc::stopping(ec); +} + +// Dispatch. +// ---------------------------------------------------------------------------- + +void protocol_btcd::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::dispatch_websocket( + const network::http::request& request) NOEXCEPT +{ + BC_ASSERT(stranded()); + + // Websocket frames are parsed as json-rpc requests (websocket_rpc). + if (!request.body().contains()) + { + stop(error::invalid_argument); + return; + } + + const auto& message = request.body().get().message; + + // Cache request context for response building (version + id). + set_rpc_request(message); + + const auto authenticate = + (message.method == btcd_interface::authenticate::name); + + if (authenticate == authorized()) + { + send_error(network::error::unauthorized); + return; + } + + if (!authenticate && !permitted(message.method)) + { + send_error(network::error::unauthorized); + return; + } + + const auto code = btcd_dispatcher_.notify(message); + + // Defer to the base websocket transport on a method name miss. + if (code == network::error::unexpected_method) + { + protocol_bitcoind_rpc::dispatch_websocket(request); + return; + } + + if (code) + stop(code); +} + +// Handlers (administrative). +// ---------------------------------------------------------------------------- + +bool protocol_btcd::handle_authenticate(const code& ec, + btcd_interface::authenticate, const std::string& username, + const std::string& password) NOEXCEPT +{ + if (stopped(ec)) + return false; + + // Reachable only when not authorized (see dispatch_websocket). + set_authorized(network::config::credential::to_digest(username, password)); + + if (authorized()) + { + send_result({}, 4); + return true; + } + + // Failed authentication ends session once error has reached caller. + const code unauthorized{ network::error::unauthorized }; + const auto size = two * unauthorized.message().size(); + send_error(unauthorized, size, unauthorized); + 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. + const auto size = two * btcd_interface::names.size(); + send_result(std::string{ btcd_interface::names }, size); + return true; +} + +bool protocol_btcd::handle_session(const code& ec, + btcd_interface::session) NOEXCEPT +{ + if (stopped(ec)) + return false; + + object_t result{}; + result.emplace("id", identifier()); + send_result(std::move(result), 32); + return true; +} + +bool protocol_btcd::handle_stop(const code& ec, + btcd_interface::stop) NOEXCEPT +{ + if (stopped(ec)) + return false; + + // The server/node cannot stop itself. + send_error(error::not_implemented); + return true; +} + +// Handlers (subscription). +// ---------------------------------------------------------------------------- + +bool protocol_btcd::handle_notify_blocks(const code& ec, + btcd_interface::notify_blocks) NOEXCEPT +{ + if (stopped(ec)) + return false; + + subscribed_blocks_.store(true, relaxed); + send_result({}, 4); + return true; +} + +bool protocol_btcd::handle_stop_notify_blocks(const code& ec, + btcd_interface::stop_notify_blocks) NOEXCEPT +{ + if (stopped(ec)) + return false; + + subscribed_blocks_.store(false, relaxed); + send_result({}, 4); + return true; +} + +bool protocol_btcd::handle_notify_new_transactions(const code& ec, + btcd_interface::notify_new_transactions, bool) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::not_implemented); + return true; +} + +bool protocol_btcd::handle_stop_notify_new_transactions(const code& ec, + btcd_interface::stop_notify_new_transactions) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::not_implemented); + return true; +} + +// Handlers (deprecated, not_implemented). +// ---------------------------------------------------------------------------- + +bool protocol_btcd::handle_notify_received(const code& ec, + btcd_interface::notify_received, const value_t&) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::not_implemented); + return true; +} + +bool protocol_btcd::handle_stop_notify_received(const code& ec, + btcd_interface::stop_notify_received, const value_t&) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::not_implemented); + return true; +} + +bool protocol_btcd::handle_notify_spent(const code& ec, + btcd_interface::notify_spent, const value_t&) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::not_implemented); + return true; +} + +bool protocol_btcd::handle_stop_notify_spent(const code& ec, + btcd_interface::stop_notify_spent, const value_t&) NOEXCEPT +{ + if (stopped(ec)) return false; + send_error(error::not_implemented); + 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, + const std::string&) NOEXCEPT +{ + if (stopped(ec)) return false; + + hash_digest begin_hash{}; + if (!decode_hash(begin_hash, beginblock)) + { + send_error(error::not_found, two * beginblock.size()); + return true; + } + + const auto& query = archive(); + if (query.to_header(begin_hash).is_terminal()) + { + send_error(error::not_found, two * beginblock.size()); + return true; + } + + const auto has_addresses = std::holds_alternative( + addresses.value()) && !std::get(addresses.value()).empty(); + const auto has_outpoints = std::holds_alternative( + outpoints.value()) && !std::get(outpoints.value()).empty(); + + if (has_addresses || has_outpoints) + { + send_error(error::not_implemented); + return true; + } + + const auto top = query.get_top_confirmed(); + const auto confirmed = query.to_confirmed(top); + const auto header = query.get_header(confirmed); + if (!header) + { + send_error(error::not_found); + return true; + } + + send_result({}, 4); + send_notification("rescanfinished", array_t + { + encode_hash(header->hash()), + top, + header->timestamp() + }, 256); + return true; +} + +// Chase events. +// ---------------------------------------------------------------------------- + +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. + if (stopped()) + return false; + + switch (event_) + { + case node::chase::organized: + { + 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(relaxed)) + { + BC_ASSERT(std::holds_alternative(value)); + POST_NOTIFY(do_disconnected, std::get(value)); + } + break; + } + default: + { + break; + } + } + + return !stopped(); +} + +// Senders. +// ---------------------------------------------------------------------------- + +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); + rpc::request notification{ { .size_hint = size_hint } }; + notification.message.jsonrpc = version::v1; + notification.message.method = method; + notification.message.params = params_t{ std::move(params) }; + + http::response message{ status::ok, 11 }; + message.set(field::content_type, json); + message.body() = std::move(notification); + message.prepare_payload(); + + // The notification strand poster shadows notify. + this->network::protocol_http::template notify(std::move(message), + &CLASS::handle_complete, _1, error::success); +} + +BC_POP_WARNING() +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/btcd/protocol_btcd_filter.cpp b/src/protocols/btcd/protocol_btcd_filter.cpp new file mode 100644 index 00000000..f9da6056 --- /dev/null +++ b/src/protocols/btcd/protocol_btcd_filter.cpp @@ -0,0 +1,485 @@ +/** + * 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 +#include +#include +#include +#include + +namespace libbitcoin { +namespace server { + +#define CLASS protocol_btcd + +// 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; +constexpr auto relaxed = std::memory_order_relaxed; + +BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) +BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) +BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) + +// Handlers (filters). +// ---------------------------------------------------------------------------- + +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 +{ + if (stopped(ec)) + return false; + + hashes keys{}; + if (const auto fault = btcd::filter_keys(keys, addresses, p2kh_, p2sh_, + witness_)) + { + send_error(fault); + return true; + } + + chain::points points{}; + if (const auto fault = btcd::filter_points(points, outpoints)) + { + send_error(fault); + return true; + } + + 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::do_load_tx_filter(bool reload, const hashes& keys, + const chain::points& points) NOEXCEPT +{ + BC_ASSERT(notification_strand_.running_in_this_thread()); + + if (reload) + { + address_watches_.clear(); + outpoint_watches_.clear(); + } + + histories discard{}; + 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 (ceilinged_add(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) + { + 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 (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{}); + 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::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::handle_rescan_blocks(const code& ec, + btcd_interface::rescan_blocks, const value_t& blockhashes) NOEXCEPT +{ + if (stopped(ec)) + return false; + + if (!std::holds_alternative(blockhashes.value())) + { + send_error(error::invalid_argument); + return true; + } + + hash_digest hash{}; + hashes block_hashes{}; + for (const auto& item: std::get(blockhashes.value())) + { + if (!std::holds_alternative(item.value()) || + !decode_hash(hash, std::get(item.value()))) + { + send_error(error::invalid_argument); + return true; + } + + block_hashes.push_back(hash); + } + + if (!archive().address_enabled()) + { + send_error(error::not_implemented); + return true; + } + + monitor(true); + 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_ptr& block_hashes) NOEXCEPT +{ + BC_ASSERT(notification_strand_.running_in_this_thread()); + + hashes keys{}; + keys.reserve(address_watches_.size()); + for (const auto& watch: address_watches_) + keys.push_back(watch.first); + + chain::points points{}; + points.reserve(outpoint_watches_.size()); + for (const auto& watch: outpoint_watches_) + points.push_back(watch.first); + + PARALLEL(do_rescan_watches, block_hashes, std::move(keys), + std::move(points)); +} + +void protocol_btcd::do_rescan_watches(const hashes_ptr& block_hashes, + const hashes& keys, chain::points& points) NOEXCEPT +{ + BC_ASSERT(!stranded()); + + sizes heights{}; + const auto& query = archive(); + std::vector> named{}; + + // Resolve named blocks to heights (result retains request order). + for (auto& hash: *block_hashes) + { + if (stopping_) + return; + + size_t height{}; + if (!query.get_height(height, query.to_header(hash))) + { + POST_BTCD(complete_rescan_blocks, error::not_found, + to_shared()); + return; + } + + heights.insert(height); + named.emplace_back(std::move(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(object_t + { + { "hash", encode_hash(hash) }, + { "transactions", serialize_matches(at->second) } + }); + } + } + + POST_BTCD(complete_rescan_blocks, error::success, + emplace_shared(std::move(discovered))); +} + +void protocol_btcd::complete_rescan_blocks(const code& ec, + const array_ptr& discovered) NOEXCEPT +{ + BC_ASSERT(stranded()); + + monitor(false); + if (stopped()) + return; + + if (ec) + { + send_error(ec); + return; + } + + send_result(std::move(*discovered), 256); +} + +// Notification event handlers. +// ---------------------------------------------------------------------------- + +void protocol_btcd::do_connected(node::header_t link_value) NOEXCEPT +{ + BC_ASSERT(notification_strand_.running_in_this_thread()); + + const auto& query = archive(); + const database::header_link link{ link_value }; + + 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 sizes 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, header, height, + emplace_shared(std::move(txs))); +} + +void protocol_btcd::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, header, height); +} + +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(relaxed)) + return; + + // 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); +} + +void protocol_btcd::notify_disconnected(const header_cptr& header, + size_t height) NOEXCEPT +{ + BC_ASSERT(stranded()); + + if (stopped() || !subscribed_blocks_.load(relaxed)) + return; + + 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); + + 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). +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; + if (const auto ec = query.get_history(stopping_, sub.cursor, delta, key, + limit, turbo_); 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; +} + +// 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 +{ + 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::serialize_matches(const matched_txs& txs) NOEXCEPT +{ + array_t out{}; + constexpr auto witness = true; + const auto& query = archive(); + for (const auto& [position, hash]: txs) + { + if (stopping_) + return out; + + 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; +} + +BC_POP_WARNING() +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin diff --git a/src/protocols/btcd/protocol_btcd_rpc.cpp b/src/protocols/btcd/protocol_btcd_rpc.cpp deleted file mode 100644 index 04f30fed..00000000 --- a/src/protocols/btcd/protocol_btcd_rpc.cpp +++ /dev/null @@ -1,653 +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 . - */ -#include - -#include -#include -#include - -namespace libbitcoin { -namespace server { - -#define CLASS protocol_btcd_rpc -#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; -using namespace std::placeholders; -using namespace boost::json; - -BC_PUSH_WARNING(NO_THROW_IN_NOEXCEPT) -BC_PUSH_WARNING(SMART_PTR_NOT_NEEDED) -BC_PUSH_WARNING(NO_VALUE_OR_CONST_REF_SHARED_PTR) - -// Start. -// ---------------------------------------------------------------------------- - -void protocol_btcd_rpc::start() NOEXCEPT -{ - BC_ASSERT(stranded()); - - if (started()) - return; - - subscribe_chase(BIND(handle_chase, _1, _2, _3)); - - SUBSCRIBE_BTCD(handle_authenticate, _1, _2, _3, _4); - SUBSCRIBE_BTCD(handle_session, _1, _2); - SUBSCRIBE_BTCD(handle_get_current_net, _1, _2); - SUBSCRIBE_BTCD(handle_get_best_block, _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); - 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); - 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); - 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); - 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); - 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. - protocol_bitcoind_rpc::start(); -} - -void protocol_btcd_rpc::stopping(const code& ec) NOEXCEPT -{ - BC_ASSERT(stranded()); - btcd_dispatcher_.stop(ec); - unsubscribe_chase(); - protocol_bitcoind_rpc::stopping(ec); -} - -// 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. - -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()) - { - send_btcd_error(error::invalid_argument); - return; - } - - const auto& body = request.body().get(); - const auto& message = body.message; - - // 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)) - { - send_btcd_error(network::error::unauthorized); - 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. - auto code = btcd_dispatcher_.notify(message); - if (code == network::error::unexpected_method) - code = dispatch_rpc(message); - - 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). - if (code == network::error::unexpected_method) - send_btcd_error(code); - else - stop(code); -} - -// 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). -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 (session/handshake). -// ---------------------------------------------------------------------------- - -bool protocol_btcd_rpc::handle_authenticate(const code& ec, - btcd_interface::authenticate, const std::string& username, - const std::string& password) NOEXCEPT -{ - 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). - 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)) - { - btcd_channel_->set_authenticated(digest); - send_btcd_result({}, 4); - 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. - const code unauthorized{ network::error::unauthorized }; - send_btcd_error(unauthorized, two * unauthorized.message().size(), - unauthorized); - return true; -} - -bool protocol_btcd_rpc::handle_session(const code& ec, - btcd_interface::session) NOEXCEPT -{ - if (stopped(ec)) - return false; - - // The channel identifier is unique per connection for the process - // lifetime, satisfying btcd'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); - return true; -} - -bool protocol_btcd_rpc::handle_get_current_net(const code& ec, - btcd_interface::get_current_net) NOEXCEPT -{ - 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. - send_btcd_result( - value_t{ possible_sign_cast(network_settings().identifier) }, - 20); - return true; -} - -bool protocol_btcd_rpc::handle_get_best_block(const code& ec, - btcd_interface::get_best_block) NOEXCEPT -{ - 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. - 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_btcd_result(value_t{ std::move(result) }, 96); - return true; -} - -// Handlers (block subscription). -// ---------------------------------------------------------------------------- - -bool protocol_btcd_rpc::handle_notify_blocks(const code& ec, - btcd_interface::notify_blocks) NOEXCEPT -{ - if (stopped(ec)) - return false; - - subscribed_blocks_.store(true, std::memory_order_relaxed); - send_btcd_result({}, 4); - return true; -} - -bool protocol_btcd_rpc::handle_stop_notify_blocks(const code& ec, - btcd_interface::stop_notify_blocks) NOEXCEPT -{ - if (stopped(ec)) - return false; - - subscribed_blocks_.store(false, std::memory_order_relaxed); - send_btcd_result({}, 4); - return true; -} - -// Handlers (mempool subscription, not_implemented pending v5 mempool). -// ---------------------------------------------------------------------------- - -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); - return true; -} - -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); - return true; -} - -// Handlers (address/outpoint filtering): see protocol_btcd_rpc_filter.cpp. -// ---------------------------------------------------------------------------- - -// Handler (admin, permanently not_implemented). -// ---------------------------------------------------------------------------- - -bool protocol_btcd_rpc::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_btcd_error(error::not_implemented); - return true; -} - -// Handlers (deprecated, permanently not_implemented). -// ---------------------------------------------------------------------------- - -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); - return true; -} - -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); - return true; -} - -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); - return true; -} - -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); - return true; -} - -bool protocol_btcd_rpc::handle_rescan(const code& ec, - btcd_interface::rescan, const std::string& beginblock, - const value_t& addresses, const value_t& outpoints, - const std::string&) NOEXCEPT -{ - 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). - hash_digest begin_hash{}; - if (!decode_hash(begin_hash, beginblock)) - { - send_btcd_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()); - return true; - } - - const auto has_addresses = std::holds_alternative( - addresses.value()) && !std::get(addresses.value()).empty(); - const auto has_outpoints = std::holds_alternative( - outpoints.value()) && !std::get(outpoints.value()).empty(); - - if (has_addresses || has_outpoints) - { - send_btcd_error(error::not_implemented); - return true; - } - - // 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). - const auto top = query.get_top_confirmed(); - const auto header = query.get_header(query.to_confirmed(top)); - if (!header) - { - send_btcd_error(error::not_found); - 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. - send_btcd_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); - return true; -} - -// Chase events. -// ---------------------------------------------------------------------------- - -bool protocol_btcd_rpc::handle_chase(const code& ec, node::chase event_, - node::event_value value) NOEXCEPT -{ - // Do not pass ec to stopped, it is not a call status. - if (stopped()) - return false; - - switch (event_) - { - case node::chase::organized: - if (subscribed_blocks_.load(std::memory_order_relaxed)) - { - BC_ASSERT(std::holds_alternative(value)); - POST_BTCD(do_block_connected, std::get(value)); - } - break; - - case node::chase::reorganized: - if (subscribed_blocks_.load(std::memory_order_relaxed)) - { - BC_ASSERT(std::holds_alternative(value)); - POST_BTCD(do_block_disconnected, std::get(value)); - } - break; - - default: - break; - } - - 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] (positional, - // matching btcjson.BlockConnectedNtfn -- the unfiltered notification - // fired by notifyblocks, distinct from loadtxfilter's filtered variant). - 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_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- - // 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_btcd_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_btcd_notification("blockdisconnected", std::move(params), 256); - - // btcd 'filteredblockdisconnected': [height, header] (no subscribedtxs - // field -- matching btcjson.FilteredBlockDisconnectedNtfn). - 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_btcd_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, - 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; - notification.message.params = params_t{ std::move(params) }; - - http::response message{ status::ok, 11 }; - message.set(field::content_type, json); - 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(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 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. - SEND(std::move(message), handle_complete, _1, close_reason); -} - -BC_POP_WARNING() -BC_POP_WARNING() -BC_POP_WARNING() - -} // namespace server -} // namespace libbitcoin diff --git a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp b/src/protocols/btcd/protocol_btcd_rpc_filter.cpp deleted file mode 100644 index b5d78a5e..00000000 --- a/src/protocols/btcd/protocol_btcd_rpc_filter.cpp +++ /dev/null @@ -1,332 +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 . - */ -#include - -#include -#include -#include - -namespace libbitcoin { -namespace server { - -#define CLASS protocol_btcd_rpc - -using namespace system; -using namespace network; -using namespace network::rpc; - -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). -// ---------------------------------------------------------------------------- -// 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 -{ - 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 (!std::holds_alternative(item.value())) - return error::invalid_argument; - - const auto& text = std::get(item.value()); - - // 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; - } - } - - return error::success; -} - -code protocol_btcd_rpc::parse_filter_outpoints(bool reload, - const value_t& outpoints) NOEXCEPT -{ - BC_ASSERT(stranded()); - - if (reload) - filter_outpoints_.clear(); - - 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; - - hash_digest hash{}; - uint32_t index{}; - if (!decode_hash(hash, std::get(hash_it->second.value())) || - !to_integer(index, std::get(index_it->second.value()))) - return error::invalid_argument; - - filter_outpoints_.emplace(hash, index); - } - - return error::success; -} - -// Handlers (address/outpoint filtering). -// ---------------------------------------------------------------------------- - -bool protocol_btcd_rpc::handle_load_tx_filter(const code& ec, - btcd_interface::load_tx_filter, bool reload, const value_t& addresses, - const value_t& outpoints) NOEXCEPT -{ - if (stopped(ec)) - return false; - - if (const auto fault = parse_filter_addresses(reload, addresses)) - { - send_btcd_error(fault); - return true; - } - - if (const auto fault = parse_filter_outpoints(reload, outpoints)) - { - send_btcd_error(fault); - return true; - } - - send_btcd_result({}, 4); - return true; -} - -bool protocol_btcd_rpc::handle_rescan_blocks(const code& ec, - btcd_interface::rescan_blocks, const value_t& blockhashes) NOEXCEPT -{ - if (stopped(ec)) - return false; - - if (!std::holds_alternative(blockhashes.value())) - { - send_btcd_error(error::invalid_argument); - return true; - } - - constexpr auto witness = true; - const auto& query = archive(); - array_t discovered{}; - - for (const auto& item: std::get(blockhashes.value())) - { - if (!std::holds_alternative(item.value())) - { - send_btcd_error(error::invalid_argument); - return true; - } - - const auto& text = std::get(item.value()); - hash_digest hash{}; - if (!decode_hash(hash, text)) - { - send_btcd_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); - return true; - } - - auto matched = match_filtered_transactions(*block); - if (!matched.empty()) - { - discovered.emplace_back(value_t{ object_t - { - { "hash", value_t{ text } }, - { "transactions", value_t{ std::move(matched) } } - } }); - } - } - - send_btcd_result(value_t{ std::move(discovered) }, 256); - return true; -} - -// 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. - -array_t protocol_btcd_rpc::match_filtered_transactions( - const chain::block& block) NOEXCEPT -{ - BC_ASSERT(stranded()); - constexpr auto witness = true; - - array_t matched{}; - for (const auto& tx: *block.transactions_ptr()) - { - auto hit = false; - - for (const auto& input: *tx->inputs_ptr()) - if (filter_outpoints_.contains(input->point())) - hit = true; - - 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); - - 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 (hit) - matched.emplace_back(value_t{ to_text(*tx, - tx->serialized_size(witness), witness) }); - } - - return matched; -} - -BC_POP_WARNING() -BC_POP_WARNING() -BC_POP_WARNING() - -} // namespace server -} // namespace libbitcoin diff --git a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp b/src/protocols/btcd/protocol_btcd_rpc_utility.cpp deleted file mode 100644 index 548b8013..00000000 --- a/src/protocols/btcd/protocol_btcd_rpc_utility.cpp +++ /dev/null @@ -1,413 +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 . - */ -#include - -#include -#include -#include -#include - -namespace libbitcoin { -namespace server { - -#define CLASS protocol_btcd_rpc - -using namespace system; -using namespace network; -using namespace network::rpc; -using namespace boost::json; - -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). -// ---------------------------------------------------------------------------- - -bool protocol_btcd_rpc::handle_get_difficulty(const code& ec, - btcd_interface::get_difficulty) NOEXCEPT -{ - if (stopped(ec)) - return false; - - const auto& query = archive(); - const auto header = query.get_header(query.to_confirmed( - query.get_top_confirmed())); - if (!header) - { - send_btcd_error(database::error::integrity); - return true; - } - - send_btcd_result(header->difficulty(), 20); - return true; -} - -bool protocol_btcd_rpc::handle_get_info(const code& ec, - btcd_interface::get_info) NOEXCEPT -{ - if (stopped(ec)) - return false; - - const auto& query = archive(); - const auto top = query.get_top_confirmed(); - const auto header = query.get_header(query.to_confirmed(top)); - if (!header) - { - send_btcd_error(database::error::integrity); - 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. - send_btcd_result(object_t - { - { "version", 0 }, - { "protocolversion", 70016 }, - { "blocks", possible_sign_cast(top) }, - { "timeoffset", 0 }, - { "connections", 0 }, - { "proxy", std::string{} }, - { "difficulty", header->difficulty() }, - { "testnet", chain_name(query) != "main" }, - { "relayfee", 0.00001 }, - { "errors", std::string{} } - }, 256); - return true; -} - -bool protocol_btcd_rpc::handle_get_net_totals(const code& ec, - btcd_interface::get_net_totals) NOEXCEPT -{ - 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). - send_btcd_result(object_t - { - { "totalbytesrecv", 0 }, - { "totalbytessent", 0 }, - { "timemillis", possible_sign_cast(zulu_time()) * 1000 } - }, 64); - return true; -} - -bool protocol_btcd_rpc::handle_get_network_hash_ps(const code& ec, - btcd_interface::get_network_hash_ps, uint32_t, int32_t height) NOEXCEPT -{ - if (stopped(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. - 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 header = query.get_header(query.to_confirmed(target)); - if (!header) - { - send_btcd_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( - header->difficulty() * two_to_the_32 / average_seconds_per_block, 20); - return true; -} - -// 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 -{ - 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_rpc::handle_create_raw_transaction(const code& ec, - btcd_interface::create_raw_transaction, const array_t& inputs, - const object_t& outputs, uint32_t locktime) NOEXCEPT -{ - if (stopped(ec)) - return false; - - chain::inputs ins{}; - ins.reserve(inputs.size()); - for (const auto& item: inputs) - { - if (!std::holds_alternative(item.value())) - { - send_btcd_error(error::invalid_argument); - return true; - } - - const auto& fields = std::get(item.value()); - const auto txid_it = fields.find("txid"); - const auto vout_it = fields.find("vout"); - if (txid_it == fields.end() || vout_it == fields.end() || - !std::holds_alternative(txid_it->second.value()) || - !std::holds_alternative(vout_it->second.value())) - { - send_btcd_error(error::invalid_argument); - 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()))) - { - send_btcd_error(error::invalid_argument); - return true; - } - - ins.emplace_back(chain::point{ hash, vout }, chain::script{}, - 0xffffffff_u32); - } - - chain::outputs outs{}; - outs.reserve(outputs.size()); - for (const auto& pair: outputs) - { - if (!std::holds_alternative(pair.second.value())) - { - send_btcd_error(error::invalid_argument); - return true; - } - - chain::script script{}; - if (const auto fault = parse_output_script(pair.first, script)) - { - send_btcd_error(fault); - return true; - } - - const auto btc = std::get(pair.second.value()); - const auto satoshi = static_cast( - std::llround(btc * chain::satoshi_per_bitcoin)); - outs.emplace_back(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; - send_btcd_result(to_text(tx, tx.serialized_size(witness), witness), 400); - return true; -} - -bool protocol_btcd_rpc::handle_decode_raw_transaction(const code& ec, - btcd_interface::decode_raw_transaction, - const std::string& hexstring) NOEXCEPT -{ - if (stopped(ec)) - return false; - - data_chunk data{}; - if (!decode_base16(data, hexstring)) - { - send_btcd_error(error::invalid_argument); - return true; - } - - constexpr auto witness = true; - const chain::transaction tx{ data, witness }; - if (!tx.is_valid()) - { - send_btcd_error(error::invalid_argument); - 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. - send_btcd_result(value_from(bitcoind(tx)), - two * tx.serialized_size(witness)); - return true; -} - -bool protocol_btcd_rpc::handle_decode_script(const code& ec, - btcd_interface::decode_script, const std::string& hex) NOEXCEPT -{ - if (stopped(ec)) - return false; - - data_chunk data{}; - if (!decode_base16(data, hex)) - { - send_btcd_error(error::invalid_argument); - return true; - } - - // false: raw script bytes, no serialized length prefix. - const chain::script script{ data, false }; - if (!script.is_valid()) - { - send_btcd_error(error::invalid_argument); - return true; - } - - using namespace chain; - object_t result{}; - result.emplace("asm", value_t{ script.to_string(flags::all_rules, true) }); - - std::string kind{ "nonstandard" }; - switch (script.output_pattern()) - { - 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; - } - result.emplace("type", value_t{ 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", 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. - const wallet::payment_address wrapped{ script, p2sh_ }; - if (wrapped) - result.emplace("p2sh", value_t{ wrapped.encoded() }); - - send_btcd_result(value_t{ std::move(result) }, 256); - return true; -} - -bool protocol_btcd_rpc::handle_validate_address(const code& ec, - btcd_interface::validate_address, const std::string& address) NOEXCEPT -{ - if (stopped(ec)) - return false; - - const wallet::payment_address base58(address); - if (base58) - { - send_btcd_result(object_t - { - { "isvalid", true }, - { "address", value_t{ base58.encoded() } }, - { "isscript", base58.prefix() == p2sh_ }, - { "iswitness", false } - }, 128); - return true; - } - - const wallet::witness_address segwit(address); - if (segwit) - { - send_btcd_result(object_t - { - { "isvalid", true }, - { "address", value_t{ 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()) } } - }, 128); - return true; - } - - send_btcd_result(object_t{ { "isvalid", false } }, 32); - return true; -} - -bool protocol_btcd_rpc::handle_help(const code& ec, btcd_interface::help, - const std::string&) NOEXCEPT -{ - 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. - send_btcd_result(std::string{ - "authenticate session getcurrentnet getbestblock notifyblocks " - "stopnotifyblocks loadtxfilter rescanblocks getdifficulty getinfo " - "getnettotals getnetworkhashps createrawtransaction " - "decoderawtransaction decodescript validateaddress help stop" }, 256); - return true; -} - -BC_POP_WARNING() -BC_POP_WARNING() -BC_POP_WARNING() - -} // namespace server -} // namespace libbitcoin diff --git a/src/protocols/btcd/protocol_btcd_utility.cpp b/src/protocols/btcd/protocol_btcd_utility.cpp new file mode 100644 index 00000000..db1a2a7e --- /dev/null +++ b/src/protocols/btcd/protocol_btcd_utility.cpp @@ -0,0 +1,397 @@ +/** + * 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 +#include +#include +#include +#include +#include + +namespace libbitcoin { +namespace server { + +#define CLASS protocol_btcd + +using namespace system; +using namespace network; +using namespace network::rpc; +using namespace boost::json; + +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; + + const auto& query = archive(); + 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; + + send_result(network_settings().identifier, 20); + return true; +} + +bool protocol_btcd::handle_get_difficulty(const code& ec, + btcd_interface::get_difficulty) NOEXCEPT +{ + if (stopped(ec)) + return false; + + const auto& query = archive(); + const auto top = query.get_top_confirmed(); + const auto header = query.get_header(query.to_confirmed(top)); + if (!header) + { + send_error(database::error::integrity); + return true; + } + + send_result(header->difficulty(), 20); + return true; +} + +bool protocol_btcd::handle_get_info(const code& ec, + btcd_interface::get_info) NOEXCEPT +{ + if (stopped(ec)) + return false; + + const auto& query = archive(); + const auto top = query.get_top_confirmed(); + const auto header = query.get_header(query.to_confirmed(top)); + if (!header) + { + send_error(database::error::integrity); + 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]; + + send_result(object_t + { + { "version", version }, + { "protocolversion", network_settings().protocol_maximum }, + { "blocks", top }, + { "timeoffset", 0 }, + { "connections", 0 }, + { "proxy", std::string{} }, + { "difficulty", header->difficulty() }, + { "testnet", chain_name(query) != "main" }, + { "relayfee", node_settings().minimum_fee_rate }, + { "errors", std::string{} } + }, 256); + return true; +} + +bool protocol_btcd::handle_get_net_totals(const code& ec, + btcd_interface::get_net_totals) NOEXCEPT +{ + if (stopped(ec)) + return false; + + send_result(object_t + { + { "totalbytesrecv", 0 }, + { "totalbytessent", 0 }, + { "timemillis", possible_wide_cast(zulu_time()) * 1'000 } + }, 64); + return true; +} + +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)) + return false; + + const auto& query = archive(); + const auto top = query.get_top_confirmed(); + const auto target = is_negative(height) ? top : + std::min(sign_cast(height), top); + + const auto header = query.get_header(query.to_confirmed(target)); + if (!header) + { + send_error(database::error::integrity); + return true; + } + + const auto period = system_settings().block_spacing_seconds; + const auto span = to_floating(power2(32u)); + send_result(header->difficulty() * span / period, 20); + return true; +} + +// Handlers (tools). +// ---------------------------------------------------------------------------- + +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 +{ + if (stopped(ec)) + return false; + + 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())) + { + send_error(error::invalid_argument); + return true; + } + + const auto& fields = std::get(item.value()); + const auto txid_it = fields.find("txid"); + const auto vout_it = fields.find("vout"); + if (txid_it == fields.end() || vout_it == fields.end() || + !std::holds_alternative(txid_it->second.value()) || + !std::holds_alternative(vout_it->second.value())) + { + send_error(error::invalid_argument); + return true; + } + + if (!decode_hash(hash, std::get(txid_it->second.value())) || + !to_integer(vout, std::get(vout_it->second.value()))) + { + send_error(error::invalid_argument); + return true; + } + + ins->push_back(to_shared(point{ hash, vout }, script{}, + max_input_sequence)); + } + + 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())) + { + send_error(error::invalid_argument); + return true; + } + + if (const auto fault = btcd::output_script(script, pair.first, p2kh_, + p2sh_, witness_)) + { + send_error(fault); + return true; + } + + const auto btc = std::get(pair.second.value()); + if (!to_integer(satoshi, btc * satoshi_per_bitcoin, false)) + { + send_error(error::invalid_argument); + return true; + } + + outs->push_back(to_shared(satoshi, std::move(script))); + } + + constexpr auto witness = false; + const transaction tx{ 1, ins, outs, locktime }; + send_result(to_text(tx, tx.serialized_size(witness), witness), 400); + return true; +} + +bool protocol_btcd::handle_decode_raw_transaction(const code& ec, + btcd_interface::decode_raw_transaction, + const std::string& hexstring) NOEXCEPT +{ + if (stopped(ec)) + return false; + + data_chunk data{}; + if (!decode_base16(data, hexstring)) + { + send_error(error::invalid_argument); + return true; + } + + constexpr auto witness = true; + const chain::transaction tx{ data, witness }; + if (!tx.is_valid()) + { + send_error(error::invalid_argument); + return true; + } + + send_result(value_from(bitcoind(tx)), two * tx.serialized_size(witness)); + return true; +} + +bool protocol_btcd::handle_decode_script(const code& ec, + btcd_interface::decode_script, const std::string& hex) NOEXCEPT +{ + if (stopped(ec)) + return false; + + data_chunk data{}; + if (!decode_base16(data, hex)) + { + send_error(error::invalid_argument); + return true; + } + + 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 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)); + + if (pattern == script_pattern::pay_key_hash || + pattern == script_pattern::pay_script_hash) + { + const auto pay = payment_address::extract_output(script, p2kh_, p2sh_); + if (pay) + result.emplace("address", pay.encoded()); + } + + const payment_address pay{ script, p2sh_ }; + if (pay) + result.emplace("p2sh", pay.encoded()); + + send_result(std::move(result), 256); + return true; +} + +bool protocol_btcd::handle_validate_address(const code& ec, + btcd_interface::validate_address, const std::string& address) NOEXCEPT +{ + if (stopped(ec)) + return false; + + using namespace wallet; + const payment_address base58(address); + if (base58) + { + send_result(object_t + { + { "isvalid", true }, + { "address", base58.encoded() }, + { "isscript", base58.prefix() == p2sh_ }, + { "iswitness", false } + }, 128); + return true; + } + + 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", version0_p2sh }, + { "iswitness", true }, + { "witness_version", witness.version() }, + { "witness_program", encode_base16(witness.program()) } + }, 128); + return true; + } + + send_result(object_t{ { "isvalid", false } }, 32); + return true; +} + +BC_POP_WARNING() +BC_POP_WARNING() +BC_POP_WARNING() + +} // namespace server +} // namespace libbitcoin 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()) { 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 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"); diff --git a/test/protocols/bitcoind/bitcoind_rpc.cpp b/test/protocols/bitcoind/bitcoind_rpc.cpp index 61850ee8..c1b5f3cc 100644 --- a/test/protocols/bitcoind/bitcoind_rpc.cpp +++ b/test/protocols/bitcoind/bitcoind_rpc.cpp @@ -126,17 +126,14 @@ 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. 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) @@ -202,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) @@ -309,9 +306,67 @@ BOOST_AUTO_TEST_CASE(bitcoind_rpc__batch__empty__dropped) REQUIRE_NO_THROW_TRUE(response.at("dropped").as_bool()); } +// websocket +// ---------------------------------------------------------------------------- + +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) +// witness // ---------------------------------------------------------------------------- BOOST_FIXTURE_TEST_SUITE(bitcoind_witness_tests, bitcoind_witness_setup_fixture) 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 diff --git a/test/protocols/btcd/btcd_rpc.cpp b/test/protocols/btcd/btcd_rpc.cpp index 548871a2..082879a1 100644 --- a/test/protocols/btcd/btcd_rpc.cpp +++ b/test/protocols/btcd/btcd_rpc.cpp @@ -20,292 +20,311 @@ #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 subscription_limit{ server::error::subscription_limit }; +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 other_address{ "1JqDybm2nWTENrHvMyafbSXXtTk5Uv5QAn" }; +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 +// administrative // ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__no_auth_configured__null_result) +BOOST_AUTO_TEST_CASE(btcd_rpc__authenticate__no_credential_configured__unauthorized) +{ + // 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__authenticate__http_post__unexpected_method) { - // Without basic auth configured, authenticate is a no-op success. - const auto response = rpc("authenticate", R"(["user","pass"])"); - BOOST_REQUIRE(!has_error(response)); + // 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__session__returns_id) +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"); - 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__stop__default__not_implemented) { - // 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. - 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_REQUIRE_EQUAL(rpc_error("stop"), not_implemented.value()); } +// getters +// ---------------------------------------------------------------------------- + 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)); + 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__getcurrentnet__mainnet__magic) { - // 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. - 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")); + // The fixture configures mainnet, magic 3652501241 (0xd9b4bef9). + const auto response = rpc("getcurrentnet"); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 3652501241); } -// block subscription -// ---------------------------------------------------------------------------- +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__notifyblocks__returns_null_result) +BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__ten_block_store__nine) { - const auto response = rpc("notifyblocks"); - BOOST_REQUIRE(!has_error(response)); + 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__stopnotifyblocks__returns_null_result) +BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__http_post__dispatched) { - // Subscribe first so stop is meaningful. - rpc("notifyblocks"); - const auto response = rpc("stopnotifyblocks"); - BOOST_REQUIRE(!has_error(response)); + // 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")); } -// 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__getnettotals__untracked_counters__zero) +{ + const auto response = rpc("getnettotals"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_object()); -BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__ten_block_store__nine) + 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) { - // 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)); - BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); + const auto response = rpc("getnetworkhashps"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_double()); } -// Address/outpoint filtering (loadtxfilter, filteredblockconnected/ -// disconnected, rescanblocks). +// tools // ---------------------------------------------------------------------------- -// 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. -namespace { +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); +} -const short_hash& filter_test_hash() NOEXCEPT +BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__block1_coinbase__expected_txid) { - static const short_hash hash{ base16_array( - "0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a0a") }; - return hash; + 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))); } -const wallet::payment_address& filter_test_address() NOEXCEPT +BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__malformed_hex__invalid_argument) { - static const wallet::payment_address address{ filter_test_hash() }; - return address; + const auto result = rpc_error("decoderawtransaction", R"(["not-hex"])"); + BOOST_REQUIRE_EQUAL(result, invalid_argument.value()); } -const std::string& filter_test_address_text() NOEXCEPT +BOOST_AUTO_TEST_CASE(btcd_rpc__decodescript__pay_public_key__pubkey) { - static const std::string text{ filter_test_address().encoded() }; - return text; + // 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")); } -const chain::block& block10() NOEXCEPT +BOOST_AUTO_TEST_CASE(btcd_rpc__validateaddress__valid_address__isvalid_true) { - 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 - }; + 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()); - // 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. - return chain::block - { - chain::header - { - 1, test::block9_hash, null_hash, - test::block9.header().timestamp() + 600, - test::block9.header().bits(), 0 - }, - chain::transactions{ coinbase } - }; - }() - }; + 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); +} - return instance; +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); } -} // namespace +// subscription +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(btcd_rpc__notifyblocks__default__null_result) +{ + const auto response = rpc("notifyblocks"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} -BOOST_AUTO_TEST_CASE(btcd_rpc__loadtxfilter__valid_address__acknowledges) +BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyblocks__subscribed__null_result) { - const auto response = rpc("loadtxfilter", - "[true,[\"" + filter_test_address_text() + "\"],[]]"); - BOOST_REQUIRE(!has_error(response)); + // Subscribe first so stop is meaningful. + const auto subscribed = rpc("notifyblocks"); + REQUIRE_NO_THROW_TRUE(subscribed.at("result").is_null()); + + const auto response = rpc("stopnotifyblocks"); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); +} + +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()); +} + +// 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. + +BOOST_AUTO_TEST_CASE(btcd_rpc__loadtxfilter__valid_address__null_result) +{ + 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); @@ -313,286 +332,206 @@ BOOST_AUTO_TEST_CASE(btcd_rpc__filteredblockconnected__address_match__delivered) BOOST_REQUIRE_EQUAL(params[2].as_array().size(), 1u); } -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)); -} - -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)); - - const auto finished = receive_notification(); - BOOST_REQUIRE_EQUAL(finished.at("method").as_string(), "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(params[1].as_int64(), 9); -} +// deprecated +// ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__with_addresses__not_implemented) +BOOST_AUTO_TEST_CASE(btcd_rpc__notifyreceived__default__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)); + BOOST_REQUIRE_EQUAL(rpc_error("notifyreceived", "[[]]"), not_implemented.value()); } -// Generic btcd-tooling compatibility (no lnd consumer) -// ---------------------------------------------------------------------------- - -BOOST_AUTO_TEST_CASE(btcd_rpc__getdifficulty__returns_number) +BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyreceived__default__not_implemented) { - const auto response = rpc("getdifficulty"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - BOOST_REQUIRE(response.at("result").is_double()); + BOOST_REQUIRE_EQUAL(rpc_error("stopnotifyreceived", "[[]]"), not_implemented.value()); } -BOOST_AUTO_TEST_CASE(btcd_rpc__getinfo__ten_block_store__nine) +BOOST_AUTO_TEST_CASE(btcd_rpc__notifyspent__default__not_implemented) { - const auto response = rpc("getinfo"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - 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_REQUIRE_EQUAL(rpc_error("notifyspent", "[[]]"), not_implemented.value()); } -BOOST_AUTO_TEST_CASE(btcd_rpc__getnettotals__returns_object) +BOOST_AUTO_TEST_CASE(btcd_rpc__stopnotifyspent__default__not_implemented) { - const auto response = rpc("getnettotals"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - const auto& result = response.at("result").as_object(); - BOOST_REQUIRE(result.contains("totalbytesrecv")); - BOOST_REQUIRE(result.contains("totalbytessent")); - BOOST_REQUIRE(result.at("timemillis").as_int64() > 0); + BOOST_REQUIRE_EQUAL(rpc_error("stopnotifyspent", "[[]]"), not_implemented.value()); } -BOOST_AUTO_TEST_CASE(btcd_rpc__getnetworkhashps__returns_number) +BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__unknown_beginblock__not_found) { - const auto response = rpc("getnetworkhashps"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); - BOOST_REQUIRE(response.at("result").is_double()); + 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__createrawtransaction__well_formed_hex) +BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__no_addresses_or_outpoints__rescan_finished) { - 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 response = rpc("rescan", (boost_format(R"(["%1%",[],[],""])") % block9).str()); + REQUIRE_NO_THROW_TRUE(response.at("result").is_null()); - data_chunk data{}; - BOOST_REQUIRE(decode_base16(data, response.at("result").as_string())); + const auto finished = receive_notification(); + BOOST_REQUIRE_EQUAL(as_text(finished.at("method")), "rescanfinished"); - 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); + const auto& params = finished.at("params").as_array(); + BOOST_REQUIRE_EQUAL(params.size(), 3u); + BOOST_REQUIRE_EQUAL(as_text(params[0]), block9); + BOOST_REQUIRE_EQUAL(params[1].as_int64(), 9); } -BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__block1_coinbase) +BOOST_AUTO_TEST_CASE(btcd_rpc__rescan__with_addresses__not_implemented) { - const auto& coinbase = *test::block1.transactions_ptr()->front(); - const auto hex = encode_base16(coinbase.to_data(false)); + 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()); +} - const auto response = rpc("decoderawtransaction", "[\"" + hex + "\"]"); - BOOST_REQUIRE(!has_error(response)); - BOOST_REQUIRE(has_result(response)); +// bitcoind interface methods (bridged into the ws dispatcher) +// ---------------------------------------------------------------------------- - const auto& result = response.at("result").as_object(); - BOOST_REQUIRE_EQUAL(result.at("txid").as_string(), - encode_hash(coinbase.hash(false))); +BOOST_AUTO_TEST_CASE(btcd_rpc__getblockcount__ten_block_store__nine) +{ + const auto response = rpc("getblockcount"); + BOOST_REQUIRE_EQUAL(response.at("result").as_int64(), 9); } -BOOST_AUTO_TEST_CASE(btcd_rpc__decoderawtransaction__malformed_hex__invalid_argument) +// response envelope +// ---------------------------------------------------------------------------- + +BOOST_AUTO_TEST_CASE(btcd_rpc__response__sequential_requests__id_matches_request) { - const auto response = rpc("decoderawtransaction", R"(["not-hex"])"); - BOOST_REQUIRE(has_error(response)); + // request_id_ increments each call: first call uses id=0, second id=1. + const auto r0 = rpc("session"); + const auto r1 = rpc("notifyblocks"); + BOOST_REQUIRE_EQUAL(r0.at("id").as_int64(), 0); + BOOST_REQUIRE_EQUAL(r1.at("id").as_int64(), 1); } -BOOST_AUTO_TEST_CASE(btcd_rpc__decodescript__pubkey_script__typed_correctly) +BOOST_AUTO_TEST_CASE(btcd_rpc__unknown_method__default__unexpected_method) { - // 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)); + BOOST_REQUIRE_EQUAL(rpc_error("nosuchmethod"), unexpected_method.value()); - const auto& result = response.at("result").as_object(); - BOOST_REQUIRE_EQUAL(result.at("type").as_string(), "pubkey"); - BOOST_REQUIRE(result.contains("asm")); - BOOST_REQUIRE(result.contains("p2sh")); + // The connection survives an unknown method (as btcd). + const auto follow_up = rpc("session"); + REQUIRE_NO_THROW_TRUE(follow_up.at("result").is_object()); } -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)); +BOOST_AUTO_TEST_SUITE_END() - 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(result.at("iswitness").as_bool(), false); -} +// Filter limit (btcd.maximum_filters): loadtxfilter watches are bounded per +// channel (DoS guard). +// ---------------------------------------------------------------------------- -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); -} +BOOST_FIXTURE_TEST_SUITE(btcd_limited_filter_tests, btcd_limited_filter_setup_fixture) -BOOST_AUTO_TEST_CASE(btcd_rpc__help__returns_method_list) +BOOST_AUTO_TEST_CASE(btcd_limited_filter__loadtxfilter__over_limit__subscription_limit) { - 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); + // 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()); } -// not implemented stubs +BOOST_AUTO_TEST_SUITE_END() + +// Address index requirement: address watching and rescan are index queries, +// not_implemented when the index is disabled. // ---------------------------------------------------------------------------- -BOOST_AUTO_TEST_CASE(btcd_rpc__not_implemented__error) -{ - const std::vector> methods - { - { "stop", "[]" }, - { "notifynewtransactions", "[false]" }, - { "stopnotifynewtransactions", "[]" }, - { "notifyreceived", "[[]]" }, - { "stopnotifyreceived", "[[]]" }, - { "notifyspent", "[[]]" }, - { "stopnotifyspent", "[[]]" } - }; +BOOST_FIXTURE_TEST_SUITE(btcd_no_index_tests, btcd_no_index_setup_fixture) - for (const auto& method: methods) - BOOST_REQUIRE_MESSAGE(has_error(rpc(method.first, method.second)), - method.first); +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()); } -// response envelope -// ---------------------------------------------------------------------------- - -BOOST_AUTO_TEST_CASE(btcd_rpc__response__id_matches_request) +BOOST_AUTO_TEST_CASE(btcd_no_index__loadtxfilter__outpoint_only__null_result) { - // request_id_ increments each call: first call uses id=0, second id=1. - const auto r0 = rpc("session"); - const auto r1 = rpc("notifyblocks"); - BOOST_REQUIRE_EQUAL(r0.at("id").as_int64(), 0); - BOOST_REQUIRE_EQUAL(r1.at("id").as_int64(), 1); + // 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_rpc__unknown_method__error_keeps_connection_alive) +BOOST_AUTO_TEST_CASE(btcd_no_index__rescanblocks__any__not_implemented) { - // 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)); - - const auto follow_up = rpc("session"); - BOOST_REQUIRE(!has_error(follow_up)); + 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_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. +// call, enforced over ws by protocol_btcd::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 +// 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). // ---------------------------------------------------------------------------- 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 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()); +} + +BOOST_AUTO_TEST_CASE(btcd_auth__session__authenticated__permitted) { - const auto auth = rpc("authenticate", - R"([")" BTCD_TEST_USERNAME R"(", ")" BTCD_TEST_PASSWORD R"("])"); - BOOST_REQUIRE(!has_error(auth)); + BOOST_REQUIRE(authenticate()); - // Connection survives a successful handshake; other methods now work. + // 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: 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.cpp b/test/protocols/btcd/btcd_setup_fixture.cpp index 135e1a6b..874f7a5a 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) + bool address_index, const configurator& configure) : config_ { system::chain::selection::mainnet, @@ -40,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; }() @@ -55,19 +57,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 +103,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 +142,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 bc9d080b..ecb3dc85 100644 --- a/test/protocols/btcd/btcd_setup_fixture.hpp +++ b/test/protocols/btcd/btcd_setup_fixture.hpp @@ -23,33 +23,38 @@ #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), 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). explicit btcd_setup_fixture(const initializer& setup, - std::string_view username={}, std::string_view password={}, - std::string_view methods={}); + 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 - // 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) -- 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 = "[]"); @@ -77,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 @@ -93,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 { @@ -106,15 +108,19 @@ 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) + }, true, [](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 +// verify channel_http::permitted() enforces per-method credential scoping // over ws once authenticated. struct btcd_scoped_credential_setup_fixture : btcd_setup_fixture @@ -123,7 +129,45 @@ 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) + }, true, [](configuration& config) + { + config.server.btcd.credentials = + { + { BTCD_TEST_USERNAME ":" BTCD_TEST_PASSWORD ":" + BTCD_TEST_SCOPED_METHOD } + }; + }) + { + } +}; + +// 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) { } };