Join our community of builders on

Telegram!Telegram

Setup

Getting the stack running locally.

Prerequisites

  • Docker and Docker Compose v2+
  • Foundry (forge, cast, anvil)
  • Rust/Cargo (for make dev-operator)
  • jq

Config Structure

config/
├── environments/           # Per-network config (local.json, testnet.json)
├── templates/              # Service config templates (oz-monitor, oz-relayer)
├── oz-monitor/             # Static monitor config
└── oz-relayer/             # Static relayer config and keystores

deployments/
└── <env>.json              # Canonical deployment addresses

generated/
└── <env>/                  # Generated runtime config and message cache

How it works:

  1. make deploy deploys contracts and updates deployments/<env>.json
  2. make deploy and make start generate provider-specific runtime config under generated/<env>/
  3. Docker containers mount from generated/<env>/

To customize configs: Edit templates in config/templates/, then rerun make deploy or make start.

Environment Setup

Bootstrap local .env and operator keys:

make setup

This generates .env from .env.example and creates BLS keys and relayer keystores.

make start also auto-bootstraps .env and keystores if missing, so make setup is only needed for explicit regeneration.

Environment Variables

VariableDescription
PRIVATE_KEYDeployer key (default: Anvil account 0)
LOG_LEVELLogging level (debug, info, warn, error)
WEBHOOK_SECRETHMAC secret for webhook auth (min 32 chars)
OZ_RELAYER_WEBHOOK_SECRETSecret for OZ Relayer webhook auth (min 32 chars)
OZ_RELAYER_API_KEYRequired. Relayer API authentication
SIDECAR_*_SECRET_KEYSBLS keys per operator (generated)

Generate secrets with:

openssl rand -hex 32

The operator will fail to start if WEBHOOK_SECRET, OZ_RELAYER_WEBHOOK_SECRET, or OZ_RELAYER_API_KEY are missing. Secrets must be at least 32 characters.

Provider Selection

Provider is set in config/environments/<env>.json:

{
  "activeProvider": "layerzero"
}

All make commands (start, send, watch, e2e) are provider-aware based on this field.

See provider-specific config in LayerZero or Chainlink CCV.

Running Locally

# Start the full stack (auto-bootstrap + deploy + start services)
make start

# Check service health
make status

# Send a test message and watch it complete
make e2e

Common Commands

make start              Start the full local stack
make deploy             Deploy contracts and generate service config
make stop               Stop all containers (preserve state)
make clean              Full reset (stop + remove volumes + markers)

make restart-operators  Rebuild and restart all 3 operators
make restart-monitor    Restart oz-monitor (config reload)
make restart-relayer    Restart oz-relayer
make restart-relays     Restart symbiotic-relay-1/2/3

make dev-operator       Run operator-1 locally (cargo run)
make rebuild-operators  Docker rebuild + restart all operators
make shell              Interactive shell with addresses loaded

make test               Run unit tests (forge + cargo)
make test-contracts     Run contract tests only

make logs-operators     Follow all 3 operator logs
make logs-operator-N    Follow operator-N logs (N=1,2,3)
make logs-monitor       Follow oz-monitor logs
make logs-relayer       Follow oz-relayer logs
make logs-relays        Follow symbiotic-relay-1/2/3 logs

make status             Show running containers and health
make help               Show all available commands

Local Operator Development

Run operator-1 outside Docker for fast iteration:

# Start the full stack first
make start

# Run operator-1 locally (replaces the Docker container)
make dev-operator

This runs cargo run with the generated config and RUST_LOG=debug. The local operator connects to the same Docker services and receives the same webhooks.

Operator Configuration

Runtime configs are generated at generated/<env>/operator-{n}/config.json.

Key settings:

SettingDefaultDescription
signer.event_poll_interval15sHow often to check for new pending messages
signer.sign_job_interval1sHow often to retry pending Merkle roots
signer.sign_worker_count5Concurrent signing workers
signer.min_batch_size1Minimum messages before creating a tree
oz_relayer.poll_interval5sHow often to check for signed trees to submit
oz_relayer.status_poll_interval30sHow often to poll OZ Relayer for tx status
symbiotic_relay.key_tag15BLS key identifier in the sidecar

Contract Addresses

After deployment, canonical addresses are in deployments/<env>.json.

For manual testing, make shell opens an interactive shell with .env sourced and ENV_CONFIG / DEPLOYMENTS_FILE exported:

make shell
# Then use jq to extract addresses:
jq '.source.layerzero.dvn' $DEPLOYMENTS_FILE

Service Ports

ServicePortPurpose
anvil (source)8545Source chain RPC
anvil (dest)8546Destination chain RPC
operator-1/2/33001-3003Operator debug APIs
symbiotic-relay-1/2/38081-8083BLS sidecars
oz-relayer8080Transaction relayer