From 2835c9840ac1d1c1dc964479b70b9a22f16f4f2e Mon Sep 17 00:00:00 2001 From: Kammerlo Date: Mon, 3 Aug 2026 09:36:58 +0200 Subject: [PATCH] docs: updated spec --- specs/schemes/exact/scheme_exact_cardano.md | 462 +++++++++++++++----- 1 file changed, 356 insertions(+), 106 deletions(-) diff --git a/specs/schemes/exact/scheme_exact_cardano.md b/specs/schemes/exact/scheme_exact_cardano.md index 27e21533e2..6bcd800793 100644 --- a/specs/schemes/exact/scheme_exact_cardano.md +++ b/specs/schemes/exact/scheme_exact_cardano.md @@ -98,19 +98,20 @@ The protocol flow for `exact` on Cardano is client-driven. 6. The Cardano blockchain includes the transaction in the mempool or a block and returns the transaction hash and confirmation status. 7. **Resource Server** receives the transaction hash and status: - - If submitted via the **Facilitator**, it receives a settlement response containing the `txHash` and `status`. + - If submitted via the **Facilitator**, it receives a settlement response containing the `transaction` hash and `extra.status`. - Cardano uses Ouroboros Praos, which has probabilistic finality. A transaction that appears in the mempool or even in a recent block can be rolled back. Granting access upon mempool inclusion (`status: "mempool"`) is therefore **strongly discouraged** and SHOULD NOT be used for any resource with real economic value. Servers that choose to accept mempool status MUST document this risk and accept full liability for rolled-back transactions. -8. **Resource Server** grants the **Client** access to the requested resource, returning an HTTP 200 OK response with an `PAYMENT-RESPONSE` header containing: - - `txHash`: The Cardano transaction hash +8. **Resource Server** grants the **Client** access to the requested resource, returning an HTTP 200 OK response with a `PAYMENT-RESPONSE` header containing: + - `success`: Whether settlement succeeded + - `transaction`: The Cardano transaction hash - `network`: The Cardano network (e.g., `cardano:mainnet`) - - `status`: The transaction status (e.g., `confirmed` or `mempool`) + - `extra.status`: The transaction status (e.g., `confirmed` or `mempool`) -### `PaymentRequirementsResponse` +### `PaymentRequired` #### Default Schema -When the Resource Server responds with a `402 Payment Required`, the body of the response contains the payment requirements in the following schema: +When the Resource Server responds with a `402 Payment Required`, it returns the payment requirements as a base64-encoded `PaymentRequired` object in the **`PAYMENT-REQUIRED` response header**. Decoded, the object has the following schema: ```js { @@ -125,8 +126,8 @@ When the Resource Server responds with a `402 Payment Required`, the body of the { "scheme": "exact", "network": "cardano:mainnet", // cardano:preprod or cardano:preview for public testnets - "amount": "10000", // 1 USDM = 1000000000 - "asset": "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad.0014df105553444d", // ${policyId}.${assetName} The policy id in this example is the USDM policy id on Cardano Mainnet - use e675b46e4d2242c991a8932a99db3044e80515ae14b4c4ccf6b3f4c9 for USDM on Preprod. The asset name is the hex representation of '(333) USDM' + "amount": "10000", // atomic units of `asset`; USDM has 6 decimals, so 1 USDM = 1000000 + "asset": "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad.0014df105553444d", // ${policyId}.${assetNameHex} — USDM on Cardano Mainnet. BOTH parts differ on Preprod: policy id e675b46e4d2242c991a8932a99db3044e80515ae14b4c4ccf6b3f4c9, asset name 0014df10745553444d. The asset name is the CIP-68 (333) label followed by the hex of 'USDM' (mainnet) / 'tUSDM' (preprod) "payTo": "addr1...", "maxTimeoutSeconds": 600, // Has to be set to a higher amount of time because of the Cardano Network speed "extra": { @@ -139,35 +140,36 @@ When the Resource Server responds with a `402 Payment Required`, the body of the #### Masumi assetTransferMethod Schema -When the Resource Server requires payment via **Masumi**, the buyer locks the payment into the Masumi escrow contract (`vested_pay`) rather than paying the seller directly. `payTo` is the **Masumi escrow script address** for the network, and `extra` carries the fields needed to construct the escrow **datum**. +When the Resource Server requires payment via **Masumi**, the buyer locks the payment into the Masumi V2 escrow contract (`vested_pay`) rather than paying the seller directly. `payTo` is the **Masumi escrow script address** for the deployment, and `extra` carries the seller-signed terms and the request commitment needed to construct the escrow **datum**. -**Scope:** x402 covers only the **lock** step — the escrow's initial `FundsLocked` state. Result submission, seller withdrawal, buyer refund, and dispute resolution all happen later, in **separate transactions** governed by the contract and the Masumi Payment Service, and are out of scope for this scheme. A successful `/settle` for Masumi therefore means **"funds are locked in escrow", not "payment delivered"**: the resource server grants access on the lock and trusts the Masumi lifecycle to release funds to the agent. +**Scope:** x402 covers only the **lock** step — the escrow's initial `FundsLocked` state. Result submission, seller withdrawal, buyer refund, and dispute resolution all happen later, in **separate transactions** governed by the contract, and are out of scope for this scheme. A successful `/settle` for Masumi therefore means **"funds are locked in escrow", not "payment delivered"**: the resource server grants access on the lock and trusts the Masumi lifecycle to release the funds. -**End-to-end flow.** Steps 6–7 are the x402 payment; everything else is Masumi's own protocol (MIP-003 + the Payment Service API) and happens outside this scheme. The ordering below is fixed by Masumi's API contracts, not by x402: +This method supports **Masumi V2 only**. A single Masumi payment locks **one** asset — lovelace, or one native token plus its structural lovelace. `PaymentRequirements` carries a single `asset`/`amount`, so a multi-asset basket is out of scope for this scheme. -1. **Seller registers** an agent in the Masumi Registry → `agentIdentifier`, the agent NFT's policy id + asset name (57–250 characters). -2. **Buyer generates `identifierFromPurchaser`.** MIP-003 types it only as a purchaser-defined string, but the Payment Service validates it as a **14–26 character hex** nonce (`min(14).max(26)` in both `createPaymentsSchemaInput` and `createPurchaseInitSchemaInput`), so that range is what interoperates. -3. **Buyer calls the agent's MIP-003 `POST /start_job`** with `identifier_from_purchaser` (required) and `input_data`. *The buyer's nonce reaches the seller here* — this is why a payment request cannot exist before the buyer has spoken. -4. **Seller creates the payment request** with its Payment Service (`POST /payment`), passing `inputHash`, `agentIdentifier`, `identifierFromPurchaser` and the time bounds. `payByTime`/`submitResultTime` MUST be supplied explicitly — the service's schema "default" resolves to a fixed 1970 epoch offset, which the handler then rejects as being in the past; only `unlockTime`/`externalDisputeUnlockTime` have working fallbacks (derived from `submitResultTime`). The service's selling wallet then **signs** (CIP-30 `signData`) the SHA-256 hash of the canonical-JSON payload of all binding fields — that signature and its key become the datum's `reference_signature` and `reference_key`. -5. **`blockchainIdentifier` packs the datum's binding fields.** Per `generateBlockchainIdentifier`, it is `hex(LZString([sellerNonce, buyerNonce, referenceSignature, referenceKey, smartContractAddress?].join(".")))`, where `seller_nonce` is a 64-character service hash with `agentIdentifier` concatenated after it and the optional 5th segment is the V2 escrow address. Decoding it therefore yields `reference_key`, `reference_signature`, `seller_nonce`, `buyer_nonce`, `agent_identifier` and the contract address — every seller-side datum field in one token, made tamper-evident by the signature from step 4. MIP-003 **requires** the `/start_job` response to return it together with all four time bounds, `agentIdentifier`, `sellerVKey` and `input_hash`. -6. **Buyer locks the funds.** In native Masumi the buyer computes `inputHash` per MIP-004 — `SHA256(identifierFromPurchaser + ";" + canonicalJSON(input_data))`, binding its own nonce into the hash — and calls `POST /purchase`. Under this scheme the lock is the x402 payment instead: the client builds the escrow output with the full datum and retries with the `PAYMENT-SIGNATURE` header, and the facilitator's `/verify` checks the lock against the declared fields. -7. **`/settle`** submits the lock transaction. x402's involvement ends here. The seller's Payment Service discovers the lock by **chain observation** — its tx-sync matches the on-chain datum to the payment request — and records the purchase's on-chain state as `FundsLocked`. Detection is chain-driven, which is why a lock produced by an x402 client rather than `POST /purchase` is recognized all the same. -8. **Seller submits the result** (`POST /payment/submit-result` with a 64-character SHA-256 `submitResultHash`): the on-chain `SubmitResult` path requires the continuation datum to carry a **non-empty** `result_hash` and a fresh seller cooldown (`current_time + cooldown_period`). After `unlock_time` the seller's node collects the payment automatically. Before it, the buyer may call `POST /purchase/request-refund` (accepted while the on-chain state is `FundsLocked` or `ResultSubmitted`); past `external_dispute_unlock_time`, a `Disputed` escrow is settled by the threshold of `admin_vks` signatures (`WithdrawDisputed`). +**End-to-end flow.** Within the x402 flow the initial protected-resource request takes the place of the MIP-003 `POST /start_job` handshake; native MIP-003 agents continue to use `/start_job` directly. The party that issues the requirements — the resource server itself, or a service acting for it — is the **requirements issuer**. It needs no prior handshake with the buyer: -Because step 8 is entirely out of band, the datum's deadlines and collateral are the only thing bounding it — which is why a facilitator MUST enforce them at lock time (see the invariants below). +1. The client sends the initial protected-resource request. +2. The issuer captures the request content it commits to and derives `inputHash` from it (see [Request commitment](#request-commitment)). +3. The issuer assembles `extra.terms`, projects the binding `PaymentRequirements` fields into them, and has the **seller's own key** sign the resulting `termsDigest` (see [Seller-signed terms](#seller-signed-terms)). That signature and its key become the datum's `reference_signature` and `reference_key`. +4. The issuer returns the 402. The client verifies every commitment digest and the seller signature independently before it builds anything. +5. The client builds the escrow output with the full 19-field datum, computes the structural lovelace itself, signs, and retries with the `PAYMENT-SIGNATURE` header. +6. The facilitator re-verifies the commitment, the signature, the derived escrow address and the datum; `/settle` then submits the lock. x402's involvement ends here. -**Who supplies which field.** The datum is filled from two sides. When the resource request itself played the `/start_job` role (steps 3–5 above), the server already holds the buyer's nonce and input hash and MAY declare them; but a 402 answering a plain unauthenticated request has no such handshake behind it, and the server then cannot know any buyer-side value. The split below therefore fixes what a server MUST declare and what it MUST NOT *require*: +Because the post-lock lifecycle is entirely out of band, the datum's deadlines and collateral are the only thing bounding it — which is why a facilitator MUST enforce them at lock time (see the invariants below). -- **Server-declared (REQUIRED in `extra`).** `contractAddress`, `sellerAddress`, `sellerNonce`, `agentIdentifier`, `referenceKey`, `referenceSignature`, the four time bounds, and optionally `sellerReturnAddress` / `collateralReturnLovelace`. These come from the seller's **payment request**, created with the Masumi Payment Service for an agent registered in the Masumi Registry. They bind the locked UTxO to that payment request, so the client MUST NOT invent or randomize them — a value that does not match yields an escrow the Masumi service cannot settle. The facilitator matches every declared field against the datum. -- **Buyer-supplied (MUST NOT be required in `extra`).** `identifierFromPurchaser` (datum `buyer_nonce`), `inputHash`, and `buyerReturnAddress`. The buyer creates its **purchase** against the payment request *after* receiving the 402, so these are client inputs: the client fills them from its purchase, or generates a fresh nonce and takes the contract defaults (`input_hash` empty, `buyer_return_address` `None`). A server that already knows the purchase MAY declare `identifierFromPurchaser` or `inputHash`, and the facilitator then matches those; `buyerReturnAddress` is never matched. The buyer is still pinned by the `buyer == payer` rule. +**Producing the 402.** The following are normative for the requirements issuer: + +- It **MUST** generate a fresh `sellerNonce` for every new requirements object, using a cryptographically secure random generator, and **MUST NOT** reuse one for a later initial request. +- It **MUST** store the requirements object it issued and **replay it verbatim** on the paid retry — including `sellerNonce`, the time bounds and the commitment. It **MUST NOT** regenerate them. The resource server rebuilds `PaymentRequirements` when the retry arrives and compares them against the `accepted` block the client echoed from the 402; a re-read clock or a freshly minted nonce fails that comparison and the request is rejected **before** the facilitator is consulted, with the client seeing only a second 402. Regeneration also invalidates the seller signature, which covers those exact values. +- The four time bounds are properties of the **issued requirements**, not of the server process. A `payByTime` fixed at server start expires while the process keeps serving it, and because the lock transaction's TTL is anchored to `pay_by_time` (see the invariants below), every payment after that point is unbuildable. ```js { "x402Version": 2, "error": "PAYMENT-SIGNATURE header is required", "resource": { - "url": "https://api.example.com/premium-data", - "description": "Access to premium market data", + "url": "https://agent.example.com/weather", + "description": "Agent job", "mimeType": "application/json" }, "accepts": [ @@ -176,72 +178,285 @@ Because step 8 is entirely out of band, the datum's deadlines and collateral are "network": "cardano:preprod", // cardano:mainnet or cardano:preview "amount": "5000000", // amount locked into the escrow (of `asset`) "asset": "lovelace", // lovelace, or a single native token (`policyId.assetNameHex`) - "payTo": "addr_test1w...", // the Masumi `vested_pay` escrow script address for this network + "payTo": "addr_test1w...", // the Masumi `vested_pay` escrow script address for this deployment "maxTimeoutSeconds": 600, // Has to be set to a higher amount of time because of the Cardano Network speed "extra": { "assetTransferMethod": "masumi", - "paymentType": "Web3CardanoV2", // Masumi PaymentSourceType — selects the contract generation (v2 = vested_pay) - "contractAddress": "addr_test1w...", // REQUIRED; escrow address for this deployment. MUST equal payTo - "sellerAddress": "addr_test1q...", // datum `seller` (full, key-credential address) - "sellerReturnAddress": "addr_test1q...", // optional; datum `seller_return_address` - "referenceKey": "", // datum `reference_key` - "referenceSignature": "", // datum `reference_signature` (>= 16 bytes, unique per UTxO) - "sellerNonce": "", // datum `seller_nonce` - "agentIdentifier": "", // datum `agent_identifier` - "collateralReturnLovelace": "0", // datum `collateral_return_lovelace` (>= 0) - // buyer_nonce / input_hash / buyer_return_address are NOT declared here: - // the server cannot know them on an unauthenticated request. The client - // fills them when it builds the lock. - "payByTime": "1713626260000", // POSIX milliseconds, from THIS request's payment request - "submitResultTime": "1713636260000", - "unlockTime": "1713636260000", - "externalDisputeUnlockTime": "1713636260000" + "inputCommitment": { + "version": "1", + "algorithm": "sha256", + "parts": [ + { + "name": "body", + "canonicalization": "jcs", + "mediaType": "application/json", + "content": { "days": 3, "units": "metric" }, + "digest": "<32-byte lowercase hex>" + } + ], + "digest": "<32-byte lowercase hex — equals terms.inputHash>" + }, + "terms": { + "version": "1", + "paymentType": "Web3CardanoV2", + "sellerAddress": "addr_test1q...", // datum `seller` (key-credential address) + "sellerReturnAddress": "addr_test1q...", // optional; omit when absent, `null` is invalid + "sellerNonce": "<32-byte lowercase hex>", + "buyerNonce": "", // empty, or 14–26 lowercase hex characters + "agentIdentifier": "", // omit entirely for an unregistered seller + "inputHash": "", + "payByTime": "1713626260000", // POSIX milliseconds + "submitResultTime": "1713636260000", + "unlockTime": "1713640260000", + "externalDisputeUnlockTime": "1713644260000" + }, + "referenceKey": "", + "referenceSignature": "", + "blockchainIdentifier": "", + "deployment": { // optional; omit for the canonical deployment + "requiredAdmins": "2", + "adminVkeys": [""], + "cooldownPeriod": "420000" + } } } ] } ``` -The client constructs the escrow **datum** (a Plutus `Constr 0` with the fields below) and attaches it as an **inline datum** on the output paying `payTo`: +`extra`, `inputCommitment`, every commitment part, `terms` and `deployment` are **closed objects**: an unknown field is invalid. `terms` **MUST NOT** repeat a field that is projected into `signedTerms` from the top level. `collateral_return_lovelace` is deliberately absent — the seller never supplies or signs it (see [Escrow datum](#escrow-datum-and-client-computed-collateral)). + +Wire constraints for the `extra` fields: + +| Field | Constraint | +|---|---| +| `assetTransferMethod` | literal `masumi` | +| `referenceKey` | lowercase even-length hex of one complete CBOR `COSE_Key` | +| `referenceSignature` | lowercase even-length hex of one complete CBOR `COSE_Sign1` | +| `blockchainIdentifier` | lowercase even-length hex of the complete LZString-compressed compatibility identifier | +| `deployment.requiredAdmins` | positive canonical base-10 integer string, no greater than the length of `adminVkeys` | +| `deployment.adminVkeys` | ordered non-empty array of 28-byte lowercase hex verification-key hashes; duplicates are preserved and carry voting weight (see [Deployment and escrow address](#deployment-and-escrow-address)) | +| `deployment.cooldownPeriod` | non-negative canonical base-10 POSIX-millisecond integer string | + +Constraints for `terms`: + +| Field | Constraint | +|---|---| +| `version` | literal string `1` | +| `paymentType` | literal `Web3CardanoV2`. Any other value MUST be rejected — this field selects the contract generation and is not advisory | +| `sellerAddress` | key-credential Cardano address on the selected network | +| `sellerReturnAddress` | optional key-credential address on the selected network; **omitted** when absent, JSON `null` is invalid | +| `sellerNonce` | exactly 32 fresh cryptographically random bytes as 64 lowercase hex characters | +| `buyerNonce` | empty string, or 7–13 bytes as 14–26 even-count lowercase hex characters | +| `agentIdentifier` | **omitted entirely** for an unregistered seller; otherwise a non-empty even-length lowercase hex registry asset identifier. `null` and `""` are invalid — they are additional encodings of "unregistered" that would each produce a different `termsDigest` | +| `inputHash` | exactly equal to `inputCommitment.digest` | +| the four `*Time` fields | positive canonical base-10 POSIX-millisecond strings with no leading zero, satisfying the interval minimums below | + +##### Request commitment + +The commitment is built from the **content of the buyer's protected-resource request as the requirements issuer received it** — the parameters, the parsed body, and where needed the raw request bytes — and nothing else. It is what the escrow's `input_hash` binds the locked funds to, so the payment is tied to exactly the job that was requested and a later dispute can be arbitrated against it. This derivation deliberately replaces MIP-004's `SHA256(identifierFromPurchaser + ";" + canonicalJSON(input_data))`: the buyer's nonce is not an input here, which is what allows a `masumi` 402 to answer a first-contact request with no prior handshake. A Masumi Payment Service therefore cannot reproduce an x402 `input_hash` with the MIP-004 formula. + +The requirements issuer returns the exact content it commits to, and the client verifies every digest before it pays. `inputCommitment.parts` is an ordered array with unique `name` values; conventional names are `parameters`, `body` and `raw`, but applications MAY define others. Every part carries: + +- `name` — a unique non-empty string +- `canonicalization` — `jcs` or `raw` +- `mediaType` — optional, preserved byte-for-byte +- `content` — an RFC 8785-compatible JSON value for `jcs`, or an unpadded base64url string for `raw` +- `digest` — lowercase hex `SHA-256(partBytes)`, exactly 64 characters + +For `jcs`, `partBytes = UTF-8(RFC8785-JCS(content))`. For `raw`, `partBytes = base64url-decode(content)`. A `raw` part MUST identify a stable capture point before a parser changes the bytes; for HTTP that is normally the entity body at that point, never the full HTTP message. + +To build the manifest, omit each part's `content` property and the top-level `digest` property — do **not** set them to `null` or an empty value. Keep all other fields and the part order. Then: + +```text +inputHash = SHA-256( + UTF-8("masumi:x402:input:v1\n") || + UTF-8(JCS(manifest)) +) +``` + +**Content echo.** Because the manifest excludes `content` by construction, omitting `content` on the wire does not change `inputHash`. `content` is therefore **REQUIRED** only for parts the issuer originates — resolved parameters, pricing terms, anything the buyer has not already seen — and **OPTIONAL** for parts derived from the client's own request bytes, which the client recomputes from what it sent. This keeps a large request body out of the `PAYMENT-REQUIRED` header without weakening the commitment. + +The client **MUST** recompute every part digest and `inputHash`, using the issuer's `content` where present and its own request bytes where absent, and MUST reject a mismatch. The application **MUST** present issuer-originated content for approval before payment; the facilitator checks digests but cannot judge their application meaning. The resource server rebuilds the commitment from the signed retry and rejects a mismatch. + +The complete `PAYMENT-REQUIRED` header must fit the server's actual transport limit. Servers **MUST NOT** truncate content or replace it with a URL; they SHOULD return an application error such as HTTP 413 when it does not fit, and SHOULD keep `raw` parts small. Servers **MUST NOT** capture secrets, cookies, authorization headers, x402 headers or the complete HTTP request, and MUST NOT log payment headers or committed content. + +##### Seller-signed terms + +The seller signs one digest covering the price, asset, contract, request hash, identity and deadlines. Client and facilitator reconstruct: + +```text +signedTerms = { + ...terms, + scheme: PaymentRequirements.scheme, + assetTransferMethod: extra.assetTransferMethod, + network: PaymentRequirements.network, + contractAddress: PaymentRequirements.payTo, + amount: PaymentRequirements.amount, + asset: PaymentRequirements.asset, + maxTimeoutSeconds: PaymentRequirements.maxTimeoutSeconds +} + +termsDigest = SHA-256( + UTF-8("masumi:x402:terms:v1\n") || + UTF-8(JCS(signedTerms)) +) +``` + +> The field set above is normative for this scheme version. `termsDigest` is only reproducible when both sides agree on it exactly, so any change to the member list is a breaking change to the scheme, not an implementation detail. + +The seller calls [CIP-30](https://cips.cardano.org/cip/CIP-0030) `signData(sellerAddress, lowercaseHex(termsDigest))`; the returned `DataSignature` follows [CIP-8](https://cips.cardano.org/cip/CIP-0008). `referenceKey` carries the complete CBOR `COSE_Key` as lowercase hex and `referenceSignature` the complete CBOR `COSE_Sign1`. The attached payload is the 32-byte `termsDigest`, `hashed` is `false`, and the external AAD is empty. + +Client and facilitator **MUST** decode and verify both COSE objects, checking: + +- `kty = OKP (1)`, `alg = EdDSA (-8)`, `crv = Ed25519 (6)` +- a 32-byte public key with no private material +- protected `COSE_Sign1` headers carrying `alg = EdDSA (-8)` and the raw `sellerAddress` +- an unprotected `hashed = false` header and empty external AAD +- an attached payload equal to `termsDigest` +- a valid Ed25519 `Sig_structure` +- equal `kid` values when both are present +- `Blake2b-224(publicKey)` equal to the seller's payment-key credential + +A `sellerAddress` with a script payment credential is invalid. The last check is what binds the signature to the **address** rather than to an arbitrary key, and MUST NOT be skipped. + +##### Identity and compatibility identifier + +A non-empty `agentIdentifier` makes a Masumi registry claim; an omitted field means the seller is unregistered. When it is present, client and facilitator **MUST** validate the V2 registry claim independently — asset, seller authorization, metadata, endpoint, network and price — and a registered price MUST resolve to the signed top-level `amount` and `asset`. When it is omitted, the datum's `agent_identifier` is empty bytes and no component may claim registry identity or reputation. + +The compatibility identifier lets Masumi tooling locate the payment: + +```text +agentIdentifierHex = terms.agentIdentifier when present, otherwise "" +sellerIdentifierHex = sellerNonceHex + agentIdentifierHex + +identifierText = + sellerIdentifierHex + "." + + buyerNonceHex + "." + + referenceSignatureHex + "." + + referenceKeyHex + "." + + contractAddressBech32 + +blockchainIdentifier = hex(LZString.compressToUint8Array(identifierText)) +``` + +`identifierText` is ASCII-range UTF-8. Implementations **MUST** join the encoded text values before compression and **MUST NOT** hex-decode the first four segments first. The decompressed value has five period-delimited segments; the first 64 characters of segment one are the `sellerNonce` and any remainder is `agentIdentifier`. Segment two MAY be empty and the empty segment MUST be preserved. + +The issuer returns the complete identifier; the client does not construct it for transport. Client and facilitator reconstruct it and require: + +```text +PaymentRequirements.payTo + == signedTerms.contractAddress + == decode(blockchainIdentifier).smartContractAddress + == the transaction's escrow output address +``` + +##### Escrow datum and client-computed collateral + +The client constructs the escrow **datum** — a Plutus `Constr 0` with the 19 ordered fields below — and attaches it as an **inline datum** on the output paying `payTo`. The scheme-level datum schema version is `masumi.vested_pay.v2`. | # | Datum field | Value at lock | Source | |---|-------------|---------------|--------| -| 0 | `buyer` | the **payer**'s address (key-credential) | client wallet | -| 1 | `buyer_return_address` | `None`, or an address | **client** (or `extra.buyerReturnAddress` if declared) | -| 2 | `seller` | seller address (key-credential) | `extra.sellerAddress` | -| 3 | `seller_return_address` | `None`, or an address | `extra.sellerReturnAddress` | +| 0 | `buyer` | key address whose payment credential controls the `payload.nonce` input | client wallet | +| 1 | `buyer_return_address` | `None`, or a buyer key address | **client** — buyer-chosen, never declared | +| 2 | `seller` | seller key address | `terms.sellerAddress` | +| 3 | `seller_return_address` | `None`, or a seller key address | `terms.sellerReturnAddress` | | 4 | `reference_key` | bytes | `extra.referenceKey` | -| 5 | `reference_signature` | bytes, length ≥ 16, unique per script UTxO | `extra.referenceSignature` | -| 6 | `seller_nonce` | bytes | `extra.sellerNonce` | -| 7 | `buyer_nonce` | bytes | **client** — its purchase, else a fresh random nonce | -| 8 | `agent_identifier` | bytes | `extra.agentIdentifier` | -| 9 | `collateral_return_lovelace` | integer ≥ 0 | `extra.collateralReturnLovelace` | -| 10 | `input_hash` | bytes, may be empty | **client** (or `extra.inputHash` if declared) | +| 5 | `reference_signature` | bytes, length ≥ 16 | `extra.referenceSignature` | +| 6 | `seller_nonce` | bytes | `terms.sellerNonce` | +| 7 | `buyer_nonce` | bytes, possibly empty | `terms.buyerNonce` | +| 8 | `agent_identifier` | bytes, empty when unregistered | `terms.agentIdentifier` | +| 9 | `collateral_return_lovelace` | integer ≥ 0 | **client calculation** | +| 10 | `input_hash` | 32 bytes | `terms.inputHash` | | 11 | `result_hash` | **empty** | — | -| 12 | `pay_by_time` | POSIX ms | `extra.payByTime` | -| 13 | `submit_result_time` | POSIX ms | `extra.submitResultTime` | -| 14 | `unlock_time` | POSIX ms | `extra.unlockTime` | -| 15 | `external_dispute_unlock_time` | POSIX ms | `extra.externalDisputeUnlockTime` | +| 12 | `pay_by_time` | POSIX ms | `terms.payByTime` | +| 13 | `submit_result_time` | POSIX ms | `terms.submitResultTime` | +| 14 | `unlock_time` | POSIX ms | `terms.unlockTime` | +| 15 | `external_dispute_unlock_time` | POSIX ms | `terms.externalDisputeUnlockTime` | | 16 | `seller_cooldown_time` | `0` | — | | 17 | `buyer_cooldown_time` | `0` | — | | 18 | `state` | `FundsLocked` | — | -Because the `vested_pay` validator only runs on spend (never on the lock itself), a malformed datum is **not** rejected at lock time — it silently strands the funds, and the Masumi Payment Service validates the lock **off-chain**. Facilitators MUST therefore enforce — and clients SHOULD validate before signing — to parity with that off-chain validation, for the lock: +Implementations MUST build the following Plutus Data structures. They MUST NOT encode a Cardano address as Bech32 text or as raw address bytes: + +| Type | Plutus Data encoding | +|---|---| +| `Address` | `Constr 0 [paymentCredential, stakeCredentialOption]` | +| payment or stake `VerificationKey` credential | `Constr 0 [Bytes(28-byte key hash)]` | +| payment or stake `Script` credential | `Constr 1 [Bytes(28-byte script hash)]` | +| `Option
.Some(address)` | `Constr 0 [address]` | +| `Option
.None` | `Constr 1 []` | +| stake credential `Some(Inline(credential))` | `Constr 0 [Constr 0 [credential]]` | +| stake credential `Some(Pointer(slot, txIndex, certIndex))` | `Constr 0 [Constr 1 [Int(slot), Int(txIndex), Int(certIndex)]]` | +| stake credential `None` | `Constr 1 []` | +| every byte-string field | `Bytes` containing the decoded bytes, not hexadecimal text | +| every time, cooldown and lovelace field | `Int` | +| `FundsLocked` | `Constr 0 []` | + +`buyer`, `seller` and both return addresses use **verification-key** payment credentials under this scheme, even though the CIP-57 `Address` type can represent script credentials. + +**Encoding test vector.** Enterprise addresses, `None` for both return addresses: buyer payment-key hash `11` × 28, seller `22` × 28, `reference_key` `a10101`, `reference_signature` `55` × 16, `seller_nonce` `33` × 32, empty `buyer_nonce` / `agent_identifier` / `result_hash`, `collateral_return_lovelace` `1435230`, `input_hash` `44` × 32, deadlines `1785756000000` / `1785759600000` / `1785763200000` / `1785766800000`, both cooldowns `0`, state `FundsLocked`. The ledger Plutus Data CBOR is: + +```text +d8799fd8799fd8799f581c11111111111111111111111111111111111111111111111111111111ffd87a80ffd87a80d8799fd8799f581c22222222222222222222222222222222222222222222222222222222ffd87a80ffd87a8043a1010150555555555555555555555555555555555820333333333333333333333333333333333333333333333333333333333333333340401a0015e65e58204444444444444444444444444444444444444444444444444444444444444444401b0000019fc75a1f001b0000019fc7910d801b0000019fc7c7fc001b0000019fc7feea800000d87980ff +``` + +An implementation MUST preserve the same Plutus Data tree when it decodes and re-encodes this CBOR; raw CBOR byte equality is not required. + +**Collateral.** The seller never supplies or signs `collateral_return_lovelace` — the client computes it after it has selected its final addresses and output shape, from the requested asset and live protocol parameters. Let `requestedLovelace` be the top-level `amount` for a lovelace payment, or `0` for a native-token payment. The escrow output MUST satisfy: + +```text +lockedLovelace = requestedLovelace + collateral_return_lovelace +``` + +`collateral_return_lovelace` MUST be `0` or at least **1,435,230**, and MUST be chosen so that `lockedLovelace` also clears the protocol min-UTXO of the datum **after `SubmitResult`** (a 32-byte `result_hash` and non-zero cooldowns) — otherwise the seller can never spend the escrow. For a **native-token** payment `requestedLovelace` is `0`, so a zero collateral cannot satisfy both rules and `collateral_return_lovelace` MUST therefore be at least the larger of 1,435,230 and that post-`SubmitResult` minimum. The token quantity is exact and the output carries no other native token; structural lovelace is the only extra value. Client and facilitator calculate the minimum independently. + +##### Deployment and escrow address -- `buyer` and `seller` are **public-key** (not script) credential addresses, and `buyer` equals the transaction's payer. -- No datum address is the escrow itself: `buyer`, `seller`, and both return addresses MUST differ from the escrow address. `vested_pay` re-parses every output at the script address as a continuation datum (`expect new_datum: Datum`), so a payout aimed back at the escrow aborts every spend path — and Masumi's off-chain decoder (`decodeV2ContractDatum`) rejects such a datum outright. -- `state` is `FundsLocked`, `result_hash` is empty, and **both cooldown timers are `0`**; the escrow output carries **no reference script**. -- `pay_by_time ≤ submit_result_time ≤ unlock_time ≤ external_dispute_unlock_time`, and the transaction's validity upper bound (TTL) is on/before `pay_by_time` so the lock cannot settle after the deadline. -- `reference_signature` is at least 16 bytes. -- `collateral_return_lovelace` is `0` or ≥ **1,435,230**, does **not** exceed the locked lovelace (a collateral above the locked ADA bricks the seller's on-chain spend), and — for a lovelace payment — the locked lovelace covers `amount + collateral_return_lovelace`. -- The escrow output holds enough lovelace for the protocol min-UTXO of the datum **after `SubmitResult`** (a 32-byte `result_hash` and non-zero cooldowns), not merely at lock time — otherwise the seller can never spend. -- `seller_return_address` matches the declared value exactly (declared ⇒ present in the datum; omitted ⇒ `None`). `buyer_return_address` is buyer-chosen and is **not** matched against `extra`. The escrow output carries **exactly** the requested asset set (no extra native tokens). +The escrow address is deployment-specific: the validator parameters (`required_admins_multi_sig`, `admin_vks`, `cooldown_period`) are baked into the script hash, so a different parameterization yields a different address and the hash alone cannot be checked against the un-applied blueprint. This scheme uses the canonical CIP-57 blueprint from [`masumi-payment-service`](https://github.com/masumi-network/masumi-payment-service/blob/d74b2c319228bcbef36632de37875c388dcee7ce/smart-contracts/payment-v2/plutus.json): -The escrow script address is deployment-specific: the validator parameters (`required_admins_multi_sig`, `admin_vks`, `cooldown_period`) are baked into the script hash, so a different parameterization (e.g. a self-hosted Masumi) yields a different address, and the hash alone cannot be checked against the un-applied blueprint. The resource server therefore MUST declare its deployment's escrow address in `extra.contractAddress` (from its payment request) — it is never defaulted, since locking to a wrong escrow silently strands the funds. A facilitator MUST verify that `payTo` equals that declared escrow address before accepting a Masumi payment — this binds the lock to the specific escrow the purchase expects, since a look-alike `vested_pay` with different admins is a different trust domain. +| Property | Canonical value | +|---|---| +| datum schema | `masumi.vested_pay.v2` | +| CIP-57 validator title | `vested_pay.vested_pay.spend` | +| Plutus version | `v3` | +| blueprint digest | `SHA-256(JCS(blueprint))` = `6249de17bb87c5246106af6b0f33de22b44ca24b9c1445fa36d10eb8b583dec7` | +| default `requiredAdmins` | `2` | +| default ordered `adminVkeys` | `fc16a1fcf309aed03ec18bb2176f5ea29acea70bb79145ebaffa8e75`, `7f78161369549d8e2b138fee724c9fa606d6107a66720bdb4c48ada6`, `89eef9ea84e0ee7fe4921fa93eb2873ff6e34473f751d5d52cb75aa6` | +| default `cooldownPeriod` | `420000` ms | + +When `extra.deployment` is absent, the verifier applies these default parameters. When it is present, it replaces **only** those three applied parameters against the same canonical compiled validator, preserving order and duplicate admin hashes. Either way the verifier derives the validator hash and network address itself and **MUST** require the derived address to equal `payTo`. `payTo` is never defaulted or inferred: it is signed into `signedTerms` as `contractAddress` and a mismatch is a rejection, since a look-alike `vested_pay` with different admins is a different trust domain. Preview has no canonical default and therefore requires `extra.deployment`. + +An application **MUST** explicitly allow a non-default parameter set; the seller signature alone is not approval. Custom parameters need no second copy inside `signedTerms` — applying them changes the validator hash, the address contains that hash, and the signed `payTo` binds the deployment. Use the generic `script` method for any other validator or datum layout. + +**What the admin keys can do.** `admin_vks` and `required_admins_multi_sig` define the escrow's dispute arbitrators, and choosing a deployment is choosing them. Their authority is deliberately narrow, but within it, absolute: + +- They can settle a **`Disputed`** escrow, and only that, through the `WithdrawDisputed` redeemer. No other redeemer in `vested_pay` consults `admin_vks`, so a `FundsLocked`, `ResultSubmitted` or `RefundRequested` escrow is beyond their reach — normal completion and normal refund never involve them. An escrow becomes `Disputed` only on genuine conflict: the buyer has requested a refund *and* the seller has submitted a result hash. +- They cannot act before `external_dispute_unlock_time`, and they do not submit the settling transaction themselves. Each admin produces a CIP-8 signature over `blake2b_224(cbor(DisputeWithdrawal { own_ref, buyer_value, seller_value }))`; **anyone** may then build and land the transaction. Because `own_ref` is inside the signed payload, a signature authorizes exactly one UTxO and cannot be replayed against another — including against another deployment of the same validator. +- Settlement requires the number of `admin_vks` entries carrying a valid signature to reach `required_admins_multi_sig`. +- The signed `buyer_value` and `seller_value` are **minimum** payouts, not exact amounts. Any escrow value above their sum is unconstrained on-chain and accrues to whoever submits the transaction — an intentional finder's fee so a third party will cover the settlement cost. There is no on-chain cap on that residual: within a dispute, the admin signature set is trusted completely. Since anyone can send assets to a script address, the residual can also grow after the signatures are produced. + +**Weighted voting.** `admin_vks` MAY repeat a key, and a key appearing *n* times counts *n* times toward the threshold — one physical signature filling several slots. This is intended by the contract, not a deployment bug. It means a raw count misrepresents the real authority: `requiredAdmins: 2` over `adminVkeys: [x, x, y]` is not "2 of 3", it is "`x` alone can settle". A client or wallet that surfaces a deployment for approval **MUST** present the **effective weight per distinct key** and the threshold as a fraction of total weight, never the raw array length. An issuer modelling a weighted authority MUST construct `adminVkeys` with the intended duplication. + +##### Lock invariants + +Because the `vested_pay` validator only runs on spend (never on the lock itself), a malformed datum is **not** rejected at lock time — it silently strands the funds. Facilitators MUST therefore enforce, and clients SHOULD validate before signing: + +- `buyer` and `seller` are **public-key** (not script) credential addresses. +- The payment credential in `buyer` **controls the input named by `payload.nonce`**, and the transaction carries its valid witness. This identifies the buyer without assuming a single owner controls every input. +- No datum address is the escrow itself: `buyer`, `seller` and both return addresses MUST differ from the escrow address. `vested_pay` re-parses every output at the script address as a continuation datum (`expect new_datum: Datum`), so a payout aimed back at the escrow aborts every spend path — and Masumi's own decoder (`decodeV2ContractDatum`) rejects such a datum outright. +- The effective buyer payout target (`buyer_return_address`, else `buyer`) MUST differ from the effective seller payout target (`seller_return_address`, else `seller`); this scheme does not allow aggregated payouts. +- `state` is `FundsLocked`, `result_hash` is empty, **both cooldown timers are `0`**, and the escrow output carries **no reference script**. +- The deadlines are ordered and clear these minimum intervals: `pay_by_time + 5 min ≤ submit_result_time`, `submit_result_time + 15 min ≤ unlock_time`, `unlock_time + 15 min ≤ external_dispute_unlock_time`. +- At issuance `pay_by_time` is in the future and does not exceed the issuance time plus `maxTimeoutSeconds`; `submit_result_time` is at least 15 minutes in the future. The issuer MUST choose `maxTimeoutSeconds` large enough for its settlement expectations. +- The transaction's validity upper bound (TTL) is on or before `pay_by_time`, so the lock cannot settle after the deadline. +- The datum carries the exact COSE key and signature bytes, and `reference_signature` is at least 16 bytes. +- The collateral and value rules above hold, and the escrow output carries **exactly** the requested asset set. +- `seller_return_address` matches the signed terms exactly (declared ⇒ present in the datum with matching credentials; omitted ⇒ `None`). `buyer_return_address` is buyer-chosen and is **not** matched against `extra`. #### Script assetTransferMethod Schema -When the Resource Server requires payment to a script, the `extra` field in the `PaymentRequirementsResponse` contains additional fields required for script interactions. +When the Resource Server requires payment to a script, the `extra` field in the `PaymentRequired` object contains additional fields required for script interactions. ```js { @@ -256,8 +471,8 @@ When the Resource Server requires payment to a script, the `extra` field in the { "scheme": "exact", "network": "cardano:mainnet", // cardano:preprod or cardano:preview for public testnets - "amount": "10000", // 1 USDM = 1000000000 - "asset": "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad.0014df105553444d", // ${policyId}.${assetName} The policy id in this example is the USDM policy id on Cardano Mainnet - use 16a55b2a349361ff88c03788f93e1e966e5d689605d044fef722ddde for USDM on Preprod. The asset name is the hex representation of '(333) USDM' + "amount": "10000", // atomic units of `asset`; USDM has 6 decimals, so 1 USDM = 1000000 + "asset": "c48cbb3d5e57ed56e276bc45f99ab39abe94e6cd7ac39fb402da47ad.0014df105553444d", // ${policyId}.${assetNameHex} — USDM on Cardano Mainnet. BOTH parts differ on Preprod: policy id e675b46e4d2242c991a8932a99db3044e80515ae14b4c4ccf6b3f4c9, asset name 0014df10745553444d "payTo": "addr1...", // In case of script payments, this is the script address (the address should match the script provided in extra after applying parameters. In case of additional parameters provided, the client needs to pass the additional parameters to the server in the PAYMENT-SIGNATURE header, so that the server can reconstruct the script address and verify the payment) "maxTimeoutSeconds": 600, // Has to be set to a higher amount of time because of the Cardano Network speed "extra": { @@ -292,12 +507,15 @@ The PAYMENT-SIGNATURE header is base64-encoded and sent in the client's request The payload field of the PAYMENT-SIGNATURE header must contain the following fields: -transaction: The signed Cardano transaction (Base64 encoded). +- `transaction`: The signed Cardano transaction (Base64 encoded). +- `nonce`: A UTXO reference (`txHash#index`) that is also one of the transaction's inputs. This is the replay guard the facilitator enforces (Verification Rule 5), so a payload without it MUST be rejected. + Example: ```js { - "transaction": "AAAIAQDi1HwjSnS6M+WGvD73iEyUY2FRKNj0MlRp7+3SHZM3xCvMdB0AAAAAIFRgPKOstGBLCnbcyGoOXugUYAWwVzNrpMjPCzXK4KQWAQCMoE29VLGwftex8rhIlOuFLFNfxLIJlHqGXoXA8hx6l+LMdB0AAAAAIHbPucTRIEWgO6lzqukswPZ6i72IHEKK5LyM1l9HJNZNAQBthSeHDVK8Xr5/zp3JMZPLtG5uAoVgedTA4pEnp+h8qUlUzRwAAAAAIACH0swYW/QfGCFczGnjAVPHPqZrQE5vfvJr36i6KVEFAQAC7W4K5vCwB+nprjxcNlLiOQ7SIIfyCZjmj2qSis2iTsCuzBwAAAAAIAkSUkXOoeq52GNdhwpbs+jZqqrqPdmiN3oPw5EzDIanAQAIyFNGWD6OxiFIyXSxrNEcFG0npm+nImk6InUssXb1EZgx1hwAAAAAILhsjmMKyM0n75Cd7z6ufH2LNhOMibFOGhNlLgV5RFuEAQC+Mh4kGkLwrw/11729oUQnt3xOmOreE6PcnuN6M68ZBcCuzBwAAAAAIO2PQhSSqSAawCbRr005lfjBgFOqIHo4zb2GcQ/WCxAlAAgA+QKVAAAAAAAgjiAHD0X4HNSdVPpJtf2E6W2uRc8kbvCHYkgEQ1B+w1MDAwEAAAUBAQABAgABAwABBAABBQACAQAAAQEGAAEBAgEAAQcAHrfFfj8r0Pxsudz/0UPqlX5NmPgFw1hzP3be4GZ/4LEB5XXrONxGw0qOUsq3yNKeUhOCOgCIwaa4pswKaer66EKqPGwdAAAAACBrOIN4poutFUmHfB6FbFJu8GgXoPPTGQWREqFpPfvO1B63xX4/K9D8bLnc/9FD6pV+TZj4BcNYcz923uBmf+Cx7gIAAAAAAABg4xYAAAAAAAA=" + "transaction": "AAAIAQDi1HwjSnS6M+WGvD73iEyUY2FRKNj0MlRp7+3SHZM3xCvMdB0AAAAAIFRgPKOstGBLCnbcyGoOXugUYAWwVzNrpMjPCzXK4KQWAQCMoE29VLGwftex8rhIlOuFLFNfxLIJlHqGXoXA8hx6l+LMdB0AAAAAIHbPucTRIEWgO6lzqukswPZ6i72IHEKK5LyM1l9HJNZNAQBthSeHDVK8Xr5/zp3JMZPLtG5uAoVgedTA4pEnp+h8qUlUzRwAAAAAIACH0swYW/QfGCFczGnjAVPHPqZrQE5vfvJr36i6KVEFAQAC7W4K5vCwB+nprjxcNlLiOQ7SIIfyCZjmj2qSis2iTsCuzBwAAAAAIAkSUkXOoeq52GNdhwpbs+jZqqrqPdmiN3oPw5EzDIanAQAIyFNGWD6OxiFIyXSxrNEcFG0npm+nImk6InUssXb1EZgx1hwAAAAAILhsjmMKyM0n75Cd7z6ufH2LNhOMibFOGhNlLgV5RFuEAQC+Mh4kGkLwrw/11729oUQnt3xOmOreE6PcnuN6M68ZBcCuzBwAAAAAIO2PQhSSqSAawCbRr005lfjBgFOqIHo4zb2GcQ/WCxAlAAgA+QKVAAAAAAAgjiAHD0X4HNSdVPpJtf2E6W2uRc8kbvCHYkgEQ1B+w1MDAwEAAAUBAQABAgABAwABBAABBQACAQAAAQEGAAEBAgEAAQcAHrfFfj8r0Pxsudz/0UPqlX5NmPgFw1hzP3be4GZ/4LEB5XXrONxGw0qOUsq3yNKeUhOCOgCIwaa4pswKaer66EKqPGwdAAAAACBrOIN4poutFUmHfB6FbFJu8GgXoPPTGQWREqFpPfvO1B63xX4/K9D8bLnc/9FD6pV+TZj4BcNYcz923uBmf+Cx7gIAAAAAAABg4xYAAAAAAAA=", + "nonce": "662cbf645fcd8914eb89115b83970a950493dd2fbaf39dea3b96e8cbdc132939#0" } ``` @@ -323,7 +541,7 @@ Full PAYMENT-SIGNATURE header: } }, "payload": { - "transaction": "AAAIAQDi1HwjSnS6M+WGvD73iEyUY2FRKNj0MlRp7+3SHZM3xCvMdB0AAAAAIFRgPKOstGBLCnbcyGoOXugUYAWwVzNrpMjPCzXK4KQWAQCMoE29VLGwftex8rhIlOuFLFNfxLIJlHqGXoXA8hx6l+LMdB0AAAAAIHbPucTRIEWgO6lzqukswPZ6i72IHEKK5LyM1l9HJNZNAQBthSeHDVK8Xr5/zp3JMZPLtG5uAoVgedTA4pEnp+h8qUlUzRwAAAAAIACH0swYW/QfGCFczGnjAVPHPqZrQE5vfvJr36i6KVEFAQAC7W4K5vCwB+nprjxcNlLiOQ7SIIfyCZjmj2qSis2iTsCuzBwAAAAAIAkSUkXOoeq52GNdhwpbs+jZqqrqPdmiN3oPw5EzDIanAQAIyFNGWD6OxiFIyXSxrNEcFG0npm+nImk6InUssXb1EZgx1hwAAAAAILhsjmMKyM0n75Cd7z6ufH2LNhOMibFOGhNlLgV5RFuEAQC+Mh4kGkLwrw/11729oUQnt3xOmOreE6PcnuN6M68ZBcCuzBwAAAAAIO2PQhSSqSAawCbRr005lfjBgFOqIHo4zb2GcQ/WCxAlAAgA+QKVAAAAAAAgjiAHD0X4HNSdVPpJtf2E6W2uRc8kbvCHYkgEQ1B+w1MDAwEAAAUBAQABAgABAwABBAABBQACAQAAAQEGAAEBAgEAAQcAHrfFfj8r0Pxsudz/0UPqlX5NmPgFw1hzP3be4GZ/4LEB5XXrONxGw0qOUsq3yNKeUhOCOgCIwaa4pswKaer66EKqPGwdAAAAACBrOIN4poutFUmHfB6FbFJu8GgXoPPTGQWREqFpPfvO1B63xX4/K9D8bLnc/9FD6pV+TZj4BcNYcz923uBmf+Cx7gIAAAAAAABg4xYAAAAAAAA=" + "transaction": "AAAIAQDi1HwjSnS6M+WGvD73iEyUY2FRKNj0MlRp7+3SHZM3xCvMdB0AAAAAIFRgPKOstGBLCnbcyGoOXugUYAWwVzNrpMjPCzXK4KQWAQCMoE29VLGwftex8rhIlOuFLFNfxLIJlHqGXoXA8hx6l+LMdB0AAAAAIHbPucTRIEWgO6lzqukswPZ6i72IHEKK5LyM1l9HJNZNAQBthSeHDVK8Xr5/zp3JMZPLtG5uAoVgedTA4pEnp+h8qUlUzRwAAAAAIACH0swYW/QfGCFczGnjAVPHPqZrQE5vfvJr36i6KVEFAQAC7W4K5vCwB+nprjxcNlLiOQ7SIIfyCZjmj2qSis2iTsCuzBwAAAAAIAkSUkXOoeq52GNdhwpbs+jZqqrqPdmiN3oPw5EzDIanAQAIyFNGWD6OxiFIyXSxrNEcFG0npm+nImk6InUssXb1EZgx1hwAAAAAILhsjmMKyM0n75Cd7z6ufH2LNhOMibFOGhNlLgV5RFuEAQC+Mh4kGkLwrw/11729oUQnt3xOmOreE6PcnuN6M68ZBcCuzBwAAAAAIO2PQhSSqSAawCbRr005lfjBgFOqIHo4zb2GcQ/WCxAlAAgA+QKVAAAAAAAgjiAHD0X4HNSdVPpJtf2E6W2uRc8kbvCHYkgEQ1B+w1MDAwEAAAUBAQABAgABAwABBAABBQACAQAAAQEGAAEBAgEAAQcAHrfFfj8r0Pxsudz/0UPqlX5NmPgFw1hzP3be4GZ/4LEB5XXrONxGw0qOUsq3yNKeUhOCOgCIwaa4pswKaer66EKqPGwdAAAAACBrOIN4poutFUmHfB6FbFJu8GgXoPPTGQWREqFpPfvO1B63xX4/K9D8bLnc/9FD6pV+TZj4BcNYcz923uBmf+Cx7gIAAAAAAABg4xYAAAAAAAA=", "nonce": "662cbf645fcd8914eb89115b83970a950493dd2fbaf39dea3b96e8cbdc132939#0" } } @@ -350,22 +568,39 @@ Expanded Schema based on assetTransferMethods: "maxTimeoutSeconds": 600, "extra": { "assetTransferMethod": "masumi", - "paymentType": "Web3CardanoV2", - "contractAddress": "addr_test1w...", - "sellerAddress": "addr_test1q...", - "referenceKey": "", - "referenceSignature": "", - "sellerNonce": "", - "agentIdentifier": "", - "collateralReturnLovelace": "0", - "payByTime": "1713626260000", - "submitResultTime": "1713636260000", - "unlockTime": "1713636260000", - "externalDisputeUnlockTime": "1713636260000" + "inputCommitment": { + "version": "1", + "algorithm": "sha256", + "parts": [ + { + "name": "body", + "canonicalization": "jcs", + "mediaType": "application/json", + "content": { "days": 3, "units": "metric" }, + "digest": "<32-byte lowercase hex>" + } + ], + "digest": "<32-byte lowercase hex>" + }, + "terms": { + "version": "1", + "paymentType": "Web3CardanoV2", + "sellerAddress": "addr_test1q...", + "sellerNonce": "<32-byte lowercase hex>", + "buyerNonce": "", + "inputHash": "", + "payByTime": "1713626260000", + "submitResultTime": "1713636260000", + "unlockTime": "1713640260000", + "externalDisputeUnlockTime": "1713644260000" + }, + "referenceKey": "", + "referenceSignature": "", + "blockchainIdentifier": "" } }, "payload": { - "transaction": "AAAIAQDi1HwjSnS6M+WGvD73iEyUY2FRKNj0MlRp7+3SHZM3xCvMdB0AAAAAIFRgPKOstGBLCnbcyGoOXugUYAWwVzNrpMjPCzXK4KQWAQCMoE29VLGwftex8rhIlOuFLFNfxLIJlHqGXoXA8hx6l+LMdB0AAAAAIHbPucTRIEWgO6lzqukswPZ6i72IHEKK5LyM1l9HJNZNAQBthSeHDVK8Xr5/zp3JMZPLtG5uAoVgedTA4pEnp+h8qUlUzRwAAAAAIACH0swYW/QfGCFczGnjAVPHPqZrQE5vfvJr36i6KVEFAQAC7W4K5vCwB+nprjxcNlLiOQ7SIIfyCZjmj2qSis2iTsCuzBwAAAAAIAkSUkXOoeq52GNdhwpbs+jZqqrqPdmiN3oPw5EzDIanAQAIyFNGWD6OxiFIyXSxrNEcFG0npm+nImk6InUssXb1EZgx1hwAAAAAILhsjmMKyM0n75Cd7z6ufH2LNhOMibFOGhNlLgV5RFuEAQC+Mh4kGkLwrw/11729oUQnt3xOmOreE6PcnuN6M68ZBcCuzBwAAAAAIO2PQhSSqSAawCbRr005lfjBgFOqIHo4zb2GcQ/WCxAlAAgA+QKVAAAAAAAgjiAHD0X4HNSdVPpJtf2E6W2uRc8kbvCHYkgEQ1B+w1MDAwEAAAUBAQABAgABAwABBAABBQACAQAAAQEGAAEBAgEAAQcAHrfFfj8r0Pxsudz/0UPqlX5NmPgFw1hzP3be4GZ/4LEB5XXrONxGw0qOUsq3yNKeUhOCOgCIwaa4pswKaer66EKqPGwdAAAAACBrOIN4poutFUmHfB6FbFJu8GgXoPPTGQWREqFpPfvO1B63xX4/K9D8bLnc/9FD6pV+TZj4BcNYcz923uBmf+Cx7gIAAAAAAABg4xYAAAAAAAA=" + "transaction": "AAAIAQDi1HwjSnS6M+WGvD73iEyUY2FRKNj0MlRp7+3SHZM3xCvMdB0AAAAAIFRgPKOstGBLCnbcyGoOXugUYAWwVzNrpMjPCzXK4KQWAQCMoE29VLGwftex8rhIlOuFLFNfxLIJlHqGXoXA8hx6l+LMdB0AAAAAIHbPucTRIEWgO6lzqukswPZ6i72IHEKK5LyM1l9HJNZNAQBthSeHDVK8Xr5/zp3JMZPLtG5uAoVgedTA4pEnp+h8qUlUzRwAAAAAIACH0swYW/QfGCFczGnjAVPHPqZrQE5vfvJr36i6KVEFAQAC7W4K5vCwB+nprjxcNlLiOQ7SIIfyCZjmj2qSis2iTsCuzBwAAAAAIAkSUkXOoeq52GNdhwpbs+jZqqrqPdmiN3oPw5EzDIanAQAIyFNGWD6OxiFIyXSxrNEcFG0npm+nImk6InUssXb1EZgx1hwAAAAAILhsjmMKyM0n75Cd7z6ufH2LNhOMibFOGhNlLgV5RFuEAQC+Mh4kGkLwrw/11729oUQnt3xOmOreE6PcnuN6M68ZBcCuzBwAAAAAIO2PQhSSqSAawCbRr005lfjBgFOqIHo4zb2GcQ/WCxAlAAgA+QKVAAAAAAAgjiAHD0X4HNSdVPpJtf2E6W2uRc8kbvCHYkgEQ1B+w1MDAwEAAAUBAQABAgABAwABBAABBQACAQAAAQEGAAEBAgEAAQcAHrfFfj8r0Pxsudz/0UPqlX5NmPgFw1hzP3be4GZ/4LEB5XXrONxGw0qOUsq3yNKeUhOCOgCIwaa4pswKaer66EKqPGwdAAAAACBrOIN4poutFUmHfB6FbFJu8GgXoPPTGQWREqFpPfvO1B63xX4/K9D8bLnc/9FD6pV+TZj4BcNYcz923uBmf+Cx7gIAAAAAAABg4xYAAAAAAAA=", "nonce": "662cbf645fcd8914eb89115b83970a950493dd2fbaf39dea3b96e8cbdc132939#0" } } @@ -402,7 +637,7 @@ Expanded Schema based on assetTransferMethods: } }, "payload": { - "transaction": "AAAIAQDi1HwjSnS6M+WGvD73iEyUY2FRKNj0MlRp7+3SHZM3xCvMdB0AAAAAIFRgPKOstGBLCnbcyGoOXugUYAWwVzNrpMjPCzXK4KQWAQCMoE29VLGwftex8rhIlOuFLFNfxLIJlHqGXoXA8hx6l+LMdB0AAAAAIHbPucTRIEWgO6lzqukswPZ6i72IHEKK5LyM1l9HJNZNAQBthSeHDVK8Xr5/zp3JMZPLtG5uAoVgedTA4pEnp+h8qUlUzRwAAAAAIACH0swYW/QfGCFczGnjAVPHPqZrQE5vfvJr36i6KVEFAQAC7W4K5vCwB+nprjxcNlLiOQ7SIIfyCZjmj2qSis2iTsCuzBwAAAAAIAkSUkXOoeq52GNdhwpbs+jZqqrqPdmiN3oPw5EzDIanAQAIyFNGWD6OxiFIyXSxrNEcFG0npm+nImk6InUssXb1EZgx1hwAAAAAILhsjmMKyM0n75Cd7z6ufH2LNhOMibFOGhNlLgV5RFuEAQC+Mh4kGkLwrw/11729oUQnt3xOmOreE6PcnuN6M68ZBcCuzBwAAAAAIO2PQhSSqSAawCbRr005lfjBgFOqIHo4zb2GcQ/WCxAlAAgA+QKVAAAAAAAgjiAHD0X4HNSdVPpJtf2E6W2uRc8kbvCHYkgEQ1B+w1MDAwEAAAUBAQABAgABAwABBAABBQACAQAAAQEGAAEBAgEAAQcAHrfFfj8r0Pxsudz/0UPqlX5NmPgFw1hzP3be4GZ/4LEB5XXrONxGw0qOUsq3yNKeUhOCOgCIwaa4pswKaer66EKqPGwdAAAAACBrOIN4poutFUmHfB6FbFJu8GgXoPPTGQWREqFpPfvO1B63xX4/K9D8bLnc/9FD6pV+TZj4BcNYcz923uBmf+Cx7gIAAAAAAABg4xYAAAAAAAA=" + "transaction": "AAAIAQDi1HwjSnS6M+WGvD73iEyUY2FRKNj0MlRp7+3SHZM3xCvMdB0AAAAAIFRgPKOstGBLCnbcyGoOXugUYAWwVzNrpMjPCzXK4KQWAQCMoE29VLGwftex8rhIlOuFLFNfxLIJlHqGXoXA8hx6l+LMdB0AAAAAIHbPucTRIEWgO6lzqukswPZ6i72IHEKK5LyM1l9HJNZNAQBthSeHDVK8Xr5/zp3JMZPLtG5uAoVgedTA4pEnp+h8qUlUzRwAAAAAIACH0swYW/QfGCFczGnjAVPHPqZrQE5vfvJr36i6KVEFAQAC7W4K5vCwB+nprjxcNlLiOQ7SIIfyCZjmj2qSis2iTsCuzBwAAAAAIAkSUkXOoeq52GNdhwpbs+jZqqrqPdmiN3oPw5EzDIanAQAIyFNGWD6OxiFIyXSxrNEcFG0npm+nImk6InUssXb1EZgx1hwAAAAAILhsjmMKyM0n75Cd7z6ufH2LNhOMibFOGhNlLgV5RFuEAQC+Mh4kGkLwrw/11729oUQnt3xOmOreE6PcnuN6M68ZBcCuzBwAAAAAIO2PQhSSqSAawCbRr005lfjBgFOqIHo4zb2GcQ/WCxAlAAgA+QKVAAAAAAAgjiAHD0X4HNSdVPpJtf2E6W2uRc8kbvCHYkgEQ1B+w1MDAwEAAAUBAQABAgABAwABBAABBQACAQAAAQEGAAEBAgEAAQcAHrfFfj8r0Pxsudz/0UPqlX5NmPgFw1hzP3be4GZ/4LEB5XXrONxGw0qOUsq3yNKeUhOCOgCIwaa4pswKaer66EKqPGwdAAAAACBrOIN4poutFUmHfB6FbFJu8GgXoPPTGQWREqFpPfvO1B63xX4/K9D8bLnc/9FD6pV+TZj4BcNYcz923uBmf+Cx7gIAAAAAAABg4xYAAAAAAAA=", "nonce": "662cbf645fcd8914eb89115b83970a950493dd2fbaf39dea3b96e8cbdc132939#0" } } @@ -422,26 +657,28 @@ A facilitator MUST enforce all of the following rules before accepting a payment 5. **Nonce / Replay Prevention**: The `payload.nonce` MUST be a valid UTXO reference (`txHash#index`) that is included as an input in the transaction. The facilitator MUST verify that this UTXO exists in the current on-chain UTXO set and has not been spent. This ensures uniqueness and prevents replay attacks. -6. **TTL / Expiry Check**: The transaction's TTL (time-to-live slot) MUST not have already passed at the time of verification. The facilitator MUST reject transactions whose TTL is in the past. The TTL SHOULD be consistent with `PaymentRequirements.maxTimeoutSeconds`. +6. **TTL / Expiry Check**: The transaction's TTL (time-to-live slot) MUST not have already passed at the time of verification. The facilitator MUST reject transactions whose TTL is in the past, and the TTL MUST NOT be later than the slot corresponding to the current network time plus `PaymentRequirements.maxTimeoutSeconds`. Both client and facilitator MUST convert wall-clock time to a slot using the current system-start and era summary; they MUST NOT compare seconds against slots as raw values, and MUST NOT assume one slot per second (this holds only from Shelley onward, and is a protocol parameter rather than a constant). 7. **Minimum UTXO Check**: The output paying `payTo` SHOULD carry at least the protocol minimum lovelace for its serialized size, `(160 + |serialized_output|) * coinsPerUtxoByte` (see [Minimum UTXO Value](#minimum-utxo-value-min-ada)). An output below this minimum yields a transaction the node rejects at submission, so the facilitator SHOULD reject it during `verify()` rather than let `settle()` fail. Because `coinsPerUtxoByte` is governance-settable, the facilitator MUST read it from live protocol parameters. A facilitator without access to live protocol parameters MAY skip this check and rely on the node to reject an undersized output at submission. **Masumi assetTransferMethod — additional rules.** When `requirements.extra.assetTransferMethod` is `masumi`, the facilitator MUST additionally enforce (rule 2's "recipient" is the escrow output paying `payTo`): -- `payTo` equals the deployment's **Masumi escrow (`vested_pay`) script address**, declared in `extra.contractAddress`. The declaration is **REQUIRED** — a missing `contractAddress` is a rejection, never defaulted. -- The output paying `payTo` carries an **inline datum** decoding to the lock datum with `state == FundsLocked`, empty `result_hash`, and **both cooldown timers `0`**, and the output carries **no reference script**. -- `buyer` equals the transaction's payer and `seller` equals `extra.sellerAddress`; both are **public-key** credential addresses. -- Neither participant nor either return address equals the escrow address — a payout aimed back at the script is unspendable (see the lock invariants above). -- `reference_key`, `reference_signature`, `seller_nonce`, `agent_identifier`, `collateral_return_lovelace`, and the four time bounds in the datum match the corresponding `extra` values, and `reference_signature` is at least 16 bytes. -- The buyer-supplied fields are not required in `extra`: `buyer_nonce` and `input_hash` are matched **only when the server declared them**, and `buyer_return_address` is never matched (the buyer chooses its own refund address). A server issuing a 402 for an unauthenticated request cannot know these, so their absence MUST NOT be a rejection; the buyer remains bound by the `buyer` = payer rule. -- `seller_return_address` matches `extra` **exactly**: declared in `extra` ⇒ present in the datum with matching credentials; omitted from `extra` ⇒ `None` in the datum. -- **Value.** For the requested `asset`: **lovelace** MAY be overpaid but the locked lovelace MUST be ≥ `amount + collateral_return_lovelace`; a **native token** MUST match `amount` exactly. The escrow output MUST carry **exactly** the requested asset set — no extra native tokens (zero tokens for a lovelace payment, exactly the one requested token otherwise). `collateral_return_lovelace` MUST be `0` or ≥ **1,435,230** and MUST NOT exceed the locked lovelace. -- **Deadline.** The transaction MUST carry a validity upper bound (TTL) whose slot time is on/before `pay_by_time`. +- **Schema.** `extra`, `inputCommitment`, each commitment part, `terms` and `deployment` validate as **closed objects**; an unknown field, a `terms` field that duplicates a projected top-level field, or a missing required field is a rejection. `paymentType` MUST be `Web3CardanoV2`; any other value is rejected rather than ignored. +- **Commitment.** Every part digest and `inputCommitment.digest` recompute correctly, and `terms.inputHash` equals `inputCommitment.digest`. +- **Seller authorization.** `signedTerms` reconstructs from `terms` plus the projected `PaymentRequirements` fields, `termsDigest` recomputes, and both COSE objects decode and verify against it — including `Blake2b-224(publicKey) == ` the seller's payment-key credential. A `sellerAddress` with a script payment credential is rejected. +- **Escrow address.** The verifier applies the canonical (or explicitly allowed custom) deployment parameters to the canonical blueprint, derives the validator hash and network address itself, and requires it to equal `payTo`. `payTo` MUST also equal `signedTerms.contractAddress` and the contract address decoded from `blockchainIdentifier`. There is **exactly one** escrow output at `payTo`. +- **Identity.** When `terms.agentIdentifier` is present, the V2 registry claim is validated independently (asset, seller authorization, metadata, endpoint, network, exact price) and the registered price resolves to the signed `amount`/`asset`. When it is omitted, the datum's `agent_identifier` MUST be empty bytes. +- The output paying `payTo` carries an **inline datum** decoding against the `masumi.vested_pay.v2` schema, with `state == FundsLocked`, empty `result_hash`, and **both cooldown timers `0`**; the output carries **no reference script**. +- `seller` equals `terms.sellerAddress`, and the payment credential in `buyer` **controls the input named by `payload.nonce`** with a valid witness present. Both are **public-key** credential addresses. +- Neither participant nor either return address equals the escrow address, and the effective buyer payout target differs from the effective seller payout target (see the lock invariants above). +- `reference_key`, `reference_signature`, `seller_nonce`, `buyer_nonce`, `agent_identifier`, `input_hash` and the four time bounds in the datum match the signed terms exactly, and `reference_signature` is at least 16 bytes. `buyer_return_address` is buyer-chosen and is **not** matched; `seller_return_address` matches the signed terms exactly (declared ⇒ present with matching credentials; omitted ⇒ `None`). +- **Value.** `lockedLovelace` equals `requestedLovelace + collateral_return_lovelace`, where `requestedLovelace` is `amount` for a lovelace payment and `0` for a native-token payment; a native token MUST match `amount` exactly. The escrow output MUST carry **exactly** the requested asset set — no extra native tokens. `collateral_return_lovelace` MUST be `0` or ≥ **1,435,230**, and MUST be large enough that `lockedLovelace` clears the post-`SubmitResult` min-UTXO. +- **Deadline.** The transaction MUST carry a validity upper bound (TTL) whose slot time is on/before `pay_by_time`, and the four deadlines MUST clear the minimum intervals in the lock invariants. - **Minimum UTXO.** The escrow output MUST hold enough lovelace for the protocol min-UTXO of the datum **after `SubmitResult`** (32-byte `result_hash` + non-zero cooldowns), so the seller's later spend stays above min-UTXO. Rule 7's carve-out applies: a facilitator without live protocol parameters MAY skip this check and rely on the node's own min-UTXO rejection at submission. A single Masumi payment locks **one** asset — lovelace, or one native token plus its structural lovelace (which covers the collateral and min-UTXO). `PaymentRequirements` carries a single `asset`/`amount`, so a multi-asset basket is out of scope for this scheme. -A valid Masumi settlement means the funds are **locked in the escrow**, not delivered to the seller; releasing them is governed by the contract and the Masumi Payment Service in later transactions outside this scheme. +A valid Masumi settlement means the funds are **locked in the escrow**, not delivered to the seller; releasing them is governed by the contract in later transactions outside this scheme. **Script assetTransferMethod — additional rules.** When `requirements.extra.assetTransferMethod` is `script`, the facilitator MUST: @@ -463,10 +700,10 @@ Schema: { "success": true, // true or false "network": "cardano:mainnet", - "transaction": "2f9a7b3c..." // Transaction hash of the payment if successful + "transaction": "2f9a7b3c...", // Transaction hash of the payment if successful "extra": { - "status": "confirmed", // "confirmed" is the recommended value; "mempool" is permitted but strongly discouraged — see settlement warning above - } + "status": "confirmed" // "confirmed" is the recommended value; "mempool" is permitted but strongly discouraged — see settlement warning above + }, // Optional error field in case of failure "errorReason": "Utxo not found in utxo set" // Example error reason } @@ -511,12 +748,25 @@ Cardano's eUTXO model provides strong replay protection once a payment settles: Merchants and/or Facilitators SHOULD maintain a short-term, in-memory cache of transaction payloads that are currently being settled. Before proceeding with settlement, the merchant/facilitator checks whether the transaction has already been seen: -1. After verification succeeds, derive a cache key from the transaction payload (e.g., the base64-encoded signed transaction string). -2. If the key is already present in the cache, reject the settlement with a `"duplicate_settlement"` error. +1. After verification succeeds, derive the cache key from the **canonical Cardano transaction ID** computed over the transaction body. The key MUST NOT be the complete signed CBOR or its encoding: witness sets and equally valid CBOR encodings differ without changing the ledger transaction ID, so an encoding-level key is trivially bypassed by re-serializing the same logical transaction. +2. If the key is already present in the cache, return its cached outcome or await the in-flight first submission, and reject the duplicate settlement with a `"duplicate_settlement"` error rather than executing the protected operation twice. 3. If the key is not present, insert it into the cache — atomically, before the first `await` on submission, so concurrent calls cannot both pass the check — and proceed with submission. -4. Evict entries older than ~120 seconds. The on-chain nonce spend is the durable replay guard, so the cache only needs to cover the unconfirmed window; a transaction whose TTL has already passed can no longer land regardless. +4. Retain each entry until the transaction's TTL has passed **plus** the implementation's confirmation and rollback grace period. A shorter fixed timeout (e.g. a flat 120 seconds) reopens the race while the transaction can still land. + +The claim SHOULD be released only after it is established that no submission occurred, or after a definitive ledger rejection, so a legitimate retry can re-attempt. A timeout, transport failure, unknown node result or mempool-only result MUST retain the claim; the service then reconciles by transaction ID. + +This approach requires no external storage — only an in-process map with time-based eviction. It preserves the facilitator's otherwise stateless design while closing the duplicate settlement attack vector. Note that the cache is per-process: across multiple facilitator instances it does not deduplicate, but the on-chain nonce spend (Rule 5) remains the authoritative cross-instance replay guard. + +### Masumi logical replay + +Transaction-level deduplication is **not sufficient** for `masumi`. A client can build several *different* transactions from the same 402 — different inputs, change, or fee — each carrying a valid escrow datum for the same seller terms. Every one of them passes verification and locks funds, and the tx-ID cache above catches none of them, so the buyer pays repeatedly for one job. + +`masumi` therefore has a logical replay key in addition to the transaction ID: **`termsDigest`**, which by construction covers the price, asset, contract, request commitment, identity and deadlines of exactly one issued 402. -The claim SHOULD be released if submission fails with a transient error, so a legitimate retry can re-attempt; it SHOULD be retained on success (including `mempool`-only inclusion) so retries cannot rebroadcast the same transaction. +- The requirements issuer SHOULD bind each `termsDigest` to the **first** durably claimed transaction ID, atomically. A retry carrying the same transaction is idempotent; a *different* transaction for the same digest is a conflict and MUST NOT start further work, even if it lands earlier in canonical ledger order. +- Once claimed, a `termsDigest` SHOULD stay bound to that transaction ID, including after rejection or expiry. A failed payment needs new requirements with a fresh `sellerNonce` — reusing the terms would reuse the signature. +- The facilitator MAY cache `termsDigest` → transaction ID → outcome as an advisory check, retained through the signed validity window plus the confirmation and rollback grace period. +- The verified `blockchainIdentifier` is a compatibility lookup key for the same record; it does not replace the `termsDigest` binding. -This approach requires no external storage or long-lived state — only an in-process map with time-based eviction. It preserves the facilitator's otherwise stateless design while closing the duplicate settlement attack vector. Note that the cache is per-process: across multiple facilitator instances it does not deduplicate, but the on-chain nonce spend (Rule 5) remains the authoritative cross-instance replay guard. +Every additional valid lock for one `termsDigest` is a duplicate deposit and remains governed by the Masumi V2 refund and withdrawal paths — x402 does not recover it.