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:
# 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 policyConfigure 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.yamlConfiguration Priority
Configuration is loaded in the following order (highest priority first):
- Command-line flags
- Environment variables (with
SYMB_prefix) - 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.shDownload and Run
Pre-built Docker images are available from Docker Hub:
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:
api:
http-gateway: trueOr via command-line flag:
./relay_sidecar --api.http-gateway=trueClient Libraries
| Language | Repository |
|---|---|
| Go | relay |
| TypeScript | relay-client-ts |
| Rust | relay-client-rs |
Snippets
Check out multiple simple snippets how to use the clients mentioned above:
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)
}import { createClient } from "@connectrpc/connect";
import { SymbioticAPIService } from "@symbioticfi/relay-client-ts";
import {
GetLastAllCommittedRequestSchema,
SignMessageRequestSchema,
ListenProofsRequestSchema,
} from "@symbioticfi/relay-client-ts";
import { create } from "@bufbuild/protobuf";
async function main() {
const relayClient = createClient(SymbioticAPIService, transport);
const getLastAllCommittedResponse = await relayClient.getLastAllCommitted(create(GetLastAllCommittedRequestSchema));
const suggestedEpoch = getLastAllCommittedResponse.suggestedEpochInfo.lastCommittedEpoch;
const signMessageRequest = create(SignMessageRequestSchema, {
keyTag: 15, // default key tag - BN254
message: encode(taskId),
requiredEpoch: suggestedEpoch,
});
const signMessageResponse = await relayClient.signMessage(signMessageRequest);
const listenProofsRequest = create(ListenProofsRequestSchema, { startEpoch: suggestedEpoch });
const proofsStream = await relayClient.listenProofs(listenProofsRequest);
let aggregationProof;
for await (const proofResponse of proofsStream) {
if (proofResponse.requestId === signMessageResponse.requestId) {
aggregationProof = proofResponse.aggregationProof?.proof;
break;
}
}
await appContract.completeTask(taskId, signMessageResponse.epoch, aggregationProof);
}use symbiotic_relay_client::generated::api::proto::v1::{
GetLastAllCommittedRequest, SignMessageRequest, ListenProofsRequest,
symbiotic_api_service_client::SymbioticApiServiceClient,
};
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut relay_client = SymbioticApiServiceClient::new(channel);
let epoch_infos_response = relay_client.get_last_all_committed(tonic::Request::new(GetLastAllCommittedRequest {})).await?;
let epoch_infos_data = epoch_infos_response.into_inner();
let mut suggested_epoch = epoch_infos_data.suggested_epoch_info.last_committed_epoch;
let sign_request = tonic::Request::new(SignMessageRequest {
key_tag: 15, // default key tag - BN254
message: encode(task_id).into(),
required_epoch: suggested_epoch,
});
let sign_response = relay_client.sign_message(sign_request).await?;
let sign_data = sign_response.into_inner();
let listen_proofs_request = tonic::Request::new(ListenProofsRequest { start_epoch: suggested_epoch });
let proofs_stream = relay_client.listen_proofs(listen_proofs_request).await?.into_inner();
let mut aggregation_proof = None;
while let Some(proof_response) = proofs_stream.next().await {
match proof_response {
Ok(proof_data) => {
if proof_data.request_id == sign_data.request_id {
if let Some(proof) = proof_data.aggregation_proof {
aggregation_proof = Some(proof.proof);
break;
}
}
}
Err(e) => { break; }
}
}
appContract.complete_task(task_id, sign_data.epoch, aggregation_proof.unwrap()).await?;
}Integration Examples
For a complete end-to-end examples application using the relay, see:
| Repository | Description |
|---|---|
| Symbiotic Super Sum | A simple task-based network |
| Cosmos Relay SDK | An application built using the Cosmos SDK and Symbiotic Relay |
