diff --git a/Cargo.lock b/Cargo.lock index 1da6263789..0d18721573 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1784,12 +1784,27 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + [[package]] name = "cast" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cbc" version = "0.1.2" @@ -2101,6 +2116,33 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "color_quant" version = "1.1.0" @@ -2123,6 +2165,20 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if 1.0.1", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "concat-kdf" version = "0.1.0" @@ -2141,7 +2197,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width 0.2.0", "windows-sys 0.59.0", ] @@ -2154,7 +2210,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width 0.2.1", + "unicode-width 0.2.0", "windows-sys 0.60.2", ] @@ -2224,6 +2280,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "cooked-waker" version = "5.0.0" @@ -2468,6 +2533,49 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "mio", + "parking_lot 0.12.4", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "derive_more 2.0.1", + "document-features", + "mio", + "parking_lot 0.12.4", + "rustix 1.0.7", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -2941,6 +3049,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ + "convert_case 0.7.1", "proc-macro2", "quote", "syn 2.0.104", @@ -3075,6 +3184,15 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ccf673e0848ef09fa4aeeba78e681cf651c0c7d35f76ee38cec8e55bc32fa111" +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "downcast-rs" version = "1.2.1" @@ -3353,6 +3471,16 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + [[package]] name = "env_logger" version = "0.8.4" @@ -3796,6 +3924,7 @@ dependencies = [ "tokio-util", "tracing", "tracing-subscriber 0.3.19", + "tui-logger", "vergen-git2", ] @@ -3876,6 +4005,10 @@ dependencies = [ "bincode", "bytes", "cfg-if 1.0.1", + "chrono", + "clap 4.5.40", + "color-eyre", + "crossterm 0.29.0", "directories", "envy", "ethereum-types 0.15.1", @@ -3886,6 +4019,7 @@ dependencies = [ "ethrex-l2-common", "ethrex-levm", "ethrex-metrics", + "ethrex-p2p", "ethrex-rlp", "ethrex-rpc", "ethrex-sdk", @@ -3898,6 +4032,7 @@ dependencies = [ "keccak-hash", "lazy_static", "rand 0.8.5", + "ratatui", "reqwest 0.12.22", "secp256k1", "serde", @@ -3905,10 +4040,14 @@ dependencies = [ "serde_with", "spawned-concurrency", "spawned-rt", + "tabwriter", "thiserror 2.0.12", "tokio", "tokio-util", "tracing", + "tui-big-text", + "tui-logger", + "tui-scrollview", "vergen-git2", "zkvm_interface", ] @@ -4498,6 +4637,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "font8x8" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" + [[package]] name = "fontconfig-parser" version = "0.5.8" @@ -5777,11 +5922,17 @@ checksum = "4adb2ee6ad319a912210a36e56e3623555817bcc877a7e6e8802d1d69c4d8056" dependencies = [ "console 0.16.0", "portable-atomic", - "unicode-width 0.2.1", + "unicode-width 0.2.0", "unit-prefix", "web-time", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + [[package]] name = "inout" version = "0.1.4" @@ -5792,6 +5943,19 @@ dependencies = [ "generic-array 0.14.7", ] +[[package]] +name = "instability" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "instant" version = "0.1.13" @@ -6423,6 +6587,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "load_test" version = "0.1.0" @@ -6776,6 +6946,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -7241,6 +7412,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + [[package]] name = "p256" version = "0.13.2" @@ -8496,6 +8673,27 @@ dependencies = [ "num-traits", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.9.1", + "cassowary", + "compact_str", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum 0.26.3", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "rawpointer" version = "0.2.1" @@ -8648,6 +8846,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "remove_dir_all" version = "0.5.3" @@ -9415,6 +9619,36 @@ dependencies = [ "paste", ] +[[package]] +name = "rstest" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version 0.4.1", +] + +[[package]] +name = "rstest_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +dependencies = [ + "cfg-if 1.0.1", + "glob", + "proc-macro-crate 3.3.0", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version 0.4.1", + "syn 2.0.104", + "unicode-ident", +] + [[package]] name = "ruint" version = "1.15.0" @@ -10206,6 +10440,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.5" @@ -11183,6 +11438,15 @@ dependencies = [ "libc", ] +[[package]] +name = "tabwriter" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce91f2f0ec87dff7e6bcbbeb267439aa1188703003c6055193c821487400432" +dependencies = [ + "unicode-width 0.2.0", +] + [[package]] name = "tap" version = "1.0.1" @@ -11894,6 +12158,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber 0.3.19", +] + [[package]] name = "tracing-forest" version = "0.1.6" @@ -11977,6 +12251,47 @@ dependencies = [ "tokio", ] +[[package]] +name = "tui-big-text" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97cefa9f1425ab6146db2961241cec86845d11105b5dd6bb504294b0cdd21af" +dependencies = [ + "derive_builder", + "font8x8", + "itertools 0.14.0", + "ratatui", +] + +[[package]] +name = "tui-logger" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ea457a31a3fff1073f83e5c9e1c61a7805c435b2476b1df3a78f934adebabe" +dependencies = [ + "chrono", + "env_filter", + "fxhash", + "lazy_static", + "log", + "parking_lot 0.12.4", + "ratatui", + "tracing", + "tracing-subscriber 0.3.19", + "unicode-segmentation", +] + +[[package]] +name = "tui-scrollview" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6e1d736488ba64c2e74637089a6b9ca666ccd2eaade3ab83854f415f1d260b" +dependencies = [ + "indoc", + "ratatui", + "rstest", +] + [[package]] name = "tungstenite" version = "0.20.1" @@ -12227,6 +12542,17 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "unicode-vo" version = "0.1.0" @@ -12241,9 +12567,9 @@ checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" diff --git a/Cargo.toml b/Cargo.toml index 7a09483555..2f29c151d8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -102,6 +102,7 @@ futures = "0.3.31" spawned-concurrency = {git = "https://github.com/lambdaclass/spawned.git", tag = "v0.1.2-alpha"} spawned-rt = {git = "https://github.com/lambdaclass/spawned.git", tag = "v0.1.2-alpha"} lambdaworks-crypto = "0.11.0" +tui-logger = { version = "0.17.3", features = ["tracing-support"] } [patch.crates-io] secp256k1 = { git = "https://github.com/sp1-patches/rust-secp256k1", tag = "patch-0.29.1-sp1-5.0.0" } diff --git a/cmd/ethrex/Cargo.toml b/cmd/ethrex/Cargo.toml index 7c8606f35d..8d3c027f07 100644 --- a/cmd/ethrex/Cargo.toml +++ b/cmd/ethrex/Cargo.toml @@ -42,6 +42,7 @@ secp256k1 = { workspace = true } keccak-hash.workspace = true reqwest.workspace = true itertools = "0.14.0" +tui-logger.workspace = true cfg-if = "1.0.0" diff --git a/cmd/ethrex/ethrex.rs b/cmd/ethrex/ethrex.rs index a7ad4e0bda..564274acb6 100644 --- a/cmd/ethrex/ethrex.rs +++ b/cmd/ethrex/ethrex.rs @@ -63,12 +63,12 @@ async fn server_shutdown( async fn main() -> eyre::Result<()> { let CLI { opts, command } = CLI::parse(); - init_tracing(&opts); - if let Some(subcommand) = command { return subcommand.run(&opts).await; } + init_tracing(&opts); + let data_dir = set_datadir(&opts.datadir); let network = get_network(&opts); diff --git a/cmd/ethrex/l2/command.rs b/cmd/ethrex/l2/command.rs index 245bd75477..efd0731008 100644 --- a/cmd/ethrex/l2/command.rs +++ b/cmd/ethrex/l2/command.rs @@ -127,6 +127,8 @@ impl Command { panic!("L2 Doesn't support REVM, use LEVM instead."); } + l2::initializers::init_tracing(&opts); + let data_dir = set_datadir(&opts.node_opts.datadir); let rollup_store_dir = data_dir.clone() + "/rollup_store"; @@ -267,7 +269,7 @@ impl Command { current_block, current_block, contract_address, - event_signature, + vec![event_signature], ) .await?; diff --git a/cmd/ethrex/l2/initializers.rs b/cmd/ethrex/l2/initializers.rs index 67e316f4ee..b3cf6b1578 100644 --- a/cmd/ethrex/l2/initializers.rs +++ b/cmd/ethrex/l2/initializers.rs @@ -13,9 +13,12 @@ use tokio::sync::Mutex; use tokio_util::sync::CancellationToken; use tokio_util::task::TaskTracker; use tracing::warn; +use tracing_subscriber::EnvFilter; +use tracing_subscriber::layer::SubscriberExt; +use tui_logger::{LevelFilter, TuiTracingSubscriberLayer}; use crate::cli::Options as L1Options; -use crate::initializers::{get_authrpc_socket_addr, get_http_socket_addr}; +use crate::initializers::{self, get_authrpc_socket_addr, get_http_socket_addr}; use crate::l2::L2Options; use crate::utils::{get_client_version, read_jwtsecret_file}; @@ -114,3 +117,18 @@ pub fn init_metrics(opts: &L1Options, tracker: TaskTracker) { ); tracker.spawn(metrics_api); } + +pub fn init_tracing(opts: &L2Options) { + if opts.sequencer_opts.monitor { + let level_filter = EnvFilter::builder() + .parse_lossy("debug,tower_http::trace=debug,reqwest_tracing=off,hyper=off,libsql=off,ethrex::initializers=off,ethrex::l2::initializers=off,ethrex::l2::command=off"); + let subscriber = tracing_subscriber::registry() + .with(TuiTracingSubscriberLayer) + .with(level_filter); + tracing::subscriber::set_global_default(subscriber) + .expect("setting default subscriber failed"); + tui_logger::init_logger(LevelFilter::max()).expect("Failed to initialize tui_logger"); + } else { + initializers::init_tracing(&opts.node_opts); + } +} diff --git a/cmd/ethrex/l2/options.rs b/cmd/ethrex/l2/options.rs index 49bf700190..0ed1146b0d 100644 --- a/cmd/ethrex/l2/options.rs +++ b/cmd/ethrex/l2/options.rs @@ -4,7 +4,10 @@ use ethrex_common::Address; use ethrex_l2::{ BasedConfig, BlockFetcherConfig, BlockProducerConfig, CommitterConfig, EthConfig, L1WatcherConfig, ProofCoordinatorConfig, SequencerConfig, StateUpdaterConfig, - sequencer::{configs::AlignedConfig, utils::resolve_aligned_network}, + sequencer::{ + configs::{AlignedConfig, MonitorConfig}, + utils::resolve_aligned_network, + }, }; use ethrex_rpc::clients::eth::{ BACKOFF_FACTOR, MAX_NUMBER_OF_RETRIES, MAX_RETRY_DELAY, MIN_RETRY_DELAY, @@ -62,6 +65,8 @@ pub struct SequencerOptions { pub based_opts: BasedOptions, #[command(flatten)] pub aligned_opts: AlignedOptions, + #[command(flatten)] + pub monitor_opts: MonitorOptions, #[arg( long = "validium", default_value = "false", @@ -79,6 +84,14 @@ pub struct SequencerOptions { help_heading = "Based options" )] pub based: bool, + #[clap( + long, + default_value = "false", + value_name = "BOOLEAN", + env = "ETHREX_MONITOR", + help_heading = "Sequencer options" + )] + pub monitor: bool, } impl From for SequencerConfig { @@ -154,6 +167,10 @@ impl From for SequencerConfig { fee_estimate: opts.aligned_opts.fee_estimate, aligned_sp1_elf_path: opts.aligned_opts.aligned_sp1_elf_path.unwrap_or_default(), }, + monitor: MonitorConfig { + enabled: opts.monitor, + tick_rate: opts.monitor_opts.tick_rate, + }, } } } @@ -549,3 +566,10 @@ pub struct BlockFetcherOptions { )] pub fetch_block_step: u64, } + +#[derive(Parser, Default)] +pub struct MonitorOptions { + /// time in ms between two ticks. + #[arg(short, long, default_value_t = 1000)] + tick_rate: u64, +} diff --git a/crates/common/types/transaction.rs b/crates/common/types/transaction.rs index bd3afb08fa..c4b9c36cc3 100644 --- a/crates/common/types/transaction.rs +++ b/crates/common/types/transaction.rs @@ -1,4 +1,4 @@ -use std::cmp::min; +use std::{cmp::min, fmt::Display}; use bytes::Bytes; use ethereum_types::{Address, H256, U256}; @@ -274,6 +274,19 @@ impl From for u8 { } } +impl Display for TxType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TxType::Legacy => write!(f, "Legacy"), + TxType::EIP2930 => write!(f, "EIP2930"), + TxType::EIP1559 => write!(f, "EIP1559"), + TxType::EIP4844 => write!(f, "EIP4844"), + TxType::EIP7702 => write!(f, "EIP7702"), + TxType::Privileged => write!(f, "Privileged"), + } + } +} + pub trait Signable { /// Both sign and sign_in_place return error when the payload hash calculated that has to /// be signed is not exactly 32 bytes long. Currently it's an unreachable error @@ -1925,7 +1938,7 @@ mod serde_impl { ) .map(Transaction::PrivilegedL2Transaction) .map_err(|e| { - serde::de::Error::custom(format!("Couldn't Deserialize Privileged {e}")) + serde::de::Error::custom(format!("Couldn't Deserialize Privileged: {e}")) }), } } diff --git a/crates/l2/Cargo.toml b/crates/l2/Cargo.toml index dda10cde8b..3565a4fc52 100644 --- a/crates/l2/Cargo.toml +++ b/crates/l2/Cargo.toml @@ -26,6 +26,7 @@ ethrex-l2-common.workspace = true ethrex-dev = { path = "../../crates/blockchain/dev", default-features = false } ethrex-metrics = { path = "../blockchain/metrics", default-features = false } ethrex-sdk = { path = "./sdk" } +ethrex-p2p.workspace = true hex.workspace = true bytes.workspace = true jsonwebtoken.workspace = true @@ -43,11 +44,18 @@ lazy_static.workspace = true aligned-sdk = { git = "https://github.com/yetanotherco/aligned_layer", tag = "v0.16.1" } ethers = "2.0" cfg-if.workspace = true +chrono = "0.4.41" +clap.workspace = true +crossterm = "0.29.0" +ratatui = "0.29.0" +tui-big-text = "0.7.1" +tui-scrollview = "0.5.1" +tui-logger.workspace = true +tabwriter = "1.4.1" +color-eyre = "0.6.5" zkvm_interface = { path = "./prover/zkvm/interface/" } -[dev-dependencies] -rand = "0.8.5" [build-dependencies] vergen-git2 = { version = "1.0.7" } diff --git a/crates/l2/based/block_fetcher.rs b/crates/l2/based/block_fetcher.rs index 7c6b52011b..3c151b3696 100644 --- a/crates/l2/based/block_fetcher.rs +++ b/crates/l2/based/block_fetcher.rs @@ -242,7 +242,7 @@ async fn get_logs( state.last_l1_block_fetched + 1, new_last_l1_fetched_block, state.on_chain_proposer_address, - keccak(b"BatchCommitted(uint256,bytes32)"), + vec![keccak(b"BatchCommitted(uint256,bytes32)")], ) .await?; @@ -253,7 +253,7 @@ async fn get_logs( state.last_l1_block_fetched + 1, new_last_l1_fetched_block, state.on_chain_proposer_address, - keccak(b"BatchVerified(uint256)"), + vec![keccak(b"BatchVerified(uint256)")], ) .await?; diff --git a/crates/l2/based/sequencer_state.rs b/crates/l2/based/sequencer_state.rs index 5e1349018a..c774257b53 100644 --- a/crates/l2/based/sequencer_state.rs +++ b/crates/l2/based/sequencer_state.rs @@ -1,5 +1,6 @@ use std::sync::Arc; +use serde::{Deserialize, Serialize}; use tokio::sync::Mutex; #[derive(Debug, Clone)] @@ -21,7 +22,7 @@ impl From for SequencerState { } } -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, Serialize, Deserialize)] pub enum SequencerStatus { Sequencing, #[default] diff --git a/crates/l2/l2.rs b/crates/l2/l2.rs index 0ac8e4cf39..f2e9abed0c 100644 --- a/crates/l2/l2.rs +++ b/crates/l2/l2.rs @@ -1,5 +1,6 @@ pub mod based; pub mod errors; +pub mod monitor; pub mod sequencer; pub mod utils; diff --git a/crates/l2/monitor/app.rs b/crates/l2/monitor/app.rs new file mode 100644 index 0000000000..6ef9719d93 --- /dev/null +++ b/crates/l2/monitor/app.rs @@ -0,0 +1,343 @@ +use std::io; +use std::time::{Duration, Instant}; + +use crossterm::{ + event::{self, DisableMouseCapture, EnableMouseCapture, KeyCode, MouseEventKind}, + execute, + terminal::{EnterAlternateScreen, LeaveAlternateScreen, disable_raw_mode, enable_raw_mode}, +}; +use ethrex_rpc::EthClient; +use ethrex_storage::Store; +use ethrex_storage_rollup::StoreRollup; +use ratatui::buffer::Buffer; +use ratatui::layout::{Constraint, Layout, Rect}; +use ratatui::style::{Color, Modifier, Style}; +use ratatui::text::{Line, Span}; +use ratatui::widgets::{Block, Paragraph, StatefulWidget, Tabs, Widget}; +use ratatui::{ + Terminal, + backend::{Backend, CrosstermBackend}, +}; +use tui_logger::{TuiLoggerLevelOutput, TuiLoggerSmartWidget, TuiWidgetEvent, TuiWidgetState}; + +use crate::based::sequencer_state::SequencerState; +use crate::monitor::widget::{ETHREX_LOGO, LATEST_BLOCK_STATUS_TABLE_LENGTH_IN_DIGITS}; +use crate::{ + SequencerConfig, + monitor::widget::{ + BatchesTable, BlocksTable, GlobalChainStatusTable, L1ToL2MessagesTable, + L2ToL1MessagesTable, MempoolTable, NodeStatusTable, tabs::TabsState, + }, + sequencer::errors::MonitorError, +}; + +pub struct EthrexMonitor { + pub title: String, + pub should_quit: bool, + pub tabs: TabsState, + pub tick_rate: u64, + + pub logger: TuiWidgetState, + pub node_status: NodeStatusTable, + pub global_chain_status: GlobalChainStatusTable, + pub mempool: MempoolTable, + pub batches_table: BatchesTable, + pub blocks_table: BlocksTable, + pub l1_to_l2_messages: L1ToL2MessagesTable, + pub l2_to_l1_messages: L2ToL1MessagesTable, + + pub eth_client: EthClient, + pub rollup_client: EthClient, + pub store: Store, + pub rollup_store: StoreRollup, +} + +impl EthrexMonitor { + pub async fn new( + sequencer_state: SequencerState, + store: Store, + rollup_store: StoreRollup, + cfg: &SequencerConfig, + ) -> Self { + let eth_client = EthClient::new(cfg.eth.rpc_url.first().expect("No RPC URLs provided")) + .expect("Failed to create EthClient"); + // TODO: De-hardcode the rollup client URL + let rollup_client = + EthClient::new("http://localhost:1729").expect("Failed to create RollupClient"); + + EthrexMonitor { + title: if cfg.based.based { + "Based Ethrex Monitor".to_string() + } else { + "Ethrex Monitor".to_string() + }, + should_quit: false, + tabs: TabsState::default(), + tick_rate: cfg.monitor.tick_rate, + global_chain_status: GlobalChainStatusTable::new( + ð_client, + cfg, + &store, + &rollup_store, + ) + .await, + logger: TuiWidgetState::new().set_default_display_level(tui_logger::LevelFilter::Info), + node_status: NodeStatusTable::new(sequencer_state.clone(), &store).await, + mempool: MempoolTable::new(&rollup_client).await, + batches_table: BatchesTable::new( + cfg.l1_committer.on_chain_proposer_address, + ð_client, + &rollup_store, + ) + .await, + blocks_table: BlocksTable::new(&store).await, + l1_to_l2_messages: L1ToL2MessagesTable::new( + cfg.l1_watcher.bridge_address, + ð_client, + &store, + ) + .await, + l2_to_l1_messages: L2ToL1MessagesTable::new( + cfg.l1_watcher.bridge_address, + ð_client, + &rollup_client, + ) + .await, + eth_client, + rollup_client, + store, + rollup_store, + } + } + + pub async fn start(mut self) -> Result<(), MonitorError> { + // setup terminal + enable_raw_mode().map_err(MonitorError::Io)?; + let mut stdout = io::stdout(); + execute!(stdout, EnterAlternateScreen, EnableMouseCapture).map_err(MonitorError::Io)?; + let backend = CrosstermBackend::new(stdout); + let mut terminal = Terminal::new(backend).map_err(MonitorError::Io)?; + + let app_result = self.run(&mut terminal).await; + + // restore terminal + disable_raw_mode().map_err(MonitorError::Io)?; + execute!( + terminal.backend_mut(), + LeaveAlternateScreen, + DisableMouseCapture + ) + .map_err(MonitorError::Io)?; + terminal.show_cursor().map_err(MonitorError::Io)?; + + let _ = app_result.inspect_err(|err| { + eprintln!("Monitor error: {err}"); + }); + + Ok(()) + } + + async fn run(&mut self, terminal: &mut Terminal) -> Result<(), MonitorError> + where + B: Backend, + { + let mut last_tick = Instant::now(); + loop { + self.draw(terminal)?; + + let timeout = Duration::from_millis(self.tick_rate).saturating_sub(last_tick.elapsed()); + if !event::poll(timeout)? { + self.on_tick().await; + last_tick = Instant::now(); + continue; + } + let event = event::read()?; + if let Some(key) = event.as_key_press_event() { + self.on_key_event(key.code); + } + if let Some(mouse) = event.as_mouse_event() { + self.on_mouse_event(mouse.kind); + } + if self.should_quit { + return Ok(()); + } + } + } + + fn draw(&mut self, terminal: &mut Terminal) -> Result<(), MonitorError> { + terminal.draw(|frame| { + frame.render_widget(self, frame.area()); + })?; + Ok(()) + } + + pub fn on_key_event(&mut self, code: KeyCode) { + match (&self.tabs, code) { + (TabsState::Logs, KeyCode::Left) => self.logger.transition(TuiWidgetEvent::LeftKey), + (TabsState::Logs, KeyCode::Down) => self.logger.transition(TuiWidgetEvent::DownKey), + (TabsState::Logs, KeyCode::Up) => self.logger.transition(TuiWidgetEvent::UpKey), + (TabsState::Logs, KeyCode::Right) => self.logger.transition(TuiWidgetEvent::RightKey), + (TabsState::Logs, KeyCode::Char('h')) => { + self.logger.transition(TuiWidgetEvent::HideKey) + } + (TabsState::Logs, KeyCode::Char('f')) => { + self.logger.transition(TuiWidgetEvent::FocusKey) + } + (TabsState::Logs, KeyCode::Char('+')) => { + self.logger.transition(TuiWidgetEvent::PlusKey) + } + (TabsState::Logs, KeyCode::Char('-')) => { + self.logger.transition(TuiWidgetEvent::MinusKey) + } + (TabsState::Overview | TabsState::Logs, KeyCode::Char('Q')) => self.should_quit = true, + (TabsState::Overview | TabsState::Logs, KeyCode::Tab) => self.tabs.next(), + _ => {} + } + } + + pub fn on_mouse_event(&mut self, kind: MouseEventKind) { + match (&self.tabs, kind) { + (TabsState::Logs, MouseEventKind::ScrollDown) => { + self.logger.transition(TuiWidgetEvent::NextPageKey) + } + (TabsState::Logs, MouseEventKind::ScrollUp) => { + self.logger.transition(TuiWidgetEvent::PrevPageKey) + } + _ => {} + } + } + + pub async fn on_tick(&mut self) { + self.node_status.on_tick(&self.store).await; + self.global_chain_status + .on_tick(&self.eth_client, &self.store, &self.rollup_store) + .await; + self.mempool.on_tick(&self.rollup_client).await; + self.batches_table + .on_tick(&self.eth_client, &self.rollup_store) + .await; + self.blocks_table.on_tick(&self.store).await; + self.l1_to_l2_messages + .on_tick(&self.eth_client, &self.store) + .await; + self.l2_to_l1_messages + .on_tick(&self.eth_client, &self.rollup_client) + .await; + } +} + +impl Widget for &mut EthrexMonitor { + fn render(self, area: Rect, buf: &mut Buffer) + where + Self: Sized, + { + let chunks = Layout::vertical([Constraint::Length(3), Constraint::Min(0)]).split(area); + let tabs = Tabs::default() + .titles([TabsState::Overview.to_string(), TabsState::Logs.to_string()]) + .block( + Block::bordered() + .border_style(Style::default().fg(Color::Cyan)) + .title(Span::styled( + self.title.clone(), + Style::default().add_modifier(Modifier::BOLD), + )), + ) + .highlight_style(Style::default().add_modifier(Modifier::BOLD)) + .select(self.tabs.clone()); + + tabs.render(chunks[0], buf); + + match self.tabs { + TabsState::Overview => { + let chunks = Layout::vertical([ + Constraint::Length(10), + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Fill(1), + Constraint::Length(1), + ]) + .split(chunks[1]); + { + let constraints = vec![ + Constraint::Fill(1), + Constraint::Length(LATEST_BLOCK_STATUS_TABLE_LENGTH_IN_DIGITS), + ]; + + let chunks = Layout::horizontal(constraints).split(chunks[0]); + + let logo = Paragraph::new(ETHREX_LOGO) + .centered() + .style(Style::default()) + .block(Block::bordered().border_style(Style::default().fg(Color::Cyan))); + + logo.render(chunks[0], buf); + + { + let constraints = vec![Constraint::Fill(1), Constraint::Fill(1)]; + + let chunks = Layout::horizontal(constraints).split(chunks[1]); + + let mut node_status_state = self.node_status.state.clone(); + self.node_status + .render(chunks[0], buf, &mut node_status_state); + + let mut global_chain_status_state = self.global_chain_status.state.clone(); + self.global_chain_status.render( + chunks[1], + buf, + &mut global_chain_status_state, + ); + } + } + let mut batches_table_state = self.batches_table.state.clone(); + self.batches_table + .render(chunks[1], buf, &mut batches_table_state); + + let mut blocks_table_state = self.blocks_table.state.clone(); + self.blocks_table + .render(chunks[2], buf, &mut blocks_table_state); + + let mut mempool_state = self.mempool.state.clone(); + self.mempool.render(chunks[3], buf, &mut mempool_state); + + let mut l1_to_l2_messages_state = self.l1_to_l2_messages.state.clone(); + self.l1_to_l2_messages + .render(chunks[4], buf, &mut l1_to_l2_messages_state); + + let mut l2_to_l1_messages_state = self.l2_to_l1_messages.state.clone(); + self.l2_to_l1_messages + .render(chunks[5], buf, &mut l2_to_l1_messages_state); + + let help = Line::raw("tab: switch tab | Q: quit").centered(); + + help.render(chunks[6], buf); + } + TabsState::Logs => { + let chunks = + Layout::vertical([Constraint::Fill(1), Constraint::Length(1)]).split(chunks[1]); + let log_widget = TuiLoggerSmartWidget::default() + .style_error(Style::default().fg(Color::Red)) + .style_debug(Style::default().fg(Color::LightBlue)) + .style_warn(Style::default().fg(Color::Yellow)) + .style_trace(Style::default().fg(Color::Magenta)) + .style_info(Style::default().fg(Color::White)) + .border_style(Style::default().fg(Color::Cyan)) + .output_separator(' ') + .output_timestamp(Some("%F %H:%M:%S%.3f".to_string())) + .output_level(Some(TuiLoggerLevelOutput::Long)) + .output_target(true) + .output_file(false) + .output_line(false) + .state(&self.logger); + + log_widget.render(chunks[0], buf); + + let help = Line::raw("tab: switch tab | Q: quit | ↑/↓: select target | f: focus target | ←/→: display level | +/-: filter level | h: hide target selector").centered(); + + help.render(chunks[1], buf); + } + }; + } +} diff --git a/crates/l2/monitor/mod.rs b/crates/l2/monitor/mod.rs new file mode 100644 index 0000000000..3a7ae19dfe --- /dev/null +++ b/crates/l2/monitor/mod.rs @@ -0,0 +1,27 @@ +// TODO: Handle this expects +#![expect(clippy::expect_used)] +#![expect(clippy::panic)] +#![expect(clippy::indexing_slicing)] + +pub(crate) mod app; +pub(crate) mod utils; +pub(crate) mod widget; + +pub use app::EthrexMonitor; +use ethrex_storage::Store; +use ethrex_storage_rollup::StoreRollup; + +use crate::SequencerConfig; +use crate::based::sequencer_state::SequencerState; +use crate::sequencer::errors::SequencerError; + +pub async fn start_monitor( + sequencer_state: SequencerState, + store: Store, + rollup_store: StoreRollup, + cfg: SequencerConfig, +) -> Result<(), SequencerError> { + let app = EthrexMonitor::new(sequencer_state, store, rollup_store, &cfg).await; + app.start().await?; + Ok(()) +} diff --git a/crates/l2/monitor/utils.rs b/crates/l2/monitor/utils.rs new file mode 100644 index 0000000000..3d315b2ada --- /dev/null +++ b/crates/l2/monitor/utils.rs @@ -0,0 +1,43 @@ +use std::cmp::min; + +use ethrex_common::{Address, U256}; +use ethrex_rpc::{EthClient, types::receipt::RpcLog}; +use keccak_hash::keccak; + +pub async fn get_logs( + last_block_fetched: &mut U256, + emitter: Address, + logs_signatures: Vec<&str>, + client: &EthClient, +) -> Vec { + let last_block_number = client + .get_block_number() + .await + .expect("Failed to get latest L1 block"); + + let mut batch_committed_logs = Vec::new(); + while *last_block_fetched < last_block_number { + let new_last_l1_fetched_block = min(*last_block_fetched + 50, last_block_number); + + // Fetch logs from the L1 chain for the BatchCommitted event. + let logs = client + .get_logs( + *last_block_fetched + 1, + new_last_l1_fetched_block, + emitter, + logs_signatures + .iter() + .map(|log_signature| keccak(log_signature.as_bytes())) + .collect(), + ) + .await + .unwrap_or_else(|_| panic!("Failed to fetch {logs_signatures:?} logs from {emitter}")); + + // Update the last L1 block fetched. + *last_block_fetched = new_last_l1_fetched_block; + + batch_committed_logs.extend_from_slice(&logs); + } + + batch_committed_logs +} diff --git a/crates/l2/monitor/widget/batches.rs b/crates/l2/monitor/widget/batches.rs new file mode 100644 index 0000000000..c0e204f0a7 --- /dev/null +++ b/crates/l2/monitor/widget/batches.rs @@ -0,0 +1,193 @@ +use ethrex_common::{Address, H256, types::batch::Batch}; +use ethrex_rpc::EthClient; +use ethrex_storage_rollup::StoreRollup; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Rect}, + style::{Color, Modifier, Style}, + text::Span, + widgets::{Block, Row, StatefulWidget, Table, TableState}, +}; + +use crate::monitor::widget::{HASH_LENGTH_IN_DIGITS, NUMBER_LENGTH_IN_DIGITS}; + +pub struct BatchesTable { + pub state: TableState, + // batch number | # blocks | # messages | commit tx hash | verify tx hash + #[expect(clippy::type_complexity)] + pub items: Vec<(u64, u64, usize, Option, Option)>, + last_l1_block_fetched: u64, + on_chain_proposer_address: Address, +} + +impl BatchesTable { + pub async fn new( + on_chain_proposer_address: Address, + eth_client: &EthClient, + rollup_store: &StoreRollup, + ) -> Self { + let mut last_l1_block_fetched = 0; + let items = Self::fetch_new_items( + &mut last_l1_block_fetched, + on_chain_proposer_address, + eth_client, + rollup_store, + ) + .await; + Self { + state: TableState::default(), + items, + last_l1_block_fetched, + on_chain_proposer_address, + } + } + + pub async fn on_tick(&mut self, eth_client: &EthClient, rollup_store: &StoreRollup) { + let mut new_latest_batches = Self::fetch_new_items( + &mut self.last_l1_block_fetched, + self.on_chain_proposer_address, + eth_client, + rollup_store, + ) + .await; + new_latest_batches.truncate(50); + + let n_new_latest_batches = new_latest_batches.len(); + self.items.truncate(50 - n_new_latest_batches); + self.refresh_items(rollup_store).await; + self.items.extend_from_slice(&new_latest_batches); + self.items.rotate_right(n_new_latest_batches); + } + + async fn refresh_items(&mut self, rollup_store: &StoreRollup) { + if self.items.is_empty() { + return; + } + + let mut from = self.items.last().expect("Expected items in the table").0 - 1; + + let refreshed_batches = Self::get_batches( + &mut from, + self.items.first().expect("Expected items in the table").0, + rollup_store, + ) + .await; + + let refreshed_items = Self::process_batches(refreshed_batches).await; + + self.items = refreshed_items; + } + + async fn fetch_new_items( + last_l2_batch_fetched: &mut u64, + on_chain_proposer_address: Address, + eth_client: &EthClient, + rollup_store: &StoreRollup, + ) -> Vec<(u64, u64, usize, Option, Option)> { + let last_l2_batch_number = eth_client + .get_last_committed_batch(on_chain_proposer_address) + .await + .expect("Failed to get latest L2 batch"); + + let new_batches = + Self::get_batches(last_l2_batch_fetched, last_l2_batch_number, rollup_store).await; + + Self::process_batches(new_batches).await + } + + async fn get_batches(from: &mut u64, to: u64, rollup_store: &StoreRollup) -> Vec { + let mut new_batches = Vec::new(); + + for batch_number in *from + 1..=to { + let batch = rollup_store + .get_batch(batch_number) + .await + .unwrap_or_else(|err| { + panic!("Failed to get batch by number ({batch_number}): {err}") + }) + .unwrap_or_else(|| panic!("Batch {batch_number} not found in the rollup store")); + + *from = batch_number; + + new_batches.push(batch); + } + + new_batches + } + + async fn process_batches( + new_batches: Vec, + ) -> Vec<(u64, u64, usize, Option, Option)> { + let mut new_blocks_processed = new_batches + .iter() + .map(|batch| { + ( + batch.number, + batch.last_block - batch.first_block + 1, + batch.message_hashes.len(), + batch.commit_tx, + batch.verify_tx, + ) + }) + .collect::>(); + + new_blocks_processed + .sort_by(|(number_a, _, _, _, _), (number_b, _, _, _, _)| number_b.cmp(number_a)); + + new_blocks_processed + } +} + +impl StatefulWidget for &mut BatchesTable { + type State = TableState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let constraints = vec![ + Constraint::Length(NUMBER_LENGTH_IN_DIGITS), + Constraint::Length(NUMBER_LENGTH_IN_DIGITS), + Constraint::Length(17), + Constraint::Length(HASH_LENGTH_IN_DIGITS), + Constraint::Length(HASH_LENGTH_IN_DIGITS), + ]; + let rows = self.items.iter().map( + |(number, n_blocks, n_messages, commit_tx_hash, verify_tx_hash)| { + Row::new(vec![ + Span::styled(number.to_string(), Style::default()), + Span::styled(n_blocks.to_string(), Style::default()), + Span::styled(n_messages.to_string(), Style::default()), + Span::styled( + commit_tx_hash + .map_or_else(|| "Uncommitted".to_string(), |hash| format!("{hash:#x}")), + Style::default(), + ), + Span::styled( + verify_tx_hash + .map_or_else(|| "Unverified".to_string(), |hash| format!("{hash:#x}")), + Style::default(), + ), + ]) + }, + ); + let committed_batches_table = Table::new(rows, constraints) + .header( + Row::new(vec![ + "Number", + "# Blocks", + "# L2 to L1 Messages", + "Commit Tx Hash", + "Verify Tx Hash", + ]) + .style(Style::default()), + ) + .block( + Block::bordered() + .border_style(Style::default().fg(Color::Cyan)) + .title(Span::styled( + "L2 Batches", + Style::default().add_modifier(Modifier::BOLD), + )), + ); + + committed_batches_table.render(area, buf, state); + } +} diff --git a/crates/l2/monitor/widget/blocks.rs b/crates/l2/monitor/widget/blocks.rs new file mode 100644 index 0000000000..77aa04e53a --- /dev/null +++ b/crates/l2/monitor/widget/blocks.rs @@ -0,0 +1,174 @@ +use std::cmp::min; + +use ethrex_common::{Address, H256, types::Block}; +use ethrex_rlp::encode::RLPEncode; +use ethrex_storage::Store; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Rect}, + style::{Color, Modifier, Style}, + text::Span, + widgets::{Row, StatefulWidget, Table, TableState}, +}; + +use crate::monitor::widget::{ + ADDRESS_LENGTH_IN_DIGITS, BLOCK_SIZE_LENGTH_IN_DIGITS, GAS_USED_LENGTH_IN_DIGITS, + HASH_LENGTH_IN_DIGITS, NUMBER_LENGTH_IN_DIGITS, TX_NUMBER_LENGTH_IN_DIGITS, +}; + +pub struct BlocksTable { + pub state: TableState, + // block number | #transactions | hash | coinbase | gas | blob gas | size + pub items: Vec<(String, String, String, String, String, String, String)>, + last_l2_block_known: u64, +} + +impl BlocksTable { + pub async fn new(store: &Store) -> Self { + let mut last_l2_block_known = 0; + let items = Self::refresh_items(&mut last_l2_block_known, store).await; + Self { + state: TableState::default(), + items, + last_l2_block_known, + } + } + + pub async fn on_tick(&mut self, store: &Store) { + let mut new_blocks = Self::refresh_items(&mut self.last_l2_block_known, store).await; + new_blocks.truncate(50); + + let n_new_blocks = new_blocks.len(); + self.items.truncate(50 - n_new_blocks); + self.items.extend_from_slice(&new_blocks); + self.items.rotate_right(n_new_blocks); + } + + async fn refresh_items( + last_l2_block_known: &mut u64, + store: &Store, + ) -> Vec<(String, String, String, String, String, String, String)> { + let new_blocks = Self::get_blocks(last_l2_block_known, store).await; + + let new_blocks_processed = Self::process_blocks(new_blocks).await; + + new_blocks_processed + .iter() + .map(|(number, n_txs, hash, coinbase, gas, blob_gas, size)| { + ( + number.to_string(), + n_txs.to_string(), + format!("{hash:#x}"), + format!("{coinbase:#x}"), + gas.to_string(), + blob_gas.map_or("0".to_string(), |bg| bg.to_string()), + size.to_string(), + ) + }) + .collect() + } + + async fn get_blocks(last_l2_block_known: &mut u64, store: &Store) -> Vec { + let last_l2_block_number = store + .get_latest_block_number() + .await + .expect("Failed to get latest L2 block"); + + let mut new_blocks = Vec::new(); + while *last_l2_block_known < last_l2_block_number { + let new_last_l1_fetched_block = min(*last_l2_block_known + 1, last_l2_block_number); + + let new_block = store + .get_block_by_number(new_last_l1_fetched_block) + .await + .unwrap_or_else(|_| { + panic!("Failed to get block by number ({new_last_l1_fetched_block})") + }) + .unwrap_or_else(|| { + panic!("Block {new_last_l1_fetched_block} not found in the store") + }); + + // Update the last L1 block fetched. + *last_l2_block_known = new_last_l1_fetched_block; + + new_blocks.push(new_block); + } + + new_blocks + } + + async fn process_blocks( + new_blocks: Vec, + ) -> Vec<(u64, usize, H256, Address, u64, Option, usize)> { + let mut new_blocks_processed = new_blocks + .iter() + .map(|block| { + ( + block.header.number, + block.body.transactions.len(), + block.header.hash(), + block.header.coinbase, + block.header.gas_used, + block.header.blob_gas_used, + block.encode_to_vec().len(), + ) + }) + .collect::>(); + + new_blocks_processed.sort_by( + |(number_a, _, _, _, _, _, _), (number_b, _, _, _, _, _, _)| number_b.cmp(number_a), + ); + + new_blocks_processed + } +} + +impl StatefulWidget for &mut BlocksTable { + type State = TableState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) + where + Self: Sized, + { + let constraints = vec![ + Constraint::Length(NUMBER_LENGTH_IN_DIGITS), + Constraint::Length(TX_NUMBER_LENGTH_IN_DIGITS), + Constraint::Length(HASH_LENGTH_IN_DIGITS), + Constraint::Length(ADDRESS_LENGTH_IN_DIGITS), + Constraint::Length(GAS_USED_LENGTH_IN_DIGITS), + Constraint::Length(GAS_USED_LENGTH_IN_DIGITS), + Constraint::Length(BLOCK_SIZE_LENGTH_IN_DIGITS), + ]; + let rows = self + .items + .iter() + .map(|(number, n_txs, hash, coinbase, gas, blob_bas, size)| { + Row::new(vec![ + Span::styled(number, Style::default()), + Span::styled(n_txs.to_string(), Style::default()), + Span::styled(hash, Style::default()), + Span::styled(coinbase, Style::default()), + Span::styled(gas.to_string(), Style::default()), + Span::styled(blob_bas.to_string(), Style::default()), + Span::styled(size.to_string(), Style::default()), + ]) + }); + let latest_blocks_table = Table::new(rows, constraints) + .header( + Row::new(vec![ + "Number", "#Txs", "Hash", "Coinbase", "Gas", "Blob Gas", "Size", + ]) + .style(Style::default()), + ) + .block( + ratatui::widgets::Block::bordered() + .border_style(Style::default().fg(Color::Cyan)) + .title(Span::styled( + "L2 Blocks", + Style::default().add_modifier(Modifier::BOLD), + )), + ); + + latest_blocks_table.render(area, buf, state); + } +} diff --git a/crates/l2/monitor/widget/chain_status.rs b/crates/l2/monitor/widget/chain_status.rs new file mode 100644 index 0000000000..cd64d4aa76 --- /dev/null +++ b/crates/l2/monitor/widget/chain_status.rs @@ -0,0 +1,218 @@ +use ethrex_common::{Address, H256}; +use ethrex_l2_sdk::calldata::encode_calldata; +use ethrex_rpc::{EthClient, clients::Overrides}; +use ethrex_storage::Store; +use ethrex_storage_rollup::StoreRollup; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Rect}, + style::{Color, Modifier, Style}, + text::Span, + widgets::{Block, Row, StatefulWidget, Table, TableState}, +}; + +use crate::SequencerConfig; + +pub struct GlobalChainStatusTable { + pub state: TableState, + pub items: Vec<(String, String)>, + pub on_chain_proposer_address: Address, + pub sequencer_registry_address: Option
, +} + +impl GlobalChainStatusTable { + pub async fn new( + eth_client: &EthClient, + cfg: &SequencerConfig, + store: &Store, + rollup_store: &StoreRollup, + ) -> Self { + let sequencer_registry_address = + if cfg.based.state_updater.sequencer_registry == Address::default() { + None + } else { + Some(cfg.based.state_updater.sequencer_registry) + }; + Self { + state: TableState::default(), + items: Self::refresh_items( + eth_client, + cfg.l1_committer.on_chain_proposer_address, + sequencer_registry_address, + store, + rollup_store, + ) + .await, + on_chain_proposer_address: cfg.l1_committer.on_chain_proposer_address, + sequencer_registry_address, + } + } + + pub async fn on_tick( + &mut self, + eth_client: &EthClient, + store: &Store, + rollup_store: &StoreRollup, + ) { + self.items = Self::refresh_items( + eth_client, + self.on_chain_proposer_address, + self.sequencer_registry_address, + store, + rollup_store, + ) + .await; + } + + async fn refresh_items( + eth_client: &EthClient, + on_chain_proposer_address: Address, + sequencer_registry_address: Option
, + store: &Store, + rollup_store: &StoreRollup, + ) -> Vec<(String, String)> { + let last_update = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + + let lead_sequencer = if let Some(sequencer_registry_address) = sequencer_registry_address { + let calldata = encode_calldata("leaderSequencer()", &[]) + .expect("Failed to encode leadSequencer calldata"); + + let raw_lead_sequencer: H256 = eth_client + .call( + sequencer_registry_address, + calldata.into(), + Overrides::default(), + ) + .await + .expect("Failed to call leaderSequencer") + .parse() + .unwrap_or_default(); + + Address::from_slice(&raw_lead_sequencer.as_fixed_bytes()[12..]) + } else { + Address::default() + }; + + let last_committed_batch = eth_client + .get_last_committed_batch(on_chain_proposer_address) + .await + .expect("Failed to get last committed batch"); + + let last_verified_batch = eth_client + .get_last_verified_batch(on_chain_proposer_address) + .await + .expect("Failed to get last verified batch"); + + let last_committed_block = if last_committed_batch == 0 { + 0 + } else { + match rollup_store + .get_block_numbers_by_batch(last_committed_batch) + .await + .expect("Failed to get blocks by batch") + { + Some(block_numbers) => block_numbers.last().copied().unwrap_or(0), + None => 0, + } + }; + + let last_verified_block = if last_verified_batch == 0 { + 0 + } else { + match rollup_store + .get_block_numbers_by_batch(last_verified_batch) + .await + .expect("Failed to get blocks by batch") + { + Some(block_numbers) => block_numbers.last().copied().unwrap_or(0), + None => 0, + } + }; + + let current_block = store + .get_latest_block_number() + .await + .expect("Failed to get latest L1 block") + + 1; + + let current_batch = if sequencer_registry_address.is_some() { + "NaN".to_string() // TODO: Implement current batch retrieval (should be last known + 1) + } else { + (last_committed_batch + 1).to_string() + }; + + if sequencer_registry_address.is_some() { + vec![ + ("Last Update:".to_string(), last_update), + ( + "Lead Sequencer:".to_string(), + format!("{lead_sequencer:#x}"), + ), + ("Current Batch:".to_string(), current_batch.to_string()), + ("Current Block:".to_string(), current_block.to_string()), + ( + "Last Committed Batch:".to_string(), + last_committed_batch.to_string(), + ), + ( + "Last Committed Block:".to_string(), + last_committed_block.to_string(), + ), + ( + "Last Verified Batch:".to_string(), + last_verified_batch.to_string(), + ), + ( + "Last Verified Block:".to_string(), + last_verified_block.to_string(), + ), + ] + } else { + vec![ + ("Last Update:".to_string(), last_update), + ("Current Batch:".to_string(), current_batch.to_string()), + ("Current Block:".to_string(), current_block.to_string()), + ( + "Last Committed Batch:".to_string(), + last_committed_batch.to_string(), + ), + ( + "Last Committed Block:".to_string(), + last_committed_block.to_string(), + ), + ( + "Last Verified Batch:".to_string(), + last_verified_batch.to_string(), + ), + ( + "Last Verified Block:".to_string(), + last_verified_block.to_string(), + ), + ] + } + } +} + +impl StatefulWidget for &mut GlobalChainStatusTable { + type State = TableState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let constraints = vec![Constraint::Percentage(50), Constraint::Percentage(50)]; + let rows = self.items.iter().map(|(key, value)| { + Row::new(vec![ + Span::styled(key, Style::default()), + Span::styled(value, Style::default()), + ]) + }); + let global_chain_status_table = Table::new(rows, constraints).block( + Block::bordered() + .border_style(Style::default().fg(Color::Cyan)) + .title(Span::styled( + "Global Chain Status", + Style::default().add_modifier(Modifier::BOLD), + )), + ); + + global_chain_status_table.render(area, buf, state); + } +} diff --git a/crates/l2/monitor/widget/l1_to_l2_messages.rs b/crates/l2/monitor/widget/l1_to_l2_messages.rs new file mode 100644 index 0000000000..91054f271a --- /dev/null +++ b/crates/l2/monitor/widget/l1_to_l2_messages.rs @@ -0,0 +1,257 @@ +use std::fmt::Display; + +use ethrex_common::{Address, H256, U256}; +use ethrex_l2_sdk::COMMON_BRIDGE_L2_ADDRESS; +use ethrex_rpc::{EthClient, types::receipt::RpcLog}; +use ethrex_storage::Store; +use keccak_hash::keccak; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Rect}, + style::{Color, Modifier, Style}, + text::Span, + widgets::{Block, Row, StatefulWidget, Table, TableState}, +}; + +use crate::{ + monitor::{self, widget::HASH_LENGTH_IN_DIGITS}, + sequencer::l1_watcher::PrivilegedTransactionData, +}; + +// kind | status | L1 tx hash | L2 tx hash | amount +pub type L1ToL2MessagesRow = (L1ToL2MessageKind, L1ToL2MessageStatus, H256, H256, U256); + +pub struct L1ToL2MessagesTable { + pub state: TableState, + pub items: Vec, + last_l1_block_fetched: U256, + common_bridge_address: Address, +} + +#[derive(Debug, Clone)] +pub enum L1ToL2MessageStatus { + Unknown = 0, + Pending = 1, + ProcessedOnL2 = 3, + Committed = 4, + Verified = 5, +} + +impl L1ToL2MessageStatus { + pub async fn for_tx( + l2_tx_hash: H256, + common_bridge_address: Address, + eth_client: &EthClient, + store: &Store, + ) -> Self { + if let Ok(Some(_tx)) = store.get_transaction_by_hash(l2_tx_hash).await { + Self::ProcessedOnL2 + } else if eth_client + .get_pending_privileged_transactions(common_bridge_address) + .await + .expect("Failed to get pending L1 to L2 messages") + .contains(&l2_tx_hash) + { + Self::Pending + } else { + Self::Unknown + } + } +} + +impl Display for L1ToL2MessageStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + L1ToL2MessageStatus::Unknown => write!(f, "Unknown"), + L1ToL2MessageStatus::Pending => write!(f, "Pending"), + L1ToL2MessageStatus::ProcessedOnL2 => write!(f, "Processed on L2"), + L1ToL2MessageStatus::Committed => write!(f, "Committed"), + L1ToL2MessageStatus::Verified => write!(f, "Verified"), + } + } +} + +#[derive(Debug, Clone)] +pub enum L1ToL2MessageKind { + Deposit, + Message, +} + +impl From<&PrivilegedTransactionData> for L1ToL2MessageKind { + fn from(data: &PrivilegedTransactionData) -> Self { + if data.from == COMMON_BRIDGE_L2_ADDRESS && data.to_address == COMMON_BRIDGE_L2_ADDRESS { + Self::Deposit + } else { + Self::Message + } + } +} + +impl Display for L1ToL2MessageKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + L1ToL2MessageKind::Deposit => write!(f, "Deposit"), + L1ToL2MessageKind::Message => write!(f, "Message"), + } + } +} + +impl L1ToL2MessagesTable { + pub async fn new( + common_bridge_address: Address, + eth_client: &EthClient, + store: &Store, + ) -> Self { + let mut last_l1_block_fetched = eth_client + .get_last_fetched_l1_block(common_bridge_address) + .await + .expect("Failed to get last fetched L1 block") + .into(); + let items = Self::fetch_new_items( + &mut last_l1_block_fetched, + common_bridge_address, + eth_client, + store, + ) + .await; + Self { + state: TableState::default(), + items, + last_l1_block_fetched, + common_bridge_address, + } + } + + pub async fn on_tick(&mut self, eth_client: &EthClient, store: &Store) { + let mut new_l1_to_l2_messages = Self::fetch_new_items( + &mut self.last_l1_block_fetched, + self.common_bridge_address, + eth_client, + store, + ) + .await; + new_l1_to_l2_messages.truncate(50); + + let n_new_latest_batches = new_l1_to_l2_messages.len(); + self.items.truncate(50 - n_new_latest_batches); + self.refresh_items(eth_client, store).await; + self.items.extend_from_slice(&new_l1_to_l2_messages); + self.items.rotate_right(n_new_latest_batches); + } + + async fn refresh_items(&mut self, eth_client: &EthClient, store: &Store) { + for (_kind, status, _l1_tx_hash, l2_tx_hash, ..) in self.items.iter_mut() { + *status = L1ToL2MessageStatus::for_tx( + *l2_tx_hash, + self.common_bridge_address, + eth_client, + store, + ) + .await; + } + } + + async fn fetch_new_items( + last_l1_block_fetched: &mut U256, + common_bridge_address: Address, + eth_client: &EthClient, + store: &Store, + ) -> Vec { + let logs = monitor::utils::get_logs( + last_l1_block_fetched, + common_bridge_address, + vec!["PrivilegedTxSent(address,address,uint256,uint256,uint256,bytes)"], + eth_client, + ) + .await; + Self::process_logs(&logs, common_bridge_address, eth_client, store).await + } + + async fn process_logs( + logs: &[RpcLog], + common_bridge_address: Address, + eth_client: &EthClient, + store: &Store, + ) -> Vec { + let mut processed_logs = Vec::new(); + + for log in logs { + let l1_to_l2_message = PrivilegedTransactionData::from_log(log.log.clone()) + .expect("Failed to parse PrivilegedTxSent log"); + + let l1_to_l2_message_hash = keccak( + [ + l1_to_l2_message.from.as_bytes(), + l1_to_l2_message.to_address.as_bytes(), + &l1_to_l2_message.transaction_id.to_big_endian(), + &l1_to_l2_message.value.to_big_endian(), + &l1_to_l2_message.gas_limit.to_big_endian(), + keccak(&l1_to_l2_message.calldata).as_bytes(), + ] + .concat(), + ); + + processed_logs.push(( + L1ToL2MessageKind::from(&l1_to_l2_message), + L1ToL2MessageStatus::for_tx( + l1_to_l2_message_hash, + common_bridge_address, + eth_client, + store, + ) + .await, + log.transaction_hash, + l1_to_l2_message_hash, + l1_to_l2_message.value, + )); + } + + processed_logs + } +} + +impl StatefulWidget for &mut L1ToL2MessagesTable { + type State = TableState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) + where + Self: Sized, + { + let constraints = vec![ + Constraint::Length(10), + Constraint::Length(15), + Constraint::Length(HASH_LENGTH_IN_DIGITS), + Constraint::Length(HASH_LENGTH_IN_DIGITS), + Constraint::Fill(1), + ]; + + let rows = self + .items + .iter() + .map(|(kind, status, l1_tx_hash, l2_tx_hash, amount)| { + Row::new(vec![ + Span::styled(format!("{kind}"), Style::default()), + Span::styled(format!("{status}"), Style::default()), + Span::styled(format!("{l1_tx_hash:#x}"), Style::default()), + Span::styled(format!("{l2_tx_hash:#x}"), Style::default()), + Span::styled(amount.to_string(), Style::default()), + ]) + }); + + let l1_to_l2_messages_table = Table::new(rows, constraints) + .header( + Row::new(vec!["Kind", "Status", "L1 Tx Hash", "L2 Tx Hash", "Value"]) + .style(Style::default()), + ) + .block( + Block::bordered() + .border_style(Style::default().fg(Color::Cyan)) + .title(Span::styled( + "L1 to L2 Messages", + Style::default().add_modifier(Modifier::BOLD), + )), + ); + + l1_to_l2_messages_table.render(area, buf, state); + } +} diff --git a/crates/l2/monitor/widget/l2_to_l1_messages.rs b/crates/l2/monitor/widget/l2_to_l1_messages.rs new file mode 100644 index 0000000000..ec955a5eab --- /dev/null +++ b/crates/l2/monitor/widget/l2_to_l1_messages.rs @@ -0,0 +1,274 @@ +use std::fmt::Display; + +use ethrex_common::{Address, H256, U256}; +use ethrex_l2_common::calldata::Value; +use ethrex_l2_sdk::{COMMON_BRIDGE_L2_ADDRESS, calldata::encode_calldata}; +use ethrex_rpc::{EthClient, clients::Overrides, types::receipt::RpcLog}; +use keccak_hash::keccak; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Rect}, + style::{Color, Modifier, Style}, + text::Span, + widgets::{Block, Row, StatefulWidget, Table, TableState}, +}; + +use crate::monitor::{ + self, + widget::{ADDRESS_LENGTH_IN_DIGITS, HASH_LENGTH_IN_DIGITS, NUMBER_LENGTH_IN_DIGITS}, +}; + +#[derive(Debug, Clone)] +pub enum L2ToL1MessageStatus { + WithdrawalInitiated, + WithdrawalClaimed, + Sent, + Delivered, +} + +impl L2ToL1MessageStatus { + pub async fn for_tx( + l2_tx_hash: H256, + common_bridge_address: Address, + eth_client: &EthClient, + ) -> Self { + let withdrawal_is_claimed = { + let calldata = encode_calldata( + "claimedWithdrawals(bytes32)", + &[Value::FixedBytes(l2_tx_hash.as_bytes().to_vec().into())], + ) + .expect("Failed to encode claimedWithdrawals(bytes32) calldata"); + + let raw_withdrawal_is_claimed: H256 = eth_client + .call(common_bridge_address, calldata.into(), Overrides::default()) + .await + .expect("Failed to call claimedWithdrawals(bytes32)") + .parse() + .unwrap_or_default(); + + U256::from_big_endian(raw_withdrawal_is_claimed.as_fixed_bytes()) == U256::one() + }; + + if withdrawal_is_claimed { + Self::WithdrawalClaimed + } else { + Self::WithdrawalInitiated + } + } +} + +impl Display for L2ToL1MessageStatus { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + L2ToL1MessageStatus::WithdrawalInitiated => write!(f, "Initiated"), + L2ToL1MessageStatus::WithdrawalClaimed => write!(f, "Claimed"), + L2ToL1MessageStatus::Sent => write!(f, "Sent"), + L2ToL1MessageStatus::Delivered => write!(f, "Delivered"), + } + } +} + +#[derive(Debug, Clone)] +pub enum L2ToL1MessageKind { + ETHWithdraw, + ERC20Withdraw, + Message, +} + +impl Display for L2ToL1MessageKind { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + L2ToL1MessageKind::ETHWithdraw => write!(f, "Withdraw (ETH)"), + L2ToL1MessageKind::ERC20Withdraw => write!(f, "Withdraw (ERC20)"), + L2ToL1MessageKind::Message => write!(f, "Message"), + } + } +} + +pub type L2ToL1MessageRow = ( + L2ToL1MessageKind, + L2ToL1MessageStatus, + Address, // receiver in L1 + U256, // value + Address, // token (L2) + Address, // token (L1) + H256, // L2 tx hash +); + +pub struct L2ToL1MessagesTable { + pub state: TableState, + pub items: Vec, + last_l2_block_fetched: U256, + common_bridge_address: Address, +} + +impl L2ToL1MessagesTable { + pub async fn new( + common_bridge_address: Address, + eth_client: &EthClient, + rollup_client: &EthClient, + ) -> Self { + let mut last_l2_block_fetched = U256::zero(); + let items = Self::fetch_new_items( + &mut last_l2_block_fetched, + common_bridge_address, + eth_client, + rollup_client, + ) + .await; + Self { + state: TableState::default(), + items, + last_l2_block_fetched, + common_bridge_address, + } + } + + pub async fn on_tick(&mut self, eth_client: &EthClient, rollup_client: &EthClient) { + let mut new_l1_to_l2_messages = Self::fetch_new_items( + &mut self.last_l2_block_fetched, + self.common_bridge_address, + eth_client, + rollup_client, + ) + .await; + new_l1_to_l2_messages.truncate(50); + + let n_new_latest_batches = new_l1_to_l2_messages.len(); + self.items.truncate(50 - n_new_latest_batches); + self.refresh_items(eth_client).await; + self.items.extend_from_slice(&new_l1_to_l2_messages); + self.items.rotate_right(n_new_latest_batches); + } + + async fn refresh_items(&mut self, eth_client: &EthClient) { + for (_kind, status, .., l2_tx_hash) in self.items.iter_mut() { + *status = + L2ToL1MessageStatus::for_tx(*l2_tx_hash, self.common_bridge_address, eth_client) + .await; + } + } + + async fn fetch_new_items( + last_l2_block_fetched: &mut U256, + common_bridge_address: Address, + eth_client: &EthClient, + rollup_client: &EthClient, + ) -> Vec { + let logs = monitor::utils::get_logs( + last_l2_block_fetched, + COMMON_BRIDGE_L2_ADDRESS, + vec![], + rollup_client, + ) + .await; + Self::process_logs(&logs, common_bridge_address, eth_client).await + } + + async fn process_logs( + logs: &[RpcLog], + common_bridge_address: Address, + eth_client: &EthClient, + ) -> Vec { + let mut processed_logs = Vec::new(); + + let eth_withdrawal_topic = keccak(b"WithdrawalInitiated(address,address,uint256)"); + let erc20_withdrawal_topic = + keccak(b"ERC20WithdrawalInitiated(address,address,address,uint256)"); + + for log in logs { + let withdrawal_status = L2ToL1MessageStatus::for_tx( + log.transaction_hash, + common_bridge_address, + eth_client, + ) + .await; + match log.log.topics[0] { + topic if topic == eth_withdrawal_topic => { + processed_logs.push(( + L2ToL1MessageKind::ETHWithdraw, + withdrawal_status, + Address::from_slice(&log.log.topics[1].as_fixed_bytes()[12..]), + U256::from_big_endian(log.log.topics[2].as_fixed_bytes()), + Address::default(), + Address::default(), + log.transaction_hash, + )); + } + topic if topic == erc20_withdrawal_topic => { + processed_logs.push(( + L2ToL1MessageKind::ERC20Withdraw, + withdrawal_status, + Address::from_slice(&log.log.topics[3].as_fixed_bytes()[12..]), + U256::from_big_endian(&log.log.data[log.log.data.len() - 32..]), + Address::from_slice(&log.log.topics[1].as_fixed_bytes()[12..]), + Address::from_slice(&log.log.topics[2].as_fixed_bytes()[12..]), + log.transaction_hash, + )); + } + _ => { + continue; + } + } + } + + processed_logs + } +} + +impl StatefulWidget for &mut L2ToL1MessagesTable { + type State = TableState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) + where + Self: Sized, + { + let constraints = vec![ + Constraint::Length(16), + Constraint::Length(9), + Constraint::Length(ADDRESS_LENGTH_IN_DIGITS), + Constraint::Length(NUMBER_LENGTH_IN_DIGITS), + Constraint::Length(ADDRESS_LENGTH_IN_DIGITS), + Constraint::Length(ADDRESS_LENGTH_IN_DIGITS), + Constraint::Length(HASH_LENGTH_IN_DIGITS), + ]; + + let rows = self.items.iter().map( + |(kind, status, receiver_on_l1, value, token_l1, token_l2, l2_tx_hash)| { + Row::new(vec![ + Span::styled(format!("{kind}"), Style::default()), + Span::styled(format!("{status}"), Style::default()), + Span::styled(format!("{receiver_on_l1:#x}"), Style::default()), + Span::styled(value.to_string(), Style::default()), + Span::styled(format!("{token_l1:#x}"), Style::default()), + Span::styled(format!("{token_l2:#x}"), Style::default()), + Span::styled(format!("{l2_tx_hash:#x}"), Style::default()), + ]) + }, + ); + + let l1_to_l2_messages_table = Table::new(rows, constraints) + .header( + Row::new(vec![ + "Kind", + "Status", + "Receiver on L1", + "Value", + "Token L1", + "Token L2", + "L2 Tx Hash", + ]) + .style(Style::default()), + ) + .block( + Block::bordered() + .border_style(Style::default().fg(Color::Cyan)) + .title(Span::styled( + "L2 to L1 Messages", + Style::default().add_modifier(Modifier::BOLD), + )), + ); + + l1_to_l2_messages_table.render(area, buf, state); + } +} diff --git a/crates/l2/monitor/widget/mempool.rs b/crates/l2/monitor/widget/mempool.rs new file mode 100644 index 0000000000..05d9175a99 --- /dev/null +++ b/crates/l2/monitor/widget/mempool.rs @@ -0,0 +1,97 @@ +use ethrex_rpc::EthClient; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Rect}, + style::{Color, Modifier, Style}, + text::Span, + widgets::{Block, Row, StatefulWidget, Table, TableState}, +}; + +use crate::monitor::widget::{ + ADDRESS_LENGTH_IN_DIGITS, HASH_LENGTH_IN_DIGITS, NUMBER_LENGTH_IN_DIGITS, +}; + +pub struct MempoolTable { + pub state: TableState, + // type | hash | sender | nonce + pub items: Vec<(String, String, String, String)>, +} + +impl MempoolTable { + pub async fn new(rollup_client: &EthClient) -> Self { + Self { + state: TableState::default(), + items: Self::refresh_items(rollup_client).await, + } + } + + pub async fn on_tick(&mut self, rollup_client: &EthClient) { + self.items = Self::refresh_items(rollup_client).await; + } + + async fn refresh_items(rollup_client: &EthClient) -> Vec<(String, String, String, String)> { + let mempool = rollup_client + .tx_pool_content() + .await + .expect("Failed to get mempool content"); + + let mut pending_txs = mempool + .pending + .iter() + .flat_map(|(sender, txs_sorted_by_nonce)| { + txs_sorted_by_nonce.iter().map(|(nonce, tx)| { + ( + format!("{}", tx.tx.tx_type()), + format!("{:#x}", tx.hash), + format!("{:#x}", *sender), + format!("{nonce}"), + ) + }) + }) + .collect::>(); + + pending_txs.sort_by( + |(_tx_type_a, _, sender_a, nonce_a), (_tx_type_b, _, sender_b, nonce_b)| { + sender_a.cmp(sender_b).then(nonce_a.cmp(nonce_b)) + }, + ); + + pending_txs + } +} + +impl StatefulWidget for &mut MempoolTable { + type State = TableState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) + where + Self: Sized, + { + let constraints = vec![ + Constraint::Length(10), // tx_type + Constraint::Length(HASH_LENGTH_IN_DIGITS), + Constraint::Length(ADDRESS_LENGTH_IN_DIGITS), + Constraint::Length(NUMBER_LENGTH_IN_DIGITS), + ]; + let rows = self.items.iter().map(|(tx_type, hash, sender, nonce)| { + Row::new(vec![ + Span::styled(tx_type, Style::default()), + Span::styled(hash, Style::default()), + Span::styled(sender, Style::default()), + Span::styled(nonce, Style::default()), + ]) + }); + let mempool_table = Table::new(rows, constraints) + .header(Row::new(vec!["Type", "Hash", "Sender", "Nonce"]).style(Style::default())) + .block( + Block::bordered() + .border_style(Style::default().fg(Color::Cyan)) + .title(Span::styled( + "Mempool", + Style::default().add_modifier(Modifier::BOLD), + )), + ); + + mempool_table.render(area, buf, state); + } +} diff --git a/crates/l2/monitor/widget/mod.rs b/crates/l2/monitor/widget/mod.rs new file mode 100644 index 0000000000..02aeea3414 --- /dev/null +++ b/crates/l2/monitor/widget/mod.rs @@ -0,0 +1,39 @@ +pub mod batches; +pub mod blocks; +pub mod chain_status; +pub mod l1_to_l2_messages; +pub mod l2_to_l1_messages; +pub mod mempool; +pub mod node_status; +pub mod tabs; + +pub use batches::BatchesTable; +pub use blocks::BlocksTable; +pub use chain_status::GlobalChainStatusTable; +pub use l1_to_l2_messages::L1ToL2MessagesTable; +pub use l2_to_l1_messages::L2ToL1MessagesTable; +pub use mempool::MempoolTable; +pub use node_status::NodeStatusTable; + +pub const ETHREX_LOGO: &str = r#" +███████╗████████╗██╗░░██╗██████╗░███████╗██╗░░██╗ +██╔════╝╚══██╔══╝██║░░██║██╔══██╗██╔════╝╚██╗██╔╝ +█████╗░░░░░██║░░░███████║██████╔╝█████╗░░░╚███╔╝░ +██╔══╝░░░░░██║░░░██╔══██║██╔══██╗██╔══╝░░░██╔██╗░ +███████╗░░░██║░░░██║░░██║██║░░██║███████╗██╔╝╚██╗ +╚══════╝░░░╚═╝░░░╚═╝░░╚═╝╚═╝░░╚═╝╚══════╝╚═╝░░╚═╝"#; + +pub const HASH_LENGTH_IN_DIGITS: u16 = 66; // 64 hex characters + 2 for "0x" prefix +pub const ADDRESS_LENGTH_IN_DIGITS: u16 = 42; // 40 hex characters + 2 for "0x" prefix +pub const NUMBER_LENGTH_IN_DIGITS: u16 = 9; // 1e8 +pub const TX_NUMBER_LENGTH_IN_DIGITS: u16 = 4; +pub const GAS_USED_LENGTH_IN_DIGITS: u16 = 8; // 1e7 +pub const BLOCK_SIZE_LENGTH_IN_DIGITS: u16 = 6; // 1e6 + +pub const LATEST_BLOCK_STATUS_TABLE_LENGTH_IN_DIGITS: u16 = NUMBER_LENGTH_IN_DIGITS + + TX_NUMBER_LENGTH_IN_DIGITS + + HASH_LENGTH_IN_DIGITS + + ADDRESS_LENGTH_IN_DIGITS + + GAS_USED_LENGTH_IN_DIGITS + + GAS_USED_LENGTH_IN_DIGITS + + BLOCK_SIZE_LENGTH_IN_DIGITS; diff --git a/crates/l2/monitor/widget/node_status.rs b/crates/l2/monitor/widget/node_status.rs new file mode 100644 index 0000000000..899bf1f7db --- /dev/null +++ b/crates/l2/monitor/widget/node_status.rs @@ -0,0 +1,84 @@ +use ethrex_storage::Store; +use ratatui::{ + buffer::Buffer, + layout::{Constraint, Rect}, + style::{Color, Modifier, Style}, + text::Span, + widgets::{Block, Row, StatefulWidget, Table, TableState}, +}; + +use crate::based::sequencer_state::SequencerState; + +pub struct NodeStatusTable { + pub state: TableState, + pub items: [(String, String); 5], + sequencer_state: SequencerState, +} + +impl NodeStatusTable { + pub async fn new(sequencer_state: SequencerState, store: &Store) -> Self { + Self { + state: TableState::default(), + items: Self::refresh_items(&sequencer_state, store).await, + sequencer_state, + } + } + + pub async fn on_tick(&mut self, store: &Store) { + self.items = Self::refresh_items(&self.sequencer_state, store).await; + } + + async fn refresh_items( + sequencer_state: &SequencerState, + store: &Store, + ) -> [(String, String); 5] { + let last_update = chrono::Local::now().format("%Y-%m-%d %H:%M:%S").to_string(); + let status = sequencer_state.status().await; + let last_known_batch = "NaN"; // TODO: Implement last known batch retrieval + let last_known_block = store + .get_latest_block_number() + .await + .expect("Failed to get latest known L2 block"); + let follower_nodes = "NaN"; // TODO: Implement follower nodes retrieval + + [ + ("Last Update:".to_string(), last_update), + ("Status:".to_string(), status.to_string()), + ( + "Last Known Batch:".to_string(), + last_known_batch.to_string(), + ), + ( + "Last Known Block:".to_string(), + last_known_block.to_string(), + ), + ("Peers:".to_string(), follower_nodes.to_string()), + ] + } +} + +impl StatefulWidget for &mut NodeStatusTable { + type State = TableState; + + fn render(self, area: Rect, buf: &mut Buffer, state: &mut Self::State) { + let constraints = vec![Constraint::Percentage(50), Constraint::Percentage(50)]; + + let rows = self.items.iter().map(|(key, value)| { + Row::new(vec![ + Span::styled(key, Style::default()), + Span::styled(value, Style::default()), + ]) + }); + + let node_status_table = Table::new(rows, constraints).block( + Block::bordered() + .border_style(Style::default().fg(Color::Cyan)) + .title(Span::styled( + "Node Status", + Style::default().add_modifier(Modifier::BOLD), + )), + ); + + node_status_table.render(area, buf, state); + } +} diff --git a/crates/l2/monitor/widget/tabs.rs b/crates/l2/monitor/widget/tabs.rs new file mode 100644 index 0000000000..c92769cf81 --- /dev/null +++ b/crates/l2/monitor/widget/tabs.rs @@ -0,0 +1,42 @@ +use std::fmt::Display; + +#[derive(Debug, Clone, Default)] +pub enum TabsState { + #[default] + Overview = 0, + Logs = 1, +} + +impl TabsState { + pub fn next(&mut self) { + match self { + TabsState::Overview => *self = TabsState::Logs, + TabsState::Logs => *self = TabsState::Overview, + } + } + + pub fn previous(&mut self) { + match self { + TabsState::Overview => *self = TabsState::Logs, + TabsState::Logs => *self = TabsState::Overview, + } + } +} + +impl Display for TabsState { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + TabsState::Overview => write!(f, "Overview"), + TabsState::Logs => write!(f, "Logs"), + } + } +} + +impl From for Option { + fn from(state: TabsState) -> Self { + match state { + TabsState::Overview => Some(0), + TabsState::Logs => Some(1), + } + } +} diff --git a/crates/l2/sequencer/configs.rs b/crates/l2/sequencer/configs.rs index afad604ddf..af2e285eba 100644 --- a/crates/l2/sequencer/configs.rs +++ b/crates/l2/sequencer/configs.rs @@ -13,6 +13,7 @@ pub struct SequencerConfig { pub proof_coordinator: ProofCoordinatorConfig, pub based: BasedConfig, pub aligned: AlignedConfig, + pub monitor: MonitorConfig, } // TODO: Move to blockchain/dev @@ -91,3 +92,10 @@ pub struct AlignedConfig { pub fee_estimate: String, pub aligned_sp1_elf_path: String, } + +#[derive(Clone, Debug)] +pub struct MonitorConfig { + pub enabled: bool, + /// time in ms between two ticks. + pub tick_rate: u64, +} diff --git a/crates/l2/sequencer/errors.rs b/crates/l2/sequencer/errors.rs index 86cba5ff46..c4e11efe0b 100644 --- a/crates/l2/sequencer/errors.rs +++ b/crates/l2/sequencer/errors.rs @@ -43,6 +43,8 @@ pub enum SequencerError { FailedAccessingRollUpStore(#[from] RollupStoreError), #[error("Failed to resolve network")] AlignedNetworkError(String), + #[error("Failed to start EthrexMonitor: {0}")] + MonitorError(#[from] MonitorError), } #[derive(Debug, thiserror::Error)] @@ -290,3 +292,9 @@ pub enum ConnectionHandlerError { #[error("Spawned GenServer Error")] GenServerError(GenServerError), } + +#[derive(Debug, thiserror::Error)] +pub enum MonitorError { + #[error("Failed because of io error: {0}")] + Io(#[from] std::io::Error), +} diff --git a/crates/l2/sequencer/l1_watcher.rs b/crates/l2/sequencer/l1_watcher.rs index c39ad08f2c..49ca0dfc70 100644 --- a/crates/l2/sequencer/l1_watcher.rs +++ b/crates/l2/sequencer/l1_watcher.rs @@ -206,7 +206,7 @@ pub async fn get_privileged_transactions( state.last_block_fetched + 1, new_last_block, state.address, - topic, + vec![topic], ) .await .inspect_err(|error| { diff --git a/crates/l2/sequencer/mod.rs b/crates/l2/sequencer/mod.rs index ba4bf86824..125786fd4c 100644 --- a/crates/l2/sequencer/mod.rs +++ b/crates/l2/sequencer/mod.rs @@ -2,7 +2,7 @@ use std::sync::Arc; use crate::based::sequencer_state::SequencerState; use crate::based::sequencer_state::SequencerStatus; -use crate::{BlockFetcher, SequencerConfig, StateUpdater}; +use crate::{BlockFetcher, SequencerConfig, StateUpdater, monitor}; use block_producer::BlockProducer; use ethrex_blockchain::Blockchain; use ethrex_l2_common::prover::ProverType; @@ -147,11 +147,26 @@ pub async fn start_l2( error!("Error starting State Updater: {err}"); }); - let _ = BlockFetcher::spawn(&cfg, store, rollup_store, blockchain, shared_state) - .await - .inspect_err(|err| { - error!("Error starting Block Fetcher: {err}"); - }); + let _ = BlockFetcher::spawn( + &cfg, + store.clone(), + rollup_store.clone(), + blockchain, + shared_state.clone(), + ) + .await + .inspect_err(|err| { + error!("Error starting Block Fetcher: {err}"); + }); + } + + if cfg.monitor.enabled { + task_set.spawn(monitor::start_monitor( + shared_state.clone(), + store.clone(), + rollup_store.clone(), + cfg.clone(), + )); } while let Some(res) = task_set.join_next().await { diff --git a/crates/l2/tee/quote-gen/Cargo.lock b/crates/l2/tee/quote-gen/Cargo.lock index 948c6af6d5..902cc43236 100644 --- a/crates/l2/tee/quote-gen/Cargo.lock +++ b/crates/l2/tee/quote-gen/Cargo.lock @@ -80,6 +80,12 @@ dependencies = [ "url", ] +[[package]] +name = "allocator-api2" +version = "0.2.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923" + [[package]] name = "alloy-consensus" version = "0.11.1" @@ -993,6 +999,21 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "cassowary" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" + +[[package]] +name = "castaway" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0abae9be0aaf9ea96a3b1b8b1b55c602ca751eba1b1500220cea4ecbafe7c0d5" +dependencies = [ + "rustversion", +] + [[package]] name = "cc" version = "1.2.27" @@ -1024,8 +1045,10 @@ checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-link", ] @@ -1158,12 +1181,53 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "color-eyre" +version = "0.6.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5920befb47832a6d61ee3a3a846565cfa39b331331e68a3b1d1116630f2f26d" +dependencies = [ + "backtrace", + "color-spantrace", + "eyre", + "indenter", + "once_cell", + "owo-colors", + "tracing-error", +] + +[[package]] +name = "color-spantrace" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8b88ea9df13354b55bc7234ebcce36e6ef896aca2e42a15de9e10edce01b427" +dependencies = [ + "once_cell", + "owo-colors", + "tracing-core", + "tracing-error", +] + [[package]] name = "colorchoice" version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "compact_str" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b79c4069c6cad78e2e0cdfcbd26275770669fb39fd308a752dc110e83b9af32" +dependencies = [ + "castaway", + "cfg-if 1.0.1", + "itoa", + "rustversion", + "ryu", + "static_assertions", +] + [[package]] name = "concat-kdf" version = "0.1.0" @@ -1188,7 +1252,7 @@ dependencies = [ "encode_unicode", "libc", "once_cell", - "unicode-width", + "unicode-width 0.2.0", "windows-sys 0.59.0", ] @@ -1252,6 +1316,15 @@ dependencies = [ "unicode-segmentation", ] +[[package]] +name = "convert_case" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb402b8d4c85569410425650ce3eddc7d698ed96d39a73f941b08fb63082f1e7" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.4" @@ -1398,6 +1471,49 @@ version = "0.8.21" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" +[[package]] +name = "crossterm" +version = "0.28.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "mio", + "parking_lot", + "rustix 0.38.44", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8b9f2e4c67f833b660cdb0a3523065869fb35570177239812ed4c905aeff87b" +dependencies = [ + "bitflags 2.9.1", + "crossterm_winapi", + "derive_more 2.0.1", + "document-features", + "mio", + "parking_lot", + "rustix 1.0.7", + "signal-hook", + "signal-hook-mio", + "winapi", +] + +[[package]] +name = "crossterm_winapi" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdd7c62a3665c7f6830a51635d9ac9b23ed385797f70a83bb8bafe9c572ab2b" +dependencies = [ + "winapi", +] + [[package]] name = "crunchy" version = "0.2.4" @@ -1575,7 +1691,7 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" dependencies = [ - "convert_case", + "convert_case 0.6.0", "proc-macro2", "quote", "syn 2.0.104", @@ -1588,6 +1704,7 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3" dependencies = [ + "convert_case 0.7.1", "proc-macro2", "quote", "syn 2.0.104", @@ -1690,6 +1807,15 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "document-features" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95249b50c6c185bee49034bcb378a49dc2b5dff0be90ff6616d31d64febab05d" +dependencies = [ + "litrs", +] + [[package]] name = "dunce" version = "1.0.5" @@ -1796,6 +1922,16 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "env_filter" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "186e05a59d4c50738528153b83b0b0194d3a29507dfec16eccd4b342903397d0" +dependencies = [ + "log", + "regex", +] + [[package]] name = "envy" version = "0.4.2" @@ -2206,6 +2342,7 @@ dependencies = [ "hex", "k256", "keccak-hash", + "kzg-rs", "lazy_static", "once_cell", "rayon", @@ -2246,6 +2383,10 @@ dependencies = [ "bincode", "bytes", "cfg-if 1.0.1", + "chrono", + "clap", + "color-eyre", + "crossterm 0.29.0", "directories", "envy", "ethereum-types 0.15.1", @@ -2256,6 +2397,7 @@ dependencies = [ "ethrex-l2-common", "ethrex-levm", "ethrex-metrics", + "ethrex-p2p", "ethrex-rlp", "ethrex-rpc", "ethrex-sdk", @@ -2268,6 +2410,7 @@ dependencies = [ "keccak-hash", "lazy_static", "rand 0.8.5", + "ratatui", "reqwest 0.12.22", "secp256k1", "serde", @@ -2275,10 +2418,14 @@ dependencies = [ "serde_with", "spawned-concurrency", "spawned-rt", + "tabwriter", "thiserror 2.0.12", "tokio", "tokio-util", "tracing", + "tui-big-text", + "tui-logger", + "tui-scrollview", "vergen-git2", "zkvm_interface", ] @@ -2314,7 +2461,6 @@ dependencies = [ "ethrex-rlp", "k256", "keccak-hash", - "kzg-rs", "lambdaworks-math 0.11.0", "lazy_static", "num-bigint 0.4.6", @@ -2643,6 +2789,12 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2" +[[package]] +name = "font8x8" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "875488b8711a968268c7cf5d139578713097ca4635a76044e8fe8eedf831d07e" + [[package]] name = "foreign-types" version = "0.3.2" @@ -2951,6 +3103,8 @@ version = "0.15.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5971ac85611da7067dbfcabef3c70ebb5606018acd9e2a3903a0da507521e0d5" dependencies = [ + "allocator-api2", + "equivalent", "foldhash", "serde", ] @@ -3451,6 +3605,12 @@ dependencies = [ "serde", ] +[[package]] +name = "indoc" +version = "2.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c7245a08504955605670dbf141fceab975f15ca21570696aebe9d2e71576bd" + [[package]] name = "inout" version = "0.1.4" @@ -3460,6 +3620,19 @@ dependencies = [ "generic-array", ] +[[package]] +name = "instability" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bf9fed6d91cfb734e7476a06bde8300a1b94e217e1b523b6f0cd1a01998c71d" +dependencies = [ + "darling", + "indoc", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "instant" version = "0.1.13" @@ -3792,6 +3965,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "linux-raw-sys" +version = "0.4.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d26c52dbd32dccf2d10cac7725f8eae5296885fb5703b261f7d0a0739ec807ab" + [[package]] name = "linux-raw-sys" version = "0.9.4" @@ -3804,6 +3983,12 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "241eaef5fd12c88705a01fc1066c48c4b36e0dd4377dcdc7ec3942cea7a69956" +[[package]] +name = "litrs" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4ce301924b7887e9d637144fdade93f9dfff9b60981d4ac161db09720d39aa5" + [[package]] name = "lock_api" version = "0.4.13" @@ -3820,6 +4005,15 @@ version = "0.4.27" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +[[package]] +name = "lru" +version = "0.12.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" +dependencies = [ + "hashbrown 0.15.4", +] + [[package]] name = "macro-string" version = "0.1.4" @@ -3899,6 +4093,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78bed444cc8a2160f01cbcf811ef18cac863ad68ae8ca62092e8db51d51c761c" dependencies = [ "libc", + "log", "wasi 0.11.1+wasi-snapshot-preview1", "windows-sys 0.59.0", ] @@ -4181,6 +4376,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" +[[package]] +name = "owo-colors" +version = "4.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48dd4f4a2c8405440fd0462561f0e5806bd0f77e86f51c761481bdd4018b545e" + [[package]] name = "p256" version = "0.13.2" @@ -4732,7 +4933,6 @@ dependencies = [ "ethrex-vm", "hex", "keccak-hash", - "kzg-rs", "secp256k1", "serde", "serde_json", @@ -4823,6 +5023,27 @@ dependencies = [ "rand_core 0.9.3", ] +[[package]] +name = "ratatui" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eabd94c2f37801c20583fc49dd5cd6b0ba68c716787c2dd6ed18571e1e63117b" +dependencies = [ + "bitflags 2.9.1", + "cassowary", + "compact_str", + "crossterm 0.28.1", + "indoc", + "instability", + "itertools 0.13.0", + "lru", + "paste", + "strum", + "unicode-segmentation", + "unicode-truncate", + "unicode-width 0.2.0", +] + [[package]] name = "rayon" version = "1.10.0" @@ -4927,6 +5148,12 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "relative-path" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" + [[package]] name = "reqwest" version = "0.11.27" @@ -5169,6 +5396,36 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "rstest" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0a2c585be59b6b5dd66a9d2084aa1d8bd52fbdb806eafdeffb52791147862035" +dependencies = [ + "futures", + "futures-timer", + "rstest_macros", + "rustc_version 0.4.1", +] + +[[package]] +name = "rstest_macros" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "825ea780781b15345a146be27eaefb05085e337e869bff01b4306a4fd4a9ad5a" +dependencies = [ + "cfg-if 1.0.1", + "glob", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "relative-path", + "rustc_version 0.4.1", + "syn 2.0.104", + "unicode-ident", +] + [[package]] name = "ruint" version = "1.15.0" @@ -5238,6 +5495,19 @@ dependencies = [ "semver 1.0.26", ] +[[package]] +name = "rustix" +version = "0.38.44" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" +dependencies = [ + "bitflags 2.9.1", + "errno", + "libc", + "linux-raw-sys 0.4.15", + "windows-sys 0.59.0", +] + [[package]] name = "rustix" version = "1.0.7" @@ -5247,7 +5517,7 @@ dependencies = [ "bitflags 2.9.1", "errno", "libc", - "linux-raw-sys", + "linux-raw-sys 0.9.4", "windows-sys 0.59.0", ] @@ -5716,6 +5986,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" +[[package]] +name = "signal-hook" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d881a16cf4426aa584979d30bd82cb33429027e42122b169753d6ef1085ed6e2" +dependencies = [ + "libc", + "signal-hook-registry", +] + +[[package]] +name = "signal-hook-mio" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34db1a06d485c9142248b7a054f034b349b212551f3dfd19c94d45a754a217cd" +dependencies = [ + "libc", + "mio", + "signal-hook", +] + [[package]] name = "signal-hook-registry" version = "1.4.5" @@ -6081,6 +6372,15 @@ dependencies = [ "libc", ] +[[package]] +name = "tabwriter" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fce91f2f0ec87dff7e6bcbbeb267439aa1188703003c6055193c821487400432" +dependencies = [ + "unicode-width 0.2.0", +] + [[package]] name = "tap" version = "1.0.1" @@ -6096,7 +6396,7 @@ dependencies = [ "fastrand", "getrandom 0.3.3", "once_cell", - "rustix", + "rustix 1.0.7", "windows-sys 0.59.0", ] @@ -6470,6 +6770,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-error" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b1581020d7a273442f5b45074a6a57d5757ad0a47dac0e9f0bd57b81936f3db" +dependencies = [ + "tracing", + "tracing-subscriber", +] + [[package]] name = "tracing-futures" version = "0.2.5" @@ -6515,6 +6825,47 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tui-big-text" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97cefa9f1425ab6146db2961241cec86845d11105b5dd6bb504294b0cdd21af" +dependencies = [ + "derive_builder", + "font8x8", + "itertools 0.14.0", + "ratatui", +] + +[[package]] +name = "tui-logger" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57ea457a31a3fff1073f83e5c9e1c61a7805c435b2476b1df3a78f934adebabe" +dependencies = [ + "chrono", + "env_filter", + "fxhash", + "lazy_static", + "log", + "parking_lot", + "ratatui", + "tracing", + "tracing-subscriber", + "unicode-segmentation", +] + +[[package]] +name = "tui-scrollview" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef6e1d736488ba64c2e74637089a6b9ca666ccd2eaade3ab83854f415f1d260b" +dependencies = [ + "indoc", + "ratatui", + "rstest", +] + [[package]] name = "tungstenite" version = "0.20.1" @@ -6608,11 +6959,28 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" +[[package]] +name = "unicode-truncate" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b3644627a5af5fa321c95b9b235a72fd24cd29c648c2c379431e6628655627bf" +dependencies = [ + "itertools 0.13.0", + "unicode-segmentation", + "unicode-width 0.1.14", +] + [[package]] name = "unicode-width" -version = "0.2.1" +version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a1a07cc7db3810833284e8d372ccdc6da29741639ecc70c9ec107df0fa6154c" +checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" + +[[package]] +name = "unicode-width" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fc81956842c57dac11422a97c3b8195a1ff727f06e85c84ed2e8aa277c9a0fd" [[package]] name = "unicode-xid" @@ -7396,8 +7764,6 @@ dependencies = [ "ethrex-storage", "ethrex-trie", "ethrex-vm", - "keccak-hash", - "kzg-rs", "serde", "serde_json", "serde_with", diff --git a/crates/l2/tests/tests.rs b/crates/l2/tests/tests.rs index c3f3cd2033..c57f2ccce0 100644 --- a/crates/l2/tests/tests.rs +++ b/crates/l2/tests/tests.rs @@ -201,7 +201,12 @@ async fn test_privileged_tx_with_contract_call( let topic = keccak(b"NumberSet(uint256)"); while proposer_client - .get_logs(first_block, block_number, deployed_contract_address, topic) + .get_logs( + first_block, + block_number, + deployed_contract_address, + vec![topic], + ) .await .is_ok_and(|logs| logs.is_empty()) { @@ -211,7 +216,12 @@ async fn test_privileged_tx_with_contract_call( } let logs = proposer_client - .get_logs(first_block, block_number, deployed_contract_address, topic) + .get_logs( + first_block, + block_number, + deployed_contract_address, + vec![topic], + ) .await?; let number_emitted = U256::from_big_endian( @@ -1396,7 +1406,7 @@ async fn wait_for_l2_deposit_receipt( U256::from(l1_receipt_block_number), U256::from(l1_receipt_block_number), common_bridge_address(), - topic, + vec![topic], ) .await?; let data = PrivilegedTransactionData::from_log(logs.first().unwrap().log.clone())?; diff --git a/crates/networking/rpc/clients/eth/mod.rs b/crates/networking/rpc/clients/eth/mod.rs index b1f8530e18..0d98306b06 100644 --- a/crates/networking/rpc/clients/eth/mod.rs +++ b/crates/networking/rpc/clients/eth/mod.rs @@ -707,7 +707,7 @@ impl EthClient { from_block: U256, to_block: U256, address: Address, - topic: H256, + topics: Vec, ) -> Result, EthClientError> { let request = RpcRequest { id: RpcRequestId::Number(1), @@ -718,7 +718,7 @@ impl EthClient { "fromBlock": format!("{:#x}", from_block), "toBlock": format!("{:#x}", to_block), "address": format!("{:#x}", address), - "topics": [format!("{:#x}", topic)] + "topics": topics.iter().map(|topic| format!("{topic:#x}")).collect::>() } )]), }; diff --git a/crates/networking/rpc/lib.rs b/crates/networking/rpc/lib.rs index 1bd358427b..21c86e7dc4 100644 --- a/crates/networking/rpc/lib.rs +++ b/crates/networking/rpc/lib.rs @@ -23,7 +23,7 @@ pub use eth::{ transaction::EstimateGasRequest, }; pub use rpc::{ - NodeData, RpcApiContext, RpcHandler, RpcRequestWrapper, map_eth_requests, map_http_requests, - rpc_response, shutdown_signal, + NodeData, RpcApiContext, RpcHandler, RpcRequestWrapper, map_debug_requests, map_eth_requests, + map_http_requests, rpc_response, shutdown_signal, }; pub use utils::{RpcErr, RpcErrorMetadata, RpcNamespace}; diff --git a/crates/networking/rpc/mempool.rs b/crates/networking/rpc/mempool.rs index 7766ad9dd1..65eba67416 100644 --- a/crates/networking/rpc/mempool.rs +++ b/crates/networking/rpc/mempool.rs @@ -13,8 +13,8 @@ type MempoolContentEntry = HashMap>; /// Transactions are grouped by sender and indexed by nonce #[derive(Serialize, Deserialize)] pub struct MempoolContent { - pending: MempoolContentEntry, - queued: MempoolContentEntry, + pub pending: MempoolContentEntry, + pub queued: MempoolContentEntry, } #[derive(Serialize, Deserialize)] diff --git a/crates/networking/rpc/types/block.rs b/crates/networking/rpc/types/block.rs index 669976690d..49ac771af5 100644 --- a/crates/networking/rpc/types/block.rs +++ b/crates/networking/rpc/types/block.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; pub struct RpcBlock { hash: H256, #[serde(with = "serde_utils::u64::hex_str")] - size: u64, + pub size: u64, #[serde(flatten)] pub header: BlockHeader, #[serde(flatten)]