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 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_config(&bridge_config)?;
31
32 validate_network_config(&bridge_config)?;
34
35 Ok(bridge_config)
37}
38
39fn 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 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
231fn 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 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 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 assert!(!config.auth.durability);
1205 assert_eq!(config.auth.api_key.prefix, "ave_node_");
1206
1207 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 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 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}