drive_abci/abci/
config.rs

1//! Configuration of ABCI Application server
2
3use crate::utils::from_opt_str_or_number;
4use serde::{Deserialize, Serialize};
5
6// We allow changes in the ABCI configuration, but there should be a social process
7// involved in making this change.
8// @append_only
9/// AbciAppConfig stores configuration of the ABCI Application.
10#[derive(Clone, Debug, Serialize, Deserialize)]
11pub struct AbciConfig {
12    /// Address to listen for ABCI connections
13    ///
14    /// Address should be an URL with scheme `tcp://` or `unix://`, for example:
15    /// - `tcp://127.0.0.1:1234`
16    /// - `unix:///var/run/abci.sock`
17    #[serde(rename = "abci_consensus_bind_address")]
18    pub consensus_bind_address: String,
19
20    /// Height of genesis block; defaults to 1
21    #[serde(default = "AbciConfig::default_genesis_height")]
22    pub genesis_height: u64,
23
24    /// Height of core at genesis
25    #[serde(default = "AbciConfig::default_genesis_core_height")]
26    pub genesis_core_height: u32,
27
28    /// Chain ID of the network to use
29    #[serde(default)]
30    pub chain_id: String,
31
32    /// Logging configuration
33    // Note it is parsed directly in PlatformConfig::from_env() so here we just set defaults.
34    #[serde(default)]
35    pub log: crate::logging::LogConfigs,
36
37    /// Maximum time limit (in ms) to process state transitions to prepare proposal
38    #[serde(default, deserialize_with = "from_opt_str_or_number")]
39    pub proposer_tx_processing_time_limit: Option<u16>,
40}
41
42impl AbciConfig {
43    pub(crate) fn default_genesis_height() -> u64 {
44        1
45    }
46
47    pub(crate) fn default_genesis_core_height() -> u32 {
48        1
49    }
50}
51
52impl Default for AbciConfig {
53    fn default() -> Self {
54        Self {
55            consensus_bind_address: "tcp://127.0.0.1:1234".to_string(),
56            genesis_height: AbciConfig::default_genesis_height(),
57            genesis_core_height: AbciConfig::default_genesis_core_height(),
58            chain_id: "chain_id".to_string(),
59            log: Default::default(),
60            proposer_tx_processing_time_limit: Default::default(),
61        }
62    }
63}
64
65#[cfg(test)]
66mod tests {
67    use super::*;
68
69    #[test]
70    fn default_genesis_height_returns_one() {
71        assert_eq!(AbciConfig::default_genesis_height(), 1);
72    }
73
74    #[test]
75    fn default_genesis_core_height_returns_one() {
76        assert_eq!(AbciConfig::default_genesis_core_height(), 1);
77    }
78
79    #[test]
80    fn default_config_has_expected_values() {
81        let config = AbciConfig::default();
82        assert_eq!(config.consensus_bind_address, "tcp://127.0.0.1:1234");
83        assert_eq!(config.genesis_height, 1);
84        assert_eq!(config.genesis_core_height, 1);
85        assert_eq!(config.chain_id, "chain_id");
86        assert!(config.log.is_empty());
87        assert!(config.proposer_tx_processing_time_limit.is_none());
88    }
89
90    #[test]
91    fn config_serializes_and_deserializes_roundtrip() {
92        // proposer_tx_processing_time_limit uses a custom string-only deserializer,
93        // so roundtrip only works when the field is None (serializes as null)
94        let config = AbciConfig {
95            consensus_bind_address: "tcp://0.0.0.0:5678".to_string(),
96            genesis_height: 42,
97            genesis_core_height: 10,
98            chain_id: "test-chain".to_string(),
99            log: Default::default(),
100            proposer_tx_processing_time_limit: None,
101        };
102
103        let serialized = serde_json::to_string(&config).expect("should serialize");
104        let deserialized: AbciConfig =
105            serde_json::from_str(&serialized).expect("should deserialize");
106
107        assert_eq!(deserialized.consensus_bind_address, "tcp://0.0.0.0:5678");
108        assert_eq!(deserialized.genesis_height, 42);
109        assert_eq!(deserialized.genesis_core_height, 10);
110        assert_eq!(deserialized.chain_id, "test-chain");
111        assert!(deserialized.proposer_tx_processing_time_limit.is_none());
112    }
113
114    #[test]
115    fn config_deserialization_uses_defaults_for_missing_fields() {
116        // Only the renamed field is mandatory; all others have defaults
117        let json = r#"{"abci_consensus_bind_address": "tcp://1.2.3.4:9999"}"#;
118        let config: AbciConfig = serde_json::from_str(json).expect("should deserialize");
119
120        assert_eq!(config.consensus_bind_address, "tcp://1.2.3.4:9999");
121        assert_eq!(config.genesis_height, 1);
122        assert_eq!(config.genesis_core_height, 1);
123        assert_eq!(config.chain_id, "");
124        assert!(config.proposer_tx_processing_time_limit.is_none());
125    }
126
127    #[test]
128    fn config_serialization_uses_renamed_field() {
129        let config = AbciConfig::default();
130        let serialized = serde_json::to_value(&config).expect("should serialize");
131
132        // The field should be serialized as "abci_consensus_bind_address"
133        assert!(serialized.get("abci_consensus_bind_address").is_some());
134        assert!(serialized.get("consensus_bind_address").is_none());
135    }
136
137    #[test]
138    fn config_clone_produces_equal_values() {
139        let config = AbciConfig {
140            consensus_bind_address: "unix:///tmp/test.sock".to_string(),
141            genesis_height: 100,
142            genesis_core_height: 50,
143            chain_id: "clone-test".to_string(),
144            log: Default::default(),
145            proposer_tx_processing_time_limit: Some(1000),
146        };
147
148        let cloned = config.clone();
149        assert_eq!(cloned.consensus_bind_address, config.consensus_bind_address);
150        assert_eq!(cloned.genesis_height, config.genesis_height);
151        assert_eq!(cloned.genesis_core_height, config.genesis_core_height);
152        assert_eq!(cloned.chain_id, config.chain_id);
153        assert_eq!(
154            cloned.proposer_tx_processing_time_limit,
155            config.proposer_tx_processing_time_limit
156        );
157    }
158
159    #[test]
160    fn config_debug_format_is_not_empty() {
161        let config = AbciConfig::default();
162        let debug_str = format!("{:?}", config);
163        assert!(!debug_str.is_empty());
164        assert!(debug_str.contains("AbciConfig"));
165    }
166
167    #[test]
168    fn config_deserializes_proposer_tx_processing_time_limit_from_string() {
169        // The from_opt_str_or_number deserializer should accept string values
170        let json = r#"{"abci_consensus_bind_address": "tcp://x:1", "proposer_tx_processing_time_limit": "250"}"#;
171        let config: AbciConfig = serde_json::from_str(json).expect("should deserialize");
172        assert_eq!(config.proposer_tx_processing_time_limit, Some(250));
173    }
174
175    #[test]
176    fn config_rejects_raw_number_for_proposer_tx_processing_time_limit() {
177        // The custom from_opt_str_or_number deserializer only accepts strings,
178        // not raw JSON numbers
179        let json = r#"{"abci_consensus_bind_address": "tcp://x:1", "proposer_tx_processing_time_limit": 750}"#;
180        let result = serde_json::from_str::<AbciConfig>(json);
181        assert!(result.is_err());
182    }
183
184    #[test]
185    fn config_deserializes_null_proposer_tx_processing_time_limit() {
186        let json = r#"{"abci_consensus_bind_address": "tcp://x:1", "proposer_tx_processing_time_limit": null}"#;
187        let config: AbciConfig = serde_json::from_str(json).expect("should deserialize");
188        assert!(config.proposer_tx_processing_time_limit.is_none());
189    }
190
191    #[test]
192    fn config_deserializes_empty_string_as_none_for_proposer_tx_processing_time_limit() {
193        let json = r#"{"abci_consensus_bind_address": "tcp://x:1", "proposer_tx_processing_time_limit": ""}"#;
194        let config: AbciConfig = serde_json::from_str(json).expect("should deserialize");
195        assert!(config.proposer_tx_processing_time_limit.is_none());
196    }
197}