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.
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.
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.
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:
uint8 discriminant;discriminant can be:
GovernanceConstants.CHANGE_GOVERNANCE = 0x00- change internalbytes32 governanceaddress tonewGovernance
bytes32 newGovernance;GovernanceConstants.PAUSE_PROXY = 0x01- pauseproxy, note thatproxyis one ofWrappedVara,MessageQueue, andERC20Managersmart contracts
address proxy;GovernanceConstants.UNPAUSE_PROXY = 0x02- unpauseproxy
address proxy;GovernanceConstants.UPGRADE_PROXY = 0x03- upgradeproxytonewImplementationand calldataon it,datais the encoded method name and arguments to be executed upon update
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.
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:
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.
| Component | Address |
|---|---|
MessageQueue | 0x8E01Fbf136cA97627ca241dB9EFf1DFE3f2195F6 |
Now we need to go to "Contract" -> "Read as proxy" tab and query address of GovernanceAdmin smart contract.
| Component | Address |
|---|---|
GovernanceAdmin | 0x3681A3e25F5652389B8f52504D517E96352830C3 |
Once you find GovernanceAdmin smart contract address, query it for bytes32 governance.
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.
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>).
Specifically, in this transaction:
destination = 0x3681A3e25F5652389B8f52504D517E96352830C3payload = 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:
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.
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.
bytes32 message1Hash = message1.hash();
assert(message1Hash == 0x74e982b853662b477f2571c57508ca7dc25e0f95fcec29956a755d36fe9c4e7a);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).
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:
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:
_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:
mapping(uint256 messageNonce => bool isProcessed) private _processedMessages;_processedMessages[message.nonce] = true;How can I participate in verification of pause/unpause/upgrade procedure if I am a token holder?
Installing dependencies:
# ~1.4 GB
sudo apt install curl git jq build-essential clang cmake pkg-config libssl-dev binaryenInstalling Rust:
# ~1.3 GB
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y
. "$HOME/.cargo/env"Installing Foundry:
# ~380 MB
curl -L https://foundry.paradigm.xyz | bash
source "$HOME/.bashrc"
foundryupBuild utility for generating payloads for Ethereum contracts:
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 --helpLet's say you want to verify that encoded update message is indeed correct. You need to do following:
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.
source: 0xae3d2870312437cdcc0a887bf532343b6595a7ed97d6aa71efe58b677817393a
destination: 0x3681A3e25F5652389B8f52504D517E96352830C3
payload: 0x038e01fbf136ca97627ca241db9eff1dfe3f2195f6256cbaeaa34acfed715fc60301e088575410ca636c2eb350For 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
