@@ -7,11 +7,30 @@ use crate::{primitives::reth::engine_api_builder::EnginePeer, tx_signer::Signer}
7
7
use alloy_rpc_types_engine:: JwtSecret ;
8
8
use anyhow:: { anyhow, Result } ;
9
9
use reth_optimism_node:: args:: RollupArgs ;
10
+ use serde:: Deserialize ;
10
11
use std:: path:: PathBuf ;
11
12
use url:: Url ;
12
13
14
+ /// Configuration structure for engine peers loaded from TOML
15
+ #[ derive( Debug , Clone , Deserialize ) ]
16
+ pub struct EnginePeersConfig {
17
+ /// Default JWT file path used by all peers unless overridden
18
+ pub default_jwt_path : PathBuf ,
19
+ /// List of engine peers
20
+ pub peers : Vec < EnginePeerConfig > ,
21
+ }
22
+
23
+ /// Configuration for a single engine peer
24
+ #[ derive( Debug , Clone , Deserialize ) ]
25
+ pub struct EnginePeerConfig {
26
+ /// URL of the engine peer
27
+ pub url : Url ,
28
+ /// Optional JWT path override for this peer
29
+ pub jwt_path : Option < PathBuf > ,
30
+ }
31
+
13
32
/// Parameters for rollup configuration
14
- #[ derive( Debug , Clone , Default , PartialEq , Eq , clap:: Args ) ]
33
+ #[ derive( Debug , Clone , Default , clap:: Args ) ]
15
34
#[ command( next_help_heading = "Rollup" ) ]
16
35
pub struct OpRbuilderArgs {
17
36
/// Rollup configuration
@@ -51,8 +70,8 @@ pub struct OpRbuilderArgs {
51
70
pub playground : Option < PathBuf > ,
52
71
#[ command( flatten) ]
53
72
pub flashblocks : FlashblocksArgs ,
54
- /// List or builders in the network that FCU would be propagated to
55
- #[ arg( long = "builder.engine-api-peer " , value_parser = parse_engine_peer_arg , action = clap :: ArgAction :: Append ) ]
73
+ /// Path to TOML configuration file for engine peers
74
+ #[ arg( long = "builder.engine-peers-config " , env = "ENGINE_PEERS_CONFIG" , value_parser = parse_engine_peers_config ) ]
56
75
pub engine_peers : Vec < EnginePeer > ,
57
76
}
58
77
@@ -64,39 +83,6 @@ fn expand_path(s: &str) -> Result<PathBuf> {
64
83
. map_err ( |e| anyhow ! ( "invalid path after expansion: {e}" ) )
65
84
}
66
85
67
- /// Parse engine peer configuration string for clap argument parsing.
68
- ///
69
- /// Format: "url@jwt_path" (JWT path is required)
70
- /// - url: HTTP/HTTPS endpoint of the peer builder
71
- /// - jwt_path: File path to JWT token for authentication (required after @)
72
- fn parse_engine_peer_arg ( s : & str ) -> Result < EnginePeer > {
73
- let s = s. trim ( ) ;
74
-
75
- if s. is_empty ( ) {
76
- return Err ( anyhow ! ( "Engine peer cannot be empty" ) ) ;
77
- }
78
-
79
- // Find the @ delimiter - it's required
80
- // Caution: this will misshandle cases when pathname contains `@` symbols, we do not expect such filenames tho
81
- let ( url_part, jwt_path_part) = s. rsplit_once ( '@' ) . ok_or_else ( || anyhow ! ( "Engine peer must include JWT path after '@' (format: url@jwt_path). Urls with @ in the path are not accepted." ) ) ?;
82
-
83
- if url_part. is_empty ( ) {
84
- return Err ( anyhow ! ( "URL part cannot be empty" ) ) ;
85
- }
86
-
87
- if jwt_path_part. is_empty ( ) {
88
- return Err ( anyhow ! ( "JWT path cannot be empty (format: url@jwt_path)" ) ) ;
89
- }
90
-
91
- let url = Url :: parse ( url_part) ?;
92
-
93
- let jwt_path = PathBuf :: from ( jwt_path_part) ;
94
-
95
- let jwt_secret = JwtSecret :: from_file ( & jwt_path) ?;
96
-
97
- Ok ( EnginePeer :: new ( url, jwt_secret) )
98
- }
99
-
100
86
/// Parameters for Flashblocks configuration
101
87
/// The names in the struct are prefixed with `flashblocks` to avoid conflicts
102
88
/// with the standard block building configuration since these args are flattened
@@ -139,3 +125,134 @@ pub struct FlashblocksArgs {
139
125
) ]
140
126
pub flashblocks_block_time : u64 ,
141
127
}
128
+
129
+ impl EnginePeersConfig {
130
+ /// Load configuration from a TOML file
131
+ pub fn from_file ( path : & PathBuf ) -> Result < Self > {
132
+ let content = std:: fs:: read_to_string ( path) . map_err ( |e| {
133
+ anyhow ! (
134
+ "Failed to read engine peers config file {}: {}" ,
135
+ path. display( ) ,
136
+ e
137
+ )
138
+ } ) ?;
139
+
140
+ let config: Self = toml:: from_str ( & content) . map_err ( |e| {
141
+ anyhow ! (
142
+ "Failed to parse engine peers config file {}: {}" ,
143
+ path. display( ) ,
144
+ e
145
+ )
146
+ } ) ?;
147
+
148
+ Ok ( config)
149
+ }
150
+
151
+ /// Convert to vector of EnginePeer instances
152
+ pub fn to_engine_peers ( & self ) -> Result < Vec < EnginePeer > > {
153
+ let mut engine_peers = Vec :: new ( ) ;
154
+
155
+ for peer in & self . peers {
156
+ let jwt_path = peer. jwt_path . as_ref ( ) . unwrap_or ( & self . default_jwt_path ) ;
157
+ let jwt_secret = JwtSecret :: from_file ( jwt_path)
158
+ . map_err ( |e| anyhow ! ( "Failed to load JWT from {}: {}" , jwt_path. display( ) , e) ) ?;
159
+
160
+ engine_peers. push ( EnginePeer :: new ( peer. url . clone ( ) , jwt_secret) ) ;
161
+ }
162
+
163
+ Ok ( engine_peers)
164
+ }
165
+ }
166
+
167
+ /// Parse engine peers configuration from TOML file for clap
168
+ fn parse_engine_peers_config ( s : & str ) -> Result < Vec < EnginePeer > > {
169
+ let path = PathBuf :: from ( s) ;
170
+ let config = EnginePeersConfig :: from_file ( & path) ?;
171
+ config. to_engine_peers ( )
172
+ }
173
+
174
+ #[ cfg( test) ]
175
+ mod tests {
176
+ use super :: * ;
177
+ use std:: io:: Write ;
178
+ use tempfile:: NamedTempFile ;
179
+
180
+ #[ test]
181
+ fn test_engine_peers_config_parsing ( ) -> Result < ( ) > {
182
+ // Create temporary JWT files for testing
183
+ let mut temp_default_jwt = NamedTempFile :: new ( ) ?;
184
+ let mut temp_custom_jwt = NamedTempFile :: new ( ) ?;
185
+
186
+ // Write dummy JWT content (for testing purposes)
187
+ temp_default_jwt. write_all ( b"dummy.jwt.token" ) ?;
188
+ temp_custom_jwt. write_all ( b"custom.jwt.token" ) ?;
189
+ temp_default_jwt. flush ( ) ?;
190
+ temp_custom_jwt. flush ( ) ?;
191
+
192
+ let toml_content = format ! (
193
+ r#"
194
+ default_jwt_path = "{}"
195
+
196
+ [[peers]]
197
+ url = "http://builder1.example.com:8551"
198
+
199
+ [[peers]]
200
+ url = "http://builder2.example.com:8551"
201
+ jwt_path = "{}"
202
+ "# ,
203
+ temp_default_jwt. path( ) . display( ) ,
204
+ temp_custom_jwt. path( ) . display( )
205
+ ) ;
206
+
207
+ let config: EnginePeersConfig = toml:: from_str ( & toml_content) ?;
208
+
209
+ assert_eq ! ( config. peers. len( ) , 2 ) ;
210
+
211
+ // First peer should use default JWT
212
+ assert_eq ! (
213
+ config. peers[ 0 ] . url. as_str( ) ,
214
+ "http://builder1.example.com:8551"
215
+ ) ;
216
+
217
+ // Second peer should have custom JWT
218
+ assert_eq ! (
219
+ config. peers[ 1 ] . url. as_str( ) ,
220
+ "http://builder2.example.com:8551"
221
+ ) ;
222
+
223
+ // Test that we can convert to engine peers successfully
224
+ let engine_peers = config. to_engine_peers ( ) ?;
225
+ assert_eq ! ( engine_peers. len( ) , 2 ) ;
226
+
227
+ Ok ( ( ) )
228
+ }
229
+
230
+ #[ test]
231
+ fn test_engine_peers_config_from_file ( ) -> Result < ( ) > {
232
+ // Create temporary JWT file
233
+ let mut temp_jwt = NamedTempFile :: new ( ) ?;
234
+ temp_jwt. write_all ( b"test.jwt.token" ) ?;
235
+ temp_jwt. flush ( ) ?;
236
+
237
+ let toml_content = format ! (
238
+ r#"
239
+ default_jwt_path = "{}"
240
+
241
+ [[peers]]
242
+ url = "http://test.example.com:8551"
243
+ "# ,
244
+ temp_jwt. path( ) . display( )
245
+ ) ;
246
+
247
+ let mut temp_file = NamedTempFile :: new ( ) ?;
248
+ temp_file. write_all ( toml_content. as_bytes ( ) ) ?;
249
+ temp_file. flush ( ) ?;
250
+
251
+ let config = EnginePeersConfig :: from_file ( & temp_file. path ( ) . to_path_buf ( ) ) ?;
252
+
253
+ assert_eq ! ( config. peers. len( ) , 1 ) ;
254
+ assert_eq ! ( config. peers[ 0 ] . url. as_str( ) , "http://test.example.com:8551" ) ;
255
+
256
+ Ok ( ( ) )
257
+ }
258
+ }
0 commit comments