refactor(chain): replace the ChainSource enum with a pluggable chain ability seam - #7
Open
tonible14012002 wants to merge 5 commits into
Open
tonible14012002 wants to merge 5 commits into
tonible14012002 wants to merge 5 commits into
Conversation
Pure indirection. Every consumer now reaches the chain through ChainLayer instead of the ChainSource enum; ChainLayer delegates every call to the untouched enum. No behaviour change is intended or possible yet. Node::sync_wallets' three-arm match on the source kind moves into ChainLayer::sync_wallets_once(). The per-engine call order is reproduced exactly, including tx-based syncing the Lightning wallet before the on-chain wallet. The match now sits inside the chain module, where the engine extraction will delete it. Also adds an empty [workspace] table so a local clone placed inside a consuming monorepo is not captured by that repo's workspace. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UFffyxzPikJsaiFYUcxezB
The three per-source fee arms become three FeeAdapter implementations. ChainLayer owns the shared tail: install the cache, log completion, advance and persist the fee-rate-cache metrics timestamp. The adapter contract is target-shaped, not block-count-shaped, because bitcoind selects Conservative vs Economical estimation BY ConfirmationTarget; a block-count-keyed contract would lose that and silently change bitcoind's fee behaviour. Electrum's client already returns a finished cache with its own timeout and completeness policy, so adapters own their wire timeout and network policy while the seam owns only installation. FeeUpdate carries the two non-obvious pre-seam outcomes so they survive verbatim: bitcoind's soft testnet failure returns Skip, leaving BOTH the cache and the metrics timestamp untouched (previously a bare 'return Ok(())'), and bitcoind logs completion only when the cache actually changed while esplora and electrum always log. Background sync loops now refresh fees through the layer's slot rather than through the chain source, so a swapped adapter also takes effect for background updates. Adds a startup log naming the adapter in each slot. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UFffyxzPikJsaiFYUcxezB
The three per-source broadcast arms become BroadcastAdapter implementations; ChainLayer owns only the queue drain loop. The arms were not interchangeable, so the contract preserves their differences rather than unifying them: esplora and bitcoind each own a per-transaction timeout while electrum's client owns its own and has none here, and esplora classifies an HTTP 400 as benign (bitcoind usually already knows the transaction) and logs it far more quietly than a real failure. Error classification and log level therefore belong to the adapter, not the seam. BroadcastAdapter::ready() reproduces electrum's pre-loop client check. Pre-seam that returned early from the whole pass; since the queue is drained once per second this is the same behaviour -- skip this pass, retry on the next tick. Broadcast stays deliberately lossy: failures are logged and dropped, never propagated. An ordered fallback across adapters is a later change and must not arrive with the seam. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UFffyxzPikJsaiFYUcxezB
LOOKUP is a narrow QUERY ability, not a sync engine: the status of an arbitrary transaction, and whether channel announcements can be verified. The two wallet-sync architectures share no interface and stay a separate explicit axis, extracted in the next block. swap_query_tx and as_utxo_source move out of the enum into LookupAdapter implementations. The fail-closed contract (E6) is now stated on the trait rather than implied by three separate arms: an adapter that cannot answer MUST return Unreachable, because callers arm CSV and claim deadlines off this result. utxo_source() turns the old '_ => None' fallthrough into an explicit per-adapter declaration. Only bitcoind can answer gettxout, so only bitcoind verifies announcements; esplora and electrum now say so. The node logs this at startup, because a node whose routing graph carries unverified channel capacities previously reported nothing at all. latest_chain_tip becomes an Arc so the lookup adapter can read the cached tip -- used only as the fail-soft height fallback -- without reaching back through the enum. Verified: fork lib compiles, 34 unit tests pass (incl. the B5 fail-closed suite), node-app-ldk-node builds against it unchanged. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UFffyxzPikJsaiFYUcxezB
The enum is gone. ChainLayer now holds three ability slots plus a
SyncEngine, and nothing in the crate branches on which backend is
configured.
Done as one commit rather than the planned move-then-delete pair: the
engine structs and the enum variants hold the same fields, so keeping
both would have duplicated live state -- Mutexes, a header cache and a
cached tip -- across two owners. That is worse than a larger diff.
Against D-A, SyncEngine is a TRAIT, not an enum. D-A assumed two
engines. There are three types: esplora and electrum are both
transaction-based but share no code, and an enum keeps the three-way
matches plus the four unreachable!() arms this block exists to remove.
The moved bodies are verbatim. Each method re-binds its state with
'let Self { .. } = self;' so the relocated lines are byte-identical to
the match arms they came from, and the diff is reviewable as a pure
move. The shared tx-based background loop is lifted once into
run_tx_based_sync_loop rather than copied into both tx engines.
Verified: 0 errors, no new warnings, cargo fmt clean, 34/34 unit tests
pass, node-app-ldk-node builds against it unchanged. E1 holds (no
ChainSource type remains) and E7 holds (no unreachable!() in chain/).
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01UFffyxzPikJsaiFYUcxezB
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Replaces the
ChainSourceenum with a seam: three pluggable chain ability slots plus an explicit sync-engine axis. Behaviour-neutral — no new feature, no config change, no public API change.Based on
logging-reduction(677e116), the rev the consuming monorepo pins today, so this diff is exactly the five commits below.Why
The abstraction boundary was drawn at the chain source, not the chain ability. One closed enum decided fee estimation, lookup, broadcast and wallet-sync strategy together, so a backend could not be swapped for one ability without swapping all four. It also forced four
unreachable!()arms, because each sync strategy had to exist on the other and panic.What
src/chain/mod.rs: 2176 → 519 lines; the layer is now ~2833 lines across 11 focused files.d08a4b1ChainLayerwrapper — pure indirection1911cb60646abc2dffd5123b826fChainSourcedeletedDesign decisions the code forced
FeeAdapter is target-shaped, not block-count-shaped. bitcoind selects Conservative vs Economical estimation per
ConfirmationTarget, not per block count. A block-count-keyed contract loses that and silently changes bitcoind's fee behaviour.Adapters own their wire timeout and error classification; the seam owns only installing the result. Electrum's client already has its own timeout, batching and completeness policy, and Esplora's "HTTP 400 is benign" rule has no analogue elsewhere. Hoisting either changes behaviour.
FeeUpdate::Skipexists for one pre-seam line. bitcoind's soft testnet failure was a barereturn Ok(())that left both the fee cache and the metrics timestamp untouched. A plainResult<HashMap, _>would have advanced the timestamp as though an update had landed.SyncEngineis a trait, not an enum. There are three types, not two: esplora and electrum are both transaction-based but share no code. An enum keeps the three-way matches and the fourunreachable!()arms this change exists to remove.LOOKUP is a narrow query ability, not a sync engine. Its consumers are
swap_query_tx,derive_tx_statusand the gossip UTXO verifier. Wallet sync is a separate explicit axis, selected but deliberately not abstracted.Background loops refresh fees through the SLOT, not through the backend — otherwise a swapped fee adapter would apply to foreground calls and silently not to background ones.
Reviewing the moved code
The relocated bodies are byte-identical. Each method re-binds its state with
let Self { .. } = self;so the moved lines match the match arms they came from exactly; the diff reads as a relocation, not a rewrite. The shared transaction-based background loop is lifted once intorun_tx_based_sync_looprather than copied into both tx engines.Behaviour
Intended to be identical. Preserved deliberately:
ldk_node::Errorvariants at the boundaryTwo intended additions, both
log_infoat startup:The second surfaces an existing, previously silent condition:
as_utxo_source()returnsNonefor esplora and electrum, soadd_utxo_lookupis never called and channel announcements are accepted unverified. That was true before this PR; it just said nothing.One accepted deviation: the four
unreachable!()arms no longer exist — panics that could never fire.Testing
The integration suite runs against real bitcoind 27.2 + electrs + esplora and covers all four backends:
A channel full cycle cannot complete unless
Filter::register_tx/register_outputstill reach the tx-sync client — the funding transaction would never confirm — so this exercises the highest-risk path of the refactor on every transaction-based backend.Also verified: the consuming app builds against this branch unchanged, confirming the public builder API is untouched.
Not covered: the
swaps/cyclesprimitives have unit coverage only; there is no integration test forswap_query_txin this suite.Adds one dependency
async-trait— required for object-safe async methods onArc<dyn FeeAdapter>and friends. Already present in the consuming monorepo's graph, so it costs no additional crates downstream.🤖 Generated with Claude Code
https://claude.ai/code/session_01UFffyxzPikJsaiFYUcxezB