From a5f50fde23a158c5b0a4af6364f40b3cd9645d01 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Thu, 30 Jul 2026 09:48:24 +0000 Subject: [PATCH 1/3] docs: add order discovery specification --- docs/discovery.md | 450 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 450 insertions(+) create mode 100644 docs/discovery.md diff --git a/docs/discovery.md b/docs/discovery.md new file mode 100644 index 00000000..5d5a355f --- /dev/null +++ b/docs/discovery.md @@ -0,0 +1,450 @@ +# Order discovery specification + +Version: 1 (draft) + +This document specifies how off-chain consumers discover and interpret +conditional orders without prior knowledge of their handlers. It covers three +surfaces: + +1. **Handler descriptors** (`IOrderDescriptor`) — declarative metadata for + decoding and rendering a handler's orders. +2. **Order modules** (`IOrderModule`) — executable extensions that let an + off-chain monitoring service (the polling agent historically called a + watch-tower; "monitoring service" hereafter) service handlers whose orders + require constructed `offchainInput`, including handler-specific off-chain + data. +3. **Proof payload URIs** — merkle-root payload locations expressed in the + same URI format as every other discovery surface, plus the payload document + that lets a consumer enumerate every published sub-order of a root. + +The key words MUST, MUST NOT, SHOULD, and MAY are to be interpreted as +described in RFC 2119. + +## Design principles + +- **The chain is the sole authority.** Every economically material field of an + order (tokens, amounts, receiver, kind, validity) is derived from + `generateOrder` / `tryGenerateOrder` / `IOrderManifest`, never from metadata. + Descriptors and modules are presentation and tooling hints. A consumer that + renders or signs from metadata instead of chain-derived data is broken. +- **Polling verdicts are 100 % on-chain.** A monitoring service needs nothing + in this document to poll correctly with empty `offchainInput`: `poll()` + verdicts, `getNextPollTimestamp`, `tryGenerateOrder`, and manifest pages are + self-describing. Modules extend service coverage to handlers that require + constructed `offchainInput`; they never mediate verdicts, which always come + from the chain. +- **Executable code is pulled only against an on-chain content hash.** Module + bytes MUST verify against `moduleDigest()` before execution, regardless of + transport. Digest-verified modules MAY be fetched and executed + automatically, but only inside the sandbox and budget regime of §2. +- **Feature detection, never gating.** Every interface here is a sidecar with + its own ERC-165 id, detected independently, absent without penalty, and never + on the settlement path. +- **One URI format everywhere.** Descriptors, modules, and proof payloads all + reference off-chain bytes as URIs under a single scheme policy and a single + hardened fetcher. What differs per surface is only the integrity source: + descriptor and module bytes verify against an on-chain digest; proof payload + bytes verify by recomputing the merkle root. +- **Fail closed.** Unknown URI schemes, unsupported or reverting views, and + unverifiable documents are treated as "no discovery", never as errors to + retry aggressively and never as data to trust. + +## 1. Handler descriptors + +### 1.1 Interface + +```solidity +interface IOrderDescriptor { + /** + * @notice Emitted when the descriptor location or commitment changes. + * @dev MUST be emitted from the constructor of implementing contracts so + * indexers discover the descriptor without polling. + */ + event DescriptorUpdate(string[] uris, bytes32 digest); + + /** + * @notice Locations of the handler descriptor document. + * @dev All URIs MUST reference the same document bytes (redundant + * mirrors), never alternative content. + */ + function descriptorURI() external view returns (string[] memory uris); + + /** + * @notice keccak256 of the exact descriptor document bytes as published. + * @dev Consumers MUST verify fetched bytes against this digest before + * parsing when the URI is not content-addressed. bytes32(0) means + * uncommitted; consumers MUST treat such descriptors as untrusted. + */ + function descriptorDigest() external view returns (bytes32); +} +``` + +- The interface is intentionally read-only. Implementations that support + rotation expose their own access-controlled setter and MUST emit + `DescriptorUpdate` on every change. Immutable implementations have no setter + and emit exactly once, from the constructor. Consumers detect mutability + behaviorally: any `DescriptorUpdate` after the constructor event SHOULD be + treated as a trust downgrade. +- The descriptor is contract-level. A handler is a type; individual orders are + rendered client-side from the descriptor's `staticInput` schema, ABI + decoding, and `describeOrder`. +- `BaseConditionalOrder` does not implement this interface; concrete handlers + opt in. Advertising the interface while returning empty values is + non-conformant. + +### 1.2 URI policy + +Descriptor URIs MUST be one of: + +| Scheme | Integrity source | +|---|---| +| `bzz://` | content address | +| `ipfs://` | content address | +| `data:` | in-band | +| `ni:` (RFC 6920) | hash embedded in the URI; `.well-known/ni/` HTTPS mapping applies | +| `https:` | permitted ONLY when `descriptorDigest()` is non-zero | + +`http:`, `file:`, and any URI resolving to loopback, link-local, or private +address ranges are prohibited. Fetchers SHOULD disable redirects (or re-validate +every hop against this policy), enforce a size cap (256 KiB RECOMMENDED), and +enforce a total timeout. + +### 1.3 Document + +The descriptor document is JSON, validated against the published descriptor-v1 +JSON Schema (draft 2020-12, content-addressed `$id`; published separately). +Producers MUST serialize with RFC 8785 (JSON Canonicalization Scheme); the +digest commits to the exact published bytes, and consumers verify bytes before +parsing. + +```json +{ + "version": "1", + "name": "TWAP", + "description": "Sells a fixed amount in n equal parts at a fixed interval.", + "handler": { "chainId": 100, "address": "0x…" }, + "staticInput": { "components": [ { "name": "sellToken", "type": "address" } ] }, + "offchainInput": { "required": false }, + "display": { "summary": "TWAP: sell {{partSellAmount|amount(sellToken)}} × {{n}} every {{t|duration}}" }, + "errors": { "0x…": { "name": "BeforeTwapStart", "label": "Not started yet" } }, + "links": { "source": "…" }, + "extensions": {} +} +``` + +Field notes (normative semantics; full schema separate): + +- `staticInput.components` is a JSON-ABI components fragment — the decoded + shape of the handler's `staticInput` bytes, with field names. +- `offchainInput.required`: when `true`, orders need constructed + `offchainInput`. Module discovery is on-chain (`IOrderModule` via ERC-165, + §2), independent of this field; `required == true` without a module means + only operator-specific tooling can service the handler. +- `errors` maps reason selectors (`bytes4`, as carried in `reasonCode`) to + names and optional human labels. Names for open-source handlers are + derivable from the verified ABI; this map serves closed-source handlers and + display labels. +- `display` templates are data (mustache-style with a small filter set), + never code. +- `handler.{chainId,address}` MUST match the contract the descriptor was + resolved from; a mismatch invalidates the document. + +Consumers SHOULD run a divergence check before presenting descriptor-derived +summaries: derive the actual order via `tryGenerateOrder`, compare material +fields, and surface any mismatch prominently. A descriptor that contradicts +observed behavior is a red flag, not a reconciliation problem. + +### 1.4 Generation + +Descriptors are derived, not hand-written: + +- `staticInput.components` from the build artifact AST (the struct never + crosses an external ABI boundary); +- `errors` from the handler ABI (every reason error is a declared error); +- a small author overlay supplies `name`, `description`, `display`, and + `links`; +- `handler` is stamped at deployment, making the digest per-deployment; + deploy tooling canonicalizes, hashes, publishes, and passes + `(uris, digest)` to the constructor. + +## 2. Order modules + +A handler MAY ship an executable module for consumers that service its orders. +Modules exist to construct `offchainInput` — the only aspect of servicing an +order that cannot be derived on-chain — including when construction requires +handler-specific off-chain data (external APIs, signed quotes, orderbook +state). Responsibility for such data sits solely with the handler/module pair; +a monitoring service never needs handler-specific knowledge beyond what the +module encapsulates. + +A handler that cannot generate without `offchainInput` signals it at the +verdict layer: it reverts `PollNeedsOffchainInput(bytes4 reasonCode)`, which +decodes to the `NEEDS_INPUT` verdict — semantically "empty-input polling is +futile; acquire input or park", never a timed retry. This is the module +discovery trigger: consumers seeing `NEEDS_INPUT` probe `IOrderModule` via +ERC-165. Consumers MUST NOT schedule empty-input re-polls of a `NEEDS_INPUT` +order. + +### 2.1 Interface + +```solidity +interface IOrderModule { + /** + * @notice Emitted when the module location or commitment changes. + * @dev MUST be emitted from the constructor of implementing contracts. + */ + event ModuleUpdate(string[] uris, bytes32 digest); + + /** + * @notice Locations of the module. All URIs MUST reference the same bytes. + */ + function moduleURI() external view returns (string[] memory uris); + + /** + * @notice keccak256 of the exact module bytes. MUST be non-zero. + * @dev The module's canonical identity and the final pre-execution gate. + * Fetch integrity is per-transport (a Swarm reference, CID, or + * RFC 6920 hash verifies the fetch); the digest is what consent + * lists, caches, and budgets key by, so mirror rotation never + * invalidates operator trust in byte-identical code. Consumers MUST + * verify keccak256(bytes) == moduleDigest() before execution and + * MUST NOT serve cached bytes against any other key. + */ + function moduleDigest() external view returns (bytes32); +} +``` + +- Sidecar with its own ERC-165 id, detected independently of + `IOrderDescriptor`: a module without presentation metadata is valid, and + vice versa. +- A zero `moduleDigest` is non-conformant; consumers MUST refuse to execute + unverifiable bytes. This is the content-hash gate: no module runs whose + bytes do not match the on-chain commitment. +- Mutability by omission, as for descriptors: no setter in the interface; + immutable implementations emit `ModuleUpdate` once from the constructor; + post-constructor updates are a trust signal consumers MAY act on. + +### 2.2 Execution model + +Monitoring services MAY fetch and execute digest-verified modules +automatically. The safety argument is structural: **module output is untrusted +input to on-chain verification.** Constructed `offchainInput` feeds +`generateOrder`/`poll`, and the settlement path re-verifies everything a +module could influence — a lying module can only fail to produce serviceable +orders, never cause an unauthorized order to validate. The residual risks are +host compromise and resource burn, addressed by the following requirements, +which are MUSTs for any automatic execution: + +- **Sandbox, no ambient authority**: no filesystem, no environment, no keys, + no arbitrary network. I/O is limited to + - a read-only EIP-1193 provider (`eth_call`, `eth_getStorageAt`, + `eth_blockNumber`), and + - fetch scoped to the origins the module declares (§2.3), with SSRF policy + applied (public addresses only, no redirects leaving the declared set, + size and time caps). +- **Budgets**: hard CPU/wall-clock/memory limits per invocation; a module that + exceeds them is parked per handler under the same bounded-retry policy as + unavailable payloads, never hot-retried. +- **Output distrust**: the host treats returned `offchainInput` as opaque + candidate bytes for on-chain calls — never as truth about the order. + +Frontends executing modules additionally broker all signature requests through +the host UI, which displays the chain-derived order, never the module's +claims. + +### 2.3 Module contract (v1) + +This specification normatively defines only the **portability core** below — +the minimum every module MUST satisfy regardless of which host loads it. The +full runtime interface — host API semantics (provider method set, scoped-fetch +behavior), packaging and loading, concrete resource budgets, optional exports +(e.g. frontend rendering), and the evolution of the module manifest — is +delegated to **shepherd**, the reference off-chain monitoring service, whose +module-interface specification is authoritative and independently versioned. +Modules SHOULD target the shepherd module interface; other hosts implementing +the same interface inherit module compatibility. + +The portability core: a single-file ES module, dependencies bundled, no +runtime imports: + +```js +export const version = "1"; +export const capabilities = { origins: ["https://api.example.com"] }; + +export async function buildOffchainInput({ chainId, provider, fetch, owner, params, ctx }) { + // provider: read-only EIP-1193 (eth_call, eth_getStorageAt, eth_blockNumber) + // fetch: host-supplied, restricted to `capabilities.origins` + return "0x…"; // offchainInput bytes +} +``` + +- `capabilities.origins` declares every external origin the module may + contact; the host grants fetch to exactly that set and nothing else. An + empty list means chain-state-only. +- `buildOffchainInput` SHOULD be deterministic given a block and the external + data it fetches; output is best-effort by construction, since verification + is on-chain. +- Constructed `offchainInput` is used exclusively as input to on-chain calls + (`poll` / `getTradeableOrderWithSignature`) — never for display, never for + scheduling beyond the returned verdict. +- Optional exports (e.g. `renderSummary` for frontend rendering) and other + module types (e.g. wasm) are defined by the shepherd module-interface + specification, not here. + +## 3. Proof payload URIs and the merkle payload + +### 3.1 Location as URIs + +`ComposableCow.setRoot` takes the payload location as URI mirrors, replacing +the numeric location registry of the upstream design: + +```solidity +struct Proof { + /// @dev Mirrors for the payload document; all URIs MUST reference the + /// same bytes. URIs are never interpreted on-chain. + string[] uris; + /// @dev EIP-4844 versioned hashes of the blobs carrying the payload + /// document (split convention in §3.2). For every listed hash, + /// `setRoot` verifies via `blobhash()` that the blob is attached to + /// this transaction. Empty: no blob publication. + bytes32[] blobVersionedHashes; +} +``` + +Private (no discovery expected, consumers MUST NOT retry) is expressed as +empty `uris` and empty `blobVersionedHashes`. + +- All mirrors carry identical payload bytes; a consumer MAY fetch from any of + them and MUST verify by recomputing the root (§3.3) regardless of source. + Because the payload is self-verifying against the on-chain root, no digest + accompanies these URIs — transport integrity is immaterial. +- Unknown schemes are skipped (fail closed per mirror); a root whose mirrors + are all unknown or unavailable is parked under the bounded-retry policy. +- Scheme guidance: + +| Scheme | Notes | Availability | +|---|---|---| +| `bzz://` | 64-hex reference (or 128-hex encrypted) | best-effort | +| `ipfs://` | CIDv1 | best-effort | +| `ni:` (RFC 6920) | `.well-known/ni/` HTTPS mapping applies | best-effort | +| `https:` | safe here without a digest — the root is the integrity anchor; SSRF policy of §1.2 applies | best-effort | +| `data:` | in-band payload (the successor of the upstream `EMITTED` location); SHOULD only be used for small trees — calldata and log cost make it self-limiting | on-chain | + +- Publishers SHOULD pair a guaranteed-publication channel (blobs or `data:`) + with a content-addressed mirror (`bzz://`, `ipfs://`) for late consumers — + a combination the upstream single-location design could not express. + +### 3.2 Blob publication + +A non-empty `blobVersionedHashes` binds publication to authorization: +`setRoot` requires (via the `BLOBHASH` opcode, typed error `BlobNotAttached`) +that **every** listed blob is attached to the transaction setting the root. +The root cannot be set without the payload being published in the same +transaction. The hashes are dedicated `bytes32` values — never URIs — so +verification involves no on-chain string interpretation; `uris` are opaque to +the contract. + +Multi-blob split convention: the payload document's field-element streams are +decoded per blob (31 bytes per field element) and concatenated in array +order; a single byte-length prefix lives in the first field element of the +first blob. One blob carries ~126.9 KiB usable (≈ 800 leaves); the array +lifts the guaranteed channel to multi-blob transactions (≈ 4,800 leaves at +six blobs). + +Retention advisory: blobs guarantee *publication*, not permanent retrieval +(consensus retention ≈ 4096 epochs / 18 days). Whether any order under a +root can outlive the window is not mechanically checkable, so this is +guidance, not prohibition: publishers of trees that may outlive retention +SHOULD pair a retention-independent mirror; consumers surface +blob-expired-and-unmirrored roots as an operator alert, never a silent +failure. + +- `blobhash()` observes the transaction's blobs from any call depth, so an + inner Safe call conforms when the executing EOA sends a type-3 transaction + carrying the blob. +- Blob payload packing: canonical document bytes at 31 bytes per field + element, zero-padded, with the byte length in the first field element. +- Blobs guarantee publication, not permanent retrieval: consensus-layer + retention is bounded (~4096 epochs). Indexers SHOULD capture within the + window; publishers SHOULD mirror as in §3.1. + +### 3.3 Payload document + +The payload is the complete leaf set — no proofs. Once all leaves are held, +every inclusion proof is recomputable locally, and completeness is checked by +recomputing the root. + +```json +{ + "version": "1", + "chainId": 100, + "root": "0x…", + "leafEncoding": "v1", + "leaves": [ + { "handler": "0x…", "salt": "0x…", "staticInput": "0x…" } + ] +} +``` + +- `leafEncoding: "v1"` pins the full tree construction, byte-exact against + `_auth`: `leaf = keccak256(abi.encode(ConditionalOrderParams))`; the tree is + built bottom-up over the ascending-sorted leaf array; each internal node is + `keccak256(sorted-pair(a, b))` (OpenZeppelin `MerkleProof` convention); an + odd trailing node at any level is promoted unchanged to the next level. + Sorted-pair hashing alone does not determine tree shape — implementations + MUST follow this construction (note: OpenZeppelin's `StandardMerkleTree` + double-hashes leaves and yields different roots; it is NOT this encoding). + Reference test vectors are published alongside the contracts. +- `leaves` MUST be sorted ascending by leaf hash and deduplicated; consumers + MUST reject on the first out-of-order or duplicate leaf. +- Producers MUST serialize with RFC 8785; content addresses and digests commit + to exact bytes. +- Consumers MUST verify `root` recomputes from `leaves` before use; a mismatch + rejects the whole payload. Consumers SHOULD enforce a leaf-count/byte cap + and abort oversized payloads before hashing. +- **Soundness is trustless; completeness is cooperative.** Recomputation proves + every published leaf is under the root; it cannot prove no leaf was + withheld. Consumers MUST present enumeration results as "orders published + for this root", never as the complete set. +- The `ctx` for merkle-authorized orders derives from the root slot written by + `_setRoot` (zero slot by default; the `setRootWithContext` value otherwise), + identically for every leaf under the root. +- `setRoot(bytes32(0), …)` is an explicit clear — supersede-to-nothing: a + zero root can authorize no leaf, so consumers tombstone every order under + the prior root. The proof MUST be empty on a clear (`ProofDataMalformed` + otherwise); no distinct event exists — `MerkleRootSet` with a zero root is + the clear signal. +- Availability failures (no mirror resolvable) are handled with bounded + retries and negative caching; a payload unavailable after the retry budget + parks the root. + +Per-leaf discovery composes with the manifest: root → payload → each leaf is a +full `ConditionalOrderParams` → `IOrderManifest` pages enumerate that leaf's +discrete orders. + +Handlers whose manifests expose multiple concurrently postable orders either +accept the module-less convention `offchainInput = abi.encode(uint256 +manifestIndex)` or are module-requiring (`NEEDS_INPUT`); a manifest page +alone does not tell a consumer how to select among concurrent orders. + +## 4. Consumer trust tiers + +Monitoring-service policy is a gradient, not a binary: + +1. **Curated allowlist** — full service; operators MAY relax sandbox budgets + for modules they have audited. +2. **Discovered, digest-verified** — on-chain commitments check out + (`moduleDigest` for modules, `descriptorDigest`/content address for + descriptors): poll, post, display, and automatic module execution under + the full §2.2 sandbox and budget regime. +3. **Bare probe** — ERC-165 advertises the generator interface; + `tryGenerateOrder(owner, params, "")` is the black-box serviceability + test. Typed verdicts with real reason selectors: poll at low priority with + rate limits. Garbage reverts, zero reason codes, or gas-bomb behavior: + park. No module (or an unverifiable one) means empty-`offchainInput` + probing only. This tier is what makes permissionless handler deployment + serviceable. + +Discovery views MUST be gas-bounded (paginated where unbounded); consumers set +an `eth_call` gas cap and treat reverts and out-of-gas as "unsupported", never +as fatal. From ad0053e4e98252e1a2b5df986b40fad13ffa946d Mon Sep 17 00:00:00 2001 From: mfw78 Date: Fri, 31 Jul 2026 03:51:55 +0000 Subject: [PATCH 2/3] docs: specify order modules as WASIP2 components Replaces the single-file ES module contract with a WebAssembly component targeting a WIT world, and reworks the commitment model that locates and verifies it. The export contract (`ccow:module`) is normative here and versions with `IOrderModule`: the commitment and the WIT are two halves of one agreement, where the chain says a module exists and the WIT says what a module is. The import surface (`videre:ccow`) is the environment and versions with the runtime instead. `local-store` is deliberately absent, which makes the idempotence requirement structural rather than a rule to enforce. Commitments become `(bytes32 digest, PackageKind kind)`, hoisted into a new section shared by descriptors and modules. Two kinds, trading the same pair of properties against each other so a publisher picks which one it wants: - `BZZ_MANIFEST` is a hash over a structure, so only Swarm can recompute it. That is what buys per-entry verification and partial fetch, and what costs cross-scheme mirroring. It also locates itself, so no URI is published and no gateway operator is written into a deployed contract. - `TAR_ZST` is a hash over bytes, so any transport can be checked by hashing what it delivered. That is what buys mirroring across bzz, ipfs and https, and what costs verifying nothing until the whole archive is in hand. - No per-entry digests are declared off-chain. A manifest already carries the root of each entry, and an archive digest covers every byte, so restating them would create a second source of truth and a disagreement to specify around without adding a check. What an archive does need, and a manifest does not, is extraction hardening: duplicate paths, traversal, symlinks, and decompression bombs. - A UnixFS CID is not usable as a commitment: it depends on chunker and DAG layout, which are publisher settings rather than properties of the content, so it cannot be recomputed from bytes fetched elsewhere. `ipfs://` remains a location. Modules are no longer required to be deterministic given a block. A module may fetch, so repeated calls may legitimately differ; what is required is idempotence. Determinism was never achievable here and is not needed, because output is verified on-chain rather than trusted. Interface changes land with the interfaces themselves. --- docs/discovery.md | 347 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 264 insertions(+), 83 deletions(-) diff --git a/docs/discovery.md b/docs/discovery.md index 5d5a355f..63f97f8d 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -33,22 +33,80 @@ described in RFC 2119. self-describing. Modules extend service coverage to handlers that require constructed `offchainInput`; they never mediate verdicts, which always come from the chain. -- **Executable code is pulled only against an on-chain content hash.** Module - bytes MUST verify against `moduleDigest()` before execution, regardless of - transport. Digest-verified modules MAY be fetched and executed - automatically, but only inside the sandbox and budget regime of §2. +- **Executable code is pulled only against an on-chain commitment.** Module + bytes MUST verify against `moduleCommitment()` before execution. The + commitment is a root in a named addressing scheme (§0), so verification uses + that scheme's own primitives rather than a separate hash over the fetched + bytes. Verified modules MAY be fetched and executed automatically, but only + inside the sandbox and budget regime of §2. - **Feature detection, never gating.** Every interface here is a sidecar with its own ERC-165 id, detected independently, absent without penalty, and never on the settlement path. -- **One URI format everywhere.** Descriptors, modules, and proof payloads all - reference off-chain bytes as URIs under a single scheme policy and a single - hardened fetcher. What differs per surface is only the integrity source: - descriptor and module bytes verify against an on-chain digest; proof payload - bytes verify by recomputing the merkle root. +- **One commitment format everywhere.** Descriptors and modules are committed + to identically: a `(digest, kind)` pair under §0, verified by the addressing + scheme's own primitives. What differs per surface is only the structure the + commitment resolves to: a descriptor resolves to one JSON document, a module + to a package. Proof payloads are the exception and verify by recomputing the + merkle root, since they are authored per root rather than published once. +- **Content addressing locates as well as verifies.** For `BZZ` and `IPFS` the + commitment already names where the bytes live, so a handler publishes no URI + and no gateway is written into a deployed contract. Retrieval is the host's + concern. URIs exist for `SHA256`, where the digest locates nothing. - **Fail closed.** Unknown URI schemes, unsupported or reverting views, and unverifiable documents are treated as "no discovery", never as errors to retry aggressively and never as data to trust. +## 0. Commitments + +Descriptors (§1) and modules (§2) are both committed to on-chain as a +`(bytes32 digest, DigestKind kind)` pair. `kind` names the addressing scheme +the digest is expressed in; verification uses that scheme's own primitives. + +```solidity +enum DigestKind { + BZZ, // digest is a Swarm BMT root + IPFS, // digest is the sha2-256 multihash digest of the root CID + SHA256 // digest is sha256 over the published bytes +} +``` + +`kind` is what makes the digest interpretable, and it is the reason no +canonical byte serialisation is specified anywhere in this document: a +mantaray manifest and a UnixFS DAG each already define a deterministic +structure, so there is nothing left to canonicalise. `SHA256` covers the +non-content-addressed case, where the published bytes are hashed directly. + +### 0.1 Location + +`BZZ` and `IPFS` are content-addressed: the commitment names the bytes, so it +also locates them. For these kinds the URI list SHOULD be empty and a host +resolves the digest through whatever Swarm or IPFS access it has. A handler +that lists URIs anyway supplies non-normative hints; a host MAY use them and +MUST still verify against the commitment. + +`SHA256` is not self-locating. Its URI list MUST be non-empty, and every URI +MUST resolve to bytes whose sha256 equals the digest. + +A `bytes32(0)` digest is uncommitted. Consumers MUST treat the surface as +absent rather than fetching anything. + +### 0.2 Verification + +Verification is a chain from the on-chain commitment to the bytes actually +used, with no step taken on trust: + +| kind | commitment resolves to | entries verify by | +|---|---|---| +| `BZZ` | mantaray manifest, or a leaf for a single document | BMT roots carried in the manifest | +| `IPFS` | UnixFS directory, or a file for a single document | CIDs carried in the DAG | +| `SHA256` | a zip archive, or the document bytes | the archive digest covers every byte | + +No per-entry digest is declared anywhere off-chain. For `BZZ` and `IPFS` the +manifest already carries the root of each entry, and for `SHA256` verifying +the archive authenticates all of its contents at once. Restating those +digests in a manifest file would create a second source of truth and a +disagreement to specify around, without adding a check. + ## 1. Handler descriptors ### 1.1 Interface @@ -60,22 +118,26 @@ interface IOrderDescriptor { * @dev MUST be emitted from the constructor of implementing contracts so * indexers discover the descriptor without polling. */ - event DescriptorUpdate(string[] uris, bytes32 digest); + event DescriptorUpdate(string[] uris, bytes32 digest, DigestKind kind); /** * @notice Locations of the handler descriptor document. - * @dev All URIs MUST reference the same document bytes (redundant - * mirrors), never alternative content. + * @dev Empty for content-addressed kinds, which the commitment locates. + * Required for `SHA256`. Any URI listed is a hint: all MUST resolve + * to the same document bytes, never alternative content. */ function descriptorURI() external view returns (string[] memory uris); /** - * @notice keccak256 of the exact descriptor document bytes as published. - * @dev Consumers MUST verify fetched bytes against this digest before - * parsing when the URI is not content-addressed. bytes32(0) means - * uncommitted; consumers MUST treat such descriptors as untrusted. + * @notice Commitment to the descriptor document. + * @dev `digest` is the document root in `kind`'s addressing. Consumers + * MUST verify fetched bytes against it before parsing. + * `bytes32(0)` means uncommitted; treat the descriptor as absent. */ - function descriptorDigest() external view returns (bytes32); + function descriptorCommitment() + external + view + returns (bytes32 digest, DigestKind kind); } ``` @@ -94,21 +156,25 @@ interface IOrderDescriptor { ### 1.2 URI policy -Descriptor URIs MUST be one of: +Integrity comes from the commitment (§0), never from the URI, so this policy +governs retrieval only. Where a URI is used it MUST be one of: -| Scheme | Integrity source | +| Scheme | Used with | |---|---| -| `bzz://` | content address | -| `ipfs://` | content address | -| `data:` | in-band | -| `ni:` (RFC 6920) | hash embedded in the URI; `.well-known/ni/` HTTPS mapping applies | -| `https:` | permitted ONLY when `descriptorDigest()` is non-zero | +| `bzz://` | `BZZ`, as a hint; the digest already locates the document | +| `ipfs://` | `IPFS`, as a hint | +| `data:` | any kind, for a document published in-band | +| `https:` | `SHA256`, where it is the only way to locate the document | `http:`, `file:`, and any URI resolving to loopback, link-local, or private address ranges are prohibited. Fetchers SHOULD disable redirects (or re-validate every hop against this policy), enforce a size cap (256 KiB RECOMMENDED), and enforce a total timeout. +An `https:` URI is never trusted on its own: bytes fetched from one are used +only after they verify against the commitment, which for `SHA256` is the whole +of the integrity argument. + ### 1.3 Document The descriptor document is JSON, validated against the published descriptor-v1 @@ -193,33 +259,37 @@ interface IOrderModule { * @notice Emitted when the module location or commitment changes. * @dev MUST be emitted from the constructor of implementing contracts. */ - event ModuleUpdate(string[] uris, bytes32 digest); + event ModuleUpdate(string[] uris, bytes32 digest, DigestKind kind); /** - * @notice Locations of the module. All URIs MUST reference the same bytes. + * @notice Locations of the module package. + * @dev Empty for content-addressed kinds, which the commitment locates. + * Required for `SHA256`. Any URI listed is a hint. */ function moduleURI() external view returns (string[] memory uris); /** - * @notice keccak256 of the exact module bytes. MUST be non-zero. - * @dev The module's canonical identity and the final pre-execution gate. - * Fetch integrity is per-transport (a Swarm reference, CID, or - * RFC 6920 hash verifies the fetch); the digest is what consent - * lists, caches, and budgets key by, so mirror rotation never - * invalidates operator trust in byte-identical code. Consumers MUST - * verify keccak256(bytes) == moduleDigest() before execution and - * MUST NOT serve cached bytes against any other key. + * @notice Commitment to the module package. `digest` MUST be non-zero. + * @dev The package root in `kind`'s addressing, and the module's + * canonical identity: consent lists, caches, and budgets key by it, + * so changing where a package is mirrored never invalidates operator + * trust in the same code. Consumers MUST verify the package against + * it before execution and MUST NOT serve cached bytes against any + * other key. */ - function moduleDigest() external view returns (bytes32); + function moduleCommitment() + external + view + returns (bytes32 digest, DigestKind kind); } ``` - Sidecar with its own ERC-165 id, detected independently of `IOrderDescriptor`: a module without presentation metadata is valid, and vice versa. -- A zero `moduleDigest` is non-conformant; consumers MUST refuse to execute - unverifiable bytes. This is the content-hash gate: no module runs whose - bytes do not match the on-chain commitment. +- A zero digest is non-conformant; consumers MUST refuse to execute + unverifiable bytes. This is the gate: no module runs whose package does not + resolve from the on-chain commitment. - Mutability by omission, as for descriptors: no setter in the interface; immutable implementations emit `ModuleUpdate` once from the constructor; post-constructor updates are a trust signal consumers MAY act on. @@ -235,16 +305,24 @@ orders, never cause an unauthorized order to validate. The residual risks are host compromise and resource burn, addressed by the following requirements, which are MUSTs for any automatic execution: -- **Sandbox, no ambient authority**: no filesystem, no environment, no keys, - no arbitrary network. I/O is limited to - - a read-only EIP-1193 provider (`eth_call`, `eth_getStorageAt`, - `eth_blockNumber`), and - - fetch scoped to the origins the module declares (§2.3), with SSRF policy - applied (public addresses only, no redirects leaving the declared set, - size and time caps). +- **No ambient authority**: a module is a WebAssembly component and reaches + the outside world only through what its world imports (§2.3). No filesystem, + no environment, no keys, no arbitrary network — not as a host promise, but + because those interfaces are absent from the world it is instantiated in. + What remains is chain access, an outbound HTTP client the host restricts to + the origins the package declares, and logging. +- **Authenticated capability grant**: the origin allowlist lives inside the + package, and the package root is the on-chain commitment (§0). A host + therefore cannot be handed a wider grant than the handler author committed + to, and an operator can audit the grant from chain state alone. - **Budgets**: hard CPU/wall-clock/memory limits per invocation; a module that exceeds them is parked per handler under the same bounded-retry policy as unavailable payloads, never hot-retried. +- **Idempotence, not determinism**: a module MAY fetch, so repeated calls with + identical input MAY return different bytes. What is required is that + `build-offchain-input` has no observable side effects and is safe to call + any number of times with results discarded. Determinism is neither achievable + nor needed, because output is verified on-chain rather than trusted. - **Output distrust**: the host treats returned `offchainInput` as opaque candidate bytes for on-chain calls — never as truth about the order. @@ -254,42 +332,143 @@ claims. ### 2.3 Module contract (v1) -This specification normatively defines only the **portability core** below — -the minimum every module MUST satisfy regardless of which host loads it. The -full runtime interface — host API semantics (provider method set, scoped-fetch -behavior), packaging and loading, concrete resource budgets, optional exports -(e.g. frontend rendering), and the evolution of the module manifest — is -delegated to **shepherd**, the reference off-chain monitoring service, whose -module-interface specification is authoritative and independently versioned. -Modules SHOULD target the shepherd module interface; other hosts implementing -the same interface inherit module compatibility. - -The portability core: a single-file ES module, dependencies bundled, no -runtime imports: - -```js -export const version = "1"; -export const capabilities = { origins: ["https://api.example.com"] }; - -export async function buildOffchainInput({ chainId, provider, fetch, owner, params, ctx }) { - // provider: read-only EIP-1193 (eth_call, eth_getStorageAt, eth_blockNumber) - // fetch: host-supplied, restricted to `capabilities.origins` - return "0x…"; // offchainInput bytes +A module is a WebAssembly component (WASI Preview 2). This specification +normatively defines the **portability core**: what a module MUST export, +below. What a module MAY import — the host surface, capability grants, and +resource budgets — is the *environment*, defined by `videre:ccow` and +delegated to the host that implements it. **shepherd** is the reference +monitoring service; other hosts implementing the same world inherit module +compatibility. + +The split is deliberate. The export contract is versioned alongside +`IOrderModule`, because `moduleCommitment()` and the WIT are two halves of one +agreement: the chain says a module exists, and the WIT says what a module is. +The import surface versions with the runtime instead. + +#### Portability core + +```wit +package ccow:module@1.0.0; + +interface order-module { + /// 20 bytes. + type address = list; + /// 32 bytes. + type word = list; + + record params { handler: address, salt: word, static-input: list } + + /// The order, plus the block the host will evaluate the resulting + /// `offchainInput` against. + record poll { + chain-id: u64, + block-number: u64, + owner: address, + /// Zero word when the order is authorised by merkle root. + ctx: word, + params: params, + } + + record not-ready { + reason: string, + /// Suggested wait before retrying. Absent leaves cadence to the host. + retry-after-ms: option, + } + + variant outcome { + /// The `offchainInput` bytes. Empty is valid and common. + ready(list), + /// No input at this block; the order stays live. + not-ready(not-ready), + /// This module can never service this order. The host stops asking. + unserviceable(string), + } + + build-offchain-input: func(req: poll) -> result; +} +``` + +`outcome` mirrors the on-chain verdict trichotomy — postable, wait, terminal — +so a handler author meets no new vocabulary. Not being ready is an outcome +rather than an error, exactly as a `WAIT` verdict is not a revert. The error +case is a log string: every host-actionable state is already in `outcome`, and +every structured host failure (rate limiting, a denied origin, a timeout) is +observed first-hand by the host at the call it mediated, so a module echoing +one back adds nothing. + +#### Environment + +```wit +package videre:ccow@0.1.0; + +world order-module { + use nexum:host/types@0.2.0.{config, host-error}; + + import nexum:host/chain@0.2.0; + import nexum:host/http@0.2.0; + import nexum:host/logging@0.2.0; + + export init: func(config: config) -> result<_, host-error>; + export ccow:module/order-module@1.0.0; } ``` -- `capabilities.origins` declares every external origin the module may - contact; the host grants fetch to exactly that set and nothing else. An - empty list means chain-state-only. -- `buildOffchainInput` SHOULD be deterministic given a block and the external - data it fetches; output is best-effort by construction, since verification - is on-chain. -- Constructed `offchainInput` is used exclusively as input to on-chain calls - (`poll` / `getTradeableOrderWithSignature`) — never for display, never for - scheduling beyond the returned verdict. -- Optional exports (e.g. `renderSummary` for frontend rendering) and other - module types (e.g. wasm) are defined by the shepherd module-interface - specification, not here. +Deliberately absent: `local-store`, `messaging`, `identity`, `remote-store`. +Persistent state would let two invocations with identical input diverge, which +is the idempotence requirement of §2.2; leaving the interface out makes that +structural instead of a rule to enforce. + +#### Package + +The commitment resolves to a package whose layout is the same across kinds: + +``` +nexum.toml +component.wasm +``` + +```toml +[module] +name = "twap-oracle" +version = "1.0.0" +world = "videre:ccow/order-module@0.1.0" +component = "component.wasm" + +[capabilities] +required = ["chain", "http", "logging"] + +[capabilities.http] +allow = ["https://api.example.com"] + +[config] +pair = "WETH/USDC" +``` + +- `[capabilities.http].allow` is the complete set of origins the module may + contact; the host grants exactly that and nothing else. Absent or empty + means chain-state only. +- `world` lets a host reject a component built against the wrong world before + linking, rather than failing on a missing import. +- `[config]` is passed verbatim to `init`. A module that cannot run with the + configuration it is given MUST fail there, not per poll. +- No file digests are declared. §0.2 covers why: the commitment already + authenticates every entry. + +#### Execution + +1. Resolve the commitment (§0.1) and verify the package root (§0.2). +2. Read `nexum.toml` from the package, verifying its entry. +3. Instantiate `component.wasm` in the `videre:ccow/order-module` world, with + HTTP restricted to `[capabilities.http].allow`. +4. `init` with `[config]`. A failure here parks the handler; it is a + packaging fault, not a transient one. +5. `build-offchain-input` per poll, dispatching on `outcome`: `ready` supplies + `offchainInput` to `getTradeableOrderWithSignature`; `not-ready` parks the + order for `retry-after-ms`; `unserviceable` stops polling that handler. + +Constructed `offchainInput` is used exclusively as input to on-chain calls +(`poll` / `getTradeableOrderWithSignature`) — never for display, never for +scheduling beyond the returned verdict. ## 3. Proof payload URIs and the merkle payload @@ -432,11 +611,13 @@ alone does not tell a consumer how to select among concurrent orders. Monitoring-service policy is a gradient, not a binary: 1. **Curated allowlist** — full service; operators MAY relax sandbox budgets - for modules they have audited. -2. **Discovered, digest-verified** — on-chain commitments check out - (`moduleDigest` for modules, `descriptorDigest`/content address for - descriptors): poll, post, display, and automatic module execution under - the full §2.2 sandbox and budget regime. + for modules they have audited. Allowlists key by the commitment digest, + which is why it is a single canonical value rather than one per mirror: an + audit of a module covers it wherever it is fetched from. +2. **Discovered, commitment-verified** — `moduleCommitment()` and + `descriptorCommitment()` resolve and verify per §0.2: poll, post, display, + and automatic module execution under the full §2.2 sandbox and budget + regime. 3. **Bare probe** — ERC-165 advertises the generator interface; `tryGenerateOrder(owner, params, "")` is the black-box serviceability test. Typed verdicts with real reason selectors: poll at low priority with From d24ea86a97e85dc4934e35fd468d1a7dc1ff8c26 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Fri, 31 Jul 2026 06:07:56 +0000 Subject: [PATCH 3/3] docs: narrow commitments to two package kinds Replaces `DigestKind {BZZ, IPFS, SHA256}` with `PackageKind {BZZ_MANIFEST, TAR_ZST}`, and states the trade between them as a choice a publisher makes rather than one compromise imposed on everyone. The previous set could not express what publishing to Swarm, IPFS and HTTPS at once actually requires. A mantaray root and a UnixFS root are hashes over different structures, so one digest cannot cover both, and an HTTPS mirror can only be checked against a structure root by serving that structure's chunk data. The split is between hashing a *structure* and hashing *bytes*: - `BZZ_MANIFEST` hashes a structure. Only Swarm can recompute it, which is what buys per-entry verification and reading `nexum.toml` before fetching the component, and what costs cross-scheme mirroring. - `TAR_ZST` hashes bytes. Any transport can be checked by hashing what it delivered, which is what makes mirroring across bzz, ipfs and https work, and what costs verifying anything before the whole archive is in hand. `IPFS` is dropped as a kind and kept as a location. A UnixFS CID depends on chunker and DAG layout, which are publisher settings rather than properties of the content, so it cannot be recomputed from bytes fetched elsewhere. One archive format rather than several: each additional format is a parser in every consumer, and archive parsers are a standing vulnerability surface for artefacts fetched from untrusted URLs. Adds the extraction rules an archive needs and a manifest does not (duplicate paths, traversal, symlinks, decompression bombs), and the compression asymmetry: `TAR_ZST` stores entries uncompressed because the archive compresses them, while `BZZ_MANIFEST` stores the component zstd-compressed because mantaray has no archive-level compression and Swarm charges per chunk. `nexum.toml` stays uncompressed under both, and names the logical artifact, so it is byte-identical whichever kind is chosen. --- docs/discovery.md | 151 +++++++++++++++++++++++++++++----------------- 1 file changed, 96 insertions(+), 55 deletions(-) diff --git a/docs/discovery.md b/docs/discovery.md index 63f97f8d..55cb9bdc 100644 --- a/docs/discovery.md +++ b/docs/discovery.md @@ -35,8 +35,8 @@ described in RFC 2119. from the chain. - **Executable code is pulled only against an on-chain commitment.** Module bytes MUST verify against `moduleCommitment()` before execution. The - commitment is a root in a named addressing scheme (§0), so verification uses - that scheme's own primitives rather than a separate hash over the fetched + commitment is a root in a named addressing packaging (§0), so verification uses + that packaging's own primitives rather than a separate hash over the fetched bytes. Verified modules MAY be fetched and executed automatically, but only inside the sandbox and budget regime of §2. - **Feature detection, never gating.** Every interface here is a sidecar with @@ -48,10 +48,10 @@ described in RFC 2119. commitment resolves to: a descriptor resolves to one JSON document, a module to a package. Proof payloads are the exception and verify by recomputing the merkle root, since they are authored per root rather than published once. -- **Content addressing locates as well as verifies.** For `BZZ` and `IPFS` the +- **Content addressing locates as well as verifies.** A `BZZ_MANIFEST` commitment already names where the bytes live, so a handler publishes no URI and no gateway is written into a deployed contract. Retrieval is the host's - concern. URIs exist for `SHA256`, where the digest locates nothing. + concern. URIs exist for `TAR_ZST`, where the digest locates nothing. - **Fail closed.** Unknown URI schemes, unsupported or reverting views, and unverifiable documents are treated as "no discovery", never as errors to retry aggressively and never as data to trust. @@ -59,53 +59,87 @@ described in RFC 2119. ## 0. Commitments Descriptors (§1) and modules (§2) are both committed to on-chain as a -`(bytes32 digest, DigestKind kind)` pair. `kind` names the addressing scheme -the digest is expressed in; verification uses that scheme's own primitives. +`(bytes32 digest, PackageKind kind)` pair. `kind` names how the bytes are +packaged and therefore how the digest is computed and verified. ```solidity -enum DigestKind { - BZZ, // digest is a Swarm BMT root - IPFS, // digest is the sha2-256 multihash digest of the root CID - SHA256 // digest is sha256 over the published bytes +enum PackageKind { + BZZ_MANIFEST, // mantaray manifest root + TAR_ZST // sha256 of a .tar.zst archive } ``` -`kind` is what makes the digest interpretable, and it is the reason no -canonical byte serialisation is specified anywhere in this document: a -mantaray manifest and a UnixFS DAG each already define a deterministic -structure, so there is nothing left to canonicalise. `SHA256` covers the -non-content-addressed case, where the published bytes are hashed directly. +The two kinds trade the same two properties against each other, and a +publisher picks which one it wants: + +| | `BZZ_MANIFEST` | `TAR_ZST` | +|---|---|---| +| locates itself | yes, the root is the Swarm reference | no, a URI is required | +| verification | per entry, from BMT roots in the manifest | whole archive, in one pass | +| partial fetch | yes, read `nexum.toml` before the component | no | +| mirrors | Swarm only | bzz, ipfs, https, anywhere | + +The asymmetry is structural rather than a preference. A mantaray root is a +hash over a *structure*, so only Swarm can recompute it; that is exactly what +buys per-entry verification and costs cross-scheme mirroring. A `TAR_ZST` +digest is a hash over *bytes*, so any transport can be checked by hashing what +it delivered, at the cost of verifying nothing until the whole archive is in +hand. + +Neither kind requires a canonical byte serialisation to be specified. Mantaray +already defines a deterministic structure, and an archive is committed to as +the exact bytes published. ### 0.1 Location -`BZZ` and `IPFS` are content-addressed: the commitment names the bytes, so it -also locates them. For these kinds the URI list SHOULD be empty and a host -resolves the digest through whatever Swarm or IPFS access it has. A handler -that lists URIs anyway supplies non-normative hints; a host MAY use them and -MUST still verify against the commitment. +`BZZ_MANIFEST` is content-addressed: the commitment names the bytes, so it also +locates them. The URI list SHOULD be empty and a host resolves the digest +through whatever Swarm access it has. A handler that lists URIs anyway supplies +non-normative hints; a host MAY use them and MUST still verify against the +commitment. -`SHA256` is not self-locating. Its URI list MUST be non-empty, and every URI -MUST resolve to bytes whose sha256 equals the digest. +`TAR_ZST` is not self-locating. Its URI list MUST be non-empty. Any scheme may +appear there, including `ipfs://`, which verifies its own content in transit +but whose CID is not the commitment: a UnixFS CID depends on chunker and DAG +layout, which are publisher settings rather than properties of the content, so +it cannot be recomputed from bytes fetched elsewhere. A `bytes32(0)` digest is uncommitted. Consumers MUST treat the surface as absent rather than fetching anything. ### 0.2 Verification -Verification is a chain from the on-chain commitment to the bytes actually -used, with no step taken on trust: +`BZZ_MANIFEST`: resolve the root, then verify each entry against the BMT root +the manifest carries for it. A single document (a descriptor) is committed to +as a leaf rather than a manifest. -| kind | commitment resolves to | entries verify by | -|---|---|---| -| `BZZ` | mantaray manifest, or a leaf for a single document | BMT roots carried in the manifest | -| `IPFS` | UnixFS directory, or a file for a single document | CIDs carried in the DAG | -| `SHA256` | a zip archive, or the document bytes | the archive digest covers every byte | +`TAR_ZST`: fetch the archive from any URI, verify `sha256(archive)` equals the +digest, then extract. Verifying the archive authenticates every byte in it, so +no per-entry digest is declared anywhere. Restating entry digests inside the +package would create a second source of truth and a disagreement to specify +around, without adding a check. + +Extraction is the attack surface an archive brings and a manifest does not. +Consumers MUST, before writing anything: + +- reject duplicate entry paths, which is where archive parsers disagree; +- reject absolute paths, `..` traversal, and symlinks; +- cap entry count and total decompressed size, refusing the package rather + than expanding it. + +### 0.3 Compression + +Each container compresses at the level it can, so the component is stored +differently in each and `nexum.toml` is byte-identical across both: + +- `TAR_ZST`: entries are stored uncompressed; the archive compresses them. +- `BZZ_MANIFEST`: mantaray has no archive-level compression, so the component + is stored zstd-compressed at `.zst`, which is worth doing on a + network that charges per chunk. `nexum.toml` is stored uncompressed: it is + small, and a consumer reads and verifies it before deciding whether to fetch + the component at all. -No per-entry digest is declared anywhere off-chain. For `BZZ` and `IPFS` the -manifest already carries the root of each entry, and for `SHA256` verifying -the archive authenticates all of its contents at once. Restating those -digests in a manifest file would create a second source of truth and a -disagreement to specify around, without adding a check. +The decompressed-size cap of §0.2 applies to both. ## 1. Handler descriptors @@ -118,13 +152,13 @@ interface IOrderDescriptor { * @dev MUST be emitted from the constructor of implementing contracts so * indexers discover the descriptor without polling. */ - event DescriptorUpdate(string[] uris, bytes32 digest, DigestKind kind); + event DescriptorUpdate(string[] uris, bytes32 digest, PackageKind kind); /** * @notice Locations of the handler descriptor document. - * @dev Empty for content-addressed kinds, which the commitment locates. - * Required for `SHA256`. Any URI listed is a hint: all MUST resolve - * to the same document bytes, never alternative content. + * @dev Empty for `BZZ_MANIFEST`, which the commitment locates. Required + * for `TAR_ZST`. Any URI listed is a hint: all MUST resolve to the + * same document bytes, never alternative content. */ function descriptorURI() external view returns (string[] memory uris); @@ -137,7 +171,7 @@ interface IOrderDescriptor { function descriptorCommitment() external view - returns (bytes32 digest, DigestKind kind); + returns (bytes32 digest, PackageKind kind); } ``` @@ -161,10 +195,10 @@ governs retrieval only. Where a URI is used it MUST be one of: | Scheme | Used with | |---|---| -| `bzz://` | `BZZ`, as a hint; the digest already locates the document | -| `ipfs://` | `IPFS`, as a hint | -| `data:` | any kind, for a document published in-band | -| `https:` | `SHA256`, where it is the only way to locate the document | +| `bzz://` | `BZZ_MANIFEST`, as a hint; the commitment already locates the document | +| `ipfs://` | `TAR_ZST`, as a location; the CID is not the commitment | +| `data:` | either kind, for a document published in-band | +| `https:` | `TAR_ZST`, the common case | `http:`, `file:`, and any URI resolving to loopback, link-local, or private address ranges are prohibited. Fetchers SHOULD disable redirects (or re-validate @@ -172,7 +206,7 @@ every hop against this policy), enforce a size cap (256 KiB RECOMMENDED), and enforce a total timeout. An `https:` URI is never trusted on its own: bytes fetched from one are used -only after they verify against the commitment, which for `SHA256` is the whole +only after they verify against the commitment, which for `TAR_ZST` is the whole of the integrity argument. ### 1.3 Document @@ -259,12 +293,12 @@ interface IOrderModule { * @notice Emitted when the module location or commitment changes. * @dev MUST be emitted from the constructor of implementing contracts. */ - event ModuleUpdate(string[] uris, bytes32 digest, DigestKind kind); + event ModuleUpdate(string[] uris, bytes32 digest, PackageKind kind); /** * @notice Locations of the module package. - * @dev Empty for content-addressed kinds, which the commitment locates. - * Required for `SHA256`. Any URI listed is a hint. + * @dev Empty for `BZZ_MANIFEST`, which the commitment locates. Required + * for `TAR_ZST`. Any URI listed is a hint. */ function moduleURI() external view returns (string[] memory uris); @@ -280,7 +314,7 @@ interface IOrderModule { function moduleCommitment() external view - returns (bytes32 digest, DigestKind kind); + returns (bytes32 digest, PackageKind kind); } ``` @@ -420,11 +454,12 @@ structural instead of a rule to enforce. #### Package -The commitment resolves to a package whose layout is the same across kinds: +The commitment resolves to a package with the same logical layout under either +kind, differing only in how the component is stored (§0.3): ``` -nexum.toml -component.wasm +nexum.toml # uncompressed under both kinds +component.wasm # stored as component.wasm.zst under BZZ_MANIFEST ``` ```toml @@ -451,15 +486,21 @@ pair = "WETH/USDC" linking, rather than failing on a missing import. - `[config]` is passed verbatim to `init`. A module that cannot run with the configuration it is given MUST fail there, not per poll. +- `component` names the logical artifact, not the stored entry, so this file + is byte-identical whichever kind a publisher chooses. - No file digests are declared. §0.2 covers why: the commitment already authenticates every entry. #### Execution -1. Resolve the commitment (§0.1) and verify the package root (§0.2). -2. Read `nexum.toml` from the package, verifying its entry. -3. Instantiate `component.wasm` in the `videre:ccow/order-module` world, with - HTTP restricted to `[capabilities.http].allow`. +1. Resolve the commitment (§0.1) and verify it (§0.2), applying the extraction + rules for `TAR_ZST`. +2. Read `nexum.toml`. Under `BZZ_MANIFEST` this is one entry, verified and + parsed before the component is fetched at all; under `TAR_ZST` the whole + archive is already in hand. +3. Fetch the component, decompressing it under `BZZ_MANIFEST` (§0.3), and + instantiate it in the `videre:ccow/order-module` world with HTTP restricted + to `[capabilities.http].allow`. 4. `init` with `[config]`. A failure here parks the handler; it is a packaging fault, not a transient one. 5. `build-offchain-input` per poll, dispatching on `outcome`: `ready` supplies