feat(mirror): wire a production broadcaster for the mirror lifecycle (#424) - #434
Draft
MichaelTaylor3d wants to merge 5 commits into
Draft
feat(mirror): wire a production broadcaster for the mirror lifecycle (#424)#434MichaelTaylor3d wants to merge 5 commits into
MichaelTaylor3d wants to merge 5 commits into
Conversation
`production_broadcaster()` was a literal `None`, so `sign_and_broadcast` refused before signing and both the spend-record writer (#411) and the mirror signer (#410) were inert. `ChainTransport::broadcaster()` is the narrow counterpart to the existing public `chain_source()`: it returns a broadcaster built on the ONE shared client, so `shared_client` stays `pub(crate)` and no caller gains the client itself. The seam keeps its single-derivation role and gains a three-way answer, so an unreachable chain is no longer reported as a switched-off flag. On a default install no broadcaster is constructed and no chain is dialed. Refs #424
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.
DO NOT MERGE — the gate round has not returned. This PR stays DRAFT until it has.
production_broadcaster()was a literalNone, andsign_and_broadcastrefuses on it before signing. That single fact made three things inert: #411's spend-record writer (which is unbypassable but never fired), #410's mirror half, and reclaims entirely. This wires it.Shape
shared_clientstayspub(crate). Nothing was widened.The stated obstacle — "wiring
ChiaQueryBroadcasterneedsChainTransport::shared_client, which ispub(crate)" — does not hold.ChiaQueryBroadcaster::newis alreadypuband takes anArc<ChiaQuery>, never aChainTransport. And the pattern for exposing a shared-client-derived capability without exposing the client already exists in that same file:ChainTransport::chain_sourceispub, callsshared_client()internally, and returns a purpose-shaped provider.So this adds the narrow counterpart,
ChainTransport::broadcaster()— it can push a signed bundle and nothing else.Why not just make
shared_clientpublic. That hands every future caller the client itself, which is the capabilitychain_source's own comment records as having previously given a live node two independent peer pools with two notions of the peak (dig_ecosystem#2761). It fails open and silently: nothing breaks at the call site, and the damage appears as a node disagreeing with itself about the chain.It is deliberately not
impl Broadcaster for ChainTransport. An unused one sat in that file once and made a one-line.with_broadcaster(chain.clone())compile, pass every test, and silently enable node-custodied sending on a default install (sage/chain.rs:578). A caller must now ask by name, and asking is a visible line in a diff.ChiaQueryBroadcaster, notConfirmingBroadcaster—sign_and_broadcasttreatsOk(())as reached the mempool and states that only a chain observation may promote that to confirmed. A confirming broadcaster would block the synchronous pass on block inclusion and blur the boundary the journal is built on.What stays off by default
enable_live_broadcastdefaults false (config.rs:176/:242). On a default install the broadcaster is now never constructed and no chain is dialed — it is not merely unused, so there is nothing in scope for a later edit to attach. The gate is the first statement of the seam.The single seam, preserved
production_broadcasterkeeps its role as the one derivation read by both the announced capability (open_signer) and the effects the scheduler builds. It gains the inputs it needs and becomesasync; both call sites still call the same function.SpendCapability::Availablenow holds exactly when a broadcaster is handed over, and that equivalence is asserted directly rather than by comparing today's literal values.SpendCapability::BroadcasterUnwiredis removed: it meant "this build wires no broadcaster", which this change makes false, and no branch could produce it. It is replaced byChainUnreachable, a genuinely reachable and genuinely different condition — a wallet, live broadcast on, and no chain to send through. Reporting that asAvailablewould announce a power the node lacks; reporting it asBroadcastDisabledwould send an operator to set a flag they already set.Stated limitation: the announced capability is derived once at bring-up while the seam is re-read per pass, so a node that was offline at start-up announces
ChainUnreachablewhile later passes may broadcast. That understates the node's power — it fails in the safe direction.Blast radius checked
production_broadcasterhad exactly two non-test call sites (lifecycle.rs:567open_signer,server.rs:2806spawn_mirror_passes); both are updated and both still read the one seam.open_signerhad three call sites (two production, one test), all updated.SpendCapabilityis consumed only by four log arms inserver.rs:2712-2727.shared_clientgained no new external caller. MEASURED on a fresh per-worktree gitnexus index (not grep-derived; an earlier draft of this body said the tools were unreachable, which was wrong -- I had not triedToolSearch).WARNING:
production_broadcasteris risk HIGH -- 11 impacted, 4 direct, 1 affected process. The rating comes from reachability: the seam is on the node startup path, so depth 2-3 pulls inserve_with_shutdown->serve->run_serviceand threetests/server.rsintegration tests, all transitive throughspawn_mirror_passes.Direct dependents are exactly
spawn_mirror_passes,open_signer, and the two new tests -- so both production call sites still read the one seam, measured rather than asserted.open_signeris LOW with 3 direct (1 production, 2 tests; an earlier count here said 2 production).ChainTransport::shared_clienthas 5 direct callers, all inside dig-wallet (build_with,build_live_wallet, its own test,chain_source, and the newbroadcaster) -- no caller outside the crate, so thepub(crate)boundary is intact.detect_changesagainstorigin/mainshows no unexpected symbol, and exactly one process with changed steps beyond its entry:Spawn_mirror_passes -> Clientat steps 2/3/4 (open_signer->production_broadcaster->broadcaster) -- the seam chain itself.Note: gitnexus
analyzewrites tracked files; it modifiedAGENTS.mdandCLAUDE.md, which were reverted. HEAD is unchanged andgit statusis clean.Preserved, verified against the diff
git difftouches none of these; each confirmed still in place:RefCell<HashSet<String>>atlifecycle.rs:147,borrow_mut().extend(..)at:285inside the broadcast-reached-mempool arm only, selection borrow scoped and dropped at:404;createstill a synchronousfn, with no.awaitanywhere in it;create, ahead of any chain read or coin selection.The structural guard
no_signer_or_broadcaster_is_ever_installed_on_the_served_wallet_backendis untouched and passes: no.with_signer(/.with_broadcaster(string is introduced in any guarded file. The lifecycle asks for a broadcaster by name and holds it itself.§908
The only key reached is the §16.4 operator wallet
open_signeralready opened.MirrorSigneris unmodified, nothing is attached to the servedWalletBackend, and nothing here can reach a user key.Verification
cargo test -p dig-node-service --lib mirror::lifecycle— a--libunit target, compiled and run by the same invocation. 10/10 green.Each new test revert-proofed by reverting only its production change (committed first; reverted via a file copy, never
git checkout <path>), confirming RED on its own assertion rather than a compile error, then restoring:!live_broadcastgatea_default_install_builds_no_broadcaster_and_cannot_spendlifecycle.rs:785— "must report the switched-off flag, never a built broadcaster"ChainUnreachable => Availablethe_capability_decision_names_each_seam_outcomeleft: Available, right: ChainUnreachablethe_announced_capability_holds_exactly_when_a_broadcaster_is_handed_overleft: true, right: falseThe default-install test stayed green under the second revert — the tests are independent, not one property asserted twice.
Full red/green output on the ticket.
SPEC.md§25 updated in the same unit: the "BUILT and not SENT" paragraph replaced with what is now true.Version: 0.178.0 (
feat→ minor),Cargo.toml+Cargo.lock, verified consistent undercargo metadata --locked.Closes #424