Skip to content

Relay Off-Chain

The Symbiotic Relay operates as a distributed middleware layer that facilitates:

  • Validator Set Management: Derives and maintains validator sets across different epochs based on on-chain state
  • Signature Aggregation: Collects individual validator signatures and aggregates them using BLS signatures or zero-knowledge proofs
  • Cross-Chain Coordination: Manages validator sets across multiple EVM-compatible blockchains

Architecture

The relay consists of several key components:

  • P2P Layer: Uses libp2p with GossipSub for decentralized communication
  • Signer Nodes: Sign messages using BLS/ECDSA keys
  • Aggregator Nodes: Collect and aggregate signatures with configurable policies
  • Committer Nodes: Submit aggregated proofs to settlement chains
  • API Server: Exposes gRPC API for external clients

For detailed architecture information, see here.

Configure

Create a config.yaml file with the following structure:

config.yaml
# Logging
log:
    level: "debug" # Options: debug, info, warn, error
    mode: "pretty" # Options: json, text, pretty
 
# Storage
storage-dir: ".data" # Directory for persistent data
circuits-dir: "" # Path to ZK circuits (optional, empty disables ZK proofs)
 
# API Server
api:
    listen: ":8080" # API server address
    verbose-logging: false # Enable verbose API logging
 
# Metrics (optional)
metrics:
    listen: ":9090" # Metrics endpoint address
    pprof: false # Enable pprof debug endpoints
 
# Driver Contract
driver:
    chain-id: 31337 # Chain ID where driver contract is deployed
    address: "0x..." # Driver contract address
 
# Secret Keys
secret-keys:
    - namespace: "symb" # Namespace for the key
      key-type: 0 # 0=BLS-BN254, 1=ECDSA
      key-id: 15 # Key identifier
      secret: "0x..." # Private key hex
 
    - namespace: "evm"
      key-type: 1
      key-id: 31337
      secret: "0x..."
 
    - namespace: "p2p"
      key-type: 1
      key-id: 1
      secret: "0x..."
 
# Alternatively, use keystore
# keystore:
#   path: "/path/to/keystore.json"
#   password: "your-password"
 
# Signal Configuration, used for internal messages and event queues
signal:
    worker-count: 10 # Number of signal workers
    buffer-size: 20 # Signal buffer size
 
# Cache Configuration, used for in memorylookups for db queries
cache:
    network-config-size: 10 # Network config cache size
    validator-set-size: 10 # Validator set cache size
 
# Sync Configuration, sync signatures and proofs over p2p to recover missing information
sync:
    enabled: true # Enable P2P sync
    period: 5s # Sync period
    timeout: 1m # Sync timeout
    epochs: 5 # Number of epochs to sync
 
# Key Cache, used for fast public key lookups
key-cache:
    size: 100 # Key cache size
    enabled: true # Enable key caching
 
# P2P Configuration
p2p:
    listen: "/ip4/0.0.0.0/tcp/8880" # P2P listen address
    bootnodes: # List of bootstrap nodes (optional)
        - /dns4/node1/tcp/8880/p2p/...
    dht-mode: "server" # Options: auto, server, client, disabled, default: server (ideally should not change)
    mdns: true # Enable mDNS local discovery (useful for local networks)
 
# EVM Configuration
evm:
    chains: # List of settlement chain RPC endpoints
        - "http://localhost:8545"
        - "http://localhost:8546"
    max-calls: 30 # Max calls in multicall batches
 
# Aggregation Policy
aggregation-policy-max-unsigners: 50 # Max unsigners for low-cost policy

Configure via Command-Line Flags

You can override config file values with command-line flags:

./relay_sidecar \
  --config config.yaml \
  --log.level debug \
  --storage-dir /var/lib/relay \
  --api.listen ":8080" \
  --p2p.listen "/ip4/0.0.0.0/tcp/8880" \
  --driver.chain-id 1 \
  --driver.address "0x..." \
  --secret-keys "symb/0/15/0x...,evm/1/31337/0x..." \
  --evm.chains "http://localhost:8545"

Configure via Environment Variables

Environment variables use the SYMB_ prefix with underscores instead of dashes and dots:

export SYMB_LOG_LEVEL=debug
export SYMB_LOG_MODE=pretty
export SYMB_STORAGE_DIR=/var/lib/relay
export SYMB_API_LISTEN=":8080"
export SYMB_P2P_LISTEN="/ip4/0.0.0.0/tcp/8880"
export SYMB_DRIVER_CHAIN_ID=1
export SYMB_DRIVER_ADDRESS="0x..."
 
./relay_sidecar --config config.yaml

Configuration Priority

Configuration is loaded in the following order (highest priority first):

  1. Command-line flags
  2. Environment variables (with SYMB_ prefix)
  3. Configuration file (specified by --config)

Example

For reference, see how configurations are generated in the E2E setup:

# See the template in e2e/scripts/sidecar-start.sh (lines 11-27)
cat e2e/scripts/sidecar-start.sh

Download and Run

Pre-built Docker images are available from Docker Hub:

Pull the image

The latest image:

bash
docker pull symbioticfi/relay:latest

Or a specific version:

bash
docker pull symbioticfi/relay:<tag>

Run the relay sidecar

bash
docker run -v $(pwd)/config.yaml:/config.yaml \
  symbioticfi/relay:latest \
  --config /config.yaml

API

The relay exposes both gRPC and HTTP/JSON REST APIs for interacting with the network:

gRPC API

HTTP/JSON REST API Gateway

The relay includes an optional HTTP/JSON REST API gateway that translates HTTP requests to gRPC:

  • Swagger File
  • Endpoints: All gRPC methods accessible via RESTful HTTP at /api/v1/*

Enable via configuration:

config.yaml
api:
    http-gateway: true

Or via command-line flag:

bash
./relay_sidecar --api.http-gateway=true

Client Libraries

LanguageRepository
Gorelay
TypeScriptrelay-client-ts
Rustrelay-client-rs

Snippets

Check out multiple simple snippets how to use the clients mentioned above:

myApp.go
import (
    relay "github.com/symbioticfi/relay/api/client/v1"
)
 
func main() {
    relayClient = relay.NewSymbioticClient(conn)
 
    epochInfos, _ := relayClient.GetLastAllCommitted(ctx, &relay.GetLastAllCommittedRequest{})
    suggestedEpoch := epochInfos.SuggestedEpochInfo.GetLastCommittedEpoch()
 
    signMessageResponse, _ := relayClient.SignMessage(ctx, &relay.SignMessageRequest{
        KeyTag:        15, // default key tag - BN254
        Message:       encode(taskId),
        RequiredEpoch: &suggestedEpoch,
    })
 
    listenProofsRequest := &relay.ListenProofsRequest{
        StartEpoch: suggestedEpoch,
    }
    proofsStream, _ := relayClient.ListenProofs(ctx, listenProofsRequest)
    aggregationProof = []byte{}
    while proofResponse, _ := proofsStream.Recv() {
        if proofResponse.GetRequestId() == signMessageResponse.GetRequestId() {
            aggregationProof = proofResponse.GetAggregationProof().GetProof()
            break
        }
    }
 
    appContract.CompleteTask(taskId, signMessageResponse.Epoch, aggregationProof)
}

Integration Examples

For a complete end-to-end examples application using the relay, see:

RepositoryDescription
Symbiotic Super SumA simple task-based network
Cosmos Relay SDKAn application built using the Cosmos SDK and Symbiotic Relay

Next Steps

Helpful Core contracts' Endpoints

Check out Core contracts' endpoints that may help you during the journey!