diff --git a/SUMMARY.md b/SUMMARY.md index 47d1dc9..94882e5 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -103,3 +103,9 @@ - [Echanges: Coinbase](tools/exchanges-coinbase/README.md) - [HTTP](tools/http/README.md) - [Templating: Jinja](tools/templating-jinja/README.md) + +## Looking for a home + +* [nexus/adr/x3dh-removal-from-nexus.md](nexus/adr/x3dh-removal-from-nexus.md) +* [nexus/guides/tool-communication.md](nexus/guides/tool-communication.md) +* [nexus/packages/reference/nexus_workflow/network_auth.md](nexus/packages/reference/nexus_workflow/network_auth.md) diff --git a/nexus/TAP/default-tap.md b/nexus/TAP/default-tap.md index dfedffc..489cd12 100644 --- a/nexus/TAP/default-tap.md +++ b/nexus/TAP/default-tap.md @@ -9,12 +9,10 @@ The Default TAP is a useful helper component for Nexus agent developers. It serv The Default TAP implements the [Nexus Interface V1][nexus-interface-v1] specification, which defines the required functionality for any Talus Agent Package to integrate with the Nexus workflow engine. Key interface requirements include: 1. **Version Management** - - Must declare and maintain interface version compatibility. - Must support version checking for backward compatibility. 1. **Workflow Management** - - Must handle worksheet management and state tracking. - Must support tool evaluation confirmation. @@ -147,13 +145,11 @@ fun get_witness(self: &DefaultTAP): &DefaultTAPV1Witness { The Default TAP works in conjunction with the Nexus workflow engine, which provides: 1. **DAG Implementation** - - Directed Acyclic Graph data structure for modeling complex workflows. - Support for vertices, edges, and input/output ports. - Entry group management for workflow initiation. 1. **Tool Registry** - - Registration and management of available tools. 1. **Tool Invocation** @@ -297,7 +293,6 @@ public(package) fun new(ctx: &mut TxContext) { /// Invokes the provided entry vertex on a DAG with the provided input data for /// each input port. -#[allow(lint(share_owned))] public fun begin_dag_execution( self: &mut DefaultTAP, dag: &DAG, diff --git a/nexus/adr/x3dh-removal-from-nexus.md b/nexus/adr/x3dh-removal-from-nexus.md new file mode 100644 index 0000000..204337b --- /dev/null +++ b/nexus/adr/x3dh-removal-from-nexus.md @@ -0,0 +1,117 @@ +# ADR-1: Removal of X3DH+ Encryption from Nexus + +**Status:** Proposed +**Date:** 2025-02-17 +**Authors:** David ([@davidrotari19](https://github.com/davidrotari19)) +**Deciders:** Pavel ([@kouks](https://github.com/kouks)), David ([@davidrotari19](https://github.com/davidrotari19)), Isaac, Augusto +**Consulted:** Stephen, Christos +**Informed:** Engineering Team + +**Constraint Tags:** Technical | Architectural | Timeline + +--- + +## Context + +### What problem are we solving? + +The current X3DH+ encryption implementation places decryption authority in the Leader. This design conflicts with our eventual goal of a permissionless leader network, where tools—not the Leader—should hold authority over secrets. + +The encryption was intended to protect secrets (API keys) and sensitive data (prompts, chat completions) in Leader-Tool communication. However, the current design where Leader manages encryption context: + +1. **Does not align with target architecture** - Authority should reside in tools, not Leader +1. **Requires complete redesign** - Stephen's intended design places encryption responsibility in tools, which is incompatible with current implementation +1. **Is not achievable within timeline** - Proper redesign cannot be completed before March mainnet + +For the AvA Gaming use case specifically, the identified encryption needs are solvable without X3DH+: + +- **API keys**: Solvable with "fat tools" that manage their own keys and charge per invocation +- **Prompts/completions**: Product decision—plaintext on-chain (via Walrus) is acceptable since all users have equal access to the same information, maintaining fair market dynamics + +### Relevant Constraints + +**Architectural:** Current encryption places authority in Leader; target architecture requires authority in tools. These are fundamentally incompatible approaches. + +**Timeline:** March mainnet deadline does not permit the complete redesign required to implement encryption correctly. + +**Product:** For AvA Gaming, on-chain visibility of game state data is acceptable—tech-savvy users having seconds of advantage is fair given equal access to information. + +### Architectural Position + +This decision affects the **Capability Layer** (Leader-Tool communication protocol). Rather than implement an ad-hoc solution that contradicts our architectural direction, we remove encryption entirely and solve the underlying needs through alternative mechanisms. + +--- + +## Decision + +### What are we doing? + +We will remove X3DH+ encryption from Nexus entirely. The needs it was intended to address will be solved through alternative mechanisms: + +- **API key protection**: Tools manage their own keys internally ("fat tools") and charge users per invocation +- **Sensitive data (prompts/completions)**: Stored in Walrus; plaintext is acceptable given equal access for all users + +### Why this approach? + +Implementing encryption properly requires authority to reside in tools, not Leader. The current implementation inverts this, and correcting it requires a complete redesign we cannot complete before March mainnet. Rather than ship an ad-hoc solution that contradicts our architectural direction, we remove encryption and solve the actual requirements (API key security, fair game dynamics) through simpler mechanisms that align with our target architecture. + +--- + +## Alternatives Considered + +### Alternative 1: Continue with current Leader-based encryption + +**Description:** Complete David's in-progress changes to the existing encryption design. + +**Why not:** The design fundamentally places authority in the wrong component (Leader instead of tools). Completing it would ship something incompatible with our eventual permissionless leader network goal. + +### Alternative 2: Redesign encryption with tool-based authority + +**Description:** Implement Stephen's design where tools manage their own encryption context. + +**Why not:** Requires complete redesign incompatible with current implementation. Timeline to March mainnet does not permit this scope of work. + +### Alternative 3: Do Nothing (keep partial implementation) + +**Description:** Leave encryption code in place but incomplete. + +**Why not:** Adds complexity without benefit. Encryption is a prerequisite for leader distribution, so incomplete implementation blocks other work while providing no security value. + +--- + +## Consequences + +### Positive Consequences + +- Development effort redirected to agents and other critical path items +- Avoids shipping architecture that contradicts long-term direction +- Simplifies Leader implementation ahead of distribution work +- Existing encryption code may be reusable in Nexus SDK for tool developers implementing DTP + +### Negative Consequences + +- Prompts and chat completions visible in plaintext on Walrus (mitigated: equal access maintains fairness) +- API keys must be managed within tools rather than passed through workflows +- Time invested in X3DH+ implementation is sunk cost + +### Neutral Consequences + +- Tools become "fatter" with more responsibility for managing their own authority and secrets +- Tool pricing must account for resource usage (e.g., OpenAI token costs) + +### Reversibility Assessment + +- **Reversibility:** Moderate +- **Reversal cost:** Future encryption implementation should follow tool-based authority design, not resurrect Leader-based approach +- **Point of no return:** None—this decision explicitly defers proper encryption to post-mainnet + +--- + +## Context Evolution Tracking + +### Review Schedule + +- **Next review:** Post-mainnet, when designing tool-based encryption for permissionless leader network +- **Review criteria:** New use cases requiring encrypted data that cannot be solved with fat tools; move toward permissionless leader network + +--- diff --git a/nexus/crates/leader.md b/nexus/crates/leader.md index eba2db5..5142ecd 100644 --- a/nexus/crates/leader.md +++ b/nexus/crates/leader.md @@ -2,14 +2,14 @@ ## Sequence diagram -High-level sequence diagram describing the duties of the Leader. +High level sequence diagram describing the duties of the Leader. ```mermaid sequenceDiagram - participant RG as [On-chain] Registry - participant WF as [On-chain] Workflow - participant LD as [Off-chain] Leader - participant DB as [Off-chain] Indexer + participant RG as [On chain] Registry + participant WF as [On chain] Workflow + participant LD as [Off chain] Leader + participant DB as [Off chain] Indexer participant TL as [Either] Tool critical initialize service @@ -77,6 +77,10 @@ There are multiple processes running in parallel in the leader node. These are a - Verifies output data from Tools based on its output schemas. - Finally, it halts until a channel is open to send a TX back to Workflow (this can happen at any point during the execution if it errors). +Tool invocation details (HTTPS/TLS, signed HTTP headers, key discovery/caching): + +- [Tool communication (HTTPS + signed HTTP)](../guides/tool-communication.md) + 1. **Workflow communication channel** - N channels can run in parallel where N is the number of `Coin` objects the Leader has available. @@ -92,7 +96,7 @@ There are multiple processes running in parallel in the leader node. These are a ## Checkpoint clock (time sync) -A checkpoint-driven clock provides the leader with a conservative, monotonic view of on-chain time to gate time-sensitive work. It derives bounds from Sui checkpoints, caps drift by observed cadence/headroom, surfaces staleness, and refreshes via gRPC when stale. +A checkpoint-driven clock provides the leader with a conservative, monotonic view of on-chain time to gate time sensitive work. It derives bounds from Sui checkpoints, caps drift by observed cadence/headroom, surfaces staleness, and refreshes via gRPC when stale. - [Checkpoint clock details](./leader-checkpoint-clock.md) @@ -101,7 +105,7 @@ A checkpoint-driven clock provides the leader with a conservative, monotonic vie Some parts of the Leader service use a custom channel implementation that handles indexing of messages sent over this channel, as well as retries and sweeps of stale messages. Notably, the event listener<>event executor and the event executor<>merchant processes communicate via this channel. ### Why queue discipline and resource gating -- Avoid head-of-line blocking when a queued item cannot run (shared-object locks, external rate limits, or missing resources). +- Avoid head of line blocking when a queued item cannot run (shared object locks, external rate limits, or missing resources). - Let domains swap in their own ordering policy without touching channel internals. - Prevent wasted retries by dispatching only when capacity for the payload exists. @@ -142,7 +146,7 @@ flowchart LR **Dispatcher + receiver**: a semaphore enforces the configured capacity. The dispatcher asks the queue discipline for the next ID, activates it, loads the payload, consults the resource pool, and delivers `(payload, handle, retries)` to consumers. The handle’s `ack`/`nack` remove or requeue the message (data, active, retries) and notify the queue discipline. -**Persistence surface**: Redis stores queued IDs, active IDs with timestamps, payload data, and retry counters. Metrics track delivery latency and in-flight counts. +**Persistence surface**: Redis stores queued IDs, active IDs with timestamps, payload data, and retry counters. Metrics track delivery latency and in flight counts. {% hint style="info" %} Note that this channel "assumes" it has a stable Redis connection. There are edge cases, where dropping events is very unlikely, but possible. One such edge case is if sending a message over this channel fails due to Redis being unavailable but the Sui event listener successfully saves the next page cursor to Redis. This can in the future be improved by handling Redis errors within the channel differently (by for example, halting). diff --git a/nexus/flow-controls/looping.md b/nexus/flow-controls/looping.md index 2c6bd26..15ac07a 100644 --- a/nexus/flow-controls/looping.md +++ b/nexus/flow-controls/looping.md @@ -47,12 +47,10 @@ Think of `for_each` as a **map** operation, and `collect` as the **reduce back t Suppose Tool `A` outputs an array `[1, 2, 3]`. 1. A `for_each` edge connects `A` → `B`. - - Tool `B` runs three times with inputs `1`, `2`, and `3`. - Each run produces an incremented number, outputting `2`, `3`, and `4`. 1. A `collect` edge connects `B` → `C`. - - Tool `C` receives `[2, 3, 4]`. This creates a parallel map-like computation: @@ -78,7 +76,6 @@ The **`do_while`** / **`break`** edge pair enables conditional loops. - Both `do_while` and `break` edges must originate from **two distinct output variants** of the **same vertex**. - Both edges **must** be present for the loop to be valid. - On each iteration: - 1. The vertex produces an output. 1. If the **`do_while` edge** is taken, execution loops back, overwriting the input data with the new outputs. 1. If the **`break` edge** is taken, execution exits the loop and continues forward. @@ -87,7 +84,6 @@ The **`do_while`** / **`break`** edge pair enables conditional loops. 1. Tool `A` adds `+1` to a number. 1. Tool `B` checks whether the number is `< 3`. - - If **true**, the `do_while` edge loops back to `A`. - If **false**, the `break` edge continues the walk. @@ -107,6 +103,20 @@ flowchart TD --- +## Static Edges + +Static edges originate from outside a loop and provide data for vertices inside the loop. Normally, vertices inside a loop must wait for all their input ports to be filled with data for the _current iteration_. Static edges bypass this and the same data is re-used for each iteration of the loop. + +{% hint style="info" %} +All entry port data is considered to be static. +{% endhint %} + +{% hint style="warning" %} +Static edges **must be evaluated** before the loop begins. Otherwise the loop might not execute correctly. +{% endhint %} + +--- + ## Loop Execution Limits - Loops are **bounded** with an iteration cap of `0xff` (255 iterations). diff --git a/nexus/glossary.md b/nexus/glossary.md index f8917ef..9f114cd 100644 --- a/nexus/glossary.md +++ b/nexus/glossary.md @@ -1,6 +1,12 @@ # Glossary - **`Tool Registry`** - Onchain object that holds Tool definitions so that the Leader knows where and how to invoke them. +- **`Network Auth`** - Onchain trusted binding registry that maps offchain identities (Tools and Leader nodes) to Ed25519 public keys for message signing and verification (supports rotation/revocation and active key discovery). +- **`Key Binding`** - Per-identity record in Network Auth that stores registered keys, the active key id, and rotation/revocation state. +- **`Key id (kid)`** - Monotonic identifier for a key within a Key Binding, used to support key rotation. +- **`Active key`** - The currently selected key id in a Key Binding; verifiers should accept signatures from this key only. +- **`Proof of identity`** - Onchain capability-based proof that a transaction is authorized to act for an identity (used to create/modify a Key Binding). +- **`Proof of possession (PoP)`** - Signature-based proof that the registrant controls the private key corresponding to a public key being registered (prevents registering keys you don’t control). - **`Tool`** - HTTP service or a smart contract with a predefined interface, executing a specific task. It is a Vertex in the Nexus DAG. - **`DAG`** - Directed acyclic graph describes how outputs from Tools flow into inputs of other Tools. This is a static definition. - **`JSON DAG`** - JSON representation of a DAG with a Nexus provided schema. diff --git a/nexus/guides/tool-communication.md b/nexus/guides/tool-communication.md new file mode 100644 index 0000000..8227e75 --- /dev/null +++ b/nexus/guides/tool-communication.md @@ -0,0 +1,338 @@ +# Tool communication (HTTPS + signed HTTP) + +This guide describes how Nexus **Leader nodes** invoke **off-chain Tools**, how tool invocations are secured, and how public keys are discovered and cached for signature verification. + +It is the implementation contract for: + +- tool developers (what your HTTP server must do, what to deploy, how to rotate keys) +- operator/deployers (how to terminate TLS, what certificates are accepted, what to configure) +- anyone integrating another runtime (what headers/claims must be produced and verified) + +{% hint style="info" %} +This guide is about **off-chain** Tools (HTTP services). On-chain Tool execution is a separate mechanism. +{% endhint %} + +## Overview + +Off-chain Tools are **HTTP servers**. Leader nodes invoke tools by sending: + +- `POST /invoke` with a JSON body (tool input) + +Leader nodes connect to tools over **HTTPS** and validate the tool certificate against **system root certificates** (plus an optional extra root bundle). + +Additionally, invocations can use **signed HTTP**: an application layer Ed25519 signature carried in `X-Nexus-Sig-*` headers. Signed HTTP provides: + +- authentication / provenance (“who authored this request/response”) +- integrity binding to the HTTP message and body +- replay resistance (time bounded validity + nonce) + +Signed HTTP keys are discovered via the on-chain `nexus_workflow::network_auth` registry and cached in Redis by the leader. + +## Terminology and identifiers + +- **Tool id**: the tool fully qualified name (FQN), e.g. `xyz.taluslabs.llm.openai-chat-completion@1`. This is the `tool_id` used in signed HTTP claims. +- **Leader id**: the Sui address of the leader node, used as `leader_id` in signed HTTP claims. +- **`kid`**: a monotonically increasing key identifier (`u64`) used to support key rotation. Verifiers accept signatures from the *active* `kid` only. +- **TLS termination**: a reverse proxy / load balancer that speaks HTTPS to the internet and forwards plaintext HTTP to your tool server (your tool code stays a simple HTTP server). + +## End to end flow + +```mermaid +sequenceDiagram + participant LD as Leader node + participant RS as Redis + participant NA as Sui network_auth + participant TL as Tool (HTTP server) + + LD->>LD: build invoke URL = base_path + "/invoke" (clear query/fragment) + LD->>TL: POST /invoke (JSON body) + Note over LD,TL: HTTPS (TLS cert validated via system roots) + + opt signed HTTP enabled + Note over LD,TL: LD signs request (X-Nexus-Sig-*) + TL-->>TL: verify leader signature (policy) + end + + TL->>LD: HTTP response (JSON body) + + opt tool responds with signature headers + LD->>RS: get cached tool key (kid, pk) + alt cache miss / stale + LD->>NA: read active tool key from on-chain binding + LD->>RS: cache tool key (+ optional negative cache) + end + LD-->>LD: verify tool response signature + end +``` + +## Leader side response verification (decision flow) + +This diagram captures the leader’s runtime behavior when signed HTTP is enabled. + +```mermaid +flowchart TD + A[HTTP response from tool] --> B{signed HTTP enabled?} + B -- no --> Z[Accept response (no signature checks)] + B -- yes --> C{Any X-Nexus-Sig-* headers?} + + C -- no --> D{mode = required?} + D -- yes --> E[Fail: missing signed_http headers] + D -- no --> F[Accept unsigned response] + + C -- yes --> G[Resolve tool key: Redis cache -> on-chain network_auth] + G --> H[Verify signature + claims] + H -- ok --> I[Accept response] + H -- UnknownToolKey --> J[Refresh active key from chain] + J --> K[Verify again] + K -- ok --> I + K -- err --> L{mode = required?} + H -- other err --> L + L -- yes --> M[Fail tool invocation] + L -- no --> N[Warn + accept response] +``` + +## HTTPS / TLS (transport security) + +Leader nodes invoke tools over HTTPS. TLS provides confidentiality + integrity in transit; signed HTTP provides application layer authentication/provenance. + +### What the leader enforces + +- **HTTPS only**: in staging/production the leader refuses to call `http://...` tools. +- **TLS 1.3 only**: in staging/production the leader pins min/max TLS version to 1.3. +- **Certificate validation**: the tool’s certificate chain is validated using **system roots** (like `curl`). +- **Optional extra root**: you can add a PEM bundle via `EXECUTOR_TOOL_TLS_ROOT_PEM_PATH` (e.g. private PKI). + +### Current limitation: no TLS client authentication (mTLS) + +Leader nodes currently do **not** authenticate themselves to tools at the TLS layer (no mTLS). That means: + +- tools cannot rely on TLS client certificates to authenticate the caller +- tools that want “only Leader nodes can call me” must enforce this at the application layer (signed HTTP verification + policy) + +{% hint style="info" %} +mTLS (and support for self signed tool certificates) is expected to be added later. Today, use standard CA signed certificates (or provide a private root bundle to the leader). +{% endhint %} + +### TLS termination patterns (tool deployment) + +Tools do not need to implement TLS themselves. Common production deployments run the tool server behind a TLS terminating reverse proxy / load balancer: + +- **Caddy (recommended for simplicity)**: https://caddyserver.com/docs/automatic-https +- **Nginx + Certbot (Let’s Encrypt)**: https://certbot.eff.org/instructions?ws=nginx +- **Traefik (ACME)**: https://doc.traefik.io/traefik/https/acme/ +- **Cloud load balancers** (AWS ALB/ACM, GCP HTTPS LB, etc.) +- **Cloudflare** (CDN/proxy + TLS): https://developers.cloudflare.com/ssl/ + +Operational notes: + +- ensure your proxy forwards `X-Nexus-Sig-*` headers (some hardening configs strip unknown headers) +- set request/response size limits at the proxy layer (DoS/abuse protection) +- enable keep alives (reduces TLS handshake overhead) + +## Signed HTTP (application layer signatures) + +Signed HTTP v1 uses Ed25519 signatures carried in headers. The tool request/response body remains the normal tool JSON schema; signatures do not wrap the body in an envelope. + +### Headers + +Every signed request/response carries these headers: + +- `X-Nexus-Sig-V`: protocol version (`"1"`) +- `X-Nexus-Sig-Input`: base64url(no pad) of raw JSON claims bytes +- `X-Nexus-Sig`: base64url(no pad) of the 64 byte Ed25519 signature + +If your proxy drops these headers, signed HTTP will fail (and in `required` mode, tool invocation fails closed). + +### What gets signed (high level) + +The signature is over a protocol specific domain separator (request vs response) plus the exact `sig_input` bytes (the JSON encoding of the claims). Claims bind: + +- the sender identity + `kid` +- request intent metadata (method/path/query) +- body integrity (`sha256(body_bytes)`) +- freshness (`iat_ms`, `exp_ms`) +- uniqueness / replay resistance (`nonce`) +- for responses: binding to a specific request (`req_sig_input_sha256` + `nonce`) + +### Time window fields + +Signed HTTP uses millisecond timestamps (UTC) since Unix epoch: + +- `iat_ms`: **issued at** / start of validity window +- `exp_ms`: **expiry** / end of validity window + +Verifiers enforce: + +- allowed clock skew (`max_clock_skew_ms`) +- maximum validity window (`max_validity_ms`) + +### Replay resistance + +`nonce` is the replay key. Tools should track nonce usage (commonly keyed by `(leader_id, nonce)`) so that: + +- identical retries are allowed (same nonce + same request binding) +- conflicting replays are rejected (same nonce used for a different request) + +### Tool side request authentication (recommended) + +Signed HTTP is bidirectional: tools can (and typically should) verify the **leader’s request signature** before executing any work. + +To verify a leader request, the tool needs the leader’s public key for `(leader_id, leader_kid)`. Common approaches: + +- **On-chain discovery**: read the active leader key from `network_auth` and cache it locally (recommended when you want to accept requests from “any Leader node”). +- **Static allowlist**: configure an allowlist of `(leader_id, leader_kid, public_key)` in the tool deployment (recommended when you want strict control over which leaders can call the tool). + +Leader nodes register (and rotate) their message signing keys on-chain when signed HTTP is enabled, so tools can discover them via `network_auth` when using the on-chain approach. + +## Key discovery: on-chain `network_auth` + +`nexus_workflow::network_auth` is a trusted on-chain binding registry from identity → Ed25519 public keys used for message signing and verification. + +It provides: + +- key discovery (active key) +- key rotation (multiple `kid`s, only one active) +- key revocation + +Reference: + +- [`nexus_workflow::network_auth`](../packages/reference/nexus_workflow/network_auth.md) + +## Key rotation (operational flow) + +Key rotation is designed to be seamless for callers/verifiers: the on-chain registry advances `kid`, and verifiers accept signatures from the active `kid` only. + +```mermaid +sequenceDiagram + participant OP as Tool operator + participant NA as Sui network_auth + participant RS as Redis + participant LD as Leader node + participant TL as Tool runtime + + OP->>NA: register new tool key (kid = 1) + set active + OP->>TL: deploy tool runtime with signing key kid = 1 + + LD->>TL: POST /invoke + TL->>LD: signed response (tool_kid = 1) + + Note over LD,RS: cached key still at kid = 0 + LD-->>LD: verify fails: UnknownToolKey + LD->>NA: refresh active key from chain + LD->>RS: cache active key (kid = 1) + LD-->>LD: verify succeeds on retry +``` + +## Leader runtime behavior (implementation contract) + +This section documents what the leader does today so tool developers can integrate reliably. + +### Invoke URL canonicalization + +Given a tool base URL, the leader computes the invoke URL as: + +- `invoke_path = base_path.trim_end_matches('/') + "/invoke"` +- query and fragment are cleared (ignored) + +Examples: + +- `https://api.example.com/tool` → `https://api.example.com/tool/invoke` +- `https://api.example.com/tool/` → `https://api.example.com/tool/invoke` + +### Response size limiting + +Leader nodes read tool responses with a hard size limit: + +- `EXECUTOR_TOOL_MAX_RESPONSE_BYTES` (default `10 MiB`) +- set to `0` to disable the limit + +This is a safety guard against accidental or malicious large responses. + +### Signed HTTP modes + +Leader nodes support: + +- `EXECUTOR_SIGNED_HTTP_MODE=disabled`: do not sign requests; do not verify responses +- `EXECUTOR_SIGNED_HTTP_MODE=optional`: sign/verify when configured, but tolerate missing/invalid signatures (log and accept) +- `EXECUTOR_SIGNED_HTTP_MODE=required`: missing/invalid signatures fail the invocation + +Important edge case: + +- `optional` mode **without** `EXECUTOR_SIGNED_HTTP_SIGNING_KEY` falls back to **unsigned** tool calls (request not signed, response not verified). + +### Response verification strategy + +If signed HTTP is enabled and the response includes signature headers: + +- the leader verifies the response using the tool’s active key from `network_auth` +- the key is cached in Redis +- if verification fails with `UnknownToolKey` (kid mismatch), the leader refreshes the key from chain and retries verification once (supports key rotation) + +In `optional` mode, verification failures are logged and the response is accepted. + +### Tool key caching (Redis) + +Leader nodes cache tool keys in Redis: + +- positive cache entry: `{ kid, public_key, cached_at_ms }` +- negative cache entry: “tool has no active key” marker + +Knobs: + +- `EXECUTOR_TOOL_KEY_CACHE_TTL` +- `EXECUTOR_TOOL_KEY_NEGATIVE_CACHE_TTL` +- `EXECUTOR_TOOL_KEY_CACHE_MAX_STALENESS` + +To reduce stampedes inside one leader process, refresh is deduplicated per tool id using sharded in process locks (not a distributed lock). + +## Tool expectations (developer/operator checklist) + +### Tool developer (HTTP server) + +- Implement `GET /health`, `GET /meta`, `POST /invoke` (see [`docs/tool.md`](../tool.md)). +- Enforce input/output schema validation (recommended) and reject invalid requests. +- If you require “only Leader nodes can call me”, verify signed HTTP requests and apply a policy (allowlist, rate limits, etc). +- If you produce signed HTTP responses, ensure `tool_id` in claims exactly matches your registered tool FQN. + +### Tool operator (deployment) + +- Expose the tool via HTTPS (TLS termination proxy/LB in front of your HTTP server). +- Ensure `X-Nexus-Sig-*` headers are forwarded. +- Set request size limits and rate limits at the edge to protect the tool. +- Manage key rotation: + - register a new key on-chain (new `kid`) + - deploy the tool runtime using the new signing key + - keep the previous key available only for the transition period if needed + +## Troubleshooting + +Common failures and what they usually mean: + +| Symptom | Likely cause | Fix | +|---|---|---| +| `refusing to invoke tool ... over non HTTPS URL` | tool URL is `http://` but HTTPS is required | expose the tool via HTTPS (TLS termination) | +| `missing signed_http headers` | tool didn’t sign response or proxy stripped headers | ensure tool signs; ensure proxy forwards `X-Nexus-Sig-*` | +| `unknown tool key` | tool key not registered/active on-chain, or rotation mismatch | register/activate tool key; retry after propagation | +| `invalid signature` | tool signed with wrong key, wrong `kid`, or claims mismatch | verify tool is using the active key and correct `tool_id` | +| `response body exceeded limit` | response too large | reduce response size or raise `EXECUTOR_TOOL_MAX_RESPONSE_BYTES` | + +{% hint style="info" %} +Signature errors can also be caused by clock skew. Keep leader and tool clocks synchronized (NTP) and avoid very large validity windows. +{% endhint %} + +## Configuration reference (Leader nodes) + +Tool communication is primarily configured via these environment variables: + +- `ENVIRONMENT` +- `EXECUTOR_TOOL_INVOKE_TIMEOUT` +- `EXECUTOR_TOOL_MAX_RESPONSE_BYTES` +- `EXECUTOR_TOOL_TLS_ROOT_PEM_PATH` +- `EXECUTOR_SIGNED_HTTP_MODE` +- `EXECUTOR_SIGNED_HTTP_SIGNING_KEY` +- `EXECUTOR_SIGNED_HTTP_LEADER_KID` +- `EXECUTOR_SIGNED_HTTP_MAX_CLOCK_SKEW_MS` +- `EXECUTOR_SIGNED_HTTP_MAX_VALIDITY_MS` +- `EXECUTOR_TOOL_KEY_CACHE_TTL` +- `EXECUTOR_TOOL_KEY_NEGATIVE_CACHE_TTL` +- `EXECUTOR_TOOL_KEY_CACHE_MAX_STALENESS` diff --git a/nexus/index.md b/nexus/index.md index aeee5e1..51fbd20 100644 --- a/nexus/index.md +++ b/nexus/index.md @@ -9,7 +9,7 @@ For the purposes of this documentation we make distinction between different use - **Nexus maintainer.** Core team member that maintains the Nexus codebase. - **Tool developer.** Outside contributor that develops Tools to be used by Agents. - **Agent developer.** Outside contributor that creates DAGs and subsequently deploys the Agent smart contract. -- **Agent user.** End-user that interacts with the ecosystem through clients built by us or outside contributors. +- **Agent user.** End user that interacts with the ecosystem through clients built by us or outside contributors. ## [Glossary][glossary] @@ -45,7 +45,7 @@ Docs: ## [Tools][tool] -Tools are Vertices in the Nexus workflow DAG. They are services with [Nexus-defined interface][tool] schema that perform specific tasks. These Tools are what Agent Developers orchestrate in a workflow DAG to create an Agent. +Tools are Vertices in the Nexus workflow DAG. They are services with [Nexus defined interface][tool] schema that perform specific tasks. These Tools are what Agent Developers orchestrate in a workflow DAG to create an Agent. There are a few standard Nexus tools, they can be found in the [Nexus SDK repository's tools][nexus-sdk-tools] folder. @@ -58,6 +58,7 @@ Some examples of what a Tool is: Docs: - [Tool][tool] +- [Tool communication (HTTPS + signed HTTP)][tool-communication] ## Flow Controls @@ -79,7 +80,7 @@ Docs: ## Nexus SDK -Nexus offers [tool and agent developers][actors] an easy-to-use SDK consisting of a CLI and Toolkit to streamline their development. The codebase resides in [this repository][nexus-sdk-repo] and is the main entry point for developers to interact with Nexus. It has a separate section in the developer docs dedicated to it. +Nexus offers [tool and agent developers][actors] an easy to use SDK consisting of a CLI and Toolkit to streamline their development. The codebase resides in [this repository][nexus-sdk-repo] and is the main entry point for developers to interact with Nexus. It has a separate section in the developer docs dedicated to it. Docs: @@ -95,6 +96,7 @@ Docs: [ref-api]: ../developer-docs/index/nexus-core-api-docs/README.md [sui-move-conventions]: conventions/sui-move.md [tool]: tool.md +[tool-communication]: guides/tool-communication.md [leader]: crates/leader.md [leader-clock]: crates/leader-checkpoint-clock.md [actors]: #actors diff --git a/nexus/packages/reference/nexus_primitives/data.md b/nexus/packages/reference/nexus_primitives/data.md index 25f72a4..cecae55 100644 --- a/nexus/packages/reference/nexus_primitives/data.md +++ b/nexus/packages/reference/nexus_primitives/data.md @@ -40,7 +40,7 @@ Serialization conventions are agreed on by the off-chain realm. storage: vector<u8>
- Will be b"inline" if stored on chain, or an identifier of the storage. + Will be b"inline" if stored on-chain, or an identifier of the storage. Inline data are useful for short data that can be stored on-chain such as configurations.
diff --git a/nexus/packages/reference/nexus_workflow/network_auth.md b/nexus/packages/reference/nexus_workflow/network_auth.md new file mode 100644 index 0000000..b599169 --- /dev/null +++ b/nexus/packages/reference/nexus_workflow/network_auth.md @@ -0,0 +1,1358 @@ + + + +# Module `(nexus_workflow=0x0)::network_auth` + +## Identity and key bindings + +`network_auth` is an on-chain registry that binds off-chain identities (Leader nodes and Tools) to Ed25519 public keys used for message signing and verification. + +It acts as a trusted binding registry for: + +- Key discovery: find the currently active public key for an identity. +- Key rotation: register multiple keys over time and switch which key is active. +- Key revocation: explicitly revoke keys that should no longer be accepted. + +### Identities + +Identities are represented by `IdentityKey`: + +- `Leader { address }` identifies a Leader node by Sui address. +- `Tool { fqn }` identifies a Tool by its fully-qualified name (FQN). + +### Proofs + +Key registration uses two independent proofs: + +- `ProofOfIdentity`: proves the transaction is authorized to act for the identity (via on-chain capabilities). +- `ProofOfKey`: proof-of-possession (PoP) that the registrant controls the private key for the public key being registered. + +### Verification rule + +Verifiers should accept signatures from the binding’s active key only (`KeyBinding::active_key_id`). Non-active keys are treated as invalid even if they have not been revoked. + + + +- [Struct `NetworkAuth`](#(nexus_workflow=0x0)_network_auth_NetworkAuth) +- [Struct `KeyBinding`](#(nexus_workflow=0x0)_network_auth_KeyBinding) +- [Struct `KeyRecord`](#(nexus_workflow=0x0)_network_auth_KeyRecord) +- [Struct `ProofOfIdentity`](#(nexus_workflow=0x0)_network_auth_ProofOfIdentity) +- [Struct `ProofOfKey`](#(nexus_workflow=0x0)_network_auth_ProofOfKey) +- [Struct `NetworkAuthCreatedEvent`](#(nexus_workflow=0x0)_network_auth_NetworkAuthCreatedEvent) +- [Struct `KeyBindingCreatedEvent`](#(nexus_workflow=0x0)_network_auth_KeyBindingCreatedEvent) +- [Struct `KeyRegisteredEvent`](#(nexus_workflow=0x0)_network_auth_KeyRegisteredEvent) +- [Struct `KeyRevokedEvent`](#(nexus_workflow=0x0)_network_auth_KeyRevokedEvent) +- [Struct `ActiveKeyUpdatedEvent`](#(nexus_workflow=0x0)_network_auth_ActiveKeyUpdatedEvent) +- [Enum `IdentityKey`](#(nexus_workflow=0x0)_network_auth_IdentityKey) +- [Constants](#@Constants_0) +- [Function `new`](#(nexus_workflow=0x0)_network_auth_new) +- [Function `share`](#(nexus_workflow=0x0)_network_auth_share) +- [Function `prove_leader`](#(nexus_workflow=0x0)_network_auth_prove_leader) +- [Function `prove_offchain_tool`](#(nexus_workflow=0x0)_network_auth_prove_offchain_tool) +- [Function `proof_identity`](#(nexus_workflow=0x0)_network_auth_proof_identity) +- [Function `new_proof_of_key`](#(nexus_workflow=0x0)_network_auth_new_proof_of_key) +- [Function `binding_address`](#(nexus_workflow=0x0)_network_auth_binding_address) +- [Function `binding_exists`](#(nexus_workflow=0x0)_network_auth_binding_exists) +- [Function `create_binding`](#(nexus_workflow=0x0)_network_auth_create_binding) +- [Function `register_key`](#(nexus_workflow=0x0)_network_auth_register_key) +- [Function `revoke_key`](#(nexus_workflow=0x0)_network_auth_revoke_key) +- [Function `set_active_key`](#(nexus_workflow=0x0)_network_auth_set_active_key) +- [Function `key_binding_identity`](#(nexus_workflow=0x0)_network_auth_key_binding_identity) +- [Function `key_binding_active_key_id`](#(nexus_workflow=0x0)_network_auth_key_binding_active_key_id) +- [Function `key_binding_next_key_id`](#(nexus_workflow=0x0)_network_auth_key_binding_next_key_id) +- [Function `key_binding_key`](#(nexus_workflow=0x0)_network_auth_key_binding_key) +- [Function `assert_identity`](#(nexus_workflow=0x0)_network_auth_assert_identity) +- [Function `append_bytes`](#(nexus_workflow=0x0)_network_auth_append_bytes) +- [Function `clone_bytes`](#(nexus_workflow=0x0)_network_auth_clone_bytes) + + +
use (nexus_primitives=0x0)::event;
+use (nexus_primitives=0x0)::owner_cap;
+use (nexus_workflow=0x0)::leader_cap;
+use (nexus_workflow=0x0)::tool_registry;
+use std::address;
+use std::ascii;
+use std::bcs;
+use std::option;
+use std::string;
+use std::type_name;
+use std::vector;
+use sui::accumulator;
+use sui::accumulator_metadata;
+use sui::accumulator_settlement;
+use sui::address;
+use sui::bag;
+use sui::balance;
+use sui::bcs;
+use sui::clock;
+use sui::coin;
+use sui::config;
+use sui::deny_list;
+use sui::derived_object;
+use sui::dynamic_field;
+use sui::dynamic_object_field;
+use sui::ed25519;
+use sui::event;
+use sui::funds_accumulator;
+use sui::hash;
+use sui::hex;
+use sui::object;
+use sui::object_bag;
+use sui::party;
+use sui::sui;
+use sui::table;
+use sui::transfer;
+use sui::tx_context;
+use sui::types;
+use sui::url;
+use sui::vec_map;
+use sui::vec_set;
+
+ + + + + +## Struct `NetworkAuth` + +Shared registry for identity key bindings. + + +
public struct NetworkAuth has key
+
+ + + +
+Fields + + +
+
+id: sui::object::UID +
+
+ Object ID of the registry. +
+
+identities: sui::vec_set::VecSet<(nexus_workflow=0x0)::network_auth::IdentityKey> +
+
+ Discoverable set of identities that have a [KeyBinding]. + This enables indexers/tooling to enumerate which identities have created + bindings, without needing to guess identities and derived addresses. +
+
+ + +
+ + + +## Struct `KeyBinding` + +Per-identity key binding stored at a deterministic derived address. + +This object holds the full key lifecycle state for one identity: +- key registration (with PoP), +- active key selection (the only key verifiers accept), +- revocations (for incident response / decommissioning). + + +
public struct KeyBinding has key, store
+
+ + + +
+Fields + + +
+
+id: sui::object::UID +
+
+ Object ID of the binding. +
+
+identity: (nexus_workflow=0x0)::network_auth::IdentityKey +
+
+ Identity this binding belongs to. +
+
+description: std::option::Option<vector<u8>> +
+
+ Optional description for operators and tooling. +
+
+next_key_id: u64 +
+
+ Monotonically increasing key identifier. + This is used as the key id for the next registration and as the PoP + nonce to prevent replay of PoP signatures. +
+
+active_key_id: std::option::Option<u64> +
+
+ Active key identifier for verification. + Off-chain verifiers MUST accept signatures from this key only. + This allows key rotation while keeping verification unambiguous. +
+
+keys: sui::table::Table<u64, (nexus_workflow=0x0)::network_auth::KeyRecord> +
+
+ Key records indexed by key id. +
+
+ + +
+ + + +## Struct `KeyRecord` + +Single key record stored in a binding. + +Keys are append-only (registered under a new key_id) and can be revoked. + + +
public struct KeyRecord has store
+
+ + + +
+Fields + + +
+
+scheme: u8 +
+
+ Key scheme identifier (Ed25519 only). +
+
+public_key: vector<u8> +
+
+ Raw public key bytes. +
+
+added_at_ms: u64 +
+
+ Timestamp when the key was registered. +
+
+revoked_at_ms: std::option::Option<u64> +
+
+ Timestamp when the key was revoked, if any. +
+
+ + +
+ + + +## Struct `ProofOfIdentity` + +Ephemeral proof that the caller is authorized to act for an identity. + +This prevents unauthorized parties from creating bindings or registering keys +for identities they do not control. + + +
public struct ProofOfIdentity has drop
+
+ + + +
+Fields + + +
+
+identity: (nexus_workflow=0x0)::network_auth::IdentityKey +
+
+ Identity proven by on-chain capabilities. +
+
+ + +
+ + + +## Struct `ProofOfKey` + +Ephemeral proof that a key is controlled by the signer. + +This proves possession of the private key corresponding to public_key +without revealing it, and is valid only for a single registration slot +(bound to [KeyBinding::next_key_id]). + + +
public struct ProofOfKey has drop
+
+ + + +
+Fields + + +
+
+scheme: u8 +
+
+ Key scheme identifier (Ed25519 only). +
+
+public_key: vector<u8> +
+
+ Public key proven by proof-of-possession. +
+
+key_id: u64 +
+
+ Key id this proof is valid for. + This must match [KeyBinding::next_key_id] when registering the key. +
+
+ + +
+ + + +## Struct `NetworkAuthCreatedEvent` + +Emitted when a new network auth registry is created. + + +
public struct NetworkAuthCreatedEvent has copy, drop
+
+ + + +
+Fields + + +
+
+registry: sui::object::ID +
+
+ Registry object ID. +
+
+ + +
+ + + +## Struct `KeyBindingCreatedEvent` + +Emitted when a new key binding is created. + + +
public struct KeyBindingCreatedEvent has copy, drop
+
+ + + +
+Fields + + +
+
+binding: sui::object::ID +
+
+ Binding object ID. +
+
+identity: (nexus_workflow=0x0)::network_auth::IdentityKey +
+
+ Identity associated with the binding. +
+
+ + +
+ + + +## Struct `KeyRegisteredEvent` + +Emitted when a key is registered. + + +
public struct KeyRegisteredEvent has copy, drop
+
+ + + +
+Fields + + +
+
+binding: sui::object::ID +
+
+ Binding object ID. +
+
+key_id: u64 +
+
+ Registered key identifier. +
+
+scheme: u8 +
+
+ Key scheme identifier. +
+
+public_key: vector<u8> +
+
+ Public key bytes. +
+
+added_at_ms: u64 +
+
+ Timestamp when the key was registered. +
+
+ + +
+ + + +## Struct `KeyRevokedEvent` + +Emitted when a key is revoked. + + +
public struct KeyRevokedEvent has copy, drop
+
+ + + +
+Fields + + +
+
+binding: sui::object::ID +
+
+ Binding object ID. +
+
+key_id: u64 +
+
+ Revoked key identifier. +
+
+revoked_at_ms: u64 +
+
+ Timestamp when the key was revoked. +
+
+ + +
+ + + +## Struct `ActiveKeyUpdatedEvent` + +Emitted when the active key changes. + + +
public struct ActiveKeyUpdatedEvent has copy, drop
+
+ + + +
+Fields + + +
+
+binding: sui::object::ID +
+
+ Binding object ID. +
+
+active_key_id: std::option::Option<u64> +
+
+ New active key identifier, or none if cleared. +
+
+ + +
+ + + +## Enum `IdentityKey` + +Canonical identity key namespace for key bindings. + +This value is used as: +- the key for the derived [KeyBinding] address, and +- the identity commitment inside PoP signatures (via bcs(IdentityKey)). + + +
public enum IdentityKey has copy, drop, store
+
+ + + +
+Variants + + +
+
+Variant Leader +
+
+ Leader identity keyed by address. +
+ +
+
+address: address +
+
+ Leader address. +
+
+ +
+Variant Tool +
+
+ Tool identity keyed by FQN. +
+ +
+
+fqn: std::ascii::String +
+
+ Fully qualified tool name. +
+
+ +
+ + +
+ + + +## Constants + + + + +Identifier for Ed25519 keys. + + +
const KEY_SCHEME_ED25519: u8 = 0;
+
+ + + + + +Required byte length of an Ed25519 public key. + + +
const ED25519_PUBLIC_KEY_LEN: u64 = 32;
+
+ + + + + +Required byte length of an Ed25519 signature. + + +
const ED25519_SIGNATURE_LEN: u64 = 64;
+
+ + + + + +Default domain separator for proof-of-possession. + + +
const POP_DOMAIN: vector<u8> = vector[110, 101, 120, 117, 115, 95, 119, 111, 114, 107, 102, 108, 111, 119, 46, 110, 101, 116, 119, 111, 114, 107, 95, 97, 117, 116, 104, 46, 112, 111, 112, 95, 118, 49];
+
+ + + + + + + +
#[error]
+const EUnsupportedKeyScheme: vector<u8> = b"Only Ed25519 keys are supported";
+
+ + + + + + + +
#[error]
+const EInvalidPublicKey: vector<u8> = b"Invalid Ed25519 public key length";
+
+ + + + + + + +
#[error]
+const EInvalidSignature: vector<u8> = b"Invalid Ed25519 signature length";
+
+ + + + + + + +
#[error]
+const EInvalidProofOfPossession: vector<u8> = b"Invalid proof of possession";
+
+ + + + + + + +
#[error]
+const EBindingAlreadyExists: vector<u8> = b"Key binding already exists";
+
+ + + + + + + +
#[error]
+const EIdentityMismatch: vector<u8> = b"Proof identity does not match key binding";
+
+ + + + + + + +
#[error]
+const EKeyNotFound: vector<u8> = b"Key not found";
+
+ + + + + + + +
#[error]
+const EKeyAlreadyRevoked: vector<u8> = b"Key already revoked";
+
+ + + + + + + +
#[error]
+const EKeyIdMismatch: vector<u8> = b"Proof key id does not match binding slot";
+
+ + + + + +## Function `new` + +Create a new registry with the empty identity set. + + +
public(package) fun new(ctx: &mut sui::tx_context::TxContext): (nexus_workflow=0x0)::network_auth::NetworkAuth
+
+ + + +
+Implementation + + +
public(package) fun new(ctx: &mut TxContext): NetworkAuth {
+    let registry = NetworkAuth {
+        id: object::new(ctx),
+        identities: vec_set::empty(),
+    };
+    event::emit(NetworkAuthCreatedEvent {
+        registry: object::id(®istry),
+    });
+    registry
+}
+
+ + + +
+ + + +## Function `share` + +Share the registry as a shared object. + + +
public(package) fun share(self: (nexus_workflow=0x0)::network_auth::NetworkAuth)
+
+ + + +
+Implementation + + +
public(package) fun share(self: NetworkAuth) {
+    share_object(self);
+}
+
+ + + +
+ + + +## Function `prove_leader` + +Create proof for the leader (sender) using its leader capability. + +The leader capability serves as the on-chain authorization to act as a +Leader identity and register/rotate keys for IdentityKey::Leader { address: +ctx.sender() }. + + +
public fun prove_leader(_leader_cap: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::leader_cap::OverNetwork>, ctx: &sui::tx_context::TxContext): (nexus_workflow=0x0)::network_auth::ProofOfIdentity
+
+ + + +
+Implementation + + +
public fun prove_leader(
+    _leader_cap: &CloneableOwnerCap<OverNetwork>,
+    ctx: &TxContext,
+): ProofOfIdentity {
+    ProofOfIdentity {
+        identity: IdentityKey::Leader {
+            address: ctx.sender(),
+        }
+    }
+}
+
+ + + +
+ + + +## Function `prove_offchain_tool` + +Create proof for an off-chain tool using its owner cap. + +This uses the off-chain tool registry and expects an off-chain tool FQN. +The owner cap is validated against the registry to bind the tool identity. + + +
public fun prove_offchain_tool(registry: &(nexus_workflow=0x0)::tool_registry::ToolRegistry, owner_cap: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverTool>, fqn: std::ascii::String): (nexus_workflow=0x0)::network_auth::ProofOfIdentity
+
+ + + +
+Implementation + + +
public fun prove_offchain_tool(
+    registry: &ToolRegistry,
+    owner_cap: &CloneableOwnerCap<OverTool>,
+    fqn: AsciiString,
+): ProofOfIdentity {
+    registry.assert_tool_owner(owner_cap, fqn);
+    ProofOfIdentity {
+        identity: IdentityKey::Tool {
+            fqn,
+        }
+    }
+}
+
+ + + +
+ + + +## Function `proof_identity` + +Return the identity proven by the proof. + + +
public fun proof_identity(self: &(nexus_workflow=0x0)::network_auth::ProofOfIdentity): (nexus_workflow=0x0)::network_auth::IdentityKey
+
+ + + +
+Implementation + + +
public fun proof_identity(self: &ProofOfIdentity): IdentityKey {
+    self.identity
+}
+
+ + + +
+ + + +## Function `new_proof_of_key` + +Create proof-of-possession for registering the given public key. +Only Ed25519 keys are supported. + +The signature must verify over +POP_DOMAIN || bcs(IdentityKey) || bcs(key_id) || public_key +using the same public key, proving control of the private key for this +specific identity and key-id slot. + + +
public fun new_proof_of_key(binding: &(nexus_workflow=0x0)::network_auth::KeyBinding, identity: &(nexus_workflow=0x0)::network_auth::ProofOfIdentity, public_key: vector<u8>, signature: vector<u8>): (nexus_workflow=0x0)::network_auth::ProofOfKey
+
+ + + +
+Implementation + + +
public fun new_proof_of_key(
+    binding: &KeyBinding,
+    identity: &ProofOfIdentity,
+    public_key: vector<u8>,
+    signature: vector<u8>,
+): ProofOfKey {
+    assert_identity(binding, identity);
+    assert!(public_key.length() == ED25519_PUBLIC_KEY_LEN, EInvalidPublicKey);
+    assert!(signature.length() == ED25519_SIGNATURE_LEN, EInvalidSignature);
+    let mut msg = copy POP_DOMAIN;
+    let identity_bytes = bcs::to_bytes(&identity.identity);
+    append_bytes(&mut msg, &identity_bytes);
+    let key_id = binding.next_key_id;
+    let key_id_bytes = bcs::to_bytes(&key_id);
+    append_bytes(&mut msg, &key_id_bytes);
+    append_bytes(&mut msg, &public_key);
+    assert!(
+        ed25519::ed25519_verify(&signature, &public_key, &msg),
+        EInvalidProofOfPossession
+    );
+    ProofOfKey {
+        scheme: KEY_SCHEME_ED25519,
+        public_key,
+        key_id,
+    }
+}
+
+ + + +
+ + + +## Function `binding_address` + +Deterministic derived address for the key binding. + +Uses the registry object ID and the identity as the derivation key. + +This allows any caller to deterministically compute where the [KeyBinding] +for an identity lives on-chain. + + +
public fun binding_address(registry: &(nexus_workflow=0x0)::network_auth::NetworkAuth, identity: (nexus_workflow=0x0)::network_auth::IdentityKey): address
+
+ + + +
+Implementation + + +
public fun binding_address(registry: &NetworkAuth, identity: IdentityKey): address {
+    derived_object::derive_address(object::id(registry), identity)
+}
+
+ + + +
+ + + +## Function `binding_exists` + +Check whether a binding has been created for the given identity. + + +
public fun binding_exists(registry: &(nexus_workflow=0x0)::network_auth::NetworkAuth, identity: (nexus_workflow=0x0)::network_auth::IdentityKey): bool
+
+ + + +
+Implementation + + +
public fun binding_exists(registry: &NetworkAuth, identity: IdentityKey): bool {
+    derived_object::exists(®istry.id, identity)
+}
+
+ + + +
+ + + +## Function `create_binding` + +Create a new key binding for the given identity. + +This claims the derived object ID, initializes the binding state, and +inserts the identity into the registry's discovery set. + + +
public fun create_binding(registry: &mut (nexus_workflow=0x0)::network_auth::NetworkAuth, identity: (nexus_workflow=0x0)::network_auth::ProofOfIdentity, description: std::option::Option<vector<u8>>, ctx: &mut sui::tx_context::TxContext): (nexus_workflow=0x0)::network_auth::KeyBinding
+
+ + + +
+Implementation + + +
public fun create_binding(
+    registry: &mut NetworkAuth,
+    identity: ProofOfIdentity,
+    description: Option<vector<u8>>,
+    ctx: &mut TxContext,
+): KeyBinding {
+    let identity_key = identity.identity;
+    assert!(!derived_object::exists(®istry.id, identity_key), EBindingAlreadyExists);
+    let binding_id = derived_object::claim(&mut registry.id, identity_key);
+    let binding = KeyBinding {
+        id: binding_id,
+        identity: identity_key,
+        description,
+        next_key_id: 0,
+        active_key_id: option::none(),
+        keys: table::new(ctx),
+    };
+    registry.identities.insert(identity_key);
+    event::emit(KeyBindingCreatedEvent {
+        binding: object::id(&binding),
+        identity: identity_key,
+    });
+    binding
+}
+
+ + + +
+ + + +## Function `register_key` + +Register a new key and set it as active. + +This assigns a monotonically increasing key id, stores the key record, and +updates the active key pointer. + + +
public fun register_key(binding: &mut (nexus_workflow=0x0)::network_auth::KeyBinding, identity: &(nexus_workflow=0x0)::network_auth::ProofOfIdentity, proof_of_key: (nexus_workflow=0x0)::network_auth::ProofOfKey, clock: &sui::clock::Clock)
+
+ + + +
+Implementation + + +
public fun register_key(
+    binding: &mut KeyBinding,
+    identity: &ProofOfIdentity,
+    proof_of_key: ProofOfKey,
+    clock: &Clock,
+) {
+    assert_identity(binding, identity);
+    assert!(proof_of_key.scheme == KEY_SCHEME_ED25519, EUnsupportedKeyScheme);
+    // PoP is one-time-use for the current slot.
+    assert!(proof_of_key.key_id == binding.next_key_id, EKeyIdMismatch);
+    let key_id = binding.next_key_id;
+    binding.next_key_id = key_id + 1;
+    let added_at_ms = clock.timestamp_ms();
+    let public_key_copy = clone_bytes(&proof_of_key.public_key);
+    binding.keys.add(key_id, KeyRecord {
+        scheme: proof_of_key.scheme,
+        public_key: proof_of_key.public_key,
+        added_at_ms,
+        revoked_at_ms: option::none(),
+    });
+    binding.active_key_id = option::some(key_id);
+    event::emit(KeyRegisteredEvent {
+        binding: object::id(binding),
+        key_id,
+        scheme: KEY_SCHEME_ED25519,
+        public_key: public_key_copy,
+        added_at_ms,
+    });
+    event::emit(ActiveKeyUpdatedEvent {
+        binding: object::id(binding),
+        active_key_id: option::some(key_id),
+    });
+}
+
+ + + +
+ + + +## Function `revoke_key` + +Revoke an existing key. + +This sets the revocation timestamp and clears the active key if needed. + + +
public fun revoke_key(binding: &mut (nexus_workflow=0x0)::network_auth::KeyBinding, identity: &(nexus_workflow=0x0)::network_auth::ProofOfIdentity, key_id: u64, clock: &sui::clock::Clock)
+
+ + + +
+Implementation + + +
public fun revoke_key(
+    binding: &mut KeyBinding,
+    identity: &ProofOfIdentity,
+    key_id: u64,
+    clock: &Clock,
+) {
+    assert_identity(binding, identity);
+    assert!(binding.keys.contains(key_id), EKeyNotFound);
+    let record = binding.keys.borrow_mut(key_id);
+    assert!(record.revoked_at_ms.is_none(), EKeyAlreadyRevoked);
+    let revoked_at_ms = clock.timestamp_ms();
+    record.revoked_at_ms = option::some(revoked_at_ms);
+    if (binding.active_key_id == option::some(key_id)) {
+        binding.active_key_id = option::none();
+        event::emit(ActiveKeyUpdatedEvent {
+            binding: object::id(binding),
+            active_key_id: option::none(),
+        });
+    };
+    event::emit(KeyRevokedEvent {
+        binding: object::id(binding),
+        key_id,
+        revoked_at_ms,
+    });
+}
+
+ + + +
+ + + +## Function `set_active_key` + +Set the active key to an existing, non-revoked key. + +This switches the active key pointer without altering key records. + + +
public fun set_active_key(binding: &mut (nexus_workflow=0x0)::network_auth::KeyBinding, identity: &(nexus_workflow=0x0)::network_auth::ProofOfIdentity, key_id: u64)
+
+ + + +
+Implementation + + +
public fun set_active_key(
+    binding: &mut KeyBinding,
+    identity: &ProofOfIdentity,
+    key_id: u64,
+) {
+    assert_identity(binding, identity);
+    assert!(binding.keys.contains(key_id), EKeyNotFound);
+    let record = binding.keys.borrow(key_id);
+    assert!(record.revoked_at_ms.is_none(), EKeyAlreadyRevoked);
+    binding.active_key_id = option::some(key_id);
+    event::emit(ActiveKeyUpdatedEvent {
+        binding: object::id(binding),
+        active_key_id: option::some(key_id),
+    });
+}
+
+ + + +
+ + + +## Function `key_binding_identity` + +Return the identity associated with a key binding. + + +
public fun key_binding_identity(self: &(nexus_workflow=0x0)::network_auth::KeyBinding): (nexus_workflow=0x0)::network_auth::IdentityKey
+
+ + + +
+Implementation + + +
public fun key_binding_identity(self: &KeyBinding): IdentityKey { self.identity }
+
+ + + +
+ + + +## Function `key_binding_active_key_id` + +Return the active key id for a binding. + +Off-chain verifiers must accept signatures from this key only. + + +
public fun key_binding_active_key_id(self: &(nexus_workflow=0x0)::network_auth::KeyBinding): std::option::Option<u64>
+
+ + + +
+Implementation + + +
public fun key_binding_active_key_id(self: &KeyBinding): Option<u64> { self.active_key_id }
+
+ + + +
+ + + +## Function `key_binding_next_key_id` + +Return the next key id that will be assigned on registration. + +This value is also committed into PoP signatures to make them one-time-use. + + +
public fun key_binding_next_key_id(self: &(nexus_workflow=0x0)::network_auth::KeyBinding): u64
+
+ + + +
+Implementation + + +
public fun key_binding_next_key_id(self: &KeyBinding): u64 { self.next_key_id }
+
+ + + +
+ + + +## Function `key_binding_key` + +Borrow a key record by id. + + +
public fun key_binding_key(self: &(nexus_workflow=0x0)::network_auth::KeyBinding, key_id: u64): &(nexus_workflow=0x0)::network_auth::KeyRecord
+
+ + + +
+Implementation + + +
public fun key_binding_key(self: &KeyBinding, key_id: u64): &KeyRecord {
+    &self.keys[key_id]
+}
+
+ + + +
+ + + +## Function `assert_identity` + +Ensure the proof identity matches the binding identity. + + +
fun assert_identity(binding: &(nexus_workflow=0x0)::network_auth::KeyBinding, identity: &(nexus_workflow=0x0)::network_auth::ProofOfIdentity)
+
+ + + +
+Implementation + + +
fun assert_identity(binding: &KeyBinding, identity: &ProofOfIdentity) {
+    assert!(binding.identity == identity.identity, EIdentityMismatch);
+}
+
+ + + +
+ + + +## Function `append_bytes` + +Append bytes to a vector without reallocating intermediate vectors. + + +
fun append_bytes(out: &mut vector<u8>, bytes: &vector<u8>)
+
+ + + +
+Implementation + + +
fun append_bytes(out: &mut vector<u8>, bytes: &vector<u8>) {
+    let mut i = 0;
+    while (i < bytes.length()) {
+        out.push_back(bytes[i]);
+        i = i + 1;
+    };
+}
+
+ + + +
+ + + +## Function `clone_bytes` + +Clone bytes into a new vector. + + +
fun clone_bytes(bytes: &vector<u8>): vector<u8>
+
+ + + +
+Implementation + + +
fun clone_bytes(bytes: &vector<u8>): vector<u8> {
+    let mut out = vector[];
+    append_bytes(&mut out, bytes);
+    out
+}
+
+ + + +
diff --git a/nexus/packages/reference/nexus_workflow/tool_registry.md b/nexus/packages/reference/nexus_workflow/tool_registry.md index 3c33aa3..6d79369 100644 --- a/nexus/packages/reference/nexus_workflow/tool_registry.md +++ b/nexus/packages/reference/nexus_workflow/tool_registry.md @@ -3,38 +3,53 @@ # Module `(nexus_workflow=0x0)::tool_registry` - - -- [Struct `ToolRegistry`](#(nexus_workflow=0x0)_tool_registry_ToolRegistry) -- [Struct `OffChainTool`](#(nexus_workflow=0x0)_tool_registry_OffChainTool) -- [Struct `OverSlashing`](#(nexus_workflow=0x0)_tool_registry_OverSlashing) -- [Struct `OverTool`](#(nexus_workflow=0x0)_tool_registry_OverTool) -- [Struct `ToolRegistryCreatedEvent`](#(nexus_workflow=0x0)_tool_registry_ToolRegistryCreatedEvent) -- [Struct `OffChainToolRegisteredEvent`](#(nexus_workflow=0x0)_tool_registry_OffChainToolRegisteredEvent) -- [Struct `ToolUnregisteredEvent`](#(nexus_workflow=0x0)_tool_registry_ToolUnregisteredEvent) -- [Struct `ToolSlashedEvent`](#(nexus_workflow=0x0)_tool_registry_ToolSlashedEvent) -- [Constants](#@Constants_0) -- [Function `new`](#(nexus_workflow=0x0)_tool_registry_new) -- [Function `share`](#(nexus_workflow=0x0)_tool_registry_share) -- [Function `slash_off_chain_tool`](#(nexus_workflow=0x0)_tool_registry_slash_off_chain_tool) -- [Function `set_mist_collateral_to_lock`](#(nexus_workflow=0x0)_tool_registry_set_mist_collateral_to_lock) -- [Function `set_lock_duration_ms`](#(nexus_workflow=0x0)_tool_registry_set_lock_duration_ms) -- [Function `register_off_chain_tool_for_self`](#(nexus_workflow=0x0)_tool_registry_register_off_chain_tool_for_self) - - [Slashing](#@Slashing_1) - - [Owner Cap](#@Owner_Cap_2) - - [Gas Tickets](#@Gas_Tickets_3) -- [Function `register_off_chain_tool`](#(nexus_workflow=0x0)_tool_registry_register_off_chain_tool) -- [Function `unregister_off_chain_tool`](#(nexus_workflow=0x0)_tool_registry_unregister_off_chain_tool) -- [Function `claim_collateral_for_self`](#(nexus_workflow=0x0)_tool_registry_claim_collateral_for_self) -- [Function `claim_collateral_for_off_chain_tool`](#(nexus_workflow=0x0)_tool_registry_claim_collateral_for_off_chain_tool) -- [Function `deescalate`](#(nexus_workflow=0x0)_tool_registry_deescalate) -- [Function `did_unregister_period_pass`](#(nexus_workflow=0x0)_tool_registry_did_unregister_period_pass) -- [Function `assert_tool_registered`](#(nexus_workflow=0x0)_tool_registry_assert_tool_registered) -- [Function `assert_tool_owner`](#(nexus_workflow=0x0)_tool_registry_assert_tool_owner) -- [Function `assert_tool_owner_unchecked_generic`](#(nexus_workflow=0x0)_tool_registry_assert_tool_owner_unchecked_generic) -- [Function `register_off_chain_tool_`](#(nexus_workflow=0x0)_tool_registry_register_off_chain_tool_) -- [Function `did_unregister_period_pass_`](#(nexus_workflow=0x0)_tool_registry_did_unregister_period_pass_) - +## Tool Verification Status + +Tools are registered as **Unverified** by default. The holder of +`OverSlashing` (admin/governance) may update a tool to **Verified** or back to +**Unverified** via `set_tool_status`. + +Verified means the slashing authority has explicitly endorsed the tool entry. +It does **not** imply correctness, safety, availability, or that inputs/outputs +are benign. Signed HTTP provides authenticity of responses; slashing is an +after-the-fact enforcement mechanism. + +- [Struct `ToolRegistry`](#(nexus_workflow=0x0)_tool_registry_ToolRegistry) +- [Struct `ToolStatus`](#(nexus_workflow=0x0)_tool_registry_ToolStatus) +- [Struct `OffChainTool`](#(nexus_workflow=0x0)_tool_registry_OffChainTool) +- [Struct `OverSlashing`](#(nexus_workflow=0x0)_tool_registry_OverSlashing) +- [Struct `OverTool`](#(nexus_workflow=0x0)_tool_registry_OverTool) +- [Struct `ToolRegistryCreatedEvent`](#(nexus_workflow=0x0)_tool_registry_ToolRegistryCreatedEvent) +- [Struct `OffChainToolRegisteredEvent`](#(nexus_workflow=0x0)_tool_registry_OffChainToolRegisteredEvent) +- [Struct `ToolUnregisteredEvent`](#(nexus_workflow=0x0)_tool_registry_ToolUnregisteredEvent) +- [Struct `ToolSlashedEvent`](#(nexus_workflow=0x0)_tool_registry_ToolSlashedEvent) +- [Struct `ToolStatusUpdatedEvent`](#(nexus_workflow=0x0)_tool_registry_ToolStatusUpdatedEvent) +- [Constants](#@Constants_0) +- [Function `new`](#(nexus_workflow=0x0)_tool_registry_new) +- [Function `share`](#(nexus_workflow=0x0)_tool_registry_share) +- [Function `slash_off_chain_tool`](#(nexus_workflow=0x0)_tool_registry_slash_off_chain_tool) +- [Function `set_mist_collateral_to_lock`](#(nexus_workflow=0x0)_tool_registry_set_mist_collateral_to_lock) +- [Function `set_lock_duration_ms`](#(nexus_workflow=0x0)_tool_registry_set_lock_duration_ms) +- [Function `set_tool_status`](#(nexus_workflow=0x0)_tool_registry_set_tool_status) +- [Function `register_off_chain_tool_for_self`](#(nexus_workflow=0x0)_tool_registry_register_off_chain_tool_for_self) + - [Slashing](#@Slashing_1) + - [Owner Cap](#@Owner_Cap_2) + - [Gas Tickets](#@Gas_Tickets_3) +- [Function `register_off_chain_tool`](#(nexus_workflow=0x0)_tool_registry_register_off_chain_tool) +- [Function `unregister_off_chain_tool`](#(nexus_workflow=0x0)_tool_registry_unregister_off_chain_tool) +- [Function `claim_collateral_for_self`](#(nexus_workflow=0x0)_tool_registry_claim_collateral_for_self) +- [Function `claim_collateral_for_off_chain_tool`](#(nexus_workflow=0x0)_tool_registry_claim_collateral_for_off_chain_tool) +- [Function `deescalate`](#(nexus_workflow=0x0)_tool_registry_deescalate) +- [Function `did_unregister_period_pass`](#(nexus_workflow=0x0)_tool_registry_did_unregister_period_pass) +- [Function `assert_tool_registered`](#(nexus_workflow=0x0)_tool_registry_assert_tool_registered) +- [Function `assert_tool_owner`](#(nexus_workflow=0x0)_tool_registry_assert_tool_owner) +- [Function `assert_tool_owner_unchecked_generic`](#(nexus_workflow=0x0)_tool_registry_assert_tool_owner_unchecked_generic) +- [Function `tool_status_unverified`](#(nexus_workflow=0x0)_tool_registry_tool_status_unverified) +- [Function `tool_status_verified`](#(nexus_workflow=0x0)_tool_registry_tool_status_verified) +- [Function `tool_status`](#(nexus_workflow=0x0)_tool_registry_tool_status) +- [Function `tool_is_verified`](#(nexus_workflow=0x0)_tool_registry_tool_is_verified) +- [Function `register_off_chain_tool_`](#(nexus_workflow=0x0)_tool_registry_register_off_chain_tool_) +- [Function `did_unregister_period_pass_`](#(nexus_workflow=0x0)_tool_registry_did_unregister_period_pass_)
use (nexus_primitives=0x0)::event;
 use (nexus_primitives=0x0)::owner_cap;
@@ -67,23 +82,16 @@
 use sui::vec_set;
 
- - ## Struct `ToolRegistry` - -
public struct ToolRegistry has key
 
- -
Fields -
id: sui::object::UID @@ -111,6 +119,34 @@
+
+ + + +## Enum `ToolStatus` + +Verification status for a tool entry. + +
public enum ToolStatus has copy, drop, store
+
+ +
+Variants + +
+
+Variant Unverified +
+
+ Default status for newly registered tools. +
+
+Variant Verified +
+
+ Explicitly endorsed by the slashing authority. +
+
@@ -120,16 +156,12 @@ Dynamic object. -
public struct OffChainTool has key, store
 
- -
Fields -
id: sui::object::UID @@ -155,6 +187,12 @@ Dynamic object. Must follow our meta schema for output schemas.
+tool_status: tool_registry::ToolStatus +
+
+ Verification status set by the slashing authority. Tools are registered as unverified by default. +
+
vault: sui::balance::Balance<sui::sui::SUI>
@@ -178,7 +216,6 @@ Dynamic object.
-
@@ -187,20 +224,15 @@ Dynamic object. Will identify admin cap for slashing permissions. -
public struct OverSlashing has drop
 
- -
Fields -
-
@@ -209,37 +241,27 @@ Will identify admin cap for slashing permissions. Will identify admin cap over a tool in this registry. -
public struct OverTool has drop
 
- -
Fields -
-
## Struct `ToolRegistryCreatedEvent` - -
public struct ToolRegistryCreatedEvent has copy, drop
 
- -
Fields -
registry: sui::object::ID @@ -253,24 +275,18 @@ Will identify admin cap over a tool in this registry.
-
## Struct `OffChainToolRegisteredEvent` - -
public struct OffChainToolRegisteredEvent has copy, drop
 
- -
Fields -
registry: sui::object::ID @@ -310,24 +326,18 @@ Will identify admin cap over a tool in this registry.
-
## Struct `ToolUnregisteredEvent` - -
public struct ToolUnregisteredEvent has copy, drop
 
- -
Fields -
tool: sui::object::ID @@ -341,7 +351,6 @@ Will identify admin cap over a tool in this registry.
-
@@ -350,16 +359,12 @@ Will identify admin cap over a tool in this registry. The admin has slashed some collateral from a tool. -
public struct ToolSlashedEvent has copy, drop
 
- -
Fields -
tool: sui::object::ID @@ -379,6 +384,42 @@ The admin has slashed some collateral from a tool.
+
+ + + +## Struct `ToolStatusUpdatedEvent` + +Emitted when a tool's verification status is updated. + +
public struct ToolStatusUpdatedEvent has copy, drop
+
+ +
+Fields + +
+
+registry: sui::object::ID +
+
+
+
+tool: sui::object::ID +
+
+
+
+fqn: std::ascii::String +
+
+
+
+status: tool_registry::ToolStatus +
+
+
+
@@ -386,156 +427,105 @@ The admin has slashed some collateral from a tool. ## Constants - - -
#[error]
 const ENotEnoughSuiToLock: vector<u8> = b"Not enough SUI to lock collateral";
 
- - - -
#[error]
 const ECollateralNotReadyToClaim: vector<u8> = b"Collateral not ready to reclaim";
 
- - - -
#[error]
 const EFqnNotFound: vector<u8> = b"FQN not found";
 
- - - -
#[error]
 const EUnauthorized: vector<u8> = b"Only the creator can unregister a tool";
 
- - - -
#[error]
 const EToolUnregistered: vector<u8> = b"Tool has been unregistered";
 
- - - -
#[error]
 const EFqnAlreadyExists: vector<u8> = b"FQN already exists";
 
- - How much [SUI] (in MIST) to lock to register a tool. TODO: This is only a testing value. -
const DEFAULT_MIST_COLLATERAL_TO_LOCK: u64 = 1;
 
- - How long is the collateral locked for in milliseconds after unregistering. TODO: This is only a testing value. -
const DEFAULT_LOCK_DURATION_MS: u64 = 1;
 
- - ## Function `new` - -
public(package) fun new(ctx: &mut sui::tx_context::TxContext): ((nexus_workflow=0x0)::tool_registry::ToolRegistry, (nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverSlashing>)
 
- - - - ## Function `share` - -
public(package) fun share(self: (nexus_workflow=0x0)::tool_registry::ToolRegistry)
 
- - - - ## Function `slash_off_chain_tool` Takes given amount of collateral from a tool. -
public fun slash_off_chain_tool(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, _: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverSlashing>, fqn: std::ascii::String, amount: u64, clock: &sui::clock::Clock): sui::balance::Balance<sui::sui::SUI>
 
- - - - ## Function `set_mist_collateral_to_lock` - -
public fun set_mist_collateral_to_lock(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, _: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverSlashing>, new_mist_collateral_to_lock: u64)
 
- - - - ## Function `set_lock_duration_ms` - -
public fun set_lock_duration_ms(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, _: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverSlashing>, new_duration_ms: u64)
 
+ +## Function `set_tool_status` +Set a tool's verification status (verified or unverified). +
public fun set_tool_status(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, _: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverSlashing>, fqn: std::ascii::String, status: (nexus_workflow=0x0)::tool_registry::ToolStatus)
+
@@ -552,53 +542,36 @@ See to learn about the expected format of the input and output schemas and the expected format of the FQN and URL. - ### Slashing - If the tool does not uphold its contract defined by off-chain process the admin has access to [slash_off_chain_tool] to slash collateral. - ### Owner Cap - Is transferred to the sender of this tx. - ### Gas Tickets - By default the tool does not require gas for its invocations. Gas needs to be configured, see the nexus_workflow::gas module. -
public entry fun register_off_chain_tool_for_self(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, fqn: std::ascii::String, url: vector<u8>, input_schema: vector<u8>, output_schema: vector<u8>, pay_with: &mut sui::coin::Coin<sui::sui::SUI>, ctx: &mut sui::tx_context::TxContext)
 
- - - - ## Function `register_off_chain_tool` - -
public fun register_off_chain_tool(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, fqn: std::ascii::String, url: vector<u8>, input_schema: vector<u8>, output_schema: vector<u8>, pay_with: &mut sui::coin::Coin<sui::sui::SUI>, ctx: &mut sui::tx_context::TxContext): (nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverTool>
 
- - - - ## Function `unregister_off_chain_tool` @@ -607,14 +580,9 @@ Permissioned way to unregister a tool. Soon after this point all DAGs that use this tool will stop working. -
public entry fun unregister_off_chain_tool(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, owner_cap: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverTool>, fqn: std::ascii::String, clock: &sui::clock::Clock)
 
- - - - ## Function `claim_collateral_for_self` @@ -622,28 +590,18 @@ Soon after this point all DAGs that use this tool will stop working. Same as [claim_collateral_for_off_chain_tool] but transfers the collateral to the sender of this tx. -
public entry fun claim_collateral_for_self(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, owner_cap: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverTool>, fqn: std::ascii::String, clock: &sui::clock::Clock, ctx: &mut sui::tx_context::TxContext)
 
- - - - ## Function `claim_collateral_for_off_chain_tool` Return collateral to a tool owner. -
public fun claim_collateral_for_off_chain_tool(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, owner_cap: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverTool>, fqn: std::ascii::String, clock: &sui::clock::Clock): sui::balance::Balance<sui::sui::SUI>
 
- - - - ## Function `deescalate` @@ -653,53 +611,30 @@ generic type that doesn't have any permissions within this module. See also [assert_tool_owner_unchecked_generic]. -
public fun deescalate<T: drop>(self: &(nexus_workflow=0x0)::tool_registry::ToolRegistry, owner_cap: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverTool>, fqn: std::ascii::String, witness: T, ctx: &mut sui::tx_context::TxContext): (nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<T>
 
- - - - ## Function `did_unregister_period_pass` - -
public fun did_unregister_period_pass(self: &(nexus_workflow=0x0)::tool_registry::ToolRegistry, fqn: std::ascii::String, clock: &sui::clock::Clock): bool
 
- - - - ## Function `assert_tool_registered` - -
public fun assert_tool_registered(self: &(nexus_workflow=0x0)::tool_registry::ToolRegistry, fqn: std::ascii::String)
 
- - - - ## Function `assert_tool_owner` - -
public fun assert_tool_owner(self: &(nexus_workflow=0x0)::tool_registry::ToolRegistry, owner_cap: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverTool>, fqn: std::ascii::String)
 
- - - - ## Function `assert_tool_owner_unchecked_generic` @@ -709,37 +644,57 @@ type to be used. See also [deescalate]. -
public fun assert_tool_owner_unchecked_generic<T>(self: &(nexus_workflow=0x0)::tool_registry::ToolRegistry, owner_cap: &(nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<T>, fqn: std::ascii::String)
 
+ +## Function `tool_status_unverified` +Construct [ToolStatus::Unverified]. +
public fun tool_status_unverified(): (nexus_workflow=0x0)::tool_registry::ToolStatus
+
- - -## Function `register_off_chain_tool_` + +## Function `tool_status_verified` +Construct [ToolStatus::Verified]. -
fun register_off_chain_tool_(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, fqn: std::ascii::String, url: vector<u8>, input_schema: vector<u8>, output_schema: vector<u8>, vault: sui::balance::Balance<sui::sui::SUI>, ctx: &mut sui::tx_context::TxContext): (nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverTool>
+
public fun tool_status_verified(): (nexus_workflow=0x0)::tool_registry::ToolStatus
 
+ +## Function `tool_status` +Return the tool verification status (verified or unverified). +
public fun tool_status(self: &(nexus_workflow=0x0)::tool_registry::ToolRegistry, fqn: std::ascii::String): (nexus_workflow=0x0)::tool_registry::ToolStatus
+
- + -## Function `did_unregister_period_pass_` +## Function `tool_is_verified` -Has the tool been unregistered for longer than the lock duration? +Return true iff the tool is verified. +
public fun tool_is_verified(self: &(nexus_workflow=0x0)::tool_registry::ToolRegistry, fqn: std::ascii::String): bool
+
-
fun did_unregister_period_pass_(self: &(nexus_workflow=0x0)::tool_registry::OffChainTool, clock: &sui::clock::Clock): bool
+
+
+## Function `register_off_chain_tool_`
+
+
fun register_off_chain_tool_(self: &mut (nexus_workflow=0x0)::tool_registry::ToolRegistry, fqn: std::ascii::String, url: vector<u8>, input_schema: vector<u8>, output_schema: vector<u8>, vault: sui::balance::Balance<sui::sui::SUI>, ctx: &mut sui::tx_context::TxContext): (nexus_primitives=0x0)::owner_cap::CloneableOwnerCap<(nexus_workflow=0x0)::tool_registry::OverTool>
 
+ +## Function `did_unregister_period_pass_` +Has the tool been unregistered for longer than the lock duration? +
fun did_unregister_period_pass_(self: &(nexus_workflow=0x0)::tool_registry::OffChainTool, clock: &sui::clock::Clock): bool
+
diff --git a/nexus/packages/workflow.md b/nexus/packages/workflow.md index 2f537b9..07569c8 100644 --- a/nexus/packages/workflow.md +++ b/nexus/packages/workflow.md @@ -7,7 +7,27 @@ In a nutshell, the workflow engine executes walks over a directed acyclic graph ## Scheduler -The workflow package also includes an on-chain scheduler module for time-based orchestration (queue + periodic scheduling) that can gate DAG execution. See [On-chain scheduler architecture](../scheduler/index.md). +The workflow package also includes an on-chain scheduler module for time based orchestration (queue + periodic scheduling) that can gate DAG execution. See [On-chain scheduler architecture](../scheduler/index.md). + +## Network Auth (identity key bindings) + +The workflow package includes the `network_auth` module: a shared on-chain registry that binds off-chain identities (Tools and Leader nodes) to Ed25519 public keys used for message signing and verification. + +This enables any verifier to discover the currently active public key for an identity and validate signed messages offline, while supporting key rotation and revocation (via proof of identity + proof of possession). + +Reference: + +- [`nexus_workflow::network_auth`](./reference/nexus_workflow/network_auth.md) + +## Network Auth (identity key bindings) + +The workflow package includes the `network_auth` module: a shared on-chain registry that binds off-chain identities (Tools and Leader nodes) to Ed25519 public keys used for message signing and verification. + +This enables any verifier to discover the currently active public key for an identity and validate signed messages offline, while supporting key rotation and revocation (via proof-of-identity + proof-of-possession). + +Reference: + +- [`nexus_workflow::network_auth`](./reference/nexus_workflow/network_auth.md) {% hint style="info" %} The terms used in the context of the DAG can be found in the [glossary](../glossary.md#dag-related-terms). Familiarize yourself with them before moving on. @@ -31,7 +51,6 @@ The terms used in the context of the DAG can be found in the [glossary](../gloss - An end state is a `Vertex` with no outgoing `Edge`s. - A walk can either halt as _(i)_ successful, _(ii)_ failed, or _(iii)_ consumed. - - _(i)_ successful halt means that the walk has reached an end state; - _(ii)_ failed halt means that the walk cannot reach an end state; - _(iii)_ consumed halt means that the walk has been joined with another walk. @@ -186,7 +205,7 @@ An `EntryGroup` consists of a set of vertices. To start the execution via a specific entry group, the user must provide input data for each _entry port_ of each vertex included in the entry group. If a vertex has no entry ports, the vertex name _must_ still be specified with an empty `VecMap` of entry ports. -Vertices included in an entry group but with an _empty_ set of input ports will be scheduled for execution immediately at the start, provided they don't have any incoming edges. If a vertex in this case _does_ have incoming edges, this is a no-op - it has no effect on the execution and the vertex is treated as-if it was not included in the entry group. +Vertices included in an entry group but with an _empty_ set of input ports will be scheduled for execution immediately at the start, provided they don't have any incoming edges. If a vertex in this case _does_ have incoming edges, this is a no op - it has no effect on the execution and the vertex is treated as if it was not included in the entry group. A default entry group (named `_default_group`) is available for convenience. @@ -228,19 +247,13 @@ The leader cap can be cloned and given to other wallets. The tool registry is an onchain shared object that holds [tool definitions](../tool.md#tool-definitions). -To register a tool the creator must deposit a time-locked collateral to prevent spamming the registry. +To register a tool the creator must deposit a time locked collateral to prevent spamming the registry. The amount of `SUI` locked and the interval after which they can be reclaimed is configured in the `nexus-next` repo. -## PreKey vault - -The pre_key vault is an onchain shared object that holds pre_keys for initiating encrypted communication with the workflow. - -To claim a pre_key, the user must first deposit some gas budget in `SUI` to the gas service. Claiming a pre_key is also rate limited per wallet. - ## Notes - We have considered a stricter rule `5.` where `InputPort` can have only one incoming `OutputVariantPort`. However, relaxing this rule into its current form meant expressive workflows and simpler runtime state management in exchange for more complex static analysis algorithm. - This trade-off was deemed worthy. + This trade off was deemed worthy. - When using the `ProofOfUid` primitive, it must be created with a type that matches the `UID`. The type should be considered an authorization ticket and should be treated just as any other capability type. diff --git a/nexus/tokenomics/gas-service.md b/nexus/tokenomics/gas-service.md index 9e7b810..d8f4a06 100644 --- a/nexus/tokenomics/gas-service.md +++ b/nexus/tokenomics/gas-service.md @@ -4,354 +4,534 @@ The concepts related to tokenomics as explained in [the tokenomics section][toke ## Overview -The gas payment and settlement system in Nexus provides a flexible way to handle gas payments for tool invocations. It supports multiple modes of operation and allows for different payment strategies through gas extensions. +The gas payment and settlement system in Nexus provides a flexible and **two-phase gas locking model** for tool invocations. -## Core Concepts +Gas is: -### Gas Service +1. **Locked before invocation** +1. **Finalized (paid) only after successful execution** +1. **Refunded if execution aborts** -The `GasService` is a shared object that manages all gas-related operations. It maintains: +This ensures: -- Gas tickets for tools. -- Gas budgets for different scopes. -- Execution gas settlement state. -- Tool-specific gas settings. +- Tools only get paid for successful invocations. +- Users never permanently lose funds for aborted executions. +- Gas settlement is deterministic and idempotent. +- Offchain actors can verify settlement state. -### Gas Tickets +--- -Gas tickets represent prepaid access to tool invocations. They can be configured with different modes of operation: +# Core Concepts -1. **Expiry Mode**: Allows unlimited invocations within a specific time period. -1. **Limited Invocations Mode**: Allows a fixed number of invocations. -1. **Upon Discretion of Tool Mode**: Tool owner has complete control over ticket validity. +## GasService -### Scopes +`GasService` is a shared object that: -Gas tickets and budgets can be associated with different scopes: +- Tracks tool default invocation costs +- Derives `ToolGas`, `ExecutionGas`, and `InvokerGas` +- Coordinates gas locking and settlement -1. **Execution Scope**: Specific to a single DAG execution. -1. **Worksheet Type Scope**: Applies to all executions of a specific worksheet type, i.e. the TAP. -1. **Invoker Address Scope**: Applies to all executions initiated by a specific address. +It does **not** hold funds directly — funds live in: -{% hint style="warning" %} -Gas is secured before the tool is invoked to ensure that the tool always gets paid for its usage according to the selected mode. +- `ToolGas.vault` +- `InvokerGas.vault` -This does expose the tool owner to slashing risk, if they do not deliver the service according to the Nexus defined protocol. -{% endhint %} +--- -### Other Data Structures +## ExecutionGas -#### ExecutionGas +`ExecutionGas` is derived per `DAGExecution`. -Tracks gas settlement state for a specific DAG execution. This struct maintains a record of which vertices in the execution have had their gas settled. Once a vertex is marked as settled, it can be invoked once. +It maintains: -#### GasBudgets +- `locked_vertices` → vertices whose gas has been locked +- `tool_cost_snapshot` → price snapshot at execution start +- `claimed_leader_gas` → audit trail for leader claims -Manages gas budgets for different scopes in the system. The inner Bag stores balances of SUI coins, with the key being a Scope. These budgets are used as a fallback payment method when no gas tickets are available. See [Default Gas Budget](#1-default-gas-budget). +### Important Semantics -#### ToolGas +A vertex being “locked” means: -Manages all gas-related state for a specific tool. This includes: +> Gas has been reserved and the vertex can be invoked once. -- Collected gas payments in the vault -- Default cost per invocation -- Tool-specific gas settings -- Available gas tickets for different scopes +It does **not** mean the tool has been paid yet. -## Gas Payment Modes +Payment happens later via: -### 1. Default Gas Budget +```move +finalize_gas_state_for_vertex(...) +``` + +If execution aborts, locked entries are refunded via: + +```move +refund_aborted_execution_gas_for_tool(...) +``` + +--- + +## ToolGas + +Derived per tool (`tool.fqn()`). + +Holds: + +- `vault` → collected payments +- `tickets` → prepaid access +- `settings` → extension config + +The vault only increases when: + +```move +finalize_gas_state_for_vertex(...) +``` + +is called successfully. + +--- + +## InvokerGas + +Derived per invoker address. + +Holds gas budgets scoped by: + +- Execution +- Worksheet type +- Invoker address + +Each budget tracks: + +```move +GasFunds { + bal: Balance, + locked: u64 +} +``` + +`locked` ensures: + +- Funds are reserved before invocation +- Funds are reduced only on finalization +- Locked funds can be unlocked on abort + +--- + +# Gas Tickets + +Gas tickets represent prepaid access to tool invocations. They are implemented in [the default gas extension][default-gas-extension]. + +They are stored in `ToolGas.tickets` and can operate in three modes: + +## 1. Expiry + +Unlimited invocations until: + +```move +created_at_ms + valid_for_ms +``` + +Validation is performed against: + +```move +execution.execution_created_at() +``` + +(not current time). + +No funds are locked per invocation. + +--- + +## 2. Limited Invocations + +Allows a fixed number of invocations. + +Tracks: + +```move +total +used +locked +``` + +Lock phase: + +- `locked += 1` + +Finalize phase: -Tools can set a default cost per invocation. When no gas tickets are available, the system will attempt to charge from the gas budget in the following order: +- `used += 1` +- `locked -= 1` +- ticket removed if fully consumed -1. Execution-specific budget -1. Worksheet type budget -1. Invoker address budget +Abort phase: -### 2. Gas Extensions +- `locked -= 1` +- `used` unchanged -Gas extensions provide alternative payment strategies. The [default extension][default-gas-extension] implements an expiry-based system where users can: +This ensures correct accounting across retries and aborts. -- Buy access for a specific duration (e.g., 10 minutes). -- Pay a fixed rate per minute. -- Get unlimited invocations during the purchased period. +--- -## Gas Settlement Process +## 3. Upon Discretion of Tool -1. When a vertex (tool) is invoked, the system checks if gas has been settled. -1. If not settled, it attempts to find a valid gas ticket for the different scopes in order. -1. Gas tickets are validated against the execution's creation timestamp, not the current time. -1. If no valid ticket is found, it attempts to charge from the gas budget. -1. Once settled, the vertex can be invoked. -1. Gas settlement is idempotent - subsequent checks won't charge again. +Tool owner can revoke at any time. -### Events +No internal locking counters. +No automatic accounting. +Used for custom gas strategies. -The system emits several types of events for gas-related operations: +--- + +# Scopes + +Gas tickets and budgets can be associated with: + +1. **Execution Scope** +1. **Worksheet Type Scope** +1. **Invoker Address Scope** + +When locking gas, the system searches in this order (specific -> general): + +```move +Execution → WorksheetType → InvokerAddress +``` + +--- + +# Gas Locking Model + +The system follows a strict two-phase model. + +--- + +## Phase 1 — Lock + +Triggered via: + +```move +lock_gas_state_for_vertex(...) +``` + +or + +```move +lock_gas_state_for_tool(...) +``` + +Internally calls: + +```move +try_lock_gas_state_for_vertex(...) +``` + +Locking: + +1. Attempts to stamp a valid gas ticket. +1. Otherwise attempts to reserve budget. +1. Otherwise marks vertex as Free (if cost is zero). + +If successful: + +- Entry added to `ExecutionGas.locked_vertices` +- Budget `locked` value increased (if budget used) +- `GasLockUpdateEvent` emitted + +Locking is: + +- Permissionless +- Idempotent +- Safe to call multiple times + +--- + +## Phase 2 — Finalize (Success Path) + +Called after successful tool invocation: + +```move +finalize_gas_state_for_vertex(...) +``` + +Behavior depends on how the vertex was settled: + +### Ticket (Expiry / UponDiscretion) + +No fund transfer. + +### Ticket (LimitedInvocations) + +- Decrement `locked` +- Increment `used` +- Remove ticket if exhausted + +### Budget + +- Reduce `locked` +- Split funds from `bal` +- Transfer to `ToolGas.vault` + +Emits: + +```move +GasUnlockUpdateEvent { was_refunded: false } +``` + +--- + +## Phase 3 — Abort (Failure Path) + +Called for aborted executions: + +```move +refund_aborted_execution_gas_for_tool(...) +``` -1. `GasSettlementUpdateEvent` for each gas settlement attempt: +For each locked vertex: + +### Budget + +- Reduce `locked` +- Funds remain in user balance + +### Ticket (LimitedInvocations) + +- Decrement `locked` +- `used` unchanged + +### Ticket (Expiry / UponDiscretion) + +No state mutation needed + +Emits: + +```move +GasUnlockUpdateEvent { was_refunded: true } +``` + +After all tools are processed: + +```move +assert_aborted_execution_fully_refunded(...) +``` + +can be called as a sanity check. + +--- + +# Gas Payment Modes + +## 1. Default Gas Budget + +If no ticket applies: + +System attempts to reserve from budgets in order: + +1. Execution scope +1. Worksheet type scope +1. Invoker address scope + +Reservation occurs during lock. +Transfer occurs during finalize. + +--- + +## 2. Gas Extensions + +Gas extensions rely on: + +- ToolGas settings +- Custom tickets +- Ticket logic + +Extensions can: + +- Implement time-based access +- Implement subscription models +- Implement metered invocations + +--- + +# Leader Gas + +Leaders claim gas for: + +- Execution +- Priority +- Pre-key handshake + +Functions: + +```move +claim_leader_gas(...) +claim_leader_gas_for_self(...) +claim_leader_gas_for_pre_key(...) +``` + +Leader gas is charged immediately from `InvokerGas`. + +Each claim emits: + +```move +LeaderClaimedGasEvent { + network, + amount, + purpose +} +``` + +Claims are recorded in: + +```move +ExecutionGas.claimed_leader_gas +``` + +⚠ Current limitation: +Leader can claim arbitrary amounts. +External observers must validate via events. + +--- + +# Events + +## GasLockUpdateEvent + +Emitted on lock attempt. ```rust -public struct GasSettlementUpdateEvent has copy, drop { +public struct GasLockUpdateEvent { execution: ID, - vertex: dag::Vertex, + vertex: dag::RuntimeVertex, tool_fqn: AsciiString, - /// Whether the gas ticket was stamped ok. - /// - /// Multiple of these events can be emitted for the same execution/vertex/tool_fqn - /// combination, but only one of them will have this field set to true. - was_settled: bool, + was_locked: bool, } ``` -1. `LeaderClaimedGasEvent` for tracking gas claims by leaders: +- Multiple events may be emitted +- Only one will have `was_locked = true` + +--- + +## GasUnlockUpdateEvent + +Emitted on finalize or refund. + +```rust +public struct GasUnlockUpdateEvent { + execution: ID, + vertex: dag::RuntimeVertex, + tool_fqn: AsciiString, + was_refunded: bool, +} +``` + +- `was_refunded = false` → successful payment +- `was_refunded = true` → refund after abort + +--- + +## LeaderClaimedGasEvent ```rust -public struct LeaderClaimedGasEvent has copy, drop { - /// Who is the leader. +public struct LeaderClaimedGasEvent { network: ID, - /// How much was claimed. amount: u64, + purpose: AsciiString, } ``` -### Checking Gas Payment Status +Used for auditing leader behavior. -There are several ways to check if gas has been paid for a tool invocation: +--- -1. **Using GasService State** - - Anyone can check if gas has been settled for a specific vertex in an execution using [`is_execution_vertex_settled`](#view-operations). - - This is useful both for onchain and offchain actors. - - This is the most direct way to verify gas payment status. - - Returns a boolean indicating whether the vertex can be invoked. -1. **Listening to Events** - - The system emits `GasSettlementUpdateEvent` for each gas settlement attempt. - - Anyone can listen to these events to track gas settlement status. -1. **Checking Gas Tickets** - - Tool owners can check their tool's gas tickets and settings. - - Users can check their own gas budgets and tickets. - - This is useful _before_ the invocation is requested to know whether it's possible to execute with given gas tickets. -1. **Viewing Gas Budgets** - - Users can check their remaining gas budgets for different scopes. - - This is useful _before_ the invocation is requested to know whether it's possible to execute with given gas budgets. - -## Owner Capabilities - -Tool owners have two levels of capabilities for managing their tool's gas operations: - -1. `OverTool` - The main owner cap that provides full control over the tool, including gas operations -1. `OverGas` - A de-escalated version of `OverTool` that provides limited permissions focused only on gas-related operations - -The `OverGas` cap is designed to make tool owners more comfortable using gas extensions by providing a more restricted set of permissions. It allows them to: - -- Add and remove gas tickets. -- Change gas settings. -- Manage gas-related operations. - -without giving them access to other important tool state. This separation of concerns helps maintain security while enabling tool owners to manage their gas operations effectively. - -## Security Considerations - -1. Gas tickets with expiry or limited invocations cannot be revoked. -1. Only tickets in "Upon Discretion of Tool" mode can be revoked. -1. Tool owners can claim gas at any time. -1. Gas budgets can be refunded if the execution is finished. -1. Workflows that want to pay gas on behalf of the user must assert that they execute in a network with a trusted leader. See [current limitation below](#current-limitation) - -### Current limitation - -In the current implementation the Nexus leader can claim any amount of gas it wants from anybody who uploads gas budgets.\ -To hold the leader accountable we emit `LeaderClaimedGasEvent` event which can be read by 3rd parties that check that the amount claimed is not over the top. - -However, this implies that as of right now, the leader is has to be trusted. - -## Key Operations - -
- -Tool Owner Operations - -### Gas Cost Management - -1. **Setting Default Cost**\ - Sets the default cost in MIST for a single tool invocation. Calling this function enables gas collection by the tool so it's imperative the tool owners calls it to collect fees for tool execution. - - ```rust - public fun set_single_invocation_cost_mist( - gas_service: &mut GasService, - tool_registry: &ToolRegistry, - owner_cap: &CloneableOwnerCap, - fqn: AsciiString, - single_invocation_cost_mist: u64, - ctx: &mut TxContext, - ) - ``` - - > Set `single_invocation_cost_mist` to 2^64-1 to enable gas collection but require a gas extension to do it. -1. **Claiming Gas**\ - Allows the tool owner to withdraw all collected gas payments for their tool. - - ```rust - public fun claim_gas( - gas_service: &mut GasService, - tool_registry: &ToolRegistry, - owner_cap: &CloneableOwnerCap, - fqn: AsciiString, - ctx: &mut TxContext, - ): Balance - ``` - -### Gas Ticket Management - -1. **Adding Gas Tickets**\ - Creates a new gas ticket with specified scope and mode of operation. - - ```rust - public fun add_gas_ticket( - gas_service: &mut GasService, - tool_registry: &ToolRegistry, - owner_cap: &CloneableOwnerCap, - fqn: AsciiString, - scope: Scope, - modus_operandi: ModusOperandi, - clock: &Clock, - ctx: &mut TxContext, - ) - ``` - -The tool owner can use "upon discretion of the tool" mode to be able to `revoke_gas_ticket` _at will_. - -1. **Revoking Gas Tickets**\ - Revokes a gas ticket that was created with the "Upon Discretion of Tool" mode. - - ```rust - public fun revoke_gas_ticket( - gas_service: &mut GasService, - tool_registry: &ToolRegistry, - owner_cap: &CloneableOwnerCap, - fqn: AsciiString, - scope: Scope, - ctx: &mut TxContext, - ) - ``` +# Checking Gas Payment Status -1. **Managing Gas Settings**\ - The tool owner can set the gas settings for the tool. +## 1. Onchain - ```rust - public fun get_tool_gas_setting_mut( - gas_service: &mut GasService, - tool_registry: &ToolRegistry, - owner_cap: &CloneableOwnerCap, - fqn: AsciiString, - ctx: &mut TxContext, - ): &mut Bag - ``` - -1. **De-escalating Permissions**\ - Converts a tool owner cap into a gas owner cap with reduced permissions. - - ```rust - public fun deescalate( - tool_registry: &ToolRegistry, - owner_cap: &CloneableOwnerCap, - fqn: AsciiString, - ctx: &mut TxContext, - ): CloneableOwnerCap - ``` - -
- -
- -User Operations - -#### Gas Budget Management - -1. **Donating to Tool**\ - Donate given balance to the tool's gas total. This is used to charge the user from gas extensions and make it available to the tool. +Use: -```rust -public fun donate_to_tool( - self: &mut GasService, fqn: AsciiString, amount: Balance, -) { - let tool_gas = self.tools_gas.borrow_mut(fqn); - tool_gas.vault.join(amount); -} +```move +is_execution_vertex_locked(...) ``` -1. **Adding Gas Budget**\ - Adds a gas budget for a specific scope (execution, worksheet type, or invoker address). - - ```rust - public fun add_gas_budget( - gas_service: &mut GasService, - scope: Scope, - budget: Balance, - ) - ``` - -1. **Refunding Execution Gas Budget**\ - Refunds any remaining gas budget for a completed execution to the invoker. This operation also cleans up storage by removing the execution gas state, helping to reduce storage costs. - - ```rust - public fun refund_execution_gas_budget( - gas_service: &mut GasService, - execution: &dag::DAGExecution, - ctx: &mut TxContext, - ) - ``` - -1. **Refunding Invoker Gas Budget**\ - Refunds any remaining gas budget associated with the invoker's address. - - ```rust - public fun refund_invoker_gas_budget( - gas_service: &mut GasService, - ctx: &mut TxContext, - ): Balance - ``` - -1. **Refunding Worksheet Gas Budget**\ - Refunds any remaining gas budget associated with a specific worksheet type. - - ```rust - public fun refund_worksheet_gas_budget( - gas_service: &mut GasService, - _witness: &T, - ): Balance - ``` - -
- -
- -View Operations - -#### View Operations - -1. **Checking Vertex Settlement**\ - Verifies if gas has been settled for a specific vertex in an execution. - - ```rust - public fun is_execution_vertex_settled( - gas_service: &GasService, - execution: &dag::DAGExecution, - vertex: dag::Vertex, - ): bool - ``` - -1. **Reading Tool Gas Settings**\ - Gets read-only access to a tool's gas settings. - - ```rust - public fun get_tool_gas_setting( - gas_service: &GasService, - fqn: AsciiString, - ): &Bag - ``` - -
+If `true`, the vertex can be invoked once. + +After finalization, it will no longer appear locked. + +--- + +## 2. Events + +Monitor: + +- `GasLockUpdateEvent` +- `GasUnlockUpdateEvent` +- `LeaderClaimedGasEvent` + +--- + +# Refunds + +Users can reclaim unused funds: + +- `refund_execution_gas_budget` +- `refund_invoker_gas_budget` +- `refund_worksheet_gas_budget` + +Execution refund transfers remaining unlocked funds to invoker. + +Aborted executions require: + +```move +refund_aborted_execution_gas_for_tool(...) +``` + +for each tool involved. + +--- + +# Owner Capabilities + +Two caps exist: + +## `OverTool` + +Full tool control (including vault withdrawal) + +## `OverGas` + +Restricted to gas management only: + +- Add tickets +- Revoke discretionary tickets +- Modify settings + +De-escalation: + +```move +deescalate(...) +``` + +--- + +# Security Considerations + +1. Gas is locked before invocation. +1. Tools are paid only after successful execution. +1. Aborted executions must be explicitly refunded. +1. Expiry and Limited tickets cannot be revoked. +1. Leader claims must be externally audited. + +--- + +# Key Semantics Summary + +| Stage | Budget | Limited Ticket | Expiry Ticket | +| -------- | ---------------- | ---------------------- | ------------- | +| Lock | locked += amount | locked += 1 | mark locked | +| Finalize | transfer to tool | used += 1, locked -= 1 | remove lock | +| Abort | locked -= amount | locked -= 1 | remove lock | diff --git a/nexus/tool.md b/nexus/tool.md index 3f0ca72..9a5425a 100644 --- a/nexus/tool.md +++ b/nexus/tool.md @@ -1,6 +1,6 @@ # Tool -A Tool is an offchain HTTP service or an onchain smart contract. These are invoked by the [Leader](crates/leader.md) based on instructions provided by the onchain [Workflow](packages/workflow.md). +A Tool is an off-chain HTTP service or an on-chain smart contract. These are invoked by [Leader nodes](crates/leader.md) based on instructions provided by the on-chain [Workflow](packages/workflow.md). ## Tool definitions @@ -44,7 +44,7 @@ subject to change. In addition we assume a UTF-8 representation. These will be `draft-2020-12` JSON schema definitions of the Tool inputs and outputs. This lets the Leader parse and verify data going in and coming out of the Tool. -Note that for `output_schema`, top-level `oneOf` has to be enforced by the Tool deployment process to adhere to the `OutputVariant` DAG definition. +Note that for `output_schema`, top level `oneOf` has to be enforced by the Tool deployment process to adhere to the `OutputVariant` DAG definition. For onchain tools, both of these schemas will be generated automatically during tool registration. @@ -52,9 +52,13 @@ For onchain tools, both of these schemas will be generated automatically during Off-chain Tools will expose 3 HTTP endpoints: -1. `GET /health` - operational health, should retfqn 200 if and only if the Tool is ready to be invoked -2. `GET /meta` - retfqns the Tool definition JSON -3. `POST /invoke` - this endpoints invokes the Tool logic, it accepts data in its `input_schema` format and outputs data in its `output_schema` format +1. `GET /health` - operational health, should return `200` if and only if the Tool is ready to be invoked +2. `GET /meta` - returns the Tool definition JSON +3. `POST /invoke` - this endpoint invokes the Tool logic; it accepts data in its `input_schema` format and outputs data in its `output_schema` format + +For the full transport/security contract (HTTPS/TLS and signed HTTP), see: + +- [Tool communication (HTTPS + signed HTTP)](guides/tool-communication.md) ## Onchain Tool interface @@ -67,7 +71,7 @@ Every onchain tool must provide an `execute` function with the following signatu ```move public fun execute( worksheet: &mut ProofOfUID, - // ... tool-specific parameters ... + // ... tool specific parameters ... ctx: &mut TxContext, ): ToolOutput ``` @@ -172,6 +176,14 @@ The CLI automatically: - Allows for optional customization of input and output schemas - Registers the tool in the Tool Registry with the appropriate data -## Tool authorization +## Tool authentication and key discovery (Network Auth) + +Off-chain Tools are invoked by Nexus Leader nodes. Tools and Leader nodes need a way to authenticate signed messages and discover which public key is currently valid for an identity (with support for rotation and revocation). + +Nexus uses the on-chain `nexus_workflow::network_auth` module as a trusted binding registry from identity → Ed25519 public keys. Any verifier can read the binding state on-chain to obtain the active public key for an identity and verify signed messages offline. + +Network Auth is protocol-agnostic: it does not define a transport or wire format. It only defines identities, proofs, and key lifecycle. + +Reference: -Once there are community Tools, we will need a way to authorize communication between the Leader and a Tool. This has been discussed superficially and it needs to be researched in depth in the future. +- [`nexus_workflow::network_auth`](packages/reference/nexus_workflow/network_auth.md)