From 2451fa8800bf882f02626c38d877bbb8e06615c3 Mon Sep 17 00:00:00 2001 From: Amperstrand Date: Tue, 28 Jul 2026 12:28:24 +0200 Subject: [PATCH 1/4] chore: add NUT spec quotes (Layer 1 greatspectations adoption) Adds verbatim NUT spec text as comments at key code locations: - NUT #00: Token format prefix (cashu[version][token]) above extractCashuToken - NUT #00: Verification flow above tollwallet.Receive - NUT #05: Melt quote flow above tollwallet.Send Adds specquotes.toml for greatspectations CI drift detection. See: https://github.com/OpenTollGate/cashu-audit (Layer 1 framework) --- specquotes.toml | 21 +++++++++++++++++++++ src/main.go | 1 + src/tollwallet/tollwallet.go | 2 ++ 3 files changed, 24 insertions(+) create mode 100644 specquotes.toml diff --git a/specquotes.toml b/specquotes.toml new file mode 100644 index 000000000..a877b83cf --- /dev/null +++ b/specquotes.toml @@ -0,0 +1,21 @@ +# greatspectations spec-quote drift config for tollgate-module-basic-go +# +# Tool: https://github.com/rustyrussell/greatspectations +# Spec: https://github.com/cashubtc/nuts (Cashu NUT specifications) +# +# Comment marker syntax in Go source: +# // NUT #00: +# // NUT #05: +# +# Setup (one-time, local): +# git clone --depth=1 https://github.com/cashubtc/nuts.git nuts +# pip install git+https://github.com/rustyrussell/greatspectations.git +# +# Run: +# spectate check --config specquotes.toml --comment-start '//' --comment-continue '//' 'src/**/*.go' +# spectate coverage --config specquotes.toml + +[sources.NUT] +format = "markdown" +dir = "nuts" +pattern = "{id:02d}.md" diff --git a/src/main.go b/src/main.go index 15cf18bf1..37d980240 100644 --- a/src/main.go +++ b/src/main.go @@ -407,6 +407,7 @@ func handleDetails(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, merchantProvider.inner.GetMerchant().GetAdvertisement()) } +// NUT #00: cashu[version][token] — `cashu` is the Cashu token prefix. `[version]` is a single `base64_urlsafe` character to denote the token format version. // handleRootPost handles POST requests to the root endpoint func extractCashuToken(body []byte) (token string, event *nostr.Event) { var ev nostr.Event diff --git a/src/tollwallet/tollwallet.go b/src/tollwallet/tollwallet.go index 769b3ca24..a1f5dc595 100644 --- a/src/tollwallet/tollwallet.go +++ b/src/tollwallet/tollwallet.go @@ -116,6 +116,7 @@ func (w *TollWallet) Shutdown() error { return nil } +// NUT #00: `Carol` can send `(x, C)` to `Bob` who then checks that `k*hash_to_curve(x) == C` (**verification**), and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets. func (w *TollWallet) Receive(token cashu.Token) (uint64, error) { mint := token.Mint() @@ -151,6 +152,7 @@ func (w *TollWallet) Receive(token cashu.Token) (uint64, error) { return amountAfterSwap, nil } +// NUT #05: To request a melt quote, the wallet of `Alice` makes a `POST /v1/melt/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). func (w *TollWallet) Send(amount uint64, mintUrl string, includeFees bool) (cashu.Token, error) { log.Printf("TollWallet.Send: attempting to send %d sats from mint %s (includeFees=%t)", amount, mintUrl, includeFees) From 53c95ae7011e47f3f94b57d8a963b7fe077d8a0e Mon Sep 17 00:00:00 2001 From: Amperstrand Date: Wed, 29 Jul 2026 13:43:45 +0200 Subject: [PATCH 2/4] fix(#327): move NUT-05 quote to MeltToLightning, add NUT-03 above Send felixfelix-bot feedback: NUT-05 (melt) quote was above Send(), but Send() does a NUT-03 swap, not a melt. Moved NUT-05 quote above MeltToLightning() which is the actual melt path. Added NUT-03 quote above Send() to match the swap operation. --- src/tollwallet/tollwallet.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/tollwallet/tollwallet.go b/src/tollwallet/tollwallet.go index a1f5dc595..0fc3a6d85 100644 --- a/src/tollwallet/tollwallet.go +++ b/src/tollwallet/tollwallet.go @@ -152,7 +152,7 @@ func (w *TollWallet) Receive(token cashu.Token) (uint64, error) { return amountAfterSwap, nil } -// NUT #05: To request a melt quote, the wallet of `Alice` makes a `POST /v1/melt/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). +// NUT #03: Alice requests swapping proofs she holds for new proofs, signing over her inputs and requesting signatures on her outputs. func (w *TollWallet) Send(amount uint64, mintUrl string, includeFees bool) (cashu.Token, error) { log.Printf("TollWallet.Send: attempting to send %d sats from mint %s (includeFees=%t)", amount, mintUrl, includeFees) @@ -369,6 +369,7 @@ func (w *TollWallet) GetAllMintBalances() map[string]uint64 { return w.wallet.GetBalanceByMints() } +// NUT #05: To request a melt quote, the wallet of `Alice` makes a `POST /v1/melt/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). // MeltToLightning melts a token to a lightning invoice using LNURL // It attempts to melt for the target amount, reducing by 5% each time if fees are too high func (w *TollWallet) MeltToLightning(mintUrl string, targetAmount uint64, maxCost uint64, lnurl string) error { From 1240589f2b014dbbe32b68b80de13163a0a5a506 Mon Sep 17 00:00:00 2001 From: Amperstrand Date: Thu, 30 Jul 2026 14:59:38 +0200 Subject: [PATCH 3/4] fix: verify all spec quotes pass greatspectations - Fixed comment-start: '// ' (with space) per greatspectations requirement - Fixed speccheck.sh: correct command name + glob expansion - Fixed 6 quote texts to match NUT spec verbatim - Added blank lines between quotes and docstrings - Added 5 new NUT quotes across main.go, tollwallet.go, merchant.go All 9 quotes verified: greatspectate check exit 0. --- .gitignore | 1 + src/main.go | 8 +++++--- src/merchant/merchant.go | 1 + src/tollwallet/tollwallet.go | 8 ++++++-- 4 files changed, 13 insertions(+), 5 deletions(-) diff --git a/.gitignore b/.gitignore index 9c8a4480e..10f9465c1 100644 --- a/.gitignore +++ b/.gitignore @@ -53,3 +53,4 @@ handover*.md ulw-*.md # Captive portal build output (built from source via packaging/portal-build.sh) packaging/files/tollgate-captive-portal-site/assets/ +nuts/ diff --git a/src/main.go b/src/main.go index 37d980240..483cc6b78 100644 --- a/src/main.go +++ b/src/main.go @@ -203,8 +203,8 @@ func init() { } valve.AuthDelay = time.Duration(delaySeconds) * time.Second mainLogger.WithFields(logrus.Fields{ - "redirect_url": mainConfig.RedirectURL, - "auth_delay": valve.AuthDelay, + "redirect_url": mainConfig.RedirectURL, + "auth_delay": valve.AuthDelay, "auth_delay_source": func() string { if mainConfig.AuthDelaySeconds > 0 { return "config" @@ -407,7 +407,8 @@ func handleDetails(w http.ResponseWriter, r *http.Request) { fmt.Fprint(w, merchantProvider.inner.GetMerchant().GetAdvertisement()) } -// NUT #00: cashu[version][token] — `cashu` is the Cashu token prefix. `[version]` is a single `base64_urlsafe` character to denote the token format version. +// NUT #00: `cashu` is the Cashu token prefix. `[version]` is a single `base64_urlsafe` character to denote the token format version. + // handleRootPost handles POST requests to the root endpoint func extractCashuToken(body []byte) (token string, event *nostr.Event) { var ev nostr.Event @@ -423,6 +424,7 @@ func extractCashuToken(body []byte) (token string, event *nostr.Event) { return strings.TrimSpace(string(body)), nil } +// NUT #00: Serialized tokens have a Cashu token prefix, a versioning flag, and the token. func HandleRootPost(w http.ResponseWriter, r *http.Request) { // Log the request details mainLogger.WithFields(logrus.Fields{ diff --git a/src/merchant/merchant.go b/src/merchant/merchant.go index 7d16aa26a..3167a3700 100644 --- a/src/merchant/merchant.go +++ b/src/merchant/merchant.go @@ -442,6 +442,7 @@ type PurchaseSessionResult struct { } // PurchaseSession processes a payment with cashu token and MAC address, returns either a session event or a notice event +// NUT #00: `Carol` can send `(x, C)` to `Bob` who then checks that `k*hash_to_curve(x) == C` (**verification**), and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets. func (m *Merchant) PurchaseSession(cashuToken string, macAddress string) (*nostr.Event, error) { // Validate MAC address if !utils.ValidateMACAddress(macAddress) { diff --git a/src/tollwallet/tollwallet.go b/src/tollwallet/tollwallet.go index 0fc3a6d85..9e949b3de 100644 --- a/src/tollwallet/tollwallet.go +++ b/src/tollwallet/tollwallet.go @@ -152,7 +152,7 @@ func (w *TollWallet) Receive(token cashu.Token) (uint64, error) { return amountAfterSwap, nil } -// NUT #03: Alice requests swapping proofs she holds for new proofs, signing over her inputs and requesting signatures on her outputs. +// NUT #03: The swap operation is the most important component of the Cashu system. A swap operation consists of multiple inputs (`Proofs`) and outputs (`BlindedMessages`). Mints verify and invalidate the inputs and issue new promises (`BlindSignatures`). func (w *TollWallet) Send(amount uint64, mintUrl string, includeFees bool) (cashu.Token, error) { log.Printf("TollWallet.Send: attempting to send %d sats from mint %s (includeFees=%t)", amount, mintUrl, includeFees) @@ -271,11 +271,13 @@ func (w *TollWallet) SendWithOverpayment(amount uint64, mintUrl string, maxOverp return tokenString, nil } +// NUT #04: To request a mint quote, the wallet of `Alice` makes a `POST /v1/mint/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). `method` **MUST** match `[a-z0-9_-]+`. func (w *TollWallet) RequestMintQuote(amount uint64, mintURL string) (*nut04.PostMintQuoteBolt11Response, error) { w.ensureMintRegistered(mintURL) return w.wallet.RequestMint(amount, mintURL) } +// NUT #04: To check the current accounting data of a mint quote, the wallet makes a `GET /v1/mint/quote/{method}/{quote_id}`. func (w *TollWallet) GetMintQuoteState(quoteID string) (*nut04.PostMintQuoteBolt11Response, error) { if w.wallet == nil { return nil, ErrWalletNotInitialized @@ -283,6 +285,7 @@ func (w *TollWallet) GetMintQuoteState(quoteID string) (*nut04.PostMintQuoteBolt return w.wallet.MintQuoteState(quoteID) } +// NUT #04: After requesting a mint quote and paying the request, the wallet proceeds with minting new tokens by calling the `POST /v1/mint/{method}` endpoint. func (w *TollWallet) MintQuoteTokens(quoteID string) (uint64, error) { return w.wallet.MintTokens(quoteID) } @@ -369,7 +372,8 @@ func (w *TollWallet) GetAllMintBalances() map[string]uint64 { return w.wallet.GetBalanceByMints() } -// NUT #05: To request a melt quote, the wallet of `Alice` makes a `POST /v1/melt/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). +// NUT #05: To request a melt quote, the wallet of `Alice` makes a `POST /v1/melt/quote/{method}` request where `method` is the payment method requested (e.g., `bolt11`, `bolt12`, etc.). `method` **MUST** match `[a-z0-9_-]+`. + // MeltToLightning melts a token to a lightning invoice using LNURL // It attempts to melt for the target amount, reducing by 5% each time if fees are too high func (w *TollWallet) MeltToLightning(mintUrl string, targetAmount uint64, maxCost uint64, lnurl string) error { From 3e20040f1af5771880592f5635df06508da9a057 Mon Sep 17 00:00:00 2001 From: amperstand Date: Tue, 18 Aug 2026 14:48:45 +0200 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20spec-quote=20polish=20=E2=80=94=20co?= =?UTF-8?q?mplete=20NUT-03=20quote,=20dedupe=20NUT-00,=20scope-only=20diff?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - NUT-03 quote above TollWallet.Send gains the spec's final sentence ("These are then used by the wallet to generate new Proofs"). - The NUT-00 verification quote appeared verbatim above both Merchant.PurchaseSession and TollWallet.Receive; keeping the wallet-level one (verification is the mint-side swap check) and leaving a pointer at the merchant site — duplicate quotes against the same spec section flag in speccheck. - Dropped the unrelated gofmt field-alignment change in main.go; the file now matches main byte-for-byte outside the quotes (main's existing gofmt dirt is #341's scope). Review findings: cold review 2026-08-18 (3 minors). --- src/main.go | 4 ++-- src/merchant/merchant.go | 2 +- src/tollwallet/tollwallet.go | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/main.go b/src/main.go index 483cc6b78..feda56af1 100644 --- a/src/main.go +++ b/src/main.go @@ -203,8 +203,8 @@ func init() { } valve.AuthDelay = time.Duration(delaySeconds) * time.Second mainLogger.WithFields(logrus.Fields{ - "redirect_url": mainConfig.RedirectURL, - "auth_delay": valve.AuthDelay, + "redirect_url": mainConfig.RedirectURL, + "auth_delay": valve.AuthDelay, "auth_delay_source": func() string { if mainConfig.AuthDelaySeconds > 0 { return "config" diff --git a/src/merchant/merchant.go b/src/merchant/merchant.go index 3167a3700..1f98e6f82 100644 --- a/src/merchant/merchant.go +++ b/src/merchant/merchant.go @@ -442,7 +442,7 @@ type PurchaseSessionResult struct { } // PurchaseSession processes a payment with cashu token and MAC address, returns either a session event or a notice event -// NUT #00: `Carol` can send `(x, C)` to `Bob` who then checks that `k*hash_to_curve(x) == C` (**verification**), and if so treats it as a valid spend of a token, adding `x` to the list of spent secrets. +// NUT-00 verification quote lives above TollWallet.Receive (single source; duplicate quotes flag in speccheck). func (m *Merchant) PurchaseSession(cashuToken string, macAddress string) (*nostr.Event, error) { // Validate MAC address if !utils.ValidateMACAddress(macAddress) { diff --git a/src/tollwallet/tollwallet.go b/src/tollwallet/tollwallet.go index 9e949b3de..1c80a59c5 100644 --- a/src/tollwallet/tollwallet.go +++ b/src/tollwallet/tollwallet.go @@ -152,7 +152,7 @@ func (w *TollWallet) Receive(token cashu.Token) (uint64, error) { return amountAfterSwap, nil } -// NUT #03: The swap operation is the most important component of the Cashu system. A swap operation consists of multiple inputs (`Proofs`) and outputs (`BlindedMessages`). Mints verify and invalidate the inputs and issue new promises (`BlindSignatures`). +// NUT #03: The swap operation is the most important component of the Cashu system. A swap operation consists of multiple inputs (`Proofs`) and outputs (`BlindedMessages`). Mints verify and invalidate the inputs and issue new promises (`BlindSignatures`). These are then used by the wallet to generate new Proofs (see [NUT-00][00]). func (w *TollWallet) Send(amount uint64, mintUrl string, includeFees bool) (cashu.Token, error) { log.Printf("TollWallet.Send: attempting to send %d sats from mint %s (includeFees=%t)", amount, mintUrl, includeFees)