simulation/lab: exp-030, misattribution manufactures innocence

In this commit, we close the unknown-times-shift interaction with a
mechanism and a measured fix. The quarantine was being disarmed by
false exculpatory evidence: a shifted failure report writes a hard
lower bound on the guilty channel for the very amount it refused,
clearing it off the suspect list and concentrating suspicion on
innocent channels — one in ten convictions on the mix tier landed on
a channel that never failed, measured against the simulator's ground
truth. The fix narrows the trust boundary to the only evidence class
misattribution cannot forge: a settlement. ProvenOK, written by
settlements alone, replaces LowerOK in the quarantine's three
suppression rules, recovering the mix above its pre-quarantine
reference at no measurable cost anywhere else. The quarantine stays
in the release candidate.
This commit is contained in:
Olaoluwa Osuntokun 2026-07-30 17:19:32 -07:00
parent 55cbf11332
commit 71e93d2dce
6 changed files with 2461 additions and 0 deletions

View file

@ -1121,3 +1121,24 @@ third decimal, gaining under the production-default fee limit while
paying 267 fewer ppm. The WHY.md circularity caveat is now a
measured quantity, and the quantity is approximately zero. What
remains is replay, next week, on a real node's history.
## 2026-07-30 — exp-030: misattribution manufactures innocence
The unknown-times-shift interaction gave up its mechanism in one
ablation: the whole 0.034 loss is the quarantine, disarmed by
evidence the rest of the stack fabricates. A named failure writes
hard lower bounds on every hop before the reported index; a shifted
report names the wrong hop; so the guilty channel collects a bound
proving it can carry the amount it just refused, walks off the
suspect list, and leaves its 1/sqrt(n) share of suspicion
concentrated on the innocent. One channel in ten convicted on the
mix tier never failed at all, against zero under unknown-only — the
counters read the simulator's ground truth, so this is measured, not
inferred. The fix is a trust boundary, one field wide: only a
settlement proves innocence. ProvenOK, written by settlements alone,
replaces LowerOK in the quarantine's three suppression rules, takes
the mix ABOVE its pre-quarantine reference, and leaves the clean
tier identical to seven decimals. The quarantine keeps its place in
the release candidate, and the PR narrative swaps "unexplained
interaction on the record" for a mechanism, a fix, and a magnitude
caveat pending a wider corpus.

View file

@ -0,0 +1,92 @@
# exp-030, the deg_hard_mix interaction: every command
W = SCRATCH/exp030-degmix
ISIM = /Users/roasbeef/gocode/src/github.com/lightningnetwork/lnd-isim (tip ffe5e5537)
GEPA = /Users/roasbeef/gocode/src/github.com/lightningnetwork/lnd-gepa (LOCKED, live run)
**Nothing was committed.** The integration agent owns `interval-sim`. All
diagnostic edits live in a detached throwaway worktree at `$W/tree`, and the
worktree was `git checkout --`'d back to clean between every build. `$GEPA` was
never touched. Concurrency 4, `nice -n 10`.
## 1. Reading the mechanism before measuring it
grep -n "suspect\|quarantin" routing/interval_session.go routing/interval_belief.go
sed -n '845,1050p' routing/interval_session.go # ReportAttemptFailure
sed -n '400,500p' routing/interval_belief.go # normalize / recordSuspect
Three rules turned out to interact, all of them keyed on `LowerOK`:
1. a NAMED failure writes `RecordProbe` (a hard `LowerOK`) on every hop
BEFORE the reported index, and `RecordFailure` (a hard `UpperFail`) on the
index itself;
2. `recordUnattributedFailure` drops any candidate whose `LowerOK >= amt`
from the suspect list, and the weight each surviving suspect receives is
`1/sqrt(len(suspects))`, so a shorter list convicts faster;
3. `recordSuspect` early-returns on `LowerOK >= amt`, and `normalize`'s
contradiction rule calls `clearSuspect()` on `LowerOK >= SuspectAmt`.
## 2. Throwaway worktree and variants
git -C $ISIM worktree add --detach $W/tree ffe5e5537
cd $W/tree
go build -o $W/bins/routesim_V0 ./cmd/routesim # unmodified tip
# V1: quarantine off -- drop the RecordSuspectFailure call
# V2: no LowerOK written for hops before a named failure
# V3: new ProvenOK field, written only by recordSettlement; the three
# suppression rules read ProvenOK instead of LowerOK
# V4: a payment that has seen an unreadable failure stops recording
# forwarding evidence inferred from later named reports
# each: python3 patch -> go build -o $W/bins/routesim_V<n> -> git checkout -- routing/
## 3. Instrumented build (counters + ground truth)
# routing/exp030_counters.go (new, throwaway): package-level counters
# hooks in interval_session.go and interval_belief.go on every path the
# mechanism touches
# hook in sim_attribution.go recording the TRUE failing directed pair
# BEFORE degradation, plus a shift counter
# cmd/routesim/main.go: defer exp030Dump() -> one "EXP030 {json}" line on stderr
go build -o $W/bins/routesim_V0I ./cmd/routesim
git checkout -- routing/ cmd/routesim/ && rm routing/exp030_counters.go
## 4. Runs
cd $W && nice -n 10 python3 run.py round2 V0 V1_no_quarantine V2_no_probe_on_named
cd $W && nice -n 10 python3 run.py V3_probe_no_suppress V4_corroborate
# 300 cells, 0 errors; 32 replicates on deg_hard_mix, 16 elsewhere
python3 ... # the V0I counter pass, 1 run per file, 50 files
The reference arm `round2` is `SCRATCH/isim/bins/routesim_base`, the interval
router before rounds 3-6, which is the binary the loss was originally measured
against.
## 5. Stats
python3 $W/stats.py # -> results-summary.json (tiers, vs_round2, recovery)
python3 ... # -> vs_V0 block, each variant paired against the tip
python3 $W/verdict.py # -> counters, mechanism_verdict, fix_candidates
python3 ... # -> fix_surface, statistical_caveat
python3 ... > tables.txt
Bootstrap 10k percentile CIs at seed 20260729, two-sided exact sign tests, all
deltas paired per file. Two z statistics are reported and they answer different
questions: the paired bootstrap CI asks whether the effect would hold on other
FILES, the replicate z asks whether the code change did it on THESE files.
## Outputs
results-summary.json tiers, vs_round2, vs_V0, recovery, counters,
mechanism_verdict, fix_candidates, fix_surface,
statistical_caveat
tables.txt 5 sections
counters.json per-tier counter sums from the instrumented build
manifest.json the five reproducing tiers
run.py / stats.py / verdict.py
raw/ 300 cached per-cell aggregates (replicate means)
bins/ V0, V1, V2, V3, V4, V0I (all throwaway)
## Cleanup
git -C $ISIM worktree remove --force $W/tree

View file

@ -0,0 +1,77 @@
{
"hard_test": {
"clear_suspect_by_lowerok": 0,
"distinct_bounded_pairs": 412,
"distinct_true_fail_pairs": 0,
"hard_bounds_on_guilty": 0,
"hard_bounds_on_innocent": 552,
"named_hard_bounds": 552,
"probes_written": 3777,
"promotions": 0,
"shifted_attributions": 0,
"single_suspect_hard": 0,
"suspects_filtered_lowerok": 0,
"suspects_recorded": 0,
"zero_suspect_contradiction": 0
},
"deg_hard_unk20": {
"clear_suspect_by_lowerok": 77,
"distinct_bounded_pairs": 508,
"distinct_true_fail_pairs": 604,
"hard_bounds_on_guilty": 596,
"hard_bounds_on_innocent": 0,
"named_hard_bounds": 592,
"probes_written": 4534,
"promotions": 8,
"shifted_attributions": 0,
"single_suspect_hard": 4,
"suspects_filtered_lowerok": 791,
"suspects_recorded": 1243,
"zero_suspect_contradiction": 1
},
"deg_hard_shift10": {
"clear_suspect_by_lowerok": 0,
"distinct_bounded_pairs": 505,
"distinct_true_fail_pairs": 492,
"hard_bounds_on_guilty": 653,
"hard_bounds_on_innocent": 60,
"named_hard_bounds": 713,
"probes_written": 4870,
"promotions": 0,
"shifted_attributions": 74,
"single_suspect_hard": 0,
"suspects_filtered_lowerok": 0,
"suspects_recorded": 0,
"zero_suspect_contradiction": 0
},
"deg_hard_mix": {
"clear_suspect_by_lowerok": 133,
"distinct_bounded_pairs": 578,
"distinct_true_fail_pairs": 632,
"hard_bounds_on_guilty": 630,
"hard_bounds_on_innocent": 67,
"named_hard_bounds": 692,
"probes_written": 5257,
"promotions": 17,
"shifted_attributions": 79,
"single_suspect_hard": 5,
"suspects_filtered_lowerok": 1100,
"suspects_recorded": 1527,
"zero_suspect_contradiction": 2
},
"deg_hard_unk30": {
"clear_suspect_by_lowerok": 121,
"distinct_bounded_pairs": 444,
"distinct_true_fail_pairs": 574,
"hard_bounds_on_guilty": 523,
"hard_bounds_on_innocent": 1,
"named_hard_bounds": 516,
"probes_written": 3747,
"promotions": 11,
"shifted_attributions": 0,
"single_suspect_hard": 8,
"suspects_filtered_lowerok": 1086,
"suspects_recorded": 1922,
"zero_suspect_contradiction": 2
}
}

View file

@ -0,0 +1,89 @@
# EXP-030 — The unknown×shift interaction: misattribution manufactures innocence
**Date:** 2026-07-30.
**Status:** complete. Mechanism CONFIRMED by ablation and ground-truth
counters; fix (V3) measured and dispatched to the integration branch.
## The question
exp-027 round 6 left one anomaly on the integrated router's record:
deg_hard_mix (unknown 0.2 + shift 0.1) costs it 0.034 of objective
(z=11.1 against replicate noise) while unknown-only, shift-only, and
unknown-at-0.3 all sit at or ABOVE the pre-quarantine reference. The
two attribution failures together cost five times the sum of their
parts. With the ship target set, the mechanism had to be found before
an upstream reviewer found it.
## The mechanism, in three rules
The ablation is decisive: disable the quarantine and change nothing
else, and the mix recovers +0.0355 while every other tier stays put.
The whole loss is the quarantine — but not because the quarantine's
own logic is wrong. It is disarmed by evidence the rest of the stack
manufactures:
1. A NAMED failure writes a hard `LowerOK` on every hop before the
reported index — a hop that forwarded has proven it can carry the
amount. Sound only if the report is honest.
2. A SHIFTED attribution names the wrong hop. Blame shifted
downstream puts the true culprit before the reported index, so the
guilty channel collects a lower bound saying it can carry the
amount it just refused.
3. That false bound disarms the quarantine on the guilty channel
through all three of its `LowerOK`-keyed suppression rules (the
suspect-list filter, `recordSuspect`'s early return, `normalize`'s
contradiction clearing) — and removing the culprit from the
suspect list concentrates the `1/sqrt(n)` weight on the innocent
survivors.
Unknown alone never fires it: no shift, no false bound (0.0% of
promoted bounds land on innocent channels). Shift alone barely runs
the quarantine (zero suspects recorded — every failure names a
channel). Together: 9.6% of the mix tier's promoted hard bounds land
on channels that NEVER failed, more promotions than unknown-at-0.3
despite fewer unreadable failures, and the suppression rules firing
39-73% more often. The counters come from the simulator's ground
truth (the true failing channel pre-degradation), not from inference.
## The fix ladder
Four variants measured; V3 wins and is principled:
| variant | mix vs tip | unk20 | unk30 | clean |
|---|---|---|---|---|
| V3 proven-only | +0.0406 | 0.0073 | 0.0092 | 0.000009 |
| V1 no-quarantine | +0.0355 | 0.0050 | 0.0063 | +0.0000 |
| V2 no-probe-bounds | +0.0283 | 0.0593 | 0.0854 | 0.0005 |
| V4 corroborate | +0.0212 | 0.0452 | 0.0454 | 0.0000 |
V3: a `ProvenOK` field written only by settlements; the quarantine's
three suppression rules read it instead of `LowerOK`. `LowerOK` keeps
its full pathfinding role — it just stops counting as proof of
innocence, because a hop before a named index is only proven if the
naming is honest, which is precisely the assumption misattribution
breaks. A settlement is ground truth. V3 takes the mix to 0.5105,
ABOVE the pre-quarantine reference (0.5039), leaves the clean tier
identical to seven decimals, and gives back only an insignificant
sliver of the unknown-only gains. No variant found recovers the mix
AND keeps the full unk30 gain; V3 is the best point on the trade.
This also resolves the quarantine keep/drop question the data had
left open: with V3 the quarantine is positive or neutral on every
degraded tier, so it KEEPS (still severable via the exp-027-era gate).
## Caveats
The mechanism is established (ablation + ground-truth counters + the
replicate-noise z). The MAGNITUDE is pinned on one 10-file corpus
with large between-file variance — at n=10 the paired bootstrap
straddles zero for every variant including the original loss. A wider
degraded corpus should re-pin the numbers before the upstream PR
quotes them.
## Artifacts
`exp-030-results-summary.json` (ablation + fix tables, verdict),
`exp-030-counters.json` (the ground-truth attribution counters),
`exp-030-tables.txt.gz`, `exp-030-commands.md`. Throwaway variant
builds were never committed anywhere; V3 proper is being implemented
on interval-router with tests and doc updates.

File diff suppressed because it is too large Load diff

Binary file not shown.