Ethereum Near Bridging
- date: 2023-02-04
- last updated: 2023-02-04
Overview
This document reviews the Ethereum 2.0 specifications including Light Client specifications. It does a detailed review of the NEAR Rainbow Bridge implementation and also includes references to Harmony's design to support Mountain Merkle Ranges.
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.
Ethereum 2.0 Specifications
- Beacon Chain Specification
- Extended light client protocol
- Altair Light Client -- Light Client
- Altair Light Client -- Sync Protocol
- Beacon Chain Fork Choice
Ethereum 2.0 Light Client Support
How light client implementation and verification of ETH and ETH2 can be done via smart contracts in other protocols.
For this we review three Key items
- Light Client Specifications including Extended light client protocol described by Altair Light Client -- Sync Protocol and the The Portal Network Specification
- Near Rainbow Bridge Light Client Walkthrough include eth2near-block-relay-rs, nearbridge contracts and nearprover contracts
- Prysm light-client prototype
Note: Time on Ethereum 2.0 Proof of Stake is divided into slots and epochs. One slot is 12 seconds. One epoch is 6.4 minutes, consisting of 32 slots. One block can be created for each slot.
Light Client Specification
Altair Light Client -- Sync Protocol
-
Altair Light Client -- Sync Protocol: The beacon chain is designed to be light client friendly for constrained environments to access Ethereum with reasonable safety and liveness.
Such environments include resource-constrained devices (e.g. phones for trust-minimized wallets)and metered VMs (e.g. blockchain VMs for cross-chain bridges).
This document suggests a minimal light client design for the beacon chain thatuses sync committees introduced in this beacon chain extension.
Additional documents describe how the light client sync protocol can be used:
-
Light client sync process: explains how light clients MAY obtain light client data to sync with the network.
- The light client MUST be configured out-of-band with a spec/preset (including fork schedule), with
genesis_state
(includinggenesis_time
andgenesis_validators_root
), and with a trusted block root. The trusted block SHOULD be within the weak subjectivity period, and its root SHOULD be from a finalizedCheckpoint
. - The local clock is initialized based on the configured
genesis_time
, and the current fork digest is determined to browse for and connect to relevant light client data providers. - The light client fetches a
LightClientBootstrap
object for the configured trusted block root. Thebootstrap
object is passed toinitialize_light_client_store
to obtain a localLightClientStore
. - The light client tracks the sync committee periods
finalized_period
fromstore.finalized_header.slot
,optimistic_period
fromstore.optimistic_header.slot
, andcurrent_period
fromcurrent_slot
based on the local clock.- When
finalized_period == optimistic_period
andis_next_sync_committee_known
indicatesFalse
, the light client fetches aLightClientUpdate
forfinalized_period
. Iffinalized_period == current_period
, this fetch SHOULD be scheduled at a random time beforecurrent_period
advances. - When
finalized_period + 1 < current_period
, the light client fetches aLightClientUpdate
for each sync committee period in range[finalized_period + 1, current_period)
(current period excluded) - When
finalized_period + 1 >= current_period
, the light client keeps observingLightClientFinalityUpdate
andLightClientOptimisticUpdate
. Received objects are passed toprocess_light_client_finality_update
andprocess_light_client_optimistic_update
. This ensures thatfinalized_header
andoptimistic_header
reflect the latest blocks.
- When
process_light_client_store_force_update
MAY be called based on use case dependent heuristics if light client sync appears stuck. If available, falling back to an alternative syncing mechanism to cover the affected sync committee period is preferred.
- The light client MUST be configured out-of-band with a spec/preset (including fork schedule), with
The Portal Network
-
The Portal Network: The Portal Network is an in progess effort to enable lightweight protocol access by resource constrained devices. The term "portal" is used to indicate that these networks provide a view into the protocol but are not critical to the operation of the core Ethereum protocol.
The Portal Network is comprised of multiple peer-to-peer networks which together provide the data and functionality necessary to expose the standard JSON-RPC API. These networks are specially designed to ensure that clients participating in these networks can do so with minimal expenditure of networking bandwidth, CPU, RAM, and HDD resources.
The term 'Portal Client' describes a piece of software which participates in these networks. Portal Clients typically expose the standard JSON-RPC API.
-
Motivation: The Portal Network is focused on delivering reliable, lightweight, and decentralized access to the Ethereum protocol.
-
Prior Work on the "Light Ethereum Subprotocol" (LES): The term "light client" has historically refered to a client of the existing DevP2P based LES network. This network is designed using a client/server architecture. The LES network has a total capacity dictated by the number of "servers" on the network. In order for this network to scale, the "server" capacity has to increase. This also means that at any point in time the network has some total capacity which if exceeded will cause service degradation across the network. Because of this the LES network is unreliable when operating near capacity.
-
-
Block Relay
-
Beacon State: A client has a trusted beacon state root, and it wants to access some parts of the state. Each of the access request corresponds to some leave nodes of the beacon state. The request is a content lookup on a DHT. The response is a Merkle proof.
A Distributed Hash Table (DHT) allows network participants to have retrieve data on-demand based on a content
-
Syncing Block Headers: A beacon chain client could sync committee to perform state updates. The data object LightClientSkipSyncUpdate allows a client to quickly sync to a recent header with the appropriate sync committee. Once the client establishes a recent header, it could sync to other headers by processing LightClientUpdates. These two data types allow a client to stay up-to-date with the beacon chain.
-
Sync State: A client uses SkipSyncUpdate to skip sync from a known header to a recent header. A client with a trusted but outdated header cannot use the messages in the gossip channel bc-light-client-update to update. The client's sync-committee in the stored snapshot is too old and not connected to any update messages. The client look for the appropriate SkipSyncUpdate to skip sync its header.
-
Advance Block Headers: A beacon chain client could sync committee to perform state updates. The data object LightClientSkipSyncUpdate allows a client to quickly sync to a recent header with the appropriate sync committee. Once the client establishes a recent header, it could sync to other headers by processing LightClientUpdates. These two data types allow a client to stay up-to-date with the beacon chain.
These two data types are placed into separate sub-networks. A light client make find-content requests on
skip-sync-network
at start of the sync to get a header with the sameSyncCommittee
object as in the current sync period. The client uses messages in the gossip topicbc-light-client-update
to advance its header.The gossip topics described in this document is part of a proposal for a beacon chain light client.
-
-
Transaction Proofs
-
Retrieving Beacon State: A client has a trusted beacon state root, and it wants to access some parts of the state. Each of the access request corresponds to some leave nodes of the beacon state. The request is a content lookup on a DHT. The response is a Merkle proof.
A Distributed Hash Table (DHT) allows network participants to have retrieve data on-demand based on a content key. A portal-network DHT is different than a traditional one in that each participant could selectively limit its workload by choosing a small interest radius r. A participants only process messages that are within its chosen radius boundary.
-
Wire Protocol: For a subprotocol, we need to further define the following to be able to instantiate the wire format of each message type. 1.
content_key
2.content_id
3.payload
The content of the message is a Merkle proof contains multiple leave nodes for a BeaconState.
Finally, we define the necessary encodings. A light client only knows the root of the beacon state. The client wants to know the details of some leave nodes. The client has to be able to construct the
content_key
only knowing the root and which leave nodes it wants see. Thecontent_key
is the ssz serialization of the paths. The paths represent the part of the beacon state that one wants to know about. The paths are represented by generalized indices. Note thathash_tree_root
andserialize
are the same as those defined in sync-gossip.
-
-
TODO: Review of Retrieving a transaction proof not just retrieving data on-demand
References
-
Ethereum 2.0 Specifications
- Beacon Chain Specification
- Extended light client protocol
- Altair Light Client -- Light Client
- Altair Light Client -- Sync Protocol
- Beacon Chain Fork Choice
- The Portal Network Specification: an in progess effort to enable lightweight protocol access by resource constrained devices.
-
Light Ethereum Subprotocol (LES): the protocol used by "light" clients, which only download block headers as they appear and fetch other parts of the blockchain on-demand.
-
BlockDaemon: Ethereum Altair Hard Folk: Light Clients & Sync Committees
-
Efficient algorithms for CBC Casper: Review of LMD GHOST (Latest Message Driven, Greediest Heaviest Observed Sub-Tree)
-
SSZ: Simple Serialize: Overview of Simple serialize (SSZ) is the serialization method used on the Beacon Chain. (including merkalization and multiproofs)
-
The Noise Protocol Framework: Noise is a framework for crypto protocols based on Diffie-Hellman key agreement.
-
Optimistic Sync Specification: Optimistic Sync is a stop-gap measure to allow execution nodes to sync via established methods until future Ethereum roadmap items are implemented (e.g., statelessness).
-
Consensus Light Client Server Implementation Notes: How Lodestar beacon node was tweaked to serve light clients
-
beacon chain light client design doc: notes about the design/implementation of a beacon chain light client using standard APIs and protocol features
-
A Beacon Chain Light Client Proposal: proposing a light client implementation that goes a step further than the minimum light client described in the altair consensus-spec. The proposed client aims to allow queries into the beacon state.
-
Distributed Hash Table (DHT) Overview: allows network participants to have retrieve data on-demand based on a content key.
-
(WIP) Light client p2p interface Specification: a PR to get the conversation going about a p2p approach.
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.
At a high level the ethereum light client contract- Optionally accepts client updates only from a trusted client
- Can pause functions
- Validates a sync committee exists for the curremt slot
- Validates sync committe has greater than the minimum required sync committee members
- Validates 2/3 or more of the committe members have signed the blocks
- Validates bls signatures (i.e. the bls signatures of the sync comittee for the blocks propogated)
- Stores 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. - Stores the Ethereum Network (e.g. mainnet, kiln)
- Stores Hashes of the finalized execution blocks mapped to their numbers.
- Stores All unfinalized execution blocks' headers hashes mapped to their
HeaderInfo
. - Stores
AccountId
s mapped to their number of submitted headers. - Stores 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)
- Stores minimum balance that should be attached to register a new submitter account.
- Stores finalized beacon header
- Stores finalized execution header
- Stores current_sync_committee
- Stores next_sync_committee
Ethereum to NEAR block propagation flow
- Light Clients are deployed on Near:
- init_contract: The eth2near relayer is called with an argument to initialize the eth2-client contract
- eth_client_contract: is created using a contract_wrapper
let mut eth_client_contract = EthClientContract::new(get_eth_contract_wrapper(&config));
- EthClientContract Wrapper: creates an instance of eth2-client contract with the following arguments
network
- the name of Ethereum network such asmainnet
,goerli
,kiln
, etc.finalized_execution_header
- the finalized execution header to start initialization with.finalized_beacon_header
- correspondent finalized beacon header.current_sync_committee
- sync committee correspondent for finalized block.next_sync_committee
- sync committee for the next period after period for finalized block.hashes_gs_threshold
- the maximum number of stored finalized blocks.max_submitted_block_by_account
- the maximum number of unfinalized blocks which one relay can store in the client's storage.trusted_signer
- the account address of the trusted signer which is allowed to submit light client updates.
- eth_client_contract: is created using a contract_wrapper
- init_contract: The eth2near relayer is called with an argument to initialize the eth2-client contract
- Relayer is Created:
- eth2near_relay is created using the following arguments
let mut eth2near_relay = Eth2NearRelay::init(&config, get_eth_client_contract(&config), args.enable_binary_search, args.submit_only_finalized_blocks,);
- eth2near_relay is created using the following arguments
- Relayer is Started:
- The relayer is started using
eth2near_relay.run(None);
- This executes the eth2near_relay run function
pub fn run(&mut self, max_iterations: Option<u64>)
which runs until terminated doing using the following loopwhile !self.terminate
self.wait_for_synchronization(),
: gets the sync statussleep(Duration::from_secs(12));
: waits for 12 secondsself.get_max_slot_for_submission()
: gets the maximum slot for submission from Ethereumself.get_last_eth2_slot_on_near
: gets the latest slot propogated from Ethereum to NEARif last_eth2_slot_on_near < max_slot_for_submission
: If there are slots to processself.get_execution_blocks_between(last_eth2_slot_on_near + 1, max_slot_for_submission,),
: Get the execution blocks to be processedself.submit_execution_blocks(headers, current_slot, &mut last_eth2_slot_on_near)
: submit themwere_submission_on_iter = true;
: flags that there were submissions
were_submission_on_iter |= self.send_light_client_updates_with_checks(last_eth2_slot_on_near);
: send light_client updates with checks and updates the submission flag to true if if passes. Following is some key logicself.is_enough_blocks_for_light_client_update
: Checks if there are enough blocks for a light client updateself.send_light_client_updates
callssend_light_client_update
whichif last_finalized_slot_on_eth >= last_finalized_slot_on_near + self.max_blocks_for_finalization
: checks if the gap is too big (i.e. we are at a new slot) between slot of finalized block on NEAR and ETH. If it is it sends a hand made client update (which will loop getting the new slots sync committees) otherwise it sends a regular client update (which propogates the block headers)self.send_hand_made_light_client_update(last_finalized_slot_on_near);
let include_next_sync_committee = BeaconRPCClient::get_period_for_slot (last_finalized_slot_on_near) != BeaconRPCClient::get_period_for_slot(attested_slot);
self.send_regular_light_client_update(last_finalized_slot_on_eth, last_finalized_slot_on_near,);
self.send_specific_light_client_update(light_client_update)
is called for both regular and hand made updates.self.eth_client_contract.is_known_block
: Checks if the block is already known on the Etherum Client Contract on NEARself.verify_bls_signature_for_finality_update(&light_client_update)
: Verifies the BLS signatures. This callsis_correct_finality_update
ineth2near/finality-update-verify/src/lib.rs
*self.eth_client_contract.send_light_client_update(light_client_update.clone())
: Updates the light client with the finalized blockself.beacon_rpc_client.get_block_number_for_slot(types::Slot::new(light_client_update.finality_update.header_update.beacon_header.slot.as_u64())),
: Validates Finalized block number is correct on Ethereum usng thebeacon_rpc_client
.sleep(Duration::from_secs(self.sleep_time_after_submission_secs));
: sleeps for the configured submission sleep time.
if !were_submission_on_iter {thread::sleep(Duration::from_secs(self.sleep_time_on_sync_secs));}
: if there were submissions sleep for however many seconds were configured for sync sleep time.
- The relayer is started using
Ethereum to NEAR block propagation components
-
EthClientContract Wrapper: supports eth2-client contract functions
impl EthClientContractTrait for EthClientContract
fn get_last_submitted_slot(&self) -> u64
fn is_known_block(&self, execution_block_hash: &H256) -> Result<bool, Box<dyn Error>>
fn send_light_client_update(&mut self, light_client_update: LightClientUpdate,) -> Result<FinalExecutionOutcomeView, Box<dyn Error>>
fn get_finalized_beacon_block_hash(&self) -> Result<H256, Box<dyn Error>>
fn get_finalized_beacon_block_slot(&self) -> Result<u64, Box<dyn Error>>
fn send_headers(&mut self, headers: &[BlockHeader], end_slot: u64,) -> Result<FinalExecutionOutcomeView, Box<dyn std::error::Error>>
fn get_min_deposit(&self) -> Result<Balance, Box<dyn Error>>
fn register_submitter(&self) -> Result<FinalExecutionOutcomeView, Box<dyn Error>>
fn is_submitter_registered(&self,account_id: Option<AccountId>,) -> Result<bool, Box<dyn Error>>
fn get_light_client_state(&self) -> Result<LightClientState, Box<dyn Error>>
fn get_num_of_submitted_blocks_by_account(&self) -> Result<u32, Box<dyn Error>>
fn get_max_submitted_blocks_by_account(&self) -> Result<u32, Box<dyn Error>>
-
-
High level storage overview
-
provides the
Eth2Client
public data stucturepub 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>, }
-
-
eth2-client dependencies relys heavily on the lighthouse codebase for it's consensus and cryptogrphic primitives. See Cryptographic Primitives for more information.
ethereum-types = "0.9.2"
eth-types = { path = "../eth-types" }
eth2-utility = { path = "../eth2-utility" }
tree_hash = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" }
merkle_proof = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" }
bls = { git = "https://github.com/aurora-is-near/lighthouse.git", optional = true, rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec", default-features = false, features = ["milagro"]}
admin-controlled = { path = "../admin-controlled" }
near-sdk = "4.0.0"
borsh = "0.9.3"
bitvec = "1.0.0"
-
eth2-client contract functions: provides the following functions in
impl Eth2Client
fn validate_light_client_update(&self, update: &LightClientUpdate)
fn verify_finality_branch(&self, update: &LightClientUpdate, finalized_period: u64)
fn verify_bls_signatures(&self, update: &LightClientUpdate, sync_committee_bits: BitVec<u8>, finalized_period: u64,)
fn update_finalized_header(&mut self, finalized_header: ExtendedBeaconBlockHeader)
fn commit_light_client_update(&mut self, update: LightClientUpdate)
fn gc_finalized_execution_blocks(&mut self, mut header_number: u64)
fn update_submitter(&mut self, submitter: &AccountId, value: i64)
fn is_light_client_update_allowed(&self)
-
Eth2NearRelay: has the following public structure
pub struct Eth2NearRelay { beacon_rpc_client: BeaconRPCClient, eth1_rpc_client: Eth1RPCClient, near_rpc_client: NearRPCClient, eth_client_contract: Box<dyn EthClientContractTrait>, headers_batch_size: u64, ethereum_network: String, interval_between_light_client_updates_submission_in_epochs: u64, max_blocks_for_finalization: u64, near_network_name: String, last_slot_searcher: LastSlotSearcher, terminate: bool, submit_only_finalized_blocks: bool, next_light_client_update: Option<LightClientUpdate>, sleep_time_on_sync_secs: u64, sleep_time_after_submission_secs: u64, max_submitted_blocks_by_account: u32, }
-
Eth2NearRelay: Implements the following functions
fn get_max_slot_for_submission(&self) -> Result<u64, Box<dyn Error>>
fn get_last_eth2_slot_on_near(&mut self, max_slot: u64) -> Result<u64, Box<dyn Error>>
fn get_last_finalized_slot_on_near(&self) -> Result<u64, Box<dyn Error>>
fn get_last_finalized_slot_on_eth(&self) -> Result<u64, Box<dyn Error>>
-
pub fn run(&mut self, max_iterations: Option<u64>)
fn wait_for_synchronization(&self) -> Result<(), Box<dyn Error>>
fn get_light_client_update_from_file(config: &Config, beacon_rpc_client: &BeaconRPCClient,) -> Result<Option<LightClientUpdate>, Box<dyn Error>>
fn set_terminate(&mut self, iter_id: u64, max_iterations: Option<u64>)
fn get_execution_blocks_between(&self, start_slot: u64, last_eth2_slot_on_eth_chain: u64,) -> Result<(Vec<BlockHeader>, u64), Box<dyn Error>>
fn submit_execution_blocks(&mut self, headers: Vec<BlockHeader>, current_slot: u64,last_eth2_slot_on_near: &mut u64,)
fn verify_bls_signature_for_finality_update(&mut self, light_client_update: &LightClientUpdate,) -> Result<bool, Box<dyn Error>>
fn get_execution_block_by_slot(&self, slot: u64) -> Result<BlockHeader, Box<dyn Error>>
-
Eth2NearRelay: has a second implementation of functions for submitting light client updates
fn is_enough_blocks_for_light_client_update(&self, last_submitted_slot: u64,last_finalized_slot_on_near: u64, last_finalized_slot_on_eth: u64,) -> bool
fn is_shot_run_mode(&self) -> bool
fn send_light_client_updates_with_checks(&mut self, last_submitted_slot: u64) -> bool
fn send_light_client_updates(&mut self, last_submitted_slot: u64, last_finalized_slot_on_near: u64, last_finalized_slot_on_eth: u64,)
fn send_light_client_update_from_file(&mut self, last_submitted_slot: u64)
fn send_regular_light_client_update(&mut self, last_finalized_slot_on_eth: u64,last_finalized_slot_on_near: u64,)
fn get_attested_slot(&mut self, last_finalized_slot_on_near: u64,) -> Result<u64, Box<dyn Error>>
fn send_hand_made_light_client_update(&mut self, last_finalized_slot_on_near: u64)
fn send_specific_light_client_update(&mut self, light_client_update: LightClientUpdate)
-
eth2-contract-init includes (but not limited to) the following additional components
- init_contract.rs: Verifies light client snapshot and initializes the Ethereum Light Contract on Near.
pub fn verify_light_client_snapshot(block_root: String, light_client_snapshot: &LightClientSnapshotWithProof,) -> bool
: Verifies the light client by checking the snapshot format getting the current consensus branch and verifying it via a merkle proof.pub fn init_contract(config: &Config, eth_client_contract: &mut EthClientContract, mut init_block_root: String,) -> Result<(), Box<dyn std::error::Error>>
: Initializes the Ethereum Light Client Contract on Near.
- init_contract.rs: Verifies light client snapshot and initializes the Ethereum Light Contract on Near.
-
eth_rpc_client includes (but not limited to) the following additional components
-
eth1_rpc_client.rs: Is used to get block headers and check sync status. It has the following functions
pub fn new(endpoint_url: &str) -> Self
pub fn get_block_header_by_number(&self, number: u64) -> Result<BlockHeader, Box<dyn Error>>
pub fn is_syncing(&self) -> Result<bool, Box<dyn Error>>
-
execution_block_proof.rs:
ExecutionBlockProof
contains ablock_hash
(execution block) and a proof of its inclusion in theBeaconBlockBody
tree hash. Theblock_hash
is the 12th field in execution_payload, which is the 9th field inBeaconBlockBody
. The first 4 elements in proof correspondent to the proof of inclusion ofblock_hash
in Merkle tree built forExecutionPayload
. The last 4 elements of the proof ofExecutionPayload
in the Merkle tree are built on high-levelBeaconBlockBody
fields. The proof starts from the leaf. It has the following structure and functionspub struct ExecutionBlockProof {block_hash: H256, proof: [H256; Self::PROOF_SIZE],}
pub fn construct_from_raw_data(block_hash: &H256, proof: &[H256; Self::PROOF_SIZE]) -> Self
pub fn construct_from_beacon_block_body(beacon_block_body: &BeaconBlockBody<MainnetEthSpec>,) -> Result<Self, Box<dyn Error>>
pub fn get_proof(&self) -> [H256; Self::PROOF_SIZE]
pub fn get_execution_block_hash(&self) -> H256
pub fn verify_proof_for_hash(&self, beacon_block_body_hash: &H256,) -> Result<bool, IncorrectBranchLength>
fn merkle_root_from_branch(leaf: H256, branch: &[H256], depth: usize, index: usize,) -> Result<H256, IncorrectBranchLength>
-
beacon_block_body_merkle_tree.rs: implements merkle trees for the Beacon and the ExecutionPayload
BeaconBlockBodyMerkleTree
is built on theBeaconBlockBody
data structure, where the leaves of the Merkle Tree are the hashes of the high-level fields of theBeaconBlockBody
. The hashes of each element are produced by usingssz
serialization.ExecutionPayloadMerkleTree
is a built on theExecutionPayload
data structure, where the leaves of the Merkle Tree are the hashes of the high-level fields of theExecutionPayload
. The hashes of each element are produced by usingssz
serialization.ExecutionPayload
is one of the field in BeaconBlockBody. The hash of the root ofExecutionPlayloadMerkleTree
is the 9th leaf in BeaconBlockBody Merkle Tree.
-
beacon_rpc_client.rs: allows getting beacon block body, beacon block header and light client updates using Beacon RPC API. It has the following functions
pub fn new(endpoint_url: &str, timeout_seconds: u64, timeout_state_seconds: u64) -> Self
: CreatesBeaconRPCClient
for the given BeaconAPIendpoint_url
pub fn get_beacon_block_body_for_block_id(&self, block_id: &str,) -> Result<BeaconBlockBody<MainnetEthSpec>, Box<dyn Error>>
: ReturnsBeaconBlockBody
struct for the givenblock_id
. It uses the following argumentsblock_id
- Block identifier. Can be one of:"head" (canonical head in node's view),"genesis", "finalized", <slot>, <hex encoded blockRoot with 0x prefix>
(see beacon-APIs/#/Beacon/getBlockV2).
pub fn get_beacon_block_header_for_block_id(&self, block_id: &str,) -> Result<types::BeaconBlockHeader, Box<dyn Error>>
: ReturnsBeaconBlockHeader
struct for the givenblock_id
. It uses the following argumentsblock_id
- Block identifier. Can be one of:"head" (canonical head in node's view),"genesis", "finalized", <slot>, <hex encoded blockRoot with 0x prefix>
(see beacon-APIs/#/Beacon/getBlockV2).
pub fn get_light_client_update(&self, period: u64,) -> Result<LightClientUpdate, Box<dyn Error>>
: ReturnsLightClientUpdate
struct for the givenperiod
. It uses the following argumentsperiod
- period id for whichLightClientUpdate
is fetched. On Mainnet, one period consists of 256 epochs, and one epoch consists of 32 slots
pub fn get_bootstrap(&self, block_root: String,) -> Result<LightClientSnapshotWithProof, Box<dyn Error>>
: Fetch a bootstrapping state with a proof to a trusted block root. The trusted block root should be fetched with similar means to a weak subjectivity checkpoint. Only block roots for checkpoints are guaranteed to be available.pub fn get_checkpoint_root(&self) -> Result<String, Box<dyn Error>>
pub fn get_last_finalized_slot_number(&self) -> Result<types::Slot, Box<dyn Error>>
: Return the last finalized slot in the Beacon chainpub fn get_last_slot_number(&self) -> Result<types::Slot, Box<dyn Error>>
: Return the last slot in the Beacon chainpub fn get_slot_by_beacon_block_root(&self, beacon_block_hash: H256,) -> Result<u64, Box<dyn Error>>
pub fn get_block_number_for_slot(&self, slot: types::Slot) -> Result<u64, Box<dyn Error>>
pub fn get_finality_light_client_update(&self) -> Result<LightClientUpdate, Box<dyn Error>>
pub fn get_finality_light_client_update_with_sync_commity_update(&self,) -> Result<LightClientUpdate, Box<dyn Error>>
pub fn get_beacon_state(&self, state_id: &str,) -> Result<BeaconState<MainnetEthSpec>, Box<dyn Error>>
pub fn is_syncing(&self) -> Result<bool, Box<dyn Error>>
fn get_json_from_client(client: &Client, url: &str) -> Result<String, Box<dyn Error>>
fn get_json_from_raw_request(&self, url: &str) -> Result<String, Box<dyn Error>>
fn get_body_json_from_rpc_result(block_json_str: &str,) -> Result<std::string::String, Box<dyn Error>>
fn get_header_json_from_rpc_result(json_str: &str,) -> Result<std::string::String, Box<dyn Error>>
fn get_attested_header_from_light_client_update_json_str(light_client_update_json_str: &str,) -> Result<BeaconBlockHeader, Box<dyn Error>>
fn get_sync_aggregate_from_light_client_update_json_str(light_client_update_json_str: &str,) -> Result<SyncAggregate, Box<dyn Error>>
fn get_signature_slot(&self, light_client_update_json_str: &str,) -> Result<Slot, Box<dyn Error>>
:signature_slot
is not provided in the current API. The slot is brute-forced untilSyncAggregate
inBeconBlockBody
in the current slot is equal toSyncAggregate
inLightClientUpdate
fn get_finality_update_from_light_client_update_json_str(&self, light_client_update_json_str: &str,) -> Result<FinalizedHeaderUpdate, Box<dyn Error>>
fn get_sync_committee_update_from_light_client_update_json_str(light_client_update_json_str: &str,) -> Result<SyncCommitteeUpdate, Box<dyn Error>>
pub fn get_period_for_slot(slot: u64) -> u64
pub fn get_non_empty_beacon_block_header(&self, start_slot: u64,) -> Result<types::BeaconBlockHeader, Box<dyn Error>>
fn check_block_found_for_slot(&self, json_str: &str) -> Result<(), Box<dyn Error>>
-
hand_made_finality_light_client_update.rs: Has two implementations
- The first implementation which calls functions in the second
pub fn get_finality_light_client_update(beacon_rpc_client: &BeaconRPCClient, attested_slot: u64, include_next_sync_committee: bool,) -> Result<LightClientUpdate, Box<dyn Error>>
pub fn get_finality_light_client_update_from_file(beacon_rpc_client: &BeaconRPCClient, file_name: &str,) -> Result<LightClientUpdate, Box<dyn Error>>
pub fn get_light_client_update_from_file_with_next_sync_committee(beacon_rpc_client: &BeaconRPCClient, attested_state_file_name: &str, finality_state_file_name: &str,) -> Result<LightClientUpdate, Box<dyn Error>>
- The second implementation
fn get_attested_slot_with_enough_sync_committee_bits_sum(beacon_rpc_client: &BeaconRPCClient,attested_slot: u64,) -> Result<(u64, u64), Box<dyn Error>>
fn get_state_from_file(file_name: &str) -> Result<BeaconState<MainnetEthSpec>, Box<dyn Error>>
fn get_finality_light_client_update_for_state(beacon_rpc_client: &BeaconRPCClient,attested_slot: u64, signature_slot: u64, beacon_state: BeaconState<MainnetEthSpec>, finality_beacon_state: Option<BeaconState<MainnetEthSpec>>,) -> Result<LightClientUpdate, Box<dyn Error>>
fn get_next_sync_committee(beacon_state: &BeaconState<MainnetEthSpec>,) -> Result<SyncCommitteeUpdate, Box<dyn Error>>
fn from_lighthouse_beacon_header(beacon_header: &BeaconBlockHeader,) -> eth_types::eth2::BeaconBlockHeader
fn get_sync_committee_bits(sync_committee_signature: &types::SyncAggregate<MainnetEthSpec>,) -> Result<[u8; 64], Box<dyn Error>>
fn get_finality_branch(beacon_state: &BeaconState<MainnetEthSpec>,) -> Result<Vec<H256>, Box<dyn Error>>
fn get_finality_update(finality_header: &BeaconBlockHeader, beacon_state: &BeaconState<MainnetEthSpec>, finalized_block_body: &BeaconBlockBody<MainnetEthSpec>,) -> Result<FinalizedHeaderUpdate, Box<dyn Error>>
- The first implementation which calls functions in the second
-
light_client_snapshot_with_proof.rs: contains the structure for
LightClientSnapshotWithProof
pub struct LightClientSnapshotWithProof { pub beacon_header: BeaconBlockHeader, pub current_sync_committee: SyncCommittee, pub current_sync_committee_branch: Vec<H256>, }
-
-
eth2near-block-relay-rs includes (but not limited to) the following additional components
- config.rs:
- last_slot_searcher.rs: Implementation of functions for searching last slot on NEAR contract. Supports both binary and linear searches.
pub fn get_last_slot(&mut self, last_eth_slot: u64, beacon_rpc_client: &BeaconRPCClient, eth_client_contract: &Box<dyn EthClientContractTrait>,) -> Result<u64, Box<dyn Error>>
n binary_slot_search(&self, slot: u64, finalized_slot: u64, last_eth_slot: u64, beacon_rpc_client: &BeaconRPCClient, eth_client_contract: &Box<dyn EthClientContractTrait>,) -> Result<u64, Box<dyn Error>>
: Search for the slot before the first unknown slot on NEAR. Assumptions: (1) start_slot is known on NEAR (2) last_slot is unknown on NEAR. Return error in case of problem with network connection.fn binsearch_slot_forward(&self, slot: u64, max_slot: u64, beacon_rpc_client: &BeaconRPCClient,eth_client_contract: &Box<dyn EthClientContractTrait>,) -> Result<u64, Box<dyn Error>> {
: Search for the slot before the first unknown slot on NEAR. Assumptions: (1) start_slot is known on NEAR (2) last_slot is unknown on NEAR. Return error in case of problem with network connection.fn binsearch_slot_range(&self, start_slot: u64, last_slot: u64, beacon_rpc_client: &BeaconRPCClient, eth_client_contract: &Box<dyn EthClientContractTrait>,) -> Result<u64, Box<dyn Error>>
: Search for the slot before the first unknown slot on NEAR. Assumptions: (1) start_slot is known on NEAR (2) last_slot is unknown on NEAR. Return error in case of problem with network connection.fn linear_slot_search(&self, slot: u64, finalized_slot: u64, last_eth_slot: u64, beacon_rpc_client: &BeaconRPCClient, eth_client_contract: &Box<dyn EthClientContractTrait>,) -> Result<u64, Box<dyn Error>>
: Returns the last slot known with block known on NEAR.Slot
-- expected last known slot.finalized_slot
-- last finalized slot on NEAR, assume as known slot.last_eth_slot
-- head slot on Eth.fn linear_search_forward(&self, slot: u64, max_slot: u64, beacon_rpc_client: &BeaconRPCClient,eth_client_contract: &Box<dyn EthClientContractTrait>,) -> Result<u64, Box<dyn Error>>
: Returns the slot before the first unknown block on NEAR. The search range is [slot .. max_slot). If there is no unknown block in this range max_slot - 1 will be returned. Assumptions: (1) block for slot is submitted to NEAR. (2) block for max_slot is not submitted to NEAR.fn linear_search_backward(&self, start_slot: u64, last_slot: u64, beacon_rpc_client: &BeaconRPCClient, eth_client_contract: &Box<dyn EthClientContractTrait>,) -> Result<u64, Box<dyn Error>>
: Returns the slot before the first unknown block on NEAR. The search range is [last_slot .. start_slot). If no such block are found the start_slot will be returned. Assumptions: (1) block for start_slot is submitted to NEAR (2) block for last_slot + 1 is not submitted to NEAR.fn find_left_non_error_slot(&self, left_slot: u64, right_slot: u64, step: i8, beacon_rpc_client: &BeaconRPCClient, eth_client_contract: &Box<dyn EthClientContractTrait>,) -> (u64, bool)
: Find the leftmost non-empty slot. Search range: [left_slot, right_slot). Returns pair: (1) slot_id and (2) is this block already known on Eth client on NEAR. Assume that right_slot is non-empty and it's block were submitted to NEAR, so if non correspondent block is found we return (right_slot, false).fn block_known_on_near( &self, slot: u64, beacon_rpc_client: &BeaconRPCClient,eth_client_contract: &Box<dyn EthClientContractTrait>,) -> Result<bool, Box<dyn Error>>
: Check if the block for current slot in Eth2 already were submitted to NEAR. Returns Error if slot doesn't contain any block.
- main.rs: Command Line Argument Parser used to run the Ethereum to Near Block Relay. It contains the following functions
fn get_eth_contract_wrapper(config: &Config) -> Box<dyn ContractWrapper>
fn get_dao_contract_wrapper(config: &Config) -> Box<dyn ContractWrapper>
fn get_eth_client_contract(config: &Config) -> Box<dyn EthClientContractTrait>
fn init_log(args: &Arguments, config: &Config)
fn main() -> Result<(), Box<dyn std::error::Error>>
- near_rpc_client.rs
pub fn new(endpoint_url: &str) -> Self
pub fn check_account_exists(&self, account_id: &str) -> Result<bool, Box<dyn Error>>
pub fn is_syncing(&self) -> Result<bool, Box<dyn Error>>
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.
-
Dependencies in Cargo.toml
eth-types = { path ="../../contracts/near/eth-types/", features = ["eip1559"]}
bls = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" }
eth2-utility = { path ="../../contracts/near/eth2-utility"}
tree_hash = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" }
types = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" }
bitvec = "1.0.0"
-
Functions in lib.rs
fn h256_to_hash256(hash: H256) -> Hash256
fn tree_hash_h256_to_eth_type_h256(hash: tree_hash::Hash256) -> eth_types::H256
fn to_lighthouse_beacon_block_header(bridge_beacon_block_header: &BeaconBlockHeader,) -> types::BeaconBlockHeader
pub fn is_correct_finality_update(ethereum_network: &str, light_client_update: &LightClientUpdate, sync_committee: SyncCommittee,) -> Result<bool, Box<dyn Error>>
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
- bitvec: Addresses memory by bits, for packed collections and bitfields
- eth2_serde_utils: Serialization and deserialization utilities useful for JSON representations of Ethereum 2.0 types.
- eth2_hashing: Hashing primitives used in Ethereum 2.0
- blst: The blst crate provides a rust interface to the blst BLS12-381 signature library.
- tree_hash: Efficient Merkle-hashing as used in Ethereum 2.0
- eth2_ssz_types: Provides types with unique properties required for SSZ serialization and Merklization.
Some Primitives from Lighthouse
- bls: Boneh–Lynn–Shacham digital signature support
- impls: Implementations
- blst
- fake_crypto
- milagro: support for Apache Milagro
- functionality
- impls: Implementations
- merkle_proof
- tree_hash
- types: Implements Ethereum 2.0 types including but not limited to
- attestation
- beacon_block
- beacon_committee
- beacon_state
- builder_bid
- chain_spec
- checkpoint
- contribution_and_proof: A Validators aggregate sync committee contribution and selection proof.
- deposit: A deposit to potentially become a beacon chain validator.
- enr_fork_id: Specifies a fork which allows nodes to identify each other on the network. This fork is used in a nodes local ENR.
- eth_spec: Ethereum Foundation specifications.
- execution_block_hash
- execution_payload
- fork: Specifies a fork of the
BeaconChain
, to prevent replay attacks. - free_attestation: Note: this object does not actually exist in the spec. We use it for managing attestations that have not been aggregated.
- payload
- signed_aggregate_and_proof: A Validators signed aggregate proof to publish on the
beacon_aggregate_and_proof
gossipsub topic. - signed_beacon_block: A
BeaconBlock
and a signature from its proposer. - slot_data: A trait providing a
Slot
getter for messages that are related to a single slot. Useful in making parts of attestation and sync committee processing generic. - slot_epoch: The
Slot
andEpoch
types are defined as new types over u64 to enforce type-safety between the two types. Note: Time on Ethereum 2.0 Proof of Stake is divided into slots and epochs. One slot is 12 seconds. One epoch is 6.4 minutes, consisting of 32 slots. One block can be created for each slot. - sync_aggregate: Create a
SyncAggregate
from a slice ofSyncCommitteeContribution
s. Equivalent toprocess_sync_committee_contributions
from the spec. - sync_committee
- tree_hash_impls: contains custom implementations of
CachedTreeHash
for ETH2-specific types. It makes some assumptions about the layouts and update patterns of other structs in this crate, and should be updated carefully whenever those structs are changed. - validator: Information about a
BeaconChain
validator.
Some Smart Contracts deployed on Ethereum
- nearprover
- nearbridge
- NearDecoder.sol: handles decoing of Public Keys, Signatures, BlockProducers and LightClientBlocks using
Borsh.sol
- Utils.sol: handles reading and writing to memory, memoryToBytes and has functions such as
keccak256Raw
andsha256Raw
- Borsh.sol: Borsh: Binary Object Representation Serializer for Hashing. It is meant to be used in security-critical projects as it prioritizes consistency, safety, speed; and comes with a strict specification.
- Ed25519.sol: Ed25519 high-speed high-security signatures
- NearDecoder.sol: handles decoing of Public Keys, Signatures, BlockProducers and LightClientBlocks using
Some Primitives from NEAR Rainbow Bridge
- eth-types: utilities to serialize and encode eth2 types using borsh and rlp.
- eth2-utility: Utility functions used for Ethereum 2.0 Consensus. Functions include
fn from_str(input: &str) -> Result<Network, Self::Err>
pub fn new(network: &Network) -> Self
pub fn compute_fork_version(&self, epoch: Epoch) -> Option<ForkVersion>
pub fn compute_fork_version_by_slot(&self, slot: Slot) -> Option<ForkVersion>
pub const fn compute_epoch_at_slot(slot: Slot) -> u64
pub const fn compute_sync_committee_period(slot: Slot) -> u64
pub const fn floorlog2(x: u32) -> u32
: Compute floor of log2 of a u32.pub const fn get_subtree_index(generalized_index: u32) -> u32
pub fn compute_domain(domain_constant: DomainType, fork_version: ForkVersion, genesis_validators_root: H256,) -> H256
pub fn compute_signing_root(object_root: H256, domain: H256) -> H256
pub fn get_participant_pubkeys(public_keys: &[PublicKeyBytes], sync_committee_bits: &BitVec<u8, Lsb0>,) -> Vec<PublicKeyBytes>
pub fn convert_branch(branch: &[H256]) -> Vec<ethereum_types::H256>
pub fn validate_beacon_block_header_update(header_update: &HeaderUpdate) -> bool
pub fn calculate_min_storage_balance_for_submitter(max_submitted_blocks_by_account: u32,) -> Balance
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 OverviewThe 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.
NEAR to Ethereum block propagation costing
The following links provide the production Ethereum addresses and blockexplorer views for NearBridge.sol and the ERC20 Locker
- Ethereum Mainnet Bridge addresses and parameters
- NearBridge.sol on Ethereum Block Explorer
- NEAR ERC20Locker on Ethereum Block Explorer
At time of writing (Oct 26th, 2022).
- NEAR Light Client Blocks are propogated every
4 hours
- Sample Transaction fee
0.061600109576901025 Ether ($96.56)
- Daily Transaction fees cost approximately
$600
- Note: Infrastructure costs for running relayer, watchdog, etc are not included.
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 Clients are deployed on Ethereum via the CLI using eth-contracts.js
- init-eth-ed25519: Deploys
Ed25519.sol
see more information under nearbridge Cryptographic Primitives - init-eth-client: Deploys
NearBridge.sol
see more information under NEAR to Ethereum block propagation components. It takes the following argumentsethEd25519Address
: The address of the ECDSA signature checker using Ed25519 curve (see here)lockEthAmount
: The amount thatBLOCK_PRODUCERS
need to deposit (in wei)to be able to provide blocks. This amount will be slashed if the block is challenged and proven not to have a valid signature. Default value is 100000000000000000000 WEI = 100 ETH.lockDuration
: 30 secondsreplaceDuration
: 60 seconds it is passed in nanoseconds, because it is a difference between NEAR timestamps.ethAdminAddress
: Bridge Administrator Address0
: Indicates nothing is pausedUNPAUSE_ALL
- init-eth-prover: Deploys
NearProver.sol
see more information under NEAR to Ethereum block propagation components. It takes the following argumentsethClientAddress
: Interface toNearBridge.sol
ethAdminAddress
: Administrator address0
: paused indicator defaults toUNPAUSE_ALL = 0
- init-eth-ed25519: Deploys
-
-
Relayer is started using the following command
cli/index.js start near2eth-relay \ --eth-node-url http://127.0.0.1:8545/ \ --eth-master-sk 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 \ --near-node-url https://rpc.testnet.near.org/ \ --near-network-id testnet \ --eth-client-address 0xe7f1725e7734ce288f8367e1bb143e90bb3f0512 \ --eth-use-eip-1559 true \ --near2eth-relay-max-delay 10 \ --near2eth-relay-block-select-duration 30 \ --near2eth-relay-after-submit-delay-ms 1000 \ --log-verbose true \ --daemon false
-
-
- Loops
while (true)
- Get the bridge state (including
currentHeight
,nextTimestamp
,nextValidAt
,numBlockProducers
) - Get the
currentBlockHash
the hash of the current untrursted block based onlastValidAt
- Gets the
lastBlock
by calling the NEAR rpcnext_light_client_block
using the hash of last untrusted blockbs58.encode(currentBlockHash)
- Get's the
replaceDuration
byclientContract.methods.replaceDuration().call()
this will be 60 seconds if we deployedNearBridge.sol
with the default values above - Sets
nextValidAt
from the bridge stateweb3.utils.toBN(bridgeState.nextValidAt)
- Sets
replaceDelay
to 0 then updates it to thenextTimestamp
+replaceDuration
-lastBlock.inner_lite.timestamp
i.e. The new block has to be at least 60 seconds after the current block stored on the light client. - Checks the height of the
currentHeight
of the bridge is less than thelastblock
from the near light client(bridgeState.currentHeight < lastBlock.inner_lite.height)
- Serializes the
lastBlock
using Borsh and check that the block is suitable - Checks that the
replaceDelay
has been met, if not sleeps until it has - Checks that the Master Account (the one submitting the block) has enough locked ETH (if not tries to deposit more). So that it can be slashed if the block proposed is invalid.
- Adds the light client block
await clientContract.methods.addLightClientBlock(nextBlockSelection.borshBlock).send
- Checks
NearBridge.sol
(the light client) has been initialized - Checks
balanceOf[msg.sender] >= lockEthAmount
that the sender has locked enough Eth to allow them to submit blocks - Decodes the nearBlock using
Borsh.from(data)
andborsh.decodeLightClientBlock()
- Commis the previous block, or make sure that it is OK to replace it using
lastValidAt = 0;
blockHashes_[curHeight] = untrustedHash;
blockMerkleRoots_[curHeight] = untrustedMerkleRoot;
- Check that the new block's height is greater than the current one's.
nearBlock.inner_lite.height > curHeight
- Check that the new block is from the same epoch as the current one, or from the next one.
- Check that the new block is signed by more than 2/3 of the validators.
- If the block is from the next epoch, make sure that the Block producers
next_bps
are supplied and have a correct hash. - Add the Block to the Light client
- Updates untrusted information to this block including
untrustedHeight
,untrustedTimestamp
,untrustedHash
,untrustedMerkleRoot
,untrustedNextHash
,untrustedSignatureSet
,untrustedNextEpoch
- If
fromNextEpoch
also update the Block Producers - Updates the
lastSubmitter
andlastValidAt
- Updates untrusted information to this block including
- Checks
- Cleans up the selected block to prevent submitting the same block again
await sleep(afterSubmitDelayMs)
- Sets the HeightGauuges to the correct block height
clientHeightGauge.set(Number(BigInt(bridgeState.currentHeight))
chainHeightGauge.set(Number(BigInt(lastBlock.inner_lite.height)))
- Sleeps for delay calculated from the maximum of the relayer days (10 seconds) and differnce between the current and next block time stamps and
await sleep(1000 * delay)
- Get the bridge state (including
- Loops
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
.
- watchdog is started from the CLI
- watchdog logic
- Initializes monitoring information on
Prometheus
const httpPrometheus = new HttpPrometheus(this.metricsPort, 'near_bridge_watchdog_')
const lastBlockVerified = httpPrometheus.gauge('last_block_verified', 'last block that was already verified')
const totBlockProducers = httpPrometheus.gauge('block_producers', 'number of block producers for current block')
const incorrectBlocks = httpPrometheus.counter('incorrect_blocks', 'number of incorrect blocks found')
const challengesSubmitted = httpPrometheus.counter('challenges_submitted', 'number of blocks challenged')
- Loops
while (true)
- Gets the
bridgeState
- Loops through all blockProducers checking their signatures
for (let i = 0; i < numBlockProducers; i++)
- Check each signature
this.clientContract.methods.checkBlockProducerSignatureInHead(i).call()
- If invalid challenge the signature:
this.clientContract.methods.challenge(this.ethMasterAccount, i).encodeABI()
calls challenge functionfunction challenge(address payable receiver, uint signatureIndex) external override pausable(PAUSED_CHALLENGE)
- checks block.timestamp is less than lastValidAt
block.timestamp < lastValidAt,
- Check if the signature is valid
!checkBlockProducerSignatureInHead(signatureIndex)
- slashes the last submitter
balanceOf[lastSubmitter] = balanceOf[lastSubmitter] - lockEthAmount;
- resets lastValidAt
lastValidAt = 0;
- Refunds half of the funds to the watchdog account
receiver.call{value: lockEthAmount / 2}("");
- checks block.timestamp is less than lastValidAt
- Sleeps for watchdog Delay seconds
await sleep(watchdogDelay * 1000)
- Check each signature
- Gets the
- Initializes monitoring information on
NEAR to Ethereum block propagation components
- eth2near-relay: Command to start the NEAR to Ethereum relay. See sample invocation here
- near2eth-block-relay is written in javascript
- Has dependencies including rainbow-bridge-utils see here for more information. It's other dependencies are also included in
rainbow-bridge-utils
.- ethereumjs-util: A collection of utility functions for Ethereum.
- Has the following functions and classes
class Near2EthRelay
async initialize ({nearNodeUrl, nearNetworkId, ethNodeUrl, ethMasterSk, ethClientArtifactPath, ethClientAddress, ethGasMultiplier, metricsPort })
async withdraw ({ethGasMultiplier})
async runInternal ({submitInvalidBlock, near2ethRelayMinDelay, near2ethRelayMaxDelay, near2ethRelayErrorDelay, near2ethRelayBlockSelectDuration, near2ethRelayNextBlockSelectDelayMs, near2ethRelayAfterSubmitDelayMs, ethGasMultiplier, ethUseEip1559, logVerbose})
run (options) {return this.runInternal({...options, submitInvalidBlock: false}) }
- Has dependencies including rainbow-bridge-utils see here for more information. It's other dependencies are also included in
- NearBridge.sol: Is the NEAR light client deployed on ethereum.
- It imports the following contracts (see nearbridge cryptographic primitives)
import "./AdminControlled.sol";
import "./INearBridge.sol";
import "./NearDecoder.sol";
import "./Ed25519.sol";
- It provides the following structure for Bridge State. If there is currently no unconfirmed block, the last three fields are zero.
uint currentHeight;
: Height of the current confirmed blockuint nextTimestamp;
: Timestamp of the current unconfirmed blockuint nextValidAt;
: Timestamp when the current unconfirmed block will be confirmeduint numBlockProducers;
: Number of block producers for the current unconfirmed block
- It provides the following storage
uint constant MAX_BLOCK_PRODUCERS = 100;
: Assumed to be even and to not exceed 256.struct Epoch {bytes32 epochId; uint numBPs; bytes [MAX_BLOCK_PRODUCERS] keys; bytes32[MAX_BLOCK_PRODUCERS / 2] packedStakes; uint256 stakeThreshold;}
uint256 public lockEthAmount;
uint256 public lockDuration;
: lockDuration and replaceDuration shouldn't be extremely big, so adding them to an uint64 timestamp should not overflow uint256.uint256 public replaceDuration;
: replaceDuration is in nanoseconds, because it is a difference between NEAR timestamps.Ed25519 immutable edwards;
uint256 public lastValidAt;
: End of challenge period. If zero,untrusted
fields andlastSubmitter
are not meaningful.uint64 curHeight;
uint64 untrustedHeight;
: The most recently added block. May still be in its challenge period, so should not be trusted.address lastSubmitter;
: Address of the account which submitted the last block.bool public initialized;
: Whether the contract was initialized.bool untrustedNextEpoch;
bytes32 untrustedHash;
bytes32 untrustedMerkleRoot;
bytes32 untrustedNextHash;
uint256 untrustedTimestamp;
uint256 untrustedSignatureSet;
NearDecoder.Signature[MAX_BLOCK_PRODUCERS] untrustedSignatures;
Epoch[3] epochs;
uint256 curEpoch;
mapping(uint64 => bytes32) blockHashes_;
mapping(uint64 => bytes32) blockMerkleRoots_;
mapping(address => uint256) public override balanceOf;
- It provides the following functions
constructor(Ed25519 ed, uint256 lockEthAmount_, uint256 lockDuration_, uint256 replaceDuration_, address admin_, uint256 pausedFlags_)
: _Note: require thelockDuration
(in seconds) to be at least one second less than thereplaceDuration
(in nanoseconds)require(replaceDuration* > lockDuration* _ 1000000000);
ethEd25519Address
: The address of the ECDSA signature checker using Ed25519 curve (see here)lockEthAmount
: The amount thatBLOCK_PRODUCERS
need to deposit (in wei)to be able to provide blocks. This amount will be slashed if the block is challenged and proven not to have a valid signature. Default value is 100000000000000000000 WEI = 100 ETH.lockDuration
: 30 secondsreplaceDuration
: 60 seconds it is passed in nanoseconds, because it is a difference between NEAR timestamps.ethAdminAddress
: Bridge Administrator Address0
: Indicates nothing is pausedUNPAUSE_ALL
function deposit() public payable override pausable(PAUSED_DEPOSIT)
function withdraw() public override pausable(PAUSED_WITHDRAW)
function challenge(address payable receiver, uint signatureIndex) external override pausable(PAUSED_CHALLENGE
function checkBlockProducerSignatureInHead(uint signatureIndex) public view override returns (bool)
function initWithValidators(bytes memory data) public override onlyAdmin
: The first part of initialization -- setting the validators of the current epoch.function initWithBlock(bytes memory data) public override onlyAdmin
: The second part of the initialization -- setting the current head.function bridgeState() public view returns (BridgeState memory res)
function bridgeState() public view returns (BridgeState memory res)
function addLightClientBlock(bytes memory data) public override pausable(PAUSED_ADD_BLOCK)
function setBlockProducers(NearDecoder.BlockProducer[] memory src, Epoch storage epoch) internal
function blockHashes(uint64 height) public view override pausable(PAUSED_VERIFY) returns (bytes32 res)
function blockMerkleRoots(uint64 height) public view override pausable(PAUSED_VERIFY) returns (bytes32 res)
- It imports the following contracts (see nearbridge cryptographic primitives)
- NearProver.sol: Is used to prove the validity of NEAR blocks on Ethereum.
- It imports the following contracts (see nearbridge cryptographic primitives)
import "rainbow-bridge-sol/nearbridge/contracts/NearDecoder.sol";
import "./ProofDecoder.sol";
- It has the following functions
constructor(INearBridge _bridge, address _admin, uint _pausedFlags)
_bridge
: Interface toNearBridge.sol
_admin
: Administrator address_pausedFlags
: paused indicator defaults toUNPAUSE_ALL = 0
function proveOutcome(bytes memory proofData, uint64 blockHeight)
function _computeRoot(bytes32 node, ProofDecoder.MerklePath memory proof) internal pure returns (bytes32 hash)
- It imports the following contracts (see nearbridge cryptographic primitives)
NEAR Rainbow Bridge Utils
rainbow-bridge-utils provides a set of utilities for the near rainbow bridge written in javascript.
- It has the following dependencies
- bn.js: Big number implementation in pure javascript
- bsert: Minimal assert with type checking.
- bs58: JavaScript component to compute base 58 encoding
- change-case: Transform a string between camelCase, PascalCase, Capital Case, snake_case, param-case, CONSTANT_CASE and others.
- configstore: Easily load and save config without having to think about where and how
- eth-object: re-usable and composable objects that you can just call Object.from to ingest new data to serialize Ethereum Trie / LevelDB data from hex, buffers and rpc into the same format.
- eth-util-lite: a low-dependency utility for Ethereum. It replaces a small subset of the ethereumjs-util and ethjs-util APIs.
- lodash: A set of utilities for working with arrays, numbers, objects, strings, etc.
- near-api-js: JavaScript library to interact with NEAR Protocol via RPC API
- web3: Ethereum JavaScript API
- It provides the following functions
- address-watcher: Watches a group of near and ethereum acccounts polling NEAR and Ethereum every second and updating
nearAccount.balanceGauge
,nearAccount.stateStorageGauge
andethereumAccount.balanceGauge
. - borsh: provides the following functions for Binary Object Representation Serializer for Hashing borsh
function serializeField (schema, value, fieldType, writer)
function deserializeField (schema, fieldType, reader)
function serialize (schema, fieldType, obj)
: Serialize given object using schema of the form:{ class_name -> [ [field_name, field_type], .. ], .. }
class BinaryReader
: Includes utilities to read numbers, strings arrays and burggersfunction deserialize (schema, fieldType, buffer)
const signAndSendTransactionAsync = async (accessKey, account, receiverId,actions) =>
const txnStatus = async (account, txHash, retries = RETRY_TX_STATUS, wait = 1000) =>
function getBorshTransactionLastResult (txResult)
class BorshContract {
constructor (borshSchema, account, contractId, options)
async accessKeyInit ()
function borshify (block)
function borshifyInitialValidators (initialValidators)
const hexToBuffer = (hex) =>
const readerToHex = (len) =>
- borshify-proof
function borshifyOutcomeProof (proof)
- robust: his module gives a few utils for robust error handling, and wrap web3 with error handling and retry
- utils
async function setupNear (config)
async function setupEth (config)
async function setupEthNear (config)
: Setup connection to NEAR and Ethereum from given configuration.function remove0x (value)
: Remove 0x if prependedfunction normalizeHex (value)
async function accountExists (connection, accountId)
async function createLocalKeyStore (networkId, keyPath)
function getWeb3 (config)
function getEthContract (web3, path, address)
function addSecretKey (web3, secretKey)
async function ethCallContract (contract, methodName, args)
: Wrap pure calls to Web3 contract to handle errors/reverts/gas usage.
- address-watcher: Watches a group of near and ethereum acccounts polling NEAR and Ethereum every second and updating
nearbridge Cryptographic Primitives
- Ed25519.sol: Solidity implementation of the Ed25519 which is the EdDSA signature scheme using SHA-512 (SHA-2) and Curve25519 (see here). It has the following functions
function pow22501(uint256 v) private pure returns (uint256 p22501, uint256 p11)
: Computes (v^(2^250-1), v^11) mod pfunction check(bytes32 k, bytes32 r, bytes32 s, bytes32 m1, bytes9 m2)
: has the following steps- Step 1: compute SHA-512(R, A, M)
- Step 2: unpack k
- Step 3: compute multiples of k
- Step 4: compute sG - hA
- Step 5: compare the points
- Utils.sol: A set of utilty functions for byte manipulation, memory updates and keccak functions.
function swapBytes2(uint16 v) internal pure returns (uint16)
function swapBytes4(uint32 v) internal pure returns (uint32)
function swapBytes8(uint64 v) internal pure returns (uint64)
function swapBytes16(uint128 v) internal pure returns (uint128)
function swapBytes32(uint256 v) internal pure returns (uint256)
function readMemory(uint ptr) internal pure returns (uint res)
function writeMemory(uint ptr, uint value) internal pure
function memoryToBytes(uint ptr, uint length) internal pure returns (bytes memory res)
function keccak256Raw(uint ptr, uint length) internal pure returns (bytes32 res)
function sha256Raw(uint ptr, uint length) internal view returns (bytes32 res)
- Borsh.sol provides Binary Object Representation Serializer for Hashing borsh functionality and imports
Utils.sols
. Structures and functions includestruct Data {uint ptr; uint end;}
function from(bytes memory data) internal pure returns (Data memory res)
function requireSpace(Data memory data, uint length) internal pure
: This function assumes that length is reasonably small, so that data.ptr + length will not overflow. In the current code, length is always less than 2^32.function read(Data memory data, uint length) internal pure returns (bytes32 res)
function done(Data memory data) internal pure
function peekKeccak256(Data memory data, uint length) internal pure returns (bytes32)
: Same considerations as for requireSpace.function peekSha256(Data memory data, uint length) internal view returns (bytes32)
: Same considerations as for requireSpace.function decodeU8(Data memory data) internal pure returns (uint8)
function decodeU16(Data memory data) internal pure returns (uint16)
function decodeU32(Data memory data) internal pure returns (uint32)
function decodeU64(Data memory data) internal pure returns (uint64)
function decodeU128(Data memory data) internal pure returns (uint128)
function decodeU256(Data memory data) internal pure returns (uint256)
function decodeBytes20(Data memory data) internal pure returns (bytes20)
function decodeBytes32(Data memory data) internal pure returns (bytes32)
function decodeBool(Data memory data) internal pure returns (bool)
function skipBytes(Data memory data) internal pure
function decodeBytes(Data memory data) internal pure returns (bytes memory res)
- NearDecoder.sol: Imports
Borsh.sol
and has utilities for decoding Public Keys, Signatures, Block Producers, Block Headers and Light Client Blocks.function decodePublicKey(Borsh.Data memory data) internal pure returns (PublicKey memory res)
function decodeSignature(Borsh.Data memory data) internal pure returns (Signature memory res)
function decodeBlockProducer(Borsh.Data memory data) internal pure returns (BlockProducer memory res)
function decodeBlockProducers(Borsh.Data memory data) internal pure returns (BlockProducer[] memory res)
function decodeOptionalBlockProducers(Borsh.Data memory data) internal view returns (OptionalBlockProducers memory res)
function decodeOptionalSignature(Borsh.Data memory data) internal pure returns (OptionalSignature memory res)
function decodeBlockHeaderInnerLite(Borsh.Data memory data) internal view returns (BlockHeaderInnerLite memory res)
function decodeLightClientBlock(Borsh.Data memory data) internal view returns (LightClientBlock memory res)
- ProofDecoder.sol: Imports
Borsh.sol
andNearDecoder.sol
and has utilities for decoding Proofs, BlockHeader, ExecutionStatus, ExecutionOutcome and MerklePaths. Structures and functions includestruct FullOutcomeProof {ExecutionOutcomeWithIdAndProof outcome_proof; MerklePath outcome_root_proof; BlockHeaderLight block_header_lite; MerklePath block_proof;}
function decodeFullOutcomeProof(Borsh.Data memory data) internal view returns (FullOutcomeProof memory proof)
struct BlockHeaderLight {bytes32 prev_block_hash; bytes32 inner_rest_hash; NearDecoder.BlockHeaderInnerLite inner_lite; bytes32 hash;}
function decodeBlockHeaderLight(Borsh.Data memory data) internal view returns (BlockHeaderLight memory header)
struct ExecutionStatus {uint8 enumIndex; bool unknown; bool failed; bytes successValue; bytes32 successReceiptId;}
successValue
indicates if the final action succeeded and returned some value or an empty vec.successReceiptId
is the final action of the receipt returned a promise or the signed transaction was converted to a receipt. Contains the receipt_id of the generated receipt.
function decodeExecutionStatus(Borsh.Data memory data) internal pure returns (ExecutionStatus memory executionStatus)
struct ExecutionOutcome {bytes[] logs; bytes32[] receipt_ids; uint64 gas_burnt; uint128 tokens_burnt; bytes executor_id; ExecutionStatus status; bytes32[] merkelization_hashes;}
bytes[] logs;
: Logs from this transaction or receipt.bytes32[] receipt_ids;
: Receipt IDs generated by this transaction or receipt.uint64 gas_burnt;
: The amount of the gas burnt by the given transaction or receipt.uint128 tokens_burnt;
: The total number of the tokens burnt by the given transaction or receipt.bytes executor_id;
: Hash of the transaction or receipt id that produced this outcome.ExecutionStatus status
: Execution status. Contains the result in case of successful execution.bytes32[] merkelization_hashes;
function decodeExecutionOutcome(Borsh.Data memory data) internal view returns (ExecutionOutcome memory outcome)
struct ExecutionOutcomeWithId {bytes32 id; ExecutionOutcome outcome; bytes32 hash;}
bytes32 id
: is the transaction hash or the receipt ID.
function decodeExecutionOutcomeWithId(Borsh.Data memory data) internal view returns (ExecutionOutcomeWithId memory outcome)
struct MerklePathItem {bytes32 hash; uint8 direction;}
uint8 direction
: where 0 = left, 1 = right
function decodeMerklePathItem(Borsh.Data memory data) internal pure returns (MerklePathItem memory item)
struct MerklePath {MerklePathItem[] items;}
function decodeMerklePath(Borsh.Data memory data) internal pure returns (MerklePath memory path)
struct ExecutionOutcomeWithIdAndProof {MerklePath proof; bytes32 block_hash; ExecutionOutcomeWithId outcome_with_id;}
function decodeExecutionOutcomeWithIdAndProof(Borsh.Data memory data)internal view returns (ExecutionOutcomeWithIdAndProof memory outcome)
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
- Once on NEAR, transactions will confirm in 1-2 seconds and cost well under $1 in most cases.
- Since the Bridge requires transactions on Ethereum for NEAR and Ethereum, the following costs are expected.
- Sending assets from Ethereum to NEAR takes about six minutes (20 blocks) and for ERC-20 costs about $10 on average.
- 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.
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 sidecontract 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;
}
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
}
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
This will create <<hex(erc20)>.<bridge_token_factory>>
NEP141-compatible contract.
- User sends
<erc20>.approve(<erc20locker>, <amount>)
Ethereum transaction. - User sends
<erc20locker>.lock(<erc20>, <amount>, <destination>)
Ethereum transaction. This transaction will createLocked
event. - Relayers will be sending Ethereum blocks to the
EthClient
on NEAR side. - After sufficient number of confirmations on top of the mined Ethereum block that contain the
lock
transaction, user or relayer can callBridgeTokenFactory.deposit(proof)
. Proof is the extracted information from the event on Ethereum side. BridgeTokenFactory.deposit
function will callEthProver
and verify that proof is correct and relies on a block with sufficient number of confirmations.EthProver
will return callback toBridgeTokenFactory
confirming that proof is correct.BridgeTokenFactory
will call<<hex(erc20)>.<bridge_token_factory>>.mint(<near_account_id>, <amount>)
.- User can use
<<hex(erc20)>.<bridge_token_factory>>
token in other applications now on NEAR.
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>
.
erc20-bridge-token
-BridgeTokenFactory
andBridgeToken
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
.
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 Ethereum side
cd erc20-connector
yarn
yarn run test
Testing NEAR side
make res/bridge_token_factory.wasm
cargo test --all
Token Transfer Components
Note: This uses Ethreum ERC20 and NEAR NEP-141 initally developed for NEP-21
- rainbow-token-connector
- NEAR rust based contracts
- bridge-common: Common functions for NEAR, currently only
pub fn parse_recipient(recipient: String) -> Recipient
- bridge-token-factory: Functions for managing tokens on NEAR including but not limited to
update_metadata
,deposit
,get_tokens
,finish_updating_metadata
,finish_updating_metadata
,finish_withdraw
,deploy_bridge_token
,get_bridge_token_account_id
,is_used_proof
,record_proof
- bridge-token: Token functions on NEAR including but not limited to
mint
andwithdraw
- token-locker: Token Locker functions on NEAR including but not limited to
withdraw
,finish_deposit
,is_used_proof
- bridge-common: Common functions for NEAR, currently only
- Ethereum solidity based contracts
- erc20-bridge-token: Ethereum Bridge token contracts including but not limited to
- erc20-connector: has ERC20Locker.sol which is used to lock and unlock tokens. It is linked to the bridge token factory on NEAR side. It also links to the prover that it uses to unlock the tokens. (see here)
- NEAR rust based contracts
References
-
Lighthouse Documentation: ETH 2.0 Consensus Client Lighthouse documentation
-
Lighthouse Github: ETH 2.0 Consensus Client Lighthouse Github
-
Lighthouse: Blog: ETH 2.0 Consensus Client Lighthouse Blog
Prysm Light Client
References
-
Prysm: Light-client Client WIP: An independent light client client
-
Prysm: light-client server PR: a feature PR that implements the basic production level changes to Prysm to comply as a light-client server to begin serving light client requests
Harmony Merkle Mount Range
- Harmony MMR PR Review and latest PR uses Merkle Mountain Ranges to facilitate light client development against Harmony's sharded Proof of Stake Chain
Near Rainbow Bridge Review
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
- interacting with the beacon chain now for finality
is_correct_finality_update
see finality-update-verify - Updated execution block proof to use the BEACONRPCClient and with an updated merkle tree
- Design can be found in PR-762
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.*
-
Smart Contracts Deployed on NEAR
- eth2-client implements the Ethereum Light Client on Near
-
it provides functions including but not limited to:
- validate the light client
- verify the finality branch
- verify bls signatures
- update finalized headers
- updates the submittes
- prune finalized blocks.
-
It interacts with the beach chain, uses Borsh for serialization and lighthouse for Ethereum 2.0 Consensus and tree_hash functions as well as bls signatures. See here for more information on lighthouse. Below is a list of dependencies from eth2-client/Cargo.toml
[dependencies] ethereum-types = "0.9.2" eth-types = { path = "../eth-types" } eth2-utility = { path = "../eth2-utility" } tree_hash = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" } merkle_proof = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" } bls = { git = "https://github.com/aurora-is-near/lighthouse.git", optional = true, rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec", default-features = false, features = ["milagro"]} admin-controlled = { path = "../admin-controlled" } near-sdk = "4.0.0" borsh = "0.9.3" bitvec = "1.0.0"
-
- eth2-client implements the Ethereum Light Client on Near
-
eth2near supports the relaying of blocks and the verification of finality between etherum and Near. It has the following components
-
contract_wrapper: provides rust wrappers for interacting with the solidity contracts on near
-
Contracts include (from
lib.rs
)pub mod contract_wrapper_trait; pub mod dao_contract; pub mod dao_eth_client_contract; pub mod dao_types; pub mod errors; pub mod eth_client_contract; pub mod eth_client_contract_trait; pub mod file_eth_client_contract; pub mod near_contract_wrapper; pub mod sandbox_contract_wrapper; pub mod utils;
-
Dependencies include (from contract_wrapper/Cargo.toml)
[dependencies] borsh = "0.9.3" futures = "0.3.21" async-std = "1.12.0" near-sdk = "4.0.0" near-jsonrpc-client = "=0.4.0-beta.0" near-crypto = "0.14.0" near-primitives = "0.14.0" near-chain-configs = "0.14.0" near-jsonrpc-primitives = "0.14.0" tokio = { version = "1.1", features = ["rt", "macros"] } reqwest = { version = "0.11", features = ["blocking"] } serde_json = "1.0.74" serde = { version = "1.0", features = ["derive"] } eth-types = { path = "../../contracts/near/eth-types/", features = ["eip1559"]} workspaces = "0.5.0" anyhow = "1.0"
-
-
eth2near-block-relay-rs is built in rust and integrates with the Ethereum 2.0 lgihthouse consensus client to propogate blocks to near.
-
Functionality includes (from lib.rs)
pub mod beacon_block_body_merkle_tree; pub mod beacon_rpc_client; pub mod config; pub mod eth1_rpc_client; pub mod eth2near_relay; pub mod execution_block_proof; pub mod hand_made_finality_light_client_update; pub mod init_contract; pub mod last_slot_searcher; pub mod light_client_snapshot_with_proof; pub mod logger; pub mod near_rpc_client; pub mod prometheus_metrics; pub mod relay_errors;
-
Dependencies include (from eth2near-block-relay-rs/Cargo.toml)
types = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" } tree_hash = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" } merkle_proof = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" } eth2_hashing = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" } eth2_ssz = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" } eth-types = { path = "../../contracts/near/eth-types/", features = ["eip1559"]} eth2-utility = { path = "../../contracts/near/eth2-utility" } contract_wrapper = { path = "../contract_wrapper" } finality-update-verify = { path = "../finality-update-verify" } log = { version = "0.4", features = ["std", "serde"] } serde_json = "1.0.74" serde = { version = "1.0", features = ["derive"] } ethereum-types = "0.9.2" reqwest = { version = "0.11", features = ["blocking"] } clap = { version = "3.1.6", features = ["derive"] } tokio = { version = "1.1", features = ["macros", "rt", "time"] } env_logger = "0.9.0" borsh = "0.9.3" near-sdk = "4.0.0" futures = { version = "0.3.21", default-features = false } async-std = "1.12.0" hex = "*" toml = "0.5.9" atomic_refcell = "0.1.8" bitvec = "*" primitive-types = "0.7.3" near-jsonrpc-client = "=0.4.0-beta.0" near-crypto = "0.14.0" near-primitives = "0.14.0" near-chain-configs = "0.14.0" near-jsonrpc-primitives = "0.14.0" prometheus = { version = "0.9", features = ["process"] } lazy_static = "1.4" warp = "0.2" thread = "*"
-
-
eth2near-block-relay is built using javascript and supports ETH 1.0 Proof of Work (
ethhash
) using merkle patrica trees.-
key classes from index.js include
Ethashproof
: which has functions togetParseBlock
andcalculateNextEpoch
Eth2NearRelay
: which interacts with theethClientContract
and has arun()
function which loops through relaying blocks and includes additional functions such asgetParseBlock
,submitBlock
-
Dependencies include (from package.json)
"dependencies": { "bn.js": "^5.1.3", "eth-object": "https://github.com/near/eth-object#383b6ea68c7050bea4cab6950c1d5a7fa553e72b", "eth-util-lite": "near/eth-util-lite#master", "@ethereumjs/block": "^3.4.0", "merkle-patricia-tree": "^2.1.2", "prom-client": "^12.0.0", "promisfy": "^1.2.0", "rainbow-bridge-utils": "1.0.0", "got": "^11.8.5" },
-
-
ethhashproof: is a commandline to calculate proof data for an ethash POW, it is used by project
SmartPool
and a decentralizedbridge between Etherum and EOS developed by Kyber Network team. It is written inGO
.-
Features Include 1. Calculate merkle root of the ethash dag dataset with given epoch 2. Calculate merkle proof of the pow (dataset elements and their merkle proofs) given the pow submission with given block header 3. Generate dag datase
-
Dependencies include (from ethahsproof/go.mod)
require ( github.com/deckarep/golang-set v1.7.1 github.com/edsrzf/mmap-go v1.0.0 github.com/ethereum/go-ethereum v1.10.4 github.com/hashicorp/golang-lru v0.5.5-0.20210104140557-80c98217689d golang.org/x/crypto v0.0.0-20210322153248-0c34fe9e7dc2 )
-
-
finality-update-verify checks and updates finality using the lighthouse beacon blocks.
-
Functions include (from lib.rs)
fn h256_to_hash256(hash: H256) -> Hash256
fn tree_hash_h256_to_eth_type_h256(hash: tree_hash::Hash256) -> eth_types::H256
fn to_lighthouse_beacon_block_header(bridge_beacon_block_header: &BeaconBlockHeader,) -> types::BeaconBlockHeader {types::BeaconBlockHeader
pub fn is_correct_finality_update(ethereum_network: &str, light_client_update: &LightClientUpdate, sync_committee: SyncCommittee, ) -> Result<bool, Box<dyn Error>>
-
Dependencies include (from finality-update-verify/Cargo.toml)
[dependencies] eth-types = { path ="../../contracts/near/eth-types/", features = ["eip1559"]} bls = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" } eth2-utility = { path ="../../contracts/near/eth2-utility"} tree_hash = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" } types = { git = "https://github.com/aurora-is-near/lighthouse.git", rev = "b624c3f0d3c5bc9ea46faa14c9cb2d90ee1e1dec" } bitvec = "1.0.0" [dev-dependencies] eth2_to_near_relay = { path = "../eth2near-block-relay-rs"} serde_json = "1.0.74" serde = { version = "1.0", features = ["derive"] } toml = "0.5.9"
-
-
The following smart contracts are deployed on Ethereum and used for propogating blocks from NEAR to Ethereum.
- Smart Contracts deployed on Ethereum including
-
Near Bridge Contracts including NearBridge.sol which the interface INearBridge.sol
-
Interface Overview
interface INearBridge { event BlockHashAdded(uint64 indexed height, bytes32 blockHash); event BlockHashReverted(uint64 indexed height, bytes32 blockHash); function blockHashes(uint64 blockNumber) external view returns (bytes32); function blockMerkleRoots(uint64 blockNumber) external view returns (bytes32); function balanceOf(address wallet) external view returns (uint256); function deposit() external payable; function withdraw() external; function initWithValidators(bytes calldata initialValidators) external; function initWithBlock(bytes calldata data) external; function addLightClientBlock(bytes calldata data) external; function challenge(address payable receiver, uint256 signatureIndex) external; function checkBlockProducerSignatureInHead(uint256 signatureIndex) external view returns (bool); }
-
Key Storage items for epoch and block information
Epoch[3] epochs; uint256 curEpoch; mapping(uint64 => bytes32) blockHashes_; mapping(uint64 => bytes32) blockMerkleRoots_; mapping(address => uint256) public override balanceOf;
-
Signing and Serializing Primitives
- NearDecoder.sol: handles decoing of Public Keys, Signatures, BlockProducers and LightClientBlocks using
Borsh.sol
- Utils.sol: handles reading and writing to memory, memoryToBytes and has functions such as
keccak256Raw
andsha256Raw
- Borsh.sol: Borsh: Binary Object Representation Serializer for Hashing. It is meant to be used in security-critical projects as it prioritizes consistency, safety, speed; and comes with a strict specification.
- Ed25519.sol: Ed25519 high-speed high-security signatures
- NearDecoder.sol: handles decoing of Public Keys, Signatures, BlockProducers and LightClientBlocks using
-
- NearProver.sol: Has a
proveOutcome
which validates the outcome merkle proof and the block proof is valid using_computeRoot
which is passed in abytes32 node, ProofDecoder.MerklePath memory proof
- ProofDecoder.sol: Uses MerklePaths to provide decoding functions such as
decodeExecutionStatus
,decodeExecutionOutcome
,decodeExecutionOutcomeWithId
,decodeMerklePathItem
,decodeMerklePath
anddecodeExecutionOutcomeWithIdAndProof
. It relies on the primitivesBorsh.sol
andNearDecoder.sol
above.
- NearProver.sol: Has a
-