Keyset final_expiry silently kills held balances — wallets must rotate off inactive keysets (NUT-02)
Maintainers usually reply within 1 day
Nobody has claimed this yet.
Assessment
- Difficulty
- 5/5
- Estimated time
- Over a week
- Newbie friendliness
- 28/100
Research direction
Start with tests/cloud-lab/run-keyset-rotation.sh and the related issues #409, #413, and #416 to understand the existing rotation and error-classification work. The issue proposes several separable directions, so confirm which one is in scope before starting; for the lifecycle monitor, inspect the keyset refresh and held-proof handling entry points. Done means the selected behavior is covered by tests, including the expired-keyset case.
Written by the indexing model from the issue text.
Description
Wallet hygiene: keyset final_expiry silently kills held balances — wallets must rotate off inactive keysets (NUT-02 SHOULD). Proven in the cloud lab.
What happened (lab evidence, #416): paying with proofs whose keyset was expired fails with "could not swap proofs: Keyset has expired" — the proofs are permanently unspendable at that mint. This is spec-correct mint behavior, not a bug: NUT-02 Keyset Final Expiry says that after final_expiry the mint "is no longer obliged to fulfill promises signed with the keys from that keyset" and may irrevocably delete the keyset's nullifiers. The spec even standardized the refusal: error code 12003 "Keyset has expired" (nuts PR #367). Implementations differ — cdk enforces expiry, nutshell never expires (nutshell #7, tracked in the cashu-audit divergence database) — and the spec's active flag conflates mintable vs redeemable, but none of that is ours to fix upstream.
Why it matters to TollGate: any wallet balance held across a keyset rotation+expiry window can be wiped:
- Reseller mode: the reseller holds balances at upstream mints (
min_balanceinaccepted_mints) between purchases — an upstream that rotates with expiry (cdk mints do) silently destroys that float. - Operator payouts: the merchant wallet holds received funds between payout cycles (
min_payout_amount,payout_interval_seconds); a fee-charging cdk-style mint rotating keysets mid-cycle destroys receivables. - Customers: a customer holding a token from an expired keyset gets a refusal — currently misclassified as
payment-error-mint-unreachable(see the note on #409), telling them to retry a permanently dead token.
Suggested work items (split-able):
- Keyset lifecycle monitor: on keyset refresh, detect
active=falsefor keysets with held proofs and proactively swap them to the active keyset (NUT-02: wallets SHOULD prioritize this; NUT-03 swap). React tofinal_expiryin the keyset response with an operator warning when held value is affected. - Classifier fix (already noted on #409): match "Keyset has expired" / code 12003 to a dedicated
payment-error-keyset-expirednotice instead of mint-unreachable, keeping the cause in the message. The pin intest_keyset_rotation.pyupdates consciously with it. - Advertisement/trust surface (product question): consider surfacing keyset-expiry policy (e.g. keysets with
final_expiryset) in mint metadata evaluation, so routers can prefer mints without expiry time bombs for float.
Repro: tests/cloud-lab/run-keyset-rotation.sh (phase A mints on a 100-ppk keyset; phase B expires it; the stashed token dies with 12003-class refusal).
Refs: #416 (rotation lane + pins), #409 (classifier), #413 (fee mint), cashu-audit issues/keyset-active-flag-semantics.md + divergences/CDK-VS-NUTSHELL-ALIGNMENT-2026-07-29.md ("#7 old keysets never expire"), nuts #367 (error 12003), NUT-02 §Keyset Final Expiry.
- Dominant language
- Go
- Stars
- 12
- Forks
- 14
- Avg merge
- 1d 5h
- Merged PRs (30d)
- 232
Getting set up
- No Dockerfile or Docker Compose file
- No pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from OpenTollGate/tollgate-module-basic-go
-
Session event emits the customer's MAC address in the p tag — spec-invalid value on every payment responsePossibly taken @Amperstrand claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
OpenTollGate/tollgate-module-basic-go#949 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 68/100
OpenTollGate/tollgate-module-basic-go#833 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 65/100
OpenTollGate/tollgate-module-basic-go#813 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
OpenTollGate/tollgate-module-basic-go#804 ·
Maintainers usually reply within 1 day
-
go-battery needs an ndsctl on PATH: TestPurchaseSessionGuardHoldsThroughTheOutcomeUnknownWindow fails on bare hosts (passes with stub)Possibly taken @Amperstrand claimed this 5 days ago. Open
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
OpenTollGate/tollgate-module-basic-go#726 · 2 comments ·
Maintainers usually reply within 1 day
All issues in OpenTollGate/tollgate-module-basic-go
Similar issues
-
bug
Difficulty 2/5 Half a day Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
kind/bug
Difficulty 2/5 1-3 hours Newbie friendliness 62/100
kubernetes-sigs/external-dns#6816 ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
Maintainers usually reply within 1 day
-
ANSI translator ignores SGR 22/24 (and other attribute-off codes): bold and underline never turn offOpen
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
-
Named Tuple element with a space in its name fails to parsePossibly taken @spicysharkco claimed this today. Open
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
ClickHouse/clickhouse-go#2073 ·
Maintainers usually reply within 1 day