diff --git a/PIP-01-escrow-descriptor.md b/PIP-01-escrow-descriptor.md index 64660b3..5a1d6d7 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,205 @@ 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 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. 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 + +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` + - 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` + - 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 + +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 +- `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 + +### 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 +- 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. 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 +- `cancel` — request cancellation of an unfunded or unresolved escrow instance + +Example shared-instance create flow: + +```text +participant A -> create() +service -> invitation=enroll-abc123 +participant B -> create(invitation=enroll-abc123) +``` + +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. + +### 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. + +### 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 + +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 + +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_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. @@ -382,7 +582,25 @@ 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 `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 + +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. diff --git a/README.md b/README.md index 89db8d6..a0736a3 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 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` @@ -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