diff --git a/content/symbiotic/acceptance-hooks.mdx b/content/symbiotic/acceptance-hooks.mdx new file mode 100644 index 00000000..9275a603 --- /dev/null +++ b/content/symbiotic/acceptance-hooks.mdx @@ -0,0 +1,143 @@ +--- +title: Acceptance Hooks +--- + +Acceptance hooks run before a message is batched for signing. They let operators enforce local policy or call an external approval service without changing provider event decoding. + +## Decisions + +| Decision | Meaning | +| --- | --- | +| `accept` | Message proceeds to batching. | +| `reject` | Message is marked `Rejected` and is not evaluated again. | +| `defer` | Message is held until the returned RFC 3339 `until` timestamp. | + +When multiple hooks are configured, hooks run in order. The first `reject` stops evaluation. Otherwise all hooks run, and the final decision is the latest `defer` timestamp if any hook deferred, or `accept`. + +## Operator Config + +Hooks are configured in `config/environments/.json` under `operator.acceptanceHooks`: + +```json +{ + "operator": { + "acceptanceHooks": [ + { "type": "native", "name": "provider" }, + { + "type": "webhook", + "name": "approval", + "url": "http://approval-service:8088/", + "secret": "shared-secret", + "headers": { + "Authorization": { "type": "env", "value": "APPROVAL_HOOK_AUTHORIZATION" }, + "X-Operator": "operator-a" + }, + "timeout": "5s", + "errorBackoff": "30s", + "maxAttempts": 3 + } + ] + } +} +``` + +If `acceptanceHooks` is empty or omitted, the operator runs the built-in native `provider` hook for compatibility. Today the only named native hook is `provider`. + +Webhook hooks may include a `headers` object for additional outbound HTTP headers. Header values can be plain strings or `{ "type": "env", "value": "ENV_VAR_NAME" }` references; env references are resolved at operator startup and fail startup if missing or empty. `Content-Type` and `X-Hook-Signature` are reserved by the framework. + +## Native Hooks + +Native hooks are Rust code compiled into the operator. Use them for low-latency checks that need direct access to provider types or operator-local state. + +The supported native hook today is: + +| Name | Where to implement | Reference | +| --- | --- | --- | +| `provider` | Override `Provider::acceptance_hook` in the active provider implementation | `operator/src/provider/layerzero.rs` has the minimal pass-through implementation | + +The trait contract lives in `operator/src/provider/mod.rs`: + +```rust +async fn acceptance_hook( + &self, + msg: &MessageData, + context: &AcceptanceContext, +) -> Result; +``` + +Guidelines for native hook changes: + +- Return `AcceptanceDecision::accept()` when the message should proceed. +- Return `AcceptanceDecision::Reject { reason }` only for terminal policy failures. The message will not be evaluated again. +- Return `AcceptanceDecision::Defer { until, reason }` for temporary holds. The operator persists the defer state and re-evaluates on or after `until`. +- Use `context.defer_count` to give up after repeated defers and return `reject` when appropriate. This is a total framework defer count, including policy defers and operator-driven defers caused by hook errors. +- Use `context.previous_defer_reason` only for diagnostics or policy continuity; do not parse it as a stable machine contract. +- Avoid long blocking work in native hooks. If policy depends on a slow external system, prefer a webhook hook. +- Do not implement retry loops in the hook. Return the current decision and let the operator re-evaluate later. +- Avoid side effects. If unavoidable, key them by `message_id` and make them idempotent. +- Return `Err` only for true implementation failures. Native hook errors are converted into terminal `reject` decisions by the framework, so transient conditions should return `defer` instead. + +Minimal native accept: + +```rust +async fn acceptance_hook( + &self, + _msg: &MessageData, + _context: &AcceptanceContext, +) -> Result { + Ok(AcceptanceDecision::accept()) +} +``` + +Native reject/defer examples are covered by the signer tests in `operator/src/signer/mod.rs` (`RejectAllProvider` and `DeferAllProvider`). + +If a new named native hook is needed outside the provider hook, add a new `AcceptanceHookConfig::Native` name and wire it in `SignerJob::evaluate_acceptance_hooks`. + +## Webhook Contract + +The operator sends: + +```http +POST +Content-Type: application/json +X-Hook-Signature: sha256= +``` + +```json +{ + "message": { + "metadata": { + "source_chain": 1, + "destination_chain": 31338, + "block_number": 12345, + "message_id": "0x...", + "event_tx_hash": "0x..." + }, + "data": "" + }, + "context": { + "defer_count": 0, + "previous_defer_reason": null + } +} +``` + +The webhook returns `200 OK` with one of: + +```json +{ "decision": "accept" } +{ "decision": "reject", "reason": "amount above cap" } +{ "decision": "defer", "until": "2027-05-15T12:34:56Z", "reason": "awaiting approval" } +``` + +`reason` is optional. `until` is required for `defer`. + +`context.defer_count` is the total number of times the framework has deferred this message. It includes successful policy defers and operator-driven defers caused by webhook errors. Webhook unreachability is handled by `maxAttempts`, so do not treat `defer_count` as policy-only history. + +Webhook errors are not rejections. A non-2xx response, connection error, timeout, malformed JSON, or missing/invalid `until` auto-defers the message by `errorBackoff`. After `maxAttempts` consecutive errors for the same hook and message, the operator rejects the message with reason `approval service unreachable after N attempts`. + +Webhook URLs must use `http` or `https`. For production, prefer `https` or trusted in-cluster networking because message metadata is sent in the JSON body. + +Configured headers are sent with every request after startup-time validation. Use them for service-specific auth mechanisms such as `Authorization: Bearer ...` or tenant routing headers. Do not configure `Content-Type` or `X-Hook-Signature`; the operator owns those headers. + +The reference FastAPI implementation lives in `examples/webhook-hook/`. It is intentionally not Rust: webhook hooks are language-agnostic, and the example demonstrates the wire contract an external service must implement. diff --git a/content/symbiotic/architecture.mdx b/content/symbiotic/architecture.mdx new file mode 100644 index 00000000..913f5969 --- /dev/null +++ b/content/symbiotic/architecture.mdx @@ -0,0 +1,123 @@ +--- +title: Architecture +--- + +System overview for the Symbiotic multi-provider template. + +## Core Model + +1. One active provider per running stack, selected in `config/environments/.json`. +2. Shared off-chain runtime: + - OZ Monitor for ingress + - 3 operator processes + - 3 Symbiotic relay sidecars for BLS signatures + - OZ Relayer for destination submission + - Redis queue +3. Provider-specific on-chain contracts and calldata format. + +## Provider Matrix + +| Provider | Source ingress event | Destination submit call | Local | Testnet | Mainnet | +| --- | --- | --- | --- | --- | --- | +| [`layerzero`](/symbiotic/layerzero) | `JobAssigned` | `SymbioticLayerZeroDVN.submitProof(...)` | Supported | Supported | Verified end-to-end (operator-owned config) | +| [`chainlink_ccv`](/symbiotic/chainlink-ccv) | `CCIPMessageSent` | `OffRamp.execute(...)` | Supported | Supported | Not yet | + +## Shared Off-Chain Runtime + +```mermaid +flowchart LR + subgraph source["Source Chain"] + Event["Provider Ingress Event"] + end + + Event --> Monitor["OZ Monitor"] + Monitor --> Operators["Operators (x3)"] + Operators <--> Relays["Symbiotic Relays\n(BLS sidecars)"] + Operators --> Relayer["OZ Relayer"] + + subgraph dest["Destination Chain"] + Submit["Provider Destination Call"] + end + + Relayer --> Submit +``` + +The provider abstraction decides: +- which source-chain event the monitor watches +- how operators encode the signed payload +- which destination call the relayer submits + +## Merkle Batching + +Messages are collected into Merkle trees so one quorum signature can cover many messages: + +1. ingress events become message records +2. operators batch message leaves into a Merkle root +3. the root is signed through the Symbiotic relay sidecars +4. proofs let the destination verify individual messages against that signed root + +## Symbiotic Integration + +Symbiotic provides the shared security layer: + +- operators register BLS public keys +- settlement verifies quorum signatures +- voting power and epoch rules define signature validity + +## Operator Internals + +| Module | Location | Purpose | +|--------|----------|---------| +| API Server | `operator/src/api/` | Axum HTTP server, webhook endpoint, debug routes | +| Provider | `operator/src/provider/` | Provider trait, event decoding, message storage | +| SignerJob | `operator/src/signer/` | Batches messages into Merkle trees, requests BLS signatures | +| RelaySubmitterJob | `operator/src/relay_submitter/` | Submits signed proofs via OZ Relayer | +| Storage | `operator/src/storage/` | redb key-value store for messages, Merkle trees, and submissions | +| Crypto | `operator/src/crypto/` | Merkle tree construction, leaf hashing, signing message encoding | + +## Adding a New Provider + +1. Implement the `Provider` trait in `operator/src/provider/.rs`. + +```rust +#[async_trait] +pub trait Provider: Send + Sync + 'static { + fn name(&self) -> &'static str; + async fn handle_webhook_event(&self, event: &WebhookEvent) -> Result<(), ProviderError>; + + // Required for a functional provider: the trait ships default impls for + // these that return an error, so the signing/submission path fails until + // you override all three. + fn compute_leaf_hash(&self, message: &MessageData) -> Result; + fn encode_signing_message(&self, tree: &MerkleTreeData) -> Result, ProviderError>; + fn prepare_submission( + &self, + message: &MessageData, + tree: &MerkleTreeData, + proof: &MerkleProof, + target_address: &str, + ) -> Result; + + // Optional overrides (sensible defaults provided): + fn register_api_routes(&self, router: Router) -> Router { router } + fn max_batch_size(&self) -> usize { usize::MAX } + async fn acceptance_hook( + &self, + _msg: &MessageData, + _context: &AcceptanceContext, + ) -> Result { + Ok(AcceptanceDecision::accept()) + } + fn verifier_result_for( + &self, + _id: &B256, + ) -> Result, ProviderError> { + Ok(None) + } +} +``` + +2. Add provider config to `operator/src/config/mod.rs`. +3. Register the provider in `operator/src/provider/mod.rs`. +4. Add monitor templates under `config/templates/oz-monitor/`. +5. Add `docs/.mdx` and update [the docs index](/symbiotic). diff --git a/content/symbiotic/chainlink-ccv.mdx b/content/symbiotic/chainlink-ccv.mdx new file mode 100644 index 00000000..0c38ee5a --- /dev/null +++ b/content/symbiotic/chainlink-ccv.mdx @@ -0,0 +1,93 @@ +--- +title: Chainlink CCV +--- + +Symbiotic-secured Cross-Chain Verifier for Chainlink CCIP-compatible message verification. + +## Overview + +The CCV provider watches `CCIPMessageSent` events, builds the verifier payload, collects BLS attestations through Symbiotic relay sidecars, and submits execution calldata to the destination OffRamp path. Success is confirmed when the destination emits `MessageExecuted(messageId)`. + + + +This template supports the Symbiotic CCV path only. The Chainlink auxiliary devenv stack is not required. + + + +## Message Flow + +```mermaid +sequenceDiagram + participant App as Source App + participant OnRamp as OnRamp (Source) + participant Monitor as OZ Monitor + participant Operators as Operators (x3) + participant Relay as Symbiotic Relay (BLS) + participant Relayer as OZ Relayer + participant OffRamp as OffRamp (Dest) + participant CCV as SymbioticCCV (Dest) + + App->>OnRamp: sendMessage() + OnRamp-->>Monitor: CCIPMessageSent event + Monitor->>Operators: HMAC webhook + Operators->>Operators: build CCV payload + Merkle tree + Operators->>Relay: sign Merkle root + Relay-->>Operators: aggregated signature + Operators->>Relayer: OffRamp.execute calldata + Relayer->>OffRamp: execute(message, ccvs, verifierResults) + OffRamp->>CCV: verifyMessage(message, messageId, verifierResults) + CCV-->>OffRamp: verified + OffRamp-->>OffRamp: emit MessageExecuted(messageId) +``` + +## Contracts and Code + +### Contracts + +- `contracts/src/ccv/SymbioticCCV.sol` +- `contracts/src/ccv/interfaces/` +- `contracts/src/ccv/libraries/` +- `contracts/src/symbiotic/Settlement.sol` +- `contracts/src/symbiotic/KeyRegistry.sol` +- `contracts/src/symbiotic/Driver.sol` + +### Operator + +- `operator/src/provider/chainlink_ccv.rs` +- `operator/src/provider/mod.rs` + +### Monitor Templates + +- `config/templates/oz-monitor/monitors/ccip_message_sent.json` + +## Configuration + +Select CCV in `config/environments/.json`: + +```json +{ + "activeProvider": "chainlink_ccv" +} +``` + +CCIP chain selectors come from `chains.source.ccipChainSelector` and `chains.destination.ccipChainSelector`, which are required for non-local environments. They can be overridden at runtime via the `CCV_SOURCE_CHAIN_SELECTOR` / `CCV_DEST_CHAIN_SELECTOR` environment variables. For local Anvil only (chainId `31337`), the selector falls back to `chainId` when `ccipChainSelector` is unset. + +Address resolution order: + +1. `CCV_*` environment variables +2. `deployments/.json` + +Available overrides: + +| Variable | Description | +|----------|-------------| +| `CCV_SOURCE_ADDRESS` | SymbioticCCV on source chain | +| `CCV_DEST_ADDRESS` | SymbioticCCV on destination chain | +| `CCV_SOURCE_ONRAMP_ADDRESS` | Source OnRamp-compatible contract | +| `CCV_DEST_OFFRAMP_ADDRESS` | Destination OffRamp submit target | +| `CCV_SOURCE_CHAIN_SELECTOR` | Override the source CCIP chain selector | +| `CCV_DEST_CHAIN_SELECTOR` | Override the destination CCIP chain selector | + +Local and testnet are supported (testnet uses the `testnet-ccv` environment: `config/environments/testnet-ccv.json` and `deployments/testnet-ccv.json`, run via `ENV=testnet-ccv`); mainnet is not yet supported for CCV. `make watch` only succeeds once destination `MessageExecuted(messageId)` is observed. + +See [Setup](/symbiotic/setup) and [CLI & API Reference](/symbiotic/cli) for operation. diff --git a/content/symbiotic/cli.mdx b/content/symbiotic/cli.mdx new file mode 100644 index 00000000..ce735454 --- /dev/null +++ b/content/symbiotic/cli.mdx @@ -0,0 +1,145 @@ +--- +title: CLI & API Reference +--- + +Command-line and HTTP interfaces for the Symbiotic template. + +When `activeProvider` is `layerzero`, `make send` and `make e2e` use the managed `ExampleOApp` starter contract. If `layerzero.oapp.enabled` is `false`, those commands are unavailable for that environment. + +## Core Commands + +### `make send` + +Send one test message through the active provider. + +```bash +make send +make send MSG="test message" +make send ENV=testnet MSG="hello" +``` + +### `make watch` + +Watch a previously sent message until the destination path succeeds. + +```bash +make watch +make watch ENV=testnet TIMEOUT=300 +make watch GUID=0x... +make watch TX=0x... +``` + +| Variable | Description | +|----------|-------------| +| `ENV` | Environment (default: local) | +| `TIMEOUT` | Max wait in seconds | +| `GUID` | Watch a specific message by GUID | +| `TX` | Watch a message by source tx hash | + +### `make e2e` + +Send a message, then watch it to completion. + +```bash +make e2e +make e2e MSG="custom message" +make e2e ENV=testnet MSG="hello" TIMEOUT=180 +``` + +LayerZero starter OApp addresses are published under: + +```text +deployments/.json -> layerzero.oapp.{source,destination} +``` + +## Direct xtask Usage + +If you want to bypass `make`, use the Rust CLI directly: + +```bash +cargo xtask --env local msg send "hello" +cargo xtask --env local msg watch --timeout 120 +cargo xtask --env local msg e2e "hello" --timeout 120 +``` + +Global xtask overrides: + +- `--env` selects the named environment +- `--env-config` overrides the environment JSON path +- `--deployments` overrides the deployments JSON path +- `--generated-dir` overrides the generated runtime output directory + +## Manual Operator Invocation + +`make dev-operator` is the normal entrypoint, but the raw binary requires four flags: + +```bash +cargo run -p operator -- \ + --environment config/environments/local.json \ + --deployments deployments/local.json \ + --sidecar-address http://localhost:8081 \ + --relayer-id operator-relayer-1 +``` + +| Flag | Purpose | +|------|---------| +| `--environment` | Environment JSON input | +| `--deployments` | Deployment addresses JSON input | +| `--sidecar-address` | Symbiotic relay sidecar gRPC address | +| `--relayer-id` | OZ Relayer identity to bind to the destination target | + +## Message Cache + +After `send`, xtask stores message details in: + +```text +generated//msg-cache.json +``` + +`watch` uses this cache when `--id` and `--tx` are omitted. + +## HTTP API + +Each operator exposes HTTP endpoints on ports `3001-3003`. + +### Ingress Webhooks + +| Endpoint | Purpose | Auth | +|----------|---------|------| +| `POST /webhook/events` | Provider ingress events from OZ Monitor | `X-Signature` + `X-Timestamp` using `WEBHOOK_SECRET` | +| `POST /api/v1/webhooks/oz-relayer` | Transaction status updates from OZ Relayer | `X-Signature` using `OZ_RELAYER_WEBHOOK_SECRET` | + +### Debug and Health + +| Endpoint | Purpose | +|----------|---------| +| `GET /debug/v1/messages` | List messages with processing and submission state | +| `GET /debug/v1/messages/:message_id` | Read one message by ID | +| `GET /debug/v1/pending` | List pending Merkle roots | +| `GET /healthz` | Liveness check | + +Example: + +```bash +curl -s http://localhost:3001/debug/v1/messages +curl "http://localhost:3001/debug/v1/messages?status=pending&limit=10" +``` + +### LayerZero Proof Endpoints + +LayerZero operators also expose: + +| Endpoint | Purpose | +|----------|---------| +| `POST /api/v1/layerzero/proof` | Return Merkle proofs for processed messages | +| `POST /api/v1/layerzero/verify` | Verify a Merkle proof in test workflows | + +## Webhook Template Requirements + +OZ Monitor trigger templates live under `config/templates/oz-monitor/triggers/` and are copied into `generated//oz-monitor/triggers/`. + +Required settings: + +- `url.value`: operator webhook URL +- `secret.value`: must match the operator webhook secret +- `payload_mode: "raw"` diff --git a/content/symbiotic/deployment.mdx b/content/symbiotic/deployment.mdx new file mode 100644 index 00000000..b1100d62 --- /dev/null +++ b/content/symbiotic/deployment.mdx @@ -0,0 +1,91 @@ +--- +title: Deployment +--- + +Deploying and operating the stack beyond local development. + + + +Testnet supports both providers: `layerzero` (`config/environments/testnet.json`, `ENV=testnet`) and `chainlink_ccv` (`config/environments/testnet-ccv.json`, `ENV=testnet-ccv`). The workflow below uses `ENV=testnet` (LayerZero); for the CCV path substitute `ENV=testnet-ccv`. Mainnet CCV is not yet supported. + + + +## Testnet Workflow + +### 1. Generate keystores + +```bash +cargo xtask --env testnet generate-signer \ + --name deployer --name operator-1 --name operator-2 --name operator-3 + +cargo xtask --env testnet generate-signer \ + --name signer-1 --name signer-2 --name signer-3 +``` + +Each command prompts for a passphrase interactively (hidden input, confirmed). Use separate passphrases for operator and relayer keystores. The `--env testnet` flag writes keys under `config/keys/testnet/`, where `config/environments/testnet.json` expects them — without it, keys default to `config/keys/local/` and `make deploy ENV=testnet` will not find them. + +Relayer keystores (`signer-1/2/3`) are generated automatically by `make deploy` for any environment if they don't already exist (using `KEYSTORE_PASSPHRASE`). Generating them manually above is optional — it only lets you set the passphrase interactively instead of reading it from `.env.testnet`. + +### 2. Configure environment + +Copy `.env.example` to `.env.testnet` and fill in the RPC URLs (Base Sepolia source, Sepolia destination) and keystore passphrases. Signer definitions in `config/environments/testnet.json` reference these passphrases. + +### 3. Deploy and run + +```bash +make validate ENV=testnet +make deploy ENV=testnet +make refresh-genesis ENV=testnet # only when validation says genesis is stale +make start ENV=testnet +make e2e ENV=testnet MSG="hello" +``` + +- `validate` runs read-only checks against config, RPC reachability, deployment state, operator state, and relayer signer safety. +- `deploy` deploys managed contracts and regenerates `deployments/testnet.json` and `generated/testnet/`. +- `refresh-genesis` repairs stale settlement genesis without redeploying contracts. +- `start` detects the environment type and starts the appropriate services. + +By default, the LayerZero testnet config also deploys the starter `ExampleOApp`, so `make e2e ENV=testnet` remains part of the main workflow. + +If you want provider infrastructure only, set `layerzero.oapp.enabled` to `false` in `config/environments/testnet.json` before deploying. `validate` will still pass, but `make send` and `make e2e` will be unavailable. + +## Required Inputs + +Testnet settings live in `config/environments/.json`. `.env.` must provide: + +```bash +DEPLOYER_PASSPHRASE= +OPERATOR_1_PASSPHRASE= +OPERATOR_2_PASSPHRASE= +OPERATOR_3_PASSPHRASE= +KEYSTORE_PASSPHRASE= # used by OZ Relayer at runtime +``` + +Operator and deployer passphrases are resolved by `xtask` via the `signers` config. `KEYSTORE_PASSPHRASE` is read directly by the OZ Relayer container to decrypt relayer keystores at runtime. + +## Operational Notes + +- Local uses `make chains && make deploy && make start`; testnet uses `make deploy ENV=testnet` plus `make start ENV=testnet`. +- Testnet uses `docker-compose.yml` only, not the local overlay. +- Canonical deployment addresses always live in `deployments/.json`. + +## Sidecar Image Compatibility + +The Symbiotic relay sidecar (`symbioticfi/relay`) is still in release-candidate phase. The pinned version in `docker-compose.yml` and the `RELAY_IMAGE` default in `xtask/src/genesis.rs` must stay in sync. Known states observed during integration: + +| Tag | Status | Notes | +| --- | --- | --- | +| `1.0.1-rc4` | works | Current pin. First image where aggregation produces proofs against real mainnet. | +| `1.0.1-rc3.0.20260507060511-...` | broken | Starts cleanly but the aggregator never produces proofs even with full quorum signing. | +| `1.0.1-20260326074346-da0bce8ba949` | broken on non-local | Panics at config init with an `errors.As` target-type bug when multiple chains are configured. Local works because the codepath differs. | + +If you bump the tag, run a full testnet e2e first and verify proofs are aggregated end-to-end. The `1.0.1` GA tag has not yet shipped from Symbiotic — track upstream releases before promoting any tag past `rc4`. + +## Common Deployment Failures + +- `validation failed: genesis stale`: run `make refresh-genesis ENV=`. +- `insufficient funds for gas`: fund the deployer account from the deployer keystore address. +- Sidecars hit RPC rate limits: use stronger RPCs or reduce sidecar load temporarily. +- Keys look drained immediately: do not reuse known local/dev keys on public networks. + +For runtime failures after deployment, see [Troubleshooting](/symbiotic/troubleshooting). diff --git a/content/symbiotic/index.mdx b/content/symbiotic/index.mdx new file mode 100644 index 00000000..e9db01b9 --- /dev/null +++ b/content/symbiotic/index.mdx @@ -0,0 +1,28 @@ +--- +title: Symbiotic Templates +--- + +## For Operators + +Running, configuring, and monitoring the stack. + +1. [Setup](/symbiotic/setup) -- Config structure, environment setup, running locally +2. [Deployment](/symbiotic/deployment) -- Testnet deployment +3. Choose your provider: + - [LayerZero](/symbiotic/layerzero) -- DVN for LayerZero V2 + - [Chainlink CCV](/symbiotic/chainlink-ccv) -- Cross-Chain Verifier for CCIP +4. [Acceptance Hooks](/symbiotic/acceptance-hooks) -- Native and webhook policy checks before batching +5. [CLI & API Reference](/symbiotic/cli) -- Commands, HTTP endpoints, webhook config +6. [Troubleshooting](/symbiotic/troubleshooting) -- Common issues and debugging + +## For Integrators + +Understanding the system and adding new providers. + +1. [Architecture](/symbiotic/architecture) -- Provider model, shared infra, Merkle batching, BLS signing +2. Choose your provider: + - [LayerZero](/symbiotic/layerzero) -- Message flow, contracts, code pointers + - [Chainlink CCV](/symbiotic/chainlink-ccv) -- Message flow, contracts, code pointers +3. [Acceptance Hooks](/symbiotic/acceptance-hooks) -- Hook contract and webhook wire format +4. [Architecture: Adding a New Provider](/symbiotic/architecture#adding-a-new-provider) -- Provider trait, registration, templates +5. [Security](/symbiotic/security) -- Trust model, access control, invariants diff --git a/content/symbiotic/layerzero.mdx b/content/symbiotic/layerzero.mdx new file mode 100644 index 00000000..1c932c8f --- /dev/null +++ b/content/symbiotic/layerzero.mdx @@ -0,0 +1,117 @@ +--- +title: LayerZero +--- + +Symbiotic-secured DVN for LayerZero V2 cross-chain messaging. + +## Overview + +The LayerZero provider watches `JobAssigned` events emitted by the `SymbioticLayerZeroDVN` source DVN (whose `assignJob` function is called by `SendUln302`), batches them into Merkle trees, collects BLS attestations through Symbiotic relay sidecars, and submits proofs to the destination DVN. The destination DVN verifies the quorum via Settlement and forwards verification into `ReceiveUln302`. + +## Message Flow + +```mermaid +sequenceDiagram + participant App as User App + participant SendUln as SendUln302 (Source) + participant DVN_S as DVN.assignJob (Source) + participant Monitor as OZ Monitor + participant Operators as Operators (x3) + participant Relay as Symbiotic Relay (BLS) + participant Relayer as OZ Relayer + participant DVN_D as DVN.submitProof (Dest) + participant Settlement as Settlement (BLS verify) + participant RecvUln as ReceiveUln302 (Dest) + + App->>SendUln: send message + SendUln->>DVN_S: assignJob() + DVN_S-->>Monitor: JobAssigned event + Monitor->>Operators: HMAC webhook + Operators->>Operators: batch into Merkle tree + Operators->>Relay: sign Merkle root + Relay-->>Operators: aggregated signature + Operators->>Relayer: submitProof calldata + Relayer->>DVN_D: submitProof(root, proof, signatures) + DVN_D->>Settlement: verify BLS quorum + Settlement-->>DVN_D: quorum valid + DVN_D->>RecvUln: verify() +``` + +## Contracts and Code + +### Contracts + +- `contracts/src/SymbioticLayerZeroDVN.sol` +- `contracts/src/symbiotic/Settlement.sol` +- `contracts/src/symbiotic/KeyRegistry.sol` +- `contracts/src/symbiotic/VotingPowers.sol` +- `contracts/src/symbiotic/Driver.sol` +- `contracts/src/examples/ExampleOApp.sol` + +### Operator + +- `operator/src/provider/layerzero.rs` +- `operator/src/provider/mod.rs` +- `operator/src/crypto/mod.rs` +- `operator/src/signer/mod.rs` +- `operator/src/relay_submitter/mod.rs` + +### Monitor Templates + +- `config/templates/oz-monitor/monitors/layerzero_job_assigned.json` +- `config/templates/oz-monitor/triggers/webhook_layerzero.json` + +## Configuration + +Select LayerZero in `config/environments/.json`: + +```json +{ + "activeProvider": "layerzero" +} +``` + +Shared chain config: + +| Field | Description | +|-------|-------------| +| `chains.source.chainId` | Source chain ID | +| `chains.destination.chainId` | Destination chain ID | +| `chains.source.eid` | LayerZero endpoint ID for source | +| `chains.destination.eid` | LayerZero endpoint ID for destination | + +LayerZero predeploys for non-local environments live under `chains..predeploys.layerzero`: + +```json +{ + "predeploys": { + "layerzero": { + "endpoint": "0x6EDCE65403992e310A62460808c4b910D972f10f", + "sendUln302": "0xC1868e054425D378095A003EcbA3823a5D0135C9" + } + } +} +``` + +`make deploy` and `make start` use these values to generate the runtime chain maps under `generated//`. + +The template also manages a starter OApp by default: + +```json +{ + "layerzero": { + "oapp": { + "enabled": true + } + } +} +``` + +- `true`: deploy and wire `ExampleOApp` on both chains +- `false`: skip the starter OApp and run LayerZero in provider-only mode + +Starter OApp deployments are published under `deployments/.json` at `layerzero.oapp.source` and `layerzero.oapp.destination`. + +Provider validation does not require the starter OApp to exist. Disabling it only removes the `make send` and `make e2e` demo flow for that environment. + +Local and testnet are supported. See [Setup](/symbiotic/setup), [Deployment](/symbiotic/deployment), and [CLI & API Reference](/symbiotic/cli) for operation. diff --git a/content/symbiotic/security.mdx b/content/symbiotic/security.mdx new file mode 100644 index 00000000..d5ffd3ae --- /dev/null +++ b/content/symbiotic/security.mdx @@ -0,0 +1,67 @@ +--- +title: Security +--- + +Security architecture and trust assumptions for the Symbiotic template. + +## Shared Trust Model + +| Entity | Trust Level | Notes | +|--------|-------------|-------| +| Settlement | Trusted | Verifies BLS quorum signatures | +| Authorized submitters | Semi-trusted | Can submit proofs, but cannot forge valid signatures | +| Owner | Trusted | Handles pause, unpause, and submitter management | +| External users | Untrusted | No privileged access | + +Webhook ingress between OZ Monitor, OZ Relayer, and operators uses HMAC-SHA256 shared secrets. See [CLI & API Reference](/symbiotic/cli#http-api) for the header format. + +## Runtime API Security + +Operator runtime behavior that is easy to miss: + +- `/healthz` bypasses authentication so orchestration health checks keep working. +- `/webhook/events` verifies HMAC over the raw body plus `X-Timestamp`, and rejects requests outside a 300s timestamp window. +- `/api/v1/webhooks/oz-relayer` verifies a separate base64 HMAC signature over the raw JSON body. +- Authentication failures on ingress webhooks intentionally return generic `401` responses instead of detailed errors. +- CORS is off by default. +- Debug routes are security-gated and may be hidden by middleware before the handler runs. + +Secrets are loaded from `WEBHOOK_SECRET` and `OZ_RELAYER_WEBHOOK_SECRET` at startup, not from the environment JSON. + +## LayerZero DVN Security + +The LayerZero provider trusts `SendUln302` as the only valid source-chain caller for `assignJob`. + +### Access Control + +| Function | Caller | Purpose | +|----------|--------|---------| +| `assignJob` | `SendUln302` only | Register verification jobs and emit `JobAssigned` | +| `getFee` | Anyone | Quote verification fee | +| `submitProof` | Authorized submitters | Submit signed Merkle proofs | +| `addSubmitter` / `removeSubmitter` | Owner | Manage submitter whitelist | +| `setBaseFee` / `pause` / `unpause` / `withdraw` / `transferOwnership` | Owner | Administrative controls | + +### Invariants + +1. `verifiedLeaves[leaf]` only moves from `false` to `true`. +2. `verifiedRoots[root]` only moves from `false` to `true`. +3. Uncached roots require a valid BLS quorum from Settlement. +4. Verified packet headers must have the expected length and destination EID. +5. `assignJob` rejects `msg.value > 0`; the DVN does not custody fees. + +## Chainlink CCV Security + +### Access Control + +| Function | Caller | Purpose | +|----------|--------|---------| +| `forwardToVerifier` | OnRamp | Register a message for CCV verification | +| `verifyMessage` | OffRamp | Verify the message on the destination path | +| `getFee` | Anyone | Quote verification fee | + +### Invariants + +1. Verification requires a valid BLS quorum signature from Settlement. +2. The derived message ID must match the message payload being verified. +3. Settlement epoch data must be fresh, or verification reverts with `EpochTooStale`. diff --git a/content/symbiotic/setup.mdx b/content/symbiotic/setup.mdx new file mode 100644 index 00000000..85198e11 --- /dev/null +++ b/content/symbiotic/setup.mdx @@ -0,0 +1,230 @@ +--- +title: Setup +--- + +Getting the stack running locally. + +## Prerequisites + +- Docker and Docker Compose v2+ +- [Foundry](https://book.getfoundry.sh/getting-started/installation) (`forge`, `cast`, `anvil`) +- [pnpm](https://pnpm.io/installation) (for `make install`, which installs the contract dependencies) +- [Rust/Cargo](https://rustup.rs/) (required — `make chains`, `deploy`, `start`, `status`, `e2e`, and `validate` all run `cargo xtask`) +- `jq` + +## Config Layout + +```text +config/ +├── environments/ # Per-environment chain, provider, and signer config +├── keys/ # Encrypted keystores (generated by generate-signer) +├── templates/ # OZ Monitor / OZ Relayer templates +├── oz-monitor/ # Static monitor config +└── oz-relayer/ # Static relayer config + +deployments/ +└── .json # Canonical deployment addresses + +generated/ +└── / # Generated runtime config and message cache +``` + +`make deploy` and `make start` read from `config/` and write runtime state into `deployments/.json` and `generated//`. + +## Signers + +Signer keys are configured in `config/environments/.json` under `signers`. Three signer types are supported: + +### Anvil (local only) + +Derives keys from Anvil's well-known mnemonic by index. Zero setup needed. + +```json +{ + "signers": { + "deployer": { "type": "anvil", "index": 0 }, + "operator-1": { "type": "anvil", "index": 1 } + } +} +``` + +### Local (encrypted keystore) + +Reads from an encrypted keystore file. Generate one with: + +```bash +cargo xtask --env generate-signer --name deployer +``` + +The command prompts for a passphrase interactively (hidden input, confirmed). For CI, pass `--passphrase` explicitly. + +Then reference it in the environment config: + +```json +{ + "signers": { + "deployer": { + "type": "local", + "path": "config/keys//deployer.json", + "passphrase": { "type": "env", "value": "DEPLOYER_PASSPHRASE" } + } + } +} +``` + +### Env (environment variable) + +Reads a raw private key from an environment variable. Useful for CI/CD where keys are injected via secrets. + +```json +{ + "signers": { + "deployer": { + "type": "env", + "value": "DEPLOYER_PRIVATE_KEY" + } + } +} +``` + +### Relayer keystores + +The OZ Relayer needs its own encrypted keystores (`signer-1.json`, `signer-2.json`, `signer-3.json` in `config/keys//`). `make deploy` generates these automatically for any environment if they don't already exist (using `KEYSTORE_PASSPHRASE`). Generating them manually is optional — it lets you set the passphrase via an interactive prompt instead of reading it from the env file: + +```bash +cargo xtask --env generate-signer --name signer-1 --name signer-2 --name signer-3 +``` + +## Environment Files + +Each environment reads secrets from `.env.` (e.g. `.env.local`, `.env.testnet`). + +- **Local**: no `.env.local` needed for keys — anvil signers derive keys automatically, and relayer keystores are generated on first deploy. +- **Testnet**: copy `.env.example` to `.env.testnet` and fill in the RPC URLs and keystore passphrases (the relevant sections are commented out by default). + + + +Runtime startup requires `WEBHOOK_SECRET`, `OZ_RELAYER_WEBHOOK_SECRET`, and `OZ_RELAYER_API_KEY`. The two webhook secrets (`WEBHOOK_SECRET` and `OZ_RELAYER_WEBHOOK_SECRET`) must each be at least 32 characters. + + + +## Select a Provider + +Set the active provider in `config/environments/.json`: + +```json +{ + "activeProvider": "layerzero" +} +``` + +Supported values: +- `layerzero` +- `chainlink_ccv` + +Provider-specific configuration lives in [LayerZero](/symbiotic/layerzero#configuration) and [Chainlink CCV](/symbiotic/chainlink-ccv#configuration). + +## Environment JSON + +`config/environments/.json` is the source of truth for deploy, validate, runtime rendering, and operator startup. + +### Chain Settings + +Each chain entry under `chains.source` and `chains.destination` supports: + +| Field | Purpose | +|-------|---------| +| `name` | Human label for logs and rendered runtime files | +| `chainId` | EVM chain ID | +| `eid` | LayerZero endpoint ID | +| `confirmations` | Confirmation depth used in rendered monitor and relayer config | +| `blockTimeMs` | Block time hint used when rendering monitor and relayer networks | +| `rpcUrls` | Non-local RPC sources; supports plain URLs or `{ "type": "env", "value": "SOURCE_RPC_URL" }` style env references | +| `predeploys` | Provider-specific or shared predeployed contract addresses | + +### Relay Timing + +The `relay` block controls settlement and genesis timing used by deploy and refresh flows: + +| Field | Purpose | +|-------|---------| +| `epochDurationSeconds` | Epoch length for settlement timing | +| `slashingWindowSeconds` | Slashing window length | +| `epochStartDelaySeconds` | Delay before a new epoch becomes active | + +### Monitor and Relayer Rendering + +For non-local environments, these blocks are required to render runtime artifacts: + +| Block | Field | Purpose | +|-------|-------|---------| +| `ozMonitor` | `cronSchedule` | Poll cadence for the source-chain monitor | +| `ozMonitor` | `maxPastBlocks` | Backfill window for source-chain log reads | +| `ozRelayer` | `defaultSpeed` | Default submission speed tier for OZ Relayer | +| `ozRelayer` | `minBalanceWei` | Runtime threshold used by `validate` to flag low relayer-signer balances | +| `funding` | `operatorAmountWei` | Wei sent to each operator EOA during `make deploy` | +| `funding` | `signerAmountWei` | Wei sent to each explicit relayer signer during `make deploy` | +| `funding` | `minBalanceThresholdWei` | Skip funding if the target balance is already at or above this threshold. Must be ≤ `operatorAmountWei` and `signerAmountWei` (validated at config load) | + +Local environments copy static monitor and relayer config, so `ozMonitor` and `ozRelayer` mainly matter on non-local envs. `funding` is required on every env; it drives both the Solidity deploy script and the genesis-time `cast send` topups, eliminating the per-call-site defaults that previously existed for the same parameter. + +## Operator Settings + +The user-editable operator tuning lives in `config/environments/.json` under `operator`: + +```json +{ + "operator": { + "logLevel": "info", + "eventPollInterval": "15s", + "signJobInterval": "1s", + "signWorkerCount": 5, + "minBatchSize": 1, + "acceptanceHooks": [], + "enableDebugEndpoints": false + } +} +``` + +If omitted, the defaults are: + +| Field | Default | Purpose | +|-------|---------|---------| +| `logLevel` | `info` | Operator log verbosity | +| `eventPollInterval` | `15s` | How often to scan for pending messages to batch | +| `signJobInterval` | `1s` | How often to retry pending signing jobs | +| `signWorkerCount` | `5` | Concurrent signing workers | +| `minBatchSize` | `1` | Minimum messages required before building a Merkle tree | +| `acceptanceHooks` | native provider hook | Ordered hooks that can accept, reject, or defer messages before batching | +| `enableDebugEndpoints` | `false` | Expose `/debug/v1/*` endpoints on operator HTTP servers | + +See [Acceptance Hooks](/symbiotic/acceptance-hooks) for native and webhook configuration. + +The operator reads `config/environments/.json` directly (it is mounted into each operator container), so that file is the source of truth. Change the environment JSON, then rerun `make deploy` or `make start`. + +## Run Locally + +```bash +make install # one-time: install contract dependencies (pnpm) +make chains # start local Anvil chains +make deploy # deploy contracts +make start # start services +make status # check everything is running +make e2e # send and verify a test message +``` + +The workflow is split into three steps: `chains` starts Anvil, `deploy` deploys contracts, and `start` brings up services. Use `make start RESET=1` to wipe local runtime state before starting. + +For faster Rust iteration, start the stack once and then run: + +```bash +make dev-operator +``` + +## Generated Runtime State + +- `deployments/.json` for canonical deployment addresses +- `generated//` for rendered monitor, relayer, and sidecar runtime config +- `generated//msg-cache.json` for the last message watched by xtask + +See [CLI & API Reference](/symbiotic/cli) for command details and [Deployment](/symbiotic/deployment) for testnet operation. diff --git a/content/symbiotic/troubleshooting.mdx b/content/symbiotic/troubleshooting.mdx new file mode 100644 index 00000000..347616c7 --- /dev/null +++ b/content/symbiotic/troubleshooting.mdx @@ -0,0 +1,72 @@ +--- +title: Troubleshooting +--- + +Common operator-side failures and the shortest useful checks for each. + +## First Checks + +```bash +make status +make logs-operators +make logs-monitor +make logs-relayer +curl -s http://localhost:3001/debug/v1/messages +curl -s http://localhost:3001/debug/v1/pending +``` + +## Shared Failures + +### Webhook or authentication failures + +- Confirm operators are reachable from OZ Monitor. +- Verify `WEBHOOK_SECRET` and `OZ_RELAYER_WEBHOOK_SECRET` match the sender config exactly. +- Ensure trigger templates still use `payload_mode: "raw"`. + +### BLS signatures do not aggregate + +- Check sidecar health with `docker compose ps symbiotic-relay-1`. +- Check sidecar logs with `docker compose logs symbiotic-relay-1`. +- Verify operator keys are registered in Settlement and all operators saw the same ingress event. + +### Quorum is never reached + +- Make sure all required operators are running. +- Confirm all operators received the same message. +- Check Settlement voting power and registered keys. + +### Relayer submission keeps failing + +- Verify the relayer health endpoint responds with your API key. +- Check `make logs-relayer` for rate limits, estimate-gas failures, or permanent 4xx errors. +- For 429s, back off or use stronger RPC capacity. + +### Fresh local state is stuck or inconsistent + +- If contracts, genesis, or generated config look out of sync, reset with `make clean && make deploy && make start`. +- If Anvil stopped responding, restart the relevant containers before doing a full reset. + +## LayerZero + +### `submitProof` reverts + +- Check that the relayer submitter address is whitelisted on the DVN. +- Check Settlement state: registered keys, quorum threshold, and current epoch assumptions. +- Search `make logs-relayer` for the revert reason before changing config. + +## Chainlink CCV + +### `EpochTooStale` / `0xf5ab0d81` + +- Settlement epoch data is stale. +- Refresh genesis or adjust the epoch timing parameters before retrying. + +### `make watch` never succeeds + +- CCV success is destination `MessageExecuted(messageId)`, not just relayer submission. +- If the relayer submitted but watch still fails, inspect destination execution and relayer estimate-gas logs. + +### Estimate-gas failures + +- Common causes are stale epoch data, wrong CCV addresses, or uninitialized settlement state. +- Start with `make logs-relayer | grep -E "estimate_gas|custom error"`. diff --git a/src/navigation/ethereum-evm.json b/src/navigation/ethereum-evm.json index 8ce1463b..f88c76ed 100644 --- a/src/navigation/ethereum-evm.json +++ b/src/navigation/ethereum-evm.json @@ -797,6 +797,62 @@ "type": "separator", "name": "Open Source Tools" }, + { + "type": "folder", + "name": "Symbiotic Templates", + "index": { + "type": "page", + "name": "Overview", + "url": "/symbiotic" + }, + "children": [ + { + "type": "page", + "name": "Setup", + "url": "/symbiotic/setup" + }, + { + "type": "page", + "name": "Architecture", + "url": "/symbiotic/architecture" + }, + { + "type": "page", + "name": "LayerZero", + "url": "/symbiotic/layerzero" + }, + { + "type": "page", + "name": "Chainlink CCV", + "url": "/symbiotic/chainlink-ccv" + }, + { + "type": "page", + "name": "Acceptance Hooks", + "url": "/symbiotic/acceptance-hooks" + }, + { + "type": "page", + "name": "CLI & API Reference", + "url": "/symbiotic/cli" + }, + { + "type": "page", + "name": "Deployment", + "url": "/symbiotic/deployment" + }, + { + "type": "page", + "name": "Security", + "url": "/symbiotic/security" + }, + { + "type": "page", + "name": "Troubleshooting", + "url": "/symbiotic/troubleshooting" + } + ] + }, { "type": "folder", "name": "Relayer",