From 765b77521c1a10a099e9787122813a35137ca862 Mon Sep 17 00:00:00 2001 From: dancoombs Date: Mon, 20 Jan 2025 17:01:22 -0600 Subject: [PATCH] feat: signature aggregators --- Cargo.lock | 13 + Cargo.toml | 2 + bin/rundler/Cargo.toml | 1 + bin/rundler/src/cli/aggregator.rs | 38 +++ bin/rundler/src/cli/builder.rs | 4 +- bin/rundler/src/cli/mod.rs | 25 +- bin/rundler/src/cli/node/mod.rs | 4 +- bin/rundler/src/cli/pool.rs | 4 +- bin/rundler/src/cli/rpc.rs | 13 +- crates/aggregators/bls/Cargo.toml | 15 + crates/aggregators/bls/src/bls.rs | 127 ++++++++ crates/aggregators/bls/src/lib.rs | 26 ++ crates/builder/src/bundle_proposer.rs | 273 ++++++++++-------- crates/builder/src/emit.rs | 2 + crates/pool/proto/op_pool/op_pool.proto | 35 ++- crates/pool/src/mempool/pool.rs | 22 +- crates/pool/src/mempool/uo_pool.rs | 141 ++++++++- crates/pool/src/server/remote/error.rs | 57 ++-- crates/pool/src/server/remote/protos.rs | 23 +- crates/provider/Cargo.toml | 1 + crates/provider/src/alloy/da/arbitrum.rs | 9 +- crates/provider/src/alloy/da/local/bedrock.rs | 15 +- crates/provider/src/alloy/da/local/nitro.rs | 22 +- crates/provider/src/alloy/da/mod.rs | 17 +- crates/provider/src/alloy/da/optimism.rs | 7 + crates/provider/src/alloy/entry_point/v0_6.rs | 20 +- crates/provider/src/alloy/entry_point/v0_7.rs | 20 +- crates/provider/src/traits/da.rs | 2 + crates/provider/src/traits/entry_point.rs | 3 +- crates/provider/src/traits/test_utils.rs | 4 + crates/rpc/src/eth/error.rs | 25 +- crates/rpc/src/eth/events/v0_6.rs | 4 +- crates/rpc/src/types/v0_6.rs | 7 + crates/rpc/src/types/v0_7.rs | 9 + crates/sim/src/estimation/mod.rs | 5 +- crates/sim/src/estimation/v0_6.rs | 51 +++- crates/sim/src/estimation/v0_7.rs | 43 ++- crates/sim/src/gas/gas.rs | 16 +- crates/sim/src/precheck.rs | 3 + crates/sim/src/simulation/mod.rs | 12 +- crates/sim/src/simulation/simulator.rs | 158 +++++----- crates/sim/src/simulation/unsafe_sim.rs | 30 +- crates/sim/src/simulation/v0_6/context.rs | 1 + crates/types/Cargo.toml | 1 + crates/types/src/aggregator.rs | 127 ++++++++ crates/types/src/authorization.rs | 6 +- crates/types/src/chain.rs | 31 +- crates/types/src/lib.rs | 2 + crates/types/src/pool/error.rs | 15 +- crates/types/src/pool/types.rs | 6 +- crates/types/src/user_operation/mod.rs | 194 +++++++++++-- crates/types/src/user_operation/v0_6.rs | 168 +++++++++-- crates/types/src/user_operation/v0_7.rs | 176 ++++++++++- docs/architecture/aggregators.md | 106 +++++++ docs/cli.md | 2 + 55 files changed, 1727 insertions(+), 416 deletions(-) create mode 100644 bin/rundler/src/cli/aggregator.rs create mode 100644 crates/aggregators/bls/Cargo.toml create mode 100644 crates/aggregators/bls/src/bls.rs create mode 100644 crates/aggregators/bls/src/lib.rs create mode 100644 crates/types/src/aggregator.rs create mode 100644 docs/architecture/aggregators.md diff --git a/Cargo.lock b/Cargo.lock index 4f04e9bfd..ed9f63363 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4781,6 +4781,7 @@ dependencies = [ "metrics-util", "paste", "reth-tasks", + "rundler-bls", "rundler-builder", "rundler-pool", "rundler-provider", @@ -4810,6 +4811,16 @@ dependencies = [ "cc", ] +[[package]] +name = "rundler-bls" +version = "0.4.0" +dependencies = [ + "alloy-primitives", + "async-trait", + "rundler-provider", + "rundler-types", +] + [[package]] name = "rundler-builder" version = "0.4.0" @@ -4935,6 +4946,7 @@ dependencies = [ "futures-util", "mockall", "pin-project", + "rand", "reqwest", "reth-tasks", "rundler-bindings-fastlz", @@ -5049,6 +5061,7 @@ dependencies = [ "alloy-sol-types", "anyhow", "async-trait", + "auto_impl", "cargo-husky", "chrono", "const-hex", diff --git a/Cargo.toml b/Cargo.toml index 9888c77a6..f41b1aed7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,7 @@ [workspace] members = [ "bin/rundler/", + "crates/aggregators/bls/", "crates/bindings/fastlz/", "crates/builder/", "crates/contracts/", @@ -24,6 +25,7 @@ repository = "https://github.com/alchemyplatform/rundler" [workspace.dependencies] # rundler crates +rundler-bls = { path = "crates/aggregators/bls" } rundler-contracts = { path = "crates/contracts" } rundler-provider = { path = "crates/provider" } rundler-sim = { path = "crates/sim" } diff --git a/bin/rundler/Cargo.toml b/bin/rundler/Cargo.toml index f8665bc15..664904420 100644 --- a/bin/rundler/Cargo.toml +++ b/bin/rundler/Cargo.toml @@ -11,6 +11,7 @@ ERC-4337 bundler implementation publish = false [dependencies] +rundler-bls.workspace = true rundler-builder.workspace = true rundler-pool.workspace = true rundler-provider.workspace = true diff --git a/bin/rundler/src/cli/aggregator.rs b/bin/rundler/src/cli/aggregator.rs new file mode 100644 index 000000000..4e32f9f5f --- /dev/null +++ b/bin/rundler/src/cli/aggregator.rs @@ -0,0 +1,38 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::sync::Arc; + +use rundler_bls::BlsSignatureAggregatorV0_7; +use rundler_provider::Providers; +use rundler_types::{aggregator::SignatureAggregatorRegistry, chain::ChainSpec}; + +use super::CommonArgs; + +/// Instantiate aggregators and pass to chain spec +pub fn instantiate_aggregators( + args: &CommonArgs, + chain_spec: &mut ChainSpec, + providers: &(impl Providers + 'static), +) { + let mut registry = SignatureAggregatorRegistry::default(); + + if args.bls_aggregation_enabled { + if let Some(ep_v0_7) = providers.ep_v0_7() { + let bls_aggregator = BlsSignatureAggregatorV0_7::new(ep_v0_7.clone()); + registry.register(Arc::new(bls_aggregator)); + } + } + + chain_spec.set_signature_aggregators(Arc::new(registry)); +} diff --git a/bin/rundler/src/cli/builder.rs b/bin/rundler/src/cli/builder.rs index 79d4f7984..ae3c748a3 100644 --- a/bin/rundler/src/cli/builder.rs +++ b/bin/rundler/src/cli/builder.rs @@ -21,6 +21,7 @@ use rundler_builder::{ TransactionSenderArgs, TransactionSenderKind, }; use rundler_pool::RemotePoolClient; +use rundler_provider::Providers; use rundler_sim::{MempoolConfigs, PriorityFeeMode}; use rundler_task::{ server::{connect_with_retries_shutdown, format_socket_addr}, @@ -433,6 +434,7 @@ pub async fn spawn_tasks( chain_spec: ChainSpec, builder_args: BuilderCliArgs, common_args: CommonArgs, + providers: impl Providers + 'static, ) -> anyhow::Result<()> { let BuilderCliArgs { builder: builder_args, @@ -469,7 +471,7 @@ pub async fn spawn_tasks( event_sender, LocalBuilderBuilder::new(REQUEST_CHANNEL_CAPACITY), pool, - super::construct_providers(&common_args, &chain_spec)?, + providers, ) .spawn(task_spawner) .await?; diff --git a/bin/rundler/src/cli/mod.rs b/bin/rundler/src/cli/mod.rs index 7cab00982..66511a4b5 100644 --- a/bin/rundler/src/cli/mod.rs +++ b/bin/rundler/src/cli/mod.rs @@ -17,6 +17,7 @@ use alloy_primitives::U256; use anyhow::{bail, Context}; use clap::{builder::PossibleValuesParser, Args, Parser, Subcommand}; +mod aggregator; mod builder; mod chain_spec; mod json; @@ -66,19 +67,24 @@ pub async fn run() -> anyhow::Result<()> { ) .context("metrics server should start")?; - let cs = chain_spec::resolve_chain_spec(&opt.common.network, &opt.common.chain_spec); + let mut cs = chain_spec::resolve_chain_spec(&opt.common.network, &opt.common.chain_spec); tracing::info!("Chain spec: {:#?}", cs); + let providers = construct_providers(&opt.common, &cs)?; + aggregator::instantiate_aggregators(&opt.common, &mut cs, &providers); + match opt.command { Command::Node(args) => { - node::spawn_tasks(task_spawner.clone(), cs, *args, opt.common).await? + node::spawn_tasks(task_spawner.clone(), cs, *args, opt.common, providers).await? } Command::Pool(args) => { - pool::spawn_tasks(task_spawner.clone(), cs, args, opt.common).await? + pool::spawn_tasks(task_spawner.clone(), cs, args, opt.common, providers).await? + } + Command::Rpc(args) => { + rpc::spawn_tasks(task_spawner.clone(), cs, args, opt.common, providers).await? } - Command::Rpc(args) => rpc::spawn_tasks(task_spawner.clone(), cs, args, opt.common).await?, Command::Builder(args) => { - builder::spawn_tasks(task_spawner.clone(), cs, args, opt.common).await? + builder::spawn_tasks(task_spawner.clone(), cs, args, opt.common, providers).await? } } @@ -361,6 +367,13 @@ pub struct CommonArgs { env = "MAX_EXPECTED_STORAGE_SLOTS" )] pub max_expected_storage_slots: Option, + + #[arg( + long = "bls_aggregation_enabled", + name = "bls_aggregation_enabled", + env = "BLS_AGGREGATION_ENABLED" + )] + pub bls_aggregation_enabled: bool, } const SIMULATION_GAS_OVERHEAD: u64 = 100_000; @@ -606,7 +619,7 @@ where pub fn construct_providers( args: &CommonArgs, chain_spec: &ChainSpec, -) -> anyhow::Result { +) -> anyhow::Result { let provider = Arc::new(rundler_provider::new_alloy_provider( args.node_http.as_ref().context("must provide node_http")?, args.provider_client_timeout_seconds, diff --git a/bin/rundler/src/cli/node/mod.rs b/bin/rundler/src/cli/node/mod.rs index bb834c649..c7ad0ed5c 100644 --- a/bin/rundler/src/cli/node/mod.rs +++ b/bin/rundler/src/cli/node/mod.rs @@ -14,6 +14,7 @@ use clap::Args; use rundler_builder::{BuilderEvent, BuilderTask, LocalBuilderBuilder}; use rundler_pool::{LocalPoolBuilder, PoolEvent, PoolTask}; +use rundler_provider::Providers; use rundler_rpc::RpcTask; use rundler_task::TaskSpawnerExt; use rundler_types::chain::ChainSpec; @@ -49,6 +50,7 @@ pub async fn spawn_tasks( chain_spec: ChainSpec, bundler_args: NodeCliArgs, common_args: CommonArgs, + providers: impl Providers + 'static, ) -> anyhow::Result<()> { let NodeCliArgs { pool: pool_args, @@ -109,8 +111,6 @@ pub async fn spawn_tasks( let builder_builder = LocalBuilderBuilder::new(REQUEST_CHANNEL_CAPACITY); let builder_handle = builder_builder.get_handle(); - let providers = super::construct_providers(&common_args, &chain_spec)?; - PoolTask::new( pool_task_args, op_pool_event_sender, diff --git a/bin/rundler/src/cli/pool.rs b/bin/rundler/src/cli/pool.rs index a977d54d3..2b4650a4e 100644 --- a/bin/rundler/src/cli/pool.rs +++ b/bin/rundler/src/cli/pool.rs @@ -17,6 +17,7 @@ use alloy_primitives::Address; use anyhow::Context; use clap::Args; use rundler_pool::{LocalPoolBuilder, PoolConfig, PoolTask, PoolTaskArgs}; +use rundler_provider::Providers; use rundler_sim::MempoolConfigs; use rundler_task::TaskSpawnerExt; use rundler_types::{chain::ChainSpec, EntryPointVersion}; @@ -305,6 +306,7 @@ pub async fn spawn_tasks( chain_spec: ChainSpec, pool_args: PoolCliArgs, common_args: CommonArgs, + providers: impl Providers + 'static, ) -> anyhow::Result<()> { let PoolCliArgs { pool: pool_args } = pool_args; let (event_sender, event_rx) = broadcast::channel(EVENT_CHANNEL_CAPACITY); @@ -325,7 +327,7 @@ pub async fn spawn_tasks( task_args, event_sender, LocalPoolBuilder::new(REQUEST_CHANNEL_CAPACITY, BLOCK_CHANNEL_CAPACITY), - super::construct_providers(&common_args, &chain_spec)?, + providers, ) .spawn(task_spawner) .await?; diff --git a/bin/rundler/src/cli/rpc.rs b/bin/rundler/src/cli/rpc.rs index a68ad211c..46eb1667a 100644 --- a/bin/rundler/src/cli/rpc.rs +++ b/bin/rundler/src/cli/rpc.rs @@ -17,6 +17,7 @@ use anyhow::Context; use clap::Args; use rundler_builder::RemoteBuilderClient; use rundler_pool::RemotePoolClient; +use rundler_provider::Providers; use rundler_rpc::{EthApiSettings, RpcTask, RpcTaskArgs, RundlerApiSettings}; use rundler_sim::{EstimationSettings, PrecheckSettings}; use rundler_task::{server::connect_with_retries_shutdown, TaskSpawnerExt}; @@ -160,6 +161,7 @@ pub async fn spawn_tasks( chain_spec: ChainSpec, rpc_args: RpcCliArgs, common_args: CommonArgs, + providers: impl Providers + 'static, ) -> anyhow::Result<()> { let RpcCliArgs { rpc: rpc_args, @@ -192,14 +194,9 @@ pub async fn spawn_tasks( ) .await?; - RpcTask::new( - task_args, - pool, - builder, - super::construct_providers(&common_args, &chain_spec)?, - ) - .spawn(task_spawner) - .await?; + RpcTask::new(task_args, pool, builder, providers) + .spawn(task_spawner) + .await?; Ok(()) } diff --git a/crates/aggregators/bls/Cargo.toml b/crates/aggregators/bls/Cargo.toml new file mode 100644 index 000000000..0f146627d --- /dev/null +++ b/crates/aggregators/bls/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "rundler-bls" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +license.workspace = true +repository.workspace = true +publish = false + +[dependencies] +rundler-provider.workspace = true +rundler-types.workspace = true + +alloy-primitives.workspace = true +async-trait.workspace = true diff --git a/crates/aggregators/bls/src/bls.rs b/crates/aggregators/bls/src/bls.rs new file mode 100644 index 000000000..803ef0b08 --- /dev/null +++ b/crates/aggregators/bls/src/bls.rs @@ -0,0 +1,127 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +use std::fmt::Debug; + +use alloy_primitives::{address, bytes, Address, Bytes}; +use rundler_provider::{AggregatorOut, SignatureAggregator as EpSignatureAggregator}; +use rundler_types::{ + aggregator::{ + AggregatorCosts, SignatureAggregator, SignatureAggregatorError, SignatureAggregatorResult, + }, + v0_7::UserOperation, + UserOperationVariant, +}; + +const BLS_AGGREGATOR_ADDRESS: Address = address!("9d3a231e887a495ce6c454e7a38ed5e734bd5de4"); +const BLS_AGGREGATOR_FIXED_GAS: u128 = 125_000; +const BLS_AGGREGATOR_VARIABLE_GAS: u128 = 120_000; +const BLS_AGGREGATOR_SIG_FIXED_LENGTH: u128 = 64; +const BLS_AGGREGATOR_SIG_VARIABLE_LENGTH: u128 = 0; + +static BLS_DUMMY_UO_SIG: Bytes = bytes!(""); // UO signatures are empty for BLS +static BLS_AGGREGATOR_COSTS: AggregatorCosts = AggregatorCosts { + execution_fixed_gas: BLS_AGGREGATOR_FIXED_GAS, + execution_variable_gas: BLS_AGGREGATOR_VARIABLE_GAS, + sig_fixed_length: BLS_AGGREGATOR_SIG_FIXED_LENGTH, + sig_variable_length: BLS_AGGREGATOR_SIG_VARIABLE_LENGTH, +}; + +/// BLS signature aggregator +pub struct BlsSignatureAggregatorV0_7 { + entry_point: EP, +} + +#[async_trait::async_trait] +impl SignatureAggregator for BlsSignatureAggregatorV0_7 +where + EP: EpSignatureAggregator, +{ + fn address(&self) -> Address { + BLS_AGGREGATOR_ADDRESS + } + + fn costs(&self) -> &AggregatorCosts { + &BLS_AGGREGATOR_COSTS + } + + fn dummy_uo_signature(&self) -> &Bytes { + &BLS_DUMMY_UO_SIG + } + + async fn validate_user_op_signature( + &self, + user_op: &UserOperationVariant, + ) -> SignatureAggregatorResult { + if !user_op.is_v0_7() { + return Err(SignatureAggregatorError::InvalidUserOperation( + "User operation is not v0.7".to_string(), + )); + } + + let uo = user_op.clone().into(); + match self + .entry_point + .validate_user_op_signature(BLS_AGGREGATOR_ADDRESS, uo) + .await + { + Ok(sig) => match sig { + AggregatorOut::ValidationReverted => { + Err(SignatureAggregatorError::ValidationReverted) + } + AggregatorOut::SuccessWithInfo(into) => Ok(into.signature), + }, + Err(e) => Err(SignatureAggregatorError::ProviderError(e.to_string())), + } + } + + async fn aggregate_signatures( + &self, + uos: Vec, + ) -> SignatureAggregatorResult { + let uos = uos + .into_iter() + .map(|uo| { + if !uo.is_v0_7() { + Err(SignatureAggregatorError::InvalidUserOperation( + "User operation is not v0.7".to_string(), + )) + } else { + Ok(uo.into()) + } + }) + .collect::>>()?; + + match self + .entry_point + .aggregate_signatures(BLS_AGGREGATOR_ADDRESS, uos) + .await + { + Ok(sig) => Ok(sig.unwrap_or_default()), + Err(e) => Err(SignatureAggregatorError::ProviderError(e.to_string())), + } + } +} + +impl BlsSignatureAggregatorV0_7 { + /// Create a new BLS signature aggregator + pub fn new(entry_point: EP) -> Self { + Self { entry_point } + } +} + +impl Debug for BlsSignatureAggregatorV0_7 { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("BlsSignatureAggregator").finish() + } +} diff --git a/crates/aggregators/bls/src/lib.rs b/crates/aggregators/bls/src/lib.rs new file mode 100644 index 000000000..b81204248 --- /dev/null +++ b/crates/aggregators/bls/src/lib.rs @@ -0,0 +1,26 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +#![warn(missing_docs, unreachable_pub)] +#![deny(unused_must_use, rust_2018_idioms)] +#![doc(test( + no_crate_inject, + attr(deny(warnings, rust_2018_idioms), allow(dead_code, unused_variables)) +))] + +//! BLS signature aggregation for Rundler +//! +//! Contracts found here: https://github.com/eth-infinitism/account-abstraction-samples/tree/master/contracts/bls + +mod bls; +pub use bls::BlsSignatureAggregatorV0_7; diff --git a/crates/builder/src/bundle_proposer.rs b/crates/builder/src/bundle_proposer.rs index 834e84d08..69b837099 100644 --- a/crates/builder/src/bundle_proposer.rs +++ b/crates/builder/src/bundle_proposer.rs @@ -31,13 +31,14 @@ use metrics_derive::Metrics; use mockall::automock; use rundler_provider::{ BundleHandler, DAGasOracleSync, DAGasProvider, EntryPoint, EvmProvider, HandleOpsOut, - ProvidersWithEntryPointT, SignatureAggregator, + ProvidersWithEntryPointT, }; use rundler_sim::{ ExpectedStorage, FeeEstimator, PriorityFeeMode, SimulationError, SimulationResult, Simulator, ViolationError, }; use rundler_types::{ + aggregator::SignatureAggregatorResult, chain::ChainSpec, da::DAGasBlockData, pool::{Pool, PoolOperation, SimulationViolation}, @@ -397,16 +398,22 @@ where return Some(op); } + // TODO(bundle): assuming a bundle size of 1 + let bundle_size = 1; + let required_da_gas = if self.settings.da_gas_tracking_enabled && da_block_data.is_some() && self.ep_providers.da_gas_oracle_sync().is_some() { let da_gas_oracle = self.ep_providers.da_gas_oracle_sync().as_ref().unwrap(); let da_block_data = da_block_data.unwrap(); + let extra_data_len = op.uo.extra_data_len(bundle_size); + da_gas_oracle.calc_da_gas_sync( &op.da_gas_data, da_block_data, op.uo.gas_price(base_fee), + extra_data_len, ) } else { match self @@ -416,6 +423,7 @@ where op.uo.clone().into(), block_hash.into(), op.uo.gas_price(base_fee), + bundle_size, ) .await { @@ -438,10 +446,11 @@ where } }; - // This assumes a bundle size of 1 - let required_pvg = - op.uo - .required_pre_verification_gas(&self.settings.chain_spec, 1, required_da_gas); + let required_pvg = op.uo.required_pre_verification_gas( + &self.settings.chain_spec, + bundle_size, + required_da_gas, + ); if op.uo.pre_verification_gas() < required_pvg { self.emit(BuilderEvent::skipped_op( @@ -651,7 +660,7 @@ where context .groups_by_aggregator - .entry(simulation.aggregator_address()) + .entry(op.aggregator()) .or_default() .ops_with_simulations .push(OpWithSimulation { @@ -897,19 +906,31 @@ where &self, aggregator: Address, group: &AggregatorGroup<::UO>, - ) -> (Address, anyhow::Result>) { - let ops = group + ) -> (Address, SignatureAggregatorResult) { + let Some(agg) = self + .settings + .chain_spec + .get_signature_aggregator(&aggregator) + else { + // this should be checked prior to calling this function + panic!("BUG: aggregator {aggregator:?} not found in chain spec"); + }; + + let uos = group .ops_with_simulations .iter() - .map(|op_with_simulation| op_with_simulation.op.clone()) - .collect(); - let result = self - .ep_providers - .entry_point() - .aggregate_signatures(aggregator, ops) - .await - .map_err(anyhow::Error::from); - (aggregator, result) + // Mempool ops are transformed during insertion - use the original signature to aggregate + .map(|op| op.op.clone().with_original_signature().into()) + .collect::>(); + + let aggregated = match agg.aggregate_signatures(uos).await { + Ok(aggregated) => aggregated, + Err(e) => { + return (aggregator, Err(e)); + } + }; + + (aggregator, Ok(aggregated)) } async fn process_failed_op( @@ -1113,6 +1134,23 @@ where let mut gas_left = math::increase_by_percent(self.settings.max_bundle_gas, 10); let mut ops_in_bundle = Vec::new(); for op in ops { + // if the op has an aggregator, check if the aggregator is supported, if not skip + if let Some(agg) = op.uo.aggregator() { + if self + .settings + .chain_spec + .get_signature_aggregator(&agg) + .is_none() + { + self.emit(BuilderEvent::skipped_op( + self.builder_index, + self.op_hash(&op.uo), + SkipReason::UnsupportedAggregator(agg), + )); + continue; + } + } + // Here we use optimistic gas limits for the UOs by assuming none of the paymaster UOs use postOp calls. // This way after simulation once we have determined if each UO actually uses a postOp call or not we can still pack a full bundle let gas = op.uo.computation_gas_limit(&self.settings.chain_spec, None); @@ -1213,17 +1251,6 @@ struct OpWithSimulation { simulation: SimulationResult, } -impl OpWithSimulation { - fn op_with_replaced_sig(&self) -> UO { - let mut op = self.op.clone(); - if self.simulation.aggregator.is_some() { - // if using an aggregator, clear out the user op signature - op.clear_signature(); - } - op - } -} - /// A struct used internally to represent the current state of a proposed bundle /// as it goes through iterations. Contains similar data to the /// `Vec` that will eventually be passed to the entry @@ -1269,11 +1296,10 @@ impl ProposalContext { fn apply_aggregation_signature_result( &mut self, aggregator: Address, - result: anyhow::Result>, + result: SignatureAggregatorResult, ) { match result { - Ok(Some(sig)) => self.groups_by_aggregator[&Some(aggregator)].signature = sig, - Ok(None) => self.reject_aggregator(aggregator), + Ok(sig) => self.groups_by_aggregator[&Some(aggregator)].signature = sig, Err(error) => { error!("Failed to compute aggregator signature: {error}"); self.groups_by_aggregator.remove(&Some(aggregator)); @@ -1336,13 +1362,13 @@ impl ProposalContext { #[must_use = "rejected entity but did not update aggregator signatures"] fn reject_entity(&mut self, entity: Entity, is_staked: bool) -> Vec
{ let ret = match entity.kind { - EntityType::Aggregator => { - self.reject_aggregator(entity.address); - vec![] - } EntityType::Paymaster => self.reject_paymaster(entity.address), EntityType::Factory => self.reject_factory(entity.address), EntityType::Account => self.reject_sender(entity.address), + _ => { + error!("Cannot reject entity of type {}", entity.kind); + vec![] + } }; self.entity_updates.insert( entity.address, @@ -1359,17 +1385,6 @@ impl ProposalContext { ret } - fn reject_aggregator(&mut self, address: Address) { - if let Some(group) = self.groups_by_aggregator.remove(&Some(address)) { - for op in group.ops_with_simulations { - if let Some(paymaster) = op.op.paymaster() { - self.add_erep_015_paymaster_amendment(paymaster, 1); - } - self.rejected_ops.push((op.op, op.simulation.entity_infos)); - } - } - } - fn reject_paymaster(&mut self, address: Address) -> Vec
{ self.filter_reject(false, |op| op.paymaster() == Some(address)) } @@ -1434,7 +1449,7 @@ impl ProposalContext { user_ops: group .ops_with_simulations .iter() - .map(|op| op.op_with_replaced_sig()) + .map(|op| op.op.clone()) .collect(), aggregator: aggregator.unwrap_or_default(), signature: group.signature.clone(), @@ -1443,18 +1458,33 @@ impl ProposalContext { } fn get_bundle_gas_limit(&self, chain_spec: &ChainSpec) -> u128 { - // TODO(danc): in the 0.7 entrypoint we could optimize this by removing the need for - // the 10K gas and 63/64 gas overheads for each op in the bundle and instead calculate exactly - // the limit needed to include that overhead for each op. - // - // In the 0.6 entrypoint we're assuming that we need 1 verification gas buffer for each op in the bundle - // regardless of if it uses a post op or not. We can optimize to calculate the exact gas overhead - // needed to have the buffer for each op. + // Bundle overhead + let mut gas_limit = rundler_types::bundle_shared_gas(chain_spec); + + // Per aggregator fixed gas + for agg in self.groups_by_aggregator.keys().flatten() { + if agg.is_zero() { + continue; + } + + let Some(agg) = chain_spec.get_signature_aggregator(agg) else { + // this should be checked prior to calling this function + panic!("BUG: aggregator {agg:?} not found in chain spec"); + }; + + gas_limit += agg.costs().execution_fixed_gas; - self.iter_ops_with_simulations() + // NOTE: this assumes that the DA cost for the aggregated signature is covered by the gas limits + // from the UOs (on chains that have DA gas in gas limit). This is enforced during fee check phase. + } + + // per UO gas, bundle_size == None to signal to exclude shared gas + gas_limit += self + .iter_ops_with_simulations() .map(|sim_op| sim_op.op.gas_limit(chain_spec, None)) - .sum::() - + rundler_types::bundle_shared_gas(chain_spec) + .sum::(); + + gas_limit } fn iter_ops_with_simulations(&self) -> impl Iterator> + '_ { @@ -1680,11 +1710,11 @@ mod tests { use alloy_primitives::{utils::parse_units, Address, B256}; use anyhow::anyhow; use rundler_provider::{ - AggregatorSimOut, MockDAGasOracleSync, MockEntryPointV0_6, MockEvmProvider, - ProvidersWithEntryPoint, + MockDAGasOracleSync, MockEntryPointV0_6, MockEvmProvider, ProvidersWithEntryPoint, }; use rundler_sim::{MockFeeEstimator, MockSimulator}; use rundler_types::{ + aggregator::{AggregatorCosts, MockSignatureAggregator, SignatureAggregatorRegistry}, da::BedrockDAGasBlockData, pool::{MockPool, SimulationViolation}, v0_6::UserOperation, @@ -1866,6 +1896,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; assert_eq!( @@ -1903,6 +1934,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; assert_eq!( @@ -1951,6 +1983,7 @@ mod tests { false, ExpectedStorage::default(), true, + vec![], ) .await; assert_eq!( @@ -1974,17 +2007,21 @@ mod tests { async fn test_aggregators() { // One op with no aggregator, two from aggregator A, and one from // aggregator B. - let unaggregated_op = op_with_sender(address(1)); - let aggregated_op_a1 = op_with_sender(address(2)); - let aggregated_op_a2 = op_with_sender(address(3)); - let aggregated_op_b = op_with_sender(address(4)); let aggregator_a_address = address(10); let aggregator_b_address = address(11); - let op_a1_aggregated_sig = 11; - let op_a2_aggregated_sig = 12; - let op_b_aggregated_sig = 21; + let unaggregated_op = op_with_sender(address(1)); + let mut aggregated_op_a1 = op_with_sender(address(2)); + aggregated_op_a1.aggregator = Some(aggregator_a_address); + let mut aggregated_op_a2 = op_with_sender(address(3)); + aggregated_op_a2.aggregator = Some(aggregator_a_address); + let mut aggregated_op_b = op_with_sender(address(4)); + aggregated_op_b.aggregator = Some(aggregator_b_address); let aggregator_a_signature = 101; let aggregator_b_signature = 102; + + let agg_a = mock_signature_aggregator(aggregator_a_address, bytes(aggregator_a_signature)); + let agg_b = mock_signature_aggregator(aggregator_b_address, bytes(aggregator_b_signature)); + let mut bundle = mock_make_bundle( vec![ MockOp { @@ -1993,39 +2030,15 @@ mod tests { }, MockOp { op: aggregated_op_a1.clone(), - simulation_result: Box::new(move || { - Ok(SimulationResult { - aggregator: Some(AggregatorSimOut { - address: aggregator_a_address, - signature: bytes(op_a1_aggregated_sig), - }), - ..Default::default() - }) - }), + simulation_result: Box::new(|| Ok(SimulationResult::default())), }, MockOp { op: aggregated_op_a2.clone(), - simulation_result: Box::new(move || { - Ok(SimulationResult { - aggregator: Some(AggregatorSimOut { - address: aggregator_a_address, - signature: bytes(op_a2_aggregated_sig), - }), - ..Default::default() - }) - }), + simulation_result: Box::new(|| Ok(SimulationResult::default())), }, MockOp { op: aggregated_op_b.clone(), - simulation_result: Box::new(move || { - Ok(SimulationResult { - aggregator: Some(AggregatorSimOut { - address: aggregator_b_address, - signature: bytes(op_b_aggregated_sig), - }), - ..Default::default() - }) - }), + simulation_result: Box::new(|| Ok(SimulationResult::default())), }, ], vec![ @@ -2045,6 +2058,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![agg_a, agg_b], ) .await; // Ops should be grouped by aggregator. Further, the `signature` field @@ -2136,6 +2150,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; @@ -2200,6 +2215,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; @@ -2259,6 +2275,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; @@ -2357,6 +2374,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; @@ -2408,6 +2426,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; @@ -2534,6 +2553,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; @@ -2569,6 +2589,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; @@ -2584,13 +2605,16 @@ mod tests { #[tokio::test] async fn test_post_op_revert_agg() { - let unaggregated_op = op_with_sender(address(1)); - let aggregated_op_a1 = op_with_sender(address(2)); - let aggregated_op_a2 = op_with_sender(address(3)); let aggregator_a_address = address(10); - let op_a1_aggregated_sig = 11; - let op_a2_aggregated_sig = 12; + let unaggregated_op = op_with_sender(address(1)); + let mut aggregated_op_a1 = op_with_sender(address(2)); + aggregated_op_a1.aggregator = Some(aggregator_a_address); + let mut aggregated_op_a2 = op_with_sender(address(3)); + aggregated_op_a2.aggregator = Some(aggregator_a_address); let aggregator_a_signature = 101; + + let agg_a = mock_signature_aggregator(aggregator_a_address, bytes(aggregator_a_signature)); + let bundle = mock_make_bundle( vec![ MockOp { @@ -2599,27 +2623,11 @@ mod tests { }, MockOp { op: aggregated_op_a1.clone(), - simulation_result: Box::new(move || { - Ok(SimulationResult { - aggregator: Some(AggregatorSimOut { - address: aggregator_a_address, - signature: bytes(op_a1_aggregated_sig), - }), - ..Default::default() - }) - }), + simulation_result: Box::new(|| Ok(SimulationResult::default())), }, MockOp { op: aggregated_op_a2.clone(), - simulation_result: Box::new(move || { - Ok(SimulationResult { - aggregator: Some(AggregatorSimOut { - address: aggregator_a_address, - signature: bytes(op_a2_aggregated_sig), - }), - ..Default::default() - }) - }), + simulation_result: Box::new(|| Ok(SimulationResult::default())), }, ], vec![MockAggregator { @@ -2638,6 +2646,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![agg_a], ) .await; @@ -2680,6 +2689,7 @@ mod tests { true, actual_storage, false, + vec![], ) .await; @@ -2719,6 +2729,7 @@ mod tests { true, actual_storage, false, + vec![], ) .await; @@ -2752,6 +2763,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; @@ -2798,6 +2810,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await; @@ -2831,6 +2844,7 @@ mod tests { false, ExpectedStorage::default(), false, + vec![], ) .await } @@ -2848,6 +2862,7 @@ mod tests { notify_condition_not_met: bool, actual_storage: ExpectedStorage, da_gas_tracking_enabled: bool, + aggregators: Vec, ) -> Bundle { let entry_point_address = address(123); let sender_eoa = address(124); @@ -2962,11 +2977,21 @@ mod tests { .returning(move |_| Ok(bd_cloned.clone())); da_oracle .expect_calc_da_gas_sync() - .returning(move |_, bd, _| { + .returning(move |_, bd, _, _| { assert_eq!(*bd, block_data); 100_000 }); + let mut chain_spec = ChainSpec { + da_pre_verification_gas: da_gas_tracking_enabled, + ..Default::default() + }; + let mut agg_registry = SignatureAggregatorRegistry::default(); + for agg in aggregators { + agg_registry.register(Arc::new(agg)); + } + chain_spec.set_signature_aggregators(Arc::new(agg_registry)); + let mut proposer = BundleProposerImpl::new( 0, ProvidersWithEntryPoint::new( @@ -2976,10 +3001,7 @@ mod tests { ), BundleProposerProviders::new(pool_client, simulator, fee_estimator), Settings { - chain_spec: ChainSpec { - da_pre_verification_gas: da_gas_tracking_enabled, - ..Default::default() - }, + chain_spec, max_bundle_size, max_bundle_gas: 10_000_000, sender_eoa, @@ -3109,4 +3131,13 @@ mod tests { ..Default::default() } } + + fn mock_signature_aggregator(address: Address, signature: Bytes) -> MockSignatureAggregator { + let mut agg = MockSignatureAggregator::default(); + agg.expect_address().return_const(address); + agg.expect_costs().return_const(AggregatorCosts::default()); + agg.expect_aggregate_signatures() + .returning(move |_| Ok(signature.clone())); + agg + } } diff --git a/crates/builder/src/emit.rs b/crates/builder/src/emit.rs index 0855848c5..9ab4933e3 100644 --- a/crates/builder/src/emit.rs +++ b/crates/builder/src/emit.rs @@ -192,6 +192,8 @@ pub enum SkipReason { ExpectedStorageLimit, /// Transaction size limit reached TransactionSizeLimit, + /// UO uses an unsupported aggregator + UnsupportedAggregator(Address), /// Other reason, typically internal errors Other { reason: Arc }, } diff --git a/crates/pool/proto/op_pool/op_pool.proto b/crates/pool/proto/op_pool/op_pool.proto index a4fc7fa38..bcad6ad73 100644 --- a/crates/pool/proto/op_pool/op_pool.proto +++ b/crates/pool/proto/op_pool/op_pool.proto @@ -66,6 +66,8 @@ message UserOperationV06 { bytes signature = 11; // authorization tuple for 7702 txns AuthorizationTuple authorization_tuple = 12; + // aggregator + bytes aggregator = 13; } message UserOperationV07 { @@ -107,6 +109,8 @@ message UserOperationV07 { // authorization tuple for 7702 txns AuthorizationTuple authorization_tuple = 18; + // aggregator + bytes aggregator = 19; } enum EntityType { @@ -513,6 +517,8 @@ enum ReputationStatus { // MEMPOOL ERRORS message MempoolError { + reserved 9; + oneof error { string internal = 1; OperationAlreadyKnownError operation_already_known = 2; @@ -522,7 +528,7 @@ message MempoolError { DiscardedOnInsertError discarded_on_insert = 6; PrecheckViolationError precheck_violation = 7; SimulationViolationError simulation_violation = 8; - UnsupportedAggregatorError unsupported_aggregator = 9; + //UnsupportedAggregatorError unsupported_aggregator = 9; InvalidSignatureError invalid_signature = 10; UnknownEntryPointError unknown_entry_point = 11; MultipleRolesViolation multiple_roles_violation = 12; @@ -533,7 +539,8 @@ message MempoolError { PreOpGasLimitEfficiencyTooLow pre_op_gas_limit_efficiency_too_low = 17; ExecutionGasLimitEfficiencyTooLow execution_gas_limit_efficiency_too_low = 18; TooManyExpectedStorageSlots too_many_expected_storage_slots = 19; - UseUnsupportedEIP use_unsupported_eip= 20; + UseUnsupportedEIP use_unsupported_eip = 20; + AggregatorError aggregator = 21; } } @@ -574,8 +581,12 @@ message SenderAddressUsedAsAlternateEntity { message DiscardedOnInsertError {} -message UnsupportedAggregatorError { - bytes aggregator_address = 1; +// message UnsupportedAggregatorError { +// bytes aggregator_address = 1; +// } + +message AggregatorError { + string reason = 1; } message InvalidSignatureError {} @@ -694,6 +705,8 @@ message FactoryMustBeEmpty{ // SIMULATION VIOLATIONS message SimulationViolationError { + reserved 16, 18; + oneof violation { InvalidSignature invalid_signature = 1; UnintendedRevertWithMessage unintended_revert_with_message = 2; @@ -710,9 +723,9 @@ message SimulationViolationError { AccessedUndeployedContract accessed_undeployed_contract = 13; CalledBannedEntryPointMethod called_banned_entry_point_method = 14; CodeHashChanged code_hash_changed = 15; - AggregatorValidationFailed aggregator_validation_failed = 16; + //AggregatorValidationFailed aggregator_validation_failed = 16; UnstakedPaymasterContext unstaked_paymaster_context = 17; - UnstakedAggregator unstaked_aggregator = 18; + //UnstakedAggregator unstaked_aggregator = 18; VerificationGasLimitBufferTooLow verification_gas_limit_buffer_too_low = 19; ValidationRevert validation_revert = 20; InvalidAccountSignature invalid_account_signature = 21; @@ -720,6 +733,7 @@ message SimulationViolationError { AssociatedStorageDuringDeploy associated_storage_during_deploy = 23; InvalidTimeRange invalid_time_range = 24; AccessedUnsupportedContractType accessed_unsupported_contract_type = 25; + AggregatorMismatch aggregator_mismatch = 26; } } @@ -729,7 +743,7 @@ message InvalidAccountSignature {} message InvalidPaymasterSignature {} -message UnstakedAggregator {} +// message UnstakedAggregator {} message UnstakedPaymasterContext {} @@ -810,7 +824,7 @@ message CalledBannedEntryPointMethod { message CodeHashChanged {} -message AggregatorValidationFailed {} +// message AggregatorValidationFailed {} message VerificationGasLimitBufferTooLow { bytes limit = 1; @@ -844,3 +858,8 @@ message AccessedUnsupportedContractType { string contract_type = 1; bytes contract_address = 2; } + +message AggregatorMismatch { + bytes expected = 1; + bytes actual = 2; +} diff --git a/crates/pool/src/mempool/pool.rs b/crates/pool/src/mempool/pool.rs index 9f823fa46..3f09871d1 100644 --- a/crates/pool/src/mempool/pool.rs +++ b/crates/pool/src/mempool/pool.rs @@ -281,6 +281,9 @@ where // check for eligibility if self.da_gas_oracle.is_some() && block_da_data.is_some() { + // TODO(bundle): assuming a bundle size of 1 + let bundle_size = 1; + let da_gas_oracle = self.da_gas_oracle.as_ref().unwrap(); let block_da_data = block_da_data.unwrap(); @@ -288,11 +291,12 @@ where &op.po.da_gas_data, block_da_data, op.uo().gas_price(gas_fees.base_fee), + op.uo().extra_data_len(bundle_size), ); let required_pvg = op.uo().required_pre_verification_gas( &self.config.chain_spec, - 1, + bundle_size, required_da_gas, ); let actual_pvg = op.uo().pre_verification_gas(); @@ -1324,7 +1328,7 @@ mod tests { let mut oracle = MockDAGasOracleSync::default(); oracle .expect_calc_da_gas_sync() - .returning(move |_, _, _| da_pvg - 1); + .returning(move |_, _, _, _| da_pvg - 1); let mut pool = pool_with_conf_oracle(conf.clone(), oracle); @@ -1359,7 +1363,7 @@ mod tests { let mut oracle = MockDAGasOracleSync::default(); oracle .expect_calc_da_gas_sync() - .returning(move |_, _, _| da_pvg + 1); + .returning(move |_, _, _, _| da_pvg + 1); let mut pool = pool_with_conf_oracle(conf.clone(), oracle); @@ -1394,7 +1398,7 @@ mod tests { let mut oracle = MockDAGasOracleSync::default(); oracle .expect_calc_da_gas_sync() - .returning(move |_, _, _| da_pvg); + .returning(move |_, _, _, _| da_pvg); let mut pool = pool_with_conf_oracle(conf.clone(), oracle); @@ -1429,10 +1433,12 @@ mod tests { .pre_verification_da_gas_limit(&conf.chain_spec, Some(1)); let mut oracle = MockDAGasOracleSync::default(); - oracle.expect_calc_da_gas_sync().returning(move |_, _, gp| { - assert_eq!(gp, po1_gas_price); - da_pvg + 1 - }); + oracle + .expect_calc_da_gas_sync() + .returning(move |_, _, gp, _| { + assert_eq!(gp, po1_gas_price); + da_pvg + 1 + }); let mut pool = pool_with_conf_oracle(conf.clone(), oracle); diff --git a/crates/pool/src/mempool/uo_pool.rs b/crates/pool/src/mempool/uo_pool.rs index 2640bfbbb..6808aa017 100644 --- a/crates/pool/src/mempool/uo_pool.rs +++ b/crates/pool/src/mempool/uo_pool.rs @@ -473,7 +473,7 @@ where async fn add_operation( &self, origin: OperationOrigin, - op: UserOperationVariant, + mut op: UserOperationVariant, ) -> MempoolResult { // Initial state checks let to_replace = { @@ -545,9 +545,36 @@ where // added to the pool and can lead to an overdraft self.paymaster.check_operation_cost(&op).await?; - // Prechecks - let versioned_op = op.clone().into(); + // If using an aggregator, transform with calculated signature + if let Some(aggregator) = op.aggregator() { + let Some(agg) = self.config.chain_spec.get_signature_aggregator(&aggregator) else { + return Err(MempoolError::AggregatorError(format!( + "Unsupported aggregator {:?}", + aggregator + ))); + }; + + let signature = match agg.validate_user_op_signature(&op).await { + Ok(sig) => sig, + Err(e) => { + return Err(MempoolError::AggregatorError(format!( + "Error validating signature: {:?}", + e + ))); + } + }; + + op = op.transform_for_aggregator( + &self.config.chain_spec, + aggregator, + agg.costs().clone(), + signature, + ); + } + + let versioned_op: UP::UO = op.clone().into(); + // Prechecks let precheck_ret = self .pool_providers .prechecker() @@ -565,11 +592,6 @@ where self.check_execution_gas_limit_efficiency(op.clone(), block_hash); let (sim_result, _) = tokio::try_join!(sim_fut, execution_gas_check_future)?; - // No aggregators supported for now - if let Some(agg) = &sim_result.aggregator { - return Err(MempoolError::UnsupportedAggregator(agg.address)); - } - // Check if op has more than the maximum allowed expected storage slots let expected_slots = sim_result.expected_storage.num_slots(); if expected_slots > self.config.max_expected_storage_slots { @@ -956,7 +978,7 @@ struct UoPoolMetrics { mod tests { use std::{collections::HashMap, vec}; - use alloy_primitives::{uint, Bytes}; + use alloy_primitives::{bytes, uint, Bytes}; use mockall::Sequence; use rundler_provider::{ DepositInfo, ExecutionResult, MockDAGasOracleSync, MockEntryPointV0_6, MockEvmProvider, @@ -967,6 +989,10 @@ mod tests { SimulationError, SimulationResult, SimulationSettings, ViolationError, }; use rundler_types::{ + aggregator::{ + AggregatorCosts, MockSignatureAggregator, SignatureAggregatorError, + SignatureAggregatorRegistry, + }, authorization::Eip7702Auth, chain::ChainSpec, da::DAGasUOData, @@ -1920,6 +1946,103 @@ mod tests { assert_eq!(best.len(), 0); } + #[tokio::test] + async fn test_unsupported_aggregator() { + let unsupported = Address::random(); + let op = create_op_from_op_v0_6(UserOperation { + aggregator: Some(unsupported), // unsupported aggregator + ..Default::default() + }); + + let ops = vec![op.clone()]; + let pool = create_pool(ops); + let err = pool + .add_operation(OperationOrigin::Local, op.op) + .await + .err() + .unwrap(); + + assert!(matches!(err, MempoolError::AggregatorError(_))); + } + + #[tokio::test] + async fn test_aggregator_transform() { + let mut config = default_config(); + let agg_address = Address::random(); + let agg_sig = bytes!("deadbeef"); + let org_sig = bytes!("012345"); + + let mut agg = MockSignatureAggregator::default(); + let agg_sig_clone = agg_sig.clone(); + agg.expect_address().return_const(agg_address); + agg.expect_costs().return_const(AggregatorCosts::default()); + agg.expect_validate_user_op_signature() + .returning(move |_| Ok(agg_sig_clone.clone())); + + let mut registry = SignatureAggregatorRegistry::default(); + registry.register(Arc::new(agg)); + + config + .chain_spec + .set_signature_aggregators(Arc::new(registry)); + + let op = create_op_from_op_v0_6(UserOperation { + aggregator: Some(agg_address), + signature: org_sig.clone(), + ..Default::default() + }); + + let ops = vec![op.clone()]; + let pool = create_pool_with_config(config, ops); + let hash = pool + .add_operation(OperationOrigin::Local, op.op) + .await + .unwrap(); + + let pool_op = pool.get_user_operation_by_hash(hash).unwrap(); + + if let UserOperationVariant::V0_6(uo) = &pool_op.uo { + assert_eq!(uo.signature, agg_sig); + assert_eq!(uo.original_signature, org_sig); + } else { + panic!("Expected V0_6 variant"); + } + } + + #[tokio::test] + async fn test_aggregator_fail() { + let mut config = default_config(); + let agg_address = Address::random(); + + let mut agg = MockSignatureAggregator::default(); + agg.expect_address().return_const(agg_address); + agg.expect_costs().return_const(AggregatorCosts::default()); + agg.expect_validate_user_op_signature() + .returning(move |_| Err(SignatureAggregatorError::ValidationReverted)); + + let mut registry = SignatureAggregatorRegistry::default(); + registry.register(Arc::new(agg)); + + config + .chain_spec + .set_signature_aggregators(Arc::new(registry)); + + let op = create_op_from_op_v0_6(UserOperation { + aggregator: Some(agg_address), + ..Default::default() + }); + + let ops = vec![op.clone()]; + let pool = create_pool_with_config(config, ops); + let err = pool + .add_operation(OperationOrigin::Local, op.op) + .await + .err() + .unwrap(); + + assert!(matches!(err, MempoolError::AggregatorError(_))); + } + #[derive(Clone, Debug)] struct OpWithErrors { op: UserOperationVariant, diff --git a/crates/pool/src/server/remote/error.rs b/crates/pool/src/server/remote/error.rs index 92c07c01a..b089a4230 100644 --- a/crates/pool/src/server/remote/error.rs +++ b/crates/pool/src/server/remote/error.rs @@ -23,9 +23,9 @@ use rundler_types::{ use super::protos::{ mempool_error, precheck_violation_error, simulation_violation_error, validation_revert, - AccessedUndeployedContract, AccessedUnsupportedContractType, AggregatorValidationFailed, - AssociatedStorageDuringDeploy, AssociatedStorageIsAlternateSender, CallGasLimitTooLow, - CallHadValue, CalledBannedEntryPointMethod, CodeHashChanged, DidNotRevert, + AccessedUndeployedContract, AccessedUnsupportedContractType, AggregatorError, + AggregatorMismatch, AssociatedStorageDuringDeploy, AssociatedStorageIsAlternateSender, + CallGasLimitTooLow, CallHadValue, CalledBannedEntryPointMethod, CodeHashChanged, DidNotRevert, DiscardedOnInsertError, Entity, EntityThrottledError, EntityType, EntryPointRevert, ExecutionGasLimitEfficiencyTooLow, ExistingSenderWithInitCode, FactoryCalledCreate2Twice, FactoryIsNotContract, FactoryMustBeEmpty, InvalidAccountSignature, InvalidPaymasterSignature, @@ -38,10 +38,9 @@ use super::protos::{ SenderAddressUsedAsAlternateEntity, SenderFundsTooLow, SenderIsNotContractAndNoInitCode, SimulationViolationError as ProtoSimulationViolationError, TooManyExpectedStorageSlots, TotalGasLimitTooHigh, UnintendedRevert, UnintendedRevertWithMessage, UnknownEntryPointError, - UnknownRevert, UnstakedAggregator, UnstakedPaymasterContext, UnsupportedAggregatorError, - UseUnsupportedEip, UsedForbiddenOpcode, UsedForbiddenPrecompile, - ValidationRevert as ProtoValidationRevert, VerificationGasLimitBufferTooLow, - VerificationGasLimitTooHigh, WrongNumberOfPhases, + UnknownRevert, UnstakedPaymasterContext, UseUnsupportedEip, UsedForbiddenOpcode, + UsedForbiddenPrecompile, ValidationRevert as ProtoValidationRevert, + VerificationGasLimitBufferTooLow, VerificationGasLimitTooHigh, WrongNumberOfPhases, }; impl TryFrom for PoolError { @@ -99,9 +98,7 @@ impl TryFrom for MempoolError { Some(mempool_error::Error::SimulationViolation(e)) => { MempoolError::SimulationViolation(e.try_into()?) } - Some(mempool_error::Error::UnsupportedAggregator(e)) => { - MempoolError::UnsupportedAggregator(from_bytes(&e.aggregator_address)?) - } + Some(mempool_error::Error::Aggregator(e)) => MempoolError::AggregatorError(e.reason), Some(mempool_error::Error::UnknownEntryPoint(e)) => { MempoolError::UnknownEntryPoint(from_bytes(&e.entry_point)?) } @@ -222,12 +219,8 @@ impl From for ProtoMempoolError { MempoolError::SimulationViolation(violation) => ProtoMempoolError { error: Some(mempool_error::Error::SimulationViolation(violation.into())), }, - MempoolError::UnsupportedAggregator(agg) => ProtoMempoolError { - error: Some(mempool_error::Error::UnsupportedAggregator( - UnsupportedAggregatorError { - aggregator_address: agg.to_proto_bytes(), - }, - )), + MempoolError::AggregatorError(reason) => ProtoMempoolError { + error: Some(mempool_error::Error::Aggregator(AggregatorError { reason })), }, MempoolError::UnknownEntryPoint(entry_point) => ProtoMempoolError { error: Some(mempool_error::Error::UnknownEntryPoint( @@ -616,11 +609,6 @@ impl From for ProtoSimulationViolationError { DidNotRevert {}, )), }, - SimulationViolation::UnstakedAggregator => ProtoSimulationViolationError { - violation: Some(simulation_violation_error::Violation::UnstakedAggregator( - UnstakedAggregator {}, - )), - }, SimulationViolation::WrongNumberOfPhases(num_phases) => ProtoSimulationViolationError { violation: Some(simulation_violation_error::Violation::WrongNumberOfPhases( WrongNumberOfPhases { num_phases }, @@ -676,13 +664,16 @@ impl From for ProtoSimulationViolationError { )), } } - SimulationViolation::AggregatorValidationFailed => ProtoSimulationViolationError { - violation: Some( - simulation_violation_error::Violation::AggregatorValidationFailed( - AggregatorValidationFailed {}, - ), - ), - }, + SimulationViolation::AggregatorMismatch(expected, actual) => { + ProtoSimulationViolationError { + violation: Some(simulation_violation_error::Violation::AggregatorMismatch( + AggregatorMismatch { + expected: expected.to_proto_bytes(), + actual: actual.to_proto_bytes(), + }, + )), + } + } SimulationViolation::VerificationGasLimitBufferTooLow(limit, needed) => { ProtoSimulationViolationError { violation: Some( @@ -828,9 +819,6 @@ impl TryFrom for SimulationViolation { Some(simulation_violation_error::Violation::DidNotRevert(_)) => { SimulationViolation::DidNotRevert } - Some(simulation_violation_error::Violation::UnstakedAggregator(_)) => { - SimulationViolation::UnstakedAggregator - } Some(simulation_violation_error::Violation::WrongNumberOfPhases(e)) => { SimulationViolation::WrongNumberOfPhases(e.num_phases) } @@ -858,8 +846,11 @@ impl TryFrom for SimulationViolation { Some(simulation_violation_error::Violation::CodeHashChanged(_)) => { SimulationViolation::CodeHashChanged } - Some(simulation_violation_error::Violation::AggregatorValidationFailed(_)) => { - SimulationViolation::AggregatorValidationFailed + Some(simulation_violation_error::Violation::AggregatorMismatch(e)) => { + SimulationViolation::AggregatorMismatch( + from_bytes(&e.expected)?, + from_bytes(&e.actual)?, + ) } Some(simulation_violation_error::Violation::VerificationGasLimitBufferTooLow(e)) => { SimulationViolation::VerificationGasLimitBufferTooLow( diff --git a/crates/pool/src/server/remote/protos.rs b/crates/pool/src/server/remote/protos.rs index e2e3caa98..9e352c216 100644 --- a/crates/pool/src/server/remote/protos.rs +++ b/crates/pool/src/server/remote/protos.rs @@ -72,6 +72,10 @@ impl From<&v0_6::UserOperation> for UserOperation { paymaster_and_data: op.paymaster_and_data.to_proto_bytes(), signature: op.signature.to_proto_bytes(), authorization_tuple, + aggregator: op + .aggregator + .map(|a| a.to_proto_bytes()) + .unwrap_or_default(), }; UserOperation { uo: Some(user_operation::Uo::V06(op)), @@ -104,6 +108,11 @@ impl TryUoFromProto for v0_6::UserOperation { .authorization_tuple .as_ref() .map(|authorization| Eip7702Auth::from(authorization.clone())); + let aggregator = if !op.aggregator.is_empty() { + Some(from_bytes(&op.aggregator)?) + } else { + None + }; Ok(v0_6::UserOperationBuilder::new( chain_spec, @@ -122,6 +131,7 @@ impl TryUoFromProto for v0_6::UserOperation { }, ExtendedUserOperation { authorization_tuple, + aggregator, }, ) .build()) @@ -149,6 +159,10 @@ impl From<&v0_7::UserOperation> for UserOperation { entry_point: op.entry_point.to_proto_bytes(), chain_id: op.chain_id, authorization_tuple: None, + aggregator: op + .aggregator + .map(|a| a.to_proto_bytes()) + .unwrap_or_default(), }; UserOperation { uo: Some(user_operation::Uo::V07(op)), @@ -189,13 +203,14 @@ impl TryUoFromProto for v0_7::UserOperation { op.paymaster_data.into(), ); } - + if !op.factory.is_empty() { + builder = builder.factory(from_bytes(&op.factory)?, op.factory_data.into()); + } if authorization_tuple.is_some() { builder = builder.authorization_tuple(authorization_tuple); } - - if !op.factory.is_empty() { - builder = builder.factory(from_bytes(&op.factory)?, op.factory_data.into()); + if !op.aggregator.is_empty() { + builder = builder.aggregator(from_bytes(&op.aggregator)?); } Ok(builder.build()) diff --git a/crates/provider/Cargo.toml b/crates/provider/Cargo.toml index be07752dc..99d45978e 100644 --- a/crates/provider/Cargo.toml +++ b/crates/provider/Cargo.toml @@ -35,6 +35,7 @@ auto_impl.workspace = true const-hex.workspace = true futures-util.workspace = true pin-project.workspace = true +rand.workspace = true reqwest.workspace = true thiserror.workspace = true tokio.workspace = true diff --git a/crates/provider/src/alloy/da/arbitrum.rs b/crates/provider/src/alloy/da/arbitrum.rs index 9814260de..a76c4b385 100644 --- a/crates/provider/src/alloy/da/arbitrum.rs +++ b/crates/provider/src/alloy/da/arbitrum.rs @@ -68,10 +68,17 @@ where to: Address, block: BlockHashOrNumber, _gas_price: u128, + extra_data_len: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { + let data = if extra_data_len > 0 { + super::extend_bytes_with_random(uo_data, extra_data_len) + } else { + uo_data + }; + let ret = self .node_interface - .gasEstimateL1Component(to, true, uo_data) + .gasEstimateL1Component(to, true, data) .block(block.into()) .call() .await?; diff --git a/crates/provider/src/alloy/da/local/bedrock.rs b/crates/provider/src/alloy/da/local/bedrock.rs index ef48704f6..275fa565f 100644 --- a/crates/provider/src/alloy/da/local/bedrock.rs +++ b/crates/provider/src/alloy/da/local/bedrock.rs @@ -78,10 +78,11 @@ where to: Address, block: BlockHashOrNumber, gas_price: u128, + extra_bytes_len: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { let block_data = self.block_data(block).await?; let uo_data = self.uo_data(data, to, block).await?; - let da_gas = self.calc_da_gas_sync(&uo_data, &block_data, gas_price); + let da_gas = self.calc_da_gas_sync(&uo_data, &block_data, gas_price, extra_bytes_len); Ok((da_gas, uo_data, block_data)) } } @@ -119,6 +120,7 @@ where uo_data: &DAGasUOData, block_data: &DAGasBlockData, gas_price: u128, + extra_data_len: usize, ) -> u128 { let block_da_data = match block_data { DAGasBlockData::Bedrock(block_da_data) => block_da_data, @@ -132,7 +134,10 @@ where let fee_scaled = (block_da_data.base_fee_scalar * 16 * block_da_data.l1_base_fee + block_da_data.blob_base_fee_scalar * block_da_data.blob_base_fee) as u128; - let l1_fee = (uo_data.uo_units as u128 * fee_scaled) / DECIMAL_SCALAR; + + let units = uo_data.uo_units + extra_data_to_units(extra_data_len); + + let l1_fee = (units as u128 * fee_scaled) / DECIMAL_SCALAR; l1_fee.checked_div(gas_price).unwrap_or(u128::MAX) } } @@ -249,3 +254,9 @@ where }) } } + +fn extra_data_to_units(extra_data_len: usize) -> u64 { + // https://github.com/ethereum-optimism/optimism/blob/d39eb247e60584c87b75baec937ddd20701225a5/packages/contracts-bedrock/src/L2/GasPriceOracle.sol#L239 + // bytes are all scaled up by 1e6 + (extra_data_len * 1_000_000) as u64 +} diff --git a/crates/provider/src/alloy/da/local/nitro.rs b/crates/provider/src/alloy/da/local/nitro.rs index 5711b8351..2af5a4596 100644 --- a/crates/provider/src/alloy/da/local/nitro.rs +++ b/crates/provider/src/alloy/da/local/nitro.rs @@ -60,7 +60,8 @@ where data: Bytes, to: Address, block: BlockHashOrNumber, - _gas_price: u128, + gas_price: u128, + extra_bytes_len: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { let mut cache = self.block_data_cache.lock().await; match cache.get(&block) { @@ -72,7 +73,8 @@ where let uo_data = self.get_uo_data(to, data, block).await?; let uo_data = DAGasUOData::Nitro(uo_data); let block_data = DAGasBlockData::Nitro(block_da_data); - let l1_gas_estimate = self.calc_da_gas_sync(&uo_data, &block_data, _gas_price); + let l1_gas_estimate = + self.calc_da_gas_sync(&uo_data, &block_data, gas_price, extra_bytes_len); Ok((l1_gas_estimate, uo_data, block_data)) } @@ -128,6 +130,7 @@ where uo_data: &DAGasUOData, block_data: &DAGasBlockData, _gas_price: u128, + extra_data_len: usize, ) -> u128 { let uo_units = match uo_data { DAGasUOData::Nitro(uo_data) => uo_data.uo_units, @@ -138,7 +141,9 @@ where _ => panic!("NitroDAGasOracle only supports Nitro DAGasBlockData"), }; - calculate_da_fee(uo_units, block_data) + let units = uo_units + extra_data_to_units(extra_data_len); + + calculate_da_fee(units, block_data) } } @@ -241,3 +246,14 @@ fn calculate_uo_units(da_fee: u128, block_da_data: &NitroDAGasBlockData) -> u128 b.saturating_div(block_da_data.l1_base_fee) } } + +// https://github.com/ethereum/go-ethereum/blob/52766bedb9316cd6cddacbb282809e3bdfba143e/params/protocol_params.go#L94 +const TX_NON_ZERO_GAS_EIP_2028: u128 = 16; + +fn extra_data_to_units(extra_data_len: usize) -> u128 { + (extra_data_len as u128) + .saturating_mul(TX_NON_ZERO_GAS_EIP_2028) + .saturating_mul(CACHE_UNITS_SCALAR) + .saturating_mul(101) + .saturating_div(100) +} diff --git a/crates/provider/src/alloy/da/mod.rs b/crates/provider/src/alloy/da/mod.rs index 414145841..6c2fe403c 100644 --- a/crates/provider/src/alloy/da/mod.rs +++ b/crates/provider/src/alloy/da/mod.rs @@ -40,6 +40,7 @@ impl DAGasOracle for ZeroDAGasOracle { _to: Address, _block: BlockHashOrNumber, _gas_price: u128, + _extra_data_len: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { Ok((0, DAGasUOData::Empty, DAGasBlockData::Empty)) } @@ -90,6 +91,12 @@ where } } +fn extend_bytes_with_random(data: Bytes, len: usize) -> Bytes { + let mut new_data = data.to_vec(); + new_data.extend((0..len).map(|_| rand::random::())); + new_data.into() +} + #[cfg(test)] mod tests { use alloy_primitives::{address, b256, bytes, uint, U256}; @@ -184,7 +191,7 @@ mod tests { let cached_res = cached_e2e(cached_oracle, block, to, uo.clone()).await; let contract_res = contract_oracle - .estimate_da_gas(uo, to, block.into(), 1) + .estimate_da_gas(uo, to, block.into(), 1, 0) .await .unwrap() .0; @@ -206,7 +213,7 @@ mod tests { let cached_res = cached_e2e(cached_oracle, block, to, uo.clone()).await; let contract_res = contract_oracle - .estimate_da_gas(uo, to, block.into(), 1) + .estimate_da_gas(uo, to, block.into(), 1, 0) .await .unwrap() .0; @@ -277,11 +284,11 @@ mod tests { let to = Address::random(); let (gas_a, _, _) = cached_oracle - .estimate_da_gas(data.clone(), to, block, gas_price) + .estimate_da_gas(data.clone(), to, block, gas_price, 0) .await .unwrap(); let (gas_b, _, _) = contract_oracle - .estimate_da_gas(data, to, block, gas_price) + .estimate_da_gas(data, to, block, gas_price, 0) .await .unwrap(); @@ -317,7 +324,7 @@ mod tests { ) -> u128 { let block_data = oracle.block_data(block.into()).await.unwrap(); let uo_data = oracle.uo_data(data, to, block.into()).await.unwrap(); - oracle.calc_da_gas_sync(&uo_data, &block_data, 1) + oracle.calc_da_gas_sync(&uo_data, &block_data, 1, 0) } fn opt_provider() -> impl AlloyProvider + Clone { diff --git a/crates/provider/src/alloy/da/optimism.rs b/crates/provider/src/alloy/da/optimism.rs index 22d9dc2d0..ac8d1680c 100644 --- a/crates/provider/src/alloy/da/optimism.rs +++ b/crates/provider/src/alloy/da/optimism.rs @@ -64,11 +64,18 @@ where _to: Address, block: BlockHashOrNumber, gas_price: u128, + extra_data_len: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { if gas_price == 0 { Err(anyhow::anyhow!("gas price cannot be zero"))?; } + let data = if extra_data_len > 0 { + super::extend_bytes_with_random(data, extra_data_len) + } else { + data + }; + let l1_fee: u128 = self .oracle .getL1Fee(data) diff --git a/crates/provider/src/alloy/entry_point/v0_6.rs b/crates/provider/src/alloy/entry_point/v0_6.rs index def2c6563..2e2b458b9 100644 --- a/crates/provider/src/alloy/entry_point/v0_6.rs +++ b/crates/provider/src/alloy/entry_point/v0_6.rs @@ -175,8 +175,12 @@ where match result { Ok(ret) => Ok(Some(ret.aggregatedSignature)), Err(ContractError::TransportError(TransportError::ErrorResp(resp))) => { - if resp.as_revert_data().is_some() { - Ok(None) + if let Some(revert) = resp.as_revert_data() { + let msg = format!( + "Aggregator contract should aggregate signatures. Revert: {revert}", + ); + tracing::error!(msg); + Err(anyhow::anyhow!(msg).into()) } else { Err(TransportError::ErrorResp(resp).into()) } @@ -324,8 +328,11 @@ where user_op: UserOperation, block: BlockHashOrNumber, gas_price: u128, + bundle_size: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { let au = user_op.authorization_tuple(); + let extra_data_len = user_op.extra_data_len(bundle_size); + let mut txn_request = self .i_entry_point .handleOps(vec![user_op.into()], Address::random()) @@ -336,11 +343,18 @@ where let data = txn_request.input.into_input().unwrap(); + // TODO(bundle): assuming a bundle size of 1 let bundle_data = super::max_bundle_transaction_data(*self.i_entry_point.address(), data, gas_price); self.da_gas_oracle - .estimate_da_gas(bundle_data, *self.i_entry_point.address(), block, gas_price) + .estimate_da_gas( + bundle_data, + *self.i_entry_point.address(), + block, + gas_price, + extra_data_len, + ) .await } } diff --git a/crates/provider/src/alloy/entry_point/v0_7.rs b/crates/provider/src/alloy/entry_point/v0_7.rs index 662ffbd84..243851831 100644 --- a/crates/provider/src/alloy/entry_point/v0_7.rs +++ b/crates/provider/src/alloy/entry_point/v0_7.rs @@ -184,8 +184,12 @@ where match result { Ok(ret) => Ok(Some(ret.aggregatedSignature)), Err(ContractError::TransportError(TransportError::ErrorResp(resp))) => { - if resp.as_revert_data().is_some() { - Ok(None) + if let Some(revert) = resp.as_revert_data() { + let msg = format!( + "Aggregator contract should aggregate signatures. Revert: {revert}", + ); + tracing::error!(msg); + Err(anyhow::anyhow!(msg).into()) } else { Err(TransportError::ErrorResp(resp).into()) } @@ -317,8 +321,10 @@ where user_op: UserOperation, block: BlockHashOrNumber, gas_price: u128, + bundle_size: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)> { let au = user_op.authorization_tuple(); + let extra_data_len = user_op.extra_data_len(bundle_size); let mut txn_req = self .i_entry_point @@ -329,11 +335,19 @@ where } let data = txn_req.input.into_input().unwrap(); + + // TODO(bundle): assuming a bundle size of 1 let bundle_data = super::max_bundle_transaction_data(*self.i_entry_point.address(), data, gas_price); self.da_gas_oracle - .estimate_da_gas(bundle_data, *self.i_entry_point.address(), block, gas_price) + .estimate_da_gas( + bundle_data, + *self.i_entry_point.address(), + block, + gas_price, + extra_data_len, + ) .await } } diff --git a/crates/provider/src/traits/da.rs b/crates/provider/src/traits/da.rs index 75edef6a2..e5e56ca60 100644 --- a/crates/provider/src/traits/da.rs +++ b/crates/provider/src/traits/da.rs @@ -31,6 +31,7 @@ pub trait DAGasOracle: Send + Sync { to: Address, block: BlockHashOrNumber, gas_price: u128, + extra_data_len: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)>; } @@ -67,5 +68,6 @@ pub trait DAGasOracleSync: DAGasOracle { uo_data: &DAGasUOData, block_data: &DAGasBlockData, gas_price: u128, + extra_data_len: usize, ) -> u128; } diff --git a/crates/provider/src/traits/entry_point.rs b/crates/provider/src/traits/entry_point.rs index 851b4f1df..07087d85d 100644 --- a/crates/provider/src/traits/entry_point.rs +++ b/crates/provider/src/traits/entry_point.rs @@ -33,8 +33,6 @@ pub struct AggregatorSimOut { /// Result of a signature aggregator call #[derive(Debug)] pub enum AggregatorOut { - /// No aggregator used - NotNeeded, /// Successful call SuccessWithInfo(AggregatorSimOut), /// Aggregator validation function reverted @@ -176,6 +174,7 @@ pub trait DAGasProvider: Send + Sync { uo: Self::UO, block: BlockHashOrNumber, gas_price: u128, + bundle_size: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)>; } diff --git a/crates/provider/src/traits/test_utils.rs b/crates/provider/src/traits/test_utils.rs index def7b80ca..8de1b0046 100644 --- a/crates/provider/src/traits/test_utils.rs +++ b/crates/provider/src/traits/test_utils.rs @@ -177,6 +177,7 @@ mockall::mock! { op: v0_6::UserOperation, block: BlockHashOrNumber, gas_price: u128, + bundle_size: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)>; } @@ -268,6 +269,7 @@ mockall::mock! { op: v0_7::UserOperation, block: BlockHashOrNumber, gas_price: u128, + bundle_size: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)>; } @@ -310,6 +312,7 @@ mockall::mock! { uo_data: &DAGasUOData, block_data: &DAGasBlockData, gas_price: u128, + extra_bytes_len: usize, ) -> u128; } @@ -321,6 +324,7 @@ mockall::mock! { to: Address, block: BlockHashOrNumber, gas_price: u128, + extra_data_len: usize, ) -> ProviderResult<(u128, DAGasUOData, DAGasBlockData)>; } } diff --git a/crates/rpc/src/eth/error.rs b/crates/rpc/src/eth/error.rs index 4ceb3dee8..c1bc8b63d 100644 --- a/crates/rpc/src/eth/error.rs +++ b/crates/rpc/src/eth/error.rs @@ -105,12 +105,15 @@ pub enum EthRpcError { /// The user operation uses a paymaster that returns a context while being unstaked #[error("Unstaked paymaster must not return context")] UnstakedPaymasterContext, - /// The user operation uses an aggregator entity and it is not staked - #[error("An aggregator must be staked, regardless of storager usage")] - UnstakedAggregator, /// Unsupported aggregator #[error("unsupported aggregator")] UnsupportedAggregator(UnsupportedAggregatorData), + /// Aggregator error + #[error("signature aggregator error: {0}")] + AggregatorError(String), + /// Aggregator mismatch + #[error("signature aggregator mismatch. Expected: {0:?}, got: {1:?}")] + AggregatorMismatch(Address, Address), /// Replacement underpriced #[error("replacement underpriced")] ReplacementUnderpriced(ReplacementUnderpricedData), @@ -304,9 +307,7 @@ impl From for EthRpcError { } MempoolError::PrecheckViolation(violation) => violation.into(), MempoolError::SimulationViolation(violation) => violation.into(), - MempoolError::UnsupportedAggregator(a) => { - Self::UnsupportedAggregator(UnsupportedAggregatorData { aggregator: a }) - } + MempoolError::AggregatorError(a) => Self::AggregatorError(a), MempoolError::UnknownEntryPoint(a) => { Self::EntryPointValidationRejected(format!("unknown entry point: {}", a)) } @@ -377,7 +378,7 @@ impl From for EthRpcError { U32::from(stake_data.min_unstake_delay), ))) } - SimulationViolation::AggregatorValidationFailed => Self::SignatureCheckFailed, + SimulationViolation::AggregatorMismatch(e, a) => Self::AggregatorMismatch(e, a), SimulationViolation::OutOfGas(entity) => Self::OutOfGas(entity), SimulationViolation::ValidationRevert(revert) => Self::ValidationRevert(revert.into()), _ => Self::SimulationFailed(value), @@ -402,7 +403,6 @@ impl From for ErrorObjectOwned { EthRpcError::OpcodeViolation(_, _) | EthRpcError::OpcodeViolationMap(_) | EthRpcError::OutOfGas(_) - | EthRpcError::UnstakedAggregator | EthRpcError::MultipleRolesViolation(_) | EthRpcError::UnstakedPaymasterContext | EthRpcError::SenderAddressUsedAsAlternateEntity(_) @@ -426,9 +426,9 @@ impl From for ErrorObjectOwned { EthRpcError::MaxOperationsReached(_, _) => rpc_err(STAKE_TOO_LOW_CODE, msg), EthRpcError::SignatureCheckFailed | EthRpcError::AccountSignatureCheckFailed - | EthRpcError::PaymasterSignatureCheckFailed => { - rpc_err(SIGNATURE_CHECK_FAILED_CODE, msg) - } + | EthRpcError::PaymasterSignatureCheckFailed + | EthRpcError::AggregatorError(_) + | EthRpcError::AggregatorMismatch(_, _) => rpc_err(SIGNATURE_CHECK_FAILED_CODE, msg), EthRpcError::PrecheckFailed(_) => rpc_err(CALL_EXECUTION_FAILED_CODE, msg), EthRpcError::ExecutionReverted(_) => rpc_err(EXECUTION_REVERTED, msg), EthRpcError::ExecutionRevertedWithBytes(data) => { @@ -527,6 +527,9 @@ impl From for EthRpcError { GasEstimationError::ProviderError(provider_error) => { EthRpcError::from(ProviderErrorWithContext::from(provider_error)) } + GasEstimationError::UnsupportedAggregator(aggregator) => { + Self::UnsupportedAggregator(UnsupportedAggregatorData { aggregator }) + } GasEstimationError::Other(error) => { let context = error.to_string(); match error.downcast::() { diff --git a/crates/rpc/src/eth/events/v0_6.rs b/crates/rpc/src/eth/events/v0_6.rs index 52229ab4c..0fa3dc01f 100644 --- a/crates/rpc/src/eth/events/v0_6.rs +++ b/crates/rpc/src/eth/events/v0_6.rs @@ -92,6 +92,7 @@ impl EntryPointEvents for EntryPointFiltersV0_6 { op, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ) .ok() @@ -103,12 +104,13 @@ impl EntryPointEvents for EntryPointFiltersV0_6 { .opsPerAggregator .into_iter() .flat_map(|ops| { - ops.userOps.into_iter().filter_map(|op| { + ops.userOps.into_iter().filter_map(move |op| { UserOperationBuilder::from_contract( chain_spec, op, ExtendedUserOperation { authorization_tuple: None, + aggregator: Some(ops.aggregator), }, ) .ok() diff --git a/crates/rpc/src/types/v0_6.rs b/crates/rpc/src/types/v0_6.rs index 7ae4a61fb..6268f77ce 100644 --- a/crates/rpc/src/types/v0_6.rs +++ b/crates/rpc/src/types/v0_6.rs @@ -38,7 +38,10 @@ pub(crate) struct RpcUserOperation { max_priority_fee_per_gas: U128, paymaster_and_data: Bytes, signature: Bytes, + #[serde(skip_serializing_if = "Option::is_none")] eip7702_auth: Option, + #[serde(skip_serializing_if = "Option::is_none")] + aggregator: Option
, } impl From for RpcUserOperation { @@ -56,6 +59,7 @@ impl From for RpcUserOperation { paymaster_and_data: op.paymaster_and_data, signature: op.signature, eip7702_auth: op.authorization_tuple.map(|a| a.into()), + aggregator: op.aggregator, } } } @@ -79,6 +83,7 @@ impl FromRpc for UserOperation { }, ExtendedUserOperation { authorization_tuple: def.eip7702_auth.map(|a| a.into()), + aggregator: def.aggregator, }, ) .build() @@ -100,6 +105,7 @@ pub(crate) struct RpcUserOperationOptionalGas { paymaster_and_data: Bytes, signature: Bytes, eip7702_auth_address: Option
, + aggregator: Option
, } impl From for UserOperationOptionalGas { @@ -117,6 +123,7 @@ impl From for UserOperationOptionalGas { paymaster_and_data: def.paymaster_and_data, signature: def.signature, eip7702_auth_address: def.eip7702_auth_address, + aggregator: def.aggregator, } } } diff --git a/crates/rpc/src/types/v0_7.rs b/crates/rpc/src/types/v0_7.rs index c5f52c556..60e6e27dc 100644 --- a/crates/rpc/src/types/v0_7.rs +++ b/crates/rpc/src/types/v0_7.rs @@ -50,6 +50,8 @@ pub(crate) struct RpcUserOperation { signature: Bytes, #[serde(skip_serializing_if = "Option::is_none")] eip7702_auth: Option, + #[serde(skip_serializing_if = "Option::is_none")] + aggregator: Option
, } impl From for RpcUserOperation { @@ -88,6 +90,7 @@ impl From for RpcUserOperation { paymaster_data, signature: op.signature, eip7702_auth: op.authorization_tuple.map(|a| a.into()), + aggregator: op.aggregator, } } } @@ -126,6 +129,10 @@ impl FromRpc for UserOperation { if def.eip7702_auth.is_some() { builder = builder.authorization_tuple(def.eip7702_auth.map(|a| a.into())); } + if def.aggregator.is_some() { + builder = builder.aggregator(def.aggregator.unwrap()); + } + builder.build() } } @@ -160,6 +167,7 @@ pub(crate) struct RpcUserOperationOptionalGas { paymaster_data: Option, signature: Bytes, eip7702_auth: Option, + aggregator: Option
, } impl From for UserOperationOptionalGas { @@ -181,6 +189,7 @@ impl From for UserOperationOptionalGas { paymaster_data: def.paymaster_data.unwrap_or_default(), signature: def.signature, eip7702_auth_address: def.eip7702_auth.map(|a| a.address), + aggregator: def.aggregator, } } } diff --git a/crates/sim/src/estimation/mod.rs b/crates/sim/src/estimation/mod.rs index 5cb4267c7..1da0f8c75 100644 --- a/crates/sim/src/estimation/mod.rs +++ b/crates/sim/src/estimation/mod.rs @@ -11,7 +11,7 @@ // You should have received a copy of the GNU General Public License along with Rundler. // If not, see https://www.gnu.org/licenses/. -use alloy_primitives::Bytes; +use alloy_primitives::{Address, Bytes}; #[cfg(feature = "test-utils")] use mockall::automock; use rundler_provider::{ProviderError, StateOverride}; @@ -58,6 +58,9 @@ pub enum GasEstimationError { /// The total amount of gas used by the UO is greater than allowed #[error("total gas used by the user operation {0} is greater than the allowed limit: {1}")] GasTotalTooLarge(u128, u128), + /// Unsupported signature aggregator + #[error("unsupported signature aggregator: {0:?}")] + UnsupportedAggregator(Address), /// Error from provider #[error(transparent)] ProviderError(#[from] ProviderError), diff --git a/crates/sim/src/estimation/v0_6.rs b/crates/sim/src/estimation/v0_6.rs index 61472764e..2ab59bd88 100644 --- a/crates/sim/src/estimation/v0_6.rs +++ b/crates/sim/src/estimation/v0_6.rs @@ -72,6 +72,15 @@ where ) -> Result { self.check_provided_limits(&op)?; + let agg = if let Some(agg) = &op.aggregator { + let Some(agg) = self.chain_spec.get_signature_aggregator(agg) else { + return Err(GasEstimationError::UnsupportedAggregator(*agg)); + }; + Some(agg) + } else { + None + }; + let (block_hash, _) = self .provider .get_latest_block_hash_and_number() @@ -80,7 +89,7 @@ where let pre_verification_gas = self.estimate_pre_verification_gas(&op, block_hash).await?; - let full_op = op + let mut full_op = op .clone() .into_user_operation_builder( &self.chain_spec, @@ -89,6 +98,14 @@ where ) .pre_verification_gas(pre_verification_gas) .build(); + if let Some(agg) = agg { + full_op = full_op.transform_for_aggregator( + &self.chain_spec, + agg.address(), + agg.costs().clone(), + agg.dummy_uo_signature().clone(), + ); + } let verification_future = self.estimate_verification_gas(&op, &full_op, block_hash, state_override.clone()); @@ -289,6 +306,12 @@ where } }; + if let Some(agg) = &optional_op.aggregator { + if self.chain_spec.get_signature_aggregator(agg).is_none() { + return Err(GasEstimationError::UnsupportedAggregator(*agg)); + }; + } + Ok(gas::estimate_pre_verification_gas( &self.chain_spec, &self.entry_point, @@ -569,6 +592,7 @@ mod tests { paymaster_and_data: Bytes::new(), signature: Bytes::new(), eip7702_auth_address: None, + aggregator: None, } } @@ -590,6 +614,7 @@ mod tests { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ) .build() @@ -638,7 +663,7 @@ mod tests { let (mut entry, provider) = create_base_config(); entry .expect_calc_da_gas() - .returning(|_a, _b, _c| Ok((TEST_FEE, Default::default(), Default::default()))); + .returning(|_a, _b, _c, _d| Ok((TEST_FEE, Default::default(), Default::default()))); let settings = Settings { max_verification_gas: 10000000000, @@ -714,7 +739,7 @@ mod tests { entry .expect_calc_da_gas() - .returning(|_a, _b, _c| Ok((TEST_FEE, Default::default(), Default::default()))); + .returning(|_a, _b, _c, _d| Ok((TEST_FEE, Default::default(), Default::default()))); let settings = Settings { max_verification_gas: 10000000000, @@ -1494,6 +1519,26 @@ mod tests { )) } + #[tokio::test] + async fn test_unsupported_aggregator() { + let (entry, provider) = create_base_config(); + let (estimator, _) = create_estimator(entry, provider); + let mut op = demo_user_op_optional_gas(None); + let unsupported = Address::random(); + op.aggregator = Some(unsupported); + + let err = estimator + .estimate_op_gas(op, StateOverride::default()) + .await + .err() + .unwrap(); + + assert!(matches!( + err, + GasEstimationError::UnsupportedAggregator(x) if x == unsupported, + )); + } + #[test] fn test_proxy_target_offset() { let proxy_target_bytes = hex::decode(PROXY_IMPLEMENTATION_ADDRESS_MARKER).unwrap(); diff --git a/crates/sim/src/estimation/v0_7.rs b/crates/sim/src/estimation/v0_7.rs index fa04061c4..d667a4ba8 100644 --- a/crates/sim/src/estimation/v0_7.rs +++ b/crates/sim/src/estimation/v0_7.rs @@ -75,6 +75,15 @@ where provider, settings, .. } = self; + let agg = if let Some(agg) = &op.aggregator { + let Some(agg) = self.chain_spec.get_signature_aggregator(agg) else { + return Err(GasEstimationError::UnsupportedAggregator(*agg)); + }; + Some(agg) + } else { + None + }; + let (block_hash, _) = provider .get_latest_block_hash_and_number() .await @@ -82,7 +91,7 @@ where let pre_verification_gas = self.estimate_pre_verification_gas(&op, block_hash).await?; - let full_op = op + let mut full_op = op .clone() .into_user_operation_builder( &self.chain_spec, @@ -92,6 +101,14 @@ where ) .pre_verification_gas(pre_verification_gas) .build(); + if let Some(agg) = agg { + full_op = full_op.transform_for_aggregator( + &self.chain_spec, + agg.address(), + agg.costs().clone(), + agg.dummy_uo_signature().clone(), + ); + } let verification_gas_future = self.estimate_verification_gas(&op, &full_op, block_hash, state_override.clone()); @@ -265,9 +282,9 @@ where let GetOpWithLimitArgs { gas, fee } = args; // set call gas to 0 to avoid simulating the call, keep paymasterPostOpGasLimit as is because it is often checked during verification UserOperationBuilder::from_uo(op, &self.chain_spec) - .verification_gas_limit(gas) .max_fee_per_gas(fee) .max_priority_fee_per_gas(fee) + .verification_gas_limit(gas) .call_gas_limit(0) .build() }; @@ -615,6 +632,7 @@ mod tests { factory: None, factory_data: Bytes::new(), eip7702_auth_address: None, + aggregator: None, } } @@ -848,6 +866,7 @@ mod tests { factory: None, factory_data: Bytes::new(), eip7702_auth_address: None, + aggregator: None, }; let estimation = estimator @@ -862,6 +881,26 @@ mod tests { )); } + #[tokio::test] + async fn test_unsupported_aggregator() { + let (entry, provider) = create_base_config(); + let (estimator, _) = create_estimator(entry, provider); + let mut op = demo_user_op_optional_gas(None); + let unsupported = Address::random(); + op.aggregator = Some(unsupported); + + let err = estimator + .estimate_op_gas(op, StateOverride::default()) + .await + .err() + .unwrap(); + + assert!(matches!( + err, + GasEstimationError::UnsupportedAggregator(x) if x == unsupported, + )); + } + #[test] fn test_proxy_target_offset() { let proxy_target_bytes = hex::decode(PROXY_IMPLEMENTATION_ADDRESS_MARKER).unwrap(); diff --git a/crates/sim/src/gas/gas.rs b/crates/sim/src/gas/gas.rs index 2cf6de75c..77f3ffd22 100644 --- a/crates/sim/src/gas/gas.rs +++ b/crates/sim/src/gas/gas.rs @@ -44,17 +44,19 @@ pub async fn estimate_pre_verification_gas anyhow::Result { + // TODO(bundle): assuming a bundle size of 1 + let bundle_size = 1; + let da_gas = if chain_spec.da_pre_verification_gas { entry_point - .calc_da_gas(random_op.clone(), block, gas_price) + .calc_da_gas(random_op.clone(), block, gas_price, bundle_size) .await? .0 } else { 0 }; - // Currently assume 1 op bundle - Ok(full_op.required_pre_verification_gas(chain_spec, 1, da_gas)) + Ok(full_op.required_pre_verification_gas(chain_spec, bundle_size, da_gas)) } /// Calculate the required pre_verification_gas for the given user operation and the provided base fee. @@ -67,18 +69,20 @@ pub async fn calc_required_pre_verification_gas anyhow::Result<(u128, DAGasUOData)> { + // TODO(bundle): assuming a bundle size of 1 + let bundle_size = 1; + let (da_gas, uo_data) = if chain_spec.da_pre_verification_gas { let (da_gas, uo_data, _) = entry_point - .calc_da_gas(op.clone(), block, op.gas_price(base_fee)) + .calc_da_gas(op.clone(), block, op.gas_price(base_fee), bundle_size) .await?; (da_gas, uo_data) } else { (0, DAGasUOData::Empty) }; - // Currently assume 1 op bundle Ok(( - op.required_pre_verification_gas(chain_spec, 1, da_gas), + op.required_pre_verification_gas(chain_spec, bundle_size, da_gas), uo_data, )) } diff --git a/crates/sim/src/precheck.rs b/crates/sim/src/precheck.rs index e406232a2..4c000b1ab 100644 --- a/crates/sim/src/precheck.rs +++ b/crates/sim/src/precheck.rs @@ -533,6 +533,7 @@ mod tests { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ) .build(); @@ -584,6 +585,7 @@ mod tests { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ) .build(); @@ -636,6 +638,7 @@ mod tests { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ) .build(); diff --git a/crates/sim/src/simulation/mod.rs b/crates/sim/src/simulation/mod.rs index 7c00c8a51..ffdba2910 100644 --- a/crates/sim/src/simulation/mod.rs +++ b/crates/sim/src/simulation/mod.rs @@ -18,7 +18,7 @@ use alloy_primitives::uint; use alloy_primitives::{Address, B256, U256}; #[cfg(feature = "test-utils")] use mockall::automock; -use rundler_provider::{AggregatorSimOut, ProviderError}; +use rundler_provider::ProviderError; use rundler_types::{ pool::{MempoolError, SimulationViolation}, EntityInfos, UserOperation, ValidTimeRange, @@ -53,9 +53,6 @@ pub struct SimulationResult { pub pre_op_gas: u128, /// The time range for which this operation is valid pub valid_time_range: ValidTimeRange, - /// If using an aggregator, the result of the aggregation - /// simulation - pub aggregator: Option, /// Code hash of all accessed contracts pub code_hash: B256, /// Whether the sender account is staked @@ -73,13 +70,6 @@ pub struct SimulationResult { pub entity_infos: EntityInfos, } -impl SimulationResult { - /// Get the aggregator address if one was used - pub fn aggregator_address(&self) -> Option
{ - self.aggregator.as_ref().map(|agg| agg.address) - } -} - /// The result of a failed simulation. We return a list of the violations that ocurred during the failed simulation /// and also information about all the entities used in the op to handle entity penalties #[derive(Clone, Debug)] diff --git a/crates/sim/src/simulation/simulator.rs b/crates/sim/src/simulation/simulator.rs index e4bb0d172..c4b5db0d7 100644 --- a/crates/sim/src/simulation/simulator.rs +++ b/crates/sim/src/simulation/simulator.rs @@ -17,13 +17,9 @@ use std::{ }; use alloy_primitives::{Address, B256, U256}; -use anyhow::Context; use async_trait::async_trait; use futures_util::TryFutureExt; -use rundler_provider::{ - AggregatorOut, AggregatorSimOut, EntryPoint, EvmProvider, SignatureAggregator, - SimulationProvider, -}; +use rundler_provider::{EntryPoint, EvmProvider, SimulationProvider}; use rundler_types::{ pool::{NeedsStakeInformation, SimulationViolation}, v0_6::UserOperation as UserOperationV0_6, @@ -55,10 +51,7 @@ pub fn new_v0_6_simulator( ) -> impl Simulator where P: EvmProvider + Clone, - E: EntryPoint - + SignatureAggregator - + SimulationProvider - + Clone, + E: EntryPoint + SimulationProvider + Clone, { SimulatorImpl::new( provider.clone(), @@ -78,10 +71,7 @@ pub fn new_v0_7_simulator( ) -> impl Simulator where P: EvmProvider + Clone, - E: EntryPoint - + SignatureAggregator - + SimulationProvider - + Clone, + E: EntryPoint + SimulationProvider + Clone, { SimulatorImpl::new( provider.clone(), @@ -118,7 +108,7 @@ impl SimulatorImpl where UO: UserOperation, P: EvmProvider, - E: EntryPoint + SignatureAggregator, + E: EntryPoint, V: ValidationContextProvider, { /// Create a new simulator @@ -156,22 +146,6 @@ where } } - async fn validate_aggregator_signature( - &self, - op: UO, - aggregator_address: Option
, - ) -> Result { - let Some(aggregator_address) = aggregator_address else { - return Ok(AggregatorOut::NotNeeded); - }; - - Ok(self - .entry_point - .validate_user_op_signature(aggregator_address, op) - .await - .context("should call validate user op signature")?) - } - // Parse the output from tracing and return a list of violations. // Most violations found during this stage are allowlistable and can be added // to the list of allowlisted violations on a given mempool. @@ -346,10 +320,19 @@ where )); } - if let Some(aggregator_info) = entry_point_out.aggregator_info { - if !context::is_staked(aggregator_info.stake_info, &self.sim_settings) { - // [EREP-040] - violations.push(SimulationViolation::UnstakedAggregator) + if let Some(agg_info) = entry_point_out.aggregator_info { + if let Some(agg) = context.op.aggregator() { + if agg_info.address != agg { + violations.push(SimulationViolation::AggregatorMismatch( + agg, + agg_info.address, + )); + } + } else { + violations.push(SimulationViolation::AggregatorMismatch( + Address::ZERO, + agg_info.address, + )); } } @@ -393,38 +376,29 @@ where } // Check the code hash of the entities associated with the user operation - // if needed, validate that the signature is valid for the aggregator. // Violations during this stage are always errors. - async fn check_contracts( + async fn check_code_hash( &self, - op: UO, context: &mut ValidationContext, expected_code_hash: Option, - ) -> Result<(B256, Option), SimulationError> { + ) -> Result { let &mut ValidationContext { block_id, ref mut tracer_out, - ref entry_point_out, .. } = context; // collect a vector of violations to ensure a deterministic error message let mut violations = vec![]; - let aggregator_address = entry_point_out.aggregator_info.map(|info| info.address); - let code_hash_future = self + let code_hash = self .provider .get_code_hash( tracer_out.accessed_contracts.keys().cloned().collect(), Some(block_id), ) - .map_err(|e| SimulationError::from(anyhow::anyhow!("should call get_code_hash {e:?}"))); - - let aggregator_signature_future = - self.validate_aggregator_signature(op, aggregator_address); - - let (code_hash, aggregator_out) = - tokio::try_join!(code_hash_future, aggregator_signature_future)?; + .map_err(|e| SimulationError::from(anyhow::anyhow!("should call get_code_hash {e:?}"))) + .await?; if let Some(expected_code_hash) = expected_code_hash { // [COD-010] @@ -432,14 +406,6 @@ where violations.push(SimulationViolation::CodeHashChanged) } } - let aggregator = match aggregator_out { - AggregatorOut::NotNeeded => None, - AggregatorOut::SuccessWithInfo(info) => Some(info), - AggregatorOut::ValidationReverted => { - violations.push(SimulationViolation::AggregatorValidationFailed); - None - } - }; if !violations.is_empty() { return Err(SimulationError { @@ -448,7 +414,7 @@ where }); } - Ok((code_hash, aggregator)) + Ok(code_hash) } } @@ -457,7 +423,7 @@ impl Simulator for SimulatorImpl where UO: UserOperation, P: EvmProvider, - E: EntryPoint + SignatureAggregator, + E: EntryPoint, V: ValidationContextProvider, { type UO = UO; @@ -496,9 +462,8 @@ where } }; - // Check code hash and aggregator signature, these can't fail - let (code_hash, aggregator) = self - .check_contracts(op, &mut context, expected_code_hash) + let code_hash = self + .check_code_hash(&mut context, expected_code_hash) .await?; // Transform outputs into success struct @@ -530,7 +495,6 @@ where mempools, pre_op_gas, valid_time_range: ValidTimeRange::new(valid_after, valid_until), - aggregator, code_hash, account_is_staked, accessed_addresses, @@ -666,15 +630,13 @@ fn override_infos_staked(eis: &mut EntityInfos, allow_unstaked_addresses: &HashS mod tests { use alloy_primitives::{address, b256, bytes, uint, Bytes}; use context::ContractInfo; - use rundler_provider::{ - AggregatorOut, BlockId, BlockNumberOrTag, MockEntryPointV0_6, MockEvmProvider, - }; + use rundler_provider::{BlockId, BlockNumberOrTag, MockEntryPointV0_6, MockEvmProvider}; use rundler_types::{ chain::ChainSpec, v0_6::{ ExtendedUserOperation, UserOperation, UserOperationBuilder, UserOperationRequiredFields, }, - Opcode, StakeInfo, + AggregatorInfo, Opcode, StakeInfo, }; use self::context::{Phase, TracerOutput}; @@ -854,7 +816,7 @@ mod tests { #[tokio::test] async fn test_simulate_validation() { - let (mut provider, mut entry_point, mut context) = create_base_config(); + let (mut provider, entry_point, mut context) = create_base_config(); provider .expect_get_latest_block_hash_and_number() @@ -878,10 +840,6 @@ mod tests { .expect_get_specific_violations() .returning(|_| Ok(vec![])); - entry_point - .expect_validate_user_op_signature() - .returning(|_, _| Ok(AggregatorOut::NotNeeded)); - let user_operation = UserOperationBuilder::new(&ChainSpec::default(),UserOperationRequiredFields { sender: address!("b856dbd4fa1a79a46d426f537455e7d3e79ab7c4"), nonce: U256::from(264), @@ -897,6 +855,7 @@ mod tests { }, ExtendedUserOperation{ authorization_tuple: None, + aggregator: None, }, ).build(); @@ -1167,4 +1126,61 @@ mod tests { )] ); } + + #[tokio::test] + async fn test_aggregator_mismatch() { + let (provider, mut ep, mut context_provider) = create_base_config(); + ep.expect_address() + .return_const(address!("5ff137d4b0fdcd49dca30c7cf57e578a026d2789")); + context_provider + .expect_get_specific_violations() + .returning(|_| Ok(vec![])); + + let actual_agg = Address::random(); + let bad_agg = Address::random(); + + let mut context = get_test_context(); + context.op.aggregator = Some(actual_agg); + context.entry_point_out.aggregator_info = Some(AggregatorInfo { + address: bad_agg, + stake_info: StakeInfo::default(), + }); + + let simulator = create_simulator(provider, ep, context_provider); + let res = simulator.gather_context_violations(&mut context); + + assert_eq!( + res.unwrap(), + vec![SimulationViolation::AggregatorMismatch(actual_agg, bad_agg)] + ); + } + + #[tokio::test] + async fn test_aggregator_mismatch_zero() { + let (provider, mut ep, mut context_provider) = create_base_config(); + ep.expect_address() + .return_const(address!("5ff137d4b0fdcd49dca30c7cf57e578a026d2789")); + context_provider + .expect_get_specific_violations() + .returning(|_| Ok(vec![])); + + let bad_agg = Address::random(); + + let mut context = get_test_context(); + context.entry_point_out.aggregator_info = Some(AggregatorInfo { + address: bad_agg, + stake_info: StakeInfo::default(), + }); + + let simulator = create_simulator(provider, ep, context_provider); + let res = simulator.gather_context_violations(&mut context); + + assert_eq!( + res.unwrap(), + vec![SimulationViolation::AggregatorMismatch( + Address::ZERO, + bad_agg + )] + ); + } } diff --git a/crates/sim/src/simulation/unsafe_sim.rs b/crates/sim/src/simulation/unsafe_sim.rs index 92e6656dc..2a80e7abd 100644 --- a/crates/sim/src/simulation/unsafe_sim.rs +++ b/crates/sim/src/simulation/unsafe_sim.rs @@ -13,8 +13,8 @@ use std::marker::PhantomData; -use alloy_primitives::B256; -use rundler_provider::{AggregatorOut, EntryPoint, SignatureAggregator, SimulationProvider}; +use alloy_primitives::{Address, B256}; +use rundler_provider::{EntryPoint, SignatureAggregator, SimulationProvider}; use rundler_types::{pool::SimulationViolation, EntityInfos, UserOperation, ValidTimeRange}; use crate::{SimulationError, SimulationResult, Simulator, ViolationError}; @@ -100,23 +100,18 @@ where let mut violations = vec![]; - let aggregator = if let Some(aggregator_info) = validation_result.aggregator_info { - let agg_out = self - .entry_point - .validate_user_op_signature(aggregator_info.address, op) - .await?; - - match agg_out { - AggregatorOut::NotNeeded => None, - AggregatorOut::SuccessWithInfo(info) => Some(info), - AggregatorOut::ValidationReverted => { - violations.push(SimulationViolation::AggregatorValidationFailed); - None + if let Some(agg) = op.aggregator() { + if let Some(agg_info) = validation_result.aggregator_info { + if agg_info.address != agg { + violations.push(SimulationViolation::AggregatorMismatch( + agg, + agg_info.address, + )); } + } else { + violations.push(SimulationViolation::AggregatorMismatch(agg, Address::ZERO)); } - } else { - None - }; + } if validation_result.return_info.account_sig_failed { violations.push(SimulationViolation::InvalidAccountSignature); @@ -138,7 +133,6 @@ where valid_time_range, requires_post_op, entity_infos, - aggregator, ..Default::default() }) } diff --git a/crates/sim/src/simulation/v0_6/context.rs b/crates/sim/src/simulation/v0_6/context.rs index 6ba0de03b..e7de0be52 100644 --- a/crates/sim/src/simulation/v0_6/context.rs +++ b/crates/sim/src/simulation/v0_6/context.rs @@ -342,6 +342,7 @@ mod tests { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ).build(); diff --git a/crates/types/Cargo.toml b/crates/types/Cargo.toml index 69fdb96e2..d12660b23 100644 --- a/crates/types/Cargo.toml +++ b/crates/types/Cargo.toml @@ -17,6 +17,7 @@ alloy-sol-types.workspace = true anyhow.workspace = true async-trait.workspace = true +auto_impl.workspace = true chrono = "0.4.38" const-hex.workspace = true constcat = "0.5.0" diff --git a/crates/types/src/aggregator.rs b/crates/types/src/aggregator.rs new file mode 100644 index 000000000..a5a3153d4 --- /dev/null +++ b/crates/types/src/aggregator.rs @@ -0,0 +1,127 @@ +// This file is part of Rundler. +// +// Rundler is free software: you can redistribute it and/or modify it under the +// terms of the GNU Lesser General Public License as published by the Free Software +// Foundation, either version 3 of the License, or (at your option) any later version. +// +// Rundler is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; +// without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. +// See the GNU General Public License for more details. +// +// You should have received a copy of the GNU General Public License along with Rundler. +// If not, see https://www.gnu.org/licenses/. + +//! Signature aggregator types and registry + +use std::{collections::HashMap, fmt::Debug, sync::Arc}; + +use alloy_primitives::{Address, Bytes}; + +use crate::UserOperationVariant; + +/// Costs associated with an aggregator +#[derive(Clone, Debug, Default, PartialEq, Eq)] +pub struct AggregatorCosts { + /// Fixed gas of the aggregator's `validateSignatures` function + pub execution_fixed_gas: u128, + /// Variable gas of the aggregator's `validateSignatures` function + pub execution_variable_gas: u128, + /// Fixed length of the aggregated signature + pub sig_fixed_length: u128, + /// Variable length of the aggregated signature + pub sig_variable_length: u128, +} + +/// Signature aggregator errors +#[derive(Debug, thiserror::Error)] +pub enum SignatureAggregatorError { + /// Aggregator is not supported + #[error("Unsupported aggregator: {0}")] + UnsupportedAggregator(Address), + /// Signature validation reverted + #[error("Signature validation reverted")] + ValidationReverted, + /// Invalid user operation + #[error("Invalid user operation: {0}")] + InvalidUserOperation(String), + /// Provider error + #[error("Provider error: {0}")] + ProviderError(String), +} + +/// Result type for signature aggregator functions +pub type SignatureAggregatorResult = Result; + +/// Trait for signature aggregators +#[async_trait::async_trait] +#[auto_impl::auto_impl(&, &mut, Rc, Arc, Box)] +pub trait SignatureAggregator: Sync + Send + Debug { + /// Onchain address of the aggregator + fn address(&self) -> Address; + + /// Costs associated with the aggregator + fn costs(&self) -> &AggregatorCosts; + + /// Dummy signature for the aggregator + fn dummy_uo_signature(&self) -> &Bytes; + + /// Validate the signature of a user operation + async fn validate_user_op_signature( + &self, + user_op: &UserOperationVariant, + ) -> SignatureAggregatorResult; + + /// Aggregate multiple signatures + async fn aggregate_signatures( + &self, + uos: Vec, + ) -> SignatureAggregatorResult; +} + +/// Registry of signature aggregators +#[derive(Default, Debug)] +pub struct SignatureAggregatorRegistry { + aggregators: HashMap>, +} + +impl SignatureAggregatorRegistry { + /// Register an aggregator + pub fn register(&mut self, aggregator: Arc) { + self.aggregators.insert(aggregator.address(), aggregator); + } + + /// Get an aggregator by address + pub fn get(&self, address: &Address) -> Option<&Arc> { + self.aggregators.get(address) + } +} + +#[cfg(feature = "test-utils")] +mockall::mock! { + #[derive(Debug)] + pub SignatureAggregator {} + + #[async_trait::async_trait] + impl SignatureAggregator for SignatureAggregator { + /// Onchain address of the aggregator + fn address(&self) -> Address; + + /// Costs associated with the aggregator + fn costs(&self) -> &AggregatorCosts; + + /// Dummy signature for the aggregator + fn dummy_uo_signature(&self) -> &Bytes; + + /// Validate the signature of a user operation + async fn validate_user_op_signature( + &self, + user_op: &UserOperationVariant, + ) -> SignatureAggregatorResult; + + /// Aggregate multiple signatures + async fn aggregate_signatures( + &self, + uos: Vec, + ) -> SignatureAggregatorResult; + } +} diff --git a/crates/types/src/authorization.rs b/crates/types/src/authorization.rs index da6f1382e..e3ea69480 100644 --- a/crates/types/src/authorization.rs +++ b/crates/types/src/authorization.rs @@ -26,11 +26,11 @@ pub struct Eip7702Auth { pub address: Address, /// The nonce for the authorization. pub nonce: u64, - /// signed authorizzation tuple. + /// signed authorization tuple. pub y_parity: u8, - /// signed authorizzation tuple. + /// signed authorization tuple. pub r: U256, - /// signed authorizzation tuple. + /// signed authorization tuple. pub s: U256, } diff --git a/crates/types/src/chain.rs b/crates/types/src/chain.rs index b47ab90d2..d6c73a396 100644 --- a/crates/types/src/chain.rs +++ b/crates/types/src/chain.rs @@ -13,12 +13,15 @@ //! Chain specification for Rundler -use std::str::FromStr; +use std::{str::FromStr, sync::Arc}; use alloy_primitives::Address; use serde::{Deserialize, Serialize}; -use crate::da::DAGasOracleType; +use crate::{ + aggregator::{SignatureAggregator, SignatureAggregatorRegistry}, + da::DAGasOracleType, +}; const ENTRY_POINT_ADDRESS_V6_0: &str = "0x5FF137D4b0FDCD49DcA30c7CF57E578a026d2789"; const ENTRY_POINT_ADDRESS_V7_0: &str = "0x0000000071727De22E5E9d8BAf0edAc6f37da032"; @@ -118,6 +121,13 @@ pub struct ChainSpec { */ /// Size of the chain history to keep to handle reorgs pub chain_history_size: u64, + + /* + * Signature Aggregators + */ + /// Registry of signature aggregators + #[serde(skip)] + pub signature_aggregators: Arc, } /// Type of oracle for estimating priority fees @@ -162,6 +172,7 @@ impl Default for ChainSpec { flashbots_status_url: None, bloxroute_enabled: false, chain_history_size: 64, + signature_aggregators: Arc::new(SignatureAggregatorRegistry::default()), } } } @@ -216,4 +227,20 @@ impl ChainSpec { pub fn per_user_op_deploy_overhead_gas(&self) -> u128 { self.per_user_op_deploy_overhead_gas as u128 } + + /// Set the signature aggregator registry + pub fn set_signature_aggregators( + &mut self, + signature_aggregators: Arc, + ) { + self.signature_aggregators = signature_aggregators; + } + + /// Get a signature aggregator from the registry + pub fn get_signature_aggregator( + &self, + address: &Address, + ) -> Option<&Arc> { + self.signature_aggregators.get(address) + } } diff --git a/crates/types/src/lib.rs b/crates/types/src/lib.rs index 87b65da2b..07080d2b5 100644 --- a/crates/types/src/lib.rs +++ b/crates/types/src/lib.rs @@ -20,6 +20,8 @@ //! Rundler common types +pub mod aggregator; + pub mod builder; pub mod chain; diff --git a/crates/types/src/pool/error.rs b/crates/types/src/pool/error.rs index 583166366..b6dd57e7d 100644 --- a/crates/types/src/pool/error.rs +++ b/crates/types/src/pool/error.rs @@ -85,9 +85,9 @@ pub enum MempoolError { /// Operation was rejected due to a simulation violation #[error("Operation violation during simulation {0}")] SimulationViolation(SimulationViolation), - /// Operation was rejected because it used an unsupported aggregator - #[error("Unsupported aggregator {0}")] - UnsupportedAggregator(Address), + /// Operation was rejected because of an aggregator error + #[error("Aggregator error {0}")] + AggregatorError(String), /// An unknown entry point was specified #[error("Unknown entry point {0}")] UnknownEntryPoint(Address), @@ -213,9 +213,6 @@ pub enum SimulationViolation { /// The user operation uses a paymaster that returns a context while being unstaked #[display("Unstaked paymaster must not return context")] UnstakedPaymasterContext, - /// The user operation uses an aggregator entity and it is not staked - #[display("An aggregator must be staked, regardless of storager usage")] - UnstakedAggregator, /// Simulation reverted with an unintended reason, containing a message #[display("reverted while simulating {0} validation: {1}")] UnintendedRevertWithMessage(EntityType, String, Option
), @@ -234,9 +231,9 @@ pub enum SimulationViolation { /// The user operation ran out of gas during validation #[display("ran out of gas during {0.kind} validation")] OutOfGas(Entity), - /// The user operation aggregator signature validation failed - #[display("aggregator signature validation failed")] - AggregatorValidationFailed, + /// The returned signature aggregator did not match the expected one + #[display("signature aggregator mismatch. Expected: {0:?}, Actual: {1:?}")] + AggregatorMismatch(Address, Address), /// Verification gas limit doesn't have the required buffer on the measured gas #[display("verification gas limit doesn't have the required buffer on the measured gas, limit: {0}, needed: {1}")] VerificationGasLimitBufferTooLow(u128, u128), diff --git a/crates/types/src/pool/types.rs b/crates/types/src/pool/types.rs index 3cc6c54d1..a78a89fc1 100644 --- a/crates/types/src/pool/types.rs +++ b/crates/types/src/pool/types.rs @@ -15,8 +15,8 @@ use alloy_primitives::{Address, B256, U256}; use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; use crate::{ - da::DAGasUOData, entity::EntityInfos, Entity, StakeInfo, UserOperation, UserOperationVariant, - ValidTimeRange, + da::DAGasUOData, entity::EntityInfos, Entity, EntityType, StakeInfo, UserOperation, + UserOperationVariant, ValidTimeRange, }; /// The new head of the chain, as viewed by the pool @@ -144,7 +144,7 @@ impl PoolOperation { /// Return all the unstaked entities that are used in this operation. pub fn unstaked_entities(&'_ self) -> impl Iterator + '_ { self.entity_infos.entities().filter_map(|(t, ei)| { - if ei.is_staked { + if ei.is_staked || ei.kind() == EntityType::Aggregator { None } else { Entity::new(t, ei.entity.address).into() diff --git a/crates/types/src/user_operation/mod.rs b/crates/types/src/user_operation/mod.rs index cb02344c7..e28811fbe 100644 --- a/crates/types/src/user_operation/mod.rs +++ b/crates/types/src/user_operation/mod.rs @@ -22,7 +22,7 @@ pub mod v0_6; /// User Operation types for Entry Point v0.7 pub mod v0_7; -use crate::{authorization::Eip7702Auth, chain::ChainSpec, Entity}; +use crate::{aggregator::AggregatorCosts, authorization::Eip7702Auth, chain::ChainSpec, Entity}; /// A user op must be valid for at least this long into the future to be included. pub const TIME_RANGE_BUFFER: Duration = Duration::from_secs(60); @@ -86,6 +86,9 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// Get the user operation factory address, if any fn factory(&self) -> Option
; + /// Get the user operation aggregator address, if any + fn aggregator(&self) -> Option
; + /// Get the user operation calldata fn call_data(&self) -> &Bytes; @@ -140,11 +143,6 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// This does NOT include any shared gas costs for a bundle (i.e. intrinsic gas) fn static_pre_verification_gas(&self, chain_spec: &ChainSpec) -> u128; - /// Clear the signature field of the user op - /// - /// Used when a user op is using a signature aggregator prior to being submitted - fn clear_signature(&mut self); - /// Abi encode size of the user operation fn abi_encoded_size(&self) -> usize; @@ -153,6 +151,37 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { self.abi_encoded_size() + BUNDLE_BYTE_OVERHEAD + USER_OP_OFFSET_WORD_SIZE } + /// Transform the user operation for a given aggregator + /// + /// Updates: + /// 1) Replaces the signature + /// 2) Modifies the PVG calculations based on the aggregator costs + /// 3) Updates any internally cached values + fn transform_for_aggregator( + self, + chain_spec: &ChainSpec, + aggregator: Address, + aggregator_costs: AggregatorCosts, + new_signature: Bytes, + ) -> Self; + + /// Returns the original signature of the user operation + /// Post-aggregator transformation. + /// + /// Empty if the user operation has not been transformed for an aggregator. + fn original_signature(&self) -> &Bytes; + + /// Sets the original signature back to the user operation + fn with_original_signature(self) -> Self; + + /// Returns the length of any extra data that is included alongside the user operation in a transaction. + /// + /// This is used during DA calculation to charge for the cost of this extra data. It is assumed that all of this + /// data is random and not compressible. + /// + /// An example of extra data is the portion of an aggregated signature that this UO contributes. + fn extra_data_len(&self, bundle_size: usize) -> usize; + /// Gas limit functions /// /// Gas limit: Total as limit for the bundle transaction @@ -231,14 +260,10 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { chain_spec: &ChainSpec, bundle_size: Option, ) -> u128 { - // On some chains (OP bedrock, Arbitrum) the DA gas fee is charged via pre_verification_gas - // but this not part of the EXECUTION gas limit of the transaction. - // - // On other chains, the DA portion is zero. - // - // Thus, only consider the static portion of the pre_verification_gas in the gas limit. self.static_pre_verification_gas(chain_spec) .saturating_add(optional_bundle_per_uo_shared_gas(chain_spec, bundle_size)) + .saturating_add(self.authorization_gas_limit()) + .saturating_add(self.aggregator_gas_limit(chain_spec, bundle_size)) } /// Returns the portion of pre-verification gas that applies to a bundle's total gas limit @@ -265,16 +290,11 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { bundle_size: usize, da_gas: u128, ) -> u128 { - let authorization_gas = if self.authorization_tuple().is_some() { - alloy_eips::eip7702::constants::PER_AUTH_BASE_COST - + alloy_eips::eip7702::constants::PER_EMPTY_ACCOUNT_COST - } else { - 0 - }; self.static_pre_verification_gas(chain_spec) .saturating_add(bundle_per_uo_shared_gas(chain_spec, bundle_size)) .saturating_add(da_gas) - .saturating_add(authorization_gas as u128) + .saturating_add(self.authorization_gas_limit()) + .saturating_add(self.aggregator_gas_limit(chain_spec, Some(bundle_size))) } /// Returns true if the user operation has enough pre-verification gas to be included in a bundle @@ -314,6 +334,21 @@ pub trait UserOperation: Debug + Clone + Send + Sync + 'static { /// Returns the limit of gas that may be used during the paymaster post operation fn paymaster_post_op_gas_limit(&self) -> u128; + + /// Returns the gas limit for the signature aggregator, 0 if no aggregator is used + /// + /// `bundle_size` is the size of the bundle if applying shared gas to the gas limit, otherwise `None`. + fn aggregator_gas_limit(&self, chain_spec: &ChainSpec, bundle_size: Option) -> u128; + + /// Returns the gas limit for the authorization + fn authorization_gas_limit(&self) -> u128 { + if self.authorization_tuple().is_some() { + alloy_eips::eip7702::constants::PER_AUTH_BASE_COST as u128 + + alloy_eips::eip7702::constants::PER_EMPTY_ACCOUNT_COST as u128 + } else { + 0 + } + } } /// Returns the total shared gas for a bundle @@ -328,7 +363,7 @@ pub fn bundle_per_uo_shared_gas(chain_spec: &ChainSpec, bundle_size: usize) -> u if bundle_size == 0 { 0 } else { - bundle_shared_gas(chain_spec) / bundle_size as u128 + bundle_shared_gas(chain_spec).div_ceil(bundle_size as u128) } } @@ -340,6 +375,48 @@ fn optional_bundle_per_uo_shared_gas(chain_spec: &ChainSpec, bundle_size: Option } } +fn aggregator_gas_limit( + chain_spec: &ChainSpec, + agg_costs: &AggregatorCosts, + bundle_size: Option, +) -> u128 { + let shared_portion = if let Some(size) = bundle_size { + (agg_costs.execution_fixed_gas + + agg_costs.sig_fixed_length * chain_spec.calldata_non_zero_byte_gas()) + .div_ceil(size as u128) + } else { + 0 + }; + + let variable_portion = agg_costs.execution_variable_gas + + agg_costs.sig_variable_length * chain_spec.calldata_non_zero_byte_gas(); + + shared_portion + variable_portion +} + +fn extra_data_len(agg_costs: &AggregatorCosts, bundle_size: usize) -> usize { + let len = + agg_costs.sig_fixed_length.div_ceil(bundle_size as u128) + agg_costs.sig_variable_length; + len as usize +} + +// PANICS: if the aggregator is not found in the chain spec +fn transform_for_aggregator( + uo: UO, + aggregator: Address, + chain_spec: &ChainSpec, +) -> UO { + let Some(agg) = chain_spec.get_signature_aggregator(&aggregator) else { + panic!("Aggregator {aggregator:?} not found in chain spec"); + }; + uo.transform_for_aggregator( + chain_spec, + agg.address(), + agg.costs().clone(), + agg.dummy_uo_signature().clone(), + ) +} + /// User operation enum #[derive(Debug, Clone, Eq, PartialEq)] pub enum UserOperationVariant { @@ -398,6 +475,13 @@ impl UserOperation for UserOperationVariant { } } + fn aggregator(&self) -> Option
{ + match self { + UserOperationVariant::V0_6(op) => op.aggregator(), + UserOperationVariant::V0_7(op) => op.aggregator(), + } + } + fn call_data(&self) -> &Bytes { match self { UserOperationVariant::V0_6(op) => op.call_data(), @@ -489,10 +573,62 @@ impl UserOperation for UserOperationVariant { } } - fn clear_signature(&mut self) { + fn aggregator_gas_limit(&self, chain_spec: &ChainSpec, bundle_size: Option) -> u128 { match self { - UserOperationVariant::V0_6(op) => op.clear_signature(), - UserOperationVariant::V0_7(op) => op.clear_signature(), + UserOperationVariant::V0_6(op) => op.aggregator_gas_limit(chain_spec, bundle_size), + UserOperationVariant::V0_7(op) => op.aggregator_gas_limit(chain_spec, bundle_size), + } + } + + fn transform_for_aggregator( + self, + chain_spec: &ChainSpec, + aggregator: Address, + aggregator_costs: AggregatorCosts, + new_signature: Bytes, + ) -> Self { + match self { + UserOperationVariant::V0_6(op) => { + UserOperationVariant::V0_6(op.transform_for_aggregator( + chain_spec, + aggregator, + aggregator_costs, + new_signature, + )) + } + UserOperationVariant::V0_7(op) => { + UserOperationVariant::V0_7(op.transform_for_aggregator( + chain_spec, + aggregator, + aggregator_costs, + new_signature, + )) + } + } + } + + fn original_signature(&self) -> &Bytes { + match self { + UserOperationVariant::V0_6(op) => op.original_signature(), + UserOperationVariant::V0_7(op) => op.original_signature(), + } + } + + fn with_original_signature(self) -> Self { + match self { + UserOperationVariant::V0_6(op) => { + UserOperationVariant::V0_6(op.with_original_signature()) + } + UserOperationVariant::V0_7(op) => { + UserOperationVariant::V0_7(op.with_original_signature()) + } + } + } + + fn extra_data_len(&self, bundle_size: usize) -> usize { + match self { + UserOperationVariant::V0_6(op) => op.extra_data_len(bundle_size), + UserOperationVariant::V0_7(op) => op.extra_data_len(bundle_size), } } @@ -533,6 +669,16 @@ impl UserOperationVariant { UserOperationVariant::V0_7(_) => EntryPointVersion::V0_7, } } + + /// True if the UO is v0.7 type + pub fn is_v0_7(&self) -> bool { + matches!(self, UserOperationVariant::V0_7(_)) + } + + /// True if the UO is v0.6 type + pub fn is_v0_6(&self) -> bool { + matches!(self, UserOperationVariant::V0_6(_)) + } } /// User operation optional gas enum @@ -584,7 +730,7 @@ pub struct UserOpsPerAggregator { } pub(crate) fn op_calldata_gas_cost( - uo: UO, + uo: &UO, zero_byte_cost: u128, non_zero_byte_cost: u128, per_word_cost: u128, diff --git a/crates/types/src/user_operation/v0_6.rs b/crates/types/src/user_operation/v0_6.rs index 07451e5c4..045bfd7b7 100644 --- a/crates/types/src/user_operation/v0_6.rs +++ b/crates/types/src/user_operation/v0_6.rs @@ -22,6 +22,7 @@ use super::{ UserOperationVariant, }; use crate::{ + aggregator::AggregatorCosts, authorization::Eip7702Auth, chain::ChainSpec, entity::{Entity, EntityType}, @@ -79,11 +80,19 @@ pub struct UserOperation { /// Signature pub signature: Bytes, - /// eip 7702 - list of authorities. - pub authorization_tuple: Option, - /// Cached calldata gas cost pub calldata_gas_cost: u128, + + /// eip 7702 - list of authorities. + pub authorization_tuple: Option, + /// Aggregator + pub aggregator: Option
, + /// The full original signature, after the `signature` field is modified post-aggregation + pub original_signature: Bytes, + /// The original calldata cost + pub original_calldata_cost: u128, + /// The costs associated with the aggregator + pub aggregator_costs: AggregatorCosts, } #[cfg(feature = "test-utils")] @@ -106,6 +115,7 @@ impl Default for UserOperation { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ) .build() @@ -195,6 +205,10 @@ impl UserOperationTrait for UserOperation { Self::get_address_from_field(&self.paymaster_and_data) } + fn aggregator(&self) -> Option
{ + self.aggregator + } + fn call_data(&self) -> &Bytes { &self.call_data } @@ -263,12 +277,8 @@ impl UserOperationTrait for UserOperation { } fn static_pre_verification_gas(&self, chain_spec: &ChainSpec) -> u128 { - super::op_calldata_gas_cost( - ContractUserOperation::from(self.clone()), - chain_spec.calldata_zero_byte_gas(), - chain_spec.calldata_non_zero_byte_gas(), - chain_spec.per_user_op_word_gas(), - ) + chain_spec.per_user_op_v0_6_gas() + self.calldata_gas_cost + + chain_spec.per_user_op_v0_6_gas() + (if self.factory().is_some() { chain_spec.per_user_op_deploy_overhead_gas() } else { @@ -276,8 +286,53 @@ impl UserOperationTrait for UserOperation { }) } - fn clear_signature(&mut self) { - self.signature = Bytes::default(); + fn aggregator_gas_limit(&self, chain_spec: &ChainSpec, bundle_size: Option) -> u128 { + if self.aggregator.is_none() { + return 0; + } + super::aggregator_gas_limit(chain_spec, &self.aggregator_costs, bundle_size) + } + + fn transform_for_aggregator( + mut self, + chain_spec: &ChainSpec, + aggregator: Address, + aggregator_costs: AggregatorCosts, + new_signature: Bytes, + ) -> Self { + self.aggregator = Some(aggregator); + self.aggregator_costs = aggregator_costs; + self.original_calldata_cost = self.calldata_gas_cost; + self.original_signature = self.signature; + self.signature = new_signature; + + let cuo = ContractUserOperation::from(self.clone()); + self.calldata_gas_cost = super::op_calldata_gas_cost( + &cuo, + chain_spec.calldata_zero_byte_gas(), + chain_spec.calldata_non_zero_byte_gas(), + chain_spec.per_user_op_word_gas(), + ); + + self + } + + fn original_signature(&self) -> &Bytes { + &self.original_signature + } + + fn with_original_signature(mut self) -> Self { + self.signature = self.original_signature.clone(); + self.calldata_gas_cost = self.original_calldata_cost; + self + } + + fn extra_data_len(&self, bundle_size: usize) -> usize { + if self.aggregator.is_some() { + super::extra_data_len(&self.aggregator_costs, bundle_size) + } else { + 0 + } } fn abi_encoded_size(&self) -> usize { @@ -401,13 +456,17 @@ pub struct UserOperationOptionalGas { /// Signature (required, dummy value for gas estimation) pub signature: Bytes, - /// eip 7702 - tuple of authority. + /// eip 7702 - authorized address pub eip7702_auth_address: Option
, + /// Signature aggregator, if any + pub aggregator: Option
, } impl UserOperationOptionalGas { /// Fill in the optional and dummy fields of the user operation with values /// that will cause the maximum possible calldata gas cost. + /// + /// PANICS: if the aggregator on the user operation is not found in chain spec. Check this before calling this function. pub fn max_fill(&self, chain_spec: &ChainSpec) -> UserOperation { let max_4 = u32::MAX as u128; let max_8 = u64::MAX as u128; @@ -429,10 +488,17 @@ impl UserOperationOptionalGas { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: self.aggregator, }, ); - builder.build() + let uo = builder.build(); + + if let Some(agg) = uo.aggregator { + super::transform_for_aggregator(uo, agg, chain_spec) + } else { + uo + } } /// Fill in the optional and dummy fields of the user operation with random values. @@ -444,6 +510,8 @@ impl UserOperationOptionalGas { // /// Note that this will slightly overestimate the calldata gas needed as it uses /// the worst case scenario for the unknown gas values and paymaster_and_data. + /// + /// PANICS: if the aggregator on the user operation is not found in chain spec. Check this before calling this function. pub fn random_fill(&self, chain_spec: &ChainSpec) -> UserOperation { let builder = UserOperationBuilder::new( chain_spec, @@ -462,10 +530,17 @@ impl UserOperationOptionalGas { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: self.aggregator, }, ); - builder.build() + let uo = builder.build(); + + if let Some(agg) = uo.aggregator { + super::transform_for_aggregator(uo, agg, chain_spec) + } else { + uo + } } /// Convert into a user operation builder. @@ -503,6 +578,7 @@ impl UserOperationOptionalGas { }; let extended = ExtendedUserOperation { authorization_tuple, + aggregator: self.aggregator, }; UserOperationBuilder::new(chain_spec, required, extended) @@ -551,6 +627,8 @@ pub struct UserOperationBuilder<'a> { pub struct ExtendedUserOperation { /// EIP 7702: authorization tuples. pub authorization_tuple: Option, + /// Signature aggregator, if any + pub aggregator: Option
, } /// User operation required fields @@ -631,8 +709,6 @@ impl<'a> UserOperationBuilder<'a> { /// Create a builder from a user operation pub fn from_uo(uo: UserOperation, chain_spec: &'a ChainSpec) -> Self { - let authorization_tuple = uo.authorization_tuple; - Self { chain_spec, required: UserOperationRequiredFields { @@ -650,7 +726,8 @@ impl<'a> UserOperationBuilder<'a> { }, contract_uo: None, extended: ExtendedUserOperation { - authorization_tuple, + authorization_tuple: uo.authorization_tuple, + aggregator: uo.aggregator, }, } } @@ -704,8 +781,12 @@ impl<'a> UserOperationBuilder<'a> { max_priority_fee_per_gas: self.required.max_priority_fee_per_gas, paymaster_and_data: self.required.paymaster_and_data, signature: self.required.signature, + aggregator: None, authorization_tuple: self.extended.authorization_tuple, calldata_gas_cost: 0, + original_calldata_cost: 0, + original_signature: Bytes::default(), + aggregator_costs: AggregatorCosts::default(), }; let cuo = self @@ -713,7 +794,7 @@ impl<'a> UserOperationBuilder<'a> { .unwrap_or_else(|| ContractUserOperation::from(uo.clone())); uo.calldata_gas_cost = super::op_calldata_gas_cost( - cuo, + &cuo, self.chain_spec.calldata_zero_byte_gas(), self.chain_spec.calldata_non_zero_byte_gas(), self.chain_spec.per_user_op_word_gas(), @@ -768,6 +849,7 @@ mod tests { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ) .build(); @@ -828,6 +910,7 @@ mod tests { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ) .build(); @@ -880,6 +963,7 @@ mod tests { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ) .build(); @@ -908,6 +992,7 @@ mod tests { }, ExtendedUserOperation { authorization_tuple: None, + aggregator: None, }, ) .build(); @@ -939,6 +1024,7 @@ mod tests { max_fee_per_gas: None, max_priority_fee_per_gas: None, eip7702_auth_address: None, + aggregator: None, } .max_fill(&ChainSpec::default()); @@ -946,4 +1032,50 @@ mod tests { let cuo = ContractUserOperation::from(max_op).abi_encode(); assert_eq!(size, cuo.len()); } + + #[test] + fn test_aggregator() { + let cs = ChainSpec::default(); + let aggregator = Address::random(); + let orig_sig = bytes!("deadbeef"); + let new_sig = bytes!("1234123412341234"); // calldata costs more + let uo = UserOperationBuilder::new( + &ChainSpec::default(), + UserOperationRequiredFields { + sender: address!("0000000000000000000000000000000000000000"), + nonce: U256::ZERO, + init_code: Bytes::default(), + call_data: Bytes::default(), + call_gas_limit: 0, + verification_gas_limit: 0, + pre_verification_gas: 0, + max_fee_per_gas: 0, + max_priority_fee_per_gas: 0, + paymaster_and_data: Bytes::default(), + signature: orig_sig.clone(), + }, + ExtendedUserOperation { + authorization_tuple: None, + aggregator: Some(aggregator), + }, + ) + .build(); + + let orig_calldata_cost = uo.calldata_gas_cost; + + let uo = uo.transform_for_aggregator( + &cs, + aggregator, + AggregatorCosts::default(), + new_sig.clone(), + ); + + assert_eq!(uo.signature, new_sig); + assert_eq!(uo.original_signature, orig_sig); + assert!(uo.calldata_gas_cost > orig_calldata_cost); + + let uo = uo.with_original_signature(); + assert_eq!(uo.signature, orig_sig); + assert_eq!(uo.calldata_gas_cost, orig_calldata_cost); + } } diff --git a/crates/types/src/user_operation/v0_7.rs b/crates/types/src/user_operation/v0_7.rs index 111139d88..b3eb6e0df 100644 --- a/crates/types/src/user_operation/v0_7.rs +++ b/crates/types/src/user_operation/v0_7.rs @@ -19,7 +19,10 @@ use super::{ random_bytes, random_bytes_array, UserOperation as UserOperationTrait, UserOperationId, UserOperationVariant, }; -use crate::{authorization::Eip7702Auth, chain::ChainSpec, Entity, EntryPointVersion}; +use crate::{ + aggregator::AggregatorCosts, authorization::Eip7702Auth, chain::ChainSpec, Entity, + EntryPointVersion, +}; /// Gas overhead required by the entry point contract for the inner call pub const ENTRY_POINT_INNER_GAS_OVERHEAD: u128 = 10_000; @@ -98,6 +101,18 @@ pub struct UserOperation { pub packed: PackedUserOperation, /// The gas cost of the calldata pub calldata_gas_cost: u128, + + /* + * Signature aggregator fields + */ + /// Signature aggregator address + pub aggregator: Option
, + /// The full original signature, after the `signature` field is modified post-aggregation + pub original_signature: Bytes, + /// The original calldata costs + pub original_calldata_cost: u128, + /// The costs associated with the aggregator + pub aggregator_costs: AggregatorCosts, } impl UserOperationTrait for UserOperation { @@ -134,6 +149,10 @@ impl UserOperationTrait for UserOperation { self.factory } + fn aggregator(&self) -> Option
{ + self.aggregator + } + fn call_data(&self) -> &Bytes { &self.call_data } @@ -225,10 +244,56 @@ impl UserOperationTrait for UserOperation { / 63) } - fn clear_signature(&mut self) { - self.signature = Bytes::new(); + fn aggregator_gas_limit(&self, chain_spec: &ChainSpec, bundle_size: Option) -> u128 { + if self.aggregator.is_none() { + return 0; + } + super::aggregator_gas_limit(chain_spec, &self.aggregator_costs, bundle_size) + } + + fn transform_for_aggregator( + mut self, + chain_spec: &ChainSpec, + aggregator: Address, + aggregator_costs: AggregatorCosts, + new_signature: Bytes, + ) -> Self { + self.aggregator = Some(aggregator); + self.aggregator_costs = aggregator_costs; + self.original_signature = self.signature; + self.original_calldata_cost = self.calldata_gas_cost; + self.signature = new_signature; + + // re-pack, hash stays the same as only signature changed + self.packed = pack_user_operation(self.clone()); + // recalculate calldata gas cost + self.calldata_gas_cost = super::op_calldata_gas_cost( + &self.packed, + chain_spec.calldata_zero_byte_gas(), + chain_spec.calldata_non_zero_byte_gas(), + chain_spec.per_user_op_word_gas(), + ); + + self + } + + fn original_signature(&self) -> &Bytes { + &self.original_signature + } + + fn with_original_signature(mut self) -> Self { + self.signature = self.original_signature.clone(); self.packed = pack_user_operation(self.clone()); - self.hash = hash_packed_user_operation(&self.packed, self.entry_point, self.chain_id); + self.calldata_gas_cost = self.original_calldata_cost; + self + } + + fn extra_data_len(&self, bundle_size: usize) -> usize { + if self.aggregator.is_some() { + super::extra_data_len(&self.aggregator_costs, bundle_size) + } else { + 0 + } } fn abi_encoded_size(&self) -> usize { @@ -341,11 +406,15 @@ pub struct UserOperationOptionalGas { pub paymaster_data: Bytes, /// 7702 authorization contract address. pub eip7702_auth_address: Option
, + /// Signature aggregator address + pub aggregator: Option
, } impl UserOperationOptionalGas { /// Fill in the optional and dummy fields of the user operation with values /// that will cause the maximum possible calldata gas cost. + /// + /// PANICS: if the aggregator on the user operation is not found in chain spec. Check this before calling this function. pub fn max_fill(&self, chain_spec: &ChainSpec) -> UserOperation { let max_4 = u32::MAX as u128; let max_8 = u64::MAX as u128; @@ -373,22 +442,30 @@ impl UserOperationOptionalGas { vec![255_u8; self.paymaster_data.len()].into(), ); } + if self.factory.is_some() { + builder = builder.factory( + self.factory.unwrap(), + vec![255_u8; self.factory_data.len()].into(), + ); + } if let Some(eip_7702_auth_address) = self.eip7702_auth_address { builder = builder.authorization_tuple(Some(Eip7702Auth { address: eip_7702_auth_address, chain_id: chain_spec.id, - // fake value for gas estimation. ..Default::default() })); } - if self.factory.is_some() { - builder = builder.factory( - self.factory.unwrap(), - vec![255_u8; self.factory_data.len()].into(), - ); + if let Some(aggregator) = self.aggregator { + builder = builder.aggregator(aggregator); } - builder.build() + let uo = builder.build(); + + if let Some(agg) = uo.aggregator { + super::transform_for_aggregator(uo, agg, chain_spec) + } else { + uo + } } /// Fill in the optional and dummy fields of the user operation with random values. @@ -400,6 +477,8 @@ impl UserOperationOptionalGas { // /// Note that this will slightly overestimate the calldata gas needed as it uses /// the worst case scenario for the unknown gas values and paymaster_and_data. + /// + /// PANICS: if the aggregator on the user operation is not found in chain spec. Check this before calling this function. pub fn random_fill(&self, chain_spec: &ChainSpec) -> UserOperation { let mut builder = UserOperationBuilder::new( chain_spec, @@ -427,8 +506,24 @@ impl UserOperationOptionalGas { if self.factory.is_some() { builder = builder.factory(self.factory.unwrap(), random_bytes(self.factory_data.len())) } + if let Some(eip_7702_auth_address) = self.eip7702_auth_address { + builder = builder.authorization_tuple(Some(Eip7702Auth { + address: eip_7702_auth_address, + chain_id: chain_spec.id, + ..Default::default() + })); + } + if let Some(aggregator) = self.aggregator { + builder = builder.aggregator(aggregator); + } + + let uo = builder.build(); - builder.build() + if let Some(agg) = uo.aggregator { + super::transform_for_aggregator(uo, agg, chain_spec) + } else { + uo + } } /// Convert into a builder for producing a full user operation. @@ -540,6 +635,7 @@ pub struct UserOperationBuilder<'a> { /// eip 7702 - tuple of authority. authorization_tuple: Option, + aggregator: Option
, } /// Required fields for UserOperation v0.7 @@ -578,6 +674,7 @@ impl<'a> UserOperationBuilder<'a> { paymaster_data: Bytes::new(), packed_uo: None, authorization_tuple: None, + aggregator: None, } } @@ -651,6 +748,7 @@ impl<'a> UserOperationBuilder<'a> { paymaster_data: uo.paymaster_data, packed_uo: None, authorization_tuple: uo.authorization_tuple, + aggregator: uo.aggregator, } } @@ -733,6 +831,12 @@ impl<'a> UserOperationBuilder<'a> { self } + /// Sets the aggregator + pub fn aggregator(mut self, aggregator: Address) -> Self { + self.aggregator = Some(aggregator); + self + } + /// Builds the UserOperation pub fn build(self) -> UserOperation { let uo = UserOperation { @@ -757,6 +861,10 @@ impl<'a> UserOperationBuilder<'a> { hash: B256::ZERO, packed: PackedUserOperation::default(), calldata_gas_cost: 0, + aggregator: self.aggregator, + original_signature: Bytes::new(), + original_calldata_cost: 0, + aggregator_costs: AggregatorCosts::default(), }; let packed = self @@ -768,7 +876,7 @@ impl<'a> UserOperationBuilder<'a> { self.chain_spec.id, ); let calldata_gas_cost = super::op_calldata_gas_cost( - packed.clone(), + &packed, self.chain_spec.calldata_zero_byte_gas(), self.chain_spec.calldata_non_zero_byte_gas(), self.chain_spec.per_user_op_word_gas(), @@ -1008,4 +1116,46 @@ mod tests { assert_eq!(uo.paymaster_verification_gas_limit, 10); assert_eq!(uo.paymaster_post_op_gas_limit, 20); } + + #[test] + fn test_aggregator() { + let cs = ChainSpec::default(); + let aggregator = Address::random(); + let orig_sig = bytes!("deadbeef"); + let new_sig = bytes!("12341234"); + let uo = UserOperationBuilder::new( + &cs, + UserOperationRequiredFields { + sender: Address::ZERO, + nonce: U256::ZERO, + call_data: Bytes::new(), + call_gas_limit: 0, + verification_gas_limit: 0, + pre_verification_gas: 0, + max_priority_fee_per_gas: 0, + max_fee_per_gas: 0, + signature: orig_sig.clone(), + }, + ) + .aggregator(aggregator) + .build(); + + let original_calldata_cost = uo.calldata_gas_cost; + + let uo = uo.transform_for_aggregator( + &cs, + aggregator, + AggregatorCosts::default(), + new_sig.clone(), + ); + + assert_eq!(uo.signature, new_sig); + assert_eq!(uo.original_signature, orig_sig); + assert_eq!(uo.packed.signature, new_sig); + + let uo = uo.with_original_signature(); + assert_eq!(uo.signature, orig_sig); + assert_eq!(uo.packed.signature, orig_sig); + assert_eq!(uo.calldata_gas_cost, original_calldata_cost); + } } diff --git a/docs/architecture/aggregators.md b/docs/architecture/aggregators.md new file mode 100644 index 000000000..aac912aa9 --- /dev/null +++ b/docs/architecture/aggregators.md @@ -0,0 +1,106 @@ +# Signature Aggregation + +This is an overview of Rundler's support for [ERC-7766: Signature Aggregation](https://eips.ethereum.org/EIPS/eip-7766). + +Rundler requires explicit implementations and registration of each signature aggregator that it supports. Due to: + +* Signature aggregators have a large amount of "power" in a ERC-4337 bundle. The bundler must trust the signature aggregator implementation to not DOS. + * Staking and reputation could also work here - but Rundler requires an explicit registry due to the following points. +* The ERC-4337 entrypoint does not provide a way to "meter" the gas used by an aggregator on chain. Thus, bundlers must know how to charge for the aggregator's gas using `preVerificationGas`. This requires explicit knowledge of the aggregator's code. +* Most useful aggregators require an offchain component to perform the signature aggregation efficiently. + +## Trait & Registry + +The primary aggregator trait is `SignatureAggregator`. Each supported aggregator must implement this trait. This trait includes methods to: + +* Return the aggregator's address, costs, and associated dummy UO signature +* Verify UO signatures prior to aggregation and return the UO component of the signature, stripping away anything that isn't needed onchain. +* Aggregate UO signatures + +### Costs + +The `SignatureAggregator::costs(...) -> AggregatorCosts` function requires special care during implementation. Its return value consists of: + +* `execution_fixed_gas`: The fixed gas cost of the onchain aggregator's `validateSignatures` function. It *may* be amortized across all of the UOs in a bundle. +* `execution_variable_gas`: The per UO added gas cost of the the onchain aggregator's `validateSignatures` function. Each UO using the aggregator will always be charged for this. +* `sig_fixed_length`: The fixed length of an aggregator's signature. The calldata & DA cost for this signature *may* be amortized across all of the UOs in a bundle. +* `sig_variable_length`: The per UO added signature length. The calldata & DA cost for this will always be charged to the UO. + +Developers should determine these values using simulation tools and hardcode their values (or calculations) into their implementations. + +### Chain Spec Registry + +Each signature aggregator is registered on the `ChainSpec` object for access by most components in Rundler. This registry happens in the `bin/rundler` crate's `instantiate_aggregators` function. Developers should add their aggregators to this function for support. Ideally, registration is gated behind (1) CLI flags (2) compile time feature flags (especially if bringing in large dependency crates) (3) a combination of both. + +## `PreVerificationGas` + +Gas costs associated with signature aggregators are always charged via `PreVerificationGas` (PVG) as the entry point does not provide onchain metering. + +### Bundle Size + +The PVG calculations, during estimation and fee checks, are done against a specific bundle size. Shared costs *may* be amortized across UOs in the bundle. + +NOTE: Rundler does not currently support dynamic bundle sizes during estimation and fee checks. UOs are always charged as if the bundle is size 1. See [here](#dynamic-bundle-size) for more detail. + +### PVG Components + +* Execution: `execution_fixed_gas` and `execution_variable_gas` from the aggregator contribute here. +* Calldata: `sig_fixed_length` and `sig_variable_length` from the aggregator contribute here. +* DA:`sig_fixed_length` and `sig_variable_length` from the aggregator also contribute here. During DA gas cost estimation the aggregator signature is assumed to be random bytes that have a compression ratio of 1 (i.e. they don't compress). A future update may allow aggregators to specify the compression ratio of their signatures on various L2 stack types. + +## Task Support + +### RPC + +In both `eth_estimateUserOperationGas` and `eth_sendUserOperation`, Rundler has added a `aggregator: Optional
` field to the UO. UOs using an aggregator MUST send the aggregator address they're using as part of these RPC calls. This field is NOT included in the `PackedUserOperation` that the UO uses to generate a signature and is not submitted onchain. + +NOTE: this is a deviation from the current ERC-4337 and ERC-7766 specs. It allows the bundler to perform logic on the aggregator without needing to run the UO's `validateUserOp` function, improving latency and simplifying code. + +### Pool + +When a UO with an `aggregator` set is added to the pool, the pool performs a series of tasks: + +1. Checks if the UO's signature aggregator is supported and retrieves the aggregator from the chain spec. +2. Calls the aggregators `validate_user_op_signature`, ensure's that its valid, and retrieves the UO signature object. +3. Transforms the UO with its new signature (retaining the old signature for later aggregation), captures some metadata about the aggregator alongside the UO, and adds to the mempool. + +### Builder + +During bundle building the builder: + +1. Throws out UOs that contain unsupported aggregators (this shouldn't happen in a correctly configured system). +2. Calculates an aggregated signature for the UOs and submits the bundle via the entry point's `handleAggregatedOps` function. + +## Future Work + +### Builder <> Aggregator Affinity + +In order to maximize the amount of UOs aggregated together, UOs that use the same aggregator should have affinity to a builder (or group of builders). This will be built to allow a configurable tradeoff between time to mine latency and aggregation size. + +### Dynamic Bundle Size + +Rundler currently assumes a bundle size of 1 during: + +* PVG gas estimation +* Mempool precheck fee check +* Mempool UO candidate DA fee checking +* Builder bundle inclusion fee check. + +This unfortunately means that each UO is charged for the full amount of the fixed components of the aggregators costs. This renders a large class of aggregators as not useful. + +A future update may improve this. However, the design here is not straightforward and will require research. High level ideas can be found in a Github issue tracking the work. + +## Implementations + +Implementations can be found [here](../../crates/aggregators/). Each implementation is its own crate. + +### [BLS](../../crates/aggregators/bls/) + +The BLS aggregator adds support for the BLS aggregator contracts from [eth-infinitism](https://github.com/eth-infinitism/account-abstraction-samples/tree/master/contracts/bls). + +Currently, all aggregator functions are implemented using entrypoint calls, as opposed to local BLS logic. A future update may improve this. + +Enabling the BLS aggregator is controlled by the `--bls_aggregation_enabled` CLI flag. + +NOTE: This is implemented mostly as a POC of aggregation in Rundler. Due to the bundle size [limitations](#dynamic-bundle-size) this aggregator has little practical use and is not recommended for production. + diff --git a/docs/cli.md b/docs/cli.md index 688c2c314..96fb92078 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -79,6 +79,8 @@ See [chain spec](./architecture/chain_spec.md) for a detailed description of cha - env: *DA_GAS_TRACKING_ENABLED* - `--max_expected_storage_slots`: Optionally set the maximum number of expected storage slots to submit with a conditional transaction. (default: `None`) - env: *MAX_EXPECTED_STORAGE_SLOTS* +- `--bls_aggregation_enabled`: Enable BLS signature aggregation (default: `false`) + - env: *BLS_AGGREGATION_ENABLED* ## Metrics Options