Skip to content

fix(forwarded-ask): a hop's absence_established lie STRENGTHENS this node's claim, contradicting the NC-12 comment above it #508

Description

@MichaelTaylor3d

Task

crates/dig-node-core/src/seams/dig_peer/forwarded_ask.rs:437-439 asserts a monotonicity property
that the code does not have. Decide whether the CODE should be changed to match the claim, or the
CLAIM narrowed to match the code — and land whichever, with the test that pins it.

The comment, verbatim:

A hop can of course LIE here, like anything else it tells us (NC-12) — but the value can only
ever WEAKEN the claim this node goes on to make, never strengthen it. There is no direction in
which lying about this field buys reach.

The first sentence is false. The second is true.

The measured trace

A peer answering {"result":{"items":[{"available":false,"absence_established":true}]}} when its own
subtree search did NOT complete moves this node from inconclusive to proven-absent:

  1. forwarded_ask.rs:349-356SubtreeClaim::Established maps to AskOutcome::Answered(records);
    NotEstablished | NoClaim map to AskOutcome::AnsweredInconclusive(records).
  2. download.rs:2047-2064 — the Answered arm extends the records and does not clear
    answers.conclusive. Every other arm, including AnsweredInconclusive, sets it false.
  3. download.rs:1822-1829LocatedHolders { records, conclusive: first_hand_conclusive && forwarded.conclusive }.
  4. download.rs:973-975establishes_absence() is self.records.is_empty() && self.conclusive.
  5. download.rs:963-966 names the two production consumers: NodeContent::miss_outcome and
    Node::availability_answer. The second re-emits absence_established: true to the next hop,
    so this node republishes the lie as its own establishment and the falsehood travels.

So the honest answer (false, or the field absent) yields an inconclusive miss the requestor may
retry, and the lie (true) yields a settled not-found. That is a strengthening, in the exact
direction the comment says is impossible — and it is the manufactured not-found dig-node#273
exists to prevent, which this same doc block names as the harm four paragraphs earlier
(forwarded_ask.rs:425-428).

The file's own tests already demonstrate the mechanism. forwarded_ask.rs:1036-1050
(absence_established_is_read_as_three_states_and_absent_is_not_true) asserts
subtree_claim(Some(true)) == Established and subtree_claim(Some(false)) == NotEstablished. The
comment contradicts the assertions sitting 600 lines below it in the same file.

What bounds it, stated so severity is not overstated

  • The forwarded/recursive leg ships DISABLED, so a stock node never reaches this function.
  • answers.conclusive is an AND-fold over every peer asked, so a liar cannot manufacture an
    absence over an honest peer's objection. Its marginal power is to remove its own veto
    decisive exactly when it is the only otherwise-inconclusive voice, which includes the one-peer case.
  • The lie buys no ranking or conduct advantage, which is why the comment's second sentence is
    true: ask_routing.rs:178-190 folds Answered([]) and AnsweredInconclusive([]) both to
    dig_sex::AskOutcome::Inconclusive, and download.rs:2038-2044 folds both to
    ConductEvidence::HonestAnswer. A peer gains nothing by lying here except the one flip above.
    That equivalence is itself untested and is worth pinning in the same pass.

The fork this needs decided

Trusting one peer's absence_established: true at face value is a deliberate design choice
(SubtreeClaim::Established, forwarded_ask.rs:411), not an oversight — so this is not a
change to make inside a doc audit.

  • (a) Narrow the claim. Say what is actually true: a lie can remove the liar's own veto on
    conclusiveness, and nothing more; it buys no reach. Cheapest, and honest.
  • (b) Make the code match the claim — require corroboration before an established absence is
    relayed onward, which is NC-12's ~5-peers-must-agree shape applied to absence as it already is to
    content. Larger, and it changes the wire meaning of a conclusive miss.

Either way the outcome is pinned by a test that fails if the property changes.

Evidence bar

Whichever way it lands, the guard must fail for the RIGHT reason. A test that distinguishes
"this node relayed the peer's establishment" from "this node reached its own conclusion" needs
two distinguishable inputs and a pinned inequality — a single-value fixture cannot tell threading
from ignoring (the lesson from PR #353's relay fixture).

Provenance

Found by the NC-12 doc-claim audit, dig-node#356 / PR #504, which measured this claim rather than
rewording it. Not fixed there — the audit's scope discipline holds a security change out of a
sweep so the sweep stays gateable.

Parent: https://github.com/DIG-Network/dig_ecosystem/issues/3128
Audit: #356

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    kind:businessa person can DO something new, or money moves, or a shipped surface stops lying to them

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions