Skip to content
LogoLogo

Generated — do not edit. Source of truth: crates/terp-rs/tools/hash-market/docs/sidecar.md. Edit there, then rebuild docs.

Sidecar Setup

The hash-market sidecar runs alongside a Terp validator. It consists of two binaries that can run on the same or separate machines.

Quick start

cd tools/hash-market
 
# Build both binaries
cargo build --release --features server
cargo build --release --features client
 
# Copy example configs
cp config.example.toml config.toml
cp client.example.toml client.toml
 
# Generate a signing key
openssl rand -hex 32
# Put the output in config.toml → signing_key
 
# Start the server (alongside your validator)
./target/release/hash-market-server -c config.toml
 
# Start the client (can run anywhere with ETH RPC access)
./target/release/hash-market-client -c client.toml

Server configuration

The server uses a [[providers]] array — each entry is a named data source for a specific foreign chain. Multiple providers run concurrently, similar to how Skip Connect registers multiple oracle providers.

# config.toml
bind = "0.0.0.0:9090"
chain_id = "terp-mainnet-1"
signing_key = "abcdef..."
 
[[providers]]
name = "ethereum-mainnet"
chain_uid = "ethereum-mainnet"
algo = "keccak256"
mode = "grpc"
address = "0.0.0.0:9091"
 
[[providers]]
name = "arbitrum-one"
chain_uid = "arbitrum-one"
algo = "keccak256"
mode = "grpc"
address = "0.0.0.0:9092"
 
[[providers]]
name = "cosmoshub"
chain_uid = "cosmoshub-4"
algo = "sha256"
mode = "http_poll"
address = "http://cosmos-poller:8080/data"
interval_secs = 6

Each provider has:

FieldRequiredDescription
nameyesHuman-readable label (shown in logs and /providers)
chain_uidyesMust match a RegisteredChain in the hashmerchant module
algonoHash algorithm, default "keccak256"
modeyesTransport: "grpc", "http_poll", or "websocket"
addressyesListen address (grpc) or remote URL (http_poll, websocket)
interval_secsnoPolling interval for http_poll, default 12

Transport modes

Modeaddress fieldUse case
grpcListen address (e.g. 0.0.0.0:9091)Client on same machine or LAN
http_pollURL to poll (e.g. http://client:8080/data)Client behind a firewall
websocketWebSocket URLReal-time streaming (requires tokio-tungstenite)

API endpoints

EndpointMethodPurpose
/healthGETLiveness + provider status + oracle bounds summary
/providersGETList all providers with status, last update time, foreign height (under /ve when nested)
/oracle/boundsGETConnect-style aggregated mid (?market_id=ETH/USD); role=bound_only
/oracle/ticksGETRaw attributed source ticks (debug)
/extend-votePOSTProduce a signed vote extension for a chain
/verify-vote-extensionPOSTVerify a peer's signed extension

Price bounds (Connect-style)

Multi-source pricing aggregates off-chain in the sidecar (Skip Connect pattern): many HTTP tickers → median → one mid per market_id. See oracle-connect-bounds.md.

Ticker URL bodies should look like {"price":"3450.12"} or {"price":3450.12,"timestamp":…}.

/extend-vote request

{
  "height": 12345,
  "chain_uid": "ethereum-mainnet",
  "algo": "keccak256"
}

chain_uid is required when multiple providers are registered. If only one provider has data, it is used automatically. The response includes the chain metadata:

{
  "chain_uid": "ethereum-mainnet",
  "algo": "keccak256",
  "foreign_height": 19500000,
  "extension": "0a09...",
  "signature": "3045...",
  "public_key": "02ab..."
}

/providers response

[
  {
    "name": "ethereum-mainnet",
    "chain_uid": "ethereum-mainnet",
    "algo": "keccak256",
    "running": true,
    "last_update": 1700000000,
    "foreign_height": 19500000
  },
  {
    "name": "arbitrum-one",
    "chain_uid": "arbitrum-one",
    "algo": "keccak256",
    "running": true,
    "last_update": 1700000012,
    "foreign_height": 250000000
  }
]

Client configuration

# client.toml
eth_rpc = "https://eth-mainnet.g.alchemy.com/v2/YOUR_KEY"
sidecar_url = "http://localhost:9090"
runtime_id = "eth-poller-1"
chain_uid = "ethereum-mainnet"
interval_secs = 12
account_address = "0x..."
storage_keys = []

The client polls eth_getBlockByNumber and eth_getProof every interval_secs, transforms the state root through Keccak256 → Pallas Fp reduction, and sends the result to the server's transport endpoint.

Run one client instance per foreign chain. Each connects to a different provider port on the server:

# Client for Ethereum mainnet → server provider on :9091
./hash-market-client -c eth-mainnet.toml
 
# Client for Arbitrum → server provider on :9092
./hash-market-client -c arbitrum.toml

Custody

See the Custody page for the full guide. Summary:

BackendConfigUse when
Local secp256k1signing_key = "hex..."Devnet, testing
TKMSTCP to external KMSMainnet, shared infra
CustomImplement Custody traitCloud KMS, HSM, threshold

Feature flag reference

Build only what you need:

# Types only (for importing in other crates)
cargo build --features msg
 
# Server without client deps
cargo build --features server
 
# Client without server deps
cargo build --features client
 
# Everything
cargo build --features server,client
FeaturePulls inSize impact
msganybufMinimal
custodyk256, ed25519-dalek, tokio, serde~2MB
vecustody + sha2+100KB
ethreqwest, serde_json+4MB (TLS)
pallastiny-keccak+50KB
transporttokio (rt, net, sync)+1MB
servertransport + custody + ve + axum~8MB total
clienteth + pallas + reqwest + tokio~6MB total

Logging

Both binaries use tracing with RUST_LOG env filter:

RUST_LOG=info ./hash-market-server -c config.toml
RUST_LOG=hash_market=debug ./hash-market-client -c client.toml