Union is a zero-knowledge IBC infrastructure that enables trust-minimized cross-chain message passing, asset transfers, and DeFi across heterogeneous blockchains including Cosmos-SDK chains, EVM networks, Sui, Starknet, and Movement/Aptos. This guide covers architecture, deployment, configuration, security, operations, and developer internals for operators, architects, and contributors working with the Union protocol stack.
1. Architecture Overview
What the system does
Union implements the Inter-Blockchain Communication (IBC) protocol with a ZK-proof-based consensus verification layer. Instead of relying on multisig committees or trusted third parties, Union uses BLS aggregate signatures with Groth16 zero-knowledge proofs to verify cross-chain state transitions. The core chain (uniond) runs CometBFT consensus with BN254 BLS curve support (CometBLS), enabling efficient on-chain proof verification.
The relayer system (Voyager) orchestrates IBC packet lifecycle across chains — indexing events, updating light clients, constructing proofs, batching transactions, and relaying packets. The ZK proving service (Galois) generates Groth16 proofs that attest to validator set transitions, allowing any chain to verify Union consensus without replaying the full validator set logic.
Smart contract deployments span CosmWasm (for Cosmos chains), Solidity (for EVM chains), Cairo (for Starknet), and Move (for Aptos/Sui), each implementing the IBC core protocol and the ZKGM (UCS03) token transfer specification natively in their respective VMs.
Architecture diagram
┌─────────────────────────────────────────────────────────────┐
│ Union Chain │
│ ┌─────────┐ ┌──────────┐ ┌───────────┐ ┌────────────┐ │
│ │ uniond │ │ CometBLS │ │ CosmWasm │ │ IBC Core │ │
│ │ (Go) │ │ (BN254) │ │ Contracts │ │ (ibc-go v8)│ │
│ └────┬────┘ └────┬─────┘ └─────┬─────┘ └─────┬──────┘ │
│ └────────────┼──────────────┼───────────────┘ │
└─────────────────────┼──────────────┼─────────────────────────┘
│ │
┌────────────┘ │
▼ ▼
┌─────────────────┐ ┌──────────────────┐
│ Galois (ZK) │ │ Voyager │
│ ┌────────────┐ │ │ ┌────────────┐ │
│ │ galoisd │ │ │ │ Relayer │ │
│ │ (Go/Gnark) │ │ │ │ (Rust) │ │
│ │ gRPC API │ │ │ │ PostgreSQL │ │
│ │ BLS12-381 │ │ │ │ Plugin Sys │ │
│ │ BN254 │ │ │ └────────────┘ │
│ └────────────┘ │ │ │
└─────────────────┘ │ Modules: │
│ ├── client │
┌─────────────────┐ │ ├── finality │
│ unionvisor │ │ ├── proof │
│ (Rust) │ │ ├── state │
│ Process Mgr │ │ └── bootstrap │
└─────────────────┘ │ │
│ Plugins: │
│ ├── event-source│
│ ├── client-upd │
│ ├── transaction │
│ ├── packet-* │
│ └── zkgm-filter │
└──────────────────┘
│
┌───────────┬───────────┼───────────┬───────────┐
▼ ▼ ▼ ▼ ▼
┌──────────┐ ┌─────────┐ ┌────────┐ ┌─────────┐ ┌──────┐
│ Ethereum │ │Arbitrum │ │ Base │ │ BSC │ │ Sui │
│ Solidity │ │ Solidity│ │Solidity│ │Solidity │ │ Move │
│ IBC Core │ │ IBC Core│ │IBC Core│ │IBC Core │ │ IBC │
└──────────┘ └─────────┘ └────────┘ └─────────┘ └──────┘
Component inventory
| Component | Path | Language | Role |
|---|---|---|---|
| uniond | uniond/ |
Go | Cosmos-SDK chain node with CometBLS consensus, custom BLS staking, IBC-go v8 |
| Voyager | voyager/ |
Rust | Modular IBC relayer with PostgreSQL-backed queue and plugin architecture |
| Galois | galoisd/ |
Go | ZK proving service (Gnark circuits for BLS12-381 + BN254 Groth16 proofs) |
| unionvisor | unionvisor/ |
Rust | Process supervisor for uniond binary upgrades (similar to cosmovisor) |
| CosmWasm contracts | cosmwasm/ |
Rust | IBC core, light clients, ZKGM (UCS03), token minters, access management |
| EVM contracts | evm/contracts/ |
Solidity | IBC core handler, client registry, channel management, ZKGM app |
| Cairo contracts | cairo/ |
Cairo | CometBLS light client, ZK verifier, ZKGM, and IBC for Starknet |
| Move modules | aptos/ |
Move | IBC and ZKGM implementations for Aptos |
| Sui modules | sui/ |
Move (Sui) | IBC, ZKGM, escrow vaults, ping-pong for Sui |
| TypeScript SDK | ts-sdk/ |
TypeScript | Cross-chain transfer intents using Effect-TS |
| Web UI (app2) | app2/ |
SvelteKit | Bridge interface at app.union.build |
| MPC ceremony | ceremony/, mpc/ |
Rust/TS | Multi-party computation for trusted setup |
| Lib crates | lib/ |
Rust | 80+ shared libraries (types, verifiers, encodings, primitives) |
| Tools | tools/ |
Rust | Utilities: u (swiss-army CLI), tidy, move-bindgen, devnet-utils |
| Drip | drip/ |
Rust | Faucet service for testnet token distribution |
Technology stack
| Layer | Technology |
|---|---|
| Consensus | CometBFT with BN254 BLS aggregate signatures (CometBLS) |
| Chain framework | Cosmos SDK v0.50+ with depinject |
| IBC | ibc-go v8, custom IBC-Union spec |
| ZK proving | Gnark (Go), Groth16 over BN254, BLS12-381 pairing |
| Relayer runtime | Rust async (tokio), PostgreSQL queue (pg-queue), JSON-RPC plugins |
| Smart contracts | CosmWasm, Solidity (Foundry), Cairo, Move |
| Build system | Nix flakes (reproducible), Cargo workspace (200+ crates), Go modules |
| Frontend | SvelteKit (app2), Astro (docs site) |
| Monitoring | OpenTelemetry (Voyager metrics), tracing crate |
Data flow: cross-chain transfer
1. User submits transfer intent (Web UI / TS SDK / CLI)
│
2. Source chain: ZKGM contract escrows tokens, emits SendPacket event
│
3. Voyager event-source plugin indexes the event
│
4. Voyager client-update plugin fetches latest header from source chain
│
5. Galois generates Groth16 ZK proof of source chain consensus
│
6. Voyager submits UpdateClient tx on destination chain with ZK proof
│
7. Voyager constructs membership proof for the packet commitment
│
8. Voyager submits PacketRecv tx on destination chain
│
9. Destination chain ZKGM contract verifies proof, mints/releases tokens
│
10. Voyager relays acknowledgement back to source chain
Note
The ZK proof generation step (5) is what distinguishes Union from traditional IBC relaying. Rather than submitting full validator set and signature data, only a compact Groth16 proof is verified on-chain, reducing gas costs significantly on EVM chains.
2. System Requirements
Hardware requirements
| Component | CPU | Memory | Storage | Network |
|---|---|---|---|---|
| uniond (validator) | 4+ cores | 16 GB+ | 500 GB+ SSD (NVMe recommended) | 100 Mbps+, low latency |
| uniond (full node) | 2+ cores | 8 GB+ | 500 GB+ SSD | 50 Mbps+ |
| Voyager | 2+ cores | 4 GB+ | 50 GB (PostgreSQL) | Stable connections to all relayed chains |
| Galois | 8+ cores | 32 GB+ | 10 GB | Low latency to uniond |
| unionvisor | Minimal (wraps uniond) | Minimal | Same as uniond | Same as uniond |
Important
Galois ZK proof generation is CPU-intensive. The
example-provecommand with BN254 curves can take significant time. Production deployments should run Galois on dedicated hardware with high single-thread performance.
Software requirements
| Dependency | Version | Purpose |
|---|---|---|
| Nix | 2.18+ with flakes | Reproducible builds for all components |
| Go | 1.22+ | uniond, galoisd |
| Rust | nightly (via rust-toolchain.toml) | Voyager, unionvisor, CosmWasm contracts, all lib crates |
| PostgreSQL | 14+ | Voyager queue backend |
| Node.js | 20+ | TypeScript SDK, web frontend |
| Foundry | Latest | EVM contract compilation and testing |
| Docker | 24+ | Optional: containerized deployment |
Supported chain connections (production)
| Chain | Chain ID | IBC Interface | Light Client Type |
|---|---|---|---|
| Union (mainnet) | union-1 |
IBC-Union | CometBLS |
| Ethereum | 1 |
EVM (Solidity) | Ethereum sync protocol |
| Arbitrum | 42161 |
EVM (Solidity) | Arbitrum L2 (state-lens) |
| Base | 8453 |
EVM (Solidity) | Base L2 (state-lens) |
| Berachain | 80094 |
EVM (Solidity) | Berachain (ethermint-based) |
| Bob | 60808 |
EVM (Solidity) | Bob L2 (state-lens) |
| BSC | 56 |
EVM (Solidity) | Parlia consensus |
| Corn | 21000000 |
EVM (Solidity) | Corn L2 |
| Babylon | bbn-1 |
CosmWasm | Tendermint |
| Osmosis | osmosis-1 |
CosmWasm | Tendermint |
| Sei | 1329 |
EVM/CosmWasm | Sei consensus |
| Starknet | — | Cairo | Starknet |
| Aptos | — | Move | Movement/Aptos |
| Sui | — | Move (Sui) | Sui consensus |
Note
Channel configurations, gas fees, and SLA targets for each chain connection are defined in
deployments/channels.json. SLA values use ISO 8601 duration format (e.g.,PT3H= 3 hours,PT20M= 20 minutes).
3. Configuration Reference
uniond configuration
uniond uses standard Cosmos SDK configuration files generated via uniond init:
| File | Location | Purpose |
|---|---|---|
config.toml |
~/.union/config/ |
CometBFT consensus, P2P, RPC settings |
app.toml |
~/.union/config/ |
Application-level settings (API, gRPC, state sync, pruning) |
genesis.json |
~/.union/config/ |
Chain genesis state |
client.toml |
~/.union/config/ |
CLI client defaults |
Key environment variables:
| Variable | Type | Default | Description |
|---|---|---|---|
MEMPOOL_PRIORITY |
string | "" |
Mempool priority mode (read in app.go) |
The address prefix is union (Bech32). The chain uses standard Cosmos SDK v0.50 configuration plus:
- Custom BLS staking module (
uniond/x/staking/) — validators must provide proof-of-possession for BLS keys - CosmWasm — enabled with stargate queries
- IBC-go v8 — full IBC handler with CometBLS light client support
- Skip feemarket module — dynamic fee market for gas pricing
Important
Validators must use the
create-union-validatorcommand instead of the standardcreate-validator. This command requires aproof_of_possessionargument — a non-prefixed hex-encoded BLS proof binding the validator's consensus key to their operator address.
Voyager configuration
Voyager uses a JSON/JSONC configuration file specified via --config-file-path (-c):
{
"$schema": "...",
"chains": {
"union-1": { ... },
"ethereum.1": { ... }
},
"plugins": [ ... ],
"queue": {
"database_url": "postgresql://user:pass@localhost/voyager",
"max_connections": 10,
"min_connections": 1,
"idle_timeout": null,
"max_lifetime": null,
"optimize_batch_limit": null,
"retryable_error_expo_backoff_max": 3600.0,
"retryable_error_expo_backoff_multiplier": 2.0,
"vacuum_on_boot": false
}
}
Global CLI flags:
| Flag | Short | Default | Description |
|---|---|---|---|
--config-file-path |
-c |
Required | Path to config file (JSON or JSONC) |
--log-format |
-l |
text |
Log output format: text or json |
--stack-size |
— | 2097152 (2 MB) |
Thread stack size in bytes |
Queue configuration (PgQueueConfig):
| Key | Type | Default | Description |
|---|---|---|---|
database_url |
string | Required | PostgreSQL connection string |
max_connections |
u32 | 10 |
Connection pool maximum |
min_connections |
u32 | 1 |
Connection pool minimum |
idle_timeout |
duration | null | Connection idle timeout |
max_lifetime |
duration | null | Connection max lifetime |
optimize_batch_limit |
i64 | null | Batch optimization limit |
retryable_error_expo_backoff_max |
f64 | 3600.0 |
Max backoff seconds for retryable errors |
retryable_error_expo_backoff_multiplier |
f64 | 2.0 |
Exponential backoff multiplier |
vacuum_on_boot |
bool | false |
Run VACUUM on queue tables at startup |
Environment variables used by Voyager plugins:
| Variable | Description |
|---|---|
RUST_LOG_FORMAT |
Override log format at runtime (used in lib/voyager-plugin) |
unionvisor configuration
unionvisor reads its configuration from CLI arguments and environment variables:
| Parameter | Env Var | Default | Description |
|---|---|---|---|
--root |
UNIONVISOR_ROOT |
Required | Path to unionvisor root directory (contains chain data) |
--bundle / -b |
UNIONVISOR_BUNDLE |
Required | Path to bundle directory containing versioned binaries |
--poll-interval |
UNIONVISOR_POLL_INTERVAL |
— | Interval to poll for upgrade signals |
Bundle structure (meta.json):
{
"binary_name": "uniond",
"fallback_version": "genesis",
"versions_directory": "versions"
}
bundle/
├── genesis.json
├── meta.json
└── versions/
├── genesis/
│ └── uniond
├── v0.8.0/
│ └── uniond
└── v0.9.0/
└── uniond
Warning
Each version binary is validated by calling
--helpbefore activation. If the binary is not executable or does not respond to--help, unionvisor will report aValidateVersionPathError. Ensure binaries have execute permissions (chmod +x) before deployment.
Galois configuration
galoisd is configured entirely via CLI flags:
| Command | Description |
|---|---|
galoisd serve |
Start gRPC prover service |
galoisd serve-bls12381 |
Start BLS12-381 specific prover |
galoisd example-prove [uri] [nb_validators] |
Simulation proof request |
galoisd example-verify [uri] |
Verify a proof |
galoisd export-vk |
Export verification key |
galoisd gen-contract |
Generate on-chain verifier contract |
gRPC connection flags:
| Flag | Values | Description |
|---|---|---|
--tls |
yes/true/1 or anything else |
Enable TLS for gRPC connection |
The gRPC API is defined in galoisd/proto/api/v3/ and exposes the UnionProverAPI service. Client connections have a 1-hour timeout by default.
Tip
Use
galoisd gen-contractto generate Solidity verifier contracts that can be deployed on EVM chains. This is the recommended approach for adding Union ZK verification to new EVM deployments.
4. Deployment & Scaling
uniond deployment
From Nix (recommended):
nix build .#uniond
./result/bin/uniond init <moniker> --chain-id union-1
From source:
cd uniond
go build -o uniond ./cmd/uniond
./uniond init <moniker> --chain-id union-1
Validator registration:
uniond tx union-staking create-union-validator \
/path/to/validator.json \
<proof_of_possession_hex> \
--from <key_name> \
--chain-id union-1
Caution
The
proof_of_possessionmust be a non-prefixed hex-encoded string. Prefixed hex (0x...) will fail with:failed to decode proof of possession. The value must be a non-prefixed hex-encoded string.
uniond ante handler chain (18 decorators):
The transaction processing pipeline in uniond/app/ante.go chains these decorators in order:
SetUpContextDecorator— initialize context (must be first)LimitSimulationGasDecorator— enforce CosmWasm simulation gas limitsCountTXDecorator— CosmWasm TX counterTxContractsDecorator— CosmWasm contract validationGasRegisterDecorator— CosmWasm gas registrationCircuitBreakerDecorator— circuit breaker for emergency haltsExtensionOptionsDecorator— validate extension optionsValidateBasicDecorator— basic tx validationTxTimeoutHeightDecorator— block height timeoutValidateMemoDecorator— memo validationConsumeGasForTxSizeDecorator— charge gas for tx sizeDeductFeeDecorator— deduct transaction feesSetPubKeyDecorator— set public keys (must precede sig verification)ValidateSigCountDecorator— validate signature countSigGasConsumeDecorator— consume gas for signaturesSigVerificationDecorator— verify transaction signaturesIncrementSequenceDecorator— increment account sequenceRedundantRelayDecorator— IBC redundant relay preventionFeeMarketCheckDecorator— Skip feemarket dynamic pricing check
unionvisor deployment
unionvisor wraps uniond to handle chain upgrades automatically:
# Initialize unionvisor with a bundle
unionvisor --root /data/union --bundle /opt/union-bundle init
# Run uniond under unionvisor supervision
unionvisor --root /data/union --bundle /opt/union-bundle run
# Manually set the active uniond version
unionvisor --root /data/union --bundle /opt/union-bundle set-uniond-version v0.9.0
# Call uniond through unionvisor
unionvisor --root /data/union --bundle /opt/union-bundle call -- status
Note
unionvisor monitors for on-chain upgrade signals and automatically switches to the new binary version. The bundle must contain the target version binary before the upgrade height is reached.
Voyager deployment
# Build Voyager
cargo build --release -p voyager
# Print config schema for editor support
voyager -c config.json config schema > voyager-schema.json
# Print default config
voyager -c config.json config default
# Start the relayer
voyager -c config.json start
PostgreSQL setup:
CREATE DATABASE voyager;
CREATE USER voyager WITH PASSWORD 'secure_password';
GRANT ALL PRIVILEGES ON DATABASE voyager TO voyager;
The queue automatically creates its schema on first run. The schema uses:
id SERIAL AUTO INCREMENT— message orderingstatus 0..2— message state (pending/processing/done)item JSONB— serialized operationerror TEXT— failure details
Warning
The PgQueue is designed for ~1k items/sec throughput. For high-volume relaying across many chains, monitor PostgreSQL performance and adjust
max_connectionsaccordingly. Consider runningvacuum_on_boot: truefor long-running instances.
Galois deployment
# Build galoisd
cd galoisd
go build -o galoisd ./cmd/galoisd
# Start the prover service
galoisd serve --tls yes
For MPC ceremony setup:
# Coordinator
cargo run --release -p mpc-coordinator -- --format json <subcommand>
# Client
cargo run --release -p mpc-client
Scaling considerations
| Component | Scaling Strategy |
|---|---|
| uniond | Horizontal: add more validator nodes. Vertical: increase CPU/RAM for faster block processing |
| Voyager | One instance per relaying path. Multiple instances share PostgreSQL with separate queue tables |
| Galois | CPU-bound: scale vertically (faster cores) or run multiple instances behind load balancer |
| PostgreSQL | Standard PostgreSQL scaling (connection pooling, read replicas for monitoring) |
5. Operations
uniond operations
Key commands:
# Check node status
uniond status
# Query blockchain state
uniond query <module> <query>
# Submit transactions
uniond tx <module> <msg> --from <key>
# Export state
uniond export
# Prove BLS key possession (for validator setup)
uniond prove-possession <private_key>
# Multi-node testnet setup
uniond multi-node
Upgrade workflow:
- Governance proposal passes with upgrade height
- unionvisor detects upgrade signal at the specified height
- unionvisor stops current uniond process
- unionvisor validates new binary version from bundle
- unionvisor starts new uniond version
- Node resumes consensus with upgraded binary
Upgrade history (from source):
| Version | Module | Path |
|---|---|---|
| v1.1.0 | Upgrade handler | uniond/app/upgrades/v1_1_0/ |
| v1.2.0 | Upgrade handler | uniond/app/upgrades/v1_2_0/ |
Tip
Run uniond under unionvisor in production. This eliminates manual intervention during chain upgrades and provides automatic rollback if a version fails validation.
Voyager operations
Queue management:
# View queue statistics
voyager -c config.json queue stats
# Enqueue a new operation
voyager -c config.json queue enqueue '<json_op>'
# Truncate specific queue tables
voyager -c config.json queue truncate --queue --done --failed
# Vacuum queue tables
voyager -c config.json queue vacuum --queue --done --failed --optimize
# Query failed messages
voyager -c config.json queue query-failed --page 1 --per-page 50
Index operations:
# Start indexing events from latest height
voyager -c config.json index <chain_id>
# Index from a specific height
voyager -c config.json index <chain_id> --from <height>
# Index a specific block range
voyager -c config.json index <chain_id> --from <start> --to <end>
# Index a single block
voyager -c config.json index <chain_id> --exact <height>
# Auto-enqueue the index op
voyager -c config.json index <chain_id> --enqueue
Plugin operations:
# List loaded plugins
voyager -c config.json plugin list
# Check plugin interest
voyager -c config.json plugin interest
# Get plugin info
voyager -c config.json plugin info
# Call plugin directly
voyager -c config.json plugin call <args>
RPC operations:
# Query client state
voyager -c config.json rpc --rpc-url <url> client-state <chain_id> <client_id>
# Query consensus state
voyager -c config.json rpc --rpc-url <url> consensus-state <chain_id> <client_id> <height>
# Get latest height
voyager -c config.json rpc --rpc-url <url> latest-height <chain_id>
# Query IBC state/proof
voyager -c config.json rpc --rpc-url <url> ibc-state <chain_id> <path>
voyager -c config.json rpc --rpc-url <url> ibc-proof <chain_id> <path> <height>
# Create client
voyager -c config.json msg create-client <args>
Note
The
queuesubcommand can be aliased asq. Theenqueuesubcommand can be aliased ase. Thestatssubcommand can be aliased ass.
Monitoring
Voyager OpenTelemetry metrics:
Voyager emits OpenTelemetry metrics through the opentelemetry crate. Key metrics from the cache and queue systems:
| Metric | Type | Description |
|---|---|---|
cache.state.size |
Gauge | Current state cache size |
cache.state.hit |
Counter | State cache hits |
cache.state.miss |
Counter | State cache misses |
cache.client_info.size |
Gauge | Client info cache size |
cache.client_info.hit |
Counter | Client info cache hits |
cache.client_info.miss |
Counter | Client info cache misses |
chain.latest_height |
Gauge | Latest known height per chain |
Galois health endpoint:
GET /health
This is the only HTTP endpoint exposed by galoisd (defined in galoisd/cmd/galoisd/cmd/query_stats_health.go).
Tip
Configure alerting on
cache.state.missrate increases — a spike may indicate chain RPC issues or configuration problems that prevent Voyager from reading on-chain state efficiently.
Log configuration
| Component | Log Framework | Configuration |
|---|---|---|
| uniond | Cosmos SDK logger | --log_level flag, config.toml log settings |
| Voyager | tracing crate |
RUST_LOG env var, --log-format flag (text/json) |
| galoisd | Go log package |
Standard Go logging |
| unionvisor | tracing crate |
RUST_LOG env var |
6. Performance
Voyager queue tuning
The PostgreSQL-backed queue (lib/pg-queue) is the performance-critical path for Voyager:
// Default configurations from source
max_connections: 10,
min_connections: 1,
retryable_error_expo_backoff_max: 3600.0, // 1 hour max
retryable_error_expo_backoff_multiplier: 2.0,
Optimization strategies:
-
Connection pool sizing — set
max_connectionsto match expected concurrent operations. For single-chain relaying, 5-10 is sufficient. For multi-chain, increase proportionally. -
Batch optimization — set
optimize_batch_limitto group related operations for fewer database round-trips. -
Vacuum policy — enable
vacuum_on_boot: truefor long-running instances to reclaim storage from processed messages. For active relayers, schedule periodicqueue vacuumoperations. -
Backoff tuning — reduce
retryable_error_expo_backoff_maxif you need faster retry cycles, but be cautious of overwhelming failing endpoints.
Voyager caching
The Voyager cache system (lib/voyager-core/src/cache.rs) uses the Moka LRU cache with configurable TTLs:
| Cache | Capacity | TTL | Purpose |
|---|---|---|---|
| State cache | Configurable | Configurable time_to_live + time_to_idle |
On-chain state lookups |
| Client info cache | Same as state | ~1000 years (effectively never expires) | Immutable client metadata |
| Latest height | 100 entries | ~1000 years | Per-chain latest heights (updated externally) |
| Latest timestamp | 100 entries | ~1000 years | Per-chain latest timestamps |
Note
The client info cache uses an extremely long TTL because client info is assumed to be immutable once created. This is a valid assumption for IBC client types, which are registered once and never change their fundamental properties.
Engine processing loop
The Voyager VM engine (lib/voyager-vm/src/engine.rs) processes operations in a spin loop with a 10ms sleep between iterations to prevent 100% CPU usage:
sleep(Duration::from_millis(10)).then(|()| {
self.queue.process(self.filter, async |op, id| { ... })
})
This provides ~100 operations/second baseline throughput per engine instance.
Channel SLA targets
Production channels define forward and reverse SLA targets in deployments/channels.json:
| Route | Forward SLA | Reverse SLA |
|---|---|---|
| Union ↔ Arbitrum | 3 hours | 20 minutes |
| Union ↔ Base | 3 hours | 20 minutes |
| Union ↔ Sei | 20 minutes | 20 minutes |
| Babylon ↔ Osmosis | 20 minutes | 20 minutes |
| Berachain ↔ Union | 15 hours | 20 minutes |
| Corn ↔ Babylon | 29 hours | 20 minutes |
Warning
SLA targets vary significantly by chain. Routes involving Ethereum L1 finality have longer forward SLAs due to the 12-minute slot times and finalization delay. Monitor relay latency against these targets to detect degradation.
7. Security
Consensus security: CometBLS
Union's consensus uses BN254 BLS aggregate signatures, allowing the entire validator set's signatures to be aggregated into a single proof. The security model:
- Validator key management — validators use BN254 BLS keys (not Ed25519). Key possession is proven via
prove-possessioncommand. - ZK verification — Groth16 proofs attest to ≥2/3 voting power signing a block. Proofs are verified on destination chains without accessing the full validator set.
- Trusted setup — the MPC ceremony (
ceremony/,mpc/) generates the proving/verification key pair. The ceremony uses a multi-party computation protocol ensuring security as long as at least one participant is honest.
Caution
The security of all ZK-verified cross-chain operations depends on the integrity of the MPC trusted setup. If the toxic waste (tau) is reconstructed, an attacker could forge proofs. The ceremony is designed so this requires ALL participants to collude.
IBC security model
Light client verification chain:
- Source chain light client tracks consensus state on destination
- Voyager submits state updates with ZK proofs (for Union) or traditional proofs (for other chains)
- Each chain independently verifies proofs before accepting state transitions
- No trusted third party in the verification path
Light client types and their verification:
| Client Type | Verification Method | Implementation |
|---|---|---|
| CometBLS | Groth16 ZK proof of BLS aggregate signature | 11-cometbls/ (Go), cosmwasm/lightclient/cometbls/ |
| Ethereum | Sync committee signature + execution state proof | cosmwasm/lightclient/ethereum/, EVM contracts |
| Tendermint | Standard Tendermint light client verification | cosmwasm/lightclient/tendermint/ |
| Parlia (BSC) | Parlia consensus proof with MPT state verification | cosmwasm/lightclient/parlia/ |
| Arbitrum | L1 → L2 state lens (derives security from Ethereum) | cosmwasm/lightclient/state-lens-ics23-mpt/ |
| Base | L1 → L2 state lens | cosmwasm/lightclient/state-lens-ics23-mpt/ |
| Bob | L1 → L2 state lens | cosmwasm/lightclient/bob/ |
| Berachain | Ethermint-based consensus proof | cosmwasm/lightclient/berachain/ |
| Movement | Movement consensus | cosmwasm/lightclient/movement/ |
| Starknet | Starknet state verification | cosmwasm/lightclient/starknet/ |
| Sui | Sui consensus | cosmwasm/lightclient/sui/ |
| Attested | Attested/witnessed verification | cosmwasm/lightclient/attested/ |
| Proof-lens | State-lens with proof aggregation | cosmwasm/lightclient/proof-lens/ |
CosmWasm contract security
Access management — contracts use the access-managed crate with role-based access control:
Authorityenum tracks contract admin/ownerEnsureCanCallResultpattern for permission checksPausabletrait for emergency circuit breakingUpgradabletrait for controlled contract migrations
IBC core security (cosmwasm/core):
- Client registration is restricted (
MsgRegisterClient) MsgForceUpdateClientexists for emergency situations (governance-gated)- Commitment paths use
COMMITMENT_MAGICandCOMMITMENT_MAGIC_ACKprefixes to prevent path confusion - Non-membership proofs use
NON_MEMBERSHIP_COMMITMENT_VALUEsentinel
EVM contract security
EVM IBC contracts (evm/contracts/core/) implement:
- Client registry (
02-client/) — maps client IDs to light client contract addresses - Connection management (
03-connection/) — IBC connection handshake - Channel management (
04-channel/) — IBC channel lifecycle - Port binding (
05-port/) — application module registration - Handler (
25-handler/) — unified message dispatcher - Host (
24-host/) — storage layout and commitment paths
ZKGM (UCS03) app (evm/contracts/apps/ucs/03-zkgm/) — handles token escrow, minting, and fungible asset order execution.
Warning
The
MsgForceUpdateClientmessage bypasses normal light client verification and should only be accessible through governance. Misconfiguration of access controls on this message could allow an attacker to set arbitrary consensus states.
Network security
- uniond — standard Cosmos SDK P2P network with configurable seed nodes, persistent peers, and private peer IDs
- Voyager — requires outbound RPC/REST connections to all relayed chains. No inbound connections required except for the optional JSON-RPC interface
- Galois — gRPC service; supports TLS (
--tls yes) - Inter-component — Voyager communicates with Galois via gRPC, with uniond via CometBFT RPC/REST
8. Developer Internals
Repository structure
unionlabs/union/
├── 11-cometbls/ CometBLS light client (Go, protobuf)
├── aptos/ Move modules (IBC, UCS03-ZKGM, ping-pong)
├── app2/ Bridge web UI (SvelteKit)
├── cairo/ Starknet contracts (CometBLS verifier, IBC, ZKGM)
├── ceremony/ MPC ceremony web interface (SvelteKit)
├── cosmwasm/ CosmWasm contracts
│ ├── access-managed/ Role-based access control
│ ├── access-manager/ Permission management
│ ├── app/ IBC applications (UCS00 ping-pong, UCS03 ZKGM)
│ ├── core/ IBC core handler (contract.rs: main entry point)
│ │ ├── light-client-interface/ Light client query trait
│ │ └── msg/ IBC message types
│ ├── cw-account/ Cross-chain account abstraction
│ ├── cw-escrow-vault/ Token escrow for bridging
│ ├── deployer/ Contract deployment tool
│ ├── gatekeeper/ Access gating
│ ├── lightclient/ Light client implementations (14 types)
│ ├── lst/ Liquid staking token
│ ├── lst-staker/ LST staking operations
│ ├── migrations/ Contract migration scripts
│ ├── multicall/ Batched contract calls
│ ├── pausable/ Emergency pause functionality
│ └── upgradable/ Safe upgrade patterns
├── deployments/ Production channel configs, bridged token mappings
├── devnet-compose/ Docker Compose for local devnet
├── docs/ Documentation site (Astro)
├── drip/ Testnet faucet (Rust)
├── e2e/ End-to-end tests
├── effect-svelte/ Svelte + Effect-TS integration lib
├── evm/ Solidity contracts
│ ├── contracts/
│ │ ├── apps/ucs/ UCS00 (ping-pong), UCS03 (ZKGM)
│ │ ├── clients/ EVM light client implementations
│ │ ├── core/ IBC core (client, connection, channel, handler, host)
│ │ └── internal/ Shared internal libraries
│ └── tests/ Foundry tests
├── galoisd/ ZK prover service (Go/Gnark)
│ ├── cmd/galoisd/ CLI entry point and commands
│ ├── grpc/ gRPC server and API definitions
│ ├── pkg/ Proving logic (BLS, merkle, lightclient, emulated)
│ └── proto/ Protobuf definitions (v1, v2, v3 API versions)
├── generated/ Auto-generated code (Rust protos, Aptos Move)
├── lib/ Shared Rust libraries (80+ crates)
│ ├── arbitrum-*/ Arbitrum types, verifier, client
│ ├── beacon-api*/ Ethereum beacon chain API types
│ ├── bob-*/ Bob chain types, verifier, client
│ ├── cometbft-*/ CometBFT types and RPC client
│ ├── cometbls-*/ CometBLS types and Groth16 verifier
│ ├── cosmos-*/ Cosmos SDK client, signer, events
│ ├── cosmwasm-event/ Derive macro for CosmWasm events
│ ├── ethereum-*/ Ethereum types, sync protocol
│ ├── fork-schedules/ Chain fork schedule definitions
│ ├── ics23/ ICS-23 proof verification
│ ├── pg-queue/ PostgreSQL FIFO queue (Voyager backend)
│ ├── voyager-*/ Voyager core, message, VM, SDK, RPC, plugins
│ └── unionlabs*/ Core primitives, encoding, types
├── mpc/ MPC ceremony infrastructure
│ ├── client/ MPC participant client
│ ├── coordinator/ MPC coordinator service
│ ├── edge/ Supabase edge functions
│ └── shared/ Shared MPC types
├── networks/ Network configurations
│ ├── genesis/ Genesis files (testnet-2 through testnet-10, devnet)
│ ├── services/ Support services (blockscout)
│ └── simulation/ Network simulation configs
├── patches/ Third-party patches
├── sentinel2/ Network monitoring service
├── site/ Marketing website (Astro)
├── sui/ Sui Move modules (IBC, ZKGM, vaults, ping-pong, muno)
├── tools/ Utility tools
│ ├── u/ Swiss-army CLI (zkgm, cometbft, arbitrum, parlia, codec, vanity)
│ ├── devnet-utils/ Devnet management utilities
│ ├── move-bindgen/ Move binding generator
│ ├── tidy/ Code tidying
│ └── update-deployments/ Deployment config updater
├── ts-sdk/ TypeScript SDK (Effect-TS)
│ ├── src/ Source (aptos, cosmos, evm, sui, ucs03, schema)
│ ├── examples/ 23 example files
│ └── dtslint/ Type-level tests
├── ts-sdk-cosmos/ Cosmos-specific TS SDK
├── ts-sdk-evm/ EVM-specific TS SDK
├── ts-sdk-sui/ Sui-specific TS SDK
├── typescript-sdk/ Legacy TypeScript SDK
├── uniond/ Chain node
│ ├── app/ Application wiring (ante.go, app.go, wasm.go, upgrades)
│ ├── cmd/uniond/ CLI entry point and commands
│ ├── proto/ Protobuf definitions (staking, IBC light clients)
│ └── x/staking/ Custom BLS staking module
├── unionvisor/ Process supervisor
│ └── src/ Bundle management, supervisor, CLI
├── versions/ Version tracking
├── voyager/ IBC relayer
│ ├── src/ Core relayer (cli.rs, config, main)
│ ├── modules/ Modular implementations
│ │ ├── client/ Light client modules (15 implementations)
│ │ ├── client-bootstrap/ Client initialization (16 implementations)
│ │ ├── finality/ Finality modules (12 implementations)
│ │ ├── proof/ Proof generation (7 implementations)
│ │ └── state/ State query modules (4: cosmwasm, evm, movement, sui)
│ └── plugins/ Plugin system
│ ├── attestor/ EVM attestation
│ ├── client-update/ Client update handlers (16 chain-specific)
│ ├── event-source/ Event indexing (5: cosmwasm, evm, movement, starknet, sui)
│ ├── packet-*/ Packet processing (batch, filter, index, timeout)
│ ├── periodic-client-update/ Scheduled client updates
│ ├── sui-*/ Sui-specific plugins
│ ├── transaction/ TX submission (4: aptos, cosmos, evm, sui)
│ ├── transaction-batch*/ TX batching
│ └── zkgm-filter/ ZKGM packet filtering
└── zkgm-dev/ ZKGM development playground (SvelteKit)
Voyager plugin architecture
Voyager uses a dynamic plugin system where each module/plugin is a separate Rust binary communicating via JSON-RPC:
Module categories:
| Category | Purpose | Count |
|---|---|---|
client |
Light client state management | 15 (one per chain type) |
client-bootstrap |
Initial client creation | 16 |
finality |
Finality tracking | 12 |
proof |
Proof construction | 7 |
state |
On-chain state queries | 4 |
Plugin categories:
| Category | Purpose | Count |
|---|---|---|
event-source |
Block/event indexing | 5 |
client-update |
Light client update logic | 16 |
transaction |
TX submission to chains | 4 |
packet-* |
Packet lifecycle (batch, filter, index, timeout) | 4 |
transaction-batch* |
TX batching optimization | 2 |
attestor |
EVM attestation | 1 |
periodic-client-update |
Scheduled updates | 1 |
zkgm-filter |
ZKGM packet filtering | 1 |
sui-* |
Sui-specific handling | 2 |
Interest filter system:
Each plugin declares interest in specific operation types. The Voyager VM engine routes operations to plugins based on their declared interest:
pub enum FilterResult<'a> {
Interest(Interest<'a>), // Plugin wants this op
NoInterest, // Plugin ignores this op
}
Operations matching a plugin's interest are placed in an optimization queue tagged with the plugin's identifier.
IBC-Union specification
Union implements a custom IBC specification (ibc-union-spec) that extends standard IBC with:
- ClientId, ConnectionId, ChannelId — strongly typed identifiers
- Batch operations —
MsgBatchSend,MsgBatchAcksfor multi-packet transactions - Intent-based packets —
MsgIntentPacketRecvfor intent-matching protocols - Force update —
MsgForceUpdateClientfor governance-gated emergency updates - Membership proofs —
MsgCommitMembershipProof,MsgCommitNonMembershipProof - Commitment paths —
COMMITMENT_MAGIC(0x01) prefix for packet commitments,COMMITMENT_MAGIC_ACKfor ack commitments
ZKGM (UCS03) protocol
The Zero-Knowledge General Message (ZKGM) protocol implements cross-chain token transfers via fungible asset orders:
Intent types (TypeScript SDK):
| Intent | Source | Destination |
|---|---|---|
EvmToEvmIntent |
EVM chain | EVM chain |
EvmToCosmosIntent |
EVM chain | Cosmos chain |
CosmosToEvmIntent |
Cosmos chain | EVM chain |
CosmosToCosmosIntent |
Cosmos chain | Cosmos chain |
AptosToCosmosIntent |
Aptos | Cosmos chain |
AptosToEvmIntent |
Aptos | EVM chain |
Contract implementations:
cosmwasm/app/ucs03-zkgm/— CosmWasm ZKGM with token minter API, solvable intents, pausable, upgradableevm/contracts/apps/ucs/03-zkgm/— Solidity ZKGMcairo/zkgm/— Cairo ZKGM for Starknetsui/ucs03_zkgm/— Sui Move ZKGMaptos/ucs03-zkgm/— Aptos Move ZKGM
CosmWasm IBC core internals
The IBC core contract (cosmwasm/core/src/contract.rs) manages:
State stores:
| Store | Purpose |
|---|---|
ClientRegistry |
Maps client types to implementation addresses |
ClientTypes |
Maps client IDs to their types |
ClientStates |
Client state data |
ClientConsensusStates |
Consensus states per client per height |
ClientImpls |
Light client contract addresses |
ClientStore |
Generic client storage |
Connections |
Connection state |
Channels |
Channel state |
ChannelOwner |
Maps channels to owning modules |
ContractChannels |
Channels belonging to a contract |
Commitments |
Packet/ack commitments |
NextClientId, NextConnectionId, NextChannelId |
Auto-increment counters |
Events emitted:
register_client, create_client, update_client, force_update_client, connection_open_init, connection_open_try, connection_open_ack, channel_open_init, channel_open_try, channel_open_ack, channel_open_confirm, channel_close_init, channel_close_confirm, packet_send, packet_recv, packet_ack, intent_packet_recv, batch_send, batch_acks, timeout_packet, write_ack, commit_membership_proof, commit_non_membership_proof.
Build system
Nix flakes (primary):
The entire repository uses Nix flakes for reproducible builds. The flake.nix at the root defines outputs for all components.
# Build specific components
nix build .#uniond
nix build .#voyager
nix build .#galoisd
nix build .#unionvisor
# Enter development shell
nix develop
Cargo workspace:
The Rust codebase uses a workspace with 200+ crates. Key workspace settings are inherited (authors, edition, license-file, publish, repository). All crates use version = "0.0.0" (versioning is handled at the release/tag level).
Foundry (EVM):
cd evm
forge build
forge test
Testing strategy
| Test Type | Location | Framework |
|---|---|---|
| Rust unit tests | Inline #[cfg(test)] modules |
cargo test |
| CosmWasm integration | cosmwasm/*/src/tests/ |
cw-multi-test |
| EVM tests | evm/tests/src/ |
Foundry (forge test) |
| Cairo tests | cairo/*/tests/ |
Cairo test framework |
| Sui tests | sui/*/tests/ |
Sui Move test framework |
| Aptos tests | aptos/*/tests/ |
Aptos Move test framework |
| E2E tests | e2e/ |
Custom Rust harness (access-manager-tests, ensure-blocks) |
| TypeScript tests | ts-sdk/test/, ts-sdk-*/test/ |
Vitest |
| SSZ conformance | lib/ssz/tests/spec_conformance/ |
Spec test vectors |
| Fuzz tests | galoisd/pkg/*/testdata/fuzz/, lib/unionlabs/fuzz_targets/ |
Go fuzzing, cargo-fuzz |
Code patterns and conventions
Rust patterns:
thiserrorfor error type derivationtracingfor structured logging (withinfo,debug,trace,warn,errormacros)serdefor JSON/JSONC serializationclapfor CLI argument parsing- Workspace-level lint configuration
crate-type = ["cdylib", "rlib"]for CosmWasm contracts
Go patterns:
cobrafor CLI commands (uniond, galoisd)depinjectfor dependency injection (Cosmos SDK)- Protobuf for serialization
TypeScript patterns:
- Effect-TS for composable, type-safe operations
- Separate SDK packages per chain type (
ts-sdk-evm,ts-sdk-cosmos,ts-sdk-sui)
Known issues from GitHub
| Issue | Description | Status |
|---|---|---|
| #5163 | Login error / user-facing error handling | Open |
| #4962 | HTTP method override vulnerability | Open |
| #5428 | Error handling improvements needed | Open |
| #738 | Voyager documentation gaps | Open |
Note
Issue #4962 describes an HTTP method override concern. If deploying Voyager's JSON-RPC interface publicly, ensure proper HTTP method validation at the reverse proxy level.
9. Integration Points
TypeScript SDK integration
npm install @unionlabs/ts-sdk
Package structure:
| Package | Purpose |
|---|---|
@unionlabs/ts-sdk |
Core SDK with cross-chain intents |
@unionlabs/ts-sdk-evm |
EVM-specific operations |
@unionlabs/ts-sdk-cosmos |
Cosmos-specific operations |
@unionlabs/ts-sdk-sui |
Sui-specific operations |
Key environment variables for examples:
| Variable | Purpose |
|---|---|
PRIVATE_KEY |
EVM private key (for Aptos examples) |
MNEMONIC |
Cosmos wallet mnemonic |
SUI_MNEMONIC |
Sui wallet mnemonic |
RECIPIENT |
Transfer recipient address |
MEMO |
Transaction memo |
Example: programmatic EVM-to-Cosmos transfer:
import { createEvmToCosmosIntent } from "@unionlabs/ts-sdk/ucs03";
const intent = createEvmToCosmosIntent({
sourceChainId: "1", // Ethereum
destinationChainId: "union-1",
sender: "0x...",
receiver: "union1...",
token: "0x...",
amount: BigInt("1000000"),
channelId: 1,
});
Tip
The TypeScript SDK uses Effect-TS for composable error handling and async operations. Wrap SDK calls in Effect pipelines for proper error propagation and resource cleanup.
CosmWasm contract integration
Deploy ZKGM and IBC contracts on Cosmos chains:
# Build contracts
cd cosmwasm
cargo build --release --target wasm32-unknown-unknown
# Deploy using the deployer tool
cargo run --release -p cosmwasm-deployer -- \
--chain-id <target_chain_id> \
--rpc <rpc_url>
IBC application integration:
Implement the IbcUnionMsg execute handler in your CosmWasm contract to receive cross-chain messages:
use ibc_union_msg::module::{ExecuteMsg as ModuleMsg, IbcUnionMsg};
Register your contract as a channel owner via MsgChannelOpenInit through the IBC core contract.
EVM contract integration
Deploying IBC on a new EVM chain:
- Generate verifier contract:
galoisd gen-contract - Deploy IBC core contracts from
evm/contracts/core/ - Deploy light client contract for the target chain type
- Register the light client: call
registerClienton the handler - Deploy ZKGM app from
evm/contracts/apps/ucs/03-zkgm/ - Open channel through IBC handshake
Building the EVM deployer transaction:
cargo run --release -p build-evm-deployer-tx
Voyager plugin development
Create a new Voyager plugin:
- Create a new Rust crate under
voyager/plugins/<category>/<name>/ - Implement the plugin trait from
lib/voyager-plugin/ - Define interest filter for operation routing
- Register in Voyager configuration
use voyager_plugin::Plugin;
// Plugin entry point
fn main() {
// Plugin initializes and connects via JSON-RPC
}
Environment configuration for plugins:
The lib/voyager-plugin/src/lib.rs reads RUST_LOG_FORMAT for log formatting.
Chain channel configuration
Production channel mappings are defined in deployments/channels.json. Each channel entry specifies:
{
"<chain_id>": {
"<channel_number>": {
"comments": "<counterparty description>",
"fees": {
"forward": {
"PACKET_RECV": "<gas_units>",
"PACKET_SEND_LC_UPDATE_L0": "<gas_units>",
"PACKET_SEND_LC_UPDATE_L1": "<gas_units>",
"PACKET_SEND_LC_UPDATE_L2": "<gas_units>"
},
"reverse": { ... }
},
"sla": {
"forward": "<ISO_8601_duration>",
"reverse": "<ISO_8601_duration>"
},
"tags": ["canonical", "tokenorder-v2"]
}
}
}
Fee levels:
PACKET_RECV— gas for receiving a packetPACKET_SEND_LC_UPDATE_L0— gas for light client update (level 0)PACKET_SEND_LC_UPDATE_L1— gas for light client update (level 1, L2 chains)PACKET_SEND_LC_UPDATE_L2— gas for light client update (level 2, deep verification)
Bridged tokens:
Token mappings between chains are stored in evm/bridged_tokens_v1.json, containing base_token ↔ quote_token pairs with their associated channel IDs and direction (in/out).
Drip faucet integration
The drip service (drip/) provides testnet token distribution:
cargo run --release -p drip
Example request configurations are in drip/example-requests/.
u utility tool
The u tool (tools/u/) is a developer swiss-army knife with subcommands for:
| Subcommand | Purpose |
|---|---|
zkgm make |
Construct ZKGM test data |
cometbft |
CometBFT debugging utilities |
arbitrum |
Arbitrum state inspection |
parlia |
BSC Parlia consensus tools |
codec |
Encoding/decoding utilities |
vanity |
Vanity address generation |
Troubleshooting
Error: account keeper is required for ante builder
Cause: uniond NewAnteHandler is missing a required keeper in the HandlerOptions. This occurs during node initialization if dependency injection fails.
Solution:
- Verify the
app.gowiring correctly provides all keepers via depinject - Check that all required modules are registered in the app configuration
- Verify Cosmos SDK version compatibility
Prevention: Use the Nix build system to ensure consistent dependency versions.
Error: failed to decode proof of possession
Cause: The create-union-validator command received a proof of possession with a 0x prefix or invalid hex encoding.
Solution:
# Remove the 0x prefix
uniond tx union-staking create-union-validator validator.json abc123... --from mykey
# NOT: 0xabc123...
Prevention: Always use non-prefixed hex for BLS proof of possession values.
Error: unable to read the config file
Cause: Voyager cannot find or parse the configuration file specified via --config-file-path.
Solution:
- Verify the file path exists:
ls -la /path/to/config.json - Check JSON syntax:
jq . /path/to/config.json - For JSONC files, ensure the extension is
.jsonc
Prevention: Use voyager config schema to generate a JSON Schema for editor validation.
Error: version was not found in bundle
Cause: unionvisor attempted to activate a version that doesn't exist in the bundle directory.
Solution:
- Check bundle structure:
ls -la /path/to/bundle/versions/ - Ensure the target version directory contains the binary:
ls -la /path/to/bundle/versions/<version>/uniond - Verify
meta.jsonhas correctversions_directoryandbinary_name
Prevention: Pre-populate bundle with all expected version binaries before upgrade heights.
Error: permission denied when executing version in bundle
Cause: The uniond binary in the bundle lacks execute permissions.
Solution:
chmod +x /path/to/bundle/versions/<version>/uniond
Prevention: Include chmod +x in your binary deployment scripts.
Error: Invalid Sui signer: expected Ed25519Keypair
Cause: The Sui SDK integration received an incorrect keypair type.
Solution: Ensure you're using Ed25519Keypair from the Sui SDK, not other key types.
Error: error while reading wasm config
Cause: uniond failed to read CosmWasm configuration during initialization.
Solution:
- Verify
app.tomlcontains valid wasm configuration - Check that the wasm directory exists and has correct permissions
- Ensure the CosmWasm module is properly configured in genesis
Prevention: Use uniond init to generate default configuration before modifying wasm settings.
Error: failed to register snapshot extension
Cause: The CosmWasm snapshot extension failed to register during node startup.
Solution:
- Check that the state directory is not corrupted
- Verify sufficient disk space
- Try resetting state with
uniond tendermint unsafe-reset-all(destroys local state)
Prevention: Monitor disk space and filesystem health on validator nodes.
Voyager queue grows unbounded
Cause: Failed operations accumulate in the PostgreSQL queue without cleanup.
Solution:
- Check failed messages:
voyager -c config.json queue query-failed - Inspect error details for recurring failures
- Truncate failed queue if errors are transient:
voyager -c config.json queue truncate --failed - Run vacuum:
voyager -c config.json queue vacuum --queue --done --failed
Prevention: Enable vacuum_on_boot: true in queue config. Monitor queue stats regularly.
Tip
For persistent queue growth, check the
retryable_error_expo_backoff_maxsetting. A value that's too high (default: 3600s) can cause operations to back up during extended chain outages.
Galois proof generation timeout
Cause: ZK proof generation exceeds the 1-hour gRPC client timeout.
Solution:
- Check galoisd resource usage (CPU, memory)
- Reduce validator count if testing:
galoisd example-prove <uri> <smaller_count> - Ensure sufficient RAM (32 GB+ for production proofs)
Prevention: Monitor galoisd with the /health endpoint. Scale hardware for production validator set sizes.
Warning
The default gRPC timeout in galoisd is 1 hour (
1 * time.Hour). For very large validator sets, proof generation may approach this limit. Monitor proof generation times and adjust timeouts if necessary.
Error: connection state must be 1..=3
Cause: Voyager's Sui state module encountered an invalid IBC connection state value during state query.
Solution:
- Verify the Sui IBC contract is deployed correctly
- Check that the connection was properly established through the IBC handshake
- Query the connection state directly on Sui to inspect the raw value
Prevention: Ensure IBC channel/connection establishment completes all handshake steps before relaying packets.
Error: channel state must be 1..=4
Cause: Voyager's Sui state module encountered an invalid IBC channel state value.
Solution: Same approach as connection state errors — verify the IBC channel was fully established.
FAQ
Q: How does Union differ from other cross-chain bridges?
Union uses ZK proofs (Groth16) to verify consensus instead of multisig committees or optimistic verification. This provides cryptographic security guarantees — the bridge is as secure as the underlying math, not dependent on honest majority assumptions among bridge operators.
Q: What is CometBLS and why use BN254?
CometBLS is a modified CometBFT consensus that uses BN254 BLS aggregate signatures instead of Ed25519. BN254 was chosen because it enables efficient pairing-based ZK proofs (Groth16), allowing the entire validator set's signatures to be verified on-chain via a single compact proof rather than checking each signature individually.
Q: Can I run Voyager without Galois?
Yes, for chains that don't require ZK proof verification (standard Tendermint-to-Tendermint relaying), Voyager operates independently. Galois is only needed when relaying to/from chains that verify Union consensus via ZK proofs.
Q: How do I add support for a new chain?
- Implement a Voyager client module (
voyager/modules/client/<chain>/) - Implement finality tracking (
voyager/modules/finality/<chain>/) - Implement state queries (
voyager/modules/state/<chain>/) - Implement proof construction (
voyager/modules/proof/<chain>/) - Implement client-bootstrap (
voyager/modules/client-bootstrap/<chain>/) - Add event-source plugin (
voyager/plugins/event-source/<chain>/) - Add client-update plugin (
voyager/plugins/client-update/<chain>/) - Add transaction plugin (
voyager/plugins/transaction/<chain>/) - Deploy IBC contracts on the target chain
Q: What is the u tool?
u is a developer utility tool at tools/u/ that provides subcommands for ZKGM data construction, CometBFT debugging, Arbitrum/Parlia state inspection, codec utilities, and vanity address generation. It's a Swiss-army knife for Union protocol development.
Q: How does the Voyager queue work?
Voyager uses a PostgreSQL-backed FIFO queue (lib/pg-queue). Operations are serialized as JSONB and processed by the engine in a spin loop with 10ms sleep between iterations. Each operation passes through an interest filter that routes it to the appropriate plugin. Failed operations use exponential backoff with configurable maximum delay.
Q: What are the state-lens light clients?
State-lens light clients are Union's approach for L2 chains that derive security from L1. Instead of directly verifying L2 consensus, a state-lens client reads L2 state through the L1 settlement layer's proof system. This means Arbitrum and Base security inherits from Ethereum L1 verification. Implementations exist as state-lens/ics23-mpt (Merkle Patricia Trie), state-lens/ics23-smt (Sparse Merkle Tree), and state-lens/ics23-ics23.
Q: How are EVM contracts organized?
The IBC core is split into modules under evm/contracts/core/: 02-client (client registry), 03-connection (connection handshake), 04-channel (channel lifecycle), 05-port (port binding), 24-host (storage), 25-handler (dispatcher). Applications live under evm/contracts/apps/ucs/. Tests use Foundry under evm/tests/.
Q: What is the MPC ceremony for?
The Multi-Party Computation ceremony generates the trusted setup parameters (proving and verification keys) for the Groth16 ZK circuits used in CometBLS verification. The ceremony ensures that no single participant learns the toxic waste, maintaining the soundness of the proof system. Components: ceremony/ (web UI), mpc/coordinator/ (orchestrator), mpc/client/ (participant).
Q: How do bridged token mappings work?
Token mappings in evm/bridged_tokens_v1.json link base tokens on one chain to wrapped (quote) tokens on another, along with channel IDs and direction. When a user bridges a token, the ZKGM contract uses these mappings to determine the correct wrapped token to mint on the destination chain.
Q: What happened to the legacy TypeScript SDK?
The typescript-sdk/ directory contains the original SDK. It has been superseded by the new modular ts-sdk/ (core), ts-sdk-evm/, ts-sdk-cosmos/, and ts-sdk-sui/ packages that use Effect-TS for composable, type-safe cross-chain operations.