Skip to main content

ave_bridge/settings/
mod.rs

1use config::Config;
2use std::collections::HashSet;
3use tracing::{error, warn};
4
5pub mod command;
6use crate::config::Config as BridgeConfig;
7use crate::error::BridgeError;
8
9pub fn build_config(file: &str) -> Result<BridgeConfig, BridgeError> {
10    // file configuration (json, yaml or toml)
11    let bridge_config = if !file.is_empty() {
12        let mut config = Config::builder();
13
14        config = config.add_source(config::File::with_name(file));
15
16        let config = config.build().map_err(|e| {
17            error!(file = %file, error = %e, "Failed to build configuration");
18            BridgeError::ConfigBuild(e.to_string())
19        })?;
20
21        config.try_deserialize().map_err(|e| {
22            error!(file = %file, error = %e, "Failed to deserialize configuration");
23            BridgeError::ConfigDeserialize(e.to_string())
24        })?
25    } else {
26        BridgeConfig::default()
27    };
28
29    // Validate HTTPS configuration
30    validate_https_config(&bridge_config)?;
31
32    // Validate network configuration
33    validate_network_config(&bridge_config)?;
34
35    // Mix configurations.
36    Ok(bridge_config)
37}
38
39/// Validate network configuration
40fn validate_network_config(config: &BridgeConfig) -> Result<(), BridgeError> {
41    let network = &config.node.network;
42
43    network.memory_limits.validate().map_err(|e| {
44        error!(error = %e, "Invalid network configuration");
45        BridgeError::ConfigBuild(e)
46    })?;
47
48    if network.max_app_message_bytes == 0 {
49        let msg =
50            "network.max_app_message_bytes must be greater than 0".to_owned();
51        error!(error = %msg, "Invalid network configuration");
52        return Err(BridgeError::ConfigBuild(msg));
53    }
54
55    if network.max_pending_outbound_bytes_per_peer > 0
56        && network.max_pending_outbound_bytes_per_peer
57            < network.max_app_message_bytes
58    {
59        let msg = format!(
60            "network.max_pending_outbound_bytes_per_peer ({}) must be >= network.max_app_message_bytes ({})",
61            network.max_pending_outbound_bytes_per_peer,
62            network.max_app_message_bytes
63        );
64        error!(error = %msg, "Invalid network configuration");
65        return Err(BridgeError::ConfigBuild(msg));
66    }
67
68    if network.max_pending_inbound_bytes_per_peer > 0
69        && network.max_pending_inbound_bytes_per_peer
70            < network.max_app_message_bytes
71    {
72        let msg = format!(
73            "network.max_pending_inbound_bytes_per_peer ({}) must be >= network.max_app_message_bytes ({})",
74            network.max_pending_inbound_bytes_per_peer,
75            network.max_app_message_bytes
76        );
77        error!(error = %msg, "Invalid network configuration");
78        return Err(BridgeError::ConfigBuild(msg));
79    }
80
81    if network.max_pending_outbound_bytes_total > 0
82        && network.max_pending_outbound_bytes_total
83            < network.max_app_message_bytes
84    {
85        let msg = format!(
86            "network.max_pending_outbound_bytes_total ({}) must be >= network.max_app_message_bytes ({})",
87            network.max_pending_outbound_bytes_total,
88            network.max_app_message_bytes
89        );
90        error!(error = %msg, "Invalid network configuration");
91        return Err(BridgeError::ConfigBuild(msg));
92    }
93
94    if network.max_pending_inbound_bytes_total > 0
95        && network.max_pending_inbound_bytes_total
96            < network.max_app_message_bytes
97    {
98        let msg = format!(
99            "network.max_pending_inbound_bytes_total ({}) must be >= network.max_app_message_bytes ({})",
100            network.max_pending_inbound_bytes_total,
101            network.max_app_message_bytes
102        );
103        error!(error = %msg, "Invalid network configuration");
104        return Err(BridgeError::ConfigBuild(msg));
105    }
106
107    for addr in &network.listen_addresses {
108        if addr.trim().is_empty() {
109            let msg =
110                "network.listen_addresses contains an empty address".to_owned();
111            error!(error = %msg, "Invalid network configuration");
112            return Err(BridgeError::ConfigBuild(msg));
113        }
114    }
115
116    for addr in &network.external_addresses {
117        if addr.trim().is_empty() {
118            let msg = "network.external_addresses contains an empty address"
119                .to_owned();
120            error!(error = %msg, "Invalid network configuration");
121            return Err(BridgeError::ConfigBuild(msg));
122        }
123    }
124
125    for (index, node) in network.boot_nodes.iter().enumerate() {
126        if node.peer_id.trim().is_empty() {
127            let msg = format!("network.boot_nodes[{index}].peer_id is empty");
128            error!(error = %msg, "Invalid network configuration");
129            return Err(BridgeError::ConfigBuild(msg));
130        }
131        if node.address.is_empty() {
132            let msg = format!(
133                "network.boot_nodes[{index}] must contain at least one address"
134            );
135            error!(error = %msg, "Invalid network configuration");
136            return Err(BridgeError::ConfigBuild(msg));
137        }
138        if node.address.iter().any(|addr| addr.trim().is_empty()) {
139            let msg = format!(
140                "network.boot_nodes[{index}] contains an empty address"
141            );
142            error!(error = %msg, "Invalid network configuration");
143            return Err(BridgeError::ConfigBuild(msg));
144        }
145    }
146
147    let control_list = &network.control_list;
148    if control_list.get_interval_request().is_zero() {
149        let msg =
150            "network.control_list.interval_request must be greater than 0"
151                .to_owned();
152        error!(error = %msg, "Invalid network configuration");
153        return Err(BridgeError::ConfigBuild(msg));
154    }
155
156    if control_list.get_request_timeout().is_zero() {
157        let msg = "network.control_list.request_timeout must be greater than 0"
158            .to_owned();
159        error!(error = %msg, "Invalid network configuration");
160        return Err(BridgeError::ConfigBuild(msg));
161    }
162
163    if control_list.get_request_timeout() > control_list.get_interval_request()
164    {
165        let msg = format!(
166            "network.control_list.request_timeout ({:?}) must be <= network.control_list.interval_request ({:?})",
167            control_list.get_request_timeout(),
168            control_list.get_interval_request()
169        );
170        error!(error = %msg, "Invalid network configuration");
171        return Err(BridgeError::ConfigBuild(msg));
172    }
173
174    // `max_concurrent_requests = 0` is accepted and normalized at runtime to 1
175    // (see network/utils.rs request_peer_lists buffer_unordered max(1)).
176
177    for service in control_list.get_service_allow_list() {
178        if !(service.starts_with("http://") || service.starts_with("https://"))
179        {
180            let msg = format!(
181                "network.control_list.service_allow_list contains an invalid URL: {service}"
182            );
183            error!(error = %msg, "Invalid network configuration");
184            return Err(BridgeError::ConfigBuild(msg));
185        }
186    }
187
188    for service in control_list.get_service_block_list() {
189        if !(service.starts_with("http://") || service.starts_with("https://"))
190        {
191            let msg = format!(
192                "network.control_list.service_block_list contains an invalid URL: {service}"
193            );
194            error!(error = %msg, "Invalid network configuration");
195            return Err(BridgeError::ConfigBuild(msg));
196        }
197    }
198
199    if control_list.get_enable() {
200        let has_allow_source = !control_list.get_allow_list().is_empty()
201            || !control_list.get_service_allow_list().is_empty()
202            || !network.boot_nodes.is_empty();
203        if !has_allow_source {
204            let msg = "network.control_list.enable is true but there are no allow sources (allow_list, service_allow_list or boot_nodes)".to_owned();
205            error!(error = %msg, "Invalid network configuration");
206            return Err(BridgeError::ConfigBuild(msg));
207        }
208
209        let allow: HashSet<String> = control_list
210            .get_allow_list()
211            .into_iter()
212            .map(|peer| peer.trim().to_owned())
213            .collect();
214        let block: HashSet<String> = control_list
215            .get_block_list()
216            .into_iter()
217            .map(|peer| peer.trim().to_owned())
218            .collect();
219        if let Some(peer) = allow.intersection(&block).next() {
220            let msg = format!(
221                "network.control_list has peer present in both allow_list and block_list: {peer}"
222            );
223            error!(error = %msg, "Invalid network configuration");
224            return Err(BridgeError::ConfigBuild(msg));
225        }
226    }
227
228    Ok(())
229}
230
231/// Validate HTTPS configuration consistency
232fn validate_https_config(config: &BridgeConfig) -> Result<(), BridgeError> {
233    let http = &config.http;
234
235    if http.https_address.is_some()
236        && (http.https_cert_path.is_none()
237            || http.https_private_key_path.is_none())
238    {
239        let msg = "HTTPS is enabled (https_address is set) but https_cert_path \
240                   and/or https_private_key_path are missing";
241        error!(error = %msg, "Invalid HTTPS configuration");
242        return Err(BridgeError::ConfigBuild(msg.to_owned()));
243    }
244
245    if http.self_signed_cert.enabled && http.https_address.is_none() {
246        warn!(
247            "self_signed_cert.enabled is true but https_address is not set, \
248             self-signed certificates will not be used"
249        );
250    }
251
252    Ok(())
253}
254
255#[cfg(test)]
256mod tests {
257    use std::{
258        collections::{BTreeMap, BTreeSet},
259        path::PathBuf,
260        time::Duration,
261    };
262
263    use ave_common::identity::{HashAlgorithm, KeyPairAlgorithm};
264    use ave_core::{
265        config::{
266            AveExternalDBFeatureConfig, AveInternalDBFeatureConfig,
267            LoggingOutput, LoggingRotation, MachineSpec, SinkQueuePolicy,
268            SinkRoutingStrategy, SinkServer,
269        },
270        subject::sinkdata::SinkTypes,
271    };
272    use ave_network::{MemoryLimitsConfig, NodeType, RoutingNode};
273    use tempfile::TempPath;
274
275    use crate::{
276        config::Config as BridgeConfig, error::BridgeError,
277        settings::build_config,
278    };
279
280    const FULL_TOML: &str = r#"
281keys_path = "/custom/keys"
282
283[node]
284keypair_algorithm = "Ed25519"
285hash_algorithm = "Blake3"
286contracts_path = "/contracts_proof"
287always_accept = true
288tracking_size = 200
289is_service = true
290only_clear_events = true
291
292[node.sync]
293ledger_batch_size = 150
294
295[node.sync.governance]
296interval_secs = 20
297sample_size = 2
298response_timeout_secs = 7
299
300[node.sync.tracker]
301interval_secs = 30
302page_size = 200
303response_timeout_secs = 8
304update_batch_size = 2
305update_timeout_secs = 6
306
307[node.internal_db]
308db = "/data/ave.db"
309durability = true
310
311[node.external_db]
312db = "/data/ext.db"
313durability = true
314
315[node.spec]
316custom = { ram_mb = 2048, cpu_cores = 4 }
317
318[node.network]
319node_type = "Addressable"
320listen_addresses = ["/ip4/127.0.0.1/tcp/5001", "/ip4/127.0.0.1/tcp/5002"]
321external_addresses = ["/ip4/10.0.0.1/tcp/7000"]
322boot_nodes = [
323    { peer_id = "12D3KooWNode1", address = ["/ip4/1.1.1.1/tcp/1000"] },
324    { peer_id = "12D3KooWNode2", address = ["/ip4/2.2.2.2/tcp/2000"] }
325]
326max_app_message_bytes = 2097152
327max_pending_outbound_bytes_per_peer = 16777216
328max_pending_inbound_bytes_per_peer = 8388608
329max_pending_outbound_bytes_total = 33554432
330max_pending_inbound_bytes_total = 25165824
331
332[node.network.routing]
333dht_random_walk = false
334discovery_only_if_under_num = 25
335allow_private_address_in_dht = true
336allow_dns_address_in_dht = true
337allow_loop_back_address_in_dht = true
338kademlia_disjoint_query_paths = false
339
340[node.network.control_list]
341enable = true
342allow_list = ["Peer200", "Peer300"]
343block_list = ["Peer1", "Peer2"]
344service_allow_list = ["http://allow.local/list"]
345service_block_list = ["http://block.local/list"]
346interval_request = 42
347request_timeout = 7
348max_concurrent_requests = 16
349
350[node.network.memory_limits]
351type = "percentage"
352value = 0.8
353
354[logging]
355output = { stdout = false, file = true, api = true }
356api_url = "https://example.com/logs"
357file_path = "/tmp/my.log"
358rotation = "hourly"
359max_size = 52428800
360max_files = 5
361level = "debug"
362
363[sink]
364auth = "https://auth.service"
365username = "sink-user"
366
367[[sink.sinks.primary]]
368server = "SinkOne"
369events = ["Create", "All"]
370url = "https://sink.one"
371auth = true
372concurrency = 4
373queue_capacity = 2048
374queue_policy = "drop_oldest"
375routing_strategy = "unordered_round_robin"
376connect_timeout_ms = 5000
377request_timeout_ms = 30000
378max_retries = 5
379
380[[sink.sinks.primary]]
381server = "SinkTwo"
382events = ["Transfer"]
383url = "https://sink.two"
384auth = false
385concurrency = 2
386queue_capacity = 512
387queue_policy = "drop_newest"
388routing_strategy = "ordered_by_subject"
389connect_timeout_ms = 3000
390request_timeout_ms = 15000
391max_retries = 1
392
393[auth]
394enable = true
395database_path = "/var/db/auth.db"
396superadmin = "admin:supersecret"
397durability = true
398
399[auth.api_key]
400default_ttl_seconds = 3600
401max_keys_per_user = 20
402prefix = "custom_prefix_"
403
404[auth.lockout]
405max_attempts = 3
406duration_seconds = 600
407
408[auth.rate_limit]
409enable = false
410window_seconds = 120
411max_requests = 50
412limit_by_key = false
413limit_by_ip = true
414cleanup_interval_seconds = 1800
415
416[[auth.rate_limit.sensitive_endpoints]]
417endpoint = "/login"
418max_requests = 5
419window_seconds = 30
420
421[auth.session]
422audit_enable = false
423audit_retention_days = 30
424audit_max_entries = 1000000
425
426[http]
427http_address = "127.0.0.1:4000"
428https_address = "127.0.0.1:4443"
429https_cert_path = "/certs/cert.pem"
430https_private_key_path = "/certs/key.pem"
431enable_doc = true
432
433[http.proxy]
434trusted_proxies = ["10.0.0.1"]
435trust_x_forwarded_for = false
436trust_x_real_ip = false
437
438[http.cors]
439enabled = false
440allow_any_origin = false
441allowed_origins = ["https://app.example.com"]
442allow_credentials = true
443
444[http.self_signed_cert]
445enabled = true
446common_name = "localhost"
447san = ["127.0.0.1", "::1"]
448validity_days = 365
449renew_before_days = 30
450check_interval_secs = 3600
451"#;
452
453    const FULL_YAML: &str = r#"
454keys_path: /custom/keys
455node:
456  keypair_algorithm: Ed25519
457  hash_algorithm: Blake3
458  internal_db:
459    db: /data/ave.db
460    durability: true
461  external_db:
462    db: /data/ext.db
463    durability: true
464  spec:
465    custom:
466      ram_mb: 2048
467      cpu_cores: 4
468  contracts_path: /contracts_proof
469  always_accept: true
470  tracking_size: 200
471  is_service: true
472  only_clear_events: true
473  sync:
474    ledger_batch_size: 150
475    governance:
476      interval_secs: 20
477      sample_size: 2
478      response_timeout_secs: 7
479    tracker:
480      interval_secs: 30
481      page_size: 200
482      response_timeout_secs: 8
483      update_batch_size: 2
484      update_timeout_secs: 6
485  network:
486    node_type: Addressable
487    listen_addresses:
488      - /ip4/127.0.0.1/tcp/5001
489      - /ip4/127.0.0.1/tcp/5002
490    external_addresses:
491      - /ip4/10.0.0.1/tcp/7000
492    boot_nodes:
493      - peer_id: 12D3KooWNode1
494        address:
495          - /ip4/1.1.1.1/tcp/1000
496      - peer_id: 12D3KooWNode2
497        address:
498          - /ip4/2.2.2.2/tcp/2000
499    max_app_message_bytes: 2097152
500    max_pending_outbound_bytes_per_peer: 16777216
501    max_pending_inbound_bytes_per_peer: 8388608
502    max_pending_outbound_bytes_total: 33554432
503    max_pending_inbound_bytes_total: 25165824
504    routing:
505      dht_random_walk: false
506      discovery_only_if_under_num: 25
507      allow_private_address_in_dht: true
508      allow_dns_address_in_dht: true
509      allow_loop_back_address_in_dht: true
510      kademlia_disjoint_query_paths: false
511    control_list:
512      enable: true
513      allow_list: [Peer200, Peer300]
514      block_list: [Peer1, Peer2]
515      service_allow_list: [http://allow.local/list]
516      service_block_list: [http://block.local/list]
517      interval_request: 42
518      request_timeout: 7
519      max_concurrent_requests: 16
520    memory_limits:
521      type: percentage
522      value: 0.8
523logging:
524  output:
525    stdout: false
526    file: true
527    api: true
528  api_url: https://example.com/logs
529  file_path: /tmp/my.log
530  rotation: hourly
531  max_size: 52428800
532  max_files: 5
533  level: debug
534sink:
535  auth: https://auth.service
536  username: sink-user
537  sinks:
538    primary:
539      - server: SinkOne
540        events: [Create, All]
541        url: https://sink.one
542        auth: true
543        concurrency: 4
544        queue_capacity: 2048
545        queue_policy: drop_oldest
546        routing_strategy: unordered_round_robin
547        connect_timeout_ms: 5000
548        request_timeout_ms: 30000
549        max_retries: 5
550      - server: SinkTwo
551        events: [Transfer]
552        url: https://sink.two
553        auth: false
554        concurrency: 2
555        queue_capacity: 512
556        queue_policy: drop_newest
557        routing_strategy: ordered_by_subject
558        connect_timeout_ms: 3000
559        request_timeout_ms: 15000
560        max_retries: 1
561auth:
562  enable: true
563  database_path: /var/db/auth.db
564  superadmin: admin:supersecret
565  durability: true
566  api_key:
567    default_ttl_seconds: 3600
568    max_keys_per_user: 20
569    prefix: custom_prefix_
570  lockout:
571    max_attempts: 3
572    duration_seconds: 600
573  rate_limit:
574    enable: false
575    window_seconds: 120
576    max_requests: 50
577    limit_by_key: false
578    limit_by_ip: true
579    cleanup_interval_seconds: 1800
580    sensitive_endpoints:
581      - endpoint: /login
582        max_requests: 5
583        window_seconds: 30
584  session:
585    audit_enable: false
586    audit_retention_days: 30
587    audit_max_entries: 1000000
588http:
589  http_address: 127.0.0.1:4000
590  https_address: 127.0.0.1:4443
591  https_cert_path: /certs/cert.pem
592  https_private_key_path: /certs/key.pem
593  enable_doc: true
594  proxy:
595    trusted_proxies:
596      - 10.0.0.1
597    trust_x_forwarded_for: false
598    trust_x_real_ip: false
599  cors:
600    enabled: false
601    allow_any_origin: false
602    allowed_origins:
603      - https://app.example.com
604    allow_credentials: true
605  self_signed_cert:
606    enabled: true
607    common_name: localhost
608    san:
609      - "127.0.0.1"
610      - "::1"
611    validity_days: 365
612    renew_before_days: 30
613    check_interval_secs: 3600
614"#;
615
616    const FULL_JSON: &str = r#"
617{
618  "keys_path": "/custom/keys",
619  "node": {
620    "keypair_algorithm": "Ed25519",
621    "hash_algorithm": "Blake3",
622    "internal_db": {
623      "db": "/data/ave.db",
624      "durability": true
625    },
626    "external_db": {
627      "db": "/data/ext.db",
628      "durability": true
629    },
630    "spec": {
631      "custom": {
632        "ram_mb": 2048,
633        "cpu_cores": 4
634      }
635    },
636    "contracts_path": "/contracts_proof",
637    "always_accept": true,
638    "tracking_size": 200,
639    "is_service": true,
640    "only_clear_events": true,
641    "sync": {
642      "ledger_batch_size": 150,
643      "governance": {
644        "interval_secs": 20,
645        "sample_size": 2,
646        "response_timeout_secs": 7
647      },
648      "tracker": {
649        "interval_secs": 30,
650        "page_size": 200,
651        "response_timeout_secs": 8,
652        "update_batch_size": 2,
653        "update_timeout_secs": 6
654      }
655    },
656    "network": {
657      "node_type": "Addressable",
658      "listen_addresses": [
659        "/ip4/127.0.0.1/tcp/5001",
660        "/ip4/127.0.0.1/tcp/5002"
661      ],
662      "external_addresses": [
663        "/ip4/10.0.0.1/tcp/7000"
664      ],
665      "boot_nodes": [
666        {
667          "peer_id": "12D3KooWNode1",
668          "address": ["/ip4/1.1.1.1/tcp/1000"]
669        },
670        {
671          "peer_id": "12D3KooWNode2",
672          "address": ["/ip4/2.2.2.2/tcp/2000"]
673        }
674      ],
675      "max_app_message_bytes": 2097152,
676      "max_pending_outbound_bytes_per_peer": 16777216,
677      "max_pending_inbound_bytes_per_peer": 8388608,
678      "max_pending_outbound_bytes_total": 33554432,
679      "max_pending_inbound_bytes_total": 25165824,
680      "routing": {
681        "dht_random_walk": false,
682        "discovery_only_if_under_num": 25,
683        "allow_private_address_in_dht": true,
684        "allow_dns_address_in_dht": true,
685        "allow_loop_back_address_in_dht": true,
686        "kademlia_disjoint_query_paths": false
687      },
688      "control_list": {
689        "enable": true,
690        "allow_list": ["Peer200", "Peer300"],
691        "block_list": ["Peer1", "Peer2"],
692        "service_allow_list": ["http://allow.local/list"],
693        "service_block_list": ["http://block.local/list"],
694        "interval_request": 42,
695        "request_timeout": 7,
696        "max_concurrent_requests": 16
697      },
698      "memory_limits": {
699        "type": "percentage",
700        "value": 0.8
701      }
702    }
703  },
704  "logging": {
705    "output": {
706      "stdout": false,
707      "file": true,
708      "api": true
709    },
710    "api_url": "https://example.com/logs",
711    "file_path": "/tmp/my.log",
712    "rotation": "hourly",
713    "max_size": 52428800,
714    "max_files": 5,
715    "level": "debug"
716  },
717  "sink": {
718    "auth": "https://auth.service",
719    "username": "sink-user",
720    "sinks": {
721      "primary": [
722        {
723          "server": "SinkOne",
724          "events": ["Create", "All"],
725          "url": "https://sink.one",
726          "auth": true,
727          "concurrency": 4,
728          "queue_capacity": 2048,
729          "queue_policy": "drop_oldest",
730          "routing_strategy": "unordered_round_robin",
731          "connect_timeout_ms": 5000,
732          "request_timeout_ms": 30000,
733          "max_retries": 5
734        },
735        {
736          "server": "SinkTwo",
737          "events": ["Transfer"],
738          "url": "https://sink.two",
739          "auth": false,
740          "concurrency": 2,
741          "queue_capacity": 512,
742          "queue_policy": "drop_newest",
743          "routing_strategy": "ordered_by_subject",
744          "connect_timeout_ms": 3000,
745          "request_timeout_ms": 15000,
746          "max_retries": 1
747        }
748      ]
749    }
750  },
751  "auth": {
752    "enable": true,
753    "database_path": "/var/db/auth.db",
754    "superadmin": "admin:supersecret",
755    "durability": true,
756    "api_key": {
757      "default_ttl_seconds": 3600,
758      "max_keys_per_user": 20,
759      "prefix": "custom_prefix_"
760    },
761    "lockout": {
762      "max_attempts": 3,
763      "duration_seconds": 600
764    },
765    "rate_limit": {
766      "enable": false,
767      "window_seconds": 120,
768      "max_requests": 50,
769      "limit_by_key": false,
770      "limit_by_ip": true,
771      "cleanup_interval_seconds": 1800,
772      "sensitive_endpoints": [
773        { "endpoint": "/login", "max_requests": 5, "window_seconds": 30 }
774      ]
775    },
776    "session": {
777      "audit_enable": false,
778      "audit_retention_days": 30,
779      "audit_max_entries": 1000000
780    }
781  },
782  "http": {
783    "http_address": "127.0.0.1:4000",
784    "https_address": "127.0.0.1:4443",
785    "https_cert_path": "/certs/cert.pem",
786    "https_private_key_path": "/certs/key.pem",
787    "enable_doc": true,
788    "proxy": {
789      "trusted_proxies": ["10.0.0.1"],
790      "trust_x_forwarded_for": false,
791      "trust_x_real_ip": false
792    },
793    "cors": {
794      "enabled": false,
795      "allow_any_origin": false,
796      "allowed_origins": ["https://app.example.com"],
797      "allow_credentials": true
798    },
799    "self_signed_cert": {
800      "enabled": true,
801      "common_name": "localhost",
802      "san": ["127.0.0.1", "::1"],
803      "validity_days": 365,
804      "renew_before_days": 30,
805      "check_interval_secs": 3600
806    }
807  }
808}
809"#;
810
811    const PARTIAL_TOML: &str = r#"
812keys_path = "/partial/keys"
813
814[auth]
815enable = true
816
817[http]
818http_address = "127.0.0.1:8888"
819enable_doc = true
820"#;
821
822    const PARTIAL_YAML: &str = r#"
823keys_path: /partial/keys
824auth:
825  enable: true
826http:
827  http_address: 127.0.0.1:8888
828  enable_doc: true
829"#;
830
831    const PARTIAL_JSON: &str = r#"
832{
833  "keys_path": "/partial/keys",
834  "auth": {
835    "enable": true
836  },
837  "http": {
838    "http_address": "127.0.0.1:8888",
839    "enable_doc": true
840  }
841}
842"#;
843
844    #[test]
845    fn build_config_reads_full_toml() {
846        let path = write_config("toml", FULL_TOML);
847        let config = build_config(path.to_str().unwrap()).expect("toml config");
848        assert_full_config(config);
849    }
850
851    #[test]
852    fn build_config_reads_full_yaml() {
853        let path = write_config("yaml", FULL_YAML);
854        let config = build_config(path.to_str().unwrap()).expect("yaml config");
855        assert_full_config(config);
856    }
857
858    #[test]
859    fn build_config_reads_full_json() {
860        let path = write_config("json", FULL_JSON);
861        let config = build_config(path.to_str().unwrap()).expect("json config");
862        assert_full_config(config);
863    }
864
865    #[test]
866    fn build_config_fills_defaults_for_partial_toml() {
867        let path = write_config("toml", PARTIAL_TOML);
868        let config =
869            build_config(path.to_str().unwrap()).expect("partial toml config");
870        assert_partial_defaults(config);
871    }
872
873    #[test]
874    fn build_config_fills_defaults_for_partial_yaml() {
875        let path = write_config("yaml", PARTIAL_YAML);
876        let config =
877            build_config(path.to_str().unwrap()).expect("partial yaml config");
878        assert_partial_defaults(config);
879    }
880
881    #[test]
882    fn build_config_fills_defaults_for_partial_json() {
883        let path = write_config("json", PARTIAL_JSON);
884        let config =
885            build_config(path.to_str().unwrap()).expect("partial json config");
886        assert_partial_defaults(config);
887    }
888
889    fn write_config(extension: &str, content: &str) -> TempPath {
890        let file = tempfile::Builder::new()
891            .suffix(&format!(".{extension}"))
892            .tempfile()
893            .expect("create temp config file");
894        std::fs::write(file.path(), content).expect("write temp config");
895        file.into_temp_path()
896    }
897
898    fn assert_full_config(config: BridgeConfig) {
899        assert_eq!(config.keys_path, PathBuf::from("/custom/keys"));
900
901        let node = &config.node;
902        assert_eq!(node.keypair_algorithm, KeyPairAlgorithm::Ed25519);
903        assert_eq!(node.hash_algorithm, HashAlgorithm::Blake3);
904        assert!(node.always_accept);
905        assert_eq!(node.contracts_path, PathBuf::from("/contracts_proof"));
906        assert_eq!(node.tracking_size, 200);
907        assert!(node.is_service);
908        assert!(node.only_clear_events);
909        assert_eq!(node.sync.ledger_batch_size, 150);
910        assert_eq!(node.sync.governance.interval_secs, 20);
911        assert_eq!(node.sync.governance.sample_size, 2);
912        assert_eq!(node.sync.governance.response_timeout_secs, 7);
913        assert_eq!(node.sync.tracker.interval_secs, 30);
914        assert_eq!(node.sync.tracker.page_size, 200);
915        assert_eq!(node.sync.tracker.response_timeout_secs, 8);
916        assert_eq!(node.sync.tracker.update_batch_size, 2);
917        assert_eq!(node.sync.tracker.update_timeout_secs, 6);
918        assert_eq!(
919            node.internal_db.db,
920            AveInternalDBFeatureConfig::build(&PathBuf::from("/data/ave.db"))
921        );
922
923        assert!(node.internal_db.durability);
924        match &node.spec {
925            Some(MachineSpec::Custom { ram_mb, cpu_cores }) => {
926                assert_eq!(*ram_mb, 2048);
927                assert_eq!(*cpu_cores, 4);
928            }
929            _ => panic!("Expected MachineSpec::Custom"),
930        }
931        assert_eq!(
932            node.external_db.db,
933            AveExternalDBFeatureConfig::build(&PathBuf::from("/data/ext.db"))
934        );
935        assert!(node.external_db.durability);
936
937        assert_eq!(node.network.node_type, NodeType::Addressable);
938        assert_eq!(
939            node.network.listen_addresses,
940            vec![
941                "/ip4/127.0.0.1/tcp/5001".to_owned(),
942                "/ip4/127.0.0.1/tcp/5002".to_owned()
943            ]
944        );
945        assert_eq!(
946            node.network.external_addresses,
947            vec!["/ip4/10.0.0.1/tcp/7000".to_owned()]
948        );
949        let expected_boot_nodes = vec![
950            RoutingNode {
951                peer_id: "12D3KooWNode1".to_owned(),
952                address: vec!["/ip4/1.1.1.1/tcp/1000".to_owned()],
953            },
954            RoutingNode {
955                peer_id: "12D3KooWNode2".to_owned(),
956                address: vec!["/ip4/2.2.2.2/tcp/2000".to_owned()],
957            },
958        ];
959        assert_eq!(node.network.boot_nodes.len(), expected_boot_nodes.len());
960        for expected in expected_boot_nodes {
961            let Some(actual) = node
962                .network
963                .boot_nodes
964                .iter()
965                .find(|node| node.peer_id == expected.peer_id)
966            else {
967                panic!("boot node {} missing", expected.peer_id);
968            };
969            assert_eq!(actual.address, expected.address);
970        }
971        assert!(!node.network.routing.get_dht_random_walk());
972        assert_eq!(node.network.routing.get_discovery_limit(), 25);
973        assert!(node.network.routing.get_allow_private_address_in_dht());
974        assert!(node.network.routing.get_allow_dns_address_in_dht());
975        assert!(node.network.routing.get_allow_loop_back_address_in_dht());
976        assert!(!node.network.routing.get_kademlia_disjoint_query_paths());
977        assert!(node.network.control_list.get_enable());
978        assert_eq!(
979            node.network.control_list.get_allow_list(),
980            vec!["Peer200", "Peer300"]
981        );
982        assert_eq!(
983            node.network.control_list.get_block_list(),
984            vec!["Peer1", "Peer2"]
985        );
986        assert_eq!(
987            node.network.control_list.get_service_allow_list(),
988            vec!["http://allow.local/list"]
989        );
990        assert_eq!(
991            node.network.control_list.get_service_block_list(),
992            vec!["http://block.local/list"]
993        );
994        assert_eq!(
995            node.network.control_list.get_interval_request(),
996            Duration::from_secs(42)
997        );
998        assert_eq!(
999            node.network.control_list.get_request_timeout(),
1000            Duration::from_secs(7)
1001        );
1002        assert_eq!(node.network.control_list.get_max_concurrent_requests(), 16);
1003        assert_eq!(
1004            node.network.memory_limits,
1005            MemoryLimitsConfig::Percentage { value: 0.8 }
1006        );
1007        assert_eq!(node.network.max_app_message_bytes, 2097152);
1008        assert_eq!(node.network.max_pending_outbound_bytes_per_peer, 16777216);
1009        assert_eq!(node.network.max_pending_inbound_bytes_per_peer, 8388608);
1010        assert_eq!(node.network.max_pending_outbound_bytes_total, 33554432);
1011        assert_eq!(node.network.max_pending_inbound_bytes_total, 25165824);
1012        let logging = &config.logging;
1013        assert_eq!(
1014            logging.output,
1015            LoggingOutput {
1016                stdout: false,
1017                file: true,
1018                api: true
1019            }
1020        );
1021        assert_eq!(
1022            logging.api_url.as_deref(),
1023            Some("https://example.com/logs")
1024        );
1025        assert_eq!(logging.file_path, PathBuf::from("/tmp/my.log"));
1026        assert_eq!(logging.rotation, LoggingRotation::Hourly);
1027        assert_eq!(logging.max_size, 52_428_800);
1028        assert_eq!(logging.max_files, 5);
1029        assert_eq!(logging.level, "debug");
1030
1031        let mut expected_sinks = BTreeMap::new();
1032        expected_sinks.insert(
1033            "primary".to_owned(),
1034            vec![
1035                SinkServer {
1036                    server: "SinkOne".to_owned(),
1037                    events: BTreeSet::from([SinkTypes::All, SinkTypes::Create]),
1038                    url: "https://sink.one".to_owned(),
1039                    auth: true,
1040                    concurrency: 4,
1041                    queue_capacity: 2048,
1042                    queue_policy: SinkQueuePolicy::DropOldest,
1043                    routing_strategy: SinkRoutingStrategy::UnorderedRoundRobin,
1044                    connect_timeout_ms: 5_000,
1045                    request_timeout_ms: 30_000,
1046                    max_retries: 5,
1047                },
1048                SinkServer {
1049                    server: "SinkTwo".to_owned(),
1050                    events: BTreeSet::from([SinkTypes::Transfer]),
1051                    url: "https://sink.two".to_owned(),
1052                    auth: false,
1053                    concurrency: 2,
1054                    queue_capacity: 512,
1055                    queue_policy: SinkQueuePolicy::DropNewest,
1056                    routing_strategy: SinkRoutingStrategy::OrderedBySubject,
1057                    connect_timeout_ms: 3_000,
1058                    request_timeout_ms: 15_000,
1059                    max_retries: 1,
1060                },
1061            ],
1062        );
1063        assert_eq!(config.sink.sinks, expected_sinks);
1064        assert_eq!(config.sink.auth, "https://auth.service");
1065        assert_eq!(config.sink.username, "sink-user");
1066
1067        let auth = &config.auth;
1068        assert!(auth.enable);
1069        assert!(auth.durability);
1070        assert_eq!(auth.database_path, PathBuf::from("/var/db/auth.db"));
1071        assert_eq!(auth.superadmin, "admin:supersecret");
1072        assert_eq!(auth.api_key.default_ttl_seconds, 3600);
1073        assert_eq!(auth.api_key.max_keys_per_user, 20);
1074        assert_eq!(auth.api_key.prefix, "custom_prefix_");
1075        assert_eq!(auth.lockout.max_attempts, 3);
1076        assert_eq!(auth.lockout.duration_seconds, 600);
1077        assert!(!auth.rate_limit.enable);
1078        assert_eq!(auth.rate_limit.window_seconds, 120);
1079        assert_eq!(auth.rate_limit.max_requests, 50);
1080        assert!(!auth.rate_limit.limit_by_key);
1081        assert!(auth.rate_limit.limit_by_ip);
1082        assert_eq!(auth.rate_limit.cleanup_interval_seconds, 1800);
1083        assert_eq!(auth.rate_limit.sensitive_endpoints.len(), 1);
1084        assert_eq!(auth.rate_limit.sensitive_endpoints[0].endpoint, "/login");
1085        assert_eq!(auth.rate_limit.sensitive_endpoints[0].max_requests, 5);
1086        assert_eq!(
1087            auth.rate_limit.sensitive_endpoints[0].window_seconds,
1088            Some(30)
1089        );
1090        assert!(!auth.session.audit_enable);
1091        assert_eq!(auth.session.audit_retention_days, 30);
1092        assert_eq!(auth.session.audit_max_entries, 1_000_000);
1093
1094        let http = &config.http;
1095        assert_eq!(http.http_address, "127.0.0.1:4000");
1096        assert_eq!(http.https_address.as_deref(), Some("127.0.0.1:4443"));
1097        assert_eq!(
1098            http.https_cert_path.as_deref(),
1099            Some(PathBuf::from("/certs/cert.pem").as_path())
1100        );
1101        assert_eq!(
1102            http.https_private_key_path.as_deref(),
1103            Some(PathBuf::from("/certs/key.pem").as_path())
1104        );
1105        assert!(http.enable_doc);
1106        assert_eq!(http.proxy.trusted_proxies, vec!["10.0.0.1".to_owned()]);
1107        assert!(!http.proxy.trust_x_forwarded_for);
1108        assert!(!http.proxy.trust_x_real_ip);
1109        assert!(!http.cors.enabled);
1110        assert!(!http.cors.allow_any_origin);
1111        assert_eq!(http.cors.allowed_origins, vec!["https://app.example.com"]);
1112        assert!(http.cors.allow_credentials);
1113        assert!(http.self_signed_cert.enabled);
1114        assert_eq!(http.self_signed_cert.common_name, "localhost");
1115        assert_eq!(
1116            http.self_signed_cert.san,
1117            vec!["127.0.0.1".to_owned(), "::1".to_owned()]
1118        );
1119        assert_eq!(http.self_signed_cert.validity_days, 365);
1120        assert_eq!(http.self_signed_cert.renew_before_days, 30);
1121        assert_eq!(http.self_signed_cert.check_interval_secs, 3600);
1122    }
1123
1124    fn assert_partial_defaults(config: BridgeConfig) {
1125        assert_eq!(config.keys_path, PathBuf::from("/partial/keys"));
1126        assert!(config.auth.enable);
1127        assert_eq!(config.http.http_address, "127.0.0.1:8888");
1128        assert!(config.http.enable_doc);
1129
1130        // Defaults remain for everything not provided.
1131        assert_eq!(config.logging.output.stdout, true);
1132        assert_eq!(config.logging.output.file, false);
1133        assert_eq!(config.logging.rotation, LoggingRotation::Size);
1134        assert_eq!(config.logging.file_path, PathBuf::from("logs"));
1135        assert_eq!(config.logging.max_files, 3);
1136        assert_eq!(config.sink.sinks.len(), 0);
1137
1138        assert_eq!(config.node.keypair_algorithm, KeyPairAlgorithm::Ed25519);
1139        assert_eq!(config.node.hash_algorithm, HashAlgorithm::Blake3);
1140        assert_eq!(config.node.contracts_path, PathBuf::from("contracts"));
1141        assert_eq!(
1142            config.node.internal_db.db,
1143            AveInternalDBFeatureConfig::default()
1144        );
1145        assert_eq!(
1146            config.node.external_db.db,
1147            AveExternalDBFeatureConfig::default()
1148        );
1149        assert_eq!(config.node.tracking_size, 100);
1150        assert!(!config.node.is_service);
1151        assert!(!config.node.only_clear_events);
1152        assert_eq!(config.node.sync.ledger_batch_size, 100);
1153        assert_eq!(config.node.sync.governance.interval_secs, 60);
1154        assert_eq!(config.node.sync.governance.sample_size, 3);
1155        assert_eq!(config.node.sync.governance.response_timeout_secs, 10);
1156        assert_eq!(config.node.sync.tracker.interval_secs, 30);
1157        assert_eq!(config.node.sync.tracker.page_size, 50);
1158        assert_eq!(config.node.sync.tracker.response_timeout_secs, 10);
1159        assert_eq!(config.node.sync.tracker.update_batch_size, 2);
1160        assert_eq!(config.node.sync.tracker.update_timeout_secs, 10);
1161        assert_eq!(config.node.network.node_type, NodeType::Bootstrap);
1162        assert!(config.node.network.listen_addresses.is_empty());
1163        assert!(config.node.network.external_addresses.is_empty());
1164        assert!(config.node.network.boot_nodes.is_empty());
1165        assert_eq!(
1166            config.node.network.control_list.get_interval_request(),
1167            Duration::from_secs(60)
1168        );
1169        assert_eq!(
1170            config.node.network.control_list.get_request_timeout(),
1171            Duration::from_secs(5)
1172        );
1173        assert_eq!(
1174            config
1175                .node
1176                .network
1177                .control_list
1178                .get_max_concurrent_requests(),
1179            8
1180        );
1181        assert_eq!(config.node.network.max_app_message_bytes, 1024 * 1024);
1182        assert_eq!(
1183            config.node.network.max_pending_outbound_bytes_per_peer,
1184            8 * 1024 * 1024
1185        );
1186        assert_eq!(
1187            config.node.network.max_pending_inbound_bytes_per_peer,
1188            8 * 1024 * 1024
1189        );
1190        assert_eq!(config.node.network.max_pending_outbound_bytes_total, 0);
1191        assert_eq!(config.node.network.max_pending_inbound_bytes_total, 0);
1192        assert!(config.node.spec.is_none());
1193
1194        // node defaults
1195        assert!(!config.node.always_accept);
1196        assert!(!config.node.internal_db.durability);
1197        assert!(!config.node.external_db.durability);
1198        assert_eq!(
1199            config.node.network.memory_limits,
1200            MemoryLimitsConfig::Disabled
1201        );
1202
1203        // auth defaults
1204        assert!(!config.auth.durability);
1205        assert_eq!(config.auth.api_key.prefix, "ave_node_");
1206
1207        // http.cors defaults
1208        assert!(config.http.cors.enabled);
1209        assert!(config.http.cors.allow_any_origin);
1210        assert!(config.http.cors.allowed_origins.is_empty());
1211        assert!(!config.http.cors.allow_credentials);
1212
1213        // http.proxy defaults
1214        assert!(config.http.proxy.trusted_proxies.is_empty());
1215        assert!(config.http.proxy.trust_x_forwarded_for);
1216        assert!(config.http.proxy.trust_x_real_ip);
1217
1218        // http.self_signed_cert defaults
1219        assert!(!config.http.self_signed_cert.enabled);
1220        assert_eq!(config.http.self_signed_cert.common_name, "localhost");
1221        assert_eq!(
1222            config.http.self_signed_cert.san,
1223            vec!["127.0.0.1".to_owned(), "::1".to_owned()]
1224        );
1225        assert_eq!(config.http.self_signed_cert.validity_days, 365);
1226        assert_eq!(config.http.self_signed_cert.renew_before_days, 30);
1227        assert_eq!(config.http.self_signed_cert.check_interval_secs, 3600);
1228    }
1229
1230    #[test]
1231    fn build_config_rejects_invalid_network_memory_limits() {
1232        const INVALID_TOML: &str = r#"
1233        [node.network.memory_limits]
1234        type = "percentage"
1235        value = 2.0
1236        "#;
1237
1238        let path = write_config("toml", INVALID_TOML);
1239        let err =
1240            build_config(path.to_str().unwrap()).expect_err("invalid config");
1241
1242        match err {
1243            BridgeError::ConfigBuild(msg) => {
1244                assert!(msg.contains("network.memory_limits percentage"));
1245            }
1246            other => panic!("unexpected error: {other:?}"),
1247        }
1248    }
1249
1250    #[test]
1251    fn build_config_rejects_invalid_network_message_limits() {
1252        const INVALID_TOML: &str = r#"
1253        [node.network]
1254        max_app_message_bytes = 0
1255        "#;
1256
1257        let path = write_config("toml", INVALID_TOML);
1258        let err =
1259            build_config(path.to_str().unwrap()).expect_err("invalid config");
1260
1261        match err {
1262            BridgeError::ConfigBuild(msg) => {
1263                assert!(msg.contains("max_app_message_bytes"));
1264            }
1265            other => panic!("unexpected error: {other:?}"),
1266        }
1267    }
1268
1269    #[test]
1270    fn build_config_rejects_invalid_control_list_timeout() {
1271        const INVALID_TOML: &str = r#"
1272        [node.network.control_list]
1273        interval_request = 30
1274        request_timeout = 40
1275        "#;
1276
1277        let path = write_config("toml", INVALID_TOML);
1278        let err =
1279            build_config(path.to_str().unwrap()).expect_err("invalid config");
1280
1281        match err {
1282            BridgeError::ConfigBuild(msg) => {
1283                assert!(msg.contains("request_timeout"));
1284            }
1285            other => panic!("unexpected error: {other:?}"),
1286        }
1287    }
1288
1289    #[test]
1290    fn build_config_allows_zero_control_list_max_concurrency() {
1291        const ZERO_TOML: &str = r#"
1292        [node.network.control_list]
1293        max_concurrent_requests = 0
1294        "#;
1295
1296        let path = write_config("toml", ZERO_TOML);
1297        let config = build_config(path.to_str().unwrap())
1298            .expect("zero max_concurrent_requests should be accepted");
1299        assert_eq!(
1300            config
1301                .node
1302                .network
1303                .control_list
1304                .get_max_concurrent_requests(),
1305            0
1306        );
1307    }
1308
1309    #[test]
1310    fn build_config_allows_zero_pending_queue_limits() {
1311        const ZERO_LIMITS_TOML: &str = r#"
1312        [node.network]
1313        max_pending_outbound_bytes_per_peer = 0
1314        max_pending_inbound_bytes_per_peer = 0
1315        max_pending_outbound_bytes_total = 0
1316        max_pending_inbound_bytes_total = 0
1317        "#;
1318
1319        let path = write_config("toml", ZERO_LIMITS_TOML);
1320        let config = build_config(path.to_str().unwrap())
1321            .expect("zero queue limits should be accepted");
1322
1323        assert_eq!(config.node.network.max_pending_outbound_bytes_per_peer, 0);
1324        assert_eq!(config.node.network.max_pending_inbound_bytes_per_peer, 0);
1325        assert_eq!(config.node.network.max_pending_outbound_bytes_total, 0);
1326        assert_eq!(config.node.network.max_pending_inbound_bytes_total, 0);
1327    }
1328}