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:
forwarded_ask.rs:349-356 — SubtreeClaim::Established maps to AskOutcome::Answered(records);
NotEstablished | NoClaim map to AskOutcome::AnsweredInconclusive(records).
download.rs:2047-2064 — the Answered arm extends the records and does not clear
answers.conclusive. Every other arm, including AnsweredInconclusive, sets it false.
download.rs:1822-1829 — LocatedHolders { records, conclusive: first_hand_conclusive && forwarded.conclusive }.
download.rs:973-975 — establishes_absence() is self.records.is_empty() && self.conclusive.
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
Task
crates/dig-node-core/src/seams/dig_peer/forwarded_ask.rs:437-439asserts a monotonicity propertythat 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:
The first sentence is false. The second is true.
The measured trace
A peer answering
{"result":{"items":[{"available":false,"absence_established":true}]}}when its ownsubtree search did NOT complete moves this node from inconclusive to proven-absent:
forwarded_ask.rs:349-356—SubtreeClaim::Establishedmaps toAskOutcome::Answered(records);NotEstablished | NoClaimmap toAskOutcome::AnsweredInconclusive(records).download.rs:2047-2064— theAnsweredarm extends the records and does not clearanswers.conclusive. Every other arm, includingAnsweredInconclusive, sets itfalse.download.rs:1822-1829—LocatedHolders { records, conclusive: first_hand_conclusive && forwarded.conclusive }.download.rs:973-975—establishes_absence()isself.records.is_empty() && self.conclusive.download.rs:963-966names the two production consumers:NodeContent::miss_outcomeandNode::availability_answer. The second re-emitsabsence_established: trueto 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 mayretry, and the lie (
true) yields a settled not-found. That is a strengthening, in the exactdirection the comment says is impossible — and it is the manufactured not-found
dig-node#273exists 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) assertssubtree_claim(Some(true)) == Establishedandsubtree_claim(Some(false)) == NotEstablished. Thecomment contradicts the assertions sitting 600 lines below it in the same file.
What bounds it, stated so severity is not overstated
answers.conclusiveis an AND-fold over every peer asked, so a liar cannot manufacture anabsence 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.
true:
ask_routing.rs:178-190foldsAnswered([])andAnsweredInconclusive([])both todig_sex::AskOutcome::Inconclusive, anddownload.rs:2038-2044folds both toConductEvidence::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: trueat face value is a deliberate design choice(
SubtreeClaim::Established,forwarded_ask.rs:411), not an oversight — so this is not achange to make inside a doc audit.
conclusiveness, and nothing more; it buys no reach. Cheapest, and honest.
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