Home | About John | Resume/CV | References | Writing | Research |
---|
NEAR Rainbow bridge was enhanced to support Ethereum 2.0 leveraging Ethereum Light Clients. This document is a review of the design.
Key differences in supporting Ethereum 2.0 (Proof of Stake) vs Proof of Work involves removing the ETHHASH logic and SPV client and potentially replacing with MMR trees per epoch and checkpoints similar to Harmony Light Client on Ethereum.
The NEAR Rainbow bridge is in this github repository and is supported by Aurora-labs.
It recently provided support for ETH 2.0 in this Pull Request (762).
It interacts lighthouse for Ethereum 2.0 Consensus and tree_hash functions as well as bls signatures.
High Level their architecture is similar to the Horizon Bridge but with some key differences, including but not limited to
is_correct_finality_update
see finality-update-verifyNear Rainbow bridge uses a fradu proof approach.
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
.
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')
while (true)
bridgeState
for (let i = 0; i < numBlockProducers; i++)
this.clientContract.methods.checkBlockProducerSignatureInHead(i).call()
this.clientContract.methods.challenge(this.ethMasterAccount, i).encodeABI()
calls challenge function
function challenge(address payable receiver, uint signatureIndex) external override pausable(PAUSED_CHALLENGE)
block.timestamp < lastValidAt,
!checkBlockProducerSignatureInHead(signatureIndex)
balanceOf[lastSubmitter] = balanceOf[lastSubmitter] - lockEthAmount;
lastValidAt = 0;
receiver.call{value: lockEthAmount / 2}("");
await sleep(watchdogDelay * 1000)
Following is a walkthough of block propogation from Ethereum to NEAR. For a better understanding of the technical components see Appendix A.
let mut eth_client_contract = EthClientContract::new(get_eth_contract_wrapper(&config));
network
- the name of Ethereum network such as mainnet
, 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.let mut eth2near_relay = Eth2NearRelay::init(&config, get_eth_client_contract(&config), args.enable_binary_search, args.submit_only_finalized_blocks,);
eth2near_relay.run(None);
pub fn run(&mut self, max_iterations: Option<u64>)
which runs until terminated doing using the following loop while !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 process
self.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 submissionswere_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 logic
self.is_enough_blocks_for_light_client_update
: Checks if there are enough blocks for a light client update
self.send_light_client_updates
calls send_light_client_update
which
if 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 calls is_correct_finality_update
in eth2near/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 the beacon_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.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.
Ed25519.sol
see more information under nearbridge Cryptographic PrimitivesNearBridge.sol
see more information under NEAR to Ethereum block propagation components. It takes the following arguments
ethEd25519Address
: The address of the ECDSA signature checker using Ed25519 curve (see here)lockEthAmount
: The amount that BLOCK_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 paused UNPAUSE_ALL
NearProver.sol
see more information under NEAR to Ethereum block propagation components. It takes the following arguments
ethClientAddress
: Interface to NearBridge.sol
ethAdminAddress
: Administrator address0
: paused indicator defaults to UNPAUSE_ALL = 0
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
```
while (true)
currentHeight
, nextTimestamp
, nextValidAt
, numBlockProducers
)currentBlockHash
the hash of the current untrursted block based on lastValidAt
lastBlock
by calling the NEAR rpc next_light_client_block
using the hash of last untrusted block bs58.encode(currentBlockHash)
replaceDuration
by clientContract.methods.replaceDuration().call()
this will be 60 seconds if we deployed NearBridge.sol
with the default values abovenextValidAt
from the bridge state web3.utils.toBN(bridgeState.nextValidAt)
replaceDelay
to 0 then updates it to the nextTimestamp
+ 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.currentHeight
of the bridge is less than the lastblock
from the near light client (bridgeState.currentHeight < lastBlock.inner_lite.height)
lastBlock
using Borsh and check that the block is suitablereplaceDelay
has been met, if not sleeps until it hasawait clientContract.methods.addLightClientBlock(nextBlockSelection.borshBlock).send
NearBridge.sol
(the light client) has been initializedbalanceOf[msg.sender] >= lockEthAmount
that the sender has locked enough Eth to allow them to submit blocksBorsh.from(data)
and borsh.decodeLightClientBlock()
lastValidAt = 0;
blockHashes_[curHeight] = untrustedHash;
blockMerkleRoots_[curHeight] = untrustedMerkleRoot;
nearBlock.inner_lite.height > curHeight
next_bps
are supplied and have a correct hash.untrustedHeight
, untrustedTimestamp
, untrustedHash
, untrustedMerkleRoot
, untrustedNextHash
, untrustedSignatureSet
, untrustedNextEpoch
fromNextEpoch
also update the Block ProducerslastSubmitter
and lastValidAt
await sleep(afterSubmitDelayMs)
clientHeightGauge.set(Number(BigInt(bridgeState.currentHeight))
chainHeightGauge.set(Number(BigInt(lastBlock.inner_lite.height)))
await sleep(1000 * delay)
The following is a walkthrough of how a transaction executed on Ethereum is propogated to NEAR’s eth2-client. See Cryptographic Primitives for more information on the cryptography used. and Appendix B for verification components.
At a high level the ethereum light client contract
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.HeaderInfo
.AccountId
s mapped to their number of submitted headers.The following is a walkthrough of how a transaction executed on NEAR is propogated to Ethereum’s nearbridge. See nearbridge Cryptographic Primitives for more information on the cryptography used.
NearOnEthClient Overview
The following is an excerpt from a blog by near on eth-near-rainbow-bridge
NearOnEthClient is an implementation of the NEAR light client in Solidity as an Ethereum contract. Unlike EthOnNearClient it does not need to verify every single NEAR header and can skip most of them as long as it verifies at least one header per NEAR epoch, which is about 43k blocks and lasts about half a day. As a result, NearOnEthClient can memorize hashes of all submitted NEAR headers in history, so if you are making a transfer from NEAR to Ethereum and it gets interrupted you don’t need to worry and you can resume it any time, even months later. Another useful property of the NEAR light client is that every NEAR header contains a root of the merkle tree computed from all headers before it. As a result, if you have one NEAR header you can efficiently verify any event that happened in any header before it.
Another useful property of the NEAR light client is that it only accepts final blocks, and final blocks cannot leave the canonical chain in NEAR. This means that NearOnEthClient does not need to worry about forks.
However, unfortunately, NEAR uses Ed25519 to sign messages of the validators who approve the blocks, and this signature is not available as an EVM precompile. It makes verification of all signatures of a single NEAR header prohibitively expensive. So technically, we cannot verify one NEAR header within one contract call to NearOnEthClient. Therefore we adopt the optimistic approach where NearOnEthClient verifies everything in the NEAR header except the signatures. Then anyone can challenge a signature in a submitted header within a 4-hour challenge window. The challenge requires verification of a single Ed25519 signature which would cost about 500k Ethereum gas (expensive, but possible). The user submitting the NEAR header would have to post a bond in Ethereum tokens, and a successful challenge would burn half of the bond and return the other half to the challenger. The bond should be large enough to pay for the gas even if the gas price increases exponentially during the 4 hours. For instance, a 20 ETH bond would cover gas price hikes up to 20000 Gwei. This optimistic approach requires having a watchdog service that monitors submitted NEAR headers and challenges any headers with invalid signatures. For added security, independent users can run several watchdog services.
Once EIP665 is accepted, Ethereum will have the Ed25519 signature available as an EVM precompile. This will make watchdog services and the 4-hour challenge window unnecessary.
At its bare minimum, Rainbow Bridge consists of EthOnNearClient and NearOnEthClient contracts, and three services: Eth2NearRelay, Near2EthRelay, and the Watchdog. We might argue that this already constitutes a bridge since we have established a cryptographic link between two blockchains, but practically speaking it requires a large portion of additional code to make application developers even consider using the Rainbow Bridge for their applications.
The following information on sending assets from NEAR back to Ethereum is an excerpt from https://near.org/bridge/.
Sending assets from NEAR back to Ethereum currently takes a maximum of sixteen hours (due to Ethereum finality times) and costs around $60 (due to ETH gas costs and at current ETH price). These costs and speeds will improve in the near future.
The NEAR Rainbow Bridge uses ERC-20 connectors which are developed in rainbow-token-connector and rainbow-bridge-client. Also see eth2near-fun-transfer.md.
Following is an overview of timing and anticipated costs
Note: This uses Ethreum ERC20 and NEAR NEP-141 initally developed for NEP-21
Generic ERC-20/NEP-141 connector for Rainbow Bridge
Specification
Ethereum’s side
contract ERC20Locker {
constructor(bytes memory nearTokenFactory, INearProver prover) public;
function lockToken(IERC20 token, uint256 amount, string memory accountId) public;
function unlockToken(bytes memory proofData, uint64 proofBlockHeader) public;
}
NEAR’s side
struct BridgeTokenFactory {
/// The account of the prover that we can use to prove
pub prover_account: AccountId,
/// Address of the Ethereum locker contract.
pub locker_address: [u8; 20],
/// Hashes of the events that were already used.
pub used_events: UnorderedSet<Vec<u8>>,
/// Mapping from Ethereum tokens to NEAR tokens.
pub tokens: UnorderedMap<EvmAddress, AccountId>;
}
impl BridgeTokenFactory {
/// Initializes the contract.
/// `prover_account`: NEAR account of the Near Prover contract;
/// `locker_address`: Ethereum address of the locker contract, in hex.
#[init]
pub fn new(prover_account: AccountId, locker_address: String) -> Self;
/// Relays the lock event from Ethereum.
/// Uses prover to validate that proof is correct and relies on a canonical Ethereum chain.
/// Send `mint` action to the token that is specified in the proof.
#[payable]
pub fn deposit(&mut self, proof: Proof);
/// A callback from BridgeToken contract deployed under this factory.
/// Is called after tokens are burned there to create an receipt result `(amount, token_address, recipient_address)` for Ethereum to unlock the token.
pub fn finish_withdraw(token_account: AccountId, amount: Balance, recipient: EvmAddress);
/// Transfers given NEP-21 token from `predecessor_id` to factory to lock.
/// On success, leaves a receipt result `(amount, token_address, recipient_address)`.
#[payable]
pub fn lock(&mut self, token: AccountId, amount: Balance, recipient: String);
/// Relays the unlock event from Ethereum.
/// Uses prover to validate that proof is correct and relies on a canonical Ethereum chain.
/// Uses NEP-21 `transfer` action to move funds to `recipient` account.
#[payable]
pub fn unlock(&mut self, proof: Proof);
/// Deploys BridgeToken contract for the given EVM address in hex code.
/// The name of new NEP21 compatible contract will be <hex(evm_address)>.<current_id>.
/// Expects ~35N attached to cover storage for BridgeToken.
#[payable]
pub fn deploy_bridge_token(address: String);
/// Checks if Bridge Token has been successfully deployed with `deploy_bridge_token`.
/// On success, returns the name of NEP21 contract associated with given address (<hex(evm_address)>.<current_id>).
/// Otherwise, returns "token do not exists" error.
pub fn get_bridge_token_account_id(&self, address: String) -> AccountId;
}
struct BridgeToken {
controller: AccountId,
token: Token, // uses https://github.com/ilblackdragon/balancer-near/tree/master/near-lib-rs
}
impl BridgeToken {
/// Setup the Token contract with given factory/controller.
pub fn new(controller: AccountId) -> Self;
/// Mint tokens to given user. Only can be called by the controller.
pub fn mint(&mut self, account_id: AccountId, amount: Balance);
/// Withdraw tokens from this contract.
/// Burns sender's tokens and calls controller to create event for relaying.
pub fn withdraw(&mut self, amount: U128, recipient: String) -> Promise;
}
impl FungibleToken for BridgeToken {
// see example https://github.com/ilblackdragon/balancer-near/blob/master/balancer-pool/src/lib.rs#L329
}
Setup new ERC-20 on NEAR
To setup token contract on NEAR side, anyone can call <bridge_token_factory>.deploy_bridge_token(<erc20>)
where <erc20>
is the address of the token.
With this call must attach the amount of $NEAR to cover storage for (at least 30 $NEAR currently).
This will create <<hex(erc20)>.<bridge_token_factory>>
NEP141-compatible contract.
Usage flow Ethereum -> NEAR
<erc20>.approve(<erc20locker>, <amount>)
Ethereum transaction.<erc20locker>.lock(<erc20>, <amount>, <destination>)
Ethereum transaction. This transaction will create Locked
event.EthClient
on NEAR side.lock
transaction, user or relayer can call BridgeTokenFactory.deposit(proof)
. Proof is the extracted information from the event on Ethereum side.BridgeTokenFactory.deposit
function will call EthProver
and verify that proof is correct and relies on a block with sufficient number of confirmations.EthProver
will return callback to BridgeTokenFactory
confirming that proof is correct.BridgeTokenFactory
will call <<hex(erc20)>.<bridge_token_factory>>.mint(<near_account_id>, <amount>)
.<<hex(erc20)>.<bridge_token_factory>>
token in other applications now on NEAR.Usage flow NEAR -> Ethereum
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
and BridgeToken
Ethereum contracts.BridgeTokenFactory
creates new BridgeToken
that correspond to specific token account id on NEAR side.
BridgeTokenFactory
receives deposit
with proof from NEAR, verify them and mint appropriate amounts on recipient addresses.
Calling withdraw
will burn tokens of this user and will generate event <token: String, sender_id: EthAddress, amount: u256, recipient: String>
that can be relayed to token-factory
.
Caveats
Generally, this connector allows any account to call ft_transfer_call
opening for potential malicious tokens to be bridged to Ethereum.
The expectation here is that on Ethereum side, the token lists will handle this, as it’s the same attack model as malicious tokens on Uniswap and other DEXs.
Using Ethereum BridgeTokenFactory
contract can always resolve Ethereum address of a contract back to NEAR one to check that it is indeed bridging token from NEAR and is created by this factory.
Testing
Testing Ethereum side
cd erc20-connector
yarn
yarn run test
Testing NEAR side
make res/bridge_token_factory.wasm
cargo test --all
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>>
provides the Eth2Client
public data stucture
pub struct Eth2Client {
/// If set, only light client updates by the trusted signer will be accepted
trusted_signer: Option<AccountId>,
/// Mask determining all paused functions
paused: Mask,
/// Whether the client validates the updates.
/// Should only be set to `false` for debugging, testing, and diagnostic purposes
validate_updates: bool,
/// Whether the client verifies BLS signatures.
verify_bls_signatures: bool,
/// We store the hashes of the blocks for the past `hashes_gc_threshold` headers.
/// Events that happen past this threshold cannot be verified by the client.
/// It is desirable that this number is larger than 7 days' worth of headers, which is roughly
/// 51k Ethereum blocks. So this number should be 51k in production.
hashes_gc_threshold: u64,
/// Network. e.g. mainnet, kiln
network: Network,
/// Hashes of the finalized execution blocks mapped to their numbers. Stores up to `hashes_gc_threshold` entries.
/// Execution block number -> execution block hash
finalized_execution_blocks: LookupMap<u64, H256>,
/// All unfinalized execution blocks' headers hashes mapped to their `HeaderInfo`.
/// Execution block hash -> ExecutionHeaderInfo object
unfinalized_headers: UnorderedMap<H256, ExecutionHeaderInfo>,
/// `AccountId`s mapped to their number of submitted headers.
/// Submitter account -> Num of submitted headers
submitters: LookupMap<AccountId, u32>,
/// Max number of unfinalized blocks allowed to be stored by one submitter account
/// This value should be at least 32 blocks (1 epoch), but the recommended value is 1024 (32 epochs)
max_submitted_blocks_by_account: u32,
// The minimum balance that should be attached to register a new submitter account
min_storage_balance_for_submitter: Balance,
/// Light client state
finalized_beacon_header: ExtendedBeaconBlockHeader,
finalized_execution_header: LazyOption<ExecutionHeaderInfo>,
current_sync_committee: LazyOption<SyncCommittee>,
next_sync_committee: LazyOption<SyncCommittee>,
}
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"
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,
}
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>>
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)
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.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>>
ExecutionBlockProof
contains a block_hash
(execution block) and a proof of its inclusion in the BeaconBlockBody
tree hash. The block_hash
is the 12th field in execution_payload, which is the 9th field in BeaconBlockBody
. The first 4 elements in proof correspondent to the proof of inclusion of block_hash
in Merkle tree built for ExecutionPayload
. The last 4 elements of the proof of ExecutionPayload
in the Merkle tree are built on high-level BeaconBlockBody
fields. The proof starts from the leaf. It has the following structure and functions
pub 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>
BeaconBlockBodyMerkleTree
is built on the BeaconBlockBody
data structure, where the leaves of the Merkle Tree are the hashes of the high-level fields of the BeaconBlockBody
. The hashes of each element are produced by using ssz
serialization.ExecutionPayloadMerkleTree
is a built on the ExecutionPayload
data structure, where the leaves of the Merkle Tree are the hashes of the high-level fields of the ExecutionPayload
. The hashes of each element are produced by using ssz
serialization. ExecutionPayload
is one of the field in BeaconBlockBody. The hash of the root of ExecutionPlayloadMerkleTree
is the 9th leaf in BeaconBlockBody Merkle Tree.pub fn new(endpoint_url: &str, timeout_seconds: u64, timeout_state_seconds: u64) -> Self
: Creates BeaconRPCClient
for the given BeaconAPI endpoint_url
pub fn get_beacon_block_body_for_block_id(&self, block_id: &str,) -> Result<BeaconBlockBody<MainnetEthSpec>, Box<dyn Error>>
: Returns BeaconBlockBody
struct for the given block_id
. It uses the following arguments
block_id
- Block identifier. Can be one of: “head” (canonical head in node’s view),”genesis”, “finalized”, pub fn get_beacon_block_header_for_block_id(&self, block_id: &str,) -> Result<types::BeaconBlockHeader, Box<dyn Error>>
: Returns BeaconBlockHeader
struct for the given block_id
. It uses the following arguments
block_id
- Block identifier. Can be one of: “head” (canonical head in node’s view),”genesis”, “finalized”, pub fn get_light_client_update(&self, period: u64,) -> Result<LightClientUpdate, Box<dyn Error>>
: Returns LightClientUpdate
struct for the given period
. It uses the following arguments
period
- period id for which LightClientUpdate
is fetched. On Mainnet, one period consists of 256 epochs, and one epoch consists of 32 slotspub 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 until SyncAggregate
in BeconBlockBody
in the current slot is equal to SyncAggregate
in LightClientUpdate
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>>
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>>
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>>
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>,
}
```
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.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>>
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>>
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.
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"
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>>
Following are cryptographic primitives used in the eth2-client contract and finality-update-verify. Many are from the lighthouse codebase. Specifically consensus and crypto functions.
Some common primitives
Some Primitives from Lighthouse
BeaconChain
, to prevent replay attacks.beacon_aggregate_and_proof
gossipsub topic.BeaconBlock
and a signature from its proposer.Slot
and Epoch
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.SyncAggregate
from a slice of SyncCommitteeContribution
s. Equivalent to process_sync_committee_contributions
from the spec.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.BeaconChain
validator.Some Smart Contracts deployed on Ethereum
Borsh.sol
keccak256Raw
and sha256Raw
Some Primitives from NEAR Rainbow Bridge
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
Nearbridge Cryptographic Primitives
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
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)
Utils.sols
. Structures and functions include
struct 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)
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)
Borsh.sol
and NearDecoder.sol
and has utilities for decoding Proofs, BlockHeader, ExecutionStatus, ExecutionOutcome and MerklePaths. Structures and functions include
struct 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 = rightfunction 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)
The following links provide the production Ethereum addresses and blockexplorer views for NearBridge.sol and the ERC20 Locker
At time of writing (Oct 26th, 2022).
4 hours
0.061600109576901025 Ether ($96.56)
$600
rainbow-bridge-utils
.
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}) }
import "./AdminControlled.sol";
import "./INearBridge.sol";
import "./NearDecoder.sol";
import "./Ed25519.sol";
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 blockuint 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 and lastSubmitter
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;
constructor(Ed25519 ed, uint256 lockEthAmount_, uint256 lockDuration_, uint256 replaceDuration_, address admin_, uint256 pausedFlags_)
: *Note: require the lockDuration
(in seconds) to be at least one second less than the replaceDuration
(in nanoseconds) require(replaceDuration_ > lockDuration_ * 1000000000);
ethEd25519Address
: The address of the ECDSA signature checker using Ed25519 curve (see here)lockEthAmount
: The amount that BLOCK_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 paused UNPAUSE_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)
import "rainbow-bridge-sol/nearbridge/contracts/NearDecoder.sol";
import "./ProofDecoder.sol";
constructor(INearBridge _bridge, address _admin, uint _pausedFlags)
_bridge
: Interface to NearBridge.sol
_admin
: Administrator address_pausedFlags
: paused indicator defaults to UNPAUSE_ALL = 0
function proveOutcome(bytes memory proofData, uint64 blockHeight)
function _computeRoot(bytes32 node, ProofDecoder.MerklePath memory proof) internal pure returns (bytes32 hash)
rainbow-bridge-utils provides a set of utilities for the near rainbow bridge written in javascript.
nearAccount.balanceGauge
, nearAccount.stateStorageGauge
and ethereumAccount.balanceGauge
.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) =>
function borshifyOutcomeProof (proof)
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.Note: This uses Ethreum ERC20 and NEAR NEP-141 initally developed for NEP-21
pub fn parse_recipient(recipient: String) -> Recipient
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
mint
and withdraw
withdraw
, finish_deposit
, is_used_proof
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.
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"
```
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"
```
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 = "*"
```
ethhash
) using merkle patrica trees.
Ethashproof
: which has functions to getParseBlock
and calculateNextEpoch
Eth2NearRelay
: which interacts with the ethClientContract
and has a run()
function which loops through relaying blocks and includes additional functions such as getParseBlock
, 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"
},
```
SmartPool
and a decentralizedbridge between Etherum and EOS developed by Kyber Network team. It is written in GO
.
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
)
```
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.
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;
```
Borsh.sol
keccak256Raw
and sha256Raw
proveOutcome
which validates the outcome merkle proof and the block proof is valid using _computeRoot
which is passed in a bytes32 node, ProofDecoder.MerklePath memory proof
decodeExecutionStatus
, decodeExecutionOutcome
, decodeExecutionOutcomeWithId
, decodeMerklePathItem
, decodeMerklePath
and decodeExecutionOutcomeWithIdAndProof
. It relies on the primitives Borsh.sol
and NearDecoder.sol
above.Following is a walkthough of a funds transfer from Ethereum to a target chain (In this example Near), complete with light client updates, block propogation and proofs to ensure the transaction validity.
Actors From the diagram above you’ll notice that there are many actors involved, below is an overview of the actors and the operations they perform.
Sample TransactionFlow
TODO
Bridging Resources Required
Here is the storage and compuational costs per component.
Component | Data | Storage | Notes |
---|---|---|---|
Ethereum 2 Client | — | — | — |
Prover | not applicable | 0 bytes | |
DAO Contract |
TODO Review the following data structure and elements and move into the table above commenting on any mandatory requirements and structures that can be improved.
pub struct Eth2Client {
/// If set, only light client updates by the trusted signer will be accepted
trusted_signer: Option<AccountId>,
/// Mask determining all paused functions
paused: Mask,
/// Whether the client validates the updates.
/// Should only be set to `false` for debugging, testing, and diagnostic purposes
validate_updates: bool,
/// Whether the client verifies BLS signatures.
verify_bls_signatures: bool,
/// We store the hashes of the blocks for the past `hashes_gc_threshold` headers.
/// Events that happen past this threshold cannot be verified by the client.
/// It is desirable that this number is larger than 7 days' worth of headers, which is roughly
/// 51k Ethereum blocks. So this number should be 51k in production.
hashes_gc_threshold: u64,
/// Network. e.g. mainnet, kiln
network: Network,
/// Hashes of the finalized execution blocks mapped to their numbers. Stores up to `hashes_gc_threshold` entries.
/// Execution block number -> execution block hash
finalized_execution_blocks: LookupMap<u64, H256>,
/// All unfinalized execution blocks' headers hashes mapped to their `HeaderInfo`.
/// Execution block hash -> ExecutionHeaderInfo object
unfinalized_headers: UnorderedMap<H256, ExecutionHeaderInfo>,
/// `AccountId`s mapped to their number of submitted headers.
/// Submitter account -> Num of submitted headers
submitters: LookupMap<AccountId, u32>,
/// Max number of unfinalized blocks allowed to be stored by one submitter account
/// This value should be at least 32 blocks (1 epoch), but the recommended value is 1024 (32 epochs)
max_submitted_blocks_by_account: u32,
// The minimum balance that should be attached to register a new submitter account
min_storage_balance_for_submitter: Balance,
/// Light client state
finalized_beacon_header: ExtendedBeaconBlockHeader,
finalized_execution_header: LazyOption<ExecutionHeaderInfo>,
current_sync_committee: LazyOption<SyncCommittee>,
next_sync_committee: LazyOption<SyncCommittee>,
}