ADR: Deterministic Mint-Keyset Resolution in Settlement Validation
- ADR-XXX: Deterministic Mint-Keyset Resolution in Settlement Validation (Root-Cause Fix for the settlementDescriptor Flake)
ADR-XXX: Deterministic Mint-Keyset Resolution in Settlement Validation (Root-Cause Fix for the settlementDescriptor Flake)
Status
Proposed
Date
2026-09-10
Related
src/lib/auction/settlementDescriptor.ts—getSettlementDescriptorcallsawait fetchMintKeysets(winnerBid.mint)at line 512src/lib/auction/validation.ts—fetchMintKeysets(lines 49-66) performs real network I/O viaCashuMint.getKeySets(); module-globalkeysetCacheat line 50src/lib/__tests__/settlementDescriptor.test.ts— the flaky test filesrc/components/AuctionSettlement.tsx— the only production caller ofgetSettlementDescriptor(line 239)- ADR-0005: No External Service Dependencies in Tests
- ADR-0006: Local Cashu Mint for e2e Tests
- ADR-0004: Auction Settlement Descriptor
Context
The flake
settlementDescriptor.test.ts intermittently fails with a 5-second timeout on
getSettlementDescriptor. The failure is pre-existing — it is unrelated to
any specific PR (it has blocked pushes on the ADR-0011 DLEQ branch, the NDK
seam migration, and others) and passes on re-run. It erodes trust in the CI
signal and wastes developer time debugging a failure that is not caused by the
code under test.
Root cause (verified by two independent cross-family consultant reviews, 2026-09-10)
getSettlementDescriptor (settlementDescriptor.ts:512) performs a real
network call to the winning bid’s mint:
const mintKeysets = winnerBid ? await fetchMintKeysets(winnerBid.mint) : undefined
fetchMintKeysets (validation.ts:49-66) constructs a CashuMint and awaits
mint.getKeySets() — an HTTP request to the mint URL. In unit tests, the mint
URL is the inert string https://mint.example.com (per ADR-0005, mint URLs in
fixtures are data, not services). But fetchMintKeysets actually tries to
fetch it. When the network is slow, unreachable, or rate-limited, this call
hangs past the test’s 5-second timeout → flaky failure.
Verified facts:
- The call is unconditional — no try/catch, no timeout wrapper around it in
getSettlementDescriptor. fetchMintKeysetshas no timeout / AbortSignal. The cashu-ts
getKeySets()internally callsfetch(url + "/v1/keysets")with no signal
option. Thecatch { return [] }only guards against rejected promises
(DNS failure, connection refused) — it does not guard against a hanging
promise (slow/unreachable network where the TCP handshake never completes).- The test file calls
getSettlementDescriptorin ~50 places with zero
mocking offetchMintKeysetsorCashuMint. Any test with a truthy
canonicalWinnertriggers the real network call. - The
keysetCache(validation.ts:50) is module-global, shared across all
tests in a run. A slow mint in one test can poison subsequent tests.
This violates ADR-0005’s core rule: “Tests must not make network calls to
external services.” The fetchMintKeysets call inside the settlement
descriptor’s validation path is a network dependency that unit tests cannot
control.
Why it is intermittent
- When the runner has fast, reliable network,
mint.example.comresolves and
the request fails fast (DNS/connection refused) → test passes. - When the runner is slow or the network is flaky, the request hangs → 5s
timeout → test fails. - The
keysetCacheis module-global and shared across tests, so the first test
to hit a slow mint poisons subsequent tests in the same run.
Decision
Make mint-keyset resolution a deterministic, injectable seam in the
settlement validation path, and remove real network I/O from unit tests.
1. Expose mintKeysets as the injectable seam (data, not a fetcher)
Add an optional mintKeysets?: MintKeyset[] field to
GetSettlementDescriptorInput. This matches the existing pattern in the
validation architecture — ValidatePathReleaseInput and
ValidateSettlementCompletenessInput already accept mintKeysets?: MintKeyset[],
and deriveState already threads it through. The seam is data-driven, not
function-driven: the caller decides whether to pre-fetch (production) or inject
mocks (tests).
Backward compatibility: if mintKeysets is undefined, getSettlementDescriptor
falls back to the internal fetchMintKeysets (now with a bounded timeout). This
avoids breaking the existing production caller.
2. Bound the network call (defense in depth)
Even in production, fetchMintKeysets must not hang indefinitely. Add a bounded
timeout (AbortSignal.timeout(2000)) so a slow mint degrades to [] (empty
keysets) rather than blocking the descriptor. An empty keyset result already
routes to the pending/invalid path — it must never hang.
3. Negative caching + failure logging
- Negative caching: a failed fetch should populate a short-TTL negative
cache entry (cache the[]result) so a dead mint is not hammered on every
settlement UI render. - Failure logging: the
catch { return [] }silently swallows errors. Add a
console.warn/ structured log whenfetchMintKeysetsfails, so operators
can distinguish “mint is down” from “settlement is invalid.”
4. Test isolation
- Export a
clearKeysetCache()helper from validation.ts for test cleanup. - Add a
mockMintKeysets()test utility tosettlementDescriptor.test.ts
returning a minimal validMintKeyset[]fixture. - Tests inject keysets via the seam; they never trigger the real fetch or rely
on the module-global cache.
Invariants
getSettlementDescriptornever performs an unbounded network call.- Unit tests never make a real HTTP request to a mint URL.
- A slow or unreachable mint degrades to
pending/invaliddeterministically
— never a hang, never a flaky pass/fail. - The settlement descriptor’s behavior for a given set of inputs is
deterministic given the same injected keysets. - Tests never rely on the module-global
keysetCache.
Consequences
Positive
- Root-cause fix — the flake is eliminated at its source (real network I/O
in a pure validation path), not patched per-test. - Deterministic CI —
settlementDescriptor.test.tspasses or fails based
on code, not network conditions (ADR-0005’s stated goal). - Faster tests — no test waits on a network timeout.
- Production robustness — a slow mint no longer blocks the settlement UI
indefinitely; it degrades gracefully with a bounded timeout and negative
caching.
Costs
GetSettlementDescriptorInputgains amintKeysetsfield (small API surface
change).AuctionSettlement.tsx(the only production caller) must be updated to
fetch keysets externally (with timeout) or rely on the default fallback.- The module-global
keysetCacheis documented as test-hostile; tests must
inject keysets to avoid it.
Rollout / PR sequence
PR 1 — Add the mintKeysets seam + bounded timeout + negative cache + logging
- Add
mintKeysets?: MintKeyset[]toGetSettlementDescriptorInput. getSettlementDescriptoruses the seam; falls back tofetchMintKeysets
(withAbortSignal.timeout(2000)) whenmintKeysetsisundefined.- Add negative caching +
console.warnon fetch failure in validation.ts. - Export
clearKeysetCache(). - Update
AuctionSettlement.tsxto fetch keysets externally (with timeout) or
passundefinedand rely on the default.
PR 2 — Make settlementDescriptor.test.ts network-free
- Rewrite
settlementDescriptor.test.tsto inject deterministic keysets via
the seam. - Add
mockMintKeysets()test utility. - Add
beforeEachcache clearing. - Remove any reliance on the inert mint URL being fetchable.
- Verify the full suite is green and stable across repeated runs.
Ordering vs ADR-0011 (PR #1280)
PR 1 (the seam) should land first — it makes the validation path
controllable. ADR-0011’s DLEQ verification (which also touches the validation
path) should then use the seam, passing mintKeysets through rather than
hard-coding the fetch. If ADR-0011 lands first without the seam, it may add more
network I/O that also needs to be made injectable.
Notes
- This is a pre-existing flake, not introduced by ADR-0011. It has blocked
pushes on multiple unrelated branches (observed 2026-09-09 on the ADR-0011
DLEQ branch and the NDK seam migration). The fix is independent of any
feature work and can land on its own. - The
keysetCacheglobal (validation.ts:50) is a secondary concern — it is
module-global and shared across tests, so a slow mint in one test can poison
others. The seam makes this moot for unit tests (they inject keysets), but
the cache should be reviewed for test isolation. - Consultant verification (2026-09-10): two independent cross-family
reviews (kimi-consultant, worker-reviewer-qwen) both CONFIRMED the root cause
(real network I/O infetchMintKeysets, no timeout, unconditional call,
zero test mocking) and both APPROVED-WITH-CHANGES, independently converging
on: simplify the seam tomintKeysets?: MintKeyset[](data, not fetcher),
addAbortSignal.timeout(2000), exportclearKeysetCache(), add
mockMintKeysets(), updateAuctionSettlement.tsx, and clarify ADR-0011
ordering. All incorporated above.
Write a comment