From b000722e1b68327a045f3a6f4c514045a54c09c0 Mon Sep 17 00:00:00 2001 From: mfw78 Date: Sat, 25 Jul 2026 02:58:02 +0000 Subject: [PATCH] docs: trim core architecture guides and diagrams Rewrite docs 00-04 and the diagram captions as terse descriptions of the current contract, cutting rationale essays, grant-milestone and platform-target planning material, and 0.3 version promises reduced to current fact. Verify every retained claim against crates/nexum-runtime: engine.toml [limits] keys and defaults (fuel 1B, memory 64 MiB, state 50 MiB, deadline 120s), the six-primitive event-module world, and the single-file keccak256-prefixed local store (ADR-0003). Correct substantive drift: module discovery is filesystem-only in 0.2 with ENS and registry as design direction; the shepherd:cow package now carries only cow-events and order submission is the videre:venue venue-adapter contract; the local store commits per host call rather than per event. Part of #598. --- docs/00-overview.md | 230 ++++++----------------- docs/01-runtime-environment.md | 238 ++++-------------------- docs/02-modules-events-packaging.md | 66 ++----- docs/03-module-discovery.md | 277 ++-------------------------- docs/04-state-store.md | 270 +++------------------------ docs/diagrams/README.md | 10 +- docs/diagrams/diagrams.md | 8 +- 7 files changed, 163 insertions(+), 936 deletions(-) diff --git a/docs/00-overview.md b/docs/00-overview.md index 768448e1..7bec0431 100755 --- a/docs/00-overview.md +++ b/docs/00-overview.md @@ -1,27 +1,23 @@ # Nexum: Universal WASM Component Model Runtime -Nexum is a WASM Component Model runtime that provides secure, sandboxed execution for WebAssembly modules. Modules react to blockchain events, read chain state, persist data locally and to decentralised storage, communicate via decentralised messaging - all within a capability-based sandbox with zero implicit permissions. +Nexum is a WASM Component Model runtime that provides secure, sandboxed execution for WebAssembly modules. Modules react to blockchain events, read chain state, persist data locally and to decentralised storage, and communicate via decentralised messaging, all within a capability-based sandbox with zero implicit permissions. -**Shepherd** is the Nexum distribution that includes CoW Protocol extensions (`shepherd:cow` WIT package). A module compiled against the universal `nexum:host/event-module` world runs on any Nexum-compatible host. A module compiled against `shepherd:cow/shepherd` additionally gains access to CoW Protocol APIs and order submission - and requires a Shepherd host. +**Shepherd** is the Nexum distribution that adds CoW Protocol support. A module compiled against the universal `nexum:host/event-module` world runs on any Nexum-compatible host. CoW order submission is provided by the `videre:venue` venue-adapter layer (see doc 08), not by a domain-specific host interface. ### Vocabulary: engine vs. host (`nexum` vs. `nexum:host`) -Two project names look similar but mean different things - keeping them straight is load-bearing for everything that follows: - | Term | What it is | Where you find it | |---|---|---| -| **engine** (`nexum`) | A concrete *implementation* that loads and runs WASM components. The 0.2 reference engine is a wasmtime-based server daemon. Mobile / browser / embedded engines could exist later - each is a separate engine. | `crates/nexum-runtime/`, the `nexum` binary, `cargo run -p nexum-cli` | -| **host** (`nexum:host`) | The WIT *contract* - the set of host-imported interfaces (chain, identity, local-store, etc.), types, and worlds that every engine must implement and every module imports. The contract is one; engines are many. | `wit/nexum-host/`, `package nexum:host@0.1.0`, Rust path `nexum::host::*` | - -The relationship: an engine *implements* `nexum:host` so that modules *built against* `nexum:host` can run on it. The `nexum:host` package itself does not run anything - it's a specification. When this doc says "the host", it means whichever engine the module currently runs on, as seen through the `nexum:host` contract. +| **engine** (`nexum`) | A concrete implementation that loads and runs WASM components. The 0.2 reference engine is a wasmtime-based server daemon. | `crates/nexum-runtime/`, the `nexum` binary, `cargo run -p nexum-cli` | +| **host** (`nexum:host`) | The WIT contract: the host-imported interfaces (chain, identity, local-store, ...), types, and worlds that every engine implements and every module imports. | `wit/nexum-host/`, `package nexum:host@0.1.0`, Rust path `nexum::host::*` | -The reference engine ships as two crates: the `nexum-runtime` library (embeddable, no CLI surface) and the `nexum` binary in `crates/nexum-cli`, a thin consumer of it. A Rust embedder skips the binary entirely, constructs an `EngineConfig` in code, and calls `nexum_runtime::bootstrap::run_from_config`. See `crates/nexum-runtime/examples/embed.rs` for a minimal end-to-end example. +An engine implements `nexum:host` so that modules built against `nexum:host` can run on it. The reference engine ships as two crates: the `nexum-runtime` library (embeddable, no CLI surface) and the `nexum` binary in `crates/nexum-cli`. A Rust embedder constructs an `EngineConfig` in code and calls `nexum_runtime::bootstrap::run_from_config`; see `crates/nexum-runtime/examples/embed.rs`. ## Architecture ```mermaid flowchart TB - disc["Module Discovery\nStatic · ENS · On-chain Registry"] --> mm + disc["Module Discovery\nStatic (0.2) · ENS · On-chain Registry (0.3)"] --> mm subgraph nexum["Nexum Runtime"] mm["Module Manager\nLoad → Init → Run → Restart → Dead"] @@ -34,7 +30,7 @@ flowchart TB subgraph host["Host API - WIT Interfaces"] uni["nexum:host\nchain · identity · local-store · remote-store · messaging · logging"] - ext["shepherd:cow\ncow-api"] + ext["videre:venue\nvenue adapters"] end subgraph back["Backends"] @@ -60,8 +56,8 @@ flowchart TB - **Component Model from day 1** - WIT-defined API contract; structural sandboxing (no filesystem, no ambient network); multi-language guests. - **Declarative subscriptions** - modules declare events in their manifest; the runtime wires sources. -- **Transactional state** - per-event all-or-nothing semantics; commit on success, rollback on trap. -- **Content-addressed distribution** - modules are fetched by hash (Swarm, IPFS, OCI, HTTPS); integrity always verified. +- **Durable state** - each local-store write is its own fsync-durable committed transaction; state survives traps and restarts. +- **Content-addressed distribution** - modules are fetched by hash; integrity always verified. - **Self-hosted** - no centralised dependency; operator runs their own node. ## The Six Primitives @@ -77,48 +73,23 @@ Every module has access to six orthogonal capabilities through the `nexum:host` | **Messaging** | `messaging` | Decentralised pub/sub messaging | Topic-based | Waku | | **Logging** | `logging` | Diagnostic output | Per-module | tracing | -These primitives are orthogonal: - -- **Chain** is the source of truth - the blockchain consensus state. Modules read chain state and (indirectly) write to it via order submission or transactions. -- **Identity** is cryptographic identity - key management and signing. The `chain` host implementation depends on `identity` internally: signing RPC methods (`eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`) delegate to the identity backend. Modules can also import `identity` directly for raw signing operations. -- **Local Store** is the module's private scratchpad - fast, local, scoped to one module on one device. Does not replicate. -- **Remote Store** is shared persistent content - content-addressed, decentralised, survives independent of any device. Any module on any device can read what another module wrote. -- **Messaging** is real-time communication - ephemeral pub/sub messages between modules, devices, or users. Transient and topic-based. -- **Logging** is diagnostics - one-way output for debugging and monitoring. Not a data channel. +The `chain` host implementation depends on `identity`: signing RPC methods (`eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`) delegate to the identity backend. Modules may also import `identity` directly for raw signing. ## Additive 0.2 Capabilities -In addition to the six core primitives, 0.2 introduces one optional capability that modules can declare in their manifest: +Beyond the six core primitives, 0.2 adds one optional capability modules declare in their manifest: -- **`http`** - allowlisted outbound HTTP via the standard `wasi:http/outgoing-handler` interface, gated by a `[capabilities.http].allow` domain list. The capability name lives in the manifest; the wire surface is plain wasi:http. The host MUST enforce the allowlist on every outgoing request: an off-list host is denied before any connection is made. The SDK's `http::fetch` helper wraps the interface for Rust guests. This replaces the 0.1 anti-pattern of tunnelling notifications through Waku. +- **`http`** - allowlisted outbound HTTP via `wasi:http/outgoing-handler`, gated by a `[capabilities.http].allow` domain list. The host enforces the allowlist on every request: an off-list host is denied before any connection is made. The SDK's `http::fetch` helper wraps the interface. -Time and secure randomness are WASI concerns rather than Nexum capabilities: `wasi:clocks` and `wasi:random` are linked into every module store ambiently. +Time and secure randomness are WASI concerns: `wasi:clocks` and `wasi:random` are linked into every module store ambiently. -0.2 also publishes (but does not yet host) the experimental **`query-module`** world for request/response modules (wallet rule evaluators, signature validators, pricing oracles). The WIT is stable enough to target with `MockHost` tests; production host support lands in 0.3. +0.2 also publishes (but does not host) the experimental **`query-module`** world for request/response modules. The WIT is provisional and may change without a major bump; it is a target for mock-host tests only. ## WIT Worlds -The WIT is split into layered packages. The universal layer (`nexum:host`) provides blockchain-agnostic capabilities. Domain extensions (e.g. `shepherd:cow`) add protocol-specific interfaces. +The WIT is layered. The universal `nexum:host` package provides blockchain-agnostic capabilities; domain packages layer on top. -```mermaid -graph TB - subgraph l3["Layer 3 - Domain Extensions"] - cow["shepherd:cow\ncow-api"] - other["future:domain\nvault · strategy · …"] - end - - subgraph l1["Layer 1 - Universal Runtime"] - pkg["nexum:host"] - ifaces["chain · identity · local-store · remote-store · messaging · logging"] - exports["Exports: init · on-event"] - end - - cow -->|include event-module| l1 - other -->|include event-module| l1 ``` - -``` -// Universal layer - any platform, any blockchain app package nexum:host@0.1.0 world event-module { @@ -127,28 +98,18 @@ world event-module { import local-store - local key-value persistence import remote-store - decentralised storage (Swarm) import messaging - decentralised messaging (Waku) - import logging - log (trace/debug/info/warn/error) - - export init(config) - called once on load - export on_event(event) - called per subscribed event (block, logs, tick, message) -} - -// CoW Protocol extension -package shepherd:cow@0.1.0 + import logging - log (trace/debug/info/warn/error) -world shepherd { - include event-module - import cow-api - CoW Protocol REST API + order submission + export init(config) - called once on load + export on_event(event) - called per subscribed event (block, logs, tick, message) } ``` -The `event-module` world imports **six** interfaces - chain, identity, local-store, remote-store, messaging, logging. The 0.1 WIT framing claimed six primitives but only actually imported five; 0.2 brings `identity` into the world definition so the contract matches the documentation. +The `event-module` world imports no WASI interfaces. `wasi:clocks` and `wasi:random` are linked ambiently, and modules that declare the `http` capability additionally import `wasi:http`. The `chain` interface exposes a single generic `request` function (plus an additive `request-batch`); the SDK implements alloy's `Transport` on top of it, giving modules the full alloy `Provider` API with zero WIT churn. -The world imports no WASI interfaces. `wasi:clocks` and `wasi:random` are linked into every store ambiently, and modules that declare the `http` capability additionally import `wasi:http`; all other I/O is mediated through host interfaces. The `chain` interface exposes a single generic `request` function (plus an additive `request-batch` in 0.2) - the SDK implements alloy's `Transport` trait on top of it, giving modules the full alloy `Provider` API (80+ methods) with zero WIT churn. +CoW Protocol support is two packages. `shepherd:cow@0.1.0` carries `cow-events`, the canonical decoded on-chain event enum (topic-0 hashes for `ConditionalOrderCreated`, `ConditionalOrderRemoved`, `OrderPlacement`) that keepers and manifests are parity-tested against. Order submission is the `videre:venue@0.1.0` venue-adapter contract: a keeper drives venues through `videre:venue/client` by name, and each installed adapter component exports the provider face for one venue (the CoW venue is the `cow-venue` crate). See doc 08. -> Design rationale: [07-rpc-namespace-design.md](07-rpc-namespace-design.md) | Platform generalisation: [08-platform-generalisation.md](08-platform-generalisation.md) - --> Full WIT definition: [01-runtime-environment.md](01-runtime-environment.md) +> Design rationale: [07-rpc-namespace-design.md](07-rpc-namespace-design.md) | Platform generalisation and the venue layer: [08-platform-generalisation.md](08-platform-generalisation.md) | Full WIT: [01-runtime-environment.md](01-runtime-environment.md) ## Technology Stack @@ -156,7 +117,7 @@ The world imports no WASI interfaces. `wasi:clocks` and `wasi:random` are linked |---------|--------|---------| | Language | Rust | 1.90+ | | WASM runtime | wasmtime (Component Model) | 45.x | -| API contract | WIT (`nexum:host@0.1.0`, `shepherd:cow@0.1.0`) | - | +| API contract | WIT (`nexum:host@0.1.0`) | - | | Guest bindings | wit-bindgen | 0.57.x | | Async | Tokio | - | | Ethereum RPC | alloy | 1.5.x | @@ -177,9 +138,6 @@ name = "twap-monitor" version = "0.3.0" component = "sha256:9f86d081…" # content hash of module.wasm -[chains] -required = [42161] # must have RPC for these chains - [capabilities] required = ["chain", "local-store", "logging"] optional = ["messaging", "remote-store"] @@ -189,29 +147,20 @@ kind = "block" chain_id = 42161 [config] -cow_api_url = "https://api.cow.fi/arbitrum" -slippage_bps = 50 # integers stay integers in 0.2 +slippage_bps = 50 # integers stay integers ``` -The manifest declares identity, chain requirements, event subscriptions, capability grants, and typed module config - everything the runtime needs to load and run the module. In 0.2, `[capabilities]` is the canonical place to declare what host primitives a module needs; the engine cross-checks the component's WIT imports against `required` + `optional` at boot (link-time) and refuses to instantiate a module that imports an undeclared capability. Omitting `[capabilities]` falls back to "all imports required" with a deprecation warning. +The manifest declares chain requirements, event subscriptions, capability grants, and typed module config. `[capabilities]` is the canonical place to declare needed host primitives; the engine cross-checks the component's WIT imports against `required` + `optional` at boot (link-time) and refuses to instantiate a module that imports an undeclared capability. Omitting `[capabilities]` falls back to "all imports required" with a deprecation warning. -> Per-module resource caps (`[module.resources]`: `max_memory_bytes`, `max_fuel_per_event`, `max_state_bytes`) are **not in 0.2 scope** - the engine uses global defaults (`DEFAULT_FUEL_PER_EVENT = 1B`, `DEFAULT_MEMORY_LIMIT = 64 MiB`). Per-module overrides via the manifest are a future direction; today, an operator who needs different caps changes the global defaults at build time. The `optional` trap-stub fallback for absent host imports is also deferred to 0.3 - in 0.2, every linked import resolves to a real host function. +Resource caps are global in 0.2, set in `engine.toml` `[limits]` (`fuel_per_event`, default 1B; `memory_bytes`, default 64 MiB; `state_bytes`, default 50 MiB; `event_deadline_secs`, default 120). Per-module `[module.resources]` overrides are a 0.3 direction. -> Full spec: [02-modules-events-packaging.md](02-modules-events-packaging.md) ## Module Discovery -Three layers, from simplest to most decentralised: - -| Method | How it works | -|--------|-------------| -| **Static** | Operator points at a local manifest path | -| **ENS** | Module author sets ENS `contenthash` (ENSIP-7) to a Swarm/IPFS reference; runtime resolves and fetches | -| **On-chain registry** | Runtime watches contract events or ENS `TextChanged` events for module registrations | - -All methods converge: resolve content reference -> fetch via content store -> verify hash -> load. +0.2 loads modules from local filesystem paths listed in `engine.toml`. ENS `contenthash` resolution and on-chain registry discovery are a 0.3 design direction. --> Full design: [03-module-discovery.md](03-module-discovery.md) +-> Full design and current status: [03-module-discovery.md](03-module-discovery.md) ## Module Lifecycle @@ -231,12 +180,12 @@ stateDiagram-v2 Dead --> [*] ``` -- **Resolve**: fetch WASM by content hash from Swarm/IPFS/OCI/local. +- **Resolve**: fetch WASM by content hash (local in 0.2). - **Load**: compile `Component`, validate WIT world, create `InstancePre`. - **Init**: create `Store`, instantiate, call `init(config)`. - **Run**: dispatch subscribed events to `on_event`. Each call gets a fuel budget. -- **Restart**: on crash - exponential backoff (1s -> 5min cap), fresh `Store`, state persists. -- **Dead**: after N consecutive failures (poison pill) - requires manual intervention. +- **Restart**: on crash, exponential backoff (1s -> 5min cap), fresh `Store`, state persists. +- **Dead**: after N consecutive failures (poison pill), requires manual intervention. -> Full lifecycle: [02-modules-events-packaging.md](02-modules-events-packaging.md) @@ -245,109 +194,50 @@ stateDiagram-v2 - **Sources**: `block` (new heads via `eth_subscribe`), `chain-log` (filtered contract events), `cron` (schedule-based), `message` (Waku content topics). - **Shared subscriptions**: one block subscription per chain, fanned out to all subscribed modules. - **Dispatch**: concurrent across modules, sequential within a module (ordered delivery). -- **Declared in manifest**: `[[subscription]]` blocks - the runtime wires sources, not the module. +- **Declared in manifest**: `[[subscription]]` blocks; the runtime wires sources. -> Full design: [02-modules-events-packaging.md](02-modules-events-packaging.md) ## Local Store - **Backend**: redb (pure Rust, ACID, MVCC, crash-safe). -- **Isolation**: one database file per module; modules cannot access each other's state. -- **Transactions**: each `on_event` runs in an implicit write transaction - commit on success, rollback on failure. -- **Survives restarts**: state is external to WASM instance. -- **Size enforcement**: `max_state_bytes` from manifest, enforced host-side. -- **Prefix scanning**: `list-keys(prefix)` for namespaced key organisation. +- **Isolation**: modules cannot access each other's state. +- **Transactions**: each store call commits its own redb transaction; there is no per-event atomic rollback. +- **Survives restarts**: state is external to the WASM instance. +- **Isolation**: single redb file, 32-byte `keccak256(module_name)` key prefix (ADR-0003). +- **Size enforcement**: `[limits].state_bytes` quota, enforced host-side. +- **Prefix scanning**: `list-keys(prefix)` for namespaced keys. -> Full design: [04-state-store.md](04-state-store.md) ## SDK -The SDK ships as two crate pairs: `nexum-sdk`, the generic module-author SDK (host trait seam, bind macro, chain / config / address helpers, wasi:http `http::fetch`, tracing facade) with `nexum-sdk-test` providing the generic mock-host surface, and `shepherd-sdk`, the CoW-domain layer (cow-api trait, order bridging, revert decoding) with `shepherd-sdk-test` providing the CoW mock host. Modules that never touch the orderbook depend only on the nexum pair. See [ADR-0009](adr/0009-host-trait-surface.md) for the shipped host-trait seam that replaces the proc-macro design described in earlier drafts of doc 05. +The guest SDK ships as `nexum-sdk` (the generic strategy-module SDK: host-trait seam, chain/config/address helpers, `http::fetch`, tracing facade) with `nexum-sdk-test` for the mock-host surface, and `videre-sdk` (the venue and keeper SDK: the `venue-adapter` export trait and the typed venue client) with `videre-test`. Modules are built with `cargo build --target wasm32-wasip2 --release`. The operator CLI is the `nexum` binary itself. -| Crate | Provides | -|-------|----------| -| `nexum-sdk` | `host::{ChainHost, LocalStoreHost, LoggingHost, Host}` - per-capability traits + supertrait, the seam modules implement against | -| | `Fault` + the `HostFault` trait - the shared failure vocabulary and per-interface typed errors (`ChainError`) with `?` support | -| | `chain::{eth_call_params, parse_eth_call_result}` + `chain::chainlink` - JSON-RPC plumbing helpers | -| | `config` / `address` - config-table lookups, decimal scaling, address parsing | -| | `keeper::{WatchSet, Gates, Journal, Retrier, Poller}` - the conditional-commitment strategy keeper: watch registry, poll gates, receipt journal, retry dispatch over the local-store seam | -| | `http::{fetch, Fetch, FetchError, FetchOptions}` - allowlisted outbound HTTP over wasi:http on the standard `http` crate's `Request` / `Response` types | -| | `tracing` + `bind_host_via_wit_bindgen!` - guest tracing facade and the per-module adapter macro | -| | `prelude::*` - alloy primitives in one import | -| `shepherd-sdk` | `cow::{CowApiHost, CowHost}` - the cow-api trait and orderbook host bound | -| | `cow::{order, composable, error}` - CoW Protocol bridging (`gpv2_to_order_data`, `Verdict`, `LegacyRevertAdapter`, `RetryAction`, `classify_api_error`) | -| | `cow::run` - the shared poll-loop composition: run the keeper watch set, poll a `Poller`, submit `Ready` orders behind the `submitted:` journal guard and retry ledger | -| | `bind_cow_host_via_wit_bindgen!` - the CoW layering of the generic adapter macro | -| | `prelude::*` - cowprotocol order / signing / orderbook surface in one import | -| `nexum-sdk-test` | `MockHost` + per-trait `MockChain` / `MockLocalStore` / `MockLogging` + `capture_tracing` for native-Rust strategy tests | -| `shepherd-sdk-test` | CoW `MockHost` + `MockCowApi`, composing the `nexum-sdk-test` mocks | +Multi-language support: authors can target the WIT world directly from Rust, C/C++, Go, JavaScript, or Python via `wit-bindgen`. The SDK is a Rust ergonomics layer. -Future direction (not in 0.2): a `#[nexum::module]` / `#[shepherd::module]` proc macro that subsumes the `wit_bindgen::generate!` + `WitBindgenHost` adapter boilerplate, a typed `TypedState` / `Signer` / `Cow` API client, alloy `Provider` injection via `HostTransport`, and filling out `nexum-sdk` into the full universal SDK for non-CoW modules. None of those land in 0.2. - -The operator CLI is the `nexum` binary itself (`cargo run -p nexum-cli`); a separate `cargo nexum` subcommand for module authors (new / build / package / publish / check / migrate) is future direction, not in 0.2 scope. Today modules are built with `cargo build --target wasm32-wasip2 --release`. - -Multi-language support: module authors can use Rust, C/C++, Go, JavaScript, or Python - all compile to valid components against the same WIT world via `wit-bindgen`. The SDK is a Rust ergonomics layer on top of the WIT contract; non-Rust authors target the WIT directly. - --> Full design: [05-sdk-design.md](05-sdk-design.md) | M3 architectural decision: [ADR-0009](adr/0009-host-trait-surface.md) - -`nexum-sdk-test` / `shepherd-sdk-test` above are for **module business logic** - no wasm, no engine crate. Testing the *engine* itself (supervision, dispatch, capability wiring, reconnect) is a different, wasm-backed surface: see [testing-runtime-harness.md](testing-runtime-harness.md). +-> Full design: [05-sdk-design.md](05-sdk-design.md) | Host-trait seam: [ADR-0009](adr/0009-host-trait-surface.md) ## Production Hardening -### Resource Enforcement - | Resource | Mechanism | On breach | |----------|-----------|-----------| -| CPU (deterministic) | Fuel | Trap -> rollback -> restart | -| CPU (wall-clock) | Epoch interruption | Yield to Tokio | +| CPU (deterministic) | Fuel | Trap -> restart | +| CPU (wall-clock) | Epoch interruption + dispatch deadline | Yield / abort dispatch | | Memory | `ResourceLimiter` | `memory.grow` denied | | Storage | Host-side tracking | `local-store::set` returns `fault.invalid-input` | -### RPC Resilience - -Tower layer stack per chain: timeout -> retry (exponential + jitter) -> rate limit -> fallback endpoint. WebSocket subscriptions auto-reconnect with missed-block backfill. - -### Error Model - -In 0.2 each interface declares its own typed error and they share one payload-bearing `fault` vocabulary for the cross-domain cases. `fault` has seven cases: `unsupported(string)`, `unavailable(string)`, `denied(string)`, `rate-limited(rate-limit)`, `timeout`, `invalid-input(string)`, and `internal(string)`. Interfaces with nothing to add report `fault` directly (identity, local-store, remote-store, messaging, and the module exports); a richer interface embeds `fault` as one case of its own variant and adds the cases only it needs (`chain-error` adds an `rpc` case carrying the node code and decoded revert bytes). Modules match on the typed variant for retry/backoff decisions; the per-protocol error types from 0.1 (`json-rpc-error`, `msg-error`, `store-error`, `api-error`) are gone. See [ADR-0011](adr/0011-per-interface-typed-errors.md) for the model. +Each interface declares its own typed error over a shared payload-bearing `fault` vocabulary (`unsupported`, `unavailable`, `denied`, `rate-limited`, `timeout`, `invalid-input`, `internal`). Interfaces with nothing to add report `fault` directly; `chain-error` embeds `fault` and adds an `rpc` case. See [ADR-0011](adr/0011-per-interface-typed-errors.md). -### Observability - -| Signal | Stack | Endpoint | -|--------|-------|----------| -| Logs | `tracing` -> JSON | stdout | -| Metrics | `metrics` -> Prometheus | `:9100/metrics` (default; see `docs/production.md`) | - -Metrics cover three groups: runtime-level (modules loaded/dead), per-module (events, latency, fuel, restarts, state usage), per-chain RPC (requests, errors, fallbacks, blocks behind). Liveness is signalled by the metrics scrape (`/metrics` returns 200 iff the engine is running and the Prometheus exporter is up) plus the structured `tracing` JSON on stdout. A dedicated `:8080/health` JSON endpoint with a per-module table is a future direction, not in 0.2 scope - operators today scrape `/metrics` and inspect the JSON log stream. +Observability: `tracing` JSON to stdout; a Prometheus exporter on `127.0.0.1:9100/metrics` when `[engine.metrics]` is enabled (disabled by default). -> Full design: [06-production-hardening.md](06-production-hardening.md) ## Platform Generalisation -Nexum is **designed** to be portable to mobile and browser hosts: the WIT contract is the universal interface and any host that implements it can run modules unchanged. The **0.2 reference runtime ships server-only** - a Rust/Tokio/wasmtime binary. The mobile, WebView, and super-app targets remain on the roadmap and live in the docs as architectural direction, not shipping artifacts. - -| Platform | WASM Engine | Local Store | RPC Backend | Status | -|----------|-------------|-------------|-------------|--------| -| **Server** (reference) | wasmtime | redb | alloy provider | **Shipping in 0.2** | -| **Mobile** (Flutter/Dart) | wasmtime C API / wasm3 | SQLite | HTTP client | Planned - see roadmap | -| **WebView** | Browser engine + `jco` | IndexedDB | JS bridge / wallet | Planned - see roadmap | -| **Super app** | All of the above | SQLite | HTTP + wallet | Planned - see roadmap | - -The mobile/wallet host story - including the experimental `query-module` world's production support, the C ABI for non-Rust embedders, and the `nexum-host` embedder facade - is on the 0.3 roadmap, conditional on a named design partner. A minimal Rust embedding path already exists today via the `nexum-runtime` library entrypoint, with the richer facade remaining a 0.3 direction. +The `nexum:host` WIT contract is host-portable: any host implementing it can run modules unchanged. The 0.2 reference runtime is server-only (Rust/Tokio/wasmtime). Mobile, WebView, and super-app targets are architectural direction. --> Full design (and the design rationale for each target): [08-platform-generalisation.md](08-platform-generalisation.md) - -## Grant Milestones - -| # | Milestone | Effort | Key Deliverables | -|---|-----------|--------|------------------| -| 1 | Core Runtime & Event System | 120h | wasmtime Component Model host, WIT interfaces, event sources, redb local store, CLI | -| 2 | TWAP & Ethflow Modules | 100h | TWAP monitor, Ethflow monitor, ComposableCoW contract mods\* | -| 3 | SDK & Developer Experience | 60h | `shepherd-sdk` + `shepherd-sdk-test` crates (host-trait seam per ADR-0009), example modules, tutorial, docs | -| 4 | Production Hardening | 60h | Resource limits, restart policy, logging, metrics, health checks | -| 5 | Multi-Chain & Deployment | 40h | Multi-chain config, Docker image, deployment docs | - -\* **M2 divergence.** The "ComposableCoW contract mods" deliverable (enhanced polling interfaces, optimized getters, monitoring events) was intentionally met off-chain: no Solidity was modified. The TWAP module polls `getTradeableOrderWithSignature` via raw `eth_call` with SDK helpers. Contract-side interfaces would fix one concrete TWAP implementation behind the boundary and block competing strategies (ADR-0006). The functional goal stands; the literal deliverable does not. +-> Full design and the venue layer: [08-platform-generalisation.md](08-platform-generalisation.md) ## Repository Structure @@ -355,30 +245,30 @@ The mobile/wallet host story - including the experimental `query-module` world's shepherd/ ├── crates/ │ ├── nexum-runtime/ Core WASM host (server) library: event system, local store, bootstrap -│ ├── nexum-cli/ The `nexum` binary: clap CLI entry point over the runtime library -│ ├── nexum-sdk/ Generic guest SDK: host-trait seam, Fault, chain/config/address helpers, wasi:http fetch, tracing facade (ADR-0009) -│ ├── nexum-sdk-test/ Generic mock host (MockChain / MockLocalStore / MockLogging) for strategy tests -│ ├── shepherd-sdk/ CoW-domain SDK: cow-api trait + CoW Protocol helpers on top of nexum-sdk -│ └── shepherd-sdk-test/ CoW mock host (MockCowApi + composed MockHost) for strategy tests +│ ├── nexum-cli/ The `nexum` binary: clap CLI over the runtime library +│ ├── nexum-sdk/ Generic guest SDK: host-trait seam, chain/config/address helpers, wasi:http fetch +│ ├── nexum-sdk-test/ Generic mock host for strategy tests +│ ├── videre-sdk/ Venue + keeper SDK: venue-adapter export trait, typed venue client +│ ├── videre-host/ Host-side venue registry + status watch +│ ├── videre-test/ Venue/keeper test surface +│ └── cow-venue/ The CoW venue: order body types + IntentBody codec ├── modules/ │ ├── twap-monitor/ TWAP order monitoring module │ ├── ethflow-watcher/ Ethflow order monitoring module -│ └── examples/ price-alert, balance-tracker, http-probe reference modules +│ └── examples/ reference modules (price-alert, balance-tracker, http-probe, echo-*) ├── wit/ │ ├── nexum-host/ Universal WIT package (chain, identity, local-store, remote-store, messaging, logging) -│ └── shepherd-cow/ CoW Protocol WIT package (cow-api, shepherd) +│ ├── shepherd-cow/ CoW event enum (cow-events) +│ └── videre-venue/ Venue-adapter contract (client + adapter faces) ├── Dockerfile ├── docker-compose.yml └── docs/ - ├── 00-overview.md - ├── 01-runtime-environment.md … 08-platform-generalisation.md - ├── adr/ ADR-0001 … ADR-0009 (canonical architectural decisions) + ├── 00-overview.md … 08-platform-generalisation.md + ├── adr/ Architectural decision records ├── deployment/ Docker + Prometheus operator config ├── diagrams/ Mermaid diagrams + reference captions - ├── operations/ Runbooks, E2E reports, load reports, baselines + ├── operations/ Runbooks ├── production.md Operator handbook - ├── sdk.md Module-author entry point (shipped SDK reference) + ├── sdk.md Module-author entry point └── tutorial-first-module.md ``` - -The SDK split is in place: `nexum-sdk` carries the universal surface and `shepherd-sdk` layers the CoW domain on top, with no re-export between them. Shipping a `cargo-nexum` subcommand for module authors remains future direction. diff --git a/docs/01-runtime-environment.md b/docs/01-runtime-environment.md index 8ad5f84f..6dff10d1 100755 --- a/docs/01-runtime-environment.md +++ b/docs/01-runtime-environment.md @@ -2,54 +2,19 @@ ## Version Target -**wasmtime 45.x** (latest stable as of Feb 2026). +**wasmtime 45.x**, requiring **Rust 1.90.0+**. Guest bindings pin `wit-bindgen` 0.57.x. -- Release cadence: new major on the 20th of each month. -- LTS every 12th version (24 months support). Nearest LTS: v36. -- Requires **Rust 1.90.0+**. -- Repo: https://github.com/bytecodealliance/wasmtime +## Component Model -## Why wasmtime +The engine targets the Component Model, not raw core modules, for: -| Criterion | wasmtime | wasmer | wasm3 | -|-----------|----------|--------|-------| -| Rust-native embedding | First-class | Yes | C FFI | -| Async host functions | Yes | No | No | -| Component Model / WASI | Full | Partial | No | -| Fuel / epoch metering | Both | Fuel only | Injection | -| Production users | Fastly, Fermyon, Cloudflare, Zed | General | Embedded | -| Sandboxing | Proven | Similar | Similar | +- **Structural sandboxing.** A component compiled against a WIT world with no filesystem import cannot access the filesystem: enforced at the type level, not by omission of host functions. +- **Type-safe contract.** The WIT definition is the API spec; host and guest get generated bindings (`wasmtime::component::bindgen!`, `wit_bindgen::generate!`). +- **Resource types.** Opaque handles with lifecycle management via `ResourceTable`. +- **Multi-language guests.** Rust, C/C++, Go, JavaScript, Python all produce valid components against the same WIT world. +- **No WASI required.** The pure `nexum:host` world imports exactly the host APIs; zero WASI imports means zero implicit capabilities. -## Decision: Component Model from Day 1 - -### Rationale - -The Component Model is **production-viable in wasmtime 45** and gives us critical advantages over raw core modules: - -1. **Structural sandboxing.** A component compiled against a WIT world with no filesystem import literally *cannot* access the filesystem - enforced at the type level, not just by omission of host functions. This is stronger than core module sandboxing where imports are stringly-typed. - -2. **Type-safe API contract.** The WIT definition *is* the API spec. Both host and guest get generated bindings (`wasmtime::component::bindgen!` on the host, `wit_bindgen::generate!` on the guest). No manual ABI wrangling, no serialisation disagreements. - -3. **Resource types.** Opaque handles with lifecycle management (constructors, methods, destructors via `ResourceTable`). Ideal for subscription handles, RPC connections, etc. - -4. **Multi-language guests from day 1.** Module authors can use Rust, C/C++, Go, JavaScript (ComponentizeJS), or Python (componentize-py) - all producing valid components against the same WIT world. This dramatically lowers the barrier for community modules. - -5. **No WASI required.** The Component Model and WASI are architecturally separate. We define a pure `nexum:host` world with exactly our host APIs. Zero WASI imports means zero implicit capabilities. - -6. **Acceptable overhead.** The canonical ABI adds marshalling for strings/lists (memory copy across boundary), but for a plugin system with coarse-grained calls this is negligible. `InstancePre` front-loads validation costs. - -### What we give up - -- **Tooling churn.** `wit-bindgen` (v0.57) and `cargo-component` (v0.21) are functional but APIs are not yet stable. Pin versions in the SDK. -- **Native async Component Model** (`stream`, `future`) is still evolving. We use basic async host functions (`func_wrap_async`) which are stable. - -### Risk assessment - -| Aspect | Risk | -|--------|------| -| `bindgen!` macro, custom worlds, resource types | Low - stable, well-documented | -| `wit-bindgen` guest bindings | Medium - API churn between versions | -| Component Model native async (streams/futures) | High - not needed yet, avoid for now | +The engine uses wasmtime's basic async host functions (`func_wrap_async`), not the still-evolving Component Model native async (`stream`, `future`). ## Core Concepts @@ -97,9 +62,9 @@ let pre = linker.instantiate_pre(&component)?; let bindings = EventModule::instantiate_pre(&mut store, &pre)?; ``` -## WIT Worlds: Universal and CoW-Specific +## WIT Worlds -Nexum uses a two-layer WIT architecture. The **universal** package `nexum:host` defines platform-agnostic interfaces and the `event-module` world. The **CoW-specific** package `shepherd:cow` extends it with CoW Protocol interfaces and the `shepherd` world. +The **universal** package `nexum:host` defines platform-agnostic interfaces and the `event-module` world. CoW Protocol support layers on top: `shepherd:cow` carries the `cow-events` enum, and order submission is the `videre:venue` venue-adapter contract (below). ### Universal Package: `nexum:host@0.1.0` @@ -303,87 +268,44 @@ world event-module { In addition to the six core imports, 0.2 publishes one additive optional capability - `http` (allowlisted outbound HTTP) - which modules declare in their `module.toml` `[capabilities]` section. The declaration is a manifest concern only: the capability is serviced by the standard `wasi:http/outgoing-handler` interface, not a `nexum:host` one. 0.2 also publishes the experimental **`query-module`** world for request/response modules; the WIT is stable but no host implementation ships in 0.2, so it's a target for `MockHost` testing only. -### CoW-Specific Package: `shepherd:cow@0.1.0` +### CoW Protocol packages -The `shepherd:cow` package extends the universal world with CoW Protocol interfaces. In 0.2 the two 0.1 interfaces (`cow` + `order`) merge into a single `cow-api` interface to eliminate the `cow::cow::request` triple-stutter: +`shepherd:cow@0.1.0` carries a single interface, `cow-events`: the canonical decoded on-chain event enum whose variants pin each CoW Solidity signature and its topic-0 hash. Keeper constants and module manifests are parity-tested against it. ```wit package shepherd:cow@0.1.0; -interface cow-api { - use nexum:host/types.{chain-id, fault}; - - /// A non-2xx reply with no typed rejection envelope; `body` is raw text. - record http-failure { status: u16, body: option } - - /// A typed orderbook rejection, parsed host-side from `{errorType, description}`. - record order-rejection { status: u16, error-type: string, description: string, data: option } - - /// A cow-api call failure: a shared host `fault`, a raw HTTP failure, - /// or a typed order rejection. - variant cow-api-error { - fault(fault), - http(http-failure), - rejected(order-rejection), +interface cow-events { + enum cow-event { + conditional-order-created, // ComposableCoW registration + conditional-order-removed, // ComposableCoW v2 removal + order-placement, // CoWSwapOnchainOrders (EthFlow) } - - /// HTTP-style request to the CoW Protocol API. - /// - /// The host routes to the correct CoW API base URL for the given chain. - /// `method`: "GET" | "POST" | "PUT" | "DELETE" - /// `path`: relative API path, e.g. "/api/v1/orders" - /// `body`: optional JSON request body - request: func( - chain-id: chain-id, - method: string, - path: string, - body: option, - ) -> result; - - /// Submit a serialised order to the CoW Protocol. - /// (Replaces the 0.1 `order::submit` interface.) - submit-order: func(chain-id: chain-id, order-data: list) - -> result; -} - -/// CoW Protocol module world. Extends the universal event-module -/// with CoW-specific imports. -world shepherd { - include nexum:host/event-module; - - import cow-api; } ``` +Order submission is not a host interface. It is the `videre:venue@0.1.0` venue-adapter contract: a keeper calls `videre:venue/client` (`quote` / `submit` / `observe` / `status` / `cancel`) naming a venue by string, and each installed adapter component exports the provider face for one venue over scoped transport only. The CoW venue is the `cow-venue` crate. See doc 08 for the venue layer. + ### Key properties - **Constrained WASI** - the WASI p2 surface linked into every store includes `wasi:clocks` and `wasi:random` ambiently; there is no filesystem grant and no inbound network. The only network path is allowlisted outbound HTTP through `wasi:http/outgoing-handler`, available to modules that declare the `http` capability in the manifest's `[capabilities]` section. The `wasi:sockets` bindings are linked as part of the p2 surface but stay inert because the WASI context grants no network. -- **All I/O through our interfaces** - RPC reads, identity/signing, CoW API, local-store, order submission, logging. +- **All I/O through host interfaces** - RPC reads, identity/signing, local-store, messaging, logging; venue submission through `videre:venue/client`. - **Generic JSON-RPC passthrough** - the `chain` interface exposes a single `request` function (plus an additive `request-batch`). The SDK implements alloy's `Transport` trait on top of it, giving modules the full alloy `Provider` API. See doc 07 for details. -- **Identity as a first-class primitive** - the `identity` interface provides key management and signing. The `chain` host implementation depends on `identity` internally: signing RPC methods (`eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`) are intercepted and delegated to the identity backend. Modules can also import `identity` directly for `personal_sign`-style message signing, EIP-712 typed data signing, and listing accounts. (Raw-bytes signing, gated by an explicit capability, is on the 0.3 roadmap; the current `sign` MUST prepend the EIP-191 prefix.) -- **Per-interface typed errors over a shared `fault` vocabulary** - each interface declares its own error type; the cross-domain cases share one payload-bearing `fault` (`unsupported`, `unavailable`, `denied`, `rate-limited`, `timeout`, `invalid-input`, `internal`). Interfaces with nothing to add return `fault` directly (identity, local-store, remote-store, messaging, the module exports); `chain-error` embeds `fault` and adds an `rpc` case, `cow-api-error` adds `http` and `rejected`. The 0.1 per-protocol error types (`json-rpc-error`, `identity-error`, `msg-error`, `store-error`, `api-error`) are gone. Modules match on the typed variant for retry/backoff decisions. See ADR-0011. -- **`list` for raw bytes** - local-store values, order payloads, signatures, accounts, etc. The SDK provides typed wrappers. -- **Resource types** can be added later (e.g. subscription handles, cursor-based log iteration). -- **Two worlds in 0.2's reference runtime** - `nexum:host/event-module` for platform-agnostic modules; `shepherd:cow/shepherd` for CoW Protocol modules that need the `cow-api` import. The experimental `nexum:host/query-module` world is published but not yet hosted. +- **Identity as a first-class primitive** - the `identity` interface provides key management and signing. The `chain` host implementation depends on `identity`: signing RPC methods (`eth_sendTransaction`, `eth_accounts`, `eth_signTypedData_v4`, `personal_sign`) are intercepted and delegated to the identity backend. Modules can also import `identity` directly for `personal_sign` message signing, EIP-712 typed data signing, and listing accounts. `sign` prepends the EIP-191 prefix; a raw-bytes signing primitive is a 0.3 direction. +- **Per-interface typed errors over a shared `fault` vocabulary** - each interface declares its own error type; the cross-domain cases share one payload-bearing `fault` (`unsupported`, `unavailable`, `denied`, `rate-limited`, `timeout`, `invalid-input`, `internal`). Interfaces with nothing to add return `fault` directly (identity, local-store, remote-store, messaging, the module exports); `chain-error` embeds `fault` and adds an `rpc` case. Modules match on the typed variant for retry/backoff decisions. See ADR-0011. +- **`list` for raw bytes** - local-store values, signatures, accounts, order bodies. The SDK provides typed wrappers. +- **Worlds** - `nexum:host/event-module` for automation modules; `videre:venue/venue-adapter` for venue adapter components. The experimental `nexum:host/query-module` world is published but not yet hosted. ## Host-Side Embedding -The host uses `wasmtime::component::bindgen!` to generate Rust traits from the WIT. For universal interfaces, the generated traits live under `nexum::host::`. For CoW-specific interfaces, they live under `shepherd::cow::`. +The host uses `wasmtime::component::bindgen!` to generate Rust traits from the WIT; the generated traits for universal interfaces live under `nexum::host::`. ```rust -// Universal event-module world wasmtime::component::bindgen!({ path: "wit/nexum-host", world: "event-module", async: true, }); - -// CoW-specific shepherd world (extends event-module) -wasmtime::component::bindgen!({ - path: "wit/shepherd-cow", - world: "shepherd", - async: true, -}); ``` ### Identity Host Trait @@ -498,101 +420,13 @@ impl nexum::host::local_store::Host for NexumHostState { } // ... } - -impl shepherd::cow::cow_api::Host for NexumHostState { - // CoW-specific host implementation - // ... -} ``` -See doc 07 for the full `chain` and `cow-api` host implementations, method allowlisting, and the `HostTransport` that bridges this to alloy's `Provider` API on the guest side. +See doc 07 for the full `chain` host implementation, method allowlisting, and the `HostTransport` that bridges it to alloy's `Provider` API on the guest side. ## Guest-Side (Module Author) Experience -> The two subsections below describe the **0.3+ macro-driven authoring model** (`#[nexum::module]` / `#[shepherd::module]`, alloy `RootProvider` injection, `TypedState`). It is future direction, not in 0.2 scope. In 0.2, modules ship today using the host-trait seam from [ADR-0009](adr/0009-host-trait-surface.md): a `strategy.rs` (pure logic against `&impl Host`) plus a `lib.rs` `WitBindgenHost` adapter that bridges to `wit-bindgen::generate!`. See [`sdk.md`](sdk.md) and the example modules under `modules/examples/` for the shipped pattern. - -### Universal modules (future direction; `nexum-sdk`) - -In the future direction, module authors targeting the universal `event-module` world would add the `nexum-sdk` crate and use the `#[nexum::module]` proc macro. Modules can access identity for signing operations - either indirectly through `chain` (signing RPC methods are handled transparently) or directly via the `identity` interface for raw signing: - -```rust -use nexum_sdk::prelude::*; - -#[nexum::module] -struct BlockLogger; - -impl BlockLogger { - fn init(config: Config) -> Result<()> { - info!("Block logger starting"); - Ok(()) - } - - async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { - let block_num = provider.get_block_number().await?; - info!("New block: {block_num}"); - - TypedState::set("last_block", &block_num)?; - Ok(()) - } -} -``` - -### CoW Protocol modules (future direction; `shepherd-sdk` macro form) - -In the future direction, module authors targeting the CoW-specific `shepherd` world would add the `shepherd-sdk` crate and use the `#[shepherd::module]` proc macro. The macro provides **named event handlers** (`on_block`, `on_chain_logs`, `on_tick`, `on_message`) - it generates the `on_event` match dispatch, WIT export wrapper, and optional provider injection. Handlers can be `async fn` for natural `.await`: - -```rust -use shepherd_sdk::prelude::*; - -sol! { - function getTradeableOrderWithSignature( - address owner, bytes32 ctx, bytes32 orderHash - ) external view returns (bytes memory order, bytes memory signature); -} - -#[shepherd::module] -struct TwapMonitor; - -impl TwapMonitor { - fn init(config: Config) -> Result<()> { - info!("TWAP monitor starting"); - Ok(()) - } - - // Named handler - macro generates on_event match dispatch. - // provider is injected from block.chain_id. - // async fn - macro wraps in block_on (single-poll, zero overhead). - async fn on_block(block: Block, provider: &RootProvider) -> Result<()> { - // Full alloy Provider API - natural .await - let block_num = provider.get_block_number().await?; - let balance = provider.get_balance(owner).latest().await?; - - // Typed contract calls with sol! + EthCall builder - let tx = TransactionRequest::default() - .to(contract) - .input(getTradeableOrderWithSignatureCall { - owner, ctx, orderHash: order_hash, - }.abi_encode().into()); - let result = provider.call(tx).latest().await?; - let decoded = getTradeableOrderWithSignatureCall::abi_decode_returns(&result)?; - - // CoW API via typed client - let cow = Cow::new(block.chain_id); - cow.submit_order(&order)?; - - // State persistence - TypedState::set("last_block", &block_num)?; - Ok(()) - } - - // Only define handlers for events you subscribe to. - // No on_chain_logs, on_tick, or on_message → those events are silently ignored. -} -``` - -Build with `cargo component build --release` (or `cargo build --target wasm32-wasip2` + `wasm-tools component new`). - -See doc 05 for the full macro design (named handlers, provider injection, escape hatch) and doc 07 for the `HostTransport` implementation and `provider()` constructor. +Modules ship using the host-trait seam from [ADR-0009](adr/0009-host-trait-surface.md): a `strategy.rs` of pure logic against `&impl Host`, plus a `lib.rs` `WitBindgenHost` adapter that bridges to `wit-bindgen::generate!`. Build with `cargo build --target wasm32-wasip2 --release`. See [`sdk.md`](sdk.md), doc 05, and the example modules under `modules/examples/`. ## Multi-Language Guest Support @@ -605,7 +439,7 @@ See doc 05 for the full macro design (named handlers, provider injection, escape | **Python** | componentize-py (CPython) | Maturing | | **C#** | `wit-bindgen-csharp` | Emerging | -All produce valid components against the same WIT worlds (`nexum:host/event-module` for universal, `shepherd:cow/shepherd` for CoW). +All produce valid components against the `nexum:host/event-module` world. ## Execution Metering @@ -624,17 +458,11 @@ Both are needed: fuel for correctness, epochs for liveness. ## Resource Limits -Implement `ResourceLimiter` to cap per-module: - -- **Memory growth** - target <10 MB default. -- **Table growth** - max entries. -- **Instance count** - max concurrent. - -Enforced synchronously on every `memory.grow` / `table.grow`. +A `ResourceLimiter` caps linear-memory growth per module store, enforced synchronously on every `memory.grow`. The cap is `[limits].memory_bytes` from `engine.toml` (default 64 MiB). Fuel, the per-dispatch wall-clock deadline, and the local-store byte quota are the other resolved caps (`fuel_per_event` 1B, `event_deadline_secs` 120, `state_bytes` 50 MiB); all live in `crates/nexum-runtime/src/engine_config.rs` and apply uniformly, per-module overrides being a 0.3 direction. ## Async Integration -All RPC and CoW API I/O is async (alloy / reqwest on the host). wasmtime bridges this: +All RPC and outbound I/O is async (alloy / reqwest on the host). wasmtime bridges this: - `Config::async_support(true)`. - Host functions registered with `func_wrap_async` (or via `async: true` in `bindgen!`). @@ -659,7 +487,7 @@ All RPC and CoW API I/O is async (alloy / reqwest on the host). wasmtime bridges |------------------|--------------------| | Runtime process | `Engine` (one, shared) | | Universal API contract | WIT world (`nexum:host/event-module`) | -| CoW API contract | WIT world (`shepherd:cow/shepherd`) | +| Venue adapter contract | WIT world (`videre:venue/venue-adapter`) | | Compiled module | `Component` (cached, thread-safe) | | Pre-validated module | `InstancePre` (linker + component) | | Running instance | `Store` + `Instance` | @@ -669,5 +497,5 @@ All RPC and CoW API I/O is async (alloy / reqwest on the host). wasmtime bridges | Per-call budget | Fuel | | Wall-clock fairness | Epoch interruption | | Memory/table caps | `ResourceLimiter` | -| Async RPC / CoW I/O | `func_wrap_async` + Tokio | +| Async RPC / outbound I/O | `func_wrap_async` + Tokio | | Persistent state | redb (per-module database file, via `local-store` interface host fns) | diff --git a/docs/02-modules-events-packaging.md b/docs/02-modules-events-packaging.md index 15c1e0c4..e9da3b92 100755 --- a/docs/02-modules-events-packaging.md +++ b/docs/02-modules-events-packaging.md @@ -2,7 +2,7 @@ ## Module Package: the Nexum Module Bundle -A module is distributed as a **bundle** - a WASM component plus a manifest that declares its identity, event subscriptions, chain requirements, and resource limits. The manifest is the bridge between packaging, the event system, and the runtime lifecycle. +A module is distributed as a **bundle** - a WASM component plus a manifest that declares its identity, event subscriptions, chain requirements, and capability grants. The manifest is the bridge between packaging, the event system, and the runtime lifecycle. ### Manifest (`module.toml`) @@ -60,7 +60,7 @@ Key design points: - **Chain ids are declared per-subscription**, not in a top-level `[chains]` table - each `[[subscription]]` names its own `chain_id`. If `engine.toml` has no `[chains.]` entry for a chain a subscription names, the engine bails at boot, before any events dispatch (fast, clear error). - **`config`** is opaque to the runtime. 0.2 keeps 0.1's stringly-typed shape (`list>`); the host flattens TOML scalars (numbers, booleans) to their string form on the way through. A typed `config-value` variant is on the 0.3 roadmap, bundled with the manifest-parser work. -> **Future direction (not in 0.2):** per-module resource caps via `[module.resources]` (`max_memory_bytes`, `max_fuel_per_event`, `max_state_bytes`), per-module restart policy via `[module.restart]`, and `optional`-import trap stubs that return `fault.unsupported` on call. The 0.2 engine enforces resource limits using global defaults (`DEFAULT_FUEL_PER_EVENT = 1B`, `DEFAULT_MEMORY_LIMIT = 64 MiB` from `crates/nexum-runtime/src/runtime/limits.rs`) and uses a global restart policy. Per-module overrides are on the 0.3 roadmap. +> Resource caps are engine-global in 0.2, set in `engine.toml` `[limits]` (`fuel_per_event`, default 1B; `memory_bytes`, default 64 MiB; `state_bytes`, default 50 MiB; `event_deadline_secs`, default 120), resolved in `crates/nexum-runtime/src/engine_config.rs`. Per-module `[module.resources]` overrides, per-module restart policy, and `optional`-import trap stubs are 0.3 directions. ### Bundle Format @@ -165,9 +165,9 @@ stateDiagram-v2 | State | Description | |-------|-------------| | **Resolve** | Content store resolves `component` hash to local path. Fail -> `Dead`. | -| **Load** | `Component::from_file`, create `InstancePre`. Validates that the component satisfies the target WIT world (`nexum:host/event-module` or `shepherd:cow/shepherd`). Installs trap stubs for capabilities the manifest declares `optional` but the host does not provide. Fail -> `Dead`. | -| **Init** | Create `Store`, instantiate, call `init(config)` inside an implicit write transaction (same semantics as `on_event` - commit on success, rollback on failure). Module sets up internal state. Fail -> `Restart` (might be transient). | -| **Run** | Runtime dispatches events to `on_event`. Each call gets a fuel budget. Module processes events and may call host imports (chain, local-store, identity, cow-api, etc.). | +| **Load** | `Component::from_file`, create `InstancePre`. Validates that the component satisfies the `nexum:host/event-module` world. Fail -> `Dead`. | +| **Init** | Create `Store`, instantiate, call `init(config)`. Each local-store write commits its own transaction; there is no per-call atomic rollback. Module sets up internal state. Fail -> `Restart` (might be transient). | +| **Run** | Runtime dispatches events to `on_event`. Each call gets a fuel budget. Module processes events and may call host imports (chain, local-store, identity, messaging, logging). | | **Restart** | After a trap or error. Backoff: 1s -> 2s -> 4s -> ... -> 5min cap. A fresh `Store` is created (clean memory), but **local-store data persists** (it's in redb, external to the WASM instance). | | **Dead** | After N consecutive failures (poison pill detection) or explicit operator shutdown. No further event dispatch. Requires manual intervention. | @@ -275,7 +275,7 @@ The runtime serialises event data via the canonical ABI (handled automatically b ## Updated WIT Worlds -The initial WIT in `01-runtime-environment.md` is extended to support the lifecycle and config. The architecture uses two packages: `nexum:host` for universal interfaces and `shepherd:cow` for CoW Protocol extensions. +The initial WIT in `01-runtime-environment.md` is extended to support the lifecycle and config. The universal package is `nexum:host`; CoW Protocol support layers on via `shepherd:cow` (the `cow-events` enum) and the `videre:venue` venue-adapter contract. ### Universal Package: `nexum:host@0.1.0` @@ -409,61 +409,26 @@ world event-module { } ``` -### CoW-Specific Package: `shepherd:cow@0.1.0` +### CoW Protocol packages -```wit -package shepherd:cow@0.1.0; - -interface cow-api { - use nexum:host/types.{chain-id, fault}; - - /// A raw non-2xx reply, or a typed orderbook rejection parsed host-side. - record http-failure { status: u16, body: option } - record order-rejection { status: u16, error-type: string, description: string, data: option } - - /// A shared host `fault`, a raw HTTP failure, or a typed order rejection. - variant cow-api-error { fault(fault), http(http-failure), rejected(order-rejection) } - - /// HTTP-style request to the CoW Protocol API. - request: func( - chain-id: chain-id, - method: string, - path: string, - body: option, - ) -> result; - - /// Submit a serialised order. (Merged in from the 0.1 `order` interface.) - submit-order: func(chain-id: chain-id, order-data: list) - -> result; -} - -/// CoW Protocol module world - extends event-module with cow-api. -world shepherd { - include nexum:host/event-module; - - import cow-api; -} -``` +`shepherd:cow@0.1.0` carries the `cow-events` enum (canonical CoW on-chain event signatures and topic-0 hashes). Order submission is the `videre:venue@0.1.0` venue-adapter contract: a keeper drives venues through `videre:venue/client`, and each installed adapter component (the CoW venue is the `cow-venue` crate) exports the provider face for one venue. See doc 08. ## Putting It All Together Operator deploys a module: ``` -1. Operator adds entry to runtime config: +1. Operator adds entry to engine.toml: [[modules]] - manifest = "/var/nexum/twap-monitor/module.toml" + path = "/var/nexum/twap-monitor/twap_monitor.wasm" -2. Runtime reads manifest: - - Resolves component content hash → fetches from Swarm/local/OCI - - Verifies integrity (sha256 match) +2. Runtime reads the sibling module.toml and verifies sha256(module.wasm) + against the manifest component hash. 3. Runtime compiles Component, creates InstancePre: - - Validates component satisfies target world - (nexum:host/event-module or shepherd:cow/shepherd) - - Installs trap stubs for any [capabilities].optional imports the host doesn't provide - - Enforces resource limits from manifest + - Validates component satisfies the nexum:host/event-module world + - Cross-checks WIT imports against [capabilities] required + optional (link-time) 4. Runtime calls init(config): - Module receives [config] section as typed key-value pairs @@ -478,7 +443,8 @@ Operator deploys a module: Block 19_000_001 on Arbitrum → Router → twap-monitor's dispatch queue → Tokio task calls on_event(Event::Block(…)) - → Module calls chain::request (via alloy Provider), local-store get, cow-api submit-order + → Module calls chain::request (via alloy Provider), local-store get, + videre:venue/client submit → Returns Ok(()) - runtime logs success 7. On crash: diff --git a/docs/03-module-discovery.md b/docs/03-module-discovery.md index 349e601c..21812b02 100755 --- a/docs/03-module-discovery.md +++ b/docs/03-module-discovery.md @@ -1,279 +1,42 @@ # Module Discovery -Doc 02 defines how modules are packaged (bundle = `module.toml` + `module.wasm`) and how content is fetched by hash (pluggable content store). This document defines how the runtime **discovers which modules to load** - the layer above content resolution. +Doc 02 defines how modules are packaged (bundle = `module.toml` + `module.wasm`) and how content is fetched by hash. This document defines how the runtime **discovers which modules to load**. -Three discovery sources, from simplest to most decentralised: +## 0.2: static (local path) -```mermaid -flowchart TB - subgraph runtime["Nexum Runtime"] - subgraph discovery["Module Discovery"] - static["Static\n(local)"] - ens["ENS\n(name)"] - registry["Registry\n(contract)"] - end - subgraph content["Content Store (doc 02)\nSwarm / IPFS / OCI / local / HTTPS"] - end - static --> content - ens --> content - registry --> content - end -``` - -## 1. Static (local path) - -Operator points the runtime at a local manifest. No on-chain interaction. +The 0.2 engine loads modules from local filesystem paths listed in `engine.toml`. Each `[[modules]]` entry names the compiled component and, optionally, its manifest (defaulting to a sibling `module.toml`): ```toml [[modules]] -source = "static" +path = "/var/nexum/twap-monitor/twap_monitor.wasm" manifest = "/var/nexum/twap-monitor/module.toml" ``` -Use case: local development, air-gapped deployments, CI testing. - -## 2. ENS Name Resolution - -A module author publishes their bundle to Swarm (or IPFS) and associates it with an ENS name. The runtime resolves the name to a content reference, fetches the bundle, and loads it. +This is the whole of discovery in 0.2. Content-addressed resolution (Swarm / IPFS / OCI) and `[[content.sources]]` are not wired: `EngineConfig::modules` resolves a `(component.wasm, module.toml)` pair on disk, nothing more (see `crates/nexum-runtime/src/engine_config.rs`). -### How it works +## 0.3 direction: ENS and on-chain registry -ENS already has native support for content-addressed storage: - -- **`contenthash`** (ENSIP-7 / EIP-1577): binary field that encodes a protocol code + content hash. Swarm is protocol `0xe4`, IPFS is `0xe3`. This is the primary pointer to the bundle. -- **Text records** (ENSIP-5 / EIP-634): arbitrary key-value UTF-8 strings. Applications use reverse-domain keys to avoid collisions. - -A module author sets up their ENS name: - -``` -twap-monitor.shepherd.eth -├── contenthash → 0xe40101fa011b20{32-byte-keccak256} -│ (Swarm reference to the bundle) -├── text: shepherd.version → "0.2.0" -├── text: shepherd.chains → "42161,1" -└── text: shepherd.name → "twap-monitor" -``` +The remainder of this document is design contract for a future minor release, not shipped behaviour. It is retained because the WIT and manifest shapes are stable enough to build against. -The `contenthash` points to the full bundle on Swarm (a directory containing `module.toml` + `module.wasm`). Text records provide lightweight metadata the runtime can read without fetching the bundle - useful for filtering or display. +### ENS name resolution -### Runtime resolution flow +A module author publishes a bundle to Swarm (or IPFS) and points an ENS name at it. The runtime would resolve the name to a content reference, fetch the bundle, and load it. -```mermaid -sequenceDiagram - participant R as Nexum Runtime - participant ENS as ENS Registry - participant Resolver as Resolver Contract - participant Swarm as Content Store (Swarm) - - R->>ENS: 1. Resolve ENS name - ENS-->>R: Resolver contract address - R->>Resolver: 2. resolver.contenthash(namehash) - Resolver-->>R: Encoded content reference - R->>R: 3. Decode: protocol 0xe4 (Swarm) + keccak256 hash - R->>Swarm: 4. Fetch bundle from Swarm - Swarm-->>R: Bundle (module.toml + module.wasm) - R->>R: 5. Verify bundle integrity (sha256 of module.wasm matches manifest) - R->>R: 6. Load module via standard lifecycle (doc 02) -``` - -### Runtime config - -```toml -[[modules]] -source = "ens" -name = "twap-monitor.shepherd.eth" -chain_id = 1 # which chain to resolve ENS on -poll_interval = "5m" # check for updates - -[[modules]] -source = "ens" -name = "ethflow.shepherd.eth" -chain_id = 1 -poll_interval = "5m" -``` +- **`contenthash`** (ENSIP-7 / EIP-1577): binary field encoding a protocol code plus content hash (Swarm `0xe4`, IPFS `0xe3`). The primary pointer to the bundle. +- **Text records** (ENSIP-5 / EIP-634): lightweight metadata (version, chains, name) readable without fetching the bundle. -### Updates - -When the module author publishes a new version, they: -1. Upload the new bundle to Swarm → get new content hash -2. Update the ENS `contenthash` record - -The runtime detects the change on its next poll (or via event - see below), fetches the new bundle, and hot-reloads the module. - -## 3. On-Chain Registry (Contract Events) - -For fully autonomous discovery - the runtime watches a contract for registration events and auto-loads modules without operator intervention. - -### Option A: Dedicated registry contract - -A simple contract where module authors register their ENS name: - -```solidity -// SPDX-License-Identifier: AGPL-3.0 -pragma solidity ^0.8.0; - -interface INexumRegistry { - event ModuleRegistered( - string indexed ensNameHash, - string ensName, - address indexed registrant - ); - event ModuleRemoved( - string indexed ensNameHash, - string ensName - ); - - function register(string calldata ensName) external; - function remove(string calldata ensName) external; -} -``` - -The runtime subscribes to `ModuleRegistered` events, resolves the ENS name from the event, and enters the ENS resolution flow above. - -### Option B: No ad-hoc registry - contracts self-declare via ENS - -This is the more decentralised approach. Instead of a central registry: - -1. **Any contract** can associate itself with a Nexum module by setting a text record on its own ENS name. -2. The runtime watches for `TextChanged` events on the ENS Public Resolver filtered to the `shepherd.module` key. - -For example, ComposableCoW (`composablecow.cow.eth`) sets: - -``` -composablecow.cow.eth -├── text: shepherd.module → "twap-monitor.shepherd.eth" -``` +Resolution flow: resolve ENS name -> resolver `contenthash(namehash)` -> decode protocol + hash -> fetch bundle from the content store -> verify `sha256(module.wasm)` against the manifest -> load via the standard lifecycle (doc 02). On a `contenthash` change the runtime re-fetches and hot-reloads. -This says: "the Nexum module for this contract lives at `twap-monitor.shepherd.eth`". +### On-chain registry -The runtime can either: -- **Poll** known ENS names for `shepherd.module` text records. -- **Watch** `TextChanged` events on the ENS resolver, filtered to the `shepherd.module` key: +For autonomous discovery the runtime would watch a contract for registration events and enter the ENS flow. Three shapes are viable: -``` -event TextChanged( - bytes32 indexed node, - string indexed indexedKey, - string key, // "shepherd.module" - string value // "twap-monitor.shepherd.eth" -); -``` - -### Option C: Wildcard subdomain registry (ENSIP-10) - -A parent name like `modules.shepherd.eth` uses wildcard resolution (ENSIP-10). A resolver contract serves subdomains dynamically: - -``` -twap.modules.shepherd.eth → contenthash of TWAP bundle -ethflow.modules.shepherd.eth → contenthash of Ethflow bundle -*.modules.shepherd.eth → resolved by registry contract -``` - -The wildcard resolver is itself the registry - anyone can register a subdomain. The runtime subscribes to events from the resolver contract to discover new modules. - -This gives us human-readable, permissionless module discovery under a shared namespace. - -### Runtime config for registry discovery - -```toml -[[modules]] -source = "registry" -contract = "0x1234…" # registry contract address -chain_id = 1 -# All modules registered here are auto-loaded - -[[modules]] -source = "ens-watch" -resolver = "0x231b…" # ENS Public Resolver -chain_id = 1 -text_key = "shepherd.module" -# Watch for any ENS name that sets this text record -``` - -## Layered Trust Model - -Discovery is permissionless, but **execution requires operator consent**. The runtime config controls what gets auto-loaded: - -```toml -[discovery] -# "allowlist" - only load modules from these sources -# "auto" - load anything discovered (use with caution) -mode = "allowlist" - -# If mode = "allowlist", only these ENS names / registries are trusted -allowed_ens_names = [ - "twap-monitor.shepherd.eth", - "ethflow.shepherd.eth", -] -allowed_registries = [ - "0x1234…" -] - -# Resource caps applied to ALL discovered modules (override manifest if lower) -[discovery.resource_limits] -max_memory_bytes = 10_485_760 -max_fuel_per_event = 100_000 -``` - -In `auto` mode, the runtime loads any module it discovers (useful for a public "run all CoW automation" node). In `allowlist` mode, discovered modules are staged for operator review. - -## ENS Name Conventions - -Suggested naming under a shared parent (e.g. `shepherd.eth` or a subdomain of the protocol): - -``` -.shepherd.eth - community / independent modules -..eth - protocol-owned modules - -Examples: - twap-monitor.shepherd.eth - ethflow-watcher.shepherd.eth - rebalancer.shepherd.eth - twap.cow.eth -``` - -## How the Pieces Fit Together - -```mermaid -sequenceDiagram - participant Author as Module Author - participant Swarm as Swarm - participant ENS as ENS - participant Runtime as Nexum Runtime - - Note over Author: 1. Write module (Rust/Go/JS/...) - Note over Author: 2. Compile to WASM component - Note over Author: 3. Create module.toml manifest - Author->>Swarm: 4. Upload bundle - Swarm-->>Author: Content hash (bzz:abc123...) - Author->>ENS: 5. Set contenthash on twap-monitor.shepherd.eth - - Note over Runtime: 6. Config: source="ens", name="twap-monitor.shepherd.eth" - Runtime->>ENS: 7. Resolve ENS → contenthash - ENS-->>Runtime: Content reference - Runtime->>Swarm: 8. Fetch bundle - Swarm-->>Runtime: Bundle - Runtime->>Runtime: 9. Verify integrity (hash match) - Runtime->>Runtime: 10. Load module (compile, init, run) - - Note over Author, Runtime: On update - Author->>Swarm: 11. Upload new bundle - Swarm-->>Author: New content hash - Author->>ENS: 12. Update contenthash - Runtime->>ENS: 13. Detect change (poll/event) - ENS-->>Runtime: New content reference - Runtime->>Swarm: 14. Fetch new bundle - Swarm-->>Runtime: New bundle - Runtime->>Runtime: 15. Hot-reload module -``` +- **Dedicated registry contract** emitting `ModuleRegistered` / `ModuleRemoved`. +- **ENS self-declaration**: a contract sets a `shepherd.module` text record on its own ENS name pointing at the module; the runtime watches `TextChanged` filtered to that key. +- **Wildcard subdomain registry** (ENSIP-10): `*.modules.shepherd.eth` resolved by a registry contract that anyone can register a subdomain against. -## Summary +### Layered trust -| Discovery Method | Decentralisation | Operator Effort | Use Case | -|-----------------|------------------|-----------------|----------| -| Static (local path) | None | Manual | Dev, CI, air-gapped | -| ENS (named) | High | Configure names | Production, known modules | -| Registry (contract) | Full | Point at contract | Public nodes, auto-discovery | -| ENS self-declare | Full | Watch resolver | Protocol-native automation | +Discovery is permissionless; execution requires operator consent. A `[discovery]` config would gate what auto-loads (`mode = "allowlist"` with `allowed_ens_names` / `allowed_registries`, versus `mode = "auto"` for public nodes) and apply resource caps to discovered modules. -All methods converge on the same flow: resolve a content reference → fetch via content store → verify → load via module lifecycle. +All methods converge on the same flow: resolve a content reference -> fetch via content store -> verify hash -> load. diff --git a/docs/04-state-store.md b/docs/04-state-store.md index 96d5434b..0cca3712 100755 --- a/docs/04-state-store.md +++ b/docs/04-state-store.md @@ -1,48 +1,21 @@ # Local Store Architecture -## Overview - -Every Nexum module has access to a persistent key-value store that survives restarts, crashes, and module updates. The store is backed by **redb** (v3.1, pure Rust, embedded, ACID, MVCC) and exposed to modules through the `local-store` WIT interface. - -The local store is the only durable memory a module has - WASM linear memory is wiped on every restart. Modules must be written to reconstruct their working state from the store on `init`. +Every Nexum module has a persistent key-value store that survives restarts, crashes, and module updates, backed by **redb** (v3.1, pure Rust, embedded, ACID, MVCC) and exposed through the `local-store` WIT interface. It is the only durable memory a module has: WASM linear memory is wiped on every restart, so modules reconstruct working state from the store on `init`. ## redb Fundamentals | Property | Detail | |----------|--------| | Engine | Copy-on-write B-tree | -| Concurrency | MVCC - concurrent readers, single writer, no blocking | -| Durability | Crash-safe by default (fsync on commit) | -| Transactions | Full ACID - read txns and write txns | -| Key types | `&str`, `&[u8]`, integers, tuples, `Option`, fixed arrays | -| Value types | All key types + `Vec`, `f32`/`f64`, `()` | -| Size | No hard limit; v3 file format starts at ~50 KiB | +| Concurrency | MVCC: concurrent readers, single writer | +| Durability | Crash-safe (fsync on commit) | +| Transactions | Full ACID | ## Isolation Model -Each module gets its own **redb database file**. Modules cannot read or write each other's state - enforced by filesystem-level separation. - -```rust -// Runtime side - one database per module -fn open_module_db(module_id: &str) -> Result { - let path = format!("/var/nexum/state/{module_id}.redb"); - Database::create(&path) -} - -// Single table within each module's database -const LOCAL_STORE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("state"); -``` - -Module identity = `name` from `module.toml`. If two module instances share a name, they share state (intentional - enables hot-reload with state continuity). Different modules have different names and fully isolated database files. - -``` -/var/nexum/state/ -├── twap-monitor.redb → { "last_block": [...], "posted_parts": [...], ... } -├── ethflow-watcher.redb → { "pending_orders": [...], ... } -└── price-alert.redb → { "thresholds": [...], ... } -``` +A single redb file lives under `EngineConfig.engine.state_dir`. Every key is namespaced host-side by a fixed 32-byte prefix `keccak256(module_name)` prepended before the raw key, so modules sharing a key string see disjoint data and cannot forge a key into another module's range. keccak256 matches ENS node derivation (see [ADR-0003](adr/0003-local-store-namespacing.md)). The module never observes the prefix. -This per-file design ensures concurrent modules never contend on write locks (see Concurrency section below). +Module identity is `name` from `module.toml`. Two instances sharing a name share a namespace (intentional: hot-reload with state continuity). ## WIT Interface @@ -50,246 +23,51 @@ This per-file design ensures concurrent modules never contend on write locks (se interface local-store { use nexum:host/types.{fault}; - /// Get a value by key. Returns none if key doesn't exist. get: func(key: string) -> result>, fault>; - - /// Set a key-value pair. Overwrites existing value. - /// Returns fault.invalid-input or fault.internal on failure. - /// Quota exhaustion surfaces as fault.invalid-input. set: func(key: string, value: list) -> result<_, fault>; - - /// Delete a key. No-op if key doesn't exist. delete: func(key: string) -> result<_, fault>; - - /// List keys matching a prefix. Returns keys only (not values). list-keys: func(prefix: string) -> result, fault>; + contains: func(key: string) -> result; + len: func(key: string) -> result, fault>; + count: func(prefix: string) -> result; } ``` -In 0.1 `local-store` errors were bare `string` values. 0.2 replaces them with the shared `fault` vocabulary so modules can match on the `fault` case rather than parsing error strings. The interface is the failure domain, so it reports `fault` directly with no subsystem tag. +`contains`, `len`, and `count` answer existence, value length, and prefix cardinality without transferring the value or materialising the key list. Errors are the shared `fault` vocabulary; the interface is its own failure domain, so it reports `fault` directly. Keys are UTF-8 strings; values are opaque bytes. -Keys are UTF-8 strings. Values are opaque bytes - the SDK provides typed wrappers (see doc 05). - -`list-keys` enables prefix-based namespacing within a module's state: +`list-keys` and `count` enable prefix-based namespacing within a module: ``` orders/active/0x1234 → [serialised order] orders/active/0x5678 → [serialised order] -orders/completed/… → [serialised order] list_keys("orders/active/") → ["orders/active/0x1234", "orders/active/0x5678"] ``` ## Transaction Semantics -Both `init` and `on_event` execute within an **implicit write transaction**: - -```mermaid -flowchart TD - A["Event arrives (or init called)"] --> B["Runtime opens redb WriteTransaction"] - B --> C["Calls module init(config) or on_event(event)"] - C --> D["module calls local-store::set('key', value) -- buffered in txn"] - C --> E["module calls local-store::get('key') -- reads from txn (sees own writes)"] - C --> F["module calls local-store::delete('key') -- buffered in txn"] - D --> G["Call returns Ok(())"] - E --> G - F --> G - G --> H["Runtime commits WriteTransaction"] - H --> I["State changes are durable"] -``` - -**On failure** (trap, fuel exhaustion, explicit `Err`): - -```mermaid -flowchart TD - A["Call traps / returns Err"] --> B["Runtime aborts WriteTransaction"] - B --> C["No state changes persisted -- atomically rolled back"] -``` - -This gives us **all-or-nothing semantics per call**: either all state mutations from a single `init` or `on_event` callback are applied, or none are. This is critical for correctness - a module that crashes halfway through processing a block doesn't leave behind partial state. Equally, a failed `init` during restart doesn't corrupt state from the previous version. - -### Read-your-own-writes - -Within a single `on_event` call, a module sees its own uncommitted writes: - -```rust -local_store::set("counter", &42u64.to_le_bytes())?; -let val = local_store::get("counter")?; -// val == Some([42, 0, 0, 0, 0, 0, 0, 0]) ✓ -``` - -This works because all operations within one event go through the same `WriteTransaction`. - -### Concurrency: One Database Per Module - -redb allows only **one `WriteTransaction` at a time** per `Database` - a second `begin_write()` blocks until the first commits or aborts. Since modules dispatch events concurrently (doc 02), a single shared redb file would serialise all write transactions across modules, negating concurrency. - -**Design decision:** each module gets its own redb `Database` file: - -``` -/var/nexum/state/ -├── twap-monitor.redb -├── ethflow-watcher.redb -└── price-alert.redb -``` - -This gives true write isolation - module A's transaction never blocks module B. The cost is more file handles (one per module), which is negligible for the expected module count. - -Within a single module, events are already sequential (doc 02 dispatch semantics), so there is never contention on a module's own database. +Each host call is its own redb transaction: a `get` / `contains` / `len` / `count` / `list-keys` opens a read transaction, and a `set` / `delete` opens a write transaction and commits (fsync-durable) before returning. There is no transaction spanning a whole `on_event`, so a module that traps midway through processing an event keeps whatever writes already committed; per-event atomicity is the module's responsibility (checkpoint a "last processed" key last, and make `init` idempotent). A `set` rejected for quota aborts its own write untouched. ## Size Enforcement -The manifest declares `max_state_bytes`. The runtime tracks total bytes stored per module and rejects `local-store::set` calls that would exceed the limit: - -```rust -// Host-side enforcement (simplified) -impl local_store::Host for NexumHostState { - async fn set(&mut self, key: String, value: Vec) -> Result> { - let new_size = self.state_bytes_used - - self.current_value_size(&key) - + key.len() + value.len(); - - if new_size > self.module_config.max_state_bytes { - return Ok(Err(Fault::InvalidInput("state quota exceeded".into()))); - } - - self.write_txn.insert(&*key, value.as_slice())?; - self.state_bytes_used = new_size; - Ok(Ok(())) - } -} -``` - -The tracking is approximate (doesn't account for B-tree overhead) but sufficient for enforcing a meaningful cap. +A module's namespace is capped at the engine-global `[limits].state_bytes` quota (default 50 MiB). `set` charges the on-disk footprint (prefix + key + value + a fixed per-entry overhead), summed across the namespace's keys, and rejects an over-quota write with `fault.invalid-input`, leaving the store untouched. The footprint is tracked by an incremental per-namespace counter, seeded once by a prefix-range scan. ## State Lifecycle -### Init / Cold Start - -On first load, the module's table is empty. The module's `init` function should handle this: - -```rust -fn init(config: Config) -> Result<(), Fault> { - if local_store::get("initialized")?.is_none() { - // First run - set up initial state - local_store::set("initialized", &[1])?; - local_store::set("last_block", &0u64.to_le_bytes())?; - } - Ok(()) -} -``` - -### Restart (crash recovery) - -On restart, the module gets a fresh WASM instance but the **same state table**. The last committed transaction's data is intact. Any in-flight transaction from the crashed event was rolled back. - -The module should read its checkpoint from state in `init` and resume: - -```rust -fn init(_config: Config) -> Result<(), Fault> { - let last_block = local_store::get("last_block")? - .map(|b| u64::from_le_bytes(b.try_into().unwrap())) - .unwrap_or(0); - logging::log(Level::Info, &format!("resuming from block {last_block}")); - Ok(()) -} -``` - -### Module Update (new version, same name) - -When a module is updated (new WASM binary, same `name` in manifest), the new version inherits the existing state table. The new version's `init` is responsible for any migration: - -```rust -fn init(config: Config) -> Result<(), Fault> { - let version = local_store::get("schema_version")? - .map(|b| u64::from_le_bytes(b.try_into().unwrap())) - .unwrap_or(0); - - if version < 2 { - // Migrate from v1 → v2 schema - migrate_v1_to_v2()?; - local_store::set("schema_version", &2u64.to_le_bytes())?; - } - Ok(()) -} -``` - -### Module Removal - -When an operator removes a module, its state table can optionally be: -- **Retained** (default) - in case the module is re-added later. -- **Purged** - operator explicitly requests deletion via CLI. - -```bash -nexum state purge --module twap-monitor -``` - -## Backup and Compaction - -redb supports online reads during writes (MVCC), so backup is straightforward: - -```rust -// Runtime holds a read transaction, copies the file -let _guard = db.begin_read()?; -std::fs::copy("state.redb", "state.redb.backup")?; -``` - -Compaction (`db.compact()`) reclaims space from deleted keys. The runtime can run this periodically or on operator command. - -## Host-Side Implementation Sketch - -```rust -const LOCAL_STORE_TABLE: TableDefinition<&str, &[u8]> = TableDefinition::new("state"); - -struct ModuleStateCtx { - db: Database, // per-module database file - max_bytes: usize, - bytes_used: usize, - write_txn: Option, -} - -impl ModuleStateCtx { - /// Called by runtime before dispatching init or on_event - fn begin(&mut self) -> Result<()> { - self.write_txn = Some(self.db.begin_write()?); - Ok(()) - } - - /// Called by runtime after successful return - fn commit(&mut self) -> Result<()> { - if let Some(txn) = self.write_txn.take() { - txn.commit()?; - } - Ok(()) - } - - /// Called by runtime on failure/trap - fn rollback(&mut self) { - // WriteTransaction::drop aborts automatically - self.write_txn.take(); - } - - fn table<'txn>( - &self, - txn: &'txn WriteTransaction, - ) -> Result> { - txn.open_table(LOCAL_STORE_TABLE) - } -} -``` +- **Cold start.** On first load the namespace is empty; `init` seeds any initial keys. +- **Restart.** A crash yields a fresh WASM instance over the same namespace. The last committed write is intact. `init` reads its checkpoint and resumes. +- **Update.** A new version (same `name`) inherits the namespace; its `init` handles any schema migration. +- **Removal.** A removed module's keys are retained by default. ## Summary | Concern | Design | |---------|--------| | Backend | redb v3.1 (pure Rust, ACID, MVCC) | -| Isolation | One database file per module (keyed by `name`) | -| Key type | UTF-8 string | -| Value type | Opaque bytes (`list` in WIT) | -| Namespacing within module | Convention: slash-separated prefixes + `list-keys` | -| Transaction scope | Per `init` / `on_event` call - commit on success, rollback on failure | -| Read-your-own-writes | Yes (same `WriteTransaction`) | -| Size limit | Enforced per-module via manifest `max_state_bytes` | -| Survives restart | Yes - state is external to WASM instance | -| Module update | New version inherits state; `init` handles migration | -| Backup | Online copy under read transaction | +| File layout | Single redb file under `state_dir` | +| Isolation | 32-byte `keccak256(module_name)` key prefix (ADR-0003) | +| Key / value | UTF-8 string / opaque bytes (`list`) | +| Namespacing within module | Slash-separated prefixes + `list-keys` / `count` | +| Transaction scope | Per host call, committed on return | +| Size limit | Per-namespace quota from `[limits].state_bytes` | +| Survives restart | Yes, external to the WASM instance | diff --git a/docs/diagrams/README.md b/docs/diagrams/README.md index ada2eb5e..756b05f3 100644 --- a/docs/diagrams/README.md +++ b/docs/diagrams/README.md @@ -1,14 +1,16 @@ # Diagrams -Mermaid sources and rendered PNGs covering the engine architecture, the CoW workflows that the M2 modules implement (TWAP and EthFlow, both as guest modules using low-level host primitives), and the engine internals that new contributors most often need to reason about. +Mermaid sources and rendered PNGs covering the engine architecture, the CoW workflows (TWAP and EthFlow), and the engine internals that new contributors most often need to reason about. + +> The rendered CoW-flow diagrams predate the venue-adapter rework: they show a `shepherd:cow/cow-api` host interface and an in-engine `OrderBookPool`. Order submission is now the `videre:venue` venue-adapter contract (the CoW venue is the `cow-venue` crate) and `shepherd:cow` carries only the `cow-events` enum. Treat the CoW-submission path in these diagrams as historical until the sources are regenerated. ## Architecture and CoW flows | File | Type | Shows | |---|---|---| -| `architecture.png` / `.mmd` | Component | Static view: external infra, nexum internals, WASM modules (twap-monitor, ethflow-watcher) consuming low-level host primitives, and the `cowprotocol` crate (consumed via `[patch.crates-io]` and the wasm32 feature). The `shepherd:cow` package contains only `cow-api`; no specialised TWAP or EthFlow interfaces. | -| `sequence-ethflow.png` / `.mmd` | Sequence | `OrderPlacement` on-chain event handled entirely in the `ethflow-watcher` guest module: `alloy_sol_types` decodes the event, the module builds an `OrderCreation` with the EIP-1271 signing scheme using `cowprotocol` types, and submits via `cow-api/submit-order`. The orderbook error path runs through `OrderPostError::try_from(cow-api-error).retry_hint()`. | -| `sequence-twap.png` / `.mmd` | Sequence | `ConditionalOrderCreated` registration plus the per-block polling loop driven by the `twap-monitor` guest module: `alloy_sol_types` decodes registrations and `eth_call` returns, the module makes the `getTradeableOrderWithSignature` call via `chain.request`, builds `OrderCreation` via `cowprotocol` types, and submits via `cow-api/submit-order`. Orderbook errors flow through `OrderPostError::retry_hint`. | +| `architecture.png` / `.mmd` | Component | Static view: external infra, nexum internals, WASM modules (twap-monitor, ethflow-watcher) consuming host primitives, and the `cowprotocol` crate (consumed via `[patch.crates-io]` and the wasm32 feature). | +| `sequence-ethflow.png` / `.mmd` | Sequence | `OrderPlacement` on-chain event handled in the `ethflow-watcher` guest module: `alloy_sol_types` decodes the event, the module builds an order with the EIP-1271 signing scheme using `cowprotocol` types, and submits it. | +| `sequence-twap.png` / `.mmd` | Sequence | `ConditionalOrderCreated` registration plus the per-block polling loop in the `twap-monitor` guest module: `alloy_sol_types` decodes registrations and `eth_call` returns, the module makes the `getTradeableOrderWithSignature` call via `chain.request`, builds the order via `cowprotocol` types, and submits it. | ## Engine internals (for contributors) diff --git a/docs/diagrams/diagrams.md b/docs/diagrams/diagrams.md index 8e67ba53..5bd8ba1e 100644 --- a/docs/diagrams/diagrams.md +++ b/docs/diagrams/diagrams.md @@ -1,8 +1,8 @@ # Shepherd - Architecture Diagrams -Visual reference for the Shepherd engine, its interactions with Nexum, CoW Protocol, and the WASM module layer. Derived from ADRs 0001–0008 and the internal architecture document. +Visual reference for the Shepherd engine, its interactions with Nexum, CoW Protocol, and the WASM module layer. -> **Scope note** - diagrams 1–4 and 7–8 reflect the **M1 implemented state** plus the **M2 target design** as described by the ADRs. Diagrams 5–6 (TWAP, EthFlow) describe **guest-module-driven flows**: the modules do all the protocol work themselves using low-level host primitives, with no specialised `twap` or `ethflow` host interfaces. Where the current code differs from the target design, a note is included in the relevant block reference. +> **Scope note.** These diagrams predate the venue-adapter rework and still show a `shepherd:cow/cow-api` host interface with an in-engine `OrderBookPool`. Current: order submission is the `videre:venue` venue-adapter contract (a keeper drives venues through `videre:venue/client`; the CoW venue is the `cow-venue` crate), and `shepherd:cow` carries only the `cow-events` enum. Read the CoW-submission path below as historical. The universal-primitive, boot, dispatch, and lifecycle diagrams remain accurate. --- @@ -188,8 +188,8 @@ graph TD | **identity · messaging · remote-store** | Capabilities stubbed at 0.2 - they return `Unsupported`. `identity` will provide keystore-backed signing. `messaging` will send Waku messages. `remote-store` will read/write Swarm/IPFS. | | **logging** | Lightweight utility. `logging` emits to the engine's `tracing` subscriber (inherits `RUST_LOG` filters). Time and secure randomness are available ambiently via `wasi:clocks` and `wasi:random`. | | **(outbound HTTP)** | Not a `nexum:host` interface: a module that declares the `http` capability imports the standard `wasi:http/outgoing-handler`, and the host checks every outgoing request against the manifest's `[capabilities.http].allow` list before any connection is made. | -| **shepherd:cow@0.1.0** | The CoW Protocol extension package. Imports `nexum:host/types` for shared types so modules don't re-define `chain-id` or `chain-log`. Only CoW-aware modules need to import this package. Contains exactly **one** interface in 0.2: `cow-api`. | -| **cow-api** | Generic orderbook access. `request` is a raw REST passthrough (returns JSON string). `submit-order` takes raw order bytes and returns a `result` where the string is the order UID. Routes through the engine's `OrderBookPool`. This is the only protocol-level CoW interface in 0.2 - the boundary between "what CoW Protocol *is*" (orderbook submission, order types) and "what's implemented *on top* of CoW" (TWAP polling, EthFlow event handling). | +| **shepherd:cow@0.1.0** | The CoW Protocol package. In current code it carries a single interface, `cow-events`: the canonical decoded on-chain event enum (`ConditionalOrderCreated`, `ConditionalOrderRemoved`, `OrderPlacement`) with pinned topic-0 hashes, parity-tested against keeper constants and manifests. The `cow-api` host interface the diagram shows is gone. | +| **cow-api** (historical) | Orderbook access was once a host interface (`request` REST passthrough + `submit-order`) backed by an in-engine `OrderBookPool`. Order submission is now the `videre:venue` venue-adapter contract: a keeper calls `videre:venue/client`, and the `cow-venue` adapter component speaks the CoW orderbook. See doc 08. | | **(no twap interface)** | Per ADR-0006, no specialised TWAP host interface exists. The TWAP module implements polling, decoding, and submission entirely in guest code, using `chain.request` for `eth_call`, `local-store` for state, `alloy_sol_types` (in-module) for ABI decoding, `cowprotocol` types for `OrderCreation`, and `cow-api.submit-order` for orderbook submission. Multiple TWAP strategies can coexist as separate modules with different polling policies and error tolerances. | | **(no ethflow interface)** | Per ADR-0006, no specialised EthFlow host interface exists. The EthFlow module decodes `OrderPlacement` directly in guest code via `alloy_sol_types`, constructs the `OrderCreation` with the EIP-1271 signing scheme via `cowprotocol` types, and submits via `cow-api`. |