Reworked clean from the chain-audit playbook into one generic client over
CryptoNoteNetwork (Monero + Wownero share the LWS API). The LWS may be
remote/community-run, so every response is treated as hostile.
Security (closes the audit CRITICAL/HIGH/MEDIUM findings):
- The two `&str` byte-slice panics are GONE: responses are parsed via a
generic post_json -> read_capped -> serde_json::from_slice path. No
byte-indexing of an untrusted body anywhere; the JSON-error-column
slice is deleted.
- Streaming size cap (8 MiB) enforced as bytes arrive (content-length is
attacker-asserted) + shared reqwest client with request/connect
timeouts. No unbounded read, no slowloris.
- Admin key held in a redacting `Secret` (Debug -> "***"); secret-bearing
request structs drop `Debug`; #[instrument] skips view_key.
- Non-success -> generic NodeError tagged with a static endpoint label +
status code only; the upstream body is never interpolated into a log or
error. Transport (connect) errors are logged privately, redacted publicly.
- Amounts are u64 end-to-end via the fail-closed string_or_* deserializers
(garbage/overflow -> Err, never panic/silent-zero); mempool sum and
confirmation math use saturating ops; future-block confirmation -> 0.
- Ring/decoy count clamped (MAX_DECOY_COUNT) + request/parsed fan-out
capped (MAX_PARSED_VEC).
0-conf architecture: default is REST-poll (compute pending on-demand from
get_address_txs, which includes mempool on current monero-lws). HTTP
webhooks are NOT brought over (redundant + fragile per docs/lws-update.md);
ZMQ-push stays an optional, feature-flagged unit for single/own backends
wanting instant confirms. Kept the account-lifecycle calls
(modify_account_status/rescan/account_scan_height) — those are the
deep-sleep machinery REST-poll uses to wake/sleep accounts for scale.
Dropped the /login pending flow + all dead/tip surface.
Adds a shared redacting `Secret` (core::secret, grin reuses) and the
reqwest `stream` feature for bounded reads. types.rs kept (the
string_or_* fail-closed helpers) with secret request structs de-Debug'd
and error-not-panic tests. 15 L2 tests. Adversarially reviewed (7/7
audit findings closed; future-block confirmation hardened).
Electrum/Fulcrum already covers everything the backend needs for Bitcoin
and Litecoin — balance, UTXOs, history, tip, verbose tx, fee estimation,
and broadcast (blockchain.transaction.broadcast). A self-hosted Fulcrum
relays broadcasts to its own backing node, so a separate Bitcoin Core
RPC path buys nothing in the self-host model while adding the
bitcoincore-rpc + bitcoin + secp256k1 + jsonrpc dependency tree and a
duplicate broadcast surface.
- Remove the `bitcoincore-rpc` dependency (and its transitive tree).
- Slim `UtxoConfig` to pure Electrum config: drop use_local_node /
rpc_url / rpc_user / rpc_pass; keep network + electrum_primary +
electrum_fallbacks.
A future LTC-MWEB or direct-node provider is a distinct capability
(MWEB uses a scan-key model, not canonical scriptPubKeys) and would slot
in at the provider seam rather than extending this client — so nothing is
foreclosed by keeping the surface minimal now.
BTC/LTC Electrum/Fulcrum client, reworked clean from the chain-audit
playbook (not ported). Every byte from a server is treated as hostile.
Security:
- TLS verified against the webpki root store WITH hostname checking; the
accept-any-certificate verifier is gone (closes the MITM hole). The
connector is built once and reused.
- Addresses decoded via the `bech32` crate (full checksum + bech32m +
witness version) and base58check (checksum); the hand-rolled bech32
underflow panic is removed. Coin/network enforced (reject ltc-on-btc,
testnet-on-mainnet) — no byte-indexing of untrusted input.
- Amounts pass a fallible checked f64->sat converter (rejects
non-finite/negative/>2^53) with checked_add summation; no silent
saturation to 0 / u64::MAX. estimatefee requires finite, clamps.
- Bounded response read + per-exchange timeout (no OOM, no slowloris).
- JSON-RPC response id correlated with the request.
- Untrusted upstream text is control-stripped + length-capped before it
reaches a (privately-logged) NodeError.
Single `call` primitive centralizes failover + id + envelope mapping;
public methods (balance/utxos/history/tip/verbose-tx/broadcast/fee) are
thin wrappers. Tip-attribution helpers dropped; total_sent_from now
declines (None) without resolved prevouts instead of mislabeling a
receive-with-change as a send.
20 L2 tests: address->script KATs (P2WPKH/P2WSH/taproot/legacy), network
rejection, panic-safety on short/garbage input, checked-sat bounds,
envelope id correlation, bounded/oversized read, duplex round-trip.
- minimal Axum server exposing GET /health
- OpenAPI contract generated from handlers (gen_openapi -> openapi.json),
drift-gated in CI so the spec can never silently diverge from the code
- CI: secret-scan + gitleaks, openapi drift gate, fmt/clippy/build/test/doc-test, cargo-deny
- Cargo.lock tracked for reproducible builds; publish=false; pinned toolchain
- README, SECURITY.md, deny.toml, .env.example