From 85bff5b28d51f0397b98c02ad42491d553ddcf69 Mon Sep 17 00:00:00 2001 From: kvngmikey Date: Wed, 17 Jun 2026 20:02:16 +0300 Subject: [PATCH 1/3] feat: non interactive mint delegation --- README.md | 2 + error_codes.md | 6 + xx.md | 304 +++++++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 312 insertions(+) create mode 100644 xx.md diff --git a/README.md b/README.md index ea65402d..62633ce8 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio | [28][28] | Pay to Blinded Key (P2BK) | [cdk], [cashu-ts][ts] | - | | [29][29] | Batched Mint | - | - | | [30][30] | Payment Method: Onchain | - | - | +| [31][31] | Non-Interactive Mint Delegation | - | - | #### Wallets @@ -105,3 +106,4 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio [28]: 28.md [29]: 29.md [30]: 30.md +[XX]: XX.md diff --git a/error_codes.md b/error_codes.md index a9f6aec2..60cb6148 100644 --- a/error_codes.md +++ b/error_codes.md @@ -38,6 +38,11 @@ | 31002 | Blind authentication failed | [NUT-22][22] | | 31003 | Maximum BAT mint amount exceeded | [NUT-22][22] | | 31004 | BAT mint rate limit exceeded | [NUT-22][22] | +| 33001 | Proof from unknown or unmirrored keyset | [NUT-XX][XX] | +| 33002 | Delegation window not yet open | [NUT-XX][XX] | +| 33003 | Recovered reserve exhausted | [NUT-XX][XX] | +| 33004 | Proof DLEQ verification failed | [NUT-XX][XX] | +| 33005 | Firehose sequence out of range | [NUT-XX][XX] | [00]: 00.md [01]: 01.md @@ -56,3 +61,4 @@ [21]: 21.md [22]: 22.md [29]: 29.md +[XX]: XX.md diff --git a/xx.md b/xx.md new file mode 100644 index 00000000..b7678b98 --- /dev/null +++ b/xx.md @@ -0,0 +1,304 @@ +# NUT-XX: Non-Interactive Mint Delegation on Liveness Failure + +`optional` + +`depends on: NUT-02, NUT-06, NUT-07, NUT-12` + +--- + +This NUT improves the **liveness** guarantees of custodial ecash mints that hold on-chain reserves. It lets a mint operator delegate control of its reserves to one or more third-party mints in a **non-interactive** way: while the original mint is operating, it keeps unilateral control of the reserve; if it becomes unresponsive, a delegate mint can take over and process redemptions for the original mint's users. + +A mint can delegate to multiple other mints. If a set of mints all act as delegates for each other, they collectively inherit a **1-of-n liveness** property: as long as one mint in the set stays online, the users of every mint in the set can redeem their funds. + +> [!IMPORTANT] +> This is **ordered custodial failover, not trustless unilateral exit.** It improves recovery from liveness failures, not from malicious custody. Users are **not** protected if the original mint steals reserves before failing or is insolvent, and after failover they inherit the usual custodial trust assumptions under the delegate mint. It only ensures that *"the original mint disappeared"* no longer means *"the funds are lost forever."* + +## Overview + +The scheme has four parts, three of which are wire-protocol concerns specified normatively here, and one (the on-chain reserve) which is informational: + +1. **Advertisement** — a Bobnnounces its delegates, the keysets it backs, and its spent-proof feed via [NUT-06][06] (normative). +2. **Spent-proof firehose** — a sequenced feed of spent-proof identifiers that delegate mints subscribe to and mirror locally (normative). +3. **Failover redemption** — how a delegate accepts and pays out the original mint's proofs after taking over (normative). +4. **Reserve construction** — a recommended on-chain Taproot construction that enforces the failover ordering (informational, see [Appendix](#appendix-reserve-construction-informational)). + +Throughout, the original/custodial mint is **Bob** and a delegate mint is **Dave**. + +### Trust and security model + +- Delegates are **not** part of normal operation. They do not co-sign, do not see the reserve key, and have no influence over Bob's balance while it is live. +- The handover is triggered by Bitcoin consensus (relative timelocks), not by any interaction with Bob, who may be permanently gone. +- Delegates are ordered from most to least trusted. Adding later delegates never weakens earlier windows: a later delegate can only act if every earlier delegate fails to act before its own timelock matures. +- Public proof verification ([NUT-12][12]) lets Dave validate proofs it never signed, using only Bob's public keys. + +## Advertisement + +A mint signals support and announces its delegation arrangement via its [NUT-06][06] info endpoint (see [Mint info setting](#mint-info-setting)). Two roles are advertised: + +- **As a custodian**: the delegates it has nominated, in priority order, and where its spent-proof firehose lives. +- **As a delegate**: the mints it backs and the keysets it will honor on failover, so wallets can discover where to take a dead mint's tokens. + +## Spent-proof firehose + +For a delegate to safely redeem the original mint's tokens without permitting double-spends, it needs an up-to-date copy of the original mint's spent-proof state **before** the original mint disappears. The original mint exposes this as a **firehose**: a continuous, append-only, sequenced feed of spent-proof identifiers. Delegate mints subscribe to it continuously and persist a local mirror. + +The firehose carries no business logic and no spending semantics, it is an ordered event dump. A consumer that has every event up to sequence `n` has a complete view of the spent set as of that point. + +### Spent event + +The identifier of a spent proof reuses `Y = hash_to_curve(secret)` from [NUT-00][00] / [NUT-07][07], no new identifier is introduced. A `SpentEvent` is: + +```json +{ + "seq": , + "Y": , + "id": , + "amount": , + "witness": +} +``` + +- `seq` is a strictly increasing, gap-free integer sequence number assigned by the original mint. It MUST start at `0` and increment by exactly `1` per event. It MUST NOT be reordered or reused. +- `Y` is the spent proof's identifier, the hex-encoded compressed point `Y = hash_to_curve(secret)`. +- `id` is the keyset ID ([NUT-02][02]) the proof was signed under. +- `amount` is the proof's amount. +- `witness` is the serialized witness data if the proof carried a [NUT-10][10] spending condition (as in [NUT-07][07]), otherwise `null`. + +> [!IMPORTANT] +> The original mint MUST publish exactly one `SpentEvent` for every proof it transitions to `SPENT` ([NUT-07][07]), and MUST do so durably (an event, once published, is never removed). The feed reflects only `SPENT` proofs; `PENDING` proofs are not published. + +### Real-time stream (WebSocket) + +For real-time mirroring, the firehose is exposed as a WebSocket at the mint's `/v1/firehose/ws` path, reusing the JSON-RPC framing of [NUT-17][17]. + +``` +https://mint.host:3338/v1/firehose/ws +``` + +A delegate subscribes with a `WsRequest` carrying the `subscribe` method and a single `SubscriptionKind` value `spent_feed`: + +```json +{ + "jsonrpc": "2.0", + "method": "subscribe", + "params": { + "kind": "spent_feed", + "subId": , + "filters": [] + }, + "id": +} +``` + +`subId` is a UUID v7 generated by the subscriber and `filters` is empty (the feed is not filtered). The mint replies with a [NUT-17][17] `WsResponse` and then streams every `SpentEvent` as a `WsNotification` as proofs are spent: + +```json +{ + "jsonrpc": "2.0", + "method": "subscribe", + "params": { + "subId": , + "payload": + } +} +``` + +If a subscriber detects a gap (a received `seq` is greater than the last seen `seq` plus one), it MUST backfill the missing range via the catch-up endpoint below before treating its mirror as complete. + +### Catch-up (HTTP) + +A delegate that has been offline, or that is bootstrapping its mirror, fetches missed events over HTTP: + +```http +GET https://mint.host:3338/v1/firehose?since=&limit= +``` + +- `since` (optional, default `0`) returns events with `seq >= since`. +- `limit` (optional) caps the number of events returned; the mint MAY impose its own maximum. + +The mint responds with a `FirehoseResponse`: + +```json +{ + "events": , + "latest": +} +``` + +- `events` is the ordered slice of `SpentEvent`s, ascending by `seq`. +- `latest` is the highest `seq` the mint has published, so the consumer knows whether more pages remain. + +### Consistency checkpoint (optional) + +To let a delegate periodically verify its mirror matches the source without replaying the whole feed, the mint MAY expose a checkpoint that buckets the spent set by a prefix of `Y` and returns a Merkle root per bucket: + +```http +GET https://mint.host:3338/v1/firehose/checkpoint +``` + +```json +{ + "seq": , + "buckets": [ + { "prefix": , "root": , "count": }, + ... + ] +} +``` + +`seq` is the sequence number the checkpoint was taken at. A delegate compares each `root` against the same computation over its own mirror; a mismatch identifies which bucket to re-sync. The bucketing scheme (prefix length, hashing) is left to implementations and is out of scope for double-spend safety, it is a consistency aid, not a requirement for failover. + +## Failover redemption + +Once the relative timelock on the reserve matures and the delegate Dave has claimed the reserve UTXO on-chain (see the [Appendix](#appendix-reserve-construction-informational)), Dave enters **failover mode** for Bob. + +In failover mode, Dave exposes Bob's keysets as **inactive** keysets (`active: false`, [NUT-02][02]): they are accepted as inputs to swap ([NUT-03][03]) and melt ([NUT-05][05]) but are never used to sign new outputs. A user simply points their wallet at Dave and submits Bob's proofs as inputs in the normal `/v1/swap` (to receive fresh Dave ecash) or `/v1/melt` (to withdraw to Bitcoin/Lightning) flows. + +For each input proof signed under a Bob keyset, Dave accepts it **only if all** of the following hold: + +1. The proof's `id` belongs to a Bob keyset that Dave mirrors and has the public keys for. +2. The proof's signature **publicly verifies** via its DLEQ proof ([NUT-12][12]) against that keyset's amount public key. The wallet MUST include the DLEQ proof with the blinding factor `r` on the proof (the "user to user" `Proof` form of [NUT-12][12]) so Dave can verify without Bob's private key. +3. The proof's `Y` is **not** present in Dave's mirrored spent set for Bob. +4. The claimable reserve recovered for Bob is **not** exhausted. + +On success, Dave marks `Y` as spent in its mirror for Bob (so it cannot be redeemed twice within failover) and processes the payout. + +> [!NOTE] +> **Privacy cost today.** Step 2 requires the wallet to reveal the blinding factor `r` to Dave, which lets Dave link the original `BlindSignature` to the `Proof` (see the privacy note in [NUT-12][12]). This is acceptable in a disaster-recovery context but is a real cost during normal-looking redemptions. A BLS-based ecash scheme with public verification would remove this trade-off; this NUT should adopt that path once Cashu supports it. + +### Consistency model + +The delegate's mirror does **not** need perfect consistency. If Dave is slightly behind the feed at the moment Bob fails, it may accept a small number of proofs that Bob had already marked spent, creating more valid claims than recovered reserve funds. + +This is acceptable for disaster recovery. A correctly-synced delegate should never hit it. If it does, Dave simply stops processing redemptions once the recovered reserve is exhausted (error `33003`). It is better that 99% of users redeem than that all users lose their funds. + +## Circular delegation + +A set of mints can each act as delegate for the others, e.g. for three mints: + +``` +Bob (custodian) -> Dave (delegate) -> Frank (delegate) +Dave (custodian) -> Frank (delegate) -> Bob (delegate) +Frank (custodian) -> Bob (delegate) -> Dave (delegate) +``` + +If one mint dies, another takes over its reserve. If two die, the survivor can eventually take over both. This yields a 1-of-n liveness property for the whole set. + +## Mint info setting + +A mint signals support via [NUT-06][06] with the following setting. Both roles are optional and a mint may populate either or both: + +```json +"nuts": { + "31": { + "supported": true, + "reserves": [ + { + "method": "onchain", + "descriptor": , + "address": + } + ], + "delegates": [ + { + "mint_url": , + "pubkey": , + "delay": + } + ], + "firehose": { + "ws": , + "http": + }, + "delegate_for": [ + { + "mint_url": , + "keysets": + } + ] + } +} +``` + +- `reserves` describes the on-chain reserve(s) backed by this scheme. `descriptor`/`address` are informational and let third parties watch the reserve; they are not required for the wire protocol. +- `delegates` lists the nominated delegate mints **in priority order**. `pubkey` is the delegate's reserve script-path key and `delay` is the relative timelock (in blocks) after which that delegate becomes eligible. The first entry has the smallest `delay`. +- `firehose` gives the WebSocket (`ws`) and HTTP catch-up (`http`) base URLs for this mint's spent-proof feed. +- `delegate_for` lists the mints this mint backs and the specific keyset IDs ([NUT-02][02]) it will honor on failover. Wallets use this to discover, ahead of time, where a given mint's tokens can be redeemed if it goes dark. + +Example: + +```json +"nuts": { + "31": { + "supported": true, + "reserves": [ + { "method": "onchain", "address": "bc1p...", "descriptor": null } + ], + "delegates": [ + { "mint_url": "https://dave.example", "pubkey": "02ab...", "delay": 1008 }, + { "mint_url": "https://frank.example", "pubkey": "03cd...", "delay": 1152 } + ], + "firehose": { + "ws": "https://bob.example/v1/firehose/ws", + "http": "https://bob.example/v1/firehose" + }, + "delegate_for": [ + { "mint_url": "https://dave.example", "keysets": ["00ad268c4d1f5826", "0193c8..."] } + ] + } +} +``` + +## Appendix: Reserve construction (informational) + +This section is **non-normative**. It describes a recommended on-chain construction that enforces the failover ordering; the Bitcoin-layer mechanics are outside the wallet↔mint HTTP API. See [NUT-30][30] for related onchain vocabulary. + +A mint holds its reserve in a Taproot output that is: + +- **Key path:** spendable immediately by the custodian (Bob). +- **Script path:** one leaf per delegate, each timelocked relative to the UTXO age with `OP_CHECKSEQUENCEVERIFY`. The first delegate's leaf becomes spendable after `delay₁` (e.g. ~7 days), the second after `delay₂` (e.g. ~8 days), and so on. + +``` +Bob reserve (Taproot output): + key path: + immediate: Bob (custodian) + script path: + after ~7 days: OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKSIG + after ~8 days: OP_CHECKSEQUENCEVERIFY OP_DROP OP_CHECKSIG +``` + +While Bob is live, it periodically **consolidates** reserve funds by spending the UTXO back to the same script via the key path. This rolls deposits into a single reserve UTXO and **re-arms** the relative timelocks, so delegates can never reach the reserve during normal operation. If Bob stops rolling the reserve, the first delegate becomes eligible after its timelock; if that delegate fails to act before the next timelock matures, the next delegate becomes eligible, and so on. + +This pairs well with TEE-based mints. A TEE reduces custody trust but increases liveness risk (losing access to sealed key material can strand reserves). Naming a non-TEE mint as a delegate restores liveness and can remove the TEE from the disaster-recovery path entirely. + +## Error codes + +See [error_codes.md][errors]. Codes relevant to this NUT: + +| Code | Description | +| ----- | -------------------------------------------- | +| 33001 | Proof from unknown or unmirrored keyset | +| 33002 | Delegation window not yet open | +| 33003 | Recovered reserve exhausted | +| 33004 | Proof DLEQ verification failed | +| 33005 | Firehose sequence out of range | + +[00]: 00.md +[01]: 01.md +[02]: 02.md +[03]: 03.md +[04]: 04.md +[05]: 05.md +[06]: 06.md +[07]: 07.md +[08]: 08.md +[09]: 09.md +[10]: 10.md +[11]: 11.md +[12]: 12.md +[14]: 14.md +[17]: 17.md +[21]: 21.md +[22]: 22.md +[30]: 30.md +[errors]: error_codes.md \ No newline at end of file From 5846f814c05c37ba183381810a2c5997b3f3da10 Mon Sep 17 00:00:00 2001 From: kvngmikey Date: Wed, 17 Jun 2026 20:18:37 +0300 Subject: [PATCH 2/3] chore: copilot comments and formatting --- README.md | 4 ++-- error_codes.md | 2 +- xx.md | 24 ++++++++++++------------ 3 files changed, 15 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 62633ce8..0a41a66d 100644 --- a/README.md +++ b/README.md @@ -46,7 +46,7 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio | [28][28] | Pay to Blinded Key (P2BK) | [cdk], [cashu-ts][ts] | - | | [29][29] | Batched Mint | - | - | | [30][30] | Payment Method: Onchain | - | - | -| [31][31] | Non-Interactive Mint Delegation | - | - | +| [XX][XX] | Non-Interactive Mint Delegation | - | - | #### Wallets @@ -106,4 +106,4 @@ Wallets and mints `MUST` implement all mandatory specs and `CAN` implement optio [28]: 28.md [29]: 29.md [30]: 30.md -[XX]: XX.md +[XX]: xx.md diff --git a/error_codes.md b/error_codes.md index 60cb6148..3df35b70 100644 --- a/error_codes.md +++ b/error_codes.md @@ -61,4 +61,4 @@ [21]: 21.md [22]: 22.md [29]: 29.md -[XX]: XX.md +[XX]: xx.md diff --git a/xx.md b/xx.md index b7678b98..c1267291 100644 --- a/xx.md +++ b/xx.md @@ -11,13 +11,13 @@ This NUT improves the **liveness** guarantees of custodial ecash mints that hold A mint can delegate to multiple other mints. If a set of mints all act as delegates for each other, they collectively inherit a **1-of-n liveness** property: as long as one mint in the set stays online, the users of every mint in the set can redeem their funds. > [!IMPORTANT] -> This is **ordered custodial failover, not trustless unilateral exit.** It improves recovery from liveness failures, not from malicious custody. Users are **not** protected if the original mint steals reserves before failing or is insolvent, and after failover they inherit the usual custodial trust assumptions under the delegate mint. It only ensures that *"the original mint disappeared"* no longer means *"the funds are lost forever."* +> This is **ordered custodial failover, not trustless unilateral exit.** It improves recovery from liveness failures, not from malicious custody. Users are **not** protected if the original mint steals reserves before failing or is insolvent, and after failover they inherit the usual custodial trust assumptions under the delegate mint. It only ensures that _"the original mint disappeared"_ no longer means _"the funds are lost forever."_ ## Overview The scheme has four parts, three of which are wire-protocol concerns specified normatively here, and one (the on-chain reserve) which is informational: -1. **Advertisement** — a Bobnnounces its delegates, the keysets it backs, and its spent-proof feed via [NUT-06][06] (normative). +1. **Advertisement** — a mint announces its delegates, the keysets it backs, and its spent-proof feed via [NUT-06][06] (normative). 2. **Spent-proof firehose** — a sequenced feed of spent-proof identifiers that delegate mints subscribe to and mirror locally (normative). 3. **Failover redemption** — how a delegate accepts and pays out the original mint's proofs after taking over (normative). 4. **Reserve construction** — a recommended on-chain Taproot construction that enforces the failover ordering (informational, see [Appendix](#appendix-reserve-construction-informational)). @@ -190,7 +190,7 @@ A mint signals support via [NUT-06][06] with the following setting. Both roles a ```json "nuts": { - "31": { + "xx": { "supported": true, "reserves": [ { @@ -229,7 +229,7 @@ Example: ```json "nuts": { - "31": { + "xx": { "supported": true, "reserves": [ { "method": "onchain", "address": "bc1p...", "descriptor": null } @@ -275,13 +275,13 @@ This pairs well with TEE-based mints. A TEE reduces custody trust but increases See [error_codes.md][errors]. Codes relevant to this NUT: -| Code | Description | -| ----- | -------------------------------------------- | -| 33001 | Proof from unknown or unmirrored keyset | -| 33002 | Delegation window not yet open | -| 33003 | Recovered reserve exhausted | -| 33004 | Proof DLEQ verification failed | -| 33005 | Firehose sequence out of range | +| Code | Description | +| ----- | --------------------------------------- | +| 33001 | Proof from unknown or unmirrored keyset | +| 33002 | Delegation window not yet open | +| 33003 | Recovered reserve exhausted | +| 33004 | Proof DLEQ verification failed | +| 33005 | Firehose sequence out of range | [00]: 00.md [01]: 01.md @@ -301,4 +301,4 @@ See [error_codes.md][errors]. Codes relevant to this NUT: [21]: 21.md [22]: 22.md [30]: 30.md -[errors]: error_codes.md \ No newline at end of file +[errors]: error_codes.md From 4973a8431cef1abc61e57f5aa7270a7369fb49bd Mon Sep 17 00:00:00 2001 From: kvngmikey Date: Mon, 22 Jun 2026 16:32:20 +0300 Subject: [PATCH 3/3] chore: comments addressed --- xx.md | 81 +++++++++++++++++++++++++++++------------------------------ 1 file changed, 40 insertions(+), 41 deletions(-) diff --git a/xx.md b/xx.md index c1267291..b5c07d36 100644 --- a/xx.md +++ b/xx.md @@ -67,56 +67,53 @@ The identifier of a spent proof reuses `Y = hash_to_curve(secret)` from [NUT-00] > [!IMPORTANT] > The original mint MUST publish exactly one `SpentEvent` for every proof it transitions to `SPENT` ([NUT-07][07]), and MUST do so durably (an event, once published, is never removed). The feed reflects only `SPENT` proofs; `PENDING` proofs are not published. -### Real-time stream (WebSocket) +### Real-time stream (Server-Sent Events) -For real-time mirroring, the firehose is exposed as a WebSocket at the mint's `/v1/firehose/ws` path, reusing the JSON-RPC framing of [NUT-17][17]. +For real-time mirroring, the firehose is exposed as a [Server-Sent Events][sse] stream at the mint's `/v1/firehose/sse` path. SSE is a good fit because the feed is strictly uni-directional (mint to delegate), runs over plain HTTP, and has native reconnection with resume. -``` -https://mint.host:3338/v1/firehose/ws +```http +GET https://mint.host:3338/v1/firehose/sse ``` -A delegate subscribes with a `WsRequest` carrying the `subscribe` method and a single `SubscriptionKind` value `spent_feed`: +The mint responds with `Content-Type: text/event-stream` and emits one SSE event per spent proof. The event's `id` field carries the `seq`, so it doubles as the resume cursor; the `data` field is the JSON-encoded `SpentEvent`: -```json -{ - "jsonrpc": "2.0", - "method": "subscribe", - "params": { - "kind": "spent_feed", - "subId": , - "filters": [] - }, - "id": -} +``` +id: 42 +event: spent +data: {"seq": 42, "Y": "02...", "id": "00ad...", "amount": 2, "witness": null} ``` -`subId` is a UUID v7 generated by the subscriber and `filters` is empty (the feed is not filtered). The mint replies with a [NUT-17][17] `WsResponse` and then streams every `SpentEvent` as a `WsNotification` as proofs are spent: +**Reconnection.** A standard `EventSource` client reconnects automatically and sends the last received `id` back in the `Last-Event-ID` HTTP header. On reconnection with a `Last-Event-ID` of `n`, the mint MUST resume the stream from `seq == n + 1`, so no events are missed across a dropped connection without any application-layer handshake. -```json -{ - "jsonrpc": "2.0", - "method": "subscribe", - "params": { - "subId": , - "payload": - } -} -``` +If the mint cannot resume from the requested `Last-Event-ID` (for example, it only retains recent events and the requested `seq` has been pruned), it MUST first emit a `reset` event giving the range it can serve, signalling that the client should bootstrap the missing prefix via the catch-up endpoint before trusting the live stream: -If a subscriber detects a gap (a received `seq` is greater than the last seen `seq` plus one), it MUST backfill the missing range via the catch-up endpoint below before treating its mirror as complete. +``` +event: reset +data: {"earliest": , "latest": } +``` ### Catch-up (HTTP) -A delegate that has been offline, or that is bootstrapping its mirror, fetches missed events over HTTP: +A delegate that is bootstrapping its mirror, or that needs to backfill events it could not resume over SSE, fetches them with a single batch request. The request can mix arbitrary `seq` ranges and individual `seq` indexes, so a client can fill several scattered gaps at once rather than only one consecutive block: ```http -GET https://mint.host:3338/v1/firehose?since=&limit= +POST https://mint.host:3338/v1/firehose/batch +``` + +```json +{ + "ranges": [ + { "from": , "to": }, + ... + ], + "indexes": [ , ... ] +} ``` -- `since` (optional, default `0`) returns events with `seq >= since`. -- `limit` (optional) caps the number of events returned; the mint MAY impose its own maximum. +- `ranges` is an array of inclusive `[from, to]` `seq` ranges. A range that omits `to` means "from `from` up to the latest event", which is how a fresh delegate bootstraps the whole feed (`{ "from": 0 }`). +- `indexes` is an array of individual `seq` values to fetch. -The mint responds with a `FirehoseResponse`: +Both fields are optional but at least one MUST be present. The mint responds with a `FirehoseResponse`: ```json { @@ -125,8 +122,10 @@ The mint responds with a `FirehoseResponse`: } ``` -- `events` is the ordered slice of `SpentEvent`s, ascending by `seq`. -- `latest` is the highest `seq` the mint has published, so the consumer knows whether more pages remain. +- `events` contains every requested `SpentEvent` that exists, ascending by `seq`, with duplicates from overlapping ranges/indexes removed. +- `latest` is the highest `seq` the mint has published, so the consumer knows whether more events remain beyond an open-ended range. + +The mint MAY cap the number of events returned per request; a client that receives a truncated response narrows its `ranges` and requests the remainder. A client MUST ensure it has contiguous `seq` coverage from `0` to `latest` before treating its mirror as complete. ### Consistency checkpoint (optional) @@ -146,7 +145,7 @@ GET https://mint.host:3338/v1/firehose/checkpoint } ``` -`seq` is the sequence number the checkpoint was taken at. A delegate compares each `root` against the same computation over its own mirror; a mismatch identifies which bucket to re-sync. The bucketing scheme (prefix length, hashing) is left to implementations and is out of scope for double-spend safety, it is a consistency aid, not a requirement for failover. +`seq` is the sequence number the checkpoint was taken at. A delegate compares each `root` against the same computation over its own mirror; a mismatch identifies which bucket diverged, and the delegate re-syncs it by fetching the affected `seq`s through the catch-up endpoint's `ranges`/`indexes`. The bucketing scheme (prefix length, hashing) is left to implementations and is out of scope for double-spend safety, it is a consistency aid, not a requirement for failover. ## Failover redemption @@ -207,7 +206,7 @@ A mint signals support via [NUT-06][06] with the following setting. Both roles a } ], "firehose": { - "ws": , + "sse": , "http": }, "delegate_for": [ @@ -222,7 +221,7 @@ A mint signals support via [NUT-06][06] with the following setting. Both roles a - `reserves` describes the on-chain reserve(s) backed by this scheme. `descriptor`/`address` are informational and let third parties watch the reserve; they are not required for the wire protocol. - `delegates` lists the nominated delegate mints **in priority order**. `pubkey` is the delegate's reserve script-path key and `delay` is the relative timelock (in blocks) after which that delegate becomes eligible. The first entry has the smallest `delay`. -- `firehose` gives the WebSocket (`ws`) and HTTP catch-up (`http`) base URLs for this mint's spent-proof feed. +- `firehose` gives the Server-Sent Events stream (`sse`) and the HTTP catch-up/batch (`http`) URLs for this mint's spent-proof feed. - `delegate_for` lists the mints this mint backs and the specific keyset IDs ([NUT-02][02]) it will honor on failover. Wallets use this to discover, ahead of time, where a given mint's tokens can be redeemed if it goes dark. Example: @@ -239,8 +238,8 @@ Example: { "mint_url": "https://frank.example", "pubkey": "03cd...", "delay": 1152 } ], "firehose": { - "ws": "https://bob.example/v1/firehose/ws", - "http": "https://bob.example/v1/firehose" + "sse": "https://bob.example/v1/firehose/sse", + "http": "https://bob.example/v1/firehose/batch" }, "delegate_for": [ { "mint_url": "https://dave.example", "keysets": ["00ad268c4d1f5826", "0193c8..."] } @@ -297,8 +296,8 @@ See [error_codes.md][errors]. Codes relevant to this NUT: [11]: 11.md [12]: 12.md [14]: 14.md -[17]: 17.md [21]: 21.md [22]: 22.md [30]: 30.md [errors]: error_codes.md +[sse]: https://html.spec.whatwg.org/multipage/server-sent-events.html