There are lots of blockchains and scalability solutions, and it is hard to decide which one to use when we are building a product, especially when it means committing with the assets or data. Ideally, we don’t want to commit at all. We want to freely move our assets and data between the blockchains, or even better — run our product on several blockchains at the same time and leverage each of them. For example, we can leverage performance on one blockchain while leveraging community and ecosystem on another blockchain.
At NEAR, we do not want Ethereum developers to choose between NEAR and Ethereum and commit to only one. We want them to have the same asset on both blockchains and even have apps that seamlessly communicate across the boundary. So we built a bridge, called Rainbow Bridge, to connect the Ethereum and NEAR blockchains, and we created the lowest possible trust level one can have for an interoperability solution — you only need to trust what it connects, the NEAR and Ethereum blockchains, and you don’t need to trust the bridge itself. There is no authority outside Ethereum miners and NEAR validators.
Specifically, to trust the bridge, you need to:
Notice, how in addition to smart contracts that implement light clients, we have two services, called relays, that regularly send headers to the light clients. Eth2NearRelay sends every single header to the EthOnNearClient contract, while Near2EthRelay sends one header every 4 hours to the NearOnEthClient contract. For a given pair of EthOnNearClient and NearOnEthClient contracts, there could be several pairs of Eth2NearRelay and Near2EthRelay services. Each bridge maintainer can run its pair of services. These pairs of services can either compete with each other — they would try submitting the same blocks simultaneously, and only one would succeed each time, or back up each other — some services would only submit the blocks if others did not submit blocks in time. Our current implementation of the services runs in the first mode.
(Caption: One EthOnNearClient can have multiple provers talking to it replicated across multiple shards, while NearOnEthClient will have a single copy of each kind of the prover talking to it).
Similarly, RainbowLib will be able to perform transfers of non-ERC20 tokens and do contract calls.
- Trust that Ethereum blocks are final after X confirmations. Currently, bridge implementation decides X for the app developer, but soon app developers will be able to determine X for themselves. It can be 25 if you are a typical app developer or 500 if you are super cautious;
- Trust that at no time, 2/3 of the validators stake on NEAR blockchain is dishonest. Not only the bridge, but also all other applications on NEAR operate under this assumption;
- Until EIP665 is accepted, you will need to trust that it is not possible to exponentially increase the minimum gas price of Ethereum blocks by more than 2x with every block for more than 4 hours. Assuming the base gas price to be 40gwei and 14bps, you can easily calculate that attempting to increase gas price by 2x will very quickly cause it to exceed any reasonable limits way before the end of 4 hours. We will explain more on where this restriction is coming from below.
- For ETH->NEAR interactions, the latency is the speed of producing X Ethereum blocks, which is about 6 minutes for 25 blocks;
- For NEAR->ETH interactions, the latency is 4 hours, and it will be about 14 seconds once EIP665 is accepted.
- Inclusion of a transaction in a block;
- Execution of a transaction with a specific result;
- The state of the contract.
- we can bridge fungible tokens, non-fungible tokens, or any kind of asset;
- we can write Ethereum contracts that use the state of a contract or validator from NEAR;
- we can do cross-contract calls across the bridge.
Design
Anton Bukov developed a large portion of the Rainbow Bridge design during his work in NEAR. He is now the CTO of 1inch exchange, but he still guides the high-level design of the bridge. The core idea behind the bridge is that it implements two light clients:- An Ethereum light client implemented in Rust as a NEAR contract
- A NEAR light client implemented in Solidity as an Ethereum contract
Light clients
Here is the simplified diagram of the light clients operating in the bridge:EthOnNearClient
EthOnNearClient as we already said is an implementation of the Ethereum light client in Rust as a NEAR contract. It accepts Ethereum headers and maintains the canonical chain, it assumes that blocks that have finalized_gc_threshold confirmations cannot leave the canonical chain, and it memorizes up to hashes_gc_threshold of the blocks from the canonical chain, where hashes_gc_threshold>=finalized_gc_threshold. By the default finalized_gc_threshold=46k which roughly corresponds to 7 days worth of headers. This is done so that the state of the EthOnNearClient does not grow endlessly. Remember that in NEAR one is required to have a certain amount of locked tokens to be able to use the state (more info), which would require a very large ever-growing number of locked tokens if we were storing the hash of every single Ethereum canonical header in a single contract. Therefore we store a limited number of hashes of Ethereum headers. As a consequence the bridge can only be used to prove events that happened within this time horizon. So if you ever start an ERC20 transfer from Ethereum to NEAR, please make sure to finish it within 7 days if it gets interrupted in the middle. Another important nuance of EthOnNearClient is how it verifies Ethereum headers. It wouldn’t be possible to verify Ethereum PoW directly inside the contract since it would require storing the Ethereum DAG file, which would require prohibitive memory usage. Fortunately, every Ethereum block uses only a subset of the elements from the DAG file, and there is only one DAG file per Ethereum epoch. Moreover, DAG files can be precomputed for future epochs in advance. We precompute DAG files for about 4 years in advance and merkelize each of them. EthOnNearClient contract then memorizes the merkle roots of the DAG files for the next 4 years upon initialization. EthOnNearClient then only needs to receive the Ethereum header, the DAG elements, and the merkle proofs of these elements, which allows it to verify PoW without having the entire DAG file in memory. We took this approach from the EOS bridge used by Kyber network, and we even reuse some of their code. Fortunately, the Ethereum header’s verification uses only 1/3 of the max transaction gas limit and 1/10 of the block gas limit.NearOnEthClient
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.Provers
What would make clients more useful is the ability to prove that something happened on a specific blockchain. For that, we implement EthOnNearProver NEAR contract in Rust and NearOnEthProver Ethereum contract in Solidity. EthOnNearProver can verify Ethereum events, while the NearOnEthProver contract can verify NEAR contract execution results. Both EthOnNearProver and NearOnEthProver contracts are implemented separately from EthOnNearClient and NearOnEthClient for multiple reasons:- Separation of concerns — clients are responsible for keeping track of the most recent blockchain header, while provers are responsible for verifying specific cryptographic information;
- Scalability — since NEAR is a sharded blockchain, if a certain instance of the bridge becomes extremely popular, users will be able to scale the load by deploying multiple copies of a given prover;
- Specificity — in addition to the provers described above, one can implement other kinds of provers. One could verify a contract’s state, the specific content of a header, or the inclusion of a transaction. Additionally, different implementations of provers could use different optimizations.
ERC20 use case
The provers enable us to build a set of contracts that allow asset transfer or cross-chain communication. However, these contracts will be very different depending on what exactly we want to interoperate across the bridge. For example, ERC20 requires an entirely separate set of contracts from ERC721 or native token transfer. Currently, the Rainbow Bridge has out-of-the-box support for generic ERC20 token transfers, and we will add more use cases in the future. In this post, we focus only on the ERC20 use case. Suppose there is an existing ERC20 token on Ethereum, e.g., DAI. For us to move it across the bridge, we need to deploy two additional contracts:- TokenLocker Ethereum contract implemented in Solidity;
- MintableFungibleToken NEAR contract implemented in Rust.
Usage flow
Users will be able to use either RainbowCLI or any app that integrates with RainbowLib, like NEAR Wallet. In rare cases, someone might choose to work with the bridge manually by calling Ethereum and NEAR contracts from their shell using low-level contract calls. Currently, we only support RainbowCLI, but we are working on RainbowLib and Wallet integrations. From the user perspective, the transfer of a token should be straightforward: they provide credentials, choose beneficiary and amount, and initiate the transfer either from RainbowCLI, Wallet, or other applications. After some time, the transfer succeeds, and the user receives visual confirmation. Behind the scenes however, RainbowLib will perform the following complex operations:- Suppose Alice wants to transfer X DAI to Bob on NEAR blockchain and she initiates the transfer from RainbowCLI/RainbowLib;
- RainbowLib first sets an allowance to transfer X DAI from Alice to TokenLocker;
- It then calls TokenLocker to grab those tokens resulting in TokenLocker emitting event “Alice locked X tokens in favor of Bob”;
- RainbowLib then waits until EthOnNearClient receives the Ethereum header than contains this event, plus 25 blocks more for confirmation (see note on Ethereum finality in opening section)
- Then RainbowLib computes the proof of this event and submits it to the MintableFungibleToken contract;
- MintableFungibleToken contract then verifies that this proof is correct by calling EthOnNearProver;
- EthOnNearProver, in turn, verifies that the header of the proof is on the canonical chain of EthOnNearClient, and it has the required number of confirmations. It also verifies the proof itself;
- MintableFungibleToken then unpacks the Ethereum event and mints X nearDAI for Bob, finishing the transfer.
Hard forks
We want existing Rainbow Bridges not to break when NEAR or Ethereum protocol change is happening. And we want to be able to upgrade bridge contracts when serious performance upgrades become available, e.g., if EIP665 is accepted. However, we don’t want it to become less trustless or decentralized by introducing a centralized way of initiating the upgrade. There is a large variety of proxy patterns developed by the Ethereum community that allow contract updates. However, we think the safest upgrade pattern is when the upgrade decision is delegated to the users themselves. An equivalent of this user-controlled upgrade for the bridge is the following:- Suppose the user is using the bridge to transfer DAI between Ethereum and NEAR. Suppose they have X DAI locked in TokenLocker and X nearDAI available on the NEAR side;
- Suppose one day they receive an announcement that NEAR or Ethereum are making a protocol change and they need to migrate to bridge V2 within a week;
- They transfer nearDAI back to DAI using the old bridge, then transfer DAI into nearDAI V2 using bridge V2.
- Unlike regular contracts, the bridge is a more complex system that requires maintenance — someone needs to run the relays constantly, otherwise the light clients will go out of sync with the blockchains and become useless. That means we cannot ask users to manually migrate from bridge V1 to bridge V2 at their convenience. If we shut down the relays of the V1 bridge before literally everyone migrates their assets out, their assets might be permanently locked without the ability to move to V2. If we run relays for too long and wait until every user migrates we will be spending gas on the fees daily, approximately 40 USD per day at 40gwei price;
- Applications that integrate with our bridge would need to be aware of the migration as they would need to switch from an old MintableFungibleToken contract to a new MintableFungibleToken contract. Some of them might need to perform this migration themselves or guide the user through it with the UI.