Protocols¶
This page walks through each operation in the payment scheme step by step.
Registration¶
Before any minting or paying, every client must register with all servers.
sequenceDiagram
participant C as Client
participant S0 as Server 0
participant S1 as Server 1
participant S2 as …
participant S4 as Server 4
C->>S0: POST /register {id, public_key}
C->>S1: POST /register {id, public_key}
C->>S2: POST /register {id, public_key}
C->>S4: POST /register {id, public_key}
Note over S0,S4: Each server stores the client entry<br/>with initial balance and waits until<br/>all expected clients have registered.
S0-->>C: 200 OK
S1-->>C: 200 OK
S2-->>C: 200 OK
S4-->>C: 200 OK
The server blocks the response until all num_clients have registered (using asyncio.Event), ensuring a synchronized start.
The client uses a timeout that equals 2 · Δ, where Δ is the network synchronization delay.
Mint¶
Minting creates a new token of value 1, consuming 1 unit of the client's un-minted balance.
sequenceDiagram
participant C as Client
participant S as Server Quorum (f+1)
Note over C: 1. Generate fresh key pair (pk, sk)
Note over C: 2. Build TokenPayload = {public_key: pk}
Note over C: 3. Blind: (H', r) = blind(TokenPayload)
Note over C: 4. Build MintRequest = {id, blinded_message: H'}
Note over C: 5. Sign MintRequest with long-term sk
C->>S: POST /mint { payload, signature }
Note over S: Verify client identity
Note over S: Check blinded_message not reused (nullifier)
Note over S: Check balance ≥ 1
Note over S: Verify signature against client's public key
Note over S: Deduct balance by 1
Note over S: Compute σ'ᵢ = H'^{sᵢ}
S-->>C: PartialSignature {id, σ'ᵢ}
Note over C: 6. Collect f+1 partial signatures
Note over C: 7. Combine: σ' = Σ λᵢ·σ'ᵢ
Note over C: 8. Unblind: σ = r⁻¹·σ'
Note over C: 9. Store Token = {payload: TokenPayload, signature: σ}
Note over C: 10. Store sk alongside the token
What the server sees vs. what the token contains¶
| Server sees | Token contains |
|---|---|
Client identity (id) |
Not stored — token is anonymous |
H' = r·H(TokenPayload) (blinded) |
TokenPayload (plaintext public key) |
σ'ᵢ (partial blind signature) |
σ = H(TokenPayload)^{SK} (unblinded full signature) |
Because r is secret and random, the server cannot derive H(TokenPayload) from H', nor link σ back to σ'ᵢ.
Pay¶
Payment transfers a token from one client (sender) to another (recipient).
sequenceDiagram
participant Sender as Sender Client
participant Recipient as Recipient Client
participant S as Server Quorum (f+1)
Sender->>Recipient: GET /payment-key
Note over Recipient: 1. Generate fresh key pair (pk_r, sk_r)
Note over Recipient: 2. Build TokenPayload_r = {public_key: pk_r}
Note over Recipient: 3. Blind: (H'_r, r_r) = blind(TokenPayload_r)
Note over Recipient: 4. Store (sk_r, r_r, payload_bytes) in pending_keys
Recipient-->>Sender: H'_r (blinded one-time key)
Note over Sender: 5. Pick a token from wallet
Note over Sender: 6. Build Transaction = {token, recipient_blinded_payload: H'_r}
Note over Sender: 7. Sign Transaction with the token's one-time sk
Sender->>S: POST /pay { payload, signature }
Note over S: Verify token signature against system PK
Note over S: Check token not in nullifier set
Note over S: Verify transaction signature against token's pk
Note over S: Add token to nullifier set
Note over S: Compute σ'ᵢ = H'_r^{sᵢ}
S-->>Sender: PartialSignature {id, σ'ᵢ}
Note over Sender: 8. Combine: σ'_r = Σ λᵢ·σ'ᵢ
Note over Sender: 9. Build Payment = {blinded_payload: H'_r, blinded_signature: σ'_r}
Sender->>Recipient: POST /pay {blinded_payload, blinded_signature}
Note over Recipient: 10. Look up (sk_r, r_r, payload_bytes) by blinded_payload
Note over Recipient: 11. Unblind: σ_r = r_r⁻¹·σ'_r
Note over Recipient: 12. Verify: verify(payload_bytes, σ_r, system_PK) ✓
Note over Recipient: 13. Store Token = {payload: payload_bytes, signature: σ_r}
Value flow¶
- The sender's token is consumed (added to every server's nullifier set).
- The recipient receives a fresh token — independently signed, with a new one-time key pair.
- Total value is conserved: 1 token spent → 1 token created.
Broadcast and Quorum¶
The client broadcasts every request to all n servers concurrently using asyncio.gather. It collects responses and considers the operation successful once f + 1 servers respond with HTTP 200.
graph LR
C[Client] --> |POST| S0[Server 0]
C --> |POST| S1[Server 1]
C --> |POST| S2[Server 2]
C --> |POST| S3[Server 3]
C --> |POST| S4[Server 4]
S0 --> |200 ✓| C
S1 --> |200 ✓| C
S2 --> |200 ✓| C
S3 --> |timeout ✗| C
S4 --> |timeout ✗| C
style S3 stroke:#f66,stroke-width:2px
style S4 stroke:#f66,stroke-width:2px
If fewer than f + 1 servers respond successfully, the client raises a RuntimeError. This provides liveness as long as at most f servers suffer omission failures.
Communication Protocol Summary¶
All communication uses HTTP/JSON over FastAPI:
| From | To | Transport | Serialization |
|---|---|---|---|
| Client → Server | /register, /mint, /pay |
HTTP POST | Pydantic JSON |
| Server → Client | Response body | HTTP 200 | Pydantic JSON (PartialSignature) |
| Sender → Recipient | /payment-key (GET), /pay (POST) |
HTTP | Pydantic JSON |