Skip to content

Replace the electrumd dev-dependency in tests/electrum.rs with a pure-Rust wallet (BDK) #224

Description

@EddieHouston

Summary

tests/electrum.rs depends on the electrumd dev-dependency, which downloads and runs the Electrum Python wallet as a subprocess to drive the in-process electrs Electrum RPC server end-to-end. This binary download is gated to Linux x86_64 and does not build on macOS (especially Apple Silicon) without an out-of-tree patch.

This proposes dropping electrumd and replacing the wallet subprocess with BDK (bdk_wallet + bdk_electrum) — a pure-Rust, descriptor-based HD wallet that syncs against an Electrum server. This keeps a genuine independent-wallet end-to-end signal while compiling on every platform with no downloaded binary and no build.rs platform gate.

Background / problem

electrumd's build.rs resolves its download artifact via a download_filename() that is #[cfg(all(target_os = "linux", target_arch = "x86_64"))]-gated. On any other host this is unresolved and produces a hard compile error in the dependency, which blocks the entire test harness — including --lib unit tests, not just the Electrum tests.

The current workaround repoints [patch.crates-io.electrumd] at a community fork that adds a macOS path, then reverts it before committing. That fork still pulls upstream Electrum's x86_64 artifacts (run under Rosetta on Apple Silicon) — upstream Electrum ships no native headless arm64 build — so it is brittle and expected to keep breaking on Apple Silicon.

Proposed approach: BDK as the test wallet

The electrs Electrum server is already started in-process by the test harness (common::init_electrum_tester()ElectrumRPC::start); electrumd only supplies the client wallet pointed at it. Replace that wallet subprocess with BDK in-process.

BDK does the full wallet job against the server — descriptor-based HD derivation, full_scan/sync (driving batched script_get_history + transaction_get + block-header fetches), UTXO/balance computation, coin selection, PSBT signing, and broadcast — so the end-to-end "real wallet against electrs" signal is preserved. It is arguably a stronger signal than the Electrum Python wallet, because BDK is the dominant real-world consumer of electrs/esplora today.

Version alignment

Crate Version Depends on
bdk_electrum 0.24.0 electrum-client ^0.25, bdk_core ^0.6.1
bdk_wallet 1.1.0 bitcoin 0.32.6
electrs bitcoin 0.32.4

Same bitcoin major (0.32) end to end → shared Script / Transaction / Txid, no glue conversions. All pure Rust.

Cargo.toml

[dev-dependencies]
# remove:
#   electrumd = { version = "0.1.0", features = ["4_6_2"] }
bdk_wallet   = "1.1"
bdk_electrum = "0.24"

# remove the entire patch section:
# [patch.crates-io.electrumd]
# git = "https://github.com/shesek/electrumd"

Also remove the now-unused electrumd Error variant and From<electrumd::Error> impl in tests/common.rs.

Sketch

use bdk_wallet::Wallet;
use bdk_electrum::{electrum_client, BdkElectrumClient};

let (_srv, addr, mut t) = common::init_electrum_tester()?;

let mut wallet = Wallet::create(external_desc, internal_desc)
    .network(Network::Regtest)
    .create_wallet_no_persist()?;

let cli = BdkElectrumClient::new(electrum_client::Client::new(&format!("tcp://{addr}"))?);

// fund a BDK-derived address via bitcoind, mine, then sync:
let wallet_addr = wallet.next_unused_address(KeychainKind::External);
t.send(&wallet_addr.address, "0.1 BTC".parse()?)?;
t.mine()?;

let update = cli.full_scan(wallet.start_full_scan(), 5, 1, false)?;
wallet.apply_update(update)?;
assert_eq!(wallet.balance().confirmed.to_sat(), 10_000_000);

// spend: build + sign in BDK, broadcast through electrs:
let mut psbt = wallet.build_tx().add_recipient(dest_spk, Amount::from_sat(amount)).finish()?;
wallet.sign(&mut psbt, Default::default())?;
cli.transaction_broadcast(&psbt.extract_tx()?)?;

The existing tests map over cleanly: test_electrum_balance → fund + full_scan + wallet.balance(); test_electrum_historywallet.transactions() after sync; test_electrum_payment → build/sign PSBT + transaction_broadcast.

One coverage gap to handle

BDK syncs by polling (full_scan/sync), not subscriptions. Unlike the old Electrum Python wallet, it won't exercise blockchain.headers.subscribe / scripthash subscription + the server's notify() push path that the current notify_wallet() flow tests. Keep that covered by either:

  1. Keeping test_electrum_raw (already raw TCP) and adding a small blockchain.headers.subscribe / scripthash-subscribe smoke test over the socket, or
  2. Using electrum-client's subscribe methods for that one assertion.

Net coverage still increases versus today.

Fallback approach: raw electrum-client

If the heavier BDK dependency tree is unwanted, the lighter alternative is the pure-Rust electrum-client (0.25, also bitcoin ^0.32) used as a thin protocol client, with bitcoind (already available via TestRunner::node_client()) acting as the wallet. Assertions become direct protocol calls — script_get_balance (sats), script_get_history, script_list_unspent, transaction_broadcast. This is simpler and minimal-dep, but drops the independent-wallet logic from the loop. Much of that protocol-level surface is already exercised by tests/rest.rs and the raw-socket test_electrum_raw.

Approaches considered

Approach End-to-end wallet signal Cross-platform / pure Rust Deps Notes
BDK (bdk_wallet + bdk_electrum)proposed ✅ strong (real wallet) heavier Most representative consumer of electrs; no subscription coverage (handle separately)
raw electrum-client + bitcoind ⚠️ protocol-level only minimal Simplest; overlaps tests/rest.rs / test_electrum_raw
Keep electrumd + macOS fork patch ❌ (x86_64 only) Status quo; breaks on Apple Silicon
Fork electrumd to ship multi-arch binaries ⚠️ Upstream Electrum has no headless arm64 build → most work, stays brittle

Notes

  • These tests are already Bitcoin-only (#[cfg_attr(feature = "liquid", allow(dead_code))]), so the Liquid/confidential case does not need solving here.
  • Removing electrumd also eliminates the patch-revert ritual and the subprocess + wait_for_sync polling, making the suite faster.

Acceptance criteria

  • electrumd removed from Cargo.toml (dependency + [patch.crates-io.electrumd]); bdk_wallet + bdk_electrum added.
  • electrumd Error variant / From impl removed from tests/common.rs.
  • tests/electrum.rs rewritten against BDK.
  • Subscription / headers.subscribe path retained via a raw-socket test.
  • cargo test --test electrum passes on both Linux and Apple Silicon macOS.
  • No downloaded-binary or platform-patch step required to run the suite.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions