NUTbits v0.9.0
This release gives NUTbits a real payment ledger. Balances are now committed before a payment instead of deducted after it, unknown outcomes are resolved against the mint instead of guessed, and every interface shows what you can actually spend.
Homepage
The Allocation Ledger
NUTbits holds one pool of ecash. A dedicated connection's balance is a claim on part of that pool, so the claim is only worth what enforces it. One component now does that enforcing.
- Funds are committed before a payment and settled against the actual spend afterwards
- Concurrent payments can no longer both pass the same balance check and both go out
dedicated_balance_msathas exactly one writer, so the rule cannot drift back apart- Withdrawing cannot take sats already committed to a payment in flight
- Revoking a connection with a payment in flight no longer double spends
Three Payment Outcomes, Not Two
A Lightning payment can succeed, definitely fail, or leave you with no answer. Only the first two are safe to act on.
- An unclear melt is resolved by asking the mint about the quote (NUT-05)
- PAID records the spend, UNPAID hands the ecash back, and no answer keeps the sats committed
- Proofs are no longer restored after a melt the mint may already have spent
- A payment awaiting reconciliation is reported by
nutbits status, not buried in a log GET /api/v1/ledgerlists them;POST /api/v1/ledger/holds/:id/resolvesettles or returns one- Mint calls are bounded by
NUTBITS_MINT_TIMEOUT_MS, so one unresponsive mint no longer blocks every payment to it for the life of the process
Spendable vs Total
The pool total was never what you could send. Now the difference is visible everywhere instead of being discovered on a rejection.
totalis every sat held;spendableis total minus allocations minus payments in flight- CLI, TUI and GUI all gate on spendable, so none of them offers a payment the wallet will refuse
nutbits withdrawshows the exact withdrawable figure per connection and names the locked remainder- Wallets with no dedicated connections see exactly the output they saw before
Fund Safety Fixes
The private report that started this release came from Muhammed Shekho (@SYR-ROOT): two concurrent pay_invoice events on one dedicated connection could both pass the balance check and both pay. Auditing around it turned up more of the same kind.
POST /api/v1/paycompared against the raw pool with no allocations subtracted, so CLI, TUI and GUI payments could spend the funds backing every dedicated connection. No race requiredFileStore.getProofshanded out its live internal array, so a payment in flight could delete newly minted proofs as if they had been spent. Fund loss on the default backend- NUT-13 counters were not reserved atomically; two operations could derive identical blinded messages, which the mint refuses and the ecash behind is lost
- Switching storage backends dropped the NUT-13 counters entirely, re-deriving already spent secrets
- A new connection was live on relays before
dedicatedwas set, briefly granting shared wallet access - An unauthenticated
/connectracing an authenticated create could be handed the wrong connection - Concurrent incoming payments to one connection lost a credit
- Two events paying the same invoice let the loser overwrite the winner's record, so a successful payment reported itself as failed
- MySQL lost updates in
updateConnectionandupdateTx
Reliability
- A second NUTbits process on the same state backend is refused at boot. Previously it silently took over the socket and both kept running, which double spends
- Reservations survive a crash and come back held, so an interrupted payment is reported rather than forgotten
- Configuration is validated before first use.
NUTBITS_FEE_RESERVE_PCT=abcused to becomeNaN, andNaN - needed < 0is false, so the fee reserve silently stopped applying; the sameNaNturned the spending caps into "no limit" - A failed payment returns an HTTP error instead of
200 OKwithsuccess: false, which every client rendered as "Payment sent!" --versionno longer reports a stale number
Tests
NUTbits had no test suite. It has one now.
npm test, 128 tests, no mint or relay required- The reported race is one of them
- One shared mutex primitive replaces four hand written copies
Documentation
HOW-IT-WORKS.mdexplains how balances are protected and why the wallet reports two figuresDATABASE.mdandSTATE.mdadvertised MySQL for "multiple NUTbits instances". That was never safe, and they now say soAGENTS.mdcarries the ledger contract, since it is the file contributors read first- Every em dash removed from the repository
Upgrading
Existing wallets load without migration. Two things to expect:
- If you have funded dedicated connections,
nutbits paycan now send less than before. That is the fix, not a regression: those sats were never yours to spend. Runnutbits balanceto see the split - If you run more than one NUTbits process against one state file or database, the second will now refuse to start. It was never safe; nothing had stopped it
MySQL users: the reservation table is created on first start. The code path is syntax checked and mirrors SQLite statement for statement, but it has not been exercised against a live MySQL server. Take a backup first.
Breaking Changes
POST /api/v1/payno longer spends into dedicated allocations, and applies the fee reserve as the NWC path always did- A failed payment returns a 5xx instead of
200withsuccess: false GET /api/v1/statusreportsbalance_satsacross all mints, matching/api/v1/balance. It was active mint only, sospendable_satscould read higher than the balance- A second instance on the same state backend is refused at boot
createNWCconnectiontakes a profile object and returns{ app_pubkey, nwc_string }