mirror of
https://github.com/lightningnetwork/lnd.git
synced 2026-08-16 13:00:19 +02:00
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:
parent
55cbf11332
commit
71e93d2dce
6 changed files with 2461 additions and 0 deletions
|
|
@ -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.
|
||||
|
|
|
|||
92
simulation/lab/experiments/exp-030-commands.md
Normal file
92
simulation/lab/experiments/exp-030-commands.md
Normal 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
|
||||
77
simulation/lab/experiments/exp-030-counters.json
Normal file
77
simulation/lab/experiments/exp-030-counters.json
Normal 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
|
||||
}
|
||||
}
|
||||
89
simulation/lab/experiments/exp-030-degmix-mechanism.md
Normal file
89
simulation/lab/experiments/exp-030-degmix-mechanism.md
Normal 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.
|
||||
2182
simulation/lab/experiments/exp-030-results-summary.json
Normal file
2182
simulation/lab/experiments/exp-030-results-summary.json
Normal file
File diff suppressed because it is too large
Load diff
BIN
simulation/lab/experiments/exp-030-tables.txt.gz
Normal file
BIN
simulation/lab/experiments/exp-030-tables.txt.gz
Normal file
Binary file not shown.
Loading…
Add table
Add a link
Reference in a new issue