# CCIP Relay Service Off-chain relay for forwarding Chain 138 `MessageSent` events to destination relay routers/bridges. ## Current Topology Source (Chain 138) — match `.env.bsc` / operator deploy: - Router: `0x42DAb7b888Dd382bD5Adcf9E038dBF1fD03b4817` - WETH9 bridge: `0xcacfd227A040002e49e2e01626363071324f820a` Destinations: - BSC relay router: `0x4d9Bc6c74ba65E37c4139F0aEC9fc5Ddff28Dcc4` - BSC relay bridge: `0x886C6A4ABC064dbf74E7caEc460b7eeC31F1b78C` - AVAX relay router: `0x2a0023Ad5ce1Ac6072B454575996DfFb1BB11b16` - AVAX relay bridge: `0x3f8C409C6072a2B6a4Ff17071927bA70F80c725F` Direct first-hop support from Chain 138 is intentionally narrow today: - Mainnet: supported with the default `.env` profile - BSC: supported with `.env.bsc` - Avalanche: supported with `.env.avax` - Gnosis / Cronos / Celo / Polygon / Arbitrum / Optimism / Base: treat as `via Mainnet hub` unless a dedicated relay router + relay profile are added and proven live Important: on 2026-04-04, a direct `138 -> Arbitrum` WETH send produced a real source `MessageSent` event but no destination delivery because the live relay worker was running a Mainnet-only destination profile. There is currently no tracked `.env.arbitrum` profile in this folder. ## Env Profiles Use the prebuilt env files in this folder: - `.env.bsc` (template: `.env.bsc.example`) - `.env.avax` - `.env` (default/fallback) - `.env.local` only for local overrides that should beat the tracked profiles Each profile sets destination RPC, selector, relay router/bridge, and destination WETH token. ### `START_BLOCK` after catch-up When historical `MessageSent` logs are fully relayed, set **`START_BLOCK=latest`** in `.env.bsc` (or your profile) so a cold start only scans from **~current head − 1** instead of re-queuing the whole backfill range. To replay from an old height again, set an explicit decimal block (e.g. `3012930`) and restart. **BSC RPC:** Prefer a node that accepts short `eth_getLogs` windows (e.g. `https://bsc.publicnode.com`). Some Binance seeds return `-32005` for log queries the relay uses for destination checks. ### Fund BSC relay bridge (WETH) From repo root (loads `smom-dbis-138/.env` and relay `.env.bsc` for addresses): ```bash ./scripts/bridge/fund-bsc-relay-bridge.sh --dry-run ./scripts/bridge/fund-bsc-relay-bridge.sh # full deployer WETH → bridge # ./scripts/bridge/fund-bsc-relay-bridge.sh 1000000000000000 # 0.001 WETH wei ``` Wrap BNB to WETH on the deployer first (`cast send "deposit()" --value ...` on BSC) if needed. ### Fund Mainnet relay bridge (WETH) From repo root: ```bash ./scripts/bridge/fund-mainnet-relay-bridge.sh --dry-run ./scripts/bridge/fund-mainnet-relay-bridge.sh # full deployer WETH → bridge # ./scripts/bridge/fund-mainnet-relay-bridge.sh 1000000000000000 # 0.001 WETH wei ``` ## Relay shedding (save destination gas) When **no** 138→Mainnet (or configured destination) relay deliveries are needed, pause **destination-chain** transactions so the relayer does not spend native gas on `relayMessage` / direct `ccipReceive`: | Variable | Meaning | |----------|---------| | `RELAY_SHEDDING=1` | **On** — shedding active (`true` / `yes` / `on` also work). | | `RELAY_DELIVERY_ENABLED=0` | Same as shedding on (`false` / `no` / `off`). | | `RELAY_SHEDDING_SOURCE_POLL_INTERVAL_MS` | Source router log poll interval while shedding (default **60000** ms, min 5000). Reduces Chain 138 RPC usage. | | `RELAY_SHEDDING_QUEUE_POLL_MS` | Idle interval for the queue loop while shedding (default **5000** ms, min 1000). | **Behavior:** Source `MessageSent` logs are still ingested and messages queue locally. Pending queue state is now persisted to `services/relay/data/queue-state.json` by default (override with `RELAY_QUEUE_STATE_PATH`), so a restart no longer drops queued work. For production, still plan shedding around low bridge traffic so the persisted backlog stays small and intentional. ## Skip specific message IDs Use `RELAY_SKIP_MESSAGE_IDS` as a comma-separated list of source `MessageSent.messageId` values that the relay should intentionally ignore. This is the safest operational way to park an already-confirmed source message when: - destination relay inventory is below the requested release amount - you do not want the relay to keep retrying it after service restarts - there is no on-chain cancel / refund path on the source bridge Example: ```bash RELAY_SKIP_MESSAGE_IDS=0xf718c9895c0a5442349996383184d017d2fa041af7aaeb9f0c0675d3ceed756b ``` The relay checks this list during live event ingestion, historical replay, and queue processing. For the current Mainnet WETH backlog policy, see: - [`docs/03-deployment/MAINNET_WETH_RELAY_BACKLOG_POLICY.md`](../../../docs/03-deployment/MAINNET_WETH_RELAY_BACKLOG_POLICY.md) ### On-chain pause (`CCIPRelayRouter`) The destination **CCIPRelayRouter** inherits OpenZeppelin **`Pausable`**: admins with `DEFAULT_ADMIN_ROLE` may call **`pause()`** / **`unpause()`**. While paused, **`relayMessage` reverts** (no delivery through the router). **Relay service:** Before sending `relayMessage`, the worker calls **`paused()`** on the destination router (router mode only). If paused, it **re-queues** the message and waits 15s instead of broadcasting a reverting tx. Older routers without `paused()` skip this check (call errors are logged at debug). **Important:** If you `pause()` the router but leave the relay **process** running **without** `RELAY_SHEDDING=1`, failed txs are much less likely thanks to the check above, but off-chain activity (source polling, queue growth) still runs. Prefer **`RELAY_SHEDDING=1`** (or stop the service) whenever the router is paused for an extended period. **Direct-delivery** mode (`DEST_DELIVERY_MODE=direct`) calls the bridge’s `ccipReceive` directly and **does not** go through the router—pause the router alone does not stop that path; use shedding or revoke `ROUTER_ROLE` on the bridge as appropriate. ## Start Relay ```bash cd /home/intlc/projects/proxmox/smom-dbis-138/services/relay npm install # BSC relay profile ./start-relay.sh bsc # AVAX relay profile ./start-relay.sh avax # Default profile ./start-relay.sh ``` `start-relay.sh` loads env in this order: 1. `.env.` (if profile argument provided) 2. `.env.local` 3. `.env` If parent project `.env` defines `PRIVATE_KEY`, `${PRIVATE_KEY}` references in relay env files are expanded. ## Relay Health Endpoint The relay now exposes a lightweight JSON status endpoint for explorer / mission-control monitoring. - Default listen address: `0.0.0.0` - Default port: `9860` - Endpoints: `GET /healthz`, `GET /health`, `GET /status` Optional env overrides: ```bash RELAY_HEALTH_ENABLED=1 RELAY_HEALTH_HOST=0.0.0.0 RELAY_HEALTH_PORT=9860 ``` Example from another LAN host: ```bash curl http://192.168.11.11:9860/healthz | jq . ``` Example explorer backend wiring: ```bash CCIP_RELAY_HEALTH_URL=http://192.168.11.11:9860/healthz CCIP_RELAY_HEALTH_URLS=mainnet=http://192.168.11.11:9860/healthz,bsc=http://192.168.11.11:9861/healthz,avax=http://192.168.11.11:9862/healthz ``` Recommended systemd ports when running multiple relay workers on the same host: - Mainnet: `9860` - BSC: `9861` - Avalanche: `9862` ## Critical Requirements - Relayer key must hold native gas on destination chain. - Destination relay bridge must hold enough WETH for payouts. - Explicit profile token overrides like `DEST_WETH9_ADDRESS` win over the generic multichain token map. This keeps relay-backed destinations pointed at their bridge-managed wrapped token instead of a public native wrapped asset. - Source bridge destination mapping must point to the correct destination relay bridge. - Source router `feeToken()` must be a deployed ERC20 with sufficient deployer balance. ## Fast Status Checks Check source destination mappings: ```bash cast call 0xcacfd227A040002e49e2e01626363071324f820a "destinations(uint64)" 11344663589394136015 --rpc-url https://rpc.public-0138.defi-oracle.io cast call 0xcacfd227A040002e49e2e01626363071324f820a "destinations(uint64)" 6433500567565415381 --rpc-url https://rpc.public-0138.defi-oracle.io ``` Check message settlement: ```bash cast call 0x886C6A4ABC064dbf74E7caEc460b7eeC31F1b78C "processedTransfers(bytes32)(bool)" --rpc-url https://bsc.publicnode.com cast call 0x3f8C409C6072a2B6a4Ff17071927bA70F80c725F "processedTransfers(bytes32)(bool)" --rpc-url https://avalanche-c-chain.publicnode.com ``` Check destination bridge liquidity: ```bash cast call "balanceOf(address)(uint256)" --rpc-url ```