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, andvalidateall runcargo 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 cachemake 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 deployerThe 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-3Environment Files
Each environment reads secrets from .env.<name> (e.g. .env.local, .env.testnet).
- Local: no
.env.localneeded for keys — anvil signers derive keys automatically, and relayer keystores are generated on first deploy. - Testnet: copy
.env.exampleto.env.testnetand 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:
layerzerochainlink_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:
| 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/<env>.json under operator:
{
"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 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 messageThe 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-operatorGenerated Runtime State
deployments/<env>.jsonfor canonical deployment addressesgenerated/<env>/for rendered monitor, relayer, and sidecar runtime configgenerated/<env>/msg-cache.jsonfor the last message watched by xtask
See CLI & API Reference for command details and Deployment for testnet operation.