1
1
use crate :: compute:: {
2
+ computed_file:: ComputedFile ,
2
3
errors:: ReplicateStatusCause ,
3
4
utils:: env_utils:: { TeeSessionEnvironmentVariable , get_env_var_or_error} ,
4
5
} ;
@@ -151,6 +152,67 @@ impl WorkerApiClient {
151
152
Err ( response. error_for_status ( ) . unwrap_err ( ) )
152
153
}
153
154
}
155
+
156
+ /// Sends the completed computed.json file to the worker host.
157
+ ///
158
+ /// This method transmits the computed file containing task results, signatures,
159
+ /// and metadata to the worker API. The computed file is sent as JSON in the
160
+ /// request body, allowing the worker to verify and process the computation results.
161
+ ///
162
+ /// # Arguments
163
+ ///
164
+ /// * `authorization` - The authorization token/challenge to validate the request on the worker side
165
+ /// * `chain_task_id` - The blockchain task identifier associated with this computation
166
+ /// * `computed_file` - The computed file containing results and signatures to be sent
167
+ ///
168
+ /// # Returns
169
+ ///
170
+ /// * `Ok(())` - If the computed file was successfully sent (HTTP 2xx response)
171
+ /// * `Err(Error)` - If the request failed due to an HTTP error
172
+ ///
173
+ /// # Example
174
+ ///
175
+ /// ```
176
+ /// use crate::api::worker_api::WorkerApiClient;
177
+ /// use crate::compute::computed_file::ComputedFile;
178
+ ///
179
+ /// let client = WorkerApiClient::new("http://worker:13100");
180
+ /// let computed_file = ComputedFile {
181
+ /// task_id: Some("0x123456789abcdef".to_string()),
182
+ /// result_digest: Some("0xdigest".to_string()),
183
+ /// enclave_signature: Some("0xsignature".to_string()),
184
+ /// ..Default::default()
185
+ /// };
186
+ ///
187
+ /// match client.send_computed_file_to_host(
188
+ /// "Bearer auth_token",
189
+ /// "0x123456789abcdef",
190
+ /// &computed_file,
191
+ /// ) {
192
+ /// Ok(()) => println!("Computed file sent successfully"),
193
+ /// Err(error) => eprintln!("Failed to send computed file: {}", error),
194
+ /// }
195
+ /// ```
196
+ pub fn send_computed_file_to_host (
197
+ & self ,
198
+ authorization : & str ,
199
+ chain_task_id : & str ,
200
+ computed_file : & ComputedFile ,
201
+ ) -> Result < ( ) , Error > {
202
+ let url = format ! ( "{}/compute/post/{}/computed" , self . base_url, chain_task_id) ;
203
+ let response = self
204
+ . client
205
+ . post ( & url)
206
+ . header ( AUTHORIZATION , authorization)
207
+ . json ( computed_file)
208
+ . send ( ) ?;
209
+
210
+ if response. status ( ) . is_success ( ) {
211
+ Ok ( ( ) )
212
+ } else {
213
+ Err ( response. error_for_status ( ) . unwrap_err ( ) )
214
+ }
215
+ }
154
216
}
155
217
156
218
#[ cfg( test) ]
@@ -164,6 +226,9 @@ mod tests {
164
226
matchers:: { body_json, header, method, path} ,
165
227
} ;
166
228
229
+ const CHALLENGE : & str = "challenge" ;
230
+ const CHAIN_TASK_ID : & str = "0x123456789abcdef" ;
231
+
167
232
// region ExitMessage()
168
233
#[ test]
169
234
fn should_serialize_exit_message ( ) {
@@ -213,9 +278,6 @@ mod tests {
213
278
// endregion
214
279
215
280
// region send_exit_cause_for_post_compute_stage()
216
- const CHALLENGE : & str = "challenge" ;
217
- const CHAIN_TASK_ID : & str = "0x123456789abcdef" ;
218
-
219
281
#[ tokio:: test]
220
282
async fn should_send_exit_cause ( ) {
221
283
let mock_server = MockServer :: start ( ) . await ;
@@ -282,4 +344,132 @@ mod tests {
282
344
}
283
345
}
284
346
// endregion
347
+
348
+ // region send_computed_file_to_host()
349
+ #[ tokio:: test]
350
+ async fn should_send_computed_file_successfully ( ) {
351
+ let mock_server = MockServer :: start ( ) . await ;
352
+ let server_uri = mock_server. uri ( ) ;
353
+
354
+ let computed_file = ComputedFile {
355
+ task_id : Some ( CHAIN_TASK_ID . to_string ( ) ) ,
356
+ result_digest : Some ( "0xdigest" . to_string ( ) ) ,
357
+ enclave_signature : Some ( "0xsignature" . to_string ( ) ) ,
358
+ ..Default :: default ( )
359
+ } ;
360
+
361
+ let expected_path = format ! ( "/compute/post/{}/computed" , CHAIN_TASK_ID ) ;
362
+ let expected_body = json ! ( computed_file) ;
363
+
364
+ Mock :: given ( method ( "POST" ) )
365
+ . and ( path ( expected_path. as_str ( ) ) )
366
+ . and ( header ( "Authorization" , CHALLENGE ) )
367
+ . and ( body_json ( & expected_body) )
368
+ . respond_with ( ResponseTemplate :: new ( 200 ) )
369
+ . expect ( 1 )
370
+ . mount ( & mock_server)
371
+ . await ;
372
+
373
+ let result = tokio:: task:: spawn_blocking ( move || {
374
+ let client = WorkerApiClient :: new ( & server_uri) ;
375
+ client. send_computed_file_to_host ( CHALLENGE , CHAIN_TASK_ID , & computed_file)
376
+ } )
377
+ . await
378
+ . expect ( "Task panicked" ) ;
379
+
380
+ assert ! ( result. is_ok( ) ) ;
381
+ }
382
+
383
+ #[ tokio:: test]
384
+ async fn should_fail_send_computed_file_on_server_error ( ) {
385
+ let mock_server = MockServer :: start ( ) . await ;
386
+ let server_uri = mock_server. uri ( ) ;
387
+
388
+ let computed_file = ComputedFile {
389
+ task_id : Some ( CHAIN_TASK_ID . to_string ( ) ) ,
390
+ result_digest : Some ( "0xdigest" . to_string ( ) ) ,
391
+ enclave_signature : Some ( "0xsignature" . to_string ( ) ) ,
392
+ ..Default :: default ( )
393
+ } ;
394
+ let expected_path = format ! ( "/compute/post/{}/computed" , CHAIN_TASK_ID ) ;
395
+ let expected_body = json ! ( computed_file) ;
396
+
397
+ Mock :: given ( method ( "POST" ) )
398
+ . and ( path ( expected_path. as_str ( ) ) )
399
+ . and ( header ( "Authorization" , CHALLENGE ) )
400
+ . and ( body_json ( & expected_body) )
401
+ . respond_with ( ResponseTemplate :: new ( 500 ) )
402
+ . expect ( 1 )
403
+ . mount ( & mock_server)
404
+ . await ;
405
+
406
+ let result = tokio:: task:: spawn_blocking ( move || {
407
+ let client = WorkerApiClient :: new ( & server_uri) ;
408
+ client. send_computed_file_to_host ( CHALLENGE , CHAIN_TASK_ID , & computed_file)
409
+ } )
410
+ . await
411
+ . expect ( "Task panicked" ) ;
412
+
413
+ assert ! ( result. is_err( ) ) ;
414
+ if let Err ( error) = result {
415
+ assert_eq ! ( error. status( ) . unwrap( ) , 500 ) ;
416
+ }
417
+ }
418
+
419
+ #[ tokio:: test]
420
+ async fn should_handle_invalid_chain_task_id_in_url ( ) {
421
+ let mock_server = MockServer :: start ( ) . await ;
422
+ let server_uri = mock_server. uri ( ) ;
423
+
424
+ let invalid_chain_task_id = "invalidTaskId" ;
425
+ let computed_file = ComputedFile {
426
+ task_id : Some ( invalid_chain_task_id. to_string ( ) ) ,
427
+ ..Default :: default ( )
428
+ } ;
429
+
430
+ let result = tokio:: task:: spawn_blocking ( move || {
431
+ let client = WorkerApiClient :: new ( & server_uri) ;
432
+ client. send_computed_file_to_host ( CHALLENGE , invalid_chain_task_id, & computed_file)
433
+ } )
434
+ . await
435
+ . expect ( "Task panicked" ) ;
436
+
437
+ assert ! ( result. is_err( ) , "Should fail with invalid chain task ID" ) ;
438
+ if let Err ( error) = result {
439
+ assert_eq ! ( error. status( ) . unwrap( ) , 404 ) ;
440
+ }
441
+ }
442
+
443
+ #[ tokio:: test]
444
+ async fn should_send_computed_file_with_minimal_data ( ) {
445
+ let mock_server = MockServer :: start ( ) . await ;
446
+ let server_uri = mock_server. uri ( ) ;
447
+
448
+ let computed_file = ComputedFile {
449
+ task_id : Some ( CHAIN_TASK_ID . to_string ( ) ) ,
450
+ ..Default :: default ( )
451
+ } ;
452
+
453
+ let expected_path = format ! ( "/compute/post/{}/computed" , CHAIN_TASK_ID ) ;
454
+ let expected_body = json ! ( computed_file) ;
455
+
456
+ Mock :: given ( method ( "POST" ) )
457
+ . and ( path ( expected_path. as_str ( ) ) )
458
+ . and ( header ( "Authorization" , CHALLENGE ) )
459
+ . and ( body_json ( & expected_body) )
460
+ . respond_with ( ResponseTemplate :: new ( 200 ) )
461
+ . expect ( 1 )
462
+ . mount ( & mock_server)
463
+ . await ;
464
+
465
+ let result = tokio:: task:: spawn_blocking ( move || {
466
+ let client = WorkerApiClient :: new ( & server_uri) ;
467
+ client. send_computed_file_to_host ( CHALLENGE , CHAIN_TASK_ID , & computed_file)
468
+ } )
469
+ . await
470
+ . expect ( "Task panicked" ) ;
471
+
472
+ assert ! ( result. is_ok( ) ) ;
473
+ }
474
+ // endregion
285
475
}
0 commit comments