From 40ad2383330312efbf0ae59c6fe3ef8c690ab492 Mon Sep 17 00:00:00 2001 From: Ahmed Sagdati <37515857+segfault-magnet@users.noreply.github.com> Date: Tue, 28 May 2024 22:02:58 +0200 Subject: [PATCH] refactor: block watcher committer merger (#83) --- Cargo.lock | 29 +- Cargo.toml | 3 +- committer/Cargo.toml | 1 + committer/src/config.rs | 32 +- committer/src/errors.rs | 5 +- committer/src/main.rs | 35 +- committer/src/setup.rs | 133 +++---- compose.yaml | 16 +- configurations/development/config.toml | 1 + eth_node/Dockerfile | 2 +- eth_node/run.sh | 6 + packages/eth/Cargo.toml | 2 +- packages/ports/Cargo.toml | 17 +- packages/ports/src/types.rs | 2 +- packages/services/Cargo.toml | 3 +- packages/services/src/block_committer.rs | 433 +++++++++++++++++++---- packages/services/src/block_watcher.rs | 387 -------------------- packages/services/src/lib.rs | 2 - packages/storage/src/test_instance.rs | 11 +- packages/validator/Cargo.toml | 5 +- packages/validator/src/lib.rs | 1 - 21 files changed, 504 insertions(+), 622 deletions(-) delete mode 100644 packages/services/src/block_watcher.rs diff --git a/Cargo.lock b/Cargo.lock index 89604cf2..7bcf96d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1142,18 +1142,6 @@ dependencies = [ "windows-sys 0.48.0", ] -[[package]] -name = "dns-lookup" -version = "2.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5766087c2235fec47fafa4cfecc81e494ee679d0fd4a59887ea0919bfb0e4fc" -dependencies = [ - "cfg-if", - "libc", - "socket2", - "windows-sys 0.48.0", -] - [[package]] name = "docker_credential" version = "1.3.1" @@ -1730,6 +1718,7 @@ dependencies = [ "config", "eth", "fuel", + "humantime", "metrics", "ports", "serde", @@ -2304,6 +2293,12 @@ version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" +[[package]] +name = "humantime" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a3a5bfb195931eeb336b2a7b4d761daec841b97f947d34394601737a7bba5e4" + [[package]] name = "hyper" version = "0.14.28" @@ -4602,23 +4597,26 @@ checksum = "3369f5ac52d5eb6ab48c6b4ffdc8efbcad6b89c765749064ba298f2c68a16a76" [[package]] name = "testcontainers" -version = "0.16.7" +version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69d47265a44d1035a322691cf0a6cc227d79b62ef86ffb0dbc204b394fee3d07" +checksum = "025e0ac563d543e0354d984540e749859a83dbe5c0afb8d458dc48d91cef2d6a" dependencies = [ "async-trait", "bollard", "bollard-stubs", + "bytes", "dirs", - "dns-lookup", "docker_credential", "futures", "log", + "memchr", "parse-display", "serde", "serde_json", "serde_with", + "thiserror", "tokio", + "tokio-stream", "tokio-util", "url", ] @@ -5095,6 +5093,7 @@ dependencies = [ "serde", "tai64", "thiserror", + "validator", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index e73c5c1d..c8bdf431 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -50,7 +50,8 @@ serde = { version = "1.0", default-features = false } serde_json = { version = "1.0", default-features = false } sqlx = { version = "0.7.4", default-features = false } tai64 = { version = "4.0.0", default-features = false } -testcontainers = { version = "0.16", default-features = false } +humantime = { version = "2.1", default-features = false } +testcontainers = { version = "0.17", default-features = false } thiserror = { version = "1.0", default-features = false } tokio = { version = "1.37", default-features = false } tokio-util = { version = "0.7", default-features = false } diff --git a/committer/Cargo.toml b/committer/Cargo.toml index 48209da3..29746235 100644 --- a/committer/Cargo.toml +++ b/committer/Cargo.toml @@ -15,6 +15,7 @@ clap = { workspace = true, features = ["derive"] } config = { workspace = true, features = ["toml", "async"] } eth = { workspace = true } fuel = { workspace = true } +humantime = { workspace = true } metrics = { workspace = true } ports = { workspace = true } serde = { workspace = true } diff --git a/committer/src/config.rs b/committer/src/config.rs index ef3e9b9f..aeb11d1d 100644 --- a/committer/src/config.rs +++ b/committer/src/config.rs @@ -8,13 +8,13 @@ use url::Url; #[derive(Debug, Clone, Deserialize)] pub struct Config { - pub eth: EthConfig, - pub fuel: FuelConfig, - pub app: AppConfig, + pub eth: Eth, + pub fuel: Fuel, + pub app: App, } #[derive(Debug, Clone, Deserialize)] -pub struct FuelConfig { +pub struct Fuel { /// URL to a fuel-core graphql endpoint. #[serde(deserialize_with = "parse_url")] pub graphql_endpoint: Url, @@ -23,7 +23,7 @@ pub struct FuelConfig { } #[derive(Debug, Clone, Deserialize)] -pub struct EthConfig { +pub struct Eth { /// The secret key authorized by the L1 bridging contracts to post block commitments. pub wallet_key: String, /// URL to a Ethereum RPC endpoint. @@ -59,28 +59,40 @@ where } #[derive(Debug, Clone, Deserialize)] -pub struct AppConfig { +pub struct App { /// Port used by the started server pub port: u16, /// IPv4 address on which the server will listen for connections pub host: Ipv4Addr, /// Postgres database configuration pub db: DbConfig, + /// How often to check the latest fuel block + #[serde(deserialize_with = "human_readable_duration")] + pub block_check_interval: Duration, +} + +fn human_readable_duration<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let duration_str: String = Deserialize::deserialize(deserializer).unwrap(); + humantime::parse_duration(&duration_str).map_err(|e| { + let msg = format!("Failed to parse duration '{duration_str}': {e};"); + serde::de::Error::custom(msg) + }) } #[derive(Debug, Clone)] -pub struct InternalConfig { - pub fuel_polling_interval: Duration, +pub struct Internal { pub fuel_errors_before_unhealthy: usize, pub between_eth_event_stream_restablishing_attempts: Duration, pub eth_errors_before_unhealthy: usize, pub balance_update_interval: Duration, } -impl Default for InternalConfig { +impl Default for Internal { fn default() -> Self { Self { - fuel_polling_interval: Duration::from_secs(3), fuel_errors_before_unhealthy: 3, between_eth_event_stream_restablishing_attempts: Duration::from_secs(3), eth_errors_before_unhealthy: 3, diff --git a/committer/src/errors.rs b/committer/src/errors.rs index c7e9b9d2..4f989a48 100644 --- a/committer/src/errors.rs +++ b/committer/src/errors.rs @@ -39,7 +39,7 @@ impl From for Error { fn from(error: ports::l1::Error) -> Self { match error { ports::l1::Error::Network(e) => Self::Network(e), - _ => Self::Other(error.to_string()), + ports::l1::Error::Other(e) => Self::Other(e), } } } @@ -57,8 +57,7 @@ impl From for Error { match error { services::Error::Network(e) => Self::Network(e), services::Error::Storage(e) => Self::Storage(e), - services::Error::BlockValidation(e) => Self::Other(e), - services::Error::Other(e) => Self::Other(e), + services::Error::BlockValidation(e) | services::Error::Other(e) => Self::Other(e), } } } diff --git a/committer/src/main.rs b/committer/src/main.rs index 3a1a1acb..ac7d3bc5 100644 --- a/committer/src/main.rs +++ b/committer/src/main.rs @@ -5,13 +5,8 @@ mod errors; mod setup; use api::launch_api_server; -use config::InternalConfig; use errors::Result; use metrics::prometheus::Registry; -use setup::{ - create_l1_adapter, setup_logger, setup_storage, spawn_block_watcher, - spawn_l1_committer_and_listener, spawn_wallet_balance_tracker, -}; use tokio_util::sync::CancellationToken; use crate::setup::shut_down; @@ -24,41 +19,44 @@ pub type Validator = validator::BlockValidator; #[tokio::main] async fn main() -> Result<()> { - setup_logger(); + setup::logger(); let config = config::parse()?; - let storage = setup_storage(&config).await?; + let storage = setup::storage(&config).await?; - let internal_config = InternalConfig::default(); + let internal_config = config::Internal::default(); let cancel_token = CancellationToken::new(); let metrics_registry = Registry::default(); + let (fuel_adapter, fuel_health_check) = + setup::fuel_adapter(&config, &internal_config, &metrics_registry); + let (ethereum_rpc, eth_health_check) = - create_l1_adapter(&config, &internal_config, &metrics_registry).await?; + setup::l1_adapter(&config, &internal_config, &metrics_registry).await?; let commit_interval = ethereum_rpc.commit_interval(); - let (rx_fuel_block, block_watcher_handle, fuel_health_check) = spawn_block_watcher( - commit_interval, - &config, + let wallet_balance_tracker_handle = setup::wallet_balance_tracker( &internal_config, - storage.clone(), &metrics_registry, + ethereum_rpc.clone(), cancel_token.clone(), ); - let wallet_balance_tracker_handle = spawn_wallet_balance_tracker( - &internal_config, - &metrics_registry, + let committer_handle = setup::block_committer( + commit_interval, ethereum_rpc.clone(), + storage.clone(), + fuel_adapter, + &config, + &metrics_registry, cancel_token.clone(), ); - let (committer_handle, listener_handle) = spawn_l1_committer_and_listener( + let listener_handle = setup::l1_event_listener( &internal_config, - rx_fuel_block, ethereum_rpc, storage.clone(), &metrics_registry, @@ -76,7 +74,6 @@ async fn main() -> Result<()> { shut_down( cancel_token, - block_watcher_handle, wallet_balance_tracker_handle, committer_handle, listener_handle, diff --git a/committer/src/setup.rs b/committer/src/setup.rs index 2a0203a6..27afd878 100644 --- a/committer/src/setup.rs +++ b/committer/src/setup.rs @@ -2,49 +2,17 @@ use std::num::NonZeroU32; use std::time::Duration; use metrics::{prometheus::Registry, HealthChecker, RegistersMetrics}; -use ports::{storage::Storage, types::ValidatedFuelBlock}; -use services::{BlockCommitter, BlockWatcher, CommitListener, Runner, WalletBalanceTracker}; -use tokio::{sync::mpsc::Receiver, task::JoinHandle}; +use ports::storage::Storage; +use services::{BlockCommitter, CommitListener, Runner, WalletBalanceTracker}; +use tokio::task::JoinHandle; use tokio_util::sync::CancellationToken; use tracing::{error, info}; use validator::BlockValidator; -use crate::{ - config::{Config, InternalConfig}, - errors::Result, - Database, FuelApi, Validator, L1, -}; +use crate::{config, errors::Result, Database, FuelApi, L1}; -pub fn spawn_block_watcher( - commit_interval: NonZeroU32, - config: &Config, - internal_config: &InternalConfig, - storage: Database, - registry: &Registry, - cancel_token: CancellationToken, -) -> ( - Receiver, - tokio::task::JoinHandle<()>, - HealthChecker, -) { - let (fuel_adapter, fuel_connection_health) = - create_fuel_adapter(config, internal_config, registry); - - let (block_watcher, rx) = - create_block_watcher(commit_interval, config, registry, fuel_adapter, storage); - - let handle = schedule_polling( - internal_config.fuel_polling_interval, - block_watcher, - "Block Watcher", - cancel_token, - ); - - (rx, handle, fuel_connection_health) -} - -pub fn spawn_wallet_balance_tracker( - internal_config: &InternalConfig, +pub fn wallet_balance_tracker( + internal_config: &config::Internal, registry: &Registry, l1: L1, cancel_token: CancellationToken, @@ -61,46 +29,50 @@ pub fn spawn_wallet_balance_tracker( ) } -pub fn spawn_l1_committer_and_listener( - internal_config: &InternalConfig, - rx_fuel_block: Receiver, +pub fn l1_event_listener( + internal_config: &config::Internal, l1: L1, storage: Database, registry: &Registry, cancel_token: CancellationToken, -) -> (tokio::task::JoinHandle<()>, tokio::task::JoinHandle<()>) { - let committer_handler = create_block_committer(rx_fuel_block, l1.clone(), storage.clone()); - - let commit_listener = CommitListener::new(l1, storage, cancel_token.clone()); - commit_listener.register_metrics(registry); +) -> tokio::task::JoinHandle<()> { + let commit_listener_service = CommitListener::new(l1, storage, cancel_token.clone()); + commit_listener_service.register_metrics(registry); - let listener_handle = schedule_polling( + schedule_polling( internal_config.between_eth_event_stream_restablishing_attempts, - commit_listener, + commit_listener_service, "Commit Listener", cancel_token, - ); - - (committer_handler, listener_handle) + ) } -fn create_block_committer( - rx_fuel_block: Receiver, +pub fn block_committer( + commit_interval: NonZeroU32, l1: L1, storage: impl Storage + 'static, + fuel: FuelApi, + config: &config::Config, + registry: &Registry, + cancel_token: CancellationToken, ) -> tokio::task::JoinHandle<()> { - let mut block_committer = BlockCommitter::new(rx_fuel_block, l1, storage); - tokio::spawn(async move { - block_committer - .run() - .await - .expect("Errors are handled inside of run"); - }) + let validator = BlockValidator::new(config.fuel.block_producer_public_key); + + let block_committer = BlockCommitter::new(l1, storage, fuel, validator, commit_interval); + + block_committer.register_metrics(registry); + + schedule_polling( + config.app.block_check_interval, + block_committer, + "Block Committer", + cancel_token, + ) } -pub async fn create_l1_adapter( - config: &Config, - internal_config: &InternalConfig, +pub async fn l1_adapter( + config: &config::Config, + internal_config: &config::Internal, registry: &Registry, ) -> Result<(L1, HealthChecker)> { let l1 = L1::connect( @@ -142,9 +114,9 @@ fn schedule_polling( }) } -fn create_fuel_adapter( - config: &Config, - internal_config: &InternalConfig, +pub fn fuel_adapter( + config: &config::Config, + internal_config: &config::Internal, registry: &Registry, ) -> (FuelApi, HealthChecker) { let fuel_adapter = FuelApi::new( @@ -158,30 +130,7 @@ fn create_fuel_adapter( (fuel_adapter, fuel_connection_health) } -fn create_block_watcher( - commit_interval: NonZeroU32, - config: &Config, - registry: &Registry, - fuel_adapter: FuelApi, - storage: Database, -) -> ( - BlockWatcher, - Receiver, -) { - let (tx_fuel_block, rx_fuel_block) = tokio::sync::mpsc::channel(100); - let block_watcher = BlockWatcher::new( - commit_interval, - tx_fuel_block, - fuel_adapter, - storage, - BlockValidator::new(config.fuel.block_producer_public_key), - ); - block_watcher.register_metrics(registry); - - (block_watcher, rx_fuel_block) -} - -pub fn setup_logger() { +pub fn logger() { tracing_subscriber::fmt() .with_writer(std::io::stderr) .with_level(true) @@ -190,7 +139,7 @@ pub fn setup_logger() { .init(); } -pub async fn setup_storage(config: &Config) -> Result { +pub async fn storage(config: &config::Config) -> Result { let postgres = Database::connect(&config.app.db).await?; postgres.migrate().await?; @@ -199,7 +148,6 @@ pub async fn setup_storage(config: &Config) -> Result { pub async fn shut_down( cancel_token: CancellationToken, - block_watcher_handle: JoinHandle<()>, wallet_balance_tracker_handle: JoinHandle<()>, committer_handle: JoinHandle<()>, listener_handle: JoinHandle<()>, @@ -208,7 +156,6 @@ pub async fn shut_down( cancel_token.cancel(); for handle in [ - block_watcher_handle, wallet_balance_tracker_handle, committer_handle, listener_handle, diff --git a/compose.yaml b/compose.yaml index e1770be8..bc80ffc8 100644 --- a/compose.yaml +++ b/compose.yaml @@ -38,6 +38,14 @@ services: interval: 2s timeout: 30s + postgres: + image: postgres:latest + container_name: committer-db + environment: + POSTGRES_USER: username + POSTGRES_PASSWORD: password + POSTGRES_DB: test + block_committer: container_name: block-committer depends_on: @@ -62,14 +70,6 @@ services: retries: 50 interval: 2s timeout: 30s - - - postgres: - image: postgres:latest - environment: - POSTGRES_USER: username - POSTGRES_PASSWORD: password - POSTGRES_DB: test networks: default: diff --git a/configurations/development/config.toml b/configurations/development/config.toml index 639a558c..e5f073b6 100644 --- a/configurations/development/config.toml +++ b/configurations/development/config.toml @@ -11,6 +11,7 @@ block_producer_public_key = "0x73dc6cc8cc0041e4924954b35a71a22ccb520664c522198a6 [app] port = 8080 host = "0.0.0.0" +block_check_interval = "1s" [app.db] host = "localhost" diff --git a/eth_node/Dockerfile b/eth_node/Dockerfile index ea26a62b..ef271717 100644 --- a/eth_node/Dockerfile +++ b/eth_node/Dockerfile @@ -2,7 +2,7 @@ FROM alpine:3.19.1 AS fetcher RUN apk add --no-cache git RUN git clone --no-checkout https://github.com/FuelLabs/fuel-bridge \ && cd fuel-bridge \ - && git checkout c23b1f4 \ + && git checkout 85a54c9 \ && cd packages/solidity-contracts \ && rm -rf deploy deployments exports test \ && cd contracts \ diff --git a/eth_node/run.sh b/eth_node/run.sh index f1885252..d59dad35 100755 --- a/eth_node/run.sh +++ b/eth_node/run.sh @@ -1,8 +1,14 @@ #!/bin/sh set -e + PORT="9545" IP="0.0.0.0" +# Deployment needs to be done every time the container starts +if [[ -f /contracts_deployed ]]; then + rm /contracts_deployed +fi + # Functions replace_placeholders() { diff --git a/packages/eth/Cargo.toml b/packages/eth/Cargo.toml index e5360619..365cdc55 100644 --- a/packages/eth/Cargo.toml +++ b/packages/eth/Cargo.toml @@ -22,7 +22,7 @@ url = { workspace = true } [dev-dependencies] mockall = { workspace = true } -ports = { workspace = true, features = ["l1"] } +ports = { workspace = true, features = ["l1", "test-helpers"] } tokio = { workspace = true, features = ["macros"] } [features] diff --git a/packages/ports/Cargo.toml b/packages/ports/Cargo.toml index 2f08ce96..57129841 100644 --- a/packages/ports/Cargo.toml +++ b/packages/ports/Cargo.toml @@ -22,8 +22,19 @@ thiserror = { workspace = true, optional = true } validator = { workspace = true, optional = true } [features] -test-helpers = ["dep:mockall", "dep:rand"] -l1 = ["dep:ethers-core", "dep:futures", "dep:thiserror", "dep:async-trait"] -fuel = ["dep:thiserror", "dep:async-trait", "dep:fuel-core-client", "dep:validator"] +test-helpers = ["dep:mockall", "dep:rand", "validator?/test-helpers"] +l1 = [ + "dep:ethers-core", + "dep:futures", + "dep:thiserror", + "dep:async-trait", + "dep:validator", +] +fuel = [ + "dep:thiserror", + "dep:async-trait", + "dep:fuel-core-client", + "dep:validator", +] storage = ["dep:impl-tools", "dep:thiserror", "dep:async-trait"] full = ["l1", "fuel", "storage"] diff --git a/packages/ports/src/types.rs b/packages/ports/src/types.rs index 4e11eefb..4af8c53f 100644 --- a/packages/ports/src/types.rs +++ b/packages/ports/src/types.rs @@ -12,5 +12,5 @@ pub use block_submission::*; #[cfg(feature = "l1")] pub use fuel_block_committed_on_l1::*; pub use l1_height::*; -#[cfg(feature = "fuel")] +#[cfg(any(feature = "fuel", feature = "l1"))] pub use validator::block::*; diff --git a/packages/services/Cargo.toml b/packages/services/Cargo.toml index 383238a4..6f4f7c87 100644 --- a/packages/services/Cargo.toml +++ b/packages/services/Cargo.toml @@ -16,7 +16,6 @@ metrics = { workspace = true } ports = { workspace = true, features = ["full"] } serde = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true, features = ["sync"] } tokio-util = { workspace = true } tracing = { workspace = true } validator = { workspace = true } @@ -24,7 +23,9 @@ validator = { workspace = true } [dev-dependencies] fuel-crypto = { workspace = true, features = ["random"] } mockall = { workspace = true } +ports = { workspace = true, features = ["full", "test-helpers"] } rand = { workspace = true } storage = { workspace = true, features = ["test-helpers"] } tai64 = { workspace = true } +tokio = { workspace = true, features = ["macros"] } validator = { workspace = true, features = ["test-helpers"] } diff --git a/packages/services/src/block_committer.rs b/packages/services/src/block_committer.rs index 0f691e45..031fab30 100644 --- a/packages/services/src/block_committer.rs +++ b/packages/services/src/block_committer.rs @@ -1,41 +1,81 @@ +use std::num::NonZeroU32; + use async_trait::async_trait; +use metrics::{ + prometheus::{core::Collector, IntGauge, Opts}, + RegistersMetrics, +}; use ports::{ storage::Storage, types::{BlockSubmission, ValidatedFuelBlock}, }; -use tokio::sync::mpsc::Receiver; -use tracing::{error, info}; +use tracing::info; +use validator::Validator; use super::Runner; -use crate::Result; +use crate::{Error, Result}; -pub struct BlockCommitter { - rx_block: Receiver, - l1: C, +pub struct BlockCommitter { + l1_adapter: L1, + fuel_adapter: Fuel, storage: Db, + block_validator: BlockValidator, + commit_interval: NonZeroU32, + metrics: Metrics, } -impl BlockCommitter { - pub fn new(rx_block: Receiver, l1: L1, storage: Db) -> Self { +struct Metrics { + latest_fuel_block: IntGauge, +} + +impl RegistersMetrics + for BlockCommitter +{ + fn metrics(&self) -> Vec> { + vec![Box::new(self.metrics.latest_fuel_block.clone())] + } +} + +impl Default for Metrics { + fn default() -> Self { + let latest_fuel_block = IntGauge::with_opts(Opts::new( + "latest_fuel_block", + "The height of the latest fuel block.", + )) + .expect("fuel_network_errors metric to be correctly configured"); + + Self { latest_fuel_block } + } +} + +impl BlockCommitter { + pub fn new( + l1: L1, + storage: Db, + fuel_adapter: Fuel, + block_validator: BlockValidator, + commit_interval: NonZeroU32, + ) -> Self { Self { - rx_block, - l1, + l1_adapter: l1, storage, + fuel_adapter, + block_validator, + commit_interval, + metrics: Metrics::default(), } } - - async fn next_fuel_block_for_committal(&mut self) -> Option { - self.rx_block.recv().await - } } -impl BlockCommitter +impl BlockCommitter where - A: ports::l1::Contract + ports::l1::Api, + L1: ports::l1::Contract + ports::l1::Api, Db: Storage, + BlockValidator: Validator, + Fuel: ports::fuel::Api, { async fn submit_block(&self, fuel_block: ValidatedFuelBlock) -> Result<()> { - let submittal_height = self.l1.get_block_number().await?; + let submittal_height = self.l1_adapter.get_block_number().await?; let submission = BlockSubmission { block_hash: fuel_block.hash(), @@ -47,28 +87,83 @@ where self.storage.insert(submission).await?; // if we have a network failure the DB entry will be left at completed:false. - self.l1.submit(fuel_block).await?; + self.l1_adapter.submit(fuel_block).await?; Ok(()) } + + async fn fetch_latest_block(&self) -> Result { + let latest_block = self.fuel_adapter.latest_block().await?; + let validated_block = self.block_validator.validate(&latest_block)?; + + self.metrics + .latest_fuel_block + .set(i64::from(validated_block.height())); + + Ok(validated_block) + } + + async fn check_if_stale(&self, block_height: u32) -> Result { + let Some(submitted_height) = self.last_submitted_block_height().await? else { + return Ok(false); + }; + + Ok(submitted_height >= block_height) + } + + fn current_epoch_block_height(&self, current_block_height: u32) -> u32 { + current_block_height - (current_block_height % self.commit_interval) + } + + async fn last_submitted_block_height(&self) -> Result> { + Ok(self + .storage + .submission_w_latest_block() + .await? + .map(|submission| submission.block_height)) + } + + async fn fetch_block(&self, height: u32) -> Result { + let fuel_block = self + .fuel_adapter + .block_at_height(height) + .await? + .ok_or_else(|| { + Error::Other(format!( + "Fuel node could not provide block at height: {height}" + )) + })?; + + Ok(self.block_validator.validate(&fuel_block)?) + } } #[async_trait] -impl Runner for BlockCommitter +impl Runner for BlockCommitter where - A: ports::l1::Contract + ports::l1::Api, + L1: ports::l1::Contract + ports::l1::Api, Db: Storage, + Fuel: ports::fuel::Api, + BlockValidator: Validator, { async fn run(&mut self) -> Result<()> { - while let Some(fuel_block) = self.next_fuel_block_for_committal().await { - if let Err(error) = self.submit_block(fuel_block).await { - error!("{error}"); - } else { - info!("submitted {fuel_block:?}!"); - } + let current_block = self.fetch_latest_block().await?; + let current_epoch_block_height = self.current_epoch_block_height(current_block.height()); + + if self.check_if_stale(current_epoch_block_height).await? { + return Ok(()); } - info!("Block Committer stopped"); + let block = if current_block.height() == current_epoch_block_height { + current_block + } else { + self.fetch_block(current_epoch_block_height).await? + }; + + self.submit_block(block) + .await + .map_err(|e| Error::Other(e.to_string()))?; + info!("submitted {block:?}!"); Ok(()) } @@ -76,23 +171,35 @@ where #[cfg(test)] mod tests { - use std::num::NonZeroU32; - use std::time::Duration; + use std::sync::Arc; - use mockall::predicate; + use fuel_crypto::{Message, SecretKey, Signature}; + use metrics::prometheus::{proto::Metric, Registry}; + use rand::{rngs::StdRng, Rng, SeedableRng}; + use validator::BlockValidator; + + use super::*; + + use mockall::predicate::{self, eq}; use ports::{ - l1::{Contract, EventStreamer, MockApi, MockContract}, + fuel::{FuelBlock, FuelBlockId, FuelConsensus, FuelHeader, FuelPoAConsensus}, + l1::{Contract, EventStreamer, MockContract}, types::{L1Height, U256}, }; - use rand::Rng; - use storage::PostgresProcess; - - use super::*; + use storage::{Postgres, PostgresProcess}; struct MockL1 { - api: MockApi, + api: ports::l1::MockApi, contract: MockContract, } + impl MockL1 { + fn new() -> Self { + Self { + api: ports::l1::MockApi::new(), + contract: MockContract::new(), + } + } + } #[async_trait::async_trait] impl Contract for MockL1 { @@ -108,66 +215,246 @@ mod tests { } } - #[cfg_attr(feature = "test-helpers", mockall::automock)] #[async_trait::async_trait] impl ports::l1::Api for MockL1 { async fn get_block_number(&self) -> ports::l1::Result { self.api.get_block_number().await } + async fn balance(&self) -> ports::l1::Result { self.api.balance().await } } + fn given_l1_that_expects_submission(block: ValidatedFuelBlock) -> MockL1 { + let mut l1 = MockL1::new(); + + l1.contract + .expect_submit() + .with(predicate::eq(block)) + .return_once(move |_| Ok(())); + + l1.api + .expect_get_block_number() + .return_once(move || Ok(0u32.into())); + + l1 + } + #[tokio::test] - async fn block_committer_will_submit_and_write_block() { + async fn will_fetch_and_submit_missed_block() { // given - let (tx, rx) = tokio::sync::mpsc::channel(10); - let block: ValidatedFuelBlock = rand::thread_rng().gen(); - let expected_height = block.height(); + let secret_key = given_secret_key(); + let block_validator = BlockValidator::new(secret_key.public_key()); + let missed_block = given_a_block(4, &secret_key); + let latest_block = given_a_block(5, &secret_key); + let fuel_adapter = given_fetcher(vec![latest_block, missed_block.clone()]); + + let validated_missed_block = ValidatedFuelBlock::new(*missed_block.id, 4); + let l1 = given_l1_that_expects_submission(validated_missed_block); let process = PostgresProcess::shared().await.unwrap(); - let db = process.create_random_db().await.unwrap(); + let db = db_with_submissions(&process, vec![0, 2]).await; + let mut block_committer = + BlockCommitter::new(l1, db, fuel_adapter, block_validator, 2.try_into().unwrap()); + + // when + block_committer.run().await.unwrap(); + + // then + // MockL1 validates the expected calls are made + } + + #[tokio::test] + async fn will_not_reattempt_submitting_missed_block() { + // given + let secret_key = given_secret_key(); + let block_validator = BlockValidator::new(secret_key.public_key()); + let missed_block = given_a_block(4, &secret_key); + let latest_block = given_a_block(5, &secret_key); + let fuel_adapter = given_fetcher(vec![latest_block, missed_block]); + + let process = PostgresProcess::shared().await.unwrap(); + let db = db_with_submissions(&process, vec![0, 2, 4]).await; + + let mut l1 = MockL1::new(); + l1.contract.expect_submit().never(); - let mock_l1 = given_l1_that_expects_submission(block); - tx.try_send(block).unwrap(); + let mut block_committer = + BlockCommitter::new(l1, db, fuel_adapter, block_validator, 2.try_into().unwrap()); // when - spawn_committer_and_run_until_timeout(rx, mock_l1, db.clone()).await; + block_committer.run().await.unwrap(); // then - let last_submission = db.submission_w_latest_block().await.unwrap().unwrap(); - assert_eq!(expected_height, last_submission.block_height); + // Mock verifies that the submit didn't happen } - fn given_l1_that_expects_submission(block: ValidatedFuelBlock) -> MockL1 { - let mut l1 = MockL1 { - api: MockApi::new(), - contract: MockContract::new(), - }; - l1.contract - .expect_submit() - .with(predicate::eq(block)) - .return_once(move |_| Ok(())); + #[tokio::test] + async fn will_not_reattempt_committing_latest_block() { + // given + let secret_key = given_secret_key(); + let block_validator = BlockValidator::new(secret_key.public_key()); + let latest_block = given_a_block(6, &secret_key); + let fuel_adapter = given_fetcher(vec![latest_block]); - l1.api - .expect_get_block_number() - .return_once(move || Ok(0u32.into())); + let process = PostgresProcess::shared().await.unwrap(); + let db = db_with_submissions(&process, vec![0, 2, 4, 6]).await; - l1 + let mut l1 = MockL1::new(); + l1.contract.expect_submit().never(); + + let mut block_committer = + BlockCommitter::new(l1, db, fuel_adapter, block_validator, 2.try_into().unwrap()); + + // when + block_committer.run().await.unwrap(); + + // then + // MockL1 verifies that submit was not called } - async fn spawn_committer_and_run_until_timeout( - rx: Receiver, - mock_l1: MockL1, - storage: Db, - ) { - let _ = tokio::time::timeout(Duration::from_millis(250), async move { - let mut block_committer = BlockCommitter::new(rx, mock_l1, storage); - block_committer - .run() - .await - .expect("Errors are handled inside of run"); - }) - .await; + #[tokio::test] + async fn propagates_block_if_epoch_reached() { + // given + let secret_key = given_secret_key(); + let block_validator = BlockValidator::new(secret_key.public_key()); + let block = given_a_block(4, &secret_key); + let fuel_adapter = given_fetcher(vec![block.clone()]); + + let process = PostgresProcess::shared().await.unwrap(); + let db = db_with_submissions(&process, vec![0, 2]).await; + let l1 = given_l1_that_expects_submission(ValidatedFuelBlock::new(*block.id, 4)); + let mut block_committer = + BlockCommitter::new(l1, db, fuel_adapter, block_validator, 2.try_into().unwrap()); + + // when + block_committer.run().await.unwrap(); + + // then + // Mock verifies that submit was called with the appropriate block + } + + #[tokio::test] + async fn updates_block_metric_regardless_if_block_is_published() { + // given + let secret_key = given_secret_key(); + let block_validator = BlockValidator::new(secret_key.public_key()); + let block = given_a_block(5, &secret_key); + let fuel_adapter = given_fetcher(vec![block]); + + let process = PostgresProcess::shared().await.unwrap(); + let db = db_with_submissions(&process, vec![0, 2, 4]).await; + + let mut l1 = MockL1::new(); + l1.contract.expect_submit().never(); + + let mut block_committer = + BlockCommitter::new(l1, db, fuel_adapter, block_validator, 2.try_into().unwrap()); + + let registry = Registry::default(); + block_committer.register_metrics(®istry); + + // when + block_committer.run().await.unwrap(); + + //then + let metrics = registry.gather(); + let latest_block_metric = metrics + .iter() + .find(|metric| metric.get_name() == "latest_fuel_block") + .and_then(|metric| metric.get_metric().first()) + .map(Metric::get_gauge) + .unwrap(); + + assert_eq!(latest_block_metric.get_value(), 5f64); + } + + async fn db_with_submissions( + process: &Arc, + pending_submissions: Vec, + ) -> Postgres { + let db = process.create_random_db().await.unwrap(); + for height in pending_submissions { + db.insert(given_a_pending_submission(height)).await.unwrap(); + } + + db + } + + fn given_fetcher(available_blocks: Vec) -> ports::fuel::MockApi { + let mut fetcher = ports::fuel::MockApi::new(); + for block in available_blocks.clone() { + fetcher + .expect_block_at_height() + .with(eq(block.header.height)) + .returning(move |_| Ok(Some(block.clone()))); + } + if let Some(block) = available_blocks + .into_iter() + .max_by_key(|el| el.header.height) + { + fetcher + .expect_latest_block() + .returning(move || Ok(block.clone())); + } + + fetcher + } + + fn given_a_pending_submission(block_height: u32) -> BlockSubmission { + let mut submission: BlockSubmission = rand::thread_rng().gen(); + submission.block_height = block_height; + + submission + } + + fn given_a_block(height: u32, secret_key: &SecretKey) -> FuelBlock { + let header = given_header(height); + + let mut hasher = fuel_crypto::Hasher::default(); + hasher.input(header.prev_root.as_ref()); + hasher.input(header.height.to_be_bytes()); + hasher.input(header.time.0.to_be_bytes()); + hasher.input(header.application_hash.as_ref()); + + let id = FuelBlockId::from(hasher.digest()); + let id_message = Message::from_bytes(*id); + let signature = Signature::sign(secret_key, &id_message); + + FuelBlock { + id, + header, + consensus: FuelConsensus::PoAConsensus(FuelPoAConsensus { signature }), + transactions: vec![], + block_producer: Some(secret_key.public_key()), + } + } + + fn given_header(height: u32) -> FuelHeader { + let application_hash = "0x017ab4b70ea129c29e932d44baddc185ad136bf719c4ada63a10b5bf796af91e" + .parse() + .unwrap(); + + FuelHeader { + id: Default::default(), + da_height: Default::default(), + consensus_parameters_version: Default::default(), + state_transition_bytecode_version: Default::default(), + transactions_count: Default::default(), + message_receipt_count: Default::default(), + transactions_root: Default::default(), + message_outbox_root: Default::default(), + event_inbox_root: Default::default(), + height, + prev_root: Default::default(), + time: tai64::Tai64(0), + application_hash, + } + } + + fn given_secret_key() -> SecretKey { + let mut rng = StdRng::seed_from_u64(42); + + SecretKey::random(&mut rng) } } diff --git a/packages/services/src/block_watcher.rs b/packages/services/src/block_watcher.rs deleted file mode 100644 index f85259a9..00000000 --- a/packages/services/src/block_watcher.rs +++ /dev/null @@ -1,387 +0,0 @@ -use std::{num::NonZeroU32, vec}; - -use async_trait::async_trait; -use metrics::{ - prometheus::{core::Collector, IntGauge, Opts}, - RegistersMetrics, -}; -use ports::{storage::Storage, types::ValidatedFuelBlock}; -use tokio::sync::mpsc::Sender; -use validator::Validator; - -use super::Runner; -use crate::{Error, Result}; - -struct Metrics { - latest_fuel_block: IntGauge, -} - -impl RegistersMetrics for BlockWatcher { - fn metrics(&self) -> Vec> { - vec![Box::new(self.metrics.latest_fuel_block.clone())] - } -} - -impl Default for Metrics { - fn default() -> Self { - let latest_fuel_block = IntGauge::with_opts(Opts::new( - "latest_fuel_block", - "The height of the latest fuel block.", - )) - .expect("fuel_network_errors metric to be correctly configured"); - - Self { latest_fuel_block } - } -} - -pub struct BlockWatcher { - fuel_adapter: A, - tx_fuel_block: Sender, - storage: Db, - block_validator: V, - commit_interval: NonZeroU32, - metrics: Metrics, -} - -impl BlockWatcher { - pub fn new( - commit_interval: NonZeroU32, - tx_fuel_block: Sender, - fuel_adapter: A, - storage: Db, - block_validator: V, - ) -> Self { - Self { - commit_interval, - fuel_adapter, - tx_fuel_block, - storage, - block_validator, - metrics: Metrics::default(), - } - } -} - -impl BlockWatcher -where - A: ports::fuel::Api, - Db: Storage, - V: Validator, -{ - async fn fetch_latest_block(&self) -> Result { - let latest_block = self.fuel_adapter.latest_block().await?; - let validated_block = self.block_validator.validate(&latest_block)?; - - self.metrics - .latest_fuel_block - .set(i64::from(validated_block.height())); - - Ok(validated_block) - } - - async fn check_if_stale(&self, block_height: u32) -> Result { - let Some(submitted_height) = self.last_submitted_block_height().await? else { - return Ok(false); - }; - - Ok(submitted_height >= block_height) - } - - fn current_epoch_block_height(&self, current_block_height: u32) -> u32 { - current_block_height - (current_block_height % self.commit_interval) - } - - async fn last_submitted_block_height(&self) -> Result> { - Ok(self - .storage - .submission_w_latest_block() - .await? - .map(|submission| submission.block_height)) - } - - async fn fetch_block(&self, height: u32) -> Result { - let fuel_block = self - .fuel_adapter - .block_at_height(height) - .await? - .ok_or_else(|| { - Error::Other(format!( - "Fuel node could not provide block at height: {height}" - )) - })?; - - Ok(self.block_validator.validate(&fuel_block)?) - } -} - -#[async_trait] -impl Runner for BlockWatcher -where - A: ports::fuel::Api, - Db: Storage, - V: Validator, -{ - async fn run(&mut self) -> Result<()> { - let current_block = self.fetch_latest_block().await?; - let current_epoch_block_height = self.current_epoch_block_height(current_block.height()); - - if self.check_if_stale(current_epoch_block_height).await? { - return Ok(()); - } - - let block = if current_block.height() == current_epoch_block_height { - current_block - } else { - self.fetch_block(current_epoch_block_height).await? - }; - - self.tx_fuel_block - .send(block) - .await - .map_err(|e| Error::Other(e.to_string()))?; - - Ok(()) - } -} - -#[cfg(test)] -mod tests { - use std::{sync::Arc, vec}; - - use fuel_crypto::{Message, SecretKey, Signature}; - use metrics::prometheus::{proto::Metric, Registry}; - use mockall::predicate::eq; - use ports::{ - fuel::{FuelBlock, FuelBlockId, FuelConsensus, FuelHeader, FuelPoAConsensus, MockApi}, - types::BlockSubmission, - }; - use rand::{rngs::StdRng, Rng, SeedableRng}; - use storage::{Postgres, PostgresProcess}; - use validator::BlockValidator; - - use super::*; - - #[tokio::test] - async fn will_fetch_and_propagate_missed_block() { - // given - let (tx, mut rx) = tokio::sync::mpsc::channel(10); - - let secret_key = given_secret_key(); - let block_validator = BlockValidator::new(secret_key.public_key()); - let missed_block = given_a_block(4, &secret_key); - let latest_block = given_a_block(5, &secret_key); - let fuel_adapter = given_fetcher(vec![latest_block, missed_block.clone()]); - - let process = PostgresProcess::shared().await.unwrap(); - let db = db_with_submissions(&process, vec![0, 2]).await; - let mut block_watcher = - BlockWatcher::new(2.try_into().unwrap(), tx, fuel_adapter, db, block_validator); - - // when - block_watcher.run().await.unwrap(); - - //then - let Ok(announced_block) = rx.try_recv() else { - panic!("Didn't receive the block") - }; - - assert_eq!(announced_block, missed_block.into()); - } - - #[tokio::test] - async fn will_not_reattempt_committing_missed_block() { - // given - let (tx, mut rx) = tokio::sync::mpsc::channel(10); - - let secret_key = given_secret_key(); - let block_validator = BlockValidator::new(secret_key.public_key()); - let missed_block = given_a_block(4, &secret_key); - let latest_block = given_a_block(5, &secret_key); - let fuel_adapter = given_fetcher(vec![latest_block, missed_block]); - - let process = PostgresProcess::shared().await.unwrap(); - let db = db_with_submissions(&process, vec![0, 2, 4]).await; - let mut block_watcher = - BlockWatcher::new(2.try_into().unwrap(), tx, fuel_adapter, db, block_validator); - - // when - block_watcher.run().await.unwrap(); - - //then - if let Ok(block) = rx.try_recv() { - panic!("Should not have received a block. Block: {block:?}"); - } - } - - #[tokio::test] - async fn will_not_reattempt_committing_latest_block() { - // given - let (tx, mut rx) = tokio::sync::mpsc::channel(10); - - let secret_key = given_secret_key(); - let block_validator = BlockValidator::new(secret_key.public_key()); - let latest_block = given_a_block(6, &secret_key); - let fuel_adapter = given_fetcher(vec![latest_block]); - - let process = PostgresProcess::shared().await.unwrap(); - let db = db_with_submissions(&process, vec![0, 2, 4, 6]).await; - let mut block_watcher = - BlockWatcher::new(2.try_into().unwrap(), tx, fuel_adapter, db, block_validator); - - // when - block_watcher.run().await.unwrap(); - - //then - if let Ok(block) = rx.try_recv() { - panic!("Should not have received a block. Block: {block:?}"); - } - } - - #[tokio::test] - async fn propagates_block_if_epoch_reached() { - // given - let (tx, mut rx) = tokio::sync::mpsc::channel(10); - - let secret_key = given_secret_key(); - let block_validator = BlockValidator::new(secret_key.public_key()); - let block = given_a_block(4, &secret_key); - let fuel_adapter = given_fetcher(vec![block.clone()]); - - let process = PostgresProcess::shared().await.unwrap(); - let db = db_with_submissions(&process, vec![0, 2]).await; - let mut block_watcher = - BlockWatcher::new(2.try_into().unwrap(), tx, fuel_adapter, db, block_validator); - - // when - block_watcher.run().await.unwrap(); - - //then - let Ok(announced_block) = rx.try_recv() else { - panic!("Didn't receive the block") - }; - - assert_eq!(announced_block, block.into()); - } - - #[tokio::test] - async fn updates_block_metric_regardless_if_block_is_published() { - // given - let (tx, _) = tokio::sync::mpsc::channel(10); - - let secret_key = given_secret_key(); - let block_validator = BlockValidator::new(secret_key.public_key()); - let block = given_a_block(5, &secret_key); - let fuel_adapter = given_fetcher(vec![block]); - - let process = PostgresProcess::shared().await.unwrap(); - let db = db_with_submissions(&process, vec![0, 2, 4]).await; - let mut block_watcher = - BlockWatcher::new(2.try_into().unwrap(), tx, fuel_adapter, db, block_validator); - - let registry = Registry::default(); - block_watcher.register_metrics(®istry); - - // when - block_watcher.run().await.unwrap(); - - //then - let metrics = registry.gather(); - let latest_block_metric = metrics - .iter() - .find(|metric| metric.get_name() == "latest_fuel_block") - .and_then(|metric| metric.get_metric().first()) - .map(Metric::get_gauge) - .unwrap(); - - assert_eq!(latest_block_metric.get_value(), 5f64); - } - - async fn db_with_submissions( - process: &Arc, - pending_submissions: Vec, - ) -> Postgres { - let db = process.create_random_db().await.unwrap(); - for height in pending_submissions { - db.insert(given_a_pending_submission(height)).await.unwrap(); - } - - db - } - - fn given_fetcher(available_blocks: Vec) -> MockApi { - let mut fetcher = MockApi::new(); - for block in available_blocks.clone() { - fetcher - .expect_block_at_height() - .with(eq(block.header.height)) - .returning(move |_| Ok(Some(block.clone()))); - } - if let Some(block) = available_blocks - .into_iter() - .max_by_key(|el| el.header.height) - { - fetcher - .expect_latest_block() - .returning(move || Ok(block.clone())); - } - - fetcher - } - - fn given_a_pending_submission(block_height: u32) -> BlockSubmission { - let mut submission: BlockSubmission = rand::thread_rng().gen(); - submission.block_height = block_height; - - submission - } - - fn given_a_block(height: u32, secret_key: &SecretKey) -> FuelBlock { - let header = given_header(height); - - let mut hasher = fuel_crypto::Hasher::default(); - hasher.input(header.prev_root.as_ref()); - hasher.input(header.height.to_be_bytes()); - hasher.input(header.time.0.to_be_bytes()); - hasher.input(header.application_hash.as_ref()); - - let id = FuelBlockId::from(hasher.digest()); - let id_message = Message::from_bytes(*id); - let signature = Signature::sign(secret_key, &id_message); - - FuelBlock { - id, - header, - consensus: FuelConsensus::PoAConsensus(FuelPoAConsensus { signature }), - transactions: vec![], - block_producer: Some(secret_key.public_key()), - } - } - - fn given_header(height: u32) -> FuelHeader { - let application_hash = "0x017ab4b70ea129c29e932d44baddc185ad136bf719c4ada63a10b5bf796af91e" - .parse() - .unwrap(); - - FuelHeader { - id: Default::default(), - da_height: Default::default(), - consensus_parameters_version: Default::default(), - state_transition_bytecode_version: Default::default(), - transactions_count: Default::default(), - message_receipt_count: Default::default(), - transactions_root: Default::default(), - message_outbox_root: Default::default(), - event_inbox_root: Default::default(), - height, - prev_root: Default::default(), - time: tai64::Tai64(0), - application_hash, - } - } - - fn given_secret_key() -> SecretKey { - let mut rng = StdRng::seed_from_u64(42); - - SecretKey::random(&mut rng) - } -} diff --git a/packages/services/src/lib.rs b/packages/services/src/lib.rs index b7c00c8b..2682fc52 100644 --- a/packages/services/src/lib.rs +++ b/packages/services/src/lib.rs @@ -1,13 +1,11 @@ #![deny(unused_crate_dependencies)] mod block_committer; -mod block_watcher; mod commit_listener; mod health_reporter; mod status_reporter; mod wallet_balance_tracker; pub use block_committer::BlockCommitter; -pub use block_watcher::BlockWatcher; pub use commit_listener::CommitListener; pub use health_reporter::HealthReporter; pub use status_reporter::StatusReporter; diff --git a/packages/storage/src/test_instance.rs b/packages/storage/src/test_instance.rs index 3be1cd7f..7182918b 100644 --- a/packages/storage/src/test_instance.rs +++ b/packages/storage/src/test_instance.rs @@ -66,7 +66,8 @@ impl PostgresProcess { .with_env_var(("POSTGRES_PASSWORD", &password)) .with_env_var(("POSTGRES_DB", &initial_db)) .start() - .await; + .await + .map_err(|e| ports::storage::Error::Database(format!("{e}")))?; Ok(Self { username, @@ -77,9 +78,15 @@ impl PostgresProcess { } pub async fn create_random_db(&self) -> ports::storage::Result { + let port = self + .container + .get_host_port_ipv4(5432) + .await + .map_err(|e| ports::storage::Error::Database(format!("{e}")))?; + let mut config = DbConfig { host: "localhost".to_string(), - port: self.container.get_host_port_ipv4(5432).await, + port, username: self.username.clone(), password: self.password.clone(), database: self.initial_db.clone(), diff --git a/packages/validator/Cargo.toml b/packages/validator/Cargo.toml index 879d0953..1e3c7ca0 100644 --- a/packages/validator/Cargo.toml +++ b/packages/validator/Cargo.toml @@ -10,15 +10,18 @@ publish = { workspace = true } rust-version = { workspace = true } [dependencies] -fuel-crypto = { workspace = true, optional = true } fuel-core-client = { workspace = true } +fuel-crypto = { workspace = true, optional = true } mockall = { workspace = true, optional = true } rand = { workspace = true, optional = true } serde = { workspace = true, features = ["derive"] } thiserror = { workspace = true } [dev-dependencies] +fuel-crypto = { workspace = true, features = ["random"] } +rand = { workspace = true, features = ["std", "std_rng"] } tai64 = { workspace = true } +validator = { workspace = true, features = ["validator", "test-helpers"] } [features] validator = ["dep:fuel-crypto"] diff --git a/packages/validator/src/lib.rs b/packages/validator/src/lib.rs index 4a865783..3ed5d36a 100644 --- a/packages/validator/src/lib.rs +++ b/packages/validator/src/lib.rs @@ -1,4 +1,3 @@ -#![deny(unused_crate_dependencies)] pub mod block; #[cfg(feature = "validator")] mod validator;