Skip to content

Commit 1b53250

Browse files
authored
feat: Sign computed.json and send it to worker (#13)
1 parent afaff92 commit 1b53250

File tree

4 files changed

+706
-39
lines changed

4 files changed

+706
-39
lines changed

src/api/worker_api.rs

Lines changed: 193 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::compute::{
2+
computed_file::ComputedFile,
23
errors::ReplicateStatusCause,
34
utils::env_utils::{TeeSessionEnvironmentVariable, get_env_var_or_error},
45
};
@@ -151,6 +152,67 @@ impl WorkerApiClient {
151152
Err(response.error_for_status().unwrap_err())
152153
}
153154
}
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+
}
154216
}
155217

156218
#[cfg(test)]
@@ -164,6 +226,9 @@ mod tests {
164226
matchers::{body_json, header, method, path},
165227
};
166228

229+
const CHALLENGE: &str = "challenge";
230+
const CHAIN_TASK_ID: &str = "0x123456789abcdef";
231+
167232
// region ExitMessage()
168233
#[test]
169234
fn should_serialize_exit_message() {
@@ -213,9 +278,6 @@ mod tests {
213278
// endregion
214279

215280
// region send_exit_cause_for_post_compute_stage()
216-
const CHALLENGE: &str = "challenge";
217-
const CHAIN_TASK_ID: &str = "0x123456789abcdef";
218-
219281
#[tokio::test]
220282
async fn should_send_exit_cause() {
221283
let mock_server = MockServer::start().await;
@@ -282,4 +344,132 @@ mod tests {
282344
}
283345
}
284346
// 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
285475
}

0 commit comments

Comments
 (0)