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, but note the make e2e step is LayerZero-only (it drives the starter ExampleOApp) — CCV runs are confirmed with make watch instead (see Chainlink CCV). Mainnet CCV is not yet supported.
Testnet Workflow
1. Generate keystores
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-3Each 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
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"validateruns read-only checks against config, RPC reachability, deployment state, operator state, and relayer signer safety.deploydeploys managed contracts and regeneratesdeployments/testnet.jsonandgenerated/testnet/.refresh-genesisrepairs stale settlement genesis without redeploying contracts.startdetects 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/<env>.json. .env.<env> must provide:
DEPLOYER_PASSPHRASE=<deployer keystore passphrase>
OPERATOR_1_PASSPHRASE=<operator-1 keystore passphrase>
OPERATOR_2_PASSPHRASE=<operator-2 keystore passphrase>
OPERATOR_3_PASSPHRASE=<operator-3 keystore passphrase>
KEYSTORE_PASSPHRASE=<relayer keystore passphrase> # used by OZ Relayer at runtimeOperator 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.
CCV environments (ENV=testnet-ccv) additionally need the reserved factory-deployer key:
cargo xtask --env testnet-ccv generate-signer --name ccv-factory-deployerCCV_FACTORY_DEPLOYER_PASSPHRASE=<ccv-factory-deployer keystore passphrase>
CCV_STORAGE_LOCATION_URIS=<comma-separated attestation API base URLs>The factory-deployer key must be freshly generated and stay at nonce 0 on every target chain until the deploy — it anchors the resolver's CREATE2 address parity. Fund it just enough for one factory deploy per chain.
Operational Notes
- Local uses
make chains && make deploy && make start; testnet usesmake deploy ENV=testnetplusmake start ENV=testnet. - Testnet uses
docker-compose.ymlonly, not the local overlay. - Canonical deployment addresses always live in
deployments/<env>.json.
CCV Deterministic Deployment (CREATE2)
The CCV resolver must have the same address on every chain — the CCIP indexer registers that one address, and identical addresses give automatic discovery when you add a chain. The deployment flow that guarantees this:
- Deploy Chainlink's
CREATE2Factoryonce per chain from a reserved key at nonce 0. CREATE addresses depend only on (deployer, nonce), so a dedicated key that has never transacted produces the same factory address everywhere. Locally this is theccv-factory-deployersigner (Anvil account 9); on testnet/production, generate a dedicated key, use it for nothing else, and fund it just enough for one deploy per chain. The factory constructor takes an allowlist of addresses permitted to claim CREATE2 addresses through it. - Deploy the resolver through the factory:
deployResolverloads Chainlink's published resolver creation bytecode from the npm package, callsfactory.createAndTransferOwnership(code, salt, owner), asserts the result matchesfactory.computeAddress, and completes ownership withacceptOwnership()from the owner. A deploy without the accept step is not finished — the resolver would still have a pending owner. - Deploy the
SymbioticVerifier(plain CREATE — its address doesn't need parity), register it on the resolver, and configure lanes.
Non-local CCV deployments also require the real RMN contract per chain: set chainlinkCcip.rmn in the environment config (RMNRemote on CCIP staging — discoverable from OnRamp.getStaticConfig()). The verifier's constructor requires it, and fork tests (make test-fork) read it from SOURCE_CCIP_RMN_ADDRESS / DEST_CCIP_RMN_ADDRESS in .env.testnet.
Address-affecting inputs
The resolver address is a pure function of these — pin them and never change them casually:
| Input | Where pinned | Notes |
|---|---|---|
| Factory deployer key + nonce 0 | ccv-factory-deployer signer per environment | Determines the factory address |
| Resolver salt | RESOLVER_SALT in contracts/script/chainlink/DeployCCV.s.sol (keccak256("symbiotic.ccv.versioned-verifier-resolver.v1")) | |
| Resolver creation bytecode | @chainlink/contracts-ccip@2.0.0 published artifact (bytecode/v2_0_0/versioned_verifier_resolver.bin) | Using the published artifact makes the address independent of local compiler settings |
The resolver owner is not address-affecting (the stock resolver has no constructor arguments), so ownership can differ per chain without breaking parity.
Validate determinism any time with make validate-resolver-address — it deploys the full flow on two throwaway Anvil chains and asserts identical addresses and correct ownership.
Attestation storage locations
CCV_STORAGE_LOCATION_URIS (comma-separated) is a required deploy input for the verifier: it becomes the on-chain getStorageLocations() answer that executors use to fetch attestations. There is no placeholder fallback — an empty value fails the deploy. Point it at the operator attestation API (each operator serves GET /verifications; put a load balancer or an HA node in front for production — see Architecture). Rotate later with the owner-gated updateStorageLocations.
CCIP indexer onboarding
Each new protocol deployment is a per-deployment onboarding with Chainlink: the CCIP indexer must learn (a) your resolver address and (b) your attestation API endpoint before messages get automated execution. Messages verify without this step — but nobody executes them. Coordinate with your Chainlink contact (or Symbiotic/OpenZeppelin on your behalf) when standing up a new deployment, and again only when adding chains if you skipped deterministic addresses.
Onboarding is per protocol deployment, not per validator set. Operator registration and deregistration rotate the active validator set inside the same Settlement and verifier contracts at epoch boundaries — the contracts, the resolver address, and the attestation API endpoint all stay the same as the set evolves. The indexer never needs re-onboarding for validator churn; keeping the advertised endpoint reachable across it is your responsibility (see Production Topology).
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: runmake refresh-genesis ENV=<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.