diff --git a/Cargo.lock b/Cargo.lock index a074abf36ea..74edb52d613 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -43,6 +43,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom 0.2.16", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.12" @@ -154,7 +165,8 @@ dependencies = [ [[package]] name = "alloy-contract" version = "1.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=35bc323cc23911e52cb1ec8f6a58ebb5d5869b27#35bc323cc23911e52cb1ec8f6a58ebb5d5869b27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5903097e4c131ad2dd80d87065f23c715ccb9cdb905fa169dffab8e1e798bae" dependencies = [ "alloy-consensus", "alloy-dyn-abi", @@ -1055,7 +1067,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" dependencies = [ - "ahash", + "ahash 0.8.12", "ark-ff 0.5.0", "ark-poly", "ark-serialize 0.5.0", @@ -1202,7 +1214,7 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" dependencies = [ - "ahash", + "ahash 0.8.12", "ark-ff 0.5.0", "ark-serialize 0.5.0", "ark-std 0.5.0", @@ -1934,6 +1946,28 @@ version = "1.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7575182f7272186991736b70173b0ea045398f984bf5ebbb3804736ce1330c9d" +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "bytecount" version = "0.6.9" @@ -4457,6 +4491,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -4470,7 +4507,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.12", ] [[package]] @@ -5792,7 +5829,7 @@ version = "0.24.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25dea7ac8057892855ec285c440160265225438c3c45072613c25a4b26e98ef5" dependencies = [ - "ahash", + "ahash 0.8.12", "portable-atomic", ] @@ -7061,6 +7098,26 @@ dependencies = [ "syn 2.0.110", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "pulldown-cmark" version = "0.9.6" @@ -7498,6 +7555,15 @@ version = "1.9.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba39f3699c378cd8970968dcbff9c43159ea4cfbd88d43c00b22f2ef10a435d2" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.11.27" @@ -9823,13 +9889,17 @@ dependencies = [ "reth-rpc", "reth-rpc-api", "reth-rpc-engine-api", + "reth-rpc-eth-api", "reth-rpc-eth-types", "reth-rpc-server-types", "reth-tasks", "reth-tracing", "reth-transaction-pool", "reth-trie-common", + "reth-xlayer-gasprice", + "reth-xlayer-txpool", "revm", + "rust_decimal", "serde", "serde_json", "tokio", @@ -11243,6 +11313,37 @@ dependencies = [ "tracing", ] +[[package]] +name = "reth-xlayer-gasprice" +version = "1.7.0" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "parking_lot", + "reth-primitives-traits", + "reth-provider", + "reth-rpc-eth-api", + "reth-transaction-pool", + "tokio", + "tracing", +] + +[[package]] +name = "reth-xlayer-txpool" +version = "1.9.1" +dependencies = [ + "alloy-consensus", + "alloy-primitives", + "parking_lot", + "reth-chainspec", + "reth-optimism-txpool", + "reth-primitives-traits", + "reth-rpc-eth-api", + "reth-storage-api", + "reth-transaction-pool", + "tracing", +] + [[package]] name = "reth-zstd-compressors" version = "1.9.1" @@ -11499,6 +11600,35 @@ dependencies = [ "digest 0.10.7", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rlimit" version = "0.10.2" @@ -11603,7 +11733,7 @@ dependencies = [ "regex", "relative-path", "rustc_version 0.4.1", - "syn 2.0.108", + "syn 2.0.110", "unicode-ident", ] @@ -11685,6 +11815,22 @@ dependencies = [ "time 0.1.45", ] +[[package]] +name = "rust_decimal" +version = "1.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand 0.8.5", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.26" @@ -11927,7 +12073,7 @@ version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "356285bbf17bea63d9e52e96bd18f039672ac92b55b8cb997d6162a2a37d1649" dependencies = [ - "ahash", + "ahash 0.8.12", "cfg-if", "hashbrown 0.13.2", ] @@ -11944,6 +12090,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "sec1" version = "0.7.3" @@ -12342,6 +12494,12 @@ version = "0.3.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "similar" version = "2.7.0" @@ -14596,15 +14754,9 @@ dependencies = [ "rustix 1.1.2", ] -[[package]] -name = "xsum" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0637d3a5566a82fa5214bae89087bc8c9fb94cd8e8a3c07feb691bb8d9c632db" - [[package]] name = "xlayer-e2e-test" -version = "1.8.3" +version = "1.9.1" dependencies = [ "alloy-consensus", "alloy-eips", @@ -14631,6 +14783,12 @@ dependencies = [ "url", ] +[[package]] +name = "xsum" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0637d3a5566a82fa5214bae89087bc8c9fb94cd8e8a3c07feb691bb8d9c632db" + [[package]] name = "yansi" version = "1.0.1" diff --git a/Cargo.toml b/Cargo.toml index 581d72b249e..2557774f0f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -87,6 +87,8 @@ members = [ "crates/optimism/rpc/", "crates/optimism/storage", "crates/optimism/txpool/", + "crates/xlayer/gasprice/", + "crates/xlayer/txpool/", "crates/payload/basic/", "crates/payload/builder/", "crates/payload/builder-primitives/", @@ -421,6 +423,8 @@ reth-optimism-primitives = { path = "crates/optimism/primitives", default-featur reth-optimism-rpc = { path = "crates/optimism/rpc" } reth-optimism-storage = { path = "crates/optimism/storage" } reth-optimism-txpool = { path = "crates/optimism/txpool" } +reth-xlayer-gasprice = { path = "crates/xlayer/gasprice" } +reth-xlayer-txpool = { path = "crates/xlayer/txpool" } reth-payload-builder = { path = "crates/payload/builder" } reth-payload-builder-primitives = { path = "crates/payload/builder-primitives" } reth-payload-primitives = { path = "crates/payload/primitives" } @@ -567,6 +571,7 @@ parking_lot = "0.12" paste = "1.0" rand = "0.9" rayon = "1.7" +rust_decimal = { version = "1.39", default-features = false, features = ["std"] } rustc-hash = { version = "2.0", default-features = false } schnellru = "0.2" serde = { version = "1.0", default-features = false } @@ -781,7 +786,6 @@ vergen-git2 = "1.0.5" [patch.crates-io] alloy-consensus = { git = "https://github.com/alloy-rs/alloy", rev = "35bc323cc23911e52cb1ec8f6a58ebb5d5869b27" } -alloy-contract = { git = "https://github.com/alloy-rs/alloy", rev = "35bc323cc23911e52cb1ec8f6a58ebb5d5869b27" } alloy-eips = { git = "https://github.com/alloy-rs/alloy", rev = "35bc323cc23911e52cb1ec8f6a58ebb5d5869b27" } alloy-genesis = { git = "https://github.com/alloy-rs/alloy", rev = "35bc323cc23911e52cb1ec8f6a58ebb5d5869b27" } alloy-json-rpc = { git = "https://github.com/alloy-rs/alloy", rev = "35bc323cc23911e52cb1ec8f6a58ebb5d5869b27" } diff --git a/crates/optimism/node/Cargo.toml b/crates/optimism/node/Cargo.toml index 085362059f2..b31620c4ab1 100644 --- a/crates/optimism/node/Cargo.toml +++ b/crates/optimism/node/Cargo.toml @@ -25,12 +25,13 @@ reth-transaction-pool.workspace = true reth-network.workspace = true reth-evm.workspace = true reth-rpc-server-types.workspace = true -reth-tasks = { workspace = true, optional = true } +reth-tasks.workspace = true reth-trie-common.workspace = true reth-node-core.workspace = true reth-rpc-engine-api.workspace = true reth-engine-local = { workspace = true, features = ["op"] } reth-rpc-api.workspace = true +reth-rpc-eth-api.workspace = true # op-reth reth-optimism-payload-builder.workspace = true @@ -43,6 +44,10 @@ reth-optimism-consensus = { workspace = true, features = ["std"] } reth-optimism-forks.workspace = true reth-optimism-primitives = { workspace = true, features = ["serde", "serde-bincode-compat", "reth-codec"] } +# xlayer +reth-xlayer-gasprice.workspace = true +reth-xlayer-txpool.workspace = true + # revm with required optimism features # Note: this must be kept to ensure all features are properly enabled/forwarded revm = { workspace = true, features = ["secp256k1", "blst", "c-kzg", "memory_limit"] } @@ -65,6 +70,7 @@ clap.workspace = true serde.workspace = true eyre.workspace = true url.workspace = true +rust_decimal.workspace = true # test-utils dependencies reth-e2e-test-utils = { workspace = true, optional = true } @@ -101,7 +107,6 @@ js-tracer = [ "reth-rpc-eth-types/js-tracer", ] test-utils = [ - "reth-tasks", "reth-e2e-test-utils", "alloy-genesis", "serde_json", diff --git a/crates/optimism/node/src/args_xlayer.rs b/crates/optimism/node/src/args_xlayer.rs index 9b079944da6..26e8c72e1ba 100644 --- a/crates/optimism/node/src/args_xlayer.rs +++ b/crates/optimism/node/src/args_xlayer.rs @@ -1,5 +1,8 @@ +use alloy_primitives::U256; use clap::Args; use reth_optimism_payload_builder::BridgeInterceptConfig; +use rust_decimal::Decimal; +use std::time::Duration; /// X Layer specific configuration flags #[derive(Debug, Clone, Args, PartialEq, Eq, Default)] @@ -12,6 +15,9 @@ pub struct XLayerArgs { /// Enable Apollo #[command(flatten)] pub apollo: ApolloArgs, + /// XLayer gas price configuration + #[command(flatten)] + pub gas_price: XLayerGasPriceArgs, // /// Another X Layer feature // #[command(flatten)] // pub another_feature: AnotherFeatureArgs, @@ -20,8 +26,10 @@ pub struct XLayerArgs { impl XLayerArgs { /// Validate all X Layer configurations pub fn validate(&self) -> Result<(), String> { - self.intercept.validate() + self.intercept.validate()?; + self.gas_price.validate()?; // self.another_feature.validate()?; + Ok(()) } } @@ -149,6 +157,176 @@ pub struct ApolloArgs { pub apollo_namespace: String, } +/// XLayer gas price configuration +#[derive(Debug, Clone, Args, PartialEq, Eq, Default)] +#[command(next_help_heading = "XLayer Gas Price")] +pub struct XLayerGasPriceArgs { + /// Gas price calculation type: "default", "follower", or "fixed" + /// If not specified, XLayer gas price scheduler will not be initialized + #[arg(long = "xlayer.gasprice.type")] + pub price_type: Option, + + /// Gas price update period + #[arg(long = "xlayer.gasprice.update-period", value_parser = parse_duration)] + pub update_period: Option, + + /// Gas price calculation factor + #[arg(long = "xlayer.gasprice.factor")] + pub factor: Option, + + /// Kafka URL for gas price updates + #[arg(long = "xlayer.gasprice.kafka-url")] + pub kafka_url: Option, + + /// Kafka topic for gas price updates + #[arg(long = "xlayer.gasprice.topic")] + pub topic: Option, + + /// Kafka consumer group ID + #[arg(long = "xlayer.gasprice.group-id")] + pub group_id: Option, + + /// Kafka username for authentication + #[arg(long = "xlayer.gasprice.username")] + pub username: Option, + + /// Kafka password for authentication + #[arg(long = "xlayer.gasprice.password")] + pub password: Option, + + /// Path to Kafka root CA certificate + #[arg(long = "xlayer.gasprice.root-ca-path")] + pub root_ca_path: Option, + + /// L1 coin ID for price tracking + #[arg(long = "xlayer.gasprice.l1-coin-id")] + pub l1_coin_id: Option, + + /// L2 coin ID for price tracking + #[arg(long = "xlayer.gasprice.l2-coin-id")] + pub l2_coin_id: Option, + + /// Default L1 coin price (fallback value) + #[arg(long = "xlayer.gasprice.default-l1-coin-price")] + pub default_l1_coin_price: Option, + + /// Default L2 coin price (fallback value) + #[arg(long = "xlayer.gasprice.default-l2-coin-price")] + pub default_l2_coin_price: Option, + + /// Fixed gas price in USDT (for "fixed" type) + #[arg(long = "xlayer.gasprice.gas-price-usdt")] + pub gas_price_usdt: Option, + + /// Congestion threshold for dynamic gas price adjustment + #[arg(long = "xlayer.gasprice.congestion-threshold")] + pub congestion_threshold: Option, + + /// Default gas price for XLayer (in wei) + #[arg(long = "xlayer.gasprice.default")] + pub default_gas_price: Option, +} + +/// Helper function to parse duration from string +fn parse_duration(s: &str) -> Result { + // Parse duration string like "10s", "5m", "1h" + let s = s.trim(); + if s.is_empty() { + return Err("empty duration string".to_string()); + } + + let (num_str, unit) = if let Some(pos) = s.find(|c: char| !c.is_ascii_digit()) { + (&s[..pos], &s[pos..]) + } else { + return Err("duration must have a unit (s, m, h)".to_string()); + }; + + let num: u64 = num_str.parse().map_err(|e| format!("invalid number: {}", e))?; + + match unit { + "s" | "sec" | "second" | "seconds" => Ok(Duration::from_secs(num)), + "m" | "min" | "minute" | "minutes" => Ok(Duration::from_secs(num * 60)), + "h" | "hour" | "hours" => Ok(Duration::from_secs(num * 3600)), + "ms" | "millisecond" | "milliseconds" => Ok(Duration::from_millis(num)), + _ => Err(format!("unknown duration unit: {}", unit)), + } +} + +impl XLayerGasPriceArgs { + /// Validate gas price configuration + pub fn validate(&self) -> Result<(), String> { + // If price_type is not specified, no validation needed + let Some(price_type) = &self.price_type else { + return Ok(()); + }; + + // Validate price_type is one of the supported types + match price_type.as_str() { + "default" | "follower" | "fixed" => {} + _ => { + return Err(format!( + "Invalid gas price type '{}'. Must be 'default', 'follower', or 'fixed'", + price_type + )) + } + } + + // Validate update_period if specified + if let Some(period) = self.update_period { + if period.is_zero() { + return Err("Gas price update period must be greater than zero".to_string()); + } + } + + // Validate factor if specified + if let Some(factor) = self.factor { + if factor <= Decimal::ZERO { + return Err("Gas price factor must be greater than zero".to_string()); + } + } + + // Validate coin IDs if specified + if let Some(l1_coin_id) = self.l1_coin_id { + if l1_coin_id < 0 { + return Err("L1 coin ID must be non-negative".to_string()); + } + } + + if let Some(l2_coin_id) = self.l2_coin_id { + if l2_coin_id < 0 { + return Err("L2 coin ID must be non-negative".to_string()); + } + } + + // Validate default coin prices if specified + if let Some(price) = self.default_l1_coin_price { + if price <= Decimal::ZERO { + return Err("Default L1 coin price must be greater than zero".to_string()); + } + } + + if let Some(price) = self.default_l2_coin_price { + if price <= Decimal::ZERO { + return Err("Default L2 coin price must be greater than zero".to_string()); + } + } + + // TODO: add follower and fixed type validation + + Ok(()) + } +} + +impl reth_xlayer_gasprice::suggester::XLayerGasPriceArgsTrait for XLayerGasPriceArgs { + fn price_type(&self) -> Option<&str> { + self.price_type.as_deref() + } + + fn default(&self) -> Option { + self.default_gas_price + } +} + #[cfg(test)] mod tests { use super::*; @@ -168,6 +346,7 @@ mod tests { assert_eq!(args.intercept.enabled, default_args.intercept.enabled); assert_eq!(args.intercept.bridge_contract, default_args.intercept.bridge_contract); assert_eq!(args.intercept.target_token, default_args.intercept.target_token); + assert_eq!(args.gas_price, default_args.gas_price); assert!(args.validate().is_ok()); } @@ -348,4 +527,154 @@ mod tests { assert!(config.enabled); assert!(!config.wildcard); } + + #[test] + fn test_parse_xlayer_gas_price_args() { + use std::str::FromStr; + let args = CommandParser::::parse_from([ + "reth", + "--xlayer.gasprice.type", + "follower", + "--xlayer.gasprice.factor", + "1.5", + ]) + .args; + assert_eq!(args.price_type, Some("follower".to_string())); + assert_eq!(args.factor, Some(Decimal::from_str("1.5").unwrap())); + } + + #[test] + fn test_parse_duration() { + assert_eq!(parse_duration("10s").unwrap(), Duration::from_secs(10)); + assert_eq!(parse_duration("5m").unwrap(), Duration::from_secs(300)); + assert_eq!(parse_duration("1h").unwrap(), Duration::from_secs(3600)); + assert_eq!(parse_duration("500ms").unwrap(), Duration::from_millis(500)); + assert!(parse_duration("invalid").is_err()); + } + + #[test] + fn test_gas_price_validate_no_type() { + let args = XLayerGasPriceArgs::default(); + assert!(args.validate().is_ok()); + } + + #[test] + fn test_gas_price_validate_invalid_type() { + let args = XLayerGasPriceArgs { + price_type: Some("invalid".to_string()), + ..Default::default() + }; + assert!(args.validate().is_err()); + } + + #[test] + fn test_gas_price_validate_fixed_without_usdt() { + let args = XLayerGasPriceArgs { + price_type: Some("fixed".to_string()), + gas_price_usdt: None, + ..Default::default() + }; + assert!(args.validate().is_err()); + } + + #[test] + fn test_gas_price_validate_fixed_with_valid_usdt() { + use std::str::FromStr; + let args = XLayerGasPriceArgs { + price_type: Some("fixed".to_string()), + gas_price_usdt: Some(Decimal::from_str("0.001").unwrap()), + ..Default::default() + }; + assert!(args.validate().is_ok()); + } + + #[test] + fn test_gas_price_validate_fixed_with_zero_usdt() { + let args = XLayerGasPriceArgs { + price_type: Some("fixed".to_string()), + gas_price_usdt: Some(Decimal::ZERO), + ..Default::default() + }; + assert!(args.validate().is_err()); + } + + #[test] + fn test_gas_price_validate_follower_without_kafka() { + let args = XLayerGasPriceArgs { + price_type: Some("follower".to_string()), + ..Default::default() + }; + assert!(args.validate().is_err()); + } + + #[test] + fn test_gas_price_validate_follower_with_kafka() { + let args = XLayerGasPriceArgs { + price_type: Some("follower".to_string()), + kafka_url: Some("localhost:9092".to_string()), + topic: Some("gas-price".to_string()), + group_id: Some("reth-consumer".to_string()), + ..Default::default() + }; + assert!(args.validate().is_ok()); + } + + #[test] + fn test_gas_price_validate_default_type() { + let args = XLayerGasPriceArgs { + price_type: Some("default".to_string()), + ..Default::default() + }; + assert!(args.validate().is_ok()); + } + + #[test] + fn test_gas_price_validate_negative_coin_id() { + let args = XLayerGasPriceArgs { + price_type: Some("default".to_string()), + l1_coin_id: Some(-1), + ..Default::default() + }; + assert!(args.validate().is_err()); + } + + #[test] + fn test_gas_price_validate_zero_factor() { + let args = XLayerGasPriceArgs { + price_type: Some("default".to_string()), + factor: Some(Decimal::ZERO), + ..Default::default() + }; + assert!(args.validate().is_err()); + } + + #[test] + fn test_gas_price_validate_zero_update_period() { + let args = XLayerGasPriceArgs { + price_type: Some("default".to_string()), + update_period: Some(Duration::ZERO), + ..Default::default() + }; + assert!(args.validate().is_err()); + } + + #[test] + fn test_gas_price_validate_negative_congestion_threshold() { + let args = XLayerGasPriceArgs { + price_type: Some("default".to_string()), + congestion_threshold: Some(-1), + ..Default::default() + }; + assert!(args.validate().is_err()); + } + + #[test] + fn test_gas_price_validate_zero_default_coin_price() { + let args = XLayerGasPriceArgs { + price_type: Some("default".to_string()), + default_l1_coin_price: Some(Decimal::ZERO), + ..Default::default() + }; + assert!(args.validate().is_err()); + } } diff --git a/crates/optimism/node/src/lib.rs b/crates/optimism/node/src/lib.rs index 9d7da2720dc..8dec22644a3 100644 --- a/crates/optimism/node/src/lib.rs +++ b/crates/optimism/node/src/lib.rs @@ -22,6 +22,7 @@ pub use engine::OpEngineTypes; pub mod node; pub use node::*; + pub mod rpc; pub use rpc::OpEngineApiBuilder; @@ -43,8 +44,9 @@ pub use reth_optimism_evm::*; pub use reth_optimism_storage::OpStorage; +mod node_xlayer; mod args_xlayer; -pub use args_xlayer::XLayerArgs; +pub use args_xlayer::{XLayerArgs, XLayerGasPriceArgs}; use op_revm as _; use revm as _; diff --git a/crates/optimism/node/src/node.rs b/crates/optimism/node/src/node.rs index 110556acc4d..cd33a1dfd9b 100644 --- a/crates/optimism/node/src/node.rs +++ b/crates/optimism/node/src/node.rs @@ -3,7 +3,8 @@ use crate::{ args::RollupArgs, engine::OpEngineValidator, - txpool::{OpTransactionPool, OpTransactionValidator}, + node_xlayer::{initialize_xlayer_gas_price_controller, XLayerValidatorAccess}, + txpool::OpTransactionValidator, OpEngineApiBuilder, OpEngineTypes, XLayerArgs, }; use op_alloy_consensus::{interop::SafetyLevel, OpPooledTransaction}; @@ -59,10 +60,11 @@ use reth_optimism_txpool::{ use reth_provider::{providers::ProviderFactoryBuilder, CanonStateSubscriptions}; use reth_rpc_api::{eth::{EthApiTypes, RpcTypes}, DebugApiServer, L2EthApiExtServer, XLayerEthApiExtServer}; use reth_rpc_server_types::RethRpcModule; +use reth_xlayer_txpool::XLayerTransactionValidator; use reth_tracing::tracing::{debug, info}; use reth_transaction_pool::{ - blobstore::DiskFileBlobStore, EthPoolTransaction, PoolPooledTx, PoolTransaction, - TransactionPool, TransactionValidationTaskExecutor, + blobstore::DiskFileBlobStore, CoinbaseTipOrdering, EthPoolTransaction, PoolPooledTx, + PoolTransaction, TransactionPool, TransactionValidationTaskExecutor, }; use reth_trie_common::KeccakKeyHasher; use serde::de::DeserializeOwned; @@ -207,6 +209,7 @@ impl OpNode { .with_min_suggested_priority_fee(self.args.min_suggested_priority_fee) .with_historical_rpc(self.args.historical_rpc.clone()) .with_flashblocks(self.args.flashblocks_url.clone()) + .with_xlayer_args(self.args.xlayer_args.clone()) } /// Instantiates the [`ProviderFactoryBuilder`] for an opstack node. @@ -330,6 +333,8 @@ pub struct OpAddOns< /// Enable transaction conditionals. enable_tx_conditional: bool, min_suggested_priority_fee: u64, + /// XLayer arguments + xlayer_args: XLayerArgs, } impl OpAddOns @@ -348,6 +353,7 @@ where historical_rpc: Option, enable_tx_conditional: bool, min_suggested_priority_fee: u64, + xlayer_args: XLayerArgs, ) -> Self { Self { rpc_add_ons, @@ -358,6 +364,7 @@ where historical_rpc, enable_tx_conditional, min_suggested_priority_fee, + xlayer_args, } } } @@ -409,6 +416,7 @@ where historical_rpc, enable_tx_conditional, min_suggested_priority_fee, + xlayer_args, .. } = self; OpAddOns::new( @@ -420,6 +428,7 @@ where historical_rpc, enable_tx_conditional, min_suggested_priority_fee, + xlayer_args, ) } @@ -437,6 +446,7 @@ where enable_tx_conditional, min_suggested_priority_fee, historical_rpc, + xlayer_args, .. } = self; OpAddOns::new( @@ -448,6 +458,7 @@ where historical_rpc, enable_tx_conditional, min_suggested_priority_fee, + xlayer_args, ) } @@ -468,6 +479,7 @@ where enable_tx_conditional, min_suggested_priority_fee, historical_rpc, + xlayer_args, .. } = self; OpAddOns::new( @@ -479,6 +491,7 @@ where historical_rpc, enable_tx_conditional, min_suggested_priority_fee, + xlayer_args, ) } @@ -519,7 +532,10 @@ where ::ChainSpec, >, >, - Pool: TransactionPool, + Pool: TransactionPool + XLayerValidatorAccess< + Provider = N::Provider, + Transaction = <::Pool as TransactionPool>::Transaction, + >, >, EthB: EthApiBuilder, PVB: Send, @@ -543,6 +559,7 @@ where sequencer_headers, enable_tx_conditional, historical_rpc, + xlayer_args, .. } = self; @@ -593,6 +610,7 @@ where ctx.node.provider().clone(), ); + let task_executor = ctx.node.task_executor().clone(); rpc_add_ons .launch_add_ons_with(ctx, move |container| { let reth_node_builder::rpc::RpcModuleContainer { modules, auth_module, registry } = @@ -633,6 +651,21 @@ where RethRpcModule::Eth, XLayerEthApiExtServer::::into_rpc(xlayer_eth_ext), )?; + + // For xlayer: gas price controller + if let Some(ref price_type) = xlayer_args.gas_price.price_type { + if !price_type.is_empty() { + // Get validator from the transaction pool using the extension trait + let validator = registry.pool().get_xlayer_validator(); + + initialize_xlayer_gas_price_controller( + &xlayer_args.gas_price, + registry, + task_executor, + validator, + ); + } + } Ok(()) }) @@ -656,8 +689,11 @@ where ::ChainSpec, >, >, + Pool: TransactionPool + XLayerValidatorAccess< + Provider = N::Provider, + Transaction = <::Pool as TransactionPool>::Transaction, + >, >, - <::Pool as TransactionPool>::Transaction: OpPooledTx, EthB: EthApiBuilder, PVB: PayloadValidatorBuilder, EB: EngineApiBuilder, @@ -717,6 +753,8 @@ pub struct OpAddOnsBuilder { tokio_runtime: Option, /// A URL pointing to a secure websocket service that streams out flashblocks. flashblocks_url: Option, + /// XLayer arguments + xlayer_args: XLayerArgs, } impl Default for OpAddOnsBuilder { @@ -733,6 +771,7 @@ impl Default for OpAddOnsBuilder { rpc_middleware: Identity::new(), tokio_runtime: None, flashblocks_url: None, + xlayer_args: XLayerArgs::default(), } } } @@ -801,6 +840,7 @@ impl OpAddOnsBuilder { tokio_runtime, _nt, flashblocks_url, + xlayer_args, .. } = self; OpAddOnsBuilder { @@ -815,6 +855,7 @@ impl OpAddOnsBuilder { rpc_middleware, tokio_runtime, flashblocks_url, + xlayer_args, } } @@ -823,6 +864,12 @@ impl OpAddOnsBuilder { self.flashblocks_url = flashblocks_url; self } + + /// With XLayer arguments. + pub fn with_xlayer_args(mut self, xlayer_args: XLayerArgs) -> Self { + self.xlayer_args = xlayer_args; + self + } } impl OpAddOnsBuilder { @@ -848,6 +895,7 @@ impl OpAddOnsBuilder { rpc_middleware, tokio_runtime, flashblocks_url, + xlayer_args, .. } = self; @@ -871,6 +919,7 @@ impl OpAddOnsBuilder { historical_rpc, enable_tx_conditional, min_suggested_priority_fee, + xlayer_args, ) } } @@ -969,7 +1018,11 @@ where Node: FullNodeTypes>, T: EthPoolTransaction> + OpPooledTx, { - type Pool = OpTransactionPool; + type Pool = reth_transaction_pool::Pool< + TransactionValidationTaskExecutor>, + CoinbaseTipOrdering, + DiskFileBlobStore, + >; async fn build_pool(self, ctx: &BuilderContext) -> eyre::Result { let Self { pool_config_overrides, .. } = self; @@ -1004,11 +1057,13 @@ where ) .build_with_tasks(ctx.task_executor().clone(), blob_store.clone()) .map(|validator| { - OpTransactionValidator::new(validator) + let op_validator = OpTransactionValidator::new(validator) // In --dev mode we can't require gas fees because we're unable to decode // the L1 block info .require_l1_data_gas_fee(!ctx.config().dev.dev) - .with_supervisor(supervisor_client.clone()) + .with_supervisor(supervisor_client.clone()); + // For xlayer: wrap the op transaction validator within the xlayer transaction validator + XLayerTransactionValidator::new(op_validator) }); let final_pool_config = pool_config_overrides.apply(ctx.pool_config()); diff --git a/crates/optimism/node/src/node_xlayer.rs b/crates/optimism/node/src/node_xlayer.rs new file mode 100644 index 00000000000..a0665d3af06 --- /dev/null +++ b/crates/optimism/node/src/node_xlayer.rs @@ -0,0 +1,96 @@ +//! XLayer-specific node functionality for Optimism. + +use crate::XLayerGasPriceArgs; +use reth_node_api::FullNodeComponents; +use reth_node_builder::rpc::RpcRegistry; +use reth_optimism_forks::OpHardforks; +use reth_rpc_api::eth::EthApiTypes; +use reth_tasks::TaskExecutor; +use reth_tracing::tracing::info; +use reth_transaction_pool::{ + blobstore::DiskFileBlobStore, CoinbaseTipOrdering, EthPoolTransaction, PoolTransaction, + TransactionPool, TransactionValidationTaskExecutor, +}; +use reth_xlayer_txpool::XLayerTransactionValidator; +use reth_optimism_txpool::OpPooledTx; +use std::sync::Arc; + +/// Extension trait for accessing XLayer validator from the pool. +pub(crate) trait XLayerValidatorAccess { + type Provider; + type Transaction: PoolTransaction; + + /// Get the XLayer validator from the pool. + fn get_xlayer_validator(&self) -> Arc>; +} + +/// Implement the extension trait for the specific Pool type used in OpNode. +impl XLayerValidatorAccess for reth_transaction_pool::Pool< + TransactionValidationTaskExecutor>, + CoinbaseTipOrdering, + DiskFileBlobStore, +> +where + Client: reth_provider::ChainSpecProvider + reth_provider::StateProviderFactory + reth_provider::BlockReaderIdExt + Send + Sync + 'static, + Client::ChainSpec: OpHardforks, + Tx: EthPoolTransaction + OpPooledTx, +{ + type Provider = Client; + type Transaction = Tx; + + fn get_xlayer_validator(&self) -> Arc> { + self.validator().validator_arc().clone() + } +} + +/// Initialize XLayer gas price controller +/// +/// This function sets up the gas price suggester, scheduler, and spawns the background task +/// for managing XLayer gas prices. +pub(crate) fn initialize_xlayer_gas_price_controller( + gas_price: &XLayerGasPriceArgs, + registry: &mut RpcRegistry, + task_executor: TaskExecutor, + validator: Arc::Transaction>>, +) where + Node: FullNodeComponents, + Node::Pool: TransactionPool, + EthApi: EthApiTypes + reth_rpc_eth_api::helpers::XLayerFees + reth_rpc_eth_api::helpers::LegacyRpc + Clone, +{ + info!(?gas_price, "Initializing XLayer gas price scheduler"); + + // Create gas price suggester directly from args + let pricer = reth_xlayer_gasprice::suggester::new_l2_gas_price_suggester(gas_price); + + // Get EthApi to share with scheduler + let eth_api = registry.eth_api().clone(); + + // Set pricer to XLayerFees + // OpEthApi implements XLayerFees, so we can call set_pricer + eth_api.set_pricer(pricer.clone()); + + // Set pricer to XLayerTransactionValidator + validator.set_pricer(pricer.clone()); + + // Create scheduler with EthApi (which contains the shared GasPriceOracle) + let scheduler = std::sync::Arc::new( + reth_xlayer_gasprice::scheduler::XLayerScheduler::with_eth_api( + pricer, + eth_api, + gas_price.default_gas_price, + gas_price.update_period, + gas_price.congestion_threshold, + ) + ); + + // Spawn background task - run() will initialize and start the scheduler + task_executor.spawn_critical( + "xlayer-gas-price-scheduler", + Box::pin(async move { + scheduler.run().await; + }) + ); + + info!(target: "reth::cli", "XLayer gas price scheduler initialized"); +} + diff --git a/crates/optimism/rpc/src/eth/mod.rs b/crates/optimism/rpc/src/eth/mod.rs index 3a6a918edc1..ccc02a3bc52 100644 --- a/crates/optimism/rpc/src/eth/mod.rs +++ b/crates/optimism/rpc/src/eth/mod.rs @@ -7,6 +7,7 @@ pub mod ext_xlayer; mod block; mod call; +mod mod_xlayer; mod pending_block; use crate::{ @@ -30,7 +31,7 @@ use reth_optimism_flashblocks::{ use reth_rpc::eth::core::EthApiInner; use reth_rpc_eth_api::{ helpers::{ - pending_block::BuildPendingEnv, EthApiSpec, EthFees, EthState, LoadFee, LoadPendingBlock, + fee_xlayer::PricerStorage, pending_block::BuildPendingEnv, EthApiSpec, EthFees, EthState, LoadFee, LoadPendingBlock, LoadState, SpawnBlocking, Trace, }, EthApiTypes, FromEvmError, FullEthApiServer, RpcConvert, RpcConverter, RpcNodeCore, @@ -94,6 +95,7 @@ impl OpEthApi { sequencer_client, min_suggested_priority_fee, flashblocks, + pricer: PricerStorage::default(), }); Self { inner } } @@ -359,6 +361,8 @@ pub struct OpEthApiInner { /// /// If set, provides receivers for pending blocks, flashblock sequences, and build status. flashblocks: Option>, + /// L2 gas price pricer storage for XLayer support. + pricer: PricerStorage, } impl fmt::Debug for OpEthApiInner { diff --git a/crates/optimism/rpc/src/eth/mod_xlayer.rs b/crates/optimism/rpc/src/eth/mod_xlayer.rs new file mode 100644 index 00000000000..0e34a34fc6b --- /dev/null +++ b/crates/optimism/rpc/src/eth/mod_xlayer.rs @@ -0,0 +1,69 @@ +//! XLayer-specific fee implementation for Optimism Ethereum API. + +use crate::{OpEthApi, OpEthApiError, SequencerClient}; +use alloy_json_rpc::{RpcRecv, RpcSend}; +use reth_rpc_eth_api::{ + helpers::{pricer::L2GasPricer, XLayerFees}, + FromEvmError, RpcConvert, RpcNodeCore, +}; +use std::{future::Future, sync::Arc}; +use tracing::debug; + +impl XLayerFees for OpEthApi +where + N: RpcNodeCore, + OpEthApiError: FromEvmError, + Rpc: RpcConvert, +{ + type SequencerClient = SequencerClient; + + fn set_pricer(&self, pricer: Arc) { + *self.inner.pricer.write() = Some(pricer); + } + + fn get_pricer(&self) -> Option> { + self.inner.pricer.read().clone() + } + + fn sequencer_client(&self) -> Option<&Self::SequencerClient> { + self.inner.sequencer_client() + } + + /// Forwards a generic RPC request to the sequencer. + /// + /// Returns `Ok(Some(result))` if the request was successfully forwarded, + /// `Ok(None)` if no sequencer is configured or forward failed. + fn forward_to_sequencer( + &self, + method: &str, + params: Params, + ) -> impl Future, Self::Error>> + Send + where + Params: RpcSend, + Resp: RpcRecv, + { + let sequencer = self.sequencer_client().cloned(); + let method = method.to_string(); + + async move { + let Some(sequencer) = sequencer else { + return Ok(None); + }; + + debug!(target: "rpc::eth::xlayer", method = %method, "Forwarding RPC request to sequencer"); + + match sequencer.request::(&method, params).await { + Ok(result) => { + debug!(target: "rpc::eth::xlayer", method = %method, "Successfully received response from sequencer"); + Ok(Some(result)) + } + Err(_err) => { + debug!(target: "rpc::eth::xlayer", method = %method, "Failed to forward request to sequencer, will fall back to local"); + // Return Ok(None) to indicate fallback to local logic + Ok(None) + } + } + } + } +} + diff --git a/crates/optimism/rpc/src/lib.rs b/crates/optimism/rpc/src/lib.rs index 10f8ad5dccd..35fe66a760f 100644 --- a/crates/optimism/rpc/src/lib.rs +++ b/crates/optimism/rpc/src/lib.rs @@ -8,6 +8,8 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] #![cfg_attr(docsrs, feature(doc_cfg))] +use reth_rpc_convert as _; + pub mod engine; pub mod error; pub mod eth; diff --git a/crates/rpc/rpc-eth-api/src/core.rs b/crates/rpc/rpc-eth-api/src/core.rs index 237a0e7aa37..f400276e4f8 100644 --- a/crates/rpc/rpc-eth-api/src/core.rs +++ b/crates/rpc/rpc-eth-api/src/core.rs @@ -1,9 +1,9 @@ //! Implementation of the [`jsonrpsee`] generated [`EthApiServer`] trait. Handles RPC requests for //! the `eth_` namespace. use crate::{ - helpers::{EthApiSpec, EthBlocks, EthCall, EthFees, EthState, EthTransactions, FullEthApi}, - route_by_block_id, route_by_block_id_opt, route_by_number, try_local_then_legacy, RpcBlock, - RpcHeader, RpcReceipt, RpcTransaction, + helpers::{EthApiSpec, EthBlocks, EthCall, EthState, EthTransactions, FullEthApi, XLayerFees}, + route_by_block_id, route_by_block_id_opt, route_by_number, try_local_then_legacy, + RpcBlock, RpcHeader, RpcReceipt, RpcTransaction, }; use alloy_dyn_abi::TypedData; use alloy_eips::{eip2930::AccessListResult, BlockId, BlockNumberOrTag}; @@ -307,6 +307,11 @@ pub trait EthApi< #[method(name = "blobBaseFee")] async fn blob_base_fee(&self) -> RpcResult; + /// For xlayer + /// Returns the minimum gas price for XLayer transactions. + #[method(name = "minGasPrice")] + async fn min_gas_price(&self) -> RpcResult; + /// Returns the Transaction fee history /// /// Introduced in EIP-1559 for getting information on the appropriate priority fee to use. @@ -795,7 +800,7 @@ where /// Handler for: `eth_gasPrice` async fn gas_price(&self) -> RpcResult { trace!(target: "rpc::eth", "Serving eth_gasPrice"); - Ok(EthFees::gas_price(self).await?) + Ok(XLayerFees::gas_price(self).await?) } /// Handler for: `eth_getAccount` @@ -811,13 +816,20 @@ where /// Handler for: `eth_maxPriorityFeePerGas` async fn max_priority_fee_per_gas(&self) -> RpcResult { trace!(target: "rpc::eth", "Serving eth_maxPriorityFeePerGas"); - Ok(EthFees::suggested_priority_fee(self).await?) + Ok(XLayerFees::suggested_priority_fee(self).await?) } /// Handler for: `eth_blobBaseFee` async fn blob_base_fee(&self) -> RpcResult { trace!(target: "rpc::eth", "Serving eth_blobBaseFee"); - Ok(EthFees::blob_base_fee(self).await?) + Ok(XLayerFees::blob_base_fee(self).await?) + } + + // For xlayer + /// Handler for: `eth_minGasPrice` + async fn min_gas_price(&self) -> RpcResult { + trace!(target: "rpc::eth", "Serving eth_minGasPrice"); + Ok(XLayerFees::min_gas_price(self).await?) } // FeeHistory is calculated based on lazy evaluation of fees for historical blocks, and further @@ -836,7 +848,7 @@ where reward_percentiles: Option>, ) -> RpcResult { trace!(target: "rpc::eth", ?block_count, ?newest_block, ?reward_percentiles, "Serving eth_feeHistory"); - Ok(EthFees::fee_history(self, block_count.to(), newest_block, reward_percentiles).await?) + Ok(XLayerFees::fee_history(self, block_count.to(), newest_block, reward_percentiles).await?) } /// Handler for: `eth_mining` diff --git a/crates/rpc/rpc-eth-api/src/helpers/fee_xlayer.rs b/crates/rpc/rpc-eth-api/src/helpers/fee_xlayer.rs new file mode 100644 index 00000000000..f885911903d --- /dev/null +++ b/crates/rpc/rpc-eth-api/src/helpers/fee_xlayer.rs @@ -0,0 +1,254 @@ +//! XLayer-specific fee extensions for [`EthFees`]. +//! +//! Provides L2 gas price management, sequencer forwarding, and XLayer-specific +//! fee calculation logic. + +use super::{EthFees, LoadBlock}; +use crate::helpers::pricer::L2GasPricer; +use crate::FromEthApiError; +use alloy_consensus::BlockHeader; +use alloy_eips::BlockNumberOrTag; +use alloy_primitives::U256; +use alloy_rpc_types_eth::FeeHistory; +use futures::Future; +use parking_lot::RwLock; +use reth_storage_api::BlockReaderIdExt; +use std::sync::Arc; + +// Re-export for use in implementations +pub use alloy_json_rpc::{RpcRecv, RpcSend}; + +/// XLayer fee extensions with L2 gas pricer and sequencer forwarding support. +/// +/// Extends [`EthFees`] with: +/// - L2 gas price pricer management +/// - RPC request forwarding to sequencer +/// - XLayer-specific fee calculation logic +/// +/// All methods have default implementations. Implementations only need to override +/// what they require. +pub trait XLayerFees: EthFees { + /// Sequencer client for forwarding RPC requests. + /// + /// Must implement: + /// ```ignore + /// async fn request( + /// &self, method: &str, params: Params + /// ) -> Result + /// ``` + /// + /// Use `()` if sequencer forwarding is not needed. + type SequencerClient: Clone + Send + Sync; + + /// Sets the L2 gas price pricer at runtime. + fn set_pricer(&self, _pricer: Arc) {} + + + /// Returns the current L2 gas price pricer, if configured. + fn get_pricer(&self) -> Option> { + None + } + + /// Returns the sequencer client for RPC forwarding, if configured. + fn sequencer_client(&self) -> Option<&Self::SequencerClient> { + None + } + + /// Forwards an RPC request to the sequencer. + /// + /// Returns `Ok(Some(result))` on success, `Ok(None)` if forwarding is unavailable or fails. + fn forward_to_sequencer( + &self, + _method: &str, + _params: Params, + ) -> impl Future, Self::Error>> + Send + where + Params: RpcSend, + Resp: RpcRecv, + Self: Sized, + { + async move { Ok(None) } + } + + /// Returns the base fee from the latest block header. + fn get_base_fee(&self) -> Result + where + Self: LoadBlock, + Self::Provider: BlockReaderIdExt, + Self::Error: FromEthApiError, + { + let header = BlockReaderIdExt::latest_header(self.provider()).map_err(Self::Error::from_eth_err)?; + Ok(header.as_ref().and_then(|h| h.header().base_fee_per_gas()).unwrap_or_default()) + } + + /// Returns the XLayer max priority fee (gas_price - base_fee), or zero if no pricer. + fn get_xlayer_max_priority_fee(&self) -> Result + where + Self: LoadBlock, + Self::Provider: BlockReaderIdExt, + Self::Error: FromEthApiError, + { + if let Some(pricer) = self.get_pricer() { + let gas_price = pricer.get_gas_cache().get_latest(); + let base_fee = self.get_base_fee()?; + let tipcap = gas_price.saturating_sub(U256::from(base_fee)); + Ok(tipcap) + } else { + Ok(U256::ZERO) + } + } + + /// Returns suggested gas price for legacy transactions. + /// + /// Priority: sequencer > pricer > [`EthFees::gas_price`] + fn gas_price(&self) -> impl Future> + Send + where + Self: LoadBlock + 'static, + { + async move { + if self.sequencer_client().is_some() { + tracing::trace!("gas_price forwarding to sequencer"); + if let Ok(Some(gas_price)) = self.forward_to_sequencer::<(), U256>("eth_gasPrice", ()).await { + tracing::trace!("gas_price received from sequencer: {}", gas_price); + return Ok(gas_price); + } + } + + if let Some(pricer) = self.get_pricer() { + tracing::trace!("gas_price from local pricer: {}", pricer.get_gas_cache().get_latest()); + return Ok(pricer.get_gas_cache().get_latest()); + } + + EthFees::gas_price(self).await + } + } + + /// Returns suggested base fee for blob transactions. + /// + /// Priority: sequencer > [`EthFees::blob_base_fee`] + fn blob_base_fee(&self) -> impl Future> + Send + where + Self: LoadBlock + 'static, + { + async move { + if self.sequencer_client().is_some() { + tracing::trace!("blob_base_fee forwarding to sequencer"); + if let Ok(Some(blob_base_fee)) = self.forward_to_sequencer::<(), U256>("eth_blobBaseFee", ()).await { + tracing::trace!("blob_base_fee received from sequencer: {}", blob_base_fee); + return Ok(blob_base_fee); + } + } + + EthFees::blob_base_fee(self).await + } + } + + /// Returns suggested priority fee (tip). + /// + /// Priority: sequencer > XLayer max priority fee > [`EthFees::suggested_priority_fee`] + fn suggested_priority_fee(&self) -> impl Future> + Send + where + Self: 'static + LoadBlock, + Self::Provider: BlockReaderIdExt, + Self::Error: FromEthApiError, + { + async move { + if self.sequencer_client().is_some() { + tracing::trace!("suggested_priority_fee forwarding to sequencer"); + if let Ok(Some(priority_fee)) = self.forward_to_sequencer::<(), U256>("eth_maxPriorityFeePerGas", ()).await { + tracing::trace!("suggested_priority_fee received from sequencer: {}", priority_fee); + return Ok(priority_fee); + } + } + + if self.get_pricer().is_some() { + tracing::trace!("suggested_priority_fee from local pricer"); + return self.get_xlayer_max_priority_fee(); + } + + EthFees::suggested_priority_fee(self).await + } + } + + /// Returns fee history for the specified block range. + /// + /// When sequencer is configured, forwards the request. Otherwise, returns + /// [`EthFees::fee_history`] with XLayer adjustments if pricer is set + /// (ensures `reward + baseFee >= latest_gas_price`). + fn fee_history( + &self, + block_count: u64, + newest_block: BlockNumberOrTag, + reward_percentiles: Option>, + ) -> impl Future> + Send + where + Self: 'static + LoadBlock, + Self::Provider: BlockReaderIdExt, + Self::Error: FromEthApiError, + { + async move { + if self.sequencer_client().is_some() { + tracing::trace!("fee_history forwarding to sequencer"); + if let Ok(Some(fee_history)) = self.forward_to_sequencer::<(u64, BlockNumberOrTag, Option>), FeeHistory>( + "eth_feeHistory", + (block_count, newest_block, reward_percentiles.clone()) + ).await { + tracing::trace!("fee_history received from sequencer: {:?}", fee_history); + return Ok(fee_history); + } + } + + let mut fee_history = EthFees::fee_history(self, block_count, newest_block, reward_percentiles.clone()).await?; + + if let Some(pricer) = self.get_pricer() { + if let Some(ref mut rewards) = fee_history.reward { + let latest_gas_price_u128 = pricer.get_gas_cache().get_latest().to::(); + + for (block_idx, block_rewards) in rewards.iter_mut().enumerate() { + let base_fee = fee_history.base_fee_per_gas.get(block_idx).copied().unwrap_or(0u128); + let min_reward = latest_gas_price_u128.saturating_sub(base_fee); + + for reward in block_rewards.iter_mut() { + *reward = (*reward).max(min_reward); + } + } + } + } + + Ok(fee_history) + } + } + + /// Returns the minimum acceptable gas price. + /// + /// Priority: sequencer > XLayer min from recent history > base fee > zero + fn min_gas_price(&self) -> impl Future> + Send + where + Self: LoadBlock + 'static, + Self::Provider: BlockReaderIdExt, + Self::Error: FromEthApiError, + { + async move { + if self.sequencer_client().is_some() { + tracing::trace!("min_gas_price forwarding to sequencer"); + if let Ok(Some(min_gas_price)) = self.forward_to_sequencer::<(), U256>("eth_gasPrice", ()).await { + tracing::trace!("min_gas_price received from sequencer: {}", min_gas_price); + return Ok(min_gas_price); + } + } + + if let Some(pricer) = self.get_pricer() { + tracing::trace!("min_gas_price from local pricer: {}", pricer.get_gas_cache().get_min_raw_gp_recent()); + return Ok(pricer.get_gas_cache().get_min_raw_gp_recent()); + } + + let base_fee = self.get_base_fee()?; + Ok(if base_fee > 0 { U256::from(base_fee) } else { U256::ZERO }) + } + } +} + +/// Helper type for storing the pricer in a thread-safe, optional way. +pub type PricerStorage = Arc>>>; + + diff --git a/crates/rpc/rpc-eth-api/src/helpers/mod.rs b/crates/rpc/rpc-eth-api/src/helpers/mod.rs index 5a7493861f0..e419b13aa3f 100644 --- a/crates/rpc/rpc-eth-api/src/helpers/mod.rs +++ b/crates/rpc/rpc-eth-api/src/helpers/mod.rs @@ -21,7 +21,9 @@ pub mod config; pub mod estimate; pub mod fee; pub mod legacy_xlayer; +pub mod fee_xlayer; pub mod pending_block; +pub mod pricer; pub mod receipt; pub mod signer; pub mod spec; @@ -37,7 +39,9 @@ pub use legacy_xlayer::{ boxed_err_to_rpc, convert_option_via_serde, convert_via_serde, exec_legacy, internal_rpc_err, should_route_block_id_to_legacy, should_route_to_legacy, LegacyRpc, }; +pub use fee_xlayer::{PricerStorage, XLayerFees}; pub use pending_block::LoadPendingBlock; +pub use pricer::{GasPriceCacheTrait, L2GasPricer}; pub use receipt::LoadReceipt; pub use signer::EthSigner; pub use spec::EthApiSpec; @@ -62,7 +66,7 @@ pub trait FullEthApi: + EthBlocks + EthState + EthCall - + EthFees + + XLayerFees + Trace + LoadReceipt { @@ -75,7 +79,7 @@ impl FullEthApi for T where + EthBlocks + EthState + EthCall - + EthFees + + XLayerFees + Trace + LoadReceipt { diff --git a/crates/rpc/rpc-eth-api/src/helpers/pricer.rs b/crates/rpc/rpc-eth-api/src/helpers/pricer.rs new file mode 100644 index 00000000000..561a421b5c3 --- /dev/null +++ b/crates/rpc/rpc-eth-api/src/helpers/pricer.rs @@ -0,0 +1,45 @@ +//! L2 gas price pricer trait definition +//! +//! This module defines the `L2GasPricer` trait that is used for XLayer gas price calculations. +//! The trait is defined here to avoid circular dependencies between `rpc-eth-api` and +//! `xlayer-gasprice` crates. + +use alloy_primitives::U256; +use std::sync::Arc; +use std::any::Any; + +/// Gas price cache trait for accessing cached gas price data +pub trait GasPriceCacheTrait: Send + Sync { + /// Gets the latest cached gas price + fn get_latest(&self) -> U256; + + /// Sets the latest cached gas price + fn set_latest(&self, price: U256); + + /// Gets the latest raw gas price + fn get_latest_raw_gp(&self) -> U256; + + /// Sets the latest raw gas price + fn set_latest_raw_gp(&self, price: U256); + + /// Gets the minimum raw gas price from recent history + fn get_min_raw_gp_recent(&self) -> U256; +} + +/// Interface for L2 gas price calculation strategies +pub trait L2GasPricer: Send + Sync { + /// Updates the gas price average based on L1 gas price + fn update_gas_price_avg(&self, l1_gas_price: U256); + + /// Updates the configuration + /// The args parameter should be a reference to XLayerGasPriceArgs, but we use &dyn Any + /// to avoid circular dependencies. Implementations should downcast to the appropriate type. + fn update_config(&self, args: &dyn Any); + + /// Gets the last calculated raw gas price + fn get_last_raw_gp(&self) -> U256; + + /// Gets the gas price cache + fn get_gas_cache(&self) -> Arc; +} + diff --git a/crates/rpc/rpc/src/eth/core_xlayer.rs b/crates/rpc/rpc/src/eth/core_xlayer.rs index 80bd6d3bafc..6cfe39bd909 100644 --- a/crates/rpc/rpc/src/eth/core_xlayer.rs +++ b/crates/rpc/rpc/src/eth/core_xlayer.rs @@ -30,3 +30,19 @@ where } } +/// XLayer: Implement XLayerFees trait for EthApi with default L1 behavior +/// +/// For L1 Ethereum nodes, XLayer-specific features are not needed: +/// - No L2 gas pricer +/// - No sequencer forwarding +/// - Uses standard EthFees implementations +impl reth_rpc_eth_api::helpers::XLayerFees for EthApi +where + N: RpcNodeCore, + reth_rpc_eth_types::EthApiError: reth_rpc_eth_api::FromEvmError, + Rpc: RpcConvert, +{ + /// L1 nodes don't need sequencer forwarding + type SequencerClient = (); +} + diff --git a/crates/transaction-pool/src/lib.rs b/crates/transaction-pool/src/lib.rs index 7f3fa4a1177..d7a1a3f6a4c 100644 --- a/crates/transaction-pool/src/lib.rs +++ b/crates/transaction-pool/src/lib.rs @@ -364,6 +364,11 @@ where self.inner().config() } + /// Get a reference to the validator. + pub fn validator(&self) -> &V { + self.pool.validator() + } + /// Validates the given transaction async fn validate( &self, diff --git a/crates/transaction-pool/src/validate/task.rs b/crates/transaction-pool/src/validate/task.rs index fc22ce4ceb1..70ff2b974fc 100644 --- a/crates/transaction-pool/src/validate/task.rs +++ b/crates/transaction-pool/src/validate/task.rs @@ -137,6 +137,12 @@ impl TransactionValidationTaskExecutor { pub fn validator(&self) -> &V { &self.validator } + + /// Returns a reference to the inner validator wrapped in Arc. + /// This allows accessing the validator for operations that need shared ownership. + pub fn validator_arc(&self) -> &Arc { + &self.validator + } } impl TransactionValidationTaskExecutor> { diff --git a/crates/xlayer/gasprice/Cargo.toml b/crates/xlayer/gasprice/Cargo.toml new file mode 100644 index 00000000000..e1501bfeb3e --- /dev/null +++ b/crates/xlayer/gasprice/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "reth-xlayer-gasprice" +version = "1.7.0" +edition = "2021" +rust-version = "1.79" +license = "MIT OR Apache-2.0" +repository = "https://github.com/paradigmxyz/reth" +description = "XLayer gas price oracle implementation for Reth" + +[dependencies] +# Alloy dependencies +alloy-consensus.workspace = true +alloy-primitives = { workspace = true, features = ["serde"] } + +# Reth dependencies +reth-primitives-traits.workspace = true +reth-provider.workspace = true +reth-rpc-eth-api.workspace = true +reth-transaction-pool.workspace = true + +# Async runtime +tokio = { workspace = true, features = ["sync", "time", "rt"] } + +# Logging +tracing.workspace = true + +# Utilities +parking_lot = "0.12" + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "rt-multi-thread"] } + diff --git a/crates/xlayer/gasprice/src/cache.rs b/crates/xlayer/gasprice/src/cache.rs new file mode 100644 index 00000000000..ea758c88b44 --- /dev/null +++ b/crates/xlayer/gasprice/src/cache.rs @@ -0,0 +1,54 @@ +//! Gas price cache implementation for XLayer +//! +//! Provides caching mechanisms for raw gas prices with a circular buffer +//! and atomic operations for thread-safe access. + +use alloy_primitives::U256; +use parking_lot::RwLock; +use reth_rpc_eth_api::helpers::pricer::GasPriceCacheTrait; + +/// Simple gas price cache implementation for default mode +/// In default mode, latest price = latest raw gas price +#[derive(Debug)] +pub struct GasPriceCache { + latest_price: RwLock, +} + +impl GasPriceCache { + /// Creates a new gas price cache + pub fn new() -> Self { + Self { + latest_price: RwLock::new(U256::ZERO), + } + } +} + +impl Default for GasPriceCache { + fn default() -> Self { + Self::new() + } +} + +// TODO: implement the cache for follower and fixed modes +// This is cache is only be used in default mode +impl GasPriceCacheTrait for GasPriceCache { + fn get_latest(&self) -> U256 { + *self.latest_price.read() + } + + fn set_latest(&self, price: U256) { + *self.latest_price.write() = price; + } + + fn get_latest_raw_gp(&self) -> U256 { + *self.latest_price.read() + } + + fn set_latest_raw_gp(&self, rgp: U256) { + *self.latest_price.write() = rgp; + } + + fn get_min_raw_gp_recent(&self) -> U256 { + *self.latest_price.read() + } +} diff --git a/crates/xlayer/gasprice/src/default.rs b/crates/xlayer/gasprice/src/default.rs new file mode 100644 index 00000000000..7ee55ed9a2d --- /dev/null +++ b/crates/xlayer/gasprice/src/default.rs @@ -0,0 +1,90 @@ +//! Default gas price strategy +//! +//! Uses a fixed default gas price from the configuration. +//! This is the simplest strategy and doesn't require external data sources. + +use alloy_primitives::U256; +use parking_lot::RwLock; +use reth_rpc_eth_api::helpers::pricer::{GasPriceCacheTrait, L2GasPricer}; +use std::sync::Arc; + +use crate::{cache::GasPriceCache, DEFAULT_XLAYER_PRICE}; + +/// Default gas price suggester +/// +/// Always returns the configured default gas price without any calculations. +#[derive(Debug)] +pub struct DefaultGasPricer { + /// Default gas price value + default_price: U256, + /// Last calculated raw gas price + last_raw_gp: RwLock, + /// Gas price cache + gas_cache: Arc, +} + +impl DefaultGasPricer { + /// Creates a new default gas price suggester + pub fn new(default_price: Option) -> Self { + let default_price = default_price.unwrap_or(U256::from(DEFAULT_XLAYER_PRICE)); + Self { + default_price, + last_raw_gp: RwLock::new(default_price), + gas_cache: Arc::new(GasPriceCache::new()), + } + } +} + +impl L2GasPricer for DefaultGasPricer { + fn update_gas_price_avg(&self, _l1_gas_price: U256) { + // For default strategy, always use the configured default price + *self.last_raw_gp.write() = self.default_price; + tracing::debug!( + price = %self.default_price, + "Default gas price strategy: using configured default" + ); + } + + fn update_config(&self, _args: &dyn std::any::Any) { + // For default strategy, config updates are not needed + tracing::debug!("Default gas price strategy: config update ignored"); + } + + fn get_last_raw_gp(&self) -> U256 { + *self.last_raw_gp.read() + } + + fn get_gas_cache(&self) -> Arc { + Arc::clone(&self.gas_cache) as Arc + } +} + +#[cfg(test)] +mod tests { + use super::*; + use reth_rpc_eth_api::helpers::pricer::GasPriceCacheTrait; + + #[test] + fn test_default_pricer_returns_configured_price() { + let default_price = Some(U256::from(1_000_000_000u64)); // 1 GWei + let pricer = DefaultGasPricer::new(default_price); + + // Update with any L1 price, should still return default + pricer.update_gas_price_avg(U256::from(50_000_000_000u64)); + + assert_eq!(pricer.get_last_raw_gp(), U256::from(1_000_000_000u64)); + } + + #[test] + fn test_default_gas_price_cache_operations() { + let cache = GasPriceCache::new(); + cache.set_latest(U256::from(200)); + assert_eq!(cache.get_latest(), U256::from(200)); + assert_eq!(cache.get_latest_raw_gp(), U256::from(200)); + assert_eq!(cache.get_min_raw_gp_recent(), U256::from(200)); + + cache.set_latest_raw_gp(U256::from(300)); + assert_eq!(cache.get_latest(), U256::from(300)); + } +} + diff --git a/crates/xlayer/gasprice/src/lib.rs b/crates/xlayer/gasprice/src/lib.rs new file mode 100644 index 00000000000..54e820272e5 --- /dev/null +++ b/crates/xlayer/gasprice/src/lib.rs @@ -0,0 +1,43 @@ +//! XLayer gas price oracle implementation +//! +//! This crate provides gas price calculation strategies for XLayer: +//! - Default: Uses a fixed default gas price from configuration +//! - Follower: Calculates gas price based on L1 gas price and coin prices +//! - Fixed: Uses a fixed USDT price converted to native token + +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +/// Gas price cache implementation +pub mod cache; + +/// Default gas price strategy +pub mod default; + +/// Gas price scheduler +pub mod scheduler; + +/// Gas price suggester interface +pub mod suggester; + +/// Utility functions +pub mod utils; + +// Re-exports +pub use cache::GasPriceCache; +pub use scheduler::XLayerScheduler; +pub use suggester::NewL2GasPriceSuggester; +// Re-export L2GasPricer from rpc-eth-api for backward compatibility +pub use reth_rpc_eth_api::helpers::pricer::L2GasPricer; +// Re-export GasPriceCacheTrait from rpc-eth-api for backward compatibility +pub use reth_rpc_eth_api::helpers::pricer::GasPriceCacheTrait; + +/// Default XLayer gas price (1 GWei) +pub const DEFAULT_XLAYER_PRICE: u64 = 1_000_000_000; // 1 GWei + +/// Maximum cache size for raw gas prices +pub const MAX_CACHE_SIZE: usize = 30; + +/// Minimum gas price window size for recent calculations +pub const MIN_GP_WINDOW_SIZE: usize = 27; + diff --git a/crates/xlayer/gasprice/src/scheduler.rs b/crates/xlayer/gasprice/src/scheduler.rs new file mode 100644 index 00000000000..282d3efa1da --- /dev/null +++ b/crates/xlayer/gasprice/src/scheduler.rs @@ -0,0 +1,366 @@ +//! XLayer gas price scheduler +//! +//! Manages periodic gas price updates and handles the scheduling logic +//! for different gas price calculation strategies. + +use alloy_consensus::BlockHeader; +use alloy_primitives::U256; +use parking_lot::RwLock; +use reth_primitives_traits::{Block as BlockTrait, BlockBody as BlockBodyTrait}; +use reth_provider::{BlockNumReader, BlockReader, BlockReaderIdExt}; +use reth_rpc_eth_api::{helpers::{LoadFee, pricer::GasPriceCacheTrait}, RpcNodeCore}; +use reth_transaction_pool::TransactionPool; +use std::sync::Arc; +use tokio::time::interval; + +use reth_rpc_eth_api::helpers::pricer::L2GasPricer; + +use crate::{utils, DEFAULT_XLAYER_PRICE}; + +/// XLayer gas price scheduler +/// +/// Handles periodic updates of gas prices based on: +/// - L2 gas price changes (from EthApi's shared GasPriceOracle) +/// - Network congestion +/// - Configured update period +/// +/// The scheduler is generic over EthApi to access: +/// - GasPriceOracle for tip cap suggestions (shared with RPC) +/// - Provider for blockchain state +/// - Transaction pool for pending tx count +pub struct XLayerScheduler { + /// Gas price calculation strategy + pricer: Arc, + /// EthApi for accessing oracle, provider, and pool + eth_api: Option, + /// Whether the scheduler is running + is_running: RwLock, + /// Default gas price + default_gas_price: U256, + /// Update period + update_period: std::time::Duration, + /// Congestion threshold + congestion_threshold: i32, +} + +impl XLayerScheduler { + /// Creates a new XLayer scheduler without EthApi + /// This is useful for testing or when EthApi access is not needed + pub fn new( + pricer: Arc, + default_gas_price: Option, + update_period: Option, + congestion_threshold: Option, + ) -> Self { + Self { + pricer, + eth_api: None, + is_running: RwLock::new(false), + default_gas_price: default_gas_price.unwrap_or(U256::from(DEFAULT_XLAYER_PRICE)), + update_period: update_period.unwrap_or(std::time::Duration::from_secs(10)), + congestion_threshold: congestion_threshold.unwrap_or(100), + } + } + + /// Creates a new XLayer scheduler with EthApi + /// + /// The EthApi provides access to: + /// - GasPriceOracle (shared with RPC for consistent tip cap suggestions) + /// - Provider (for reading blockchain data) + /// - Pool (for checking pending transactions) + pub fn with_eth_api( + pricer: Arc, + eth_api: EthApi, + default_gas_price: Option, + update_period: Option, + congestion_threshold: Option, + ) -> Self { + Self { + pricer, + eth_api: Some(eth_api), + is_running: RwLock::new(false), + default_gas_price: default_gas_price.unwrap_or(U256::from(DEFAULT_XLAYER_PRICE)), + update_period: update_period.unwrap_or(std::time::Duration::from_secs(10)), + congestion_threshold: congestion_threshold.unwrap_or(100), + } + } + + /// Legacy constructor for backward compatibility with FullNodeComponents + pub fn with_components( + pricer: Arc, + eth_api: EthApi, + default_gas_price: Option, + update_period: Option, + congestion_threshold: Option, + ) -> Self { + Self::with_eth_api(pricer, eth_api, default_gas_price, update_period, congestion_threshold) + } + + /// Gets the gas price cache + pub fn get_gas_cache(&self) -> Arc { + self.pricer.get_gas_cache() + } + + /// Stops the gas price scheduler by setting is_running to false + pub fn stop(&self) { + *self.is_running.write() = false; + tracing::info!("XLayer gas price scheduler stop requested"); + } +} + +// Implementation for schedulers with EthApi +impl XLayerScheduler +where + EthApi: RpcNodeCore + LoadFee + Clone + Send + Sync + 'static, +{ + /// Runs the scheduler loop + /// + /// This should be spawned as a background task. + /// Initializes the scheduler and runs the update loop. + pub async fn run(&self) { + // Initialize scheduler + { + let mut is_running = self.is_running.write(); + if *is_running { + return; + } + *is_running = true; + } + + // Set initial gas price + let cache = self.pricer.get_gas_cache(); + cache.set_latest(self.default_gas_price); + cache.set_latest_raw_gp(self.default_gas_price); + + // Initialize update interval + let mut update_interval = interval(self.update_period); + update_interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Skip); + + tracing::info!("XLayer gas price scheduler started"); + + // Main update loop + loop { + // Check if stop was requested + if !*self.is_running.read() { + break; + } + + // Wait for next interval tick (no lock held during await) + update_interval.tick().await; + + // Check again after await (in case stop was called during tick) + if !*self.is_running.read() { + break; + } + + // Perform update cycle + self.tick().await; + } + + tracing::info!("Stopping l2 gas price suggester..."); + } + + /// Runs one update cycle + /// + /// This should be called periodically by the main scheduler loop + async fn tick(&self) { + // For default mode, we don't need to update based on L1 gas price + // The gas price is always the configured default value + + // Update dynamic gas price based on congestion if EthApi is available + if self.eth_api.is_some() { + // TODO: add l1gp fetching logic for follower and fixed modes + // let l1_gas_price = self.get_l1_gas_price().await; + // if let Ok(l1_gas_price) = l1_gas_price { + // self.pricer.update_gas_price_avg(l1_gas_price); + // self.pricer.get_gas_cache().set_latest_raw_gp(self.pricer.get_last_raw_gp()); + // } else { + // tracing::warn!("Failed to get L1 gas price, using default"); + // return; + // } + + self.update_dynamic_gp().await; + } else { + tracing::debug!("EthApi not available, skipping dynamic gas price update"); + } + } + + /// Updates dynamic gas price based on network congestion + async fn update_dynamic_gp(&self) { + let cache = self.pricer.get_gas_cache(); + + // Get L2 gas price (base fee + tip cap) from shared GasPriceOracle + let l2_gas_price = match self.get_l2_gas_price().await { + Ok(price) => price, + Err(err) => { + tracing::debug!("try to getting L2 gas price failed, wait for initializing finished: {}", err); + return; + } + }; + + // Check if L2 gas price is less than default, if so use default + let mut gas_result = if l2_gas_price < self.default_gas_price { + tracing::debug!( + "GasPriceOracle suggested gas price is less than xlayer default, setting to xlayer default, suggestedGasPrice={}, default={}", + l2_gas_price, + self.default_gas_price + ); + self.default_gas_price + } else { + l2_gas_price + }; + + // Get raw gas price (recommended gas price) + let raw_gp = self.pricer.get_last_raw_gp(); + + // Check if gas_result is less than raw_gp, if so use raw_gp + if gas_result < raw_gp { + tracing::debug!( + "gasResult is less than rgp, setting gasResult to recommendedGasPrice, gasResult={}, recommendedGasPrice={}", + gas_result, + raw_gp + ); + gas_result = raw_gp; + } + + // Check if network is congested + let is_congested = self.is_congested().await; + + if !is_congested { + // If not congested, use average of raw GP and current result + tracing::debug!( + "not congested, setting gasResult to avg of recommendedGasPrice and suggestGasPrice, recommendedGasPrice={}, gasResult={}", + raw_gp, + gas_result + ); + gas_result = utils::avg_price(raw_gp, gas_result); + } + + cache.set_latest(gas_result); + tracing::info!("Updated gas price: {}", gas_result); + } + + /// Checks if the network is congested + /// + /// Returns false if EthApi is not available or on error + async fn is_congested(&self) -> bool { + // If EthApi is not available, return false + let Some(ref eth_api) = self.eth_api else { + return false; + }; + + // Get latest block transaction count + let latest_block_tx_num = match get_latest_block_tx_num(eth_api).await { + Ok(count) => count, + Err(err) => { + tracing::warn!(?err, "Failed to get latest block transaction count"); + return false; + } + }; + + tracing::debug!(latest_block_tx_num = %latest_block_tx_num, "Latest block transaction count"); + + // Check if latest block is empty + // op-stack will have at least 1 tx (DepositTx) in the latest block + let is_latest_block_empty = latest_block_tx_num <= 1; + + // Get pending transaction count + let pending_count = get_pending_tx_count(eth_api); + + tracing::debug!(pending_count = %pending_count, "Pending transaction count"); + + // Get congestion threshold from scheduler config + let is_pending_tx_congested = pending_count >= self.congestion_threshold as usize; + + // Network is congested if latest block is not empty AND pending tx count exceeds threshold + !is_latest_block_empty && is_pending_tx_congested + } + + /// Gets the L2 gas price (base fee + tip cap) + /// + /// Uses the shared GasPriceOracle from EthApi to get the tip cap, + /// ensuring consistency with RPC gas price estimates. + async fn get_l2_gas_price(&self) -> Result> { + let Some(ref eth_api) = self.eth_api else { + tracing::error!("gasOracle is not available"); + return Err("gasOracle is not available".into()); + }; + + // Get tip cap from the shared GasPriceOracle + let tip_cap = eth_api.gas_oracle().suggest_tip_cap().await + .map_err(|e| { + tracing::debug!("SuggestTipCap failed: {}", e); + Box::new(e) as Box + })?; + + // Get latest header for base fee + let header = eth_api.provider().latest_header() + .map_err(|e| Box::new(e) as Box)? + .ok_or_else(|| { + tracing::error!("baseFee is not available"); + "baseFee is not available" + })?; + + let base_fee = header.base_fee_per_gas().unwrap_or_default(); + + // L2 gas price = base fee + tip cap + Ok(U256::from(base_fee) + tip_cap) + } +} + +/// Gets the transaction count from the latest block +async fn get_latest_block_tx_num(eth_api: &E) -> Result> +where + E: RpcNodeCore, +{ + let provider = eth_api.provider(); + + // Get the best (latest) block number + let best_number = provider.best_block_number() + .map_err(|e| format!("Failed to get best block number: {}", e))?; + + // Get the block with that number + let block = provider.block_by_number(best_number) + .map_err(|e| format!("Failed to get block by number: {}", e))? + .ok_or_else(|| format!("Block {} not found", best_number))?; + + // Return the transaction count + Ok(block.body().transaction_count()) +} + +/// Gets the pending transaction count from the pool +fn get_pending_tx_count(eth_api: &E) -> usize +where + E: RpcNodeCore, +{ + let pool = eth_api.pool(); + let (pending, _queued) = pool.pending_and_queued_txn_count(); + pending +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::default::DefaultGasPricer; + + #[test] + fn test_scheduler_stop() { + let pricer = Arc::new(DefaultGasPricer::new(None)); + let scheduler = XLayerScheduler::<()>::new(pricer, None, None, None); + + // Test that stop can be called + scheduler.stop(); + + // Verify the scheduler is marked as not running + assert!(!*scheduler.is_running.read()); + } + + #[test] + fn test_scheduler_creation() { + let pricer = Arc::new(DefaultGasPricer::new(None)); + let scheduler = XLayerScheduler::<()>::new(pricer, None, None, Some(100)); + + // Ensure scheduler can be created + assert_eq!(scheduler.get_gas_cache().get_latest(), alloy_primitives::U256::ZERO); + } +} diff --git a/crates/xlayer/gasprice/src/suggester.rs b/crates/xlayer/gasprice/src/suggester.rs new file mode 100644 index 00000000000..a871852f6c3 --- /dev/null +++ b/crates/xlayer/gasprice/src/suggester.rs @@ -0,0 +1,57 @@ +//! Gas price suggester interface and factory +//! +//! Provides the main interface for gas price calculation strategies +//! and a factory function to create the appropriate suggester based on configuration. + +use reth_rpc_eth_api::helpers::pricer::L2GasPricer; +use std::sync::Arc; + +use crate::{ + default::DefaultGasPricer, + // fixed::FixedGasPricer, + // follower::FollowerGasPricer, +}; + +/// Trait for XLayer gas price arguments +pub trait XLayerGasPriceArgsTrait { + fn price_type(&self) -> Option<&str>; + fn default(&self) -> Option; +} + +/// Creates a new L2 gas price suggester based on the configuration +/// +/// # Arguments +/// +/// * `args` - The XLayer gas price arguments +/// +/// # Returns +/// +/// Returns an Arc-wrapped implementation of `L2GasPricer` based on the price type: +/// - `Default`: Uses a fixed default gas price +/// - `Follower`: Calculates based on L1 gas price and coin prices +/// - `Fixed`: Uses a fixed USDT price converted to native token +pub fn new_l2_gas_price_suggester(args: &T) -> Arc { + let price_type = args.price_type().unwrap_or(""); + match price_type { + "default" => { + tracing::info!("Creating Default gas price suggester"); + Arc::new(DefaultGasPricer::new(args.default())) + } + // "follower" => { + // tracing::info!("Creating Follower gas price suggester"); + // Arc::new(FollowerGasPricer::new(args)) + // } + // "fixed" => { + // tracing::info!("Creating Fixed gas price suggester"); + // Arc::new(FixedGasPricer::new(args)) + // } + _ => { + tracing::warn!("Invalid gas price type: {}, use default mode", price_type); + Arc::new(DefaultGasPricer::new(args.default())) + } + } +} + +/// Type alias for the suggester factory function +pub type NewL2GasPriceSuggester = fn(&T) -> Arc; + diff --git a/crates/xlayer/gasprice/src/utils.rs b/crates/xlayer/gasprice/src/utils.rs new file mode 100644 index 00000000000..b19ddd22b2d --- /dev/null +++ b/crates/xlayer/gasprice/src/utils.rs @@ -0,0 +1,22 @@ +//! Utility functions for gas price calculations + +use alloy_primitives::U256; + +/// Calculates the average of two gas prices +pub fn avg_price(low: U256, high: U256) -> U256 { + (low + high) / U256::from(2) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_avg_price() { + let low = U256::from(100u64); + let high = U256::from(200u64); + let avg = avg_price(low, high); + assert_eq!(avg, U256::from(150u64)); + } +} + diff --git a/crates/xlayer/txpool/Cargo.toml b/crates/xlayer/txpool/Cargo.toml new file mode 100644 index 00000000000..86c3fa67fb4 --- /dev/null +++ b/crates/xlayer/txpool/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "reth-xlayer-txpool" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +homepage.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +# Reth +reth-optimism-txpool.workspace = true +reth-transaction-pool.workspace = true +reth-primitives-traits.workspace = true +reth-rpc-eth-api.workspace = true +reth-chainspec.workspace = true +reth-storage-api.workspace = true + +# Alloy +alloy-consensus.workspace = true +alloy-primitives.workspace = true + +# Async +parking_lot.workspace = true + +# Tracing +tracing.workspace = true + +[dev-dependencies] diff --git a/crates/xlayer/txpool/src/lib.rs b/crates/xlayer/txpool/src/lib.rs new file mode 100644 index 00000000000..cdf23762e5a --- /dev/null +++ b/crates/xlayer/txpool/src/lib.rs @@ -0,0 +1,13 @@ +//! XLayer Transaction pool. + +#![doc( + html_logo_url = "https://raw.githubusercontent.com/paradigmxyz/reth/main/assets/reth-docs.png", + html_favicon_url = "https://avatars0.githubusercontent.com/u/97369466?s=256", + issue_tracker_base_url = "https://github.com/paradigmxyz/reth/issues/" +)] +#![cfg_attr(not(test), warn(unused_crate_dependencies))] +#![cfg_attr(docsrs, feature(doc_cfg, doc_auto_cfg))] + +mod validator; +pub use validator::XLayerTransactionValidator; + diff --git a/crates/xlayer/txpool/src/validator.rs b/crates/xlayer/txpool/src/validator.rs new file mode 100644 index 00000000000..28b0ea6b354 --- /dev/null +++ b/crates/xlayer/txpool/src/validator.rs @@ -0,0 +1,319 @@ +//! XLayer transaction validator. + +use alloy_consensus::BlockHeader; +use alloy_primitives::U256; +use parking_lot::RwLock; +use reth_chainspec::ChainSpecProvider; +use reth_optimism_txpool::OpTransactionValidator; +use reth_primitives_traits::{Block, SealedBlock}; +use reth_rpc_eth_api::helpers::pricer::L2GasPricer; +use reth_storage_api::BlockReaderIdExt; +use reth_transaction_pool::{ + error::InvalidPoolTransactionError, PoolTransaction, TransactionOrigin, + TransactionValidationOutcome, TransactionValidator, +}; +use std::fmt::Debug; +use std::sync::Arc; +use tracing::{debug, trace}; + +/// Helper type for storing the pricer in a thread-safe, optional way. +type PricerStorage = Arc>>>; + +/// Validator for XLayer transactions. +/// +/// This validator wraps [`OpTransactionValidator`] and adds XLayer-specific functionality, +/// including the ability to hold and manage an L2 gas price pricer. +#[derive(Clone)] +pub struct XLayerTransactionValidator { + /// The inner Optimism transaction validator. + inner: Arc>, + /// Optional L2 gas price pricer for XLayer-specific gas price calculations. + pricer: PricerStorage, +} + +impl Debug for XLayerTransactionValidator +where + Client: Debug, + Tx: Debug, +{ + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("XLayerTransactionValidator") + .field("inner", &self.inner) + .field("pricer", &"") + .finish() + } +} + +impl XLayerTransactionValidator { + /// Create a new [`XLayerTransactionValidator`] wrapping the given [`OpTransactionValidator`]. + pub fn new(inner: OpTransactionValidator) -> Self { + Self { + inner: Arc::new(inner), + pricer: Arc::new(RwLock::new(None)), + } + } + + /// Sets the L2 gas price pricer. + /// + /// This allows setting the pricer at runtime, without requiring it + /// to be set during initialization. + pub fn set_pricer(&self, pricer: Arc) { + *self.pricer.write() = Some(pricer); + } + + /// Gets the current L2 gas price pricer, if set. + pub fn get_pricer(&self) -> Option> { + self.pricer.read().clone() + } + + /// Returns a reference to the inner [`OpTransactionValidator`]. + pub fn inner(&self) -> &OpTransactionValidator { + &self.inner + } + + /// Returns a mutable reference to the inner [`OpTransactionValidator`]. + /// Note: This is only available if you have an owned instance. + pub fn into_inner(self) -> OpTransactionValidator + where + Client: Clone, + Tx: Clone, + { + Arc::try_unwrap(self.inner).unwrap_or_else(|arc| (*arc).clone()) + } +} + +impl XLayerTransactionValidator +where + Client: ChainSpecProvider + BlockReaderIdExt, + Tx: PoolTransaction, +{ + /// Gets the current base fee from the latest block header. + /// + /// This should be called once for batch validation and reused across transactions. + fn get_base_fee(&self) -> u64 { + self.inner + .client() + .latest_header() + .ok() + .flatten() + .and_then(|header| header.header().base_fee_per_gas()) + .unwrap_or(0) + } + + /// Gets the minimum gas price from the pricer cache. + /// + /// Returns `None` if pricer is not configured or minimum gas price is unavailable. + /// This should be called once for batch validation and reused across transactions. + fn get_min_gas_price(&self) -> Option { + let pricer = self.get_pricer()?; + let min_price = pricer.get_gas_cache().get_latest(); + + if min_price.is_zero() { + None + } else { + Some(min_price) + } + } + + /// Filters a transaction based on XLayer gas price requirements. + /// + /// Returns `Some(error)` if the transaction should be rejected, `None` if it passes. + fn filter_transaction(&self, transaction: &Tx) -> Option { + let base_fee = self.get_base_fee(); + let min_price = match self.get_min_gas_price() { + Some(price) => price, + None => { + debug!(target: "reth::cli", "XLayer: No pricer configured or min price unavailable, delegating to inner validator"); + return None; + } + }; + + self.filter_transaction_with_context(transaction, base_fee, min_price) + } + + /// Filters a transaction based on XLayer gas price requirements with pre-fetched context. + /// + /// This is more efficient for batch validation as it avoids fetching the base fee and + /// minimum gas price for each transaction. + /// + /// Returns `Some(error)` if the transaction should be rejected, `None` if it passes. + fn filter_transaction_with_context( + &self, + transaction: &Tx, + base_fee: u64, + min_price: U256, + ) -> Option { + // Calculate the transaction's effective gas price + let tx_gas_price = self.calculate_effective_gas_price(transaction, base_fee); + + // Check if transaction meets minimum gas price requirement + if tx_gas_price < min_price { + debug!( + target: "reth::cli", + "XLayer: Transaction rejected due to insufficient gas price, tx_gas_price={}, min_gas_price={}", + tx_gas_price, + min_price + ); + return Some(InvalidPoolTransactionError::Underpriced); + } + + trace!( + target: "reth::cli", + "XLayer: Transaction accepted, tx_gas_price={}, min_gas_price={}", + tx_gas_price, + min_price + ); + + None + } + + /// Calculates the effective gas price for a transaction with a given base fee. + /// + /// For legacy transactions (type 0 and 1), this is simply the gas price. + /// For EIP-1559 transactions (type 2), this is min(baseFee + tip, feeCap). + fn calculate_effective_gas_price(&self, transaction: &Tx, base_fee: u64) -> U256 { + // For dynamic fee transactions (EIP-1559), calculate effective gas price + if transaction.is_dynamic_fee() { + // Calculate effective gas price: min(tip + baseFee, feeCap) + let tip = transaction.max_priority_fee_per_gas().unwrap_or(0); + let fee_cap = transaction.max_fee_per_gas(); + let tip_plus_base_fee = U256::from(tip).saturating_add(U256::from(base_fee)); + let fee_cap_u256 = U256::from(fee_cap); + + let effective_price = if tip_plus_base_fee < fee_cap_u256 { + tip_plus_base_fee + } else { + fee_cap_u256 + }; + + trace!( + target: "reth::cli", + "XLayer: Transaction effective gas price, effective_gas_price={}, base_fee={}, tip={}, fee_cap={}", + effective_price, + base_fee, + tip, + fee_cap + ); + + effective_price + } else { + // For legacy transactions, use the gas price directly + U256::from(transaction.gas_price().unwrap_or(transaction.max_fee_per_gas())) + } + } + + /// Filters a batch of validation results from inner validator based on XLayer gas price requirements. + /// + /// This method takes validation results from the inner validator and applies XLayer-specific + /// gas price filtering. Valid transactions that don't meet the minimum gas price are converted + /// to Invalid outcomes. + /// + /// # Arguments + /// * `inner_results` - Validation results from inner validator + /// + /// # Returns + /// A new vector with XLayer filtering applied, or the original results if filtering is skipped + fn filter_transactions( + &self, + inner_results: Vec>, + ) -> Vec> { + // Get base fee and min price once for all transactions to avoid repeated calls + let base_fee = self.get_base_fee(); + let min_price = match self.get_min_gas_price() { + Some(price) => price, + None => { + debug!(target: "reth::cli", "XLayer: No pricer configured or min price unavailable, skipping XLayer filtering"); + // No XLayer filtering, return original results + return inner_results; + } + }; + + // Apply XLayer gas price filtering to each result + inner_results + .into_iter() + .map(|outcome| { + match outcome { + TransactionValidationOutcome::Valid { transaction, balance, state_nonce, bytecode_hash, propagate, authorities } => { + // Check if transaction passes XLayer gas price requirements + if let Some(err) = self.filter_transaction_with_context(transaction.transaction(), base_fee, min_price) { + // Transaction failed XLayer check, convert to Invalid + TransactionValidationOutcome::Invalid(transaction.into_transaction(), err) + } else { + // Transaction passed, keep as Valid + TransactionValidationOutcome::Valid { + transaction, + balance, + state_nonce, + bytecode_hash, + propagate, + authorities, + } + } + } + // Keep Invalid and Error outcomes as-is + other => other, + } + }) + .collect() + } +} + +impl TransactionValidator for XLayerTransactionValidator +where + Client: Debug + Send + Sync + ChainSpecProvider + BlockReaderIdExt, + Tx: Debug + Send + Sync + PoolTransaction, + OpTransactionValidator: TransactionValidator, +{ + type Transaction = Tx; + + async fn validate_transaction( + &self, + origin: TransactionOrigin, + transaction: Self::Transaction, + ) -> TransactionValidationOutcome { + trace!(target: "reth::cli", "XLayer: Validating transaction"); + + // Apply XLayer gas price filtering before delegating to inner validator + if let Some(err) = self.filter_transaction(&transaction) { + return TransactionValidationOutcome::Invalid(transaction, err); + } + + self.inner.validate_transaction(origin, transaction).await + } + + async fn validate_transactions( + &self, + transactions: Vec<(TransactionOrigin, Self::Transaction)>, + ) -> Vec> { + debug!(target: "reth::cli", "XLayer: Validating {} transactions", transactions.len()); + + // First, delegate to inner validator for basic validation + let inner_results = self.inner.validate_transactions(transactions).await; + + // Apply XLayer gas price filtering to the results + self.filter_transactions(inner_results) + } + + async fn validate_transactions_with_origin( + &self, + origin: TransactionOrigin, + transactions: impl IntoIterator + Send, + ) -> Vec> { + trace!(target: "reth::cli", "XLayer: Validating transactions with origin"); + + // First, delegate to inner validator for basic validation + let inner_results = self.inner.validate_transactions_with_origin(origin, transactions).await; + + // Apply XLayer gas price filtering to the results + self.filter_transactions(inner_results) + } + + fn on_new_head_block(&self, new_tip_block: &SealedBlock) + where + B: Block, + { + trace!(target: "reth::cli", "XLayer: On new head block"); + self.inner.on_new_head_block(new_tip_block); + } +} +