All posts

Relay — Cross-Chain Sync

12 min read · ComputeFlux Team
Economic Protocol

In one sentence. Getting information from one blockchain to another normally means trusting a small group of signers; ComputeFlux deletes that group entirely and hands the job to the validators who already secure the chain.

Picture it like this. Two countries that need to honor each other's court rulings. The usual solution hires a courier and trusts them not to forge documents — and the courier promptly becomes the most valuable thing in the system to attack. The alternative is to make verification part of the receiving court's own process, so there's no courier to bribe in the first place.

Why it matters. Bridge exploits are the single largest category of theft in crypto — Ronin lost $625M, Wormhole $326M — and nearly all of them share one shape: a small signing group was the weakest link. Narrowing what a bridge is allowed to do is worth more than any amount of auditing what it does.

Want the trust-minimized endgame? Skip to Phase 2 and Merkle proofs.


Cross-chain bridges are the single most exploited attack surface in Web3, with over $2 billion lost across Wormhole, Ronin, Poly Network, and Nomad. The vulnerability is structural. A bridge is a trusted third party inside a system built to eliminate trusted third parties. It has to authenticate events on Chain A, relay them to Chain B, and guarantee the relay is neither forged nor replayed — without introducing a central signer whose compromise drains both chains.

ComputeFlux's Relay subsystem doesn't solve the general bridge problem. It avoids most of it, by narrowing scope to one-way event synchronization from the Polkadot main chain down to the TEE sidechain, and by putting the relay logic inside the same BFT validator set that already secures that sidechain.

This is also where the retry architecture from Article 9 does its second job. Settlement transactions crossing this bridge use the same block-gated, PrepareProposal-driven cadence covered there, so it isn't re-derived here.

The Trust Problem in Cross-Chain Communication

Every cross-chain bridge runs into the same impossibility. Chain A's consensus rules cannot verify Chain B's state without running Chain B's full consensus protocol — every block header, every validator set transition, every signature. No production bridge does that in full; light clients approximate it with varying degrees of trust. So every bridge inserts a relayer: an off-chain agent that watches Chain B and submits events to Chain A with some form of proof. Which leaves one question. What do you have to believe about the relayer to believe the event?

Many early bridges answered it in the most trusting way possible. The relayer is a multisig of known entities that observes Chain B, signs attestations, and submits them to Chain A — which verifies the multisig signature, not Chain B's consensus. Compromise that multisig through key theft, insider collusion, or social engineering, and the bridge will mint arbitrary tokens on Chain A. Ronin's $625M exploit and Wormhole's $326M exploit both followed exactly this shape. The multisig was the weakest link.

ComputeFlux's Phase 1 takes a narrower assumption. All relay nodes are TEE enclaves running identical code, attested through SGX remote attestation (Article 1), and they participate in the same CometBFT consensus as the sidechain validators.

Nothing is "relayed" in the traditional sense. The block proposer proposes a RelaySync transaction (SysCall_RelaySync), every validator verifies it during ProcessProposal via CheckRelayEvent, and it executes during FinalizeBlock via ApplyRelayEvent. Trust lands on the sidechain's own validator set, which already executes every other state transition on that chain. Two trust assumptions collapse into one, and the weakest-link dynamic disappears with them.

One gap remains: validators still have to learn about Chain B events from somewhere. In Phase 1, each one independently polls a Polkadot RPC endpoint, and an endpoint can lie in two directions.

Lying by omission means failing to report an event that really happened. The validator never sees it, never proposes a RelaySync, and the event never reaches the sidechain. That's a denial of service.

Lying by commission means inventing an event that never happened — considerably harder, because the fabricated event still has to pass the sidechain's own transaction validation. It arrives as a system-account contract call, not a separately re-signed artifact.

So the read path is trusted and the write path runs through the validator set that secures everything else. Phase 2, below, removes the read-path trust too.

Why Nonce-Based Replay Protection Suffices for Phase 1 (But Not Phase 2)

Nonce-based replay protection maintains a strictly monotonic counter per chain (chainID). Each RelaySync carries a nonce that must equal the current counter + 1; after execution, the counter increments. A replayed transaction — whether resubmitted by a malicious relayer or caused by a network retransmission — carries a stale nonce and is rejected.

That's enough for Phase 1, because the attack surface is bounded. An adversary controlling a relay RPC endpoint can suppress events, which is denial of service. Fabricating one still means getting it dispatched and validated through the sidechain's own consensus and contract layer — the path that secures every other state transition. What the nonce blocks is the cheaper attack: replaying a legitimate past event after the tokens were already credited. Without it, anyone who captured a valid RelaySync off the network could resubmit it indefinitely and drain the gateway contract each time.

Nonces carry a limitation, though, and it bites under Phase 2's trust-minimized model. Every validator has to agree on the current nonce, which means every validator has to have processed every prior SyncTx. A validator joining after the first 10,000 events faces two bad options: replay all 10,000 to derive the correct nonce, or accept the current value from existing validators — reintroducing precisely the trust assumption the system exists to remove.

Nonces are stateful. Merkle proofs are not. Phase 2 drops the nonce entirely and replaces it with proof that the event exists in a known Chain B block header. The sidechain stores those headers in a light client model and verifies a Merkle inclusion proof against the stored header's event root. No stateful counter, no bootstrap trust problem, no replay surface.

Polling Instead of Event-Driven Subscriptions

Most real-time systems reach for push over poll: WebSocket subscriptions, Server-Sent Events, webhooks. ComputeFlux polls Polkadot RPC on a fixed 10-second interval instead, and the reason is reliability when the network misbehaves.

A WebSocket connection to an RPC node that drops may miss a block notification. Reconnection handshakes add latency, and the missed block has to be fetched retroactively. Under real instability — congested links, RPC node overload, DDoS mitigation — that connection can flap repeatedly, leaving gaps in event coverage the relay then has to detect and backfill.

Polling is less elegant and self-correcting by construction. Each poll fetches everything since the last-seen nonce. If a poll fails, the next one retries from the same pointer. That's a far simpler recovery story than the client-side retry machinery in Article 9, precisely because there's no connection state to rebuild.

The cadence is coarser than Polkadot's ~6-second block time, so a single poll often sees several blocks' worth of events at once. The worker fetches everything between the last-seen nonce and the chain's latest on every tick rather than assuming one block per poll. That coarseness costs little: GRANDPA finality for parachain blocks adds well beyond a block time on top of production, so polling isn't the dominant latency factor anyway. It also sits comfortably inside typical public RPC rate limits, since each validator issues only a handful of requests per minute. WebSocket subscriptions don't consume that budget directly, but their reconnection and backfill logic can fire bursts of REST requests that do.

The Cache Delay and Block Finality

An event fetched from Polkadot RPC isn't immediately eligible for a RelaySync. It sits in an in-memory cache for a minimum age — relayMinCacheAge, currently 3 seconds — before the block proposer may include it, and the proposer only packs it once every earlier nonce in that chain's sequence has matured too.

The purpose is coordination more than fork resolution. Every validator polls Polkadot RPC independently, so a short mandatory cache age raises the odds that by the time a RelaySync reaches consensus, the other validators have already fetched and cached the same underlying event and can check it against their own view.

Forks are a real concern underneath. Polkadot's BABE block production can produce competing candidates at the same slot, and GRANDPA resolves them through a round of pre-votes and pre-commits that takes longer than this cache window. An event from a block GRANDPA later orphans must never reach the sidechain: the tokens on the source chain were never transferred, and the event only ever existed in a discarded fork.

So the cache window is a lightweight safeguard against acting the instant an event appears, not a substitute for full finality tracking. What bounds the remaining risk is that rolling back a bridged event means out-running Polkadot's own finality gadget — which means compromising a supermajority of the Polkadot validator set.

Pre-Signing: Taking Signing Off the Proposer's Critical Path

ComputeFlux keeps signing off the proposer's critical path by doing it early — at the moment each validator fetches and caches an event, not in a cross-node ceremony at proposal time. Each validator runs its own background fetch loop against Polkadot RPC. The instant it builds a SyncEvent for a newly observed event, it calls SignAndBuildRelayTx, which signs the encoded RelaySync with that node's own key and stores the signed bytes in the local cache as cachedEvent.txBytes, next to the event and its timestamp.

Since every validator pre-signs every event it observes, whichever one ends up proposing already has ready-to-include signed bytes waiting once the event clears the minimum cache age. No separate signing step, no extra round of coordination on the hot path of PrepareProposal.

Worth not conflating this with the DKG ceremony elsewhere in the system. This is a per-node optimization — each validator signs its own copy ahead of need. Mainchain-bound settlement transactions (Articles 9 and 10) use multi-party threshold signing instead. The two flows run in opposite directions and use different signing models.

Phase 2: Merkle Proofs and the Elimination of RPC Trust

Phase 2 replaces trust in RPC endpoints with cryptographic verification of Merkle inclusion proofs. The architecture mirrors a light client: each sidechain validator maintains a record of Polkadot block headers, synced either through the relay polling mechanism or a dedicated header chain relay. When a Polkadot event is submitted to the sidechain, the RelaySync transaction includes the event data, a Merkle proof of the event's inclusion in the block's event trie, and the block header containing the event root.

Verification takes three steps. Confirm the block header is known and finalized in the local header chain. Extract the event root from that header. Walk the Merkle proof from the event leaf up to the root, checking each hash along the way. If it verifies, the event is authenticated with no trust in the submitter at all — an RPC node that invents an event cannot produce a valid proof for it, because the invention never hashes to a leaf in the authenticated event trie.

The costs are small. Block headers run on the order of hundreds of bytes, so a day's worth is a modest fixed cost that fits inside ComputeFlux's EPC budget alongside consensus and application state (Article 1 has that breakdown). Pruning headers beyond a configurable horizon keeps storage bounded indefinitely. And verification latency is negligible: a proof over a modestly-branching event trie takes a handful of hash operations, adding microseconds to RelaySync processing — invisible next to consensus and contract execution.

Phase 2 reaches the trust-minimized ideal. The sidechain verifies Polkadot state with the same cryptographic rigor a Polkadot full node applies, without running Polkadot's full consensus protocol. One assumption remains: that the Polkadot validator set produces honest blocks. That's the same assumption every Polkadot user already makes, backed by Polkadot's own multi-billion-dollar economic security.


Key Takeaways

  • The scope is deliberately tiny. One direction only, main chain to sidechain, one kind of event. Most bridge exploits are paid for by generality that nobody actually needed.
  • Two trust assumptions collapse into one. There is no separate relayer group to compromise — the validators who already secure every other state transition on the chain handle this one too. That single change removes the weakest-link dynamic that killed Ronin and Wormhole.
  • What's still trusted today, said out loud. Validators learn about main-chain events by asking an RPC endpoint. A lying endpoint can hide an event, which is a denial of service. Inventing one is harder, because a fabricated event still has to survive the chain's own validation.
  • Polling beats live subscriptions here, against the usual instinct. A dropped connection leaves gaps someone must notice and backfill; a failed poll just tries again from the same pointer. Having no connection state to rebuild is worth more than the elegance.
  • Phase 2 removes even the RPC trust. With cryptographic inclusion proofs, an endpoint that invents an event simply cannot produce a valid proof for it. The cost is a few hundred bytes per block header and microseconds of hashing — cheap enough that the current design is a stepping stone, not a destination.

Cross-chain settlement moves value in and out of the network. The next stretch of the series is about a different kind of friction — not moving money between chains, but getting a request from a developer's existing code into whichever AI provider ends up serving it.

Next — Article 12: Protocol Adaptor & Compatibility Matrix: how an app written against OpenAI's SDK ends up talking to Claude without changing a single line.