Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 33 additions & 0 deletions crates/op-rbuilder/src/args/op.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Signer>,

/// 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<String>,
}

impl Default for OpRbuilderArgs {
Expand Down
38 changes: 38 additions & 0 deletions crates/op-rbuilder/src/builders/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,22 @@ pub struct BuilderConfig<Specific: Clone> {

/// 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<Signer>,

/// 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<String>,
}

impl<S: Debug + Clone> core::fmt::Debug for BuilderConfig<S> {
Expand All @@ -152,6 +168,18 @@ impl<S: Debug + Clone> core::fmt::Debug for BuilderConfig<S> {
.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()
}
}
Expand All @@ -171,6 +199,11 @@ impl<S: Default + Clone> Default for BuilderConfig<S> {
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,
}
}
}
Expand All @@ -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(),

Choose a reason for hiding this comment

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

This will likely change to some pool.build or something similar but works for now.

specific: S::try_from(args)?,
})
}
Expand Down
6 changes: 6 additions & 0 deletions crates/op-rbuilder/src/tests/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down
131 changes: 131 additions & 0 deletions crates/op-rbuilder/src/tests/native_bundler.rs
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
126 changes: 126 additions & 0 deletions crates/op-rbuilder/src/tests/native_bundler_config.rs
Original file line number Diff line number Diff line change
@@ -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()));
}
}