From b235ccd2fb0e1d8038e264004abe3b607d142d5d Mon Sep 17 00:00:00 2001 From: Denver <135147764+Denver-1st@users.noreply.github.com> Date: Mon, 27 Jul 2026 14:14:48 +0300 Subject: [PATCH 1/8] defined escrow service invocation --- PIP-01-escrow-descriptor.md | 163 +++++++++++++++++++++++++++++++++++- README.md | 6 +- 2 files changed, 165 insertions(+), 4 deletions(-) diff --git a/PIP-01-escrow-descriptor.md b/PIP-01-escrow-descriptor.md index 64660b3..6157b67 100644 --- a/PIP-01-escrow-descriptor.md +++ b/PIP-01-escrow-descriptor.md @@ -28,6 +28,7 @@ The escrow descriptor tells counterparties and operators: - what the funding and release rules are - how the escrow instance is referenced - what timeout and dispute assumptions apply +- whether and how the escrow may be invoked as a standalone service ## Minimum Content @@ -44,6 +45,157 @@ Minimum expected fields: - `reference_format` - `updated_at` +## Standalone Sufficiency + +An escrow descriptor has two intended use levels: + +- **compatibility and discovery** — the descriptor declares which networks, assets, reference formats, funding rules, release rules, and dispute policy an escrow supports, so that agents and swaps can select it +- **standalone operation** — the descriptor additionally declares the public service interface required to instantiate and operate an escrow instance without out-of-band negotiation + +A descriptor that omits the `service` block (see Service Interface) is sufficient for compatibility and discovery and for use inside Pontmore swap flows where the swap state machine in [PIP-02-swap-state-machine.md](./PIP-02-swap-state-machine.md) carries execution. It is NOT sufficient for a standalone application to create, fund, observe, release, or refund an escrow instance on its own. + +A descriptor that includes a `service` block is intended to be sufficient for standalone application use. When `service` is present, the descriptor is the primary source of truth for the public escrow service interface. + +The minimum content fields in Minimum Content remain required in both use levels. The `service` block is the additional, optional field that upgrades a descriptor from discovery-only to standalone-sufficient. + +## Service Interface + +The `service` content field advertises the public interface that a standalone application uses to instantiate and operate an escrow instance. It is OPTIONAL at the descriptor level. When present, it MUST conform to this section. + +### Service Fields + +When `service` is present, it MUST include: + +- `transport` + - non-empty array of supported service transports + - canonical first transport is `https` +- `interface` + - interface and version identifier; example: `pontmore_escrow_http_v1` +- `endpoint` + - base service URL for HTTPS transports +- `auth` + - non-empty array of supported authentication methods; see Authentication +- `operations` + - non-empty array of supported escrow instance operations; see Operations +- `funding_model` + - multi-party funding model; see Funding Model +- `release_decisions` + - non-empty array of accepted release-decision formats; see Release Decisions + +`service` SHOULD include: + +- `schema_url` + - machine-readable schema URL, for example an OpenAPI document or another protocol-native schema + +### Authentication + +For HTTPS transports, the recommended authentication method is `nostr_http_auth`, defined as NIP-98 (Nostr HTTP Authentication). + +- a participant authenticates by signing each HTTP request with its Nostr key, producing a NIP-98 authorization header +- identity is the participant's Nostr pubkey; the descriptor and escrow operator MUST NOT require published bearer secrets for public protocol operations +- at `create`, the requesting participant supplies the set of participant Nostr pubkeys bound to the escrow instance; the operator binds subsequent `fund_status`, `release`, `refund`, and `cancel` operations to those pubkeys +- each participant signs its own operations; threshold release decisions require signatures from the declared threshold of bound participants +- operators MAY require additional private operator-layer authorization for back-office actions, but such authorization is an operator overlay and MUST NOT be advertised as a public descriptor field + +### Operations + +The canonical escrow instance operation vocabulary is: + +- `create` — open a new escrow instance and bind its participant pubkeys +- `fund_status` — observe the funding state of a participant's side +- `release` — request release of the escrowed amount to the winner or payee +- `refund` — request refund of the escrowed amount to its funder +- `cancel` — request cancellation of an unfunded or unresolved escrow instance + +Operations not in this vocabulary MAY be advertised but are non-canonical and MUST be documented by the operator's referenced schema. + +### Funding Model + +The `funding_model` field declares how many participants fund a single escrow instance: + +- `single_funder` — one participant funds the escrow +- `two_party` — two participants each fund a side of the same escrow instance +- `n_of_m` — M of N declared participants must fund for the escrow to become active + +### Release Decisions + +`release_decisions` advertises the generic decision formats the operator accepts to authorize a `release` or `refund`. The minimum canonical vocabulary is: + +- `mutual_consent` — release or refund authorized by all declared participants +- `operator_decision` — release or refund authorized by the escrow operator as arbiter +- `oracle_signature` — release or refund authorized by a signature from a referenced oracle +- `application_signed_result` — release or refund authorized by a signed result from the originating application +- `threshold_participant_signatures` — release or refund authorized by a threshold of participant signatures + +`release_rules.release_trigger` states the public condition a specific escrow subtype requires before release; `service.release_decisions` states the generic decision formats the service accepts to satisfy such a trigger. Swap-specific triggers such as `counterparty_fiat_payment_confirmed` remain valid for Pontmore swap flows. For standalone non-swap use, `release_decisions` is the generic vocabulary an application relies on. + +### Public/Private Boundary + +The `service` block advertises only the public service interface. The following MUST NOT appear in the descriptor: + +- wallet identifiers +- custody backend identifiers +- private payment credentials +- internal account details +- private routing state +- operator-internal API keys or bearer secrets + +These are operator-layer implementation details. Their absence is what allows the same descriptor to be published openly without exposing operator internals. + +### Example: Two-Party Dice Game + +A standalone dice game discovers a published escrow descriptor and operates an escrow instance without a Pontmore swap: + +```json +{ + "version": 1, + "escrow_type": "custodial_escrow", + "networks": ["lightning"], + "funding_rules": { + "required_confirmation": "invoice_paid" + }, + "release_rules": { + "release_trigger": "application_signed_result", + "refund_trigger": "timeout_or_mutual_consent" + }, + "dispute_rules": { + "policy": "operator_resolved" + }, + "reference_format": "bolt11_or_custodial_escrow_reference", + "custody_authority": "escrow_operator", + "release_authority": "escrow_operator", + "refund_authority": "escrow_operator", + "implementations": [ + { + "network": "lightning", + "invoice_asset": "BTC", + "invoice_currency": "sats", + "invoice_amount_rule": "exact", + "payout_network": "lightning" + } + ], + "service": { + "transport": ["https"], + "interface": "pontmore_escrow_http_v1", + "endpoint": "https://escrow.example.com/pontmore/v1", + "schema_url": "https://escrow.example.com/pontmore/v1/openapi.json", + "auth": ["nostr_http_auth"], + "operations": ["create", "fund_status", "release", "refund", "cancel"], + "funding_model": "two_party", + "release_decisions": ["application_signed_result", "mutual_consent", "operator_decision"] + }, + "updated_at": 1775559028 +} +``` + +The flow validates the descriptor model: + +1. the application discovers the descriptor and reads `service` +2. both participants call `create` over HTTPS, authenticated with `nostr_http_auth`, binding both participant pubkeys to one escrow instance with `funding_model` `two_party` +3. each participant funds its side; the application reads `fund_status` until both sides are confirmed +4. the application commits a verifiable result (the die roll) and submits an `application_signed_result` release decision +5. the escrow releases both locked amounts to the winner via `release`, or refunds via `refund` if the result is unresolved at timeout + ## Network Declaration An escrow descriptor MUST declare every settlement or invoice network supported by the escrow configuration. @@ -382,8 +534,17 @@ Every agent profile should declare: That declared escrow must be usable without out-of-band negotiation at swap time. -## Open Question +For standalone (non-swap) use, an application SHOULD select a descriptor whose `service` block is present and whose advertised `operations`, `funding_model`, and `release_decisions` match the application's needs. A descriptor without `service` MUST NOT be treated as standalone-sufficient. + +## Open Questions Additional escrow mechanisms beyond `lightning_hold_invoice`, `custodial_escrow`, and `cashu_escrow` may still need their own canonical subtype-specific schemas. `cashu_escrow` introduces a dependency on a specific Cashu mint. Mint trust, mint selection, and mint failure modes are implementation assumptions rather than Pontmore protocol state. [Nimdolf](https://github.com/cashubtc/nuts/pull/390) is a possible future direction for mint liveness failover, but this proposal does not bind to it. + +Open questions for the service interface: + +- whether the service interface should remain in PIP-01 or split into a dedicated escrow service interface PIP as the operation set and transport matrix grow +- whether transports beyond HTTPS should be canonically defined, or left to descriptor extensions +- whether Nostr HTTP Auth (NIP-98) should be the single recommended authentication method, or one of several equally valid methods +- the minimum generic release-decision vocabulary beyond `mutual_consent`, `operator_decision`, `oracle_signature`, `application_signed_result`, and `threshold_participant_signatures` diff --git a/README.md b/README.md index 89db8d6..73d57b3 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Pontmore Protocol PIPs -Pontmore is a Nostr-native protocol family for agent identity, capability discovery, escrow declaration, and swap lifecycle coordination. +Pontmore is a Nostr-native protocol family for agent identity, capability discovery, escrow declaration and service interface, and swap lifecycle coordination. This repository is the canonical landing page for the Pontmore protocol and contains the `PIP` series: `Pontmore Improvement Proposal` documents that define the protocol family. @@ -22,7 +22,7 @@ Read `PIP-00` through `PIP-03` first. - [PIP-01-escrow-descriptor.md](./PIP-01-escrow-descriptor.md) - Implementation: `Required` - - public escrow declaration referenced by agents and swaps + - public escrow declaration and service interface referenced by agents and swaps - [PIP-02-swap-state-machine.md](./PIP-02-swap-state-machine.md) - Implementation: `Required` @@ -55,7 +55,7 @@ The first interoperable implementation baseline is: - [PIP-02-swap-state-machine.md](./PIP-02-swap-state-machine.md) - [PIP-03-dispute-policy.md](./PIP-03-dispute-policy.md) -These define the public discovery model, escrow declaration model, swap event lifecycle, and dispute boundary needed for a usable Pontmore-compatible implementation. +These define the public discovery model, escrow declaration and service interface model, swap event lifecycle, and dispute boundary needed for a usable Pontmore-compatible implementation. ## Contribution From 7ce493dcee4a7965492b275c81268be94e4e08b4 Mon Sep 17 00:00:00 2001 From: Denver <135147764+Denver-1st@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:07:40 +0300 Subject: [PATCH 2/8] defined escrow service invocation --- PIP-01-escrow-descriptor.md | 62 ++++++++++++++++++++++++++++--------- README.md | 2 +- 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/PIP-01-escrow-descriptor.md b/PIP-01-escrow-descriptor.md index 6157b67..38915a1 100644 --- a/PIP-01-escrow-descriptor.md +++ b/PIP-01-escrow-descriptor.md @@ -54,13 +54,13 @@ An escrow descriptor has two intended use levels: A descriptor that omits the `service` block (see Service Interface) is sufficient for compatibility and discovery and for use inside Pontmore swap flows where the swap state machine in [PIP-02-swap-state-machine.md](./PIP-02-swap-state-machine.md) carries execution. It is NOT sufficient for a standalone application to create, fund, observe, release, or refund an escrow instance on its own. -A descriptor that includes a `service` block is intended to be sufficient for standalone application use. When `service` is present, the descriptor is the primary source of truth for the public escrow service interface. +A descriptor that includes a `service` block and the required `schema_url` is intended to be sufficient for standalone application use. When `service` is present, the descriptor is the primary source of truth for the public escrow service interface. The minimum content fields in Minimum Content remain required in both use levels. The `service` block is the additional, optional field that upgrades a descriptor from discovery-only to standalone-sufficient. ## Service Interface -The `service` content field advertises the public interface that a standalone application uses to instantiate and operate an escrow instance. It is OPTIONAL at the descriptor level. When present, it MUST conform to this section. +The `service` content field advertises the public interface that a standalone application uses to instantiate and operate an escrow instance. It is OPTIONAL at the descriptor level. When present, it MUST conform to this section. A standalone client MUST reject a descriptor whose `service` block is incomplete, whose transport or authentication choices are unsupported or untrusted, or whose `schema_url` does not normatively define the advertised wire contract. ### Service Fields @@ -72,7 +72,9 @@ When `service` is present, it MUST include: - `interface` - interface and version identifier; example: `pontmore_escrow_http_v1` - `endpoint` - - base service URL for HTTPS transports + - canonical endpoint for the first advertised transport + - when `https` is advertised, the endpoint MUST be an absolute `https://` URL + - each additional advertised transport MUST resolve to exactly one endpoint using that transport's required scheme, as defined by `schema_url` - `auth` - non-empty array of supported authentication methods; see Authentication - `operations` @@ -81,11 +83,8 @@ When `service` is present, it MUST include: - multi-party funding model; see Funding Model - `release_decisions` - non-empty array of accepted release-decision formats; see Release Decisions - -`service` SHOULD include: - - `schema_url` - - machine-readable schema URL, for example an OpenAPI document or another protocol-native schema + - mandatory machine-readable schema URL for the normative wire contract; see Wire Contract ### Authentication @@ -96,17 +95,39 @@ For HTTPS transports, the recommended authentication method is `nostr_http_auth` - at `create`, the requesting participant supplies the set of participant Nostr pubkeys bound to the escrow instance; the operator binds subsequent `fund_status`, `release`, `refund`, and `cancel` operations to those pubkeys - each participant signs its own operations; threshold release decisions require signatures from the declared threshold of bound participants - operators MAY require additional private operator-layer authorization for back-office actions, but such authorization is an operator overlay and MUST NOT be advertised as a public descriptor field +- authentication identifiers MUST be registered methods; `nostr_http_auth` is the canonical HTTP method, and any extension method MUST be defined by `schema_url` with a mandatory interface schema and versioned identifier + +### Wire Contract + +`schema_url` MUST point to a schema that normatively defines the public service contract, either by embedding the contract itself or by referencing a complete protocol-native or OpenAPI schema. That schema MUST define: + +- HTTP methods and paths for `create`, `fund_status`, `release`, `refund`, `cancel`, and funding-instruction retrieval +- request and response schemas for each listed operation +- error formats and HTTP status codes +- escrow state transitions and terminal states +- idempotency rules, including the shared correlation value used by repeated `create` requests +- how `funding_model` and `release_decisions` values map to wire payload fields and validation rules +- transport-specific endpoint resolution rules for every advertised transport ### Operations The canonical escrow instance operation vocabulary is: -- `create` — open a new escrow instance and bind its participant pubkeys +- `create` — open a new escrow instance and bind its participant pubkeys. For shared-instance flows, the first participant call establishes the escrow instance and the second participant call uses the same shared invitation, client token, or idempotency key to bind its participant pubkey to that existing instance. Repeated calls with the same correlation value are idempotent and MUST NOT create a second escrow instance. - `fund_status` — observe the funding state of a participant's side - `release` — request release of the escrowed amount to the winner or payee - `refund` — request refund of the escrowed amount to its funder - `cancel` — request cancellation of an unfunded or unresolved escrow instance +Example shared-instance create flow: + +```text +participant A -> create(invitation=abc123) +participant B -> create(invitation=abc123) +``` + +The shared invitation, client token, or idempotency key is the correlation value that binds both participants to the same escrow instance. + Operations not in this vocabulary MAY be advertised but are non-canonical and MUST be documented by the operator's referenced schema. ### Funding Model @@ -115,7 +136,7 @@ The `funding_model` field declares how many participants fund a single escrow in - `single_funder` — one participant funds the escrow - `two_party` — two participants each fund a side of the same escrow instance -- `n_of_m` — M of N declared participants must fund for the escrow to become active +- `n_of_m` — the referenced schema MUST expose both the activation threshold and the declared participant count using explicit fields, such as `funding_threshold` and `participant_count`, or equivalent named fields at the locations declared by `schema_url`. `M` is the minimum number of required funders, and `N` is the total declared participant count. Clients MUST be able to read both values before the escrow can become active. ### Release Decisions @@ -127,6 +148,22 @@ The `funding_model` field declares how many participants fund a single escrow in - `application_signed_result` — release or refund authorized by a signed result from the originating application - `threshold_participant_signatures` — release or refund authorized by a threshold of participant signatures +Each release-decision format MUST be defined by the referenced schema with a verifiable standalone contract: + +- `application_signed_result` + - the schema MUST define the signed payload, the signer identity, the signature encoding, the escrow binding, the result binding, and replay protection + - the signed payload MUST include a stable escrow identifier and a result identifier or hash so the result cannot be replayed against another escrow instance + - the signer identity MUST be a named application identity that the schema or service advertises as valid for this decision type + - signatures MUST be serializable in a named encoding such as `base64` or `hex`, and the encoding MUST be declared by the schema + - replay protection MUST use a unique nonce, issuance timestamp, sequence number, or equivalent binding that is checked against the escrow instance +- `oracle_signature` + - the schema MUST name the referenced oracle explicitly and MUST bind the decision to that oracle's public key, identifier, or equivalent stable oracle reference + - anonymous oracle signatures are not sufficient +- `threshold_participant_signatures` + - the schema MUST define the threshold value and the location of the participant signatures + - the participant signature set MUST identify each signer and MUST bind each signature to the same escrow-scoped payload + - the threshold MUST be satisfied by distinct bound participants + `release_rules.release_trigger` states the public condition a specific escrow subtype requires before release; `service.release_decisions` states the generic decision formats the service accepts to satisfy such a trigger. Swap-specific triggers such as `counterparty_fiat_payment_confirmed` remain valid for Pontmore swap flows. For standalone non-swap use, `release_decisions` is the generic vocabulary an application relies on. ### Public/Private Boundary @@ -534,7 +571,7 @@ Every agent profile should declare: That declared escrow must be usable without out-of-band negotiation at swap time. -For standalone (non-swap) use, an application SHOULD select a descriptor whose `service` block is present and whose advertised `operations`, `funding_model`, and `release_decisions` match the application's needs. A descriptor without `service` MUST NOT be treated as standalone-sufficient. +For standalone (non-swap) use, an application SHOULD select a descriptor whose `service` block is present and whose advertised `transport`, `endpoint`, `auth`, `interface`, `operations`, `funding_model`, `release_decisions`, and `schema_url` all match the application's supported capabilities and trust constraints. A descriptor without `service` MUST NOT be treated as standalone-sufficient. ## Open Questions @@ -544,7 +581,4 @@ Additional escrow mechanisms beyond `lightning_hold_invoice`, `custodial_escrow` Open questions for the service interface: -- whether the service interface should remain in PIP-01 or split into a dedicated escrow service interface PIP as the operation set and transport matrix grow -- whether transports beyond HTTPS should be canonically defined, or left to descriptor extensions -- whether Nostr HTTP Auth (NIP-98) should be the single recommended authentication method, or one of several equally valid methods -- the minimum generic release-decision vocabulary beyond `mutual_consent`, `operator_decision`, `oracle_signature`, `application_signed_result`, and `threshold_participant_signatures` +- additional escrow mechanisms beyond `lightning_hold_invoice`, `custodial_escrow`, and `cashu_escrow` may still need their own canonical subtype-specific schemas diff --git a/README.md b/README.md index 73d57b3..a0736a3 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ Read `PIP-00` through `PIP-03` first. - [PIP-01-escrow-descriptor.md](./PIP-01-escrow-descriptor.md) - Implementation: `Required` - - public escrow declaration and service interface referenced by agents and swaps + - public escrow declaration and standalone-capable service interface referenced by agents and swaps; the service interface is optional during swap execution - [PIP-02-swap-state-machine.md](./PIP-02-swap-state-machine.md) - Implementation: `Required` From f4f2c11068580f64578007b5af0a17fbb35c46df Mon Sep 17 00:00:00 2001 From: Denver <135147764+Denver-1st@users.noreply.github.com> Date: Mon, 27 Jul 2026 15:25:00 +0300 Subject: [PATCH 3/8] defined escrow service invocation --- PIP-01-escrow-descriptor.md | 37 ++++++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 13 deletions(-) diff --git a/PIP-01-escrow-descriptor.md b/PIP-01-escrow-descriptor.md index 38915a1..9899525 100644 --- a/PIP-01-escrow-descriptor.md +++ b/PIP-01-escrow-descriptor.md @@ -78,13 +78,16 @@ When `service` is present, it MUST include: - `auth` - non-empty array of supported authentication methods; see Authentication - `operations` - - non-empty array of supported escrow instance operations; see Operations + - MUST include `create`, `funding_instructions`, `fund_status`, `release`, `refund`, and `cancel` for standalone service use - `funding_model` - multi-party funding model; see Funding Model - `release_decisions` - non-empty array of accepted release-decision formats; see Release Decisions - `schema_url` - mandatory machine-readable schema URL for the normative wire contract; see Wire Contract + - MUST use `https://` + - MUST resolve only to an immutable or versioned schema artifact, preferably with a digest or immutable URL + - MUST NOT point to a private, loopback, link-local, multicast, or otherwise disallowed network destination ### Authentication @@ -92,7 +95,9 @@ For HTTPS transports, the recommended authentication method is `nostr_http_auth` - a participant authenticates by signing each HTTP request with its Nostr key, producing a NIP-98 authorization header - identity is the participant's Nostr pubkey; the descriptor and escrow operator MUST NOT require published bearer secrets for public protocol operations -- at `create`, the requesting participant supplies the set of participant Nostr pubkeys bound to the escrow instance; the operator binds subsequent `fund_status`, `release`, `refund`, and `cancel` operations to those pubkeys +- `create` uses an explicitly authorized incremental enrollment flow: the initial authenticated request establishes the escrow instance, and any later participant join MUST use a service-issued invitation or enrollment token plus the joining participant's authenticated request +- the idempotency key is for request deduplication only and MUST NOT authorize participant joining or bind any additional pubkeys +- the operator binds subsequent `fund_status`, `release`, `refund`, and `cancel` operations to the pubkeys that have been explicitly enrolled for that escrow instance - each participant signs its own operations; threshold release decisions require signatures from the declared threshold of bound participants - operators MAY require additional private operator-layer authorization for back-office actions, but such authorization is an operator overlay and MUST NOT be advertised as a public descriptor field - authentication identifiers MUST be registered methods; `nostr_http_auth` is the canonical HTTP method, and any extension method MUST be defined by `schema_url` with a mandatory interface schema and versioned identifier @@ -108,12 +113,14 @@ For HTTPS transports, the recommended authentication method is `nostr_http_auth` - idempotency rules, including the shared correlation value used by repeated `create` requests - how `funding_model` and `release_decisions` values map to wire payload fields and validation rules - transport-specific endpoint resolution rules for every advertised transport +- schema fetch requirements, including HTTPS-only retrieval, redirect limits, bounded response size, allowed content types, and rejection of private or otherwise disallowed network destinations ### Operations The canonical escrow instance operation vocabulary is: -- `create` — open a new escrow instance and bind its participant pubkeys. For shared-instance flows, the first participant call establishes the escrow instance and the second participant call uses the same shared invitation, client token, or idempotency key to bind its participant pubkey to that existing instance. Repeated calls with the same correlation value are idempotent and MUST NOT create a second escrow instance. +- `create` — open a new escrow instance and bind its participant pubkeys. The initial authenticated request establishes the escrow instance. Any later participant join MUST use a service-issued invitation or enrollment token plus the joining participant's authenticated request. Repeated calls with the same idempotency key are idempotent and MUST NOT create a second escrow instance, but the idempotency key MUST NOT authorize participant joining or bind any additional pubkeys. +- `funding_instructions` — retrieve the funding instructions needed to fund the escrow instance - `fund_status` — observe the funding state of a participant's side - `release` — request release of the escrowed amount to the winner or payee - `refund` — request refund of the escrowed amount to its funder @@ -122,11 +129,12 @@ The canonical escrow instance operation vocabulary is: Example shared-instance create flow: ```text -participant A -> create(invitation=abc123) -participant B -> create(invitation=abc123) +participant A -> create() +service -> invitation=enroll-abc123 +participant B -> create(invitation=enroll-abc123) ``` -The shared invitation, client token, or idempotency key is the correlation value that binds both participants to the same escrow instance. +The shared invitation or enrollment token is the authorization value for participant joining. The idempotency key, if present, is only the correlation value used to deduplicate retries. Operations not in this vocabulary MAY be advertised but are non-canonical and MUST be documented by the operator's referenced schema. @@ -136,7 +144,7 @@ The `funding_model` field declares how many participants fund a single escrow in - `single_funder` — one participant funds the escrow - `two_party` — two participants each fund a side of the same escrow instance -- `n_of_m` — the referenced schema MUST expose both the activation threshold and the declared participant count using explicit fields, such as `funding_threshold` and `participant_count`, or equivalent named fields at the locations declared by `schema_url`. `M` is the minimum number of required funders, and `N` is the total declared participant count. Clients MUST be able to read both values before the escrow can become active. +- `n_of_m` — the referenced schema MUST expose two normative fields: `funding_threshold` and `participant_count`. `funding_threshold` is `M`, the minimum number of required funders. `participant_count` is `N`, the total declared participants. Clients MUST use these values consistently when validating funding and MUST NOT treat the escrow as active until both values are known and satisfy the declared funding condition. ### Release Decisions @@ -193,7 +201,7 @@ A standalone dice game discovers a published escrow descriptor and operates an e }, "release_rules": { "release_trigger": "application_signed_result", - "refund_trigger": "timeout_or_mutual_consent" + "refund_trigger": "timeout_requires_mutual_consent" }, "dispute_rules": { "policy": "operator_resolved" @@ -217,7 +225,7 @@ A standalone dice game discovers a published escrow descriptor and operates an e "endpoint": "https://escrow.example.com/pontmore/v1", "schema_url": "https://escrow.example.com/pontmore/v1/openapi.json", "auth": ["nostr_http_auth"], - "operations": ["create", "fund_status", "release", "refund", "cancel"], + "operations": ["create", "funding_instructions", "fund_status", "release", "refund", "cancel"], "funding_model": "two_party", "release_decisions": ["application_signed_result", "mutual_consent", "operator_decision"] }, @@ -228,10 +236,13 @@ A standalone dice game discovers a published escrow descriptor and operates an e The flow validates the descriptor model: 1. the application discovers the descriptor and reads `service` -2. both participants call `create` over HTTPS, authenticated with `nostr_http_auth`, binding both participant pubkeys to one escrow instance with `funding_model` `two_party` -3. each participant funds its side; the application reads `fund_status` until both sides are confirmed -4. the application commits a verifiable result (the die roll) and submits an `application_signed_result` release decision -5. the escrow releases both locked amounts to the winner via `release`, or refunds via `refund` if the result is unresolved at timeout +2. participant A calls `create` over HTTPS, authenticated with `nostr_http_auth`, and establishes the escrow instance +3. the service issues an invitation or enrollment token for participant B +4. participant B calls `create` with that invitation or enrollment token; the idempotency key, if used, only deduplicates the request +5. the application retrieves `funding_instructions` for the established escrow +6. each participant funds its side; the application reads `fund_status` until both sides are confirmed +7. the application commits a verifiable result (the die roll) and submits an `application_signed_result` release decision +8. if the result is unresolved at timeout, the escrow refunds only after a listed explicit decision such as `mutual_consent` or `operator_decision`; the timeout itself does not authorize refund ## Network Declaration From ef2ff8f4df0dfb6880a66715b180aac8e6f7ab76 Mon Sep 17 00:00:00 2001 From: Denver <135147764+Denver-1st@users.noreply.github.com> Date: Mon, 27 Jul 2026 22:50:13 +0300 Subject: [PATCH 4/8] .. --- PIP-01-escrow-descriptor.md | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/PIP-01-escrow-descriptor.md b/PIP-01-escrow-descriptor.md index 9899525..f6b2efb 100644 --- a/PIP-01-escrow-descriptor.md +++ b/PIP-01-escrow-descriptor.md @@ -588,8 +588,4 @@ For standalone (non-swap) use, an application SHOULD select a descriptor whose ` Additional escrow mechanisms beyond `lightning_hold_invoice`, `custodial_escrow`, and `cashu_escrow` may still need their own canonical subtype-specific schemas. -`cashu_escrow` introduces a dependency on a specific Cashu mint. Mint trust, mint selection, and mint failure modes are implementation assumptions rather than Pontmore protocol state. [Nimdolf](https://github.com/cashubtc/nuts/pull/390) is a possible future direction for mint liveness failover, but this proposal does not bind to it. - -Open questions for the service interface: - -- additional escrow mechanisms beyond `lightning_hold_invoice`, `custodial_escrow`, and `cashu_escrow` may still need their own canonical subtype-specific schemas +`cashu_escrow` introduces a dependency on a specific Cashu mint. Mint trust, mint selection, and mint failure modes are implementation assumptions rather than Pontmore protocol state. [Nimdolf](https://github.com/cashubtc/nuts/pull/390) is a possible future direction for mint liveness failover, but this proposal does not bind to it. \ No newline at end of file From 4eece595c49aa6c68043543ff858ea376256a5b7 Mon Sep 17 00:00:00 2001 From: Denver Mtange <135147764+mk-Denver@users.noreply.github.com> Date: Sat, 1 Aug 2026 22:25:26 +0300 Subject: [PATCH 5/8] Expand open questions in escrow descriptor Added open questions regarding service invocation rules, transport strategies, HTTP authentication, and release-decision vocabulary for escrow services. --- PIP-01-escrow-descriptor.md | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/PIP-01-escrow-descriptor.md b/PIP-01-escrow-descriptor.md index f6b2efb..5a1d6d7 100644 --- a/PIP-01-escrow-descriptor.md +++ b/PIP-01-escrow-descriptor.md @@ -586,6 +586,22 @@ For standalone (non-swap) use, an application SHOULD select a descriptor whose ` ## Open Questions +1. **Placement of service invocation rules** + - Should standalone escrow service invocation remain specified directly in `PIP-01`, or should `PIP-01` only reference a dedicated escrow service interface PIP? + - If split, which requirements remain normative in `PIP-01` (descriptor discovery/selection) versus the service-interface PIP (wire contract/auth/operation semantics)? + +2. **Canonical transport strategy** + - Should `https` remain the first canonical transport for `service.transport`, or should the descriptor normatively support multiple first-class transports from the beginning? + - If multiple transports are canonical, what endpoint-resolution and parity requirements are mandatory across transports? + +3. **HTTP authentication recommendation** + - For HTTP transports, should `nostr_http_auth` (NIP-98) remain the recommended baseline authentication method? + - What minimum interoperability and security requirements should apply to any alternative HTTP auth method advertised by `schema_url`? + +4. **Minimum generic release-decision vocabulary** + - Beyond swap-specific release triggers, what is the smallest required generic `service.release_decisions` set for interoperable standalone escrow services? + - Is the current baseline (`mutual_consent`, `operator_decision`, `oracle_signature`, `application_signed_result`, `threshold_participant_signatures`) appropriately minimal, or should it be reduced/expanded? + Additional escrow mechanisms beyond `lightning_hold_invoice`, `custodial_escrow`, and `cashu_escrow` may still need their own canonical subtype-specific schemas. -`cashu_escrow` introduces a dependency on a specific Cashu mint. Mint trust, mint selection, and mint failure modes are implementation assumptions rather than Pontmore protocol state. [Nimdolf](https://github.com/cashubtc/nuts/pull/390) is a possible future direction for mint liveness failover, but this proposal does not bind to it. \ No newline at end of file +`cashu_escrow` introduces a dependency on a specific Cashu mint. Mint trust, mint selection, and mint failure modes are implementation assumptions rather than Pontmore protocol state. [Nimdolf](https://github.com/cashubtc/nuts/pull/390) is a possible future direction for mint liveness failover, but this proposal does not bind to it. From eb559dbf057e9b4e093afe5771dc375a5c18d531 Mon Sep 17 00:00:00 2001 From: Denver Mtange <135147764+mk-Denver@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:13:30 +0300 Subject: [PATCH 6/8] removed example from the protocol specification --- PIP-01-escrow-descriptor.md | 57 ------------------------------------- 1 file changed, 57 deletions(-) diff --git a/PIP-01-escrow-descriptor.md b/PIP-01-escrow-descriptor.md index 5a1d6d7..d029675 100644 --- a/PIP-01-escrow-descriptor.md +++ b/PIP-01-escrow-descriptor.md @@ -187,63 +187,6 @@ The `service` block advertises only the public service interface. The following These are operator-layer implementation details. Their absence is what allows the same descriptor to be published openly without exposing operator internals. -### Example: Two-Party Dice Game - -A standalone dice game discovers a published escrow descriptor and operates an escrow instance without a Pontmore swap: - -```json -{ - "version": 1, - "escrow_type": "custodial_escrow", - "networks": ["lightning"], - "funding_rules": { - "required_confirmation": "invoice_paid" - }, - "release_rules": { - "release_trigger": "application_signed_result", - "refund_trigger": "timeout_requires_mutual_consent" - }, - "dispute_rules": { - "policy": "operator_resolved" - }, - "reference_format": "bolt11_or_custodial_escrow_reference", - "custody_authority": "escrow_operator", - "release_authority": "escrow_operator", - "refund_authority": "escrow_operator", - "implementations": [ - { - "network": "lightning", - "invoice_asset": "BTC", - "invoice_currency": "sats", - "invoice_amount_rule": "exact", - "payout_network": "lightning" - } - ], - "service": { - "transport": ["https"], - "interface": "pontmore_escrow_http_v1", - "endpoint": "https://escrow.example.com/pontmore/v1", - "schema_url": "https://escrow.example.com/pontmore/v1/openapi.json", - "auth": ["nostr_http_auth"], - "operations": ["create", "funding_instructions", "fund_status", "release", "refund", "cancel"], - "funding_model": "two_party", - "release_decisions": ["application_signed_result", "mutual_consent", "operator_decision"] - }, - "updated_at": 1775559028 -} -``` - -The flow validates the descriptor model: - -1. the application discovers the descriptor and reads `service` -2. participant A calls `create` over HTTPS, authenticated with `nostr_http_auth`, and establishes the escrow instance -3. the service issues an invitation or enrollment token for participant B -4. participant B calls `create` with that invitation or enrollment token; the idempotency key, if used, only deduplicates the request -5. the application retrieves `funding_instructions` for the established escrow -6. each participant funds its side; the application reads `fund_status` until both sides are confirmed -7. the application commits a verifiable result (the die roll) and submits an `application_signed_result` release decision -8. if the result is unresolved at timeout, the escrow refunds only after a listed explicit decision such as `mutual_consent` or `operator_decision`; the timeout itself does not authorize refund - ## Network Declaration An escrow descriptor MUST declare every settlement or invoice network supported by the escrow configuration. From 0dc49d5ba38e50e96e5da34b8f1f4d73b4b6ae55 Mon Sep 17 00:00:00 2001 From: Denver Mtange <135147764+mk-Denver@users.noreply.github.com> Date: Wed, 5 Aug 2026 15:59:46 +0300 Subject: [PATCH 7/8] Close escrow loopholes and define operator-truthfulness surface in PIP-01/PIP-03 PIP-01: - Bind enrollment tokens to the exact joining participant pubkey (Sybil-join fix) - Require escrow-identifier commitment in oracle_signature and threshold_participant_signatures (cross-instance replay fix) - Define explicit Escrow Instance State Machine with canonical states and terminal-state invariants - Add split operation and split_decision release-decision format - Bound funding phase with funding_timeout and partial-funding cancel/refund - Require a non-mutual-consent fallback for refund_trigger (deadlock fix) - Document per-subtype implementation risks and standalone suitability - Add open questions for Nostr transport, streaming/tranche funding, and operator accountability PIP-03: - Require timeout fallback resolution to avoid mutual-consent deadlock --- PIP-01-escrow-descriptor.md | 104 +++++++++++++++++++++++++++++++++--- PIP-03-dispute-policy.md | 4 ++ 2 files changed, 102 insertions(+), 6 deletions(-) diff --git a/PIP-01-escrow-descriptor.md b/PIP-01-escrow-descriptor.md index d029675..6e60b6c 100644 --- a/PIP-01-escrow-descriptor.md +++ b/PIP-01-escrow-descriptor.md @@ -78,7 +78,7 @@ When `service` is present, it MUST include: - `auth` - non-empty array of supported authentication methods; see Authentication - `operations` - - MUST include `create`, `funding_instructions`, `fund_status`, `release`, `refund`, and `cancel` for standalone service use + - MUST include `create`, `funding_instructions`, `fund_status`, `release`, `refund`, `split`, and `cancel` for standalone service use - `funding_model` - multi-party funding model; see Funding Model - `release_decisions` @@ -96,8 +96,9 @@ For HTTPS transports, the recommended authentication method is `nostr_http_auth` - a participant authenticates by signing each HTTP request with its Nostr key, producing a NIP-98 authorization header - identity is the participant's Nostr pubkey; the descriptor and escrow operator MUST NOT require published bearer secrets for public protocol operations - `create` uses an explicitly authorized incremental enrollment flow: the initial authenticated request establishes the escrow instance, and any later participant join MUST use a service-issued invitation or enrollment token plus the joining participant's authenticated request +- an enrollment token MUST bind to the exact joining participant pubkey it is intended for; the token is single-use and single-party, and the service MUST reject a `create` request whose authenticated signer does not match the pubkey the token was issued to. This prevents a token intercepted in transit from being consumed by an unrelated Nostr identity (Sybil join). When the originating participant knows the counterparty pubkey ahead of time, the service SHOULD accept it as a bound parameter in the initial `create` request and mint the enrollment token already scoped to that pubkey; when the counterparty is not known ahead of time, the originating participant MUST still constrain the token to a single redemption and the service MUST NOT accept a bound pubkey that differs from the authenticated signer of the redeeming request - the idempotency key is for request deduplication only and MUST NOT authorize participant joining or bind any additional pubkeys -- the operator binds subsequent `fund_status`, `release`, `refund`, and `cancel` operations to the pubkeys that have been explicitly enrolled for that escrow instance +- the operator binds subsequent `fund_status`, `release`, `refund`, `split`, and `cancel` operations to the pubkeys that have been explicitly enrolled for that escrow instance - each participant signs its own operations; threshold release decisions require signatures from the declared threshold of bound participants - operators MAY require additional private operator-layer authorization for back-office actions, but such authorization is an operator overlay and MUST NOT be advertised as a public descriptor field - authentication identifiers MUST be registered methods; `nostr_http_auth` is the canonical HTTP method, and any extension method MUST be defined by `schema_url` with a mandatory interface schema and versioned identifier @@ -106,10 +107,10 @@ For HTTPS transports, the recommended authentication method is `nostr_http_auth` `schema_url` MUST point to a schema that normatively defines the public service contract, either by embedding the contract itself or by referencing a complete protocol-native or OpenAPI schema. That schema MUST define: -- HTTP methods and paths for `create`, `fund_status`, `release`, `refund`, `cancel`, and funding-instruction retrieval +- HTTP methods and paths for `create`, `fund_status`, `release`, `refund`, `split`, `cancel`, and funding-instruction retrieval - request and response schemas for each listed operation - error formats and HTTP status codes -- escrow state transitions and terminal states +- escrow state transitions and terminal states, following the Escrow Instance State Machine section below - idempotency rules, including the shared correlation value used by repeated `create` requests - how `funding_model` and `release_decisions` values map to wire payload fields and validation rules - transport-specific endpoint resolution rules for every advertised transport @@ -124,6 +125,7 @@ The canonical escrow instance operation vocabulary is: - `fund_status` — observe the funding state of a participant's side - `release` — request release of the escrowed amount to the winner or payee - `refund` — request refund of the escrowed amount to its funder +- `split` — request a partial release of the escrowed amount across two or more declared participants in declared proportions. A `split` request MUST carry a `split_decision` release decision (see Release Decisions) authorized by one of the other registered formats; the operation is not valid without that decision payload - `cancel` — request cancellation of an unfunded or unresolved escrow instance Example shared-instance create flow: @@ -138,13 +140,52 @@ The shared invitation or enrollment token is the authorization value for partici Operations not in this vocabulary MAY be advertised but are non-canonical and MUST be documented by the operator's referenced schema. +### Escrow Instance State Machine + +A standalone escrow instance is a small state machine. The operator's `schema_url` MUST normatively define the states, the allowed transitions, and which operations are valid in each state. This removes cross-operator ambiguity about whether, for example, `fund_status` may be called after `cancel`, or whether `cancel` is permitted while a `release` is pending operator approval. + +The canonical states are: + +- `created` — the escrow instance exists with its bound participant pubkeys; no side is funded yet +- `partially_funded` — at least one declared participant has funded, but the funding condition is not yet satisfied +- `active` — the funding condition is satisfied (for example the threshold or both sides are funded); release, refund, and split are now reachable +- `release_pending` — a `release` or `split` request has been submitted and is awaiting the operator's decision or an authorizing signature +- `released` — terminal; the escrowed amount has been released (in whole or, after a `split`, in declared proportions) +- `refunded` — terminal; the escrowed amount has been refunded to its funder(s) +- `canceled` — terminal; the escrow instance was canceled before becoming active, and any partially funded amounts were refunded + +Canonical transition rules: + +- `create` transitions an instance from nonexistent to `created` +- funding confirmation transitions `created` to `partially_funded`, and `partially_funded` to `active` once the funding condition is satisfied +- `cancel` is valid in `created`, `partially_funded`, and `release_pending` only when the relevant authority and the refund/funding-phase rules permit it; `cancel` in `partially_funded` MUST refund any already-funded participant side (see Funding Model) +- `release`, `refund`, and `split` are valid only in `active` or `release_pending` +- `fund_status` and `funding_instructions` are valid in every non-terminal state +- once an instance is in a terminal state (`released`, `refunded`, `canceled`), the operator MUST reject every mutating operation with a terminal-state error; read operations (`fund_status`) remain valid +- at most one terminal transition may succeed; the operator MUST reject a `release`, `refund`, or `split` that races a competing terminal transition and MUST surface the already-terminal state + +The operator MAY add non-canonical intermediate states (for example an explicit `disputed` state) but MUST document them in `schema_url` and MUST NOT relax the terminal-state invariants above. + ### Funding Model The `funding_model` field declares how many participants fund a single escrow instance: - `single_funder` — one participant funds the escrow - `two_party` — two participants each fund a side of the same escrow instance -- `n_of_m` — the referenced schema MUST expose two normative fields: `funding_threshold` and `participant_count`. `funding_threshold` is `M`, the minimum number of required funders. `participant_count` is `N`, the total declared participants. Clients MUST use these values consistently when validating funding and MUST NOT treat the escrow as active until both values are known and satisfy the declared funding condition. +- `m_of_n` — the referenced schema MUST expose two normative fields: `funding_threshold` and `participant_count`. `funding_threshold` is `M`, the minimum number of required funders. `participant_count` is `N`, the total declared participants. Clients MUST use these values consistently when validating funding and MUST NOT treat the escrow as active until both values are known and satisfy the declared funding condition. + +### Funding-Phase Timeout and Partial Refund + +A funding model with more than one declared funder (`two_party` or `m_of_n`) creates a partial-funding risk: one participant funds a side and another abandons the process, locking the first participant's capital indefinitely. To prevent griefing, the descriptor and the referenced schema MUST bound the funding phase. + +- `funding_rules` SHOULD declare a `funding_timeout` (or reference a named timeout class) that bounds how long an instance may remain in `created` or `partially_funded` before it becomes cancelable +- when the `funding_timeout` elapses without the funding condition being satisfied, any bound participant or the operator MAY call `cancel` on the instance +- a `cancel` issued while the instance is `partially_funded` MUST refund every already-funded participant side back to its funder before transitioning the instance to `canceled`; partial funding MUST NOT be retained by the operator as a default +- the operator MUST NOT treat the absence of all-but-one funder as implicit consent to release; an under-funded instance can only move to `active` via the declared funding condition, or to `canceled` via `cancel` + +### Streaming and Tranche Funding + +The funding rules in this version assume all-or-nothing locks. Milestone or tranche-based funding (releasing the escrowed amount in installments against partial results) is not defined by this version of PIP-01. See Open Questions. ### Release Decisions @@ -155,6 +196,7 @@ The `funding_model` field declares how many participants fund a single escrow in - `oracle_signature` — release or refund authorized by a signature from a referenced oracle - `application_signed_result` — release or refund authorized by a signed result from the originating application - `threshold_participant_signatures` — release or refund authorized by a threshold of participant signatures +- `split_decision` — partial release authorized by splitting the escrowed amount across two or more declared participants in declared proportions; a `split_decision` is itself authorized by one of the other registered formats above and carried as a payload of that format Each release-decision format MUST be defined by the referenced schema with a verifiable standalone contract: @@ -167,13 +209,28 @@ Each release-decision format MUST be defined by the referenced schema with a ver - `oracle_signature` - the schema MUST name the referenced oracle explicitly and MUST bind the decision to that oracle's public key, identifier, or equivalent stable oracle reference - anonymous oracle signatures are not sufficient + - the signed payload MUST commit to the stable escrow identifier of the escrow instance it authorizes; an oracle signature is not valid for any escrow instance other than the one named in its signed payload. This prevents a signature minted for one escrow from being replayed to release a second, otherwise-identical escrow (for example two wagers on the same oracle event) - `threshold_participant_signatures` - the schema MUST define the threshold value and the location of the participant signatures - the participant signature set MUST identify each signer and MUST bind each signature to the same escrow-scoped payload + - the escrow-scoped payload MUST include the stable escrow identifier, so the participant signature set cannot be replayed against another escrow instance - the threshold MUST be satisfied by distinct bound participants +- `split_decision` + - the schema MUST define a partial-outcome payload that allocates the escrowed amount across two or more declared participants in declared proportions + - the schema MUST bind the split payload to the stable escrow identifier and to a result identifier or hash so the decision cannot be replayed against another escrow instance + - the schema MUST define which release-decision formats authorize a `split_decision` (for example `mutual_consent`, `operator_decision`, `oracle_signature`, or `application_signed_result`); a `split_decision` is a release decision, not an operation, and MUST be carried by one of the other registered formats + - the sum of declared proportions MUST equal the whole escrowed amount; the schema MUST reject a split payload whose proportions do not sum to the funded amount `release_rules.release_trigger` states the public condition a specific escrow subtype requires before release; `service.release_decisions` states the generic decision formats the service accepts to satisfy such a trigger. Swap-specific triggers such as `counterparty_fiat_payment_confirmed` remain valid for Pontmore swap flows. For standalone non-swap use, `release_decisions` is the generic vocabulary an application relies on. +### Refund-Trigger Fallback + +A `refund_trigger` that resolves only to `mutual_consent` (for example `timeout_requires_mutual_consent`) creates a deadlock when the participants are actively disputing and refuse consent: the escrow times out but neither party will sign the refund, so the funds are stuck unless the operator voluntarily intervenes. To avoid indefinite limbo: + +- a `refund_trigger` whose only resolution path is `mutual_consent` MUST declare a fallback resolution that does not require both participants' consent, and that fallback MUST be one of the operator's advertised `release_decisions` (for example `operator_decision`, `oracle_signature`, or `threshold_participant_signatures`) +- the descriptor's `dispute_rules.policy` SHOULD be consistent with that fallback; if `dispute_rules.policy` is `operator_resolved`, the fallback SHOULD be `operator_decision` +- a descriptor MUST NOT advertise a `refund_trigger` whose only reachable resolution is `mutual_consent` with no declared fallback; clients SHOULD treat such a descriptor as unsuitable for standalone use + ### Public/Private Boundary The `service` block advertises only the public service interface. The following MUST NOT appear in the descriptor: @@ -527,6 +584,30 @@ That declared escrow must be usable without out-of-band negotiation at swap time For standalone (non-swap) use, an application SHOULD select a descriptor whose `service` block is present and whose advertised `transport`, `endpoint`, `auth`, `interface`, `operations`, `funding_model`, `release_decisions`, and `schema_url` all match the application's supported capabilities and trust constraints. A descriptor without `service` MUST NOT be treated as standalone-sufficient. +## Implementation Risks and Standalone Suitability + +Each canonical escrow subtype carries infrastructure and execution risks that a standalone application must weigh before selecting a descriptor. The protocol surfaces these risks as descriptor facts where possible; where they cannot be made into protocol state, they are documented here as implementation assumptions. + +### `lightning_hold_invoice` + +- **Routing-node liquidity penalty.** A Lightning hold invoice locks liquidity across the entire routing path for as long as the invoice is held. If a dispute takes hours or days to resolve, intermediate routing nodes will typically force-close channels to reclaim their liquidity. +- **Standalone suitability.** This subtype is suited to rapid, near-instant conditional payments and is poorly suited to long-running standalone escrows (for example freelance contracts or milestone-based agreements). A descriptor SHOULD document its expected dispute-resolution window, and applications SHOULD avoid `lightning_hold_invoice` for escrows whose resolution is not expected within the invoice's hold lifetime. + +### `custodial_escrow` + +- **Full counterparty risk.** The `escrow_operator` is declared as `custody_authority`, `release_authority`, and `refund_authority`. In a standalone context this is, from a custody standpoint, indistinguishable from a centralized service: the protocol standardizes the API to the operator but provides no cryptographic guarantee that the operator will not misappropriate custodied funds. +- **Standalone suitability.** Selection of a `custodial_escrow` is a trust decision. Applications SHOULD prefer descriptors that reduce operator sole-authority (for example by advertising `threshold_participant_signatures` or `oracle_signature` release decisions, or by referencing an external arbiter via `dispute_rules.policy`) over descriptors that rely solely on `operator_decision`. Operator accountability references (proof of reserve, attestations, collateral) are an open question; see Open Questions. + +### `cashu_escrow` + +- **Mint liveness and reserve dependency.** Cashu escrow depends on the declared `mint_url` remaining online and backing its reserves for the duration of the P2PK lock. If the mint goes offline or fails its reserves before the locktime expires, the locked tokens can become unredeemable regardless of the operator's release/refund authority; the user's refund pubkey is only useful if the mint honors redemptions. +- **Standalone suitability.** Mint trust, mint selection, and mint failure modes are implementation assumptions rather than Pontmore protocol state (see Open Questions). Applications SHOULD treat the mint as an additional trusted party and SHOULD NOT select a `cashu_escrow` whose `mint_url` they do not independently trust. + +### Application-signed-result dependency + +- **Application-as-oracle risk.** When `release_rules.release_trigger` is satisfied by `application_signed_result`, the escrow's release depends entirely on the originating application's key security. If the application's signing backend is compromised, an attacker can mint a valid signed result and release the escrow. +- **Standalone suitability.** Applications SHOULD scope the application signing key to escrow release only, SHOULD require replay protection (escrow identifier and result binding) per Release Decisions, and SHOULD consider pairing `application_signed_result` with a `threshold_participant_signatures` or `oracle_signature` release decision for higher-value escrows. + ## Open Questions 1. **Placement of service invocation rules** @@ -543,7 +624,18 @@ For standalone (non-swap) use, an application SHOULD select a descriptor whose ` 4. **Minimum generic release-decision vocabulary** - Beyond swap-specific release triggers, what is the smallest required generic `service.release_decisions` set for interoperable standalone escrow services? - - Is the current baseline (`mutual_consent`, `operator_decision`, `oracle_signature`, `application_signed_result`, `threshold_participant_signatures`) appropriately minimal, or should it be reduced/expanded? + - Is the current baseline (`mutual_consent`, `operator_decision`, `oracle_signature`, `application_signed_result`, `threshold_participant_signatures`, `split_decision`) appropriately minimal, or should it be reduced/expanded? + - `split_decision` has been added so that arbiter-resolved standalone escrows can produce partial outcomes (for example 50/50 splits) rather than only whole-release or whole-refund. Should `split_decision` be required for any descriptor advertising `operator_decision`, since an arbiter that can resolve a dispute may need to split? + +5. **Nostr as a first-class transport** + - This version of PIP-01 fixes `service.transport` to `https` only. Since participants already authenticate with Nostr keys (NIP-98), should `nostr` (relay-based remote RPC, e.g. NIP-46-style) be added as a first-class canonical transport alongside `https`? + - If added, what operation/event mapping and endpoint-resolution rules keep `https` and `nostr` transports at parity? + +6. **Streaming and tranche-based funding** + - The funding rules in this version assume all-or-nothing locks. Should a future PIP define milestone- or tranche-based funding (releasing installments against partial results) for standalone escrows such as freelance contracts? If so, does this belong in PIP-01's `funding_model`, or in a separate escrow-lifecycle PIP? + +7. **Operator accountability references** + - Should `custodial_escrow` descriptors be REQUIRED (rather than merely able) to advertise a public accountability reference (proof of reserve, attestation, or collateral) as a descriptor field, given full counterparty risk? If so, what public reference format stays inside the PIP-01 public/private boundary? Additional escrow mechanisms beyond `lightning_hold_invoice`, `custodial_escrow`, and `cashu_escrow` may still need their own canonical subtype-specific schemas. diff --git a/PIP-03-dispute-policy.md b/PIP-03-dispute-policy.md index 4469cc1..5af9010 100644 --- a/PIP-03-dispute-policy.md +++ b/PIP-03-dispute-policy.md @@ -58,6 +58,10 @@ An operator may resolve disputes by: - cancelling and refunding - escalating to manual review +### Timeout Fallback + +A timeout class (for example `resolution timeout` or an escrow's `refund_trigger` such as `timeout_requires_mutual_consent`) MUST NOT leave the escrow in a permanent deadlock where neither participant will consent and no other resolution path is defined. When a timeout elapses, the operator MUST resolve the swap using one of the resolution modes above; a `mutual_consent`-only path with no fallback is not a valid terminal policy. The escrow descriptor referenced by the swap MUST declare a non-`mutual_consent` fallback resolution consistent with this rule (see the Refund-Trigger Fallback section of [PIP-01-escrow-descriptor.md](./PIP-01-escrow-descriptor.md)). + ## Public-Protocol Boundary ### Public From 3bcd9ea9934d01f46889cda4518ea7f1c9627a84 Mon Sep 17 00:00:00 2001 From: Denver Mtange <135147764+mk-Denver@users.noreply.github.com> Date: Wed, 5 Aug 2026 16:24:44 +0300 Subject: [PATCH 8/8] Clarify escrow descriptor and dispute timeout rules --- PIP-01-escrow-descriptor.md | 16 ++++++++++------ PIP-03-dispute-policy.md | 10 +++++++++- 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/PIP-01-escrow-descriptor.md b/PIP-01-escrow-descriptor.md index 6e60b6c..f7f2004 100644 --- a/PIP-01-escrow-descriptor.md +++ b/PIP-01-escrow-descriptor.md @@ -78,7 +78,7 @@ When `service` is present, it MUST include: - `auth` - non-empty array of supported authentication methods; see Authentication - `operations` - - MUST include `create`, `funding_instructions`, `fund_status`, `release`, `refund`, `split`, and `cancel` for standalone service use + - MUST include `create`, `funding_instructions`, `fund_status`, `release`, `refund`, and `cancel` for standalone service use; `split` MUST be included only when the descriptor also advertises `split_decision` in `release_decisions` - `funding_model` - multi-party funding model; see Funding Model - `release_decisions` @@ -112,6 +112,7 @@ For HTTPS transports, the recommended authentication method is `nostr_http_auth` - error formats and HTTP status codes - escrow state transitions and terminal states, following the Escrow Instance State Machine section below - idempotency rules, including the shared correlation value used by repeated `create` requests +- timeout-class bindings, including each declared timeout class and its non-`mutual_consent` fallback resolution - how `funding_model` and `release_decisions` values map to wire payload fields and validation rules - transport-specific endpoint resolution rules for every advertised transport - schema fetch requirements, including HTTPS-only retrieval, redirect limits, bounded response size, allowed content types, and rejection of private or otherwise disallowed network destinations @@ -125,7 +126,7 @@ The canonical escrow instance operation vocabulary is: - `fund_status` — observe the funding state of a participant's side - `release` — request release of the escrowed amount to the winner or payee - `refund` — request refund of the escrowed amount to its funder -- `split` — request a partial release of the escrowed amount across two or more declared participants in declared proportions. A `split` request MUST carry a `split_decision` release decision (see Release Decisions) authorized by one of the other registered formats; the operation is not valid without that decision payload +- `split` — request a partial release of the escrowed amount across two or more declared participants in declared proportions. When `split` is advertised, a `split` request MUST carry a `split_decision` release decision (see Release Decisions) authorized by one of the other registered formats; the operation is not valid without that decision payload - `cancel` — request cancellation of an unfunded or unresolved escrow instance Example shared-instance create flow: @@ -198,6 +199,8 @@ The funding rules in this version assume all-or-nothing locks. Milestone or tran - `threshold_participant_signatures` — release or refund authorized by a threshold of participant signatures - `split_decision` — partial release authorized by splitting the escrowed amount across two or more declared participants in declared proportions; a `split_decision` is itself authorized by one of the other registered formats above and carried as a payload of that format +A descriptor that advertises `split` in `operations` MUST also advertise `split_decision` in `release_decisions`; otherwise it MUST NOT advertise `split`. + Each release-decision format MUST be defined by the referenced schema with a verifiable standalone contract: - `application_signed_result` @@ -219,7 +222,7 @@ Each release-decision format MUST be defined by the referenced schema with a ver - the schema MUST define a partial-outcome payload that allocates the escrowed amount across two or more declared participants in declared proportions - the schema MUST bind the split payload to the stable escrow identifier and to a result identifier or hash so the decision cannot be replayed against another escrow instance - the schema MUST define which release-decision formats authorize a `split_decision` (for example `mutual_consent`, `operator_decision`, `oracle_signature`, or `application_signed_result`); a `split_decision` is a release decision, not an operation, and MUST be carried by one of the other registered formats - - the sum of declared proportions MUST equal the whole escrowed amount; the schema MUST reject a split payload whose proportions do not sum to the funded amount + - the sum of declared proportions MUST equal the whole escrowed amount minus fees if applicable; the schema MUST reject a split payload whose proportions do not sum to the funded amount `release_rules.release_trigger` states the public condition a specific escrow subtype requires before release; `service.release_decisions` states the generic decision formats the service accepts to satisfy such a trigger. Swap-specific triggers such as `counterparty_fiat_payment_confirmed` remain valid for Pontmore swap flows. For standalone non-swap use, `release_decisions` is the generic vocabulary an application relies on. @@ -227,9 +230,10 @@ Each release-decision format MUST be defined by the referenced schema with a ver A `refund_trigger` that resolves only to `mutual_consent` (for example `timeout_requires_mutual_consent`) creates a deadlock when the participants are actively disputing and refuse consent: the escrow times out but neither party will sign the refund, so the funds are stuck unless the operator voluntarily intervenes. To avoid indefinite limbo: +- for every timeout class advertised by the schema, including `payment proof timeout`, `payout timeout`, `resolution timeout`, and any `refund-trigger timeout` class, the schema MUST declare exactly one applicable non-`mutual_consent` fallback resolution, and standalone compatibility validation MUST use that explicit binding rather than inferring a fallback from the trigger name alone - a `refund_trigger` whose only resolution path is `mutual_consent` MUST declare a fallback resolution that does not require both participants' consent, and that fallback MUST be one of the operator's advertised `release_decisions` (for example `operator_decision`, `oracle_signature`, or `threshold_participant_signatures`) - the descriptor's `dispute_rules.policy` SHOULD be consistent with that fallback; if `dispute_rules.policy` is `operator_resolved`, the fallback SHOULD be `operator_decision` -- a descriptor MUST NOT advertise a `refund_trigger` whose only reachable resolution is `mutual_consent` with no declared fallback; clients SHOULD treat such a descriptor as unsuitable for standalone use +- a descriptor MUST NOT advertise a timeout class whose only reachable resolution is `mutual_consent` with no declared fallback; clients SHOULD treat such a descriptor as unsuitable for standalone use ### Public/Private Boundary @@ -628,8 +632,8 @@ Each canonical escrow subtype carries infrastructure and execution risks that a - `split_decision` has been added so that arbiter-resolved standalone escrows can produce partial outcomes (for example 50/50 splits) rather than only whole-release or whole-refund. Should `split_decision` be required for any descriptor advertising `operator_decision`, since an arbiter that can resolve a dispute may need to split? 5. **Nostr as a first-class transport** - - This version of PIP-01 fixes `service.transport` to `https` only. Since participants already authenticate with Nostr keys (NIP-98), should `nostr` (relay-based remote RPC, e.g. NIP-46-style) be added as a first-class canonical transport alongside `https`? - - If added, what operation/event mapping and endpoint-resolution rules keep `https` and `nostr` transports at parity? + - This version of PIP-01 requires `https` as the canonical first transport for `service.transport`, while still allowing additional advertised transports. Since participants already authenticate with Nostr keys (NIP-98), should `nostr` (relay-based remote RPC, e.g. NIP-46-style) be added as a first-class canonical transport alongside `https`? + - If added, what operation/event mapping and endpoint-resolution rules keep `https` and `nostr` transports at parity? 6. **Streaming and tranche-based funding** - The funding rules in this version assume all-or-nothing locks. Should a future PIP define milestone- or tranche-based funding (releasing installments against partial results) for standalone escrows such as freelance contracts? If so, does this belong in PIP-01's `funding_model`, or in a separate escrow-lifecycle PIP? diff --git a/PIP-03-dispute-policy.md b/PIP-03-dispute-policy.md index 5af9010..bc74326 100644 --- a/PIP-03-dispute-policy.md +++ b/PIP-03-dispute-policy.md @@ -35,6 +35,14 @@ This document defines how an operator should resolve disputes and timeouts for s - payment proof timeout - payout timeout - resolution timeout +- refund-trigger timeout + +Each timeout class MUST bind to exactly one non-`mutual_consent` fallback resolution: + +- payment proof timeout -> confirming the agent claim +- payout timeout -> confirming the customer claim +- resolution timeout -> escalating to manual review +- refund-trigger timeout -> the descriptor-declared fallback resolution from [PIP-01-escrow-descriptor.md](./PIP-01-escrow-descriptor.md) ## Evidence Categories @@ -60,7 +68,7 @@ An operator may resolve disputes by: ### Timeout Fallback -A timeout class (for example `resolution timeout` or an escrow's `refund_trigger` such as `timeout_requires_mutual_consent`) MUST NOT leave the escrow in a permanent deadlock where neither participant will consent and no other resolution path is defined. When a timeout elapses, the operator MUST resolve the swap using one of the resolution modes above; a `mutual_consent`-only path with no fallback is not a valid terminal policy. The escrow descriptor referenced by the swap MUST declare a non-`mutual_consent` fallback resolution consistent with this rule (see the Refund-Trigger Fallback section of [PIP-01-escrow-descriptor.md](./PIP-01-escrow-descriptor.md)). +A timeout class (for example `resolution timeout` or an escrow's `refund_trigger` such as `timeout_requires_mutual_consent`) MUST NOT leave the escrow in a permanent deadlock where neither participant will consent and no other resolution path is defined. When a timeout elapses, the operator MUST resolve the swap using the fallback resolution bound to that timeout class; a `mutual_consent`-only path with no fallback is not a valid terminal policy. The escrow descriptor referenced by the swap MUST declare the applicable fallback resolution for every timeout class it advertises, and compatibility validation MUST use that explicit binding rather than inferring a fallback from the trigger name alone (see the Refund-Trigger Fallback section of [PIP-01-escrow-descriptor.md](./PIP-01-escrow-descriptor.md)). ## Public-Protocol Boundary