Setup

Getting the stack running locally.

Prerequisites

  • Docker and Docker Compose v2+
  • Foundry (forge, cast, anvil)
  • pnpm (for make install, which installs the contract dependencies)
  • Rust/Cargo (required — make chains, deploy, start, status, e2e, and validate all run cargo xtask)
  • jq

Config Layout

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/
└── <env>.json              # Canonical deployment addresses

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

make deploy and make start read from config/ and write runtime state into deployments/<env>.json and generated/<env>/.

Signers

Signer keys are configured in config/environments/<env>.json under signers. Three signer types are supported:

Anvil (local only)

Derives keys from Anvil's well-known mnemonic by index. Zero setup needed.

{
  "signers": {
    "deployer": { "type": "anvil", "index": 0 },
    "operator-1": { "type": "anvil", "index": 1 }
  }
}

Local (encrypted keystore)

Reads from an encrypted keystore file. Generate one with:

cargo xtask --env <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:

{
  "signers": {
    "deployer": {
      "type": "local",
      "path": "config/keys/<env>/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.

{
  "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/<env>/). 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:

cargo xtask --env <env> generate-signer --name signer-1 --name signer-2 --name signer-3

Environment Files

Each environment reads secrets from .env.<name> (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/<env>.json:

{
  "activeProvider": "layerzero"
}

Supported values:

  • layerzero
  • chainlink_ccv

Provider-specific configuration lives in LayerZero and Chainlink CCV.

Environment JSON

config/environments/<env>.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:

FieldPurpose
nameHuman label for logs and rendered runtime files
chainIdEVM chain ID
eidLayerZero endpoint ID
confirmationsConfirmation depth used in rendered monitor and relayer config
blockTimeMsBlock time hint used when rendering monitor and relayer networks
rpcUrlsNon-local RPC sources; supports plain URLs or { "type": "env", "value": "SOURCE_RPC_URL" } style env references
predeploysProvider-specific or shared predeployed contract addresses

Relay Timing

The relay block controls settlement and genesis timing used by deploy and refresh flows:

FieldPurpose
epochDurationSecondsEpoch length for settlement timing
slashingWindowSecondsSlashing window length
epochStartDelaySecondsDelay before a new epoch becomes active

Monitor and Relayer Rendering

For non-local environments, these blocks are required to render runtime artifacts:

BlockFieldPurpose
ozMonitorcronSchedulePoll cadence for the source-chain monitor
ozMonitormaxPastBlocksBackfill window for source-chain log reads
ozRelayerdefaultSpeedDefault submission speed tier for OZ Relayer
ozRelayerminBalanceWeiRuntime threshold used by validate to flag low relayer-signer balances
fundingoperatorAmountWeiWei sent to each operator EOA during make deploy
fundingsignerAmountWeiWei sent to each explicit relayer signer during make deploy
fundingminBalanceThresholdWeiSkip 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/<env>.json under operator:

{
  "operator": {
    "logLevel": "info",
    "eventPollInterval": "15s",
    "signJobInterval": "1s",
    "signWorkerCount": 5,
    "minBatchSize": 1,
    "acceptanceHooks": [],
    "enableDebugEndpoints": false
  }
}

If omitted, the defaults are:

FieldDefaultPurpose
logLevelinfoOperator log verbosity
eventPollInterval15sHow often to scan for pending messages to batch
signJobInterval1sHow often to retry pending signing jobs
signWorkerCount5Concurrent signing workers
minBatchSize1Minimum messages required before building a Merkle tree
acceptanceHooksnative provider hookOrdered hooks that can accept, reject, or defer messages before batching
enableDebugEndpointsfalseExpose /debug/v1/* endpoints on operator HTTP servers

See Acceptance Hooks for native and webhook configuration.

The operator reads config/environments/<env>.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

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:

make dev-operator

Generated Runtime State

  • deployments/<env>.json for canonical deployment addresses
  • generated/<env>/ for rendered monitor, relayer, and sidecar runtime config
  • generated/<env>/msg-cache.json for the last message watched by xtask

See CLI & API Reference for command details and Deployment for testnet operation.