Skip to main content

reth_node_core/args/
engine.rs

1//! clap [Args](clap::Args) for engine purposes
2
3use clap::{
4    builder::{RangedU64ValueParser, Resettable},
5    Args,
6};
7use eyre::ensure;
8use reth_cli_util::{parse_duration_from_secs_or_ms, parsers::format_duration_as_secs_or_ms};
9use reth_engine_primitives::{
10    TreeConfig, DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD, DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE,
11    DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD, DEFAULT_SPARSE_TRIE_MAX_HOT_ACCOUNTS,
12    DEFAULT_SPARSE_TRIE_MAX_HOT_SLOTS,
13};
14use std::{sync::OnceLock, time::Duration};
15
16use crate::node_config::{
17    DEFAULT_CROSS_BLOCK_CACHE_SIZE_MB, DEFAULT_MEMORY_BLOCK_BUFFER_TARGET,
18    DEFAULT_PERSISTENCE_THRESHOLD, DEFAULT_RESERVED_CPU_CORES,
19};
20
21/// Global static engine defaults
22static ENGINE_DEFAULTS: OnceLock<DefaultEngineValues> = OnceLock::new();
23
24/// Default values for engine that can be customized
25///
26/// Global defaults can be set via [`DefaultEngineValues::try_init`].
27#[derive(Debug, Clone)]
28pub struct DefaultEngineValues {
29    persistence_threshold: u64,
30    persistence_backpressure_threshold: u64,
31    memory_block_buffer_target: u64,
32    invalid_header_hit_eviction_threshold: u8,
33    legacy_state_root_task_enabled: bool,
34    state_cache_disabled: bool,
35    prewarming_disabled: bool,
36    state_provider_metrics: bool,
37    cross_block_cache_size: usize,
38    state_root_task_compare_updates: bool,
39    accept_execution_requests_hash: bool,
40    multiproof_chunk_size: usize,
41    reserved_cpu_cores: usize,
42    precompile_cache_disabled: bool,
43    state_root_fallback: bool,
44    always_process_payload_attributes_on_canonical_head: bool,
45    allow_unwind_canonical_header: bool,
46    storage_worker_count: Option<usize>,
47    account_worker_count: Option<usize>,
48    prewarming_threads: Option<usize>,
49    cache_metrics_disabled: bool,
50    sparse_trie_max_hot_slots: usize,
51    sparse_trie_max_hot_accounts: usize,
52    slow_block_threshold: Option<Duration>,
53    disable_sparse_trie_cache_pruning: bool,
54    state_root_task_timeout: Option<String>,
55    share_execution_cache_with_payload_builder: bool,
56    share_sparse_trie_with_payload_builder: bool,
57    suppress_persistence_during_build: bool,
58    bal_parallel_execution_disabled: bool,
59    bal_parallel_state_root_disabled: bool,
60}
61
62impl DefaultEngineValues {
63    /// Initialize the global engine defaults with this configuration
64    pub fn try_init(self) -> Result<(), Self> {
65        ENGINE_DEFAULTS.set(self)
66    }
67
68    /// Get a reference to the global engine defaults
69    pub fn get_global() -> &'static Self {
70        ENGINE_DEFAULTS.get_or_init(Self::default)
71    }
72
73    /// Set the default persistence threshold
74    pub const fn with_persistence_threshold(mut self, v: u64) -> Self {
75        self.persistence_threshold = v;
76        self
77    }
78
79    /// Set the default persistence backpressure threshold
80    pub const fn with_persistence_backpressure_threshold(mut self, v: u64) -> Self {
81        self.persistence_backpressure_threshold = v;
82        self
83    }
84
85    /// Set the default memory block buffer target
86    pub const fn with_memory_block_buffer_target(mut self, v: u64) -> Self {
87        self.memory_block_buffer_target = v;
88        self
89    }
90
91    /// Set the invalid header cache hit eviction threshold
92    pub const fn with_invalid_header_hit_eviction_threshold(mut self, v: u8) -> Self {
93        self.invalid_header_hit_eviction_threshold = v;
94        self
95    }
96
97    /// Set whether to enable legacy state root task by default
98    pub const fn with_legacy_state_root_task_enabled(mut self, v: bool) -> Self {
99        self.legacy_state_root_task_enabled = v;
100        self
101    }
102
103    /// Set whether to disable state cache by default
104    pub const fn with_state_cache_disabled(mut self, v: bool) -> Self {
105        self.state_cache_disabled = v;
106        self
107    }
108
109    /// Set whether to disable prewarming by default
110    pub const fn with_prewarming_disabled(mut self, v: bool) -> Self {
111        self.prewarming_disabled = v;
112        self
113    }
114
115    /// Set whether to enable state provider metrics by default
116    pub const fn with_state_provider_metrics(mut self, v: bool) -> Self {
117        self.state_provider_metrics = v;
118        self
119    }
120
121    /// Set the default cross-block cache size in MB
122    pub const fn with_cross_block_cache_size(mut self, v: usize) -> Self {
123        self.cross_block_cache_size = v;
124        self
125    }
126
127    /// Set whether to compare state root task updates by default
128    pub const fn with_state_root_task_compare_updates(mut self, v: bool) -> Self {
129        self.state_root_task_compare_updates = v;
130        self
131    }
132
133    /// Set whether to accept execution requests hash by default
134    pub const fn with_accept_execution_requests_hash(mut self, v: bool) -> Self {
135        self.accept_execution_requests_hash = v;
136        self
137    }
138
139    /// Set the default multiproof chunk size
140    pub const fn with_multiproof_chunk_size(mut self, v: usize) -> Self {
141        self.multiproof_chunk_size = v;
142        self
143    }
144
145    /// Set the default number of reserved CPU cores
146    pub const fn with_reserved_cpu_cores(mut self, v: usize) -> Self {
147        self.reserved_cpu_cores = v;
148        self
149    }
150
151    /// Set whether to disable precompile cache by default
152    pub const fn with_precompile_cache_disabled(mut self, v: bool) -> Self {
153        self.precompile_cache_disabled = v;
154        self
155    }
156
157    /// Set whether to enable state root fallback by default
158    pub const fn with_state_root_fallback(mut self, v: bool) -> Self {
159        self.state_root_fallback = v;
160        self
161    }
162
163    /// Set whether to always process payload attributes on canonical head by default
164    pub const fn with_always_process_payload_attributes_on_canonical_head(
165        mut self,
166        v: bool,
167    ) -> Self {
168        self.always_process_payload_attributes_on_canonical_head = v;
169        self
170    }
171
172    /// Set whether to allow unwinding canonical header by default
173    pub const fn with_allow_unwind_canonical_header(mut self, v: bool) -> Self {
174        self.allow_unwind_canonical_header = v;
175        self
176    }
177
178    /// Set the default storage worker count
179    pub const fn with_storage_worker_count(mut self, v: Option<usize>) -> Self {
180        self.storage_worker_count = v;
181        self
182    }
183
184    /// Set the default account worker count
185    pub const fn with_account_worker_count(mut self, v: Option<usize>) -> Self {
186        self.account_worker_count = v;
187        self
188    }
189
190    /// Set the default prewarming thread count
191    pub const fn with_prewarming_threads(mut self, v: Option<usize>) -> Self {
192        self.prewarming_threads = v;
193        self
194    }
195
196    /// Set whether to disable cache metrics by default
197    pub const fn with_cache_metrics_disabled(mut self, v: bool) -> Self {
198        self.cache_metrics_disabled = v;
199        self
200    }
201
202    /// Set the LFU hot-slot capacity for sparse trie pruning by default
203    pub const fn with_sparse_trie_max_hot_slots(mut self, v: usize) -> Self {
204        self.sparse_trie_max_hot_slots = v;
205        self
206    }
207
208    /// Set the LFU hot-account capacity for sparse trie pruning by default
209    pub const fn with_sparse_trie_max_hot_accounts(mut self, v: usize) -> Self {
210        self.sparse_trie_max_hot_accounts = v;
211        self
212    }
213
214    /// Set the default slow block threshold.
215    pub const fn with_slow_block_threshold(mut self, v: Option<Duration>) -> Self {
216        self.slow_block_threshold = v;
217        self
218    }
219
220    /// Set whether to disable sparse trie cache pruning by default
221    pub const fn with_disable_sparse_trie_cache_pruning(mut self, v: bool) -> Self {
222        self.disable_sparse_trie_cache_pruning = v;
223        self
224    }
225
226    /// Set the default state root task timeout
227    pub fn with_state_root_task_timeout(mut self, v: Option<String>) -> Self {
228        self.state_root_task_timeout = v;
229        self
230    }
231
232    /// Set whether to share the execution cache with the payload builder by default
233    pub const fn with_share_execution_cache_with_payload_builder(mut self, v: bool) -> Self {
234        self.share_execution_cache_with_payload_builder = v;
235        self
236    }
237
238    /// Set whether to share the sparse trie with the payload builder by default
239    pub const fn with_share_sparse_trie_with_payload_builder(mut self, v: bool) -> Self {
240        self.share_sparse_trie_with_payload_builder = v;
241        self
242    }
243
244    /// Set whether to suppress persistence during payload building by default
245    pub const fn with_suppress_persistence_during_build(mut self, v: bool) -> Self {
246        self.suppress_persistence_during_build = v;
247        self
248    }
249
250    /// Set whether to disable BAL-based parallel execution by default
251    pub const fn with_bal_parallel_execution_disabled(mut self, v: bool) -> Self {
252        self.bal_parallel_execution_disabled = v;
253        self
254    }
255
256    /// Set whether to disable BAL-driven parallel state root by default
257    pub const fn with_bal_parallel_state_root_disabled(mut self, v: bool) -> Self {
258        self.bal_parallel_state_root_disabled = v;
259        self
260    }
261}
262
263impl Default for DefaultEngineValues {
264    fn default() -> Self {
265        Self {
266            persistence_threshold: DEFAULT_PERSISTENCE_THRESHOLD,
267            persistence_backpressure_threshold: DEFAULT_PERSISTENCE_BACKPRESSURE_THRESHOLD,
268            memory_block_buffer_target: DEFAULT_MEMORY_BLOCK_BUFFER_TARGET,
269            invalid_header_hit_eviction_threshold: DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD,
270            legacy_state_root_task_enabled: false,
271            state_cache_disabled: false,
272            prewarming_disabled: false,
273            state_provider_metrics: false,
274            cross_block_cache_size: DEFAULT_CROSS_BLOCK_CACHE_SIZE_MB,
275            state_root_task_compare_updates: false,
276            accept_execution_requests_hash: false,
277            multiproof_chunk_size: DEFAULT_MULTIPROOF_TASK_CHUNK_SIZE,
278            reserved_cpu_cores: DEFAULT_RESERVED_CPU_CORES,
279            precompile_cache_disabled: false,
280            state_root_fallback: false,
281            always_process_payload_attributes_on_canonical_head: false,
282            allow_unwind_canonical_header: false,
283            storage_worker_count: None,
284            account_worker_count: None,
285            prewarming_threads: None,
286            cache_metrics_disabled: false,
287            sparse_trie_max_hot_slots: DEFAULT_SPARSE_TRIE_MAX_HOT_SLOTS,
288            sparse_trie_max_hot_accounts: DEFAULT_SPARSE_TRIE_MAX_HOT_ACCOUNTS,
289            slow_block_threshold: None,
290            disable_sparse_trie_cache_pruning: false,
291            state_root_task_timeout: Some("4s".to_string()),
292            share_execution_cache_with_payload_builder: false,
293            share_sparse_trie_with_payload_builder: false,
294            suppress_persistence_during_build: false,
295            bal_parallel_execution_disabled: false,
296            bal_parallel_state_root_disabled: false,
297        }
298    }
299}
300
301fn default_persistence_backpressure_threshold(persistence_threshold: u64) -> u64 {
302    DefaultEngineValues::get_global()
303        .persistence_backpressure_threshold
304        .max(persistence_threshold.saturating_mul(2))
305}
306
307/// Parameters for configuring the engine driver.
308#[derive(Debug, Clone, Args, PartialEq, Eq)]
309#[command(next_help_heading = "Engine")]
310pub struct EngineArgs {
311    /// Configure persistence threshold for the engine. This determines how many canonical blocks
312    /// must be in-memory, ahead of the last persisted block, before flushing canonical blocks to
313    /// disk again.
314    ///
315    /// To persist blocks as fast as the node receives them, set this value to zero. This will
316    /// cause more frequent DB writes.
317    #[arg(long = "engine.persistence-threshold", default_value_t = DefaultEngineValues::get_global().persistence_threshold)]
318    pub persistence_threshold: u64,
319
320    /// Configure the maximum canonical-minus-persisted gap before engine API processing stalls.
321    ///
322    /// If omitted, this defaults to the larger of the default backpressure threshold and twice
323    /// `--engine.persistence-threshold`.
324    ///
325    /// This value must be greater than `--engine.persistence-threshold`.
326    #[arg(long = "engine.persistence-backpressure-threshold")]
327    pub persistence_backpressure_threshold: Option<u64>,
328
329    /// Configure the target number of blocks to keep in memory.
330    #[arg(long = "engine.memory-block-buffer-target", default_value_t = DefaultEngineValues::get_global().memory_block_buffer_target)]
331    pub memory_block_buffer_target: u64,
332
333    /// Configure how many cache hits an invalid header can accumulate before it is evicted and
334    /// reprocessed.
335    ///
336    /// Set to `0` to effectively disable the cache because entries are evicted on the first
337    /// lookup.
338    #[arg(long = "engine.invalid-header-cache-hit-eviction-threshold", default_value_t = DefaultEngineValues::get_global().invalid_header_hit_eviction_threshold)]
339    pub invalid_header_hit_eviction_threshold: u8,
340
341    /// Enable legacy state root
342    #[arg(long = "engine.legacy-state-root", default_value_t = DefaultEngineValues::get_global().legacy_state_root_task_enabled)]
343    pub legacy_state_root_task_enabled: bool,
344
345    /// CAUTION: This CLI flag has no effect anymore, use --engine.disable-caching-and-prewarming
346    /// if you want to disable caching and prewarming
347    #[arg(long = "engine.caching-and-prewarming", default_value = "true", hide = true)]
348    #[deprecated]
349    pub caching_and_prewarming_enabled: bool,
350
351    /// Disable state cache
352    #[arg(long = "engine.disable-state-cache", default_value_t = DefaultEngineValues::get_global().state_cache_disabled)]
353    pub state_cache_disabled: bool,
354
355    /// Disable parallel prewarming
356    #[arg(long = "engine.disable-prewarming", alias = "engine.disable-caching-and-prewarming", default_value_t = DefaultEngineValues::get_global().prewarming_disabled)]
357    pub prewarming_disabled: bool,
358
359    /// CAUTION: This CLI flag has no effect anymore. The parallel sparse trie is always enabled.
360    #[deprecated]
361    #[arg(long = "engine.parallel-sparse-trie", default_value = "true", hide = true)]
362    pub parallel_sparse_trie_enabled: bool,
363
364    /// CAUTION: This CLI flag has no effect anymore. The parallel sparse trie is always enabled.
365    #[deprecated]
366    #[arg(long = "engine.disable-parallel-sparse-trie", default_value = "false", hide = true)]
367    pub parallel_sparse_trie_disabled: bool,
368
369    /// Enable state provider latency metrics. This allows the engine to collect and report stats
370    /// about how long state provider calls took during execution, but this does introduce slight
371    /// overhead to state provider calls.
372    #[arg(long = "engine.state-provider-metrics", default_value_t = DefaultEngineValues::get_global().state_provider_metrics)]
373    pub state_provider_metrics: bool,
374
375    /// Configure the size of cross-block cache in megabytes
376    #[arg(long = "engine.cross-block-cache-size", default_value_t = DefaultEngineValues::get_global().cross_block_cache_size)]
377    pub cross_block_cache_size: usize,
378
379    /// Enable comparing trie updates from the state root task to the trie updates from the regular
380    /// state root calculation.
381    #[arg(long = "engine.state-root-task-compare-updates", default_value_t = DefaultEngineValues::get_global().state_root_task_compare_updates)]
382    pub state_root_task_compare_updates: bool,
383
384    /// Enables accepting requests hash instead of an array of requests in `engine_newPayloadV4`.
385    #[arg(long = "engine.accept-execution-requests-hash", default_value_t = DefaultEngineValues::get_global().accept_execution_requests_hash)]
386    pub accept_execution_requests_hash: bool,
387
388    /// Multiproof task chunk size for proof targets.
389    #[arg(long = "engine.multiproof-chunk-size", default_value_t = DefaultEngineValues::get_global().multiproof_chunk_size, value_parser = RangedU64ValueParser::<usize>::new().range(1..))]
390    pub multiproof_chunk_size: usize,
391
392    /// Configure the number of reserved CPU cores for non-reth processes
393    #[arg(long = "engine.reserved-cpu-cores", default_value_t = DefaultEngineValues::get_global().reserved_cpu_cores)]
394    pub reserved_cpu_cores: usize,
395
396    /// CAUTION: This CLI flag has no effect anymore, use --engine.disable-precompile-cache
397    /// if you want to disable precompile cache
398    #[arg(long = "engine.precompile-cache", default_value = "true", hide = true)]
399    #[deprecated]
400    pub precompile_cache_enabled: bool,
401
402    /// Disable precompile cache
403    #[arg(long = "engine.disable-precompile-cache", default_value_t = DefaultEngineValues::get_global().precompile_cache_disabled)]
404    pub precompile_cache_disabled: bool,
405
406    /// Enable state root fallback, useful for testing
407    #[arg(long = "engine.state-root-fallback", default_value_t = DefaultEngineValues::get_global().state_root_fallback)]
408    pub state_root_fallback: bool,
409
410    /// Always process payload attributes and begin a payload build process even if
411    /// `forkchoiceState.headBlockHash` is already the canonical head or an ancestor. See
412    /// `TreeConfig::always_process_payload_attributes_on_canonical_head` for more details.
413    ///
414    /// Note: This is a no-op on OP Stack.
415    #[arg(
416        long = "engine.always-process-payload-attributes-on-canonical-head",
417        default_value_t = DefaultEngineValues::get_global().always_process_payload_attributes_on_canonical_head
418    )]
419    pub always_process_payload_attributes_on_canonical_head: bool,
420
421    /// Allow unwinding canonical header to ancestor during forkchoice updates.
422    /// See `TreeConfig::unwind_canonical_header` for more details.
423    #[arg(long = "engine.allow-unwind-canonical-header", default_value_t = DefaultEngineValues::get_global().allow_unwind_canonical_header)]
424    pub allow_unwind_canonical_header: bool,
425
426    /// Configure the number of storage proof workers in the Tokio blocking pool.
427    /// If not specified, defaults to 2x available parallelism.
428    #[arg(long = "engine.storage-worker-count", default_value = Resettable::from(DefaultEngineValues::get_global().storage_worker_count.map(|v| v.to_string().into())))]
429    pub storage_worker_count: Option<usize>,
430
431    /// Configure the number of account proof workers in the Tokio blocking pool.
432    /// If not specified, defaults to the same count as storage workers.
433    #[arg(long = "engine.account-worker-count", default_value = Resettable::from(DefaultEngineValues::get_global().account_worker_count.map(|v| v.to_string().into())))]
434    pub account_worker_count: Option<usize>,
435
436    /// Configure the number of prewarming threads.
437    /// If not specified, defaults to available parallelism.
438    #[arg(long = "engine.prewarming-threads", default_value = Resettable::from(DefaultEngineValues::get_global().prewarming_threads.map(|v| v.to_string().into())))]
439    pub prewarming_threads: Option<usize>,
440
441    /// Disable cache metrics recording, which can take up to 50ms with large cached state.
442    #[arg(long = "engine.disable-cache-metrics", default_value_t = DefaultEngineValues::get_global().cache_metrics_disabled)]
443    pub cache_metrics_disabled: bool,
444
445    /// LFU hot-slot capacity: max storage slots retained across sparse trie prune cycles.
446    #[arg(long = "engine.sparse-trie-max-hot-slots", alias = "engine.sparse-trie-max-storage-tries", default_value_t = DefaultEngineValues::get_global().sparse_trie_max_hot_slots)]
447    pub sparse_trie_max_hot_slots: usize,
448
449    /// LFU hot-account capacity: max account addresses retained across sparse trie prune cycles.
450    #[arg(long = "engine.sparse-trie-max-hot-accounts", default_value_t = DefaultEngineValues::get_global().sparse_trie_max_hot_accounts)]
451    pub sparse_trie_max_hot_accounts: usize,
452
453    /// Configure the slow block logging threshold in milliseconds.
454    ///
455    /// When set, blocks that take longer than this threshold to execute will be logged
456    /// with detailed metrics including timing, state operations, and cache statistics.
457    ///
458    /// Set to 0 to log all blocks (useful for debugging/profiling).
459    ///
460    /// When not set, slow block logging is disabled (default).
461    #[arg(long = "engine.slow-block-threshold", value_parser = parse_duration_from_secs_or_ms, value_name = "DURATION", default_value = Resettable::from(DefaultEngineValues::get_global().slow_block_threshold.map(|threshold| format_duration_as_secs_or_ms(threshold).into())))]
462    pub slow_block_threshold: Option<Duration>,
463
464    /// Fully disable sparse trie cache pruning. When set, the cached sparse trie is preserved
465    /// without any node pruning or storage trie eviction between blocks. Useful for benchmarking
466    /// the effects of retaining the full trie cache.
467    #[arg(long = "engine.disable-sparse-trie-cache-pruning", default_value_t = DefaultEngineValues::get_global().disable_sparse_trie_cache_pruning)]
468    pub disable_sparse_trie_cache_pruning: bool,
469
470    /// Configure the timeout for the state root task before spawning a sequential fallback.
471    /// If the state root task takes longer than this, a sequential computation starts in
472    /// parallel and whichever finishes first is used.
473    ///
474    /// --engine.state-root-task-timeout 4s
475    /// --engine.state-root-task-timeout 400ms
476    ///
477    /// Set to 0s to disable.
478    #[arg(
479        long = "engine.state-root-task-timeout",
480        value_parser = humantime::parse_duration,
481        default_value = DefaultEngineValues::get_global().state_root_task_timeout.as_deref().unwrap_or("4s"),
482    )]
483    pub state_root_task_timeout: Option<Duration>,
484
485    /// Whether to share execution cache with the payload builder.
486    ///
487    /// When enabled, each payload job will get an instance of cross-block execution cache from the
488    /// engine.
489    ///
490    /// Note: this should only be enabled if node would not be requested to process any payloads in
491    /// parallel with payload building.
492    #[arg(
493        long = "engine.share-execution-cache-with-payload-builder",
494        default_value_t = DefaultEngineValues::get_global().share_execution_cache_with_payload_builder,
495    )]
496    pub share_execution_cache_with_payload_builder: bool,
497
498    /// Whether to share the sparse trie with the payload builder.
499    ///
500    /// Replaces the payload builder's blocking `state_root_with_updates()` call with the
501    /// sparse trie, computing the state root concurrently with transaction execution.
502    ///
503    /// The engine and payload builder contend for the same trie — if a builder task is
504    /// still running when `newPayload` arrives, the engine will block until the trie is
505    /// stored back.
506    ///
507    /// The builder also anchors the trie at the built block's state root, so if the next
508    /// `newPayload` is not on top of that block, the trie cache is invalidated and cleared.
509    #[arg(
510        long = "engine.share-sparse-trie-with-payload-builder",
511        default_value_t = DefaultEngineValues::get_global().share_sparse_trie_with_payload_builder,
512    )]
513    pub share_sparse_trie_with_payload_builder: bool,
514
515    /// Suppress persistence while building a payload.
516    ///
517    /// When enabled, persistence cycles are deferred from the moment an FCU with payload
518    /// attributes arrives until the next FCU clears the build. Useful on chains with short
519    /// block times where persistence I/O can interfere with block building latency.
520    #[arg(
521        long = "engine.suppress-persistence-during-build",
522        default_value_t = DefaultEngineValues::get_global().suppress_persistence_during_build,
523    )]
524    pub suppress_persistence_during_build: bool,
525
526    /// Disable BAL (Block Access List, EIP-7928) based parallel execution.
527    #[arg(long = "engine.disable-bal-parallel-execution", default_value_t = DefaultEngineValues::get_global().bal_parallel_execution_disabled)]
528    pub bal_parallel_execution_disabled: bool,
529
530    /// Disable BAL-driven parallel state root computation. This is only valid together with
531    /// `--engine.disable-bal-parallel-execution`.
532    #[arg(long = "engine.disable-bal-parallel-state-root", default_value_t = DefaultEngineValues::get_global().bal_parallel_state_root_disabled)]
533    pub bal_parallel_state_root_disabled: bool,
534
535    /// Disable BAL (Block Access List) storage prefetch IO during prewarming. When set, BAL
536    /// storage slots are not read into the execution cache.
537    #[arg(long = "engine.disable-bal-batch-io", default_value_t = false)]
538    pub disable_bal_batch_io: bool,
539
540    /// Add random jitter before each proof computation (trie-debug only).
541    /// Each proof worker sleeps for a random duration up to this value before
542    /// starting work. Useful for stress-testing timing-sensitive proof logic.
543    ///
544    /// --engine.proof-jitter 100ms
545    /// --engine.proof-jitter 1s
546    #[cfg(feature = "trie-debug")]
547    #[arg(
548        long = "engine.proof-jitter",
549        value_parser = humantime::parse_duration,
550    )]
551    pub proof_jitter: Option<Duration>,
552}
553
554#[allow(deprecated)]
555impl Default for EngineArgs {
556    fn default() -> Self {
557        let DefaultEngineValues {
558            persistence_threshold,
559            persistence_backpressure_threshold: _,
560            memory_block_buffer_target,
561            invalid_header_hit_eviction_threshold,
562            legacy_state_root_task_enabled,
563            state_cache_disabled,
564            prewarming_disabled,
565            state_provider_metrics,
566            cross_block_cache_size,
567            state_root_task_compare_updates,
568            accept_execution_requests_hash,
569            multiproof_chunk_size,
570            reserved_cpu_cores,
571            precompile_cache_disabled,
572            state_root_fallback,
573            always_process_payload_attributes_on_canonical_head,
574            allow_unwind_canonical_header,
575            storage_worker_count,
576            account_worker_count,
577            prewarming_threads,
578            cache_metrics_disabled,
579            sparse_trie_max_hot_slots,
580            sparse_trie_max_hot_accounts,
581            slow_block_threshold,
582            disable_sparse_trie_cache_pruning,
583            state_root_task_timeout,
584            share_execution_cache_with_payload_builder,
585            share_sparse_trie_with_payload_builder,
586            suppress_persistence_during_build,
587            bal_parallel_execution_disabled,
588            bal_parallel_state_root_disabled,
589        } = DefaultEngineValues::get_global().clone();
590        Self {
591            persistence_threshold,
592            persistence_backpressure_threshold: None,
593            memory_block_buffer_target,
594            invalid_header_hit_eviction_threshold,
595            legacy_state_root_task_enabled,
596            state_root_task_compare_updates,
597            caching_and_prewarming_enabled: true,
598            state_cache_disabled,
599            prewarming_disabled,
600            parallel_sparse_trie_enabled: true,
601            parallel_sparse_trie_disabled: false,
602            state_provider_metrics,
603            cross_block_cache_size,
604            accept_execution_requests_hash,
605            multiproof_chunk_size,
606            reserved_cpu_cores,
607            precompile_cache_enabled: true,
608            precompile_cache_disabled,
609            state_root_fallback,
610            always_process_payload_attributes_on_canonical_head,
611            allow_unwind_canonical_header,
612            storage_worker_count,
613            account_worker_count,
614            prewarming_threads,
615            cache_metrics_disabled,
616            sparse_trie_max_hot_slots,
617            sparse_trie_max_hot_accounts,
618            slow_block_threshold,
619            disable_sparse_trie_cache_pruning,
620            state_root_task_timeout: state_root_task_timeout
621                .as_deref()
622                .map(|s| humantime::parse_duration(s).expect("valid default duration")),
623            share_execution_cache_with_payload_builder,
624            share_sparse_trie_with_payload_builder,
625            suppress_persistence_during_build,
626            bal_parallel_execution_disabled,
627            bal_parallel_state_root_disabled,
628            disable_bal_batch_io: false,
629            #[cfg(feature = "trie-debug")]
630            proof_jitter: None,
631        }
632    }
633}
634
635impl EngineArgs {
636    /// Returns the effective persistence backpressure threshold.
637    pub fn persistence_backpressure_threshold(&self) -> u64 {
638        self.persistence_backpressure_threshold.unwrap_or_else(|| {
639            default_persistence_backpressure_threshold(self.persistence_threshold)
640        })
641    }
642
643    /// Validates cross-field engine arguments.
644    pub fn validate(&self) -> eyre::Result<()> {
645        let persistence_backpressure_threshold = self.persistence_backpressure_threshold();
646        ensure!(
647            persistence_backpressure_threshold > self.persistence_threshold,
648            "--engine.persistence-backpressure-threshold ({}) must be greater than --engine.persistence-threshold ({})",
649            persistence_backpressure_threshold,
650            self.persistence_threshold
651        );
652        ensure!(
653            self.bal_parallel_execution_disabled || !self.bal_parallel_state_root_disabled,
654            "--engine.disable-bal-parallel-state-root requires --engine.disable-bal-parallel-execution because BAL parallel execution depends on BAL prewarm state-root updates"
655        );
656        Ok(())
657    }
658
659    /// Creates a [`TreeConfig`] from the engine arguments.
660    pub fn tree_config(&self) -> TreeConfig {
661        let config = TreeConfig::default()
662            .with_persistence_backpressure_threshold(self.persistence_backpressure_threshold())
663            .with_persistence_threshold(self.persistence_threshold)
664            .with_memory_block_buffer_target(self.memory_block_buffer_target)
665            .with_invalid_header_hit_eviction_threshold(self.invalid_header_hit_eviction_threshold)
666            .with_legacy_state_root(self.legacy_state_root_task_enabled)
667            .without_state_cache(self.state_cache_disabled)
668            .without_prewarming(self.prewarming_disabled)
669            .with_state_provider_metrics(self.state_provider_metrics)
670            .with_always_compare_trie_updates(self.state_root_task_compare_updates)
671            .with_cross_block_cache_size(self.cross_block_cache_size * 1024 * 1024)
672            .with_multiproof_chunk_size(self.multiproof_chunk_size)
673            .with_reserved_cpu_cores(self.reserved_cpu_cores)
674            .without_precompile_cache(self.precompile_cache_disabled)
675            .with_state_root_fallback(self.state_root_fallback)
676            .with_always_process_payload_attributes_on_canonical_head(
677                self.always_process_payload_attributes_on_canonical_head,
678            )
679            .with_unwind_canonical_header(self.allow_unwind_canonical_header)
680            .without_cache_metrics(self.cache_metrics_disabled)
681            .with_sparse_trie_max_hot_slots(self.sparse_trie_max_hot_slots)
682            .with_sparse_trie_max_hot_accounts(self.sparse_trie_max_hot_accounts)
683            .with_slow_block_threshold(self.slow_block_threshold)
684            .with_disable_sparse_trie_cache_pruning(self.disable_sparse_trie_cache_pruning)
685            .with_state_root_task_timeout(self.state_root_task_timeout.filter(|d| !d.is_zero()))
686            .with_share_execution_cache_with_payload_builder(
687                self.share_execution_cache_with_payload_builder,
688            )
689            .with_share_sparse_trie_with_payload_builder(
690                self.share_sparse_trie_with_payload_builder,
691            )
692            .with_suppress_persistence_during_build(self.suppress_persistence_during_build)
693            .without_bal_parallel_execution(self.bal_parallel_execution_disabled)
694            .without_bal_parallel_state_root(self.bal_parallel_state_root_disabled)
695            .without_bal_batch_io(self.disable_bal_batch_io);
696        #[cfg(feature = "trie-debug")]
697        let config = config.with_proof_jitter(self.proof_jitter);
698        config
699    }
700}
701
702#[cfg(test)]
703mod tests {
704    use super::*;
705    use clap::Parser;
706
707    /// A helper type to parse Args more easily
708    #[derive(Parser)]
709    struct CommandParser<T: Args> {
710        #[command(flatten)]
711        args: T,
712    }
713
714    #[test]
715    fn test_parse_engine_args() {
716        let default_args = EngineArgs::default();
717        let args = CommandParser::<EngineArgs>::parse_from(["reth"]).args;
718        assert_eq!(args, default_args);
719        assert_eq!(
720            args.persistence_backpressure_threshold(),
721            DefaultEngineValues::get_global().persistence_backpressure_threshold
722        );
723    }
724
725    #[test]
726    fn default_backpressure_threshold_uses_parsed_persistence_args() {
727        let args = CommandParser::<EngineArgs>::parse_from([
728            "reth",
729            "--engine.persistence-threshold",
730            "100",
731            "--engine.memory-block-buffer-target",
732            "50",
733        ])
734        .args;
735
736        assert_eq!(args.persistence_backpressure_threshold(), 200);
737
738        let tree_config = args.tree_config();
739        assert_eq!(tree_config.persistence_threshold(), 100);
740        assert_eq!(tree_config.memory_block_buffer_target(), 50);
741        assert_eq!(tree_config.persistence_backpressure_threshold(), 200);
742    }
743
744    #[test]
745    fn default_backpressure_threshold_uses_global_default_when_larger() {
746        let args = CommandParser::<EngineArgs>::parse_from([
747            "reth",
748            "--engine.persistence-threshold",
749            "4",
750        ])
751        .args;
752
753        assert_eq!(
754            args.persistence_backpressure_threshold(),
755            DefaultEngineValues::get_global().persistence_backpressure_threshold
756        );
757    }
758
759    #[test]
760    fn explicit_backpressure_threshold_overrides_calculated_default() {
761        let args = CommandParser::<EngineArgs>::parse_from([
762            "reth",
763            "--engine.persistence-threshold",
764            "100",
765            "--engine.memory-block-buffer-target",
766            "50",
767            "--engine.persistence-backpressure-threshold",
768            "101",
769        ])
770        .args;
771
772        assert_eq!(args.persistence_backpressure_threshold(), 101);
773    }
774
775    #[test]
776    #[allow(deprecated)]
777    fn engine_args() {
778        let args = EngineArgs {
779            persistence_threshold: 100,
780            persistence_backpressure_threshold: Some(101),
781            memory_block_buffer_target: 50,
782            invalid_header_hit_eviction_threshold: 7,
783            legacy_state_root_task_enabled: true,
784            caching_and_prewarming_enabled: true,
785            state_cache_disabled: true,
786            prewarming_disabled: true,
787            parallel_sparse_trie_enabled: true,
788            parallel_sparse_trie_disabled: false,
789            state_provider_metrics: true,
790            cross_block_cache_size: 256,
791            state_root_task_compare_updates: true,
792            accept_execution_requests_hash: true,
793            multiproof_chunk_size: 512,
794            reserved_cpu_cores: 4,
795            precompile_cache_enabled: true,
796            precompile_cache_disabled: true,
797            state_root_fallback: true,
798            always_process_payload_attributes_on_canonical_head: true,
799            allow_unwind_canonical_header: true,
800            storage_worker_count: Some(16),
801            account_worker_count: Some(8),
802            prewarming_threads: Some(4),
803            cache_metrics_disabled: true,
804            sparse_trie_max_hot_slots: 100,
805            sparse_trie_max_hot_accounts: 500,
806            slow_block_threshold: None,
807            disable_sparse_trie_cache_pruning: true,
808            state_root_task_timeout: Some(Duration::from_secs(2)),
809            share_execution_cache_with_payload_builder: false,
810            share_sparse_trie_with_payload_builder: false,
811            suppress_persistence_during_build: false,
812            bal_parallel_execution_disabled: true,
813            bal_parallel_state_root_disabled: true,
814            disable_bal_batch_io: true,
815            #[cfg(feature = "trie-debug")]
816            proof_jitter: None,
817        };
818
819        let parsed_args = CommandParser::<EngineArgs>::parse_from([
820            "reth",
821            "--engine.persistence-threshold",
822            "100",
823            "--engine.persistence-backpressure-threshold",
824            "101",
825            "--engine.memory-block-buffer-target",
826            "50",
827            "--engine.invalid-header-cache-hit-eviction-threshold",
828            "7",
829            "--engine.legacy-state-root",
830            "--engine.disable-state-cache",
831            "--engine.disable-prewarming",
832            "--engine.state-provider-metrics",
833            "--engine.cross-block-cache-size",
834            "256",
835            "--engine.state-root-task-compare-updates",
836            "--engine.accept-execution-requests-hash",
837            "--engine.multiproof-chunk-size",
838            "512",
839            "--engine.reserved-cpu-cores",
840            "4",
841            "--engine.disable-precompile-cache",
842            "--engine.state-root-fallback",
843            "--engine.always-process-payload-attributes-on-canonical-head",
844            "--engine.allow-unwind-canonical-header",
845            "--engine.storage-worker-count",
846            "16",
847            "--engine.account-worker-count",
848            "8",
849            "--engine.prewarming-threads",
850            "4",
851            "--engine.disable-cache-metrics",
852            "--engine.sparse-trie-max-hot-slots",
853            "100",
854            "--engine.sparse-trie-max-hot-accounts",
855            "500",
856            "--engine.disable-sparse-trie-cache-pruning",
857            "--engine.state-root-task-timeout",
858            "2s",
859            "--engine.disable-bal-parallel-execution",
860            "--engine.disable-bal-parallel-state-root",
861            "--engine.disable-bal-batch-io",
862        ])
863        .args;
864
865        assert_eq!(parsed_args, args);
866    }
867
868    #[test]
869    fn validate_rejects_invalid_backpressure_threshold() {
870        let args = EngineArgs {
871            persistence_threshold: 4,
872            persistence_backpressure_threshold: Some(4),
873            ..EngineArgs::default()
874        };
875
876        let err = args.validate().unwrap_err().to_string();
877        assert!(err.contains("engine.persistence-backpressure-threshold"));
878        assert!(err.contains("engine.persistence-threshold"));
879    }
880
881    #[test]
882    fn parse_rejects_zero_multiproof_chunk_size() {
883        let result = CommandParser::<EngineArgs>::try_parse_from([
884            "reth",
885            "--engine.multiproof-chunk-size",
886            "0",
887        ]);
888
889        assert!(result.is_err());
890    }
891
892    #[test]
893    fn validate_rejects_bal_parallel_execution_without_bal_parallel_state_root() {
894        let args = EngineArgs {
895            bal_parallel_execution_disabled: false,
896            bal_parallel_state_root_disabled: true,
897            ..EngineArgs::default()
898        };
899
900        let err = args.validate().unwrap_err().to_string();
901        assert!(err.contains("engine.disable-bal-parallel-state-root"));
902        assert!(err.contains("engine.disable-bal-parallel-execution"));
903    }
904
905    #[test]
906    fn test_parse_slow_block_threshold() {
907        // Test default value (None - disabled)
908        let args = CommandParser::<EngineArgs>::parse_from(["reth"]).args;
909        assert_eq!(args.slow_block_threshold, None);
910
911        // Test setting to 0 (log all blocks)
912        let args =
913            CommandParser::<EngineArgs>::parse_from(["reth", "--engine.slow-block-threshold", "0"])
914                .args;
915        assert_eq!(args.slow_block_threshold, Some(Duration::ZERO));
916
917        // Test setting to custom value
918        let args = CommandParser::<EngineArgs>::parse_from([
919            "reth",
920            "--engine.slow-block-threshold",
921            "500",
922        ])
923        .args;
924        assert_eq!(args.slow_block_threshold, Some(Duration::from_secs(500)));
925
926        let args = CommandParser::<EngineArgs>::parse_from([
927            "reth",
928            "--engine.slow-block-threshold",
929            "500ms",
930        ])
931        .args;
932        assert_eq!(args.slow_block_threshold, Some(Duration::from_millis(500)));
933    }
934
935    #[test]
936    fn test_parse_invalid_header_hit_eviction_threshold() {
937        let args = CommandParser::<EngineArgs>::parse_from(["reth"]).args;
938        assert_eq!(
939            args.invalid_header_hit_eviction_threshold,
940            DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD
941        );
942        assert_eq!(
943            args.tree_config().invalid_header_hit_eviction_threshold(),
944            DEFAULT_INVALID_HEADER_HIT_EVICTION_THRESHOLD
945        );
946
947        let args = CommandParser::<EngineArgs>::parse_from([
948            "reth",
949            "--engine.invalid-header-cache-hit-eviction-threshold",
950            "0",
951        ])
952        .args;
953        assert_eq!(args.invalid_header_hit_eviction_threshold, 0);
954        assert_eq!(args.tree_config().invalid_header_hit_eviction_threshold(), 0);
955    }
956
957    #[test]
958    fn test_parse_share_sparse_trie_flag() {
959        let args = CommandParser::<EngineArgs>::parse_from(["reth"]).args;
960        assert!(!args.share_sparse_trie_with_payload_builder);
961        assert!(!args.tree_config().share_sparse_trie_with_payload_builder());
962
963        let args = CommandParser::<EngineArgs>::parse_from([
964            "reth",
965            "--engine.share-sparse-trie-with-payload-builder",
966        ])
967        .args;
968        assert!(args.share_sparse_trie_with_payload_builder);
969        assert!(args.tree_config().share_sparse_trie_with_payload_builder());
970    }
971}