3.9 KiB
REPORT — escrow recovery-code wordlist fix (TASK-E Part 1), 2026-07-21
Version: v0.93.0. Baseline 935904f (v0.92.1), clean tree. NOT built or deployed — no operator
action needed today; this rides the next agent publish train.
What was wrong
The EFF large wordlist contains exactly four entries that themselves contain the hyphen the words are
joined with: drop-down (L2009), felt-tip (L2528), t-shirt (L6640), yo-yo (L7748). Drawing one
produced a recovery code that reads as 11 words instead of 10 — ambiguous to transcribe in the one
situation R exists for, a customer reading a code back during a disaster.
The "known flake" was this defect, not a flaky test. Per code the hit rate is
1 − (7772/7776)^10 ≈ 0.51%; over the test's 50 draws that is ≈ 23%, which is exactly the "fails
~1/5" that had been documented in the felhom-testing skill and re-run past for weeks.
felhom.eu/REPORT.md §6 had it at 3/8 in one session.
The fix
joinSafe filters those four out of the effective wordlist at init. Generation-only.
| before | after | |
|---|---|---|
| draw space | 7776 | 7772 |
| 10-word code entropy | 129.248 bits | 129.241 bits |
| margin over the 128-bit floor | 1.248 | 1.241 |
The cost is 0.007 bits. Both numbers are asserted in TestEntropyFloorSurvivesFiltering, so a
future wordlist swap cannot quietly move the floor.
Every recovery code already issued remains valid, and this was verified rather than assumed: R is
consumed as a whole passphrase by the PBS scrypt KDF (Wrap/Unwrap), and grep confirms nothing in
the consume path ever splits it. The joiner, the word count and the KDF path are untouched.
Tests
TestGenerateRecoveryCode_EntropyAndFormat now counts words by generation count, not by splitting
the joined string — conflating those two is what made it flake. It asserts the segmentation property
separately, because that is the property joinSafe actually buys.
New in wordlist_test.go: the four filtered words are pinned by name (a wordlist swap that changes the
set fails loudly); the entropy numbers above; and a production-wiring test that drives the exported
GenerateRecoveryCode 500×, so the fix is proven at the entry point and not only in the helper.
Red-proof — RUN, both deterministic and probabilistic halves
Pre-fix shape restored (var wordlist = parseWordlist(wordlistRaw)), suite re-run:
--- FAIL: TestWordlistLoaded
effective wordlist should be 7772 words (7776 EFF - 4 hyphenated), got 7776
--- FAIL: TestGenerateRecoveryCode_EntropyAndFormat
joined code must segment into 10 words, got 11 (a drawn word contained "-")
--- FAIL: TestEntropyFloorSurvivesFiltering
filtered entropy moved: 129.248, expected 129.241
--- FAIL: TestGenerateRecoveryCode_NeverContainsAmbiguousWord
code 193 segmented into 11 parts, want 10
Fix restored → green; git diff clean.
TestGeneratedCodeSegments_FilteredVsUnfiltered is the deterministic companion: it drives the
generator against a fixture list where every word is hyphenated, so the pre-fix defect reproduces with
probability 1 rather than ~1/5, and shows the same list through joinSafe refuses to generate at all.
Gates
go build ./... && go vet ./... && go test ./... — green. The ex-flaky package was additionally
run 10× consecutively: 10/10 pass (it previously failed ~1 in 5).
Observations
- The
felhom-testingskill's "Known flake … re-run before diagnosing; it is not a regression" line is now removed. It had been actively harmful: it told every future session to dismiss a true positive. Replaced with the generalised lesson — "known flake, just re-run it" is a diagnosis and needs evidence like any other. WordlistSize()now reports the effective (filtered) draw space, 7772.WordlistFilteredOut()is new, for audit. Any external consumer expecting a literal 7776 would need updating; there is none outside the tests.