Back to Linera Protocol

Linera Bridge — Testnet Deployment Runbook

docker/README.testnet.md

0.15.1810.3 KB
Original Source

Linera Bridge — Testnet Deployment Runbook

This describes how to provision and operate a linera-bridge relayer on a single VM, bridging Base Sepolia (EVM) and Testnet Conway (Linera).

Prerequisites

  • A Linux VM with Docker + docker-compose
  • Foundry (forge, cast) and a recent linera binary on the VM (or available inside docker images)
  • Outbound network access to:
    • Base Sepolia RPC (https://sepolia.base.org or operator's provider)
    • Testnet Conway faucet (https://faucet.testnet-conway.linera.net)
  • A funded Base Sepolia account (private key) for contract deployment and addBlock signing

Initial deployment (one-time)

1. Create directories with correct ownership

bash
sudo useradd -r linera-bridge || true
sudo mkdir -p /var/lib/linera-bridge /etc/linera-bridge
sudo chown -R linera-bridge:linera-bridge /var/lib/linera-bridge
sudo chmod 750 /var/lib/linera-bridge
sudo chmod 755 /etc/linera-bridge

2. Drop the EVM signing key

bash
sudo tee /etc/linera-bridge/.env.secret >/dev/null <<'EOF'
EVM_PRIVATE_KEY=0x...
EOF
sudo chmod 600 /etc/linera-bridge/.env.secret
sudo chown root:root /etc/linera-bridge/.env.secret

3. Provision contracts, Linera apps, wallet, env file

Production provisioning tooling is out of scope of this runbook — examples/bridge-demo/setup.sh is for local development only and is not reused here. The operator must end up with:

  • /var/lib/linera-bridge/wallet.json, keystore.json, client.db: Linera wallet that owns the bridge chain (the relay container reads these via the bind-mount at /data).
  • /etc/linera-bridge/.env: env file with the keys consumed by bridge-entrypoint.sh. Use /data/... paths inside (the host bind-mount exposes /var/lib/linera-bridge at /data in the container).
# /etc/linera-bridge/.env
RPC_URL=https://sepolia.base.org
FAUCET_URL=https://faucet.testnet-conway.linera.net
EVM_BRIDGE_ADDRESS=0x...
LINERA_BRIDGE_APP=...                 # evm-bridge app ID (64 hex)
LINERA_FUNGIBLE_APP=...               # wrapped-fungible app ID (64 hex)
LINERA_BRIDGE_CHAIN_ID=...            # 64 hex
LINERA_BRIDGE_CHAIN_OWNER=0x...       # AccountOwner that owns bridge chain
LINERA_WALLET=/data/wallet.json
LINERA_KEYSTORE=/data/keystore.json
LINERA_STORAGE=rocksdb:/data/client.db
MONITOR_SCAN_INTERVAL=30
MONITOR_START_BLOCK=...               # FungibleBridge deploy block
MAX_RETRIES=10
PORT=3001

Production EVM contracts (LightClient, FungibleBridge, LineraToken-or-real-ERC20), the bridge chain, and the two Linera apps (evm-bridge, wrapped-fungible) are deployed/registered out-of-band by the team's deployment tooling. The output artifacts populate the env file above.

The linera-bridge container will read /etc/linera-bridge/.env and /etc/linera-bridge/.env.secret at startup.

Optional: explorer verification on contract deploys

When the bridge contracts are deployed via the project's forge script tooling (either via examples/bridge-demo/setup.sh for local-dev or the bridge-init container in docker-compose.bridge-test.yml), setting EXPLORER_API_KEY and VERIFIER_URL in the operator shell before invocation appends --verify and publishes the verified contract source to a block explorer atomically with the deploy.

Example for Base Sepolia:

bash
export EXPLORER_API_KEY="..."
export VERIFIER_URL="https://api-sepolia.basescan.org/api"

Both must be set for verification to be appended; either one alone is ignored.

4. Start the relayer

bash
docker compose -f docker/docker-compose.bridge-testnet.yml up -d

Verify it came up healthy:

bash
docker compose -f docker/docker-compose.bridge-testnet.yml ps
# State should be 'running (healthy)' after ~60s.
curl -sI http://localhost:3001/health | head -1
# Expected: HTTP/1.1 200 OK
curl -s http://localhost:3001/metrics | grep '^linera_bridge_' | head -10
# Expected: a handful of linera_bridge_* metrics in Prometheus text format

Routine operations

Restart (after VM reboot or image update)

bash
docker compose -f docker/docker-compose.bridge-testnet.yml up -d

Idempotent. Wallet, chain IDs, contracts persist across restarts.

Image update

bash
docker compose -f docker/docker-compose.bridge-testnet.yml pull
docker compose -f docker/docker-compose.bridge-testnet.yml up -d

Top up Base Sepolia gas

The relayer signs addBlock transactions on Base Sepolia. When ETH runs low, bridging stalls. Send ETH to the relayer's address (the public address of EVM_PRIVATE_KEY) — no relayer restart required.

To find the address from the secret:

bash
sudo cast wallet address \
  --private-key "$(grep ^EVM_PRIVATE_KEY /etc/linera-bridge/.env.secret | cut -d= -f2-)"

Inspect logs

bash
journalctl CONTAINER_TAG=linera-bridge -f
# or
docker compose -f docker/docker-compose.bridge-testnet.yml logs -f

Inspect pending bridge requests

The relay splits live work and history into separate tables: pending_* holds work the relay is currently chasing; finished_* holds completed or permanently-failed entries with a status column.

bash
# live work queue
sudo sqlite3 /var/lib/linera-bridge/bridge_relay.sqlite3 \
  'SELECT * FROM pending_deposits;'
sudo sqlite3 /var/lib/linera-bridge/bridge_relay.sqlite3 \
  'SELECT * FROM pending_burns;'

# permanent failures (status = 'failed' or 'completed')
sudo sqlite3 /var/lib/linera-bridge/bridge_relay.sqlite3 \
  "SELECT * FROM finished_deposits WHERE status = 'failed';"
sudo sqlite3 /var/lib/linera-bridge/bridge_relay.sqlite3 \
  "SELECT * FROM finished_burns WHERE status = 'failed';"

(Check the actual schema with .schema if column lists differ.)

Backup

Critical: loss of /var/lib/linera-bridge means loss of the Linera wallet that owns the bridge chain. The bridge becomes unusable and re-provisioning is the only recovery (which means a fresh bridge, new contract addresses, new chain, etc. — not a real recovery).

Nightly backup:

bash
# SQLite needs the .backup pragma for a consistent snapshot
sudo sqlite3 /var/lib/linera-bridge/bridge_relay.sqlite3 \
  ".backup /tmp/bridge_relay.sqlite3.bak"
sudo rsync -a --delete /var/lib/linera-bridge/ \
  backup-host:/snapshots/linera-bridge-$(date +%F)/
sudo rsync /tmp/bridge_relay.sqlite3.bak \
  backup-host:/snapshots/linera-bridge-$(date +%F)/bridge_relay.sqlite3
sudo rm /tmp/bridge_relay.sqlite3.bak

Also back up /etc/linera-bridge/ (both env files). Without those the deployed artifacts are unrecoverable as well.

Observability

The relayer exposes Prometheus metrics on http://127.0.0.1:3001/metrics. Key metrics for testnet operations:

All metrics are namespaced linera_bridge_*:

MetricTypeUse
linera_bridge_evm_balance_weiGaugeAlert when low (e.g., < 1e16 = 0.01 ETH)
linera_bridge_linera_balance_attoGaugeAlert when low (e.g., < 1e18)
linera_bridge_deposits_pending, linera_bridge_burns_pendingIntGaugeShould drain; if growing, check logs
linera_bridge_deposits_failed, linera_bridge_burns_failedIntGaugeAny > 0 → investigate via SQLite
linera_bridge_deposits_detected, linera_bridge_burns_detectedCounterTotal seen by scanners (cumulative)
linera_bridge_deposits_completed, linera_bridge_burns_completedCounterTotal successfully processed (cumulative)
linera_bridge_last_scanned_evm_blockIntGaugeShould track Base Sepolia head
linera_bridge_last_scanned_linera_heightIntGaugeShould track Linera bridge chain head

Suggested alert rules (apply on the external Prometheus):

yaml
- alert: LineraBridgeGasBalanceLow
  expr: linera_bridge_evm_balance_wei < 1e16
  for: 5m

- alert: LineraBridgeLineraBalanceLow
  expr: linera_bridge_linera_balance_atto < 1e18
  for: 5m

- alert: LineraBridgeDown
  expr: up{job="linera-bridge"} == 0
  for: 2m

# Permanent failure: relay marked items as terminally failed.
- alert: LineraBridgePermanentFailure
  expr: linera_bridge_deposits_failed > 0 or linera_bridge_burns_failed > 0
  for: 15m

# Throughput stall: pending work exists but nothing is being completed.
# Catches a stuck relay even when no item has been marked permanently failed yet.
- alert: LineraBridgePendingStuck
  expr: |
    (linera_bridge_deposits_pending > 0
     and rate(linera_bridge_deposits_completed[15m]) == 0)
    or
    (linera_bridge_burns_pending > 0
     and rate(linera_bridge_burns_completed[15m]) == 0)
  for: 15m

Troubleshooting

SymptomLikely causeAction
Container restart-loopMissing/malformed /etc/linera-bridge/.envCheck docker compose ... logs for clap parse errors
/health returns connection refusedRelayer process exitedrestart: unless-stopped will re-launch on exit; check logs and ps
Container "unhealthy" but listener still openProcess hung (deadlock, RPC wedge)docker compose restart relayer; investigate hang in logs
linera_bridge_evm_balance_wei reads 0RPC unreachable, or wrong keyCheck RPC_URL, verify key with cast wallet address
Pending deposits/burns stuckEVM gas too low, or RPC errorsTop up gas; check logs for retry messages
Slow startup, no metrics for minutesRocksDB cache rebuilding from chain historyWait; check linera_bridge_last_scanned_linera_height is climbing