Skip to main content

arc_malachitebft_config/
lib.rs

1use core::fmt;
2use std::net::{IpAddr, SocketAddr};
3use std::str::FromStr;
4use std::time::Duration;
5
6use bytesize::ByteSize;
7use multiaddr::Multiaddr;
8use serde::{Deserialize, Serialize};
9
10mod utils;
11
12#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
13pub struct ProtocolNames {
14    pub consensus: String,
15    pub discovery_kad: String,
16    pub discovery_regres: String,
17    pub sync: String,
18    pub broadcast: String,
19}
20
21impl Default for ProtocolNames {
22    fn default() -> Self {
23        Self {
24            consensus: "/malachitebft-core-consensus/v1beta1".to_string(),
25            discovery_kad: "/malachitebft-discovery/kad/v1beta1".to_string(),
26            discovery_regres: "/malachitebft-discovery/reqres/v1beta1".to_string(),
27            sync: "/malachitebft-sync/v1beta1".to_string(),
28            broadcast: "/malachitebft-broadcast/v1beta1".to_string(),
29        }
30    }
31}
32
33/// P2P configuration options
34#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
35pub struct P2pConfig {
36    /// Address to listen for incoming connections
37    pub listen_addr: Multiaddr,
38
39    /// List of nodes to keep persistent connections to
40    pub persistent_peers: Vec<Multiaddr>,
41
42    /// Only allow connections to/from persistent peers
43    #[serde(default)]
44    pub persistent_peers_only: bool,
45
46    /// Peer discovery
47    #[serde(default)]
48    pub discovery: DiscoveryConfig,
49
50    /// The type of pub-sub protocol to use for consensus
51    pub protocol: PubSubProtocol,
52
53    /// The maximum size of messages to send over pub-sub
54    pub pubsub_max_size: ByteSize,
55
56    /// The maximum size of messages to send over RPC
57    pub rpc_max_size: ByteSize,
58
59    /// Protocol name configuration
60    #[serde(default)]
61    pub protocol_names: ProtocolNames,
62}
63
64impl Default for P2pConfig {
65    fn default() -> Self {
66        P2pConfig {
67            listen_addr: Multiaddr::empty(),
68            persistent_peers: vec![],
69            persistent_peers_only: false,
70            discovery: Default::default(),
71            protocol: Default::default(),
72            rpc_max_size: ByteSize::mib(10),
73            pubsub_max_size: ByteSize::mib(4),
74            protocol_names: Default::default(),
75        }
76    }
77}
78
79/// Peer Discovery configuration options
80#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
81pub struct DiscoveryConfig {
82    /// Enable peer discovery
83    #[serde(default)]
84    pub enabled: bool,
85
86    /// Bootstrap protocol
87    #[serde(default)]
88    pub bootstrap_protocol: BootstrapProtocol,
89
90    /// Selector
91    #[serde(default)]
92    pub selector: Selector,
93
94    /// Number of outbound peers
95    #[serde(default = "discovery::default_num_outbound_peers")]
96    pub num_outbound_peers: usize,
97
98    /// Number of inbound peers
99    #[serde(default = "discovery::default_num_inbound_peers")]
100    pub num_inbound_peers: usize,
101
102    /// Maximum number of connections per peer
103    #[serde(default = "discovery::default_max_connections_per_peer")]
104    pub max_connections_per_peer: usize,
105
106    /// Maximum connections allowed per IP address.
107    /// Prevents DoS attacks where an attacker generates many PeerIds from the same IP.
108    /// Defaults to num_inbound_peers (effectively disabled).
109    #[serde(default = "discovery::default_num_inbound_peers")]
110    pub max_connections_per_ip: usize,
111
112    /// Ephemeral connection timeout
113    #[serde(default)]
114    #[serde(with = "humantime_serde")]
115    pub ephemeral_connection_timeout: Duration,
116
117    #[serde(default = "discovery::default_dial_max_retries")]
118    pub dial_max_retries: usize,
119
120    #[serde(default = "discovery::default_request_max_retries")]
121    pub request_max_retries: usize,
122
123    #[serde(default = "discovery::default_connect_request_max_retries")]
124    pub connect_request_max_retries: usize,
125}
126
127impl Default for DiscoveryConfig {
128    fn default() -> Self {
129        DiscoveryConfig {
130            enabled: false,
131            bootstrap_protocol: Default::default(),
132            selector: Default::default(),
133            num_outbound_peers: discovery::default_num_outbound_peers(),
134            num_inbound_peers: discovery::default_num_inbound_peers(),
135            max_connections_per_ip: discovery::default_num_inbound_peers(),
136            max_connections_per_peer: discovery::default_max_connections_per_peer(),
137            ephemeral_connection_timeout: Duration::from_secs(60),
138            dial_max_retries: discovery::default_dial_max_retries(),
139            request_max_retries: discovery::default_request_max_retries(),
140            connect_request_max_retries: discovery::default_connect_request_max_retries(),
141        }
142    }
143}
144
145mod discovery {
146    pub fn default_num_outbound_peers() -> usize {
147        50
148    }
149
150    pub fn default_num_inbound_peers() -> usize {
151        50
152    }
153
154    pub fn default_max_connections_per_peer() -> usize {
155        5
156    }
157
158    pub fn default_dial_max_retries() -> usize {
159        5
160    }
161
162    pub fn default_request_max_retries() -> usize {
163        5
164    }
165
166    pub fn default_connect_request_max_retries() -> usize {
167        3
168    }
169}
170
171#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
172#[serde(rename_all = "lowercase")]
173pub enum BootstrapProtocol {
174    #[default]
175    Kademlia,
176    Full,
177}
178
179impl BootstrapProtocol {
180    pub fn name(&self) -> &'static str {
181        match self {
182            Self::Kademlia => "kademlia",
183            Self::Full => "full",
184        }
185    }
186}
187
188impl FromStr for BootstrapProtocol {
189    type Err = String;
190
191    fn from_str(s: &str) -> Result<Self, Self::Err> {
192        match s {
193            "kademlia" => Ok(Self::Kademlia),
194            "full" => Ok(Self::Full),
195            e => Err(format!(
196                "unknown bootstrap protocol: {e}, available: kademlia, full"
197            )),
198        }
199    }
200}
201
202#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
203#[serde(rename_all = "lowercase")]
204pub enum Selector {
205    #[default]
206    Kademlia,
207    Random,
208}
209
210impl Selector {
211    pub fn name(&self) -> &'static str {
212        match self {
213            Self::Kademlia => "kademlia",
214            Self::Random => "random",
215        }
216    }
217}
218
219impl FromStr for Selector {
220    type Err = String;
221
222    fn from_str(s: &str) -> Result<Self, Self::Err> {
223        match s {
224            "kademlia" => Ok(Self::Kademlia),
225            "random" => Ok(Self::Random),
226            e => Err(format!(
227                "unknown selector: {e}, available: kademlia, random"
228            )),
229        }
230    }
231}
232
233#[derive(Copy, Clone, Debug, PartialEq, Eq, Default)]
234pub enum TransportProtocol {
235    #[default]
236    Tcp,
237    Quic,
238}
239
240impl TransportProtocol {
241    pub fn multiaddr(&self, host: &str, port: usize) -> Multiaddr {
242        match self {
243            Self::Tcp => format!("/ip4/{host}/tcp/{port}").parse().unwrap(),
244            Self::Quic => format!("/ip4/{host}/udp/{port}/quic-v1").parse().unwrap(),
245        }
246    }
247}
248
249impl FromStr for TransportProtocol {
250    type Err = String;
251
252    fn from_str(s: &str) -> Result<Self, Self::Err> {
253        match s {
254            "tcp" => Ok(Self::Tcp),
255            "quic" => Ok(Self::Quic),
256            e => Err(format!(
257                "unknown transport protocol: {e}, available: tcp, quic"
258            )),
259        }
260    }
261}
262
263/// The type of pub-sub protocol.
264/// If multiple protocols are configured in the configuration file, the first one from this list
265/// will be used.
266#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
267#[serde(tag = "type", rename_all = "lowercase")]
268pub enum PubSubProtocol {
269    GossipSub(GossipSubConfig),
270    Broadcast,
271}
272
273impl Default for PubSubProtocol {
274    fn default() -> Self {
275        Self::GossipSub(GossipSubConfig::default())
276    }
277}
278
279/// GossipSub configuration
280#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
281#[serde(from = "gossipsub::RawConfig", default)]
282pub struct GossipSubConfig {
283    /// Target number of peers for the mesh network (D in the GossipSub spec)
284    mesh_n: usize,
285
286    /// Maximum number of peers in mesh network before removing some (D_high in the GossipSub spec)
287    mesh_n_high: usize,
288
289    /// Minimum number of peers in mesh network before adding more (D_low in the spec)
290    mesh_n_low: usize,
291
292    /// Minimum number of outbound peers in the mesh network before adding more (D_out in the spec).
293    /// This value must be smaller or equal than `mesh_n / 2` and smaller than `mesh_n_low`.
294    /// When this value is set to 0 or does not meet the above constraints,
295    /// it will be calculated as `max(1, min(mesh_n / 2, mesh_n_low - 1))`
296    mesh_outbound_min: usize,
297
298    /// Enable peer scoring to prioritize nodes based on their type in mesh formation
299    enable_peer_scoring: bool,
300
301    /// Enable explicit peering for persistent peers.
302    /// When enabled, persistent peers are added as explicit peers in GossipSub,
303    /// meaning a node always sends and forwards messages to its explicit peers,
304    /// regardless of mesh membership.
305    enable_explicit_peering: bool,
306
307    /// Enable flood publishing.
308    /// When enabled the publisher sends the messages to all known peers, not just mesh peers.
309    enable_flood_publish: bool,
310}
311
312impl Default for GossipSubConfig {
313    fn default() -> Self {
314        // Peer scoring disabled and explicit peering disabled by default, flood_publish enabled by default
315        Self::new(6, 12, 4, 2, false, false, true)
316    }
317}
318
319impl GossipSubConfig {
320    /// Create a new, valid GossipSub configuration.
321    pub fn new(
322        mesh_n: usize,
323        mesh_n_high: usize,
324        mesh_n_low: usize,
325        mesh_outbound_min: usize,
326        enable_peer_scoring: bool,
327        enable_explicit_peering: bool,
328        enable_flood_publish: bool,
329    ) -> Self {
330        let mut result = Self {
331            mesh_n,
332            mesh_n_high,
333            mesh_n_low,
334            mesh_outbound_min,
335            enable_peer_scoring,
336            enable_explicit_peering,
337            enable_flood_publish,
338        };
339
340        result.adjust();
341        result
342    }
343
344    /// Adjust the configuration values.
345    pub fn adjust(&mut self) {
346        use std::cmp::{max, min};
347
348        if self.mesh_n == 0 {
349            self.mesh_n = 6;
350        }
351
352        if self.mesh_n_high == 0 || self.mesh_n_high < self.mesh_n {
353            self.mesh_n_high = self.mesh_n * 2;
354        }
355
356        if self.mesh_n_low == 0 || self.mesh_n_low > self.mesh_n {
357            self.mesh_n_low = self.mesh_n * 2 / 3;
358        }
359
360        if self.mesh_outbound_min == 0
361            || self.mesh_outbound_min > self.mesh_n / 2
362            || self.mesh_outbound_min >= self.mesh_n_low
363        {
364            self.mesh_outbound_min = max(1, min(self.mesh_n / 2, self.mesh_n_low - 1));
365        }
366
367        // Both flood_publish and explicit_peering can be enabled together.
368        // flood_publish sends to all known peers on publish, explicit peering ensures
369        // a node always sends and forwards messages to its explicit peers,
370        // regardless of mesh membership.
371    }
372
373    pub fn mesh_n(&self) -> usize {
374        self.mesh_n
375    }
376
377    pub fn mesh_n_high(&self) -> usize {
378        self.mesh_n_high
379    }
380
381    pub fn mesh_n_low(&self) -> usize {
382        self.mesh_n_low
383    }
384
385    pub fn mesh_outbound_min(&self) -> usize {
386        self.mesh_outbound_min
387    }
388
389    pub fn enable_peer_scoring(&self) -> bool {
390        self.enable_peer_scoring
391    }
392
393    pub fn enable_explicit_peering(&self) -> bool {
394        self.enable_explicit_peering
395    }
396
397    pub fn enable_flood_publish(&self) -> bool {
398        self.enable_flood_publish
399    }
400}
401
402mod gossipsub {
403    use super::utils::bool_from_anything;
404
405    fn default_enable_peer_scoring() -> bool {
406        false
407    }
408
409    fn default_enable_explicit_peering() -> bool {
410        false
411    }
412
413    fn default_enable_flood_publish() -> bool {
414        true
415    }
416
417    #[derive(serde::Deserialize)]
418    pub struct RawConfig {
419        #[serde(default)]
420        mesh_n: usize,
421        #[serde(default)]
422        mesh_n_high: usize,
423        #[serde(default)]
424        mesh_n_low: usize,
425        #[serde(default)]
426        mesh_outbound_min: usize,
427        #[serde(
428            default = "default_enable_peer_scoring",
429            deserialize_with = "bool_from_anything"
430        )]
431        enable_peer_scoring: bool,
432        #[serde(
433            default = "default_enable_explicit_peering",
434            deserialize_with = "bool_from_anything"
435        )]
436        enable_explicit_peering: bool,
437        #[serde(
438            default = "default_enable_flood_publish",
439            deserialize_with = "bool_from_anything"
440        )]
441        enable_flood_publish: bool,
442    }
443
444    impl From<RawConfig> for super::GossipSubConfig {
445        fn from(raw: RawConfig) -> Self {
446            super::GossipSubConfig::new(
447                raw.mesh_n,
448                raw.mesh_n_high,
449                raw.mesh_n_low,
450                raw.mesh_outbound_min,
451                raw.enable_peer_scoring,
452                raw.enable_explicit_peering,
453                raw.enable_flood_publish,
454            )
455        }
456    }
457}
458
459#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, Default)]
460#[serde(tag = "load_type", rename_all = "snake_case")]
461pub enum MempoolLoadType {
462    #[default]
463    NoLoad,
464    UniformLoad(mempool_load::UniformLoadConfig),
465    NonUniformLoad(mempool_load::NonUniformLoadConfig),
466}
467
468pub mod mempool_load {
469    use super::*;
470
471    #[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
472    pub struct NonUniformLoadConfig {
473        /// Base transaction count
474        pub base_count: i32,
475
476        /// Base transaction size
477        pub base_size: i32,
478
479        /// How much the transaction count can vary
480        pub count_variation: std::ops::Range<i32>,
481
482        /// How much the transaction size can vary
483        pub size_variation: std::ops::Range<i32>,
484
485        /// Chance of generating a spike.
486        /// e.g. 0.1 = 10% chance of spike
487        pub spike_probability: f64,
488
489        /// Multiplier for spike transactions
490        /// e.g. 10 = 10x more transactions during spike
491        pub spike_multiplier: usize,
492
493        /// Range of intervals between generating load, in milliseconds
494        pub sleep_interval: std::ops::Range<u64>,
495    }
496
497    impl Default for NonUniformLoadConfig {
498        fn default() -> Self {
499            Self {
500                base_count: 100,
501                base_size: 256,
502                count_variation: -100..200,
503                size_variation: -64..128,
504                spike_probability: 0.10,
505                spike_multiplier: 2,
506                sleep_interval: 1000..5000,
507            }
508        }
509    }
510
511    #[derive(Copy, Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
512    pub struct UniformLoadConfig {
513        /// Interval at which to generate load
514        #[serde(with = "humantime_serde")]
515        pub interval: Duration,
516
517        /// Number of transactions to generate
518        pub count: usize,
519
520        /// Size of each generated transaction
521        pub size: ByteSize,
522    }
523
524    impl Default for UniformLoadConfig {
525        fn default() -> Self {
526            Self {
527                interval: Duration::from_secs(1),
528                count: 1000,
529                size: ByteSize::b(256),
530            }
531        }
532    }
533}
534
535/// Mempool configuration options
536#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
537pub struct MempoolLoadConfig {
538    /// Mempool loading type
539    #[serde(flatten)]
540    pub load_type: MempoolLoadType,
541}
542
543/// Mempool configuration options
544#[derive(Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
545pub struct MempoolConfig {
546    /// P2P configuration options
547    pub p2p: P2pConfig,
548
549    /// Maximum number of transactions
550    pub max_tx_count: usize,
551
552    /// Maximum number of transactions to gossip at once in a batch
553    pub gossip_batch_size: usize,
554
555    /// Mempool load configuration options
556    pub load: MempoolLoadConfig,
557}
558
559/// ValueSync configuration options
560#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
561pub struct ValueSyncConfig {
562    /// Enable ValueSync
563    pub enabled: bool,
564
565    /// Interval at which to update other peers of our status
566    #[serde(with = "humantime_serde")]
567    pub status_update_interval: Duration,
568
569    /// Timeout duration for sync requests
570    #[serde(with = "humantime_serde")]
571    pub request_timeout: Duration,
572
573    /// Maximum size of a request
574    pub max_request_size: ByteSize,
575
576    /// Maximum size of a response
577    pub max_response_size: ByteSize,
578
579    /// Maximum number of parallel requests to send
580    pub parallel_requests: usize,
581
582    /// Scoring strategy for peers
583    #[serde(default)]
584    pub scoring_strategy: ScoringStrategy,
585
586    /// Threshold for considering a peer inactive
587    #[serde(with = "humantime_serde")]
588    pub inactive_threshold: Duration,
589
590    /// Maximum number of decided values to request in a single batch
591    pub batch_size: usize,
592}
593
594impl Default for ValueSyncConfig {
595    fn default() -> Self {
596        Self {
597            enabled: true,
598            status_update_interval: Duration::from_secs(10),
599            request_timeout: Duration::from_secs(10),
600            max_request_size: ByteSize::mib(1),
601            max_response_size: ByteSize::mib(10),
602            parallel_requests: 5,
603            scoring_strategy: ScoringStrategy::default(),
604            inactive_threshold: Duration::from_secs(60),
605            batch_size: 5,
606        }
607    }
608}
609
610#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, Serialize, Deserialize)]
611#[serde(rename_all = "lowercase")]
612pub enum ScoringStrategy {
613    #[default]
614    Ema,
615}
616
617impl ScoringStrategy {
618    pub fn name(&self) -> &'static str {
619        match self {
620            Self::Ema => "ema",
621        }
622    }
623}
624
625impl FromStr for ScoringStrategy {
626    type Err = String;
627
628    fn from_str(s: &str) -> Result<Self, Self::Err> {
629        match s {
630            "ema" => Ok(Self::Ema),
631            e => Err(format!("unknown scoring strategy: {e}, available: ema")),
632        }
633    }
634}
635
636fn default_consensus_enabled() -> bool {
637    true
638}
639
640fn default_queue_capacity() -> usize {
641    10
642}
643
644/// Consensus configuration options
645#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
646pub struct ConsensusConfig {
647    /// Enable consensus protocol participation
648    ///
649    /// When disabled, the node only runs the synchronization protocol
650    /// and does not subscribe to consensus-related topics
651    #[serde(default = "default_consensus_enabled")]
652    pub enabled: bool,
653
654    /// P2P configuration options
655    pub p2p: P2pConfig,
656
657    /// Message types that can carry values
658    pub value_payload: ValuePayload,
659
660    /// Size of the gossip input queue (number of unique heights).
661    /// Controls how many unique future heights of gossip messages
662    /// (votes, proposals, proposed values) can be buffered.
663    /// Default: 10
664    #[serde(default = "default_queue_capacity")]
665    pub queue_capacity: usize,
666}
667
668impl Default for ConsensusConfig {
669    fn default() -> Self {
670        Self {
671            enabled: true,
672            p2p: P2pConfig::default(),
673            value_payload: ValuePayload::default(),
674            queue_capacity: default_queue_capacity(),
675        }
676    }
677}
678
679/// Message types required by consensus to deliver the value being proposed
680#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
681#[serde(rename_all = "kebab-case")]
682pub enum ValuePayload {
683    #[default]
684    PartsOnly,
685    ProposalOnly, // TODO - add small block app to test this option
686    ProposalAndParts,
687}
688
689impl ValuePayload {
690    pub fn include_parts(&self) -> bool {
691        match self {
692            Self::ProposalOnly => false,
693            Self::PartsOnly | Self::ProposalAndParts => true,
694        }
695    }
696
697    pub fn include_proposal(&self) -> bool {
698        match self {
699            Self::PartsOnly => false,
700            Self::ProposalOnly | Self::ProposalAndParts => true,
701        }
702    }
703}
704
705#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
706pub struct MetricsConfig {
707    /// Enable the metrics server
708    pub enabled: bool,
709
710    /// Address at which to serve the metrics at
711    pub listen_addr: SocketAddr,
712}
713
714impl Default for MetricsConfig {
715    fn default() -> Self {
716        MetricsConfig {
717            enabled: false,
718            listen_addr: SocketAddr::new(IpAddr::from([127, 0, 0, 1]), 9000),
719        }
720    }
721}
722
723#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
724#[serde(tag = "flavor", rename_all = "snake_case")]
725pub enum RuntimeConfig {
726    /// Single-threaded runtime
727    #[default]
728    SingleThreaded,
729
730    /// Multi-threaded runtime
731    MultiThreaded {
732        /// Number of worker threads
733        worker_threads: usize,
734    },
735}
736
737impl RuntimeConfig {
738    pub fn single_threaded() -> Self {
739        Self::SingleThreaded
740    }
741
742    pub fn multi_threaded(worker_threads: usize) -> Self {
743        Self::MultiThreaded { worker_threads }
744    }
745}
746
747#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
748pub struct VoteExtensionsConfig {
749    pub enabled: bool,
750    pub size: ByteSize,
751}
752
753#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
754pub struct TestConfig {
755    pub max_block_size: ByteSize,
756    pub txs_per_part: usize,
757    pub time_allowance_factor: f32,
758    #[serde(with = "humantime_serde")]
759    pub exec_time_per_tx: Duration,
760    pub max_retain_blocks: usize,
761    #[serde(default)]
762    pub vote_extensions: VoteExtensionsConfig,
763    #[serde(default)]
764    pub stable_block_times: bool,
765    #[serde(default, with = "humantime_serde")]
766    pub target_time: Option<Duration>,
767}
768
769impl Default for TestConfig {
770    fn default() -> Self {
771        Self {
772            max_block_size: ByteSize::mib(1),
773            txs_per_part: 256,
774            time_allowance_factor: 0.5,
775            exec_time_per_tx: Duration::from_millis(1),
776            max_retain_blocks: 1000,
777            vote_extensions: VoteExtensionsConfig::default(),
778            stable_block_times: false,
779            target_time: None,
780        }
781    }
782}
783
784#[derive(Copy, Clone, Debug, Default, PartialEq, Serialize, Deserialize)]
785pub struct LoggingConfig {
786    pub log_level: LogLevel,
787    pub log_format: LogFormat,
788}
789
790#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
791#[serde(rename_all = "lowercase")]
792pub enum LogLevel {
793    Trace,
794    #[default]
795    Debug,
796    Warn,
797    Info,
798    Error,
799}
800
801impl FromStr for LogLevel {
802    type Err = String;
803
804    fn from_str(s: &str) -> Result<Self, Self::Err> {
805        match s {
806            "trace" => Ok(LogLevel::Trace),
807            "debug" => Ok(LogLevel::Debug),
808            "warn" => Ok(LogLevel::Warn),
809            "info" => Ok(LogLevel::Info),
810            "error" => Ok(LogLevel::Error),
811            e => Err(format!("Invalid log level: {e}")),
812        }
813    }
814}
815
816impl fmt::Display for LogLevel {
817    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
818        match self {
819            LogLevel::Trace => write!(f, "trace"),
820            LogLevel::Debug => write!(f, "debug"),
821            LogLevel::Warn => write!(f, "warn"),
822            LogLevel::Info => write!(f, "info"),
823            LogLevel::Error => write!(f, "error"),
824        }
825    }
826}
827
828#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
829#[serde(rename_all = "lowercase")]
830pub enum LogFormat {
831    #[default]
832    Plaintext,
833    Json,
834}
835
836impl FromStr for LogFormat {
837    type Err = String;
838
839    fn from_str(s: &str) -> Result<Self, Self::Err> {
840        match s {
841            "plaintext" => Ok(LogFormat::Plaintext),
842            "json" => Ok(LogFormat::Json),
843            e => Err(format!("Invalid log format: {e}")),
844        }
845    }
846}
847
848impl fmt::Display for LogFormat {
849    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
850        match self {
851            LogFormat::Plaintext => write!(f, "plaintext"),
852            LogFormat::Json => write!(f, "json"),
853        }
854    }
855}
856
857#[cfg(test)]
858mod tests {
859    use super::*;
860
861    #[test]
862    fn log_format() {
863        assert_eq!(
864            LogFormat::from_str("yaml"),
865            Err("Invalid log format: yaml".to_string())
866        )
867    }
868
869    #[test]
870    fn runtime_multi_threaded() {
871        assert_eq!(
872            RuntimeConfig::multi_threaded(5),
873            RuntimeConfig::MultiThreaded { worker_threads: 5 }
874        );
875    }
876
877    #[test]
878    fn log_formatting() {
879        assert_eq!(
880            format!(
881                "{} {} {} {} {}",
882                LogLevel::Trace,
883                LogLevel::Debug,
884                LogLevel::Warn,
885                LogLevel::Info,
886                LogLevel::Error
887            ),
888            "trace debug warn info error"
889        );
890
891        assert_eq!(
892            format!("{} {}", LogFormat::Plaintext, LogFormat::Json),
893            "plaintext json"
894        );
895    }
896
897    #[test]
898    fn protocol_names_default() {
899        let protocol_names = ProtocolNames::default();
900        assert_eq!(
901            protocol_names.consensus,
902            "/malachitebft-core-consensus/v1beta1"
903        );
904        assert_eq!(
905            protocol_names.discovery_kad,
906            "/malachitebft-discovery/kad/v1beta1"
907        );
908        assert_eq!(
909            protocol_names.discovery_regres,
910            "/malachitebft-discovery/reqres/v1beta1"
911        );
912        assert_eq!(protocol_names.sync, "/malachitebft-sync/v1beta1");
913    }
914
915    #[test]
916    fn protocol_names_serde() {
917        use serde_json;
918
919        // Test serialization
920        let protocol_names = ProtocolNames {
921            consensus: "/custom-consensus/v1".to_string(),
922            discovery_kad: "/custom-discovery/kad/v1".to_string(),
923            discovery_regres: "/custom-discovery/reqres/v1".to_string(),
924            sync: "/custom-sync/v1".to_string(),
925            broadcast: "/custom-broadcast/v1".to_string(),
926        };
927
928        let json = serde_json::to_string(&protocol_names).unwrap();
929
930        // Test deserialization
931        let deserialized: ProtocolNames = serde_json::from_str(&json).unwrap();
932        assert_eq!(protocol_names, deserialized);
933    }
934
935    #[test]
936    fn p2p_config_with_protocol_names() {
937        let config = P2pConfig::default();
938
939        // Verify protocol_names field exists and has defaults
940        assert_eq!(config.protocol_names, ProtocolNames::default());
941
942        // Test with custom protocol names
943        let custom_protocol_names = ProtocolNames {
944            consensus: "/test-network/consensus/v1".to_string(),
945            discovery_kad: "/test-network/discovery/kad/v1".to_string(),
946            discovery_regres: "/test-network/discovery/reqres/v1".to_string(),
947            sync: "/test-network/sync/v1".to_string(),
948            broadcast: "/test-network/broadcast/v1".to_string(),
949        };
950
951        let config_with_custom = P2pConfig {
952            protocol_names: custom_protocol_names.clone(),
953            ..Default::default()
954        };
955
956        assert_eq!(config_with_custom.protocol_names, custom_protocol_names);
957    }
958
959    #[test]
960    fn protocol_names_toml_deserialization() {
961        let toml_content = r#"
962        timeout_propose = "3s"
963        timeout_propose_delta = "500ms"
964        timeout_prevote = "1s"
965        timeout_prevote_delta = "500ms"
966        timeout_precommit = "1s"
967        timeout_precommit_delta = "500ms"
968        timeout_rebroadcast = "5s"
969        value_payload = "parts-only"
970        
971        [p2p]
972        listen_addr = "/ip4/0.0.0.0/tcp/0"
973        persistent_peers = []
974        pubsub_max_size = "4 MiB"
975        rpc_max_size = "10 MiB"
976        
977        [p2p.protocol_names]
978        consensus = "/custom-network/consensus/v2"
979        discovery_kad = "/custom-network/discovery/kad/v2"
980        discovery_regres = "/custom-network/discovery/reqres/v2"
981        sync = "/custom-network/sync/v2"
982        broadcast = "/custom-network/broadcast/v2"
983        
984        [p2p.protocol]
985        type = "gossipsub"
986        "#;
987
988        let config: ConsensusConfig = toml::from_str(toml_content).unwrap();
989
990        assert_eq!(
991            config.p2p.protocol_names.consensus,
992            "/custom-network/consensus/v2"
993        );
994        assert_eq!(
995            config.p2p.protocol_names.discovery_kad,
996            "/custom-network/discovery/kad/v2"
997        );
998        assert_eq!(
999            config.p2p.protocol_names.discovery_regres,
1000            "/custom-network/discovery/reqres/v2"
1001        );
1002        assert_eq!(config.p2p.protocol_names.sync, "/custom-network/sync/v2");
1003        assert_eq!(
1004            config.p2p.protocol_names.broadcast,
1005            "/custom-network/broadcast/v2"
1006        );
1007    }
1008
1009    #[test]
1010    fn protocol_names_toml_defaults_when_missing() {
1011        let toml_content = r#"
1012        timeout_propose = "3s"
1013        timeout_propose_delta = "500ms"
1014        timeout_prevote = "1s"
1015        timeout_prevote_delta = "500ms"
1016        timeout_precommit = "1s"
1017        timeout_precommit_delta = "500ms"
1018        timeout_rebroadcast = "5s"
1019        value_payload = "parts-only"
1020        
1021        [p2p]
1022        listen_addr = "/ip4/0.0.0.0/tcp/0"
1023        persistent_peers = []
1024        pubsub_max_size = "4 MiB"
1025        rpc_max_size = "10 MiB"
1026        
1027        [p2p.protocol]
1028        type = "gossipsub"
1029        "#;
1030
1031        let config: ConsensusConfig = toml::from_str(toml_content).unwrap();
1032
1033        // Should use defaults when protocol_names section is missing
1034        assert_eq!(config.p2p.protocol_names, ProtocolNames::default());
1035    }
1036
1037    #[test]
1038    fn p2p_config_persistent_peers_only_default() {
1039        let config = P2pConfig::default();
1040        assert!(
1041            !config.persistent_peers_only,
1042            "persistent_peers_only should default to false"
1043        );
1044    }
1045
1046    #[test]
1047    fn p2p_config_persistent_peers_only_toml() {
1048        let toml_content = r#"
1049        timeout_propose = "3s"
1050        timeout_propose_delta = "500ms"
1051        timeout_prevote = "1s"
1052        timeout_prevote_delta = "500ms"
1053        timeout_precommit = "1s"
1054        timeout_precommit_delta = "500ms"
1055        timeout_rebroadcast = "5s"
1056        value_payload = "parts-only"
1057        
1058        [p2p]
1059        listen_addr = "/ip4/0.0.0.0/tcp/0"
1060        persistent_peers = []
1061        persistent_peers_only = true
1062        pubsub_max_size = "4 MiB"
1063        rpc_max_size = "10 MiB"
1064        
1065        [p2p.protocol]
1066        type = "gossipsub"
1067        "#;
1068
1069        let config: ConsensusConfig = toml::from_str(toml_content).unwrap();
1070        assert!(
1071            config.p2p.persistent_peers_only,
1072            "persistent_peers_only should be true when set in TOML"
1073        );
1074    }
1075
1076    #[test]
1077    fn gossipsub_config_default_disables_peer_scoring() {
1078        let config = GossipSubConfig::default();
1079        assert!(!config.enable_peer_scoring());
1080    }
1081
1082    #[test]
1083    fn gossipsub_enable_peer_scoring_deserialization() {
1084        struct TestCase {
1085            name: &'static str,
1086            toml: &'static str,
1087            expected: bool,
1088        }
1089
1090        let cases = [
1091            TestCase {
1092                name: "missing field defaults to false",
1093                toml: r#"
1094                    [p2p.protocol]
1095                    type = "gossipsub"
1096                "#,
1097                expected: false,
1098            },
1099            TestCase {
1100                name: "explicit true",
1101                toml: r#"
1102                    [p2p.protocol]
1103                    type = "gossipsub"
1104                    enable_peer_scoring = true
1105                "#,
1106                expected: true,
1107            },
1108            TestCase {
1109                name: "explicit false",
1110                toml: r#"
1111                    [p2p.protocol]
1112                    type = "gossipsub"
1113                    enable_peer_scoring = false
1114                "#,
1115                expected: false,
1116            },
1117            TestCase {
1118                name: "string true",
1119                toml: r#"
1120                    [p2p.protocol]
1121                    type = "gossipsub"
1122                    enable_peer_scoring = "true"
1123                "#,
1124                expected: true,
1125            },
1126            TestCase {
1127                name: "string false",
1128                toml: r#"
1129                    [p2p.protocol]
1130                    type = "gossipsub"
1131                    enable_peer_scoring = "false"
1132                "#,
1133                expected: false,
1134            },
1135        ];
1136
1137        for case in cases {
1138            let toml_content = format!(
1139                r#"
1140                timeout_propose = "3s"
1141                timeout_propose_delta = "500ms"
1142                timeout_prevote = "1s"
1143                timeout_prevote_delta = "500ms"
1144                timeout_precommit = "1s"
1145                timeout_precommit_delta = "500ms"
1146                timeout_rebroadcast = "5s"
1147                value_payload = "parts-only"
1148                
1149                [p2p]
1150                listen_addr = "/ip4/0.0.0.0/tcp/0"
1151                persistent_peers = []
1152                pubsub_max_size = "4 MiB"
1153                rpc_max_size = "10 MiB"
1154                {}
1155                "#,
1156                case.toml
1157            );
1158
1159            let config: ConsensusConfig = toml::from_str(&toml_content)
1160                .unwrap_or_else(|e| panic!("Failed to parse {}: {}", case.name, e));
1161
1162            let PubSubProtocol::GossipSub(gossipsub) = config.p2p.protocol else {
1163                panic!("{}: expected GossipSub protocol", case.name);
1164            };
1165
1166            assert_eq!(
1167                gossipsub.enable_peer_scoring(),
1168                case.expected,
1169                "{}: expected enable_peer_scoring = {}",
1170                case.name,
1171                case.expected
1172            );
1173        }
1174    }
1175}