1- use std:: fs;
21use std:: sync:: Arc ;
32use std:: time:: Duration ;
43
@@ -9,77 +8,138 @@ use apollo_config_manager_types::communication::{
98 SharedConfigManagerClient ,
109} ;
1110use apollo_consensus_config:: config:: ConsensusDynamicConfig ;
12- use apollo_node_config:: config_utils:: DeploymentBaseAppConfig ;
13- use apollo_node_config:: definitions:: ConfigPointersMap ;
11+ use apollo_node_config:: config_utils:: normalize_pointer_groups;
1412use apollo_node_config:: node_config:: { NodeDynamicConfig , SequencerNodeConfig } ;
1513use serde_json:: Value ;
1614use starknet_api:: core:: ContractAddress ;
17- use tempfile:: NamedTempFile ;
15+ use tempfile:: TempDir ;
1816use tokio:: sync:: mpsc:: channel;
1917use tokio:: task:: yield_now;
2018use tokio:: time:: { interval, timeout} ;
2119use tracing_test:: traced_test;
2220
2321use crate :: config_manager_runner:: ConfigManagerRunner ;
2422
25- // An arbitrary hex-str config entry to be replaced.
26- const VALIDATOR_ID_CONFIG_ENTRY : & str = "validator_id" ;
23+ // The nested native path of the validator id within the `SequencerNodeConfig` field hierarchy.
24+ const VALIDATOR_ID_CONFIG_PATH : & [ & str ] =
25+ & [ "consensus_manager_config" , "consensus_manager_config" , "dynamic_config" , "validator_id" ] ;
2726const TEST_TIMEOUT_SECS : u64 = 1 ;
27+ const BASE_CONFIG_FILE_NAME : & str = "base_config.json" ;
28+ const SECRETS_CONFIG_FILE_NAME : & str = "secrets_config.json" ;
29+
30+ // `Sensitive<T>` fields whose derived `Serialize` emits an asymmetric wire shape (a JSON array or
31+ // `null`) that their string-reading `deserialize_with` cannot consume, and whose deserializer maps
32+ // the empty string back to `None`. A whole-config `to_value`/`from_value` round-trip fails on them
33+ // unless they are rewritten to that empty string (see
34+ // `apollo_node_config::config_serde_symmetry_test` and `apollo_config::converters`). The native
35+ // node loader avoids this in production by overlaying real values from the secrets file; this test
36+ // carries no secrets, so it neutralizes them instead.
37+ const NONE_ABLE_SENSITIVE_KEYS : & [ & str ] = & [ "secret_key" , "url_header_list" ] ;
38+
39+ // `EthereumBaseLayerConfig::ordered_l1_endpoint_urls: Vec<Sensitive<Url>>` shares the same
40+ // serialize-array/deserialize-string asymmetry, but its deserializer reads a (space-separated) URL
41+ // list and `validate_node_config` rejects an empty list, so it has no `None`/empty form to
42+ // substitute. Rewrite it to a single valid URL string so the config both round-trips and validates.
43+ const ENDPOINT_URLS_KEY : & str = "ordered_l1_endpoint_urls" ;
44+ const PLACEHOLDER_ENDPOINT_URL : & str = "https://localhost:8545" ;
45+
46+ /// Recursively rewrites the asymmetric `Sensitive` fields in a nested native config map to the
47+ /// string wire shape their deserializers read, so the config round-trips through serde and passes
48+ /// validation.
49+ fn neutralize_sensitive_fields ( config : & mut Value ) {
50+ match config {
51+ Value :: Object ( map) => {
52+ for ( key, value) in map. iter_mut ( ) {
53+ if NONE_ABLE_SENSITIVE_KEYS . contains ( & key. as_str ( ) ) {
54+ * value = Value :: String ( String :: new ( ) ) ;
55+ } else if key == ENDPOINT_URLS_KEY {
56+ * value = Value :: String ( PLACEHOLDER_ENDPOINT_URL . to_string ( ) ) ;
57+ } else {
58+ neutralize_sensitive_fields ( value) ;
59+ }
60+ }
61+ }
62+ Value :: Array ( items) => items. iter_mut ( ) . for_each ( neutralize_sensitive_fields) ,
63+ _ => { }
64+ }
65+ }
2866
29- /// Creates a temporary config file with specific test values and returns CLI args pointing to it.
30- fn create_temp_config_file_and_args ( ) -> ( NamedTempFile , Vec < String > , String ) {
31- let config = SequencerNodeConfig :: default ( ) ;
32- let config_pointers_map = ConfigPointersMap :: create_for_testing ( ) ;
33-
34- let base_app_config = DeploymentBaseAppConfig :: new ( config, config_pointers_map) ;
67+ /// Returns a mutable reference to the leaf `validator_id` value within a nested native config map.
68+ fn validator_id_entry ( config : & mut Value ) -> & mut Value {
69+ let mut entry = config;
70+ for segment in VALIDATOR_ID_CONFIG_PATH {
71+ entry = entry
72+ . as_object_mut ( )
73+ . expect ( "Native config node must be a JSON object" )
74+ . get_mut ( * segment)
75+ . unwrap_or_else ( || panic ! ( "Missing native config segment {segment:?}" ) ) ;
76+ }
77+ entry
78+ }
3579
36- // Create a temporary file
37- let temp_file = NamedTempFile :: new ( ) . expect ( "Failed to create temporary config file" ) ;
80+ /// Creates a temporary directory holding a native base config file and an (empty) secrets file,
81+ /// and returns CLI args pointing the native loader at both. The directory handle is returned so the
82+ /// files outlive the test. Also returns the validator id read from the base config.
83+ fn create_temp_config_files_and_args ( ) -> ( TempDir , Vec < String > , String ) {
84+ let mut config = SequencerNodeConfig :: default ( ) ;
85+ // `SequencerNodeConfig::default()` does not have internally consistent pointer-group values
86+ // (chain_id, fee tokens, etc.); reconcile them so the loaded config passes the cross-component
87+ // equality validation, mirroring what pointer resolution did at load time.
88+ normalize_pointer_groups ( & mut config) ;
89+ let mut base_config =
90+ serde_json:: to_value ( & config) . expect ( "Should be able to serialize config to value" ) ;
91+ neutralize_sensitive_fields ( & mut base_config) ;
92+
93+ let validator_id = validator_id_entry ( & mut base_config)
94+ . as_str ( )
95+ . expect ( "validator_id must be a string" )
96+ . to_string ( ) ;
3897
39- base_app_config. dump_config_file ( temp_file. path ( ) ) ;
98+ let temp_dir = TempDir :: new ( ) . expect ( "Failed to create temporary config dir" ) ;
99+ let base_config_path = temp_dir. path ( ) . join ( BASE_CONFIG_FILE_NAME ) ;
100+ let secrets_config_path = temp_dir. path ( ) . join ( SECRETS_CONFIG_FILE_NAME ) ;
40101
41- let current_validator_id = base_app_config
42- . as_value ( )
43- . get ( VALIDATOR_ID_CONFIG_ENTRY )
44- . and_then ( |v| v. as_str ( ) )
45- . expect ( "Missing or non-string hex value at VALIDATOR_ID_CONFIG_ENTRY" )
46- . to_string ( ) ;
102+ let base_content =
103+ serde_json:: to_string_pretty ( & base_config) . expect ( "Failed to serialize base config" ) ;
104+ std:: fs:: write ( & base_config_path, base_content) . expect ( "Failed to write base config file" ) ;
105+ // The native loader requires a secrets file overlaid onto the base; the test carries none.
106+ std:: fs:: write ( & secrets_config_path, "{}" ) . expect ( "Failed to write secrets config file" ) ;
47107
48- // Create cli args pointing to the temp file
108+ // The native loader expects two files: the nested base config and the flat secrets config.
49109 let cli_args = vec ! [
50110 "test_node" . to_string( ) ,
51111 CONFIG_FILE_ARG . to_string( ) ,
52- temp_file. path( ) . to_string_lossy( ) . to_string( ) ,
112+ base_config_path. to_string_lossy( ) . to_string( ) ,
113+ CONFIG_FILE_ARG . to_string( ) ,
114+ secrets_config_path. to_string_lossy( ) . to_string( ) ,
53115 ] ;
54116
55- ( temp_file , cli_args, current_validator_id )
117+ ( temp_dir , cli_args, validator_id )
56118}
57119
58- fn update_config_file ( temp_file : & NamedTempFile ) -> String {
120+ /// Bumps the validator id in the native base config file by one and returns the new value.
121+ fn update_config_file ( temp_dir : & TempDir ) -> String {
122+ let base_config_path = temp_dir. path ( ) . join ( BASE_CONFIG_FILE_NAME ) ;
59123 let current_content =
60- fs:: read_to_string ( temp_file . path ( ) ) . expect ( "Failed to read temp config file" ) ;
124+ std :: fs:: read_to_string ( & base_config_path ) . expect ( "Failed to read base config file" ) ;
61125
62- // Parse JSON (expects a top-level object/map)
63- let mut root: Value = serde_json:: from_str ( & current_content) . expect ( "Config is not valid JSON" ) ;
64- let obj = root. as_object_mut ( ) . expect ( "Config root must be a JSON object" ) ;
126+ let mut base_config: Value =
127+ serde_json:: from_str ( & current_content) . expect ( "Config is not valid JSON" ) ;
65128
66- // Get the hex string at the key VALIDATOR_ID_CONFIG_ENTRY (e.g., "validator_id": "0x00ff")
67- let current_validator_id = obj
68- . get ( VALIDATOR_ID_CONFIG_ENTRY )
69- . and_then ( |v| v. as_str ( ) )
70- . expect ( "Missing or non-string hex value at VALIDATOR_ID_CONFIG_ENTRY" ) ;
129+ let validator_id_value = validator_id_entry ( & mut base_config) ;
130+ let current_validator_id = validator_id_value. as_str ( ) . expect ( "validator_id must be a string" ) ;
71131 assert ! ( current_validator_id. starts_with( "0x" ) , "Expected a 0x-prefixed hex string" ) ;
72132
73- // Bump by 1 and preserve width
133+ // Bump by 1 and preserve width.
74134 let hex = & current_validator_id[ 2 ..] ; // drop "0x"
75135 let n = u128:: from_str_radix ( hex, 16 ) . unwrap ( ) + 1 ;
76136 let new_validator_id = format ! ( "0x{:0x}" , n) ;
77137
78- // Update JSON and write back
79- obj . insert ( VALIDATOR_ID_CONFIG_ENTRY . to_string ( ) , Value :: String ( new_validator_id . clone ( ) ) ) ;
80- let updated_content = serde_json:: to_string_pretty ( & root ) . expect ( "Failed to serialize JSON" ) ;
81- fs:: write ( temp_file . path ( ) , updated_content)
82- . expect ( "Failed to write updated config to temp file" ) ;
138+ * validator_id_value = Value :: String ( new_validator_id . clone ( ) ) ;
139+ let updated_content =
140+ serde_json:: to_string_pretty ( & base_config ) . expect ( "Failed to serialize JSON" ) ;
141+ std :: fs:: write ( & base_config_path , updated_content)
142+ . expect ( "Failed to write updated config to base config file" ) ;
83143
84144 new_validator_id
85145}
@@ -94,8 +154,8 @@ async fn config_manager_runner_update_config_with_changed_values() {
94154 // Set a config manager config.
95155 let config_manager_config = ConfigManagerConfig :: default ( ) ;
96156
97- // Create a temporary config file and get the validator id value.
98- let ( temp_file , cli_args, validator_id_value) = create_temp_config_file_and_args ( ) ;
157+ // Create temporary config files and get the validator id value.
158+ let ( temp_dir , cli_args, validator_id_value) = create_temp_config_files_and_args ( ) ;
99159
100160 let node_dynamic_config = NodeDynamicConfig :: default ( ) ;
101161
@@ -128,7 +188,7 @@ async fn config_manager_runner_update_config_with_changed_values() {
128188 ) ;
129189
130190 // Edit the config file and then trigger a config update, expecting the new validator id.
131- let new_validator_id = update_config_file ( & temp_file ) ;
191+ let new_validator_id = update_config_file ( & temp_dir ) ;
132192 let expected_validator_id = ContractAddress :: from ( hex_to_u128 ( new_validator_id. as_str ( ) ) ) ;
133193
134194 let second_update_config_result = config_manager_runner. update_config ( ) . await ;
@@ -145,8 +205,8 @@ async fn config_manager_runner_update_config_with_changed_values() {
145205
146206#[ tokio:: test]
147207async fn watcher_triggers_update_on_file_change ( ) {
148- // Prepare temp config file and CLI args.
149- let ( temp_file , cli_args, _) = create_temp_config_file_and_args ( ) ;
208+ // Prepare temp config files and CLI args.
209+ let ( temp_dir , cli_args, _) = create_temp_config_files_and_args ( ) ;
150210
151211 // Channel to observe that update_config was called.
152212 let ( tx, mut rx) = channel ( 1 ) ;
@@ -174,7 +234,7 @@ async fn watcher_triggers_update_on_file_change() {
174234 yield_now ( ) . await ;
175235
176236 // Modify the config file to trigger an event.
177- let _ = update_config_file ( & temp_file ) ;
237+ let _ = update_config_file ( & temp_dir ) ;
178238
179239 // Wait until the update call is observed or timeout.
180240 timeout ( Duration :: from_secs ( TEST_TIMEOUT_SECS ) , rx. recv ( ) )
0 commit comments