Skip to content

Commit d63a3b5

Browse files
feat: add pre-compute module
1 parent 0ce3d2c commit d63a3b5

File tree

19 files changed

+3238
-20
lines changed

19 files changed

+3238
-20
lines changed

Cargo.lock

Lines changed: 853 additions & 19 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
[workspace]
22
resolver = "3"
33
members = [
4-
"post-compute"
4+
"post-compute",
5+
"pre-compute"
56
]

pre-compute/Cargo.toml

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
[package]
2+
name = "tee-worker-pre-compute"
3+
version = "0.1.0"
4+
edition = "2024"
5+
6+
[dependencies]
7+
aes = "0.8.4"
8+
alloy-signer = "0.15.9"
9+
alloy-signer-local = "0.15.9"
10+
base64 = "0.22.1"
11+
cbc = { version = "0.1.2", features = ["alloc"] }
12+
env_logger = "0.11.8"
13+
log = "0.4.27"
14+
multiaddr = "0.18.2"
15+
reqwest = { version = "0.12.15", features = ["blocking", "json"] }
16+
serde = "1.0.219"
17+
sha256 = "1.6.0"
18+
sha3 = "0.10.8"
19+
thiserror = "2.0.12"
20+
21+
[dev-dependencies]
22+
mockall = "0.13.1"
23+
serde_json = "1.0.140"
24+
temp-env = "0.3.6"
25+
tempfile = "3.20.0"
26+
testcontainers = { version = "0.25.0", features = ["blocking"] }
27+
testing_logger = "0.1.1"
28+
tokio = { version = "1.45.0", features = ["macros", "rt-multi-thread"] }
29+
wiremock = "0.6.3"

pre-compute/src/api.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
pub mod worker_api;

pre-compute/src/api/worker_api.rs

Lines changed: 339 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,339 @@
1+
use crate::compute::{
2+
errors::ReplicateStatusCause,
3+
utils::env_utils::{TeeSessionEnvironmentVariable, get_env_var_or_error},
4+
};
5+
use log::error;
6+
use reqwest::{blocking::Client, header::AUTHORIZATION};
7+
use serde::Serialize;
8+
9+
/// Represents payload that can be sent to the worker API to report the outcome of the
10+
/// pre‑compute stage.
11+
///
12+
/// The JSON structure expected by the REST endpoint is:
13+
/// ```json
14+
/// {
15+
/// "cause": "<ReplicateStatusCause as string>"
16+
/// }
17+
/// ```
18+
///
19+
/// # Arguments
20+
///
21+
/// * `cause` - A reference to the ReplicateStatusCause indicating why the pre-compute operation exited
22+
///
23+
/// # Example
24+
///
25+
/// ```
26+
/// use tee_worker_pre_compute::api::worker_api::ExitMessage;
27+
/// use tee_worker_pre_compute::compute::errors::ReplicateStatusCause;
28+
///
29+
/// let exit_message = ExitMessage::from(&ReplicateStatusCause::PreComputeInvalidTeeSignature);
30+
/// ```
31+
#[derive(Serialize, Debug)]
32+
pub struct ExitMessage<'a> {
33+
pub cause: &'a ReplicateStatusCause,
34+
}
35+
36+
impl<'a> From<&'a ReplicateStatusCause> for ExitMessage<'a> {
37+
fn from(cause: &'a ReplicateStatusCause) -> Self {
38+
Self { cause }
39+
}
40+
}
41+
42+
/// Thin wrapper around a [`Client`] that knows how to reach the iExec worker API.
43+
///
44+
/// This client can be created directly with a base URL using [`new()`], or
45+
/// configured from environment variables using [`from_env()`].
46+
///
47+
/// # Example
48+
///
49+
/// ```
50+
/// use tee_worker_pre_compute::api::worker_api::WorkerApiClient;
51+
///
52+
/// let client = WorkerApiClient::new("http://worker:13100");
53+
/// ```
54+
pub struct WorkerApiClient {
55+
base_url: String,
56+
client: Client,
57+
}
58+
59+
const DEFAULT_WORKER_HOST: &str = "worker:13100";
60+
61+
impl WorkerApiClient {
62+
pub fn new(base_url: &str) -> Self {
63+
WorkerApiClient {
64+
base_url: base_url.to_string(),
65+
client: Client::new(),
66+
}
67+
}
68+
69+
/// Creates a new WorkerApiClient instance with configuration from environment variables.
70+
///
71+
/// This method retrieves the worker host from the [`WORKER_HOST_ENV_VAR`] environment variable.
72+
/// If the variable is not set or empty, it defaults to `"worker:13100"`.
73+
///
74+
/// # Returns
75+
///
76+
/// * `WorkerApiClient` - A new client configured with the appropriate base URL
77+
///
78+
/// # Example
79+
///
80+
/// ```
81+
/// use tee_worker_pre_compute::api::worker_api::WorkerApiClient;
82+
///
83+
/// let client = WorkerApiClient::from_env();
84+
/// ```
85+
pub fn from_env() -> Self {
86+
let worker_host = get_env_var_or_error(
87+
TeeSessionEnvironmentVariable::WorkerHostEnvVar,
88+
ReplicateStatusCause::PreComputeWorkerAddressMissing,
89+
)
90+
.unwrap_or_else(|_| DEFAULT_WORKER_HOST.to_string());
91+
92+
let base_url = format!("http://{worker_host}");
93+
Self::new(&base_url)
94+
}
95+
96+
/// Sends an exit cause for a pre-compute operation to the Worker API.
97+
///
98+
/// This method reports the exit cause of a pre-compute operation to the Worker API,
99+
/// which can be used for tracking and debugging purposes.
100+
///
101+
/// # Arguments
102+
///
103+
/// * `authorization` - The authorization token to use for the API request
104+
/// * `chain_task_id` - The chain task ID for which to report the exit cause
105+
/// * `exit_cause` - The exit cause to report
106+
///
107+
/// # Returns
108+
///
109+
/// * `Ok(())` - If the exit cause was successfully reported
110+
/// * `Err(Error)` - If the exit cause could not be reported due to an HTTP error
111+
///
112+
/// # Errors
113+
///
114+
/// This function will return an [`Error`] if the request could not be sent or
115+
/// the server responded with a non‑success status.
116+
///
117+
/// # Example
118+
///
119+
/// ```
120+
/// use tee_worker_pre_compute::api::worker_api::{ExitMessage, WorkerApiClient};
121+
/// use tee_worker_pre_compute::compute::errors::ReplicateStatusCause;
122+
///
123+
/// let client = WorkerApiClient::new("http://worker:13100");
124+
/// let exit_message = ExitMessage::from(&ReplicateStatusCause::PreComputeInvalidTeeSignature);
125+
///
126+
/// match client.send_exit_cause_for_pre_compute_stage(
127+
/// "authorization_token",
128+
/// "0x123456789abcdef",
129+
/// &exit_message,
130+
/// ) {
131+
/// Ok(()) => println!("Exit cause reported successfully"),
132+
/// Err(error) => eprintln!("Failed to report exit cause: {error}"),
133+
/// }
134+
/// ```
135+
pub fn send_exit_cause_for_pre_compute_stage(
136+
&self,
137+
authorization: &str,
138+
chain_task_id: &str,
139+
exit_cause: &ExitMessage,
140+
) -> Result<(), ReplicateStatusCause> {
141+
let url = format!("{}/compute/pre/{chain_task_id}/exit", self.base_url);
142+
match self
143+
.client
144+
.post(&url)
145+
.header(AUTHORIZATION, authorization)
146+
.json(exit_cause)
147+
.send()
148+
{
149+
Ok(resp) => {
150+
let status = resp.status();
151+
if status.is_success() {
152+
Ok(())
153+
} else {
154+
let body = resp.text().unwrap_or_default();
155+
error!("Failed to send exit cause: [status:{status}, body:{body}]");
156+
Err(ReplicateStatusCause::PreComputeFailedUnknownIssue)
157+
}
158+
}
159+
Err(err) => {
160+
error!("HTTP request failed when sending exit cause to {url}: {err:?}");
161+
Err(ReplicateStatusCause::PreComputeFailedUnknownIssue)
162+
}
163+
}
164+
}
165+
}
166+
167+
#[cfg(test)]
168+
mod tests {
169+
use super::*;
170+
use crate::compute::utils::env_utils::TeeSessionEnvironmentVariable::WorkerHostEnvVar;
171+
use serde_json::{json, to_string};
172+
use temp_env::with_vars;
173+
use wiremock::{
174+
Mock, MockServer, ResponseTemplate,
175+
matchers::{body_json, header, method, path},
176+
};
177+
178+
// region ExitMessage()
179+
#[test]
180+
fn should_serialize_exit_message() {
181+
let causes = [
182+
(
183+
ReplicateStatusCause::PreComputeInvalidTeeSignature,
184+
"PRE_COMPUTE_INVALID_TEE_SIGNATURE",
185+
),
186+
(
187+
ReplicateStatusCause::PreComputeWorkerAddressMissing,
188+
"PRE_COMPUTE_WORKER_ADDRESS_MISSING",
189+
),
190+
(
191+
ReplicateStatusCause::PreComputeFailedUnknownIssue,
192+
"PRE_COMPUTE_FAILED_UNKNOWN_ISSUE",
193+
),
194+
];
195+
196+
for (cause, message) in causes {
197+
let exit_message = ExitMessage::from(&cause);
198+
let serialized = to_string(&exit_message).expect("Failed to serialize");
199+
let expected = format!("{{\"cause\":\"{message}\"}}");
200+
assert_eq!(serialized, expected);
201+
}
202+
}
203+
// endregion
204+
205+
// region get_worker_api_client
206+
#[test]
207+
fn should_get_worker_api_client_with_env_var() {
208+
with_vars(
209+
vec![(WorkerHostEnvVar.name(), Some("custom-worker-host:9999"))],
210+
|| {
211+
let client = WorkerApiClient::from_env();
212+
assert_eq!(client.base_url, "http://custom-worker-host:9999");
213+
},
214+
);
215+
}
216+
217+
#[test]
218+
fn should_get_worker_api_client_without_env_var() {
219+
temp_env::with_vars_unset(vec![WorkerHostEnvVar.name()], || {
220+
let client = WorkerApiClient::from_env();
221+
assert_eq!(client.base_url, format!("http://{DEFAULT_WORKER_HOST}"));
222+
});
223+
}
224+
// endregion
225+
226+
// region send_exit_cause_for_pre_compute_stage()
227+
const CHALLENGE: &str = "challenge";
228+
const CHAIN_TASK_ID: &str = "0x123456789abcdef";
229+
230+
#[tokio::test]
231+
async fn should_send_exit_cause() {
232+
let mock_server = MockServer::start().await;
233+
let server_url = mock_server.uri();
234+
235+
let expected_body = json!({
236+
"cause": ReplicateStatusCause::PreComputeInvalidTeeSignature,
237+
});
238+
239+
Mock::given(method("POST"))
240+
.and(path(format!("/compute/pre/{CHAIN_TASK_ID}/exit")))
241+
.and(header("Authorization", CHALLENGE))
242+
.and(body_json(&expected_body))
243+
.respond_with(ResponseTemplate::new(200))
244+
.expect(1)
245+
.mount(&mock_server)
246+
.await;
247+
248+
let result = tokio::task::spawn_blocking(move || {
249+
let exit_message =
250+
ExitMessage::from(&ReplicateStatusCause::PreComputeInvalidTeeSignature);
251+
let worker_api_client = WorkerApiClient::new(&server_url);
252+
worker_api_client.send_exit_cause_for_pre_compute_stage(
253+
CHALLENGE,
254+
CHAIN_TASK_ID,
255+
&exit_message,
256+
)
257+
})
258+
.await
259+
.expect("Task panicked");
260+
261+
assert!(result.is_ok());
262+
}
263+
264+
#[tokio::test]
265+
async fn should_not_send_exit_cause() {
266+
testing_logger::setup();
267+
let mock_server = MockServer::start().await;
268+
let server_url = mock_server.uri();
269+
270+
Mock::given(method("POST"))
271+
.and(path(format!("/compute/pre/{CHAIN_TASK_ID}/exit")))
272+
.respond_with(ResponseTemplate::new(503).set_body_string("Service Unavailable"))
273+
.expect(1)
274+
.mount(&mock_server)
275+
.await;
276+
277+
let result = tokio::task::spawn_blocking(move || {
278+
let exit_message =
279+
ExitMessage::from(&ReplicateStatusCause::PreComputeFailedUnknownIssue);
280+
let worker_api_client = WorkerApiClient::new(&server_url);
281+
let response = worker_api_client.send_exit_cause_for_pre_compute_stage(
282+
CHALLENGE,
283+
CHAIN_TASK_ID,
284+
&exit_message,
285+
);
286+
testing_logger::validate(|captured_logs| {
287+
let logs = captured_logs
288+
.iter()
289+
.filter(|c| c.level == log::Level::Error)
290+
.collect::<Vec<&testing_logger::CapturedLog>>();
291+
292+
assert_eq!(logs.len(), 1);
293+
assert_eq!(
294+
logs[0].body,
295+
"Failed to send exit cause: [status:503 Service Unavailable, body:Service Unavailable]"
296+
);
297+
});
298+
response
299+
})
300+
.await
301+
.expect("Task panicked");
302+
303+
assert!(result.is_err());
304+
assert_eq!(
305+
result,
306+
Err(ReplicateStatusCause::PreComputeFailedUnknownIssue)
307+
);
308+
}
309+
310+
#[test]
311+
fn test_send_exit_cause_http_request_failure() {
312+
testing_logger::setup();
313+
let exit_message = ExitMessage::from(&ReplicateStatusCause::PreComputeFailedUnknownIssue);
314+
let worker_api_client = WorkerApiClient::new("wrong_url");
315+
let result = worker_api_client.send_exit_cause_for_pre_compute_stage(
316+
CHALLENGE,
317+
CHAIN_TASK_ID,
318+
&exit_message,
319+
);
320+
testing_logger::validate(|captured_logs| {
321+
let logs = captured_logs
322+
.iter()
323+
.filter(|c| c.level == log::Level::Error)
324+
.collect::<Vec<&testing_logger::CapturedLog>>();
325+
326+
assert_eq!(logs.len(), 1);
327+
assert_eq!(
328+
logs[0].body,
329+
"HTTP request failed when sending exit cause to wrong_url/compute/pre/0x123456789abcdef/exit: reqwest::Error { kind: Builder, source: RelativeUrlWithoutBase }"
330+
);
331+
});
332+
assert!(result.is_err());
333+
assert_eq!(
334+
result,
335+
Err(ReplicateStatusCause::PreComputeFailedUnknownIssue)
336+
);
337+
}
338+
// endregion
339+
}
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use env_logger::{Builder, Env, Target};
2+
use std::process;
3+
use tee_worker_pre_compute::compute::app_runner::start;
4+
5+
fn main() {
6+
Builder::from_env(Env::default().default_filter_or("info"))
7+
.target(Target::Stdout)
8+
.init();
9+
process::exit(start() as i32);
10+
}

pre-compute/src/compute.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
pub mod app_runner;
2+
pub mod errors;
3+
pub mod pre_compute_app;
4+
pub mod pre_compute_args;
5+
pub mod signer;
6+
pub mod utils;

0 commit comments

Comments
 (0)