feat(mirror): add MirrorBondState::Unadvertised, the switch-on-but-nothing-to-advertise state - #41
Merged
Conversation
…thing-to-advertise state A node whose advertise-URL list has no publishable entry creates no mirror coin, however much $DIG it holds and however definitely its own collateralisation switch is ON. The enum had no value for that, so dig-node had to choose among values that are all false about it: `unfunded` names a figure and sends the operator to buy $DIG that would bond nothing, every `deferred` reason blames a working subsystem, and `disabled` is the operator's own switch -- which this contract says a client MUST NOT present as a fault, so serving it there obliges a conforming client to stay silent about the one thing actually wrong. `Unadvertised` is that state, and it IS a fault a client must surface. That is the entire distinction from `Disabled`: both are node-wide and both mean no coin, but one is a decision the operator already made and the other is that decision silently not being honoured. Additive: a new variant, a new wire token, no existing token or payload changed. SPEC.md and the trait guidance name the pair apart, and the KAT asserts both the token and that it differs from `disabled` -- the nearest wrong implementation being the interim dig-node mapping this replaces. Co-Authored-By: Claude <noreply@anthropic.com>
This was referenced Sep 1, 2026
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.
Why
dig-node PR #484 fixes a real money lie: a node whose
DIG_MIRROR_ADVERTISE_URLSlist has no publishable entry creates no mirror coin, and the node reported every bond asunfunded, short_dig_base_units: 1010— a demand for $DIG that would have bonded nothing. Its own log had been saying the true cause at every bring-up.That fix needs a wire value, and this enum had none that is true. The gate on #484 checked the alternatives independently and reached the same conclusion:
unfundednames a figure and sends the operator to buy $DIG. False about money — the wallet may be full.CollateralUnknownReasonunderdeferredblames a working subsystem (census, record, chain, wallet read).disabledis the closest, and it is still wrong in a way that defeats the fix:results.rssays a client MUST NOT presentDisabledas a fault, andSPEC.mddefines it as the node-wide switch (§25.7) — which here is ON. Serving it obliges a conforming dig-app to stay silent about the single reason the node bonds nothing.So the honest answer replaces a wrong answer with no answer. This adds the value that is neither.
What
MirrorBondState::Unadvertised— a bare tag, no payload, node-wide likedisabled, meaning: the node holds the capsule, its collateralisation switch is ON, its wallet may be full and the price may be known, but it has no advertise URL a peer could fetch from, so it advertises nothing and creates no coin.It IS a fault a client MUST surface. That is the whole distinction from
disabled: both are node-wide and both mean "no coin", but one is a decision the operator already made, and the other is that decision silently not being honoured. The only remedy is a publishable advertise URL.It covers the general state, not the one-off: the list being empty, and the list being set but with every entry rejected as non-absolute or reachable only from this machine. dig-node's refusal (
lifecycle.rs:429) is over the post-classifyaccepted list, so the state is stated over that class.Version — 0.28.0, MINOR
Additive: one new variant, one new wire token, no existing token, payload or field changed. Nothing that decoded before decodes differently. A client that has not adopted it fails to decode the new tag rather than silently mis-reading an old one, which is the correct direction for a contract whose whole purpose is that these states are never collapsed.
Blast radius
gitnexus was NOT used and I say so explicitly. Its registered index for this repo is stale by construction (the lane analyses its own worktree, the registration points at the primary checkout), and a stale index answers
impactedCount: 0for a symbol with real callers — a false-safe zero. This was done bygit grepover the branch plus direct read.MirrorBondStatereferenced outsideresults.rs:kats.rs(fixtures and the token-uniqueness KAT),traits.rs:571(themirror_bond_statescontract prose). No totalmatchon it exists in this crate outside tests, so nothing silently absorbs the variant; the two counted assertions (7) in the uniqueness KAT failed on the addition and were updated deliberately rather than by wildcard.Verification
cargo test --lib— 188 passed, 0 failed, 0 filtered out. Count checked, not exit status:cargosurfaced through this harness as[exited with code 0]while failing, twice during this change.Red was genuine and at the decision. Before the variant existed:
Then, with the variant added but the uniqueness KAT's count left at seven:
That second red is the load-bearing one — it proves the new token is genuinely distinct on the wire rather than colliding with an existing one.
The new KAT asserts the property, not the outcome.
an_unadvertised_bond_is_its_own_state_and_not_disabledpins the literal{"bond_state":"unadvertised"}bytes and asserts the value differs fromDisabled. The second half is the one that matters: the nearest wrong implementation is exactly the interim mapping this release replaces, and a test that only round-tripped the new value would pass against it.cargo clippy --all-targets -- -D warningsclean;cargo fmt --checkclean.Coherence
SPEC.mdgains the row, the "seven of the eight states" count, and a normative paragraph stating thatdisabledandunadvertisedare both node-wide and only one of them is a fault.traits.rsgains the matching MUST on the implementation side. Both land in this PR (§4.2).Next, and it is not a follow-up ticket
dig-node #484 adopts this the moment it is on crates.io: it drops its interim
Unadvertised → Disabledmapping and restores its ownSPEC.md§25.8 definition ofdisabledto mean only the §25.7 switch. Release-first (§4.1) — this ships first, gets verified on the index, and only then does the consumer bump.