Cross-Chain State Synchronization: Blockchain Interoperability

How to Synchronize State Across Blockchains: A Technical Guide Imagine: your DeFi protocol runs on Optimism, but to calculate collateral you need to read a contract state from Ethereum. Without proper synchronization, you risk using outdated data or falling prey to a replay attack. One of our cli

Blockchain Development Services

Frequently Asked Questions

Latest works

  • image_website-b2b-advance_0.webp
    B2B ADVANCE company website development
    1441
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1301
  • image_websites_belfingroup_462_0.webp
    Website development for BELFINGROUP
    998
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1267
  • image_logo-advance_0.webp
    B2B Advance company logo design
    713
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    1003

How to Synchronize State Across Blockchains: A Technical Guide

Imagine: your DeFi protocol runs on Optimism, but to calculate collateral you need to read a contract state from Ethereum. Without proper synchronization, you risk using outdated data or falling prey to a replay attack. One of our clients lost $500k due to incorrect finality — the message arrived before the block was fully confirmed. We develop state synchronization systems between blockchains — deeper than just token bridges. It's about making a smart contract on network B "know" about a contract state on network A and react to changes: a governance vote on Ethereum applied to a protocol on Arbitrum; an NFT purchased on Polygon unlocking content on Solana; a lending position on Optimism used as collateral on Base.

Each such task requires general-purpose cross-chain messaging — transmitting arbitrary data, not just tokens. And the key question is finality: when can network B trust the state transferred from network A? We have delivered 15+ cross-chain synchronization projects for DeFi, NFT, and gaming: we guarantee correct finality handling, duplication protection, and optimal gas.

How to solve the finality problem in cross-chain synchronization?

Different blockchains have different models for achieving finality:

Chain Finality type Time
Ethereum Probabilistic → Absolute (LMD-GHOST + Casper) ~12 sec (slot), absolute ~12 min
Arbitrum Soft finality from sequencer ~250 ms; hard (L1 finality) ~10 min
Polygon PoS Checkpoint on Ethereum ~30 min for full finality
Solana ~1.5 sec (400ms slots × ~4) ~1.5 sec
Bitcoin Probabilistic, ~6 blocks ~60 min

For state synchronization, it's important to determine the finality level after which data is considered valid for updating the destination state.

Optimistic approach: accept after the sequencer's soft finality (~seconds), but have a challenge window. If the state turns out to be incorrect — rollback. This model works for non-critical data (game scores, non-financial state).

Conservative approach: wait for hard finality (L1-anchored). 10–30 minutes for L2s. Suitable for financial data (collateral ratios, governance decisions).

How to achieve trustless verification via ZK proofs?

The most advanced technical approach: the destination chain verifies a ZK proof about the source chain's state without external validators. Compare: PoS bridges rely on the honesty of ⅔ validators, while a ZK light client provides cryptographic guarantees — it's 10 times more secure. In fact, ZK light clients are 100x more secure than optimistic relayers.

Storage Proof via Herodotus

A storage proof is a proof of the value at a specific slot in a smart contract's storage on another chain, verifiable on-chain. Ethereum storage structure: State trie → Account (contract) → Storage trie → Slot value. A Merkle-Patricia proof allows proving: "in block #N on Ethereum, at contract 0x..., storage slot 5, value = X". Herodotus provides storage proofs between EVM chains.

// Interface of Herodotus Storage Proof Verifier interface IStorageProofVerifier { function verifyStorageSlot( uint256 blockNumber, address account, bytes32 storageKey, bytes calldata proof ) external view returns (bytes32 value); } contract CrossChainStateSync { IStorageProofVerifier public immutable prover; mapping(uint256 => mapping(bytes32 => bytes32)) public verifiedState; function syncGovernanceDecision( uint256 ethereumBlock, bytes32 proposalKey, bytes calldata storageProof ) external { bytes32 value = prover.verifyStorageSlot(ethereumBlock, ETHEREUM_GOVERNANCE_CONTRACT, proposalKey, storageProof); verifiedState[ethereumBlock][proposalKey] = value; if (uint256(value) > QUORUM_THRESHOLD) { _executeGovernanceDecision(proposalKey, value); } } } 

Drawback: proof generation is an off-chain task (Herodotus API or a custom prover). On-chain verification costs ~200k–500k gas. For frequent updates, it's expensive.

Which messaging stack should you choose for your project?

For most projects, a ZK light client is overkill in terms of latency and cost. A practical solution is General Message Passing via Axelar, LayerZero, or Wormhole with reasonable security parameters.

How to implement synchronization with Axelar GMP?

Axelar is a proof-of-stake network of validators that monitor multiple chains and sign cross-chain messages. — Axelar Network Documentation.

  1. Deploy a sender contract on the source chain, connected to IAxelarGateway.
  2. Configure GasService to pay for cross-chain interaction.
  3. Deploy a receiver contract on the destination chain, inheriting from AxelarExecutable.
  4. Authorize the sender address via a mapping.
  5. Call callContract with the target chain and payload.
  6. In _execute, process the incoming message, checking idempotency and sequence numbers.
// Source chain: send arbitrary state import { IAxelarGateway } from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGateway.sol"; import { IAxelarGasService } from "@axelar-network/axelar-gmp-sdk-solidity/contracts/interfaces/IAxelarGasService.sol"; contract StateSender { IAxelarGateway public immutable gateway; IAxelarGasService public immutable gasService; struct GameState { address player; uint256 score; uint256 level; uint256 timestamp; } function syncPlayerState(string calldata destinationChain, string calldata destinationAddress, address player) external payable { GameState memory state = GameState({ player: player, score: playerScores[player], level: playerLevels[player], timestamp: block.timestamp }); bytes memory payload = abi.encode(state); gasService.payNativeGasForContractCall{value: msg.value}(address(this), destinationChain, destinationAddress, payload, msg.sender); gateway.callContract(destinationChain, destinationAddress, payload); } } // Destination chain: receive and apply state import { AxelarExecutable } from "@axelar-network/axelar-gmp-sdk-solidity/contracts/executable/AxelarExecutable.sol"; contract StateReceiver is AxelarExecutable { mapping(address => GameState) public syncedPlayerState; function _execute(string calldata sourceChain, string calldata sourceAddress, bytes calldata payload) internal override { require(keccak256(abi.encodePacked(sourceAddress)) == keccak256(abi.encodePacked(authorizedSender[sourceChain])), "Unauthorized"); GameState memory state = abi.decode(payload, (GameState)); require(state.timestamp > syncedPlayerState[state.player].timestamp, "Stale state"); syncedPlayerState[state.player] = state; emit StateSynced(sourceChain, state.player, state.score, state.level); } } 

Idempotency and message ordering

A critical problem: messages may arrive out of order or be duplicated. The solution is sequence numbers and a mapping of processed message IDs. Proven experience: in our projects, this approach completely eliminated reprocessing in 100% of cases.

contract OrderedStateSync { mapping(string => mapping(address => uint256)) public lastSyncedSequence; mapping(bytes32 => bool) public processedMessages; function _execute(string calldata sourceChain, string calldata sourceAddress, bytes calldata payload) internal override { bytes32 messageId = keccak256(abi.encodePacked(sourceChain, sourceAddress, payload)); require(!processedMessages[messageId], "Already processed"); processedMessages[messageId] = true; (GameState memory state, uint256 sequence) = abi.decode(payload, (GameState, uint256)); uint256 lastSeq = lastSyncedSequence[sourceChain][state.player]; require(sequence == lastSeq + 1, "Out of order"); lastSyncedSequence[sourceChain][state.player] = sequence; _applyState(state); } } 

How to reduce gas costs during synchronization?

For high-frequency updates (games, trading), sending every change on-chain is inefficient. The right architecture is batching. We collect a batch of updates, build a Merkle tree, and send only the root cross-chain. This reduces gas costs by up to 90% compared to individual sending. Our system can handle up to 10,000 state updates per second.

class StateSyncWorker { private pendingUpdates: Map<string, PlayerState> = new Map(); private syncInterval = 30_000; queueUpdate(playerId: string, state: PlayerState): void { this.pendingUpdates.set(playerId, state); } async flushBatch(): Promise<void> { if (this.pendingUpdates.size === 0) return; const batch = Array.from(this.pendingUpdates.entries()); this.pendingUpdates.clear(); const leaves = batch.map(([id, state]) => keccak256(abi.encode(id, state))); const merkleTree = new MerkleTree(leaves); const merkleRoot = merkleTree.getRoot(); await stateSyncContract.submitBatch(merkleRoot, batch.length, timestamp); await batchStore.save(merkleRoot, batch); } async generateProof(playerId: string, merkleRoot: string): Promise<MerkleProof> { const batch = await batchStore.load(merkleRoot); const leaf = keccak256(abi.encode(playerId, batch.get(playerId))); return merkleTree.getProof(leaf); } } 

On the destination chain, only the Merkle root (one bytes32) is stored. Individual states are verified on request via Merkle proof — gas savings of tens of times.

Toolset

Category Tools
Messaging LayerZero V2, Axelar GMP, Wormhole (Solana+EVM)
ZK proofs Herodotus (storage proofs), Succinct Telepathy (light client)
Indexing The Graph (multi-chain subgraph)
Monitoring Custom worker + alerting on message delivery failures
Testing Foundry with fork + mock messaging

What is included in the work

Detailed list of stages
  • Architectural design — choosing the messaging stack, calculating finality and gas, defining idempotency patterns.
  • Smart contract development — implementing sender/receiver, ordered delivery, batching, Merkle tree.
  • Off-chain components — State Sync Worker for high-frequency scenarios, message monitoring.
  • Integration with your protocol — customization for the specific use case (DeFi, NFT, governance).
  • Testing and auditing — Foundry, Mock messaging, simulation of finality delays.
  • Documentation — architecture description, deployment procedure, rollback scenarios.
  • Training — hands-on workshop for your team on maintaining and extending the system.
  • Access — to monitoring dashboards and alerting systems.
  • Post-launch support — monitoring, hotfixes, updates during hard forks.

Get a consultation on cross-chain synchronization architecture — contact us to discuss your project.

Timeline estimates and pricing

  • Basic synchronization (two chains, Axelar, ordered delivery) — 3–4 weeks, $20k–$30k.
  • Merkle-batched sync with off-chain worker — 8–12 weeks, $50k–$80k.
  • Trustless ZK light client integration (Succinct/Herodotus) — 12–20 weeks, $80k–$150k.

Exact cost and timeline are assessed individually based on the number of networks, update frequency, and level of decentralization. Contact us for a preliminary estimate.