Vara → Ethereum App Guide

Cross-chain communication can seem complex, but at its core, it’s about securely moving messages and value between independent blockchains. To make this process clear and approachable, a simple Ping-Pong application is used as the most illustrative example, showing step by step how secure cross-chain messaging works on top of the Vara ↔ Ethereum Bridge.

The Ping-Pong Pattern

The ping-pong pattern is a classic distributed systems example used to demonstrate reliable message delivery:

  • Ping: A simple message sent from the source chain.
  • Pong: A response received back from the target chain.

This pattern confirms that the Vara-Ethereum Bridge transport layer is operating correctly in both directions.

This guide focuses on the flow from Vara to Ethereum using the bridge’s built-in ZK-based mechanism. The protocol handles the heavy lifting — cryptographic proofs, relaying, and validation — allowing developers to concentrate on the application logic. Future articles will explore the reverse direction (Ethereum → Vara) and additional cross-chain patterns.

High-Level Architecture

At a high level, the cross-chain ping-pong app consists of three main parts, each focused on its own responsibility while relying on the bridge’s secure protocol layer:

  1. On Vara: a Gear Program in Rust acts as the Ping sender. It packages a simple payload and sends it using the built-in bridge API, specifying the destination contract on Ethereum. See Vara Program Code
  2. In the middle: a permissionless relayer listens for events and — once the Merkle root for the message is submitted to Ethereum — relays the message for final delivery. See Relayer Code
  3. On Ethereum: a specialized MessageQueue contract verifies the root and proof. If valid, it forwards the message to your destination contract.
    See Ping Receiver Code

What You Don’t Implement

Abstracted Infrastructure

As a developer, you do not need to implement the transport layers yourself. The bridge infrastructure handles these complex operations automatically:

  • Calculations: Merkle root and proof generation.
  • Security: ZK-inclusion proofs.
  • Sync: Cross-chain state synchronization.

This allows you to focus purely on your application logic while the protocol manages the underlying Vara-Ethereum communication.

Your responsibility is only your app’s custom logic: how to format the message and how to process it on arrival.

If you want a deep dive into the protocol’s internals — including proof generation, validator set management, and message processing — please see the official technical overview: Vara ↔ Ethereum Bridge Technical Overview

The bridge’s protocol ensures that Ethereum can trust which messages were sent from Vara, so you never need to worry about the details of proof construction or on-chain validation.
Everything you need for secure message delivery is already built in.


Vara → Ethereum Cross-Chain App’s Implementation Details

Let’s break down the flow and key implementation details of our CrossPing app on Vara, which is responsible for initiating the cross-chain ping.

See Vara Program Source

Application Flow (Vara)

  1. Initialize the destination: The program is initialized with the address (H160) of the Ethereum contract that will receive the ping.
  2. Send a ping: When triggered, it prepares a payload (just the sender’s ActorId) and constructs a bridge request with it.
  3. Send the message via the built-in bridge: It uses a built-in actor (Gear-Ethereum Bridge) to submit the message using send_bytes_with_gas_for_reply, which handles gas and bridge fee logic.
  4. Wait for bridge response: The bridge responds with a nonce and a message hash, confirming message inclusion.
  5. Emit event for relayer: The program emits a PingSent event with all details. This is what the relayer listens for.

Key Components and Interfaces

  • Built-in Bridge Actor ID:
const BRIDGE_ACTOR_ID: [u8; 32] = [ /* ... see full source ... */ ];
  • Destination Contract Address:
    Set at initialization via new(destination: H160).

  • Bridge Message Format:

BridgeRequest::SendEthMessage { destination, payload }
  • Gas & Fee Constants:
const GAS_TO_SEND_REQUEST: u64 = 200_000_000_000;
const FEE_BRIDGE: u128 = 0;
const GAS_FOR_REPLY_DEPOSIT: u64 = 200_000_000_000;
  • Event Emission:
    Emits:
Event::PingSent(PingSent {
  nonce: Some(nonce.as_u64()),
  message_hash: hash,
})

Example: Sending a Ping from Vara

pub async fn send_ping(&mut self) -> Result<(), Error> {
    let state = unsafe { STATE.as_ref().expect("State not initialized") };
    let destination = state.destination.ok_or(Error::DestinationNotInitialized)?;
    let sender = exec::program_id();
    let payload = sender.as_ref().to_vec();
    let bridge_actor_id = ActorId::from(BRIDGE_ACTOR_ID);

    let request = BridgeRequest::SendEthMessage { destination, payload }.encode();

    let reply_bytes = msg::send_bytes_with_gas_for_reply(
        bridge_actor_id,
        request,
        GAS_TO_SEND_REQUEST,
        FEE_BRIDGE,
        GAS_FOR_REPLY_DEPOSIT,
    )
    .map_err(|_| Error::BridgeSendFailed)?
    .await
    .map_err(|_| Error::BridgeReplyFailed)?;

    let reply = BridgeResponse::decode(&mut &reply_bytes[..])
        .map_err(|_| Error::InvalidBridgeResponse)?;

    if let BridgeResponse::EthMessageQueued { nonce, hash } = reply {
        self.emit_event(Event::PingSent(PingSent {
            nonce: Some(nonce.as_u64()),
            message_hash: hash,
        }))?;
    }

    Ok(())
}

Relayer

The relayer is a Node.js service that connects both to Vara and Ethereum, listens for events, and relays messages once the Merkle root is confirmed on Ethereum.

See Relayer Code

How the Relayer Works

  1. Connects to Vara and Ethereum:
const gearApi = await connectVara();
  1. Listens for PingSent events:
await listenPingSent(async ({ nonce, blockNumber, messageHash }) => {
  console.log(`[Relay] PingSent: nonce=${nonce}, block=${blockNumber}, hash=${messageHash}`);
  // ...
});
  1. Relays the message with built-in wait mechanism:

In the latest version (@gear-js/[email protected]), the relayVaraToEth function accepts a wait: true flag that internally waits for the Merkle root to appear on Ethereum before relaying.

const result = await relayVaraToEth({
  nonce,
  blockNumber,
  ethereumPublicClient,
  ethereumWalletClient,
  ethereumAccount: account,
  gearApi,
  messageQueueAddress: MESSAGE_QUEUE_PROXY_ADDRESS,
  wait: true,
  statusCb: (status, details) => {
    console.log(`[Relay] [Status]`, status, details);
  }
});

Ethereum Side: Ping Receiver Contract

A minimal contract that accepts messages via the bridge and emits PongEmitted.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.24;

interface IMessageHandler {
    function handleMessage(bytes32 source, bytes calldata payload) external;
}

contract PingReceiver is IMessageHandler {
    event PongEmitted(bytes32 sender);

    function handleMessage(bytes32 sender, bytes calldata) external override {
        emit PongEmitted(sender);
    }
}

Summary

  • Your contract must implement the IMessageHandler interface.
  • The handleMessage(sender, payload) function is called by the MessageQueue.
  • You can do anything you want with the payload inside this method.

End-to-End Verification

After sending a ping:

  1. Check MessageQueue contract on Ethereum for message processing events.
  2. Check your PingReceiver contract for the PongEmitted event.
  3. The relayer logs the full tx hash after successful delivery:
[Relay] ✅ Success: txHash=0xabc123...

If the logs and events appear correctly on Etherscan (or your block explorer), everything worked as expected.

On this page