Skip to content

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

@EddieHouston

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
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions