Skip to content

Commit 7d9523f

Browse files
committed
Add dynamic DA limit support via miner_setMaxDASize RPC
The priority fee estimator now respects dynamic DA limits set via the miner_setMaxDASize RPC call. This allows operators to adjust DA limits at runtime without restarting the node. Key changes: - PriorityFeeEstimator accepts optional OpDAConfig for dynamic DA limits - When metering is enabled, the miner RPC module is automatically enabled - Shared OpDAConfig is passed to both OpNode (for miner RPC) and estimator - Add integration test that verifies setMaxDASize affects priority fee estimates
1 parent f813d4e commit 7d9523f

File tree

8 files changed

+194
-15
lines changed

8 files changed

+194
-15
lines changed

Cargo.lock

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1"
5656
reth-optimism-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
5757
reth-rpc-convert = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
5858
reth-optimism-rpc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
59+
reth-optimism-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
5960
reth-optimism-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
6061
reth-optimism-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
6162
reth-provider = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
@@ -69,6 +70,7 @@ reth-exex = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
6970
reth-db = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
7071
reth-testing-utils = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
7172
reth-db-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
73+
reth-rpc-server-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.1" }
7274

7375
# revm
7476
revm = { version = "31.0.0", default-features = false }

crates/metering/Cargo.toml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ reth-primitives-traits.workspace = true
2323
reth-evm.workspace = true
2424
reth-optimism-evm.workspace = true
2525
reth-optimism-chainspec.workspace = true
26+
reth-optimism-payload-builder.workspace = true
2627
reth-optimism-primitives.workspace = true
2728
reth-transaction-pool.workspace = true
2829
reth-optimism-cli.workspace = true # Enables serde & codec traits for OpReceipt/OpTxEnvelope
@@ -62,6 +63,9 @@ reth-db = { workspace = true, features = ["test-utils"] }
6263
reth-db-common.workspace = true
6364
reth-e2e-test-utils.workspace = true
6465
reth-optimism-node.workspace = true
66+
reth-optimism-payload-builder.workspace = true
67+
reth-optimism-rpc.workspace = true
68+
reth-rpc-server-types.workspace = true
6569
reth-testing-utils.workspace = true
6670
reth-tracing.workspace = true
6771
reth-transaction-pool = { workspace = true, features = ["test-utils"] }

crates/metering/src/estimator.rs

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
use crate::{MeteredTransaction, MeteringCache};
22
use alloy_primitives::U256;
33
use parking_lot::RwLock;
4+
use reth_optimism_payload_builder::config::OpDAConfig;
45
use std::sync::Arc;
56

67
/// Errors that can occur during priority fee estimation.
@@ -232,6 +233,10 @@ pub struct PriorityFeeEstimator {
232233
percentile: f64,
233234
limits: ResourceLimits,
234235
default_priority_fee: U256,
236+
/// Optional shared DA config from the miner RPC. When set, the estimator uses
237+
/// `max_da_block_size` from this config instead of `limits.data_availability_bytes`.
238+
/// This allows dynamic updates via `miner_setMaxDASize`.
239+
da_config: Option<OpDAConfig>,
235240
}
236241

237242
impl PriorityFeeEstimator {
@@ -243,17 +248,37 @@ impl PriorityFeeEstimator {
243248
/// to use for the recommended fee.
244249
/// - `limits`: Configured resource capacity limits.
245250
/// - `default_priority_fee`: Fee to return when a resource is not congested.
251+
/// - `da_config`: Optional shared DA config for dynamic DA limit updates.
246252
pub fn new(
247253
cache: Arc<RwLock<MeteringCache>>,
248254
percentile: f64,
249255
limits: ResourceLimits,
250256
default_priority_fee: U256,
257+
da_config: Option<OpDAConfig>,
251258
) -> Self {
252259
Self {
253260
cache,
254261
percentile,
255262
limits,
256263
default_priority_fee,
264+
da_config,
265+
}
266+
}
267+
268+
/// Returns the current DA block size limit, preferring the dynamic `OpDAConfig` value
269+
/// if available, otherwise falling back to the static limit.
270+
pub fn max_da_block_size(&self) -> Option<u64> {
271+
self.da_config
272+
.as_ref()
273+
.and_then(|c| c.max_da_block_size())
274+
.or(self.limits.data_availability_bytes)
275+
}
276+
277+
/// Returns the limit for the given resource kind, using dynamic config where available.
278+
fn limit_for(&self, resource: ResourceKind) -> Option<u128> {
279+
match resource {
280+
ResourceKind::DataAvailability => self.max_da_block_size().map(|v| v as u128),
281+
_ => self.limits.limit_for(resource),
257282
}
258283
}
259284

@@ -321,7 +346,7 @@ impl PriorityFeeEstimator {
321346
let Some(demand_value) = demand.demand_for(resource) else {
322347
continue;
323348
};
324-
let Some(limit_value) = self.limits.limit_for(resource) else {
349+
let Some(limit_value) = self.limit_for(resource) else {
325350
continue;
326351
};
327352

@@ -807,7 +832,7 @@ mod tests {
807832
limits: ResourceLimits,
808833
) -> (Arc<RwLock<MeteringCache>>, PriorityFeeEstimator) {
809834
let cache = Arc::new(RwLock::new(MeteringCache::new(4)));
810-
let estimator = PriorityFeeEstimator::new(cache.clone(), 0.5, limits, DEFAULT_FEE);
835+
let estimator = PriorityFeeEstimator::new(cache.clone(), 0.5, limits, DEFAULT_FEE, None);
811836
(cache, estimator)
812837
}
813838

crates/metering/src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ pub use estimator::{
1818
};
1919
pub use kafka::{KafkaBundleConsumer, KafkaBundleConsumerConfig};
2020
pub use meter::meter_bundle;
21+
pub use reth_optimism_payload_builder::config::OpDAConfig;
2122
pub use rpc::{
2223
MeteredPriorityFeeResponse, MeteringApiImpl, MeteringApiServer, ResourceFeeEstimateResponse,
2324
};

crates/metering/src/tests/rpc.rs

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
#[cfg(test)]
22
mod tests {
33
use crate::{
4-
MeteringApiImpl, MeteringApiServer, MeteringCache, PriorityFeeEstimator, ResourceLimits,
4+
MeteredTransaction, MeteringApiImpl, MeteringApiServer, MeteringCache,
5+
PriorityFeeEstimator, ResourceLimits,
56
};
67

78
const PRIORITY_FEE_PERCENTILE: f64 = 0.5;
@@ -21,9 +22,11 @@ mod tests {
2122
use reth::chainspec::Chain;
2223
use reth::core::exit::NodeExitFuture;
2324
use reth::tasks::TaskManager;
25+
use reth_rpc_server_types::{RethRpcModule, RpcModuleSelection};
2426
use reth_optimism_chainspec::OpChainSpecBuilder;
2527
use reth_optimism_node::OpNode;
2628
use reth_optimism_node::args::RollupArgs;
29+
use reth_optimism_payload_builder::config::OpDAConfig;
2730
use reth_optimism_primitives::OpTransactionSigned;
2831
use reth_provider::providers::BlockchainProvider;
2932
use reth_transaction_pool::test_utils::TransactionBuilder;
@@ -35,6 +38,7 @@ mod tests {
3538

3639
pub struct NodeContext {
3740
http_api_addr: SocketAddr,
41+
cache: Arc<RwLock<MeteringCache>>,
3842
_node_exit_future: NodeExitFuture,
3943
_node: Box<dyn Any + Sync + Send>,
4044
}
@@ -86,10 +90,19 @@ mod tests {
8690

8791
let node_config = NodeConfig::new(chain_spec.clone())
8892
.with_network(network_config.clone())
89-
.with_rpc(RpcServerArgs::default().with_unused_ports().with_http())
93+
.with_rpc(
94+
RpcServerArgs::default()
95+
.with_unused_ports()
96+
.with_http()
97+
.with_http_api(RpcModuleSelection::from([RethRpcModule::Miner])),
98+
)
9099
.with_unused_ports();
91100

92-
let node = OpNode::new(RollupArgs::default());
101+
// Create shared DA config that will be used by both the miner RPC and the estimator.
102+
// When miner_setMaxDASize is called, the OpDAConfig is updated atomically and
103+
// the estimator will see the new limits.
104+
let da_config = OpDAConfig::default();
105+
let node = OpNode::new(RollupArgs::default()).with_da_config(da_config.clone());
93106

94107
let cache = Arc::new(RwLock::new(MeteringCache::new(12)));
95108
let limits = ResourceLimits {
@@ -103,6 +116,7 @@ mod tests {
103116
PRIORITY_FEE_PERCENTILE,
104117
limits,
105118
U256::from(UNCONGESTED_PRIORITY_FEE),
119+
Some(da_config.clone()),
106120
));
107121
let estimator_for_rpc = estimator.clone();
108122

@@ -130,6 +144,7 @@ mod tests {
130144

131145
Ok(NodeContext {
132146
http_api_addr,
147+
cache,
133148
_node_exit_future: node_exit_future,
134149
_node: Box::new(node),
135150
})
@@ -487,4 +502,108 @@ mod tests {
487502

488503
Ok(())
489504
}
505+
506+
/// Creates a test transaction with specified priority fee and DA bytes.
507+
fn test_tx(priority_fee: u64, da_bytes: u64) -> MeteredTransaction {
508+
let mut hash_bytes = [0u8; 32];
509+
hash_bytes[24..].copy_from_slice(&priority_fee.to_be_bytes());
510+
MeteredTransaction {
511+
tx_hash: alloy_primitives::B256::new(hash_bytes),
512+
priority_fee_per_gas: U256::from(priority_fee),
513+
gas_used: 21_000,
514+
execution_time_us: 100,
515+
state_root_time_us: 0,
516+
data_availability_bytes: da_bytes,
517+
}
518+
}
519+
520+
#[tokio::test]
521+
async fn test_set_max_da_size_updates_priority_fee_estimates() -> eyre::Result<()> {
522+
reth_tracing::init_test_tracing();
523+
let node = setup_node().await?;
524+
let client = node.rpc_client().await?;
525+
526+
// Create a transaction to include in the bundle for DA demand calculation.
527+
// Use a funded account from genesis.json (Hardhat account #0).
528+
let sender_secret =
529+
b256!("0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80");
530+
531+
let tx = TransactionBuilder::default()
532+
.signer(sender_secret)
533+
.chain_id(84532)
534+
.nonce(0)
535+
.to(address!("0x1111111111111111111111111111111111111111"))
536+
.value(1000)
537+
.gas_limit(21_000)
538+
.max_fee_per_gas(1_000_000_000)
539+
.max_priority_fee_per_gas(1_000_000_000)
540+
.into_eip1559();
541+
542+
let signed_tx =
543+
OpTransactionSigned::Eip1559(tx.as_eip1559().expect("eip1559 transaction").clone());
544+
let envelope: OpTxEnvelope = signed_tx.into();
545+
let tx_bytes = Bytes::from(envelope.encoded_2718());
546+
547+
// Populate the cache with test transactions that have known DA bytes.
548+
// We'll create a scenario where DA is the constraining resource:
549+
// - tx1: priority=100, DA=50 bytes
550+
// - tx2: priority=50, DA=50 bytes
551+
// - tx3: priority=10, DA=50 bytes
552+
// Total DA used by existing transactions = 150 bytes
553+
{
554+
let mut cache = node.cache.write();
555+
cache.upsert_transaction(1, 0, test_tx(100, 50));
556+
cache.upsert_transaction(1, 0, test_tx(50, 50));
557+
cache.upsert_transaction(1, 0, test_tx(10, 50));
558+
}
559+
560+
// Bundle with our transaction - it will have some DA demand from the tx bytes.
561+
let bundle = create_bundle(vec![tx_bytes], 0, None);
562+
563+
// With default DA limit (120KB = 120_000 bytes), there's plenty of room.
564+
// All transactions fit (150 bytes used) plus our bundle's demand.
565+
// This should return the uncongested default fee (1 wei).
566+
//
567+
// Note: We use serde_json::Value because alloy_rpc_client can't deserialize u128 fields
568+
// when they're nested in flattened structs. The response contains totalExecutionTimeUs
569+
// as u128 which causes deserialization issues.
570+
let response: serde_json::Value = client
571+
.request("base_meteredPriorityFeePerGas", (bundle.clone(),))
572+
.await?;
573+
574+
let fee_before = response["recommendedPriorityFee"]
575+
.as_str()
576+
.expect("recommendedPriorityFee should be a string");
577+
assert_eq!(
578+
fee_before, "1",
579+
"with large DA limit, resource should be uncongested"
580+
);
581+
582+
// Now reduce the DA limit to 200 bytes via miner_setMaxDASize.
583+
// With 200 byte limit and ~100 byte bundle demand, only ~100 bytes available for others.
584+
// tx1 (50) + tx2 (50) = 100 bytes fits, but adding tx3 (50) = 150 bytes exceeds capacity.
585+
// So tx3 gets displaced. Threshold fee = tx2's fee = 50.
586+
let result: bool = client.request("miner_setMaxDASize", (1000, 200)).await?;
587+
assert!(result, "miner_setMaxDASize should succeed");
588+
589+
// Request priority fee again - now DA should be congested.
590+
let response: serde_json::Value = client
591+
.request("base_meteredPriorityFeePerGas", (bundle,))
592+
.await?;
593+
594+
// With the reduced limit, we should see a higher recommended fee.
595+
// The exact value depends on the percentile calculation, but it should
596+
// be significantly higher than the uncongested fee of 1.
597+
let fee_after = response["recommendedPriorityFee"]
598+
.as_str()
599+
.expect("recommendedPriorityFee should be a string");
600+
let fee: u64 = fee_after.parse().expect("valid u64");
601+
assert!(
602+
fee > 1,
603+
"with reduced DA limit, recommended fee should be higher than uncongested fee (1), got {}",
604+
fee
605+
);
606+
607+
Ok(())
608+
}
490609
}

crates/node/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ reth-rpc-convert.workspace = true
2828
reth-optimism-rpc.workspace = true
2929
reth-optimism-evm.workspace = true
3030
reth-optimism-chainspec.workspace = true
31+
reth-rpc-server-types.workspace = true
3132
reth-cli-util.workspace = true
3233

3334
# revm

0 commit comments

Comments
 (0)