An originator could misfile the answer to its own lookup as transit, so
discovery reported "requests went unanswered" while the replies were in
fact arriving and being reverse-path forwarded to a peer.
A request is flooded to every tree peer whose bloom filter claims the
target. At a high fill ratio a false positive sends a copy out into the
wider network, which can circulate it back to the originator. On arrival
`classify_request` applied its only identity test, `target == my_addr`,
which a lookup we originated never satisfies, so the copy was recorded in
`recent_requests` as an ordinary transit entry keyed on our own
`request_id`. When the target answered, `classify_response` consulted
`recent_requests` first, matched that entry, and relayed our own answer to
the peer that looped the request. The pending lookup was never satisfied,
the ladder retried with a fresh id, and the same race repeated.
The `None` arm's reasoning — "not a request we transited, so it claims to
answer one of ours" — held only while our own ids stayed out of the dedup
cache, and nothing enforced that. `resp_unsolicited` pinned at zero while
`resp_received == resp_forwarded` was the tell: control never reached the
originator arm. Affected nodes showed `resp_accepted` at 1 of 291.
It is a race, not a hard failure: when the genuine reply beats the looped
copy the cache is still clean and the lookup succeeds. It bites hardest at
the junction between a quiet subtree and a dense public network, where a
lookup for a descendant goes both correctly downward and outward through a
false positive, and the wide network returns it first.
Two changes close it, on both sides of the invariant:
- `classify_response` tests the pending lookups before `recent_requests`.
A response naming a target with a lookup outstanding, carrying an id
that lookup issued, is ours whatever the dedup cache holds. The id is
fresh 64-bit randomness per attempt and the target signs over it, so an
id we never issued still cannot match and the replay properties the
`Unsolicited` arm protects are unchanged.
- `classify_request` drops a request whose id a pending lookup for that
target issued, rather than recording it. Our own id never enters the
transit cache, so a duplicate reply cannot be misrouted after we accept
the first, and the returning copy is not forwarded a second time. The
guard keys on the id, so another node's lookup for the same target still
transits normally.
Verified against the reported deployment: discovery now completes where it
previously exhausted the four-attempt ladder.
Architectural and protocol-level explanations for FIPS — the why
and the how behind the wire and the system. For wire formats and
configuration keys, see reference/. For task
recipes, see how-to/. For end-to-end lessons, see
tutorials/.
Reading Order
Start with fips-concepts.md for the
novice-friendly framing of what FIPS is and why, then move to
fips-architecture.md for the protocol stack,
identity model, and two-layer encryption walkthrough. From there,
follow the protocol stack from bottom to top. After the stack,
fips-mesh-operation.md explains how the
pieces work together at runtime. Cross-cutting and supporting
documents cover specific subsystems in detail.