Introduction to sending messages from actor on Vara Network to Ethereum

Simplified diagram & code of how Vara Network Bridge works is as follows. You can also find deployed addresses of Vara Network Bridge smart contracts on our wiki. Source code for smart contracts is available on GitHub. image00

IMessageHandler is interface that needs to be implemented on Ethereum side of smart contract if you want to receive message with bytes payload from actor bytes32 source (Vara Network address). Entry to handleMessage(bytes32 source, bytes calldata payload) method is called by MessageQueue smart-contract. So it is necessary to check that if (msg.sender != address(messageQueue)) { revert InvalidSender(); } on IMessageHandler implementor side.

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

IGovernance interface contains addresses of contracts: WrappedVara, MessageQueue, and ERC20Manager. This is essentially list of contracts that can be: pause()-ed, unpause()-ed, and upgrade()-ed. IGovernance also contains bytes32 governance (address of actor on Vara Network), which can send VaraMessage to Ethereum contract implementing IGovernance (GovernanceAdmin or GovernancePauser), since it also implements IMessageHandler.

solidity
interface IGovernance is IMessageHandler { function governance() external view returns (bytes32); function wrappedVara() external view returns (address); function messageQueue() external view returns (address); function erc20Manager() external view returns (address); }

When handleMessage(bytes32 source, bytes calldata payload) is called, IGovernance implementor does if (source != governance) { revert InvalidSource(); } check, then implementor parses payload that is generated by governance-tool.

Payload format:

solidity
uint8 discriminant;

discriminant can be:

  • GovernanceConstants.CHANGE_GOVERNANCE = 0x00 - change internal bytes32 governance address to newGovernance
solidity
bytes32 newGovernance;
  • GovernanceConstants.PAUSE_PROXY = 0x01 - pause proxy, note that proxy is one of WrappedVara, MessageQueue, and ERC20Manager smart contracts
solidity
address proxy;
  • GovernanceConstants.UNPAUSE_PROXY = 0x02 - unpause proxy
solidity
address proxy;
  • GovernanceConstants.UPGRADE_PROXY = 0x03 - upgrade proxy to newImplementation and call data on it, data is the encoded method name and arguments to be executed upon update
solidity
address proxy; address newImplementation; bytes data;

GovernanceConstants.UPGRADE_PROXY action is only available on GovernanceAdmin.

IVerifier interface has method safeVerifyProof(...) that verifies zk-SNARK Plonk proof, which lets us know that blockNumber and merkleRoot are on Vara Network. publicInputs contains encoded blockNumber and merkleRoot.

solidity
interface IVerifier { function safeVerifyProof( bytes calldata proof, uint256[] calldata publicInputs ) external view returns (bool success); }

MessageQueue smart contract stores addresses of GovernanceAdmin, GovernancePauser, and Verifier. Since bridge is trustless, it also has owners: GovernanceAdmin address can call methods pause(), unpause(), and upgrade() on MessageQueue smart contract, while GovernancePauser address can call only methods pause() and unpause() on MessageQueue smart contract. However, since these smart contracts (GovernanceAdmin and GovernancePauser) don't have private keys, no one directly owns bridge.

Organization of roles and their capabilities in MessageQueue are presented below:

solidity
contract MessageQueue is Initializable, AccessControlUpgradeable, PausableUpgradeable, UUPSUpgradeable, IPausable, IMessageQueue { IGovernance private _governanceAdmin; IGovernance private _governancePauser; IVerifier private _verifier; mapping(uint256 blockNumber => bytes32 merkleRoot) private _blockNumbers; mapping(bytes32 merkleRoot => uint256 timestamp) private _merkleRootTimestamps; mapping(uint256 messageNonce => bool isProcessed) private _processedMessages; function initialize( IGovernance governanceAdmin_, IGovernance governancePauser_, // ... IVerifier verifier_ ) public { // ... _grantRole(DEFAULT_ADMIN_ROLE, address(governanceAdmin_)); _grantRole(PAUSER_ROLE, address(governanceAdmin_)); _grantRole(PAUSER_ROLE, address(governancePauser_)); _governanceAdmin = governanceAdmin_; _governancePauser = governancePauser_; _verifier = verifier_; } function pause() public onlyRole(PAUSER_ROLE) { // ... } function unpause() public onlyRole(PAUSER_ROLE) { // ... } function _authorizeUpgrade(address newImplementation) internal override onlyRole(DEFAULT_ADMIN_ROLE) { // ... } function submitMerkleRoot( uint256 blockNumber, bytes32 merkleRoot, bytes calldata proof ) external { // ... } function processMessage( uint256 blockNumber, uint256 totalLeaves, uint256 leafIndex, VaraMessage calldata message, bytes32[] calldata proof ) external { // ... } }

If no one controls bridge, how can token holders make decisions about bridge pause/unpause/upgrade? Instead of centralized governance using private keys as is done in many Ethereum smart contracts, we use special bytes32 governance (address of actor on Vara Network).

For simplicity, we'll assume that bytes32 governance is address controlled by private key, but in reality, we're using governance. Let's query bytes32 governance and convert it to an address on Vara Network Mainnet.

To do this, we need to go to Vara Network Wiki and find address of MessageQueue smart contract.

ComponentAddress
MessageQueue0x8E01Fbf136cA97627ca241dB9EFf1DFE3f2195F6

Now we need to go to "Contract" -> "Read as proxy" tab and query address of GovernanceAdmin smart contract. image01 image02

ComponentAddress
GovernanceAdmin0x3681A3e25F5652389B8f52504D517E96352830C3

Once you find GovernanceAdmin smart contract address, query it for bytes32 governance. image03

solidity
bytes32 governance = 0xae3d2870312437cdcc0a887bf532343b6595a7ed97d6aa71efe58b677817393a;

Note: bytes32 governance may have changed over time due to maintenance, but this is a valid example since it refers to older blocks.

Now you can use service ss58.org to convert 0x to Vara Network address. image04

You can open this address kGjUSu7y5UeJ3UpensatJ5h1BacWWKPVapnYvRU59CvuPUenq on Subscan.

For example, we recently completed upgrade of smart contracts on Ethereum Mainnet, and it occurred in this transaction. This transaction calls gear-eth-bridge pallet, method send_eth_message(destination: H160, payload: Vec<u8>).

image05 Specifically, in this transaction:

  • destination = 0x3681A3e25F5652389B8f52504D517E96352830C3
  • payload = 0x038e01fbf136ca97627ca241db9eff1dfe3f2195f6256cbaeaa34acfed715fc60301e088575410ca636c2eb350

As you can see, destination is 20-byte or 160-bit Ethereum address of GovernanceAdmin smart contract. Also, as you can see, first byte of payload is 0x03, which corresponds to GovernanceConstants.UPGRADE_PROXY = 0x03 (we'll get to how you can check this payload soon).

So, in end we have that address kGjUSu7y5UeJ3UpensatJ5h1BacWWKPVapnYvRU59CvuPUenq (0xae3d2870312437cdcc0a887bf532343b6595a7ed97d6aa71efe58b677817393a) sent message to Vara Network that looks like this:

solidity
struct VaraMessage { uint256 nonce; bytes32 source; address destination; bytes payload; }

Essentially, this bytes32 governance address from the Vara Network sends message to GovernanceAdmin to modify the state on Ethereum in some way.

solidity
VaraMessage memory message1 = VaraMessage({ // incremented each time in `gear-eth-bridge` pallet nonce: 655, // `bytes32 governance` source: 0xae3d2870312437cdcc0a887bf532343b6595a7ed97d6aa71efe58b677817393a, // `GovernanceAdmin` destination: 0x3681A3e25F5652389B8f52504D517E96352830C3, payload: UpgradeProxyMessage({ // `MessageQueue` proxy: 0x8E01Fbf136cA97627ca241dB9EFf1DFE3f2195F6, // https://etherscan.io/address/0x256cbaeaa34acfed715fc60301e088575410ca63 newImplementation: 0x256cbAEaA34ACFed715Fc60301E088575410CA63, // When updating, call "reinitialize()" in smart contract code // with address `newImplementation` // `cast calldata "function reinitialize()"` // `0x6c2eb350` // NOTE: `cast` is a utility from https://getfoundry.sh data: abi.encodeWithSelector(MessageQueue.reinitialize.selector) }).pack() });

This message will be added to message queue. For example, message queue was empty: message_queue = [], and after transaction is executed it becomes: message_queue = [message1].

Now we'll calculate bytes32 message1Hash and do this for each message if there are multiple messages. Hash is calculated using formula from here.

solidity
bytes32 message1Hash = message1.hash(); assert(message1Hash == 0x74e982b853662b477f2571c57508ca7dc25e0f95fcec29956a755d36fe9c4e7a);
text
message_queue_hashes = [message1Hash] message_queue_hashes = [ 0x74e982b853662b477f2571c57508ca7dc25e0f95fcec29956a755d36fe9c4e7a ]

Now we will need to build Merkle Tree from message_queue = [message1].

Merkle Tree is data structure that allows for compact and cryptographically secure verification that an element (VaraMessage) is part of data set.

How tree is constructed:

  • Leaves are hashes of original data (VaraMessages)

Hash0-0 = hash(L1)

Hash0-1 = hash(L2)

  • Internal nodes are hashes from concatenation of child nodes:

Hash0 = hash(Hash0-0 || Hash0-1)

  • Process repeats up levels until a single hash remains - tree root (Merkle Root).

image06 This means that if message queue looked like this: message_queue = [message1, message2, message3, message4], then we could find root of merkle tree of all those messages and verify that message1 is in message_queue by providing only merkle proof that is log2(message_queue.length) in size. This is very compact cryptographic proof, since log2(1,048,576) = 20. In other words, if we had million messages, Merkle proof would require about 20 hashes.

Now let's move on to these fields of MessageQueue smart contract:

solidity
mapping(uint256 blockNumber => bytes32 merkleRoot) private _blockNumbers; mapping(bytes32 merkleRoot => uint256 timestamp) private _merkleRootTimestamps;

Vara Network Bridge relayer only needs to call submitMerkleRoot(uint256 blockNumber, bytes32 merkleRoot, bytes calldata proof) method and pass blockNumber - block number on Vara Network, merkleRoot - root of merkle tree of all messages in block blockNumber, proof - a zk-SNARK Plonk proof that blockNumber and merkleRoot are on Vara Network.

You can see an example of calling submitMerkleRoot(...) in this transaction.

After calling submitMerkleRoot(...) on MessageQueue smart contract, its storage will be modified as follows:

solidity
_blockNumbers[blockNumber] = merkleRoot; _merkleRootTimestamps[merkleRoot] = block.timestamp;

This means that we, or anyone else who hosts relays, only need to own powerful enough server that can generate zk-SNARK Plonk proofs. Users simply make a JSON RPC request and build merkle tree locally/on an RPC node, then transmit sufficiently small merkle proof to Ethereum by calling processMessage(...) method. They can also pay relay fee to avoid having to do anything related to delivering message on Ethereum.

processMessage(...) method will check that VaraMessage has not yet been processed, verify that message actually matches merkle root, and then call IMessageHandler(message.destination).handleMessage(message.source, message.payload). Then GovernanceAdmin will process payload and upgrade will occur. Since GovernanceAdmin is owner of MessageQueue, it will be able to call upgradeToAndCall(address newImplementation, bytes memory data) method.

After executing processMessage(...), the storage changes will be as follows:

solidity
mapping(uint256 messageNonce => bool isProcessed) private _processedMessages;
solidity
_processedMessages[message.nonce] = true;

How can I participate in verification of pause/unpause/upgrade procedure if I am a token holder?

Installing dependencies:

bash
# ~1.4 GB sudo apt install curl git jq build-essential clang cmake pkg-config libssl-dev binaryen

Installing Rust:

bash
# ~1.3 GB curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y . "$HOME/.cargo/env"

Installing Foundry:

bash
# ~380 MB curl -L https://foundry.paradigm.xyz | bash source "$HOME/.bashrc" foundryup

Build utility for generating payloads for Ethereum contracts:

bash
git clone https://github.com/gear-tech/gear-bridges.git cd gear-bridges/tools/governance cp deployment.example.mainnet.toml deployment.toml MAINNET_RPC_URL="https://ethereum-rpc.publicnode.com" RPC_URL=$MAINNET_RPC_URL cargo +stable run --package governance-tool --release -- --ethereum-endpoint $RPC_URL --help

Let's say you want to verify that encoded update message is indeed correct. You need to do following:

bash
PROXY_MQ="MessageQueue" NEW_IMPLEMENTATION="0x256cbAEaA34ACFed715Fc60301E088575410CA63" # must exist on https://etherscan.io cargo +stable run --package governance-tool --release -- --ethereum-endpoint $RPC_URL GovernanceAdmin UpgradeProxy $PROXY_MQ $NEW_IMPLEMENTATION $(cast calldata "function reinitialize()")

Example output. You can compare it with the update message above and check that the hex strings match.

text
source: 0xae3d2870312437cdcc0a887bf532343b6595a7ed97d6aa71efe58b677817393a destination: 0x3681A3e25F5652389B8f52504D517E96352830C3 payload: 0x038e01fbf136ca97627ca241db9eff1dfe3f2195f6256cbaeaa34acfed715fc60301e088575410ca636c2eb350

For other message options, see the governance-tool's README.md.

Vara

Website | X | Discord | Telegram | Wiki | GitHub

Gear Protocol

Website | X | Discord | Telegram | GitHub | Gear IDEA | Whitepaper