Skip to content

feat(digsex): produce real conduct evidence and persist the reward ledger - #515

Merged
MichaelTaylor3d merged 5 commits into
mainfrom
loop/268-271-digsex-conduct-ledger
Sep 3, 2026
Merged

feat(digsex): produce real conduct evidence and persist the reward ledger#515
MichaelTaylor3d merged 5 commits into
mainfrom
loop/268-271-digsex-conduct-ledger

Conversation

@MichaelTaylor3d

@MichaelTaylor3d MichaelTaylor3d commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

DO NOT MERGE - gate round in progress. Head 9ff1461a.

What this PR ships, and what it deliberately does not

Documentation only. A version bump to 0.252.6, a re-lock, and one new normative section in SPEC.md. No behaviour change, no .rs edit.

That is the honest outcome of taking dig-node#268 and dig-node#271 as one family: after measuring both, neither ticket has a safely-buildable code change in this repo today, and building one anyway would have meant shipping either a censorship primitive or a vacuous record. The measurement itself is the deliverable, and this PR makes the durable half of it normative so the next lane does not re-derive it.

Refs #268 · Refs #271

#268 — conduct: the gate is real, correct, and dormant

dig_sex::dial_share returns a hard 0.0 only when proven_faults > 0. proven_faults moves only on ProvenLie or SelfContradiction, and neither is constructed anywhere outside conduct.rs's test module — the single non-test observe call site (download.rs:2035) produces only HonestAnswer and NonPerformance, which floors at 0.1. So proven_faults is always zero, every peer always passes, and dialable() returns its whole input set unconditionally. The exclusion mechanism is implemented, correctly tested, and unreachable.

Both routes out were measured; neither is safe here.

  • ProvenLie — blocked cross-repo. dig-download already surfaces a peer_id, but folds a verification failure and a transport failure into one RangeResult::Failed (select.rs:117-125, folded at orchestrator.rs:986-996); dig-node's own adapter records the loss at selector_adapter.rs:109-115. Because proven_faults never decays, wiring from today's Failed would let an attacker who merely resets a connection permanently zero an honest peer's dial share. Filed and scoped, with the precise change it needs, at dig-download issue 49: Surface a per-peer verification verdict so a proven lie is attributable to one holder dig-download#49.
  • SelfContradiction — data available, deliberately not built. Both halves exist on the right identity (forwarded_ask.rs's responder_holds/responder_record plus FirstHandHolderCache). It stays unproduced because a legitimate eviction is a retraction (dig-sex SPEC 7.1) whose announce is flooded rather than acknowledged, so a cached claim can outlive it. An attacker can force an honest peer to evict under load and race a correlating asker — branding it with a penalty that has no floor and no decay.

What was considered and rejected: making conduct RANK the dial order. dig_sex::routing::rank already ranks the fan-out on observed quality and already proves dig-sex SPEC 8.2A's ordering clause in its own tests (an_inconclusive_answer_ranks_above_silence, a_good_answerer_outranks_an_unobserved_peer_which_outranks_a_silent_one). A dial_share ordering on top would be a third reputation computation over the same two events from the same call site, in a second state map that can drift, and it would risk starving the reserved exploration slot that lets an unobserved peer earn a score.

#271 — the reward ledger cannot be persisted from here

RewardLedger (dig-sex 0.5.0 reward.rs:78) is a private HashMap with no serde derives and no accessor over its entries. A caller physically cannot write it out. dig-sex SPEC 2A.1 states this and names the fix as an upstream public-API change that "MUST be specified here before it is built"DIG-Network/dig-sex#8 (open, unstarted).

The workaround — journalling the RewardClaims fed to record() and rebuilding via from_chain_claims — was not built. It is a rival persistence path landing before the shape is specified, and it would be vacuous at both ends anyway: nothing produces a RewardClaim (claim mechanism deferred, out of scope by the ticket's own wording) and nothing consumes claimed_for (SPEC 2.4 — Tier2Bribed has no producer anywhere, and the money budget its fourth requirement needs does not exist at the seam).

Separately measured and recorded on the ticket: the sealing constraint as worded ("sealed at rest to the user key, NC-2/NC-3") is not buildable — the node never holds the user's keypair (key_mgmt/machine_key.rs:9-11, the §908 boundary), there is no generic sealed-at-rest helper (shared/at_rest.rs does presence checks and atomic writes with zero encryption), and every other persisted node file is plaintext.

The section added

A new ### 10.5. in SPEC.md binding: the three distinct per-peer models and that conduct MUST NOT become a fourth ranking input; that non-performance MUST NEVER exclude; that the exclusion arm is vacuously satisfied rather than satisfied; why each verifiable class stays unproduced and that neither may be synthesised unsafely; and that conduct state is node-local, unpersisted, bounded by retain(pool), and MUST fail to neutral if ever persisted.

Stating vacuity explicitly is the house style here — dig-sex's own SPEC does it in 2.4 and 2A.1, and a conformance claim that passes because the thing it governs never occurs reports a capability the system does not have.

Verification

  • Baseline cargo check -p dig-node-core --lib on the branch base: exit 0.
  • Version 0.252.6 read from Cargo.toml on disk; origin/main is 0.252.4. Cargo.lock re-locked to match.
  • The version was deliberately lowered from the assigned 0.262.0. dig-node#521 measured that scripts/package-version.sh fails closed above minor 255 (Windows Installer's ProductVersion cannot carry it, and a truncated field would make two releases compare equal). main is still under the ceiling at 0.252.4, so the first merge of any PR at >= 0.256.0 is what makes every subsequent release ship with no Windows installer - and the four native-package jobs are not required checks, so nothing would have blocked that landing silently. A documentation-only change is a patch under this repo's SemVer rule and had no business spending the ceiling. 0.252.5 is taken by fix(forwarded-ask): a hop's absence_established must not strengthen this node's absence claim #516, so this takes 0.252.6.
  • Ordering note for whoever merges: fix(forwarded-ask): a hop's absence_established must not strengthen this node's absence claim #516 also sits on a patch bump (0.252.5). Whichever of the two lands first, the other must re-bump - and after any rebase the version must be re-read from Cargo.toml on disk, never from the commit log, because a rebase can silently drop a bump commit whose patch is already upstream.
  • The four Native install packages jobs are red on every open dig-node PR branch (6 lanes, 12/12 recent runs), all failing at the same Resolve + validate the package version step. That is dig-node#521, pre-existing and repo-wide, not caused by this diff.
  • Conduct test module: 8 passed, 0 failed, 1093 filtered out (cargo test -p dig-node-core --lib seams::dig_peer::conduct) - a real count, not a filter that matched nothing.
  • SPEC.md verified valid UTF-8, zero CRLF.
  • One normative sentence in the new section stated the decay rate inverted on first draft ("one 600-tick step per elapsed tick"); caught on read-back and corrected in c3652efe to "one unit per 600 elapsed ticks", which is what dig_sex::decay actually computes.
  • Independent loop-reviewer and loop-security gates run in fresh contexts; the security gate was asked directly "what can a hostile peer make an honest peer's score look like?" and asked to mark each normative claim in the new section TRUE / FALSE / UNVERIFIABLE.

Not done, on purpose

No producer for either verifiable evidence class; no reward-ledger persistence; no ranking change; no SelfContradiction correlator. Each is recorded with its blocker on the ticket it belongs to, cross-linked both ways.

…er family

Anchor commit for dig-node#268 (conduct evidence producer) and dig-node#271
(reward ledger persistence). Both land on this one branch: they share the
dig-sex seam and the same node-state persistence question.

Co-Authored-By: Claude <noreply@anthropic.com>
MichaelTaylor3d and others added 3 commits September 2, 2026 17:58
Co-Authored-By: Claude <noreply@anthropic.com>
Adds SPEC 10.5 stating the three separate per-peer models (conduct
exclusion, ask-routing rank, content-source rank), the non-performance
floor that prevents an eviction-by-degradation attack, and that the
verifiable-exclusion arm (ProvenLie/SelfContradiction) is currently
vacuously satisfied rather than exercised in production.

Co-Authored-By: Claude <noreply@anthropic.com>
The clause read 'one NON_PERFORMANCE_DECAY_TICKS-sized step per elapsed
tick', which inverts the arithmetic and would have shipped as a false
normative claim. dig_sex::decay computes periods = elapsed /
NON_PERFORMANCE_DECAY_TICKS and subtracts that from non_performance, so
one unit decays per 600 ticks of elapsed time.

Also records the property the remainder-carry exists for: the penalty is
a function of elapsed time alone, so a frequently-observed peer cannot be
held un-decayed by the observation traffic itself.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Headline answer

A hostile peer cannot make an honest peer's conduct score reflect anything it did not itself do:
the sole write path (download.rs:2035) is fed only from an exchange THIS node initiated and
observed complete, keyed by the mTLS-verified RoutedPeer (SHA-256(TLS SPKI DER)), with no
gossip/relay/provider-record/DHT-contact path able to construct that key or feed an observation
about a third party. The worst an attacker can do to an honest peer today is induce sustained
NonPerformance (by loading it, occupying its slots, or partitioning it), which floors that
peer's dial share at MIN_NON_PERFORMANCE_DIAL_SHARE = 0.1 -- never zero -- and self-heals on
elapsed ticks alone the moment the pressure stops, with no action required from the victim. The
durable, zero-share exclusion (proven_faults > 0) exists in the pure crate but has NO
production producer anywhere in dig-node -- it is correctly vacuous, not incorrectly wired.

  1. Induced non-performance / decay arithmetic -- dig-sex-0.5.0/src/conduct.rs:158-178
    (decay). Verified by hand-tracing the arithmetic: last_update_ticks only advances by
    periods * NON_PERFORMANCE_DECAY_TICKS (whole consumed periods), never snapped to now_ticks
    while non_performance > 0, so a sub-period remainder is carried forward across arbitrarily many
    observe() calls -- confirmed against the crate's own
    sub_period_observations_accumulate_toward_the_next_decay test (conduct.rs:281-297), which
    pins exactly the remainder-discard bug the brief asked me to check for. No such bug exists. A
    peer observed more often than once per decay window (600 ticks/10 min) never resets its own
    clock via being talked to -- confirmed by a_distressed_peer_recovers_on_time_alone
    (conduct.rs:245-272), which is exactly the "chatty vs quiet, same span" control the brief's
    suspicion would require to falsify.
    Floor: dial_share() (conduct.rs:189-195) is (1.0 - penalty).max(0.1); with
    NON_PERFORMANCE_CEILING = 8 and +1 per observed failure, sustained pressure caps
    non_performance at 8, penalty 1.0, share pinned at exactly 0.1, never lower, for as long as
    the attacker sustains it, and only for as long as. Read-time decay
    (dig-node-core/src/seams/dig_peer/conduct.rs:84-91, dial_share()) recomputes from the
    persisted stamp without mutating the stored record, so a read never double-applies decay and a
    write (observe) always decays-then-applies against the true persisted stamp -- no discrepancy
    between what a read reports and what the next write will compute from.
    conduct_ticks() (download.rs:2089-2091) is Instant::elapsed().as_secs() -- a per-process
    monotonic clock, immune to wall-clock/NTP skew and unable to run backward.

  2. Third-party attribution -- NONE FOUND. The sole non-test producer,
    download.rs:2035-2046, observes routed, which is one of the peers returned by
    self.ask_routing.decide(...) at download.rs:1930-1937, itself drawn from dialable
    (download.rs:1925, itself routable filtered from the connected-pool keys,
    download.rs:1915-1918). Every RoutedPeer in that chain traces to
    RoutedPeer::from_pool_key (ask_routing.rs:79-83), whose only writer is
    PoolEvent::PeerAdded's peer_id.to_hex() (download.rs:1689,1709; peer.rs:2261-2293) --
    which dig-gossip computes from the verified mTLS handshake, not from anything a peer's payload
    states. holder_cache.rs (provider/holdings records) has zero references to RoutedPeer or
    ConductEvidence -- grepped and confirmed no overlap exists between the provider-record/DHT
    surface and the conduct surface. There is no frame field, gossip message, or relayed-answer path
    that can name a peer other than the one this node itself dialled and measured.

  3. Identity control / rotation cost -- Rotating identity (a fresh keypair, a fresh
    SHA-256(SPKI DER), a fresh, unpenalised ConductRecord::neutral()) is cryptographically free.
    Today this buys an attacker NOTHING, because proven_faults never becomes nonzero in production
    (see Q6) -- there is no durable penalty to shed. The only standing defense against future
    rotation-to-evade (once issue Wire dig-sex conduct (SPEC 8.2A) so peer behaviour actually feeds the dial budget #268's real ProvenLie producer lands) is the cost of being
    re-admitted to this node's connected pool, which ask_routing.rs:25-26 states as an assumption
    ("costs a keypair AND admission to this node's connected pool") but which this seam does not
    itself enforce -- that enforcement lives in dig-gossip's pool admission, outside this PR's/crate's
    scope. Flagging as a forward-looking dependency, not a defect in the audited code.

  4. Unbounded state -- Bounded, and bounded BEFORE new work in the same call.
    ConductState::dialable() (conduct.rs:104-110) calls self.retain(pool) (conduct.rs:113-115)
    FIRST, then filters; retain drops every map entry whose key is not in the CURRENT pool.
    The only place the map grows (observe, called once per dialled peer inside the SAME
    handle_forwarded_ask-style call that computed dialable moments earlier) can only insert keys
    already present in dialable (a subset of the current pool) -- so the map's size is bounded by
    current pool size immediately before AND after every mutating call; a stranger not admitted to
    the connected pool cannot cause an entry to exist at all, and churning through the pool cannot
    grow it past pool capacity at any observable instant.

  5. Reserved exploration slot -- dig-sex-0.5.0/src/routing.rs:293-329 (select_fan_out).
    Exactly ONE slot is reserved, only when fan_out >= 2, only when none of the top-fan_out
    ranked candidates is already unobserved. An attacker could in principle churn fresh identities
    (each connects, looks unobserved, wins the tiebreak against a node-local seed it cannot
    observe, gets asked once) to repeatedly claim that one slot -- but (a) being asked at all
    immediately records an outcome (ask_routing.record, download.rs:2022) which makes that
    identity observed and ineligible for the unobserved bucket on the NEXT decision, so no single
    identity can camp the slot; (b) each fresh claim costs a live, currently-admitted pool
    connection, a scarce resource gated by dig-gossip's own pool admission (out of this crate's
    scope); (c) the damage from winning is bounded to 1 of fan_out dials on a SINGLE forwarded ask
    -- the remaining fan_out minus 1 slots stay ranked-and-earned. This is bounded amplification,
    not an eclipse: it never displaces more than one slot, never persists across asks without
    repeated live-connection cost, and the crate's own tests
    (an_unobserved_peer_always_holds_a_slot_in_the_slate,
    exploration_reaches_every_peer_in_the_pool, routing.rs:468-520) pin exactly the only-one-slot,
    and-it-rotates property. Not gating on this; noting it as a place where a future
    connection-churn rate limit at the pool-admission layer would remove the residual amplification
    entirely.

  6. proven_faults dormancy -- Grepped the ENTIRE dig-node workspace
    (grep -rn "ConductEvidence::" crates/) for every construction site outside cfg(test):
    exactly one file, download.rs:2039,2042, constructing only HonestAnswer/NonPerformance.
    ProvenLie/SelfContradiction appear nowhere outside test modules in the entire crate tree I
    can see from this worktree. So: yes, proven_faults is provably always zero in a running node,
    and dialable()'s greater-than-zero filter (conduct.rs:108) never excludes anyone today. Leaving
    it dormant is the RIGHT posture, not a bug to silently patch: download.rs:2029-2034's own
    comment states why -- attributing a ProvenLie from a transport-layer error would brand an
    honest peer on unverifiable evidence, which is the exact conflation SPEC 8.2A exists to
    prevent, and the real per-peer attribution of a chunk-hash verification failure lives inside
    dig-download's engine and is not currently surfaced across this seam. The correct fix (in
    scope for issue 268, NOT done in this PR) is a real producer wired from that attribution, not
    a shortcut that infers a lie from an unverifiable signal. I found no path I have missed that
    could set it in production today.

Adjacent finding, not requested, worth recording: a third quality model is also vacuous, in the safe direction

dig_peer_selector::PeerSelector (via selector_adapter.rs) is a THIRD independent per-peer
model (claim g). Its own hard-failure floor (is_bad_source, drives a peer below cold-peer
rank) requires FailureReason::VerificationFailed
(dig-peer-selector-0.11.0/src/types.rs:218-219, is_hard()), but
selector_adapter.rs map_range_result (lines 122-132) maps dig-download's RangeResult::Failed,
which the comment there says conflates a genuine transport failure AND an actual verification
failure at this specific seam, to the SOFT FailureReason::Transport, never to
VerificationFailed. Grepped the whole crate for record_outcome and VerificationFailed: the
adapter is the only caller, and it never constructs VerificationFailed. So PeerSelector's hard
floor is ALSO vacuous in dig-node's real wiring today, for the same underlying reason as
conduct's proven_faults: per-peer verification-failure attribution exists inside dig-download
but is not surfaced across either seam. This is the safe direction of failure, under-detection
of a real liar, never over-attribution against an honest peer from a timeout or transport
signal, so it is not a live censorship risk, but it means two of the ecosystem's exclusion
mechanisms are dormant for the identical missing wire. Worth one tracking ticket so the
eventual issue-268 fix considers feeding both seams from the same real attribution rather than
fixing one and leaving the other silently dark.

Claims (a) through (g) -- verdicts

(a) TRUE. dial_share returns 0.0 only when proven_faults > 0 (conduct.rs:190-191); the floor
0.1 and the 600-tick decay constant are exact (conduct.rs:105,109,116).

(b) TRUE. Confirmed by grep across the whole crates tree: the only non-test observe call site
is download.rs:2035, producing only HonestAnswer and NonPerformance.

(c) TRUE. proven_faults is always zero in a running node; dialable() returns its whole input
unconditionally as a direct consequence.

(d) TRUE. ConductRecord and ConductEvidence carry no serde derive; ConductState derives only
Debug and Default; it is constructed exactly once (download.rs:1419) and never read or written
by anything outside this module. A restart resets it.

(e) TRUE. retain(pool) runs at the head of dialable() (conduct.rs:105) before any read or
write in that call, so the map is bounded by current pool membership at every observable
instant, not merely eventually.

(f) TRUE. Grepped every RoutedPeer construction site in the crate: only from_pool_key (fed
solely by PoolEvent::PeerAdded's mTLS-verified peer_id) and the nobody() sentinel. No
constructor exists from an address, a provider record, a dig-dht Contact, or a frame field.

(g) TRUE, with a caveat worth stating explicitly. Three independent models is correct in
count. They do not currently disagree in failure direction, but only because two of the
three durable/hard exclusion paths (conduct's proven_faults and PeerSelector's
is_bad_source/VerificationFailed) are BOTH vacuous today, for the same underlying reason. A
future fix that wires one without the other would need to re-check this claim.

PR-state note, separate from the security verdict

This PR (loop/268-271-digsex-conduct-ledger, head cdcf1f6)
is a draft anchor commit only, explicitly marked "WORK IN PROGRESS -- DO NOT MERGE, gate round
has not run" in its own body. It carries no SPEC.md change and no behavioural change yet. This
gate's PASS applies to the mechanism as shipped on origin/main today, not to any future diff
this PR will eventually carry. When the real producer and the reward ledger land, this seam
needs a fresh gate against that diff, in particular re-checking claims (c) and (g) once
proven_faults gets a live producer.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

loop-security VERDICT

PASS (on the shipped mechanism). Head audited: cdcf1f6
(loop/268-271-digsex-conduct-ledger).

Full findings are in the preceding comment on this PR. Summary:

  • No hostile peer can make an honest peer's conduct score reflect anything the honest peer did
    not itself do. The sole producer (download.rs:2035) is keyed by the mTLS-verified peer_id and
    fed only from exchanges this node itself issued and observed; there is no gossip, relay,
    provider-record, or frame-field path by which one peer's behaviour is attributed to another.
  • The worst durable consequence an attacker can inflict on an honest peer today, by sustained
    load/partition/slot-occupation, is a dial-share floor of 0.1 (never zero), which self-heals on
    elapsed monotonic ticks alone the instant the pressure stops. Hand-traced the decay/remainder
    arithmetic myself; found no truncation bug.
  • The durable, zero-share exclusion path (proven_faults) has no production producer anywhere in
    the dig-node workspace (grep-confirmed across every crate). Leaving it dormant is the correct
    posture given today's wiring, not an oversight to silently patch.
  • The conduct map is bounded by current pool membership, checked before any mutating work in
    every call path that touches it.
  • The reserved exploration slot admits a bounded, self-limiting amplification (one slot, one ask,
    requires a live paid-for pool connection per attempt) -- not an eclipse.
  • Claims (a) through (g) as drafted for publication: all TRUE, with one caveat recorded on (g) --
    the "three models agree" property currently holds partly because two of the three durable
    exclusion paths are both vacuous for the same missing wire (dig-download's per-peer
    verification-attribution is not surfaced to either the conduct seam or the peer-selector seam).
    That is a note for the eventual fix, not a defect in what ships today.

Nothing here rises to CHANGES-REQUIRED. The one adjacent, unrequested finding (PeerSelector's own
hard-failure floor is equally vacuous, in the safe/under-detecting direction) is defense-in-depth
information, not a live exposure -- recommend a follow-up ticket, do not gate on it.

Separately, and this does NOT change the verdict on the mechanism: the PR itself is a draft
anchor commit only (Cargo.lock/Cargo.toml version bump, 2 lines each), explicitly marked
"WORK IN PROGRESS -- DO NOT MERGE, gate round has not run" in its own body, with no SPEC.md
change landed and no behavioural diff to gate. There is nothing in this PR's actual diff for a
security gate to pass or fail. This PASS certifies the shipped mechanism the brief asked about;
it is not a certification of a future diff. Re-gate when the real conduct producer and the
reward ledger actually land, especially claims (c) and (g) above once proven_faults gets a live
producer.

dig-node#521 measured that scripts/package-version.sh fails closed above
minor 255, because Windows Installer's ProductVersion field cannot carry
it and a truncated field would make two releases compare EQUAL. main is
at 0.252.4 and still under the ceiling, so the first merge of any PR at
>= 0.256.0 is what makes every subsequent release ship with no Windows
installer. The four native-package jobs are not required checks, so
nothing would have blocked that landing silently.

This PR is documentation only, which is a patch under the repo's own
SemVer rule, so it has no reason to be the change that spends the
ceiling. 0.262.0 was assigned to dodge collisions with the 0.253-0.256
lanes; 0.252.6 dodges them equally (0.252.5 is taken by #516) while
staying buildable.

Also disambiguates a SPEC 10.5 citation flagged by the review gate: the
decay line numbers referred to dig-sex's conduct.rs, but every other bare
filename in that section resolves to dig-node's own conduct.rs, where
those lines are unrelated test code. Names the crate instead.

Co-Authored-By: Claude <noreply@anthropic.com>
@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

loop-security ADDENDUM -- head moved during audit, re-verified

The head moved twice while I was auditing: cdcf1f6... to c3652ef.
Two new commits landed, SPEC.md only, zero source-file changes:

  • a01e902 added SPEC 10.5 (the normative section the original brief described).
  • c3652ef immediately corrected a decay-rate error introduced in that same section (it had read
    "one NON_PERFORMANCE_DECAY_TICKS-sized step per elapsed tick", which inverts the arithmetic;
    fixed to "one unit decays per 600 ticks of elapsed time").

Since only SPEC.md changed, my source-level audit (the two preceding comments) still applies
unchanged. I additionally verified the new normative text's factual claims against the actual
code rather than taking them on trust:

  • The corrected decay-rate wording now matches dig_sex::decay exactly, as I independently
    hand-traced it in the first comment.
  • forwarded_ask.rs:364 (responder_holds) and :384 (responder_record) match the SPEC text's
    description exactly -- checked by direct read, not by trusting the SPEC's own citation.
  • The vacuous-exclusion-arm wording matches my own grep-confirmed finding precisely.
  • The SelfContradiction section is explicitly forward-looking (a constraint on a FUTURE
    producer, not a claim about current behavior) and its constraint is sound: it correctly
    identifies that a peer forced to evict under load, then raced by a correlating ask before its
    retraction propagates, could otherwise be branded with a non-decaying penalty. This is a good
    catch and matches the "never brand from an unverifiable signal" property the rest of the
    mechanism already holds elsewhere.

VERDICT UNCHANGED: PASS, now confirmed at head c3652ef. The
SPEC.md text now on this branch is an accurate normative description of the shipped mechanism,
not an aspirational or born-false one. No source code changed, so nothing new to gate.

If the head moves again before this merges, re-run check-merge-preconditions and re-diff against
c3652ef before trusting this PASS.

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

Gate 1 of 2 — loop-reviewer (fresh context): PASS

Verified against head c3652efe (the substance is unchanged in 9ff1461a, which only lowers the version and disambiguates one citation).

All eight normative claims in the new SPEC.md 10.5 came back TRUE with file:line:

claim verdict
(a) dial_share returns 0.0 only when proven_faults > 0; non-performance floors at 0.1, one unit decays per 600 elapsed ticks, remainder carried TRUE
(b) no production path constructs ProvenLie/SelfContradiction; sole non-test observe at download.rs:2035-2046 TRUE
(c) therefore proven_faults is always zero and dialable() returns its whole input — vacuous TRUE
(d) ConductState never gossiped, never persisted (no serde derive) TRUE
(e) bounded — dialable() calls retain(pool) at its head TRUE
(f) RoutedPeer constructible only from a pool key (ask_routing.rs:79) TRUE
(g) three distinct per-peer models, no shared state TRUE
(h) the verify/transport collapse is rooted in dig-download's engine, not dig-node's adapter TRUE, and correctly attributed

It also independently confirmed the two judgement calls rather than taking them on trust:

  • Rejecting a dial_share ranking is correct, not a missed opportunity. routing::rank (routing.rs:269) and its two cited tests are real and do what is claimed; a second ordering would be a third computation over the same two events from the same call site, risking drift against routing::rank and against the reserved exploration slot.
  • Wire the dig-sex reward ledger (SPEC 2A) -- record and persist; claim mechanism still blocked #271 is genuinely blocked, not avoided. RewardLedger has private fields, no serde, no entry accessor; zero references to RewardClaim/RewardLedger/claimed_for exist anywhere in dig-node production code, so the journal-and-rebuild workaround would indeed have been vacuous. The sealing-boundary reasoning checks out against machine_key.rs.

Its verdict on the shape of this hand-back, quoted because it is the question I most wanted challenged: "every 'should have been buildable' alternative was checked and correctly rejected... This is honest engineering, not a stop for convenience."

One defect found and fixed in 9ff1461a: the decay citation read (conduct.rs:158-178), but every other bare filename in that section resolves to dig-node's own conduct.rs, where those lines are unrelated test code — the decay function lives at those line numbers in the dig-sex crate. A confusable pointer in a normative document is worth the one-line fix; it now names the crate.

Gate 2 (loop-security, fresh context) is still running. It was asked directly: what can a hostile peer make an honest peer's score look like?

@MichaelTaylor3d

Copy link
Copy Markdown
Contributor Author

loop-security FINAL VERDICT

PASS. Head re-confirmed stable at 9ff1461 (fetched, diffed,
CI running against it).

The head moved twice more after my addendum: 9ff1461 changes only Cargo.lock/Cargo.toml
(a version-number correction, 0.262.0 to 0.252.6, to stay under dig-node's Windows-Installer
version ceiling -- unrelated to this audit's subject) and one SPEC.md line disambiguating a
citation (dig-sex's conduct.rs vs dig-node's own conduct.rs -- both files share the name, and
the fix names the crate explicitly). No further source-code changes. I checked the corrected
citation against both files myself: dig-node-core's conduct.rs really does have unrelated
test code at the old line numbers, so the disambiguation was necessary and is now correct.

Nothing in this round changes any prior finding. Full analysis is in the three preceding
comments on this PR (findings, verdict, and the first addendum). Verdict stands: PASS on the
shipped dig-sex conduct mechanism, and the SPEC 10.5 text now on this branch is an accurate
normative description of it, not an aspirational or born-false one.

This PR remains correctly in draft while its own CI runs. Recommend undrafting only once every
required check is green and the version-increment gate is confirmed on this final SHA.

@MichaelTaylor3d
MichaelTaylor3d marked this pull request as ready for review September 3, 2026 02:42
@MichaelTaylor3d
MichaelTaylor3d merged commit b6f3e0d into main Sep 3, 2026
16 checks passed
@MichaelTaylor3d
MichaelTaylor3d deleted the loop/268-271-digsex-conduct-ledger branch September 3, 2026 02:42
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