Skip to content

Commit 084dff3

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 8543ea2 commit 084dff3

File tree

9 files changed

+1501
-2604
lines changed

9 files changed

+1501
-2604
lines changed

Cargo.lock

Lines changed: 1297 additions & 2583 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 & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,7 @@ reth-rpc-eth-api = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3"
6060
reth-optimism-primitives = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
6161
reth-rpc-convert = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
6262
reth-optimism-rpc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
63+
reth-optimism-payload-builder = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
6364
reth-optimism-evm = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
6465
reth-optimism-chainspec = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
6566
reth-provider = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
@@ -76,6 +77,7 @@ reth-db-common = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
7677
reth-rpc-layer = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
7778
reth-ipc = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
7879
reth-node-core = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
80+
reth-rpc-server-types = { git = "https://github.com/paradigmxyz/reth", tag = "v1.9.3" }
7981

8082
# revm
8183
revm = { version = "31.0.2", default-features = false }
@@ -143,6 +145,5 @@ arc-swap = "1.7.1"
143145
once_cell = "1.19"
144146
rand = "0.9.2"
145147
httpmock = "0.8.2"
146-
tracing-subscriber = "0.3"
147148
parking_lot = "0.12.3"
148149
indexmap = "2.7.0"

crates/metering/Cargo.toml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ reth-primitives-traits.workspace = true
2222
reth-evm.workspace = true
2323
reth-optimism-evm.workspace = true
2424
reth-optimism-chainspec.workspace = true
25+
reth-optimism-payload-builder.workspace = true
2526
reth-optimism-primitives.workspace = true
2627
reth-transaction-pool.workspace = true
2728
reth-optimism-cli.workspace = true # Enables serde & codec traits for OpReceipt/OpTxEnvelope
@@ -39,6 +40,7 @@ op-alloy-flz.workspace = true
3940
jsonrpsee.workspace = true
4041

4142
# misc
43+
serde.workspace = true
4244
tracing.workspace = true
4345
eyre.workspace = true
4446
indexmap.workspace = true
@@ -57,6 +59,9 @@ reth-db = { workspace = true, features = ["test-utils"] }
5759
reth-db-common.workspace = true
5860
reth-e2e-test-utils.workspace = true
5961
reth-optimism-node.workspace = true
62+
reth-optimism-payload-builder.workspace = true
63+
reth-optimism-rpc.workspace = true
64+
reth-rpc-server-types.workspace = true
6065
reth-testing-utils.workspace = true
6166
reth-tracing.workspace = true
6267
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/rpc.rs

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -149,9 +149,9 @@ where
149149
})?;
150150

151151
let bundle_gas_price = if total_gas_used > 0 {
152-
(total_gas_fees / U256::from(total_gas_used)).to_string()
152+
total_gas_fees / U256::from(total_gas_used)
153153
} else {
154-
"0".to_string()
154+
U256::from(0)
155155
};
156156

157157
info!(
@@ -165,9 +165,9 @@ where
165165
let response = MeterBundleResponse {
166166
bundle_gas_price,
167167
bundle_hash,
168-
coinbase_diff: total_gas_fees.to_string(),
169-
eth_sent_to_coinbase: "0".to_string(),
170-
gas_fees: total_gas_fees.to_string(),
168+
coinbase_diff: total_gas_fees,
169+
eth_sent_to_coinbase: U256::from(0),
170+
gas_fees: total_gas_fees,
171171
results,
172172
state_block_number: header.number,
173173
state_flashblock_index: None,

crates/metering/src/tests/rpc.rs

Lines changed: 122 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@ mod tests {
33
use std::{any::Any, net::SocketAddr, sync::Arc};
44

55
use crate::{
6-
MeteringApiImpl, MeteringApiServer, MeteringCache, PriorityFeeEstimator, ResourceLimits,
6+
MeteredTransaction, MeteringApiImpl, MeteringApiServer, MeteringCache,
7+
PriorityFeeEstimator, ResourceLimits,
78
};
89

910
const PRIORITY_FEE_PERCENTILE: f64 = 0.5;
@@ -27,14 +28,17 @@ mod tests {
2728
};
2829
use reth_optimism_chainspec::OpChainSpecBuilder;
2930
use reth_optimism_node::{OpNode, args::RollupArgs};
31+
use reth_optimism_payload_builder::config::OpDAConfig;
3032
use reth_optimism_primitives::OpTransactionSigned;
33+
use reth_rpc_server_types::{RethRpcModule, RpcModuleSelection};
3134
use reth_provider::providers::BlockchainProvider;
3235
use reth_transaction_pool::test_utils::TransactionBuilder;
3336
use serde_json;
3437
use tips_core::types::Bundle;
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
}
@@ -84,10 +88,19 @@ mod tests {
8488

8589
let node_config = NodeConfig::new(chain_spec.clone())
8690
.with_network(network_config.clone())
87-
.with_rpc(RpcServerArgs::default().with_unused_ports().with_http())
91+
.with_rpc(
92+
RpcServerArgs::default()
93+
.with_unused_ports()
94+
.with_http()
95+
.with_http_api(RpcModuleSelection::from([RethRpcModule::Miner])),
96+
)
8897
.with_unused_ports();
8998

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

92105
let cache = Arc::new(RwLock::new(MeteringCache::new(12)));
93106
let limits = ResourceLimits {
@@ -101,6 +114,7 @@ mod tests {
101114
PRIORITY_FEE_PERCENTILE,
102115
limits,
103116
U256::from(UNCONGESTED_PRIORITY_FEE),
117+
Some(da_config.clone()),
104118
));
105119
let estimator_for_rpc = estimator.clone();
106120

@@ -125,6 +139,7 @@ mod tests {
125139

126140
Ok(NodeContext {
127141
http_api_addr,
142+
cache,
128143
_node_exit_future: node_exit_future,
129144
_node: Box::new(node),
130145
})
@@ -470,4 +485,108 @@ mod tests {
470485

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

crates/node/Cargo.toml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ base-reth-transaction-status.workspace = true
2222
reth.workspace = true
2323
reth-optimism-node.workspace = true
2424
reth-optimism-cli.workspace = true
25+
reth-rpc-server-types.workspace = true
2526
reth-cli-util.workspace = true
2627

2728
# async
@@ -30,8 +31,16 @@ futures-util.workspace = true
3031
# reth-exex
3132
reth-exex.workspace = true
3233

34+
# alloy
35+
alloy-primitives.workspace = true
36+
37+
# tokio
38+
tokio.workspace = true
39+
3340
# misc
3441
clap.workspace = true
42+
eyre.workspace = true
43+
metrics.workspace = true
3544
tracing.workspace = true
3645
url.workspace = true
3746
once_cell.workspace = true

0 commit comments

Comments
 (0)