1use crate::{cli::config::PayloadBuilderConfig, version::default_extra_data};
2use alloy_consensus::constants::MAXIMUM_EXTRA_DATA_SIZE;
3use alloy_primitives::Bytes;
4use clap::{
5 builder::{RangedU64ValueParser, TypedValueParser},
6 Arg, Args, Command,
7};
8use reth_cli_util::{
9 parse_duration_from_secs, parse_duration_from_secs_or_ms,
10 parsers::format_duration_as_secs_or_ms,
11};
12use std::{ffi::OsStr, sync::OnceLock, time::Duration};
13
14static PAYLOAD_BUILDER_DEFAULTS: OnceLock<DefaultPayloadBuilderValues> = OnceLock::new();
16
17#[derive(Debug, Clone)]
21pub struct DefaultPayloadBuilderValues {
22 extra_data: String,
24 interval: String,
26 deadline: String,
28 max_payload_tasks: usize,
30}
31
32impl DefaultPayloadBuilderValues {
33 pub fn try_init(self) -> Result<(), Self> {
35 PAYLOAD_BUILDER_DEFAULTS.set(self)
36 }
37
38 pub fn get_global() -> &'static Self {
40 PAYLOAD_BUILDER_DEFAULTS.get_or_init(Self::default)
41 }
42
43 pub fn with_extra_data(mut self, v: impl Into<String>) -> Self {
45 self.extra_data = v.into();
46 self
47 }
48
49 pub fn with_interval(mut self, v: Duration) -> Self {
51 self.interval = format_duration_as_secs_or_ms(v);
52 self
53 }
54
55 pub fn with_deadline(mut self, v: u64) -> Self {
57 self.deadline = format!("{}", v);
58 self
59 }
60
61 pub const fn with_max_payload_tasks(mut self, v: usize) -> Self {
63 self.max_payload_tasks = v;
64 self
65 }
66}
67
68impl Default for DefaultPayloadBuilderValues {
69 fn default() -> Self {
70 Self {
71 extra_data: default_extra_data(),
72 interval: "1".to_string(),
73 deadline: "12".to_string(),
74 max_payload_tasks: 3,
75 }
76 }
77}
78
79#[derive(Debug, Clone, Args, PartialEq, Eq)]
81#[command(next_help_heading = "Builder")]
82pub struct PayloadBuilderArgs {
83 #[arg(
88 long = "builder.extradata",
89 value_parser = ExtraDataValueParser::default(),
90 default_value = DefaultPayloadBuilderValues::get_global().extra_data.as_str()
91 )]
92 pub extra_data: Bytes,
93
94 #[arg(long = "builder.gaslimit", alias = "miner.gaslimit", value_name = "GAS_LIMIT")]
96 pub gas_limit: Option<u64>,
97
98 #[arg(
104 long = "builder.interval",
105 value_parser = parse_duration_from_secs_or_ms,
106 default_value = DefaultPayloadBuilderValues::get_global().interval.as_str(),
107 value_name = "DURATION"
108 )]
109 pub interval: Duration,
110
111 #[arg(
113 long = "builder.deadline",
114 value_parser = parse_duration_from_secs,
115 default_value = DefaultPayloadBuilderValues::get_global().deadline.as_str(),
116 value_name = "SECONDS"
117 )]
118 pub deadline: Duration,
119
120 #[arg(
122 long = "builder.max-tasks",
123 value_parser = RangedU64ValueParser::<usize>::new().range(1..),
124 default_value_t = DefaultPayloadBuilderValues::get_global().max_payload_tasks
125 )]
126 pub max_payload_tasks: usize,
127
128 #[arg(long = "builder.max-blobs", value_name = "COUNT")]
130 pub max_blobs_per_block: Option<u64>,
131}
132
133impl Default for PayloadBuilderArgs {
134 fn default() -> Self {
135 let defaults = DefaultPayloadBuilderValues::get_global();
136 Self {
137 extra_data: Bytes::from(defaults.extra_data.as_bytes().to_vec()),
138 interval: parse_duration_from_secs_or_ms(defaults.interval.as_str()).unwrap(),
139 gas_limit: None,
140 deadline: Duration::from_secs(defaults.deadline.parse().unwrap()),
141 max_payload_tasks: defaults.max_payload_tasks,
142 max_blobs_per_block: None,
143 }
144 }
145}
146
147impl PayloadBuilderConfig for PayloadBuilderArgs {
148 fn extra_data(&self) -> Bytes {
149 self.extra_data.clone()
150 }
151
152 fn interval(&self) -> Duration {
153 self.interval
154 }
155
156 fn deadline(&self) -> Duration {
157 self.deadline
158 }
159
160 fn gas_limit(&self) -> Option<u64> {
161 self.gas_limit
162 }
163
164 fn max_payload_tasks(&self) -> usize {
165 self.max_payload_tasks
166 }
167
168 fn max_blobs_per_block(&self) -> Option<u64> {
169 self.max_blobs_per_block
170 }
171}
172
173#[derive(Clone, Debug, Default)]
174#[non_exhaustive]
175struct ExtraDataValueParser;
176
177impl TypedValueParser for ExtraDataValueParser {
178 type Value = Bytes;
179
180 fn parse_ref(
181 &self,
182 _cmd: &Command,
183 _arg: Option<&Arg>,
184 value: &OsStr,
185 ) -> Result<Self::Value, clap::Error> {
186 let val =
187 value.to_str().ok_or_else(|| clap::Error::new(clap::error::ErrorKind::InvalidUtf8))?;
188
189 let bytes = if let Some(hex) = val.strip_prefix("0x") {
190 alloy_primitives::hex::decode(hex).map_err(|e| {
191 clap::Error::raw(
192 clap::error::ErrorKind::InvalidValue,
193 format!("Invalid hex in extradata: {e}"),
194 )
195 })?
196 } else {
197 val.as_bytes().to_vec()
198 };
199
200 if bytes.len() > MAXIMUM_EXTRA_DATA_SIZE {
201 return Err(clap::Error::raw(
202 clap::error::ErrorKind::InvalidValue,
203 format!(
204 "Payload builder extradata size exceeds {MAXIMUM_EXTRA_DATA_SIZE}-byte limit"
205 ),
206 ))
207 }
208
209 Ok(bytes.into())
210 }
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216 use clap::Parser;
217
218 #[derive(Parser)]
220 struct CommandParser<T: Args> {
221 #[command(flatten)]
222 args: T,
223 }
224
225 #[test]
226 fn test_args_with_valid_max_tasks() {
227 let args =
228 CommandParser::<PayloadBuilderArgs>::parse_from(["reth", "--builder.max-tasks", "1"])
229 .args;
230 assert_eq!(args.max_payload_tasks, 1)
231 }
232
233 #[test]
234 fn test_args_with_invalid_max_tasks() {
235 assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
236 "reth",
237 "--builder.max-tasks",
238 "0"
239 ])
240 .is_err());
241 }
242
243 #[test]
244 fn test_default_extra_data() {
245 let extra_data = default_extra_data();
246 let args = CommandParser::<PayloadBuilderArgs>::parse_from([
247 "reth",
248 "--builder.extradata",
249 extra_data.as_str(),
250 ])
251 .args;
252 assert_eq!(args.extra_data.as_ref(), extra_data.as_bytes());
253 }
254
255 #[test]
256 fn test_invalid_extra_data() {
257 let extra_data = "x".repeat(MAXIMUM_EXTRA_DATA_SIZE + 1);
258 let args = CommandParser::<PayloadBuilderArgs>::try_parse_from([
259 "reth",
260 "--builder.extradata",
261 extra_data.as_str(),
262 ]);
263 assert!(args.is_err());
264 }
265
266 #[test]
267 fn test_valid_hex_extra_data() {
268 let hex = format!("0x{}", "ab".repeat(MAXIMUM_EXTRA_DATA_SIZE));
269 let args = CommandParser::<PayloadBuilderArgs>::parse_from([
270 "reth",
271 "--builder.extradata",
272 hex.as_str(),
273 ])
274 .args;
275 assert_eq!(args.extra_data.as_ref(), vec![0xab; MAXIMUM_EXTRA_DATA_SIZE].as_slice());
276 }
277
278 #[test]
279 fn test_oversized_hex_extra_data() {
280 let hex = format!("0x{}", "ab".repeat(MAXIMUM_EXTRA_DATA_SIZE + 1));
281 assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
282 "reth",
283 "--builder.extradata",
284 hex.as_str(),
285 ])
286 .is_err());
287 }
288
289 #[test]
290 fn test_invalid_hex_extra_data() {
291 assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
292 "reth",
293 "--builder.extradata",
294 "0xZZZZ",
295 ])
296 .is_err());
297 }
298
299 #[test]
300 fn test_odd_length_hex_extra_data() {
301 assert!(CommandParser::<PayloadBuilderArgs>::try_parse_from([
302 "reth",
303 "--builder.extradata",
304 "0xabc",
305 ])
306 .is_err());
307 }
308
309 #[test]
310 fn payload_builder_args_default_sanity_check() {
311 let default_args = PayloadBuilderArgs::default();
312 let args = CommandParser::<PayloadBuilderArgs>::parse_from(["reth"]).args;
313 assert_eq!(args, default_args);
314 }
315
316 #[test]
317 fn test_args_with_s_interval() {
318 let args =
319 CommandParser::<PayloadBuilderArgs>::parse_from(["reth", "--builder.interval", "50"])
320 .args;
321 assert_eq!(args.interval, Duration::from_secs(50));
322 }
323
324 #[test]
325 fn test_args_with_ms_interval() {
326 let args =
327 CommandParser::<PayloadBuilderArgs>::parse_from(["reth", "--builder.interval", "50ms"])
328 .args;
329 assert_eq!(args.interval, Duration::from_millis(50));
330 }
331}