fix(peers): omit the wildcard address for a relay-reached peer - #347
Conversation
…er (#253) Co-Authored-By: Claude <noreply@anthropic.com>
`control.peerStatus` emitted `"address": "[::]:0"` for a peer reached over a relay circuit with no configured relay endpoint — the IPv6 unspecified address on port 0, which is the ABSENCE of an endpoint rather than one. The CLI's honest `(no address)` fallback could not fire: `unwrap_or` only catches a MISSING field, never a present string that happens to be meaningless, so `dign peers` rendered the wildcard as if it were a location. Fixed at the producer by applying `net::is_usable_contact` — the predicate the DHT-contact and download paths already trust (#1784) — at the one enumeration site that lacked it. The key is omitted rather than placeholdered, so every consumer's own absence handling works. Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d
left a comment
There was a problem hiding this comment.
Verdict: PASS (correctness gate)
Head read: 671b055. Read-only review; all probing done in a private worktree under C:\tmp\worktrees, no shared checkout mutated.
What I verified myself
1. Consumers of the per-peer address — the lane's claim holds, with one correction to its wording.
An ecosystem-wide grep for peerStatus returns matches in five repos (dig-app, dig-chrome-extension, dig-node, dig-node-control-interface, dig-rpc-protocol), so "no consumers outside dig-node" is too strong as stated. But the load-bearing claim is correct: nothing outside dig-node reads the per-peer address field.
dig-rpc-protocol'sPeerStatusSnapshot(src/types.rs:1307) has nopeers[]array at all — the typed shared contract never modelled the per-peer element, so a now-optional key cannot break a typed deserialize.dig-app's gateway only namescontrol.peerStatus/control.peers.*as method strings; it does not destructure peer rows.dig-chrome-extensionconsumes a different peer shape ({peer_id, addresses[], connection_type, direction, latency_ms},e2e/peers-tab.spec.ts:22), not this one.- In-repo the only readers are
dig-node-service/src/peers.rs:200(sort) and:215(unwrap_or("(no address)")). A missing key yields""at the sort, which groups the peer with IPv6 — harmless, and the renderer's fallback is now correct as written rather than dead.
2. is_usable_contact is the right predicate. seams/dig_peer/net.rs:104 rejects exactly an unspecified IP or port 0 and deliberately accepts loopback. Its documented question is "is this a destination", which is the same question as "should this be shown as an address". No drift; no fourth copy of the wildcard test was added.
3. Suite. cargo test -p dig-node-core --lib → test result: ok. 988 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out (454.99s). Parsed from the test result: line, single cargo process.
4. Placement revert — reproduced. With only the guard call removed and is_usable_contact still defined and compiling (grep -c "fn is_usable_contact" → 1, crate builds clean):
test result: FAILED. 0 passed; 1 failed; 0 ignored; 987 filtered out.
So the test is load-bearing behaviourally, not as a compile error — which is the distinction that matters.
5. Fixture discriminates. Both peers are TraversalKind::Relayed; only the address differs. "Emit everything" fails the wildcard assertion; "blank every relay peer" fails the 198.51.100.8:9444 control. It asserts through connected_peers_json, so a fix placed in the CLI renderer, or a helper written and never called, leaves it red. Both halves confirmed.
6. via/direction survive. The relay-reached row still carries via: "relay" and its direction, so (no address) reads as "no dialable endpoint", not "unreachable". The renderer prints id via/dir addr, so the relay fact stays on the line.
Non-gating findings (posted inline, resolved by me so they do not bar merge)
- There is a FIFTH site, and it is peer-facing —
peer.rs:1296pool_peers. Pre-existing, out of #253's scope, so not gating, but it needs a ticket. - The superseded
{peer_id, address, via, direction}phrasing survives indig-node-service/src/peers.rs:209. crates/dig-node-core/SPEC.md§7.2 (line 744) documents thepeerStatussnapshot with nopeers[]array at all — a pre-existing gap, not drift introduced here.
Out of scope, read and agreed
§2.4b is genuinely unsatisfiable here (five semver-incompatible dig-* steps); splitting rather than shimming is the right call. The reported cargo update -p dig-peer-selector result — 190 lock lines rewritten, two dig-nat and two dig-tls lines left while printing success — is exactly the internally-split shape §2.4b forbids, and reverting it was correct. The dig-gossip git-rev pin is a real NC-7 violation, invisible to any registry check, and belongs in its own ticket.
Do not merge on this comment alone — the PR is DRAFT by design and the orchestrator owns the merge. Note dig-node's cron cuts a stable tag unattended at midnight UTC, so main reaches users without a human.
What
control.peerStatusemitted"address": "[::]:0"for a peer reached over a relay circuit with noconfigured relay endpoint.
[::]:0is the IPv6 unspecified address on port 0 — the absence of anendpoint, not one — so
dign peersrendered it as if it were the peer's location:The CLI's honest fallback at
crates/dig-node-service/src/peers.rs:215(
p["address"].as_str().unwrap_or("(no address)")) is correct as written and was simply unreachable:unwrap_orfires on a MISSING field, never on a present string that happens to be meaningless.Fixed at the producer (
crates/dig-node-core/src/peer.rs,connected_peers_json) by applyingnet::is_usable_contact— the predicate that already guards the DHT-contact path (peer.rsdht_contact_from_pool_addr), the download candidate path (download.rs) and the DHT seam(
seams/dig_peer/dht.rs), and was missing at this one enumeration site. No new predicate was written;a second copy of "is this address meaningless?" would be the rival-implementation shape this ecosystem
keeps paying for.
The key is omitted, not placeholdered, so every consumer's own absence handling works.
Not fixed in the renderer by string-matching
[::]:0: that would leave the producer emitting a falsehoodand add a second place that has to know about it.
Blast radius checked
connected_peers_json— the only producer of thepeers[]array. Consumers of theaddressfield,measured repo-wide and ecosystem-wide:
dig-node-service/src/peers.rs:215—format_peer, the fallback this fix makes reachable. Correctas written, unchanged.
dig-node-service/src/peers.rs:200— the IPv6-first display sort. A missing field yields"",is_ipv4("") == false, so a no-address peer sorts with the IPv6 group. Stable, no change needed.grep -rln 'peerStatus'across every submodule(
--include=*.rs,*.ts,*.tsx) returns dig-node only. So this is a control-surface shape change with noexternal adopter today —
addressbecomes OPTIONAL, andSPEC.mdnow states that a consumer MUST treatits absence as "no known dialable address" rather than as a malformed element.
Evidence
Red first, for the right reason — the failure printed the actual falsehood:
Green:
test result: ok. 988 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out(fulldig-node-core --lib).cargo fmt --checkclean,cargo clippy -p dig-node-core --lib --all-featuresclean.
Placement revert proof — the guard call removed,
is_usable_contactleft intact, so it stillcompiles and the failure is behavioural rather than a compile error:
test result: FAILED. 0 passed; 1 failed. Load-bearing.Fixture design. Both peers are relay-reached; only the address varies, and the second is a
truthful control at a real destination. That separates this property from the two nearest wrong
implementations: emitting every address unconditionally fails the first assertion, and blanking every
relay peer's address fails the control. The assertion runs through
connected_peers_jsonrather than apure helper, so a fix placed in the renderer — or a helper written and never called — leaves it red.
Version
0.149.0->0.150.0(minor). Behaviour is a fix, but the control-surface contract changes shape:addressbecomes optional. Minor rather than patch becauseSPEC.md's documented element shape changed;minor rather than major because omitting a field a consumer must already handle is compatible, and there
are no external consumers.
§2.4b — dep drift measured, NOT taken here, and why
dig-node-core is behind on five
dig-*crates, every one a semver-incompatible0.xstep:dig-natdig-tlsdig-peerdig-dhtdig-downloadPlus
dig-gossipis pinned by git rev (51054a41) rather than a published version — an NC-7violation and invisible to any registry freshness check.
A finding worth recording: even the one apparently "free" patch bump is not free.
cargo update -p dig-peer-selector(0.9.0 -> 0.9.2, caret-compatible, no manifest edit) rewrote 190 lock lines and leftthe lock carrying two
dig-natlines (0.18.0 and 0.20.0) and twodig-tlslines (0.3.1 and 0.4.0)while reporting success — 0.9.2 requires the new line. That is exactly the internally-split shape §2.4b
forbids, so it was reverted rather than shipped.
Taking the cascade would therefore mean adopting the whole
dig-nat/dig-tlsuplift inside a five-linerendering fix, in a repo with two live lanes (#344, #237). Per §2.4b's scope limit, stated and split
rather than bridged with a shim.
chia-*is untouched deliberately: the umbrella crate has no 0.36 line(0.26 -> 0.44+), tracked on #308, and
dig-walletis stranded on 0.26.Closes #253
Parent epic: https://github.com/DIG-Network/dig_ecosystem/issues/2760