From 91d7fc471f5364fa2fc8b6040426b01c9e8fbff2 Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 21 Mar 2026 12:40:48 -0400 Subject: [PATCH 1/2] Add Solana session intent specification (draft-00) --- .../methods/solana/draft-solana-session-00.md | 1094 +++++++++++++++++ 1 file changed, 1094 insertions(+) create mode 100644 specs/methods/solana/draft-solana-session-00.md diff --git a/specs/methods/solana/draft-solana-session-00.md b/specs/methods/solana/draft-solana-session-00.md new file mode 100644 index 00000000..a948b580 --- /dev/null +++ b/specs/methods/solana/draft-solana-session-00.md @@ -0,0 +1,1094 @@ +--- +title: Solana Session Intent for HTTP Payment Authentication +abbrev: Solana Session +docname: draft-solana-session-00 +version: 00 +category: info +ipr: trust200902 +submissiontype: independent +consensus: false + +author: + - name: Alexander Attar + ins: A. Attar + email: alexanderattar@gmail.com + +normative: + RFC2119: + RFC3339: + RFC4648: + RFC8174: + RFC8259: + RFC8785: + RFC9457: + I-D.httpauth-payment: + title: "The 'Payment' HTTP Authentication Scheme" + target: https://datatracker.ietf.org/doc/draft-ietf-httpauth-payment/ + author: + - name: Jake Moxey + date: 2026-01 + I-D.solana-charge: + title: "Solana Charge Intent for HTTP Payment Authentication" + target: https://datatracker.ietf.org/doc/draft-solana-charge/ + author: + - name: Ludo Galabru + - name: Ilan Gitter + date: 2026 + +informative: + SOLANA-DOCS: + title: "Solana Documentation" + target: https://solana.com/docs + author: + - org: Solana Foundation + date: 2026 + SPL-TOKEN: + title: "SPL Token Program" + target: https://solana.com/docs/tokens + author: + - org: Solana Foundation + date: 2026 + SPL-TOKEN-2022: + title: "SPL Token-2022 Program" + target: https://solana.com/docs/tokens/extensions + author: + - org: Solana Foundation + date: 2026 + ED25519-PROGRAM: + title: "Solana Ed25519 Program" + target: https://solana.com/docs/core/programs#ed25519-program + author: + - org: Solana Foundation + date: 2026 + BASE58: + title: "Base58 Encoding Scheme" + target: https://datatracker.ietf.org/doc/html/draft-msporny-base58-03 + author: + - name: Manu Sporny + date: 2023 +--- + +--- abstract + +This document defines the "session" intent for the "solana" payment +method within the Payment HTTP Authentication Scheme +{{I-D.httpauth-payment}}. The client deposits SPL tokens into an +on-chain escrow program, creating a unidirectional payment channel; +subsequent requests are authorized by off-chain Ed25519-signed +vouchers with cumulative amounts that the server verifies locally. +Settlement occurs when the channel is partially settled or closed. + +Two credential types are supported: `type="channel_open"`, where +the client presents proof of the on-chain channel deposit, and +`type="voucher"`, where the client presents an off-chain signed +voucher authorizing cumulative payment. + +--- middle + +# Introduction + +HTTP Payment Authentication {{I-D.httpauth-payment}} defines a +challenge-response mechanism that gates access to resources behind +payments. This document registers the "session" intent for the +"solana" payment method. + +The Solana charge intent {{I-D.solana-charge}} handles one-time +payments where each request requires an on-chain transaction. +Sessions aggregate many payments into a single on-chain +settlement, making them suitable for high-frequency use cases +where per-request on-chain transactions would be +cost-prohibitive {{SOLANA-DOCS}}. + +## Channel Open Phase {#channel-open-phase} + +The client deposits SPL tokens {{SPL-TOKEN}} into an on-chain +escrow program, creating a unidirectional payment channel: + +~~~ + Client Server Solana Network + | | | + | (1) GET /resource | | + |-----------------------> | | + | | | + | (2) 402 Payment Required| | + | (recipient, amount, | | + | escrowProgram) | | + |<----------------------- | | + | | | + | (3) Build open_channel | | + | tx, deposit SPL | | + | tokens, sign | | + | | | + | (4) Send transaction | | + |-----------------------------------------------> | + | (5) Confirmation | | + |<----------------------------------------------- | + | | | + | (6) Authorization: | | + | Payment | | + | (channel_open proof)| | + |-----------------------> | | + | | (7) getTransaction | + | |----------------------> | + | | (8) Verified deposit | + | |<---------------------- | + | | | + | (9) 200 OK + Receipt | | + |<----------------------- | | + | | | +~~~ + +The client broadcasts the channel-open transaction itself and +presents the confirmed transaction signature. The server +verifies the deposit on-chain and initializes session state. + +## Active Session Phase {#active-session-phase} + +Once the channel is open, subsequent requests use off-chain +Ed25519-signed vouchers with no on-chain interaction: + +~~~ + Client Server + | | + | (1) GET /resource | + |-----------------------> | + | | + | (2) 402 Payment Required| + | (same session) | + |<----------------------- | + | | + | (3) Sign voucher with | + | incremented amount | + | | + | (4) Authorization: | + | Payment | + | (voucher + sig) | + |-----------------------> | + | | + | (5) Verify Ed25519 sig | + | (CPU-only, ~usec) | + | | + | (6) 200 OK + Receipt | + |<----------------------- | + | | +~~~ + +Verification during this phase is a single Ed25519 signature +check: pure CPU, no RPC calls, microsecond latency. + +## Relationship to the Solana Charge Intent + +This document shares the `method="solana"` payment method with +{{I-D.solana-charge}} but uses `intent="session"` instead of +`intent="charge"`. Both intents use the same encoding +conventions (JCS canonicalization, base64url encoding) and +follow the same shared field semantics for `amount`, `currency`, +and `recipient`. + +# Requirements Language + +{::boilerplate bcp14-tagged} + +# Terminology + +Payment Channel +: A unidirectional channel where a payer deposits tokens into + an on-chain escrow and issues off-chain vouchers to a + recipient. Settlement occurs when the channel is partially + settled or closed. + +Channel PDA +: A Program Derived Address on Solana that holds the escrowed + SPL tokens and channel state. Derived deterministically from + stable seeds (payer pubkey, recipient pubkey, channel nonce). + The channel PDA address serves as the channel identifier + throughout the session lifecycle. + +Voucher +: An off-chain Ed25519-signed message authorizing a cumulative + payment amount. Each voucher supersedes all previous vouchers + for the same channel. The server grants access based on the + delta between consecutive voucher amounts. + +Cumulative Amount +: The total authorized payment from channel open to the current + voucher. Each voucher's cumulative amount MUST be greater + than or equal to the previous voucher's cumulative amount. + +Voucher Nonce +: A monotonically increasing counter included in each voucher. + Prevents replay of older vouchers with the same cumulative + amount. + +Escrow Program +: The on-chain Solana program that manages payment channel + state, holds deposited tokens, and enforces settlement rules. + +Base Units +: The smallest transferable unit of an SPL token, determined + by the token's decimal precision. For example, USDC uses + 6 decimals, so 1 USDC = 1,000,000 base units. + +# Intent Identifier + +The intent identifier for this specification is "session". +It MUST be lowercase. + +# Intent: "session" + +The "session" intent represents a long-lived payment +authorization gating access to a resource over multiple +requests. The client opens a payment channel on-chain once, +then signs off-chain vouchers with monotonically increasing +cumulative amounts for each request. The server verifies each +voucher locally and grants access for the delta between the +new and previous cumulative amount. Settlement occurs +on-chain when the channel is partially settled or closed. + +# Encoding Conventions {#encoding} + +All JSON {{RFC8259}} objects carried in auth-params or HTTP +headers in this specification MUST be serialized using the JSON +Canonicalization Scheme (JCS) {{RFC8785}} before encoding. JCS +produces a deterministic byte sequence, which is required for +any digest or signature operations defined by the base spec +{{I-D.httpauth-payment}}. + +The resulting bytes MUST then be encoded using base64url +{{RFC4648}} Section 5 without padding characters (`=`). +Implementations MUST NOT append `=` padding when encoding, +and MUST accept input with or without padding when decoding. + +This encoding convention applies to: the `request` auth-param +in `WWW-Authenticate`, the credential token in `Authorization`, +and the receipt token in `Payment-Receipt`. + +# Request Schema + +## Shared Fields + +The `request` auth-param of the `WWW-Authenticate: Payment` +header contains a JCS-serialized, base64url-encoded JSON +object (see {{encoding}}). The following shared fields are +included in that object: + +amount +: REQUIRED. The cost per request in base units, encoded as a + decimal string. For SPL tokens, base units are the token's + smallest unit (e.g., for USDC with 6 decimals, "1000" + represents 0.001 USDC per request). The value MUST be a + positive integer that fits in a 64-bit unsigned integer + (max 18,446,744,073,709,551,615). + +currency +: REQUIRED. MUST be the base58-encoded {{BASE58}} mint address + of the SPL token (e.g., + `"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"` for + USDC). The mint address uniquely identifies the token and + is used by the client to construct the deposit and voucher. + MUST NOT exceed 128 characters. Native SOL sessions are + not supported in this version; use the charge intent + {{I-D.solana-charge}} for native SOL payments. + +description +: OPTIONAL. A human-readable memo describing the resource or + service being paid for. MUST NOT exceed 256 characters. + +recipient +: REQUIRED. The base58-encoded public key of the account + receiving payments. This is the owner of the destination + associated token account, not the ATA address itself. + +## Method Details + +The following fields are nested under `methodDetails` in +the request JSON: + +network +: OPTIONAL. Identifies which Solana cluster the session + operates on. MUST be one of "mainnet-beta", "devnet", + or "localnet". Defaults to "mainnet-beta" if omitted. + Clients MUST reject challenges whose network does not + match their configured cluster. + +decimals +: REQUIRED. The number of decimal places for the token + (0-9). Used by the client for voucher amount construction + and deposit instruction parameters. + +escrowProgram +: REQUIRED. The base58-encoded program ID of the on-chain + escrow program that manages payment channels. The client + uses this to construct the channel-open transaction. + +reference +: REQUIRED. A server-generated unique identifier for this + payment challenge, encoded as a string. MUST NOT exceed + 128 characters. The server uses this value to correlate + incoming credentials with issued challenges and to enforce + single-use semantics. MUST be unique per challenge. + +suggestedDeposit +: OPTIONAL. The server's recommended initial deposit amount + in base units, encoded as a decimal string. Clients SHOULD + use `min(suggestedDeposit, maxDeposit)` where `maxDeposit` + is the client's configured spending limit. If omitted, + clients MAY choose their own deposit amount. + +timeout +: OPTIONAL. Channel timeout duration in seconds, encoded as + a decimal string. After `opened_at + timeout`, the payer + may reclaim unspent tokens via the `reclaim` instruction. + Defaults to "3600" (1 hour) if omitted. + +tokenProgram +: OPTIONAL. The base58-encoded program ID of the token + program governing the token. MUST be either the Token + Program (`TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`) + or the Token-2022 Program + (`TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`) + {{SPL-TOKEN-2022}}. If omitted, clients MUST determine + the correct token program by fetching the mint account + from the network and inspecting its owner program. + Servers SHOULD include this field as a hint to avoid + the extra RPC lookup. + +### Session Challenge Example + +~~~json +{ + "amount": "1000", + "currency": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + "description": "LLM inference API", + "methodDetails": { + "network": "mainnet-beta", + "decimals": 6, + "escrowProgram": "MPPsession1111111111111111111111111111111", + "reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "suggestedDeposit": "1000000", + "timeout": "3600", + "tokenProgram": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + } +} +~~~ + +This requests a session charging 0.001 USDC (1000 base units) +per request, with a suggested initial deposit of 1 USDC +(1,000,000 base units) and a 1-hour channel timeout. + +# Credential Schema + +The `Authorization` header carries a single base64url-encoded +JSON token (no auth-params). The decoded object contains the +following top-level fields: + +challenge +: REQUIRED. An echo of the challenge auth-params from the + `WWW-Authenticate` header: `id`, `realm`, `method`, + `intent`, `request`, and (if present) `expires`. This + binds the credential to the exact challenge that was + issued. + +source +: OPTIONAL. A payer identifier string, as defined by + {{I-D.httpauth-payment}}. Solana implementations MAY + use the payer's base58-encoded public key or a DID. + +payload +: REQUIRED. A JSON object containing the Solana-specific + credential fields. The `type` field determines which + additional fields are present. Two payload types are + defined: `"channel_open"` and `"voucher"`. + +## Channel Open Payload {#channel-open-payload} + +When opening a channel (`type="channel_open"`), the client +sends proof of the on-chain deposit transaction. The client +broadcasts the channel-open transaction itself and presents +the confirmed transaction signature. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | REQUIRED | `"channel_open"` | +| `channelId` | string | REQUIRED | Base58-encoded channel PDA address | +| `signature` | string | REQUIRED | Base58-encoded transaction signature of the channel-open transaction | +| `deposit` | string | REQUIRED | Deposit amount in base units | + +The `channelId` is the base58-encoded address of the channel +PDA, which serves as the unique identifier for this payment +channel throughout the session lifecycle. + +Example (decoded): + +~~~json +{ + "challenge": { + "id": "kM9xPqWvT2nJrHsY4aDfEb", + "realm": "api.example.com", + "method": "solana", + "intent": "session", + "request": "eyJ...", + "expires": "2026-03-21T12:05:00Z" + }, + "payload": { + "type": "channel_open", + "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", + "signature": "5UfDuX7hXbPjGUpTmt9PHRLsNGJe4dEny...", + "deposit": "1000000" + } +} +~~~ + +## Voucher Payload {#voucher-payload} + +For each request during an active session +(`type="voucher"`), the client presents an off-chain +Ed25519-signed voucher with a monotonically increasing +cumulative amount. + +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `type` | string | REQUIRED | `"voucher"` | +| `channelId` | string | REQUIRED | Base58-encoded channel PDA address | +| `cumulativeAmount` | string | REQUIRED | Cumulative payment in base units | +| `nonce` | string | REQUIRED | Monotonically increasing voucher nonce | +| `expiry` | string | REQUIRED | Voucher expiry as {{RFC3339}} timestamp | +| `signature` | string | REQUIRED | Base64-encoded Ed25519 signature over the 206-byte voucher message (see {{voucher-format}}) | + +Example (decoded): + +~~~json +{ + "challenge": { + "id": "kM9xPqWvT2nJrHsY4aDfEb", + "realm": "api.example.com", + "method": "solana", + "intent": "session", + "request": "eyJ...", + "expires": "2026-03-21T12:05:00Z" + }, + "payload": { + "type": "voucher", + "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", + "cumulativeAmount": "5000", + "nonce": "5", + "expiry": "2026-03-21T13:00:00Z", + "signature": "SGVsbG8gV29ybGQhIFRoaXMgaXMgYW4g..." + } +} +~~~ + +# Voucher Format {#voucher-format} + +Vouchers are structured binary messages signed with the +payer's Ed25519 keypair. The format is domain-separated and +binds to all relevant context to prevent cross-channel, +cross-program, and cross-cluster replay. + +~~~ +Voucher message layout (206 bytes): + bytes[0..21]: "mpp-solana-session-v1" (ASCII, 21 bytes) + bytes[21..53]: channel PDA (32 bytes) + bytes[53..85]: escrow program ID (32 bytes) + bytes[85..117]: payer pubkey (32 bytes) + bytes[117..149]: recipient pubkey (32 bytes) + bytes[149..181]: mint pubkey (32 bytes) + bytes[181..189]: cumulative amount (u64 little-endian) + bytes[189..197]: voucher nonce (u64 little-endian) + bytes[197..205]: expiry timestamp (i64 little-endian, Unix seconds) + bytes[205]: cluster discriminator (0=mainnet, 1=devnet, 2=localnet) +~~~ + +The 21-byte ASCII domain tag `"mpp-solana-session-v1"` prevents +confusion with any other Ed25519-signed message format. The +signature is Ed25519 over the 206-byte message, signed by the +payer's keypair. The resulting 64-byte signature is base64-encoded +in the credential payload. + +# Verification Procedure {#verification} + +Upon receiving a request with a credential, the server MUST: + +1. Decode the base64url credential and parse the JSON. + +2. Verify that `payload.type` is present and is either + `"channel_open"` or `"voucher"`. + +3. Look up the stored challenge using + `credential.challenge.id`. If no matching challenge + is found, reject the request. + +4. Verify that all fields in `credential.challenge` + exactly match the stored challenge auth-params. + +5. Proceed with type-specific verification: + - For `type="channel_open"`: see {{channel-open-verification}}. + - For `type="voucher"`: see {{voucher-verification}}. + +## Channel Open Verification {#channel-open-verification} + +For credentials with `type="channel_open"`: + +1. Verify that `payload.signature` is present and is a + valid base58-encoded string. + +2. Fetch the transaction from the Solana network using + the RPC `getTransaction` method with `jsonParsed` + encoding and at least `confirmed` commitment level. + +3. Verify the transaction was successful (no error in + the transaction metadata). + +4. Verify the transaction contains an `open_channel` + instruction to the `escrowProgram` from the challenge. + +5. Verify the channel PDA at `payload.channelId` was + created with the correct parameters: recipient matches + the challenge `recipient`, mint matches `currency`, and + the deposit amount matches `payload.deposit`. + +6. Initialize server-side session state for this channel: + store the channel PDA address, payer pubkey, deposit + amount, and set the cumulative amount and voucher nonce + to zero. + +7. Return the resource with a Payment-Receipt header. + +## Voucher Verification {#voucher-verification} + +For credentials with `type="voucher"`: + +1. Look up the server's stored session state for the + channel at `payload.channelId`. If no active session + exists for this channel, reject the credential. + +2. Reconstruct the 206-byte voucher message from the + credential fields and the stored session parameters + (channel PDA, escrow program ID, payer pubkey, + recipient, mint, cluster). + +3. Verify the Ed25519 signature in `payload.signature` + against the reconstructed 206-byte message using the + stored payer pubkey. + +4. Verify the `cumulativeAmount` is greater than or equal + to the server's previously-recorded cumulative amount + for this channel. + +5. Verify the `nonce` is strictly greater than the server's + previously-recorded nonce for this channel. + +6. Verify the `expiry` timestamp has not passed. + +7. Atomically update the server's session state: set the + cumulative amount to `payload.cumulativeAmount` and the + nonce to `payload.nonce`. + +8. Grant access for the delta: + `cumulativeAmount - previousCumulativeAmount`. + +9. Return the resource with a Payment-Receipt header. + +The cumulative-amount update in step 7 MUST be atomic to +prevent race conditions where concurrent requests count +the same voucher delta twice. See {{server-state}}. + +# Settlement Procedure + +## Channel Lifecycle + +The channel progresses through the following states: + +### Partial Settlement {#partial-settlement} + +The server MAY submit the latest voucher on-chain via the +escrow program's `settle` instruction at any time during +an active session. This transfers the delta +(`voucher.cumulativeAmount - channel.cumulativePaid`) to +the recipient's associated token account and updates the +channel's on-chain `cumulativePaid` and `voucherNonce` +fields. The channel remains open for continued use. + +Servers SHOULD settle periodically to limit counterparty +risk (the amount at risk if the channel is abandoned). + +### Channel Close {#channel-close} + +The recipient closes the channel by submitting the latest +voucher via the escrow program's `close` instruction: + +1. Verify the voucher signature on-chain (see + {{on-chain-verification}}). +2. Transfer the final delta + (`voucher.cumulativeAmount - channel.cumulativePaid`) + to the recipient's associated token account. +3. Transfer the remainder + (`channel.deposit - voucher.cumulativeAmount`) to the + payer's associated token account. +4. Close the channel PDA and its token account. + +### Timeout Reclaim {#timeout-reclaim} + +If the current time exceeds `channel.expiryAt` (computed +as `openedAt + timeout`), the payer may call the escrow +program's `reclaim` instruction to recover +`channel.deposit - channel.cumulativePaid`. This works +whether `cumulativePaid` is zero or greater than zero. + +### Timeout Rules {#timeout-rules} + +The following rules govern which instructions are valid +relative to timestamps: + +- `settle` requires: `now <= voucher.expiry` AND + `now <= channel.expiryAt` +- `close` requires: `now <= voucher.expiry` AND + `now <= channel.expiryAt` +- `reclaim` requires: `now > channel.expiryAt` + +After `channel.expiryAt`, the recipient can no longer +settle or close. Only the payer can act, via `reclaim`. + +## On-Chain Voucher Verification {#on-chain-verification} + +On-chain voucher verification (for `settle` and `close`) +uses Solana's Ed25519 precompile program +(`Ed25519SigVerify111111111111111111111111111`) +{{ED25519-PROGRAM}}. + +The Ed25519 precompile is NOT callable via CPI. The +correct pattern: + +1. The transaction includes an Ed25519 verify instruction + that checks the payer's signature over the 206-byte + voucher message. + +2. The escrow program reads the instructions sysvar + (`Sysvar1nstructions1111111111111111111111111`) and + validates that the Ed25519 instruction exists, verified + the correct public key, and verified the correct + message bytes. + +3. If the Ed25519 instruction is missing, references a + different key, or references different message data, + the program MUST reject the transaction. + +Incorrect Ed25519 validation enables unauthorized +withdrawal of escrowed funds. See {{ed25519-security}}. + +## Channel PDA Derivation {#pda-derivation} + +The channel PDA is derived from stable seeds only: + +~~~ +seeds = [ + "mpp-channel", + payer_pubkey, + recipient_pubkey, + channel_nonce (u64 little-endian) +] +~~~ + +The `channel_nonce` is a sequential counter set once at +channel creation, NOT the voucher nonce. This produces a +stable PDA address that does not change as vouchers are +issued. The channel PDA address serves as the `channelId` +in credentials and receipts. + +## Client Transaction Construction + +### Channel Open + +The client MUST construct a transaction containing an +`open_channel` instruction to the escrow program that: + +1. Creates the channel PDA with the correct seeds + (see {{pda-derivation}}). +2. Transfers SPL tokens from the client's associated + token account to the channel's token account via + the appropriate token program {{SPL-TOKEN}}. +3. Initializes the channel state: payer, recipient, + mint, deposit amount, timeout, and timestamps. + +The client MUST be the fee payer and MUST fully sign +the transaction. The client MUST wait for at least +`confirmed` commitment before presenting the credential. + +### Voucher Signing + +For each request during an active session, the client: + +1. Increments the voucher nonce. +2. Computes the new cumulative amount + (`previousCumulativeAmount + amount`). +3. Constructs the 206-byte voucher message + (see {{voucher-format}}). +4. Signs the message with Ed25519 using the payer's + keypair. +5. Presents the signature in a `type="voucher"` + credential. + +## Confirmation Requirements + +For `type="channel_open"` credentials, clients MUST wait +for at least the `confirmed` commitment level before +presenting the credential. Servers MUST fetch the +transaction with at least `confirmed` commitment. + +## Finality + +Solana provides two commitment levels relevant to +payment verification: + +- `confirmed`: optimistic confirmation from a + supermajority of validators (~400ms). Sufficient + for most payment use cases. +- `finalized`: deterministic finality after ~31 slots + (~12 seconds). Required for high-value transactions + where rollback risk is unacceptable. + +The `confirmed` level is RECOMMENDED as the default for +channel-open verification to minimize latency. Servers +MAY require `finalized` commitment for channels with +large deposits. + +## Receipt Generation + +Upon successful verification, the server MUST include +a `Payment-Receipt` header in the 200 response. + +The receipt payload for Solana session: + +| Field | Type | Description | +|-------|------|-------------| +| `method` | string | `"solana"` | +| `challengeId` | string | The challenge `id` from `WWW-Authenticate` | +| `reference` | string | For `channel_open`: the transaction signature (base58). For `voucher`: the channel PDA address (base58). | +| `status` | string | `"success"` | +| `timestamp` | string | {{RFC3339}} verification time | + +For `type="channel_open"`, the `reference` is the on-chain +transaction signature. For `type="voucher"`, the `reference` +is the channel PDA address, since no on-chain transaction +occurs during voucher verification. + +Example receipt for a voucher credential (decoded): + +~~~json +{ + "method": "solana", + "challengeId": "kM9xPqWvT2nJrHsY4aDfEb", + "reference": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", + "status": "success", + "timestamp": "2026-03-21T12:04:58Z" +} +~~~ + +# Server State Requirements {#server-state} + +Servers MUST track per-channel session state. The following +fields are required: + +- Channel PDA address (`channelId`) +- Payer pubkey +- Recipient pubkey +- Mint pubkey +- Deposit amount +- Highest cumulative amount received +- Highest voucher nonce received + +The cumulative-amount and nonce update MUST be atomic to +prevent race conditions where concurrent requests count +the same voucher delta twice. In-process locks (e.g., +mutexes) are NOT safe across multiple server instances. +Horizontally-scaled deployments MUST use an external +atomic store (e.g., Redis with WATCH/MULTI, PostgreSQL +with row-level locks, or equivalent). + +# Error Responses + +When rejecting a credential, the server MUST return HTTP +402 (Payment Required) with a fresh +`WWW-Authenticate: Payment` challenge per +{{I-D.httpauth-payment}}. The server SHOULD include a +response body conforming to RFC 9457 {{RFC9457}} Problem +Details, with `Content-Type: application/problem+json`. +Servers MUST use the standard problem types defined in +{{I-D.httpauth-payment}}: `malformed-credential`, +`invalid-challenge`, and `verification-failed`. The +`detail` field SHOULD contain a human-readable +description of the specific failure. + +All error responses MUST include a fresh challenge in +`WWW-Authenticate`. + +Example error response body: + +~~~json +{ + "type": "https://paymentauth.org/problems/verification-failed", + "title": "Invalid Voucher", + "status": 402, + "detail": "Voucher nonce 3 is not greater than previously accepted nonce 5" +} +~~~ + +# Security Considerations + +## Transport Security + +All communication MUST use TLS 1.2 or higher. Session +credentials MUST only be transmitted over HTTPS +connections. + +## Voucher Replay Protection + +Each voucher carries a monotonically increasing nonce and +cumulative amount. The server MUST reject vouchers with a +nonce less than or equal to the highest previously-accepted +nonce for the channel. The domain-separated voucher format +(206 bytes with ASCII prefix, program ID, and cluster +discriminator) prevents cross-channel, cross-program, and +cross-cluster replay. A voucher accepted for one channel +cannot be replayed against a different channel, program, +or cluster because the signed message includes all of +these identifiers. + +## Ed25519 On-Chain Verification {#ed25519-security} + +The Solana Ed25519 precompile is NOT callable via CPI. +On-chain voucher verification (settle, close) MUST use +the instructions sysvar pattern described in +{{on-chain-verification}}. If this is implemented +incorrectly, an attacker can call settle or close with +arbitrary voucher data, bypassing signature verification +entirely. This is the single highest-risk component of +the escrow program. + +Implementations MUST include adversarial tests that +verify the following cases are rejected: + +- Missing Ed25519 verify instruction +- Ed25519 instruction verifying a different public key +- Ed25519 instruction verifying different message data + +## Escrow Program Security + +The escrow program MUST verify: + +- Only the payer can deposit and reclaim +- Only the recipient can settle and close +- Voucher signatures match the channel's payer pubkey +- Cumulative amounts only increase +- Voucher nonces strictly increase +- Timeout rules are enforced per {{timeout-rules}} + +## Counterparty Risk + +During an active session, the server carries risk equal to +the cumulative authorized amount minus the last on-chain +settlement. If the payer's signing key is compromised or +the payer disappears, the server holds the latest voucher +as its claim on escrowed funds. Servers SHOULD settle +periodically to reduce exposure. The timeout mechanism +ensures the payer can recover funds if the recipient +disappears or refuses to close the channel. + +## Client-Side Verification + +Clients MUST verify the challenge before depositing: + +1. `recipient` is the expected party +2. `amount` per request is reasonable for the service +3. `currency` matches the expected token +4. `escrowProgram` is the expected program +5. `suggestedDeposit` is within acceptable limits + +Malicious servers could request excessive deposits, +direct payments to unexpected recipients, or specify +rogue escrow programs. + +## RPC Trust + +The server relies on its Solana RPC endpoint to provide +accurate transaction data for channel-open verification. +A compromised RPC could return fabricated transaction +data, causing the server to accept deposits that were +never made. Servers SHOULD use trusted RPC providers +or run their own nodes. + +# IANA Considerations + +## Payment Method Registration + +This document uses the `solana` method identifier +registered by {{I-D.solana-charge}}. + +## Payment Intent Registration + +This document requests registration of the following +entry in the "HTTP Payment Intents" registry established +by {{I-D.httpauth-payment}}: + +| Intent | Applicable Methods | Description | Reference | +|--------|-------------------|-------------|-----------| +| `session` | `solana` | Streaming SPL token payments via payment channels | This document | + +--- back + +# Examples + +The following examples illustrate the complete HTTP exchange +for each credential type. Base64url values are shown with +their decoded JSON below. + +## Session Open (Channel Deposit) + +A session charging 0.001 USDC per request. The client +deposits 1 USDC. + +**1. Challenge (402 response):** + +~~~http +HTTP/1.1 402 Payment Required +WWW-Authenticate: Payment id="kM9xPqWvT2nJrHsY4aDfEb", + realm="api.example.com", + method="solana", + intent="session", + request="eyJhbW91bnQiOiIxMDAwIiwiY3VycmVuY3kiOiJFUGpG + V2RkNUF1ZnFTU3FlTTJxTjF4enliYXBDOEc0d0VHR2tad3lURH + QxdiIsImRlc2NyaXB0aW9uIjoiTExNIGluZmVyZW5jZSBBUEki + LCJtZXRob2REZXRhaWxzIjp7Im5ldHdvcmsiOiJtYWlubmV0LW + JldGEiLCJkZWNpbWFscyI6NiwiZXNjcm93UHJvZ3JhbSI6Ik1Q + UHNlc3Npb24xMTExMTExMTExMTExMTExMTExMTExMTExMTExMT + ExIiwicmVmZXJlbmNlIjoiZjQ3YWMxMGItNThjYy00MzcyLWE1 + NjctMGUwMmIyYzNkNDc5Iiwic3VnZ2VzdGVkRGVwb3NpdCI6Ij + EwMDAwMDAiLCJ0aW1lb3V0IjoiMzYwMCJ9LCJyZWNpcGllbnQi + OiI3eEtYdGcyQ1c4N2Q5N1RYSlNEcGJENWpCa2hlVHFBODNUWl + J1Sm9zZ0FzVSJ9", + expires="2026-03-21T12:05:00Z" +Cache-Control: no-store +~~~ + +Decoded `request`: + +~~~json +{ + "amount": "1000", + "currency": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "description": "LLM inference API", + "methodDetails": { + "network": "mainnet-beta", + "decimals": 6, + "escrowProgram": "MPPsession1111111111111111111111111111111", + "reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "suggestedDeposit": "1000000", + "timeout": "3600" + }, + "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU" +} +~~~ + +**2. Credential (channel open proof):** + +~~~http +GET /inference HTTP/1.1 +Host: api.example.com +Authorization: Payment +~~~ + +Decoded credential: + +~~~json +{ + "challenge": { + "id": "kM9xPqWvT2nJrHsY4aDfEb", + "realm": "api.example.com", + "method": "solana", + "intent": "session", + "request": "", + "expires": "2026-03-21T12:05:00Z" + }, + "payload": { + "type": "channel_open", + "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", + "signature": "5UfDuX7hXbPjGUpTmt9PHRLsNGJe4dEny...", + "deposit": "1000000" + } +} +~~~ + +**3. Response (with receipt):** + +~~~http +HTTP/1.1 200 OK +Payment-Receipt: +Content-Type: application/json + +{"model": "llama-3", "output": "Hello! How can I help?"} +~~~ + +Decoded receipt: + +~~~json +{ + "method": "solana", + "challengeId": "kM9xPqWvT2nJrHsY4aDfEb", + "reference": "5UfDuX7hXbPjGUpTmt9PHRLsNGJe4dEny...", + "status": "success", + "timestamp": "2026-03-21T12:04:58Z" +} +~~~ + +## Session Voucher (Subsequent Request) + +After the channel is open, the client signs a voucher +for each subsequent request. No on-chain transaction +occurs. + +Decoded credential: + +~~~json +{ + "challenge": { + "id": "nR8yQsXwU3oKtIsZ5bEgFc", + "realm": "api.example.com", + "method": "solana", + "intent": "session", + "request": "", + "expires": "2026-03-21T12:10:00Z" + }, + "payload": { + "type": "voucher", + "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", + "cumulativeAmount": "2000", + "nonce": "2", + "expiry": "2026-03-21T13:00:00Z", + "signature": "SGVsbG8gV29ybGQhIFRoaXMgaXMgYW4g..." + } +} +~~~ + +This is the second request in the session. The cumulative +amount is 2000 base units (0.002 USDC), representing a +delta of 1000 (0.001 USDC) from the previous voucher. + +Decoded receipt: + +~~~json +{ + "method": "solana", + "challengeId": "nR8yQsXwU3oKtIsZ5bEgFc", + "reference": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", + "status": "success", + "timestamp": "2026-03-21T12:05:02Z" +} +~~~ + +# Acknowledgements + +The author thanks the Tempo team for the session method +design that this specification adapts for Solana, and the +Solana Foundation for the charge intent specification that +this document builds upon. From c5dbbc8524b5214a5b072eb3eb258e5a1f05a39b Mon Sep 17 00:00:00 2001 From: Alexander Date: Sat, 21 Mar 2026 13:31:34 -0400 Subject: [PATCH 2/2] Reworked the draft to align with the current Solana Foundation session SDK surface (open/update/topup/close, canonical voucher JSON, verifier hooks) while preserving stricter security language around replay protection, atomic state updates, and Ed25519 onchain verification. --- .../methods/solana/draft-solana-session-00.md | 1438 +++++++++-------- 1 file changed, 731 insertions(+), 707 deletions(-) diff --git a/specs/methods/solana/draft-solana-session-00.md b/specs/methods/solana/draft-solana-session-00.md index a948b580..7535c787 100644 --- a/specs/methods/solana/draft-solana-session-00.md +++ b/specs/methods/solana/draft-solana-session-00.md @@ -72,16 +72,21 @@ informative: This document defines the "session" intent for the "solana" payment method within the Payment HTTP Authentication Scheme -{{I-D.httpauth-payment}}. The client deposits SPL tokens into an -on-chain escrow program, creating a unidirectional payment channel; -subsequent requests are authorized by off-chain Ed25519-signed -vouchers with cumulative amounts that the server verifies locally. -Settlement occurs when the channel is partially settled or closed. +{{I-D.httpauth-payment}}. + +A Solana session authorizes repeated paid access through a +channel-like lifecycle carried in MPP credentials. A client opens a +session with an initial escrowed amount and an initial signed +voucher, then submits updated signed vouchers over time with +monotonically increasing cumulative amounts. A session MAY also be +topped up or closed through additional credential actions. -Two credential types are supported: `type="channel_open"`, where -the client presents proof of the on-chain channel deposit, and -`type="voucher"`, where the client presents an off-chain signed -voucher authorizing cumulative payment. +This document defines four credential actions: +`action="open"`, `action="update"`, `action="topup"`, and +`action="close"`. Voucher signatures are generated over a +domain-separated canonical representation of the voucher object. +Servers verify voucher signatures, challenge binding, monotonic +session state, and any configured transaction proof requirements. --- middle @@ -93,97 +98,94 @@ payments. This document registers the "session" intent for the "solana" payment method. The Solana charge intent {{I-D.solana-charge}} handles one-time -payments where each request requires an on-chain transaction. -Sessions aggregate many payments into a single on-chain -settlement, making them suitable for high-frequency use cases -where per-request on-chain transactions would be -cost-prohibitive {{SOLANA-DOCS}}. - -## Channel Open Phase {#channel-open-phase} +payments where each request requires an onchain transaction. +Sessions amortize onchain settlement across multiple requests, +making them suitable for high-frequency or low-value use cases +where per-request onchain transactions would be operationally +or economically inefficient {{SOLANA-DOCS}}. -The client deposits SPL tokens {{SPL-TOKEN}} into an on-chain -escrow program, creating a unidirectional payment channel: +A Solana session consists of: -~~~ - Client Server Solana Network - | | | - | (1) GET /resource | | - |-----------------------> | | - | | | - | (2) 402 Payment Required| | - | (recipient, amount, | | - | escrowProgram) | | - |<----------------------- | | - | | | - | (3) Build open_channel | | - | tx, deposit SPL | | - | tokens, sign | | - | | | - | (4) Send transaction | | - |-----------------------------------------------> | - | (5) Confirmation | | - |<----------------------------------------------- | - | | | - | (6) Authorization: | | - | Payment | | - | (channel_open proof)| | - |-----------------------> | | - | | (7) getTransaction | - | |----------------------> | - | | (8) Verified deposit | - | |<---------------------- | - | | | - | (9) 200 OK + Receipt | | - |<----------------------- | | - | | | -~~~ +1. an initial session open step, optionally backed by an onchain + transaction proof; +2. one or more offchain voucher updates that monotonically increase + authorized value; +3. optional topup actions that increase available escrow; and +4. an optional close action that finalizes or proves session + settlement. -The client broadcasts the channel-open transaction itself and -presents the confirmed transaction signature. The server -verifies the deposit on-chain and initializes session state. +This document standardizes the HTTP-layer request, credential, and +verification semantics for Solana sessions. It does not require a +single canonical Solana escrow program ABI. Instead, it defines the +session action model, signed voucher model, verifier expectations, +and receipt semantics. A compatible settlement profile is described +informatively in {{informative-settlement-profile}}. -## Active Session Phase {#active-session-phase} +## Session Flow Overview {#session-flow-overview} -Once the channel is open, subsequent requests use off-chain -Ed25519-signed vouchers with no on-chain interaction: +A typical session proceeds as follows: ~~~ - Client Server - | | - | (1) GET /resource | - |-----------------------> | - | | - | (2) 402 Payment Required| - | (same session) | - |<----------------------- | - | | - | (3) Sign voucher with | - | incremented amount | - | | - | (4) Authorization: | - | Payment | - | (voucher + sig) | - |-----------------------> | - | | - | (5) Verify Ed25519 sig | - | (CPU-only, ~usec) | - | | - | (6) 200 OK + Receipt | - |<----------------------- | - | | + Client Server Solana + | | | + | (1) GET /resource | | + |--------------------------> | | + | | | + | (2) 402 Payment Required | | + | (session request) | | + |<-------------------------- | | + | | | + | (3) Open session | | + | create open payload | | + | + signed voucher | | + | + optional openTx | | + | | | + | (4) Authorization: | | + | Payment | | + |--------------------------> | | + | | | + | (5) Verify open | | + | initialize state | | + | verify voucher | | + | verify openTx if reqd | | + | | | + | (6) 200 OK + Receipt | | + |<-------------------------- | | + | | | + | (7) Subsequent request | | + |--------------------------> | | + | | | + | (8) 402 Payment Required | | + |<-------------------------- | | + | | | + | (9) Update payload | | + | + newer signed | | + | voucher | | + |--------------------------> | | + | | | + | (10) Verify signature | | + | verify monotonicity | | + | atomically update | | + | | | + | (11) 200 OK + Receipt | | + |<-------------------------- | | ~~~ -Verification during this phase is a single Ed25519 signature -check: pure CPU, no RPC calls, microsecond latency. +Optional `topup` and `close` actions extend the same lifecycle. ## Relationship to the Solana Charge Intent This document shares the `method="solana"` payment method with {{I-D.solana-charge}} but uses `intent="session"` instead of -`intent="charge"`. Both intents use the same encoding -conventions (JCS canonicalization, base64url encoding) and -follow the same shared field semantics for `amount`, `currency`, -and `recipient`. +`intent="charge"`. + +The charge intent authorizes a one-time payment for a single +request. The session intent authorizes repeated paid access through +a session lifecycle composed of credential actions and signed +vouchers. Both intents use the same base `Payment` authentication +scheme and the same encoding conventions for structured values +carried in `WWW-Authenticate`, `Authorization`, and +`Payment-Receipt`. # Requirements Language @@ -191,43 +193,56 @@ and `recipient`. # Terminology -Payment Channel -: A unidirectional channel where a payer deposits tokens into - an on-chain escrow and issues off-chain vouchers to a - recipient. Settlement occurs when the channel is partially - settled or closed. +Session Channel +: A long-lived Solana payment context identified by `channelId`. + The channel tracks payer, recipient, asset, authorized amount, + and any settlement-related metadata required by the verifier. -Channel PDA -: A Program Derived Address on Solana that holds the escrowed - SPL tokens and channel state. Derived deterministically from - stable seeds (payer pubkey, recipient pubkey, channel nonce). - The channel PDA address serves as the channel identifier - throughout the session lifecycle. +Signed Session Voucher +: A structured object containing a voucher, signer identity, + signature type, and signature bytes. Signed vouchers authorize + cumulative payment updates within an existing session channel. Voucher -: An off-chain Ed25519-signed message authorizing a cumulative - payment amount. Each voucher supersedes all previous vouchers - for the same channel. The server grants access based on the - delta between consecutive voucher amounts. +: The unsigned voucher object nested inside a signed session + voucher. It contains session-scoped identifiers and payment + state such as cumulative amount, sequence, recipient, + chain identifier, and channel program. Cumulative Amount -: The total authorized payment from channel open to the current - voucher. Each voucher's cumulative amount MUST be greater - than or equal to the previous voucher's cumulative amount. - -Voucher Nonce -: A monotonically increasing counter included in each voucher. - Prevents replay of older vouchers with the same cumulative - amount. - -Escrow Program -: The on-chain Solana program that manages payment channel - state, holds deposited tokens, and enforces settlement rules. +: The total amount authorized from channel open through the current + voucher. Each accepted voucher's cumulative amount MUST be greater + than or equal to the previously accepted cumulative amount for the + same session channel. + +Sequence +: A monotonically increasing integer carried in the voucher. + Prevents replay of older vouchers and provides ordering for + concurrent server-side verification. + +Server Nonce +: An opaque nonce value scoped to a session channel. Once accepted + during session open, it MUST remain constant for subsequent + updates on that session channel. + +Authorization Mode +: A string that describes which signer model is used to authorize + vouchers for a session channel. Examples include direct payer + authorization and delegated session-key authorization. + +Asset Descriptor +: The `asset` object in the session request. It identifies the + settlement asset and its amount normalization parameters. + +Pricing Descriptor +: The `pricing` object in the session request. It describes how + session usage maps to debits, including the meter name and + amount per unit. Base Units -: The smallest transferable unit of an SPL token, determined - by the token's decimal precision. For example, USDC uses - 6 decimals, so 1 USDC = 1,000,000 base units. +: The smallest transferable unit of the settlement asset. For SPL + assets, this is determined by mint decimals. For native SOL, this + is lamports. # Intent Identifier @@ -236,188 +251,175 @@ It MUST be lowercase. # Intent: "session" -The "session" intent represents a long-lived payment -authorization gating access to a resource over multiple -requests. The client opens a payment channel on-chain once, -then signs off-chain vouchers with monotonically increasing -cumulative amounts for each request. The server verifies each -voucher locally and grants access for the delta between the -new and previous cumulative amount. Settlement occurs -on-chain when the channel is partially settled or closed. +The "session" intent represents a long-lived payment authorization +gating access to a resource over multiple requests. A client opens a +session once, then submits signed vouchers with monotonically +increasing cumulative amounts for subsequent requests. The server +verifies each voucher locally and grants access for the delta +between the newly accepted cumulative amount and the previously +accepted cumulative amount. A session MAY also be topped up or +closed. # Encoding Conventions {#encoding} -All JSON {{RFC8259}} objects carried in auth-params or HTTP -headers in this specification MUST be serialized using the JSON -Canonicalization Scheme (JCS) {{RFC8785}} before encoding. JCS -produces a deterministic byte sequence, which is required for -any digest or signature operations defined by the base spec -{{I-D.httpauth-payment}}. +All JSON {{RFC8259}} objects carried in auth-params or HTTP headers +in this specification MUST be serialized using the JSON +Canonicalization Scheme (JCS) {{RFC8785}} before encoding. The resulting bytes MUST then be encoded using base64url {{RFC4648}} Section 5 without padding characters (`=`). -Implementations MUST NOT append `=` padding when encoding, -and MUST accept input with or without padding when decoding. +Implementations MUST NOT append `=` padding when encoding, and MUST +accept input with or without padding when decoding. -This encoding convention applies to: the `request` auth-param -in `WWW-Authenticate`, the credential token in `Authorization`, -and the receipt token in `Payment-Receipt`. +This encoding convention applies to the `request` auth-param in +`WWW-Authenticate`, the credential token in `Authorization`, and the +receipt token in `Payment-Receipt`. # Request Schema -## Shared Fields - -The `request` auth-param of the `WWW-Authenticate: Payment` -header contains a JCS-serialized, base64url-encoded JSON -object (see {{encoding}}). The following shared fields are -included in that object: - -amount -: REQUIRED. The cost per request in base units, encoded as a - decimal string. For SPL tokens, base units are the token's - smallest unit (e.g., for USDC with 6 decimals, "1000" - represents 0.001 USDC per request). The value MUST be a - positive integer that fits in a 64-bit unsigned integer - (max 18,446,744,073,709,551,615). - -currency -: REQUIRED. MUST be the base58-encoded {{BASE58}} mint address - of the SPL token (e.g., - `"EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v"` for - USDC). The mint address uniquely identifies the token and - is used by the client to construct the deposit and voucher. - MUST NOT exceed 128 characters. Native SOL sessions are - not supported in this version; use the charge intent - {{I-D.solana-charge}} for native SOL payments. - -description -: OPTIONAL. A human-readable memo describing the resource or - service being paid for. MUST NOT exceed 256 characters. +The `request` auth-param of the `WWW-Authenticate: Payment` header +contains a JCS-serialized, base64url-encoded JSON object +(see {{encoding}}). -recipient -: REQUIRED. The base58-encoded public key of the account - receiving payments. This is the owner of the destination - associated token account, not the ATA address itself. +The Solana session request object contains the following fields: -## Method Details +asset +: REQUIRED. Describes the asset used for session settlement. -The following fields are nested under `methodDetails` in -the request JSON: + The `asset` object contains: + + * `kind`: REQUIRED. MUST be either `"sol"` or `"spl"`. + * `decimals`: REQUIRED. Non-negative integer used for base-unit + normalization. + * `mint`: REQUIRED when `kind="spl"`. Base58-encoded SPL mint + address. MUST NOT be present when `kind="sol"`. + * `symbol`: OPTIONAL. Display-only symbol hint. + +channelProgram +: REQUIRED. Base58-encoded address of the channel program or + settlement program expected by the verifier. network -: OPTIONAL. Identifies which Solana cluster the session - operates on. MUST be one of "mainnet-beta", "devnet", - or "localnet". Defaults to "mainnet-beta" if omitted. - Clients MUST reject challenges whose network does not - match their configured cluster. - -decimals -: REQUIRED. The number of decimal places for the token - (0-9). Used by the client for voucher amount construction - and deposit instruction parameters. - -escrowProgram -: REQUIRED. The base58-encoded program ID of the on-chain - escrow program that manages payment channels. The client - uses this to construct the channel-open transaction. - -reference -: REQUIRED. A server-generated unique identifier for this - payment challenge, encoded as a string. MUST NOT exceed - 128 characters. The server uses this value to correlate - incoming credentials with issued challenges and to enforce - single-use semantics. MUST be unique per challenge. - -suggestedDeposit -: OPTIONAL. The server's recommended initial deposit amount - in base units, encoded as a decimal string. Clients SHOULD - use `min(suggestedDeposit, maxDeposit)` where `maxDeposit` - is the client's configured spending limit. If omitted, - clients MAY choose their own deposit amount. - -timeout -: OPTIONAL. Channel timeout duration in seconds, encoded as - a decimal string. After `opened_at + timeout`, the payer - may reclaim unspent tokens via the `reclaim` instruction. - Defaults to "3600" (1 hour) if omitted. - -tokenProgram -: OPTIONAL. The base58-encoded program ID of the token - program governing the token. MUST be either the Token - Program (`TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA`) - or the Token-2022 Program - (`TokenzQdBNbLqP5VEhdkAS6EPFLC1PHnBqCXEpPxuEb`) - {{SPL-TOKEN-2022}}. If omitted, clients MUST determine - the correct token program by fetching the mint account - from the network and inspecting its owner program. - Servers SHOULD include this field as a hint to avoid - the extra RPC lookup. - -### Session Challenge Example +: OPTIONAL. Solana network identifier. Examples include + `"mainnet-beta"`, `"devnet"`, and `"localnet"`. If omitted, + implementations SHOULD treat `"mainnet-beta"` as the default. + +pricing +: OPTIONAL. Describes how usage maps to debits within the session. + + The `pricing` object contains: + + * `amountPerUnit`: REQUIRED. Decimal string in base units. + * `meter`: REQUIRED. Meter identifier for usage accounting. + * `unit`: REQUIRED. Logical billed unit name. + * `minDebit`: OPTIONAL. Minimum debit per request, in base units. + +recipient +: REQUIRED. Base58-encoded recipient public key for session + settlement. + +sessionDefaults +: OPTIONAL. Server hints for default session behavior. + + The `sessionDefaults` object contains: + + * `suggestedDeposit`: OPTIONAL. Suggested initial escrow amount in + base units. + * `ttlSeconds`: OPTIONAL. Suggested session time-to-live. + * `closeBehavior`: OPTIONAL. Close policy hint. + * `settleInterval`: OPTIONAL. Settlement cadence hint. + +verifier +: OPTIONAL. Server verifier policy hints. + + The `verifier` object contains: + + * `acceptAuthorizationModes`: OPTIONAL. List of accepted + authorization-mode strings. + * `maxClockSkewSeconds`: OPTIONAL. Allowed timestamp skew when + evaluating expiry. + +## Session Request Example ~~~json { - "amount": "1000", - "currency": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", - "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", - "description": "LLM inference API", - "methodDetails": { - "network": "mainnet-beta", + "asset": { + "kind": "spl", "decimals": 6, - "escrowProgram": "MPPsession1111111111111111111111111111111", - "reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479", + "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "symbol": "USDC" + }, + "channelProgram": "MPPsession1111111111111111111111111111111", + "network": "mainnet-beta", + "pricing": { + "amountPerUnit": "1000", + "meter": "inference_request", + "unit": "request", + "minDebit": "1000" + }, + "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + "sessionDefaults": { "suggestedDeposit": "1000000", - "timeout": "3600", - "tokenProgram": "TokenkegQfeZyiNwAJbNbGKPFXCWuBvf9Ss623VQ5DA" + "ttlSeconds": 3600, + "closeBehavior": "server_may_finalize" + }, + "verifier": { + "acceptAuthorizationModes": ["regular_budget", "regular_unbounded"], + "maxClockSkewSeconds": 30 } } ~~~ -This requests a session charging 0.001 USDC (1000 base units) -per request, with a suggested initial deposit of 1 USDC -(1,000,000 base units) and a 1-hour channel timeout. +This requests a session priced at 1000 base units per request +(0.001 USDC) with a suggested initial deposit of 1 USDC. # Credential Schema -The `Authorization` header carries a single base64url-encoded -JSON token (no auth-params). The decoded object contains the -following top-level fields: +The `Authorization` header carries a single base64url-encoded JSON +token and no auth-params. The decoded object contains the following +top-level fields: challenge -: REQUIRED. An echo of the challenge auth-params from the - `WWW-Authenticate` header: `id`, `realm`, `method`, - `intent`, `request`, and (if present) `expires`. This - binds the credential to the exact challenge that was - issued. +: REQUIRED. Echo of the challenge auth-params from + `WWW-Authenticate`: `id`, `realm`, `method`, `intent`, `request`, + and, if present, `expires`. This binds the credential to the exact + challenge that was issued. source -: OPTIONAL. A payer identifier string, as defined by - {{I-D.httpauth-payment}}. Solana implementations MAY - use the payer's base58-encoded public key or a DID. +: OPTIONAL. Payer identifier string as defined by + {{I-D.httpauth-payment}}. Solana implementations MAY use the + payer's base58-encoded public key or a DID. payload -: REQUIRED. A JSON object containing the Solana-specific - credential fields. The `type` field determines which - additional fields are present. Two payload types are - defined: `"channel_open"` and `"voucher"`. +: REQUIRED. A Solana-specific session payload. The `action` field + determines which additional fields are present. -## Channel Open Payload {#channel-open-payload} +The following actions are defined: -When opening a channel (`type="channel_open"`), the client -sends proof of the on-chain deposit transaction. The client -broadcasts the channel-open transaction itself and presents -the confirmed transaction signature. +- `action="open"` +- `action="update"` +- `action="topup"` +- `action="close"` + +## Open Payload {#open-payload} + +The `open` action initializes a session channel and provides the +initial signed voucher. | Field | Type | Required | Description | |-------|------|----------|-------------| -| `type` | string | REQUIRED | `"channel_open"` | -| `channelId` | string | REQUIRED | Base58-encoded channel PDA address | -| `signature` | string | REQUIRED | Base58-encoded transaction signature of the channel-open transaction | -| `deposit` | string | REQUIRED | Deposit amount in base units | - -The `channelId` is the base58-encoded address of the channel -PDA, which serves as the unique identifier for this payment -channel throughout the session lifecycle. +| `action` | string | REQUIRED | `"open"` | +| `authorizationMode` | string | REQUIRED | Authorization mode for the session | +| `channelId` | string | REQUIRED | Session channel identifier | +| `depositAmount` | string | REQUIRED | Initial escrow amount in base units | +| `openTx` | string | REQUIRED | Onchain transaction reference proving session open | +| `payer` | string | REQUIRED | Base58-encoded payer public key | +| `expiresAt` | string | OPTIONAL | Session expiry hint as {{RFC3339}} timestamp | +| `capabilities` | object | OPTIONAL | Advertised authorizer capabilities | +| `voucher` | object | REQUIRED | Signed session voucher | + +The `capabilities` object MAY include implementation-specific hints +such as `maxCumulativeAmount` or `allowedActions`. Example (decoded): @@ -432,405 +434,365 @@ Example (decoded): "expires": "2026-03-21T12:05:00Z" }, "payload": { - "type": "channel_open", + "action": "open", + "authorizationMode": "regular_budget", "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", - "signature": "5UfDuX7hXbPjGUpTmt9PHRLsNGJe4dEny...", - "deposit": "1000000" + "depositAmount": "1000000", + "openTx": "5UfDuX7hXbPjGUpTmt9PHRLsNGJe4dEny...", + "payer": "9f2wLQ7A8sR6q7r7h6A6H9C8oP4e7nY6d2Y3vH7F5f1Q", + "voucher": { + "signature": "3QF7k8...", + "signatureType": "ed25519", + "signer": "9f2wLQ7A8sR6q7r7h6A6H9C8oP4e7nY6d2Y3vH7F5f1Q", + "voucher": { + "chainId": "solana:mainnet-beta", + "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", + "channelProgram": "MPPsession1111111111111111111111111111111", + "cumulativeAmount": "1000", + "meter": "inference_request", + "payer": "9f2wLQ7A8sR6q7r7h6A6H9C8oP4e7nY6d2Y3vH7F5f1Q", + "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + "sequence": 0, + "serverNonce": "0d6c8c9e-1111-4444-8888-16bb8a72f9c1", + "units": "1" + } + } } } ~~~ -## Voucher Payload {#voucher-payload} +## Update Payload {#update-payload} -For each request during an active session -(`type="voucher"`), the client presents an off-chain -Ed25519-signed voucher with a monotonically increasing -cumulative amount. +The `update` action submits a newer signed voucher for an existing +session channel. | Field | Type | Required | Description | |-------|------|----------|-------------| -| `type` | string | REQUIRED | `"voucher"` | -| `channelId` | string | REQUIRED | Base58-encoded channel PDA address | -| `cumulativeAmount` | string | REQUIRED | Cumulative payment in base units | -| `nonce` | string | REQUIRED | Monotonically increasing voucher nonce | -| `expiry` | string | REQUIRED | Voucher expiry as {{RFC3339}} timestamp | -| `signature` | string | REQUIRED | Base64-encoded Ed25519 signature over the 206-byte voucher message (see {{voucher-format}}) | +| `action` | string | REQUIRED | `"update"` | +| `channelId` | string | REQUIRED | Existing session channel identifier | +| `voucher` | object | REQUIRED | Signed session voucher | Example (decoded): ~~~json { "challenge": { - "id": "kM9xPqWvT2nJrHsY4aDfEb", + "id": "nR8yQsXwU3oKtIsZ5bEgFc", "realm": "api.example.com", "method": "solana", "intent": "session", "request": "eyJ...", - "expires": "2026-03-21T12:05:00Z" + "expires": "2026-03-21T12:10:00Z" }, "payload": { - "type": "voucher", + "action": "update", "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", - "cumulativeAmount": "5000", - "nonce": "5", - "expiry": "2026-03-21T13:00:00Z", - "signature": "SGVsbG8gV29ybGQhIFRoaXMgaXMgYW4g..." + "voucher": { + "signature": "4NdK2u...", + "signatureType": "ed25519", + "signer": "9f2wLQ7A8sR6q7r7h6A6H9C8oP4e7nY6d2Y3vH7F5f1Q", + "voucher": { + "chainId": "solana:mainnet-beta", + "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", + "channelProgram": "MPPsession1111111111111111111111111111111", + "cumulativeAmount": "2000", + "expiresAt": "2026-03-21T13:00:00Z", + "meter": "inference_request", + "payer": "9f2wLQ7A8sR6q7r7h6A6H9C8oP4e7nY6d2Y3vH7F5f1Q", + "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + "sequence": 1, + "serverNonce": "0d6c8c9e-1111-4444-8888-16bb8a72f9c1", + "units": "1" + } + } } } ~~~ -# Voucher Format {#voucher-format} - -Vouchers are structured binary messages signed with the -payer's Ed25519 keypair. The format is domain-separated and -binds to all relevant context to prevent cross-channel, -cross-program, and cross-cluster replay. - -~~~ -Voucher message layout (206 bytes): - bytes[0..21]: "mpp-solana-session-v1" (ASCII, 21 bytes) - bytes[21..53]: channel PDA (32 bytes) - bytes[53..85]: escrow program ID (32 bytes) - bytes[85..117]: payer pubkey (32 bytes) - bytes[117..149]: recipient pubkey (32 bytes) - bytes[149..181]: mint pubkey (32 bytes) - bytes[181..189]: cumulative amount (u64 little-endian) - bytes[189..197]: voucher nonce (u64 little-endian) - bytes[197..205]: expiry timestamp (i64 little-endian, Unix seconds) - bytes[205]: cluster discriminator (0=mainnet, 1=devnet, 2=localnet) -~~~ - -The 21-byte ASCII domain tag `"mpp-solana-session-v1"` prevents -confusion with any other Ed25519-signed message format. The -signature is Ed25519 over the 206-byte message, signed by the -payer's keypair. The resulting 64-byte signature is base64-encoded -in the credential payload. - -# Verification Procedure {#verification} - -Upon receiving a request with a credential, the server MUST: - -1. Decode the base64url credential and parse the JSON. - -2. Verify that `payload.type` is present and is either - `"channel_open"` or `"voucher"`. - -3. Look up the stored challenge using - `credential.challenge.id`. If no matching challenge - is found, reject the request. - -4. Verify that all fields in `credential.challenge` - exactly match the stored challenge auth-params. - -5. Proceed with type-specific verification: - - For `type="channel_open"`: see {{channel-open-verification}}. - - For `type="voucher"`: see {{voucher-verification}}. - -## Channel Open Verification {#channel-open-verification} - -For credentials with `type="channel_open"`: - -1. Verify that `payload.signature` is present and is a - valid base58-encoded string. - -2. Fetch the transaction from the Solana network using - the RPC `getTransaction` method with `jsonParsed` - encoding and at least `confirmed` commitment level. - -3. Verify the transaction was successful (no error in - the transaction metadata). - -4. Verify the transaction contains an `open_channel` - instruction to the `escrowProgram` from the challenge. - -5. Verify the channel PDA at `payload.channelId` was - created with the correct parameters: recipient matches - the challenge `recipient`, mint matches `currency`, and - the deposit amount matches `payload.deposit`. - -6. Initialize server-side session state for this channel: - store the channel PDA address, payer pubkey, deposit - amount, and set the cumulative amount and voucher nonce - to zero. - -7. Return the resource with a Payment-Receipt header. - -## Voucher Verification {#voucher-verification} - -For credentials with `type="voucher"`: - -1. Look up the server's stored session state for the - channel at `payload.channelId`. If no active session - exists for this channel, reject the credential. - -2. Reconstruct the 206-byte voucher message from the - credential fields and the stored session parameters - (channel PDA, escrow program ID, payer pubkey, - recipient, mint, cluster). - -3. Verify the Ed25519 signature in `payload.signature` - against the reconstructed 206-byte message using the - stored payer pubkey. - -4. Verify the `cumulativeAmount` is greater than or equal - to the server's previously-recorded cumulative amount - for this channel. - -5. Verify the `nonce` is strictly greater than the server's - previously-recorded nonce for this channel. - -6. Verify the `expiry` timestamp has not passed. - -7. Atomically update the server's session state: set the - cumulative amount to `payload.cumulativeAmount` and the - nonce to `payload.nonce`. - -8. Grant access for the delta: - `cumulativeAmount - previousCumulativeAmount`. - -9. Return the resource with a Payment-Receipt header. - -The cumulative-amount update in step 7 MUST be atomic to -prevent race conditions where concurrent requests count -the same voucher delta twice. See {{server-state}}. - -# Settlement Procedure - -## Channel Lifecycle - -The channel progresses through the following states: - -### Partial Settlement {#partial-settlement} +## Topup Payload {#topup-payload} -The server MAY submit the latest voucher on-chain via the -escrow program's `settle` instruction at any time during -an active session. This transfers the delta -(`voucher.cumulativeAmount - channel.cumulativePaid`) to -the recipient's associated token account and updates the -channel's on-chain `cumulativePaid` and `voucherNonce` -fields. The channel remains open for continued use. +The `topup` action increases the tracked escrow amount for an existing +session channel. -Servers SHOULD settle periodically to limit counterparty -risk (the amount at risk if the channel is abandoned). - -### Channel Close {#channel-close} - -The recipient closes the channel by submitting the latest -voucher via the escrow program's `close` instruction: - -1. Verify the voucher signature on-chain (see - {{on-chain-verification}}). -2. Transfer the final delta - (`voucher.cumulativeAmount - channel.cumulativePaid`) - to the recipient's associated token account. -3. Transfer the remainder - (`channel.deposit - voucher.cumulativeAmount`) to the - payer's associated token account. -4. Close the channel PDA and its token account. - -### Timeout Reclaim {#timeout-reclaim} - -If the current time exceeds `channel.expiryAt` (computed -as `openedAt + timeout`), the payer may call the escrow -program's `reclaim` instruction to recover -`channel.deposit - channel.cumulativePaid`. This works -whether `cumulativePaid` is zero or greater than zero. - -### Timeout Rules {#timeout-rules} - -The following rules govern which instructions are valid -relative to timestamps: - -- `settle` requires: `now <= voucher.expiry` AND - `now <= channel.expiryAt` -- `close` requires: `now <= voucher.expiry` AND - `now <= channel.expiryAt` -- `reclaim` requires: `now > channel.expiryAt` +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `action` | string | REQUIRED | `"topup"` | +| `channelId` | string | REQUIRED | Existing session channel identifier | +| `additionalAmount` | string | REQUIRED | Additional escrow amount in base units | +| `topupTx` | string | REQUIRED | Onchain transaction reference proving topup | -After `channel.expiryAt`, the recipient can no longer -settle or close. Only the payer can act, via `reclaim`. +## Close Payload {#close-payload} -## On-Chain Voucher Verification {#on-chain-verification} +The `close` action closes an existing session channel and MAY include +an onchain settlement transaction reference. -On-chain voucher verification (for `settle` and `close`) -uses Solana's Ed25519 precompile program -(`Ed25519SigVerify111111111111111111111111111`) -{{ED25519-PROGRAM}}. +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `action` | string | REQUIRED | `"close"` | +| `channelId` | string | REQUIRED | Existing session channel identifier | +| `closeTx` | string | OPTIONAL | Onchain settlement transaction reference | +| `voucher` | object | REQUIRED | Final signed session voucher | -The Ed25519 precompile is NOT callable via CPI. The -correct pattern: +# Signed Voucher Format {#voucher-format} -1. The transaction includes an Ed25519 verify instruction - that checks the payer's signature over the 206-byte - voucher message. +A signed session voucher consists of the following fields: -2. The escrow program reads the instructions sysvar - (`Sysvar1nstructions1111111111111111111111111`) and - validates that the Ed25519 instruction exists, verified - the correct public key, and verified the correct - message bytes. +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `signature` | string | REQUIRED | Signature over the serialized voucher bytes | +| `signatureType` | string | REQUIRED | Signature scheme discriminator | +| `signer` | string | REQUIRED | Public identifier of the signer | +| `voucher` | object | REQUIRED | Unsigned voucher object | -3. If the Ed25519 instruction is missing, references a - different key, or references different message data, - the program MUST reject the transaction. +The unsigned voucher object contains: -Incorrect Ed25519 validation enables unauthorized -withdrawal of escrowed funds. See {{ed25519-security}}. +| Field | Type | Required | Description | +|-------|------|----------|-------------| +| `chainId` | string | REQUIRED | Chain identifier, for example `solana:mainnet-beta` | +| `channelId` | string | REQUIRED | Session channel identifier | +| `channelProgram` | string | REQUIRED | Channel or settlement program identifier | +| `cumulativeAmount` | string | REQUIRED | Monotonic cumulative authorized amount | +| `expiresAt` | string | OPTIONAL | Voucher expiration timestamp | +| `meter` | string | REQUIRED | Meter identifier | +| `payer` | string | REQUIRED | Payer public key | +| `recipient` | string | REQUIRED | Recipient public key | +| `sequence` | integer | REQUIRED | Monotonic sequence number | +| `serverNonce` | string | REQUIRED | Session-scoped nonce | +| `units` | string | REQUIRED | Meter units associated with the update | -## Channel PDA Derivation {#pda-derivation} +## Voucher Serialization -The channel PDA is derived from stable seeds only: +Voucher signatures are computed over: -~~~ -seeds = [ - "mpp-channel", - payer_pubkey, - recipient_pubkey, - channel_nonce (u64 little-endian) -] -~~~ +1. the ASCII domain separator string + `"solana-mpp-session-voucher-v1:"` +2. followed by a canonical JSON serialization of the voucher object, + with object keys sorted lexicographically and undefined fields + omitted. -The `channel_nonce` is a sequential counter set once at -channel creation, NOT the voucher nonce. This produces a -stable PDA address that does not change as vouchers are -issued. The channel PDA address serves as the `channelId` -in credentials and receipts. +The resulting byte sequence is signed using the indicated signature +scheme. -## Client Transaction Construction +## Signature Types -### Channel Open +Implementations of this version MUST support `signatureType="ed25519"`. -The client MUST construct a transaction containing an -`open_channel` instruction to the escrow program that: +Implementations MAY support additional signature types. One currently +used value is `"swig-session"`, which represents an alternative +session-authorizer model. Verifiers that do not recognize a signature +type MUST reject it unless they are explicitly configured with a +custom verifier for that signature type. -1. Creates the channel PDA with the correct seeds - (see {{pda-derivation}}). -2. Transfers SPL tokens from the client's associated - token account to the channel's token account via - the appropriate token program {{SPL-TOKEN}}. -3. Initializes the channel state: payer, recipient, - mint, deposit amount, timeout, and timestamps. - -The client MUST be the fee payer and MUST fully sign -the transaction. The client MUST wait for at least -`confirmed` commitment before presenting the credential. - -### Voucher Signing +# Verification Procedure {#verification} -For each request during an active session, the client: +Upon receiving a request with a session credential, the server MUST: -1. Increments the voucher nonce. -2. Computes the new cumulative amount - (`previousCumulativeAmount + amount`). -3. Constructs the 206-byte voucher message - (see {{voucher-format}}). -4. Signs the message with Ed25519 using the payer's - keypair. -5. Presents the signature in a `type="voucher"` - credential. +1. Decode the base64url credential and parse the JSON. +2. Verify that `payload.action` is present and is one of + `"open"`, `"update"`, `"topup"`, or `"close"`. +3. Verify or resolve an outstanding challenge using + `credential.challenge.id` and the echoed challenge fields. +4. Verify that all fields in `credential.challenge` exactly match the + challenge being verified. +5. Proceed with action-specific verification. + +## Open Verification {#open-verification} + +For credentials with `action="open"`, the server MUST: + +1. Verify `payload.openTx` is present. +2. Verify `payload.depositAmount` is a valid non-negative integer + string. +3. Parse the signed session voucher. +4. Verify `voucher.channelId` equals `payload.channelId`. +5. Verify `voucher.payer` equals `payload.payer`. +6. Verify `voucher.recipient` equals the configured recipient and the + challenged recipient. +7. Verify `voucher.channelProgram` equals the challenged + `channelProgram`. +8. Verify `voucher.chainId` matches the challenged network. +9. Verify `voucher.cumulativeAmount` does not exceed + `payload.depositAmount`. +10. Verify `payload.authorizationMode`, if constrained by the + verifier, is accepted. +11. Verify `voucher.expiresAt`, if present, has not passed. +12. Verify the voucher signature. +13. Verify any configured transaction proof requirement for + `openTx`. +14. Initialize server-side session state for the channel. +15. Reject if the channel already exists. + +## Update Verification {#update-verification} + +For credentials with `action="update"`, the server MUST: + +1. Look up stored session state for `payload.channelId`. +2. Reject if no active session exists for that channel. +3. Verify the channel is open and not expired. +4. Parse the signed session voucher. +5. Verify `voucher.channelId`, `payer`, `recipient`, + `channelProgram`, and `serverNonce` match stored session state + and the challenged request. +6. Verify `voucher.chainId` matches the challenged network. +7. Verify `voucher.sequence` is strictly greater than the previously + accepted sequence. +8. Verify `voucher.cumulativeAmount` is greater than or equal to the + previously accepted cumulative amount. +9. Verify `voucher.cumulativeAmount` does not exceed the tracked + escrow amount. +10. Verify `voucher.expiresAt`, if present, has not passed. +11. Verify the voucher signature. +12. Atomically update stored session state with the new cumulative + amount and sequence. +13. Grant access only for the delta between the new cumulative amount + and the previously accepted cumulative amount. + +## Topup Verification {#topup-verification} + +For credentials with `action="topup"`, the server MUST: + +1. Look up stored session state for `payload.channelId`. +2. Reject if no active session exists for that channel. +3. Verify the channel is open and not expired. +4. Verify `payload.additionalAmount` is a valid non-negative integer + string. +5. Verify `payload.topupTx` is present. +6. Verify any configured transaction proof requirement for + `topupTx`. +7. Atomically increase the tracked escrow amount. + +## Close Verification {#close-verification} + +For credentials with `action="close"`, the server MUST: + +1. Look up stored session state for `payload.channelId`. +2. Reject if no active session exists for that channel. +3. Reject if the channel is already closed. +4. Parse the signed session voucher. +5. Apply the same binding, monotonicity, and expiry checks as + `update`. +6. Verify `voucher.cumulativeAmount` does not exceed the tracked + escrow amount. +7. Verify any configured transaction proof requirement for + `closeTx`, when required. +8. Atomically mark the channel closed and record the final accepted + cumulative amount and sequence. + +# Session State Requirements {#server-state} + +Servers MUST track per-channel session state. The following fields +are required: + +- `channelId` +- payer public key +- recipient public key +- asset descriptor +- tracked escrow amount +- highest accepted cumulative amount +- highest accepted sequence +- session-scoped `serverNonce` +- status +- any verifier-required expiry metadata + +The cumulative-amount and sequence updates MUST be atomic to prevent +race conditions where concurrent requests count the same voucher +delta twice. In-process locks are NOT safe across multiple server +instances. Horizontally-scaled deployments MUST use an external +atomic store such as Redis with WATCH/MULTI, PostgreSQL with +row-level locks, or equivalent. + +# Transaction Proof Requirements {#transaction-proofs} + +A Solana session MAY rely on onchain transaction proofs for some +actions. This document defines the HTTP-layer semantics for the +following optional transaction references: + +- `openTx` +- `topupTx` +- `closeTx` + +If a verifier requires one of these proofs, it MUST verify that the +referenced transaction: + +1. exists at the required commitment level; +2. succeeded; +3. corresponds to the expected action for the channel; +4. targets the expected program or settlement path; and +5. reflects the expected amount semantics for that action. + +This specification does not require a single canonical Solana escrow +program ABI for session settlement. Program-specific settlement logic +is implementation-defined unless separately standardized. ## Confirmation Requirements -For `type="channel_open"` credentials, clients MUST wait -for at least the `confirmed` commitment level before -presenting the credential. Servers MUST fetch the -transaction with at least `confirmed` commitment. +When `openTx`, `topupTx`, or `closeTx` are used as required proofs, +clients MUST wait for at least the `confirmed` commitment level +before presenting the credential, and servers MUST verify the +transaction at at least `confirmed` commitment. ## Finality -Solana provides two commitment levels relevant to -payment verification: +Solana provides two commitment levels commonly used in payment +verification: -- `confirmed`: optimistic confirmation from a - supermajority of validators (~400ms). Sufficient - for most payment use cases. -- `finalized`: deterministic finality after ~31 slots - (~12 seconds). Required for high-value transactions - where rollback risk is unacceptable. +- `confirmed`: optimistic confirmation from a supermajority of + validators. Sufficient for most session lifecycle proofs. +- `finalized`: stronger rollback resistance with higher latency. -The `confirmed` level is RECOMMENDED as the default for -channel-open verification to minimize latency. Servers -MAY require `finalized` commitment for channels with -large deposits. +`confirmed` is RECOMMENDED as the default for session action +verification. Servers MAY require `finalized` for higher-value +channels or more conservative settlement policies. -## Receipt Generation +# Receipt Generation -Upon successful verification, the server MUST include -a `Payment-Receipt` header in the 200 response. +Upon successful verification, the server MUST include a +`Payment-Receipt` header in the 200 response. -The receipt payload for Solana session: +The receipt payload for Solana session contains: | Field | Type | Description | |-------|------|-------------| | `method` | string | `"solana"` | | `challengeId` | string | The challenge `id` from `WWW-Authenticate` | -| `reference` | string | For `channel_open`: the transaction signature (base58). For `voucher`: the channel PDA address (base58). | +| `reference` | string | Session channel identifier, or close transaction reference when a close action uses one | | `status` | string | `"success"` | | `timestamp` | string | {{RFC3339}} verification time | -For `type="channel_open"`, the `reference` is the on-chain -transaction signature. For `type="voucher"`, the `reference` -is the channel PDA address, since no on-chain transaction -occurs during voucher verification. - -Example receipt for a voucher credential (decoded): +Example receipt (decoded): ~~~json { "method": "solana", - "challengeId": "kM9xPqWvT2nJrHsY4aDfEb", + "challengeId": "nR8yQsXwU3oKtIsZ5bEgFc", "reference": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", "status": "success", - "timestamp": "2026-03-21T12:04:58Z" + "timestamp": "2026-03-21T12:05:02Z" } ~~~ -# Server State Requirements {#server-state} - -Servers MUST track per-channel session state. The following -fields are required: +# Error Responses -- Channel PDA address (`channelId`) -- Payer pubkey -- Recipient pubkey -- Mint pubkey -- Deposit amount -- Highest cumulative amount received -- Highest voucher nonce received +When rejecting a session credential, the server MUST return HTTP 402 +(Payment Required) with a fresh `WWW-Authenticate: Payment` +challenge per {{I-D.httpauth-payment}}. -The cumulative-amount and nonce update MUST be atomic to -prevent race conditions where concurrent requests count -the same voucher delta twice. In-process locks (e.g., -mutexes) are NOT safe across multiple server instances. -Horizontally-scaled deployments MUST use an external -atomic store (e.g., Redis with WATCH/MULTI, PostgreSQL -with row-level locks, or equivalent). +The server SHOULD include a response body conforming to +RFC 9457 {{RFC9457}} Problem Details, with +`Content-Type: application/problem+json`. -# Error Responses - -When rejecting a credential, the server MUST return HTTP -402 (Payment Required) with a fresh -`WWW-Authenticate: Payment` challenge per -{{I-D.httpauth-payment}}. The server SHOULD include a -response body conforming to RFC 9457 {{RFC9457}} Problem -Details, with `Content-Type: application/problem+json`. Servers MUST use the standard problem types defined in {{I-D.httpauth-payment}}: `malformed-credential`, -`invalid-challenge`, and `verification-failed`. The -`detail` field SHOULD contain a human-readable -description of the specific failure. - -All error responses MUST include a fresh challenge in -`WWW-Authenticate`. +`invalid-challenge`, and `verification-failed`. Example error response body: ~~~json { "type": "https://paymentauth.org/problems/verification-failed", - "title": "Invalid Voucher", + "title": "Invalid Session Voucher", "status": 402, - "detail": "Voucher nonce 3 is not greater than previously accepted nonce 5" + "detail": "Voucher sequence 3 is not greater than previously accepted sequence 5" } ~~~ @@ -838,117 +800,171 @@ Example error response body: ## Transport Security -All communication MUST use TLS 1.2 or higher. Session -credentials MUST only be transmitted over HTTPS -connections. +All communication MUST use TLS 1.2 or higher. Session credentials +MUST only be transmitted over HTTPS connections. + +## Replay and Reordering Protection + +Session replay protection depends on all of the following: + +- strict channel binding through `channelId`; +- monotonic `sequence`; +- monotonic `cumulativeAmount`; +- a constant per-session `serverNonce`; and +- atomic server-side state updates. + +Servers MUST reject any voucher whose sequence is less than or equal +to the highest previously accepted sequence for the channel. -## Voucher Replay Protection +Servers MUST reject any voucher whose cumulative amount is less than +the highest previously accepted cumulative amount for the channel. -Each voucher carries a monotonically increasing nonce and -cumulative amount. The server MUST reject vouchers with a -nonce less than or equal to the highest previously-accepted -nonce for the channel. The domain-separated voucher format -(206 bytes with ASCII prefix, program ID, and cluster -discriminator) prevents cross-channel, cross-program, and -cross-cluster replay. A voucher accepted for one channel -cannot be replayed against a different channel, program, -or cluster because the signed message includes all of -these identifiers. +## Signature Verification -## Ed25519 On-Chain Verification {#ed25519-security} +Voucher verification MUST bind all of the following fields: -The Solana Ed25519 precompile is NOT callable via CPI. -On-chain voucher verification (settle, close) MUST use -the instructions sysvar pattern described in -{{on-chain-verification}}. If this is implemented -incorrectly, an attacker can call settle or close with -arbitrary voucher data, bypassing signature verification -entirely. This is the single highest-risk component of -the escrow program. +- `chainId` +- `channelId` +- `channelProgram` +- `payer` +- `recipient` +- `cumulativeAmount` +- `sequence` +- `serverNonce` +- `meter` +- `units` +- `expiresAt`, if present -Implementations MUST include adversarial tests that -verify the following cases are rejected: +A verifier MUST reject a voucher if any of these fields do not match +the expected session scope. -- Missing Ed25519 verify instruction -- Ed25519 instruction verifying a different public key -- Ed25519 instruction verifying different message data +## Authorization Modes -## Escrow Program Security +Different authorization modes can imply different acceptable signers. +Implementations MUST verify that the voucher signer is authorized for +the session's recorded authorization mode. -The escrow program MUST verify: +Implementations that support delegated session keys MUST ensure that a +delegated signer cannot authorize vouchers outside the scope granted +for that channel. -- Only the payer can deposit and reclaim -- Only the recipient can settle and close -- Voucher signatures match the channel's payer pubkey -- Cumulative amounts only increase -- Voucher nonces strictly increase -- Timeout rules are enforced per {{timeout-rules}} +## Atomic Session State + +Session state transitions MUST be atomic. Without an atomic update, +concurrent verification can cause the same voucher delta to be +counted more than once, resulting in under-charging. ## Counterparty Risk -During an active session, the server carries risk equal to -the cumulative authorized amount minus the last on-chain -settlement. If the payer's signing key is compromised or -the payer disappears, the server holds the latest voucher -as its claim on escrowed funds. Servers SHOULD settle -periodically to reduce exposure. The timeout mechanism -ensures the payer can recover funds if the recipient -disappears or refuses to close the channel. +During an active session, the server carries risk equal to the +highest accepted cumulative amount minus any realized settlement. +Servers SHOULD limit this exposure through settlement policy, +deposit sizing, and verifier constraints. ## Client-Side Verification -Clients MUST verify the challenge before depositing: - -1. `recipient` is the expected party -2. `amount` per request is reasonable for the service -3. `currency` matches the expected token -4. `escrowProgram` is the expected program -5. `suggestedDeposit` is within acceptable limits +Before opening a session, clients MUST verify at least: -Malicious servers could request excessive deposits, -direct payments to unexpected recipients, or specify -rogue escrow programs. +1. `recipient` is the expected counterparty; +2. `asset` is the expected settlement asset; +3. `channelProgram` is acceptable; +4. `pricing`, if present, is acceptable for the resource; +5. `sessionDefaults.suggestedDeposit`, if present, is within + acceptable limits. ## RPC Trust -The server relies on its Solana RPC endpoint to provide -accurate transaction data for channel-open verification. -A compromised RPC could return fabricated transaction -data, causing the server to accept deposits that were -never made. Servers SHOULD use trusted RPC providers -or run their own nodes. +When transaction proofs are required, the server relies on its +Solana RPC endpoint to provide accurate transaction data. +A compromised RPC could cause the server to accept action proofs that +did not actually occur. Servers SHOULD use trusted RPC providers or +run their own nodes. + +# Informative Settlement Profile {#informative-settlement-profile} + +This section is informative. It describes one compatible settlement +profile for Solana sessions. It is not the only possible settlement +profile for `intent="session"`. + +A compatible profile uses a unidirectional escrow program with the +following high-level lifecycle: + +- channel open +- partial settle +- channel close +- timeout reclaim + +In such a profile, the server or recipient MAY periodically settle the +latest accepted voucher onchain to reduce counterparty exposure. + +## Informative Timeout Rules + +One compatible timeout model is: + +- settle allowed while both the voucher and the channel are unexpired; +- close allowed while both the voucher and the channel are unexpired; +- reclaim allowed only after channel expiry. + +This avoids ambiguous overlap between recipient close and payer +reclaim. + +## Informative PDA Stability + +One compatible program design derives the session account from stable +seeds only, for example payer, recipient, and a channel nonce set at +open time. Voucher sequence MUST NOT be part of PDA derivation because +it changes over time. + +## Informative Ed25519 Onchain Verification {#ed25519-security} + +When a settlement profile verifies voucher signatures onchain, Solana's +Ed25519 precompile program +(`Ed25519SigVerify111111111111111111111111111`) +{{ED25519-PROGRAM}} is NOT callable via CPI. + +A compatible verification pattern is: + +1. include an Ed25519 verify instruction in the transaction; +2. read the instructions sysvar from the settlement program; and +3. verify that the Ed25519 instruction checked the correct public key + over the correct message bytes. + +If this is implemented incorrectly, an attacker can bypass signature +verification and withdraw or settle funds using arbitrary voucher +data. Implementations that use this pattern MUST include adversarial +tests covering: + +- missing Ed25519 verify instruction; +- wrong public key; and +- wrong message bytes. # IANA Considerations ## Payment Method Registration -This document uses the `solana` method identifier -registered by {{I-D.solana-charge}}. +This document uses the `solana` method identifier registered by +{{I-D.solana-charge}}. ## Payment Intent Registration -This document requests registration of the following -entry in the "HTTP Payment Intents" registry established -by {{I-D.httpauth-payment}}: +This document requests registration of the following entry in the +"HTTP Payment Intents" registry established by +{{I-D.httpauth-payment}}: | Intent | Applicable Methods | Description | Reference | |--------|-------------------|-------------|-----------| -| `session` | `solana` | Streaming SPL token payments via payment channels | This document | +| `session` | `solana` | Repeated paid access on Solana through signed session vouchers and session lifecycle actions | This document | --- back # Examples -The following examples illustrate the complete HTTP exchange -for each credential type. Base64url values are shown with -their decoded JSON below. - -## Session Open (Channel Deposit) +## Session Open -A session charging 0.001 USDC per request. The client -deposits 1 USDC. +A session priced at 0.001 USDC per request, with a suggested initial +deposit of 1 USDC. -**1. Challenge (402 response):** +**Challenge (402 response):** ~~~http HTTP/1.1 402 Payment Required @@ -956,17 +972,7 @@ WWW-Authenticate: Payment id="kM9xPqWvT2nJrHsY4aDfEb", realm="api.example.com", method="solana", intent="session", - request="eyJhbW91bnQiOiIxMDAwIiwiY3VycmVuY3kiOiJFUGpG - V2RkNUF1ZnFTU3FlTTJxTjF4enliYXBDOEc0d0VHR2tad3lURH - QxdiIsImRlc2NyaXB0aW9uIjoiTExNIGluZmVyZW5jZSBBUEki - LCJtZXRob2REZXRhaWxzIjp7Im5ldHdvcmsiOiJtYWlubmV0LW - JldGEiLCJkZWNpbWFscyI6NiwiZXNjcm93UHJvZ3JhbSI6Ik1Q - UHNlc3Npb24xMTExMTExMTExMTExMTExMTExMTExMTExMTExMT - ExIiwicmVmZXJlbmNlIjoiZjQ3YWMxMGItNThjYy00MzcyLWE1 - NjctMGUwMmIyYzNkNDc5Iiwic3VnZ2VzdGVkRGVwb3NpdCI6Ij - EwMDAwMDAiLCJ0aW1lb3V0IjoiMzYwMCJ9LCJyZWNpcGllbnQi - OiI3eEtYdGcyQ1c4N2Q5N1RYSlNEcGJENWpCa2hlVHFBODNUWl - J1Sm9zZ0FzVSJ9", + request="", expires="2026-03-21T12:05:00Z" Cache-Control: no-store ~~~ @@ -975,29 +981,29 @@ Decoded `request`: ~~~json { - "amount": "1000", - "currency": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", - "description": "LLM inference API", - "methodDetails": { - "network": "mainnet-beta", + "asset": { + "kind": "spl", "decimals": 6, - "escrowProgram": "MPPsession1111111111111111111111111111111", - "reference": "f47ac10b-58cc-4372-a567-0e02b2c3d479", - "suggestedDeposit": "1000000", - "timeout": "3600" + "mint": "EPjFWdd5AufqSSqeM2qN1xzybapC8G4wEGGkZwyTDt1v", + "symbol": "USDC" }, - "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU" + "channelProgram": "MPPsession1111111111111111111111111111111", + "network": "mainnet-beta", + "pricing": { + "amountPerUnit": "1000", + "meter": "inference_request", + "unit": "request", + "minDebit": "1000" + }, + "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + "sessionDefaults": { + "suggestedDeposit": "1000000", + "ttlSeconds": 3600, + "closeBehavior": "server_may_finalize" + } } ~~~ -**2. Credential (channel open proof):** - -~~~http -GET /inference HTTP/1.1 -Host: api.example.com -Authorization: Payment -~~~ - Decoded credential: ~~~json @@ -1011,41 +1017,46 @@ Decoded credential: "expires": "2026-03-21T12:05:00Z" }, "payload": { - "type": "channel_open", + "action": "open", + "authorizationMode": "regular_budget", "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", - "signature": "5UfDuX7hXbPjGUpTmt9PHRLsNGJe4dEny...", - "deposit": "1000000" + "depositAmount": "1000000", + "openTx": "5UfDuX7hXbPjGUpTmt9PHRLsNGJe4dEny...", + "payer": "9f2wLQ7A8sR6q7r7h6A6H9C8oP4e7nY6d2Y3vH7F5f1Q", + "voucher": { + "signature": "3QF7k8...", + "signatureType": "ed25519", + "signer": "9f2wLQ7A8sR6q7r7h6A6H9C8oP4e7nY6d2Y3vH7F5f1Q", + "voucher": { + "chainId": "solana:mainnet-beta", + "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", + "channelProgram": "MPPsession1111111111111111111111111111111", + "cumulativeAmount": "1000", + "meter": "inference_request", + "payer": "9f2wLQ7A8sR6q7r7h6A6H9C8oP4e7nY6d2Y3vH7F5f1Q", + "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + "sequence": 0, + "serverNonce": "0d6c8c9e-1111-4444-8888-16bb8a72f9c1", + "units": "1" + } + } } } ~~~ -**3. Response (with receipt):** - -~~~http -HTTP/1.1 200 OK -Payment-Receipt: -Content-Type: application/json - -{"model": "llama-3", "output": "Hello! How can I help?"} -~~~ - Decoded receipt: ~~~json { "method": "solana", "challengeId": "kM9xPqWvT2nJrHsY4aDfEb", - "reference": "5UfDuX7hXbPjGUpTmt9PHRLsNGJe4dEny...", + "reference": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", "status": "success", "timestamp": "2026-03-21T12:04:58Z" } ~~~ -## Session Voucher (Subsequent Request) - -After the channel is open, the client signs a voucher -for each subsequent request. No on-chain transaction -occurs. +## Session Update Decoded credential: @@ -1060,19 +1071,32 @@ Decoded credential: "expires": "2026-03-21T12:10:00Z" }, "payload": { - "type": "voucher", + "action": "update", "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", - "cumulativeAmount": "2000", - "nonce": "2", - "expiry": "2026-03-21T13:00:00Z", - "signature": "SGVsbG8gV29ybGQhIFRoaXMgaXMgYW4g..." + "voucher": { + "signature": "4NdK2u...", + "signatureType": "ed25519", + "signer": "9f2wLQ7A8sR6q7r7h6A6H9C8oP4e7nY6d2Y3vH7F5f1Q", + "voucher": { + "chainId": "solana:mainnet-beta", + "channelId": "6Yd4vFHRk2pLJ9NwQxGjZ8Bt...", + "channelProgram": "MPPsession1111111111111111111111111111111", + "cumulativeAmount": "2000", + "expiresAt": "2026-03-21T13:00:00Z", + "meter": "inference_request", + "payer": "9f2wLQ7A8sR6q7r7h6A6H9C8oP4e7nY6d2Y3vH7F5f1Q", + "recipient": "7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU", + "sequence": 1, + "serverNonce": "0d6c8c9e-1111-4444-8888-16bb8a72f9c1", + "units": "1" + } + } } } ~~~ -This is the second request in the session. The cumulative -amount is 2000 base units (0.002 USDC), representing a -delta of 1000 (0.001 USDC) from the previous voucher. +This update increases the cumulative amount from 1000 to 2000 base +units, so the server grants access for a delta of 1000 base units. Decoded receipt: @@ -1088,7 +1112,7 @@ Decoded receipt: # Acknowledgements -The author thanks the Tempo team for the session method -design that this specification adapts for Solana, and the -Solana Foundation for the charge intent specification that -this document builds upon. +The author thanks the Tempo team for the earlier session method work +that informed this area, and the Solana Foundation for the Solana +charge specification and session SDK work that this document builds +on. \ No newline at end of file