diff --git a/crates/op-rbuilder/src/args/op.rs b/crates/op-rbuilder/src/args/op.rs index 4dc0663c..6cb05de2 100644 --- a/crates/op-rbuilder/src/args/op.rs +++ b/crates/op-rbuilder/src/args/op.rs @@ -75,6 +75,39 @@ pub struct OpRbuilderArgs { pub flashtestations: FlashtestationsArgs, #[command(flatten)] pub gas_limiter: GasLimiterArgs, + + /// Account Abstraction (AA) Native Bundler Configuration + /// Enable AA native bundler in block builder + #[arg( + long = "builder.enable-aa-bundler", + default_value = "false", + env = "ENABLE_AA_BUNDLER" + )] + pub enable_aa_bundler: bool, + + /// Secret key for AA bundle transactions + #[arg(long = "aa.bundler-signer-key", env = "AA_BUNDLER_SIGNER_KEY")] + pub aa_bundler_signer: Option, + + /// Percentage of block gas to reserve for AA bundles after threshold + #[arg( + long = "aa.gas-reserve-percentage", + default_value = "20", + env = "AA_GAS_RESERVE_PERCENTAGE" + )] + pub aa_gas_reserve_percentage: u8, + + /// Threshold percentage of block gas before starting AA bundle reservation + #[arg( + long = "aa.gas-threshold", + default_value = "80", + env = "AA_GAS_THRESHOLD" + )] + pub aa_gas_threshold: u8, + + /// UserOperation pool URL (if not provided, AA bundling is disabled) + #[arg(long = "aa.pool-url", env = "AA_POOL_URL")] + pub aa_pool_url: Option, } impl Default for OpRbuilderArgs { diff --git a/crates/op-rbuilder/src/builders/mod.rs b/crates/op-rbuilder/src/builders/mod.rs index 48ce625b..0aa05039 100644 --- a/crates/op-rbuilder/src/builders/mod.rs +++ b/crates/op-rbuilder/src/builders/mod.rs @@ -130,6 +130,22 @@ pub struct BuilderConfig { /// Resource metering context pub resource_metering: ResourceMetering, + + /// Account Abstraction (AA) Native Bundler Configuration + /// Whether AA native bundler is enabled + pub enable_aa_bundler: bool, + + /// AA bundler signer for bundle transactions + pub aa_bundler_signer: Option, + + /// Percentage of gas to reserve for AA bundles + pub aa_gas_reserve_percentage: u8, + + /// Threshold before reserving gas for AA bundles + pub aa_gas_threshold: u8, + + /// UserOperation pool connection URL + pub aa_pool_url: Option, } impl core::fmt::Debug for BuilderConfig { @@ -152,6 +168,18 @@ impl core::fmt::Debug for BuilderConfig { .field("specific", &self.specific) .field("max_gas_per_txn", &self.max_gas_per_txn) .field("gas_limiter_config", &self.gas_limiter_config) + .field("enable_aa_bundler", &self.enable_aa_bundler) + .field( + "aa_bundler_signer", + &self + .aa_bundler_signer + .as_ref() + .map(|s| s.address.to_string()) + .unwrap_or_else(|| "None".to_string()), + ) + .field("aa_gas_reserve_percentage", &self.aa_gas_reserve_percentage) + .field("aa_gas_threshold", &self.aa_gas_threshold) + .field("aa_pool_url", &self.aa_pool_url) .finish() } } @@ -171,6 +199,11 @@ impl Default for BuilderConfig { max_gas_per_txn: None, gas_limiter_config: GasLimiterArgs::default(), resource_metering: ResourceMetering::default(), + enable_aa_bundler: false, + aa_bundler_signer: None, + aa_gas_reserve_percentage: 20, + aa_gas_threshold: 80, + aa_pool_url: None, } } } @@ -197,6 +230,11 @@ where args.enable_resource_metering, args.resource_metering_buffer_size, ), + enable_aa_bundler: args.enable_aa_bundler, + aa_bundler_signer: args.aa_bundler_signer, + aa_gas_reserve_percentage: args.aa_gas_reserve_percentage, + aa_gas_threshold: args.aa_gas_threshold, + aa_pool_url: args.aa_pool_url.clone(), specific: S::try_from(args)?, }) } diff --git a/crates/op-rbuilder/src/tests/mod.rs b/crates/op-rbuilder/src/tests/mod.rs index fd202a89..ea18b173 100644 --- a/crates/op-rbuilder/src/tests/mod.rs +++ b/crates/op-rbuilder/src/tests/mod.rs @@ -31,6 +31,12 @@ mod txpool; #[cfg(test)] mod forks; + +#[cfg(test)] +mod native_bundler; + +#[cfg(test)] +mod native_bundler_config; // If the order of deployment from the signer changes the address will change #[cfg(test)] const FLASHBLOCKS_NUMBER_ADDRESS: alloy_primitives::Address = diff --git a/crates/op-rbuilder/src/tests/native_bundler.rs b/crates/op-rbuilder/src/tests/native_bundler.rs new file mode 100644 index 00000000..cec3f2bb --- /dev/null +++ b/crates/op-rbuilder/src/tests/native_bundler.rs @@ -0,0 +1,131 @@ +//! Integration tests for native bundler functionality + +use crate::{ + args::OpRbuilderArgs, + tests::{LocalInstance, TransactionBuilderExt}, +}; +use macros::rb_test; + +/// Test that native bundler is disabled by default +#[rb_test] +async fn native_bundler_disabled_by_default(rbuilder: LocalInstance) -> eyre::Result<()> { + // The default config should have native bundler disabled + // This test verifies that existing functionality is not affected + // when the feature flag is off + + let driver = rbuilder.driver().await?; + + // Build a block without any special bundler logic + let block = driver.build_new_block_with_current_timestamp(None).await?; + + // Should only have the standard transactions (deposit + builder tx) + // No bundle transactions should be present + assert!( + block.transactions.len() >= 2, + "Block should have at least deposit and builder transactions" + ); + + Ok(()) +} + +/// Test native bundler with feature flag enabled +/// This test will be expanded once pool connection is implemented +#[rb_test(args = OpRbuilderArgs { + enable_aa_bundler: true, + aa_gas_reserve_percentage: 25, + aa_gas_threshold: 75, + // aa_pool_url will be None, so it uses mock pool + ..Default::default() +})] +async fn native_bundler_with_mock_pool(rbuilder: LocalInstance) -> eyre::Result<()> { + let driver = rbuilder.driver().await?; + + // Send some regular transactions to fill up the block + for _ in 0..5 { + driver + .create_transaction() + .random_valid_transfer() + .send() + .await?; + } + + // Build a block - with mock pool, no bundles will be created yet + // This just tests that the feature flag doesn't break block building + let block = driver.build_new_block_with_current_timestamp(None).await?; + + // Should have regular transactions + assert!( + block.transactions.len() >= 7, // 5 user txs + deposit + builder tx + "Block should include sent transactions" + ); + + // TODO: (BA-3414) Once pool connection is implemented, we would test: + // - Gas reservation occurs at threshold + // - Bundle transaction is included + // - Proper gas accounting + + Ok(()) +} + +/// Test gas reservation threshold +/// This will be properly implemented in BA-3417 +#[rb_test(args = OpRbuilderArgs { + enable_aa_bundler: true, + aa_gas_reserve_percentage: 20, + aa_gas_threshold: 80, + ..Default::default() +})] +async fn native_bundler_gas_reservation(_rbuilder: LocalInstance) -> eyre::Result<()> { + // TODO: Implement in BA-3417 + // This will test that: + // 1. Regular txs process until 80% gas used + // 2. Remaining 20% is reserved for bundles + // 3. Bundle transactions get included in reserved space + + Ok(()) +} + +#[cfg(test)] +mod cli_tests { + use crate::args::{Cli, CliExt, OpRbuilderArgs}; + use clap::Parser; + + #[test] + fn test_native_bundler_cli_parsing() { + // Test parsing with feature flag enabled + let cli = Cli::parse_from([ + "test", + "node", + "--builder.enable-aa-bundler", + "--aa.gas-reserve-percentage=30", + "--aa.gas-threshold=70", + "--aa.pool-url=http://localhost:50051", + ]); + + if let reth_optimism_cli::commands::Commands::Node(node_command) = cli.command { + let args = node_command.ext; + assert!(args.enable_aa_bundler); + assert_eq!(args.aa_gas_reserve_percentage, 30); + assert_eq!(args.aa_gas_threshold, 70); + assert_eq!(args.aa_pool_url, Some("http://localhost:50051".to_string())); + } else { + panic!("Expected node command"); + } + } + + #[test] + fn test_native_bundler_cli_defaults() { + // Test that defaults work correctly when only enabling the feature + let cli = Cli::parse_from(["test", "node", "--builder.enable-aa-bundler"]); + + if let reth_optimism_cli::commands::Commands::Node(node_command) = cli.command { + let args = node_command.ext; + assert!(args.enable_aa_bundler); + assert_eq!(args.aa_gas_reserve_percentage, 20); // default + assert_eq!(args.aa_gas_threshold, 80); // default + assert!(args.aa_pool_url.is_none()); + } else { + panic!("Expected node command"); + } + } +} diff --git a/crates/op-rbuilder/src/tests/native_bundler_config.rs b/crates/op-rbuilder/src/tests/native_bundler_config.rs new file mode 100644 index 00000000..c034be5d --- /dev/null +++ b/crates/op-rbuilder/src/tests/native_bundler_config.rs @@ -0,0 +1,126 @@ +//! Unit tests for native bundler configuration + +#[cfg(test)] +mod tests { + use crate::{args::OpRbuilderArgs, builders::BuilderConfig, tx_signer::Signer}; + + #[test] + fn test_builder_config_defaults() { + // Test that default args produce expected config + let args = OpRbuilderArgs::default(); + let config = BuilderConfig::<()>::try_from(args).unwrap(); + + assert!(!config.enable_aa_bundler); + assert_eq!(config.aa_gas_reserve_percentage, 20); + assert_eq!(config.aa_gas_threshold, 80); + assert!(config.aa_bundler_signer.is_none()); + assert!(config.aa_pool_url.is_none()); + } + + #[test] + fn test_builder_config_with_bundler_enabled() { + // Test conversion with all bundler fields set + let mut args = OpRbuilderArgs::default(); + args.enable_aa_bundler = true; + args.aa_gas_reserve_percentage = 25; + args.aa_gas_threshold = 75; + args.aa_pool_url = Some("http://localhost:50051".to_string()); + args.aa_bundler_signer = Some(Signer::random()); + + let config = BuilderConfig::<()>::try_from(args.clone()).unwrap(); + + assert!(config.enable_aa_bundler); + assert_eq!(config.aa_gas_reserve_percentage, 25); + assert_eq!(config.aa_gas_threshold, 75); + assert_eq!( + config.aa_pool_url, + Some("http://localhost:50051".to_string()) + ); + assert!(config.aa_bundler_signer.is_some()); + + // Verify the signer was properly cloned + if let (Some(arg_signer), Some(config_signer)) = + (&args.aa_bundler_signer, &config.aa_bundler_signer) + { + assert_eq!(arg_signer.address, config_signer.address); + } + } + + #[test] + fn test_builder_config_boundary_values() { + // Test with maximum percentage values (100%) + let mut args = OpRbuilderArgs::default(); + args.aa_gas_reserve_percentage = 100; + args.aa_gas_threshold = 100; + + let config = BuilderConfig::<()>::try_from(args).unwrap(); + + assert_eq!(config.aa_gas_reserve_percentage, 100); + assert_eq!(config.aa_gas_threshold, 100); + + // Test with minimum percentage values (0%) + let mut args = OpRbuilderArgs::default(); + args.aa_gas_reserve_percentage = 0; + args.aa_gas_threshold = 0; + + let config = BuilderConfig::<()>::try_from(args).unwrap(); + + assert_eq!(config.aa_gas_reserve_percentage, 0); + assert_eq!(config.aa_gas_threshold, 0); + } + + #[test] + fn test_builder_config_partial_settings() { + // Test with only some bundler settings + let mut args = OpRbuilderArgs::default(); + args.enable_aa_bundler = true; + args.aa_gas_reserve_percentage = 15; + // Leave other fields as defaults + + let config = BuilderConfig::<()>::try_from(args).unwrap(); + + assert!(config.enable_aa_bundler); + assert_eq!(config.aa_gas_reserve_percentage, 15); + assert_eq!(config.aa_gas_threshold, 80); // default + assert!(config.aa_bundler_signer.is_none()); + assert!(config.aa_pool_url.is_none()); + } + + #[test] + fn test_builder_config_debug_impl() { + // Test that Debug implementation doesn't expose sensitive data + let mut args = OpRbuilderArgs::default(); + args.enable_aa_bundler = true; + args.aa_bundler_signer = Some(Signer::random()); + + let config = BuilderConfig::<()>::try_from(args).unwrap(); + let debug_str = format!("{:?}", config); + + // Should contain the field names + assert!(debug_str.contains("enable_aa_bundler")); + assert!(debug_str.contains("aa_gas_reserve_percentage")); + assert!(debug_str.contains("aa_bundler_signer")); + + // The aa_bundler_signer should show the address, not expose the private key + // The Debug impl should use the custom formatting + if let Some(signer) = &config.aa_bundler_signer { + // Should show address, not the full signer struct + assert!(debug_str.contains(&signer.address.to_string())); + } + } + + #[test] + fn test_builder_config_clone_behavior() { + // Test that cloning args doesn't affect config + let mut args = OpRbuilderArgs::default(); + args.aa_pool_url = Some("http://original.com".to_string()); + + let config = BuilderConfig::<()>::try_from(args.clone()).unwrap(); + + // Modify the original args after creating config + args.aa_pool_url = Some("http://modified.com".to_string()); + + // Config should retain original value + assert_eq!(config.aa_pool_url, Some("http://original.com".to_string())); + } +}