diff --git a/Makefile b/Makefile new file mode 100644 index 000000000..6d90640f0 --- /dev/null +++ b/Makefile @@ -0,0 +1,68 @@ +help: + @echo Deploy rainbow bridge + @echo 1 run "make init" first time only and one time. + @echo 2 run "make start-bsc" or "make start-ethash" + @echo 3 run "make gen-contarcts" + @echo 4 run "make deploy-contarcts" + @echo 5 run "make start-relayer" + @echo 6 run "stop-all" + +init: yarn-init gen-contracts + +yarn-init: + yarn + yarn install + +# generate ether contracts +gen-contracts: + cd contracts/eth/nearbridge/ && yarn && yarn build + cd contracts/eth/nearprover/ && yarn && yarn build + +# start near blockchain and connect with ganache. +start-ethash: + cli/index.js clean + cli/index.js prepare + cli/index.js start near-node + cli/index.js start ganache + +# start near blockchain and connect with binance test net. +start-bsc: + cli/index.js clean + cli/index.js prepare + cli/index.js start near-node + cli/index.js start binance-smart-chain \ + --eth-node-url https://data-seed-prebsc-1-s1.binance.org:8545 \ + --eth-master-sk 0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200 + +# deploy contracts +full-contracts: + cli/index.js init-near-contracts + cli/index.js init-eth-ed25519 + cli/index.js init-eth-client --eth-client-lock-eth-amount 1000 --eth-client-lock-duration 10 + cli/index.js init-eth-prover + cli/index.js init-eth-erc20 + cli/index.js init-eth-locker + cli/index.js init-near-token-factory + +# deploy contracts +light-bsc-contracts: + cli/index.js init-near-contracts + cli/index.js init-near-token-factory + +# start-relayer eth2near-relay, near2eth-relay and bridge-watchdog +start-relayer: + cli/index.js start eth2near-relay + cli/index.js start near2eth-relay + cli/index.js start bridge-watchdog + pm2 logs + +stop-all: + cli/index.js stop all + +build-eth-client: + cd contracts/near/eth-client && sudo ./build.sh + +test-eth-client: + cd contracts/near/eth-client && sudo ./test.sh + +.PHONY: help init yarn-init gen-contracts start-bsc light-bsc-contracts start-relayer stop-all build-eth-client test-eth-client start-ethash \ No newline at end of file diff --git a/cli/commands/start/binance-smart-chain.js b/cli/commands/start/binance-smart-chain.js new file mode 100644 index 000000000..c9e83c57f --- /dev/null +++ b/cli/commands/start/binance-smart-chain.js @@ -0,0 +1,25 @@ +// Binance Smart chain configurations to connect with testnet +class StartBinanceSmartChainNodeCommand { + static async execute ({ ethNodeUrl, ethMasterSk, nearClientValidateHeader }) { + if (nearClientValidateHeader !== 'true' && nearClientValidateHeader !== 'false') { + nearClientValidateHeader = 'true' + } + + if (ethNodeUrl === '') { + throw new Error('--eth-node-url not set') + } + + if (ethMasterSk === '') { + throw new Error('--eth-master-sk not set') + } + + return { + ethNodeUrl: ethNodeUrl, + ethMasterSk: ethMasterSk, + nearClientValidateHeader: nearClientValidateHeader, + nearClientValidateHeaderMode: 'bsc' + } + } +} + +exports.StartBinanceSmartChainNodeCommand = StartBinanceSmartChainNodeCommand diff --git a/cli/commands/start/eth2near-relay.js b/cli/commands/start/eth2near-relay.js index cad1dcd63..8e738f8d0 100644 --- a/cli/commands/start/eth2near-relay.js +++ b/cli/commands/start/eth2near-relay.js @@ -8,7 +8,7 @@ const { nearAPI } = require('rainbow-bridge-utils') const path = require('path') class StartEth2NearRelayCommand { - static async execute({ + static async execute ({ daemon, nearNetworkId, nearNodeUrl, @@ -18,7 +18,8 @@ class StartEth2NearRelayCommand { totalSubmitBlock, gasPerTransaction, ethNodeUrl, - metricsPort + metricsPort, + nearClientValidateHeaderMode }) { if (daemon === 'true') { ProcessManager.connect((err) => { @@ -75,7 +76,7 @@ class StartEth2NearRelayCommand { ) await clientContract.accessKeyInit() console.log('Initializing eth2near-relay...', { ethNodeUrl, metricsPort }) - relay.initialize(clientContract, { ethNodeUrl, totalSubmitBlock, gasPerTransaction, metricsPort, nearNetworkId }) + relay.initialize(clientContract, { ethNodeUrl, totalSubmitBlock, gasPerTransaction, metricsPort, nearNetworkId, nearClientValidateHeaderMode }) console.log('Starting eth2near-relay...') await relay.run() } diff --git a/cli/commands/start/ganache.js b/cli/commands/start/ganache.js index 910aa7c30..9b964115b 100644 --- a/cli/commands/start/ganache.js +++ b/cli/commands/start/ganache.js @@ -24,7 +24,8 @@ class StartGanacheNodeCommand { return { ethNodeUrl: 'ws://localhost:9545', ethMasterSk: '0x2bdd21761a483f71054e14f5b827213567971c676928d9a1808cbfa4b7501200', - nearClientValidateEthash: 'false' + nearClientValidateHeader: 'false', + nearClientValidateHeaderMode: 'ethash' } } } diff --git a/cli/index.js b/cli/index.js index c832539e6..7676cd282 100755 --- a/cli/index.js +++ b/cli/index.js @@ -14,6 +14,7 @@ const { } = require('./commands/start/near2eth-relay.js') const { StartWatchdogCommand } = require('./commands/start/watchdog.js') const { StartGanacheNodeCommand } = require('./commands/start/ganache.js') +const { StartBinanceSmartChainNodeCommand } = require('./commands/start/binance-smart-chain.js') const { StartLocalNearNodeCommand } = require('./commands/start/near.js') const { AddressWatcherCommand } = require('./commands/start/address-watcher.js') const { StopManagedProcessCommand } = require('./commands/stop/process.js') @@ -93,10 +94,16 @@ RainbowConfig.declareOption( '100000000000000000000000000' ) RainbowConfig.declareOption( - 'near-client-validate-ethash', - 'Whether validate ethash of submitted eth block, should set to true on mainnet and false on PoA testnets', + 'near-client-validate-header', + 'Whether validate header of submitted eth block, should set to true on mainnet(s) and false on PoA testnets', 'true' ) + +RainbowConfig.declareOption( + 'near-client-validate-header-mode', + 'Choose between ethash and bsc', + 'ethash' +) RainbowConfig.declareOption( 'hashes-gc-threshold', 'Events that happen past this threshold cannot be verified by the client.', @@ -334,6 +341,16 @@ RainbowConfig.addOptions( [] ) +RainbowConfig.addOptions( + startCommand.command('binance-smart-chain'), + StartBinanceSmartChainNodeCommand.execute, + [ + 'eth-node-url', + 'eth-master-sk', + 'near-client-validate-header' + ] +) + RainbowConfig.addOptions( startCommand.command('eth2near-relay'), StartEth2NearRelayCommand.execute, @@ -347,7 +364,8 @@ RainbowConfig.addOptions( 'total-submit-block', 'gas-per-transaction', 'daemon', - 'metrics-port' + 'metrics-port', + 'near-client-validate-header-mode' ] ) @@ -455,7 +473,8 @@ RainbowConfig.addOptions( 'near-client-sk', 'near-client-contract-path', 'near-client-init-balance', - 'near-client-validate-ethash', + 'near-client-validate-header', + 'near-client-validate-header-mode', 'near-client-trusted-signer', 'hashes-gc-threshold', 'finalized-gc-threshold', @@ -909,6 +928,6 @@ RainbowConfig.addOptions( ['near-node-url'] ) - ; (async () => { - await program.parseAsync(process.argv) - })() +; (async () => { + await program.parseAsync(process.argv) +})() diff --git a/cli/init/near-contracts.js b/cli/init/near-contracts.js index ea2ff2543..d6b38e506 100644 --- a/cli/init/near-contracts.js +++ b/cli/init/near-contracts.js @@ -10,7 +10,7 @@ const { } = require('rainbow-bridge-eth2near-block-relay') class InitNearContracts { - static async execute({ + static async execute ({ nearMasterAccount, nearMasterSk, nearClientAccount, @@ -26,7 +26,8 @@ class InitNearContracts { nearProverInitBalance, nearNodeUrl, nearNetworkId, - nearClientValidateEthash, + nearClientValidateHeader, + nearClientValidateHeaderMode, nearClientTrustedSigner, ethNodeUrl }) { @@ -97,12 +98,24 @@ class InitNearContracts { nearClientAccount ) const robustWeb3 = new RobustWeb3(ethNodeUrl) + + // get chain id used only by the bsc verify header. + const chainID = await robustWeb3.web3.eth.net.getId() + + // check if the nearClientValidateHeaderMode is either 'ethash' or 'bsc' if not set + // 'ethash' as default + if (nearClientValidateHeaderMode !== 'ethash' && nearClientValidateHeaderMode !== 'bsc') { + nearClientValidateHeaderMode = 'ethash' + } + await clientContract.maybeInitialize( hashesGcThreshold, finalizedGcThreshold, numConfirmations, - nearClientValidateEthash === 'true', + nearClientValidateHeader === 'true', + nearClientValidateHeaderMode, nearClientTrustedSigner || null, + chainID, robustWeb3, nearNetworkId ) diff --git a/contracts/near/Cargo.lock b/contracts/near/Cargo.lock index 0516e3846..5e4da3346 100644 --- a/contracts/near/Cargo.lock +++ b/contracts/near/Cargo.lock @@ -112,6 +112,12 @@ version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b41b7ea54a0c9d92199de89e20e58d49f02f8e699814ef3fdf266f6f748d15c7" +[[package]] +name = "base64" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3441f0f7b02788e948e47f457ca01f1d7e6d92c693bc132c22b087d3141c03ff" + [[package]] name = "base64" version = "0.13.0" @@ -181,7 +187,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94cb07b0da6a73955f8fb85d24c466778e70cda767a568229b104f0264089330" dependencies = [ "byte-tools", - "crypto-mac", + "crypto-mac 0.7.0", "digest 0.8.1", "opaque-debug 0.2.3", ] @@ -197,7 +203,7 @@ dependencies = [ "cc", "cfg-if 0.1.10", "constant_time_eq", - "crypto-mac", + "crypto-mac 0.7.0", "digest 0.8.1", ] @@ -207,7 +213,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0940dc441f31689269e10ac70eb1002a3a1d3ad1390e030043662eb7fe4688b" dependencies = [ - "block-padding", + "block-padding 0.1.5", "byte-tools", "byteorder", "generic-array 0.12.3", @@ -219,6 +225,7 @@ version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" dependencies = [ + "block-padding 0.2.1", "generic-array 0.14.4", ] @@ -231,6 +238,12 @@ dependencies = [ "byte-tools", ] +[[package]] +name = "block-padding" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d696c370c750c948ada61c69a0ee2cbbb9c50b1019ddb86d9317157a99c2cae" + [[package]] name = "borsh" version = "0.6.2" @@ -411,14 +424,16 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.11" +version = "0.4.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "80094f509cf8b5ae86a4966a39b3ff66cd7e2a3e594accec3743ff3fabeab5b2" +checksum = "670ad68c9088c2a963aaa298cb369688cf3f9465ce5e2d4ca10e6e0098a1ce73" dependencies = [ + "libc", "num-integer", "num-traits", "serde", "time", + "winapi 0.3.8", ] [[package]] @@ -574,6 +589,16 @@ dependencies = [ "subtle 1.0.0", ] +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array 0.14.4", + "subtle 2.4.0", +] + [[package]] name = "curve25519-dalek" version = "2.1.0" @@ -583,7 +608,7 @@ dependencies = [ "byteorder", "digest 0.8.1", "rand_core 0.5.1", - "subtle 2.2.3", + "subtle 2.4.0", "zeroize", ] @@ -754,12 +779,14 @@ dependencies = [ "hex", "indicatif", "lazy_static", + "libsecp256k1", "near-sdk", "primal", "rlp", "rustc-hex", "serde", "serde_json", + "tiny-keccak 2.0.2", "web3", "wee_alloc", ] @@ -821,7 +848,7 @@ dependencies = [ "ethereum-types 0.9.2", "primitive-types 0.7.2", "rlp", - "sha3", + "sha3 0.8.2", ] [[package]] @@ -1082,6 +1109,27 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "644f9158b2f133fd50f5fb3242878846d9eb792e445c893805ff0e3824006e35" +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array 0.14.4", + "hmac", +] + [[package]] name = "http" version = "0.1.21" @@ -1397,6 +1445,54 @@ dependencies = [ "libc", ] +[[package]] +name = "libsecp256k1" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd1137239ab33b41aa9637a88a28249e5e70c40a42ccc92db7f12cc356c1fcd7" +dependencies = [ + "arrayref", + "base64 0.12.3", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.7.3", + "serde", + "sha2 0.9.3", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee11012b293ea30093c129173cac4335513064094619f4639a25b310fd33c11" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle 2.4.0", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32239626ffbb6a095b83b37a02ceb3672b2443a87a000a884fc3c4d16925c9c0" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76acb433e21d10f5f9892b1962c2856c58c7f39a9e4bd68ac82b9436a0ffd5b9" +dependencies = [ + "libsecp256k1-core", +] + [[package]] name = "lock_api" version = "0.2.0" @@ -1566,7 +1662,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.8.2", - "subtle 2.2.3", + "subtle 2.4.0", ] [[package]] @@ -1826,7 +1922,7 @@ dependencies = [ "near-vm-errors 0.9.1", "serde", "sha2 0.8.2", - "sha3", + "sha3 0.8.2", ] [[package]] @@ -1843,8 +1939,8 @@ dependencies = [ "near-runtime-utils", "near-vm-errors 3.0.0", "serde", - "sha2 0.8.2", - "sha3", + "sha2 0.9.3", + "sha3 0.9.1", ] [[package]] @@ -1911,7 +2007,7 @@ dependencies = [ "serde", "serde_json", "sha2 0.8.2", - "sha3", + "sha3 0.8.2", ] [[package]] @@ -2820,6 +2916,18 @@ dependencies = [ "opaque-debug 0.2.3", ] +[[package]] +name = "sha3" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81199417d4e5de3f04b1e871023acea7389672c4135918f05aa9cbf2f2fa809" +dependencies = [ + "block-buffer 0.9.0", + "digest 0.9.0", + "keccak", + "opaque-debug 0.3.0", +] + [[package]] name = "shlex" version = "0.1.1" @@ -2938,9 +3046,9 @@ checksum = "2d67a5a62ba6e01cb2192ff309324cb4875d0c451d55fe2319433abe7a05a8ee" [[package]] name = "subtle" -version = "2.2.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "502d53007c02d7605a05df1c1a73ee436952781653da5d0bf57ad608f66932c1" +checksum = "1e81da0851ada1f3e9d4312c704aa4f8806f0f9d69faaf8df2f3464b4a9437c2" [[package]] name = "syn" @@ -3023,18 +3131,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.20" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfdd070ccd8ccb78f4ad66bf1982dc37f620ef696c6b5028fe2ed83dd3d0d08" +checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.20" +version = "1.0.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd80fc12f73063ac132ac92aceea36734f04a1d93c1240c6944e23a3b8841793" +checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" dependencies = [ "proc-macro2 1.0.24", "quote 1.0.7", @@ -3730,6 +3838,6 @@ dependencies = [ [[package]] name = "zeroize" -version = "1.1.0" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3cbac2ed2ba24cc90f5e06485ac8c7c1e5449fe8911aef4d8877218af021a5b8" +checksum = "4756f7db3f7b5574938c3eb1c117038b8e07f95ee6718c0efad4ac21508f1efd" diff --git a/contracts/near/eth-client/Cargo.toml b/contracts/near/eth-client/Cargo.toml index 9ef72d9c6..4424af979 100644 --- a/contracts/near/eth-client/Cargo.toml +++ b/contracts/near/eth-client/Cargo.toml @@ -20,6 +20,8 @@ arrutil = "0.1.2" ethash = { git = "https://github.com/nearprotocol/rust-ethash" } hex = "0.4.0" rustc-hex = "2.1.0" +libsecp256k1 = "0.5.0" +tiny-keccak = { version = "2.0.0", features = ["keccak"] } [target.'cfg(not(target_arch = "wasm32"))'.dependencies] # serde's Serialize and Deserialize traits are required for `near_bindgen` macro for non-wasm32 targets @@ -35,3 +37,4 @@ indicatif = "0.14" default = ["eip1559"] expensive_tests = [] eip1559 = ["eth-types/eip1559"] +bsc = [] diff --git a/contracts/near/eth-client/src/lib.rs b/contracts/near/eth-client/src/lib.rs index 08ecaaaad..e9664ba05 100644 --- a/contracts/near/eth-client/src/lib.rs +++ b/contracts/near/eth-client/src/lib.rs @@ -5,6 +5,12 @@ use near_sdk::collections::UnorderedMap; use near_sdk::{assert_self, AccountId}; use near_sdk::{env, near_bindgen, PanicOnDefault}; +#[cfg(bsc)] +use libsecp256k1::{recover, Message, RecoveryId, Signature}; + +#[cfg(bsc)] +use tiny_keccak::{Hasher, Keccak}; + #[cfg(not(target_arch = "wasm32"))] use serde::{Deserialize, Serialize}; @@ -66,9 +72,11 @@ const PAUSE_ADD_BLOCK_HEADER: Mask = 1; #[near_bindgen] #[derive(BorshDeserialize, BorshSerialize, PanicOnDefault)] pub struct EthClient { - /// Whether client validates the PoW when accepting the header. Should only be set to `false` + /// Whether client validates the PoW or POSA when accepting the header. Should only be set to `false` /// for debugging, testing, diagnostic purposes when used with Ganache or in PoA testnets - validate_ethash: bool, + validate_header: bool, + /// Validate header mode to switch between ethash (POW) and bsc (POSA) + validate_header_mode: String, /// The epoch from which the DAG merkle roots start. dags_start_epoch: u64, /// DAG merkle roots for the next several years. @@ -105,13 +113,61 @@ pub struct EthClient { trusted_signer: Option, /// Mask determining all paused functions paused: Mask, + /// Store the bsc epoch header key + epoch_header: H256, + /// chain id + chain_id: u64, } #[near_bindgen] impl EthClient { + #[private] + #[init(ignore_state)] + pub fn migrate_state() -> Self { + // Old EthClient + #[derive(BorshDeserialize)] + pub struct OldEthClient { + validate_ethash: bool, + dags_start_epoch: u64, + dags_merkle_roots: Vec, + best_header_hash: H256, + hashes_gc_threshold: u64, + finalized_gc_threshold: u64, + num_confirmations: u64, + canonical_header_hashes: UnorderedMap, + all_header_hashes: UnorderedMap>, + headers: UnorderedMap, + infos: UnorderedMap, + trusted_signer: Option, + paused: Mask, + } + + // Deserialize the state using the old contract structure. + let old_contract: OldEthClient = env::state_read().expect("Old state doesn't exist"); + Self { + validate_header: old_contract.validate_ethash, + validate_header_mode: String::from(""), + dags_start_epoch: old_contract.dags_start_epoch, + dags_merkle_roots: old_contract.dags_merkle_roots, + best_header_hash: old_contract.best_header_hash, + hashes_gc_threshold: old_contract.hashes_gc_threshold, + finalized_gc_threshold: old_contract.finalized_gc_threshold, + num_confirmations: old_contract.num_confirmations, + canonical_header_hashes: old_contract.canonical_header_hashes, + all_header_hashes: old_contract.all_header_hashes, + headers: old_contract.headers, + infos: old_contract.infos, + trusted_signer: old_contract.trusted_signer, + paused: old_contract.paused, + epoch_header: H256([0; 32].into()), + chain_id: 0, + } + } + #[init] pub fn init( - #[serializer(borsh)] validate_ethash: bool, + #[serializer(borsh)] validate_header: bool, + #[serializer(borsh)] validate_header_mode: String, #[serializer(borsh)] dags_start_epoch: u64, #[serializer(borsh)] dags_merkle_roots: Vec, #[serializer(borsh)] first_header: Vec, @@ -119,13 +175,25 @@ impl EthClient { #[serializer(borsh)] finalized_gc_threshold: u64, #[serializer(borsh)] num_confirmations: u64, #[serializer(borsh)] trusted_signer: Option, + #[serializer(borsh)] chain_id: u64, ) -> Self { assert!(!Self::initialized(), "Already initialized"); let header: BlockHeader = rlp::decode(first_header.as_slice()).unwrap(); let header_hash = header.hash.unwrap().clone(); let header_number = header.number; + let mut epoch_header: H256 = H256([0; 32].into()); + + // check if the current mode is bsc POSA, then store the epoch header. + if validate_header_mode == String::from("bsc") { + if !EthClient::is_epoch(header.number) { + panic!("The initial header for POSA have to be an epoch header"); + } + epoch_header = header.hash.unwrap(); + } + let mut res = Self { - validate_ethash, + validate_header_mode, + epoch_header: epoch_header, dags_start_epoch, dags_merkle_roots, best_header_hash: header_hash.clone(), @@ -138,6 +206,8 @@ impl EthClient { infos: UnorderedMap::new(b"i".to_vec()), trusted_signer, paused: Mask::default(), + validate_header, + chain_id, }; res.canonical_header_hashes .insert(&header_number, &header_hash); @@ -252,6 +322,9 @@ impl EthClient { panic!("Header is too old to have a chance to appear on the canonical chain."); } + if EthClient::is_epoch(header.number) && self.validate_header_mode == String::from("bsc") { + self.epoch_header = header.hash.unwrap(); + } let parent_info = self .infos .get(&header.parent_hash) @@ -365,12 +438,187 @@ impl EthClient { env::log(format!("Finish headers GC. Used gas: {}", env::used_gas()).as_bytes()); } - /// Verify PoW of the header. fn verify_header( &self, header: &BlockHeader, prev: &BlockHeader, dag_nodes: &[DoubleNodeWithMerkleProof], + ) -> bool { + match &self.validate_header_mode[..] { + "ethash" => return self.verify_header_ethash(&header, &prev, &dag_nodes), + #[cfg(bsc)] + "bsc" => return self.verify_header_bsc(&header, &prev), + _ => return false, + } + } + + fn verify_basic(&self, header: &BlockHeader, prev: &BlockHeader) -> bool { + header.gas_used <= header.gas_limit + && header.gas_limit >= U256(5000.into()) + && header.timestamp > prev.timestamp + && header.number == prev.number + 1 + && header.parent_hash == prev.hash.unwrap() + } + + // seal and hash bsc header. + #[cfg(bsc)] + fn seal_hash(&self, header: &BlockHeader, chain_id: U256) -> [u8; 32] { + let d = SealData { chain_id, header }; + d.seal_hash() + } + + // Verify POSA of the binance chain header. + #[cfg(bsc)] + fn verify_header_bsc(&self, header: &BlockHeader, prev: &BlockHeader) -> bool { + // The genesis block is the always valid dead-end + if header.number == 0 { + return true; + } + + // verify basic header properties. + if !self.verify_basic(header, prev) { + return false; + } + + let (extra_vanity, extra_seal, validator_bytes_length) = (32, 65, 20); + let is_epoch = EthClient::is_epoch(header.number); + let signers_bytes = header.extra_data.len() - (extra_vanity + extra_seal); + // check it is not an epoch header but contains the signers. + if !is_epoch && signers_bytes != 0 { + return false; + } + + // check if it is an epoch header and contains the signers. + if is_epoch && signers_bytes % validator_bytes_length != 0 { + return false; + } + + // Ensure that the mix digest is zero as we don't have fork protection currently + if header.mix_hash != H256([0; 32].into()) { + return false; + } + + // uncles_hash should be zero + if header.uncles_hash == H256([0; 32].into()) { + return false; + } + + // Verify that the gas limit is <= 2^63-1 + if header.gas_limit > U256((0x7fffffffffffffff as u64).into()) { + return false; + } + + let prev_gas_limit = format!("{}", prev.gas_limit).parse::().unwrap(); + let header_gas_limit = format!("{}", header.gas_limit).parse::().unwrap(); + let diff = (prev_gas_limit - header_gas_limit).abs(); + let limit = prev_gas_limit / 256; + + // Verify that the gas limit remains within allowed bounds + if diff >= limit { + return false; + } + + if !self.is_author(&header) { + return false; + } + + self.is_validator(&header) + } + + // check if the block is an epoch header. + fn is_epoch(number: u64) -> bool { + number % 200 == 0 + } + + // check if the author is the signer. + #[cfg(bsc)] + fn is_author(&self, header: &BlockHeader) -> bool { + let extra_seal = 65; + let seal_hash = self.seal_hash(header, U256(self.chain_id.into())); + + // get the signature from header extra_data + let signature = header.extra_data[header.extra_data.len() - extra_seal..].to_vec(); + let mut sig = [0u8; 65]; + sig.copy_from_slice(&signature[..]); + + let v = sig[64]; + let mut r = [0u8; 32]; + let mut s = [0u8; 32]; + r.copy_from_slice(&signature[0..32]); + s.copy_from_slice(&signature[32..64]); + + let rec_id = RecoveryId::parse(v).unwrap(); + let mut data = [0u8; 64]; + data[0..32].copy_from_slice(&r[..]); + data[32..64].copy_from_slice(&s[..]); + + let sig = Signature::parse_standard(&data).unwrap(); + let msg = Message::parse_slice(&seal_hash).unwrap(); + let public_key = recover(&msg, &sig, &rec_id).unwrap(); + + let mut keccak = Keccak::v256(); + let mut result = [0u8; 32]; + keccak.update(&public_key.serialize()[1..]); + keccak.finalize(&mut result); + + let mut address = [0u8; 20]; + address.copy_from_slice(&result[12..]); + + H160(address.into()) == header.author + } + + #[cfg(bsc)] + fn get_epoch_header(&self) -> BlockHeader { + self.headers.get(&self.epoch_header).unwrap() + } + + // check if the author address is valid and is in the validator set. + #[cfg(bsc)] + fn is_validator(&self, header: &BlockHeader) -> bool { + let (extra_vanity, extra_seal, address_size) = (32, 65, 20); + + let epoch_header = if EthClient::is_epoch(header.number) { + header.clone() + } else { + self.get_epoch_header() + }; + + if !self.is_in_validator_set(&epoch_header, header.author) { + return false; + } + + // skip difficulty verification + if self.validate_header { + // Ensure that the difficulty corresponds to the turn-ness of the signer + let validators = epoch_header.extra_data + [extra_vanity..(epoch_header.extra_data.len() - extra_seal)] + .to_vec(); + + // Get validator offset position. + let offset = (header.number % ((validators.len() / address_size) as u64)) as usize; + + // validate the current author if it's turn with the difficulty. + let (diff_in_turn, diff_no_turn) = (2, 1); + let is_turn = + Address::from(&validators[(offset * address_size)..((offset + 1) * address_size)]); + + if is_turn == header.author && header.difficulty != U256(diff_in_turn.into()) { + return false; + } + + if !(is_turn == header.author) && header.difficulty != U256(diff_no_turn.into()) { + return false; + } + } + true + } + + /// Verify PoW of the header. + fn verify_header_ethash( + &self, + header: &BlockHeader, + prev: &BlockHeader, + dag_nodes: &[DoubleNodeWithMerkleProof], ) -> bool { let (_mix_hash, result) = self.hashimoto_merkle( &header.partial_hash.unwrap(), @@ -385,16 +633,12 @@ impl EthClient { // 2. Added condition: header.parent_hash() == prev.hash() // U256((result.0).0.into()) < U256(ethash::cross_boundary(header.difficulty.0)) - && (!self.validate_ethash + && (!self.validate_header || (header.difficulty < prev.difficulty * 101 / 100 && header.difficulty > prev.difficulty * 99 / 100)) - && header.gas_used <= header.gas_limit && header.gas_limit < prev.gas_limit * 1025 / 1024 && header.gas_limit > prev.gas_limit * 1023 / 1024 - && header.gas_limit >= U256(5000.into()) - && header.timestamp > prev.timestamp - && header.number == prev.number + 1 - && header.parent_hash == prev.hash.unwrap() + && self.verify_basic(header, prev) && header.extra_data.len() <= 32 } @@ -422,7 +666,7 @@ impl EthClient { // Each two nodes are packed into single 128 bytes with Merkle proof let node = &nodes[idx / 2]; - if idx % 2 == 0 && self.validate_ethash { + if idx % 2 == 0 && self.validate_header { // Divide by 2 to adjust offset for 64-byte words instead of 128-byte assert_eq!(merkle_root, node.apply_merkle_proof((offset / 2) as u64)); }; @@ -439,6 +683,24 @@ impl EthClient { (H256(pair.0), H256(pair.1)) } + + // check if the author is in the validators set. + #[cfg(bsc)] + fn is_in_validator_set(&self, epoch_header: &BlockHeader, add: Address) -> bool { + let (extra_vanity, extra_seal, address_size) = (32, 65, 20); + let validators = epoch_header.extra_data + [extra_vanity..(epoch_header.extra_data.len() - extra_seal)] + .to_vec(); + + for x in 0..(validators.len() / address_size) { + let value = &validators[(x * address_size)..((x + 1) * address_size)]; + let _add: Address = Address::from(value); + if _add == add { + return true; + } + } + false + } } admin_controlled::impl_admin_controlled!(EthClient, paused); diff --git a/contracts/near/eth-client/src/tests.rs b/contracts/near/eth-client/src/tests.rs index 20ec9892a..aba4f4398 100644 --- a/contracts/near/eth-client/src/tests.rs +++ b/contracts/near/eth-client/src/tests.rs @@ -162,6 +162,12 @@ lazy_static! { eloop.into_remote(); web3::Web3::new(transport) }; + static ref BSC_WEB3RS: web3::Web3 = { + let (eloop, transport) = + web3::transports::Http::new("https://data-seed-prebsc-1-s1.binance.org:8545").unwrap(); + eloop.into_remote(); + web3::Web3::new(transport) + }; } fn get_context(input: Vec, is_view: bool) -> VMContext { @@ -227,16 +233,17 @@ fn read_block_raw(filename: String) -> BlockWithProofsRaw { serde_json::from_reader(std::fs::File::open(std::path::Path::new(&filename)).unwrap()).unwrap() } -fn assert_hashes_equal_to_contract_hashes(contract: &EthClient, heights: &[u64], real_hashes: &[H256]) { +fn assert_hashes_equal_to_contract_hashes( + contract: &EthClient, + heights: &[u64], + real_hashes: &[H256], +) { let hashes_from_contract: Vec = heights .iter() .map(|height| contract.block_hash(*height).unwrap()) .collect(); - for (hash, hash_from_contract) in real_hashes - .into_iter() - .zip(hashes_from_contract.iter()) - { + for (hash, hash_from_contract) in real_hashes.into_iter().zip(hashes_from_contract.iter()) { assert_eq!(hash, hash_from_contract); } } @@ -245,10 +252,11 @@ fn assert_hashes_equal_to_contract_hashes(contract: &EthClient, heights: &[u64], fn add_dags_merkle_roots() { testing_env!(get_context(vec![], false)); let (blocks, _) = get_blocks(&WEB3RS, 400_000, 400_001); - + let chain_id = 3; let dmr = read_roots_collection(); let contract = EthClient::init( true, + String::from("ethash"), 0, read_roots_collection().dag_merkle_roots, blocks[0].clone(), @@ -256,6 +264,7 @@ fn add_dags_merkle_roots() { 10, 10, None, + chain_id, ); assert_eq!(dmr.dag_merkle_roots[0], contract.dag_merkle_root(0)); @@ -278,9 +287,10 @@ fn add_blocks_2_and_3() { .iter() .map(|filename| read_block((&filename).to_string())) .collect(); - + let chain_id = 3; let mut contract = EthClient::init( true, + String::from("ethash"), 0, read_roots_collection().dag_merkle_roots, blocks[0].clone(), @@ -288,6 +298,7 @@ fn add_blocks_2_and_3() { 10, 10, None, + chain_id, ); for (block, proof) in blocks @@ -309,23 +320,20 @@ fn add_blocks_before_and_after_istanbul_fork() { testing_env!(get_context(vec![], false)); const FORK_HEIGHT_ISTANBUL: usize = 9_069_000; - let (blocks, hashes) = get_blocks( - &WEB3RS, - FORK_HEIGHT_ISTANBUL - 2, - FORK_HEIGHT_ISTANBUL + 2 - ); + let (blocks, hashes) = get_blocks(&WEB3RS, FORK_HEIGHT_ISTANBUL - 2, FORK_HEIGHT_ISTANBUL + 2); let blocks_with_proofs: Vec = [ format!("./src/data/proof_block_{}.json", FORK_HEIGHT_ISTANBUL - 1), format!("./src/data/proof_block_{}.json", FORK_HEIGHT_ISTANBUL), format!("./src/data/proof_block_{}.json", FORK_HEIGHT_ISTANBUL + 1), ] - .iter() - .map(|filename| read_block((&filename).to_string())) - .collect(); - + .iter() + .map(|filename| read_block((&filename).to_string())) + .collect(); + let chain_id = 3; let mut contract = EthClient::init( true, + String::from("ethash"), 0, read_roots_collection().dag_merkle_roots, blocks[0].clone(), @@ -333,6 +341,7 @@ fn add_blocks_before_and_after_istanbul_fork() { 10, 10, None, + chain_id, ); for (block, proof) in blocks @@ -343,7 +352,6 @@ fn add_blocks_before_and_after_istanbul_fork() { contract.add_block_header(block, proof.to_double_node_with_merkle_proof_vec()); } - let heights = [ FORK_HEIGHT_ISTANBUL as u64 - 1, FORK_HEIGHT_ISTANBUL as u64, @@ -363,20 +371,30 @@ fn add_blocks_before_and_after_nov11_2020_unannounced_fork() { let (blocks, hashes) = get_blocks( &WEB3RS, FORK_HEIGHT_UNANNOUNCED_NOV_11_2020 - 2, - FORK_HEIGHT_UNANNOUNCED_NOV_11_2020 + 2 + FORK_HEIGHT_UNANNOUNCED_NOV_11_2020 + 2, ); let blocks_with_proofs: Vec = [ - format!("./src/data/proof_block_{}.json", FORK_HEIGHT_UNANNOUNCED_NOV_11_2020 - 1), - format!("./src/data/proof_block_{}.json", FORK_HEIGHT_UNANNOUNCED_NOV_11_2020), - format!("./src/data/proof_block_{}.json", FORK_HEIGHT_UNANNOUNCED_NOV_11_2020 + 1), + format!( + "./src/data/proof_block_{}.json", + FORK_HEIGHT_UNANNOUNCED_NOV_11_2020 - 1 + ), + format!( + "./src/data/proof_block_{}.json", + FORK_HEIGHT_UNANNOUNCED_NOV_11_2020 + ), + format!( + "./src/data/proof_block_{}.json", + FORK_HEIGHT_UNANNOUNCED_NOV_11_2020 + 1 + ), ] - .iter() - .map(|filename| read_block((&filename).to_string())) - .collect(); - + .iter() + .map(|filename| read_block((&filename).to_string())) + .collect(); + let chain_id = 3; let mut contract = EthClient::init( true, + String::from("ethash"), 0, read_roots_collection().dag_merkle_roots, blocks[0].clone(), @@ -384,6 +402,7 @@ fn add_blocks_before_and_after_nov11_2020_unannounced_fork() { 10, 10, None, + chain_id, ); for (block, proof) in blocks @@ -408,12 +427,20 @@ fn add_block_diverged_until_ethashproof_dataset_fix() { testing_env!(get_context(vec![], false)); const HEIGHT_DIVERGED_BLOCK: usize = 11_703_828; - let (blocks, hashes) = get_blocks(&WEB3RS, HEIGHT_DIVERGED_BLOCK - 1, HEIGHT_DIVERGED_BLOCK + 1); + let (blocks, hashes) = get_blocks( + &WEB3RS, + HEIGHT_DIVERGED_BLOCK - 1, + HEIGHT_DIVERGED_BLOCK + 1, + ); // Jan 22 2021 - let block_with_proof = read_block(format!("./src/data/proof_block_{}.json", HEIGHT_DIVERGED_BLOCK)); - + let block_with_proof = read_block(format!( + "./src/data/proof_block_{}.json", + HEIGHT_DIVERGED_BLOCK + )); + let chain_id = 3; let mut contract = EthClient::init( true, + String::from("ethash"), 0, read_roots_collection().dag_merkle_roots, blocks[0].clone(), @@ -421,10 +448,17 @@ fn add_block_diverged_until_ethashproof_dataset_fix() { 500, 20, None, + chain_id, ); - contract.add_block_header(blocks[1].clone(), block_with_proof.to_double_node_with_merkle_proof_vec()); - assert_eq!(hashes[1], contract.block_hash(HEIGHT_DIVERGED_BLOCK as u64).unwrap()); + contract.add_block_header( + blocks[1].clone(), + block_with_proof.to_double_node_with_merkle_proof_vec(), + ); + assert_eq!( + hashes[1], + contract.block_hash(HEIGHT_DIVERGED_BLOCK as u64).unwrap() + ); } #[test] @@ -440,10 +474,11 @@ fn add_400000_block_only() { // ethash result: 0x00000000000ca599ebe9913fa00da78a4d1dd2fa154c4fd2aad10ccbca52a2a1 // Proof length: 24 // [400000.json] - + let chain_id = 3; let block_with_proof = read_block(format!("./src/data/{}.json", block_height)); let mut contract = EthClient::init( true, + String::from("ethash"), 400_000 / 30000, vec![block_with_proof.merkle_root], blocks[0].clone(), @@ -451,8 +486,12 @@ fn add_400000_block_only() { 10, 10, None, + chain_id, + ); + contract.add_block_header( + blocks[1].clone(), + block_with_proof.to_double_node_with_merkle_proof_vec(), ); - contract.add_block_header(blocks[1].clone(), block_with_proof.to_double_node_with_merkle_proof_vec()); assert_eq!(hashes[1], contract.block_hash(block_height as u64).unwrap()); } @@ -469,12 +508,14 @@ fn add_two_blocks_from_8996776() { format!("./src/data/{}.json", block_height), format!("./src/data/{}.json", block_height + 1), ] - .iter() - .map(|filename| read_block((&filename).to_string())) - .collect(); + .iter() + .map(|filename| read_block((&filename).to_string())) + .collect(); + let chain_id = 3; let mut contract = EthClient::init( true, + String::from("ethash"), 0, read_roots_collection().dag_merkle_roots, blocks[0].clone(), @@ -482,6 +523,7 @@ fn add_two_blocks_from_8996776() { 10, 10, None, + chain_id, ); for (block, proof) in blocks @@ -492,15 +534,60 @@ fn add_two_blocks_from_8996776() { contract.add_block_header(block, proof.to_double_node_with_merkle_proof_vec()); } - let heights = [ - block_height as u64, - block_height as u64 + 1, - ]; + let heights = [block_height as u64, block_height as u64 + 1]; // Skip parent header hash let hashes = &hashes[1..]; assert_hashes_equal_to_contract_hashes(&contract, &heights, &hashes); } +#[test] +// Test init bsc bridge. +fn bsc_add_epoch_header() { + testing_env!(get_context(vec![], false)); + let (blocks, hashes) = get_blocks(&BSC_WEB3RS, 10_161_600, 10_161_601); + let chain_id = 97; + let contract = EthClient::init( + true, + String::from("bsc"), + 0, + read_roots_collection().dag_merkle_roots, + blocks[0].clone(), + 30, + 10, + 10, + None, + chain_id, + ); + + assert!(hashes[0] == contract.epoch_header) +} + +#[test] +// test validate bsc headers. +fn bsc_update_epoch_header() { + testing_env!(get_context(vec![], false)); + let (blocks, hashes) = get_blocks(&BSC_WEB3RS, 10_161_400, 10_161_450); + let chain_id = 97; + let mut contract = EthClient::init( + true, + String::from("bsc"), + 0, + read_roots_collection().dag_merkle_roots, + blocks[0].clone(), + 30, + 51, + 51, + None, + chain_id, + ); + + for block in blocks.into_iter().skip(1) { + contract.add_block_header(block, vec![]); + } + assert!(hashes[0] == contract.epoch_header); + assert!(contract.headers.len() == 50); +} + #[test] fn add_two_blocks_from_400000() { testing_env!(get_context(vec![], false)); @@ -519,12 +606,14 @@ fn add_two_blocks_from_400000() { format!("./src/data/{}.json", block_height), format!("./src/data/{}.json", block_height + 1), ] - .iter() - .map(|filename| read_block((&filename).to_string())) - .collect(); + .iter() + .map(|filename| read_block((&filename).to_string())) + .collect(); + let chain_id = 3; let mut contract = EthClient::init( true, + String::from("ethash"), 400_000 / 30000, vec![blocks_with_proofs.first().unwrap().merkle_root], blocks[0].clone(), @@ -532,6 +621,7 @@ fn add_two_blocks_from_400000() { 10, 10, None, + chain_id, ); for (block, proof) in blocks @@ -542,10 +632,7 @@ fn add_two_blocks_from_400000() { contract.add_block_header(block, proof.to_double_node_with_merkle_proof_vec()); } - let heights = [ - block_height as u64, - block_height as u64 + 1, - ]; + let heights = [block_height as u64, block_height as u64 + 1]; // Skip parent header hash let hashes = &hashes[1..]; assert_hashes_equal_to_contract_hashes(&contract, &heights, &hashes); @@ -583,9 +670,10 @@ fn predumped_block_can_be_added() { let start_block_height = blocks_with_proofs.first().unwrap().0; let first_block_with_proof = read_block(blocks_with_proofs.first().unwrap().1.to_string()); - + let chain_id = 3; let mut contract = EthClient::init( true, + String::from("ethash"), start_block_height / 30000, vec![first_block_with_proof.merkle_root], first_block_with_proof.header_rlp.0.clone(), @@ -593,6 +681,7 @@ fn predumped_block_can_be_added() { 10, 10, None, + chain_id, ); let bar = ProgressBar::new(blocks_with_proofs.len() as _); diff --git a/contracts/near/eth-types/Cargo.toml b/contracts/near/eth-types/Cargo.toml index ec4dd08e5..4d94f9210 100644 --- a/contracts/near/eth-types/Cargo.toml +++ b/contracts/near/eth-types/Cargo.toml @@ -18,3 +18,4 @@ derive_more = "^0.99.2" [features] eip1559 = [] +bsc = [] diff --git a/contracts/near/eth-types/src/lib.rs b/contracts/near/eth-types/src/lib.rs index 7b7f8c67d..58af481a6 100644 --- a/contracts/near/eth-types/src/lib.rs +++ b/contracts/near/eth-types/src/lib.rs @@ -476,3 +476,48 @@ pub fn near_keccak512(data: &[u8]) -> [u8; 64] { buffer.copy_from_slice(&near_sdk::env::keccak512(data).as_slice()); buffer } + +// SealData struct used by bsc to encode header to rlp and hash using keccak256. +#[cfg(bsc)] +pub struct SealData<'s>{ + pub chain_id: U256, + pub header: &'s BlockHeader, +} + +#[cfg(bsc)] +impl<'s> SealData<'s> { + // hash using keccak256 an RLP encoded header. + pub fn seal_hash(&self) -> [u8; 32]{ + near_keccak256(&self.rlp_bytes()) + } +} + +// implement RlpEncodable for SealData. +#[cfg(bsc)] +impl<'s> RlpEncodable for SealData<'s> { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(16); + stream.append(&self.chain_id); + stream.append(&self.header.parent_hash); + stream.append(&self.header.uncles_hash); + stream.append(&self.header.author); + stream.append(&self.header.state_root); + stream.append(&self.header.transactions_root); + stream.append(&self.header.receipts_root); + stream.append(&self.header.log_bloom); + stream.append(&self.header.difficulty); + stream.append(&self.header.number); + stream.append(&self.header.gas_limit); + stream.append(&self.header.gas_used); + stream.append(&self.header.timestamp); + stream.append(&self.header.extra_data[0..&self.header.extra_data.len()-65].to_vec()); + stream.append(&self.header.mix_hash); + stream.append(&self.header.nonce); + } + + fn rlp_bytes(&self) -> Vec { + let mut s = RlpStream::new(); + self.rlp_append(&mut s); + s.out() + } +} diff --git a/contracts/near/res/eth_client.wasm b/contracts/near/res/eth_client.wasm index cc905c600..67fc137cc 100755 Binary files a/contracts/near/res/eth_client.wasm and b/contracts/near/res/eth_client.wasm differ diff --git a/eth2near/eth2near-block-relay/eth-on-near-client.js b/eth2near/eth2near-block-relay/eth-on-near-client.js index 516bbbd80..b07d00a1e 100644 --- a/eth2near/eth2near-block-relay/eth-on-near-client.js +++ b/eth2near/eth2near-block-relay/eth-on-near-client.js @@ -1,20 +1,20 @@ const BN = require('bn.js') const blockFromRpc = require('@ethereumjs/block/dist/from-rpc') const Common = require('@ethereumjs/common') -const got = require('got'); +const got = require('got') const { Web3, BorshContract, hexToBuffer, readerToHex, - sleep, + sleep } = require('rainbow-bridge-utils') const roots = require('./dag_merkle_roots.json') /// Get Ethereum block by number from RPC, and returns raw json object. -async function getEthBlock(number, RobustWeb3) { - let attempts = 10; - let blockData; +async function getEthBlock (number, RobustWeb3) { + let attempts = 10 + let blockData while (attempts > 0) { /// Need to call RPC directly, since function `blockFromRpc` works @@ -22,48 +22,53 @@ async function getEthBlock(number, RobustWeb3) { /// tools that abstract this calls are missing the field `baseFeePerGas` blockData = await got.post(RobustWeb3.ethNodeUrl, { json: { - "id": 0, - "jsonrpc": "2.0", - "method": "eth_getBlockByNumber", - "params": [ - "0x" + number.toString(16), + id: 0, + jsonrpc: '2.0', + method: 'eth_getBlockByNumber', + params: [ + '0x' + number.toString(16), false ] }, - responseType: "json" - }); + responseType: 'json' + }) /// When the block to be queried is the last one produced, RPC can return null. /// Retrying fix this problem. if (blockData.body.result === null) { - attempts -= 1; - await sleep(800); + attempts -= 1 + await sleep(800) } else { - break; + break } } - return blockData.body.result; + return blockData.body.result } /// bridgeId matches nearNetworkId. It is one of two strings [testnet / mainnet] -function web3BlockToRlp(blockData, bridgeId) { - let chain; - if (bridgeId === "testnet") { - chain = "ropsten"; +function web3BlockToRlp (blockData, bridgeId, validateHeaderMode) { + if (validateHeaderMode === 'bsc') { + const block = blockFromRpc.default(blockData, [], {}) + return block.header.serialize() + } + + let chain + if (bridgeId === 'testnet') { + chain = 'ropsten' } else { - chain = "mainnet"; + chain = 'mainnet' } - const common = new Common.default({ chain }); + const common = new Common.default({ chain }) /// baseFeePerGas was introduced after london hard fork. /// TODO: Use better way to detect current hard fork. if (blockData.baseFeePerGas !== undefined) { - common.setHardfork("london") + common.setHardfork('london') common.setEIPs([1559]) } - const block = blockFromRpc.default(blockData, [], { common }); - return block.header.serialize(); + const block = blockFromRpc.default(blockData, [], { common }) + return block.header.serialize() } const borshSchema = { @@ -75,14 +80,16 @@ const borshSchema = { initInput: { kind: 'struct', fields: [ - ['validate_ethash', 'bool'], + ['validate_header', 'bool'], + ['validate_header_mode', 'string'], ['dags_start_epoch', 'u64'], ['dags_merkle_roots', ['H128']], ['first_header', ['u8']], ['hashes_gc_threshold', 'u64'], ['finalized_gc_threshold', 'u64'], ['num_confirmations', 'u64'], - ['trusted_signer', '?AccountId'] + ['trusted_signer', '?AccountId'], + ['chain_id', 'u64'] ] }, dagMerkleRootInput: { @@ -129,7 +136,7 @@ const borshSchema = { } class EthOnNearClientContract extends BorshContract { - constructor(account, contractId) { + constructor (account, contractId) { super(borshSchema, account, contractId, { viewMethods: [ { @@ -180,8 +187,8 @@ class EthOnNearClientContract extends BorshContract { } // Call initialization methods on the contract. - // If validateEthash is true will do ethash validation otherwise it won't. - async maybeInitialize(hashesGcThreshold, finalizedGcThreshold, numConfirmations, validateEthash, trustedSigner, robustWeb3, bridgeId) { + // If validateHeader is true will do header validation otherwise it won't. + async maybeInitialize (hashesGcThreshold, finalizedGcThreshold, numConfirmations, validateHeader, validateHeaderMode, trustedSigner, chainID, robustWeb3, bridgeId) { await this.accessKeyInit() let initialized = false try { @@ -189,19 +196,31 @@ class EthOnNearClientContract extends BorshContract { } catch (e) { } if (!initialized) { console.log('EthOnNearClient is not initialized, initializing...') - const lastBlockNumber = await robustWeb3.getBlockNumber() - const blockData = await getEthBlock(lastBlockNumber, robustWeb3); - const blockRlp = web3BlockToRlp(blockData, bridgeId); + let lastBlockNumber = await robustWeb3.getBlockNumber() + let blockData + + // if validateHeaderMode is bsc(POSA) we have to get the last epoch header + if (validateHeaderMode === 'bsc' && lastBlockNumber % 200 !== 0) { + lastBlockNumber = lastBlockNumber - lastBlockNumber % 200 + blockData = await robustWeb3.getBlock(lastBlockNumber) + } else { + blockData = await getEthBlock(lastBlockNumber, robustWeb3) + } + + const blockRlp = web3BlockToRlp(blockData, bridgeId, validateHeaderMode) + console.log(blockRlp) await this.init( { - validate_ethash: validateEthash, + validate_header: validateHeader, + validate_header_mode: validateHeaderMode, dags_start_epoch: 0, dags_merkle_roots: roots.dag_merkle_roots, first_header: blockRlp, hashes_gc_threshold: hashesGcThreshold, finalized_gc_threshold: finalizedGcThreshold, num_confirmations: numConfirmations, - trusted_signer: trustedSigner + trusted_signer: trustedSigner, + chain_id: chainID }, new BN('300000000000000') ) diff --git a/eth2near/eth2near-block-relay/index.js b/eth2near/eth2near-block-relay/index.js index 3b4987998..16621d47e 100644 --- a/eth2near/eth2near-block-relay/index.js +++ b/eth2near/eth2near-block-relay/index.js @@ -30,7 +30,7 @@ const { const BRIDGE_SRC_DIR = path.join(__dirname, '..', '..') const MAX_GAS_PER_BLOCK = '300000000000000' -function ethashproof(command, _callback) { +function ethashproof (command, _callback) { return new Promise((resolve) => exec(command, (error, stdout, _stderr) => { if (error) { @@ -43,48 +43,50 @@ function ethashproof(command, _callback) { // This function find the result in O(log delta) where delta is the difference between estimatedPosition and the result. // In particular if estimatedPosition is the correct value it will make two calls to predicate, so it will behave in O(1) in this case. -async function binarySearchWithEstimate(limitLo, limitHi, estimatedPosition, predicate) { - let lo = limitLo; - let hi = limitHi; - let value = await predicate(estimatedPosition); +async function binarySearchWithEstimate (limitLo, limitHi, estimatedPosition, predicate) { + let lo = limitLo + let hi = limitHi + const value = await predicate(estimatedPosition) if (value) { - hi = estimatedPosition; - let step = 1; + hi = estimatedPosition + let step = 1 while (hi - step > lo && await predicate(hi - step)) { - step *= 2; + step *= 2 } - hi -= Math.floor(step / 2); - lo = Math.max(lo, hi - step); + hi -= Math.floor(step / 2) + lo = Math.max(lo, hi - step) } else { - lo = estimatedPosition; - let step = 1; + lo = estimatedPosition + let step = 1 while (lo + step < hi && !await predicate(lo + step)) { - step *= 2; + step *= 2 } - lo += Math.floor(step / 2); - hi = Math.min(hi, lo + step); + lo += Math.floor(step / 2) + hi = Math.min(hi, lo + step) } while (lo + 1 < hi) { - let mid = Math.floor((lo + hi) / 2); + const mid = Math.floor((lo + hi) / 2) if (await predicate(mid)) { - hi = mid; + hi = mid } else { - lo = mid; + lo = mid } } - return hi; + return hi } class Eth2NearRelay { - initialize(ethClientContract, { + initialize (ethClientContract, { ethNodeUrl, totalSubmitBlock, gasPerTransaction, nearNetworkId, - metricsPort + metricsPort, + nearClientValidateHeaderMode }) { + this.validateHeaderMode = nearClientValidateHeaderMode this.gasPerTransaction = new BN(gasPerTransaction) const limitSubmitBlock = new BN(MAX_GAS_PER_BLOCK).div(this.gasPerTransaction).toNumber() this.totalSubmitBlock = parseInt(totalSubmitBlock) @@ -103,7 +105,7 @@ class Eth2NearRelay { this.metricsPort = metricsPort } - async run() { + async run () { const robustWeb3 = this.robustWeb3 const httpPrometheus = new HttpPrometheus(this.metricsPort, 'near_bridge_eth2near_') @@ -111,7 +113,7 @@ class Eth2NearRelay { const chainBlockNumberGauge = httpPrometheus.gauge('chain_block_number', 'current chain block number') const errorsOnSubmitCounter = httpPrometheus.counter('errors_on_submit', 'number of errors while submitting header') - let previousBlockNumber = undefined; + let previousBlockNumber while (true) { let clientBlockNumber @@ -130,18 +132,18 @@ class Eth2NearRelay { continue } - let predicate = async (value) => { - let blockNumber = clientBlockNumber - value; - console.log('Checking block:', blockNumber); + const predicate = async (value) => { + const blockNumber = clientBlockNumber - value + console.log('Checking block:', blockNumber) try { const chainBlock = await getEthBlock(blockNumber, robustWeb3) /// Block is not ready if (chainBlock === null) { - const seconds = 3; - console.log(`Block ${blockNumber} is not ready. Sleeping ${seconds} seconds.`); - await sleep(seconds * 1000); - return await predicate(value); + const seconds = 3 + console.log(`Block ${blockNumber} is not ready. Sleeping ${seconds} seconds.`) + await sleep(seconds * 1000) + return await predicate(value) } const chainBlockHash = chainBlock.hash @@ -149,23 +151,23 @@ class Eth2NearRelay { blockNumber ) if (clientHashes.find((x) => x === chainBlockHash)) { - return true; + return true } else { - return false; + return false } } catch (e) { console.error(e) - return await predicate(value); + return await predicate(value) } - }; + } - let estimatedValued = (previousBlockNumber === undefined) ? 0 : clientBlockNumber - (previousBlockNumber + this.totalSubmitBlock); + const estimatedValued = (previousBlockNumber === undefined) ? 0 : clientBlockNumber - (previousBlockNumber + this.totalSubmitBlock) /// In case there exist a fork, find how many steps should go backward (delta) to the first block /// in the client that is also in the main chain. If the answer is 0, then the current head is valid - let delta = await binarySearchWithEstimate(0, clientBlockNumber, estimatedValued, predicate); - clientBlockNumber -= delta; - previousBlockNumber = clientBlockNumber; + const delta = await binarySearchWithEstimate(0, clientBlockNumber, estimatedValued, predicate) + clientBlockNumber -= delta + previousBlockNumber = clientBlockNumber if (clientBlockNumber < chainBlockNumber) { try { @@ -180,8 +182,14 @@ class Eth2NearRelay { endBlock = clientBlockNumber + 1 } for (let i = clientBlockNumber + 1; i <= endBlock; i++) { - blockPromises.push(this.getParseBlock(i)) + if (this.validateHeaderMode === 'bsc') { + const block = await this.robustWeb3.getBlock(i) + blockPromises.push({ header_rlp: block }) + } else { + blockPromises.push(this.getParseBlock(i)) + } } + const blocks = await Promise.all(blockPromises) console.log( `Got and parsed block ${clientBlockNumber + 1} to block ${endBlock}` @@ -216,7 +224,7 @@ class Eth2NearRelay { } } - async getParseBlock(blockNumber) { + async getParseBlock (blockNumber) { try { const block = await getEthBlock(blockNumber, this.robustWeb3) const blockRlp = this.web3.utils.bytesToHex( @@ -231,31 +239,40 @@ class Eth2NearRelay { } } - submitBlock(block, blockNumber) { - const h512s = block.elements - .filter((_, index) => index % 2 === 0) - .map((element, index) => { - return ( - this.web3.utils.padLeft(element, 64) + - this.web3.utils.padLeft(block.elements[index * 2 + 1], 64).substr(2) - ) - }) + submitBlock (block, blockNumber) { + let args = {} - let args = { - block_header: this.web3.utils.hexToBytes(block.header_rlp), - dag_nodes: h512s + if (this.validateHeaderMode === 'bsc') { + args = { + block_header: web3BlockToRlp(block.header_rlp), + dag_nodes: [] + } + } else { + const h512s = block.elements .filter((_, index) => index % 2 === 0) .map((element, index) => { - return { - dag_nodes: [element, h512s[index * 2 + 1]], - proof: block.merkle_proofs - .slice( - index * block.proof_length, - (index + 1) * block.proof_length - ) - .map((leaf) => this.web3.utils.padLeft(leaf, 32)) - } + return ( + this.web3.utils.padLeft(element, 64) + + this.web3.utils.padLeft(block.elements[index * 2 + 1], 64).substr(2) + ) }) + + args = { + block_header: this.web3.utils.hexToBytes(block.header_rlp), + dag_nodes: h512s + .filter((_, index) => index % 2 === 0) + .map((element, index) => { + return { + dag_nodes: [element, h512s[index * 2 + 1]], + proof: block.merkle_proofs + .slice( + index * block.proof_length, + (index + 1) * block.proof_length + ) + .map((leaf) => this.web3.utils.padLeft(leaf, 32)) + } + }) + } } args = serialize(borshSchema, 'addBlockHeaderInput', args) diff --git a/utils/config/config.json b/utils/config/config.json index dd37a3d09..0d1ee8e46 100644 --- a/utils/config/config.json +++ b/utils/config/config.json @@ -1,6 +1,6 @@ { "eth2nearClientContractPath": "~/.rainbow/bridge/libs-rs/res/eth_client.wasm", - "eth2nearClientValidateEthash": "false", + "eth2nearClientValidateHeader": "false", "eth2nearProverContractPath": "~/.rainbow/bridge/libs-rs/res/eth_prover.wasm", "nearFunTokenContractPath": "~/.rainbow/bridge/libs-rs/res/bridge_token_factory.wasm", "ethLockerAbiPath": "~/.rainbow/bridge/node_modules/rainbow-token-connector/res/BridgeTokenFactory.full.abi",