Keyset final_expiry silently kills held balances — wallets must rotate off inactive keysets (NUT-02)

オープン
#417 コメント 3 件 リアクション 0 件 担当者 0 名 GitHub で見る

メンテナーはふだん 1 日以内に返信

まだ誰も着手していません。

評価

難易度
5/5
見積もり時間
1週間以上
初心者へのやさしさ
28/100
issue の種類
機能追加
明瞭さ
おおむね明確
活発さ
活発
技術スタック
go
領域
backend, payments

調査の方向性

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.

索引モデルが issue の本文から書いたものです。

説明

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_balance in accepted_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):

  1. Keyset lifecycle monitor: on keyset refresh, detect active=false for keysets with held proofs and proactively swap them to the active keyset (NUT-02: wallets SHOULD prioritize this; NUT-03 swap). React to final_expiry in the keyset response with an operator warning when held value is affected.
  2. Classifier fix (already noted on #409): match "Keyset has expired" / code 12003 to a dedicated payment-error-keyset-expired notice instead of mint-unreachable, keeping the cause in the message. The pin in test_keyset_rotation.py updates consciously with it.
  3. Advertisement/trust surface (product question): consider surfacing keyset-expiry policy (e.g. keysets with final_expiry set) 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.

主要言語
Go
スター
12
フォーク
14
平均マージ
3日 20時間
マージ済み PR(30日)
261

環境構築

はじめの一歩

  1. issue を最後まで読み、次にプロジェクトのコントリビューションガイドを読みます。
  2. 着手することを issue にコメントします — 二人が同じ作業をするのを防げます。
  3. リポジトリをフォークし、ブランチを切って変更します。
  4. issue 番号を参照したプルリクエストを送ります。

OpenTollGate/tollgate-module-basic-go のほかの issue

OpenTollGate/tollgate-module-basic-go の issue をすべて見る

似ている issue

Go の issue をもっと見る

新しい issue をメールで受け取る

初心者向けの GitHub issue を短くまとめたダイジェスト。