Skip to content

feat: carry the PEX-known DIG peer count, not just the connected one - #10

Merged
MichaelTaylor3d merged 2 commits into
mainfrom
feat/2570-known-peer-count
Aug 11, 2026
Merged

feat: carry the PEX-known DIG peer count, not just the connected one#10
MichaelTaylor3d merged 2 commits into
mainfrom
feat/2570-known-peer-count

Conversation

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor

DO NOT MERGE — work in progress, lane is live

Early draft per the standing directive that all work carries a PR from the start.

Intent (#2570)

Surface the number of DIG-network peers this node knows of, not only those it is connected to.

PeerCountsResult currently carries dig_peer_count (connected) and chia_peer_count. The node already
computes known_count() via PEX, but the contract does not carry it, so dig-app cannot render it. On my
machine that is the difference between a useless number and a useful one: connected_peers: 0 while the
relay reports 6.

PEX already exists on both sides — nothing is being built here. The relay has
dig-relay/src/pex.rs (RLY-008), embedding dig_pex::PexEngine in its introducer role, one engine per
network_id, advertising only first-hand peers and never re-advertising node-sent data ("an introducer
is not a gossip amplifier"). The node has the node↔node half (#166). This change is exposure only.

The honesty constraint that decides the field name

No count anywhere is "the total DIG network." The node's known_count() is peers this node has
learned of
— a lower bound from one node's view. The relay's is peers registered with that relay
nodes on another relay, or none, are invisible to it. Naming either "total network peers" asserts global
knowledge from a local view, the same class of false claim as reporting a wrong balance. The field is
named and documented as view-scoped, and its doc states what it does NOT include.

Existing Option semantics are preserved: the contract already documents "0 is an observed zero;
null is unobservable"
— an unknown count must not render as zero.

Also being answered with evidence

connected_peers: 0 against relay.peer_count: 6 has two very different explanations: introductions
arriving and every dial failing, or introductions never arriving at all. The lane has a live node and
logs and is measuring which, rather than inferring.

Rendering belongs to #2569's lane, which owns the header — not this branch.

Refs #2570, #2569.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Lane report — and my brief's premise was WRONG

The field shipped, but not from PexEngineHandle::known_count() as I specified. That would have
produced a fabricated number.

Why: dig_pex::PexEngine::known_count() returns the set PEX advertises, and
spawn_pool_feeder mirrors the connected gossip pool into it (upsert_known on PeerAdded,
remove_known on PeerRemoved). On the node it is a mirror of connected_peers — so the new field
would have read "6 known / 6 connected" always, and permanently "0 known" in exactly the lonely-node
case this ticket exists to diagnose. That is the fabricated-number class the honesty constraint warns
about, and I walked the lane straight at it.

What it used instead: GossipStats::known_addressesAddressManager::size(), "total distinct
tracked nodes"
. A genuine superset of connected, populated by every discovery route (relay
introductions, PEX candidates, dig.getPeers), read from the snapshot the pool loop already takes, so
no new machinery.

Also relevant: the node's PEX engine is wired only to the mTLS node↔node listener. Relay-discovered
peers arrive by a different path (dig-nat RelayStatusattach_relay_status → address book), so a
PEX-sourced count would have been blind to relay introductions regardless.

The field, for the #2569 lane

pub known_dig_peer_count: Option<u32>,   // wire key: "known_dig_peer_count"

PeerCountsResult, contract 0.10.0. 0 = looked, book empty. null = could not look — peer network
down, first pool pass not yet run, or the node predates the field (serde maps the missing key to
None, so an old node does not break a new client).

Render as "known" / "discovered", never "total" or "network size". known >= connected is normal but
documented as not an invariant — do not assert it in UI logic.

The pair is the diagnostic: 0 connected / 41 known = a reachability fault; 0 / 0 = a discovery
fault. That distinction is the whole reason the field exists.

dign peers now prints DIG network 0 connected, 41 known · Chia full nodes 3.

Version-claim collision — the recurring shape

feat/2565-sent-notifications (0e065b6) also touches src/results.rs and also bumps to
0.10.0. Different regions (a field inside PeerCountsResult vs appended structs), so a textual
conflict is unlikely — but whichever merges second must re-bump to 0.11.0 or its version-increment
gate fails.

Two concurrent branches independently claiming the same version is now the third time this shape has
cost a round tonight.

Deliberately left undone

dig-node still pins dig-node-control-interface = "0.9". The node builds this payload with json!
rather than the typed struct, so it compiles and passes on 0.9. Release-first (§4.1): the contract
publishes 0.10.0 first, then the node bumps. Pinning an unpublished version now would red the CI.

Gates

Contract: fmt, clippy -D warnings (all targets + features), 84 tests + 1 doc-test.
dig-node: fmt, clippy clean, full workspace suite EXIT=0, 37 result lines, 0 failures — asserted on
the exit code, not on a tail through a pipe.

Four mutations, each applied to a committed tree by file copy and restored:

mutation caught by
#[serde(default)]Some(0) an_older_nodes_peer_counts_decode_with_the_known_count_unknown_not_zero
rename the wire key 3 tests incl. golden_response_result_vectors_are_byte_stable
source known_peers from connected_peers (the aliasing bug) knowing_of_peers_while_connected_to_none_survives_the_snapshot
drop the sampled flag (null0) an_unsampled_known_peer_count_is_null_not_zero

Unverified

The field's live value — everything is proven by test; nothing was measured against a running
0.110.0 node, because that needs a build+install cycle the disk could not fit. "known_addresses will
show > 0 on this host" is an expectation from backed_off: 3, not a measurement.

The investigation answer, and it is bigger than this ticket

(a) — introductions arrive and every dial fails. Root cause: the peers advertise IPv6-only
candidates and this host has no global IPv6 egress. Filed as #2576 (MVP): 348 dials, 0 pool peers,
158 established connections producing zero pool members and zero log lines.

@MichaelTaylor3d
MichaelTaylor3d force-pushed the feat/2570-known-peer-count branch from 544f503 to a5b23bd Compare August 10, 2026 20:17
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review August 11, 2026 00:37
MichaelTaylor3d and others added 2 commits August 10, 2026 20:29
…t holds

`PeerCountsResult` could say a node has zero DIG peers but not why: a node
connected to nobody looks identical to a node with nobody to connect to, and the
two are different faults with different remedies. `known_dig_peer_count` adds
the node's own discovered-peer address book size beside the connected count, so
a client can tell a reachability failure from a discovery failure.

The field is view-scoped by construction and documented as such. It is ONE
node's lower bound, never the size of the network -- naming it a total would
assert global knowledge from a local view. The SPEC forbids both deriving it
from `dig_peer_count` and presenting it as a network size.

Additive and optional in both directions: an older node omits it and decodes as
`null` (unknown, never a measured zero -- pinned by a KAT that a
default-to-zero implementation fails), and an older client ignores the extra key.

Refs dig_ecosystem#2570

Co-Authored-By: Claude <noreply@anthropic.com>
0.11.0 and 0.12.0 both landed on main while this branch was open, so the
version this change ships under moved twice beneath it. A new OPTIONAL field
on an existing result type is a compatible addition, so the bump from main's
0.12.0 is MINOR.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d
MichaelTaylor3d force-pushed the feat/2570-known-peer-count branch from a5b23bd to 6fc4155 Compare August 11, 2026 03:40
@MichaelTaylor3d
MichaelTaylor3d marked this pull request as draft August 11, 2026 03:41
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Rebased onto main (now v0.12.0) and re-gated.

  • Branch/HEAD: feat/2570-known-peer-count @ 6fc4155
  • Version: 0.13.0 (was 0.11.0 on the old branch). 0.11.0 and 0.12.0 both landed while this PR was open; a new OPTIONAL field on an existing result type is a compatible addition, so MINOR from main's 0.12.0. Cargo.toml and Cargo.lock both read 0.13.0.

Conflicts, both resolved as a UNION (neither side contradicted the other):

  • SPEC.md — kept this PR's known_dig_peer_count paragraphs AND main's five-phase WalletSyncStatusResult bullet (no_wallet_enrolled/wallet_not_unlocked + watched_addresses). This PR's copy of that bullet was pre-0.11.0 three-phase text and was discarded; re-introducing it would have un-documented two shipped phase tokens. kats::the_spec_and_readme_name_every_catalogued_method (which asserts every phase token appears in its quoted wire spelling) passes, which is the proof.
  • src/kats.rs — kept both blocks in full: main's wallet-phase tests and this PR's two peer-counts tests.
  • Cargo.toml/Cargo.lock — took main's side, then bumped to 0.13.0 and re-locked.
  • src/results.rs auto-merged; known_dig_peer_count: Option<u32> is the third field of PeerCountsResult.

Zero conflict markers remain (grepped after each step — git rebase --continue checks staging, not content).

Local gate, all green: cargo fmt --check; cargo clippy --all-targets -- -D warnings; cargo test 106 passed + 7 doctests, with both new tests executed by name (an_older_nodes_peer_counts_decode_with_the_known_count_unknown_not_zero, knowing_of_peers_while_connected_to_none_is_expressible); cargo doc --no-deps; cargo llvm-cov nextest --all --fail-under-lines 8098.41% lines (results.rs 98.15%, kats.rs 99.33%).

Left DRAFT deliberately — the orchestrator undrafts immediately before merging. Remaining: CI to terminal + the gate round.

@MichaelTaylor3d MichaelTaylor3d left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Correctness gate: PASS — head 6fc415543eb4bc2ea9de575deb150705e5d24a4b

Reviewed by reading (disk at 100%; no local build). CI green on this exact tip: 8/8 incl. Coverage 98.41%, Format/Clippy/Build/Docs, version-increment, commitlint, CodeQL x4.

1. Stale-duplicate trap — REFUTED

SPEC.md carries exactly one WalletSyncStatusResult bullet (line 337), and it is the five-phase version with watched_addresses:

{phase:"not_started"|"syncing"|"synced"|"no_wallet_enrolled"|"wallet_not_unlocked", peak_height:u32|null, chia_peer_count:u32|null, watched_addresses:u32|null}
…followed by "A conforming node MUST emit one of the five tokens above". No pre-0.11.0 three-phase copy survives anywhere in the file. Exactly one PeerCountsResult bullet (line 297). README: one control.peerCounts table row (117) + one prose mention (47, correctly updated to "three integers"), one control.wallet.syncStatus row (136).

2. Union resolution lost nothing

Test-function set diffed against BOTH parents: vs a5b23bd (pre-rebase) and vs origin/mainadditions only, zero deletions in either direction. HEAD's set = main ∪ a5b23bd ∪ the two new #2570 tests.

3. Compatibility claims are true, not merely asserted

  • No deny_unknown_fields anywhere in src/ (only the explanatory comment at src/kats.rs:833). Old-client-ignores-extra-field holds.
  • Option<u32> decodes an absent key as None by serde's own rule; an_older_nodes_peer_counts_decode_with_the_known_count_unknown_not_zero pins it and is not vacuous: a #[serde(default = …)]-style zero-default lands Some(0) and fails, and the fixture's non-zero dig_peer_count: 6 means an aliasing decoder lands Some(6) and also fails. Both wrong implementations are distinguished.
  • knowing_of_peers_while_connected_to_none_is_expressible decodes 0 / 3 / 41; an aliased decoder yields Some(0) known and fails.
  • each_peer_count_key_names_its_network additionally pins the exact key SET and the exact emitted byte string.

4. SemVer / derives — correct

Field is appended last, so wire order is unchanged ({"dig_peer_count":6,"chia_peer_count":3,"known_dig_peer_count":41}). No renames, no derive changes; Option<u32> is Copy, so Copy/Eq still hold. Under cargo's 0.x rules 0.12.0 → 0.13.0 is the compatibility-breaking bump, which correctly covers the source-break for downstream struct-literal constructors.

5. Versions / changelog

Cargo.toml 0.13.0 and Cargo.lock 0.13.0 agree. CHANGELOG.md untouched vs origin/main.

6. Emission ordering — satisfied

The 0.12.0 rule (SPEC.md:420-427) is scoped to new PHASE TOKENS, which break an older consumer's parse. An appended optional field cannot: with no deny_unknown_fields, an older client ignores it. SPEC.md documents the omission direction explicitly ("The field is OPTIONAL on the wire… a consumer MUST decode that omission as null").

Non-gating notes (no fix required to merge)

  1. src/kats.rs:757 — assertion message still reads "give the two counts independently"; there are now three. Cosmetic string only, inside a passing assertion.
  2. SPEC.md:297 block documents the omission direction but not the mirror (an older CLIENT ignoring the extra field) that the rustdoc at src/results.rs:~492 does state. A reimplementer reading only SPEC.md could over-apply the emission-ordering rule to this field. One sentence would close it.
  3. PeerCountsResult is not #[non_exhaustive] — consistent with every other type in results.rs, so this PR introduces no drift; noting only that each future additive field will keep costing a 0.x-breaking bump. Crate-wide decision, not this PR's.

No inline threads opened; zero unresolved threads on the PR.

@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review August 11, 2026 04:23
@MichaelTaylor3d
MichaelTaylor3d merged commit 5eac02c into main Aug 11, 2026
8 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the feat/2570-known-peer-count branch August 11, 2026 04:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant