The 60-second fix for wallet mistakes: fail loudly, not fatally

12. September 2026September 12, 2026 · 3 Min. Lesezeit3 min read

We spent this week breaking HTLC refunds across every Cashu mint and wallet version we could get running. One finding deserves a wider discussion because it keeps recurring in different costumes.

The finding. The newest nutshell wallet cannot mint on most mints we tested. Not a typo — 2 of 9. Separately: HTLC refund spends are unspendable by both major wallet families on all current cdk mints. The reasons are boring: a signature scheme changed without a migration path; a witness type rejects the field shapes wallets actually emit, with an error that names the wrong layer ("Secret is not a HTLC secret" — the secret was fine).

The pattern behind all of it. Every failure we mapped has the same shape:

  1. Two valid-looking encodings exist for the same semantics (upper/lowercase hash; preimage present/absent/null in a refund witness).
  2. Only the mint discriminates between them. The wallet doesn't know it's doing anything unusual.
  3. The mint's error is opaque and points at the wrong thing.
  4. User-visible result: funds that exist but can't move.

This is the classic dual-representation boundary, and Cashu has several of them live right now.

The tension. You want wallets to emit uniformly — uniformity is privacy (a wallet that serializes differently is fingerprintable). But enforcing uniformity at the mint doesn't make wallets uniform; it makes their users into creditors of the mint. Every strict rejection of a tolerable deviation is a small confiscation.

The proposal: soft-failure mode. When a mint receives a spend it can verify but whose encoding deviates from canonical, it should:

We've run this in production for months on a small mint: HTLC hash locks arrive occasionally with uppercase hex. Digest comparison makes upper and lower identical, but string comparison rejects. We honor the uppercase form after a 60-second hold. Measured: 60.5s for the deviant encoding, 0.3s for canonical. Nobody loses funds; the misbehaving wallet is impossible to miss in the logs; the delay makes the deviation felt rather than fatal.

The general law we'd propose for the NUTs:

Canonical by construction at the emitter. Tolerant at the verifier. Loud — never destructive — on deviation.

Wallets: emit exactly one canonical form per construct (the spec should pin these). Mints: verify semantically, not lexically — accept what verifies. Spec: make the canonical forms normative so the tolerant surface doesn't fossilize ambiguity into permanent dialects.

There's an open question on a nutshell PR right now that this answers directly: "what do we do on internal-contract failures — reject and log? what error?" Same answer: if it verifies, it spends — loudly, slowly, observably. If it doesn't verify, it fails — clearly, with an error that names the actual layer.

(Reproducible vectors exist for each finding above. The HTLC refund one is a single paste-able test that fails on cdk main today.)


Postscript — the method: the investigation method is worth as much as the findings — frozen predictions before every run; error-identity assertions (any-4xx-is- pass suites hide exactly this class of bug); wire taps at the request layer, not just the transport; version matrices instead of latest-vs-latest. Our own headline finding survived three rounds of "the probe is wrong until proven otherwise" before it earned trust. The discipline is the tool.

#bitcoin#cashu#ecash

Comments

No comments yet. Start the conversation.