Integration

Relayer: FairyPort

FairyPort is the production-grade Go relayer that bridges Stellar and Fairyring. It monitors events on both chains, optionally pre-verifies ZK proofs, and submits cross-chain transactions.


Internal architecture

ComponentResponsibilityKey files
Event MonitorsReal-time WebSocket subscriptions with auto-reconnect; HTTP catchup on startupinternal/stellar/ws/ws.go, internal/fairy/ws/ws.go
Event RegistriesParse and type-validate blockchain events from both chainsinternal/stellar/events/wasm.go, internal/fairy/events/wasm.go
Event QueuesBounded channels (capacity 1024 each) decoupling receipt from processinginternal/app/app.go
Stellar DispatcherConvert Stellar events into Fairyring ExecuteMsg; normalize and deduplicateinternal/app/dispatcher_stellar.go
CW DispatcherConvert Fairyring wasm events into Soroban callback invocationsinternal/app/dispatcher_cw.go
Off-chain ZKP VerifierRust FFI library called via CGO; verifies transfer and withdraw proofs before relayinternal/zkpffi/ffi.go, native/zkp_verify_ffi/
TransactorsSign with relayer private key and broadcast to each chaininternal/app/tx_stellar.go, internal/app/tx_cosmos.go
Pending PollerPeriodically query Soroban for pending actions; re-trigger stale actionsinternal/app/pending_poller.go
Checkpoint StoreAtomic LevelDB writes for last processed ledger/block height and pending stateinternal/app/checkpoint.go, internal/store/store.go
Retry ManagerTrack retry counts with configurable backoff; dead-letter on exhaustioninternal/app/retry_cap.go, internal/app/failed_tx_log.go

Building from source

FairyPort is a Go binary that links against a Rust FFI library for ZKP verification. Both must be compiled.

bash
# Prerequisites: Go 1.22.10+, Rust 1.70+, Cargo, C build tools

# Clone and build (Rust FFI library + Go binary)
git clone https://github.com/Fairblock/stellar-repo
cd stellar-repo/fairyport
make install

# Verify installation
fairyport --version
ls -la native/lib/   # should contain libzkp_verify_ffi.{so,dylib}

The make install target compiles the Rust library (libzkp_verify_ffi) into native/lib/ and then links it into the Go binary via CGO. The pre-compiled library is included for macOS and Linux.

Configuration

Copy configs/configs.example.yaml as your starting point.

fairyport.config.yaml
config:
  fairyring:
    chain_id: fairyring_devnet
    bech32: fairy
    contract_address: fairy1...          # CosmWasm contract address
    relayer_account_priv_key_hex: 0x...  # 64 hex chars; keep secret
    rpc_endpoint:
      ip: 127.0.0.1
      port: 26657
      protocol: http
    grpc_endpoint:
      ip: 127.0.0.1
      port: 9090
      protocol: ""
    websocket_endpoint:
      ip: 127.0.0.1
      port: 26657
      protocol: ws
    fee_denom: ufair
    fee_amount: 0
    gas_limit: 1500000
    events:
      contracts:
        - fairy1...
      pep_events_only: false
    start_height: -1  # -1 = start from latest

  stellar:
    rpc_endpoint: https://soroban-rpc.testnet.stellar.org
    contract_address: CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA
    network_passphrase: "Test SDF Network ; September 2015"
    relayer_private_key: SXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
    start_ledger: -1  # -1 = start from latest

metrics:
  enabled: true
  listen_addr: :9102

log:
  level: info  # debug | info | warn | error

Environment variable overrides

Every config field can be overridden with an environment variable using the FAIRYPORT_ prefix. Prefer environment variables for secrets in production.

bash
# Fairyring config overrides
export FAIRYPORT_CONFIG_FAIRYRING_CHAIN_ID=fairyring_devnet
export FAIRYPORT_CONFIG_FAIRYRING_CONTRACT_ADDRESS=fairy1...
export FAIRYPORT_CONFIG_FAIRYRING_RELAYER_ACCOUNT_PRIV_KEY_HEX=0x...
export FAIRYPORT_CONFIG_FAIRYRING_RPC_ENDPOINT_IP=127.0.0.1
export FAIRYPORT_CONFIG_FAIRYRING_RPC_ENDPOINT_PORT=26657
export FAIRYPORT_CONFIG_FAIRYRING_GRPC_ENDPOINT_IP=127.0.0.1
export FAIRYPORT_CONFIG_FAIRYRING_GRPC_ENDPOINT_PORT=9090
export FAIRYPORT_CONFIG_FAIRYRING_WEBSOCKET_ENDPOINT_IP=127.0.0.1
export FAIRYPORT_CONFIG_FAIRYRING_FEE_DENOM=ufair
export FAIRYPORT_CONFIG_FAIRYRING_GAS_LIMIT=1500000

# Stellar config overrides
export FAIRYPORT_CONFIG_STELLAR_RPC_ENDPOINT=https://soroban-rpc.testnet.stellar.org
export FAIRYPORT_CONFIG_STELLAR_CONTRACT_ADDRESS=CAAAA...
export FAIRYPORT_CONFIG_STELLAR_NETWORK_PASSPHRASE="Test SDF Network ; September 2015"
export FAIRYPORT_CONFIG_STELLAR_RELAYER_PRIVATE_KEY=SXXX...

# Observability
export FAIRYPORT_CONFIG_METRICS_ENABLED=true
export FAIRYPORT_CONFIG_METRICS_LISTEN_ADDR=:9102
export FAIRYPORT_CONFIG_LOG_LEVEL=info

Running the relayer

bash
# Run with default config path (fairyport.config.yaml in CWD)
fairyport run

# Run with explicit config path
fairyport run --config /etc/fairyport/config.yaml

# Sync mode: replay events between two heights without continuous operation
fairyport sync --from-height 1000 --to-height 2000

Startup & recovery sequence

01

Load checkpoint store

Opens or creates LevelDB database. Reads last_stellar_ledger and last_cw_height from persistent storage.

02

Query current chain heights

Fetches the latest ledger number from the Stellar RPC and the latest block height from Fairyring.

03

Catchup replay

Replays all events from the checkpoint to the current height in background. Uses HTTP queries (not WebSocket) to fill the gap. Checkpoints update atomically after each batch.

04

WebSocket subscription

Once caught up, subscribes to live events via WebSocket on both chains. Auto-reconnects on disconnection.

05

Continuous processing

Dispatches events through the queue → verifier (optional) → broadcaster → confirmation poller pipeline.

Checkpoint store layout

Persisted to LevelDB at fairyport-checkpoints-{chainid}-{stellaraddr}.leveldb.

text
# LevelDB checkpoint keys

checkpoint/stellar/last_ledger              → int64 string
checkpoint/cw/last_height                   → int64 string

# Pending action deduplication keys (prevent duplicate submissions)
pending/forwarded/{kind}/{owner}/{txid}     → timestamp string
pending/retry_count/{kind}/{owner}/{txid}   → integer string
pending/dead/{kind}/{owner}/{txid}          → int64 string (dead-letter marker)

# kind values: create | deposit | transfer | apply | withdraw

Health monitoring

Prometheus metrics are exposed at the configured listen_addr.

bash
# Prometheus metrics endpoint
curl -s http://localhost:9102/metrics | grep fairyport_

# Key metrics to watch:
# fairyport_events_received_total{chain="stellar"}
# fairyport_events_received_total{chain="fairyring"}
# fairyport_transactions_submitted_total{chain="stellar",status="ok"}
# fairyport_transactions_submitted_total{chain="fairyring",status="ok"}
# fairyport_retry_count_total
# fairyport_dead_letter_total

# Check checkpoint state
ls -lh fairyport-checkpoints-*.leveldb/

# Inspect dead-lettered transactions
jq length failed_txs.json 2>/dev/null || echo "0 failed"

# Process status
ps aux | grep fairyport | grep -v grep

Error handling & recovery

Network timeout / transient error

Automatic retry with exponential backoff. Retry count tracked per (kind, owner, txid) in LevelDB.

Retry exhaustion

Transaction marked dead-lettered in LevelDB and appended to failed_txs.json for manual operator review.

Invalid ZK proof (off-chain mode)

Error callback sent directly to the Stellar contract; action discarded. No state mutation on Fairyring.

Relayer restart

Checkpoint loaded from LevelDB; catchup replay fills the gap. Duplicate tx_ids are handled idempotently by Fairyring.

Corrupt checkpoint database

Back up and delete the .leveldb directory. FairyPort will replay from config start_height or from genesis.

Pending action timeout

The Pending Poller queries Soroban for accounts with pending_action=true and re-triggers relay if no confirmation has arrived after a configured timeout.

Security guidelines

Inject private keys via environment variables, not config files, in production

Restrict config file permissions: chmod 600 fairyport.config.yaml

Private keys are never logged; sensitive fields are sanitized from error messages

Checkpoints only update after confirmed onchain finality no state regression risk

The relayer does not hold user funds; it only signs cross-chain coordination messages

Run multiple independent FairyPort instances for liveness redundancy

Testing

Three integration test scripts are provided in fairyport/scripts/:

test_end2end.py

Full Stellar → Fairyring → Stellar flow with off-chain verification. Covers create, deposit, transfer, apply, withdraw. Also exercises relay catchup by submitting while relayer is offline.

test_end2end_onchainverify.py

Same end-to-end flow but with use_offchain_verify=false (onchain ZKP verification via Fairyring).

test_sync.py

Creates an account, directly deposits into Fairyring, runs fairyport sync, and verifies the Stellar mirror state updates.