Home About John Resume/CV References Writing Research

Near Rainbow Bridge

Table of Contents

Overview

NEAR Rainbow bridge was enhanced to support Ethereum 2.0 leveraging Ethereum Light Clients. This document is a review of the design.

Key differences in supporting Ethereum 2.0 (Proof of Stake) vs Proof of Work involves removing the ETHHASH logic and SPV client and potentially replacing with MMR trees per epoch and checkpoints similar to Harmony Light Client on Ethereum.

The NEAR Rainbow bridge is in this github repository and is supported by Aurora-labs.

It recently provided support for ETH 2.0 in this Pull Request (762).

It interacts lighthouse for Ethereum 2.0 Consensus and tree_hash functions as well as bls signatures.

High Level their architecture is similar to the Horizon Bridge but with some key differences, including but not limited to

Approach

Near Rainbow bridge uses a fradu proof approach.

Proving Mechanisms

NEAR to Ethereum watchdog

The watchdog runs every 10 seconds and validates blocks on NearBridge.sol challenging blocks with incorrect signatures. Note: It uses heep-prometheus for monitoring and storing block and producer information using gauges and counters.

Relayer Mechanisms

Ethereum to NEAR block propagation flow

Following is a walkthough of block propogation from Ethereum to NEAR. For a better understanding of the technical components see Appendix A.

NEAR to Ethereum block propagation flow

NEAR Light Client Documentation gives an overview of how light clients work. At a high level the light client needs to fetch at least one block per epoch i.e. every 42,200 blocks or approxmiately 12 hours. Also Having the LightClientBlockView for block B is sufficient to be able to verify any statement about state or outcomes in any block in the ancestry of B (including B itself).

The current scripts and codebase indicates that a block would be fetched every 30 seconds with a max delay of 10 seconds. It feels that this would be expensive to update Ethereum so frequently. NEAR’s bridge documentation states Sending assets from NEAR back to Ethereum currently takes a maximum of sixteen hours (due to Ethereum finality times). This seems to align with sending light client updates once per NEAR epoch. The block fetch period is configurable in the relayer.

The RPC returns the LightClientBlock for the block as far into the future from the last known hash as possible for the light client to still accept it. Specifically, it either returns the last final block of the next epoch, or the last final known block. If there’s no newer final block than the one the light client knows about, the RPC returns an empty result.

A standalone light client would bootstrap by requesting next blocks until it receives an empty result, and then periodically request the next light client block.

A smart contract-based light client that enables a bridge to NEAR on a different blockchain naturally cannot request blocks itself. Instead external oracles query the next light client block from one of the full nodes, and submit it to the light client smart contract. The smart contract-based light client performs the same checks described above, so the oracle doesn’t need to be trusted.

Block Submitters stake ETH to be allowed to submit blocks which get’s slashed if the watchdog identifies blocks with invalid signatures.

Note: Have not identified how the block submitters are rewarded for submitting blocks. Currently have only identified them locking ETH to be able to submit blocks and being slashed if they submit blocks with invalid signatures.

Light Client Functionality

Near Rainbow Bridge Ethereum Light Client Walkthrough

The following is a walkthrough of how a transaction executed on Ethereum is propogated to NEAR’s eth2-client. See Cryptographic Primitives for more information on the cryptography used. and Appendix B for verification components.

At a high level the ethereum light client contract

Near Rainbow Bridge Near Light Client Walkthrough

The following is a walkthrough of how a transaction executed on NEAR is propogated to Ethereum’s nearbridge. See nearbridge Cryptographic Primitives for more information on the cryptography used.

NearOnEthClient Overview

The following is an excerpt from a blog by near on eth-near-rainbow-bridge

NearOnEthClient is an implementation of the NEAR light client in Solidity as an Ethereum contract. Unlike EthOnNearClient it does not need to verify every single NEAR header and can skip most of them as long as it verifies at least one header per NEAR epoch, which is about 43k blocks and lasts about half a day. As a result, NearOnEthClient can memorize hashes of all submitted NEAR headers in history, so if you are making a transfer from NEAR to Ethereum and it gets interrupted you don’t need to worry and you can resume it any time, even months later. Another useful property of the NEAR light client is that every NEAR header contains a root of the merkle tree computed from all headers before it. As a result, if you have one NEAR header you can efficiently verify any event that happened in any header before it.

Another useful property of the NEAR light client is that it only accepts final blocks, and final blocks cannot leave the canonical chain in NEAR. This means that NearOnEthClient does not need to worry about forks.

However, unfortunately, NEAR uses Ed25519 to sign messages of the validators who approve the blocks, and this signature is not available as an EVM precompile. It makes verification of all signatures of a single NEAR header prohibitively expensive. So technically, we cannot verify one NEAR header within one contract call to NearOnEthClient. Therefore we adopt the optimistic approach where NearOnEthClient verifies everything in the NEAR header except the signatures. Then anyone can challenge a signature in a submitted header within a 4-hour challenge window. The challenge requires verification of a single Ed25519 signature which would cost about 500k Ethereum gas (expensive, but possible). The user submitting the NEAR header would have to post a bond in Ethereum tokens, and a successful challenge would burn half of the bond and return the other half to the challenger. The bond should be large enough to pay for the gas even if the gas price increases exponentially during the 4 hours. For instance, a 20 ETH bond would cover gas price hikes up to 20000 Gwei. This optimistic approach requires having a watchdog service that monitors submitted NEAR headers and challenges any headers with invalid signatures. For added security, independent users can run several watchdog services.

Once EIP665 is accepted, Ethereum will have the Ed25519 signature available as an EVM precompile. This will make watchdog services and the 4-hour challenge window unnecessary.

At its bare minimum, Rainbow Bridge consists of EthOnNearClient and NearOnEthClient contracts, and three services: Eth2NearRelay, Near2EthRelay, and the Watchdog. We might argue that this already constitutes a bridge since we have established a cryptographic link between two blockchains, but practically speaking it requires a large portion of additional code to make application developers even consider using the Rainbow Bridge for their applications.

The following information on sending assets from NEAR back to Ethereum is an excerpt from https://near.org/bridge/.

Sending assets from NEAR back to Ethereum currently takes a maximum of sixteen hours (due to Ethereum finality times) and costs around $60 (due to ETH gas costs and at current ETH price). These costs and speeds will improve in the near future.

Token Lockers

Token Transfer Process Flow

The NEAR Rainbow Bridge uses ERC-20 connectors which are developed in rainbow-token-connector and rainbow-bridge-client. Also see eth2near-fun-transfer.md.

Following is an overview of timing and anticipated costs

Note: This uses Ethreum ERC20 and NEAR NEP-141 initally developed for NEP-21

Generic ERC-20/NEP-141 connector for Rainbow Bridge

Specification

Ethereum’s side

contract ERC20Locker {
  constructor(bytes memory nearTokenFactory, INearProver prover) public;
  function lockToken(IERC20 token, uint256 amount, string memory accountId) public;
  function unlockToken(bytes memory proofData, uint64 proofBlockHeader) public;
}

NEAR’s side

struct BridgeTokenFactory {
    /// The account of the prover that we can use to prove
    pub prover_account: AccountId,
    /// Address of the Ethereum locker contract.
    pub locker_address: [u8; 20],
    /// Hashes of the events that were already used.
    pub used_events: UnorderedSet<Vec<u8>>,
    /// Mapping from Ethereum tokens to NEAR tokens.
    pub tokens: UnorderedMap<EvmAddress, AccountId>;
}

impl BridgeTokenFactory {
    /// Initializes the contract.
    /// `prover_account`: NEAR account of the Near Prover contract;
    /// `locker_address`: Ethereum address of the locker contract, in hex.
    #[init]
    pub fn new(prover_account: AccountId, locker_address: String) -> Self;

    /// Relays the lock event from Ethereum.
    /// Uses prover to validate that proof is correct and relies on a canonical Ethereum chain.
    /// Send `mint` action to the token that is specified in the proof.
    #[payable]
    pub fn deposit(&mut self, proof: Proof);
  
    /// A callback from BridgeToken contract deployed under this factory.
    /// Is called after tokens are burned there to create an receipt result `(amount, token_address, recipient_address)` for Ethereum to unlock the token.
    pub fn finish_withdraw(token_account: AccountId, amount: Balance, recipient: EvmAddress);
    
    /// Transfers given NEP-21 token from `predecessor_id` to factory to lock.
    /// On success, leaves a receipt result `(amount, token_address, recipient_address)`.
    #[payable]
    pub fn lock(&mut self, token: AccountId, amount: Balance, recipient: String);

    /// Relays the unlock event from Ethereum.
    /// Uses prover to validate that proof is correct and relies on a canonical Ethereum chain.
    /// Uses NEP-21 `transfer` action to move funds to `recipient` account.
    #[payable]
    pub fn unlock(&mut self, proof: Proof);

    /// Deploys BridgeToken contract for the given EVM address in hex code.
    /// The name of new NEP21 compatible contract will be <hex(evm_address)>.<current_id>.
    /// Expects ~35N attached to cover storage for BridgeToken.
    #[payable]
    pub fn deploy_bridge_token(address: String);

    /// Checks if Bridge Token has been successfully deployed with `deploy_bridge_token`.
    /// On success, returns the name of NEP21 contract associated with given address (<hex(evm_address)>.<current_id>).
    /// Otherwise, returns "token do not exists" error.
    pub fn get_bridge_token_account_id(&self, address: String) -> AccountId;
}

struct BridgeToken {
   controller: AccountId,
   token: Token, // uses https://github.com/ilblackdragon/balancer-near/tree/master/near-lib-rs
}

impl BridgeToken {
    /// Setup the Token contract with given factory/controller.
    pub fn new(controller: AccountId) -> Self;

    /// Mint tokens to given user. Only can be called by the controller.
    pub fn mint(&mut self, account_id: AccountId, amount: Balance);

    /// Withdraw tokens from this contract.
    /// Burns sender's tokens and calls controller to create event for relaying.
    pub fn withdraw(&mut self, amount: U128, recipient: String) -> Promise;
}

impl FungibleToken for BridgeToken {
   // see example https://github.com/ilblackdragon/balancer-near/blob/master/balancer-pool/src/lib.rs#L329
}

Setup new ERC-20 on NEAR

To setup token contract on NEAR side, anyone can call <bridge_token_factory>.deploy_bridge_token(<erc20>) where <erc20> is the address of the token. With this call must attach the amount of $NEAR to cover storage for (at least 30 $NEAR currently).

This will create <<hex(erc20)>.<bridge_token_factory>> NEP141-compatible contract.

Usage flow Ethereum -> NEAR

  1. User sends <erc20>.approve(<erc20locker>, <amount>) Ethereum transaction.
  2. User sends <erc20locker>.lock(<erc20>, <amount>, <destination>) Ethereum transaction. This transaction will create Locked event.
  3. Relayers will be sending Ethereum blocks to the EthClient on NEAR side.
  4. After sufficient number of confirmations on top of the mined Ethereum block that contain the lock transaction, user or relayer can call BridgeTokenFactory.deposit(proof). Proof is the extracted information from the event on Ethereum side.
  5. BridgeTokenFactory.deposit function will call EthProver and verify that proof is correct and relies on a block with sufficient number of confirmations.
  6. EthProver will return callback to BridgeTokenFactory confirming that proof is correct.
  7. BridgeTokenFactory will call <<hex(erc20)>.<bridge_token_factory>>.mint(<near_account_id>, <amount>).
  8. User can use <<hex(erc20)>.<bridge_token_factory>> token in other applications now on NEAR.

Usage flow NEAR -> Ethereum

  1. token-locker locks NEP141 tokens on NEAR side.

To deposit funds into the locker, call ft_transfer_call where msg contains Ethereum address the funds should arrive to. This will emit <token: String, amount: u128, recipient address: EthAddress> (which arrives to deposit on Ethereum side).

Accepts Unlock(token: String, sender_id: EthAddress, amount: u256, recipient: String) event from Ethereum side with a proof, verifies its correctness. If recipient contains ‘:’ will split it into <recipient, msg> and do ft_transfer_call(recipient, amount, None, msg). Otherwise will ft_transfer to recipient.

To get metadata of token to Ethereum, need to call log_metadata, which will create a result <token: String, name: String, symbol: String, decimals: u8, blockHeight: u64>.

  1. erc20-bridge-token - BridgeTokenFactory and BridgeToken Ethereum contracts.

BridgeTokenFactory creates new BridgeToken that correspond to specific token account id on NEAR side.

BridgeTokenFactory receives deposit with proof from NEAR, verify them and mint appropriate amounts on recipient addresses.

Calling withdraw will burn tokens of this user and will generate event <token: String, sender_id: EthAddress, amount: u256, recipient: String> that can be relayed to token-factory.

Caveats

Generally, this connector allows any account to call ft_transfer_call opening for potential malicious tokens to be bridged to Ethereum. The expectation here is that on Ethereum side, the token lists will handle this, as it’s the same attack model as malicious tokens on Uniswap and other DEXs.

Using Ethereum BridgeTokenFactory contract can always resolve Ethereum address of a contract back to NEAR one to check that it is indeed bridging token from NEAR and is created by this factory.

Testing

Testing Ethereum side

cd erc20-connector
yarn
yarn run test

Testing NEAR side

make res/bridge_token_factory.wasm
cargo test --all

Multi-chain support

Economics

References

Appendices

Appendix A - Ethereum to NEAR block propagation components

Appendix B - Ethereum Light Client Finality Update Verify Components

finality-update-verify is called from fn verify_bls_signature_for_finality_update to verify signatures as part of light_client updates. It relies heavily on the lighthouse codebase for it’s consensus and cryptogrphic primitives. See Cryptographic Primitives for more information.

Appendix C - Cryptographic Primitives

Following are cryptographic primitives used in the eth2-client contract and finality-update-verify. Many are from the lighthouse codebase. Specifically consensus and crypto functions.

Some common primitives

Some Primitives from Lighthouse

Some Smart Contracts deployed on Ethereum

Some Primitives from NEAR Rainbow Bridge

Nearbridge Cryptographic Primitives

Appendix D - NEAR to Ethereum block propagation costing

The following links provide the production Ethereum addresses and blockexplorer views for NearBridge.sol and the ERC20 Locker

At time of writing (Oct 26th, 2022).

Appendix F - NEAR to Ethereum block propagation components

Appendix G - NEAR Rainbow Bridge Utils

rainbow-bridge-utils provides a set of utilities for the near rainbow bridge written in javascript.

Appendix H - Token Transfer Components

Note: This uses Ethreum ERC20 and NEAR NEP-141 initally developed for NEP-21

Appendix I - NEAR Rainbow Bridge: Component Overview

The following smart contracts are deployed on NEAR and work in conjunction with eth2near bridging functionality to propogate blocks from Ethereum to NEAR.

Note here we will focus on the eth2-client for ETH 2.0 Proof of Stake Bridging however if interested in however there is also an eth-client which was used for ETH 1.0 Proof of Work Integration using rust-ethhash.

The following smart contracts are deployed on Ethereum and used for propogating blocks from NEAR to Ethereum.

Appendix J - Ethereum to NEAR Walkthrough

Following is a walkthough of a funds transfer from Ethereum to a target chain (In this example Near), complete with light client updates, block propogation and proofs to ensure the transaction validity.

Ethereum to Near Funds Transfer

Actors From the diagram above you’ll notice that there are many actors involved, below is an overview of the actors and the operations they perform.

Sample TransactionFlow

  1. Block Propogation
    1. Get the Latest Slot: The relayer loops polling Ethereum every 12 seconds to get the latest slot. It then checks if it is for a new epoch and if so (every 6 minutes) submits an execution header (with 32 blocks in it) and a light client update with the latest approved epochs and updated sync_comittee. Relayer source code for the loop is here for retrieving the latest slot is here, for submitting execution blocks is here and for sending light client updates is here.
      1. Send Block Headers (submit_execution_header): Batch transaction which submits 32 block headers to client-eth2.bridge.near for Ethereum Blocks 16493592 to 16493623. (The second slot in epoch 176,936 to the first slot in epoch 176,937). Executed every 6 minutes when the first slot of a new epoch is found.
      2. Create Light Client update proposal(add_proposal): calls bridge-validator.sputnik-dao.near to add proposal 17410 for slot 5,661,984 in epoch 176,937.
    2. Approve Proposal (act_proposal): sends a VoteApprove action for proposal 17410 from a bridge validator to the Validator DAO Contract.
      1. act_proposal in contract bridge-validator.sputnik-dao.near
      2. submit_beacon_chain_light_client_update in client-eth2.bridge.near
      3. on_proposal_callback in contract bridge-validator.sputnik-dao.near
  2. Funds Transfer Transaction Flow
    1. Lock Funds On Ethereum: Locking 10,000 USDT to send to user on NEAR.
    2. Deposit Funds on Target Chain Bridge Contract (deposit)
      1. deposit in contract factory.bridge.near
      2. verify_log_entry in contract prover.bridge.near
      3. block_hash_safe in contract client-eth2.bridge.near
      4. finish_deposit in contract factory.bridge.near : mint of 10,000 USDT.

TODO

Bridging Resources Required

Here is the storage and compuational costs per component.

Component Data Storage Notes
Ethereum 2 Client
Prover not applicable 0 bytes  
DAO Contract      

TODO Review the following data structure and elements and move into the table above commenting on any mandatory requirements and structures that can be improved.

pub struct Eth2Client {
    /// If set, only light client updates by the trusted signer will be accepted
    trusted_signer: Option<AccountId>,
    /// Mask determining all paused functions
    paused: Mask,
    /// Whether the client validates the updates.
    /// Should only be set to `false` for debugging, testing, and diagnostic purposes
    validate_updates: bool,
    /// Whether the client verifies BLS signatures.
    verify_bls_signatures: bool,
    /// We store the hashes of the blocks for the past `hashes_gc_threshold` headers.
    /// Events that happen past this threshold cannot be verified by the client.
    /// It is desirable that this number is larger than 7 days' worth of headers, which is roughly
    /// 51k Ethereum blocks. So this number should be 51k in production.
    hashes_gc_threshold: u64,
    /// Network. e.g. mainnet, kiln
    network: Network,
    /// Hashes of the finalized execution blocks mapped to their numbers. Stores up to `hashes_gc_threshold` entries.
    /// Execution block number -> execution block hash
    finalized_execution_blocks: LookupMap<u64, H256>,
    /// All unfinalized execution blocks' headers hashes mapped to their `HeaderInfo`.
    /// Execution block hash -> ExecutionHeaderInfo object
    unfinalized_headers: UnorderedMap<H256, ExecutionHeaderInfo>,
    /// `AccountId`s mapped to their number of submitted headers.
    /// Submitter account -> Num of submitted headers
    submitters: LookupMap<AccountId, u32>,
    /// Max number of unfinalized blocks allowed to be stored by one submitter account
    /// This value should be at least 32 blocks (1 epoch), but the recommended value is 1024 (32 epochs)
    max_submitted_blocks_by_account: u32,
    // The minimum balance that should be attached to register a new submitter account
    min_storage_balance_for_submitter: Balance,
    /// Light client state
    finalized_beacon_header: ExtendedBeaconBlockHeader,
    finalized_execution_header: LazyOption<ExecutionHeaderInfo>,
    current_sync_committee: LazyOption<SyncCommittee>,
    next_sync_committee: LazyOption<SyncCommittee>,
}