You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Below is a LLM-organized and researched plan for integrating
third-party crates. My ideas but I had the LLM research the target
crates in-depth for me to see if we can easily integrate them.
Motivation
BHWI currently reimplements device protocols in bhwi. Discussion in #76
(BitBox, Trezor, and HWI maintainers) argues that reimplementation silently
diverges from upstream fixes (e.g. BitBoxSwiss/bitbox-api-rs#129 anti-klepto
hardening, message-interleaving mitigation) and repeats the maintenance
mistake that made Python HWI's vendored libraries unmaintainable.
Proposal: a two-tier support model.
Tier 1 (vendor-maintained): devices with a hardened Rust crate
maintained by or with the vendor. We depend on the crate for protocol
logic and wire types, pin it by exact version, review upgrades, and
coordinate upstream changes we need.
Tier 2 (in-repo): devices without a suitable Rust crate keep their
hand-written protocol implementations in bhwi/.
Out of scope for tier 1:
Jade: no vendor Rust crate for Bitcoin.
Coldcard: kept in-repo for backwards compatibility only; no
conversion effort planned due to recent security issues.
Non-negotiable constraint
Per docs/VISION.md, the whole point of BHWI is that protocol logic is
sans-I/O: every device is driven through the Interpreter pump
(start -> repeated exchange -> end, with Transmit/Recipient
routing), so consumers keep full control of execution — sync, async, FFI,
WASM, or custom event loops. Tier 1 does not change this. Vendor crates are
integrated inside device interpreters, never as a replacement for them.
The common types, DeviceContext, and all existing consumers
(bhwi-async, bhwi-cli, bhwi-wasm, planned FFI) are unaffected.
Integration mechanism: coroutine adapter
Vendor clients are written inversion-of-control style: the client calls a
caller-supplied transport trait. To invert that back into a pump without
threads or a runtime, bhwi gains a small adapter that treats the vendor
client's async call as a coroutine:
The interpreter owns a Pin<Box<dyn Future>> built from the vendor
client method (client constructed per command, as interpreters are
per-command today).
The vendor crate's transport trait is implemented over a pair of
in-memory channels. A transport call sends the outgoing payload and
awaits the response.
start() polls the future (noop waker) until it parks on a transport
call; the pending payload becomes the Transmit.
exchange(data) injects the response into the channel and re-polls,
yielding the next Transmit or completion.
end() returns the future's output mapped to the common Response.
Properties:
No async runtime, no threads, no block_on. Works on stable Rust,
single-threaded WASM, and behind sync FFI.
Send-bounded vendor transport traits (async-trait default) are
satisfiable, since the channel halves are Send — the JS-future/?Send
problem does not arise inside the adapter.
Invariant: the wrapped future may only ever suspend on our channel.
Any other suspension point (timer, spawn, external I/O) deadlocks the
pump. Every vendor crate must be audited for this, and violations become
upstream asks. This invariant must be documented and enforced with tests.
Candidate crates
Device
Crate
Version
bitcoin dep
Client style
Adapter fit
Ledger
ledger_bitcoin_client (LedgerHQ/app-bitcoin)
0.6.2
0.32
async client over Transport trait, pure APDU request/response
direct
BitBox02
bitbox-api
0.13.0
0.32
async client over ReadWrite trait; has a Runtime abstraction (sleep)
needs suspension-point audit
Trezor
trezor-client (trezor/trezor-firmware)
0.1.6
0.32 (optional)
sync-onlyTransport: Sync + Send; strictly one write + one read per call(), interaction flows caller-driven
two-phase bridge (see Trezor section); rusb must become optional
Tasks
Cross-cutting: coroutine adapter primitive
Add an adapter module in bhwi (e.g. bhwi::adapter): channel-backed
request/response cell, manual polling with a noop waker, generic
plumbing for wrapping an async vendor call as start/exchange/end.
Unit tests: multi-round exchange, error propagation, and a
deadlock-detection test (future parks without emitting a transmit ->
typed error, not a hang).
Document the suspension-point invariant and the per-crate audit
requirement in AGENTS.md/docs.
Confirm the adapter compiles for wasm32 and involves no
runtime/threads (CI check).
Ledger — back LedgerInterpreter with ledger_bitcoin_client
The crate's APDU exchange is pure request/response, the ideal adapter case.
It also shares lineage with our implementation (both originally by
Edouard), and is proven in production (Liana).
Add ledger_bitcoin_client = "=0.6.2" with the async feature; keep paranoid_client enabled (independent address derivation; its internal
miniscript 12.2 must not cross our API boundary — verify).
Implement the crate's async Transport over the adapter channels
(APDU struct <-> raw payload mapping at the boundary; Transmit
recipient is always Device).
Rebuild LedgerInterpreter on the adapter: app info/open app, master
fingerprint, xpub, address display, wallet registration (HMAC), PSBT
signing with registered and default policies, message signing.
Keep common::Command/DeviceContext::Ledger conversions and their
validation errors (missing context vs network mismatch vs protocol
refusal) at least as specific as today.
Audit suspension points in the crate's client methods (expected:
transport-only) and pin that finding in the integration notes.
Confirm parity for registered-policy signing and missing Merkle leaf
handling (e23dc5b, ba26f5b); extend e2e cases if crate behavior differs.
Delete bhwi/src/ledger protocol internals (apdu, merkle, psbt,
wallet, store) once parity is proven; keep the interpreter module and
common conversions.
Validation: bhwi-e2e-ledger and bhwi-e2e-cli ledger suites
before/after; wasm32 build of bhwi-wasm.
BitBox02 — back BitBoxInterpreter with bitbox-api
Largest win (the bitbox feature already pulls essentially bitbox-api's
own dep set, so the dependency-count argument is a wash). Maintainer
offered help in #76. One structural question: the crate's Runtime
abstraction.
Audit bitbox-api suspension points: where its Runtime::sleep and
any polling loops are used (unlock wait, antiklepto flow). Outcome
drives the upstream ask below.
Upstream ask (BitBoxSwiss/bitbox-api-rs): guarantee (or add a mode
where) client futures suspend only on ReadWrite calls — e.g. make
waits injectable — so the client is drivable by an external pump.
Reference Clarification on why bitbox-api-rs is not used as a dep #76 where benma offered to address blockers.
Add bitbox-api = "=0.13.0" with default-features = false; no usb/wasm features in bhwi (those stay consumer-side concerns; the
adapter replaces them).
Implement communication::ReadWrite over the adapter channels;
decide the framing boundary (their HwwCommunication/U2F framing vs
our existing transport framing in bhwi-async/bhwi-wasm) so frames
are neither double-applied nor missing.
Decide noise/pairing state handling across per-command interpreter
runs (persisted pairing config compatibility with configs written by
the current implementation, or a migration).
Verify no miniscript types cross the crate's public API (it pins
miniscript 13.0 internally; we pin a git rev) — coordinate if they do.
Delete bhwi/src/bitbox protocol internals and the now-unneeded
direct deps (noise-protocol, prost, zeroize, getrandom, ...) once
parity is proven.
Diff review: enumerate upstream fixes we gain (anti-klepto #129,
interleaving mitigation, send-to-self detection) and any behavior we
lose.
Trezor support already exists as draft PR #75 (part of #73): an in-repo TrezorInterpreter with prost-generated protobuf bindings pinned to a
firmware protob rev, plus bhwi-async/CLI/e2e/nix wiring. This issue does
not restart that work; it defines the tier-1 convergence path for it.
The blocker, re-investigated.trezor-client's Transport is
sync-only (fn write_message / fn read_message, blocking), so the async
coroutine adapter cannot drive it, and threads are ruled out (WASM, sync
FFI). However, the client has no internal transport loops: call()
performs exactly one write_message followed by one read_message, and
every multi-round flow (ButtonRequest/ack, PinMatrix, SignTx's
TxRequest/TxAck streaming) is caller-driven through the returned TrezorResponse interaction objects. Blocking therefore only ever spans a
single write -> read pair.
Solution: two-phase transport bridge (the sync analogue of the
coroutine adapter). The interpreter implements trezor_client::Transport
over two in-memory slots:
Capture phase: invoke the client step; write_message stores the
encoded message (it becomes the Transmit payload); read_message
returns a WouldBlock sentinel error, which the interpreter absorbs
while remembering the pending step.
Inject phase: on exchange(data), place the device response in the
read slot and re-invoke the same step; write_message produces an
identical payload (pure protobuf re-encode, deduped), read_message
yields the injected response, and the resulting TrezorResponse drives
the next step (ack -> next capture -> next Transmit).
Each step runs at most twice, there is never more than one outstanding
round-trip (so no replay blow-up), and the pump invariant becomes "every
client step performs exactly one write then one read" — guaranteed today
by the caller-driven design and enforceable with adapter tests that fail
loudly if an upstream version starts looping internally. Wire framing
(0x3f/## chunking) stays where PR #75 already put it (bhwi-async
transport), or can reuse the crate's ProtocolV1 over an in-memory Link, whose read_chunk never blocks at inject time.
Tasks:
Upstream ask (trezor/trezor-firmware): make rusb optional — it is
a mandatory dep today and the only hard blocker to depending on the
crate from bhwi core (prusnak offered feature-flag work in Clarification on why bitbox-api-rs is not used as a dep #76).
Upstream nice-to-have: expose split write/read halves of call_raw
(e.g. call_write/call_read) to remove the double-invoke in the
capture/inject cycle; small PR we can contribute ourselves.
Upstream long-term: async or sans-I/O client core; revisit the
bridge when it lands.
Add trezor-client = "=0.1.6" with default-features = false, features = ["bitcoin"] (excludes ethereum/solana), gated on the rusb ask.
Build the two-phase bridge in bhwi::adapter next to the coroutine
adapter, with the same test guarantees (multi-round, deadlock
detection, error propagation).
Migrate PR feat(bhwi): Trezor support #75's interpreter: replace the vendored generated proto.rs with the crate's bindings, then progressively replace
hand-written flow logic (SignTx streaming, interaction handling) with
client-driven steps through the bridge.
Keep PR feat(bhwi): Trezor support #75's bhwi-async/CLI/e2e/nix work as-is; its emulator e2e
suite becomes the parity gate for the migration.
Scope decision: docs/VISION.md currently scopes support to devices
with a screen and Miniscript support; Trezor has no Miniscript/BIP388
policy support. Explicitly extend or clarify the scope (also relevant
to HWI parity: onboard Trezor and KeepKey #73/feat(bhwi): Trezor support #75 independently of this issue).
Cross-cutting: policy and guardrails
Dependency policy in AGENTS.md/docs: exact-version pins (=x.y.z)
for vendor device crates, upgrade-review requirement, per-crate
suspension-point re-audit on every bump, fork-and-patch as the
conflict escape hatch (rust-bitcoin/miniscript lockstep is the known
recurring risk).
Keep emulator e2e suites as the compatibility gate for vendor crate
upgrades (run the device's e2e package on every bump).
Update docs/ support matrix and device onboarding docs for the
tier model.
Track binary size and cargo tree deltas per integration; flag
regressions.
Acceptance criteria
Every property promised by docs/VISION.md must survive the tier-1
integrations, not just the trait signature:
Sans-I/O core:bhwi performs no I/O of any kind. All bytes reach
devices and third parties (PIN-server-style recipients) only through
caller-handled Transmit exchanges. No transport, HTTP, HID, or browser
deps in bhwi.
No mandated execution model: interpreters are drivable sync, async,
or from any custom event loop — no function coloring, no async runtime,
no threads, no block_on anywhere in bhwi. Verified by a blocking
(non-async) consumer test alongside the existing async consumers.
FFI-ready: the core stays usable behind stable-ABI bindings; no
async types or vendor types leak across the public surface.
WASM:bhwi and all tier-1 interpreters build and run on
single-threaded wasm32; HTTP-style transmissions remain delegable to
the browser's fetch API via Recipient.
Common interface: tier-1 and tier-2 devices sit behind the same common::Command/Response/Transmit surface and the Interpreter
trait; vendor crates stay an implementation detail of their device
interpreter — only bhwi and rust-bitcoin types appear in public APIs.
Developer extensibility: consumers can still define their own
traits and execution models over interpreters; existing consumers
(bhwi-async, bhwi-cli, bhwi-wasm) require no API changes and CLI
output contracts are unchanged.
BitBox and Ledger flows pass existing emulator e2e suites with the
hand-written protocol internals deleted.
Trezor (draft PR feat(bhwi): Trezor support #75) passes its emulator e2e suite after migrating to
upstream wire types and the two-phase bridge.
Vendor crate versions are exact-pinned with documented upgrade
procedure.
Follow-up to #76.
Below is a LLM-organized and researched plan for integrating
third-party crates. My ideas but I had the LLM research the target
crates in-depth for me to see if we can easily integrate them.
Motivation
BHWI currently reimplements device protocols in
bhwi. Discussion in #76(BitBox, Trezor, and HWI maintainers) argues that reimplementation silently
diverges from upstream fixes (e.g. BitBoxSwiss/bitbox-api-rs#129 anti-klepto
hardening, message-interleaving mitigation) and repeats the maintenance
mistake that made Python HWI's vendored libraries unmaintainable.
Proposal: a two-tier support model.
maintained by or with the vendor. We depend on the crate for protocol
logic and wire types, pin it by exact version, review upgrades, and
coordinate upstream changes we need.
hand-written protocol implementations in
bhwi/.Out of scope for tier 1:
conversion effort planned due to recent security issues.
Non-negotiable constraint
Per
docs/VISION.md, the whole point of BHWI is that protocol logic issans-I/O: every device is driven through the
Interpreterpump(
start-> repeatedexchange->end, withTransmit/Recipientrouting), so consumers keep full control of execution — sync, async, FFI,
WASM, or custom event loops. Tier 1 does not change this. Vendor crates are
integrated inside device interpreters, never as a replacement for them.
The
commontypes,DeviceContext, and all existing consumers(
bhwi-async,bhwi-cli,bhwi-wasm, planned FFI) are unaffected.Integration mechanism: coroutine adapter
Vendor clients are written inversion-of-control style: the client calls a
caller-supplied transport trait. To invert that back into a pump without
threads or a runtime,
bhwigains a small adapter that treats the vendorclient's async call as a coroutine:
Pin<Box<dyn Future>>built from the vendorclient method (client constructed per command, as interpreters are
per-command today).
in-memory channels. A transport call sends the outgoing payload and
awaits the response.
start()polls the future (noop waker) until it parks on a transportcall; the pending payload becomes the
Transmit.exchange(data)injects the response into the channel and re-polls,yielding the next
Transmitor completion.end()returns the future's output mapped to the commonResponse.Properties:
block_on. Works on stable Rust,single-threaded WASM, and behind sync FFI.
Send-bounded vendor transport traits (async-trait default) aresatisfiable, since the channel halves are
Send— the JS-future/?Sendproblem does not arise inside the adapter.
Any other suspension point (timer, spawn, external I/O) deadlocks the
pump. Every vendor crate must be audited for this, and violations become
upstream asks. This invariant must be documented and enforced with tests.
Candidate crates
ledger_bitcoin_client(LedgerHQ/app-bitcoin)Transporttrait, pure APDU request/responsebitbox-apiReadWritetrait; has aRuntimeabstraction (sleep)trezor-client(trezor/trezor-firmware)Transport: Sync + Send; strictly one write + one read percall(), interaction flows caller-drivenrusbmust become optionalTasks
Cross-cutting: coroutine adapter primitive
bhwi(e.g.bhwi::adapter): channel-backedrequest/response cell, manual polling with a noop waker, generic
plumbing for wrapping an async vendor call as
start/exchange/end.deadlock-detection test (future parks without emitting a transmit ->
typed error, not a hang).
requirement in AGENTS.md/docs.
runtime/threads (CI check).
Ledger — back
LedgerInterpreterwithledger_bitcoin_clientThe crate's APDU exchange is pure request/response, the ideal adapter case.
It also shares lineage with our implementation (both originally by
Edouard), and is proven in production (Liana).
ledger_bitcoin_client = "=0.6.2"with theasyncfeature; keepparanoid_clientenabled (independent address derivation; its internalminiscript 12.2 must not cross our API boundary — verify).
Transportover the adapter channels(APDU struct <-> raw payload mapping at the boundary;
Transmitrecipient is always
Device).LedgerInterpreteron the adapter: app info/open app, masterfingerprint, xpub, address display, wallet registration (HMAC), PSBT
signing with registered and default policies, message signing.
common::Command/DeviceContext::Ledgerconversions and theirvalidation errors (missing context vs network mismatch vs protocol
refusal) at least as specific as today.
transport-only) and pin that finding in the integration notes.
handling (e23dc5b, ba26f5b); extend e2e cases if crate behavior differs.
bhwi/src/ledgerprotocol internals (apdu, merkle, psbt,wallet, store) once parity is proven; keep the interpreter module and
common conversions.
bhwi-e2e-ledgerandbhwi-e2e-cliledger suitesbefore/after; wasm32 build of
bhwi-wasm.BitBox02 — back
BitBoxInterpreterwithbitbox-apiLargest win (the
bitboxfeature already pulls essentially bitbox-api'sown dep set, so the dependency-count argument is a wash). Maintainer
offered help in #76. One structural question: the crate's
Runtimeabstraction.
bitbox-apisuspension points: where itsRuntime::sleepandany polling loops are used (unlock wait, antiklepto flow). Outcome
drives the upstream ask below.
where) client futures suspend only on
ReadWritecalls — e.g. makewaits injectable — so the client is drivable by an external pump.
Reference Clarification on why bitbox-api-rs is not used as a dep #76 where benma offered to address blockers.
bitbox-api = "=0.13.0"withdefault-features = false; nousb/wasmfeatures inbhwi(those stay consumer-side concerns; theadapter replaces them).
communication::ReadWriteover the adapter channels;decide the framing boundary (their
HwwCommunication/U2F framing vsour existing transport framing in
bhwi-async/bhwi-wasm) so framesare neither double-applied nor missing.
BitBoxInterpreteron the adapter: noise pairing/handshake,xpub, address display (path, descriptor, multisig), wallet policy
registration, PSBT signing, setup/restore/wipe/backup, toggle
passphrase.
runs (persisted pairing config compatibility with configs written by
the current implementation, or a migration).
miniscript 13.0 internally; we pin a git rev) — coordinate if they do.
bhwi/src/bitboxprotocol internals and the now-unneededdirect deps (noise-protocol, prost, zeroize, getrandom, ...) once
parity is proven.
interleaving mitigation, send-to-self detection) and any behavior we
lose.
bhwi-e2e-clibitbox suitesbefore/after; wasm32 build.
Trezor — converge draft PR #75 onto
trezor-clientTrezor support already exists as draft PR #75 (part of #73): an in-repo
TrezorInterpreterwith prost-generated protobuf bindings pinned to afirmware protob rev, plus bhwi-async/CLI/e2e/nix wiring. This issue does
not restart that work; it defines the tier-1 convergence path for it.
The blocker, re-investigated.
trezor-client'sTransportissync-only (
fn write_message/fn read_message, blocking), so the asynccoroutine adapter cannot drive it, and threads are ruled out (WASM, sync
FFI). However, the client has no internal transport loops:
call()performs exactly one
write_messagefollowed by oneread_message, andevery multi-round flow (ButtonRequest/ack, PinMatrix, SignTx's
TxRequest/TxAck streaming) is caller-driven through the returned
TrezorResponseinteraction objects. Blocking therefore only ever spans asingle write -> read pair.
Solution: two-phase transport bridge (the sync analogue of the
coroutine adapter). The interpreter implements
trezor_client::Transportover two in-memory slots:
write_messagestores theencoded message (it becomes the
Transmitpayload);read_messagereturns a
WouldBlocksentinel error, which the interpreter absorbswhile remembering the pending step.
exchange(data), place the device response in theread slot and re-invoke the same step;
write_messageproduces anidentical payload (pure protobuf re-encode, deduped),
read_messageyields the injected response, and the resulting
TrezorResponsedrivesthe next step (ack -> next capture -> next
Transmit).Each step runs at most twice, there is never more than one outstanding
round-trip (so no replay blow-up), and the pump invariant becomes "every
client step performs exactly one write then one read" — guaranteed today
by the caller-driven design and enforceable with adapter tests that fail
loudly if an upstream version starts looping internally. Wire framing
(
0x3f/##chunking) stays where PR #75 already put it (bhwi-asynctransport), or can reuse the crate's
ProtocolV1over an in-memoryLink, whoseread_chunknever blocks at inject time.Tasks:
rusboptional — it isa mandatory dep today and the only hard blocker to depending on the
crate from
bhwicore (prusnak offered feature-flag work in Clarification on why bitbox-api-rs is not used as a dep #76).call_raw(e.g.
call_write/call_read) to remove the double-invoke in thecapture/inject cycle; small PR we can contribute ourselves.
bridge when it lands.
trezor-client = "=0.1.6"withdefault-features = false,features = ["bitcoin"](excludes ethereum/solana), gated on therusbask.bhwi::adapternext to the coroutineadapter, with the same test guarantees (multi-round, deadlock
detection, error propagation).
proto.rswith the crate's bindings, then progressively replacehand-written flow logic (SignTx streaming, interaction handling) with
client-driven steps through the bridge.
suite becomes the parity gate for the migration.
docs/VISION.mdcurrently scopes support to deviceswith a screen and Miniscript support; Trezor has no Miniscript/BIP388
policy support. Explicitly extend or clarify the scope (also relevant
to HWI parity: onboard Trezor and KeepKey #73/feat(bhwi): Trezor support #75 independently of this issue).
Cross-cutting: policy and guardrails
=x.y.z)for vendor device crates, upgrade-review requirement, per-crate
suspension-point re-audit on every bump, fork-and-patch as the
conflict escape hatch (rust-bitcoin/miniscript lockstep is the known
recurring risk).
upgrades (run the device's e2e package on every bump).
docs/support matrix and device onboarding docs for thetier model.
cargo treedeltas per integration; flagregressions.
Acceptance criteria
Every property promised by
docs/VISION.mdmust survive the tier-1integrations, not just the trait signature:
bhwiperforms no I/O of any kind. All bytes reachdevices and third parties (PIN-server-style recipients) only through
caller-handled
Transmitexchanges. No transport, HTTP, HID, or browserdeps in
bhwi.or from any custom event loop — no function coloring, no async runtime,
no threads, no
block_onanywhere inbhwi. Verified by a blocking(non-async) consumer test alongside the existing async consumers.
async types or vendor types leak across the public surface.
bhwiand all tier-1 interpreters build and run onsingle-threaded wasm32; HTTP-style transmissions remain delegable to
the browser's fetch API via
Recipient.common::Command/Response/Transmitsurface and theInterpretertrait; vendor crates stay an implementation detail of their device
interpreter — only bhwi and rust-bitcoin types appear in public APIs.
(Ledger Merkle callbacks, noise handshakes, PIN server unlock) remain
inside interpreters; consumers never hand-roll protocol rounds.
traits and execution models over interpreters; existing consumers
(
bhwi-async,bhwi-cli,bhwi-wasm) require no API changes and CLIoutput contracts are unchanged.
hand-written protocol internals deleted.
upstream wire types and the two-phase bridge.
procedure.