Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

EPROD 1086 block validation with a single reth node #48

Open
wants to merge 6 commits into
base: bitfinity-archive-node
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
441 changes: 246 additions & 195 deletions Cargo.lock

Large diffs are not rendered by default.

17 changes: 13 additions & 4 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ exclude = [".github/"]
members = [
"bin/reth-bench/",
"bin/reth/",
"crates/bitfinity-block-validator",
"crates/blockchain-tree/",
"crates/blockchain-tree-api/",
"crates/chainspec/",
Expand Down Expand Up @@ -527,21 +528,29 @@ similar-asserts = "1.5.0"
test-fuzz = "5"
iai-callgrind = "0.11"

# Bitfinity crates
bitfinity-block-validator = { path = "crates/bitfinity-block-validator" }

# Bitfinity Deps
async-channel = "2"
candid = "0.10"
did = { git = "https://github.com/bitfinity-network/bitfinity-evm-sdk", package = "did", features = ["alloy-primitives-07"], tag = "v0.34.x" }
did = { git = "https://github.com/bitfinity-network/bitfinity-evm-sdk", package = "did", features = [
"alloy-primitives-07",
], branch = "EPROD-1086-block-validation-with-a-single-reth-node" }
dirs = "5.0.1"
ethereum-json-rpc-client = { git = "https://github.com/bitfinity-network/bitfinity-evm-sdk", package = "ethereum-json-rpc-client", tag = "v0.34.x", features = [
ethereum-json-rpc-client = { git = "https://github.com/bitfinity-network/bitfinity-evm-sdk", package = "ethereum-json-rpc-client", branch = "EPROD-1086-block-validation-with-a-single-reth-node", features = [

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't forget to change to the right tag

"reqwest",
] }
evm-canister-client = { git = "https://github.com/bitfinity-network/bitfinity-evm-sdk", package = "evm-canister-client", features = [
"ic-agent-client",
], tag = "v0.34.x" }
], branch = "EPROD-1086-block-validation-with-a-single-reth-node" }
ic-agent = { version = "0.39" }
ic-canister-client = { git = "https://github.com/bitfinity-network/canister-sdk", package = "ic-canister-client", features = [
"ic-agent-client",
], tag = "v0.23.x" }
ic-cbor = "2.3"
ic-certificate-verification = "2.3"
ic-certification = "2.3"
hex = "0.4"
lightspeed_scheduler = { version = "0.59.0", features = ["tracing"] }
rlp = "0.5"

5 changes: 4 additions & 1 deletion bin/reth/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -118,10 +118,14 @@ libc = "0.2"
# bitfinity dependencies
async-trait = { workspace = true }
async-channel.workspace = true
bitfinity-block-validator.workspace = true
candid.workspace = true
did.workspace = true
ethereum-json-rpc-client = { workspace = true, features = ["reqwest"] }
evm-canister-client = { workspace = true, features = ["ic-agent-client"] }
hex.workspace = true
reth-engine-tree.workspace = true
reth-evm-ethereum.workspace = true
lightspeed_scheduler = { workspace = true, features = ["tracing"] }
rlp = { workspace = true }
serial_test.workspace = true
Expand All @@ -130,7 +134,6 @@ serial_test.workspace = true
reth-discv4.workspace = true



# bitfinity dev dependencies
dirs.workspace = true
jsonrpsee = { workspace = true }
Expand Down
47 changes: 46 additions & 1 deletion bin/reth/src/commands/bitfinity_import.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
//! Command that initializes the node by importing a chain from a remote EVM node.

use crate::{dirs::DataDirPath, macros::block_executor, version::SHORT_VERSION};
use bitfinity_block_validator::BitfinityBlockValidator;
use evm_canister_client::{EvmCanisterClient, IcAgentClient};
use futures::{Stream, StreamExt};
use lightspeed_scheduler::{job::Job, scheduler::Scheduler, JobExecutor};
use reth_beacon_consensus::EthBeaconConsensus;
Expand Down Expand Up @@ -125,6 +127,28 @@ impl BitfinityImportCommand {

/// Execute the import job.
async fn single_execution(&self) -> eyre::Result<()> {
let evmc_principal = candid::Principal::from_text(&self.bitfinity.evmc_principal)
.expect("Failed to parse principal");

// make block validation client if identity path is set
let evm_block_validator =
if let Some(identity) = self.bitfinity.validate_block_ic_identity_file_path.as_ref() {
let evm_client = EvmCanisterClient::new(
IcAgentClient::with_identity(
evmc_principal,
identity,
&self.bitfinity.evm_network,
Some(Duration::from_secs(30)),
)
.await
.expect("Failed to create agent client"),
);

Some(BitfinityBlockValidator::new(evm_client, self.provider_factory.clone()))
} else {
None
};

let consensus = Arc::new(EthBeaconConsensus::new(self.chain.clone()));
debug!(target: "reth::cli - BitfinityImportCommand", "Consensus engine initialized");
let provider_factory = self.provider_factory.clone();
Expand All @@ -139,6 +163,27 @@ impl BitfinityImportCommand {
backup_url: self.bitfinity.backup_rpc_url.clone(),
max_retries: self.bitfinity.max_retries,
retry_delay: Duration::from_secs(self.bitfinity.retry_delay_secs),
evm_block_validator,
};

let ic_root_key = if self.bitfinity.fetch_ic_root_key {
let ic_identity_path = self
.bitfinity
.validate_block_ic_identity_file_path
.as_ref()
.expect("identity path not set");
let agent = evm_canister_client::agent::identity::init_agent(
&ic_identity_path,
&self.bitfinity.evm_network,
None,
)
.await?;

agent.fetch_root_key().await.expect("failed to fetch IC root key");
let root_key = agent.read_root_key();
hex::encode(root_key)
} else {
self.bitfinity.ic_root_key.clone()
};

let remote_client = Arc::new(
Expand All @@ -150,7 +195,7 @@ impl BitfinityImportCommand {
self.bitfinity.max_fetch_blocks,
Some(CertificateCheckSettings {
evmc_principal: self.bitfinity.evmc_principal.clone(),
ic_root_key: self.bitfinity.ic_root_key.clone(),
ic_root_key,
}),
)
.await?,
Expand Down
3 changes: 3 additions & 0 deletions bin/reth/tests/commands/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,9 @@ pub async fn bitfinity_import_config_data(
backup_rpc_url: backup_evm_datasource_url,
max_retries: 3,
retry_delay_secs: 3,
validate_block_ic_identity_file_path: None,
evm_network: "ic".to_string(),
fetch_ic_root_key: false,
};

Ok((
Expand Down
27 changes: 27 additions & 0 deletions crates/bitfinity-block-validator/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
[package]
name = "bitfinity-block-validator"
version.workspace = true
edition.workspace = true
rust-version.workspace = true
license.workspace = true
homepage.workspace = true
repository.workspace = true

[lints]
workspace = true

[dependencies]
async-trait.workspace = true
did.workspace = true
evm-canister-client.workspace = true
ic-agent.workspace = true
ic-canister-client.workspace = true
itertools.workspace = true
reth-db.workspace = true
reth-engine-tree.workspace = true
reth-evm.workspace = true
reth-evm-ethereum.workspace = true
reth-provider.workspace = true
reth-primitives.workspace = true
reth-revm.workspace = true
tracing.workspace = true
208 changes: 208 additions & 0 deletions crates/bitfinity-block-validator/src/lib.rs

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe add at least one unit test that would show that all the calculated hashes have expected values?

Original file line number Diff line number Diff line change
@@ -0,0 +1,208 @@
//! Bitfinity block validator.

use did::unsafe_blocks::ValidateUnsafeBlockArgs;
use did::{Transaction, H256};
use evm_canister_client::{CanisterClient, EvmCanisterClient};
use itertools::Itertools;
use reth_db::database::Database;
use reth_engine_tree::tree::MemoryOverlayStateProvider;
use reth_evm::execute::Executor as _;
use reth_evm::execute::{BlockExecutionOutput, BlockExecutorProvider as _};
use reth_evm_ethereum::execute::EthExecutorProvider;
use reth_evm_ethereum::{execute::EthBlockExecutor, EthEvmConfig};
use reth_primitives::U256;
use reth_primitives::{Address, Receipt};
use reth_primitives::{Block, BlockWithSenders};
use reth_provider::{ChainSpecProvider as _, ExecutionOutcome, ProviderFactory, StateProvider};
use reth_revm::database::StateProviderDatabase;

/// Block validator for Bitfinity.
///
/// The validator validates the block by executing it and then
/// confirming it on the EVM.
#[derive(Clone, Debug)]
pub struct BitfinityBlockValidator<C, DB>
where
C: CanisterClient,
{
evm_client: EvmCanisterClient<C>,
provider_factory: ProviderFactory<DB>,
}

impl<C, DB> BitfinityBlockValidator<C, DB>
where
C: CanisterClient,
DB: Database,
{
/// Create a new [`BitfinityBlockValidator`].
pub fn new(evm_client: EvmCanisterClient<C>, provider_factory: ProviderFactory<DB>) -> Self {
Self { evm_client, provider_factory }
}

/// Validate a block.
pub async fn validate_block(
&self,
block: Block,
transactions: &[Transaction],
) -> Result<(), Box<dyn std::error::Error>> {
if !self.unsafe_blocks_enabled().await? {
tracing::debug!("Unsafe blocks are disabled");
return Ok(());
}
Comment on lines +48 to +51

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this check have any meaning? If unsafe blocks are disabled on EVM wouldn't it just return empty list if we request them? In this case we anyway do one GET request on every iteration. But if it's enabled, with this check on every iteration we do 2 GET requests instead of 1.

let validate_args = self.execute_block(block, transactions)?;
self.validate_unsafe_block(validate_args).await?;

Ok(())
}

/// Get whether unsafe blocks are enabled.
async fn unsafe_blocks_enabled(&self) -> Result<bool, Box<dyn std::error::Error>> {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would call this method somehow different, because in rust methods names starting with unsafe might have different meaning. Maybe something like block_validation_enabled?

self.evm_client
.is_unsafe_blocks_enabled()
.await
.map_err(|err| Box::new(err) as Box<dyn std::error::Error>)
}

/// Execute block and return validation arguments.
fn execute_block(
&self,
block: Block,
transactions: &[Transaction],
) -> Result<ValidateUnsafeBlockArgs, Box<dyn std::error::Error>> {
// execute the block
let block_number = block.number;
tracing::debug!("Executing block: {block_number}",);
let executor = self.executor();
let block_with_senders = Self::convert_block(block, transactions);

let output = match executor.execute((&block_with_senders, U256::MAX).into()) {
Ok(output) => output,
Err(err) => {
tracing::error!("Failed to execute block: {err:?}");
return Ok(ValidateUnsafeBlockArgs {
block_number,
block_hash: H256::zero(),
transactions_root: H256::zero(),
state_root: H256::zero(),
receipts_root: H256::zero(),
});
}
};

// calculate the receipts root
let receipts_root = self.calculate_receipts_root(&output.receipts);
tracing::debug!("Block {block_number} receipts root: {receipts_root}",);

// calculate trasnsactions_root
let transactions_root = self.calculate_transactions_root(&block_with_senders);
tracing::debug!("Block {block_number} transactions root: {transactions_root}",);

// calculate block hash
let block_hash = self.calculate_block_hash(&block_with_senders);
tracing::debug!("Block {block_number} hash: {block_hash}",);

// calculate state root
let state_root = self.calculate_state_root(output, block_number)?;
tracing::debug!("Block {block_number} state root: {state_root}",);

Ok(ValidateUnsafeBlockArgs {
block_number,
block_hash,
transactions_root,
state_root,
receipts_root,
})
}

/// Calculate the receipts root.
fn calculate_receipts_root(&self, receipts: &[reth_primitives::Receipt]) -> H256 {
let receipts_with_bloom =
receipts.iter().map(|receipt| receipt.clone().with_bloom()).collect::<Vec<_>>();
let calculated_root = reth_primitives::proofs::calculate_receipt_root(&receipts_with_bloom);
H256::from_slice(calculated_root.as_ref())
}

/// Calculate the transactions root.
fn calculate_transactions_root(&self, block: &BlockWithSenders) -> H256 {
let calculated_root = reth_primitives::proofs::calculate_transaction_root(&block.body);
H256::from_slice(calculated_root.as_ref())
}

/// Calculate the block hash.
fn calculate_block_hash(&self, block: &BlockWithSenders) -> H256 {
let calculated_hash = block.clone().seal_slow().hash();
H256::from_slice(calculated_hash.as_ref())
}

/// Calculate the state root.
fn calculate_state_root(
&self,
execution_output: BlockExecutionOutput<Receipt>,
block_number: u64,
) -> Result<H256, Box<dyn std::error::Error>> {
// get state root
let execution_outcome = ExecutionOutcome::new(
execution_output.state,
execution_output.receipts.into(),
block_number,
vec![execution_output.requests.into()],
);
let provider = match self.provider_factory.provider() {
Ok(provider) => provider,
Err(err) => {
tracing::error!("Failed to get provider: {err:?}");
return Err(Box::new(err));
}
};
let calculated_state_root =
execution_outcome.hash_state_slow().state_root_with_updates(provider.tx_ref())?.0;

Ok(H256::from_slice(calculated_state_root.as_ref()))
}

/// Convert [`Block`] to [`BlockWithSenders`].
fn convert_block(block: Block, transactions: &[Transaction]) -> BlockWithSenders {
let senders = transactions
.iter()
.map(|tx| Address::from_slice(tx.from.0.as_ref()))
.unique()
.collect::<Vec<_>>();
tracing::debug!("Found {} unique senders in block", senders.len());

BlockWithSenders { block, senders }
}

/// Try to validate an unsafe block on the EVM.
async fn validate_unsafe_block(
&self,
validate_args: ValidateUnsafeBlockArgs,
) -> Result<(), Box<dyn std::error::Error>> {
let block_number = validate_args.block_number;
tracing::info!("Confirming block on EVM: {block_number}: {validate_args:?}",);

let res = self.evm_client.validate_unsafe_block(validate_args).await?;

tracing::info!("Validate unsafe block response for block {block_number}: {res:?}",);

if let Err(err) = res {
tracing::error!("Failed to validate unsafe block: {err:?}");
return Err(Box::new(err));
}

Ok(())
}

/// Get the block executor for the latest block.
fn executor(
&self,
) -> EthBlockExecutor<
EthEvmConfig,
StateProviderDatabase<MemoryOverlayStateProvider<Box<dyn StateProvider>>>,
> {
let historical = self.provider_factory.latest().expect("no latest provider");

let db = MemoryOverlayStateProvider::new(Vec::new(), historical);
let executor = EthExecutorProvider::ethereum(self.provider_factory.chain_spec());
executor.executor(StateProviderDatabase::new(db))
}
}
Loading
Loading