1use std::{
3 net::SocketAddr,
4 num::NonZeroU32,
5 path::{Path, PathBuf},
6 str::FromStr,
7 sync::Arc,
8 time::Duration,
9};
10
11use crate::network_identity::NetworkIdentity;
12use commonware_cryptography::ed25519::PublicKey;
13use eyre::Context;
14use tempo_consensus_config::{SigningKey, SigningKeyPassphrase};
15
16const DEFAULT_MAX_MESSAGE_SIZE_BYTES: u32 =
17 reth_consensus_common::validation::MAX_RLP_BLOCK_SIZE as u32;
18const PASSPHRASE_SECRET_WAIT_WARNING_INTERVAL: Duration = Duration::from_secs(5);
19
20#[derive(Debug, Clone, clap::Args)]
22pub struct Args {
23 #[arg(
32 long = "consensus.signing-key",
33 required_unless_present_any = ["follow", "dev"],
34 )]
35 signing_key: Option<PathBuf>,
36
37 #[arg(
44 long = "consensus.secret",
45 value_name = "PATH",
46 requires = "signing_key"
47 )]
48 secret: Option<PathBuf>,
49
50 #[arg(long = "consensus.signing-share")]
52 pub signing_share: Option<PathBuf>,
53
54 #[arg(
56 long = "consensus.network-identity",
57 requires = "network_identity_from_epoch"
58 )]
59 pub(crate) network_identity: Option<NetworkIdentity>,
60
61 #[arg(
63 long = "consensus.network-identity-from-epoch",
64 requires = "network_identity"
65 )]
66 pub(crate) network_identity_from_epoch: Option<u64>,
67
68 #[arg(long = "consensus.listen-address", default_value = "127.0.0.1:8000")]
71 pub listen_address: SocketAddr,
72
73 #[arg(long = "consensus.metrics-address", default_value = "127.0.0.1:8001")]
76 pub metrics_address: SocketAddr,
77
78 #[arg(long = "consensus.max-message-size-bytes", default_value_t = DEFAULT_MAX_MESSAGE_SIZE_BYTES)]
79 pub max_message_size_bytes: u32,
80
81 #[arg(long = "consensus.worker-threads", default_value_t = 3)]
83 pub worker_threads: usize,
84
85 #[arg(long = "consensus.message-backlog", default_value_t = 16_384)]
88 pub message_backlog: usize,
89
90 #[arg(long = "consensus.mailbox-size", default_value_t = 16_384)]
93 pub mailbox_size: usize,
94
95 #[arg(long = "consensus.deque-size", default_value_t = 10)]
98 pub deque_size: usize,
99
100 #[arg(long = "consensus.wait-for-peer-response", default_value = "2s")]
102 pub wait_for_peer_response: PositiveDuration,
103
104 #[arg(long = "consensus.wait-for-notarizations", default_value = "2s")]
107 pub wait_for_notarizations: PositiveDuration,
108
109 #[arg(long = "consensus.target-block-time", default_value = "550ms")]
114 pub target_block_time: PositiveDuration,
115
116 #[arg(long = "consensus.wait-for-proposal", default_value = "1200ms")]
119 pub wait_for_proposal: PositiveDuration,
120
121 #[arg(long = "consensus.wait-to-rebroadcast-nullify", default_value = "10s")]
124 pub wait_to_rebroadcast_nullify: PositiveDuration,
125
126 #[arg(long = "consensus.views-to-track", default_value_t = 256)]
129 pub views_to_track: u64,
130
131 #[arg(
135 long = "consensus.inactive-views-until-leader-skip",
136 default_value_t = 32
137 )]
138 pub inactive_views_until_leader_skip: u64,
139
140 #[arg(long = "consensus.network-budget", default_value = "50ms")]
145 pub network_budget: PositiveDuration,
146
147 #[arg(
149 long = "consensus.time-to-prepare-proposal-transactions",
150 value_name = "DURATION",
151 help = "Deprecated: no longer has any effect and will be removed in the next release."
152 )]
153 pub time_to_prepare_proposal_transactions: Option<PositiveDuration>,
154
155 #[arg(
157 long = "consensus.minimum-time-before-propose",
158 visible_alias = "consensus.time-to-build-proposal",
159 value_name = "DURATION",
160 help = "Deprecated: no longer has any effect and will be removed in the next release."
161 )]
162 pub minimum_time_before_propose: Option<PositiveDuration>,
163
164 #[arg(long = "consensus.time-to-build-subblock", default_value = "100ms")]
167 pub time_to_build_subblock: PositiveDuration,
168
169 #[arg(long = "consensus.use-local-defaults", default_value_t = false)]
172 pub use_local_defaults: bool,
173
174 #[arg(long = "consensus.bypass-ip-check", default_value_t = false)]
179 pub bypass_ip_check: bool,
180
181 #[arg(
183 long = "consensus.allow-private-ips",
184 default_value_t = false,
185 default_value_if("use_local_defaults", "true", "true")
186 )]
187 pub allow_private_ips: bool,
188
189 #[arg(long = "consensus.allow-dns", default_value_t = true)]
191 pub allow_dns: bool,
192
193 #[arg(long = "consensus.synchrony-bound", default_value = "5s")]
195 pub synchrony_bound: PositiveDuration,
196
197 #[arg(
200 long = "consensus.wait-before-peers-redial",
201 default_value = "1s",
202 default_value_if("use_local_defaults", "true", "500ms")
203 )]
204 pub wait_before_peers_redial: PositiveDuration,
205
206 #[arg(
208 long = "consensus.wait-before-peers-reping",
209 default_value = "50s",
210 default_value_if("use_local_defaults", "true", "5s")
211 )]
212 pub wait_before_peers_reping: PositiveDuration,
213
214 #[arg(
216 long = "consensus.wait-before-peers-discovery",
217 default_value = "60s",
218 default_value_if("use_local_defaults", "true", "30s")
219 )]
220 pub wait_before_peers_discovery: PositiveDuration,
221
222 #[arg(
225 long = "consensus.connection-per-peer-min-period",
226 default_value = "60s",
227 default_value_if("use_local_defaults", "true", "1s")
228 )]
229 pub connection_per_peer_min_period: PositiveDuration,
230
231 #[arg(
234 long = "consensus.handshake-per-ip-min-period",
235 default_value = "5s",
236 default_value_if("use_local_defaults", "true", "62ms")
237 )]
238 pub handshake_per_ip_min_period: PositiveDuration,
239
240 #[arg(
243 long = "consensus.handshake-per-subnet-min-period",
244 default_value = "15ms",
245 default_value_if("use_local_defaults", "true", "7ms")
246 )]
247 pub handshake_per_subnet_min_period: PositiveDuration,
248
249 #[arg(long = "consensus.handshake-stale-after", default_value = "10s")]
251 pub handshake_stale_after: PositiveDuration,
252
253 #[arg(long = "consensus.handshake-timeout", default_value = "5s")]
255 pub handshake_timeout: PositiveDuration,
256
257 #[arg(
259 long = "consensus.max-concurrent-handshakes",
260 default_value = "512",
261 default_value_if("use_local_defaults", "true", "1024")
262 )]
263 pub max_concurrent_handshakes: NonZeroU32,
264
265 #[arg(
267 long = "consensus.time-to-unblock-byzantine-peer",
268 default_value = "4h",
269 default_value_if("use_local_defaults", "true", "1h")
270 )]
271 pub time_to_unblock_byzantine_peer: PositiveDuration,
272
273 #[arg(long = "consensus.backfill-frequency", default_value = "8")]
275 pub backfill_frequency: std::num::NonZeroU32,
276
277 #[arg(long = "consensus.subblock-broadcast-interval", default_value = "50ms")]
283 pub subblock_broadcast_interval: PositiveDuration,
284
285 #[arg(long = "consensus.fcu-heartbeat-interval", default_value = "5m")]
290 pub fcu_heartbeat_interval: PositiveDuration,
291
292 #[clap(skip)]
294 loaded_signing_key: Arc<tokio::sync::OnceCell<Option<SigningKey>>>,
295
296 #[arg(long = "consensus.datadir", value_name = "PATH")]
299 pub storage_dir: Option<PathBuf>,
300
301 #[arg(
305 long = "consensus.finalized-blocks-retention",
306 default_value_t = crate::storage::DEFAULT_FINALIZED_BLOCKS_RETENTION,
307 )]
308 pub finalized_blocks_retention: u64,
309
310 #[arg(
313 long = "consensus.no-legacy-archive",
314 help = "Deprecated: the legacy finalized blocks archive has been removed, so this flag no longer has any effect."
315 )]
316 pub no_legacy_archive: bool,
317}
318
319#[derive(Debug, Clone, Copy)]
321pub struct PositiveDuration(jiff::SignedDuration);
322impl PositiveDuration {
323 pub fn into_duration(self) -> Duration {
324 self.0
325 .try_into()
326 .expect("must be positive. enforced when cli parsing.")
327 }
328}
329
330impl FromStr for PositiveDuration {
331 type Err = Box<dyn std::error::Error + Send + Sync + 'static>;
332
333 fn from_str(s: &str) -> Result<Self, Self::Err> {
334 let duration = s.parse::<jiff::SignedDuration>()?;
335 let _: Duration = duration.try_into().wrap_err("duration must be positive")?;
336
337 Ok(Self(duration))
338 }
339}
340
341impl Args {
342 pub(crate) async fn signing_key(&self) -> eyre::Result<Option<SigningKey>> {
347 Ok(self
348 .loaded_signing_key
349 .get_or_try_init(|| async { self.load_signing_key().await })
350 .await
351 .wrap_err("failed reading signing key")?
352 .clone())
353 }
354
355 async fn load_signing_key(&self) -> eyre::Result<Option<SigningKey>> {
356 Ok(match (self.signing_key.as_ref(), self.secret.as_ref()) {
357 (Some(path), Some(secret)) => {
358 let passphrase = read_secret(secret).await.wrap_err_with(|| {
359 format!("failed reading secret from `{}`", secret.display())
360 })?;
361 Some(
362 SigningKey::read_from_file_encrypted(path, passphrase).wrap_err_with(|| {
363 format!(
364 "failed reading ed25519 signing key from file `{}`",
365 path.display()
366 )
367 })?,
368 )
369 }
370 (Some(path), None) => Some(
371 SigningKey::read_from_file_unencrypted(path).wrap_err_with(|| {
372 format!(
373 "failed reading private ed25519 signing key from file `{}`",
374 path.display()
375 )
376 })?,
377 ),
378 (None, Some(_secret)) => {
379 unreachable!(
380 "clap enforces that `--consensus.secret` requires `--consensus.signing-key` to point at the encrypted key file"
381 );
382 }
383 (None, None) => None,
384 })
385 }
386
387 pub async fn public_key(&self) -> eyre::Result<Option<PublicKey>> {
389 Ok(self
390 .signing_key()
391 .await?
392 .map(|signing_key| signing_key.public_key()))
393 }
394
395 pub fn network_identity(&self) -> Option<tempo_chainspec::NetworkIdentity> {
396 let identity = self.network_identity?;
397 let from_epoch = self
398 .network_identity_from_epoch
399 .expect("network identity from epoch required");
400
401 Some(tempo_chainspec::NetworkIdentity {
402 from_epoch,
403 identity: identity.0,
404 })
405 }
406}
407
408async fn read_secret<P: AsRef<Path>>(path: P) -> eyre::Result<SigningKeyPassphrase> {
410 let path = path.as_ref().to_path_buf();
411 let task_path = path.clone();
412 let mut read =
413 tokio::task::spawn_blocking(move || tempo_consensus_config::read_secret(&task_path));
414 let mut warning_interval = tokio::time::interval_at(
415 tokio::time::Instant::now() + PASSPHRASE_SECRET_WAIT_WARNING_INTERVAL,
416 PASSPHRASE_SECRET_WAIT_WARNING_INTERVAL,
417 );
418
419 loop {
420 tokio::select! {
421 result = &mut read => {
422 let (passphrase, is_fifo) =
423 result
424 .map_err(eyre::Report::new)
425 .and_then(|res| res.map_err(eyre::Report::new))
426 .wrap_err("failed reading secret")?;
427 warn_if_not_fifo(is_fifo, &path);
428 return Ok(passphrase);
429 }
430 _ = warning_interval.tick() => {
431 tracing::warn_span!(
432 "signing_key_passphrase_secret",
433 path = %path.display(),
434 )
435 .in_scope(|| {
436 tracing::warn!(
437 "still waiting for signing-key passphrase from secret path; if this is a FIFO, write the passphrase and close the writer"
438 );
439 });
440 }
441 }
442 }
443}
444
445fn warn_if_not_fifo(is_fifo: bool, path: &Path) {
446 if !is_fifo {
447 tracing::warn_span!(
448 "signing_key_passphrase_secret",
449 path = %path.display(),
450 )
451 .in_scope(|| {
452 tracing::warn!(
453 "signing-key passphrase was read from a non-FIFO path; prefer a FIFO to avoid persisting the passphrase on disk"
454 );
455 });
456 }
457}
458
459#[cfg(test)]
460mod tests {
461 use std::{io::Write as _, process::Command, thread, time::Duration};
462
463 use clap::Parser as _;
464 use commonware_codec::Encode as _;
465
466 use super::Args;
467
468 const SIGNING_KEY_HEX: &str =
469 "0x7848b5d711bc9883996317a3f9c90269d56771005d540a19184939c9e8d0db2a";
470 const PASSPHRASE: &str = "correct horse battery staple";
471
472 fn raw_private_key_bytes() -> Vec<u8> {
473 tempo_consensus_config::SigningKey::try_from_hex(SIGNING_KEY_HEX)
474 .unwrap()
475 .into_inner()
476 .encode()
477 .to_vec()
478 }
479
480 #[derive(Debug, clap::Parser)]
481 struct TestCli {
482 #[arg(long = "follow")]
487 #[allow(dead_code)]
488 follow: Option<String>,
489 #[arg(long = "dev")]
490 #[allow(dead_code)]
491 dev: bool,
492
493 #[command(flatten)]
494 consensus: Args,
495 }
496
497 fn parse(args: &[&str]) -> TestCli {
498 TestCli::try_parse_from(std::iter::once("test").chain(args.iter().copied())).unwrap()
499 }
500
501 #[test]
502 fn deprecated_proposal_timing_flags_parse() {
503 for flag in [
504 "--consensus.time-to-prepare-proposal-transactions",
505 "--consensus.minimum-time-before-propose",
506 "--consensus.time-to-build-proposal",
507 ] {
508 parse(&["--dev", flag, "1ms"]);
509 }
510 }
511
512 fn encrypt(plaintext: &[u8], passphrase: &str) -> Vec<u8> {
513 let mut ct = Vec::new();
514 let mut w = age::Encryptor::with_user_passphrase(
515 tempo_consensus_config::SigningKeyPassphrase::from(passphrase),
516 )
517 .wrap_output(&mut ct)
518 .unwrap();
519 w.write_all(plaintext).unwrap();
520 w.finish().unwrap();
521 ct
522 }
523
524 fn mkfifo(path: &std::path::Path) {
525 let status = Command::new("mkfifo")
526 .arg("-m")
527 .arg("600")
528 .arg(path)
529 .status()
530 .expect("mkfifo must be available");
531 assert!(status.success(), "mkfifo failed: {status}");
532 }
533
534 #[tokio::test(flavor = "current_thread")]
535 async fn encrypted_signing_key_via_fifo_roundtrip() {
536 let dir = tempfile::tempdir().unwrap();
537 let key_file = dir.path().join("signing-key.age");
538 std::fs::write(&key_file, encrypt(&raw_private_key_bytes(), PASSPHRASE)).unwrap();
539
540 let fifo = dir.path().join("passphrase.fifo");
541 mkfifo(&fifo);
542
543 let fifo_writer = fifo.clone();
544 let writer = thread::spawn(move || {
545 thread::sleep(Duration::from_millis(50));
546 let mut f = std::fs::OpenOptions::new()
547 .write(true)
548 .open(&fifo_writer)
549 .unwrap();
550 writeln!(f, "{PASSPHRASE}").unwrap();
551 });
552
553 let cli = parse(&[
554 "--consensus.signing-key",
555 key_file.to_str().unwrap(),
556 "--consensus.secret",
557 fifo.to_str().unwrap(),
558 ]);
559
560 let key = cli
561 .consensus
562 .signing_key()
563 .await
564 .expect("signing key must load")
565 .expect("signing key must be Some when --consensus.signing-key is set");
566 writer.join().unwrap();
567
568 let expected = tempo_consensus_config::SigningKey::try_from_hex(SIGNING_KEY_HEX).unwrap();
569 assert_eq!(key.public_key(), expected.public_key());
570 }
571
572 #[tokio::test(flavor = "current_thread")]
573 async fn encrypted_signing_key_via_regular_secret_file_roundtrip() {
574 let dir = tempfile::tempdir().unwrap();
575 let key_file = dir.path().join("signing-key.age");
576 let secret_file = dir.path().join("passphrase.txt");
577 std::fs::write(&key_file, encrypt(&raw_private_key_bytes(), PASSPHRASE)).unwrap();
578 std::fs::write(&secret_file, format!("{PASSPHRASE}\n")).unwrap();
579
580 let cli = parse(&[
581 "--consensus.signing-key",
582 key_file.to_str().unwrap(),
583 "--consensus.secret",
584 secret_file.to_str().unwrap(),
585 ]);
586
587 let key = cli
588 .consensus
589 .signing_key()
590 .await
591 .expect("signing key must load")
592 .expect("signing key must be Some when --consensus.signing-key is set");
593
594 let expected = tempo_consensus_config::SigningKey::try_from_hex(SIGNING_KEY_HEX).unwrap();
595 assert_eq!(key.public_key(), expected.public_key());
596 }
597
598 #[tokio::test(flavor = "current_thread")]
599 async fn encrypted_signing_key_concurrent_calls_share_fifo_read() {
600 let dir = tempfile::tempdir().unwrap();
601 let key_file = dir.path().join("signing-key.age");
602 std::fs::write(&key_file, encrypt(&raw_private_key_bytes(), PASSPHRASE)).unwrap();
603
604 let fifo = dir.path().join("passphrase.fifo");
605 mkfifo(&fifo);
606
607 let fifo_writer = fifo.clone();
608 let writer = thread::spawn(move || {
609 thread::sleep(Duration::from_millis(50));
610 let mut f = std::fs::OpenOptions::new()
611 .write(true)
612 .open(&fifo_writer)
613 .unwrap();
614 writeln!(f, "{PASSPHRASE}").unwrap();
615 });
616
617 let cli = parse(&[
618 "--consensus.signing-key",
619 key_file.to_str().unwrap(),
620 "--consensus.secret",
621 fifo.to_str().unwrap(),
622 ]);
623
624 let consensus_a = cli.consensus.clone();
625 let consensus_b = cli.consensus.clone();
626 let (key_a, key_b) = tokio::time::timeout(Duration::from_secs(2), async {
627 tokio::join!(consensus_a.signing_key(), consensus_b.signing_key())
628 })
629 .await
630 .expect("concurrent signing-key loads must not wait for a second pipe write");
631 writer.join().unwrap();
632
633 let key_a = key_a
634 .expect("first signing key must load")
635 .expect("first signing key must be Some");
636 let key_b = key_b
637 .expect("second signing key must load")
638 .expect("second signing key must be Some");
639
640 let expected = tempo_consensus_config::SigningKey::try_from_hex(SIGNING_KEY_HEX).unwrap();
641 assert_eq!(key_a.public_key(), expected.public_key());
642 assert_eq!(key_b.public_key(), expected.public_key());
643 }
644
645 #[tokio::test(flavor = "current_thread")]
646 async fn encrypted_signing_key_wrong_passphrase_fails() {
647 let dir = tempfile::tempdir().unwrap();
648 let key_file = dir.path().join("signing-key.age");
649 std::fs::write(&key_file, encrypt(&raw_private_key_bytes(), PASSPHRASE)).unwrap();
650
651 let fifo = dir.path().join("passphrase.fifo");
652 mkfifo(&fifo);
653
654 let fifo_writer = fifo.clone();
655 let writer = thread::spawn(move || {
656 thread::sleep(Duration::from_millis(50));
657 let mut f = std::fs::OpenOptions::new()
658 .write(true)
659 .open(&fifo_writer)
660 .unwrap();
661 writeln!(f, "wrong-passphrase").unwrap();
662 });
663
664 let cli = parse(&[
665 "--consensus.signing-key",
666 key_file.to_str().unwrap(),
667 "--consensus.secret",
668 fifo.to_str().unwrap(),
669 ]);
670
671 let _ = cli
672 .consensus
673 .signing_key()
674 .await
675 .expect_err("loading with a wrong passphrase must fail");
676 writer.join().unwrap();
677 }
678}