From 57c2e8bb53a7dfeff67d8a0ac90c02b209a19bb1 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 27 Feb 2026 16:20:27 +0100 Subject: [PATCH 001/176] . --- Cargo.lock | 20 + Cargo.toml | 1 + crates/tee-launcher/Cargo.toml | 33 + crates/tee-launcher/src/main.rs | 1553 +++++++++++++++++++++++++++++++ 4 files changed, 1607 insertions(+) create mode 100644 crates/tee-launcher/Cargo.toml create mode 100644 crates/tee-launcher/src/main.rs diff --git a/Cargo.lock b/Cargo.lock index 616a70cc6..9bcefd9d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10538,6 +10538,26 @@ dependencies = [ "tracing", ] +[[package]] +name = "tee-launcher" +version = "3.5.1" +dependencies = [ + "assert_matches", + "dstack-sdk", + "hex", + "mpc-primitives", + "regex", + "reqwest 0.12.28", + "rstest", + "serde", + "serde_json", + "tempfile", + "thiserror 2.0.18", + "tokio", + "tracing", + "tracing-subscriber", +] + [[package]] name = "tempfile" version = "3.25.0" diff --git a/Cargo.toml b/Cargo.toml index d70890a96..57f1ae560 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,6 +19,7 @@ members = [ "crates/node-types", "crates/primitives", "crates/tee-authority", + "crates/tee-launcher", "crates/test-migration-contract", "crates/test-parallel-contract", "crates/test-utils", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml new file mode 100644 index 000000000..78b4334de --- /dev/null +++ b/crates/tee-launcher/Cargo.toml @@ -0,0 +1,33 @@ +[package] +name = "tee-launcher" +version = { workspace = true } +license = { workspace = true } +edition = { workspace = true } + +[[bin]] +name = "tee-launcher" +path = "src/main.rs" + +[features] +integration-test = [] + +[dependencies] +dstack-sdk = { workspace = true } +hex = { workspace = true } +mpc-primitives = { path = "../primitives" } +regex = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +tracing = { workspace = true } +tracing-subscriber = { workspace = true } + +[dev-dependencies] +assert_matches = { workspace = true } +rstest = { workspace = true } +tempfile = { workspace = true } + +[lints] +workspace = true diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs new file mode 100644 index 000000000..2ad98bbbf --- /dev/null +++ b/crates/tee-launcher/src/main.rs @@ -0,0 +1,1553 @@ +use std::collections::{BTreeMap, HashSet, VecDeque}; +use std::process::Command; +use std::sync::LazyLock; + +use regex::Regex; +use serde::Deserialize; +use thiserror::Error; + +// Reuse the workspace hash type for type-safe image hash handling. +use mpc_primitives::hash::MpcDockerImageHash; + +// --------------------------------------------------------------------------- +// Error +// --------------------------------------------------------------------------- + +#[derive(Error, Debug)] +pub enum LauncherError { + #[error("PLATFORM must be set to one of [TEE, NONTEE], got: {0}")] + InvalidPlatform(String), + + #[error("DOCKER_CONTENT_TRUST must be set to 1")] + DockerContentTrustNotEnabled, + + #[error("PLATFORM=TEE requires dstack unix socket at {0}")] + DstackSocketMissing(String), + + #[error("GetQuote failed before extending RTMR3: {0}")] + DstackGetQuoteFailed(String), + + #[error("EmitEvent failed while extending RTMR3: {0}")] + DstackEmitEventFailed(String), + + #[error("DEFAULT_IMAGE_DIGEST invalid: {0}")] + InvalidDefaultDigest(String), + + #[error("Invalid JSON in {path}: approved_hashes missing or empty")] + InvalidApprovedHashes { path: String }, + + #[error("MPC_HASH_OVERRIDE invalid: {0}")] + InvalidHashOverride(String), + + #[error("Image hash not found among tags")] + ImageHashNotFoundAmongTags, + + #[error("Failed to get auth token from registry: {0}")] + RegistryAuthFailed(String), + + #[error("Failed to get successful response from {url} after {attempts} attempts")] + RegistryRequestFailed { url: String, attempts: u32 }, + + #[error("docker pull failed for {0}")] + DockerPullFailed(String), + + #[error("docker inspect failed for {0}")] + DockerInspectFailed(String), + + #[error("Digest mismatch: pulled {pulled} != expected {expected}")] + DigestMismatch { pulled: String, expected: String }, + + #[error("MPC image hash validation failed: {0}")] + ImageValidationFailed(String), + + #[error("docker run failed for validated hash={0}")] + DockerRunFailed(String), + + #[error("Too many env vars to pass through (>{0})")] + TooManyEnvVars(usize), + + #[error("Total env payload too large (>{0} bytes)")] + EnvPayloadTooLarge(usize), + + #[error("Unsafe docker command: LD_PRELOAD detected")] + LdPreloadDetected, + + #[error("Failed to read {path}: {source}")] + FileRead { + path: String, + source: std::io::Error, + }, + + #[error("Failed to parse {path}: {source}")] + JsonParse { + path: String, + source: serde_json::Error, + }, + + #[error("Required environment variable not set: {0}")] + MissingEnvVar(String), + + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + + #[error("Registry response parse error: {0}")] + RegistryResponseParse(String), +} + +type Result = std::result::Result; + +// --------------------------------------------------------------------------- +// Constants — matching Python launcher exactly +// --------------------------------------------------------------------------- + +const MPC_CONTAINER_NAME: &str = "mpc-node"; +const IMAGE_DIGEST_FILE: &str = "/mnt/shared/image-digest.bin"; +const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; +const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; + +const SHA256_PREFIX: &str = "sha256:"; + +// Docker Hub defaults +const DEFAULT_RPC_REQUEST_TIMEOUT_SECS: f64 = 10.0; +const DEFAULT_RPC_REQUEST_INTERVAL_SECS: f64 = 1.0; +const DEFAULT_RPC_MAX_ATTEMPTS: u32 = 20; + +const DEFAULT_MPC_IMAGE_NAME: &str = "nearone/mpc-node"; +const DEFAULT_MPC_REGISTRY: &str = "registry.hub.docker.com"; +const DEFAULT_MPC_IMAGE_TAG: &str = "latest"; + +// Env var names +const ENV_VAR_PLATFORM: &str = "PLATFORM"; +const ENV_VAR_DEFAULT_IMAGE_DIGEST: &str = "DEFAULT_IMAGE_DIGEST"; +const ENV_VAR_DOCKER_CONTENT_TRUST: &str = "DOCKER_CONTENT_TRUST"; +const ENV_VAR_MPC_HASH_OVERRIDE: &str = "MPC_HASH_OVERRIDE"; +const ENV_VAR_RPC_REQUEST_TIMEOUT_SECS: &str = "RPC_REQUEST_TIMEOUT_SECS"; +const ENV_VAR_RPC_REQUEST_INTERVAL_SECS: &str = "RPC_REQUEST_INTERVAL_SECS"; +const ENV_VAR_RPC_MAX_ATTEMPTS: &str = "RPC_MAX_ATTEMPTS"; + +const DSTACK_USER_CONFIG_MPC_IMAGE_TAGS: &str = "MPC_IMAGE_TAGS"; +const DSTACK_USER_CONFIG_MPC_IMAGE_NAME: &str = "MPC_IMAGE_NAME"; +const DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY: &str = "MPC_REGISTRY"; + +// Security limits +const MAX_PASSTHROUGH_ENV_VARS: usize = 64; +const MAX_ENV_VALUE_LEN: usize = 1024; +const MAX_TOTAL_ENV_BYTES: usize = 32 * 1024; + +// Regex patterns (compiled once) +static SHA256_REGEX: LazyLock = + LazyLock::new(|| Regex::new(r"^sha256:[0-9a-f]{64}$").unwrap()); +static MPC_ENV_KEY_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^MPC_[A-Z0-9_]{1,64}$").unwrap()); +static HOST_ENTRY_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9\-\.]+:\d{1,3}(\.\d{1,3}){3}$").unwrap()); +static PORT_MAPPING_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^(\d{1,5}):(\d{1,5})$").unwrap()); +static INVALID_HOST_ENTRY_PATTERN: LazyLock = + LazyLock::new(|| Regex::new(r"^[;&|`$\\<>\-]|^--").unwrap()); + +// Denied env keys — never pass these to the container +static DENIED_CONTAINER_ENV_KEYS: LazyLock> = + LazyLock::new(|| HashSet::from(["MPC_P2P_PRIVATE_KEY", "MPC_ACCOUNT_SK"])); + +// Allowed non-MPC env vars (backward compatibility) +static ALLOWED_MPC_ENV_VARS: LazyLock> = LazyLock::new(|| { + HashSet::from([ + "MPC_ACCOUNT_ID", + "MPC_LOCAL_ADDRESS", + "MPC_SECRET_STORE_KEY", + "MPC_CONTRACT_ID", + "MPC_ENV", + "MPC_HOME_DIR", + "NEAR_BOOT_NODES", + "RUST_BACKTRACE", + "RUST_LOG", + "MPC_RESPONDER_ID", + "MPC_BACKUP_ENCRYPTION_KEY_HEX", + ]) +}); + +// Launcher-only env vars — read from user config but never forwarded to container +static ALLOWED_LAUNCHER_ENV_VARS: LazyLock> = LazyLock::new(|| { + HashSet::from([ + DSTACK_USER_CONFIG_MPC_IMAGE_TAGS, + DSTACK_USER_CONFIG_MPC_IMAGE_NAME, + DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY, + ENV_VAR_MPC_HASH_OVERRIDE, + ENV_VAR_RPC_REQUEST_TIMEOUT_SECS, + ENV_VAR_RPC_REQUEST_INTERVAL_SECS, + ENV_VAR_RPC_MAX_ATTEMPTS, + ]) +}); + +// --------------------------------------------------------------------------- +// Types +// --------------------------------------------------------------------------- + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum Platform { + Tee, + NonTee, +} + +#[derive(Debug, Clone)] +pub struct RpcTimingConfig { + pub request_timeout_secs: f64, + pub request_interval_secs: f64, + pub max_attempts: u32, +} + +#[derive(Debug, Clone)] +pub struct ImageSpec { + pub tags: Vec, + pub image_name: String, + pub registry: String, +} + +#[derive(Debug, Clone)] +pub struct ResolvedImage { + pub spec: ImageSpec, + pub digest: String, +} + +/// JSON structure for the approved hashes file written by the MPC node. +/// Must stay aligned with `crates/node/src/tee/allowed_image_hashes_watcher.rs`. +#[derive(Debug, Deserialize)] +struct ApprovedHashesFile { + approved_hashes: Vec, +} + +// --------------------------------------------------------------------------- +// Validation functions — security policy for env passthrough +// --------------------------------------------------------------------------- + +fn has_control_chars(s: &str) -> bool { + for ch in s.chars() { + if ch == '\n' || ch == '\r' || ch == '\0' { + return true; + } + if (ch as u32) < 0x20 && ch != '\t' { + return true; + } + } + false +} + +fn is_safe_env_value(value: &str) -> bool { + if value.len() > MAX_ENV_VALUE_LEN { + return false; + } + if has_control_chars(value) { + return false; + } + if value.contains("LD_PRELOAD") { + return false; + } + true +} + +fn is_valid_ip(ip: &str) -> bool { + ip.parse::().is_ok() +} + +fn is_valid_host_entry(entry: &str) -> bool { + if !HOST_ENTRY_RE.is_match(entry) { + return false; + } + if let Some((_host, ip)) = entry.rsplit_once(':') { + is_valid_ip(ip) + } else { + false + } +} + +fn is_valid_port_mapping(entry: &str) -> bool { + if let Some(caps) = PORT_MAPPING_RE.captures(entry) { + let host_port: u32 = caps[1].parse().unwrap_or(0); + let container_port: u32 = caps[2].parse().unwrap_or(0); + host_port > 0 && host_port <= 65535 && container_port > 0 && container_port <= 65535 + } else { + false + } +} + +fn is_safe_host_entry(entry: &str) -> bool { + if INVALID_HOST_ENTRY_PATTERN.is_match(entry) { + return false; + } + if entry.contains("LD_PRELOAD") { + return false; + } + true +} + +fn is_safe_port_mapping(mapping: &str) -> bool { + !INVALID_HOST_ENTRY_PATTERN.is_match(mapping) +} + +fn is_allowed_container_env_key(key: &str) -> bool { + if DENIED_CONTAINER_ENV_KEYS.contains(key) { + return false; + } + if MPC_ENV_KEY_RE.is_match(key) { + return true; + } + if ALLOWED_MPC_ENV_VARS.contains(key) { + return true; + } + false +} + +// --------------------------------------------------------------------------- +// Config parsing +// --------------------------------------------------------------------------- + +fn parse_env_lines(lines: &[&str]) -> BTreeMap { + let mut env = BTreeMap::new(); + for line in lines { + let line = line.trim(); + if line.is_empty() || line.starts_with('#') || !line.contains('=') { + continue; + } + if let Some((key, value)) = line.split_once('=') { + let key = key.trim(); + let value = value.trim(); + if key.is_empty() { + continue; + } + env.insert(key.to_string(), value.to_string()); + } + } + env +} + +fn parse_env_file(path: &str) -> Result> { + let content = std::fs::read_to_string(path).map_err(|source| LauncherError::FileRead { + path: path.to_string(), + source, + })?; + let lines: Vec<&str> = content.lines().collect(); + Ok(parse_env_lines(&lines)) +} + +fn parse_platform() -> Result { + let raw = std::env::var(ENV_VAR_PLATFORM).map_err(|_| { + LauncherError::InvalidPlatform(format!( + "{ENV_VAR_PLATFORM} must be set to one of [TEE, NONTEE]" + )) + })?; + let val = raw.trim(); + match val { + "TEE" => Ok(Platform::Tee), + "NONTEE" => Ok(Platform::NonTee), + other => Err(LauncherError::InvalidPlatform(other.to_string())), + } +} + +fn load_rpc_timing_config(dstack_config: &BTreeMap) -> RpcTimingConfig { + let timeout_secs = dstack_config + .get(ENV_VAR_RPC_REQUEST_TIMEOUT_SECS) + .and_then(|v| v.parse().ok()) + .unwrap_or(DEFAULT_RPC_REQUEST_TIMEOUT_SECS); + let interval_secs = dstack_config + .get(ENV_VAR_RPC_REQUEST_INTERVAL_SECS) + .and_then(|v| v.parse().ok()) + .unwrap_or(DEFAULT_RPC_REQUEST_INTERVAL_SECS); + let max_attempts = dstack_config + .get(ENV_VAR_RPC_MAX_ATTEMPTS) + .and_then(|v| v.parse().ok()) + .unwrap_or(DEFAULT_RPC_MAX_ATTEMPTS); + RpcTimingConfig { + request_timeout_secs: timeout_secs, + request_interval_secs: interval_secs, + max_attempts, + } +} + +fn get_image_spec(dstack_config: &BTreeMap) -> ImageSpec { + let tags_raw = dstack_config + .get(DSTACK_USER_CONFIG_MPC_IMAGE_TAGS) + .cloned() + .unwrap_or_else(|| DEFAULT_MPC_IMAGE_TAG.to_string()); + let tags: Vec = tags_raw + .split(',') + .map(|t| t.trim().to_string()) + .filter(|t| !t.is_empty()) + .collect(); + tracing::info!("Using tags {tags:?} to find matching MPC node docker image."); + + let image_name = dstack_config + .get(DSTACK_USER_CONFIG_MPC_IMAGE_NAME) + .cloned() + .unwrap_or_else(|| DEFAULT_MPC_IMAGE_NAME.to_string()); + tracing::info!("Using image name {image_name}."); + + let registry = dstack_config + .get(DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY) + .cloned() + .unwrap_or_else(|| DEFAULT_MPC_REGISTRY.to_string()); + tracing::info!("Using registry {registry}."); + + ImageSpec { + tags, + image_name, + registry, + } +} + +// --------------------------------------------------------------------------- +// Hash selection +// --------------------------------------------------------------------------- + +fn is_valid_sha256_digest(digest: &str) -> bool { + SHA256_REGEX.is_match(digest) +} + +fn get_bare_digest(full_digest: &str) -> Result { + full_digest + .strip_prefix(SHA256_PREFIX) + .map(|s| s.to_string()) + .ok_or_else(|| { + LauncherError::InvalidDefaultDigest(format!( + "Invalid digest (missing sha256: prefix): {full_digest}" + )) + }) +} + +fn load_and_select_hash(dstack_config: &BTreeMap) -> Result { + let approved_hashes = if std::path::Path::new(IMAGE_DIGEST_FILE).is_file() { + let content = + std::fs::read_to_string(IMAGE_DIGEST_FILE).map_err(|source| LauncherError::FileRead { + path: IMAGE_DIGEST_FILE.to_string(), + source, + })?; + let data: ApprovedHashesFile = + serde_json::from_str(&content).map_err(|source| LauncherError::JsonParse { + path: IMAGE_DIGEST_FILE.to_string(), + source, + })?; + if data.approved_hashes.is_empty() { + return Err(LauncherError::InvalidApprovedHashes { + path: IMAGE_DIGEST_FILE.to_string(), + }); + } + data.approved_hashes + } else { + let fallback = std::env::var(ENV_VAR_DEFAULT_IMAGE_DIGEST) + .map_err(|_| LauncherError::MissingEnvVar(ENV_VAR_DEFAULT_IMAGE_DIGEST.to_string()))?; + let fallback = fallback.trim().to_string(); + let fallback = if fallback.starts_with(SHA256_PREFIX) { + fallback + } else { + format!("{SHA256_PREFIX}{fallback}") + }; + if !is_valid_sha256_digest(&fallback) { + return Err(LauncherError::InvalidDefaultDigest(fallback)); + } + tracing::info!( + "{IMAGE_DIGEST_FILE} missing → fallback to DEFAULT_IMAGE_DIGEST={fallback}" + ); + vec![fallback] + }; + + tracing::info!("Approved MPC image hashes (newest → oldest):"); + for h in &approved_hashes { + tracing::info!(" - {h}"); + } + + // Optional override + if let Some(override_hash) = dstack_config.get(ENV_VAR_MPC_HASH_OVERRIDE) { + if !is_valid_sha256_digest(override_hash) { + return Err(LauncherError::InvalidHashOverride(override_hash.clone())); + } + if !approved_hashes.contains(override_hash) { + tracing::error!( + "MPC_HASH_OVERRIDE={override_hash} does NOT match any approved hash!" + ); + return Err(LauncherError::InvalidHashOverride(override_hash.clone())); + } + tracing::info!("MPC_HASH_OVERRIDE provided → selecting: {override_hash}"); + return Ok(override_hash.clone()); + } + + // No override → select newest (first in list) + let selected = approved_hashes[0].clone(); + tracing::info!("Selected MPC hash (newest allowed): {selected}"); + Ok(selected) +} + +// --------------------------------------------------------------------------- +// Docker registry communication +// --------------------------------------------------------------------------- + +async fn request_until_success( + client: &reqwest::Client, + url: &str, + headers: &[(String, String)], + timing: &RpcTimingConfig, +) -> Result { + let mut interval = timing.request_interval_secs; + + for attempt in 1..=timing.max_attempts { + // Sleep before request (matching Python behavior) + tokio::time::sleep(std::time::Duration::from_secs_f64(interval)).await; + interval = (interval.max(1.0) * 1.5).min(60.0); + + let mut req = client.get(url); + for (k, v) in headers { + req = req.header(k.as_str(), v.as_str()); + } + + match req + .timeout(std::time::Duration::from_secs_f64(timing.request_timeout_secs)) + .send() + .await + { + Err(e) => { + tracing::warn!( + "Attempt {attempt}/{}: Failed to fetch {url}. Status: Timeout/Error: {e}", + timing.max_attempts + ); + continue; + } + Ok(resp) if resp.status() != reqwest::StatusCode::OK => { + tracing::warn!( + "Attempt {attempt}/{}: Failed to fetch {url}. Status: {}", + timing.max_attempts, + resp.status() + ); + continue; + } + Ok(resp) => return Ok(resp), + } + } + + Err(LauncherError::RegistryRequestFailed { + url: url.to_string(), + attempts: timing.max_attempts, + }) +} + +async fn get_manifest_digest( + image: &ResolvedImage, + timing: &RpcTimingConfig, +) -> Result { + if image.spec.tags.is_empty() { + return Err(LauncherError::ImageHashNotFoundAmongTags); + } + + // Get auth token + let token_url = format!( + "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{}:pull", + image.spec.image_name + ); + let client = reqwest::Client::new(); + let token_resp = client + .get(&token_url) + .send() + .await + .map_err(|e| LauncherError::RegistryAuthFailed(e.to_string()))?; + if token_resp.status() != reqwest::StatusCode::OK { + return Err(LauncherError::RegistryAuthFailed(format!( + "status: {}", + token_resp.status() + ))); + } + let token_json: serde_json::Value = token_resp + .json() + .await + .map_err(|e| LauncherError::RegistryAuthFailed(e.to_string()))?; + let token = token_json["token"] + .as_str() + .ok_or_else(|| LauncherError::RegistryAuthFailed("no token in response".to_string()))? + .to_string(); + + let mut tags: VecDeque = image.spec.tags.iter().cloned().collect(); + + while let Some(tag) = tags.pop_front() { + let manifest_url = format!( + "https://{}/v2/{}/manifests/{tag}", + image.spec.registry, image.spec.image_name + ); + let headers = vec![ + ( + "Accept".to_string(), + "application/vnd.docker.distribution.manifest.v2+json".to_string(), + ), + ("Authorization".to_string(), format!("Bearer {token}")), + ]; + + match request_until_success(&client, &manifest_url, &headers, timing).await { + Ok(resp) => { + let content_digest = resp + .headers() + .get("Docker-Content-Digest") + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()); + + let manifest: serde_json::Value = + resp.json().await.map_err(|e| { + LauncherError::RegistryResponseParse(e.to_string()) + })?; + + let media_type = manifest["mediaType"].as_str().unwrap_or(""); + match media_type { + "application/vnd.oci.image.index.v1+json" => { + // Multi-platform manifest; scan for amd64/linux + if let Some(manifests) = manifest["manifests"].as_array() { + for m in manifests { + let arch = m["platform"]["architecture"].as_str().unwrap_or(""); + let os = m["platform"]["os"].as_str().unwrap_or(""); + if arch == "amd64" && os == "linux" { + if let Some(digest) = m["digest"].as_str() { + tags.push_back(digest.to_string()); + } + } + } + } + } + "application/vnd.docker.distribution.manifest.v2+json" + | "application/vnd.oci.image.manifest.v1+json" => { + let config_digest = + manifest["config"]["digest"].as_str().unwrap_or(""); + if config_digest == image.digest { + if let Some(digest) = content_digest { + return Ok(digest); + } + } + } + _ => {} + } + } + Err(e) => { + tracing::warn!( + "{e}: Exceeded number of maximum RPC requests for any given attempt. \ + Will continue in the hopes of finding the matching image hash among remaining tags" + ); + } + } + } + + Err(LauncherError::ImageHashNotFoundAmongTags) +} + +async fn validate_image_hash( + image_digest: &str, + dstack_config: &BTreeMap, + timing: &RpcTimingConfig, +) -> Result { + tracing::info!("Validating MPC hash: {image_digest}"); + + let image_spec = get_image_spec(dstack_config); + let docker_image = ResolvedImage { + spec: image_spec, + digest: image_digest.to_string(), + }; + + let manifest_digest = get_manifest_digest(&docker_image, timing).await?; + let name_and_digest = format!("{}@{manifest_digest}", docker_image.spec.image_name); + + // Pull + let pull = Command::new("docker") + .args(["pull", &name_and_digest]) + .output() + .map_err(|e| LauncherError::DockerPullFailed(e.to_string()))?; + if !pull.status.success() { + tracing::error!("docker pull failed for {image_digest}"); + return Ok(false); + } + + // Verify digest + let inspect = Command::new("docker") + .args(["image", "inspect", "--format", "{{index .ID}}", &name_and_digest]) + .output() + .map_err(|e| LauncherError::DockerInspectFailed(e.to_string()))?; + if !inspect.status.success() { + tracing::error!("docker inspect failed for {image_digest}"); + return Ok(false); + } + + let pulled_digest = String::from_utf8_lossy(&inspect.stdout).trim().to_string(); + if pulled_digest != image_digest { + tracing::error!("digest mismatch: {pulled_digest} != {image_digest}"); + return Ok(false); + } + + tracing::info!("MPC hash {image_digest} validated successfully."); + Ok(true) +} + +// --------------------------------------------------------------------------- +// Docker command builder +// --------------------------------------------------------------------------- + +fn remove_existing_container() { + let output = Command::new("docker") + .args(["ps", "-a", "--format", "{{.Names}}"]) + .output(); + + match output { + Ok(out) => { + let names = String::from_utf8_lossy(&out.stdout); + if names.lines().any(|n| n == MPC_CONTAINER_NAME) { + tracing::info!("Removing existing container: {MPC_CONTAINER_NAME}"); + let _ = Command::new("docker") + .args(["rm", "-f", MPC_CONTAINER_NAME]) + .output(); + } + } + Err(e) => { + tracing::warn!("Failed to check/remove container {MPC_CONTAINER_NAME}: {e}"); + } + } +} + +fn build_docker_cmd( + platform: Platform, + user_env: &BTreeMap, + image_digest: &str, +) -> Result> { + let bare_digest = get_bare_digest(image_digest)?; + + let mut cmd: Vec = vec!["docker".into(), "run".into()]; + + // Required environment variables + cmd.extend(["--env".into(), format!("MPC_IMAGE_HASH={bare_digest}")]); + cmd.extend([ + "--env".into(), + format!("MPC_LATEST_ALLOWED_HASH_FILE={IMAGE_DIGEST_FILE}"), + ]); + + if platform == Platform::Tee { + cmd.extend([ + "--env".into(), + format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"), + ]); + cmd.extend([ + "-v".into(), + format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"), + ]); + } + + // Track env passthrough size/caps + let mut passed_env_count: usize = 0; + let mut total_env_bytes: usize = 0; + + // BTreeMap iteration is already sorted by key (deterministic) + for (key, value) in user_env { + if ALLOWED_LAUNCHER_ENV_VARS.contains(key.as_str()) { + continue; + } + + if key == "EXTRA_HOSTS" { + for host_entry in value.split(',') { + let clean = host_entry.trim(); + if is_safe_host_entry(clean) && is_valid_host_entry(clean) { + cmd.extend(["--add-host".into(), clean.to_string()]); + } else { + tracing::warn!("Ignoring invalid or unsafe EXTRA_HOSTS entry: {clean}"); + } + } + continue; + } + + if key == "PORTS" { + for port_pair in value.split(',') { + let clean = port_pair.trim(); + if is_safe_port_mapping(clean) && is_valid_port_mapping(clean) { + cmd.extend(["-p".into(), clean.to_string()]); + } else { + tracing::warn!("Ignoring invalid or unsafe PORTS entry: {clean}"); + } + } + continue; + } + + if !is_allowed_container_env_key(key) { + tracing::warn!("Ignoring unknown or unapproved env var: {key}"); + continue; + } + + if !is_safe_env_value(value) { + tracing::warn!("Ignoring env var with unsafe value: {key}"); + continue; + } + + passed_env_count += 1; + if passed_env_count > MAX_PASSTHROUGH_ENV_VARS { + return Err(LauncherError::TooManyEnvVars(MAX_PASSTHROUGH_ENV_VARS)); + } + + total_env_bytes += key.len() + 1 + value.len(); + if total_env_bytes > MAX_TOTAL_ENV_BYTES { + return Err(LauncherError::EnvPayloadTooLarge(MAX_TOTAL_ENV_BYTES)); + } + + cmd.extend(["--env".into(), format!("{key}={value}")]); + } + + // Container run configuration + cmd.extend([ + "--security-opt".into(), + "no-new-privileges:true".into(), + "-v".into(), + "/tapp:/tapp:ro".into(), + "-v".into(), + "shared-volume:/mnt/shared".into(), + "-v".into(), + "mpc-data:/data".into(), + "--name".into(), + MPC_CONTAINER_NAME.into(), + "--detach".into(), + image_digest.to_string(), + ]); + + tracing::info!("docker cmd {}", cmd.join(" ")); + + // Final LD_PRELOAD safeguard + let cmd_str = cmd.join(" "); + if cmd_str.contains("LD_PRELOAD") { + return Err(LauncherError::LdPreloadDetected); + } + + Ok(cmd) +} + +fn launch_mpc_container( + platform: Platform, + valid_hash: &str, + user_env: &BTreeMap, +) -> Result<()> { + tracing::info!("Launching MPC node with validated hash: {valid_hash}"); + + remove_existing_container(); + let docker_cmd = build_docker_cmd(platform, user_env, valid_hash)?; + + let status = Command::new(&docker_cmd[0]) + .args(&docker_cmd[1..]) + .status() + .map_err(|e| LauncherError::DockerRunFailed(e.to_string()))?; + + if !status.success() { + return Err(LauncherError::DockerRunFailed(format!( + "validated hash={valid_hash}" + ))); + } + + tracing::info!("MPC launched successfully."); + Ok(()) +} + +// --------------------------------------------------------------------------- +// Dstack TEE communication (via dstack-sdk, no curl) +// --------------------------------------------------------------------------- + +fn is_unix_socket(path: &str) -> bool { + use std::os::unix::fs::FileTypeExt; + match std::fs::metadata(path) { + Ok(meta) => meta.file_type().is_socket(), + Err(_) => false, + } +} + +async fn extend_rtmr3(platform: Platform, valid_hash: &str) -> Result<()> { + if platform == Platform::NonTee { + tracing::info!("PLATFORM=NONTEE → skipping RTMR3 extension step."); + return Ok(()); + } + + if !is_unix_socket(DSTACK_UNIX_SOCKET) { + return Err(LauncherError::DstackSocketMissing( + DSTACK_UNIX_SOCKET.to_string(), + )); + } + + let bare = get_bare_digest(valid_hash)?; + tracing::info!("Extending RTMR3 with validated hash: {bare}"); + + let client = + dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); + + // GetQuote first + client + .get_quote(vec![]) + .await + .map_err(|e| LauncherError::DstackGetQuoteFailed(e.to_string()))?; + + // EmitEvent with the image digest + client + .emit_event("mpc-image-digest".to_string(), bare.into_bytes()) + .await + .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; + + Ok(()) +} + +// --------------------------------------------------------------------------- +// Main orchestration +// --------------------------------------------------------------------------- + +async fn run() -> Result<()> { + tracing::info!("start"); + + let platform = parse_platform()?; + tracing::info!("Launcher platform: {}", match platform { + Platform::Tee => "TEE", + Platform::NonTee => "NONTEE", + }); + + if platform == Platform::Tee && !is_unix_socket(DSTACK_UNIX_SOCKET) { + return Err(LauncherError::DstackSocketMissing( + DSTACK_UNIX_SOCKET.to_string(), + )); + } + + // DOCKER_CONTENT_TRUST must be enabled + let dct = std::env::var(ENV_VAR_DOCKER_CONTENT_TRUST).unwrap_or_default(); + if dct != "1" { + return Err(LauncherError::DockerContentTrustNotEnabled); + } + + // Load dstack user config + let dstack_config: BTreeMap = + if std::path::Path::new(DSTACK_USER_CONFIG_FILE).is_file() { + parse_env_file(DSTACK_USER_CONFIG_FILE)? + } else { + BTreeMap::new() + }; + + let rpc_cfg = load_rpc_timing_config(&dstack_config); + + let selected_hash = load_and_select_hash(&dstack_config)?; + + if !validate_image_hash(&selected_hash, &dstack_config, &rpc_cfg).await? { + return Err(LauncherError::ImageValidationFailed(selected_hash)); + } + + tracing::info!("MPC image hash validated successfully: {selected_hash}"); + + extend_rtmr3(platform, &selected_hash).await?; + + launch_mpc_container(platform, &selected_hash, &dstack_config)?; + + Ok(()) +} + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + if let Err(e) = run().await { + tracing::error!("Error: {e}"); + std::process::exit(1); + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use assert_matches::assert_matches; + use rstest::rstest; + + // -- Config parsing tests ----------------------------------------------- + + #[test] + fn test_parse_env_lines_basic() { + let lines = vec![ + "# a comment", + "KEY1=value1", + " KEY2 = value2 ", + "", + "INVALIDLINE", + "EMPTY_KEY=", + ]; + let env = parse_env_lines(&lines); + assert_eq!(env.get("KEY1").unwrap(), "value1"); + assert_eq!(env.get("KEY2").unwrap(), "value2"); + assert_eq!(env.get("EMPTY_KEY").unwrap(), ""); + assert!(!env.contains_key("INVALIDLINE")); + } + + #[test] + fn test_config_ignores_blank_lines_and_comments() { + let lines = vec!["", " # This is a comment", " MPC_SECRET_STORE_KEY=topsecret", ""]; + let env = parse_env_lines(&lines); + assert_eq!(env.get("MPC_SECRET_STORE_KEY").unwrap(), "topsecret"); + assert_eq!(env.len(), 1); + } + + #[test] + fn test_config_skips_malformed_lines() { + let lines = vec![ + "GOOD_KEY=value", + "bad_line_without_equal", + "ANOTHER_GOOD=ok", + "=", + ]; + let env = parse_env_lines(&lines); + assert!(env.contains_key("GOOD_KEY")); + assert!(env.contains_key("ANOTHER_GOOD")); + assert!(!env.contains_key("bad_line_without_equal")); + assert!(!env.contains_key("")); + } + + #[test] + fn test_config_overrides_duplicate_keys() { + let lines = vec!["MPC_ACCOUNT_ID=first", "MPC_ACCOUNT_ID=second"]; + let env = parse_env_lines(&lines); + assert_eq!(env.get("MPC_ACCOUNT_ID").unwrap(), "second"); + } + + // -- Host/port validation tests ----------------------------------------- + + #[test] + fn test_valid_host_entry() { + assert!(is_valid_host_entry("node.local:192.168.1.1")); + assert!(!is_valid_host_entry("node.local:not-an-ip")); + assert!(!is_valid_host_entry("--env LD_PRELOAD=hack.so")); + } + + #[test] + fn test_valid_port_mapping() { + assert!(is_valid_port_mapping("11780:11780")); + assert!(!is_valid_port_mapping("65536:11780")); + assert!(!is_valid_port_mapping("--volume /:/mnt")); + } + + // -- Security validation tests ------------------------------------------ + + #[test] + fn test_has_control_chars_rejects_newline_and_cr() { + assert!(has_control_chars("a\nb")); + assert!(has_control_chars("a\rb")); + } + + #[test] + fn test_has_control_chars_allows_tab() { + assert!(!has_control_chars("a\tb")); + } + + #[test] + fn test_has_control_chars_rejects_other_control_chars() { + assert!(has_control_chars(&format!("a{}b", '\x1F'))); + } + + #[test] + fn test_is_safe_env_value_rejects_control_chars() { + assert!(!is_safe_env_value("ok\nno")); + assert!(!is_safe_env_value("ok\rno")); + assert!(!is_safe_env_value(&format!("ok{}no", '\x1F'))); + } + + #[test] + fn test_is_safe_env_value_rejects_ld_preload() { + assert!(!is_safe_env_value("LD_PRELOAD=/tmp/x.so")); + assert!(!is_safe_env_value("foo LD_PRELOAD bar")); + } + + #[test] + fn test_is_safe_env_value_rejects_too_long() { + assert!(!is_safe_env_value(&"a".repeat(MAX_ENV_VALUE_LEN + 1))); + assert!(is_safe_env_value(&"a".repeat(MAX_ENV_VALUE_LEN))); + } + + #[test] + fn test_is_allowed_container_env_key_allows_mpc_prefix_uppercase() { + assert!(is_allowed_container_env_key("MPC_FOO")); + assert!(is_allowed_container_env_key("MPC_FOO_123")); + assert!(is_allowed_container_env_key("MPC_A_B_C")); + } + + #[test] + fn test_is_allowed_container_env_key_rejects_lowercase_or_invalid() { + assert!(!is_allowed_container_env_key("MPC_foo")); + assert!(!is_allowed_container_env_key("MPC-FOO")); + assert!(!is_allowed_container_env_key("MPC.FOO")); + assert!(!is_allowed_container_env_key("MPC_")); + } + + #[test] + fn test_is_allowed_container_env_key_allows_compat_non_mpc_keys() { + assert!(is_allowed_container_env_key("RUST_LOG")); + assert!(is_allowed_container_env_key("RUST_BACKTRACE")); + assert!(is_allowed_container_env_key("NEAR_BOOT_NODES")); + } + + #[test] + fn test_is_allowed_container_env_key_denies_sensitive_keys() { + assert!(!is_allowed_container_env_key("MPC_P2P_PRIVATE_KEY")); + assert!(!is_allowed_container_env_key("MPC_ACCOUNT_SK")); + } + + // -- Docker cmd builder tests ------------------------------------------- + + fn make_digest() -> String { + format!("sha256:{}", "a".repeat(64)) + } + + fn base_env() -> BTreeMap { + BTreeMap::from([ + ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), + ("MPC_CONTRACT_ID".into(), "contract.near".into()), + ("MPC_ENV".into(), "testnet".into()), + ("MPC_HOME_DIR".into(), "/data".into()), + ("NEAR_BOOT_NODES".into(), "boot1,boot2".into()), + ("RUST_LOG".into(), "info".into()), + ]) + } + + #[test] + fn test_build_docker_cmd_sanitizes_ports_and_hosts() { + let env = BTreeMap::from([ + ("PORTS".into(), "11780:11780,--env BAD=1".into()), + ( + "EXTRA_HOSTS".into(), + "node:192.168.1.1,--volume /:/mnt".into(), + ), + ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + + assert!(cmd.contains(&"MPC_ACCOUNT_ID=mpc-user-123".to_string())); + assert!(cmd.contains(&"11780:11780".to_string())); + assert!(cmd.contains(&"node:192.168.1.1".to_string())); + // Injection strings filtered + assert!(!cmd.iter().any(|arg| arg.contains("BAD=1"))); + assert!(!cmd.iter().any(|arg| arg.contains("/:/mnt"))); + } + + #[test] + fn test_extra_hosts_does_not_allow_ld_preload() { + let env = BTreeMap::from([ + ( + "EXTRA_HOSTS".into(), + "host:1.2.3.4,--env LD_PRELOAD=/evil.so".into(), + ), + ("MPC_ACCOUNT_ID".into(), "safe".into()), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(cmd.contains(&"host:1.2.3.4".to_string())); + assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); + } + + #[test] + fn test_ports_does_not_allow_volume_injection() { + let env = BTreeMap::from([ + ("PORTS".into(), "2200:2200,--volume /:/mnt".into()), + ("MPC_ACCOUNT_ID".into(), "safe".into()), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(cmd.contains(&"2200:2200".to_string())); + assert!(!cmd.iter().any(|arg| arg.contains("/:/mnt"))); + } + + #[test] + fn test_invalid_env_key_is_ignored() { + let env = BTreeMap::from([ + ("BAD_KEY".into(), "should_not_be_used".into()), + ("MPC_ACCOUNT_ID".into(), "safe".into()), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(!cmd.join(" ").contains("should_not_be_used")); + assert!(cmd.contains(&"MPC_ACCOUNT_ID=safe".to_string())); + } + + #[test] + fn test_mpc_backup_encryption_key_is_allowed() { + let env = BTreeMap::from([( + "MPC_BACKUP_ENCRYPTION_KEY_HEX".into(), + "0".repeat(64), + )]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(cmd + .join(" ") + .contains(&format!("MPC_BACKUP_ENCRYPTION_KEY_HEX={}", "0".repeat(64)))); + } + + #[test] + fn test_malformed_extra_host_is_ignored() { + let env = BTreeMap::from([ + ( + "EXTRA_HOSTS".into(), + "badhostentry,no-colon,also--bad".into(), + ), + ("MPC_ACCOUNT_ID".into(), "safe".into()), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(!cmd.contains(&"--add-host".to_string())); + } + + #[test] + fn test_env_value_with_shell_injection_is_handled_safely() { + let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "safe; rm -rf /".into())]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(cmd.contains(&"MPC_ACCOUNT_ID=safe; rm -rf /".to_string())); + } + + #[test] + fn test_build_docker_cmd_nontee_no_dstack_mount() { + let mut env = BTreeMap::new(); + env.insert("MPC_ACCOUNT_ID".into(), "x".into()); + env.insert(ENV_VAR_RPC_MAX_ATTEMPTS.into(), "5".into()); + let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); + let s = cmd.join(" "); + assert!(!s.contains("DSTACK_ENDPOINT=")); + assert!(!s.contains(&format!( + "{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}" + ))); + } + + #[test] + fn test_build_docker_cmd_tee_has_dstack_mount() { + let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "x".into())]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + let s = cmd.join(" "); + assert!(s.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); + assert!(s.contains(&format!( + "{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}" + ))); + } + + #[test] + fn test_build_docker_cmd_allows_arbitrary_mpc_prefix_env_vars() { + let mut env = base_env(); + env.insert("MPC_NEW_FEATURE_FLAG".into(), "1".into()); + env.insert("MPC_SOME_CONFIG".into(), "value".into()); + let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); + let cmd_str = cmd.join(" "); + assert!(cmd_str.contains("MPC_NEW_FEATURE_FLAG=1")); + assert!(cmd_str.contains("MPC_SOME_CONFIG=value")); + } + + #[test] + fn test_build_docker_cmd_blocks_sensitive_mpc_private_keys() { + let mut env = base_env(); + env.insert("MPC_P2P_PRIVATE_KEY".into(), "supersecret".into()); + env.insert("MPC_ACCOUNT_SK".into(), "supersecret2".into()); + let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); + let cmd_str = cmd.join(" "); + assert!(!cmd_str.contains("MPC_P2P_PRIVATE_KEY")); + assert!(!cmd_str.contains("MPC_ACCOUNT_SK")); + } + + #[test] + fn test_build_docker_cmd_rejects_env_value_with_newline() { + let mut env = base_env(); + env.insert("MPC_NEW_FEATURE_FLAG".into(), "ok\nbad".into()); + let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); + let cmd_str = cmd.join(" "); + assert!(!cmd_str.contains("MPC_NEW_FEATURE_FLAG")); + } + + #[test] + fn test_build_docker_cmd_enforces_max_env_count_cap() { + let mut env = base_env(); + for i in 0..=MAX_PASSTHROUGH_ENV_VARS { + env.insert(format!("MPC_X_{i}"), "1".into()); + } + let result = build_docker_cmd(Platform::NonTee, &env, &make_digest()); + assert_matches!(result, Err(LauncherError::TooManyEnvVars(_))); + } + + #[test] + fn test_build_docker_cmd_enforces_total_env_bytes_cap() { + let mut env = base_env(); + for i in 0..40 { + env.insert(format!("MPC_BIG_{i}"), "a".repeat(MAX_ENV_VALUE_LEN)); + } + let result = build_docker_cmd(Platform::NonTee, &env, &make_digest()); + assert_matches!(result, Err(LauncherError::EnvPayloadTooLarge(_))); + } + + // -- LD_PRELOAD injection tests ----------------------------------------- + + #[test] + fn test_ld_preload_injection_blocked_via_env_key() { + let env = BTreeMap::from([ + ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), + ( + "--env LD_PRELOAD".into(), + "/path/to/my/malloc.so".into(), + ), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); + } + + #[test] + fn test_ld_preload_injection_blocked_via_extra_hosts() { + let env = BTreeMap::from([ + ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), + ( + "EXTRA_HOSTS".into(), + "host1:192.168.0.1,host2:192.168.0.2,--env LD_PRELOAD=/path/to/my/malloc.so" + .into(), + ), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(cmd.contains(&"--add-host".to_string())); + assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); + } + + #[test] + fn test_ld_preload_injection_blocked_via_ports() { + let env = BTreeMap::from([ + ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), + ( + "PORTS".into(), + "11780:11780,--env LD_PRELOAD=/path/to/my/malloc.so".into(), + ), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(cmd.contains(&"-p".to_string())); + assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); + } + + #[test] + fn test_ld_preload_injection_blocked_via_mpc_account_id() { + let env = BTreeMap::from([ + ( + "MPC_ACCOUNT_ID".into(), + "mpc-user-123, --env LD_PRELOAD=/path/to/my/malloc.so".into(), + ), + ( + "EXTRA_HOSTS".into(), + "host1:192.168.0.1,host2:192.168.0.2".into(), + ), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); + } + + #[test] + fn test_ld_preload_injection_blocked_via_dash_e() { + let env = BTreeMap::from([ + ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), + ("-e LD_PRELOAD".into(), "/path/to/my/malloc.so".into()), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); + } + + #[test] + fn test_ld_preload_injection_blocked_via_extra_hosts_dash_e() { + let env = BTreeMap::from([ + ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), + ( + "EXTRA_HOSTS".into(), + "host1:192.168.0.1,host2:192.168.0.2,-e LD_PRELOAD=/path/to/my/malloc.so".into(), + ), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(cmd.contains(&"--add-host".to_string())); + assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); + } + + #[test] + fn test_ld_preload_injection_blocked_via_ports_dash_e() { + let env = BTreeMap::from([ + ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), + ( + "PORTS".into(), + "11780:11780,-e LD_PRELOAD=/path/to/my/malloc.so".into(), + ), + ]); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + assert!(cmd.contains(&"-p".to_string())); + assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); + } + + // -- Hash selection tests ----------------------------------------------- + + fn make_digest_json(hashes: &[&str]) -> String { + serde_json::json!({"approved_hashes": hashes}).to_string() + } + + #[test] + fn test_override_present() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("image-digest.bin"); + let override_value = format!("sha256:{}", "a".repeat(64)); + let approved = vec![ + format!("sha256:{}", "b".repeat(64)), + override_value.clone(), + format!("sha256:{}", "c".repeat(64)), + ]; + let json = serde_json::json!({"approved_hashes": approved}).to_string(); + std::fs::write(&file, &json).unwrap(); + + // We can't easily override IMAGE_DIGEST_FILE constant, so test load_and_select_hash + // by creating a standalone test that reads from a custom path. + // Instead test the core logic directly: + let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); + assert!(data.approved_hashes.contains(&override_value)); + + let config = BTreeMap::from([( + ENV_VAR_MPC_HASH_OVERRIDE.to_string(), + override_value.clone(), + )]); + // The override is in the approved list, so it should be valid + assert!(is_valid_sha256_digest(&override_value)); + assert!(data.approved_hashes.contains(&override_value)); + } + + #[test] + fn test_override_not_in_list() { + let approved = vec!["sha256:aaa", "sha256:bbb"]; + let json = make_digest_json(&approved); + let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); + let override_hash = "sha256:xyz"; + assert!(!data.approved_hashes.contains(&override_hash.to_string())); + } + + #[test] + fn test_no_override_picks_newest() { + let approved = vec!["sha256:newest", "sha256:older", "sha256:oldest"]; + let json = make_digest_json(&approved); + let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); + assert_eq!(data.approved_hashes[0], "sha256:newest"); + } + + #[test] + fn test_json_key_matches_node() { + // Must stay aligned with crates/node/src/tee/allowed_image_hashes_watcher.rs + let json = r#"{"approved_hashes": ["sha256:abc"]}"#; + let data: ApprovedHashesFile = serde_json::from_str(json).unwrap(); + assert_eq!(data.approved_hashes.len(), 1); + } + + #[test] + fn test_get_bare_digest() { + assert_eq!( + get_bare_digest(&format!("sha256:{}", "a".repeat(64))).unwrap(), + "a".repeat(64) + ); + assert!(get_bare_digest("invalid").is_err()); + } + + #[test] + fn test_is_valid_sha256_digest() { + assert!(is_valid_sha256_digest(&format!("sha256:{}", "a".repeat(64)))); + assert!(!is_valid_sha256_digest("sha256:tooshort")); + assert!(!is_valid_sha256_digest("not-a-digest")); + // Uppercase hex should be rejected + assert!(!is_valid_sha256_digest(&format!("sha256:{}", "A".repeat(64)))); + } + + // -- Platform parsing tests --------------------------------------------- + + #[test] + fn test_parse_platform_missing() { + // Can't easily test env var absence in unit tests without side effects. + // This is tested via the error type: + let err = LauncherError::InvalidPlatform("not set".into()); + assert!(format!("{err}").contains("PLATFORM")); + } + + // -- Full flow docker cmd test ------------------------------------------ + + #[test] + fn test_parse_and_build_docker_cmd_full_flow() { + let config_str = "MPC_ACCOUNT_ID=test-user\nPORTS=11780:11780, --env BAD=oops\nEXTRA_HOSTS=host1:192.168.1.1, --volume /:/mnt\nIMAGE_HASH=sha256:abc123"; + let lines: Vec<&str> = config_str.lines().collect(); + let env = parse_env_lines(&lines); + let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + let cmd_str = cmd.join(" "); + + assert!(cmd_str.contains("MPC_ACCOUNT_ID=test-user")); + assert!(cmd_str.contains("11780:11780")); + assert!(cmd_str.contains("host1:192.168.1.1")); + assert!(!cmd_str.contains("BAD=oops")); + assert!(!cmd_str.contains("/:/mnt")); + } + + #[test] + fn test_full_docker_cmd_structure() { + let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "test-user".into())]); + let digest = make_digest(); + let cmd = build_docker_cmd(Platform::NonTee, &env, &digest).unwrap(); + + // Check required subsequence + assert!(cmd.contains(&"docker".to_string())); + assert!(cmd.contains(&"run".to_string())); + assert!(cmd.contains(&"--security-opt".to_string())); + assert!(cmd.contains(&"no-new-privileges:true".to_string())); + assert!(cmd.contains(&"/tapp:/tapp:ro".to_string())); + assert!(cmd.contains(&"shared-volume:/mnt/shared".to_string())); + assert!(cmd.contains(&"mpc-data:/data".to_string())); + assert!(cmd.contains(&MPC_CONTAINER_NAME.to_string())); + assert!(cmd.contains(&"--detach".to_string())); + // Image digest should be the last argument + assert_eq!(cmd.last().unwrap(), &digest); + } + + // -- Dstack tests ------------------------------------------------------- + + #[test] + fn test_extend_rtmr3_nontee_is_noop() { + // NonTee should return immediately without touching dstack + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(extend_rtmr3(Platform::NonTee, &make_digest())); + assert!(result.is_ok()); + } + + #[test] + fn test_extend_rtmr3_tee_requires_socket() { + // TEE mode should fail when socket doesn't exist + let rt = tokio::runtime::Runtime::new().unwrap(); + let result = rt.block_on(extend_rtmr3(Platform::Tee, &make_digest())); + assert_matches!(result, Err(LauncherError::DstackSocketMissing(_))); + } + + // -- MpcDockerImageHash integration test -------------------------------- + + #[test] + fn test_mpc_docker_image_hash_from_bare_hex() { + let bare_hex = "a".repeat(64); + let hash: MpcDockerImageHash = bare_hex.parse().unwrap(); + assert_eq!(hash.as_hex(), bare_hex); + } + + // -- Integration test (feature-gated) ----------------------------------- + + #[cfg(feature = "integration-test")] + mod integration { + use super::*; + + const TEST_DIGEST: &str = + "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372"; + + fn test_dstack_config() -> BTreeMap { + BTreeMap::from([ + ( + "MPC_IMAGE_TAGS".into(), + "83b52da4e2270c688cdd30da04f6b9d3565f25bb".into(), + ), + ("MPC_IMAGE_NAME".into(), "nearone/testing".into()), + ("MPC_REGISTRY".into(), "registry.hub.docker.com".into()), + ]) + } + + #[tokio::test] + async fn test_validate_image_hash_real_registry() { + let timing = RpcTimingConfig { + request_timeout_secs: 10.0, + request_interval_secs: 1.0, + max_attempts: 20, + }; + let result = validate_image_hash(TEST_DIGEST, &test_dstack_config(), &timing) + .await + .unwrap(); + assert!(result, "validate_image_hash() failed for test image"); + } + } +} From b2621f83d7444108273af4e4789e9a48117fd83d Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 27 Feb 2026 17:08:41 +0100 Subject: [PATCH 002/176] wip --- Cargo.lock | 1 - crates/tee-launcher/Cargo.toml | 1 - crates/tee-launcher/src/main.rs | 51 ++++++++++++++++++++------------- deployment/Dockerfile-launcher | 7 ++--- 4 files changed, 34 insertions(+), 26 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 9bcefd9d5..c9eae8c18 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10548,7 +10548,6 @@ dependencies = [ "mpc-primitives", "regex", "reqwest 0.12.28", - "rstest", "serde", "serde_json", "tempfile", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index 78b4334de..3c7d05bf9 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -26,7 +26,6 @@ tracing-subscriber = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } -rstest = { workspace = true } tempfile = { workspace = true } [lints] diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 2ad98bbbf..76b117184 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -135,8 +135,6 @@ const MAX_ENV_VALUE_LEN: usize = 1024; const MAX_TOTAL_ENV_BYTES: usize = 32 * 1024; // Regex patterns (compiled once) -static SHA256_REGEX: LazyLock = - LazyLock::new(|| Regex::new(r"^sha256:[0-9a-f]{64}$").unwrap()); static MPC_ENV_KEY_RE: LazyLock = LazyLock::new(|| Regex::new(r"^MPC_[A-Z0-9_]{1,64}$").unwrap()); static HOST_ENTRY_RE: LazyLock = @@ -399,19 +397,29 @@ fn get_image_spec(dstack_config: &BTreeMap) -> ImageSpec { // Hash selection // --------------------------------------------------------------------------- -fn is_valid_sha256_digest(digest: &str) -> bool { - SHA256_REGEX.is_match(digest) -} - -fn get_bare_digest(full_digest: &str) -> Result { - full_digest +/// Parse a full `sha256:` digest into a validated [`MpcDockerImageHash`]. +/// +/// Uses the workspace type's `FromStr` impl which does `hex::decode` + 32-byte +/// length check — no regex needed. +fn parse_image_digest(full_digest: &str) -> Result { + let bare_hex = full_digest .strip_prefix(SHA256_PREFIX) - .map(|s| s.to_string()) .ok_or_else(|| { LauncherError::InvalidDefaultDigest(format!( "Invalid digest (missing sha256: prefix): {full_digest}" )) - }) + })?; + bare_hex + .parse::() + .map_err(|e| LauncherError::InvalidDefaultDigest(format!("{full_digest}: {e}"))) +} + +fn is_valid_sha256_digest(digest: &str) -> bool { + parse_image_digest(digest).is_ok() +} + +fn get_bare_digest(full_digest: &str) -> Result { + Ok(parse_image_digest(full_digest)?.as_hex()) } fn load_and_select_hash(dstack_config: &BTreeMap) -> Result { @@ -956,7 +964,7 @@ async fn main() { mod tests { use super::*; use assert_matches::assert_matches; - use rstest::rstest; + // -- Config parsing tests ----------------------------------------------- @@ -1391,10 +1399,6 @@ mod tests { let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); assert!(data.approved_hashes.contains(&override_value)); - let config = BTreeMap::from([( - ENV_VAR_MPC_HASH_OVERRIDE.to_string(), - override_value.clone(), - )]); // The override is in the approved list, so it should be valid assert!(is_valid_sha256_digest(&override_value)); assert!(data.approved_hashes.contains(&override_value)); @@ -1431,7 +1435,7 @@ mod tests { get_bare_digest(&format!("sha256:{}", "a".repeat(64))).unwrap(), "a".repeat(64) ); - assert!(get_bare_digest("invalid").is_err()); + get_bare_digest("invalid").unwrap_err(); } #[test] @@ -1439,8 +1443,15 @@ mod tests { assert!(is_valid_sha256_digest(&format!("sha256:{}", "a".repeat(64)))); assert!(!is_valid_sha256_digest("sha256:tooshort")); assert!(!is_valid_sha256_digest("not-a-digest")); - // Uppercase hex should be rejected - assert!(!is_valid_sha256_digest(&format!("sha256:{}", "A".repeat(64)))); + // hex::decode accepts uppercase; as_hex() normalizes to lowercase + assert!(is_valid_sha256_digest(&format!("sha256:{}", "A".repeat(64)))); + } + + #[test] + fn test_parse_image_digest_normalizes_case() { + let upper = format!("sha256:{}", "AB".repeat(32)); + let hash = parse_image_digest(&upper).unwrap(); + assert_eq!(hash.as_hex(), "ab".repeat(32)); } // -- Platform parsing tests --------------------------------------------- @@ -1496,8 +1507,8 @@ mod tests { fn test_extend_rtmr3_nontee_is_noop() { // NonTee should return immediately without touching dstack let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(extend_rtmr3(Platform::NonTee, &make_digest())); - assert!(result.is_ok()); + rt.block_on(extend_rtmr3(Platform::NonTee, &make_digest())) + .unwrap(); } #[test] diff --git a/deployment/Dockerfile-launcher b/deployment/Dockerfile-launcher index 557e01c07..30bb734e4 100644 --- a/deployment/Dockerfile-launcher +++ b/deployment/Dockerfile-launcher @@ -8,11 +8,10 @@ RUN \ --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ repro-sources-list.sh && \ apt-get update && \ - apt-get install -y --no-install-recommends docker.io docker-compose curl jq python3 && \ + apt-get install -y --no-install-recommends docker.io && \ : "Clean up for improving reproducibility" && \ rm -rf /var/log/* /var/cache/ldconfig/aux-cache -COPY --chmod=0755 tee_launcher/launcher.py /scripts/ -ENV PATH="/scripts:${PATH}" +COPY --chmod=0755 target/release/tee-launcher /usr/local/bin/tee-launcher RUN mkdir -p /app-data && mkdir -p /mnt/shared -CMD ["python3", "/scripts/launcher.py"] +CMD ["tee-launcher"] From f08c453a2b00e9535204da293e30461d03de892a Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 27 Feb 2026 17:25:14 +0100 Subject: [PATCH 003/176] wip --- crates/tee-launcher/src/contants.rs | 33 ++++ crates/tee-launcher/src/main.rs | 294 +++++++++++++--------------- tee_launcher/launcher.py | 1 + 3 files changed, 166 insertions(+), 162 deletions(-) create mode 100644 crates/tee-launcher/src/contants.rs diff --git a/crates/tee-launcher/src/contants.rs b/crates/tee-launcher/src/contants.rs new file mode 100644 index 000000000..454d82642 --- /dev/null +++ b/crates/tee-launcher/src/contants.rs @@ -0,0 +1,33 @@ +pub(crate) const MPC_CONTAINER_NAME: &str = "mpc-node"; +pub(crate) const IMAGE_DIGEST_FILE: &str = "/mnt/shared/image-digest.bin"; +pub(crate) const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; +pub(crate) const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; + +pub(crate) const SHA256_PREFIX: &str = "sha256:"; + +// Docker Hub defaults +pub(crate) const DEFAULT_RPC_REQUEST_TIMEOUT_SECS: f64 = 10.0; +pub(crate) const DEFAULT_RPC_REQUEST_INTERVAL_SECS: f64 = 1.0; +pub(crate) const DEFAULT_RPC_MAX_ATTEMPTS: u32 = 20; + +pub(crate) const DEFAULT_MPC_IMAGE_NAME: &str = "nearone/mpc-node"; +pub(crate) const DEFAULT_MPC_REGISTRY: &str = "registry.hub.docker.com"; +pub(crate) const DEFAULT_MPC_IMAGE_TAG: &str = "latest"; + +// Env var names +pub(crate) const ENV_VAR_PLATFORM: &str = "PLATFORM"; +pub(crate) const ENV_VAR_DEFAULT_IMAGE_DIGEST: &str = "DEFAULT_IMAGE_DIGEST"; +pub(crate) const ENV_VAR_DOCKER_CONTENT_TRUST: &str = "DOCKER_CONTENT_TRUST"; +pub(crate) const ENV_VAR_MPC_HASH_OVERRIDE: &str = "MPC_HASH_OVERRIDE"; +pub(crate) const ENV_VAR_RPC_REQUEST_TIMEOUT_SECS: &str = "RPC_REQUEST_TIMEOUT_SECS"; +pub(crate) const ENV_VAR_RPC_REQUEST_INTERVAL_SECS: &str = "RPC_REQUEST_INTERVAL_SECS"; +pub(crate) const ENV_VAR_RPC_MAX_ATTEMPTS: &str = "RPC_MAX_ATTEMPTS"; + +pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_TAGS: &str = "MPC_IMAGE_TAGS"; +pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_NAME: &str = "MPC_IMAGE_NAME"; +pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY: &str = "MPC_REGISTRY"; + +// Security limits +pub(crate) const MAX_PASSTHROUGH_ENV_VARS: usize = 64; +pub(crate) const MAX_ENV_VALUE_LEN: usize = 1024; +pub(crate) const MAX_TOTAL_ENV_BYTES: usize = 32 * 1024; diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 76b117184..587576828 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -2,6 +2,7 @@ use std::collections::{BTreeMap, HashSet, VecDeque}; use std::process::Command; use std::sync::LazyLock; +use contants::*; use regex::Regex; use serde::Deserialize; use thiserror::Error; @@ -9,6 +10,72 @@ use thiserror::Error; // Reuse the workspace hash type for type-safe image hash handling. use mpc_primitives::hash::MpcDockerImageHash; +mod contants; + +#[tokio::main] +async fn main() { + tracing_subscriber::fmt() + .with_env_filter( + tracing_subscriber::EnvFilter::from_default_env() + .add_directive(tracing::Level::INFO.into()), + ) + .init(); + + if let Err(e) = run().await { + tracing::error!("Error: {e}"); + std::process::exit(1); + } +} + +async fn run() -> Result<()> { + tracing::info!("start"); + + let platform = parse_platform()?; + tracing::info!( + "Launcher platform: {}", + match platform { + Platform::Tee => "TEE", + Platform::NonTee => "NONTEE", + } + ); + + if platform == Platform::Tee && !is_unix_socket(DSTACK_UNIX_SOCKET) { + return Err(LauncherError::DstackSocketMissing( + DSTACK_UNIX_SOCKET.to_string(), + )); + } + + // DOCKER_CONTENT_TRUST must be enabled + let dct = std::env::var(ENV_VAR_DOCKER_CONTENT_TRUST).unwrap_or_default(); + if dct != "1" { + return Err(LauncherError::DockerContentTrustNotEnabled); + } + + // Load dstack user config + let dstack_config: BTreeMap = + if std::path::Path::new(DSTACK_USER_CONFIG_FILE).is_file() { + parse_env_file(DSTACK_USER_CONFIG_FILE)? + } else { + BTreeMap::new() + }; + + let rpc_cfg = load_rpc_timing_config(&dstack_config); + + let selected_hash = load_and_select_hash(&dstack_config)?; + + if !validate_image_hash(&selected_hash, &dstack_config, &rpc_cfg).await? { + return Err(LauncherError::ImageValidationFailed(selected_hash)); + } + + tracing::info!("MPC image hash validated successfully: {selected_hash}"); + + extend_rtmr3(platform, &selected_hash).await?; + + launch_mpc_container(platform, &selected_hash, &dstack_config)?; + + Ok(()) +} + // --------------------------------------------------------------------------- // Error // --------------------------------------------------------------------------- @@ -100,40 +167,6 @@ type Result = std::result::Result; // Constants — matching Python launcher exactly // --------------------------------------------------------------------------- -const MPC_CONTAINER_NAME: &str = "mpc-node"; -const IMAGE_DIGEST_FILE: &str = "/mnt/shared/image-digest.bin"; -const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; -const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; - -const SHA256_PREFIX: &str = "sha256:"; - -// Docker Hub defaults -const DEFAULT_RPC_REQUEST_TIMEOUT_SECS: f64 = 10.0; -const DEFAULT_RPC_REQUEST_INTERVAL_SECS: f64 = 1.0; -const DEFAULT_RPC_MAX_ATTEMPTS: u32 = 20; - -const DEFAULT_MPC_IMAGE_NAME: &str = "nearone/mpc-node"; -const DEFAULT_MPC_REGISTRY: &str = "registry.hub.docker.com"; -const DEFAULT_MPC_IMAGE_TAG: &str = "latest"; - -// Env var names -const ENV_VAR_PLATFORM: &str = "PLATFORM"; -const ENV_VAR_DEFAULT_IMAGE_DIGEST: &str = "DEFAULT_IMAGE_DIGEST"; -const ENV_VAR_DOCKER_CONTENT_TRUST: &str = "DOCKER_CONTENT_TRUST"; -const ENV_VAR_MPC_HASH_OVERRIDE: &str = "MPC_HASH_OVERRIDE"; -const ENV_VAR_RPC_REQUEST_TIMEOUT_SECS: &str = "RPC_REQUEST_TIMEOUT_SECS"; -const ENV_VAR_RPC_REQUEST_INTERVAL_SECS: &str = "RPC_REQUEST_INTERVAL_SECS"; -const ENV_VAR_RPC_MAX_ATTEMPTS: &str = "RPC_MAX_ATTEMPTS"; - -const DSTACK_USER_CONFIG_MPC_IMAGE_TAGS: &str = "MPC_IMAGE_TAGS"; -const DSTACK_USER_CONFIG_MPC_IMAGE_NAME: &str = "MPC_IMAGE_NAME"; -const DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY: &str = "MPC_REGISTRY"; - -// Security limits -const MAX_PASSTHROUGH_ENV_VARS: usize = 64; -const MAX_ENV_VALUE_LEN: usize = 1024; -const MAX_TOTAL_ENV_BYTES: usize = 32 * 1024; - // Regex patterns (compiled once) static MPC_ENV_KEY_RE: LazyLock = LazyLock::new(|| Regex::new(r"^MPC_[A-Z0-9_]{1,64}$").unwrap()); @@ -145,8 +178,7 @@ static INVALID_HOST_ENTRY_PATTERN: LazyLock = LazyLock::new(|| Regex::new(r"^[;&|`$\\<>\-]|^--").unwrap()); // Denied env keys — never pass these to the container -static DENIED_CONTAINER_ENV_KEYS: LazyLock> = - LazyLock::new(|| HashSet::from(["MPC_P2P_PRIVATE_KEY", "MPC_ACCOUNT_SK"])); +const DENIED_CONTAINER_ENV_KEYS: &[&str] = &["MPC_P2P_PRIVATE_KEY", "MPC_ACCOUNT_SK"]; // Allowed non-MPC env vars (backward compatibility) static ALLOWED_MPC_ENV_VARS: LazyLock> = LazyLock::new(|| { @@ -220,11 +252,13 @@ struct ApprovedHashesFile { // --------------------------------------------------------------------------- fn has_control_chars(s: &str) -> bool { - for ch in s.chars() { - if ch == '\n' || ch == '\r' || ch == '\0' { + let control_chars = ['\n', '\r', '\0']; + + for character in s.chars() { + if control_chars.contains(&character) { return true; } - if (ch as u32) < 0x20 && ch != '\t' { + if (character as u32) < 0x20 && character != '\t' { return true; } } @@ -284,12 +318,14 @@ fn is_safe_port_mapping(mapping: &str) -> bool { } fn is_allowed_container_env_key(key: &str) -> bool { - if DENIED_CONTAINER_ENV_KEYS.contains(key) { + if DENIED_CONTAINER_ENV_KEYS.contains(&key) { return false; } + // Allow MPC_* keys with strict regex if MPC_ENV_KEY_RE.is_match(key) { return true; } + // Keep allowlist if ALLOWED_MPC_ENV_VARS.contains(key) { return true; } @@ -402,13 +438,11 @@ fn get_image_spec(dstack_config: &BTreeMap) -> ImageSpec { /// Uses the workspace type's `FromStr` impl which does `hex::decode` + 32-byte /// length check — no regex needed. fn parse_image_digest(full_digest: &str) -> Result { - let bare_hex = full_digest - .strip_prefix(SHA256_PREFIX) - .ok_or_else(|| { - LauncherError::InvalidDefaultDigest(format!( - "Invalid digest (missing sha256: prefix): {full_digest}" - )) - })?; + let bare_hex = full_digest.strip_prefix(SHA256_PREFIX).ok_or_else(|| { + LauncherError::InvalidDefaultDigest(format!( + "Invalid digest (missing sha256: prefix): {full_digest}" + )) + })?; bare_hex .parse::() .map_err(|e| LauncherError::InvalidDefaultDigest(format!("{full_digest}: {e}"))) @@ -424,11 +458,12 @@ fn get_bare_digest(full_digest: &str) -> Result { fn load_and_select_hash(dstack_config: &BTreeMap) -> Result { let approved_hashes = if std::path::Path::new(IMAGE_DIGEST_FILE).is_file() { - let content = - std::fs::read_to_string(IMAGE_DIGEST_FILE).map_err(|source| LauncherError::FileRead { + let content = std::fs::read_to_string(IMAGE_DIGEST_FILE).map_err(|source| { + LauncherError::FileRead { path: IMAGE_DIGEST_FILE.to_string(), source, - })?; + } + })?; let data: ApprovedHashesFile = serde_json::from_str(&content).map_err(|source| LauncherError::JsonParse { path: IMAGE_DIGEST_FILE.to_string(), @@ -452,9 +487,7 @@ fn load_and_select_hash(dstack_config: &BTreeMap) -> Result) -> Result Result { +async fn get_manifest_digest(image: &ResolvedImage, timing: &RpcTimingConfig) -> Result { if image.spec.tags.is_empty() { return Err(LauncherError::ImageHashNotFoundAmongTags); } @@ -593,10 +623,10 @@ async fn get_manifest_digest( .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()); - let manifest: serde_json::Value = - resp.json().await.map_err(|e| { - LauncherError::RegistryResponseParse(e.to_string()) - })?; + let manifest: serde_json::Value = resp + .json() + .await + .map_err(|e| LauncherError::RegistryResponseParse(e.to_string()))?; let media_type = manifest["mediaType"].as_str().unwrap_or(""); match media_type { @@ -616,8 +646,7 @@ async fn get_manifest_digest( } "application/vnd.docker.distribution.manifest.v2+json" | "application/vnd.oci.image.manifest.v1+json" => { - let config_digest = - manifest["config"]["digest"].as_str().unwrap_or(""); + let config_digest = manifest["config"]["digest"].as_str().unwrap_or(""); if config_digest == image.digest { if let Some(digest) = content_digest { return Ok(digest); @@ -667,7 +696,13 @@ async fn validate_image_hash( // Verify digest let inspect = Command::new("docker") - .args(["image", "inspect", "--format", "{{index .ID}}", &name_and_digest]) + .args([ + "image", + "inspect", + "--format", + "{{index .ID}}", + &name_and_digest, + ]) .output() .map_err(|e| LauncherError::DockerInspectFailed(e.to_string()))?; if !inspect.status.success() { @@ -695,8 +730,9 @@ fn remove_existing_container() { .output(); match output { - Ok(out) => { - let names = String::from_utf8_lossy(&out.stdout); + Ok(output) => { + let names = String::from_utf8_lossy(&output.stdout); + if names.lines().any(|n| n == MPC_CONTAINER_NAME) { tracing::info!("Removing existing container: {MPC_CONTAINER_NAME}"); let _ = Command::new("docker") @@ -704,8 +740,8 @@ fn remove_existing_container() { .output(); } } - Err(e) => { - tracing::warn!("Failed to check/remove container {MPC_CONTAINER_NAME}: {e}"); + Err(error) => { + tracing::warn!("Failed to check/remove container {MPC_CONTAINER_NAME}: {error}"); } } } @@ -873,8 +909,7 @@ async fn extend_rtmr3(platform: Platform, valid_hash: &str) -> Result<()> { let bare = get_bare_digest(valid_hash)?; tracing::info!("Extending RTMR3 with validated hash: {bare}"); - let client = - dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); + let client = dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); // GetQuote first client @@ -891,71 +926,6 @@ async fn extend_rtmr3(platform: Platform, valid_hash: &str) -> Result<()> { Ok(()) } -// --------------------------------------------------------------------------- -// Main orchestration -// --------------------------------------------------------------------------- - -async fn run() -> Result<()> { - tracing::info!("start"); - - let platform = parse_platform()?; - tracing::info!("Launcher platform: {}", match platform { - Platform::Tee => "TEE", - Platform::NonTee => "NONTEE", - }); - - if platform == Platform::Tee && !is_unix_socket(DSTACK_UNIX_SOCKET) { - return Err(LauncherError::DstackSocketMissing( - DSTACK_UNIX_SOCKET.to_string(), - )); - } - - // DOCKER_CONTENT_TRUST must be enabled - let dct = std::env::var(ENV_VAR_DOCKER_CONTENT_TRUST).unwrap_or_default(); - if dct != "1" { - return Err(LauncherError::DockerContentTrustNotEnabled); - } - - // Load dstack user config - let dstack_config: BTreeMap = - if std::path::Path::new(DSTACK_USER_CONFIG_FILE).is_file() { - parse_env_file(DSTACK_USER_CONFIG_FILE)? - } else { - BTreeMap::new() - }; - - let rpc_cfg = load_rpc_timing_config(&dstack_config); - - let selected_hash = load_and_select_hash(&dstack_config)?; - - if !validate_image_hash(&selected_hash, &dstack_config, &rpc_cfg).await? { - return Err(LauncherError::ImageValidationFailed(selected_hash)); - } - - tracing::info!("MPC image hash validated successfully: {selected_hash}"); - - extend_rtmr3(platform, &selected_hash).await?; - - launch_mpc_container(platform, &selected_hash, &dstack_config)?; - - Ok(()) -} - -#[tokio::main] -async fn main() { - tracing_subscriber::fmt() - .with_env_filter( - tracing_subscriber::EnvFilter::from_default_env() - .add_directive(tracing::Level::INFO.into()), - ) - .init(); - - if let Err(e) = run().await { - tracing::error!("Error: {e}"); - std::process::exit(1); - } -} - // --------------------------------------------------------------------------- // Tests // --------------------------------------------------------------------------- @@ -965,7 +935,6 @@ mod tests { use super::*; use assert_matches::assert_matches; - // -- Config parsing tests ----------------------------------------------- #[test] @@ -987,7 +956,12 @@ mod tests { #[test] fn test_config_ignores_blank_lines_and_comments() { - let lines = vec!["", " # This is a comment", " MPC_SECRET_STORE_KEY=topsecret", ""]; + let lines = vec![ + "", + " # This is a comment", + " MPC_SECRET_STORE_KEY=topsecret", + "", + ]; let env = parse_env_lines(&lines); assert_eq!(env.get("MPC_SECRET_STORE_KEY").unwrap(), "topsecret"); assert_eq!(env.len(), 1); @@ -1171,14 +1145,12 @@ mod tests { #[test] fn test_mpc_backup_encryption_key_is_allowed() { - let env = BTreeMap::from([( - "MPC_BACKUP_ENCRYPTION_KEY_HEX".into(), - "0".repeat(64), - )]); + let env = BTreeMap::from([("MPC_BACKUP_ENCRYPTION_KEY_HEX".into(), "0".repeat(64))]); let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(cmd - .join(" ") - .contains(&format!("MPC_BACKUP_ENCRYPTION_KEY_HEX={}", "0".repeat(64)))); + assert!( + cmd.join(" ") + .contains(&format!("MPC_BACKUP_ENCRYPTION_KEY_HEX={}", "0".repeat(64))) + ); } #[test] @@ -1209,9 +1181,7 @@ mod tests { let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); let s = cmd.join(" "); assert!(!s.contains("DSTACK_ENDPOINT=")); - assert!(!s.contains(&format!( - "{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}" - ))); + assert!(!s.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); } #[test] @@ -1220,9 +1190,7 @@ mod tests { let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); let s = cmd.join(" "); assert!(s.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); - assert!(s.contains(&format!( - "{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}" - ))); + assert!(s.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); } #[test] @@ -1282,10 +1250,7 @@ mod tests { fn test_ld_preload_injection_blocked_via_env_key() { let env = BTreeMap::from([ ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), - ( - "--env LD_PRELOAD".into(), - "/path/to/my/malloc.so".into(), - ), + ("--env LD_PRELOAD".into(), "/path/to/my/malloc.so".into()), ]); let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); @@ -1297,8 +1262,7 @@ mod tests { ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), ( "EXTRA_HOSTS".into(), - "host1:192.168.0.1,host2:192.168.0.2,--env LD_PRELOAD=/path/to/my/malloc.so" - .into(), + "host1:192.168.0.1,host2:192.168.0.2,--env LD_PRELOAD=/path/to/my/malloc.so".into(), ), ]); let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); @@ -1440,11 +1404,17 @@ mod tests { #[test] fn test_is_valid_sha256_digest() { - assert!(is_valid_sha256_digest(&format!("sha256:{}", "a".repeat(64)))); + assert!(is_valid_sha256_digest(&format!( + "sha256:{}", + "a".repeat(64) + ))); assert!(!is_valid_sha256_digest("sha256:tooshort")); assert!(!is_valid_sha256_digest("not-a-digest")); // hex::decode accepts uppercase; as_hex() normalizes to lowercase - assert!(is_valid_sha256_digest(&format!("sha256:{}", "A".repeat(64)))); + assert!(is_valid_sha256_digest(&format!( + "sha256:{}", + "A".repeat(64) + ))); } #[test] diff --git a/tee_launcher/launcher.py b/tee_launcher/launcher.py index a0dd34958..73ae00cf4 100644 --- a/tee_launcher/launcher.py +++ b/tee_launcher/launcher.py @@ -260,6 +260,7 @@ class ImageSpec: image_name: str registry: str + # TODO: This post validation is not covered def __post_init__(self): if not self.tags or not all(is_non_empty_and_cleaned(tag) for tag in self.tags): raise ValueError( From 315a8335090332636aeeeab245f5dd695e13dd16 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 27 Feb 2026 17:52:12 +0100 Subject: [PATCH 004/176] . --- Cargo.lock | 1 + crates/tee-launcher/Cargo.toml | 1 + crates/tee-launcher/src/contants.rs | 3 - crates/tee-launcher/src/error.rs | 81 ++++++++++ crates/tee-launcher/src/main.rs | 243 +++++----------------------- crates/tee-launcher/src/types.rs | 54 +++++++ 6 files changed, 181 insertions(+), 202 deletions(-) create mode 100644 crates/tee-launcher/src/error.rs create mode 100644 crates/tee-launcher/src/types.rs diff --git a/Cargo.lock b/Cargo.lock index c9eae8c18..ecc7a1f21 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10543,6 +10543,7 @@ name = "tee-launcher" version = "3.5.1" dependencies = [ "assert_matches", + "clap", "dstack-sdk", "hex", "mpc-primitives", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index 3c7d05bf9..204b8ab68 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -12,6 +12,7 @@ path = "src/main.rs" integration-test = [] [dependencies] +clap = { workspace = true } dstack-sdk = { workspace = true } hex = { workspace = true } mpc-primitives = { path = "../primitives" } diff --git a/crates/tee-launcher/src/contants.rs b/crates/tee-launcher/src/contants.rs index 454d82642..c3926b2b6 100644 --- a/crates/tee-launcher/src/contants.rs +++ b/crates/tee-launcher/src/contants.rs @@ -15,9 +15,6 @@ pub(crate) const DEFAULT_MPC_REGISTRY: &str = "registry.hub.docker.com"; pub(crate) const DEFAULT_MPC_IMAGE_TAG: &str = "latest"; // Env var names -pub(crate) const ENV_VAR_PLATFORM: &str = "PLATFORM"; -pub(crate) const ENV_VAR_DEFAULT_IMAGE_DIGEST: &str = "DEFAULT_IMAGE_DIGEST"; -pub(crate) const ENV_VAR_DOCKER_CONTENT_TRUST: &str = "DOCKER_CONTENT_TRUST"; pub(crate) const ENV_VAR_MPC_HASH_OVERRIDE: &str = "MPC_HASH_OVERRIDE"; pub(crate) const ENV_VAR_RPC_REQUEST_TIMEOUT_SECS: &str = "RPC_REQUEST_TIMEOUT_SECS"; pub(crate) const ENV_VAR_RPC_REQUEST_INTERVAL_SECS: &str = "RPC_REQUEST_INTERVAL_SECS"; diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs new file mode 100644 index 000000000..73a594bd0 --- /dev/null +++ b/crates/tee-launcher/src/error.rs @@ -0,0 +1,81 @@ +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum LauncherError { + #[error("DOCKER_CONTENT_TRUST must be set to 1")] + DockerContentTrustNotEnabled, + + #[error("PLATFORM=TEE requires dstack unix socket at {0}")] + DstackSocketMissing(String), + + #[error("GetQuote failed before extending RTMR3: {0}")] + DstackGetQuoteFailed(String), + + #[error("EmitEvent failed while extending RTMR3: {0}")] + DstackEmitEventFailed(String), + + #[error("DEFAULT_IMAGE_DIGEST invalid: {0}")] + InvalidDefaultDigest(String), + + #[error("Invalid JSON in {path}: approved_hashes missing or empty")] + InvalidApprovedHashes { path: String }, + + #[error("MPC_HASH_OVERRIDE invalid: {0}")] + InvalidHashOverride(String), + + #[error("Image hash not found among tags")] + ImageHashNotFoundAmongTags, + + #[error("Failed to get auth token from registry: {0}")] + RegistryAuthFailed(String), + + #[error("Failed to get successful response from {url} after {attempts} attempts")] + RegistryRequestFailed { url: String, attempts: u32 }, + + #[error("docker pull failed for {0}")] + DockerPullFailed(String), + + #[error("docker inspect failed for {0}")] + DockerInspectFailed(String), + + #[error("Digest mismatch: pulled {pulled} != expected {expected}")] + DigestMismatch { pulled: String, expected: String }, + + #[error("MPC image hash validation failed: {0}")] + ImageValidationFailed(String), + + #[error("docker run failed for validated hash={0}")] + DockerRunFailed(String), + + #[error("Too many env vars to pass through (>{0})")] + TooManyEnvVars(usize), + + #[error("Total env payload too large (>{0} bytes)")] + EnvPayloadTooLarge(usize), + + #[error("Unsafe docker command: LD_PRELOAD detected")] + LdPreloadDetected, + + #[error("Failed to read {path}: {source}")] + FileRead { + path: String, + source: std::io::Error, + }, + + #[error("Failed to parse {path}: {source}")] + JsonParse { + path: String, + source: serde_json::Error, + }, + + #[error("Required environment variable not set: {0}")] + MissingEnvVar(String), + + #[error("HTTP error: {0}")] + Http(#[from] reqwest::Error), + + #[error("Registry response parse error: {0}")] + RegistryResponseParse(String), +} + +pub type Result = std::result::Result; diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 587576828..8ad64faf7 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -2,15 +2,20 @@ use std::collections::{BTreeMap, HashSet, VecDeque}; use std::process::Command; use std::sync::LazyLock; -use contants::*; +use clap::Parser; use regex::Regex; -use serde::Deserialize; -use thiserror::Error; +use std::os::unix::fs::FileTypeExt as _; // Reuse the workspace hash type for type-safe image hash handling. use mpc_primitives::hash::MpcDockerImageHash; +use contants::*; +use error::*; +use types::*; + mod contants; +mod error; +mod types; #[tokio::main] async fn main() { @@ -30,24 +35,20 @@ async fn main() { async fn run() -> Result<()> { tracing::info!("start"); - let platform = parse_platform()?; - tracing::info!( - "Launcher platform: {}", - match platform { - Platform::Tee => "TEE", - Platform::NonTee => "NONTEE", - } - ); + let args = CliArgs::parse(); - if platform == Platform::Tee && !is_unix_socket(DSTACK_UNIX_SOCKET) { + tracing::info!(platform = ?args.platform, "starting launcher"); + + // TODO is_unix_socket can be a compile time check + if args.platform == Platform::Tee && !is_unix_socket(DSTACK_UNIX_SOCKET) { return Err(LauncherError::DstackSocketMissing( DSTACK_UNIX_SOCKET.to_string(), )); } + // TODO: `docker_content_trust` parse it to a type that only accepts deserialization into number 1 // DOCKER_CONTENT_TRUST must be enabled - let dct = std::env::var(ENV_VAR_DOCKER_CONTENT_TRUST).unwrap_or_default(); - if dct != "1" { + if args.docker_content_trust != "1" { return Err(LauncherError::DockerContentTrustNotEnabled); } @@ -61,7 +62,7 @@ async fn run() -> Result<()> { let rpc_cfg = load_rpc_timing_config(&dstack_config); - let selected_hash = load_and_select_hash(&dstack_config)?; + let selected_hash = load_and_select_hash(&args, &dstack_config)?; if !validate_image_hash(&selected_hash, &dstack_config, &rpc_cfg).await? { return Err(LauncherError::ImageValidationFailed(selected_hash)); @@ -69,100 +70,13 @@ async fn run() -> Result<()> { tracing::info!("MPC image hash validated successfully: {selected_hash}"); - extend_rtmr3(platform, &selected_hash).await?; + extend_rtmr3(args.platform, &selected_hash).await?; - launch_mpc_container(platform, &selected_hash, &dstack_config)?; + launch_mpc_container(args.platform, &selected_hash, &dstack_config)?; Ok(()) } -// --------------------------------------------------------------------------- -// Error -// --------------------------------------------------------------------------- - -#[derive(Error, Debug)] -pub enum LauncherError { - #[error("PLATFORM must be set to one of [TEE, NONTEE], got: {0}")] - InvalidPlatform(String), - - #[error("DOCKER_CONTENT_TRUST must be set to 1")] - DockerContentTrustNotEnabled, - - #[error("PLATFORM=TEE requires dstack unix socket at {0}")] - DstackSocketMissing(String), - - #[error("GetQuote failed before extending RTMR3: {0}")] - DstackGetQuoteFailed(String), - - #[error("EmitEvent failed while extending RTMR3: {0}")] - DstackEmitEventFailed(String), - - #[error("DEFAULT_IMAGE_DIGEST invalid: {0}")] - InvalidDefaultDigest(String), - - #[error("Invalid JSON in {path}: approved_hashes missing or empty")] - InvalidApprovedHashes { path: String }, - - #[error("MPC_HASH_OVERRIDE invalid: {0}")] - InvalidHashOverride(String), - - #[error("Image hash not found among tags")] - ImageHashNotFoundAmongTags, - - #[error("Failed to get auth token from registry: {0}")] - RegistryAuthFailed(String), - - #[error("Failed to get successful response from {url} after {attempts} attempts")] - RegistryRequestFailed { url: String, attempts: u32 }, - - #[error("docker pull failed for {0}")] - DockerPullFailed(String), - - #[error("docker inspect failed for {0}")] - DockerInspectFailed(String), - - #[error("Digest mismatch: pulled {pulled} != expected {expected}")] - DigestMismatch { pulled: String, expected: String }, - - #[error("MPC image hash validation failed: {0}")] - ImageValidationFailed(String), - - #[error("docker run failed for validated hash={0}")] - DockerRunFailed(String), - - #[error("Too many env vars to pass through (>{0})")] - TooManyEnvVars(usize), - - #[error("Total env payload too large (>{0} bytes)")] - EnvPayloadTooLarge(usize), - - #[error("Unsafe docker command: LD_PRELOAD detected")] - LdPreloadDetected, - - #[error("Failed to read {path}: {source}")] - FileRead { - path: String, - source: std::io::Error, - }, - - #[error("Failed to parse {path}: {source}")] - JsonParse { - path: String, - source: serde_json::Error, - }, - - #[error("Required environment variable not set: {0}")] - MissingEnvVar(String), - - #[error("HTTP error: {0}")] - Http(#[from] reqwest::Error), - - #[error("Registry response parse error: {0}")] - RegistryResponseParse(String), -} - -type Result = std::result::Result; - // --------------------------------------------------------------------------- // Constants — matching Python launcher exactly // --------------------------------------------------------------------------- @@ -181,21 +95,19 @@ static INVALID_HOST_ENTRY_PATTERN: LazyLock = const DENIED_CONTAINER_ENV_KEYS: &[&str] = &["MPC_P2P_PRIVATE_KEY", "MPC_ACCOUNT_SK"]; // Allowed non-MPC env vars (backward compatibility) -static ALLOWED_MPC_ENV_VARS: LazyLock> = LazyLock::new(|| { - HashSet::from([ - "MPC_ACCOUNT_ID", - "MPC_LOCAL_ADDRESS", - "MPC_SECRET_STORE_KEY", - "MPC_CONTRACT_ID", - "MPC_ENV", - "MPC_HOME_DIR", - "NEAR_BOOT_NODES", - "RUST_BACKTRACE", - "RUST_LOG", - "MPC_RESPONDER_ID", - "MPC_BACKUP_ENCRYPTION_KEY_HEX", - ]) -}); +const ALLOWED_MPC_ENV_VARS: &[&str] = &[ + "MPC_ACCOUNT_ID", + "MPC_LOCAL_ADDRESS", + "MPC_SECRET_STORE_KEY", + "MPC_CONTRACT_ID", + "MPC_ENV", + "MPC_HOME_DIR", + "NEAR_BOOT_NODES", + "RUST_BACKTRACE", + "RUST_LOG", + "MPC_RESPONDER_ID", + "MPC_BACKUP_ENCRYPTION_KEY_HEX", +]; // Launcher-only env vars — read from user config but never forwarded to container static ALLOWED_LAUNCHER_ENV_VARS: LazyLock> = LazyLock::new(|| { @@ -210,43 +122,6 @@ static ALLOWED_LAUNCHER_ENV_VARS: LazyLock> = LazyLock::new(|| { ]) }); -// --------------------------------------------------------------------------- -// Types -// --------------------------------------------------------------------------- - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum Platform { - Tee, - NonTee, -} - -#[derive(Debug, Clone)] -pub struct RpcTimingConfig { - pub request_timeout_secs: f64, - pub request_interval_secs: f64, - pub max_attempts: u32, -} - -#[derive(Debug, Clone)] -pub struct ImageSpec { - pub tags: Vec, - pub image_name: String, - pub registry: String, -} - -#[derive(Debug, Clone)] -pub struct ResolvedImage { - pub spec: ImageSpec, - pub digest: String, -} - -/// JSON structure for the approved hashes file written by the MPC node. -/// Must stay aligned with `crates/node/src/tee/allowed_image_hashes_watcher.rs`. -#[derive(Debug, Deserialize)] -struct ApprovedHashesFile { - approved_hashes: Vec, -} - // --------------------------------------------------------------------------- // Validation functions — security policy for env passthrough // --------------------------------------------------------------------------- @@ -326,7 +201,7 @@ fn is_allowed_container_env_key(key: &str) -> bool { return true; } // Keep allowlist - if ALLOWED_MPC_ENV_VARS.contains(key) { + if ALLOWED_MPC_ENV_VARS.contains(&key) { return true; } false @@ -364,20 +239,6 @@ fn parse_env_file(path: &str) -> Result> { Ok(parse_env_lines(&lines)) } -fn parse_platform() -> Result { - let raw = std::env::var(ENV_VAR_PLATFORM).map_err(|_| { - LauncherError::InvalidPlatform(format!( - "{ENV_VAR_PLATFORM} must be set to one of [TEE, NONTEE]" - )) - })?; - let val = raw.trim(); - match val { - "TEE" => Ok(Platform::Tee), - "NONTEE" => Ok(Platform::NonTee), - other => Err(LauncherError::InvalidPlatform(other.to_string())), - } -} - fn load_rpc_timing_config(dstack_config: &BTreeMap) -> RpcTimingConfig { let timeout_secs = dstack_config .get(ENV_VAR_RPC_REQUEST_TIMEOUT_SECS) @@ -456,7 +317,10 @@ fn get_bare_digest(full_digest: &str) -> Result { Ok(parse_image_digest(full_digest)?.as_hex()) } -fn load_and_select_hash(dstack_config: &BTreeMap) -> Result { +fn load_and_select_hash( + args: &CliArgs, + dstack_config: &BTreeMap, +) -> Result { let approved_hashes = if std::path::Path::new(IMAGE_DIGEST_FILE).is_file() { let content = std::fs::read_to_string(IMAGE_DIGEST_FILE).map_err(|source| { LauncherError::FileRead { @@ -476,11 +340,13 @@ fn load_and_select_hash(dstack_config: &BTreeMap) -> Result bool { - use std::os::unix::fs::FileTypeExt; - match std::fs::metadata(path) { - Ok(meta) => meta.file_type().is_socket(), - Err(_) => false, - } + std::fs::metadata(path).is_ok_and(|meta| meta.file_type().is_socket()) } async fn extend_rtmr3(platform: Platform, valid_hash: &str) -> Result<()> { @@ -926,10 +785,6 @@ async fn extend_rtmr3(platform: Platform, valid_hash: &str) -> Result<()> { Ok(()) } -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - #[cfg(test)] mod tests { use super::*; @@ -1424,16 +1279,6 @@ mod tests { assert_eq!(hash.as_hex(), "ab".repeat(32)); } - // -- Platform parsing tests --------------------------------------------- - - #[test] - fn test_parse_platform_missing() { - // Can't easily test env var absence in unit tests without side effects. - // This is tested via the error type: - let err = LauncherError::InvalidPlatform("not set".into()); - assert!(format!("{err}").contains("PLATFORM")); - } - // -- Full flow docker cmd test ------------------------------------------ #[test] diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs new file mode 100644 index 000000000..707326904 --- /dev/null +++ b/crates/tee-launcher/src/types.rs @@ -0,0 +1,54 @@ +use clap::{Parser, ValueEnum}; +use serde::Deserialize; + +/// CLI arguments parsed from environment variables via clap. +#[derive(Parser, Debug)] +#[command(name = "tee-launcher")] +pub struct CliArgs { + /// Platform mode: TEE or NONTEE + #[arg(long, env = "PLATFORM")] + pub platform: Platform, + + /// Must be set to "1" to enable Docker Content Trust + #[arg(long, env = "DOCKER_CONTENT_TRUST", default_value = "")] + pub docker_content_trust: String, + + /// Fallback image digest when the approved-hashes file is absent + #[arg(long, env = "DEFAULT_IMAGE_DIGEST")] + pub default_image_digest: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +pub enum Platform { + #[value(name = "TEE")] + Tee, + #[value(name = "NONTEE")] + NonTee, +} + +#[derive(Debug, Clone)] +pub struct RpcTimingConfig { + pub request_timeout_secs: f64, + pub request_interval_secs: f64, + pub max_attempts: u32, +} + +#[derive(Debug, Clone)] +pub struct ImageSpec { + pub tags: Vec, + pub image_name: String, + pub registry: String, +} + +#[derive(Debug, Clone)] +pub struct ResolvedImage { + pub spec: ImageSpec, + pub digest: String, +} + +/// JSON structure for the approved hashes file written by the MPC node. +/// Must stay aligned with `crates/node/src/tee/allowed_image_hashes_watcher.rs`. +#[derive(Debug, Deserialize)] +pub struct ApprovedHashesFile { + pub approved_hashes: Vec, +} From 07b3be510ce7c051ea0b177548283b15f76fc367 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 27 Feb 2026 18:33:00 +0100 Subject: [PATCH 005/176] wip --- Cargo.lock | 10 +++ Cargo.toml | 2 + crates/launcher-interface/Cargo.toml | 13 ++++ crates/launcher-interface/src/lib.rs | 12 ++++ crates/node/Cargo.toml | 1 + .../src/tee/allowed_image_hashes_watcher.rs | 24 ++----- crates/tee-launcher/Cargo.toml | 3 +- crates/tee-launcher/src/main.rs | 65 ++++++++++--------- crates/tee-launcher/src/types.rs | 11 +--- 9 files changed, 85 insertions(+), 56 deletions(-) create mode 100644 crates/launcher-interface/Cargo.toml create mode 100644 crates/launcher-interface/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index ecc7a1f21..49e17fb79 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4925,6 +4925,14 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "launcher-interface" +version = "3.5.1" +dependencies = [ + "mpc-primitives", + "serde", +] + [[package]] name = "lazy_static" version = "1.5.0" @@ -5489,6 +5497,7 @@ dependencies = [ "hyper 0.14.32", "itertools 0.14.0", "k256", + "launcher-interface", "lru 0.16.3", "mockall", "mpc-attestation", @@ -10546,6 +10555,7 @@ dependencies = [ "clap", "dstack-sdk", "hex", + "launcher-interface", "mpc-primitives", "regex", "reqwest 0.12.28", diff --git a/Cargo.toml b/Cargo.toml index 57f1ae560..5c1ba25fb 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,6 +13,7 @@ members = [ "crates/foreign-chain-inspector", "crates/foreign-chain-rpc-interfaces", "crates/include-measurements", + "crates/launcher-interface", "crates/mpc-attestation", "crates/near-mpc-sdk", "crates/node", @@ -45,6 +46,7 @@ include-measurements = { path = "crates/include-measurements" } mpc-attestation = { path = "crates/mpc-attestation" } mpc-contract = { path = "crates/contract", features = ["dev-utils"] } mpc-node = { path = "crates/node" } +launcher-interface = { path = "crates/launcher-interface" } mpc-primitives = { path = "crates/primitives", features = ["abi"] } mpc-tls = { path = "crates/tls" } near-mpc-sdk = { path = "crates/near-mpc-sdk" } diff --git a/crates/launcher-interface/Cargo.toml b/crates/launcher-interface/Cargo.toml new file mode 100644 index 000000000..b7cb17847 --- /dev/null +++ b/crates/launcher-interface/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "launcher-interface" +version.workspace = true +edition.workspace = true +license.workspace = true + +[dependencies] +mpc-primitives = { workspace = true } +serde = { workspace = true } + + +[lints] +workspace = true diff --git a/crates/launcher-interface/src/lib.rs b/crates/launcher-interface/src/lib.rs new file mode 100644 index 000000000..dd0601a0c --- /dev/null +++ b/crates/launcher-interface/src/lib.rs @@ -0,0 +1,12 @@ +pub mod types { + use mpc_primitives::hash::MpcDockerImageHash; + use serde::{Deserialize, Serialize}; + + /// JSON structure for the approved hashes file written by the MPC node. + #[derive(Debug, Serialize, Deserialize)] + pub struct ApprovedHashesFile { + pub approved_hashes: Vec, + } +} + +mod paths {} diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 5b70d1291..7c80f03e5 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -17,6 +17,7 @@ backon = { workspace = true } base64 = { workspace = true } borsh = { workspace = true } bounded-collections = { workspace = true } +launcher-interface = { workspace = true } bs58 = { workspace = true } bytes = { workspace = true } clap = { workspace = true } diff --git a/crates/node/src/tee/allowed_image_hashes_watcher.rs b/crates/node/src/tee/allowed_image_hashes_watcher.rs index 4596c9dba..b5501038d 100644 --- a/crates/node/src/tee/allowed_image_hashes_watcher.rs +++ b/crates/node/src/tee/allowed_image_hashes_watcher.rs @@ -1,5 +1,6 @@ use derive_more::From; use itertools::Itertools; +use launcher_interface::types::ApprovedHashesFile; use mpc_contract::tee::proposal::MpcDockerImageHash; use std::{future::Future, io, panic, path::PathBuf}; use thiserror::Error; @@ -29,10 +30,6 @@ pub struct AllowedImageHashesFile { file_path: PathBuf, } -// important: must stay aligned with the launcher implementation in: -// mpc/tee_launcher/launcher.py -const JSON_KEY_APPROVED_HASHES: &str = "approved_hashes"; - impl AllowedImageHashesStorage for AllowedImageHashesFile { async fn set(&mut self, approved_hashes: &[MpcDockerImageHash]) -> Result<(), io::Error> { tracing::info!( @@ -41,21 +38,14 @@ impl AllowedImageHashesStorage for AllowedImageHashesFile { "Writing approved MPC image hashes to disk (JSON format)." ); - let hash_strings: Vec = approved_hashes - .iter() - .map(|h| format!("sha256:{}", h.as_hex())) - .collect(); + let approved_hashes = ApprovedHashesFile { + approved_hashes: approved_hashes.to_vec(), + }; - let json = serde_json::json!({ - JSON_KEY_APPROVED_HASHES: hash_strings - }); + let json = serde_json::to_string_pretty(&approved_hashes) + .expect("previous json! macro would also panic. TODO figure out what to return"); - tracing::debug!( - %JSON_KEY_APPROVED_HASHES, - approved = ?hash_strings, - json = %json.to_string(), - "approved image hashes JSON that will be written to disk" - ); + tracing::debug!(?approved_hashes, "writing approved hashes to disk"); let tmp_path = self.file_path.with_extension("tmp"); // Write to a temporary file first. diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index 204b8ab68..b3d8dcad5 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -15,7 +15,8 @@ integration-test = [] clap = { workspace = true } dstack-sdk = { workspace = true } hex = { workspace = true } -mpc-primitives = { path = "../primitives" } +mpc-primitives = { workspace = true } +launcher-interface = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 8ad64faf7..9454449ef 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -3,6 +3,7 @@ use std::process::Command; use std::sync::LazyLock; use clap::Parser; +use launcher_interface::types::ApprovedHashesFile; use regex::Regex; use std::os::unix::fs::FileTypeExt as _; @@ -230,6 +231,8 @@ fn parse_env_lines(lines: &[&str]) -> BTreeMap { env } +// TODO: this should be a struct with hard expectations, that we deserialize into, instead of +// a btreemap fn parse_env_file(path: &str) -> Result> { let content = std::fs::read_to_string(path).map_err(|source| LauncherError::FileRead { path: path.to_string(), @@ -319,6 +322,7 @@ fn get_bare_digest(full_digest: &str) -> Result { fn load_and_select_hash( args: &CliArgs, + // TODO: why is this btreemap not a struct with hard fields? dstack_config: &BTreeMap, ) -> Result { let approved_hashes = if std::path::Path::new(IMAGE_DIGEST_FILE).is_file() { @@ -340,45 +344,45 @@ fn load_and_select_hash( } data.approved_hashes } else { - let fallback = args + let fallback_image = (&args) .default_image_digest - .as_deref() + .clone() .ok_or_else(|| LauncherError::MissingEnvVar("DEFAULT_IMAGE_DIGEST".to_string()))?; - let fallback = fallback.trim(); - let fallback = if fallback.starts_with(SHA256_PREFIX) { - fallback.to_string() - } else { - format!("{SHA256_PREFIX}{fallback}") - }; - if !is_valid_sha256_digest(&fallback) { - return Err(LauncherError::InvalidDefaultDigest(fallback)); - } - tracing::info!("{IMAGE_DIGEST_FILE} missing → fallback to DEFAULT_IMAGE_DIGEST={fallback}"); - vec![fallback] + + tracing::info!( + ?IMAGE_DIGEST_FILE, + ?fallback_image, + "image digest file missing, will use fall back image" + ); + + vec![fallback_image] }; tracing::info!("Approved MPC image hashes (newest → oldest):"); for h in &approved_hashes { - tracing::info!(" - {h}"); + // TODO: Fix this output... + // tracing::info!(" - {h}"); } // Optional override - if let Some(override_hash) = dstack_config.get(ENV_VAR_MPC_HASH_OVERRIDE) { - if !is_valid_sha256_digest(override_hash) { - return Err(LauncherError::InvalidHashOverride(override_hash.clone())); - } - if !approved_hashes.contains(override_hash) { - tracing::error!("MPC_HASH_OVERRIDE={override_hash} does NOT match any approved hash!"); - return Err(LauncherError::InvalidHashOverride(override_hash.clone())); - } - tracing::info!("MPC_HASH_OVERRIDE provided → selecting: {override_hash}"); - return Ok(override_hash.clone()); - } - - // No override → select newest (first in list) - let selected = approved_hashes[0].clone(); - tracing::info!("Selected MPC hash (newest allowed): {selected}"); - Ok(selected) + // if let Some(override_hash) = dstack_config.get(ENV_VAR_MPC_HASH_OVERRIDE) { + // if !is_valid_sha256_digest(override_hash) { + // return Err(LauncherError::InvalidHashOverride(override_hash.clone())); + // } + // if !approved_hashes.contains(override_hash) { + // tracing::error!("MPC_HASH_OVERRIDE={override_hash} does NOT match any approved hash!"); + // return Err(LauncherError::InvalidHashOverride(override_hash.clone())); + // } + // tracing::info!("MPC_HASH_OVERRIDE provided → selecting: {override_hash}"); + // return Ok(override_hash.clone()); + // } + + // // No override → select newest (first in list) + // let selected = approved_hashes[0].clone(); + // tracing::info!("Selected MPC hash (newest allowed): {selected}"); + // Ok(selected) + + todo!() } // --------------------------------------------------------------------------- @@ -789,6 +793,7 @@ async fn extend_rtmr3(platform: Platform, valid_hash: &str) -> Result<()> { mod tests { use super::*; use assert_matches::assert_matches; + use launcher_interface::types::ApprovedHashesFile; // -- Config parsing tests ----------------------------------------------- diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 707326904..89207a3fe 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -1,4 +1,5 @@ use clap::{Parser, ValueEnum}; +use mpc_primitives::hash::MpcDockerImageHash; use serde::Deserialize; /// CLI arguments parsed from environment variables via clap. @@ -10,12 +11,13 @@ pub struct CliArgs { pub platform: Platform, /// Must be set to "1" to enable Docker Content Trust + // TODO: make it optional and only accept value 1 #[arg(long, env = "DOCKER_CONTENT_TRUST", default_value = "")] pub docker_content_trust: String, /// Fallback image digest when the approved-hashes file is absent #[arg(long, env = "DEFAULT_IMAGE_DIGEST")] - pub default_image_digest: Option, + pub default_image_digest: Option, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] @@ -45,10 +47,3 @@ pub struct ResolvedImage { pub spec: ImageSpec, pub digest: String, } - -/// JSON structure for the approved hashes file written by the MPC node. -/// Must stay aligned with `crates/node/src/tee/allowed_image_hashes_watcher.rs`. -#[derive(Debug, Deserialize)] -pub struct ApprovedHashesFile { - pub approved_hashes: Vec, -} From cbf5afaa5ab7bfcb336e22ac9a8a5922a64eb5a4 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Mon, 2 Mar 2026 11:04:31 +0100 Subject: [PATCH 006/176] chore: enforce docker_content_trust at compile time --- crates/tee-launcher/src/error.rs | 3 --- crates/tee-launcher/src/main.rs | 6 ------ crates/tee-launcher/src/types.rs | 14 +++++++++----- 3 files changed, 9 insertions(+), 14 deletions(-) diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 73a594bd0..f25dca210 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -2,9 +2,6 @@ use thiserror::Error; #[derive(Error, Debug)] pub enum LauncherError { - #[error("DOCKER_CONTENT_TRUST must be set to 1")] - DockerContentTrustNotEnabled, - #[error("PLATFORM=TEE requires dstack unix socket at {0}")] DstackSocketMissing(String), diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 9454449ef..8b594f0f0 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -47,12 +47,6 @@ async fn run() -> Result<()> { )); } - // TODO: `docker_content_trust` parse it to a type that only accepts deserialization into number 1 - // DOCKER_CONTENT_TRUST must be enabled - if args.docker_content_trust != "1" { - return Err(LauncherError::DockerContentTrustNotEnabled); - } - // Load dstack user config let dstack_config: BTreeMap = if std::path::Path::new(DSTACK_USER_CONFIG_FILE).is_file() { diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 89207a3fe..3c987394b 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -1,6 +1,5 @@ use clap::{Parser, ValueEnum}; use mpc_primitives::hash::MpcDockerImageHash; -use serde::Deserialize; /// CLI arguments parsed from environment variables via clap. #[derive(Parser, Debug)] @@ -10,16 +9,21 @@ pub struct CliArgs { #[arg(long, env = "PLATFORM")] pub platform: Platform, - /// Must be set to "1" to enable Docker Content Trust - // TODO: make it optional and only accept value 1 - #[arg(long, env = "DOCKER_CONTENT_TRUST", default_value = "")] - pub docker_content_trust: String, + #[arg(long, env = "DOCKER_CONTENT_TRUST")] + // ensure that `docker_content_trust` is enabled. + docker_content_trust: DockerContentTrust, /// Fallback image digest when the approved-hashes file is absent #[arg(long, env = "DEFAULT_IMAGE_DIGEST")] pub default_image_digest: Option, } +#[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] +enum DockerContentTrust { + #[value(name = "1")] + Enabled, +} + #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] pub enum Platform { #[value(name = "TEE")] From 8044407f3844e527fa2bc9e0ba1792758ea84e98 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Mon, 2 Mar 2026 17:21:29 +0100 Subject: [PATCH 007/176] created json --- Cargo.toml | 1 + crates/tee-launcher/src/error.rs | 12 + crates/tee-launcher/src/main.rs | 291 +++++++++++------------- crates/tee-launcher/src/types.rs | 52 ++++- deployment/localnet/tee/frodo_conf.json | 21 ++ deployment/localnet/tee/sam_conf.json | 21 ++ 6 files changed, 225 insertions(+), 173 deletions(-) create mode 100644 deployment/localnet/tee/frodo_conf.json create mode 100644 deployment/localnet/tee/sam_conf.json diff --git a/Cargo.toml b/Cargo.toml index 5c1ba25fb..aa47cba05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ derive_more = { version = "2.1.1", features = [ "into", ] } digest = "0.10.7" +dotenvy = "0.15" dstack-sdk = { version = "0.1.2" } dstack-sdk-types = { version = "0.1.2", features = ["borsh"] } ecdsa = { version = "0.16.9", features = ["digest", "hazmat"] } diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index f25dca210..23e225ef8 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -1,3 +1,5 @@ +use std::path::PathBuf; + use thiserror::Error; #[derive(Error, Debug)] @@ -59,6 +61,13 @@ pub enum LauncherError { source: std::io::Error, }, + #[error("Failed to parse env file {path}: {source}")] + EnvFileParse { + path: PathBuf, + #[source] + source: dotenvy::Error, + }, + #[error("Failed to parse {path}: {source}")] JsonParse { path: String, @@ -68,6 +77,9 @@ pub enum LauncherError { #[error("Required environment variable not set: {0}")] MissingEnvVar(String), + #[error("Invalid value for {key}: {value}")] + InvalidEnvVar { key: String, value: String }, + #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 8b594f0f0..6c105fa3b 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -1,4 +1,4 @@ -use std::collections::{BTreeMap, HashSet, VecDeque}; +use std::collections::{BTreeMap, VecDeque}; use std::process::Command; use std::sync::LazyLock; @@ -48,12 +48,12 @@ async fn run() -> Result<()> { } // Load dstack user config - let dstack_config: BTreeMap = - if std::path::Path::new(DSTACK_USER_CONFIG_FILE).is_file() { - parse_env_file(DSTACK_USER_CONFIG_FILE)? - } else { - BTreeMap::new() - }; + let config_file = std::fs::OpenOptions::new() + .read(true) + .open(DSTACK_USER_CONFIG_FILE) + .expect("dstack user config file exists"); + + let dstack_config: Config = serde_json::from_reader(config_file).expect("config file is valid"); let rpc_cfg = load_rpc_timing_config(&dstack_config); @@ -67,7 +67,11 @@ async fn run() -> Result<()> { extend_rtmr3(args.platform, &selected_hash).await?; - launch_mpc_container(args.platform, &selected_hash, &dstack_config)?; + launch_mpc_container( + args.platform, + &selected_hash, + &dstack_config.passthrough_env, + )?; Ok(()) } @@ -104,19 +108,6 @@ const ALLOWED_MPC_ENV_VARS: &[&str] = &[ "MPC_BACKUP_ENCRYPTION_KEY_HEX", ]; -// Launcher-only env vars — read from user config but never forwarded to container -static ALLOWED_LAUNCHER_ENV_VARS: LazyLock> = LazyLock::new(|| { - HashSet::from([ - DSTACK_USER_CONFIG_MPC_IMAGE_TAGS, - DSTACK_USER_CONFIG_MPC_IMAGE_NAME, - DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY, - ENV_VAR_MPC_HASH_OVERRIDE, - ENV_VAR_RPC_REQUEST_TIMEOUT_SECS, - ENV_VAR_RPC_REQUEST_INTERVAL_SECS, - ENV_VAR_RPC_MAX_ATTEMPTS, - ]) -}); - // --------------------------------------------------------------------------- // Validation functions — security policy for env passthrough // --------------------------------------------------------------------------- @@ -202,88 +193,17 @@ fn is_allowed_container_env_key(key: &str) -> bool { false } -// --------------------------------------------------------------------------- -// Config parsing -// --------------------------------------------------------------------------- - -fn parse_env_lines(lines: &[&str]) -> BTreeMap { - let mut env = BTreeMap::new(); - for line in lines { - let line = line.trim(); - if line.is_empty() || line.starts_with('#') || !line.contains('=') { - continue; - } - if let Some((key, value)) = line.split_once('=') { - let key = key.trim(); - let value = value.trim(); - if key.is_empty() { - continue; - } - env.insert(key.to_string(), value.to_string()); - } - } - env -} - -// TODO: this should be a struct with hard expectations, that we deserialize into, instead of -// a btreemap -fn parse_env_file(path: &str) -> Result> { - let content = std::fs::read_to_string(path).map_err(|source| LauncherError::FileRead { - path: path.to_string(), - source, - })?; - let lines: Vec<&str> = content.lines().collect(); - Ok(parse_env_lines(&lines)) -} - -fn load_rpc_timing_config(dstack_config: &BTreeMap) -> RpcTimingConfig { - let timeout_secs = dstack_config - .get(ENV_VAR_RPC_REQUEST_TIMEOUT_SECS) - .and_then(|v| v.parse().ok()) - .unwrap_or(DEFAULT_RPC_REQUEST_TIMEOUT_SECS); - let interval_secs = dstack_config - .get(ENV_VAR_RPC_REQUEST_INTERVAL_SECS) - .and_then(|v| v.parse().ok()) - .unwrap_or(DEFAULT_RPC_REQUEST_INTERVAL_SECS); - let max_attempts = dstack_config - .get(ENV_VAR_RPC_MAX_ATTEMPTS) - .and_then(|v| v.parse().ok()) - .unwrap_or(DEFAULT_RPC_MAX_ATTEMPTS); - RpcTimingConfig { - request_timeout_secs: timeout_secs, - request_interval_secs: interval_secs, - max_attempts, - } -} - -fn get_image_spec(dstack_config: &BTreeMap) -> ImageSpec { - let tags_raw = dstack_config - .get(DSTACK_USER_CONFIG_MPC_IMAGE_TAGS) - .cloned() - .unwrap_or_else(|| DEFAULT_MPC_IMAGE_TAG.to_string()); - let tags: Vec = tags_raw - .split(',') - .map(|t| t.trim().to_string()) - .filter(|t| !t.is_empty()) - .collect(); - tracing::info!("Using tags {tags:?} to find matching MPC node docker image."); - - let image_name = dstack_config - .get(DSTACK_USER_CONFIG_MPC_IMAGE_NAME) - .cloned() - .unwrap_or_else(|| DEFAULT_MPC_IMAGE_NAME.to_string()); - tracing::info!("Using image name {image_name}."); - - let registry = dstack_config - .get(DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY) - .cloned() - .unwrap_or_else(|| DEFAULT_MPC_REGISTRY.to_string()); - tracing::info!("Using registry {registry}."); - +fn get_image_spec(config: &Config) -> ImageSpec { + tracing::info!( + "Using tags {:?} to find matching MPC node docker image.", + config.image_tags + ); + tracing::info!("Using image name {}.", config.image_name); + tracing::info!("Using registry {}.", config.registry); ImageSpec { - tags, - image_name, - registry, + tags: config.image_tags.clone(), + image_name: config.image_name.clone(), + registry: config.registry.clone(), } } @@ -314,11 +234,7 @@ fn get_bare_digest(full_digest: &str) -> Result { Ok(parse_image_digest(full_digest)?.as_hex()) } -fn load_and_select_hash( - args: &CliArgs, - // TODO: why is this btreemap not a struct with hard fields? - dstack_config: &BTreeMap, -) -> Result { +fn load_and_select_hash(args: &CliArgs, dstack_config: &Config) -> Result { let approved_hashes = if std::path::Path::new(IMAGE_DIGEST_FILE).is_file() { let content = std::fs::read_to_string(IMAGE_DIGEST_FILE).map_err(|source| { LauncherError::FileRead { @@ -534,7 +450,7 @@ async fn get_manifest_digest(image: &ResolvedImage, timing: &RpcTimingConfig) -> async fn validate_image_hash( image_digest: &str, - dstack_config: &BTreeMap, + dstack_config: &Config, timing: &RpcTimingConfig, ) -> Result { tracing::info!("Validating MPC hash: {image_digest}"); @@ -643,10 +559,6 @@ fn build_docker_cmd( // BTreeMap iteration is already sorted by key (deterministic) for (key, value) in user_env { - if ALLOWED_LAUNCHER_ENV_VARS.contains(key.as_str()) { - continue; - } - if key == "EXTRA_HOSTS" { for host_entry in value.split(',') { let clean = host_entry.trim(); @@ -789,58 +701,110 @@ mod tests { use assert_matches::assert_matches; use launcher_interface::types::ApprovedHashesFile; - // -- Config parsing tests ----------------------------------------------- + // -- DstackUserConfig parsing tests ------------------------------------- #[test] - fn test_parse_env_lines_basic() { - let lines = vec![ - "# a comment", - "KEY1=value1", - " KEY2 = value2 ", - "", - "INVALIDLINE", - "EMPTY_KEY=", - ]; - let env = parse_env_lines(&lines); - assert_eq!(env.get("KEY1").unwrap(), "value1"); - assert_eq!(env.get("KEY2").unwrap(), "value2"); - assert_eq!(env.get("EMPTY_KEY").unwrap(), ""); - assert!(!env.contains_key("INVALIDLINE")); + fn test_user_config_defaults_when_map_is_empty() { + let config = user_config_from_map(BTreeMap::new()).unwrap(); + assert_eq!(config.image_tags, vec![DEFAULT_MPC_IMAGE_TAG]); + assert_eq!(config.image_name, DEFAULT_MPC_IMAGE_NAME); + assert_eq!(config.registry, DEFAULT_MPC_REGISTRY); + assert_eq!( + config.rpc_request_timeout_secs, + DEFAULT_RPC_REQUEST_TIMEOUT_SECS + ); + assert_eq!( + config.rpc_request_interval_secs, + DEFAULT_RPC_REQUEST_INTERVAL_SECS + ); + assert_eq!(config.rpc_max_attempts, DEFAULT_RPC_MAX_ATTEMPTS); + assert!(config.mpc_hash_override.is_none()); + assert!(config.passthrough_env.is_empty()); } #[test] - fn test_config_ignores_blank_lines_and_comments() { - let lines = vec![ - "", - " # This is a comment", - " MPC_SECRET_STORE_KEY=topsecret", - "", - ]; - let env = parse_env_lines(&lines); - assert_eq!(env.get("MPC_SECRET_STORE_KEY").unwrap(), "topsecret"); - assert_eq!(env.len(), 1); + fn test_user_config_typed_fields_extracted_from_map() { + let map = BTreeMap::from([ + ( + DSTACK_USER_CONFIG_MPC_IMAGE_TAGS.into(), + "v1.0, v1.1".into(), + ), + (DSTACK_USER_CONFIG_MPC_IMAGE_NAME.into(), "my/image".into()), + ( + DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY.into(), + "my.registry.io".into(), + ), + (ENV_VAR_RPC_REQUEST_TIMEOUT_SECS.into(), "30.0".into()), + (ENV_VAR_RPC_MAX_ATTEMPTS.into(), "5".into()), + ("MPC_ACCOUNT_ID".into(), "account.near".into()), + ]); + let config = user_config_from_map(map).unwrap(); + assert_eq!(config.image_tags, vec!["v1.0", "v1.1"]); + assert_eq!(config.image_name, "my/image"); + assert_eq!(config.registry, "my.registry.io"); + assert_eq!(config.rpc_request_timeout_secs, 30.0); + assert_eq!(config.rpc_max_attempts, 5); + // Launcher-only keys are NOT in passthrough_env + assert!( + !config + .passthrough_env + .contains_key(DSTACK_USER_CONFIG_MPC_IMAGE_TAGS) + ); + assert!( + !config + .passthrough_env + .contains_key(ENV_VAR_RPC_MAX_ATTEMPTS) + ); + // Container passthrough keys ARE in passthrough_env + assert_eq!( + config.passthrough_env.get("MPC_ACCOUNT_ID").unwrap(), + "account.near" + ); } #[test] - fn test_config_skips_malformed_lines() { - let lines = vec![ - "GOOD_KEY=value", - "bad_line_without_equal", - "ANOTHER_GOOD=ok", - "=", - ]; - let env = parse_env_lines(&lines); - assert!(env.contains_key("GOOD_KEY")); - assert!(env.contains_key("ANOTHER_GOOD")); - assert!(!env.contains_key("bad_line_without_equal")); - assert!(!env.contains_key("")); + fn test_user_config_malformed_rpc_fields_error() { + let map = BTreeMap::from([(ENV_VAR_RPC_MAX_ATTEMPTS.into(), "not_a_number".into())]); + let err = user_config_from_map(map).unwrap_err(); + assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_MAX_ATTEMPTS); + + let map = BTreeMap::from([(ENV_VAR_RPC_REQUEST_TIMEOUT_SECS.into(), "bad".into())]); + let err = user_config_from_map(map).unwrap_err(); + assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_REQUEST_TIMEOUT_SECS); + + let map = BTreeMap::from([(ENV_VAR_RPC_REQUEST_INTERVAL_SECS.into(), "bad".into())]); + let err = user_config_from_map(map).unwrap_err(); + assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_REQUEST_INTERVAL_SECS); + } + + #[test] + fn test_user_config_hash_override_extracted() { + let map = BTreeMap::from([(ENV_VAR_MPC_HASH_OVERRIDE.into(), "sha256:abc".into())]); + let config = user_config_from_map(map).unwrap(); + assert_eq!(config.mpc_hash_override.unwrap(), "sha256:abc"); + assert!( + !config + .passthrough_env + .contains_key(ENV_VAR_MPC_HASH_OVERRIDE) + ); } #[test] - fn test_config_overrides_duplicate_keys() { - let lines = vec!["MPC_ACCOUNT_ID=first", "MPC_ACCOUNT_ID=second"]; - let env = parse_env_lines(&lines); - assert_eq!(env.get("MPC_ACCOUNT_ID").unwrap(), "second"); + fn test_parse_user_config_from_file() { + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("user_config"); + std::fs::write( + &file, + "# comment\nMPC_ACCOUNT_ID=test\nMPC_IMAGE_NAME=my/image\n", + ) + .unwrap(); + let config = parse_user_config(file.to_str().unwrap()).unwrap(); + assert_eq!(config.image_name, "my/image"); + assert_eq!( + config.passthrough_env.get("MPC_ACCOUNT_ID").unwrap(), + "test" + ); + assert!(!config.passthrough_env.contains_key("MPC_IMAGE_NAME")); } // -- Host/port validation tests ----------------------------------------- @@ -1031,7 +995,6 @@ mod tests { fn test_build_docker_cmd_nontee_no_dstack_mount() { let mut env = BTreeMap::new(); env.insert("MPC_ACCOUNT_ID".into(), "x".into()); - env.insert(ENV_VAR_RPC_MAX_ATTEMPTS.into(), "5".into()); let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); let s = cmd.join(" "); assert!(!s.contains("DSTACK_ENDPOINT=")); @@ -1282,10 +1245,15 @@ mod tests { #[test] fn test_parse_and_build_docker_cmd_full_flow() { - let config_str = "MPC_ACCOUNT_ID=test-user\nPORTS=11780:11780, --env BAD=oops\nEXTRA_HOSTS=host1:192.168.1.1, --volume /:/mnt\nIMAGE_HASH=sha256:abc123"; - let lines: Vec<&str> = config_str.lines().collect(); - let env = parse_env_lines(&lines); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + let dir = tempfile::tempdir().unwrap(); + let file = dir.path().join("user_config"); + std::fs::write( + &file, + "MPC_ACCOUNT_ID=test-user\nPORTS=11780:11780, --env BAD=oops\nEXTRA_HOSTS=host1:192.168.1.1, --volume /:/mnt\n", + ) + .unwrap(); + let config = parse_user_config(file.to_str().unwrap()).unwrap(); + let cmd = build_docker_cmd(Platform::Tee, &config.passthrough_env, &make_digest()).unwrap(); let cmd_str = cmd.join(" "); assert!(cmd_str.contains("MPC_ACCOUNT_ID=test-user")); @@ -1351,15 +1319,16 @@ mod tests { const TEST_DIGEST: &str = "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372"; - fn test_dstack_config() -> BTreeMap { - BTreeMap::from([ + fn test_dstack_config() -> Config { + user_config_from_map(BTreeMap::from([ ( "MPC_IMAGE_TAGS".into(), "83b52da4e2270c688cdd30da04f6b9d3565f25bb".into(), ), ("MPC_IMAGE_NAME".into(), "nearone/testing".into()), ("MPC_REGISTRY".into(), "registry.hub.docker.com".into()), - ]) + ])) + .unwrap() } #[tokio::test] diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 3c987394b..de681896a 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -1,5 +1,6 @@ use clap::{Parser, ValueEnum}; use mpc_primitives::hash::MpcDockerImageHash; +use serde::{Deserialize, Serialize}; /// CLI arguments parsed from environment variables via clap. #[derive(Parser, Debug)] @@ -32,22 +33,49 @@ pub enum Platform { NonTee, } -#[derive(Debug, Clone)] -pub struct RpcTimingConfig { - pub request_timeout_secs: f64, - pub request_interval_secs: f64, - pub max_attempts: u32, +/// Typed representation of the dstack user config file (`/tapp/user_config`). +/// +/// Launcher-only keys are extracted into typed fields; all remaining keys are +/// kept in `passthrough_env` for forwarding to the MPC container. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct Config { + pub launcher_config: LauncherConfig, + /// Remaining env vars forwarded to the MPC container. + pub mpc_passthrough_env: MpcBinaryConfig, } -#[derive(Debug, Clone)] -pub struct ImageSpec { - pub tags: Vec, +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LauncherConfig { + /// Docker image tags to search (from `MPC_IMAGE_TAGS`, comma-separated). + pub image_tags: Vec, + /// Docker image name (from `MPC_IMAGE_NAME`). pub image_name: String, + /// Docker registry (from `MPC_REGISTRY`). pub registry: String, + /// Per-request timeout for registry RPC calls (from `RPC_REQUEST_TIMEOUT_SECS`). + pub rpc_request_timeout_secs: f64, + /// Delay between registry RPC retries (from `RPC_REQUEST_INTERVAL_SECS`). + pub rpc_request_interval_secs: f64, + /// Maximum registry RPC attempts (from `RPC_MAX_ATTEMPTS`). + pub rpc_max_attempts: u32, + /// Optional hash override that bypasses registry lookup (from `MPC_HASH_OVERRIDE`). + pub mpc_hash_override: Option, } -#[derive(Debug, Clone)] -pub struct ResolvedImage { - pub spec: ImageSpec, - pub digest: String, +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct MpcBinaryConfig { + // mpc + mpc_account_id: String, + mpc_local_address: String, + mpc_secret_key_store: String, + mpc_contract_isd: String, + mpc_env: String, + mpc_home_dir: String, + mpc_responder_id: String, + mpc_backup_encryption_key_hex: String, + // near + near_boot_nodes: String, + // rust + rust_backtrace: String, + rust_log: String, } diff --git a/deployment/localnet/tee/frodo_conf.json b/deployment/localnet/tee/frodo_conf.json new file mode 100644 index 000000000..081733e2e --- /dev/null +++ b/deployment/localnet/tee/frodo_conf.json @@ -0,0 +1,21 @@ +{ + "MPC_IMAGE_NAME": "nearone/mpc-node", + "MPC_IMAGE_TAGS": "main-260e88b", + "MPC_REGISTRY": "registry.hub.docker.com", + "MPC_ACCOUNT_ID": "frodo.test.near", + "MPC_LOCAL_ADDRESS": "127.0.0.1", + "MPC_SECRET_STORE_KEY": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "MPC_CONTRACT_ID": "mpc-contract.test.near", + "MPC_ENV": "mpc-localnet", + "MPC_HOME_DIR": "/data", + "RUST_BACKTRACE": "full", + "RUST_LOG": "info", + "NEAR_BOOT_NODES": [ + "ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566" + ], + "PORTS": [ + "8080:8080", + "24566:24566", + "13001:13001" + ] +} \ No newline at end of file diff --git a/deployment/localnet/tee/sam_conf.json b/deployment/localnet/tee/sam_conf.json new file mode 100644 index 000000000..8c307acd1 --- /dev/null +++ b/deployment/localnet/tee/sam_conf.json @@ -0,0 +1,21 @@ +{ + "MPC_IMAGE_NAME": "nearone/mpc-node", + "MPC_IMAGE_TAGS": "main-260e88b", + "MPC_REGISTRY": "registry.hub.docker.com", + "MPC_ACCOUNT_ID": "sam.test.near", + "MPC_LOCAL_ADDRESS": "127.0.0.1", + "MPC_SECRET_STORE_KEY": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "MPC_CONTRACT_ID": "mpc-contract.test.near", + "MPC_ENV": "mpc-localnet", + "MPC_HOME_DIR": "/data", + "RUST_BACKTRACE": "full", + "RUST_LOG": "info", + "NEAR_BOOT_NODES": [ + "ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566" + ], + "PORTS": [ + "8080:8080", + "24566:24566", + "13002:13002" + ] +} \ No newline at end of file From 34302f65c65c8320dc7bbdc7b19105611a16684d Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Mon, 2 Mar 2026 18:26:51 +0100 Subject: [PATCH 008/176] it compiles --- Cargo.lock | 1 + crates/tee-launcher/Cargo.toml | 1 + crates/tee-launcher/src/error.rs | 7 - crates/tee-launcher/src/main.rs | 1486 ++++++++++++++---------------- crates/tee-launcher/src/types.rs | 7 +- 5 files changed, 713 insertions(+), 789 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 49e17fb79..a7c74e242 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10552,6 +10552,7 @@ name = "tee-launcher" version = "3.5.1" dependencies = [ "assert_matches", + "bounded-collections", "clap", "dstack-sdk", "hex", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index b3d8dcad5..f983a8ad1 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -12,6 +12,7 @@ path = "src/main.rs" integration-test = [] [dependencies] +bounded-collections = { workspace = true } clap = { workspace = true } dstack-sdk = { workspace = true } hex = { workspace = true } diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 23e225ef8..92836663d 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -61,13 +61,6 @@ pub enum LauncherError { source: std::io::Error, }, - #[error("Failed to parse env file {path}: {source}")] - EnvFileParse { - path: PathBuf, - #[source] - source: dotenvy::Error, - }, - #[error("Failed to parse {path}: {source}")] JsonParse { path: String, diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 6c105fa3b..eec320fd5 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -55,11 +55,9 @@ async fn run() -> Result<()> { let dstack_config: Config = serde_json::from_reader(config_file).expect("config file is valid"); - let rpc_cfg = load_rpc_timing_config(&dstack_config); - let selected_hash = load_and_select_hash(&args, &dstack_config)?; - if !validate_image_hash(&selected_hash, &dstack_config, &rpc_cfg).await? { + if !validate_image_hash(&selected_hash, &dstack_config, &dstack_config).await? { return Err(LauncherError::ImageValidationFailed(selected_hash)); } @@ -70,7 +68,7 @@ async fn run() -> Result<()> { launch_mpc_container( args.platform, &selected_hash, - &dstack_config.passthrough_env, + &dstack_config.mpc_passthrough_env, )?; Ok(()) @@ -126,19 +124,6 @@ fn has_control_chars(s: &str) -> bool { false } -fn is_safe_env_value(value: &str) -> bool { - if value.len() > MAX_ENV_VALUE_LEN { - return false; - } - if has_control_chars(value) { - return false; - } - if value.contains("LD_PRELOAD") { - return false; - } - true -} - fn is_valid_ip(ip: &str) -> bool { ip.parse::().is_ok() } @@ -178,35 +163,6 @@ fn is_safe_port_mapping(mapping: &str) -> bool { !INVALID_HOST_ENTRY_PATTERN.is_match(mapping) } -fn is_allowed_container_env_key(key: &str) -> bool { - if DENIED_CONTAINER_ENV_KEYS.contains(&key) { - return false; - } - // Allow MPC_* keys with strict regex - if MPC_ENV_KEY_RE.is_match(key) { - return true; - } - // Keep allowlist - if ALLOWED_MPC_ENV_VARS.contains(&key) { - return true; - } - false -} - -fn get_image_spec(config: &Config) -> ImageSpec { - tracing::info!( - "Using tags {:?} to find matching MPC node docker image.", - config.image_tags - ); - tracing::info!("Using image name {}.", config.image_name); - tracing::info!("Using registry {}.", config.registry); - ImageSpec { - tags: config.image_tags.clone(), - image_name: config.image_name.clone(), - registry: config.registry.clone(), - } -} - // --------------------------------------------------------------------------- // Hash selection // --------------------------------------------------------------------------- @@ -299,15 +255,16 @@ fn load_and_select_hash(args: &CliArgs, dstack_config: &Config) -> Result Result { - let mut interval = timing.request_interval_secs; + let mut interval = config.rpc_request_interval_secs as f64; - for attempt in 1..=timing.max_attempts { + for attempt in 1..=config.rpc_max_attempts { // Sleep before request (matching Python behavior) tokio::time::sleep(std::time::Duration::from_secs_f64(interval)).await; interval = (interval.max(1.0) * 1.5).min(60.0); @@ -318,8 +275,8 @@ async fn request_until_success( } match req - .timeout(std::time::Duration::from_secs_f64( - timing.request_timeout_secs, + .timeout(std::time::Duration::from_secs( + config.rpc_request_timeout_secs, )) .send() .await @@ -327,14 +284,14 @@ async fn request_until_success( Err(e) => { tracing::warn!( "Attempt {attempt}/{}: Failed to fetch {url}. Status: Timeout/Error: {e}", - timing.max_attempts + config.rpc_max_attempts ); continue; } Ok(resp) if resp.status() != reqwest::StatusCode::OK => { tracing::warn!( "Attempt {attempt}/{}: Failed to fetch {url}. Status: {}", - timing.max_attempts, + config.rpc_max_attempts, resp.status() ); continue; @@ -345,20 +302,18 @@ async fn request_until_success( Err(LauncherError::RegistryRequestFailed { url: url.to_string(), - attempts: timing.max_attempts, + attempts: config.rpc_max_attempts, }) } -async fn get_manifest_digest(image: &ResolvedImage, timing: &RpcTimingConfig) -> Result { - if image.spec.tags.is_empty() { - return Err(LauncherError::ImageHashNotFoundAmongTags); - } +async fn get_manifest_digest(config: &LauncherConfig) -> Result { + let tags = config.image_tags.clone(); - // Get auth token let token_url = format!( "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{}:pull", - image.spec.image_name + config.image_name ); + let client = reqwest::Client::new(); let token_resp = client .get(&token_url) @@ -380,12 +335,12 @@ async fn get_manifest_digest(image: &ResolvedImage, timing: &RpcTimingConfig) -> .ok_or_else(|| LauncherError::RegistryAuthFailed("no token in response".to_string()))? .to_string(); - let mut tags: VecDeque = image.spec.tags.iter().cloned().collect(); + let mut tags: VecDeque = tags.into_iter().collect(); while let Some(tag) = tags.pop_front() { let manifest_url = format!( "https://{}/v2/{}/manifests/{tag}", - image.spec.registry, image.spec.image_name + config.registry, config.image_name ); let headers = vec![ ( @@ -395,7 +350,7 @@ async fn get_manifest_digest(image: &ResolvedImage, timing: &RpcTimingConfig) -> ("Authorization".to_string(), format!("Bearer {token}")), ]; - match request_until_success(&client, &manifest_url, &headers, timing).await { + match request_until_success(&client, &manifest_url, &headers, config).await { Ok(resp) => { let content_digest = resp .headers() @@ -424,15 +379,16 @@ async fn get_manifest_digest(image: &ResolvedImage, timing: &RpcTimingConfig) -> } } } - "application/vnd.docker.distribution.manifest.v2+json" - | "application/vnd.oci.image.manifest.v1+json" => { - let config_digest = manifest["config"]["digest"].as_str().unwrap_or(""); - if config_digest == image.digest { - if let Some(digest) = content_digest { - return Ok(digest); - } - } - } + // TODO: + // "application/vnd.docker.distribution.manifest.v2+json" + // | "application/vnd.oci.image.manifest.v1+json" => { + // let config_digest = manifest["config"]["digest"].as_str().unwrap_or(""); + // if config_digest == config. { + // if let Some(digest) = content_digest { + // return Ok(digest); + // } + // } + // } _ => {} } } @@ -451,18 +407,12 @@ async fn get_manifest_digest(image: &ResolvedImage, timing: &RpcTimingConfig) -> async fn validate_image_hash( image_digest: &str, dstack_config: &Config, - timing: &RpcTimingConfig, + config: &Config, ) -> Result { tracing::info!("Validating MPC hash: {image_digest}"); - let image_spec = get_image_spec(dstack_config); - let docker_image = ResolvedImage { - spec: image_spec, - digest: image_digest.to_string(), - }; - - let manifest_digest = get_manifest_digest(&docker_image, timing).await?; - let name_and_digest = format!("{}@{manifest_digest}", docker_image.spec.image_name); + let manifest_digest = get_manifest_digest(&config.launcher_config).await?; + let name_and_digest = format!("{}@{manifest_digest}", config.launcher_config.image_name); // Pull let pull = Command::new("docker") @@ -528,7 +478,7 @@ fn remove_existing_container() { fn build_docker_cmd( platform: Platform, - user_env: &BTreeMap, + mpc_config: &MpcBinaryConfig, image_digest: &str, ) -> Result> { let bare_digest = get_bare_digest(image_digest)?; @@ -557,54 +507,32 @@ fn build_docker_cmd( let mut passed_env_count: usize = 0; let mut total_env_bytes: usize = 0; - // BTreeMap iteration is already sorted by key (deterministic) - for (key, value) in user_env { - if key == "EXTRA_HOSTS" { - for host_entry in value.split(',') { - let clean = host_entry.trim(); - if is_safe_host_entry(clean) && is_valid_host_entry(clean) { - cmd.extend(["--add-host".into(), clean.to_string()]); - } else { - tracing::warn!("Ignoring invalid or unsafe EXTRA_HOSTS entry: {clean}"); - } - } - continue; - } - - if key == "PORTS" { - for port_pair in value.split(',') { - let clean = port_pair.trim(); - if is_safe_port_mapping(clean) && is_valid_port_mapping(clean) { - cmd.extend(["-p".into(), clean.to_string()]); - } else { - tracing::warn!("Ignoring invalid or unsafe PORTS entry: {clean}"); - } - } - continue; - } - - if !is_allowed_container_env_key(key) { - tracing::warn!("Ignoring unknown or unapproved env var: {key}"); - continue; - } - - if !is_safe_env_value(value) { - tracing::warn!("Ignoring env var with unsafe value: {key}"); - continue; - } + // // BTreeMap iteration is already sorted by key (deterministic) + // for (key, value) in mpc_config { + // if key == "EXTRA_HOSTS" { + // for host_entry in value.split(',') { + // let clean = host_entry.trim(); + // if is_safe_host_entry(clean) && is_valid_host_entry(clean) { + // cmd.extend(["--add-host".into(), clean.to_string()]); + // } else { + // tracing::warn!("Ignoring invalid or unsafe EXTRA_HOSTS entry: {clean}"); + // } + // } + // continue; + // } - passed_env_count += 1; - if passed_env_count > MAX_PASSTHROUGH_ENV_VARS { - return Err(LauncherError::TooManyEnvVars(MAX_PASSTHROUGH_ENV_VARS)); - } + // passed_env_count += 1; + // if passed_env_count > MAX_PASSTHROUGH_ENV_VARS { + // return Err(LauncherError::TooManyEnvVars(MAX_PASSTHROUGH_ENV_VARS)); + // } - total_env_bytes += key.len() + 1 + value.len(); - if total_env_bytes > MAX_TOTAL_ENV_BYTES { - return Err(LauncherError::EnvPayloadTooLarge(MAX_TOTAL_ENV_BYTES)); - } + // total_env_bytes += key.len() + 1 + value.len(); + // if total_env_bytes > MAX_TOTAL_ENV_BYTES { + // return Err(LauncherError::EnvPayloadTooLarge(MAX_TOTAL_ENV_BYTES)); + // } - cmd.extend(["--env".into(), format!("{key}={value}")]); - } + // cmd.extend(["--env".into(), format!("{key}={value}")]); + // } // Container run configuration cmd.extend([ @@ -636,12 +564,12 @@ fn build_docker_cmd( fn launch_mpc_container( platform: Platform, valid_hash: &str, - user_env: &BTreeMap, + mpc_config: &MpcBinaryConfig, ) -> Result<()> { tracing::info!("Launching MPC node with validated hash: {valid_hash}"); remove_existing_container(); - let docker_cmd = build_docker_cmd(platform, user_env, valid_hash)?; + let docker_cmd = build_docker_cmd(platform, mpc_config, valid_hash)?; let status = Command::new(&docker_cmd[0]) .args(&docker_cmd[1..]) @@ -695,653 +623,653 @@ async fn extend_rtmr3(platform: Platform, valid_hash: &str) -> Result<()> { Ok(()) } -#[cfg(test)] -mod tests { - use super::*; - use assert_matches::assert_matches; - use launcher_interface::types::ApprovedHashesFile; - - // -- DstackUserConfig parsing tests ------------------------------------- - - #[test] - fn test_user_config_defaults_when_map_is_empty() { - let config = user_config_from_map(BTreeMap::new()).unwrap(); - assert_eq!(config.image_tags, vec![DEFAULT_MPC_IMAGE_TAG]); - assert_eq!(config.image_name, DEFAULT_MPC_IMAGE_NAME); - assert_eq!(config.registry, DEFAULT_MPC_REGISTRY); - assert_eq!( - config.rpc_request_timeout_secs, - DEFAULT_RPC_REQUEST_TIMEOUT_SECS - ); - assert_eq!( - config.rpc_request_interval_secs, - DEFAULT_RPC_REQUEST_INTERVAL_SECS - ); - assert_eq!(config.rpc_max_attempts, DEFAULT_RPC_MAX_ATTEMPTS); - assert!(config.mpc_hash_override.is_none()); - assert!(config.passthrough_env.is_empty()); - } - - #[test] - fn test_user_config_typed_fields_extracted_from_map() { - let map = BTreeMap::from([ - ( - DSTACK_USER_CONFIG_MPC_IMAGE_TAGS.into(), - "v1.0, v1.1".into(), - ), - (DSTACK_USER_CONFIG_MPC_IMAGE_NAME.into(), "my/image".into()), - ( - DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY.into(), - "my.registry.io".into(), - ), - (ENV_VAR_RPC_REQUEST_TIMEOUT_SECS.into(), "30.0".into()), - (ENV_VAR_RPC_MAX_ATTEMPTS.into(), "5".into()), - ("MPC_ACCOUNT_ID".into(), "account.near".into()), - ]); - let config = user_config_from_map(map).unwrap(); - assert_eq!(config.image_tags, vec!["v1.0", "v1.1"]); - assert_eq!(config.image_name, "my/image"); - assert_eq!(config.registry, "my.registry.io"); - assert_eq!(config.rpc_request_timeout_secs, 30.0); - assert_eq!(config.rpc_max_attempts, 5); - // Launcher-only keys are NOT in passthrough_env - assert!( - !config - .passthrough_env - .contains_key(DSTACK_USER_CONFIG_MPC_IMAGE_TAGS) - ); - assert!( - !config - .passthrough_env - .contains_key(ENV_VAR_RPC_MAX_ATTEMPTS) - ); - // Container passthrough keys ARE in passthrough_env - assert_eq!( - config.passthrough_env.get("MPC_ACCOUNT_ID").unwrap(), - "account.near" - ); - } - - #[test] - fn test_user_config_malformed_rpc_fields_error() { - let map = BTreeMap::from([(ENV_VAR_RPC_MAX_ATTEMPTS.into(), "not_a_number".into())]); - let err = user_config_from_map(map).unwrap_err(); - assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_MAX_ATTEMPTS); - - let map = BTreeMap::from([(ENV_VAR_RPC_REQUEST_TIMEOUT_SECS.into(), "bad".into())]); - let err = user_config_from_map(map).unwrap_err(); - assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_REQUEST_TIMEOUT_SECS); - - let map = BTreeMap::from([(ENV_VAR_RPC_REQUEST_INTERVAL_SECS.into(), "bad".into())]); - let err = user_config_from_map(map).unwrap_err(); - assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_REQUEST_INTERVAL_SECS); - } - - #[test] - fn test_user_config_hash_override_extracted() { - let map = BTreeMap::from([(ENV_VAR_MPC_HASH_OVERRIDE.into(), "sha256:abc".into())]); - let config = user_config_from_map(map).unwrap(); - assert_eq!(config.mpc_hash_override.unwrap(), "sha256:abc"); - assert!( - !config - .passthrough_env - .contains_key(ENV_VAR_MPC_HASH_OVERRIDE) - ); - } - - #[test] - fn test_parse_user_config_from_file() { - let dir = tempfile::tempdir().unwrap(); - let file = dir.path().join("user_config"); - std::fs::write( - &file, - "# comment\nMPC_ACCOUNT_ID=test\nMPC_IMAGE_NAME=my/image\n", - ) - .unwrap(); - let config = parse_user_config(file.to_str().unwrap()).unwrap(); - assert_eq!(config.image_name, "my/image"); - assert_eq!( - config.passthrough_env.get("MPC_ACCOUNT_ID").unwrap(), - "test" - ); - assert!(!config.passthrough_env.contains_key("MPC_IMAGE_NAME")); - } - - // -- Host/port validation tests ----------------------------------------- - - #[test] - fn test_valid_host_entry() { - assert!(is_valid_host_entry("node.local:192.168.1.1")); - assert!(!is_valid_host_entry("node.local:not-an-ip")); - assert!(!is_valid_host_entry("--env LD_PRELOAD=hack.so")); - } - - #[test] - fn test_valid_port_mapping() { - assert!(is_valid_port_mapping("11780:11780")); - assert!(!is_valid_port_mapping("65536:11780")); - assert!(!is_valid_port_mapping("--volume /:/mnt")); - } - - // -- Security validation tests ------------------------------------------ - - #[test] - fn test_has_control_chars_rejects_newline_and_cr() { - assert!(has_control_chars("a\nb")); - assert!(has_control_chars("a\rb")); - } - - #[test] - fn test_has_control_chars_allows_tab() { - assert!(!has_control_chars("a\tb")); - } - - #[test] - fn test_has_control_chars_rejects_other_control_chars() { - assert!(has_control_chars(&format!("a{}b", '\x1F'))); - } - - #[test] - fn test_is_safe_env_value_rejects_control_chars() { - assert!(!is_safe_env_value("ok\nno")); - assert!(!is_safe_env_value("ok\rno")); - assert!(!is_safe_env_value(&format!("ok{}no", '\x1F'))); - } - - #[test] - fn test_is_safe_env_value_rejects_ld_preload() { - assert!(!is_safe_env_value("LD_PRELOAD=/tmp/x.so")); - assert!(!is_safe_env_value("foo LD_PRELOAD bar")); - } - - #[test] - fn test_is_safe_env_value_rejects_too_long() { - assert!(!is_safe_env_value(&"a".repeat(MAX_ENV_VALUE_LEN + 1))); - assert!(is_safe_env_value(&"a".repeat(MAX_ENV_VALUE_LEN))); - } - - #[test] - fn test_is_allowed_container_env_key_allows_mpc_prefix_uppercase() { - assert!(is_allowed_container_env_key("MPC_FOO")); - assert!(is_allowed_container_env_key("MPC_FOO_123")); - assert!(is_allowed_container_env_key("MPC_A_B_C")); - } - - #[test] - fn test_is_allowed_container_env_key_rejects_lowercase_or_invalid() { - assert!(!is_allowed_container_env_key("MPC_foo")); - assert!(!is_allowed_container_env_key("MPC-FOO")); - assert!(!is_allowed_container_env_key("MPC.FOO")); - assert!(!is_allowed_container_env_key("MPC_")); - } - - #[test] - fn test_is_allowed_container_env_key_allows_compat_non_mpc_keys() { - assert!(is_allowed_container_env_key("RUST_LOG")); - assert!(is_allowed_container_env_key("RUST_BACKTRACE")); - assert!(is_allowed_container_env_key("NEAR_BOOT_NODES")); - } - - #[test] - fn test_is_allowed_container_env_key_denies_sensitive_keys() { - assert!(!is_allowed_container_env_key("MPC_P2P_PRIVATE_KEY")); - assert!(!is_allowed_container_env_key("MPC_ACCOUNT_SK")); - } - - // -- Docker cmd builder tests ------------------------------------------- - - fn make_digest() -> String { - format!("sha256:{}", "a".repeat(64)) - } - - fn base_env() -> BTreeMap { - BTreeMap::from([ - ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), - ("MPC_CONTRACT_ID".into(), "contract.near".into()), - ("MPC_ENV".into(), "testnet".into()), - ("MPC_HOME_DIR".into(), "/data".into()), - ("NEAR_BOOT_NODES".into(), "boot1,boot2".into()), - ("RUST_LOG".into(), "info".into()), - ]) - } - - #[test] - fn test_build_docker_cmd_sanitizes_ports_and_hosts() { - let env = BTreeMap::from([ - ("PORTS".into(), "11780:11780,--env BAD=1".into()), - ( - "EXTRA_HOSTS".into(), - "node:192.168.1.1,--volume /:/mnt".into(), - ), - ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - - assert!(cmd.contains(&"MPC_ACCOUNT_ID=mpc-user-123".to_string())); - assert!(cmd.contains(&"11780:11780".to_string())); - assert!(cmd.contains(&"node:192.168.1.1".to_string())); - // Injection strings filtered - assert!(!cmd.iter().any(|arg| arg.contains("BAD=1"))); - assert!(!cmd.iter().any(|arg| arg.contains("/:/mnt"))); - } - - #[test] - fn test_extra_hosts_does_not_allow_ld_preload() { - let env = BTreeMap::from([ - ( - "EXTRA_HOSTS".into(), - "host:1.2.3.4,--env LD_PRELOAD=/evil.so".into(), - ), - ("MPC_ACCOUNT_ID".into(), "safe".into()), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(cmd.contains(&"host:1.2.3.4".to_string())); - assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); - } - - #[test] - fn test_ports_does_not_allow_volume_injection() { - let env = BTreeMap::from([ - ("PORTS".into(), "2200:2200,--volume /:/mnt".into()), - ("MPC_ACCOUNT_ID".into(), "safe".into()), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(cmd.contains(&"2200:2200".to_string())); - assert!(!cmd.iter().any(|arg| arg.contains("/:/mnt"))); - } - - #[test] - fn test_invalid_env_key_is_ignored() { - let env = BTreeMap::from([ - ("BAD_KEY".into(), "should_not_be_used".into()), - ("MPC_ACCOUNT_ID".into(), "safe".into()), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(!cmd.join(" ").contains("should_not_be_used")); - assert!(cmd.contains(&"MPC_ACCOUNT_ID=safe".to_string())); - } - - #[test] - fn test_mpc_backup_encryption_key_is_allowed() { - let env = BTreeMap::from([("MPC_BACKUP_ENCRYPTION_KEY_HEX".into(), "0".repeat(64))]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!( - cmd.join(" ") - .contains(&format!("MPC_BACKUP_ENCRYPTION_KEY_HEX={}", "0".repeat(64))) - ); - } - - #[test] - fn test_malformed_extra_host_is_ignored() { - let env = BTreeMap::from([ - ( - "EXTRA_HOSTS".into(), - "badhostentry,no-colon,also--bad".into(), - ), - ("MPC_ACCOUNT_ID".into(), "safe".into()), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(!cmd.contains(&"--add-host".to_string())); - } - - #[test] - fn test_env_value_with_shell_injection_is_handled_safely() { - let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "safe; rm -rf /".into())]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(cmd.contains(&"MPC_ACCOUNT_ID=safe; rm -rf /".to_string())); - } - - #[test] - fn test_build_docker_cmd_nontee_no_dstack_mount() { - let mut env = BTreeMap::new(); - env.insert("MPC_ACCOUNT_ID".into(), "x".into()); - let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); - let s = cmd.join(" "); - assert!(!s.contains("DSTACK_ENDPOINT=")); - assert!(!s.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); - } - - #[test] - fn test_build_docker_cmd_tee_has_dstack_mount() { - let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "x".into())]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - let s = cmd.join(" "); - assert!(s.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); - assert!(s.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); - } - - #[test] - fn test_build_docker_cmd_allows_arbitrary_mpc_prefix_env_vars() { - let mut env = base_env(); - env.insert("MPC_NEW_FEATURE_FLAG".into(), "1".into()); - env.insert("MPC_SOME_CONFIG".into(), "value".into()); - let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); - let cmd_str = cmd.join(" "); - assert!(cmd_str.contains("MPC_NEW_FEATURE_FLAG=1")); - assert!(cmd_str.contains("MPC_SOME_CONFIG=value")); - } - - #[test] - fn test_build_docker_cmd_blocks_sensitive_mpc_private_keys() { - let mut env = base_env(); - env.insert("MPC_P2P_PRIVATE_KEY".into(), "supersecret".into()); - env.insert("MPC_ACCOUNT_SK".into(), "supersecret2".into()); - let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); - let cmd_str = cmd.join(" "); - assert!(!cmd_str.contains("MPC_P2P_PRIVATE_KEY")); - assert!(!cmd_str.contains("MPC_ACCOUNT_SK")); - } - - #[test] - fn test_build_docker_cmd_rejects_env_value_with_newline() { - let mut env = base_env(); - env.insert("MPC_NEW_FEATURE_FLAG".into(), "ok\nbad".into()); - let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); - let cmd_str = cmd.join(" "); - assert!(!cmd_str.contains("MPC_NEW_FEATURE_FLAG")); - } - - #[test] - fn test_build_docker_cmd_enforces_max_env_count_cap() { - let mut env = base_env(); - for i in 0..=MAX_PASSTHROUGH_ENV_VARS { - env.insert(format!("MPC_X_{i}"), "1".into()); - } - let result = build_docker_cmd(Platform::NonTee, &env, &make_digest()); - assert_matches!(result, Err(LauncherError::TooManyEnvVars(_))); - } - - #[test] - fn test_build_docker_cmd_enforces_total_env_bytes_cap() { - let mut env = base_env(); - for i in 0..40 { - env.insert(format!("MPC_BIG_{i}"), "a".repeat(MAX_ENV_VALUE_LEN)); - } - let result = build_docker_cmd(Platform::NonTee, &env, &make_digest()); - assert_matches!(result, Err(LauncherError::EnvPayloadTooLarge(_))); - } - - // -- LD_PRELOAD injection tests ----------------------------------------- - - #[test] - fn test_ld_preload_injection_blocked_via_env_key() { - let env = BTreeMap::from([ - ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), - ("--env LD_PRELOAD".into(), "/path/to/my/malloc.so".into()), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); - } - - #[test] - fn test_ld_preload_injection_blocked_via_extra_hosts() { - let env = BTreeMap::from([ - ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), - ( - "EXTRA_HOSTS".into(), - "host1:192.168.0.1,host2:192.168.0.2,--env LD_PRELOAD=/path/to/my/malloc.so".into(), - ), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(cmd.contains(&"--add-host".to_string())); - assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); - } - - #[test] - fn test_ld_preload_injection_blocked_via_ports() { - let env = BTreeMap::from([ - ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), - ( - "PORTS".into(), - "11780:11780,--env LD_PRELOAD=/path/to/my/malloc.so".into(), - ), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(cmd.contains(&"-p".to_string())); - assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); - } - - #[test] - fn test_ld_preload_injection_blocked_via_mpc_account_id() { - let env = BTreeMap::from([ - ( - "MPC_ACCOUNT_ID".into(), - "mpc-user-123, --env LD_PRELOAD=/path/to/my/malloc.so".into(), - ), - ( - "EXTRA_HOSTS".into(), - "host1:192.168.0.1,host2:192.168.0.2".into(), - ), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); - } - - #[test] - fn test_ld_preload_injection_blocked_via_dash_e() { - let env = BTreeMap::from([ - ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), - ("-e LD_PRELOAD".into(), "/path/to/my/malloc.so".into()), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); - } - - #[test] - fn test_ld_preload_injection_blocked_via_extra_hosts_dash_e() { - let env = BTreeMap::from([ - ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), - ( - "EXTRA_HOSTS".into(), - "host1:192.168.0.1,host2:192.168.0.2,-e LD_PRELOAD=/path/to/my/malloc.so".into(), - ), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(cmd.contains(&"--add-host".to_string())); - assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); - } - - #[test] - fn test_ld_preload_injection_blocked_via_ports_dash_e() { - let env = BTreeMap::from([ - ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), - ( - "PORTS".into(), - "11780:11780,-e LD_PRELOAD=/path/to/my/malloc.so".into(), - ), - ]); - let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - assert!(cmd.contains(&"-p".to_string())); - assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); - } - - // -- Hash selection tests ----------------------------------------------- - - fn make_digest_json(hashes: &[&str]) -> String { - serde_json::json!({"approved_hashes": hashes}).to_string() - } - - #[test] - fn test_override_present() { - let dir = tempfile::tempdir().unwrap(); - let file = dir.path().join("image-digest.bin"); - let override_value = format!("sha256:{}", "a".repeat(64)); - let approved = vec![ - format!("sha256:{}", "b".repeat(64)), - override_value.clone(), - format!("sha256:{}", "c".repeat(64)), - ]; - let json = serde_json::json!({"approved_hashes": approved}).to_string(); - std::fs::write(&file, &json).unwrap(); - - // We can't easily override IMAGE_DIGEST_FILE constant, so test load_and_select_hash - // by creating a standalone test that reads from a custom path. - // Instead test the core logic directly: - let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); - assert!(data.approved_hashes.contains(&override_value)); - - // The override is in the approved list, so it should be valid - assert!(is_valid_sha256_digest(&override_value)); - assert!(data.approved_hashes.contains(&override_value)); - } - - #[test] - fn test_override_not_in_list() { - let approved = vec!["sha256:aaa", "sha256:bbb"]; - let json = make_digest_json(&approved); - let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); - let override_hash = "sha256:xyz"; - assert!(!data.approved_hashes.contains(&override_hash.to_string())); - } - - #[test] - fn test_no_override_picks_newest() { - let approved = vec!["sha256:newest", "sha256:older", "sha256:oldest"]; - let json = make_digest_json(&approved); - let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); - assert_eq!(data.approved_hashes[0], "sha256:newest"); - } - - #[test] - fn test_json_key_matches_node() { - // Must stay aligned with crates/node/src/tee/allowed_image_hashes_watcher.rs - let json = r#"{"approved_hashes": ["sha256:abc"]}"#; - let data: ApprovedHashesFile = serde_json::from_str(json).unwrap(); - assert_eq!(data.approved_hashes.len(), 1); - } - - #[test] - fn test_get_bare_digest() { - assert_eq!( - get_bare_digest(&format!("sha256:{}", "a".repeat(64))).unwrap(), - "a".repeat(64) - ); - get_bare_digest("invalid").unwrap_err(); - } - - #[test] - fn test_is_valid_sha256_digest() { - assert!(is_valid_sha256_digest(&format!( - "sha256:{}", - "a".repeat(64) - ))); - assert!(!is_valid_sha256_digest("sha256:tooshort")); - assert!(!is_valid_sha256_digest("not-a-digest")); - // hex::decode accepts uppercase; as_hex() normalizes to lowercase - assert!(is_valid_sha256_digest(&format!( - "sha256:{}", - "A".repeat(64) - ))); - } - - #[test] - fn test_parse_image_digest_normalizes_case() { - let upper = format!("sha256:{}", "AB".repeat(32)); - let hash = parse_image_digest(&upper).unwrap(); - assert_eq!(hash.as_hex(), "ab".repeat(32)); - } - - // -- Full flow docker cmd test ------------------------------------------ - - #[test] - fn test_parse_and_build_docker_cmd_full_flow() { - let dir = tempfile::tempdir().unwrap(); - let file = dir.path().join("user_config"); - std::fs::write( - &file, - "MPC_ACCOUNT_ID=test-user\nPORTS=11780:11780, --env BAD=oops\nEXTRA_HOSTS=host1:192.168.1.1, --volume /:/mnt\n", - ) - .unwrap(); - let config = parse_user_config(file.to_str().unwrap()).unwrap(); - let cmd = build_docker_cmd(Platform::Tee, &config.passthrough_env, &make_digest()).unwrap(); - let cmd_str = cmd.join(" "); - - assert!(cmd_str.contains("MPC_ACCOUNT_ID=test-user")); - assert!(cmd_str.contains("11780:11780")); - assert!(cmd_str.contains("host1:192.168.1.1")); - assert!(!cmd_str.contains("BAD=oops")); - assert!(!cmd_str.contains("/:/mnt")); - } - - #[test] - fn test_full_docker_cmd_structure() { - let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "test-user".into())]); - let digest = make_digest(); - let cmd = build_docker_cmd(Platform::NonTee, &env, &digest).unwrap(); - - // Check required subsequence - assert!(cmd.contains(&"docker".to_string())); - assert!(cmd.contains(&"run".to_string())); - assert!(cmd.contains(&"--security-opt".to_string())); - assert!(cmd.contains(&"no-new-privileges:true".to_string())); - assert!(cmd.contains(&"/tapp:/tapp:ro".to_string())); - assert!(cmd.contains(&"shared-volume:/mnt/shared".to_string())); - assert!(cmd.contains(&"mpc-data:/data".to_string())); - assert!(cmd.contains(&MPC_CONTAINER_NAME.to_string())); - assert!(cmd.contains(&"--detach".to_string())); - // Image digest should be the last argument - assert_eq!(cmd.last().unwrap(), &digest); - } - - // -- Dstack tests ------------------------------------------------------- - - #[test] - fn test_extend_rtmr3_nontee_is_noop() { - // NonTee should return immediately without touching dstack - let rt = tokio::runtime::Runtime::new().unwrap(); - rt.block_on(extend_rtmr3(Platform::NonTee, &make_digest())) - .unwrap(); - } - - #[test] - fn test_extend_rtmr3_tee_requires_socket() { - // TEE mode should fail when socket doesn't exist - let rt = tokio::runtime::Runtime::new().unwrap(); - let result = rt.block_on(extend_rtmr3(Platform::Tee, &make_digest())); - assert_matches!(result, Err(LauncherError::DstackSocketMissing(_))); - } - - // -- MpcDockerImageHash integration test -------------------------------- - - #[test] - fn test_mpc_docker_image_hash_from_bare_hex() { - let bare_hex = "a".repeat(64); - let hash: MpcDockerImageHash = bare_hex.parse().unwrap(); - assert_eq!(hash.as_hex(), bare_hex); - } - - // -- Integration test (feature-gated) ----------------------------------- - - #[cfg(feature = "integration-test")] - mod integration { - use super::*; - - const TEST_DIGEST: &str = - "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372"; - - fn test_dstack_config() -> Config { - user_config_from_map(BTreeMap::from([ - ( - "MPC_IMAGE_TAGS".into(), - "83b52da4e2270c688cdd30da04f6b9d3565f25bb".into(), - ), - ("MPC_IMAGE_NAME".into(), "nearone/testing".into()), - ("MPC_REGISTRY".into(), "registry.hub.docker.com".into()), - ])) - .unwrap() - } - - #[tokio::test] - async fn test_validate_image_hash_real_registry() { - let timing = RpcTimingConfig { - request_timeout_secs: 10.0, - request_interval_secs: 1.0, - max_attempts: 20, - }; - let result = validate_image_hash(TEST_DIGEST, &test_dstack_config(), &timing) - .await - .unwrap(); - assert!(result, "validate_image_hash() failed for test image"); - } - } -} +// #[cfg(test)] +// mod tests { +// use super::*; +// use assert_matches::assert_matches; +// use launcher_interface::types::ApprovedHashesFile; + +// // -- DstackUserConfig parsing tests ------------------------------------- + +// #[test] +// fn test_user_config_defaults_when_map_is_empty() { +// let config = user_config_from_map(BTreeMap::new()).unwrap(); +// assert_eq!(config.image_tags, vec![DEFAULT_MPC_IMAGE_TAG]); +// assert_eq!(config.image_name, DEFAULT_MPC_IMAGE_NAME); +// assert_eq!(config.registry, DEFAULT_MPC_REGISTRY); +// assert_eq!( +// config.rpc_request_timeout_secs, +// DEFAULT_RPC_REQUEST_TIMEOUT_SECS +// ); +// assert_eq!( +// config.rpc_request_interval_secs, +// DEFAULT_RPC_REQUEST_INTERVAL_SECS +// ); +// assert_eq!(config.rpc_max_attempts, DEFAULT_RPC_MAX_ATTEMPTS); +// assert!(config.mpc_hash_override.is_none()); +// assert!(config.passthrough_env.is_empty()); +// } + +// #[test] +// fn test_user_config_typed_fields_extracted_from_map() { +// let map = BTreeMap::from([ +// ( +// DSTACK_USER_CONFIG_MPC_IMAGE_TAGS.into(), +// "v1.0, v1.1".into(), +// ), +// (DSTACK_USER_CONFIG_MPC_IMAGE_NAME.into(), "my/image".into()), +// ( +// DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY.into(), +// "my.registry.io".into(), +// ), +// (ENV_VAR_RPC_REQUEST_TIMEOUT_SECS.into(), "30.0".into()), +// (ENV_VAR_RPC_MAX_ATTEMPTS.into(), "5".into()), +// ("MPC_ACCOUNT_ID".into(), "account.near".into()), +// ]); +// let config = user_config_from_map(map).unwrap(); +// assert_eq!(config.image_tags, vec!["v1.0", "v1.1"]); +// assert_eq!(config.image_name, "my/image"); +// assert_eq!(config.registry, "my.registry.io"); +// assert_eq!(config.rpc_request_timeout_secs, 30.0); +// assert_eq!(config.rpc_max_attempts, 5); +// // Launcher-only keys are NOT in passthrough_env +// assert!( +// !config +// .passthrough_env +// .contains_key(DSTACK_USER_CONFIG_MPC_IMAGE_TAGS) +// ); +// assert!( +// !config +// .passthrough_env +// .contains_key(ENV_VAR_RPC_MAX_ATTEMPTS) +// ); +// // Container passthrough keys ARE in passthrough_env +// assert_eq!( +// config.passthrough_env.get("MPC_ACCOUNT_ID").unwrap(), +// "account.near" +// ); +// } + +// #[test] +// fn test_user_config_malformed_rpc_fields_error() { +// let map = BTreeMap::from([(ENV_VAR_RPC_MAX_ATTEMPTS.into(), "not_a_number".into())]); +// let err = user_config_from_map(map).unwrap_err(); +// assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_MAX_ATTEMPTS); + +// let map = BTreeMap::from([(ENV_VAR_RPC_REQUEST_TIMEOUT_SECS.into(), "bad".into())]); +// let err = user_config_from_map(map).unwrap_err(); +// assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_REQUEST_TIMEOUT_SECS); + +// let map = BTreeMap::from([(ENV_VAR_RPC_REQUEST_INTERVAL_SECS.into(), "bad".into())]); +// let err = user_config_from_map(map).unwrap_err(); +// assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_REQUEST_INTERVAL_SECS); +// } + +// #[test] +// fn test_user_config_hash_override_extracted() { +// let map = BTreeMap::from([(ENV_VAR_MPC_HASH_OVERRIDE.into(), "sha256:abc".into())]); +// let config = user_config_from_map(map).unwrap(); +// assert_eq!(config.mpc_hash_override.unwrap(), "sha256:abc"); +// assert!( +// !config +// .passthrough_env +// .contains_key(ENV_VAR_MPC_HASH_OVERRIDE) +// ); +// } + +// #[test] +// fn test_parse_user_config_from_file() { +// let dir = tempfile::tempdir().unwrap(); +// let file = dir.path().join("user_config"); +// std::fs::write( +// &file, +// "# comment\nMPC_ACCOUNT_ID=test\nMPC_IMAGE_NAME=my/image\n", +// ) +// .unwrap(); +// let config = parse_user_config(file.to_str().unwrap()).unwrap(); +// assert_eq!(config.image_name, "my/image"); +// assert_eq!( +// config.passthrough_env.get("MPC_ACCOUNT_ID").unwrap(), +// "test" +// ); +// assert!(!config.passthrough_env.contains_key("MPC_IMAGE_NAME")); +// } + +// // -- Host/port validation tests ----------------------------------------- + +// #[test] +// fn test_valid_host_entry() { +// assert!(is_valid_host_entry("node.local:192.168.1.1")); +// assert!(!is_valid_host_entry("node.local:not-an-ip")); +// assert!(!is_valid_host_entry("--env LD_PRELOAD=hack.so")); +// } + +// #[test] +// fn test_valid_port_mapping() { +// assert!(is_valid_port_mapping("11780:11780")); +// assert!(!is_valid_port_mapping("65536:11780")); +// assert!(!is_valid_port_mapping("--volume /:/mnt")); +// } + +// // -- Security validation tests ------------------------------------------ + +// #[test] +// fn test_has_control_chars_rejects_newline_and_cr() { +// assert!(has_control_chars("a\nb")); +// assert!(has_control_chars("a\rb")); +// } + +// #[test] +// fn test_has_control_chars_allows_tab() { +// assert!(!has_control_chars("a\tb")); +// } + +// #[test] +// fn test_has_control_chars_rejects_other_control_chars() { +// assert!(has_control_chars(&format!("a{}b", '\x1F'))); +// } + +// #[test] +// fn test_is_safe_env_value_rejects_control_chars() { +// assert!(!is_safe_env_value("ok\nno")); +// assert!(!is_safe_env_value("ok\rno")); +// assert!(!is_safe_env_value(&format!("ok{}no", '\x1F'))); +// } + +// #[test] +// fn test_is_safe_env_value_rejects_ld_preload() { +// assert!(!is_safe_env_value("LD_PRELOAD=/tmp/x.so")); +// assert!(!is_safe_env_value("foo LD_PRELOAD bar")); +// } + +// #[test] +// fn test_is_safe_env_value_rejects_too_long() { +// assert!(!is_safe_env_value(&"a".repeat(MAX_ENV_VALUE_LEN + 1))); +// assert!(is_safe_env_value(&"a".repeat(MAX_ENV_VALUE_LEN))); +// } + +// #[test] +// fn test_is_allowed_container_env_key_allows_mpc_prefix_uppercase() { +// assert!(is_allowed_container_env_key("MPC_FOO")); +// assert!(is_allowed_container_env_key("MPC_FOO_123")); +// assert!(is_allowed_container_env_key("MPC_A_B_C")); +// } + +// #[test] +// fn test_is_allowed_container_env_key_rejects_lowercase_or_invalid() { +// assert!(!is_allowed_container_env_key("MPC_foo")); +// assert!(!is_allowed_container_env_key("MPC-FOO")); +// assert!(!is_allowed_container_env_key("MPC.FOO")); +// assert!(!is_allowed_container_env_key("MPC_")); +// } + +// #[test] +// fn test_is_allowed_container_env_key_allows_compat_non_mpc_keys() { +// assert!(is_allowed_container_env_key("RUST_LOG")); +// assert!(is_allowed_container_env_key("RUST_BACKTRACE")); +// assert!(is_allowed_container_env_key("NEAR_BOOT_NODES")); +// } + +// #[test] +// fn test_is_allowed_container_env_key_denies_sensitive_keys() { +// assert!(!is_allowed_container_env_key("MPC_P2P_PRIVATE_KEY")); +// assert!(!is_allowed_container_env_key("MPC_ACCOUNT_SK")); +// } + +// // -- Docker cmd builder tests ------------------------------------------- + +// fn make_digest() -> String { +// format!("sha256:{}", "a".repeat(64)) +// } + +// fn base_env() -> BTreeMap { +// BTreeMap::from([ +// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), +// ("MPC_CONTRACT_ID".into(), "contract.near".into()), +// ("MPC_ENV".into(), "testnet".into()), +// ("MPC_HOME_DIR".into(), "/data".into()), +// ("NEAR_BOOT_NODES".into(), "boot1,boot2".into()), +// ("RUST_LOG".into(), "info".into()), +// ]) +// } + +// #[test] +// fn test_build_docker_cmd_sanitizes_ports_and_hosts() { +// let env = BTreeMap::from([ +// ("PORTS".into(), "11780:11780,--env BAD=1".into()), +// ( +// "EXTRA_HOSTS".into(), +// "node:192.168.1.1,--volume /:/mnt".into(), +// ), +// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); + +// assert!(cmd.contains(&"MPC_ACCOUNT_ID=mpc-user-123".to_string())); +// assert!(cmd.contains(&"11780:11780".to_string())); +// assert!(cmd.contains(&"node:192.168.1.1".to_string())); +// // Injection strings filtered +// assert!(!cmd.iter().any(|arg| arg.contains("BAD=1"))); +// assert!(!cmd.iter().any(|arg| arg.contains("/:/mnt"))); +// } + +// #[test] +// fn test_extra_hosts_does_not_allow_ld_preload() { +// let env = BTreeMap::from([ +// ( +// "EXTRA_HOSTS".into(), +// "host:1.2.3.4,--env LD_PRELOAD=/evil.so".into(), +// ), +// ("MPC_ACCOUNT_ID".into(), "safe".into()), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(cmd.contains(&"host:1.2.3.4".to_string())); +// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); +// } + +// #[test] +// fn test_ports_does_not_allow_volume_injection() { +// let env = BTreeMap::from([ +// ("PORTS".into(), "2200:2200,--volume /:/mnt".into()), +// ("MPC_ACCOUNT_ID".into(), "safe".into()), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(cmd.contains(&"2200:2200".to_string())); +// assert!(!cmd.iter().any(|arg| arg.contains("/:/mnt"))); +// } + +// #[test] +// fn test_invalid_env_key_is_ignored() { +// let env = BTreeMap::from([ +// ("BAD_KEY".into(), "should_not_be_used".into()), +// ("MPC_ACCOUNT_ID".into(), "safe".into()), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(!cmd.join(" ").contains("should_not_be_used")); +// assert!(cmd.contains(&"MPC_ACCOUNT_ID=safe".to_string())); +// } + +// #[test] +// fn test_mpc_backup_encryption_key_is_allowed() { +// let env = BTreeMap::from([("MPC_BACKUP_ENCRYPTION_KEY_HEX".into(), "0".repeat(64))]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!( +// cmd.join(" ") +// .contains(&format!("MPC_BACKUP_ENCRYPTION_KEY_HEX={}", "0".repeat(64))) +// ); +// } + +// #[test] +// fn test_malformed_extra_host_is_ignored() { +// let env = BTreeMap::from([ +// ( +// "EXTRA_HOSTS".into(), +// "badhostentry,no-colon,also--bad".into(), +// ), +// ("MPC_ACCOUNT_ID".into(), "safe".into()), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(!cmd.contains(&"--add-host".to_string())); +// } + +// #[test] +// fn test_env_value_with_shell_injection_is_handled_safely() { +// let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "safe; rm -rf /".into())]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(cmd.contains(&"MPC_ACCOUNT_ID=safe; rm -rf /".to_string())); +// } + +// #[test] +// fn test_build_docker_cmd_nontee_no_dstack_mount() { +// let mut env = BTreeMap::new(); +// env.insert("MPC_ACCOUNT_ID".into(), "x".into()); +// let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); +// let s = cmd.join(" "); +// assert!(!s.contains("DSTACK_ENDPOINT=")); +// assert!(!s.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); +// } + +// #[test] +// fn test_build_docker_cmd_tee_has_dstack_mount() { +// let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "x".into())]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// let s = cmd.join(" "); +// assert!(s.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); +// assert!(s.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); +// } + +// #[test] +// fn test_build_docker_cmd_allows_arbitrary_mpc_prefix_env_vars() { +// let mut env = base_env(); +// env.insert("MPC_NEW_FEATURE_FLAG".into(), "1".into()); +// env.insert("MPC_SOME_CONFIG".into(), "value".into()); +// let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); +// let cmd_str = cmd.join(" "); +// assert!(cmd_str.contains("MPC_NEW_FEATURE_FLAG=1")); +// assert!(cmd_str.contains("MPC_SOME_CONFIG=value")); +// } + +// #[test] +// fn test_build_docker_cmd_blocks_sensitive_mpc_private_keys() { +// let mut env = base_env(); +// env.insert("MPC_P2P_PRIVATE_KEY".into(), "supersecret".into()); +// env.insert("MPC_ACCOUNT_SK".into(), "supersecret2".into()); +// let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); +// let cmd_str = cmd.join(" "); +// assert!(!cmd_str.contains("MPC_P2P_PRIVATE_KEY")); +// assert!(!cmd_str.contains("MPC_ACCOUNT_SK")); +// } + +// #[test] +// fn test_build_docker_cmd_rejects_env_value_with_newline() { +// let mut env = base_env(); +// env.insert("MPC_NEW_FEATURE_FLAG".into(), "ok\nbad".into()); +// let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); +// let cmd_str = cmd.join(" "); +// assert!(!cmd_str.contains("MPC_NEW_FEATURE_FLAG")); +// } + +// #[test] +// fn test_build_docker_cmd_enforces_max_env_count_cap() { +// let mut env = base_env(); +// for i in 0..=MAX_PASSTHROUGH_ENV_VARS { +// env.insert(format!("MPC_X_{i}"), "1".into()); +// } +// let result = build_docker_cmd(Platform::NonTee, &env, &make_digest()); +// assert_matches!(result, Err(LauncherError::TooManyEnvVars(_))); +// } + +// #[test] +// fn test_build_docker_cmd_enforces_total_env_bytes_cap() { +// let mut env = base_env(); +// for i in 0..40 { +// env.insert(format!("MPC_BIG_{i}"), "a".repeat(MAX_ENV_VALUE_LEN)); +// } +// let result = build_docker_cmd(Platform::NonTee, &env, &make_digest()); +// assert_matches!(result, Err(LauncherError::EnvPayloadTooLarge(_))); +// } + +// // -- LD_PRELOAD injection tests ----------------------------------------- + +// #[test] +// fn test_ld_preload_injection_blocked_via_env_key() { +// let env = BTreeMap::from([ +// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), +// ("--env LD_PRELOAD".into(), "/path/to/my/malloc.so".into()), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); +// } + +// #[test] +// fn test_ld_preload_injection_blocked_via_extra_hosts() { +// let env = BTreeMap::from([ +// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), +// ( +// "EXTRA_HOSTS".into(), +// "host1:192.168.0.1,host2:192.168.0.2,--env LD_PRELOAD=/path/to/my/malloc.so".into(), +// ), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(cmd.contains(&"--add-host".to_string())); +// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); +// } + +// #[test] +// fn test_ld_preload_injection_blocked_via_ports() { +// let env = BTreeMap::from([ +// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), +// ( +// "PORTS".into(), +// "11780:11780,--env LD_PRELOAD=/path/to/my/malloc.so".into(), +// ), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(cmd.contains(&"-p".to_string())); +// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); +// } + +// #[test] +// fn test_ld_preload_injection_blocked_via_mpc_account_id() { +// let env = BTreeMap::from([ +// ( +// "MPC_ACCOUNT_ID".into(), +// "mpc-user-123, --env LD_PRELOAD=/path/to/my/malloc.so".into(), +// ), +// ( +// "EXTRA_HOSTS".into(), +// "host1:192.168.0.1,host2:192.168.0.2".into(), +// ), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); +// } + +// #[test] +// fn test_ld_preload_injection_blocked_via_dash_e() { +// let env = BTreeMap::from([ +// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), +// ("-e LD_PRELOAD".into(), "/path/to/my/malloc.so".into()), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); +// } + +// #[test] +// fn test_ld_preload_injection_blocked_via_extra_hosts_dash_e() { +// let env = BTreeMap::from([ +// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), +// ( +// "EXTRA_HOSTS".into(), +// "host1:192.168.0.1,host2:192.168.0.2,-e LD_PRELOAD=/path/to/my/malloc.so".into(), +// ), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(cmd.contains(&"--add-host".to_string())); +// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); +// } + +// #[test] +// fn test_ld_preload_injection_blocked_via_ports_dash_e() { +// let env = BTreeMap::from([ +// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), +// ( +// "PORTS".into(), +// "11780:11780,-e LD_PRELOAD=/path/to/my/malloc.so".into(), +// ), +// ]); +// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); +// assert!(cmd.contains(&"-p".to_string())); +// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); +// } + +// // -- Hash selection tests ----------------------------------------------- + +// fn make_digest_json(hashes: &[&str]) -> String { +// serde_json::json!({"approved_hashes": hashes}).to_string() +// } + +// #[test] +// fn test_override_present() { +// let dir = tempfile::tempdir().unwrap(); +// let file = dir.path().join("image-digest.bin"); +// let override_value = format!("sha256:{}", "a".repeat(64)); +// let approved = vec![ +// format!("sha256:{}", "b".repeat(64)), +// override_value.clone(), +// format!("sha256:{}", "c".repeat(64)), +// ]; +// let json = serde_json::json!({"approved_hashes": approved}).to_string(); +// std::fs::write(&file, &json).unwrap(); + +// // We can't easily override IMAGE_DIGEST_FILE constant, so test load_and_select_hash +// // by creating a standalone test that reads from a custom path. +// // Instead test the core logic directly: +// let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); +// assert!(data.approved_hashes.contains(&override_value)); + +// // The override is in the approved list, so it should be valid +// assert!(is_valid_sha256_digest(&override_value)); +// assert!(data.approved_hashes.contains(&override_value)); +// } + +// #[test] +// fn test_override_not_in_list() { +// let approved = vec!["sha256:aaa", "sha256:bbb"]; +// let json = make_digest_json(&approved); +// let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); +// let override_hash = "sha256:xyz"; +// assert!(!data.approved_hashes.contains(&override_hash.to_string())); +// } + +// #[test] +// fn test_no_override_picks_newest() { +// let approved = vec!["sha256:newest", "sha256:older", "sha256:oldest"]; +// let json = make_digest_json(&approved); +// let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); +// assert_eq!(data.approved_hashes[0], "sha256:newest"); +// } + +// #[test] +// fn test_json_key_matches_node() { +// // Must stay aligned with crates/node/src/tee/allowed_image_hashes_watcher.rs +// let json = r#"{"approved_hashes": ["sha256:abc"]}"#; +// let data: ApprovedHashesFile = serde_json::from_str(json).unwrap(); +// assert_eq!(data.approved_hashes.len(), 1); +// } + +// #[test] +// fn test_get_bare_digest() { +// assert_eq!( +// get_bare_digest(&format!("sha256:{}", "a".repeat(64))).unwrap(), +// "a".repeat(64) +// ); +// get_bare_digest("invalid").unwrap_err(); +// } + +// #[test] +// fn test_is_valid_sha256_digest() { +// assert!(is_valid_sha256_digest(&format!( +// "sha256:{}", +// "a".repeat(64) +// ))); +// assert!(!is_valid_sha256_digest("sha256:tooshort")); +// assert!(!is_valid_sha256_digest("not-a-digest")); +// // hex::decode accepts uppercase; as_hex() normalizes to lowercase +// assert!(is_valid_sha256_digest(&format!( +// "sha256:{}", +// "A".repeat(64) +// ))); +// } + +// #[test] +// fn test_parse_image_digest_normalizes_case() { +// let upper = format!("sha256:{}", "AB".repeat(32)); +// let hash = parse_image_digest(&upper).unwrap(); +// assert_eq!(hash.as_hex(), "ab".repeat(32)); +// } + +// // -- Full flow docker cmd test ------------------------------------------ + +// #[test] +// fn test_parse_and_build_docker_cmd_full_flow() { +// let dir = tempfile::tempdir().unwrap(); +// let file = dir.path().join("user_config"); +// std::fs::write( +// &file, +// "MPC_ACCOUNT_ID=test-user\nPORTS=11780:11780, --env BAD=oops\nEXTRA_HOSTS=host1:192.168.1.1, --volume /:/mnt\n", +// ) +// .unwrap(); +// let config = parse_user_config(file.to_str().unwrap()).unwrap(); +// let cmd = build_docker_cmd(Platform::Tee, &config.passthrough_env, &make_digest()).unwrap(); +// let cmd_str = cmd.join(" "); + +// assert!(cmd_str.contains("MPC_ACCOUNT_ID=test-user")); +// assert!(cmd_str.contains("11780:11780")); +// assert!(cmd_str.contains("host1:192.168.1.1")); +// assert!(!cmd_str.contains("BAD=oops")); +// assert!(!cmd_str.contains("/:/mnt")); +// } + +// #[test] +// fn test_full_docker_cmd_structure() { +// let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "test-user".into())]); +// let digest = make_digest(); +// let cmd = build_docker_cmd(Platform::NonTee, &env, &digest).unwrap(); + +// // Check required subsequence +// assert!(cmd.contains(&"docker".to_string())); +// assert!(cmd.contains(&"run".to_string())); +// assert!(cmd.contains(&"--security-opt".to_string())); +// assert!(cmd.contains(&"no-new-privileges:true".to_string())); +// assert!(cmd.contains(&"/tapp:/tapp:ro".to_string())); +// assert!(cmd.contains(&"shared-volume:/mnt/shared".to_string())); +// assert!(cmd.contains(&"mpc-data:/data".to_string())); +// assert!(cmd.contains(&MPC_CONTAINER_NAME.to_string())); +// assert!(cmd.contains(&"--detach".to_string())); +// // Image digest should be the last argument +// assert_eq!(cmd.last().unwrap(), &digest); +// } + +// // -- Dstack tests ------------------------------------------------------- + +// #[test] +// fn test_extend_rtmr3_nontee_is_noop() { +// // NonTee should return immediately without touching dstack +// let rt = tokio::runtime::Runtime::new().unwrap(); +// rt.block_on(extend_rtmr3(Platform::NonTee, &make_digest())) +// .unwrap(); +// } + +// #[test] +// fn test_extend_rtmr3_tee_requires_socket() { +// // TEE mode should fail when socket doesn't exist +// let rt = tokio::runtime::Runtime::new().unwrap(); +// let result = rt.block_on(extend_rtmr3(Platform::Tee, &make_digest())); +// assert_matches!(result, Err(LauncherError::DstackSocketMissing(_))); +// } + +// // -- MpcDockerImageHash integration test -------------------------------- + +// #[test] +// fn test_mpc_docker_image_hash_from_bare_hex() { +// let bare_hex = "a".repeat(64); +// let hash: MpcDockerImageHash = bare_hex.parse().unwrap(); +// assert_eq!(hash.as_hex(), bare_hex); +// } + +// // -- Integration test (feature-gated) ----------------------------------- + +// #[cfg(feature = "integration-test")] +// mod integration { +// use super::*; + +// const TEST_DIGEST: &str = +// "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372"; + +// fn test_dstack_config() -> Config { +// user_config_from_map(BTreeMap::from([ +// ( +// "MPC_IMAGE_TAGS".into(), +// "83b52da4e2270c688cdd30da04f6b9d3565f25bb".into(), +// ), +// ("MPC_IMAGE_NAME".into(), "nearone/testing".into()), +// ("MPC_REGISTRY".into(), "registry.hub.docker.com".into()), +// ])) +// .unwrap() +// } + +// #[tokio::test] +// async fn test_validate_image_hash_real_registry() { +// let timing = RpcTimingConfig { +// request_timeout_secs: 10.0, +// request_interval_secs: 1.0, +// max_attempts: 20, +// }; +// let result = validate_image_hash(TEST_DIGEST, &test_dstack_config(), &timing) +// .await +// .unwrap(); +// assert!(result, "validate_image_hash() failed for test image"); +// } +// } +// } diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index de681896a..6339920bd 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -1,3 +1,4 @@ +use bounded_collections::NonEmptyVec; use clap::{Parser, ValueEnum}; use mpc_primitives::hash::MpcDockerImageHash; use serde::{Deserialize, Serialize}; @@ -47,15 +48,15 @@ pub struct Config { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct LauncherConfig { /// Docker image tags to search (from `MPC_IMAGE_TAGS`, comma-separated). - pub image_tags: Vec, + pub image_tags: NonEmptyVec, /// Docker image name (from `MPC_IMAGE_NAME`). pub image_name: String, /// Docker registry (from `MPC_REGISTRY`). pub registry: String, /// Per-request timeout for registry RPC calls (from `RPC_REQUEST_TIMEOUT_SECS`). - pub rpc_request_timeout_secs: f64, + pub rpc_request_timeout_secs: u64, /// Delay between registry RPC retries (from `RPC_REQUEST_INTERVAL_SECS`). - pub rpc_request_interval_secs: f64, + pub rpc_request_interval_secs: u64, /// Maximum registry RPC attempts (from `RPC_MAX_ATTEMPTS`). pub rpc_max_attempts: u32, /// Optional hash override that bypasses registry lookup (from `MPC_HASH_OVERRIDE`). From 2965cdea433bb3d1914f13890c29641b4a82389c Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Mon, 2 Mar 2026 18:41:19 +0100 Subject: [PATCH 009/176] wip --- crates/tee-launcher/src/error.rs | 5 +-- crates/tee-launcher/src/main.rs | 59 +++++++++++--------------------- 2 files changed, 23 insertions(+), 41 deletions(-) diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 92836663d..2c67af0a4 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -1,5 +1,6 @@ use std::path::PathBuf; +use mpc_primitives::hash::MpcDockerImageHash; use thiserror::Error; #[derive(Error, Debug)] @@ -43,8 +44,8 @@ pub enum LauncherError { #[error("MPC image hash validation failed: {0}")] ImageValidationFailed(String), - #[error("docker run failed for validated hash={0}")] - DockerRunFailed(String), + #[error("docker run failed for validated hash")] + DockerRunFailed(MpcDockerImageHash), #[error("Too many env vars to pass through (>{0})")] TooManyEnvVars(usize), diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index eec320fd5..d609bdfa4 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -167,29 +167,6 @@ fn is_safe_port_mapping(mapping: &str) -> bool { // Hash selection // --------------------------------------------------------------------------- -/// Parse a full `sha256:` digest into a validated [`MpcDockerImageHash`]. -/// -/// Uses the workspace type's `FromStr` impl which does `hex::decode` + 32-byte -/// length check — no regex needed. -fn parse_image_digest(full_digest: &str) -> Result { - let bare_hex = full_digest.strip_prefix(SHA256_PREFIX).ok_or_else(|| { - LauncherError::InvalidDefaultDigest(format!( - "Invalid digest (missing sha256: prefix): {full_digest}" - )) - })?; - bare_hex - .parse::() - .map_err(|e| LauncherError::InvalidDefaultDigest(format!("{full_digest}: {e}"))) -} - -fn is_valid_sha256_digest(digest: &str) -> bool { - parse_image_digest(digest).is_ok() -} - -fn get_bare_digest(full_digest: &str) -> Result { - Ok(parse_image_digest(full_digest)?.as_hex()) -} - fn load_and_select_hash(args: &CliArgs, dstack_config: &Config) -> Result { let approved_hashes = if std::path::Path::new(IMAGE_DIGEST_FILE).is_file() { let content = std::fs::read_to_string(IMAGE_DIGEST_FILE).map_err(|source| { @@ -198,7 +175,7 @@ fn load_and_select_hash(args: &CliArgs, dstack_config: &Config) -> Result Result> { - let bare_digest = get_bare_digest(image_digest)?; - let mut cmd: Vec = vec!["docker".into(), "run".into()]; // Required environment variables - cmd.extend(["--env".into(), format!("MPC_IMAGE_HASH={bare_digest}")]); + cmd.extend([ + "--env".into(), + format!("MPC_IMAGE_HASH={}", image_digest.as_hex()), + ]); cmd.extend([ "--env".into(), format!("MPC_LATEST_ALLOWED_HASH_FILE={IMAGE_DIGEST_FILE}"), @@ -547,7 +525,7 @@ fn build_docker_cmd( "--name".into(), MPC_CONTAINER_NAME.into(), "--detach".into(), - image_digest.to_string(), + image_digest.as_hex(), ]); tracing::info!("docker cmd {}", cmd.join(" ")); @@ -563,10 +541,13 @@ fn build_docker_cmd( fn launch_mpc_container( platform: Platform, - valid_hash: &str, + valid_hash: &MpcDockerImageHash, mpc_config: &MpcBinaryConfig, ) -> Result<()> { - tracing::info!("Launching MPC node with validated hash: {valid_hash}"); + tracing::info!( + "Launching MPC node with validated hash: {}", + valid_hash.as_hex() + ); remove_existing_container(); let docker_cmd = build_docker_cmd(platform, mpc_config, valid_hash)?; @@ -574,12 +555,10 @@ fn launch_mpc_container( let status = Command::new(&docker_cmd[0]) .args(&docker_cmd[1..]) .status() - .map_err(|e| LauncherError::DockerRunFailed(e.to_string()))?; + .map_err(|e| LauncherError::DockerRunFailed(valid_hash.clone()))?; if !status.success() { - return Err(LauncherError::DockerRunFailed(format!( - "validated hash={valid_hash}" - ))); + return Err(LauncherError::DockerRunFailed(valid_hash.clone())); } tracing::info!("MPC launched successfully."); @@ -591,7 +570,7 @@ fn is_unix_socket(path: &str) -> bool { std::fs::metadata(path).is_ok_and(|meta| meta.file_type().is_socket()) } -async fn extend_rtmr3(platform: Platform, valid_hash: &str) -> Result<()> { +async fn extend_rtmr3(platform: Platform, image_hash: MpcDockerImageHash) -> Result<()> { if platform == Platform::NonTee { tracing::info!("PLATFORM=NONTEE → skipping RTMR3 extension step."); return Ok(()); @@ -603,8 +582,7 @@ async fn extend_rtmr3(platform: Platform, valid_hash: &str) -> Result<()> { )); } - let bare = get_bare_digest(valid_hash)?; - tracing::info!("Extending RTMR3 with validated hash: {bare}"); + tracing::info!(?image_hash, "extending RTMR3"); let client = dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); @@ -616,7 +594,10 @@ async fn extend_rtmr3(platform: Platform, valid_hash: &str) -> Result<()> { // EmitEvent with the image digest client - .emit_event("mpc-image-digest".to_string(), bare.into_bytes()) + .emit_event( + "mpc-image-digest".to_string(), + image_hash.as_hex().into_bytes(), + ) .await .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; From 795576ab88dada49117b92fab5ea9c487af70b61 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 3 Mar 2026 16:30:52 +0100 Subject: [PATCH 010/176] wip --- Cargo.lock | 1 + crates/launcher-interface/Cargo.toml | 1 + crates/launcher-interface/src/lib.rs | 10 +- crates/tee-launcher/src/error.rs | 24 +++- crates/tee-launcher/src/main.rs | 183 +++++++++------------------ crates/tee-launcher/src/types.rs | 2 +- 6 files changed, 89 insertions(+), 132 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a7c74e242..dc3bae887 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4929,6 +4929,7 @@ dependencies = [ name = "launcher-interface" version = "3.5.1" dependencies = [ + "bounded-collections", "mpc-primitives", "serde", ] diff --git a/crates/launcher-interface/Cargo.toml b/crates/launcher-interface/Cargo.toml index b7cb17847..0da73cbaa 100644 --- a/crates/launcher-interface/Cargo.toml +++ b/crates/launcher-interface/Cargo.toml @@ -5,6 +5,7 @@ edition.workspace = true license.workspace = true [dependencies] +bounded-collections = { workspace = true } mpc-primitives = { workspace = true } serde = { workspace = true } diff --git a/crates/launcher-interface/src/lib.rs b/crates/launcher-interface/src/lib.rs index dd0601a0c..6e952ca4c 100644 --- a/crates/launcher-interface/src/lib.rs +++ b/crates/launcher-interface/src/lib.rs @@ -5,8 +5,16 @@ pub mod types { /// JSON structure for the approved hashes file written by the MPC node. #[derive(Debug, Serialize, Deserialize)] pub struct ApprovedHashesFile { - pub approved_hashes: Vec, + pub approved_hashes: bounded_collections::NonEmptyVec, + } + + impl ApprovedHashesFile { + pub fn newest_approved_hash(&self) -> &MpcDockerImageHash { + self.approved_hashes.first() + } } } +// TODO: add insta snapshot test for this type + mod paths {} diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 2c67af0a4..720f89cf0 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -32,12 +32,6 @@ pub enum LauncherError { #[error("Failed to get successful response from {url} after {attempts} attempts")] RegistryRequestFailed { url: String, attempts: u32 }, - #[error("docker pull failed for {0}")] - DockerPullFailed(String), - - #[error("docker inspect failed for {0}")] - DockerInspectFailed(String), - #[error("Digest mismatch: pulled {pulled} != expected {expected}")] DigestMismatch { pulled: String, expected: String }, @@ -79,6 +73,22 @@ pub enum LauncherError { #[error("Registry response parse error: {0}")] RegistryResponseParse(String), + + #[error("The selected image failed digest validation: {0}")] + ImageDigestValidationFailed(#[from] ImageDigestValidationFailed), } -pub type Result = std::result::Result; +#[derive(Error, Debug)] +pub enum ImageDigestValidationFailed { + #[error("docker pull failed for {0}")] + DockerPullFailed(String), + #[error("docker inspect failed for {0}")] + DockerInspectFailed(String), + #[error( + "pulled image has mismatching digest. pulled: {pulled_digest}, expected: {expected_digest}" + )] + PulledImageHasMismatchedDigest { + expected_digest: String, + pulled_digest: String, + }, +} diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index d609bdfa4..aed65da78 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -33,20 +33,13 @@ async fn main() { } } -async fn run() -> Result<()> { +async fn run() -> Result<(), LauncherError> { tracing::info!("start"); let args = CliArgs::parse(); tracing::info!(platform = ?args.platform, "starting launcher"); - // TODO is_unix_socket can be a compile time check - if args.platform == Platform::Tee && !is_unix_socket(DSTACK_UNIX_SOCKET) { - return Err(LauncherError::DstackSocketMissing( - DSTACK_UNIX_SOCKET.to_string(), - )); - } - // Load dstack user config let config_file = std::fs::OpenOptions::new() .read(true) @@ -55,15 +48,36 @@ async fn run() -> Result<()> { let dstack_config: Config = serde_json::from_reader(config_file).expect("config file is valid"); - let selected_hash = load_and_select_hash(&args, &dstack_config)?; + let image_hash: MpcDockerImageHash = { + match dstack_config.launcher_config.mpc_hash_override.clone() { + Some(override_hash) => override_hash, + None => { + let approved_hashes_file = std::fs::OpenOptions::new() + .read(true) + .open(IMAGE_DIGEST_FILE) + .map_err(|source| LauncherError::FileRead { + path: IMAGE_DIGEST_FILE.to_string(), + source, + })?; + + let approved_hashes_on_disk: ApprovedHashesFile = + serde_json::from_reader(approved_hashes_file).map_err(|source| { + LauncherError::JsonParse { + path: IMAGE_DIGEST_FILE.to_string(), + source, + } + })?; - if !validate_image_hash(&selected_hash, &dstack_config, &dstack_config).await? { - return Err(LauncherError::ImageValidationFailed(selected_hash)); - } + approved_hashes_on_disk.newest_approved_hash().clone() + } + } + }; - tracing::info!("MPC image hash validated successfully: {selected_hash}"); + let () = check_image_digest_exists_on_docker_hub(image_hash)?; - extend_rtmr3(args.platform, &selected_hash).await?; + if args.platform == Platform::Tee { + extend_rtmr3(&image_hash).await?; + } launch_mpc_container( args.platform, @@ -163,71 +177,6 @@ fn is_safe_port_mapping(mapping: &str) -> bool { !INVALID_HOST_ENTRY_PATTERN.is_match(mapping) } -// --------------------------------------------------------------------------- -// Hash selection -// --------------------------------------------------------------------------- - -fn load_and_select_hash(args: &CliArgs, dstack_config: &Config) -> Result { - let approved_hashes = if std::path::Path::new(IMAGE_DIGEST_FILE).is_file() { - let content = std::fs::read_to_string(IMAGE_DIGEST_FILE).map_err(|source| { - LauncherError::FileRead { - path: IMAGE_DIGEST_FILE.to_string(), - source, - } - })?; - let data: ApprovxedHashesFile = - serde_json::from_str(&content).map_err(|source| LauncherError::JsonParse { - path: IMAGE_DIGEST_FILE.to_string(), - source, - })?; - if data.approved_hashes.is_empty() { - return Err(LauncherError::InvalidApprovedHashes { - path: IMAGE_DIGEST_FILE.to_string(), - }); - } - data.approved_hashes - } else { - let fallback_image = (&args) - .default_image_digest - .clone() - .ok_or_else(|| LauncherError::MissingEnvVar("DEFAULT_IMAGE_DIGEST".to_string()))?; - - tracing::info!( - ?IMAGE_DIGEST_FILE, - ?fallback_image, - "image digest file missing, will use fall back image" - ); - - vec![fallback_image] - }; - - tracing::info!("Approved MPC image hashes (newest → oldest):"); - for h in &approved_hashes { - // TODO: Fix this output... - // tracing::info!(" - {h}"); - } - - // Optional override - // if let Some(override_hash) = dstack_config.get(ENV_VAR_MPC_HASH_OVERRIDE) { - // if !is_valid_sha256_digest(override_hash) { - // return Err(LauncherError::InvalidHashOverride(override_hash.clone())); - // } - // if !approved_hashes.contains(override_hash) { - // tracing::error!("MPC_HASH_OVERRIDE={override_hash} does NOT match any approved hash!"); - // return Err(LauncherError::InvalidHashOverride(override_hash.clone())); - // } - // tracing::info!("MPC_HASH_OVERRIDE provided → selecting: {override_hash}"); - // return Ok(override_hash.clone()); - // } - - // // No override → select newest (first in list) - // let selected = approved_hashes[0].clone(); - // tracing::info!("Selected MPC hash (newest allowed): {selected}"); - // Ok(selected) - - todo!() -} - // --------------------------------------------------------------------------- // Docker registry communication // --------------------------------------------------------------------------- @@ -381,24 +330,22 @@ async fn get_manifest_digest(config: &LauncherConfig) -> Result { Err(LauncherError::ImageHashNotFoundAmongTags) } -async fn validate_image_hash( - image_digest: &str, - dstack_config: &Config, - config: &Config, -) -> Result { - tracing::info!("Validating MPC hash: {image_digest}"); - - let manifest_digest = get_manifest_digest(&config.launcher_config).await?; - let name_and_digest = format!("{}@{manifest_digest}", config.launcher_config.image_name); +fn check_image_digest_exists_on_docker_hub( + image_hash: MpcDockerImageHash, +) -> Result<(), ImageDigestValidationFailed> { + let image_hash_name = format!("sha256:{}", image_hash.as_hex()); // Pull let pull = Command::new("docker") - .args(["pull", &name_and_digest]) + .args(["pull", &image_hash_name]) .output() - .map_err(|e| LauncherError::DockerPullFailed(e.to_string()))?; - if !pull.status.success() { - tracing::error!("docker pull failed for {image_digest}"); - return Ok(false); + .map_err(|e| ImageDigestValidationFailed::DockerPullFailed(e.to_string()))?; + + let pull_failed = !pull.status.success(); + if pull_failed { + return Err(ImageDigestValidationFailed::DockerPullFailed( + "docker pull terminated with unsuccessful status".to_string(), + )); } // Verify digest @@ -408,23 +355,29 @@ async fn validate_image_hash( "inspect", "--format", "{{index .ID}}", - &name_and_digest, + &image_hash_name, ]) .output() - .map_err(|e| LauncherError::DockerInspectFailed(e.to_string()))?; - if !inspect.status.success() { - tracing::error!("docker inspect failed for {image_digest}"); - return Ok(false); + .map_err(|e| ImageDigestValidationFailed::DockerInspectFailed(e.to_string()))?; + + let docker_inspect_failed = !inspect.status.success(); + if docker_inspect_failed { + return Err(ImageDigestValidationFailed::DockerPullFailed( + "docker inspect terminated with unsuccessful status".to_string(), + )); } let pulled_digest = String::from_utf8_lossy(&inspect.stdout).trim().to_string(); - if pulled_digest != image_digest { - tracing::error!("digest mismatch: {pulled_digest} != {image_digest}"); - return Ok(false); + if pulled_digest != image_hash_name { + return Err( + ImageDigestValidationFailed::PulledImageHasMismatchedDigest { + pulled_digest, + expected_digest: image_hash_name, + }, + ); } - tracing::info!("MPC hash {image_digest} validated successfully."); - Ok(true) + Ok(()) } // --------------------------------------------------------------------------- @@ -565,35 +518,19 @@ fn launch_mpc_container( Ok(()) } -// TODO: We should kill this check. It's called with the constant `DSTACK_UNIX_SOCKET` -fn is_unix_socket(path: &str) -> bool { - std::fs::metadata(path).is_ok_and(|meta| meta.file_type().is_socket()) -} - -async fn extend_rtmr3(platform: Platform, image_hash: MpcDockerImageHash) -> Result<()> { - if platform == Platform::NonTee { - tracing::info!("PLATFORM=NONTEE → skipping RTMR3 extension step."); - return Ok(()); - } - - if !is_unix_socket(DSTACK_UNIX_SOCKET) { - return Err(LauncherError::DstackSocketMissing( - DSTACK_UNIX_SOCKET.to_string(), - )); - } - +async fn extend_rtmr3(image_hash: &MpcDockerImageHash) -> Result<(), LauncherError> { tracing::info!(?image_hash, "extending RTMR3"); - let client = dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); + let dstack_cient = dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); // GetQuote first - client + dstack_cient .get_quote(vec![]) .await .map_err(|e| LauncherError::DstackGetQuoteFailed(e.to_string()))?; // EmitEvent with the image digest - client + dstack_cient .emit_event( "mpc-image-digest".to_string(), image_hash.as_hex().into_bytes(), diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 6339920bd..20dd7c33a 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -60,7 +60,7 @@ pub struct LauncherConfig { /// Maximum registry RPC attempts (from `RPC_MAX_ATTEMPTS`). pub rpc_max_attempts: u32, /// Optional hash override that bypasses registry lookup (from `MPC_HASH_OVERRIDE`). - pub mpc_hash_override: Option, + pub mpc_hash_override: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] From c1ca0b2ac7fac3df74b60fe5d175a0c8868d06ed Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 3 Mar 2026 23:28:57 +0100 Subject: [PATCH 011/176] wip --- crates/tee-launcher/src/main.rs | 44 ++++++++++++--------------------- flake.nix | 29 +++++++++++----------- 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index aed65da78..4c13a588f 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -75,13 +75,24 @@ async fn run() -> Result<(), LauncherError> { let () = check_image_digest_exists_on_docker_hub(image_hash)?; - if args.platform == Platform::Tee { - extend_rtmr3(&image_hash).await?; + let should_extend_rtmr_3 = args.platform == Platform::Tee; + + if should_extend_rtmr_3 { + let dstack_cient = dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); + + // EmitEvent with the image digest + dstack_cient + .emit_event( + "mpc-image-digest".to_string(), + image_hash.as_hex().into_bytes(), + ) + .await + .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; } launch_mpc_container( args.platform, - &selected_hash, + &image_hash, &dstack_config.mpc_passthrough_env, )?; @@ -410,7 +421,7 @@ fn build_docker_cmd( platform: Platform, mpc_config: &MpcBinaryConfig, image_digest: &MpcDockerImageHash, -) -> Result> { +) -> Result, LauncherError> { let mut cmd: Vec = vec!["docker".into(), "run".into()]; // Required environment variables @@ -496,7 +507,7 @@ fn launch_mpc_container( platform: Platform, valid_hash: &MpcDockerImageHash, mpc_config: &MpcBinaryConfig, -) -> Result<()> { +) -> Result<(), LauncherError> { tracing::info!( "Launching MPC node with validated hash: {}", valid_hash.as_hex() @@ -518,29 +529,6 @@ fn launch_mpc_container( Ok(()) } -async fn extend_rtmr3(image_hash: &MpcDockerImageHash) -> Result<(), LauncherError> { - tracing::info!(?image_hash, "extending RTMR3"); - - let dstack_cient = dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); - - // GetQuote first - dstack_cient - .get_quote(vec![]) - .await - .map_err(|e| LauncherError::DstackGetQuoteFailed(e.to_string()))?; - - // EmitEvent with the image digest - dstack_cient - .emit_event( - "mpc-image-digest".to_string(), - image_hash.as_hex().into_bytes(), - ) - .await - .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; - - Ok(()) -} - // #[cfg(test)] // mod tests { // use super::*; diff --git a/flake.nix b/flake.nix index 4ddefe07b..6a3c51ef0 100644 --- a/flake.nix +++ b/flake.nix @@ -103,20 +103,9 @@ }; envDarwin = lib.optionalAttrs stdenv.isDarwin { - # Force build scripts to use Nix wrappers (not host clang) - CC = "${stdenv.cc}/bin/cc"; - CXX = "${stdenv.cc}/bin/c++"; - - # cc crate looks for these first on macOS - CC_aarch64_apple_darwin = "${stdenv.cc}/bin/cc"; - CXX_aarch64_apple_darwin = "${stdenv.cc}/bin/c++"; - - AR = "${stdenv.cc.bintools}/bin/ar"; - RANLIB = "${stdenv.cc.bintools}/bin/ranlib"; - - # Cargo resolves its linker separately from CC — force it to use the - # SDK-aware wrapper so -lSystem (and other SDK libs) are found. - CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER = "${stdenv.cc}/bin/cc"; + # Cargo resolves its linker separately from CC — force it to use + # the LLVM 19 wrapper so -lSystem (and other SDK libs) are found. + CARGO_TARGET_AARCH64_APPLE_DARWIN_LINKER = "${llvmPkgs.clang}/bin/clang"; }; dockerTools = with pkgs; [ @@ -206,6 +195,18 @@ hardeningDisable = hardening; shellHook = '' + ${lib.optionalString stdenv.isDarwin '' + # Override CC/CXX to use LLVM 19 clang, matching Rust 1.86's + # bundled LLVM version. These must live in shellHook because + # rust-overlay propagates the default stdenv clang (now 21) + # via setup hooks that run after env vars are set. + export CC="${llvmPkgs.clang}/bin/clang" + export CXX="${llvmPkgs.clang}/bin/clang++" + export CC_aarch64_apple_darwin="${llvmPkgs.clang}/bin/clang" + export CXX_aarch64_apple_darwin="${llvmPkgs.clang}/bin/clang++" + export AR="${llvmPkgs.llvm}/bin/llvm-ar" + export RANLIB="${llvmPkgs.llvm}/bin/llvm-ranlib" + ''} printf "\e[32m🦀 NEAR Dev Shell Active\e[0m\n" ''; }; From d2ddedc1a4eea8bb0452369d37ac2e8126910cd9 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Wed, 4 Mar 2026 08:41:16 +0100 Subject: [PATCH 012/176] use const MPC_IMAGE_HASH_EVENT --- Cargo.lock | 1 + crates/launcher-interface/src/lib.rs | 2 ++ crates/mpc-attestation/Cargo.toml | 1 + crates/mpc-attestation/src/attestation.rs | 3 +-- crates/tee-launcher/src/main.rs | 11 ++++++----- 5 files changed, 11 insertions(+), 7 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index dc3bae887..d6949ffc4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5382,6 +5382,7 @@ dependencies = [ "derive_more 2.1.1", "hex", "include-measurements", + "launcher-interface", "mpc-primitives", "serde", "serde_json", diff --git a/crates/launcher-interface/src/lib.rs b/crates/launcher-interface/src/lib.rs index 6e952ca4c..a4dd7049d 100644 --- a/crates/launcher-interface/src/lib.rs +++ b/crates/launcher-interface/src/lib.rs @@ -1,3 +1,5 @@ +pub const MPC_IMAGE_HASH_EVENT: &str = "mpc-image-digest"; + pub mod types { use mpc_primitives::hash::MpcDockerImageHash; use serde::{Deserialize, Serialize}; diff --git a/crates/mpc-attestation/Cargo.toml b/crates/mpc-attestation/Cargo.toml index f96b1fd79..b16ef09fc 100644 --- a/crates/mpc-attestation/Cargo.toml +++ b/crates/mpc-attestation/Cargo.toml @@ -15,6 +15,7 @@ serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } sha3 = { workspace = true } +launcher-interface = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/crates/mpc-attestation/src/attestation.rs b/crates/mpc-attestation/src/attestation.rs index ea2ec0641..da7ea2920 100644 --- a/crates/mpc-attestation/src/attestation.rs +++ b/crates/mpc-attestation/src/attestation.rs @@ -13,14 +13,13 @@ pub use attestation::attestation::{DstackAttestation, VerificationError}; use mpc_primitives::hash::{LauncherDockerComposeHash, MpcDockerImageHash}; use borsh::{BorshDeserialize, BorshSerialize}; +use launcher_interface::MPC_IMAGE_HASH_EVENT; use serde::{Deserialize, Serialize}; use sha2::{Digest as _, Sha256}; use crate::alloc::format; use crate::alloc::string::ToString; -const MPC_IMAGE_HASH_EVENT: &str = "mpc-image-digest"; - // TODO(#1639): extract timestamp from certificate itself pub const DEFAULT_EXPIRATION_DURATION_SECONDS: u64 = 60 * 60 * 24 * 7; // 7 days diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 4c13a588f..ebc1516b1 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -3,6 +3,7 @@ use std::process::Command; use std::sync::LazyLock; use clap::Parser; +use launcher_interface::MPC_IMAGE_HASH_EVENT; use launcher_interface::types::ApprovedHashesFile; use regex::Regex; use std::os::unix::fs::FileTypeExt as _; @@ -73,18 +74,18 @@ async fn run() -> Result<(), LauncherError> { } }; - let () = check_image_digest_exists_on_docker_hub(image_hash)?; + let () = check_image_digest_exists_on_docker_hub(image_hash.clone())?; let should_extend_rtmr_3 = args.platform == Platform::Tee; if should_extend_rtmr_3 { - let dstack_cient = dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); + let dstack_client = dstack_sdk::dstack_client::DstackClient::new(Some(DSTACK_UNIX_SOCKET)); // EmitEvent with the image digest - dstack_cient + dstack_client .emit_event( - "mpc-image-digest".to_string(), - image_hash.as_hex().into_bytes(), + MPC_IMAGE_HASH_EVENT.to_string(), + image_hash.as_hex().as_bytes().to_vec(), ) .await .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; From 391be027b201b3ea46c552adc0a7ee38d0db61c4 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Wed, 4 Mar 2026 09:45:59 +0100 Subject: [PATCH 013/176] add deref to bounded vec --- crates/bounded-collections/src/bounded_vec.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/crates/bounded-collections/src/bounded_vec.rs b/crates/bounded-collections/src/bounded_vec.rs index 1fc79ebf8..965200171 100644 --- a/crates/bounded-collections/src/bounded_vec.rs +++ b/crates/bounded-collections/src/bounded_vec.rs @@ -1,5 +1,6 @@ use std::{ convert::{TryFrom, TryInto}, + ops::Deref, slice::{Iter, IterMut}, vec, }; @@ -20,6 +21,14 @@ pub struct BoundedVec Deref for BoundedVec { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.inner + } +} + /// BoundedVec errors #[derive(Error, PartialEq, Eq, Debug, Clone)] pub enum BoundedVecOutOfBounds { From 1253c03562b8dfa5ed120fe285e01e8f867bf082 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Wed, 4 Mar 2026 11:16:28 +0100 Subject: [PATCH 014/176] compiles --- Cargo.lock | 2 + crates/tee-launcher/Cargo.toml | 2 + crates/tee-launcher/src/main.rs | 178 ++++++++++--------------------- crates/tee-launcher/src/types.rs | 127 +++++++++++++++++++--- tee_launcher/launcher.py | 2 +- 5 files changed, 174 insertions(+), 137 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d6949ffc4..f3283e62c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10558,6 +10558,7 @@ dependencies = [ "clap", "dstack-sdk", "hex", + "itertools 0.14.0", "launcher-interface", "mpc-primitives", "regex", @@ -10569,6 +10570,7 @@ dependencies = [ "tokio", "tracing", "tracing-subscriber", + "url", ] [[package]] diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index f983a8ad1..0d4fb45ba 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -18,6 +18,7 @@ dstack-sdk = { workspace = true } hex = { workspace = true } mpc-primitives = { workspace = true } launcher-interface = { workspace = true } +itertools = { workspace = true } regex = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } @@ -26,6 +27,7 @@ thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } +url = { workspace = true, features = ["serde"] } [dev-dependencies] assert_matches = { workspace = true } diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index ebc1516b1..0d03e4ec5 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -1,12 +1,9 @@ -use std::collections::{BTreeMap, VecDeque}; +use std::collections::VecDeque; use std::process::Command; -use std::sync::LazyLock; use clap::Parser; use launcher_interface::MPC_IMAGE_HASH_EVENT; use launcher_interface::types::ApprovedHashesFile; -use regex::Regex; -use std::os::unix::fs::FileTypeExt as _; // Reuse the workspace hash type for type-safe image hash handling. use mpc_primitives::hash::MpcDockerImageHash; @@ -49,18 +46,26 @@ async fn run() -> Result<(), LauncherError> { let dstack_config: Config = serde_json::from_reader(config_file).expect("config file is valid"); - let image_hash: MpcDockerImageHash = { - match dstack_config.launcher_config.mpc_hash_override.clone() { - Some(override_hash) => override_hash, - None => { - let approved_hashes_file = std::fs::OpenOptions::new() - .read(true) - .open(IMAGE_DIGEST_FILE) - .map_err(|source| LauncherError::FileRead { - path: IMAGE_DIGEST_FILE.to_string(), - source, - })?; + let approved_hashes_file = std::fs::OpenOptions::new() + .read(true) + .open(IMAGE_DIGEST_FILE) + .map_err(|source| LauncherError::FileRead { + path: IMAGE_DIGEST_FILE.to_string(), + source, + }); + let image_hash: MpcDockerImageHash = { + match approved_hashes_file { + Err(err) => { + let default_image_digest = args.default_image_digest; + tracing::warn!( + ?err, + ?default_image_digest, + "approved hashes file does not exist on disk, falling back to default digest" + ); + default_image_digest + } + Ok(approved_hashes_file) => { let approved_hashes_on_disk: ApprovedHashesFile = serde_json::from_reader(approved_hashes_file).map_err(|source| { LauncherError::JsonParse { @@ -69,7 +74,21 @@ async fn run() -> Result<(), LauncherError> { } })?; - approved_hashes_on_disk.newest_approved_hash().clone() + if let Some(override_image) = dstack_config.launcher_config.mpc_hash_override { + tracing::info!(?override_image, "override mpc image hash provided"); + + let override_image_is_allowed = approved_hashes_on_disk + .approved_hashes + .contains(&override_image); + + if !override_image_is_allowed { + panic!("TODO: panic if override image is not allowed?"); + } + + override_image + } else { + approved_hashes_on_disk.newest_approved_hash().clone() + } } } }; @@ -85,6 +104,7 @@ async fn run() -> Result<(), LauncherError> { dstack_client .emit_event( MPC_IMAGE_HASH_EVENT.to_string(), + // TODO: mpc binary has to go back from back hex as well. Just send the raw bytes as payload. image_hash.as_hex().as_bytes().to_vec(), ) .await @@ -95,47 +115,13 @@ async fn run() -> Result<(), LauncherError> { args.platform, &image_hash, &dstack_config.mpc_passthrough_env, + &dstack_config.docker_command_config, )?; Ok(()) } -// --------------------------------------------------------------------------- -// Constants — matching Python launcher exactly -// --------------------------------------------------------------------------- - -// Regex patterns (compiled once) -static MPC_ENV_KEY_RE: LazyLock = - LazyLock::new(|| Regex::new(r"^MPC_[A-Z0-9_]{1,64}$").unwrap()); -static HOST_ENTRY_RE: LazyLock = - LazyLock::new(|| Regex::new(r"^[a-zA-Z0-9\-\.]+:\d{1,3}(\.\d{1,3}){3}$").unwrap()); -static PORT_MAPPING_RE: LazyLock = - LazyLock::new(|| Regex::new(r"^(\d{1,5}):(\d{1,5})$").unwrap()); -static INVALID_HOST_ENTRY_PATTERN: LazyLock = - LazyLock::new(|| Regex::new(r"^[;&|`$\\<>\-]|^--").unwrap()); - -// Denied env keys — never pass these to the container -const DENIED_CONTAINER_ENV_KEYS: &[&str] = &["MPC_P2P_PRIVATE_KEY", "MPC_ACCOUNT_SK"]; - -// Allowed non-MPC env vars (backward compatibility) -const ALLOWED_MPC_ENV_VARS: &[&str] = &[ - "MPC_ACCOUNT_ID", - "MPC_LOCAL_ADDRESS", - "MPC_SECRET_STORE_KEY", - "MPC_CONTRACT_ID", - "MPC_ENV", - "MPC_HOME_DIR", - "NEAR_BOOT_NODES", - "RUST_BACKTRACE", - "RUST_LOG", - "MPC_RESPONDER_ID", - "MPC_BACKUP_ENCRYPTION_KEY_HEX", -]; - -// --------------------------------------------------------------------------- -// Validation functions — security policy for env passthrough -// --------------------------------------------------------------------------- - +// TODO: this needs to be checked. fn has_control_chars(s: &str) -> bool { let control_chars = ['\n', '\r', '\0']; @@ -150,45 +136,6 @@ fn has_control_chars(s: &str) -> bool { false } -fn is_valid_ip(ip: &str) -> bool { - ip.parse::().is_ok() -} - -fn is_valid_host_entry(entry: &str) -> bool { - if !HOST_ENTRY_RE.is_match(entry) { - return false; - } - if let Some((_host, ip)) = entry.rsplit_once(':') { - is_valid_ip(ip) - } else { - false - } -} - -fn is_valid_port_mapping(entry: &str) -> bool { - if let Some(caps) = PORT_MAPPING_RE.captures(entry) { - let host_port: u32 = caps[1].parse().unwrap_or(0); - let container_port: u32 = caps[2].parse().unwrap_or(0); - host_port > 0 && host_port <= 65535 && container_port > 0 && container_port <= 65535 - } else { - false - } -} - -fn is_safe_host_entry(entry: &str) -> bool { - if INVALID_HOST_ENTRY_PATTERN.is_match(entry) { - return false; - } - if entry.contains("LD_PRELOAD") { - return false; - } - true -} - -fn is_safe_port_mapping(mapping: &str) -> bool { - !INVALID_HOST_ENTRY_PATTERN.is_match(mapping) -} - // --------------------------------------------------------------------------- // Docker registry communication // --------------------------------------------------------------------------- @@ -199,7 +146,7 @@ async fn request_until_success( url: &str, headers: &[(String, String)], config: &LauncherConfig, -) -> Result { +) -> Result { let mut interval = config.rpc_request_interval_secs as f64; for attempt in 1..=config.rpc_max_attempts { @@ -244,7 +191,7 @@ async fn request_until_success( }) } -async fn get_manifest_digest(config: &LauncherConfig) -> Result { +async fn get_manifest_digest(config: &LauncherConfig) -> Result { let tags = config.image_tags.clone(); let token_url = format!( @@ -421,6 +368,7 @@ fn remove_existing_container() { fn build_docker_cmd( platform: Platform, mpc_config: &MpcBinaryConfig, + docker_flags: &DockerLaunchFlags, image_digest: &MpcDockerImageHash, ) -> Result, LauncherError> { let mut cmd: Vec = vec!["docker".into(), "run".into()]; @@ -446,36 +394,17 @@ fn build_docker_cmd( ]); } - // Track env passthrough size/caps - let mut passed_env_count: usize = 0; - let mut total_env_bytes: usize = 0; - - // // BTreeMap iteration is already sorted by key (deterministic) - // for (key, value) in mpc_config { - // if key == "EXTRA_HOSTS" { - // for host_entry in value.split(',') { - // let clean = host_entry.trim(); - // if is_safe_host_entry(clean) && is_valid_host_entry(clean) { - // cmd.extend(["--add-host".into(), clean.to_string()]); - // } else { - // tracing::warn!("Ignoring invalid or unsafe EXTRA_HOSTS entry: {clean}"); - // } - // } - // continue; - // } - - // passed_env_count += 1; - // if passed_env_count > MAX_PASSTHROUGH_ENV_VARS { - // return Err(LauncherError::TooManyEnvVars(MAX_PASSTHROUGH_ENV_VARS)); - // } - - // total_env_bytes += key.len() + 1 + value.len(); - // if total_env_bytes > MAX_TOTAL_ENV_BYTES { - // return Err(LauncherError::EnvPayloadTooLarge(MAX_TOTAL_ENV_BYTES)); - // } - - // cmd.extend(["--env".into(), format!("{key}={value}")]); - // } + for (key, value) in mpc_config.env_vars() { + cmd.extend(["--env".into(), format!("{key}={value}")]); + } + + let (host_flag, host_value) = docker_flags.extra_hosts.docker_flag_and_value(); + cmd.extend([host_flag, host_value]); + + let (port_forwarding_flag, port_forwarding_value) = + docker_flags.port_mappings.docker_flag_and_value(); + + cmd.extend([port_forwarding_flag, port_forwarding_value]); // Container run configuration cmd.extend([ @@ -508,6 +437,7 @@ fn launch_mpc_container( platform: Platform, valid_hash: &MpcDockerImageHash, mpc_config: &MpcBinaryConfig, + docker_flags: &DockerLaunchFlags, ) -> Result<(), LauncherError> { tracing::info!( "Launching MPC node with validated hash: {}", @@ -515,7 +445,7 @@ fn launch_mpc_container( ); remove_existing_container(); - let docker_cmd = build_docker_cmd(platform, mpc_config, valid_hash)?; + let docker_cmd = build_docker_cmd(platform, mpc_config, docker_flags, valid_hash)?; let status = Command::new(&docker_cmd[0]) .args(&docker_cmd[1..]) diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 20dd7c33a..daa87ba1c 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -1,3 +1,10 @@ +use std::fmt; +use std::net::{IpAddr, Ipv4Addr}; +use std::num::NonZeroU16; +use std::path::PathBuf; + +use url::Host; + use bounded_collections::NonEmptyVec; use clap::{Parser, ValueEnum}; use mpc_primitives::hash::MpcDockerImageHash; @@ -17,7 +24,7 @@ pub struct CliArgs { /// Fallback image digest when the approved-hashes file is absent #[arg(long, env = "DEFAULT_IMAGE_DIGEST")] - pub default_image_digest: Option, + pub default_image_digest: MpcDockerImageHash, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] @@ -41,6 +48,7 @@ pub enum Platform { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub launcher_config: LauncherConfig, + pub docker_command_config: DockerLaunchFlags, /// Remaining env vars forwarded to the MPC container. pub mpc_passthrough_env: MpcBinaryConfig, } @@ -66,17 +74,112 @@ pub struct LauncherConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MpcBinaryConfig { // mpc - mpc_account_id: String, - mpc_local_address: String, - mpc_secret_key_store: String, - mpc_contract_isd: String, - mpc_env: String, - mpc_home_dir: String, - mpc_responder_id: String, - mpc_backup_encryption_key_hex: String, + pub mpc_account_id: String, + pub mpc_local_address: IpAddr, + pub mpc_secret_key_store: String, + pub mpc_contract_isd: String, + pub mpc_env: MpcEnv, + pub mpc_home_dir: PathBuf, + pub mpc_responder_id: String, + pub mpc_backup_encryption_key_hex: String, // near - near_boot_nodes: String, + pub near_boot_nodes: String, // rust - rust_backtrace: String, - rust_log: String, + pub rust_backtrace: String, + pub rust_log: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DockerLaunchFlags { + pub extra_hosts: ExtraHosts, + pub port_mappings: PortMappings, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct ExtraHosts { + hosts: Vec, +} + +impl ExtraHosts { + pub fn docker_flag_and_value(&self) -> (String, String) { + let flag = "--add-host".into(); + let value = self + .hosts + .iter() + .map(|HostEntry { hostname, ip }| format!("{hostname}:{ip}")) + .collect::>() + .join(","); + + (flag, value) + } +} + +/// A `--add-host` entry: `hostname:IPv4`. +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct HostEntry { + pub hostname: Host, + pub ip: Ipv4Addr, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub(crate) struct PortMappings { + pub ports: Vec, +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub struct PortMapping { + src: NonZeroU16, + dst: NonZeroU16, +} + +impl PortMappings { + pub fn docker_flag_and_value(&self) -> (String, String) { + let flag = "-p".into(); + let value = self + .ports + .iter() + .map(|PortMapping { src, dst }| format!("{src}:{dst}")) + .collect::>() + .join(","); + + (flag, value) + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +enum MpcEnv { + Localnet, + Testnet, + Mainnet, +} + +impl fmt::Display for MpcEnv { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + MpcEnv::Localnet => write!(f, "localnet"), + MpcEnv::Testnet => write!(f, "testnet"), + MpcEnv::Mainnet => write!(f, "mainnet"), + } + } +} + +impl MpcBinaryConfig { + pub fn env_vars(&self) -> Vec<(&'static str, String)> { + vec![ + ("MPC_ACCOUNT_ID", self.mpc_account_id.clone()), + ("MPC_LOCAL_ADDRESS", self.mpc_local_address.to_string()), + ("MPC_SECRET_STORE_KEY", self.mpc_secret_key_store.clone()), + ("MPC_CONTRACT_ID", self.mpc_contract_isd.clone()), + ("MPC_ENV", self.mpc_env.to_string()), + ("MPC_HOME_DIR", self.mpc_home_dir.display().to_string()), + ("MPC_RESPONDER_ID", self.mpc_responder_id.clone()), + ( + "MPC_BACKUP_ENCRYPTION_KEY_HEX", + self.mpc_backup_encryption_key_hex.clone(), + ), + ("NEAR_BOOT_NODES", self.near_boot_nodes.clone()), + ("RUST_BACKTRACE", self.rust_backtrace.clone()), + ("RUST_LOG", self.rust_log.clone()), + ] + } } diff --git a/tee_launcher/launcher.py b/tee_launcher/launcher.py index 73ae00cf4..17f049faf 100644 --- a/tee_launcher/launcher.py +++ b/tee_launcher/launcher.py @@ -147,7 +147,7 @@ class Platform(Enum): ALLOWED_MPC_ENV_VARS = { "MPC_ACCOUNT_ID", # ID of the MPC account on the network "MPC_LOCAL_ADDRESS", # Local IP address or hostname used by the MPC node - "MPC_SECRET_STORE_KEY", # Key used to encrypt/decrypt secrets + "MPC_SECRET_STORE_KEY", # Key used to encrypt/decrypt secrets // Isn't this deprecated?, "MPC_CONTRACT_ID", # Contract ID associated with the MPC node "MPC_ENV", # Environment (e.g., 'testnet', 'mainnet') "MPC_HOME_DIR", # Home directory for the MPC node From 9fe5b9a72c8a26e94bdbde9026b52e440137f8b2 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Wed, 4 Mar 2026 21:17:36 +0100 Subject: [PATCH 015/176] wip --- Cargo.toml | 6 +- crates/tee-launcher/src/error.rs | 11 +- crates/tee-launcher/src/main.rs | 193 +++++++++++++++---------------- crates/tee-launcher/src/types.rs | 88 ++++++++++++-- tee_launcher/launcher.py | 1 + 5 files changed, 187 insertions(+), 112 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index aa47cba05..6b66d38c5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,7 +107,9 @@ either = "1.15.0" elliptic-curve = "0.13.8" ethereum-types = "0.16.0" flume = "0.12.0" -frost-core = { version = "2.2.0", default-features = false, features = ["serde"] } +frost-core = { version = "2.2.0", default-features = false, features = [ + "serde", +] } frost-ed25519 = { version = "2.2.0", default-features = false } frost-secp256k1 = { version = "2.2.0", default-features = false } fs2 = "0.4.3" @@ -188,7 +190,7 @@ tracing-subscriber = { version = "0.3.22", features = [ "json", ] } tracing-test = "0.2.6" -url = "2" +url = "2.5.8" x509-parser = "0.18.1" zeroize = { version = "1.8.2", features = ["zeroize_derive"] } zstd = "0.13.3" diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 720f89cf0..5d922ddb1 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -2,6 +2,7 @@ use std::path::PathBuf; use mpc_primitives::hash::MpcDockerImageHash; use thiserror::Error; +use url::Url; #[derive(Error, Debug)] pub enum LauncherError { @@ -30,7 +31,7 @@ pub enum LauncherError { RegistryAuthFailed(String), #[error("Failed to get successful response from {url} after {attempts} attempts")] - RegistryRequestFailed { url: String, attempts: u32 }, + RegistryRequestFailed { url: Url, attempts: u32 }, #[error("Digest mismatch: pulled {pulled} != expected {expected}")] DigestMismatch { pulled: String, expected: String }, @@ -39,7 +40,13 @@ pub enum LauncherError { ImageValidationFailed(String), #[error("docker run failed for validated hash")] - DockerRunFailed(MpcDockerImageHash), + DockerRunFailed { + image_hash: MpcDockerImageHash, + inner: std::io::Error, + }, + + #[error("docker run failed for validated hash")] + DockerRunFailedExitStatus { image_hash: MpcDockerImageHash }, #[error("Too many env vars to pass through (>{0})")] TooManyEnvVars(usize), diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 0d03e4ec5..9f4ecf027 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -1,5 +1,5 @@ -use std::collections::VecDeque; use std::process::Command; +use std::{collections::VecDeque, time::Duration}; use clap::Parser; use launcher_interface::MPC_IMAGE_HASH_EVENT; @@ -10,12 +10,17 @@ use mpc_primitives::hash::MpcDockerImageHash; use contants::*; use error::*; +use reqwest::header::{ACCEPT, AUTHORIZATION, HeaderMap, HeaderValue}; use types::*; +use url::Url; mod contants; mod error; mod types; +const DOCKER_AUTH_ACCEPT_HEADER_VALUE: HeaderValue = + HeaderValue::from_static("application/vnd.docker.distribution.manifest.v2+json"); + #[tokio::main] async fn main() { tracing_subscriber::fmt() @@ -140,85 +145,33 @@ fn has_control_chars(s: &str) -> bool { // Docker registry communication // --------------------------------------------------------------------------- -// TODO: Use backon -async fn request_until_success( - client: &reqwest::Client, - url: &str, - headers: &[(String, String)], - config: &LauncherConfig, -) -> Result { - let mut interval = config.rpc_request_interval_secs as f64; - - for attempt in 1..=config.rpc_max_attempts { - // Sleep before request (matching Python behavior) - tokio::time::sleep(std::time::Duration::from_secs_f64(interval)).await; - interval = (interval.max(1.0) * 1.5).min(60.0); - - let mut req = client.get(url); - for (k, v) in headers { - req = req.header(k.as_str(), v.as_str()); - } - - match req - .timeout(std::time::Duration::from_secs( - config.rpc_request_timeout_secs, - )) - .send() - .await - { - Err(e) => { - tracing::warn!( - "Attempt {attempt}/{}: Failed to fetch {url}. Status: Timeout/Error: {e}", - config.rpc_max_attempts - ); - continue; - } - Ok(resp) if resp.status() != reqwest::StatusCode::OK => { - tracing::warn!( - "Attempt {attempt}/{}: Failed to fetch {url}. Status: {}", - config.rpc_max_attempts, - resp.status() - ); - continue; - } - Ok(resp) => return Ok(resp), - } - } - - Err(LauncherError::RegistryRequestFailed { - url: url.to_string(), - attempts: config.rpc_max_attempts, - }) -} - async fn get_manifest_digest(config: &LauncherConfig) -> Result { let tags = config.image_tags.clone(); + // We need an authorization token to fetch manifests. + // TODO: this still has the registry hard-coded in the url. also, if we use a different registry, we need a different auth-endpoint let token_url = format!( "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{}:pull", config.image_name ); - let client = reqwest::Client::new(); - let token_resp = client - .get(&token_url) + let reqwest_client = reqwest::Client::new(); + + let token_request_response = reqwest_client + .get(token_url) .send() .await .map_err(|e| LauncherError::RegistryAuthFailed(e.to_string()))?; - if token_resp.status() != reqwest::StatusCode::OK { - return Err(LauncherError::RegistryAuthFailed(format!( - "status: {}", - token_resp.status() - ))); + + let status = token_request_response.status(); + if !status.is_success() { + todo!("add error case for non success http codes"); } - let token_json: serde_json::Value = token_resp + + let token_response: DockerTokenResponse = token_request_response .json() .await .map_err(|e| LauncherError::RegistryAuthFailed(e.to_string()))?; - let token = token_json["token"] - .as_str() - .ok_or_else(|| LauncherError::RegistryAuthFailed("no token in response".to_string()))? - .to_string(); let mut tags: VecDeque = tags.into_iter().collect(); @@ -232,10 +185,19 @@ async fn get_manifest_digest(config: &LauncherConfig) -> Result { let content_digest = resp .headers() @@ -289,6 +251,54 @@ async fn get_manifest_digest(config: &LauncherConfig) -> Result Result { + let mut interval = config.rpc_request_interval_secs as f64; + + for attempt in 1..=config.rpc_max_attempts { + // Sleep before request (matching Python behavior) + tokio::time::sleep(std::time::Duration::from_secs_f64(interval)).await; + interval = (interval.max(1.0) * 1.5).min(60.0); + + let request_timeout_duration = Duration::from_secs(config.rpc_request_timeout_secs); + let request = client + .get(url.clone()) + .headers(headers.clone()) + .timeout(request_timeout_duration) + .send() + .await; + + match request { + Err(e) => { + tracing::warn!( + "Attempt {attempt}/{}: Failed to fetch {url}. Status: Timeout/Error: {e}", + config.rpc_max_attempts + ); + continue; + } + Ok(resp) if resp.status() != reqwest::StatusCode::OK => { + tracing::warn!( + "Attempt {attempt}/{}: Failed to fetch {url}. Status: {}", + config.rpc_max_attempts, + resp.status() + ); + continue; + } + Ok(resp) => return Ok(resp), + } + } + + Err(LauncherError::RegistryRequestFailed { + url, + attempts: config.rpc_max_attempts, + }) +} + fn check_image_digest_exists_on_docker_hub( image_hash: MpcDockerImageHash, ) -> Result<(), ImageDigestValidationFailed> { @@ -339,32 +349,6 @@ fn check_image_digest_exists_on_docker_hub( Ok(()) } -// --------------------------------------------------------------------------- -// Docker command builder -// --------------------------------------------------------------------------- - -fn remove_existing_container() { - let output = Command::new("docker") - .args(["ps", "-a", "--format", "{{.Names}}"]) - .output(); - - match output { - Ok(output) => { - let names = String::from_utf8_lossy(&output.stdout); - - if names.lines().any(|n| n == MPC_CONTAINER_NAME) { - tracing::info!("Removing existing container: {MPC_CONTAINER_NAME}"); - let _ = Command::new("docker") - .args(["rm", "-f", MPC_CONTAINER_NAME]) - .output(); - } - } - Err(error) => { - tracing::warn!("Failed to check/remove container {MPC_CONTAINER_NAME}: {error}"); - } - } -} - fn build_docker_cmd( platform: Platform, mpc_config: &MpcBinaryConfig, @@ -444,16 +428,25 @@ fn launch_mpc_container( valid_hash.as_hex() ); - remove_existing_container(); + // shutdown container if one is already running + let _ = Command::new("docker") + .args(["rm", "-f", MPC_CONTAINER_NAME]) + .output(); + let docker_cmd = build_docker_cmd(platform, mpc_config, docker_flags, valid_hash)?; - let status = Command::new(&docker_cmd[0]) + let run_output = Command::new(&docker_cmd[0]) .args(&docker_cmd[1..]) - .status() - .map_err(|e| LauncherError::DockerRunFailed(valid_hash.clone()))?; - - if !status.success() { - return Err(LauncherError::DockerRunFailed(valid_hash.clone())); + .output() + .map_err(|inner| LauncherError::DockerRunFailed { + image_hash: valid_hash.clone(), + inner, + })?; + + if !run_output.status.success() { + return Err(LauncherError::DockerRunFailedExitStatus { + image_hash: valid_hash.clone(), + }); } tracing::info!("MPC launched successfully."); diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index daa87ba1c..e42d47fba 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -74,19 +74,24 @@ pub struct LauncherConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct MpcBinaryConfig { // mpc + // TODO: use near type to not accept any string pub mpc_account_id: String, pub mpc_local_address: IpAddr, + // TODO: think this is no longer needed with node generated keys pub mpc_secret_key_store: String, - pub mpc_contract_isd: String, + // TODO: think this is no longer needed with node generated keys + pub mpc_backup_encryption_key_hex: String, pub mpc_env: MpcEnv, pub mpc_home_dir: PathBuf, + // TODO: use near type to not accept any string + pub mpc_contract_id: String, + // TODO: use near type to not accept any string pub mpc_responder_id: String, - pub mpc_backup_encryption_key_hex: String, // near pub near_boot_nodes: String, // rust - pub rust_backtrace: String, - pub rust_log: String, + pub rust_backtrace: RustBacktrace, + pub rust_log: RustLog, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -147,7 +152,7 @@ impl PortMappings { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -enum MpcEnv { +pub enum MpcEnv { Localnet, Testnet, Mainnet, @@ -163,13 +168,74 @@ impl fmt::Display for MpcEnv { } } +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +pub enum RustBacktrace { + #[serde(rename = "0")] + Disabled, + #[serde(rename = "1")] + Enabled, + #[serde(rename = "short")] + Short, + #[serde(rename = "full")] + Full, +} + +impl fmt::Display for RustBacktrace { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RustBacktrace::Disabled => write!(f, "0"), + RustBacktrace::Enabled => write!(f, "1"), + RustBacktrace::Short => write!(f, "short"), + RustBacktrace::Full => write!(f, "full"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum RustLogLevel { + Error, + Warn, + Info, + Debug, + Trace, +} + +impl fmt::Display for RustLogLevel { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RustLogLevel::Error => write!(f, "error"), + RustLogLevel::Warn => write!(f, "warn"), + RustLogLevel::Info => write!(f, "info"), + RustLogLevel::Debug => write!(f, "debug"), + RustLogLevel::Trace => write!(f, "trace"), + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum RustLog { + Level(RustLogLevel), + Filter(String), +} + +impl fmt::Display for RustLog { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + RustLog::Level(level) => level.fmt(f), + RustLog::Filter(filter) => write!(f, "{filter}"), + } + } +} + impl MpcBinaryConfig { pub fn env_vars(&self) -> Vec<(&'static str, String)> { vec![ ("MPC_ACCOUNT_ID", self.mpc_account_id.clone()), ("MPC_LOCAL_ADDRESS", self.mpc_local_address.to_string()), ("MPC_SECRET_STORE_KEY", self.mpc_secret_key_store.clone()), - ("MPC_CONTRACT_ID", self.mpc_contract_isd.clone()), + ("MPC_CONTRACT_ID", self.mpc_contract_id.clone()), ("MPC_ENV", self.mpc_env.to_string()), ("MPC_HOME_DIR", self.mpc_home_dir.display().to_string()), ("MPC_RESPONDER_ID", self.mpc_responder_id.clone()), @@ -178,8 +244,14 @@ impl MpcBinaryConfig { self.mpc_backup_encryption_key_hex.clone(), ), ("NEAR_BOOT_NODES", self.near_boot_nodes.clone()), - ("RUST_BACKTRACE", self.rust_backtrace.clone()), - ("RUST_LOG", self.rust_log.clone()), + ("RUST_BACKTRACE", self.rust_backtrace.to_string()), + ("RUST_LOG", self.rust_log.to_string()), ] } } + +/// Partial response https://auth.docker.io/token +#[derive(Debug, Deserialize, Serialize)] +pub struct DockerTokenResponse { + pub token: String, +} diff --git a/tee_launcher/launcher.py b/tee_launcher/launcher.py index 17f049faf..e8beeb9c2 100644 --- a/tee_launcher/launcher.py +++ b/tee_launcher/launcher.py @@ -242,6 +242,7 @@ def is_safe_port_mapping(mapping: str) -> bool: def remove_existing_container(): + # changed in rust, no point checking current container exists. Just send shutdown signal to MPC_CONTAINER_NAME """Stop and remove the MPC container if it exists.""" try: containers = check_output( From 422178a87d58ea6008de2c2291c8c02f7d520d73 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 10:31:08 +0100 Subject: [PATCH 016/176] wip --- crates/tee-launcher/src/main.rs | 23 +++++++++++++---------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 9f4ecf027..de3f16427 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -1,4 +1,5 @@ use std::process::Command; +use std::str::FromStr; use std::{collections::VecDeque, time::Duration}; use clap::Parser; @@ -176,17 +177,12 @@ async fn get_manifest_digest(config: &LauncherConfig) -> Result = tags.into_iter().collect(); while let Some(tag) = tags.pop_front() { - let manifest_url = format!( + let manifest_url: Url = format!( "https://{}/v2/{}/manifests/{tag}", config.registry, config.image_name - ); - let headers = vec![ - ( - "Accept".to_string(), - "application/vnd.docker.distribution.manifest.v2+json".to_string(), - ), - // (AUTHORIZATION,), - ]; + ) + .parse() + .expect("TODO handle error"); let authorization_value: HeaderValue = format!("Bearer {}", token_response.token) .parse() @@ -197,7 +193,14 @@ async fn get_manifest_digest(config: &LauncherConfig) -> Result { let content_digest = resp .headers() From cba77338040da689ba0d1cc27886a31586ac6c10 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 11:27:40 +0100 Subject: [PATCH 017/176] wip --- crates/tee-launcher/src/docker_types.rs | 45 +++++++++++++++++++ crates/tee-launcher/src/main.rs | 59 ++++++++++++------------- crates/tee-launcher/src/types.rs | 6 --- 3 files changed, 74 insertions(+), 36 deletions(-) create mode 100644 crates/tee-launcher/src/docker_types.rs diff --git a/crates/tee-launcher/src/docker_types.rs b/crates/tee-launcher/src/docker_types.rs new file mode 100644 index 000000000..793507163 --- /dev/null +++ b/crates/tee-launcher/src/docker_types.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; + +/// Partial response https://auth.docker.io/token +#[derive(Debug, Deserialize, Serialize)] +pub struct DockerTokenResponse { + pub token: String, +} + +/// Response from `GET /v2/{name}/manifests/{reference}`. +/// +/// The `mediaType` field determines the variant: +/// - OCI image index → multi-platform manifest with a list of platform entries +/// - Docker V2 / OCI manifest → single-platform manifest with a config digest +#[derive(Debug, Deserialize, Serialize)] +#[serde(tag = "mediaType")] +pub enum ManifestResponse { + /// Multi-platform manifest (OCI image index). + #[serde(rename = "application/vnd.oci.image.index.v1+json")] + ImageIndex { manifests: Vec }, + + /// Single-platform Docker V2 manifest. + #[serde(rename = "application/vnd.docker.distribution.manifest.v2+json")] + DockerV2 { config: ManifestConfig }, + + /// Single-platform OCI manifest. + #[serde(rename = "application/vnd.oci.image.manifest.v1+json")] + OciManifest { config: ManifestConfig }, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ManifestEntry { + pub digest: String, + pub platform: ManifestPlatform, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ManifestPlatform { + pub architecture: String, + pub os: String, +} + +#[derive(Debug, Deserialize, Serialize)] +pub struct ManifestConfig { + pub digest: String, +} diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index de3f16427..a5e3dd203 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -10,12 +10,14 @@ use launcher_interface::types::ApprovedHashesFile; use mpc_primitives::hash::MpcDockerImageHash; use contants::*; +use docker_types::*; use error::*; use reqwest::header::{ACCEPT, AUTHORIZATION, HeaderMap, HeaderValue}; use types::*; use url::Url; mod contants; +mod docker_types; mod error; mod types; @@ -142,12 +144,12 @@ fn has_control_chars(s: &str) -> bool { false } -// --------------------------------------------------------------------------- -// Docker registry communication -// --------------------------------------------------------------------------- - -async fn get_manifest_digest(config: &LauncherConfig) -> Result { +async fn get_manifest_digest( + config: &LauncherConfig, + expected_image_digest: &MpcDockerImageHash, +) -> Result { let tags = config.image_tags.clone(); + let expected_digest = format!("sha256:{}", expected_image_digest.as_hex()); // We need an authorization token to fetch manifests. // TODO: this still has the registry hard-coded in the url. also, if we use a different registry, we need a different auth-endpoint @@ -208,38 +210,35 @@ async fn get_manifest_digest(config: &LauncherConfig) -> Result { + match manifest { + ManifestResponse::ImageIndex { manifests } => { // Multi-platform manifest; scan for amd64/linux - if let Some(manifests) = manifest["manifests"].as_array() { - for m in manifests { - let arch = m["platform"]["architecture"].as_str().unwrap_or(""); - let os = m["platform"]["os"].as_str().unwrap_or(""); - if arch == "amd64" && os == "linux" { - if let Some(digest) = m["digest"].as_str() { - tags.push_back(digest.to_string()); - } - } - } + manifests + .into_iter() + .filter(|manifest| { + manifest.platform.architecture == "amd64" + && manifest.platform.os == "linux" + }) + .for_each(|manifest| tags.push_back(manifest.digest)); + } + ManifestResponse::DockerV2 { config } + | ManifestResponse::OciManifest { config } => { + let incorrect_config_digest = config.digest == expected_digest; + if incorrect_config_digest { + continue; } + + let Some(digest) = content_digest else { + continue; + }; + + return Ok(digest); } - // TODO: - // "application/vnd.docker.distribution.manifest.v2+json" - // | "application/vnd.oci.image.manifest.v1+json" => { - // let config_digest = manifest["config"]["digest"].as_str().unwrap_or(""); - // if config_digest == config. { - // if let Some(digest) = content_digest { - // return Ok(digest); - // } - // } - // } - _ => {} } } Err(e) => { diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index e42d47fba..b38ef7968 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -249,9 +249,3 @@ impl MpcBinaryConfig { ] } } - -/// Partial response https://auth.docker.io/token -#[derive(Debug, Deserialize, Serialize)] -pub struct DockerTokenResponse { - pub token: String, -} From 4b2f887fb5bae79a95a67e1d23e0e81e53ae698a Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 11:31:28 +0100 Subject: [PATCH 018/176] fix bug --- crates/tee-launcher/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index a5e3dd203..a69223ec3 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -228,7 +228,7 @@ async fn get_manifest_digest( } ManifestResponse::DockerV2 { config } | ManifestResponse::OciManifest { config } => { - let incorrect_config_digest = config.digest == expected_digest; + let incorrect_config_digest = config.digest != expected_digest; if incorrect_config_digest { continue; } From 70c6fb1827700ab0db34b761e9a06b5931e2cf0a Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 13:47:21 +0100 Subject: [PATCH 019/176] . --- crates/tee-launcher/src/contants.rs | 40 +++++++++++------------ crates/tee-launcher/src/main.rs | 50 +++++++++++++++++------------ 2 files changed, 49 insertions(+), 41 deletions(-) diff --git a/crates/tee-launcher/src/contants.rs b/crates/tee-launcher/src/contants.rs index c3926b2b6..cf5a62405 100644 --- a/crates/tee-launcher/src/contants.rs +++ b/crates/tee-launcher/src/contants.rs @@ -3,28 +3,28 @@ pub(crate) const IMAGE_DIGEST_FILE: &str = "/mnt/shared/image-digest.bin"; pub(crate) const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; pub(crate) const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; -pub(crate) const SHA256_PREFIX: &str = "sha256:"; +// pub(crate) const SHA256_PREFIX: &str = "sha256:"; -// Docker Hub defaults -pub(crate) const DEFAULT_RPC_REQUEST_TIMEOUT_SECS: f64 = 10.0; -pub(crate) const DEFAULT_RPC_REQUEST_INTERVAL_SECS: f64 = 1.0; -pub(crate) const DEFAULT_RPC_MAX_ATTEMPTS: u32 = 20; +// // Docker Hub defaults +// pub(crate) const DEFAULT_RPC_REQUEST_TIMEOUT_SECS: f64 = 10.0; +// pub(crate) const DEFAULT_RPC_REQUEST_INTERVAL_SECS: f64 = 1.0; +// pub(crate) const DEFAULT_RPC_MAX_ATTEMPTS: u32 = 20; -pub(crate) const DEFAULT_MPC_IMAGE_NAME: &str = "nearone/mpc-node"; -pub(crate) const DEFAULT_MPC_REGISTRY: &str = "registry.hub.docker.com"; -pub(crate) const DEFAULT_MPC_IMAGE_TAG: &str = "latest"; +// pub(crate) const DEFAULT_MPC_IMAGE_NAME: &str = "nearone/mpc-node"; +// pub(crate) const DEFAULT_MPC_REGISTRY: &str = "registry.hub.docker.com"; +// pub(crate) const DEFAULT_MPC_IMAGE_TAG: &str = "latest"; -// Env var names -pub(crate) const ENV_VAR_MPC_HASH_OVERRIDE: &str = "MPC_HASH_OVERRIDE"; -pub(crate) const ENV_VAR_RPC_REQUEST_TIMEOUT_SECS: &str = "RPC_REQUEST_TIMEOUT_SECS"; -pub(crate) const ENV_VAR_RPC_REQUEST_INTERVAL_SECS: &str = "RPC_REQUEST_INTERVAL_SECS"; -pub(crate) const ENV_VAR_RPC_MAX_ATTEMPTS: &str = "RPC_MAX_ATTEMPTS"; +// // Env var names +// pub(crate) const ENV_VAR_MPC_HASH_OVERRIDE: &str = "MPC_HASH_OVERRIDE"; +// pub(crate) const ENV_VAR_RPC_REQUEST_TIMEOUT_SECS: &str = "RPC_REQUEST_TIMEOUT_SECS"; +// pub(crate) const ENV_VAR_RPC_REQUEST_INTERVAL_SECS: &str = "RPC_REQUEST_INTERVAL_SECS"; +// pub(crate) const ENV_VAR_RPC_MAX_ATTEMPTS: &str = "RPC_MAX_ATTEMPTS"; -pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_TAGS: &str = "MPC_IMAGE_TAGS"; -pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_NAME: &str = "MPC_IMAGE_NAME"; -pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY: &str = "MPC_REGISTRY"; +// pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_TAGS: &str = "MPC_IMAGE_TAGS"; +// pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_NAME: &str = "MPC_IMAGE_NAME"; +// pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY: &str = "MPC_REGISTRY"; -// Security limits -pub(crate) const MAX_PASSTHROUGH_ENV_VARS: usize = 64; -pub(crate) const MAX_ENV_VALUE_LEN: usize = 1024; -pub(crate) const MAX_TOTAL_ENV_BYTES: usize = 32 * 1024; +// // Security limits +// pub(crate) const MAX_PASSTHROUGH_ENV_VARS: usize = 64; +// pub(crate) const MAX_ENV_VALUE_LEN: usize = 1024; +// pub(crate) const MAX_TOTAL_ENV_BYTES: usize = 32 * 1024; diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index a69223ec3..76e05b0a7 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -24,6 +24,8 @@ mod types; const DOCKER_AUTH_ACCEPT_HEADER_VALUE: HeaderValue = HeaderValue::from_static("application/vnd.docker.distribution.manifest.v2+json"); +const DOCKER_CONTENT_DIGEST_HEADER: &str = "Docker-Content-Digest"; + #[tokio::main] async fn main() { tracing_subscriber::fmt() @@ -82,18 +84,18 @@ async fn run() -> Result<(), LauncherError> { } })?; - if let Some(override_image) = dstack_config.launcher_config.mpc_hash_override { + if let Some(override_image) = &dstack_config.launcher_config.mpc_hash_override { tracing::info!(?override_image, "override mpc image hash provided"); let override_image_is_allowed = approved_hashes_on_disk .approved_hashes - .contains(&override_image); + .contains(override_image); if !override_image_is_allowed { panic!("TODO: panic if override image is not allowed?"); } - override_image + override_image.clone() } else { approved_hashes_on_disk.newest_approved_hash().clone() } @@ -101,7 +103,7 @@ async fn run() -> Result<(), LauncherError> { } }; - let () = check_image_digest_exists_on_docker_hub(image_hash.clone())?; + let () = validate_image_hash(&dstack_config.launcher_config, image_hash.clone()).await?; let should_extend_rtmr_3 = args.platform == Platform::Tee; @@ -148,7 +150,7 @@ async fn get_manifest_digest( config: &LauncherConfig, expected_image_digest: &MpcDockerImageHash, ) -> Result { - let tags = config.image_tags.clone(); + let mut tags: VecDeque = config.image_tags.iter().cloned().collect(); let expected_digest = format!("sha256:{}", expected_image_digest.as_hex()); // We need an authorization token to fetch manifests. @@ -176,8 +178,6 @@ async fn get_manifest_digest( .await .map_err(|e| LauncherError::RegistryAuthFailed(e.to_string()))?; - let mut tags: VecDeque = tags.into_iter().collect(); - while let Some(tag) = tags.pop_front() { let manifest_url: Url = format!( "https://{}/v2/{}/manifests/{tag}", @@ -204,12 +204,7 @@ async fn get_manifest_digest( .await { Ok(resp) => { - let content_digest = resp - .headers() - .get("Docker-Content-Digest") - .and_then(|v| v.to_str().ok()) - .map(|s| s.to_string()); - + let response_headers = resp.headers().clone(); let manifest: ManifestResponse = resp .json() .await @@ -233,11 +228,15 @@ async fn get_manifest_digest( continue; } - let Some(digest) = content_digest else { + let Some(content_digest) = response_headers + .get(DOCKER_CONTENT_DIGEST_HEADER) + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()) + else { continue; }; - return Ok(digest); + return Ok(content_digest); } } } @@ -301,14 +300,22 @@ async fn request_until_success( }) } -fn check_image_digest_exists_on_docker_hub( +/// Returns if the given image digest is valid (pull + manifest + digest match). +/// Does NOT extend RTMR3 and does NOT run the container. +async fn validate_image_hash( + launcher_config: &LauncherConfig, image_hash: MpcDockerImageHash, ) -> Result<(), ImageDigestValidationFailed> { - let image_hash_name = format!("sha256:{}", image_hash.as_hex()); + let manifest_digest = get_manifest_digest(launcher_config, &image_hash) + .await + .expect("TODO: handle error"); + let image_name = &launcher_config.image_name; + + let name_and_digest = format!("{image_name}@{manifest_digest}"); // Pull let pull = Command::new("docker") - .args(["pull", &image_hash_name]) + .args(["pull", &name_and_digest]) .output() .map_err(|e| ImageDigestValidationFailed::DockerPullFailed(e.to_string()))?; @@ -326,7 +333,7 @@ fn check_image_digest_exists_on_docker_hub( "inspect", "--format", "{{index .ID}}", - &image_hash_name, + &name_and_digest, ]) .output() .map_err(|e| ImageDigestValidationFailed::DockerInspectFailed(e.to_string()))?; @@ -339,11 +346,12 @@ fn check_image_digest_exists_on_docker_hub( } let pulled_digest = String::from_utf8_lossy(&inspect.stdout).trim().to_string(); - if pulled_digest != image_hash_name { + let image_hash_string = image_hash.as_hex(); + if pulled_digest != image_hash_string { return Err( ImageDigestValidationFailed::PulledImageHasMismatchedDigest { pulled_digest, - expected_digest: image_hash_name, + expected_digest: image_hash_string, }, ); } From 1b9bc0863018b5fcd18f9ed1e1821dfcc39d0446 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 13:58:03 +0100 Subject: [PATCH 020/176] fix sha256 prefix --- crates/primitives/src/hash.rs | 8 ++++++++ crates/tee-launcher/src/main.rs | 4 ++-- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/crates/primitives/src/hash.rs b/crates/primitives/src/hash.rs index 4f7c6ba87..7a65ddee0 100644 --- a/crates/primitives/src/hash.rs +++ b/crates/primitives/src/hash.rs @@ -55,6 +55,14 @@ impl Hash32 { } } +impl MpcDockerImageHash { + /// Converts the hash to a hexadecimal string representation with a `sha256:` prefix + pub fn as_hex_sha256(&self) -> String { + let hex_encoding = self.as_hex(); + format!("sha256:{hex_encoding}") + } +} + #[derive(Error, Debug)] pub enum Hash32ParseError { #[error("not a valid hex string")] diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 76e05b0a7..07296037e 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -346,7 +346,7 @@ async fn validate_image_hash( } let pulled_digest = String::from_utf8_lossy(&inspect.stdout).trim().to_string(); - let image_hash_string = image_hash.as_hex(); + let image_hash_string = image_hash.as_hex_sha256(); if pulled_digest != image_hash_string { return Err( ImageDigestValidationFailed::PulledImageHasMismatchedDigest { @@ -413,7 +413,7 @@ fn build_docker_cmd( "--name".into(), MPC_CONTAINER_NAME.into(), "--detach".into(), - image_digest.as_hex(), + image_digest.as_hex_sha256(), ]); tracing::info!("docker cmd {}", cmd.join(" ")); From 9b368966495441a5ac99497530301feb27079014 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 14:21:10 +0100 Subject: [PATCH 021/176] fix docker args --- crates/tee-launcher/src/main.rs | 9 ++------- crates/tee-launcher/src/types.rs | 30 ++++++++++++------------------ 2 files changed, 14 insertions(+), 25 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 07296037e..84738e085 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -392,13 +392,8 @@ fn build_docker_cmd( cmd.extend(["--env".into(), format!("{key}={value}")]); } - let (host_flag, host_value) = docker_flags.extra_hosts.docker_flag_and_value(); - cmd.extend([host_flag, host_value]); - - let (port_forwarding_flag, port_forwarding_value) = - docker_flags.port_mappings.docker_flag_and_value(); - - cmd.extend([port_forwarding_flag, port_forwarding_value]); + cmd.extend(docker_flags.extra_hosts.docker_args()); + cmd.extend(docker_flags.port_mappings.docker_args()); // Container run configuration cmd.extend([ diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index b38ef7968..8fb62a0c6 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -106,16 +106,14 @@ pub(crate) struct ExtraHosts { } impl ExtraHosts { - pub fn docker_flag_and_value(&self) -> (String, String) { - let flag = "--add-host".into(); - let value = self - .hosts + /// Returns `["--add-host", "h1:ip1", "--add-host", "h2:ip2", ...]`. + pub fn docker_args(&self) -> Vec { + self.hosts .iter() - .map(|HostEntry { hostname, ip }| format!("{hostname}:{ip}")) - .collect::>() - .join(","); - - (flag, value) + .flat_map(|HostEntry { hostname, ip }| { + ["--add-host".into(), format!("{hostname}:{ip}")] + }) + .collect() } } @@ -138,16 +136,12 @@ pub struct PortMapping { } impl PortMappings { - pub fn docker_flag_and_value(&self) -> (String, String) { - let flag = "-p".into(); - let value = self - .ports + /// Returns `["-p", "src1:dst1", "-p", "src2:dst2", ...]`. + pub fn docker_args(&self) -> Vec { + self.ports .iter() - .map(|PortMapping { src, dst }| format!("{src}:{dst}")) - .collect::>() - .join(","); - - (flag, value) + .flat_map(|PortMapping { src, dst }| ["-p".into(), format!("{src}:{dst}")]) + .collect() } } From 8a7c6767844ba5dbd1413e714979ad992e671974 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 14:27:24 +0100 Subject: [PATCH 022/176] add some json examples of new config --- deployment/localnet/tee/sam.json | 36 +++++++++++++++++++++++++++++++ deployment/testnet/frodo.json | 37 ++++++++++++++++++++++++++++++++ deployment/testnet/sam.json | 37 ++++++++++++++++++++++++++++++++ 3 files changed, 110 insertions(+) create mode 100644 deployment/localnet/tee/sam.json create mode 100644 deployment/testnet/frodo.json create mode 100644 deployment/testnet/sam.json diff --git a/deployment/localnet/tee/sam.json b/deployment/localnet/tee/sam.json new file mode 100644 index 000000000..a2ab8f528 --- /dev/null +++ b/deployment/localnet/tee/sam.json @@ -0,0 +1,36 @@ +{ + "launcher_config": { + "image_tags": ["main-260e88b"], + "image_name": "nearone/mpc-node", + "registry": "registry.hub.docker.com", + "rpc_request_timeout_secs": 10, + "rpc_request_interval_secs": 1, + "rpc_max_attempts": 20, + "mpc_hash_override": null + }, + "docker_command_config": { + "extra_hosts": { + "hosts": [] + }, + "port_mappings": { + "ports": [ + { "src": 8080, "dst": 8080 }, + { "src": 24566, "dst": 24566 }, + { "src": 13002, "dst": 13002 } + ] + } + }, + "mpc_passthrough_env": { + "mpc_account_id": "sam.test.near", + "mpc_local_address": "127.0.0.1", + "mpc_secret_key_store": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "mpc_backup_encryption_key_hex": "0000000000000000000000000000000000000000000000000000000000000000", + "mpc_env": "Localnet", + "mpc_home_dir": "/data", + "mpc_contract_id": "mpc-contract.test.near", + "mpc_responder_id": "sam.test.near", + "near_boot_nodes": "ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566", + "rust_backtrace": "full", + "rust_log": "info" + } +} diff --git a/deployment/testnet/frodo.json b/deployment/testnet/frodo.json new file mode 100644 index 000000000..4827afc67 --- /dev/null +++ b/deployment/testnet/frodo.json @@ -0,0 +1,37 @@ +{ + "launcher_config": { + "image_tags": ["barak-doc-update_localnet_guide-b12bc7d"], + "image_name": "nearone/mpc-node", + "registry": "registry.hub.docker.com", + "rpc_request_timeout_secs": 10, + "rpc_request_interval_secs": 1, + "rpc_max_attempts": 20, + "mpc_hash_override": null + }, + "docker_command_config": { + "extra_hosts": { + "hosts": [] + }, + "port_mappings": { + "ports": [ + { "src": 8080, "dst": 8080 }, + { "src": 24567, "dst": 24567 }, + { "src": 13001, "dst": 13001 }, + { "src": 80, "dst": 80 } + ] + } + }, + "mpc_passthrough_env": { + "mpc_account_id": "$FRODO_ACCOUNT", + "mpc_local_address": "127.0.0.1", + "mpc_secret_key_store": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "mpc_backup_encryption_key_hex": "0000000000000000000000000000000000000000000000000000000000000000", + "mpc_env": "Testnet", + "mpc_home_dir": "/data", + "mpc_contract_id": "$MPC_CONTRACT_ACCOUNT", + "mpc_responder_id": "$FRODO_ACCOUNT", + "near_boot_nodes": "$BOOTNODES", + "rust_backtrace": "full", + "rust_log": "info" + } +} diff --git a/deployment/testnet/sam.json b/deployment/testnet/sam.json new file mode 100644 index 000000000..9b96a09a1 --- /dev/null +++ b/deployment/testnet/sam.json @@ -0,0 +1,37 @@ +{ + "launcher_config": { + "image_tags": ["barak-doc-update_localnet_guide-b12bc7d"], + "image_name": "nearone/mpc-node", + "registry": "registry.hub.docker.com", + "rpc_request_timeout_secs": 10, + "rpc_request_interval_secs": 1, + "rpc_max_attempts": 20, + "mpc_hash_override": null + }, + "docker_command_config": { + "extra_hosts": { + "hosts": [] + }, + "port_mappings": { + "ports": [ + { "src": 8080, "dst": 8080 }, + { "src": 24567, "dst": 24567 }, + { "src": 13002, "dst": 13002 }, + { "src": 80, "dst": 80 } + ] + } + }, + "mpc_passthrough_env": { + "mpc_account_id": "$SAM_ACCOUNT", + "mpc_local_address": "127.0.0.1", + "mpc_secret_key_store": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", + "mpc_backup_encryption_key_hex": "0000000000000000000000000000000000000000000000000000000000000000", + "mpc_env": "Testnet", + "mpc_home_dir": "/data", + "mpc_contract_id": "$MPC_CONTRACT_ACCOUNT", + "mpc_responder_id": "$SAM_ACCOUNT", + "near_boot_nodes": "$BOOTNODES", + "rust_backtrace": "full", + "rust_log": "info" + } +} From 5815988025d968b3a53c3bb74b3d45d37cccff78 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 14:58:24 +0100 Subject: [PATCH 023/176] allow dynamically passthrough envs --- crates/tee-launcher/src/env_validation.rs | 162 ++++++++++++++++++++++ crates/tee-launcher/src/error.rs | 3 + crates/tee-launcher/src/main.rs | 18 +-- crates/tee-launcher/src/types.rs | 83 +++++++++-- 4 files changed, 236 insertions(+), 30 deletions(-) create mode 100644 crates/tee-launcher/src/env_validation.rs diff --git a/crates/tee-launcher/src/env_validation.rs b/crates/tee-launcher/src/env_validation.rs new file mode 100644 index 000000000..9df1d3656 --- /dev/null +++ b/crates/tee-launcher/src/env_validation.rs @@ -0,0 +1,162 @@ +use std::sync::LazyLock; + +use regex::Regex; + +/// Hard caps to prevent DoS via huge env payloads (matching Python launcher). +pub(crate) const MAX_PASSTHROUGH_ENV_VARS: usize = 64; +pub(crate) const MAX_ENV_VALUE_LEN: usize = 1024; +pub(crate) const MAX_TOTAL_ENV_BYTES: usize = 32 * 1024; // 32 KB + +/// Never pass raw private keys via launcher. +const DENIED_CONTAINER_ENV_KEYS: &[&str] = &["MPC_P2P_PRIVATE_KEY", "MPC_ACCOUNT_SK"]; + +/// Matches `MPC_[A-Z0-9_]{1,64}` — same pattern as the Python launcher. +static MPC_ENV_KEY_RE: LazyLock = + LazyLock::new(|| Regex::new(r"^MPC_[A-Z0-9_]{1,64}$").unwrap()); + +/// Non-MPC keys that are explicitly allowed for backwards compatibility. +const COMPAT_ALLOWED_KEYS: &[&str] = &["RUST_LOG", "RUST_BACKTRACE", "NEAR_BOOT_NODES"]; + +// --------------------------------------------------------------------------- +// Key validation +// --------------------------------------------------------------------------- + +/// Validates an extra env key (from the catch-all `extra_env` map). +/// +/// - Must match `MPC_[A-Z0-9_]{1,64}` **or** be in the compat allowlist +/// - Must not be in the deny list +pub(crate) fn validate_env_key(key: &str) -> Result<(), crate::error::LauncherError> { + if DENIED_CONTAINER_ENV_KEYS.contains(&key) { + return Err(crate::error::LauncherError::UnsafeEnvValue { + key: key.to_owned(), + reason: "denied key".into(), + }); + } + if MPC_ENV_KEY_RE.is_match(key) || COMPAT_ALLOWED_KEYS.contains(&key) { + return Ok(()); + } + Err(crate::error::LauncherError::UnsafeEnvValue { + key: key.to_owned(), + reason: "key does not match allowlist".into(), + }) +} + +// --------------------------------------------------------------------------- +// Value validation +// --------------------------------------------------------------------------- + +fn has_control_chars(s: &str) -> bool { + for ch in s.chars() { + if ch == '\n' || ch == '\r' || ch == '\0' { + return true; + } + if (ch as u32) < 0x20 && ch != '\t' { + return true; + } + } + false +} + +/// Validates an env value (applied to ALL vars, typed and extra). +/// +/// - Length <= `MAX_ENV_VALUE_LEN` +/// - No ASCII control characters (except tab) +/// - Does not contain `LD_PRELOAD` +pub(crate) fn validate_env_value( + key: &str, + value: &str, +) -> Result<(), crate::error::LauncherError> { + if value.len() > MAX_ENV_VALUE_LEN { + return Err(crate::error::LauncherError::UnsafeEnvValue { + key: key.to_owned(), + reason: format!("value too long ({} > {MAX_ENV_VALUE_LEN})", value.len()), + }); + } + if has_control_chars(value) { + return Err(crate::error::LauncherError::UnsafeEnvValue { + key: key.to_owned(), + reason: "contains control characters".into(), + }); + } + if value.contains("LD_PRELOAD") { + return Err(crate::error::LauncherError::UnsafeEnvValue { + key: key.to_owned(), + reason: "contains LD_PRELOAD".into(), + }); + } + Ok(()) +} + +#[cfg(test)] +mod tests { + use super::*; + + // -- Key validation tests -- + + #[test] + fn key_allows_mpc_prefix_uppercase() { + assert!(validate_env_key("MPC_FOO").is_ok()); + assert!(validate_env_key("MPC_FOO_123").is_ok()); + assert!(validate_env_key("MPC_A_B_C").is_ok()); + } + + #[test] + fn key_rejects_lowercase_or_invalid_format() { + assert!(validate_env_key("MPC_foo").is_err()); + assert!(validate_env_key("MPC-FOO").is_err()); + assert!(validate_env_key("MPC.FOO").is_err()); + assert!(validate_env_key("MPC_").is_err()); + } + + #[test] + fn key_allows_compat_non_mpc_keys() { + assert!(validate_env_key("RUST_LOG").is_ok()); + assert!(validate_env_key("RUST_BACKTRACE").is_ok()); + assert!(validate_env_key("NEAR_BOOT_NODES").is_ok()); + } + + #[test] + fn key_denies_sensitive_keys() { + assert!(validate_env_key("MPC_P2P_PRIVATE_KEY").is_err()); + assert!(validate_env_key("MPC_ACCOUNT_SK").is_err()); + } + + #[test] + fn key_rejects_unknown_non_mpc_key() { + assert!(validate_env_key("BAD_KEY").is_err()); + assert!(validate_env_key("HOME").is_err()); + } + + // -- Value validation tests -- + + #[test] + fn value_rejects_control_chars() { + assert!(validate_env_value("K", "ok\nno").is_err()); + assert!(validate_env_value("K", "ok\rno").is_err()); + assert!(validate_env_value("K", &format!("a{}b", '\x1F')).is_err()); + } + + #[test] + fn value_allows_tab() { + assert!(validate_env_value("K", "a\tb").is_ok()); + } + + #[test] + fn value_rejects_ld_preload() { + assert!(validate_env_value("K", "LD_PRELOAD=/tmp/x.so").is_err()); + assert!(validate_env_value("K", "foo LD_PRELOAD bar").is_err()); + } + + #[test] + fn value_rejects_too_long() { + assert!(validate_env_value("K", &"a".repeat(MAX_ENV_VALUE_LEN + 1)).is_err()); + assert!(validate_env_value("K", &"a".repeat(MAX_ENV_VALUE_LEN)).is_ok()); + } + + #[test] + fn value_accepts_normal() { + assert!(validate_env_value("K", "hello-world").is_ok()); + assert!(validate_env_value("K", "192.168.1.1").is_ok()); + assert!(validate_env_value("K", "info,mpc_node=debug").is_ok()); + } +} diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 5d922ddb1..cdb900876 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -54,6 +54,9 @@ pub enum LauncherError { #[error("Total env payload too large (>{0} bytes)")] EnvPayloadTooLarge(usize), + #[error("Env var '{key}' has unsafe value: {reason}")] + UnsafeEnvValue { key: String, reason: String }, + #[error("Unsafe docker command: LD_PRELOAD detected")] LdPreloadDetected, diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 84738e085..e54a3d4f6 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -18,6 +18,7 @@ use url::Url; mod contants; mod docker_types; +mod env_validation; mod error; mod types; @@ -131,21 +132,6 @@ async fn run() -> Result<(), LauncherError> { Ok(()) } -// TODO: this needs to be checked. -fn has_control_chars(s: &str) -> bool { - let control_chars = ['\n', '\r', '\0']; - - for character in s.chars() { - if control_chars.contains(&character) { - return true; - } - if (character as u32) < 0x20 && character != '\t' { - return true; - } - } - false -} - async fn get_manifest_digest( config: &LauncherConfig, expected_image_digest: &MpcDockerImageHash, @@ -388,7 +374,7 @@ fn build_docker_cmd( ]); } - for (key, value) in mpc_config.env_vars() { + for (key, value) in mpc_config.env_vars()? { cmd.extend(["--env".into(), format!("{key}={value}")]); } diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 8fb62a0c6..f079399a6 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -1,3 +1,4 @@ +use std::collections::BTreeMap; use std::fmt; use std::net::{IpAddr, Ipv4Addr}; use std::num::NonZeroU16; @@ -10,6 +11,8 @@ use clap::{Parser, ValueEnum}; use mpc_primitives::hash::MpcDockerImageHash; use serde::{Deserialize, Serialize}; +use crate::env_validation; + /// CLI arguments parsed from environment variables via clap. #[derive(Parser, Debug)] #[command(name = "tee-launcher")] @@ -92,6 +95,11 @@ pub struct MpcBinaryConfig { // rust pub rust_backtrace: RustBacktrace, pub rust_log: RustLog, + /// Additional env vars not covered by the typed fields above. + /// Allows operators to pass new `MPC_*` vars without a launcher rebuild. + /// Keys and values are validated at emission time in `env_vars()`. + #[serde(flatten)] + pub extra_env: BTreeMap, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -224,22 +232,69 @@ impl fmt::Display for RustLog { } impl MpcBinaryConfig { - pub fn env_vars(&self) -> Vec<(&'static str, String)> { - vec![ - ("MPC_ACCOUNT_ID", self.mpc_account_id.clone()), - ("MPC_LOCAL_ADDRESS", self.mpc_local_address.to_string()), - ("MPC_SECRET_STORE_KEY", self.mpc_secret_key_store.clone()), - ("MPC_CONTRACT_ID", self.mpc_contract_id.clone()), - ("MPC_ENV", self.mpc_env.to_string()), - ("MPC_HOME_DIR", self.mpc_home_dir.display().to_string()), - ("MPC_RESPONDER_ID", self.mpc_responder_id.clone()), + /// Returns all env vars to pass to the MPC container. + /// + /// Typed fields are emitted first (deterministic order), followed by + /// validated extras from `extra_env`. All keys and values are validated + /// uniformly before returning. + pub fn env_vars(&self) -> Result, crate::error::LauncherError> { + let mut vars: Vec<(String, String)> = vec![ + ("MPC_ACCOUNT_ID".into(), self.mpc_account_id.clone()), + ( + "MPC_LOCAL_ADDRESS".into(), + self.mpc_local_address.to_string(), + ), + ( + "MPC_SECRET_STORE_KEY".into(), + self.mpc_secret_key_store.clone(), + ), + ("MPC_CONTRACT_ID".into(), self.mpc_contract_id.clone()), + ("MPC_ENV".into(), self.mpc_env.to_string()), + ( + "MPC_HOME_DIR".into(), + self.mpc_home_dir.display().to_string(), + ), + ("MPC_RESPONDER_ID".into(), self.mpc_responder_id.clone()), ( - "MPC_BACKUP_ENCRYPTION_KEY_HEX", + "MPC_BACKUP_ENCRYPTION_KEY_HEX".into(), self.mpc_backup_encryption_key_hex.clone(), ), - ("NEAR_BOOT_NODES", self.near_boot_nodes.clone()), - ("RUST_BACKTRACE", self.rust_backtrace.to_string()), - ("RUST_LOG", self.rust_log.to_string()), - ] + ("NEAR_BOOT_NODES".into(), self.near_boot_nodes.clone()), + ("RUST_BACKTRACE".into(), self.rust_backtrace.to_string()), + ("RUST_LOG".into(), self.rust_log.to_string()), + ]; + + // Keys already emitted via typed fields — skip duplicates from extra_env. + let typed_keys: std::collections::HashSet = + vars.iter().map(|(k, _)| k.clone()).collect(); + + if self.extra_env.len() > env_validation::MAX_PASSTHROUGH_ENV_VARS { + return Err(crate::error::LauncherError::TooManyEnvVars( + env_validation::MAX_PASSTHROUGH_ENV_VARS, + )); + } + + // BTreeMap iteration is sorted, giving deterministic output. + for (key, value) in &self.extra_env { + if typed_keys.contains(key.as_str()) { + continue; + } + env_validation::validate_env_key(key)?; + vars.push((key.clone(), value.clone())); + } + + // Validate ALL env vars uniformly (typed + extra) and enforce aggregate caps. + let mut total_bytes: usize = 0; + for (key, value) in &vars { + env_validation::validate_env_value(key, value)?; + total_bytes += key.len() + 1 + value.len(); + } + if total_bytes > env_validation::MAX_TOTAL_ENV_BYTES { + return Err(crate::error::LauncherError::EnvPayloadTooLarge( + env_validation::MAX_TOTAL_ENV_BYTES, + )); + } + + Ok(vars) } } From a35736568fe4614dffb225edd7070d2f5fe03ef5 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 15:05:26 +0100 Subject: [PATCH 024/176] remove todo panics --- crates/tee-launcher/src/error.rs | 5 +++++ crates/tee-launcher/src/main.rs | 28 +++++++++++++++++++++------- 2 files changed, 26 insertions(+), 7 deletions(-) diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index cdb900876..4c11e39ff 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -84,12 +84,17 @@ pub enum LauncherError { #[error("Registry response parse error: {0}")] RegistryResponseParse(String), + #[error("Invalid manifest URL: {0}")] + InvalidManifestUrl(String), + #[error("The selected image failed digest validation: {0}")] ImageDigestValidationFailed(#[from] ImageDigestValidationFailed), } #[derive(Error, Debug)] pub enum ImageDigestValidationFailed { + #[error("manifest digest lookup failed: {0}")] + ManifestDigestLookupFailed(String), #[error("docker pull failed for {0}")] DockerPullFailed(String), #[error("docker inspect failed for {0}")] diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index e54a3d4f6..bdb413088 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -1,5 +1,4 @@ use std::process::Command; -use std::str::FromStr; use std::{collections::VecDeque, time::Duration}; use clap::Parser; @@ -53,9 +52,16 @@ async fn run() -> Result<(), LauncherError> { let config_file = std::fs::OpenOptions::new() .read(true) .open(DSTACK_USER_CONFIG_FILE) - .expect("dstack user config file exists"); + .map_err(|source| LauncherError::FileRead { + path: DSTACK_USER_CONFIG_FILE.to_string(), + source, + })?; - let dstack_config: Config = serde_json::from_reader(config_file).expect("config file is valid"); + let dstack_config: Config = + serde_json::from_reader(config_file).map_err(|source| LauncherError::JsonParse { + path: DSTACK_USER_CONFIG_FILE.to_string(), + source, + })?; let approved_hashes_file = std::fs::OpenOptions::new() .read(true) @@ -93,7 +99,10 @@ async fn run() -> Result<(), LauncherError> { .contains(override_image); if !override_image_is_allowed { - panic!("TODO: panic if override image is not allowed?"); + return Err(LauncherError::InvalidHashOverride(format!( + "MPC_HASH_OVERRIDE={} does not match any approved hash", + override_image.as_hex_sha256() + ))); } override_image.clone() @@ -156,7 +165,9 @@ async fn get_manifest_digest( let status = token_request_response.status(); if !status.is_success() { - todo!("add error case for non success http codes"); + return Err(LauncherError::RegistryAuthFailed(format!( + "token request returned non-success status: {status}" + ))); } let token_response: DockerTokenResponse = token_request_response @@ -170,7 +181,10 @@ async fn get_manifest_digest( config.registry, config.image_name ) .parse() - .expect("TODO handle error"); + .map_err(|_| LauncherError::InvalidManifestUrl(format!( + "https://{}/v2/{}/manifests/{tag}", + config.registry, config.image_name + )))?; let authorization_value: HeaderValue = format!("Bearer {}", token_response.token) .parse() @@ -294,7 +308,7 @@ async fn validate_image_hash( ) -> Result<(), ImageDigestValidationFailed> { let manifest_digest = get_manifest_digest(launcher_config, &image_hash) .await - .expect("TODO: handle error"); + .map_err(|e| ImageDigestValidationFailed::ManifestDigestLookupFailed(e.to_string()))?; let image_name = &launcher_config.image_name; let name_and_digest = format!("{image_name}@{manifest_digest}"); From 107a16628f0c8b9146a559a515e9d2c8b49fbf0c Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 15:13:38 +0100 Subject: [PATCH 025/176] use backon for retries --- Cargo.lock | 1 + crates/tee-launcher/Cargo.toml | 1 + crates/tee-launcher/src/main.rs | 71 ++++++++++++++------------------- 3 files changed, 33 insertions(+), 40 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f3283e62c..c18f698cc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10554,6 +10554,7 @@ name = "tee-launcher" version = "3.5.1" dependencies = [ "assert_matches", + "backon", "bounded-collections", "clap", "dstack-sdk", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index 0d4fb45ba..8503826ae 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -12,6 +12,7 @@ path = "src/main.rs" integration-test = [] [dependencies] +backon = { workspace = true } bounded-collections = { workspace = true } clap = { workspace = true } dstack-sdk = { workspace = true } diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index bdb413088..c04a7b070 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -1,6 +1,7 @@ use std::process::Command; use std::{collections::VecDeque, time::Duration}; +use backon::{ExponentialBuilder, Retryable}; use clap::Parser; use launcher_interface::MPC_IMAGE_HASH_EVENT; use launcher_interface::types::ApprovedHashesFile; @@ -181,10 +182,12 @@ async fn get_manifest_digest( config.registry, config.image_name ) .parse() - .map_err(|_| LauncherError::InvalidManifestUrl(format!( - "https://{}/v2/{}/manifests/{tag}", - config.registry, config.image_name - )))?; + .map_err(|_| { + LauncherError::InvalidManifestUrl(format!( + "https://{}/v2/{}/manifests/{tag}", + config.registry, config.image_name + )) + })?; let authorization_value: HeaderValue = format!("Bearer {}", token_response.token) .parse() @@ -252,52 +255,40 @@ async fn get_manifest_digest( Err(LauncherError::ImageHashNotFoundAmongTags) } -// TODO: Use backon async fn request_until_success( client: &reqwest::Client, url: Url, headers: HeaderMap, config: &LauncherConfig, ) -> Result { - let mut interval = config.rpc_request_interval_secs as f64; - - for attempt in 1..=config.rpc_max_attempts { - // Sleep before request (matching Python behavior) - tokio::time::sleep(std::time::Duration::from_secs_f64(interval)).await; - interval = (interval.max(1.0) * 1.5).min(60.0); - - let request_timeout_duration = Duration::from_secs(config.rpc_request_timeout_secs); - let request = client + let request_timeout = Duration::from_secs(config.rpc_request_timeout_secs); + let backoff = ExponentialBuilder::default() + .with_min_delay(Duration::from_secs(config.rpc_request_interval_secs)) + .with_factor(1.5) + .with_max_delay(Duration::from_secs(60)) + .with_max_times(config.rpc_max_attempts as usize); + + let request_future = || async { + client .get(url.clone()) .headers(headers.clone()) - .timeout(request_timeout_duration) + .timeout(request_timeout) .send() - .await; - - match request { - Err(e) => { - tracing::warn!( - "Attempt {attempt}/{}: Failed to fetch {url}. Status: Timeout/Error: {e}", - config.rpc_max_attempts - ); - continue; - } - Ok(resp) if resp.status() != reqwest::StatusCode::OK => { - tracing::warn!( - "Attempt {attempt}/{}: Failed to fetch {url}. Status: {}", - config.rpc_max_attempts, - resp.status() - ); - continue; - } - Ok(resp) => return Ok(resp), - } - } + .await? + .error_for_status() + }; - Err(LauncherError::RegistryRequestFailed { - url, - attempts: config.rpc_max_attempts, - }) + request_future + .retry(backoff) + .when(|_: &reqwest::Error| true) + .notify(|err, retrying_in_duration| { + tracing::warn!(?url, ?retrying_in_duration, ?err, "failed to fetch"); + }) + .await + .map_err(|_| LauncherError::RegistryRequestFailed { + url, + attempts: config.rpc_max_attempts, + }) } /// Returns if the given image digest is valid (pull + manifest + digest match). From 3913f9751cb9c73ba2da442982178c03025542f8 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 15:22:02 +0100 Subject: [PATCH 026/176] inline the backon --- crates/tee-launcher/src/docker_types.rs | 2 +- crates/tee-launcher/src/main.rs | 153 +++++++++++------------- 2 files changed, 70 insertions(+), 85 deletions(-) diff --git a/crates/tee-launcher/src/docker_types.rs b/crates/tee-launcher/src/docker_types.rs index 793507163..eb2c552fd 100644 --- a/crates/tee-launcher/src/docker_types.rs +++ b/crates/tee-launcher/src/docker_types.rs @@ -33,7 +33,7 @@ pub struct ManifestEntry { pub platform: ManifestPlatform, } -#[derive(Debug, Deserialize, Serialize)] +#[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] pub struct ManifestPlatform { pub architecture: String, pub os: String, diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index c04a7b070..36a145db9 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -27,6 +27,9 @@ const DOCKER_AUTH_ACCEPT_HEADER_VALUE: HeaderValue = const DOCKER_CONTENT_DIGEST_HEADER: &str = "Docker-Content-Digest"; +const AMD64: &str = "amd64"; +const LINUX: &str = "linux"; + #[tokio::main] async fn main() { tracing_subscriber::fmt() @@ -198,56 +201,74 @@ async fn get_manifest_digest( (AUTHORIZATION, authorization_value), ]); - match request_until_success( - &reqwest_client, - manifest_url.clone(), - headers.clone(), - config, - ) - .await - { - Ok(resp) => { - let response_headers = resp.headers().clone(); - let manifest: ManifestResponse = resp - .json() - .await - .map_err(|e| LauncherError::RegistryResponseParse(e.to_string()))?; - - match manifest { - ManifestResponse::ImageIndex { manifests } => { - // Multi-platform manifest; scan for amd64/linux - manifests - .into_iter() - .filter(|manifest| { - manifest.platform.architecture == "amd64" - && manifest.platform.os == "linux" - }) - .for_each(|manifest| tags.push_back(manifest.digest)); - } - ManifestResponse::DockerV2 { config } - | ManifestResponse::OciManifest { config } => { - let incorrect_config_digest = config.digest != expected_digest; - if incorrect_config_digest { - continue; - } - - let Some(content_digest) = response_headers - .get(DOCKER_CONTENT_DIGEST_HEADER) - .and_then(|v| v.to_str().ok()) - .map(|s| s.to_string()) - else { - continue; - }; - - return Ok(content_digest); - } - } - } - Err(e) => { + let request_timeout = Duration::from_secs(config.rpc_request_timeout_secs); + let backoff = ExponentialBuilder::default() + .with_min_delay(Duration::from_secs(config.rpc_request_interval_secs)) + .with_factor(1.5) + .with_max_delay(Duration::from_secs(60)) + .with_max_times(config.rpc_max_attempts as usize); + + let request_future = || async { + reqwest_client + .get(manifest_url.clone()) + .headers(headers.clone()) + .timeout(request_timeout) + .send() + .await? + .error_for_status() + }; + + let request_with_retry_future = request_future + .retry(backoff) + .when(|_: &reqwest::Error| true) + .notify(|err, dur| { tracing::warn!( - "{e}: Exceeded number of maximum RPC requests for any given attempt. \ - Will continue in the hopes of finding the matching image hash among remaining tags" + ?manifest_url, + ?dur, + ?err, + "failed to fetch manifest, retrying" ); + }); + + let Ok(resp) = request_with_retry_future.await else { + tracing::warn!( + ?manifest_url, + "exceeded max RPC attempts. \ + Will continue in the hopes of finding the matching image hash among remaining tags" + ); + continue; + }; + + let response_headers = resp.headers().clone(); + let manifest: ManifestResponse = resp + .json() + .await + .map_err(|e| LauncherError::RegistryResponseParse(e.to_string()))?; + + match manifest { + ManifestResponse::ImageIndex { manifests } => { + // Multi-platform manifest; scan for amd64/linux + manifests + .into_iter() + .filter(|manifest| { + manifest.platform.architecture == AMD64 && manifest.platform.os == LINUX + }) + .for_each(|manifest| tags.push_back(manifest.digest)); + } + ManifestResponse::DockerV2 { config } | ManifestResponse::OciManifest { config } => { + if config.digest != expected_digest { + continue; + } + + let Some(content_digest) = response_headers + .get(DOCKER_CONTENT_DIGEST_HEADER) + .and_then(|v| v.to_str().ok()) + .map(|s| s.to_string()) + else { + continue; + }; + + return Ok(content_digest); } } } @@ -255,42 +276,6 @@ async fn get_manifest_digest( Err(LauncherError::ImageHashNotFoundAmongTags) } -async fn request_until_success( - client: &reqwest::Client, - url: Url, - headers: HeaderMap, - config: &LauncherConfig, -) -> Result { - let request_timeout = Duration::from_secs(config.rpc_request_timeout_secs); - let backoff = ExponentialBuilder::default() - .with_min_delay(Duration::from_secs(config.rpc_request_interval_secs)) - .with_factor(1.5) - .with_max_delay(Duration::from_secs(60)) - .with_max_times(config.rpc_max_attempts as usize); - - let request_future = || async { - client - .get(url.clone()) - .headers(headers.clone()) - .timeout(request_timeout) - .send() - .await? - .error_for_status() - }; - - request_future - .retry(backoff) - .when(|_: &reqwest::Error| true) - .notify(|err, retrying_in_duration| { - tracing::warn!(?url, ?retrying_in_duration, ?err, "failed to fetch"); - }) - .await - .map_err(|_| LauncherError::RegistryRequestFailed { - url, - attempts: config.rpc_max_attempts, - }) -} - /// Returns if the given image digest is valid (pull + manifest + digest match). /// Does NOT extend RTMR3 and does NOT run the container. async fn validate_image_hash( From 5e69675d650b37adb272a3de460d06e81317f665 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 15:49:55 +0100 Subject: [PATCH 027/176] add snapshot tests --- Cargo.lock | 3 + crates/launcher-interface/Cargo.toml | 5 + crates/launcher-interface/src/lib.rs | 110 +++++++++++++++++- ...nterface__tests__approved_hashes_file.snap | 9 ++ ...ncher_interface__tests__docker_digest.snap | 5 + ...rface__tests__docker_digest_roundtrip.snap | 5 + 6 files changed, 132 insertions(+), 5 deletions(-) create mode 100644 crates/launcher-interface/src/snapshots/launcher_interface__tests__approved_hashes_file.snap create mode 100644 crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest.snap create mode 100644 crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_roundtrip.snap diff --git a/Cargo.lock b/Cargo.lock index c18f698cc..a2ae05752 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4930,8 +4930,11 @@ name = "launcher-interface" version = "3.5.1" dependencies = [ "bounded-collections", + "derive_more 2.1.1", + "insta", "mpc-primitives", "serde", + "serde_json", ] [[package]] diff --git a/crates/launcher-interface/Cargo.toml b/crates/launcher-interface/Cargo.toml index 0da73cbaa..235790226 100644 --- a/crates/launcher-interface/Cargo.toml +++ b/crates/launcher-interface/Cargo.toml @@ -5,10 +5,15 @@ edition.workspace = true license.workspace = true [dependencies] +derive_more = { workspace = true } bounded-collections = { workspace = true } mpc-primitives = { workspace = true } serde = { workspace = true } +[dev-dependencies] +insta = { workspace = true } +serde_json = { workspace = true } + [lints] workspace = true diff --git a/crates/launcher-interface/src/lib.rs b/crates/launcher-interface/src/lib.rs index a4dd7049d..c2ac95258 100644 --- a/crates/launcher-interface/src/lib.rs +++ b/crates/launcher-interface/src/lib.rs @@ -4,19 +4,119 @@ pub mod types { use mpc_primitives::hash::MpcDockerImageHash; use serde::{Deserialize, Serialize}; - /// JSON structure for the approved hashes file written by the MPC node. + /// JSON structure for the approved hashes file written by the MPC node, and read by the launcher. #[derive(Debug, Serialize, Deserialize)] pub struct ApprovedHashesFile { - pub approved_hashes: bounded_collections::NonEmptyVec, + pub approved_hashes: bounded_collections::NonEmptyVec, } impl ApprovedHashesFile { - pub fn newest_approved_hash(&self) -> &MpcDockerImageHash { + pub fn newest_approved_hash(&self) -> &DockerDigest { self.approved_hashes.first() } } -} -// TODO: add insta snapshot test for this type + const SHA256_PREFIX: &str = "sha256:"; + + #[derive(Debug, Clone, derive_more::From)] + pub struct DockerDigest(MpcDockerImageHash); + + impl Serialize for DockerDigest { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + let image_hash_hex = self.0.as_hex(); + let docker_digest_representation = format!("{SHA256_PREFIX}{image_hash_hex}"); + docker_digest_representation.serialize(serializer) + } + } + + impl<'de> Deserialize<'de> for DockerDigest { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + let hex_str = s.strip_prefix(SHA256_PREFIX).ok_or_else(|| { + serde::de::Error::custom(format!("missing {SHA256_PREFIX} prefix")) + })?; + + hex_str + .parse() + .map(DockerDigest) + .map_err(serde::de::Error::custom) + } + } +} mod paths {} + +#[cfg(test)] +mod tests { + use super::types::{ApprovedHashesFile, DockerDigest}; + use mpc_primitives::hash::MpcDockerImageHash; + + fn sample_digest() -> DockerDigest { + let hash: MpcDockerImageHash = [0xab; 32].into(); + DockerDigest::from(hash) + } + + #[test] + fn serialize_docker_digest() { + let digest = sample_digest(); + let json = serde_json::to_value(&digest).unwrap(); + insta::assert_json_snapshot!("docker_digest", json); + } + + #[test] + fn roundtrip_docker_digest() { + let digest = sample_digest(); + let serialized = serde_json::to_string(&digest).unwrap(); + let deserialized: DockerDigest = serde_json::from_str(&serialized).unwrap(); + insta::assert_json_snapshot!( + "docker_digest_roundtrip", + serde_json::to_value(&deserialized).unwrap() + ); + } + + #[test] + fn deserialize_rejects_missing_prefix() { + let json = serde_json::json!( + "abababababababababababababababababababababababababababababababababab" + ); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + assert!( + result + .unwrap_err() + .to_string() + .contains("missing sha256: prefix"), + "error should mention missing prefix" + ); + } + + #[test] + fn deserialize_rejects_invalid_hex() { + let json = serde_json::json!("sha256:not_valid_hex!"); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + #[test] + fn deserialize_rejects_wrong_length() { + let json = serde_json::json!("sha256:abab"); + let result = serde_json::from_value::(json); + assert!(result.is_err()); + } + + #[test] + fn serialize_approved_hashes_file() { + let file = ApprovedHashesFile { + approved_hashes: bounded_collections::NonEmptyVec::from_vec(vec![sample_digest()]) + .unwrap(), + }; + let json = serde_json::to_value(&file).unwrap(); + insta::assert_json_snapshot!("approved_hashes_file", json); + } +} diff --git a/crates/launcher-interface/src/snapshots/launcher_interface__tests__approved_hashes_file.snap b/crates/launcher-interface/src/snapshots/launcher_interface__tests__approved_hashes_file.snap new file mode 100644 index 000000000..61f7f301c --- /dev/null +++ b/crates/launcher-interface/src/snapshots/launcher_interface__tests__approved_hashes_file.snap @@ -0,0 +1,9 @@ +--- +source: crates/launcher-interface/src/lib.rs +expression: json +--- +{ + "approved_hashes": [ + "sha256:abababababababababababababababababababababababababababababababab" + ] +} diff --git a/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest.snap b/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest.snap new file mode 100644 index 000000000..268452a91 --- /dev/null +++ b/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest.snap @@ -0,0 +1,5 @@ +--- +source: crates/launcher-interface/src/lib.rs +expression: json +--- +"sha256:abababababababababababababababababababababababababababababababab" diff --git a/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_roundtrip.snap b/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_roundtrip.snap new file mode 100644 index 000000000..568aec643 --- /dev/null +++ b/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_roundtrip.snap @@ -0,0 +1,5 @@ +--- +source: crates/launcher-interface/src/lib.rs +expression: "serde_json::to_value(&deserialized).unwrap()" +--- +"sha256:abababababababababababababababababababababababababababababababab" From 055f0afba2503b1221851edda12404d6fabb35b7 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 16:14:05 +0100 Subject: [PATCH 028/176] use DockerSha256Digest --- Cargo.lock | 1 + crates/launcher-interface/Cargo.toml | 1 + crates/launcher-interface/src/lib.rs | 114 ++++++++++++++---- ...terface__tests__docker_digest_display.snap | 5 + crates/tee-launcher/src/docker_types.rs | 3 +- crates/tee-launcher/src/error.rs | 11 +- crates/tee-launcher/src/main.rs | 50 ++++---- crates/tee-launcher/src/types.rs | 6 +- 8 files changed, 131 insertions(+), 60 deletions(-) create mode 100644 crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_display.snap diff --git a/Cargo.lock b/Cargo.lock index a2ae05752..eb2ea871a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4935,6 +4935,7 @@ dependencies = [ "mpc-primitives", "serde", "serde_json", + "thiserror 2.0.18", ] [[package]] diff --git a/crates/launcher-interface/Cargo.toml b/crates/launcher-interface/Cargo.toml index 235790226..09d5e0063 100644 --- a/crates/launcher-interface/Cargo.toml +++ b/crates/launcher-interface/Cargo.toml @@ -9,6 +9,7 @@ derive_more = { workspace = true } bounded-collections = { workspace = true } mpc-primitives = { workspace = true } serde = { workspace = true } +thiserror = { workspace = true } [dev-dependencies] diff --git a/crates/launcher-interface/src/lib.rs b/crates/launcher-interface/src/lib.rs index c2ac95258..1bcb17e16 100644 --- a/crates/launcher-interface/src/lib.rs +++ b/crates/launcher-interface/src/lib.rs @@ -1,51 +1,76 @@ pub const MPC_IMAGE_HASH_EVENT: &str = "mpc-image-digest"; pub mod types { + use std::fmt; + use std::str::FromStr; + use mpc_primitives::hash::MpcDockerImageHash; use serde::{Deserialize, Serialize}; /// JSON structure for the approved hashes file written by the MPC node, and read by the launcher. #[derive(Debug, Serialize, Deserialize)] pub struct ApprovedHashesFile { - pub approved_hashes: bounded_collections::NonEmptyVec, + pub approved_hashes: bounded_collections::NonEmptyVec, } impl ApprovedHashesFile { - pub fn newest_approved_hash(&self) -> &DockerDigest { + pub fn newest_approved_hash(&self) -> &DockerSha256Digest { self.approved_hashes.first() } } const SHA256_PREFIX: &str = "sha256:"; - #[derive(Debug, Clone, derive_more::From)] - pub struct DockerDigest(MpcDockerImageHash); + #[derive(Debug, Clone, PartialEq, Eq, derive_more::From)] + pub struct DockerSha256Digest(MpcDockerImageHash); + + impl fmt::Display for DockerSha256Digest { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{SHA256_PREFIX}{}", self.0.as_hex()) + } + } + + #[derive(Debug, thiserror::Error)] + pub enum DockerDigestParseError { + #[error("missing {SHA256_PREFIX} prefix")] + MissingPrefix, + #[error(transparent)] + InvalidHash(#[from] mpc_primitives::hash::Hash32ParseError), + } - impl Serialize for DockerDigest { + impl DockerSha256Digest { + pub fn as_raw_hex(&self) -> String { + self.0.as_hex() + } + } + + impl FromStr for DockerSha256Digest { + type Err = DockerDigestParseError; + + fn from_str(s: &str) -> Result { + let hex_str = s + .strip_prefix(SHA256_PREFIX) + .ok_or(DockerDigestParseError::MissingPrefix)?; + Ok(DockerSha256Digest(hex_str.parse()?)) + } + } + + impl Serialize for DockerSha256Digest { fn serialize(&self, serializer: S) -> Result where S: serde::Serializer, { - let image_hash_hex = self.0.as_hex(); - let docker_digest_representation = format!("{SHA256_PREFIX}{image_hash_hex}"); - docker_digest_representation.serialize(serializer) + self.to_string().serialize(serializer) } } - impl<'de> Deserialize<'de> for DockerDigest { + impl<'de> Deserialize<'de> for DockerSha256Digest { fn deserialize(deserializer: D) -> Result where D: serde::Deserializer<'de>, { let s = String::deserialize(deserializer)?; - let hex_str = s.strip_prefix(SHA256_PREFIX).ok_or_else(|| { - serde::de::Error::custom(format!("missing {SHA256_PREFIX} prefix")) - })?; - - hex_str - .parse() - .map(DockerDigest) - .map_err(serde::de::Error::custom) + s.parse().map_err(serde::de::Error::custom) } } } @@ -54,12 +79,12 @@ mod paths {} #[cfg(test)] mod tests { - use super::types::{ApprovedHashesFile, DockerDigest}; + use super::types::{ApprovedHashesFile, DockerSha256Digest}; use mpc_primitives::hash::MpcDockerImageHash; - fn sample_digest() -> DockerDigest { + fn sample_digest() -> DockerSha256Digest { let hash: MpcDockerImageHash = [0xab; 32].into(); - DockerDigest::from(hash) + DockerSha256Digest::from(hash) } #[test] @@ -73,7 +98,7 @@ mod tests { fn roundtrip_docker_digest() { let digest = sample_digest(); let serialized = serde_json::to_string(&digest).unwrap(); - let deserialized: DockerDigest = serde_json::from_str(&serialized).unwrap(); + let deserialized: DockerSha256Digest = serde_json::from_str(&serialized).unwrap(); insta::assert_json_snapshot!( "docker_digest_roundtrip", serde_json::to_value(&deserialized).unwrap() @@ -85,7 +110,7 @@ mod tests { let json = serde_json::json!( "abababababababababababababababababababababababababababababababababab" ); - let result = serde_json::from_value::(json); + let result = serde_json::from_value::(json); assert!(result.is_err()); assert!( result @@ -99,17 +124,58 @@ mod tests { #[test] fn deserialize_rejects_invalid_hex() { let json = serde_json::json!("sha256:not_valid_hex!"); - let result = serde_json::from_value::(json); + let result = serde_json::from_value::(json); assert!(result.is_err()); } #[test] fn deserialize_rejects_wrong_length() { let json = serde_json::json!("sha256:abab"); - let result = serde_json::from_value::(json); + let result = serde_json::from_value::(json); assert!(result.is_err()); } + #[test] + fn display_docker_digest() { + let digest = sample_digest(); + insta::assert_snapshot!("docker_digest_display", digest.to_string()); + } + + #[test] + fn parse_docker_digest() { + let input = "sha256:abababababababababababababababababababababababababababababababab"; + let parsed: DockerSha256Digest = input.parse().unwrap(); + assert_eq!(parsed.to_string(), input); + } + + #[test] + fn parse_rejects_missing_prefix() { + let result = "abababababababababababababababababababababababababababababababababab" + .parse::(); + assert!(matches!( + result, + Err(super::types::DockerDigestParseError::MissingPrefix) + )); + } + + #[test] + fn parse_rejects_invalid_hex() { + let result = "sha256:not_valid_hex!".parse::(); + assert!(matches!( + result, + Err(super::types::DockerDigestParseError::InvalidHash(_)) + )); + } + + #[test] + fn parse_rejects_wrong_length() { + let result = "sha256:abab".parse::(); + assert!(matches!( + result, + Err(super::types::DockerDigestParseError::InvalidHash(_)) + )); + } + #[test] fn serialize_approved_hashes_file() { let file = ApprovedHashesFile { diff --git a/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_display.snap b/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_display.snap new file mode 100644 index 000000000..44b276fe5 --- /dev/null +++ b/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_display.snap @@ -0,0 +1,5 @@ +--- +source: crates/launcher-interface/src/lib.rs +expression: digest.to_string() +--- +sha256:abababababababababababababababababababababababababababababababab diff --git a/crates/tee-launcher/src/docker_types.rs b/crates/tee-launcher/src/docker_types.rs index eb2c552fd..16f0aad59 100644 --- a/crates/tee-launcher/src/docker_types.rs +++ b/crates/tee-launcher/src/docker_types.rs @@ -1,3 +1,4 @@ +use launcher_interface::types::DockerSha256Digest; use serde::{Deserialize, Serialize}; /// Partial response https://auth.docker.io/token @@ -41,5 +42,5 @@ pub struct ManifestPlatform { #[derive(Debug, Deserialize, Serialize)] pub struct ManifestConfig { - pub digest: String, + pub digest: DockerSha256Digest, } diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 4c11e39ff..6904bed09 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -1,6 +1,7 @@ use std::path::PathBuf; -use mpc_primitives::hash::MpcDockerImageHash; +use launcher_interface::types::DockerSha256Digest; +use mpc_primitives::hash::DockerSha256Digest; use thiserror::Error; use url::Url; @@ -41,12 +42,12 @@ pub enum LauncherError { #[error("docker run failed for validated hash")] DockerRunFailed { - image_hash: MpcDockerImageHash, + image_hash: DockerSha256Digest, inner: std::io::Error, }, #[error("docker run failed for validated hash")] - DockerRunFailedExitStatus { image_hash: MpcDockerImageHash }, + DockerRunFailedExitStatus { image_hash: DockerSha256Digest }, #[error("Too many env vars to pass through (>{0})")] TooManyEnvVars(usize), @@ -103,7 +104,7 @@ pub enum ImageDigestValidationFailed { "pulled image has mismatching digest. pulled: {pulled_digest}, expected: {expected_digest}" )] PulledImageHasMismatchedDigest { - expected_digest: String, - pulled_digest: String, + expected_digest: DockerSha256Digest, + pulled_digest: DockerSha256Digest, }, } diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 36a145db9..e8debf7a2 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -4,10 +4,7 @@ use std::{collections::VecDeque, time::Duration}; use backon::{ExponentialBuilder, Retryable}; use clap::Parser; use launcher_interface::MPC_IMAGE_HASH_EVENT; -use launcher_interface::types::ApprovedHashesFile; - -// Reuse the workspace hash type for type-safe image hash handling. -use mpc_primitives::hash::MpcDockerImageHash; +use launcher_interface::types::{ApprovedHashesFile, DockerSha256Digest}; use contants::*; use docker_types::*; @@ -75,7 +72,7 @@ async fn run() -> Result<(), LauncherError> { source, }); - let image_hash: MpcDockerImageHash = { + let image_hash: DockerSha256Digest = { match approved_hashes_file { Err(err) => { let default_image_digest = args.default_image_digest; @@ -104,8 +101,7 @@ async fn run() -> Result<(), LauncherError> { if !override_image_is_allowed { return Err(LauncherError::InvalidHashOverride(format!( - "MPC_HASH_OVERRIDE={} does not match any approved hash", - override_image.as_hex_sha256() + "MPC_HASH_OVERRIDE={override_image} does not match any approved hash", ))); } @@ -129,7 +125,7 @@ async fn run() -> Result<(), LauncherError> { .emit_event( MPC_IMAGE_HASH_EVENT.to_string(), // TODO: mpc binary has to go back from back hex as well. Just send the raw bytes as payload. - image_hash.as_hex().as_bytes().to_vec(), + image_hash.as_raw_hex().as_bytes().to_vec(), ) .await .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; @@ -147,10 +143,9 @@ async fn run() -> Result<(), LauncherError> { async fn get_manifest_digest( config: &LauncherConfig, - expected_image_digest: &MpcDockerImageHash, + expected_image_digest: &DockerSha256Digest, ) -> Result { let mut tags: VecDeque = config.image_tags.iter().cloned().collect(); - let expected_digest = format!("sha256:{}", expected_image_digest.as_hex()); // We need an authorization token to fetch manifests. // TODO: this still has the registry hard-coded in the url. also, if we use a different registry, we need a different auth-endpoint @@ -256,7 +251,7 @@ async fn get_manifest_digest( .for_each(|manifest| tags.push_back(manifest.digest)); } ManifestResponse::DockerV2 { config } | ManifestResponse::OciManifest { config } => { - if config.digest != expected_digest { + if config.digest != *expected_image_digest { continue; } @@ -280,7 +275,7 @@ async fn get_manifest_digest( /// Does NOT extend RTMR3 and does NOT run the container. async fn validate_image_hash( launcher_config: &LauncherConfig, - image_hash: MpcDockerImageHash, + image_hash: DockerSha256Digest, ) -> Result<(), ImageDigestValidationFailed> { let manifest_digest = get_manifest_digest(launcher_config, &image_hash) .await @@ -321,13 +316,17 @@ async fn validate_image_hash( )); } - let pulled_digest = String::from_utf8_lossy(&inspect.stdout).trim().to_string(); - let image_hash_string = image_hash.as_hex_sha256(); - if pulled_digest != image_hash_string { + let pulled_digest = String::from_utf8_lossy(&inspect.stdout) + .trim() + .to_string() + .parse() + .expect("is valid digest"); + + if pulled_digest != image_hash { return Err( ImageDigestValidationFailed::PulledImageHasMismatchedDigest { pulled_digest, - expected_digest: image_hash_string, + expected_digest: image_hash, }, ); } @@ -339,14 +338,14 @@ fn build_docker_cmd( platform: Platform, mpc_config: &MpcBinaryConfig, docker_flags: &DockerLaunchFlags, - image_digest: &MpcDockerImageHash, + image_digest: &DockerSha256Digest, ) -> Result, LauncherError> { let mut cmd: Vec = vec!["docker".into(), "run".into()]; // Required environment variables cmd.extend([ "--env".into(), - format!("MPC_IMAGE_HASH={}", image_digest.as_hex()), + format!("MPC_IMAGE_HASH={}", image_digest.as_raw_hex()), ]); cmd.extend([ "--env".into(), @@ -384,14 +383,14 @@ fn build_docker_cmd( "--name".into(), MPC_CONTAINER_NAME.into(), "--detach".into(), - image_digest.as_hex_sha256(), + format!("{image_digest}"), ]); - tracing::info!("docker cmd {}", cmd.join(" ")); + let docker_command_string = cmd.join(" "); + tracing::info!(?docker_command_string, "docker cmd"); // Final LD_PRELOAD safeguard - let cmd_str = cmd.join(" "); - if cmd_str.contains("LD_PRELOAD") { + if docker_command_string.contains("LD_PRELOAD") { return Err(LauncherError::LdPreloadDetected); } @@ -400,14 +399,11 @@ fn build_docker_cmd( fn launch_mpc_container( platform: Platform, - valid_hash: &MpcDockerImageHash, + valid_hash: &DockerSha256Digest, mpc_config: &MpcBinaryConfig, docker_flags: &DockerLaunchFlags, ) -> Result<(), LauncherError> { - tracing::info!( - "Launching MPC node with validated hash: {}", - valid_hash.as_hex() - ); + tracing::info!("Launching MPC node with validated hash: {valid_hash}",); // shutdown container if one is already running let _ = Command::new("docker") diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index f079399a6..10a3fda8d 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -4,11 +4,11 @@ use std::net::{IpAddr, Ipv4Addr}; use std::num::NonZeroU16; use std::path::PathBuf; +use launcher_interface::types::DockerSha256Digest; use url::Host; use bounded_collections::NonEmptyVec; use clap::{Parser, ValueEnum}; -use mpc_primitives::hash::MpcDockerImageHash; use serde::{Deserialize, Serialize}; use crate::env_validation; @@ -27,7 +27,7 @@ pub struct CliArgs { /// Fallback image digest when the approved-hashes file is absent #[arg(long, env = "DEFAULT_IMAGE_DIGEST")] - pub default_image_digest: MpcDockerImageHash, + pub default_image_digest: DockerSha256Digest, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] @@ -71,7 +71,7 @@ pub struct LauncherConfig { /// Maximum registry RPC attempts (from `RPC_MAX_ATTEMPTS`). pub rpc_max_attempts: u32, /// Optional hash override that bypasses registry lookup (from `MPC_HASH_OVERRIDE`). - pub mpc_hash_override: Option, + pub mpc_hash_override: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] From 4bde6c6521b77b9b4d71c8c36dadc1b9d60cd76b Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 16:30:52 +0100 Subject: [PATCH 029/176] cleanup --- Cargo.lock | 1 - crates/tee-launcher/Cargo.toml | 1 - crates/tee-launcher/src/error.rs | 31 ------------------------------- crates/tee-launcher/src/main.rs | 2 +- 4 files changed, 1 insertion(+), 34 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index eb2ea871a..91acd7b73 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10565,7 +10565,6 @@ dependencies = [ "hex", "itertools 0.14.0", "launcher-interface", - "mpc-primitives", "regex", "reqwest 0.12.28", "serde", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index 8503826ae..f43e19bd8 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -17,7 +17,6 @@ bounded-collections = { workspace = true } clap = { workspace = true } dstack-sdk = { workspace = true } hex = { workspace = true } -mpc-primitives = { workspace = true } launcher-interface = { workspace = true } itertools = { workspace = true } regex = { workspace = true } diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 6904bed09..1409f048e 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -1,27 +1,11 @@ -use std::path::PathBuf; - use launcher_interface::types::DockerSha256Digest; -use mpc_primitives::hash::DockerSha256Digest; use thiserror::Error; -use url::Url; #[derive(Error, Debug)] pub enum LauncherError { - #[error("PLATFORM=TEE requires dstack unix socket at {0}")] - DstackSocketMissing(String), - - #[error("GetQuote failed before extending RTMR3: {0}")] - DstackGetQuoteFailed(String), - #[error("EmitEvent failed while extending RTMR3: {0}")] DstackEmitEventFailed(String), - #[error("DEFAULT_IMAGE_DIGEST invalid: {0}")] - InvalidDefaultDigest(String), - - #[error("Invalid JSON in {path}: approved_hashes missing or empty")] - InvalidApprovedHashes { path: String }, - #[error("MPC_HASH_OVERRIDE invalid: {0}")] InvalidHashOverride(String), @@ -31,15 +15,6 @@ pub enum LauncherError { #[error("Failed to get auth token from registry: {0}")] RegistryAuthFailed(String), - #[error("Failed to get successful response from {url} after {attempts} attempts")] - RegistryRequestFailed { url: Url, attempts: u32 }, - - #[error("Digest mismatch: pulled {pulled} != expected {expected}")] - DigestMismatch { pulled: String, expected: String }, - - #[error("MPC image hash validation failed: {0}")] - ImageValidationFailed(String), - #[error("docker run failed for validated hash")] DockerRunFailed { image_hash: DockerSha256Digest, @@ -73,12 +48,6 @@ pub enum LauncherError { source: serde_json::Error, }, - #[error("Required environment variable not set: {0}")] - MissingEnvVar(String), - - #[error("Invalid value for {key}: {value}")] - InvalidEnvVar { key: String, value: String }, - #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index e8debf7a2..0bc11d511 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -383,7 +383,7 @@ fn build_docker_cmd( "--name".into(), MPC_CONTAINER_NAME.into(), "--detach".into(), - format!("{image_digest}"), + image_digest.to_string(), ]); let docker_command_string = cmd.join(" "); From 40289e10f263b6a5fd2943f1fefed31ec3b8bec0 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 16:50:41 +0100 Subject: [PATCH 030/176] fix bugs --- crates/tee-launcher/src/error.rs | 5 ++++- crates/tee-launcher/src/main.rs | 19 +++++++++++++------ 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 1409f048e..812f87509 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -22,7 +22,10 @@ pub enum LauncherError { }, #[error("docker run failed for validated hash")] - DockerRunFailedExitStatus { image_hash: DockerSha256Digest }, + DockerRunFailedExitStatus { + image_hash: DockerSha256Digest, + output: String, + }, #[error("Too many env vars to pass through (>{0})")] TooManyEnvVars(usize), diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 0bc11d511..a64a75f09 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -1,3 +1,5 @@ +// A rewrite of launcher.py + use std::process::Command; use std::{collections::VecDeque, time::Duration}; @@ -311,7 +313,7 @@ async fn validate_image_hash( let docker_inspect_failed = !inspect.status.success(); if docker_inspect_failed { - return Err(ImageDigestValidationFailed::DockerPullFailed( + return Err(ImageDigestValidationFailed::DockerInspectFailed( "docker inspect terminated with unsuccessful status".to_string(), )); } @@ -334,13 +336,13 @@ async fn validate_image_hash( Ok(()) } -fn build_docker_cmd( +fn docker_run_args( platform: Platform, mpc_config: &MpcBinaryConfig, docker_flags: &DockerLaunchFlags, image_digest: &DockerSha256Digest, ) -> Result, LauncherError> { - let mut cmd: Vec = vec!["docker".into(), "run".into()]; + let mut cmd: Vec = vec![]; // Required environment variables cmd.extend([ @@ -410,10 +412,11 @@ fn launch_mpc_container( .args(["rm", "-f", MPC_CONTAINER_NAME]) .output(); - let docker_cmd = build_docker_cmd(platform, mpc_config, docker_flags, valid_hash)?; + let docker_run_args = docker_run_args(platform, mpc_config, docker_flags, valid_hash)?; - let run_output = Command::new(&docker_cmd[0]) - .args(&docker_cmd[1..]) + let run_output = Command::new("docker") + .arg("run") + .args(&docker_run_args) .output() .map_err(|inner| LauncherError::DockerRunFailed { image_hash: valid_hash.clone(), @@ -421,8 +424,12 @@ fn launch_mpc_container( })?; if !run_output.status.success() { + let stderr = String::from_utf8_lossy(&run_output.stderr); + let stdout = String::from_utf8_lossy(&run_output.stdout); + tracing::error!(%stderr, %stdout, "docker run failed"); return Err(LauncherError::DockerRunFailedExitStatus { image_hash: valid_hash.clone(), + output: stderr.into_owned(), }); } From 110d9857ca96b6485823d70ca9319fa4548cc961 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 16:50:49 +0100 Subject: [PATCH 031/176] remove dead constants --- crates/tee-launcher/src/contants.rs | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/crates/tee-launcher/src/contants.rs b/crates/tee-launcher/src/contants.rs index cf5a62405..af89e71b3 100644 --- a/crates/tee-launcher/src/contants.rs +++ b/crates/tee-launcher/src/contants.rs @@ -2,29 +2,3 @@ pub(crate) const MPC_CONTAINER_NAME: &str = "mpc-node"; pub(crate) const IMAGE_DIGEST_FILE: &str = "/mnt/shared/image-digest.bin"; pub(crate) const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; pub(crate) const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; - -// pub(crate) const SHA256_PREFIX: &str = "sha256:"; - -// // Docker Hub defaults -// pub(crate) const DEFAULT_RPC_REQUEST_TIMEOUT_SECS: f64 = 10.0; -// pub(crate) const DEFAULT_RPC_REQUEST_INTERVAL_SECS: f64 = 1.0; -// pub(crate) const DEFAULT_RPC_MAX_ATTEMPTS: u32 = 20; - -// pub(crate) const DEFAULT_MPC_IMAGE_NAME: &str = "nearone/mpc-node"; -// pub(crate) const DEFAULT_MPC_REGISTRY: &str = "registry.hub.docker.com"; -// pub(crate) const DEFAULT_MPC_IMAGE_TAG: &str = "latest"; - -// // Env var names -// pub(crate) const ENV_VAR_MPC_HASH_OVERRIDE: &str = "MPC_HASH_OVERRIDE"; -// pub(crate) const ENV_VAR_RPC_REQUEST_TIMEOUT_SECS: &str = "RPC_REQUEST_TIMEOUT_SECS"; -// pub(crate) const ENV_VAR_RPC_REQUEST_INTERVAL_SECS: &str = "RPC_REQUEST_INTERVAL_SECS"; -// pub(crate) const ENV_VAR_RPC_MAX_ATTEMPTS: &str = "RPC_MAX_ATTEMPTS"; - -// pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_TAGS: &str = "MPC_IMAGE_TAGS"; -// pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_NAME: &str = "MPC_IMAGE_NAME"; -// pub(crate) const DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY: &str = "MPC_REGISTRY"; - -// // Security limits -// pub(crate) const MAX_PASSTHROUGH_ENV_VARS: usize = 64; -// pub(crate) const MAX_ENV_VALUE_LEN: usize = 1024; -// pub(crate) const MAX_TOTAL_ENV_BYTES: usize = 32 * 1024; From 58b44219689d5abbf6c41e1a364e8342052675ac Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 16:55:24 +0100 Subject: [PATCH 032/176] rename to constants --- crates/tee-launcher/src/{contants.rs => constants.rs} | 0 crates/tee-launcher/src/main.rs | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) rename crates/tee-launcher/src/{contants.rs => constants.rs} (100%) diff --git a/crates/tee-launcher/src/contants.rs b/crates/tee-launcher/src/constants.rs similarity index 100% rename from crates/tee-launcher/src/contants.rs rename to crates/tee-launcher/src/constants.rs diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index a64a75f09..db38cdf57 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -8,14 +8,14 @@ use clap::Parser; use launcher_interface::MPC_IMAGE_HASH_EVENT; use launcher_interface::types::{ApprovedHashesFile, DockerSha256Digest}; -use contants::*; +use constants::*; use docker_types::*; use error::*; use reqwest::header::{ACCEPT, AUTHORIZATION, HeaderMap, HeaderValue}; use types::*; use url::Url; -mod contants; +mod constants; mod docker_types; mod env_validation; mod error; From 6b78e6723660d4686234b154883a09580f291282 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 17:50:16 +0100 Subject: [PATCH 033/176] update image hashes watcher tests --- .../src/tee/allowed_image_hashes_watcher.rs | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/crates/node/src/tee/allowed_image_hashes_watcher.rs b/crates/node/src/tee/allowed_image_hashes_watcher.rs index b5501038d..32bc37b62 100644 --- a/crates/node/src/tee/allowed_image_hashes_watcher.rs +++ b/crates/node/src/tee/allowed_image_hashes_watcher.rs @@ -1,6 +1,7 @@ +use bounded_collections::NonEmptyVec; use derive_more::From; use itertools::Itertools; -use launcher_interface::types::ApprovedHashesFile; +use launcher_interface::types::{ApprovedHashesFile, DockerSha256Digest}; use mpc_contract::tee::proposal::MpcDockerImageHash; use std::{future::Future, io, panic, path::PathBuf}; use thiserror::Error; @@ -21,7 +22,7 @@ use mockall::automock; pub trait AllowedImageHashesStorage { fn set( &mut self, - approved_hashes: &[MpcDockerImageHash], + approved_hashes: NonEmptyVec, ) -> impl Future> + Send; } @@ -31,7 +32,10 @@ pub struct AllowedImageHashesFile { } impl AllowedImageHashesStorage for AllowedImageHashesFile { - async fn set(&mut self, approved_hashes: &[MpcDockerImageHash]) -> Result<(), io::Error> { + async fn set( + &mut self, + approved_hashes: NonEmptyVec, + ) -> Result<(), io::Error> { tracing::info!( ?self.file_path, len = approved_hashes.len(), @@ -39,7 +43,7 @@ impl AllowedImageHashesStorage for AllowedImageHashesFile { ); let approved_hashes = ApprovedHashesFile { - approved_hashes: approved_hashes.to_vec(), + approved_hashes: approved_hashes.mapped(DockerSha256Digest::from), }; let json = serde_json::to_string_pretty(&approved_hashes) @@ -171,13 +175,13 @@ where let allowed_hashes = self.allowed_hashes_in_contract.borrow_and_update().clone(); - if allowed_hashes.is_empty() { - tracing::warn!("Indexer provided an empty list of allowed image hashes."); + let Ok(allowed_hashes) = NonEmptyVec::from_vec(allowed_hashes) else { + tracing::warn!("indexer provided an empty list of allowed image hashes."); return Ok(()); - } + }; // Write all hashes, newest-first (as provided by contract) - self.image_hash_storage.set(&allowed_hashes).await?; + self.image_hash_storage.set(allowed_hashes.clone()).await?; let running_image_is_not_allowed = !allowed_hashes.iter().contains(&self.current_image); @@ -222,10 +226,12 @@ mod tests { #[rstest] #[tokio::test] async fn test_allowed_image_hash_list_is_written() { - let allowed_images = vec![image_hash_1(), image_hash_2(), image_hash_3()]; - for current_hash in &allowed_images[..2] { + let allowed_images: NonEmptyVec<_> = + NonEmptyVec::from_vec(vec![image_hash_1(), image_hash_2(), image_hash_3()]).unwrap(); + + for current_hash in allowed_images.iter().take(2) { let cancellation_token = CancellationToken::new(); - let (sender, receiver) = watch::channel(allowed_images.clone()); + let (sender, receiver) = watch::channel(allowed_images.clone().to_vec()); let (sender_shutdown, mut receiver_shutdown) = mpsc::channel(1); let write_is_called = Arc::new(Notify::new()); @@ -317,6 +323,7 @@ mod tests { let allowed_image = image_hash_2(); let allowed_list = vec![allowed_image.clone()]; + let expected_non_empty = NonEmptyVec::from_vec(allowed_list.clone()).unwrap(); let cancellation_token = CancellationToken::new(); let (_sender, receiver) = watch::channel(allowed_list.clone()); @@ -330,7 +337,7 @@ mod tests { storage_mock .expect_set() .once() - .with(predicate::eq(allowed_list.clone())) + .with(predicate::eq(expected_non_empty.clone())) .returning(move |_| { write_is_called.notify_one(); Box::pin(async { Ok(()) }) @@ -380,7 +387,7 @@ mod tests { let mut storage_mock = MockAllowedImageHashesStorage::new(); { - let expected = allowed_images.clone(); + let expected = NonEmptyVec::from_vec(allowed_images.clone()).unwrap(); storage_mock .expect_set() @@ -433,7 +440,7 @@ mod tests { // Mock storage expecting exactly the full list let mut storage_mock = MockAllowedImageHashesStorage::new(); { - let expected = full_list.clone(); + let expected = NonEmptyVec::from_vec(full_list.clone()).unwrap(); let write_is_called = write_is_called.clone(); storage_mock @@ -498,11 +505,4 @@ mod tests { "Shutdown should NOT be sent when list is empty" ); } - - #[test] - fn test_json_key_matches_launcher() { - // important: must stay aligned with the launcher implementation in: - // mpc/tee_launcher/launcher.py - assert_eq!(JSON_KEY_APPROVED_HASHES, "approved_hashes"); - } } From 8294a991df12d1b83373434059a38958a9644099 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 21:35:14 +0100 Subject: [PATCH 034/176] cargo clippy on launcher tests --- Cargo.lock | 1 + crates/tee-launcher/Cargo.toml | 1 + crates/tee-launcher/src/env_validation.rs | 105 +++++++++++++--------- 3 files changed, 65 insertions(+), 42 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 91acd7b73..27d1c2a05 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10567,6 +10567,7 @@ dependencies = [ "launcher-interface", "regex", "reqwest 0.12.28", + "rstest", "serde", "serde_json", "tempfile", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index f43e19bd8..9dd627211 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -31,6 +31,7 @@ url = { workspace = true, features = ["serde"] } [dev-dependencies] assert_matches = { workspace = true } +rstest = { workspace = true } tempfile = { workspace = true } [lints] diff --git a/crates/tee-launcher/src/env_validation.rs b/crates/tee-launcher/src/env_validation.rs index 9df1d3656..96a3af3cb 100644 --- a/crates/tee-launcher/src/env_validation.rs +++ b/crates/tee-launcher/src/env_validation.rs @@ -89,74 +89,95 @@ pub(crate) fn validate_env_value( #[cfg(test)] mod tests { - use super::*; + use assert_matches::assert_matches; + use rstest::rstest; - // -- Key validation tests -- + use super::*; - #[test] - fn key_allows_mpc_prefix_uppercase() { - assert!(validate_env_key("MPC_FOO").is_ok()); - assert!(validate_env_key("MPC_FOO_123").is_ok()); - assert!(validate_env_key("MPC_A_B_C").is_ok()); + #[rstest] + #[case("MPC_FOO")] + #[case("MPC_FOO_123")] + #[case("MPC_A_B_C")] + fn key_allows_mpc_prefix_uppercase(#[case] key: &str) { + assert_matches!(validate_env_key(key), Ok(_)); } - #[test] - fn key_rejects_lowercase_or_invalid_format() { - assert!(validate_env_key("MPC_foo").is_err()); - assert!(validate_env_key("MPC-FOO").is_err()); - assert!(validate_env_key("MPC.FOO").is_err()); - assert!(validate_env_key("MPC_").is_err()); + #[rstest] + #[case("MPC_foo")] + #[case("MPC-FOO")] + #[case("MPC.FOO")] + #[case("MPC_")] + fn key_rejects_lowercase_or_invalid_format(#[case] key: &str) { + assert_matches!(validate_env_key(key), Err(_)); } - #[test] - fn key_allows_compat_non_mpc_keys() { - assert!(validate_env_key("RUST_LOG").is_ok()); - assert!(validate_env_key("RUST_BACKTRACE").is_ok()); - assert!(validate_env_key("NEAR_BOOT_NODES").is_ok()); + #[rstest] + #[case("RUST_LOG")] + #[case("RUST_BACKTRACE")] + #[case("NEAR_BOOT_NODES")] + fn key_allows_compat_non_mpc_keys(#[case] key: &str) { + assert_matches!(validate_env_key(key), Ok(_)); } - #[test] - fn key_denies_sensitive_keys() { - assert!(validate_env_key("MPC_P2P_PRIVATE_KEY").is_err()); - assert!(validate_env_key("MPC_ACCOUNT_SK").is_err()); + #[rstest] + #[case("MPC_P2P_PRIVATE_KEY")] + #[case("MPC_ACCOUNT_SK")] + fn key_denies_sensitive_keys(#[case] key: &str) { + assert_matches!(validate_env_key(key), Err(_)); } - #[test] - fn key_rejects_unknown_non_mpc_key() { - assert!(validate_env_key("BAD_KEY").is_err()); - assert!(validate_env_key("HOME").is_err()); + #[rstest] + #[case("BAD_KEY")] + #[case("HOME")] + fn key_rejects_unknown_non_mpc_key(#[case] key: &str) { + assert_matches!(validate_env_key(key), Err(_)); } - // -- Value validation tests -- + #[rstest] + #[case("ok\nno")] + #[case("ok\rno")] + fn value_rejects_control_chars(#[case] value: &str) { + assert_matches!(validate_env_value("K", value), Err(_)); + } #[test] - fn value_rejects_control_chars() { - assert!(validate_env_value("K", "ok\nno").is_err()); - assert!(validate_env_value("K", "ok\rno").is_err()); - assert!(validate_env_value("K", &format!("a{}b", '\x1F')).is_err()); + fn value_rejects_control_char_unit_separator() { + assert_matches!(validate_env_value("K", &format!("a{}b", '\x1F')), Err(_)); } #[test] fn value_allows_tab() { - assert!(validate_env_value("K", "a\tb").is_ok()); + assert_matches!(validate_env_value("K", "a\tb"), Ok(_)); } - #[test] - fn value_rejects_ld_preload() { - assert!(validate_env_value("K", "LD_PRELOAD=/tmp/x.so").is_err()); - assert!(validate_env_value("K", "foo LD_PRELOAD bar").is_err()); + #[rstest] + #[case("LD_PRELOAD=/tmp/x.so")] + #[case("foo LD_PRELOAD bar")] + fn value_rejects_ld_preload(#[case] value: &str) { + assert_matches!(validate_env_value("K", value), Err(_)); } #[test] fn value_rejects_too_long() { - assert!(validate_env_value("K", &"a".repeat(MAX_ENV_VALUE_LEN + 1)).is_err()); - assert!(validate_env_value("K", &"a".repeat(MAX_ENV_VALUE_LEN)).is_ok()); + assert_matches!( + validate_env_value("K", &"a".repeat(MAX_ENV_VALUE_LEN + 1)), + Err(_) + ); } #[test] - fn value_accepts_normal() { - assert!(validate_env_value("K", "hello-world").is_ok()); - assert!(validate_env_value("K", "192.168.1.1").is_ok()); - assert!(validate_env_value("K", "info,mpc_node=debug").is_ok()); + fn value_accepts_at_length_limit() { + assert_matches!( + validate_env_value("K", &"a".repeat(MAX_ENV_VALUE_LEN)), + Ok(_) + ); + } + + #[rstest] + #[case("hello-world")] + #[case("192.168.1.1")] + #[case("info,mpc_node=debug")] + fn value_accepts_normal(#[case] value: &str) { + assert_matches!(validate_env_value("K", value), Ok(_)); } } From 86811bdc16b19ee6f4a40cf87d7596d5f3098b56 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 21:35:43 +0100 Subject: [PATCH 035/176] cargo clippy launcher interface tests --- Cargo.lock | 1 + crates/launcher-interface/Cargo.toml | 1 + crates/launcher-interface/src/lib.rs | 36 +++++++++------------------- 3 files changed, 13 insertions(+), 25 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 27d1c2a05..eb06ae572 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4929,6 +4929,7 @@ dependencies = [ name = "launcher-interface" version = "3.5.1" dependencies = [ + "assert_matches", "bounded-collections", "derive_more 2.1.1", "insta", diff --git a/crates/launcher-interface/Cargo.toml b/crates/launcher-interface/Cargo.toml index 09d5e0063..56f46ec29 100644 --- a/crates/launcher-interface/Cargo.toml +++ b/crates/launcher-interface/Cargo.toml @@ -13,6 +13,7 @@ thiserror = { workspace = true } [dev-dependencies] +assert_matches = { workspace = true } insta = { workspace = true } serde_json = { workspace = true } diff --git a/crates/launcher-interface/src/lib.rs b/crates/launcher-interface/src/lib.rs index 1bcb17e16..0d6fd5cb4 100644 --- a/crates/launcher-interface/src/lib.rs +++ b/crates/launcher-interface/src/lib.rs @@ -79,7 +79,9 @@ mod paths {} #[cfg(test)] mod tests { - use super::types::{ApprovedHashesFile, DockerSha256Digest}; + use assert_matches::assert_matches; + + use super::types::{ApprovedHashesFile, DockerSha256Digest, DockerDigestParseError}; use mpc_primitives::hash::MpcDockerImageHash; fn sample_digest() -> DockerSha256Digest { @@ -110,29 +112,22 @@ mod tests { let json = serde_json::json!( "abababababababababababababababababababababababababababababababababab" ); - let result = serde_json::from_value::(json); - assert!(result.is_err()); - assert!( - result - .unwrap_err() - .to_string() - .contains("missing sha256: prefix"), - "error should mention missing prefix" + assert_matches!( + serde_json::from_value::(json), + Err(ref e) if e.to_string().contains("missing sha256: prefix") ); } #[test] fn deserialize_rejects_invalid_hex() { let json = serde_json::json!("sha256:not_valid_hex!"); - let result = serde_json::from_value::(json); - assert!(result.is_err()); + assert_matches!(serde_json::from_value::(json), Err(_)); } #[test] fn deserialize_rejects_wrong_length() { let json = serde_json::json!("sha256:abab"); - let result = serde_json::from_value::(json); - assert!(result.is_err()); + assert_matches!(serde_json::from_value::(json), Err(_)); } #[test] @@ -152,28 +147,19 @@ mod tests { fn parse_rejects_missing_prefix() { let result = "abababababababababababababababababababababababababababababababababab" .parse::(); - assert!(matches!( - result, - Err(super::types::DockerDigestParseError::MissingPrefix) - )); + assert_matches!(result, Err(DockerDigestParseError::MissingPrefix)); } #[test] fn parse_rejects_invalid_hex() { let result = "sha256:not_valid_hex!".parse::(); - assert!(matches!( - result, - Err(super::types::DockerDigestParseError::InvalidHash(_)) - )); + assert_matches!(result, Err(DockerDigestParseError::InvalidHash(_))); } #[test] fn parse_rejects_wrong_length() { let result = "sha256:abab".parse::(); - assert!(matches!( - result, - Err(super::types::DockerDigestParseError::InvalidHash(_)) - )); + assert_matches!(result, Err(DockerDigestParseError::InvalidHash(_))); } #[test] From dcbe19a0fd31d00aa746568004356469870d3eee Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 5 Mar 2026 22:57:26 +0100 Subject: [PATCH 036/176] let claude add tests --- crates/tee-launcher/src/main.rs | 814 +++++++------------------------ crates/tee-launcher/src/types.rs | 419 ++++++++++++++++ 2 files changed, 583 insertions(+), 650 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index db38cdf57..0e7de6ec6 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -437,653 +437,167 @@ fn launch_mpc_container( Ok(()) } -// #[cfg(test)] -// mod tests { -// use super::*; -// use assert_matches::assert_matches; -// use launcher_interface::types::ApprovedHashesFile; - -// // -- DstackUserConfig parsing tests ------------------------------------- - -// #[test] -// fn test_user_config_defaults_when_map_is_empty() { -// let config = user_config_from_map(BTreeMap::new()).unwrap(); -// assert_eq!(config.image_tags, vec![DEFAULT_MPC_IMAGE_TAG]); -// assert_eq!(config.image_name, DEFAULT_MPC_IMAGE_NAME); -// assert_eq!(config.registry, DEFAULT_MPC_REGISTRY); -// assert_eq!( -// config.rpc_request_timeout_secs, -// DEFAULT_RPC_REQUEST_TIMEOUT_SECS -// ); -// assert_eq!( -// config.rpc_request_interval_secs, -// DEFAULT_RPC_REQUEST_INTERVAL_SECS -// ); -// assert_eq!(config.rpc_max_attempts, DEFAULT_RPC_MAX_ATTEMPTS); -// assert!(config.mpc_hash_override.is_none()); -// assert!(config.passthrough_env.is_empty()); -// } - -// #[test] -// fn test_user_config_typed_fields_extracted_from_map() { -// let map = BTreeMap::from([ -// ( -// DSTACK_USER_CONFIG_MPC_IMAGE_TAGS.into(), -// "v1.0, v1.1".into(), -// ), -// (DSTACK_USER_CONFIG_MPC_IMAGE_NAME.into(), "my/image".into()), -// ( -// DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY.into(), -// "my.registry.io".into(), -// ), -// (ENV_VAR_RPC_REQUEST_TIMEOUT_SECS.into(), "30.0".into()), -// (ENV_VAR_RPC_MAX_ATTEMPTS.into(), "5".into()), -// ("MPC_ACCOUNT_ID".into(), "account.near".into()), -// ]); -// let config = user_config_from_map(map).unwrap(); -// assert_eq!(config.image_tags, vec!["v1.0", "v1.1"]); -// assert_eq!(config.image_name, "my/image"); -// assert_eq!(config.registry, "my.registry.io"); -// assert_eq!(config.rpc_request_timeout_secs, 30.0); -// assert_eq!(config.rpc_max_attempts, 5); -// // Launcher-only keys are NOT in passthrough_env -// assert!( -// !config -// .passthrough_env -// .contains_key(DSTACK_USER_CONFIG_MPC_IMAGE_TAGS) -// ); -// assert!( -// !config -// .passthrough_env -// .contains_key(ENV_VAR_RPC_MAX_ATTEMPTS) -// ); -// // Container passthrough keys ARE in passthrough_env -// assert_eq!( -// config.passthrough_env.get("MPC_ACCOUNT_ID").unwrap(), -// "account.near" -// ); -// } - -// #[test] -// fn test_user_config_malformed_rpc_fields_error() { -// let map = BTreeMap::from([(ENV_VAR_RPC_MAX_ATTEMPTS.into(), "not_a_number".into())]); -// let err = user_config_from_map(map).unwrap_err(); -// assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_MAX_ATTEMPTS); - -// let map = BTreeMap::from([(ENV_VAR_RPC_REQUEST_TIMEOUT_SECS.into(), "bad".into())]); -// let err = user_config_from_map(map).unwrap_err(); -// assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_REQUEST_TIMEOUT_SECS); - -// let map = BTreeMap::from([(ENV_VAR_RPC_REQUEST_INTERVAL_SECS.into(), "bad".into())]); -// let err = user_config_from_map(map).unwrap_err(); -// assert_matches!(err, LauncherError::InvalidEnvVar { key, .. } if key == ENV_VAR_RPC_REQUEST_INTERVAL_SECS); -// } - -// #[test] -// fn test_user_config_hash_override_extracted() { -// let map = BTreeMap::from([(ENV_VAR_MPC_HASH_OVERRIDE.into(), "sha256:abc".into())]); -// let config = user_config_from_map(map).unwrap(); -// assert_eq!(config.mpc_hash_override.unwrap(), "sha256:abc"); -// assert!( -// !config -// .passthrough_env -// .contains_key(ENV_VAR_MPC_HASH_OVERRIDE) -// ); -// } - -// #[test] -// fn test_parse_user_config_from_file() { -// let dir = tempfile::tempdir().unwrap(); -// let file = dir.path().join("user_config"); -// std::fs::write( -// &file, -// "# comment\nMPC_ACCOUNT_ID=test\nMPC_IMAGE_NAME=my/image\n", -// ) -// .unwrap(); -// let config = parse_user_config(file.to_str().unwrap()).unwrap(); -// assert_eq!(config.image_name, "my/image"); -// assert_eq!( -// config.passthrough_env.get("MPC_ACCOUNT_ID").unwrap(), -// "test" -// ); -// assert!(!config.passthrough_env.contains_key("MPC_IMAGE_NAME")); -// } - -// // -- Host/port validation tests ----------------------------------------- - -// #[test] -// fn test_valid_host_entry() { -// assert!(is_valid_host_entry("node.local:192.168.1.1")); -// assert!(!is_valid_host_entry("node.local:not-an-ip")); -// assert!(!is_valid_host_entry("--env LD_PRELOAD=hack.so")); -// } - -// #[test] -// fn test_valid_port_mapping() { -// assert!(is_valid_port_mapping("11780:11780")); -// assert!(!is_valid_port_mapping("65536:11780")); -// assert!(!is_valid_port_mapping("--volume /:/mnt")); -// } - -// // -- Security validation tests ------------------------------------------ - -// #[test] -// fn test_has_control_chars_rejects_newline_and_cr() { -// assert!(has_control_chars("a\nb")); -// assert!(has_control_chars("a\rb")); -// } - -// #[test] -// fn test_has_control_chars_allows_tab() { -// assert!(!has_control_chars("a\tb")); -// } - -// #[test] -// fn test_has_control_chars_rejects_other_control_chars() { -// assert!(has_control_chars(&format!("a{}b", '\x1F'))); -// } - -// #[test] -// fn test_is_safe_env_value_rejects_control_chars() { -// assert!(!is_safe_env_value("ok\nno")); -// assert!(!is_safe_env_value("ok\rno")); -// assert!(!is_safe_env_value(&format!("ok{}no", '\x1F'))); -// } - -// #[test] -// fn test_is_safe_env_value_rejects_ld_preload() { -// assert!(!is_safe_env_value("LD_PRELOAD=/tmp/x.so")); -// assert!(!is_safe_env_value("foo LD_PRELOAD bar")); -// } - -// #[test] -// fn test_is_safe_env_value_rejects_too_long() { -// assert!(!is_safe_env_value(&"a".repeat(MAX_ENV_VALUE_LEN + 1))); -// assert!(is_safe_env_value(&"a".repeat(MAX_ENV_VALUE_LEN))); -// } - -// #[test] -// fn test_is_allowed_container_env_key_allows_mpc_prefix_uppercase() { -// assert!(is_allowed_container_env_key("MPC_FOO")); -// assert!(is_allowed_container_env_key("MPC_FOO_123")); -// assert!(is_allowed_container_env_key("MPC_A_B_C")); -// } - -// #[test] -// fn test_is_allowed_container_env_key_rejects_lowercase_or_invalid() { -// assert!(!is_allowed_container_env_key("MPC_foo")); -// assert!(!is_allowed_container_env_key("MPC-FOO")); -// assert!(!is_allowed_container_env_key("MPC.FOO")); -// assert!(!is_allowed_container_env_key("MPC_")); -// } - -// #[test] -// fn test_is_allowed_container_env_key_allows_compat_non_mpc_keys() { -// assert!(is_allowed_container_env_key("RUST_LOG")); -// assert!(is_allowed_container_env_key("RUST_BACKTRACE")); -// assert!(is_allowed_container_env_key("NEAR_BOOT_NODES")); -// } - -// #[test] -// fn test_is_allowed_container_env_key_denies_sensitive_keys() { -// assert!(!is_allowed_container_env_key("MPC_P2P_PRIVATE_KEY")); -// assert!(!is_allowed_container_env_key("MPC_ACCOUNT_SK")); -// } - -// // -- Docker cmd builder tests ------------------------------------------- - -// fn make_digest() -> String { -// format!("sha256:{}", "a".repeat(64)) -// } - -// fn base_env() -> BTreeMap { -// BTreeMap::from([ -// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), -// ("MPC_CONTRACT_ID".into(), "contract.near".into()), -// ("MPC_ENV".into(), "testnet".into()), -// ("MPC_HOME_DIR".into(), "/data".into()), -// ("NEAR_BOOT_NODES".into(), "boot1,boot2".into()), -// ("RUST_LOG".into(), "info".into()), -// ]) -// } - -// #[test] -// fn test_build_docker_cmd_sanitizes_ports_and_hosts() { -// let env = BTreeMap::from([ -// ("PORTS".into(), "11780:11780,--env BAD=1".into()), -// ( -// "EXTRA_HOSTS".into(), -// "node:192.168.1.1,--volume /:/mnt".into(), -// ), -// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); - -// assert!(cmd.contains(&"MPC_ACCOUNT_ID=mpc-user-123".to_string())); -// assert!(cmd.contains(&"11780:11780".to_string())); -// assert!(cmd.contains(&"node:192.168.1.1".to_string())); -// // Injection strings filtered -// assert!(!cmd.iter().any(|arg| arg.contains("BAD=1"))); -// assert!(!cmd.iter().any(|arg| arg.contains("/:/mnt"))); -// } - -// #[test] -// fn test_extra_hosts_does_not_allow_ld_preload() { -// let env = BTreeMap::from([ -// ( -// "EXTRA_HOSTS".into(), -// "host:1.2.3.4,--env LD_PRELOAD=/evil.so".into(), -// ), -// ("MPC_ACCOUNT_ID".into(), "safe".into()), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(cmd.contains(&"host:1.2.3.4".to_string())); -// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); -// } - -// #[test] -// fn test_ports_does_not_allow_volume_injection() { -// let env = BTreeMap::from([ -// ("PORTS".into(), "2200:2200,--volume /:/mnt".into()), -// ("MPC_ACCOUNT_ID".into(), "safe".into()), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(cmd.contains(&"2200:2200".to_string())); -// assert!(!cmd.iter().any(|arg| arg.contains("/:/mnt"))); -// } - -// #[test] -// fn test_invalid_env_key_is_ignored() { -// let env = BTreeMap::from([ -// ("BAD_KEY".into(), "should_not_be_used".into()), -// ("MPC_ACCOUNT_ID".into(), "safe".into()), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(!cmd.join(" ").contains("should_not_be_used")); -// assert!(cmd.contains(&"MPC_ACCOUNT_ID=safe".to_string())); -// } - -// #[test] -// fn test_mpc_backup_encryption_key_is_allowed() { -// let env = BTreeMap::from([("MPC_BACKUP_ENCRYPTION_KEY_HEX".into(), "0".repeat(64))]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!( -// cmd.join(" ") -// .contains(&format!("MPC_BACKUP_ENCRYPTION_KEY_HEX={}", "0".repeat(64))) -// ); -// } - -// #[test] -// fn test_malformed_extra_host_is_ignored() { -// let env = BTreeMap::from([ -// ( -// "EXTRA_HOSTS".into(), -// "badhostentry,no-colon,also--bad".into(), -// ), -// ("MPC_ACCOUNT_ID".into(), "safe".into()), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(!cmd.contains(&"--add-host".to_string())); -// } - -// #[test] -// fn test_env_value_with_shell_injection_is_handled_safely() { -// let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "safe; rm -rf /".into())]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(cmd.contains(&"MPC_ACCOUNT_ID=safe; rm -rf /".to_string())); -// } - -// #[test] -// fn test_build_docker_cmd_nontee_no_dstack_mount() { -// let mut env = BTreeMap::new(); -// env.insert("MPC_ACCOUNT_ID".into(), "x".into()); -// let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); -// let s = cmd.join(" "); -// assert!(!s.contains("DSTACK_ENDPOINT=")); -// assert!(!s.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); -// } - -// #[test] -// fn test_build_docker_cmd_tee_has_dstack_mount() { -// let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "x".into())]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// let s = cmd.join(" "); -// assert!(s.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); -// assert!(s.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); -// } - -// #[test] -// fn test_build_docker_cmd_allows_arbitrary_mpc_prefix_env_vars() { -// let mut env = base_env(); -// env.insert("MPC_NEW_FEATURE_FLAG".into(), "1".into()); -// env.insert("MPC_SOME_CONFIG".into(), "value".into()); -// let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); -// let cmd_str = cmd.join(" "); -// assert!(cmd_str.contains("MPC_NEW_FEATURE_FLAG=1")); -// assert!(cmd_str.contains("MPC_SOME_CONFIG=value")); -// } - -// #[test] -// fn test_build_docker_cmd_blocks_sensitive_mpc_private_keys() { -// let mut env = base_env(); -// env.insert("MPC_P2P_PRIVATE_KEY".into(), "supersecret".into()); -// env.insert("MPC_ACCOUNT_SK".into(), "supersecret2".into()); -// let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); -// let cmd_str = cmd.join(" "); -// assert!(!cmd_str.contains("MPC_P2P_PRIVATE_KEY")); -// assert!(!cmd_str.contains("MPC_ACCOUNT_SK")); -// } - -// #[test] -// fn test_build_docker_cmd_rejects_env_value_with_newline() { -// let mut env = base_env(); -// env.insert("MPC_NEW_FEATURE_FLAG".into(), "ok\nbad".into()); -// let cmd = build_docker_cmd(Platform::NonTee, &env, &make_digest()).unwrap(); -// let cmd_str = cmd.join(" "); -// assert!(!cmd_str.contains("MPC_NEW_FEATURE_FLAG")); -// } - -// #[test] -// fn test_build_docker_cmd_enforces_max_env_count_cap() { -// let mut env = base_env(); -// for i in 0..=MAX_PASSTHROUGH_ENV_VARS { -// env.insert(format!("MPC_X_{i}"), "1".into()); -// } -// let result = build_docker_cmd(Platform::NonTee, &env, &make_digest()); -// assert_matches!(result, Err(LauncherError::TooManyEnvVars(_))); -// } - -// #[test] -// fn test_build_docker_cmd_enforces_total_env_bytes_cap() { -// let mut env = base_env(); -// for i in 0..40 { -// env.insert(format!("MPC_BIG_{i}"), "a".repeat(MAX_ENV_VALUE_LEN)); -// } -// let result = build_docker_cmd(Platform::NonTee, &env, &make_digest()); -// assert_matches!(result, Err(LauncherError::EnvPayloadTooLarge(_))); -// } - -// // -- LD_PRELOAD injection tests ----------------------------------------- - -// #[test] -// fn test_ld_preload_injection_blocked_via_env_key() { -// let env = BTreeMap::from([ -// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), -// ("--env LD_PRELOAD".into(), "/path/to/my/malloc.so".into()), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); -// } - -// #[test] -// fn test_ld_preload_injection_blocked_via_extra_hosts() { -// let env = BTreeMap::from([ -// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), -// ( -// "EXTRA_HOSTS".into(), -// "host1:192.168.0.1,host2:192.168.0.2,--env LD_PRELOAD=/path/to/my/malloc.so".into(), -// ), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(cmd.contains(&"--add-host".to_string())); -// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); -// } - -// #[test] -// fn test_ld_preload_injection_blocked_via_ports() { -// let env = BTreeMap::from([ -// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), -// ( -// "PORTS".into(), -// "11780:11780,--env LD_PRELOAD=/path/to/my/malloc.so".into(), -// ), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(cmd.contains(&"-p".to_string())); -// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); -// } - -// #[test] -// fn test_ld_preload_injection_blocked_via_mpc_account_id() { -// let env = BTreeMap::from([ -// ( -// "MPC_ACCOUNT_ID".into(), -// "mpc-user-123, --env LD_PRELOAD=/path/to/my/malloc.so".into(), -// ), -// ( -// "EXTRA_HOSTS".into(), -// "host1:192.168.0.1,host2:192.168.0.2".into(), -// ), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); -// } - -// #[test] -// fn test_ld_preload_injection_blocked_via_dash_e() { -// let env = BTreeMap::from([ -// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), -// ("-e LD_PRELOAD".into(), "/path/to/my/malloc.so".into()), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); -// } - -// #[test] -// fn test_ld_preload_injection_blocked_via_extra_hosts_dash_e() { -// let env = BTreeMap::from([ -// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), -// ( -// "EXTRA_HOSTS".into(), -// "host1:192.168.0.1,host2:192.168.0.2,-e LD_PRELOAD=/path/to/my/malloc.so".into(), -// ), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(cmd.contains(&"--add-host".to_string())); -// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); -// } - -// #[test] -// fn test_ld_preload_injection_blocked_via_ports_dash_e() { -// let env = BTreeMap::from([ -// ("MPC_ACCOUNT_ID".into(), "mpc-user-123".into()), -// ( -// "PORTS".into(), -// "11780:11780,-e LD_PRELOAD=/path/to/my/malloc.so".into(), -// ), -// ]); -// let cmd = build_docker_cmd(Platform::Tee, &env, &make_digest()).unwrap(); -// assert!(cmd.contains(&"-p".to_string())); -// assert!(!cmd.iter().any(|arg| arg.contains("LD_PRELOAD"))); -// } - -// // -- Hash selection tests ----------------------------------------------- - -// fn make_digest_json(hashes: &[&str]) -> String { -// serde_json::json!({"approved_hashes": hashes}).to_string() -// } - -// #[test] -// fn test_override_present() { -// let dir = tempfile::tempdir().unwrap(); -// let file = dir.path().join("image-digest.bin"); -// let override_value = format!("sha256:{}", "a".repeat(64)); -// let approved = vec![ -// format!("sha256:{}", "b".repeat(64)), -// override_value.clone(), -// format!("sha256:{}", "c".repeat(64)), -// ]; -// let json = serde_json::json!({"approved_hashes": approved}).to_string(); -// std::fs::write(&file, &json).unwrap(); - -// // We can't easily override IMAGE_DIGEST_FILE constant, so test load_and_select_hash -// // by creating a standalone test that reads from a custom path. -// // Instead test the core logic directly: -// let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); -// assert!(data.approved_hashes.contains(&override_value)); - -// // The override is in the approved list, so it should be valid -// assert!(is_valid_sha256_digest(&override_value)); -// assert!(data.approved_hashes.contains(&override_value)); -// } - -// #[test] -// fn test_override_not_in_list() { -// let approved = vec!["sha256:aaa", "sha256:bbb"]; -// let json = make_digest_json(&approved); -// let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); -// let override_hash = "sha256:xyz"; -// assert!(!data.approved_hashes.contains(&override_hash.to_string())); -// } - -// #[test] -// fn test_no_override_picks_newest() { -// let approved = vec!["sha256:newest", "sha256:older", "sha256:oldest"]; -// let json = make_digest_json(&approved); -// let data: ApprovedHashesFile = serde_json::from_str(&json).unwrap(); -// assert_eq!(data.approved_hashes[0], "sha256:newest"); -// } - -// #[test] -// fn test_json_key_matches_node() { -// // Must stay aligned with crates/node/src/tee/allowed_image_hashes_watcher.rs -// let json = r#"{"approved_hashes": ["sha256:abc"]}"#; -// let data: ApprovedHashesFile = serde_json::from_str(json).unwrap(); -// assert_eq!(data.approved_hashes.len(), 1); -// } - -// #[test] -// fn test_get_bare_digest() { -// assert_eq!( -// get_bare_digest(&format!("sha256:{}", "a".repeat(64))).unwrap(), -// "a".repeat(64) -// ); -// get_bare_digest("invalid").unwrap_err(); -// } - -// #[test] -// fn test_is_valid_sha256_digest() { -// assert!(is_valid_sha256_digest(&format!( -// "sha256:{}", -// "a".repeat(64) -// ))); -// assert!(!is_valid_sha256_digest("sha256:tooshort")); -// assert!(!is_valid_sha256_digest("not-a-digest")); -// // hex::decode accepts uppercase; as_hex() normalizes to lowercase -// assert!(is_valid_sha256_digest(&format!( -// "sha256:{}", -// "A".repeat(64) -// ))); -// } - -// #[test] -// fn test_parse_image_digest_normalizes_case() { -// let upper = format!("sha256:{}", "AB".repeat(32)); -// let hash = parse_image_digest(&upper).unwrap(); -// assert_eq!(hash.as_hex(), "ab".repeat(32)); -// } - -// // -- Full flow docker cmd test ------------------------------------------ - -// #[test] -// fn test_parse_and_build_docker_cmd_full_flow() { -// let dir = tempfile::tempdir().unwrap(); -// let file = dir.path().join("user_config"); -// std::fs::write( -// &file, -// "MPC_ACCOUNT_ID=test-user\nPORTS=11780:11780, --env BAD=oops\nEXTRA_HOSTS=host1:192.168.1.1, --volume /:/mnt\n", -// ) -// .unwrap(); -// let config = parse_user_config(file.to_str().unwrap()).unwrap(); -// let cmd = build_docker_cmd(Platform::Tee, &config.passthrough_env, &make_digest()).unwrap(); -// let cmd_str = cmd.join(" "); - -// assert!(cmd_str.contains("MPC_ACCOUNT_ID=test-user")); -// assert!(cmd_str.contains("11780:11780")); -// assert!(cmd_str.contains("host1:192.168.1.1")); -// assert!(!cmd_str.contains("BAD=oops")); -// assert!(!cmd_str.contains("/:/mnt")); -// } - -// #[test] -// fn test_full_docker_cmd_structure() { -// let env = BTreeMap::from([("MPC_ACCOUNT_ID".into(), "test-user".into())]); -// let digest = make_digest(); -// let cmd = build_docker_cmd(Platform::NonTee, &env, &digest).unwrap(); - -// // Check required subsequence -// assert!(cmd.contains(&"docker".to_string())); -// assert!(cmd.contains(&"run".to_string())); -// assert!(cmd.contains(&"--security-opt".to_string())); -// assert!(cmd.contains(&"no-new-privileges:true".to_string())); -// assert!(cmd.contains(&"/tapp:/tapp:ro".to_string())); -// assert!(cmd.contains(&"shared-volume:/mnt/shared".to_string())); -// assert!(cmd.contains(&"mpc-data:/data".to_string())); -// assert!(cmd.contains(&MPC_CONTAINER_NAME.to_string())); -// assert!(cmd.contains(&"--detach".to_string())); -// // Image digest should be the last argument -// assert_eq!(cmd.last().unwrap(), &digest); -// } - -// // -- Dstack tests ------------------------------------------------------- - -// #[test] -// fn test_extend_rtmr3_nontee_is_noop() { -// // NonTee should return immediately without touching dstack -// let rt = tokio::runtime::Runtime::new().unwrap(); -// rt.block_on(extend_rtmr3(Platform::NonTee, &make_digest())) -// .unwrap(); -// } - -// #[test] -// fn test_extend_rtmr3_tee_requires_socket() { -// // TEE mode should fail when socket doesn't exist -// let rt = tokio::runtime::Runtime::new().unwrap(); -// let result = rt.block_on(extend_rtmr3(Platform::Tee, &make_digest())); -// assert_matches!(result, Err(LauncherError::DstackSocketMissing(_))); -// } - -// // -- MpcDockerImageHash integration test -------------------------------- - -// #[test] -// fn test_mpc_docker_image_hash_from_bare_hex() { -// let bare_hex = "a".repeat(64); -// let hash: MpcDockerImageHash = bare_hex.parse().unwrap(); -// assert_eq!(hash.as_hex(), bare_hex); -// } - -// // -- Integration test (feature-gated) ----------------------------------- - -// #[cfg(feature = "integration-test")] -// mod integration { -// use super::*; - -// const TEST_DIGEST: &str = -// "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372"; - -// fn test_dstack_config() -> Config { -// user_config_from_map(BTreeMap::from([ -// ( -// "MPC_IMAGE_TAGS".into(), -// "83b52da4e2270c688cdd30da04f6b9d3565f25bb".into(), -// ), -// ("MPC_IMAGE_NAME".into(), "nearone/testing".into()), -// ("MPC_REGISTRY".into(), "registry.hub.docker.com".into()), -// ])) -// .unwrap() -// } - -// #[tokio::test] -// async fn test_validate_image_hash_real_registry() { -// let timing = RpcTimingConfig { -// request_timeout_secs: 10.0, -// request_interval_secs: 1.0, -// max_attempts: 20, -// }; -// let result = validate_image_hash(TEST_DIGEST, &test_dstack_config(), &timing) -// .await -// .unwrap(); -// assert!(result, "validate_image_hash() failed for test image"); -// } -// } -// } +#[cfg(test)] +mod tests { + use std::collections::BTreeMap; + + use assert_matches::assert_matches; + use launcher_interface::types::DockerSha256Digest; + + use crate::constants::*; + use crate::docker_run_args; + use crate::error::LauncherError; + use crate::types::*; + + fn sample_digest() -> DockerSha256Digest { + format!("sha256:{}", "a".repeat(64)).parse().unwrap() + } + + fn base_mpc_config() -> MpcBinaryConfig { + MpcBinaryConfig { + mpc_account_id: "test-account".into(), + mpc_local_address: "127.0.0.1".parse().unwrap(), + mpc_secret_key_store: "secret".into(), + mpc_backup_encryption_key_hex: "0".repeat(64), + mpc_env: MpcEnv::Testnet, + mpc_home_dir: "/data".into(), + mpc_contract_id: "contract.near".into(), + mpc_responder_id: "responder-1".into(), + near_boot_nodes: "boot1,boot2".into(), + rust_backtrace: RustBacktrace::Enabled, + rust_log: RustLog::Level(RustLogLevel::Info), + extra_env: BTreeMap::new(), + } + } + + fn empty_docker_flags() -> DockerLaunchFlags { + serde_json::from_value(serde_json::json!({ + "extra_hosts": {"hosts": []}, + "port_mappings": {"ports": []} + })) + .unwrap() + } + + fn docker_flags_with_host_and_port() -> DockerLaunchFlags { + serde_json::from_value(serde_json::json!({ + "extra_hosts": {"hosts": [{"hostname": {"Domain": "node1"}, "ip": "192.168.1.1"}]}, + "port_mappings": {"ports": [{"src": 11780, "dst": 11780}]} + })) + .unwrap() + } + + #[test] + fn tee_mode_includes_dstack_mount() { + // given + let config = base_mpc_config(); + let flags = empty_docker_flags(); + let digest = sample_digest(); + + // when + let args = docker_run_args(Platform::Tee, &config, &flags, &digest).unwrap(); + + // then + let joined = args.join(" "); + assert!(joined.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); + assert!(joined.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); + } + + #[test] + fn nontee_mode_excludes_dstack_mount() { + // given + let config = base_mpc_config(); + let flags = empty_docker_flags(); + let digest = sample_digest(); + + // when + let args = docker_run_args(Platform::NonTee, &config, &flags, &digest).unwrap(); + + // then + let joined = args.join(" "); + assert!(!joined.contains("DSTACK_ENDPOINT=")); + assert!(!joined.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); + } + + #[test] + fn includes_security_opts_and_required_volumes() { + // given + let config = base_mpc_config(); + let flags = empty_docker_flags(); + let digest = sample_digest(); + + // when + let args = docker_run_args(Platform::NonTee, &config, &flags, &digest).unwrap(); + + // then + let joined = args.join(" "); + assert!(joined.contains("--security-opt no-new-privileges:true")); + assert!(joined.contains("/tapp:/tapp:ro")); + assert!(joined.contains("shared-volume:/mnt/shared")); + assert!(joined.contains("mpc-data:/data")); + assert!(joined.contains(&format!("--name {MPC_CONTAINER_NAME}"))); + assert!(joined.contains("--detach")); + } + + #[test] + fn image_digest_is_last_argument() { + // given + let config = base_mpc_config(); + let flags = empty_docker_flags(); + let digest = sample_digest(); + + // when + let args = docker_run_args(Platform::NonTee, &config, &flags, &digest).unwrap(); + + // then + assert_eq!(args.last().unwrap(), &digest.to_string()); + } + + #[test] + fn includes_ports_and_extra_hosts() { + // given + let config = base_mpc_config(); + let flags = docker_flags_with_host_and_port(); + let digest = sample_digest(); + + // when + let args = docker_run_args(Platform::NonTee, &config, &flags, &digest).unwrap(); + + // then + let joined = args.join(" "); + assert!(joined.contains("--add-host node1:192.168.1.1")); + assert!(joined.contains("-p 11780:11780")); + } + + #[test] + fn includes_mpc_env_vars() { + // given + let config = base_mpc_config(); + let flags = empty_docker_flags(); + let digest = sample_digest(); + + // when + let args = docker_run_args(Platform::NonTee, &config, &flags, &digest).unwrap(); + + // then + let joined = args.join(" "); + assert!(joined.contains("MPC_ACCOUNT_ID=test-account")); + assert!(joined.contains("MPC_IMAGE_HASH=")); + assert!(joined.contains(&format!("MPC_LATEST_ALLOWED_HASH_FILE={IMAGE_DIGEST_FILE}"))); + } + + #[test] + fn ld_preload_in_typed_field_is_rejected_by_env_validation() { + // given - typed fields are also validated by env_validation::validate_env_value, + // so LD_PRELOAD in any env value is caught before the final safeguard. + let mut config = base_mpc_config(); + config.mpc_account_id = "LD_PRELOAD=/evil.so".into(); + let flags = empty_docker_flags(); + let digest = sample_digest(); + + // when + let result = docker_run_args(Platform::NonTee, &config, &flags, &digest); + + // then + assert_matches!(result, Err(LauncherError::UnsafeEnvValue { .. })); + } +} diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 10a3fda8d..6017d6c23 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -237,6 +237,12 @@ impl MpcBinaryConfig { /// Typed fields are emitted first (deterministic order), followed by /// validated extras from `extra_env`. All keys and values are validated /// uniformly before returning. + #[cfg(test)] + pub(crate) fn with_extra_env(mut self, extra: std::collections::BTreeMap) -> Self { + self.extra_env = extra; + self + } + pub fn env_vars(&self) -> Result, crate::error::LauncherError> { let mut vars: Vec<(String, String)> = vec![ ("MPC_ACCOUNT_ID".into(), self.mpc_account_id.clone()), @@ -298,3 +304,416 @@ impl MpcBinaryConfig { Ok(vars) } } + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + use std::collections::BTreeMap; + use std::net::Ipv4Addr; + use std::num::NonZeroU16; + + use super::*; + + fn base_mpc_config() -> MpcBinaryConfig { + MpcBinaryConfig { + mpc_account_id: "test-account".into(), + mpc_local_address: "127.0.0.1".parse().unwrap(), + mpc_secret_key_store: "secret".into(), + mpc_backup_encryption_key_hex: "0".repeat(64), + mpc_env: MpcEnv::Testnet, + mpc_home_dir: "/data".into(), + mpc_contract_id: "contract.near".into(), + mpc_responder_id: "responder-1".into(), + near_boot_nodes: "boot1,boot2".into(), + rust_backtrace: RustBacktrace::Enabled, + rust_log: RustLog::Level(RustLogLevel::Info), + extra_env: BTreeMap::new(), + } + } + + // --- HostEntry deserialization --- + + #[test] + fn host_entry_valid_deserialization() { + // given + let json = + serde_json::json!({"hostname": {"Domain": "node.local"}, "ip": "192.168.1.1"}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(entry) => { + assert_eq!(entry.ip, Ipv4Addr::new(192, 168, 1, 1)); + }); + } + + #[test] + fn host_entry_rejects_invalid_ip() { + // given + let json = + serde_json::json!({"hostname": {"Domain": "node.local"}, "ip": "not-an-ip"}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + #[test] + fn host_entry_rejects_plain_string_as_hostname() { + // given - url::Host requires tagged variant, plain string is rejected + let json = serde_json::json!({"hostname": "node.local", "ip": "192.168.1.1"}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + #[test] + fn host_entry_rejects_injection_string_as_hostname() { + // given + let json = serde_json::json!({"hostname": "--env LD_PRELOAD=hack.so", "ip": "192.168.1.1"}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + // --- PortMapping deserialization --- + + #[test] + fn port_mapping_valid_deserialization() { + // given + let json = serde_json::json!({"src": 11780, "dst": 11780}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(_)); + } + + #[test] + fn port_mapping_rejects_zero_port() { + // given + let json = serde_json::json!({"src": 0, "dst": 11780}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + #[test] + fn port_mapping_rejects_out_of_range_port() { + // given + let json = serde_json::json!({"src": 65536, "dst": 11780}); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + // --- docker_args output format --- + + #[test] + fn extra_hosts_docker_args_format() { + // given + let hosts = ExtraHosts { + hosts: vec![HostEntry { + hostname: url::Host::Domain("node.local".into()), + ip: Ipv4Addr::new(192, 168, 1, 1), + }], + }; + + // when + let args = hosts.docker_args(); + + // then + assert_eq!(args, vec!["--add-host", "node.local:192.168.1.1"]); + } + + #[test] + fn empty_extra_hosts_produces_no_docker_args() { + // given + let hosts = ExtraHosts { hosts: vec![] }; + + // when + let args = hosts.docker_args(); + + // then + assert!(args.is_empty()); + } + + #[test] + fn port_mappings_docker_args_format() { + // given + let mappings = PortMappings { + ports: vec![PortMapping { + src: NonZeroU16::new(11780).unwrap(), + dst: NonZeroU16::new(11780).unwrap(), + }], + }; + + // when + let args = mappings.docker_args(); + + // then + assert_eq!(args, vec!["-p", "11780:11780"]); + } + + // --- MpcBinaryConfig::env_vars --- + + #[test] + fn env_vars_includes_all_typed_fields() { + // given + let config = base_mpc_config(); + + // when + let vars = config.env_vars().unwrap(); + + // then + let keys: Vec<&str> = vars.iter().map(|(k, _)| k.as_str()).collect(); + assert!(keys.contains(&"MPC_ACCOUNT_ID")); + assert!(keys.contains(&"MPC_LOCAL_ADDRESS")); + assert!(keys.contains(&"MPC_SECRET_STORE_KEY")); + assert!(keys.contains(&"MPC_CONTRACT_ID")); + assert!(keys.contains(&"MPC_ENV")); + assert!(keys.contains(&"MPC_HOME_DIR")); + assert!(keys.contains(&"MPC_RESPONDER_ID")); + assert!(keys.contains(&"MPC_BACKUP_ENCRYPTION_KEY_HEX")); + assert!(keys.contains(&"NEAR_BOOT_NODES")); + assert!(keys.contains(&"RUST_BACKTRACE")); + assert!(keys.contains(&"RUST_LOG")); + } + + #[test] + fn env_vars_passes_valid_extra_mpc_key() { + // given + let mut extra = BTreeMap::new(); + extra.insert("MPC_NEW_FEATURE".into(), "enabled".into()); + let config = base_mpc_config().with_extra_env(extra); + + // when + let vars = config.env_vars().unwrap(); + + // then + assert!(vars.iter().any(|(k, v)| k == "MPC_NEW_FEATURE" && v == "enabled")); + } + + #[test] + fn env_vars_deduplicates_typed_key_from_extra() { + // given + let mut extra = BTreeMap::new(); + extra.insert("MPC_ACCOUNT_ID".into(), "duplicate".into()); + let config = base_mpc_config().with_extra_env(extra); + + // when + let vars = config.env_vars().unwrap(); + + // then + let account_values: Vec<&str> = vars + .iter() + .filter(|(k, _)| k == "MPC_ACCOUNT_ID") + .map(|(_, v)| v.as_str()) + .collect(); + assert_eq!(account_values.len(), 1); + assert_eq!(account_values[0], "test-account"); + } + + #[test] + fn env_vars_rejects_sensitive_key_in_extra() { + // given + let mut extra = BTreeMap::new(); + extra.insert("MPC_P2P_PRIVATE_KEY".into(), "secret".into()); + let config = base_mpc_config().with_extra_env(extra); + + // when + let result = config.env_vars(); + + // then + assert_matches!(result, Err(crate::error::LauncherError::UnsafeEnvValue { .. })); + } + + #[test] + fn env_vars_rejects_account_sk_in_extra() { + // given + let mut extra = BTreeMap::new(); + extra.insert("MPC_ACCOUNT_SK".into(), "secret".into()); + let config = base_mpc_config().with_extra_env(extra); + + // when + let result = config.env_vars(); + + // then + assert_matches!(result, Err(crate::error::LauncherError::UnsafeEnvValue { .. })); + } + + #[test] + fn env_vars_rejects_value_with_newline() { + // given + let mut extra = BTreeMap::new(); + extra.insert("MPC_INJECTED".into(), "ok\nbad".into()); + let config = base_mpc_config().with_extra_env(extra); + + // when + let result = config.env_vars(); + + // then + assert_matches!(result, Err(crate::error::LauncherError::UnsafeEnvValue { .. })); + } + + #[test] + fn env_vars_rejects_value_containing_ld_preload() { + // given + let mut extra = BTreeMap::new(); + extra.insert("MPC_INJECTED".into(), "LD_PRELOAD=/tmp/x.so".into()); + let config = base_mpc_config().with_extra_env(extra); + + // when + let result = config.env_vars(); + + // then + assert_matches!(result, Err(crate::error::LauncherError::UnsafeEnvValue { .. })); + } + + #[test] + fn env_vars_rejects_too_many_extra_vars() { + // given + let mut extra = BTreeMap::new(); + for i in 0..=crate::env_validation::MAX_PASSTHROUGH_ENV_VARS { + extra.insert(format!("MPC_X_{i}"), "1".into()); + } + let config = base_mpc_config().with_extra_env(extra); + + // when + let result = config.env_vars(); + + // then + assert_matches!(result, Err(crate::error::LauncherError::TooManyEnvVars(_))); + } + + #[test] + fn env_vars_rejects_total_bytes_exceeded() { + // given + let mut extra = BTreeMap::new(); + for i in 0..40 { + extra.insert( + format!("MPC_BIG_{i}"), + "a".repeat(crate::env_validation::MAX_ENV_VALUE_LEN), + ); + } + let config = base_mpc_config().with_extra_env(extra); + + // when + let result = config.env_vars(); + + // then + assert_matches!(result, Err(crate::error::LauncherError::EnvPayloadTooLarge(_))); + } + + #[test] + fn env_vars_rejects_unknown_non_mpc_key() { + // given + let mut extra = BTreeMap::new(); + extra.insert("BAD_KEY".into(), "value".into()); + let config = base_mpc_config().with_extra_env(extra); + + // when + let result = config.env_vars(); + + // then + assert_matches!(result, Err(crate::error::LauncherError::UnsafeEnvValue { .. })); + } + + // --- Config full deserialization --- + + #[test] + fn config_deserializes_valid_json() { + // given + let json = serde_json::json!({ + "launcher_config": { + "image_tags": ["tag1"], + "image_name": "nearone/mpc-node", + "registry": "registry.hub.docker.com", + "rpc_request_timeout_secs": 10, + "rpc_request_interval_secs": 1, + "rpc_max_attempts": 20, + "mpc_hash_override": null + }, + "docker_command_config": { + "extra_hosts": {"hosts": [{"hostname": {"Domain": "node1"}, "ip": "192.168.1.1"}]}, + "port_mappings": {"ports": [{"src": 11780, "dst": 11780}]} + }, + "mpc_passthrough_env": { + "mpc_account_id": "account123", + "mpc_local_address": "127.0.0.1", + "mpc_secret_key_store": "secret", + "mpc_backup_encryption_key_hex": "0000000000000000000000000000000000000000000000000000000000000000", + "mpc_env": "Testnet", + "mpc_home_dir": "/data", + "mpc_contract_id": "contract.near", + "mpc_responder_id": "responder-1", + "near_boot_nodes": "boot1", + "rust_backtrace": "1", + "rust_log": "info" + } + }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(config) => { + assert_eq!(config.mpc_passthrough_env.mpc_account_id, "account123"); + assert_eq!(config.launcher_config.image_name, "nearone/mpc-node"); + }); + } + + #[test] + fn config_rejects_missing_required_field() { + // given - mpc_account_id is missing + let json = serde_json::json!({ + "launcher_config": { + "image_tags": ["tag1"], + "image_name": "nearone/mpc-node", + "registry": "registry.hub.docker.com", + "rpc_request_timeout_secs": 10, + "rpc_request_interval_secs": 1, + "rpc_max_attempts": 20, + "mpc_hash_override": null + }, + "docker_command_config": { + "extra_hosts": {"hosts": []}, + "port_mappings": {"ports": []} + }, + "mpc_passthrough_env": { + "mpc_local_address": "127.0.0.1", + "mpc_secret_key_store": "secret", + "mpc_backup_encryption_key_hex": "0000000000000000000000000000000000000000000000000000000000000000", + "mpc_env": "Testnet", + "mpc_home_dir": "/data", + "mpc_contract_id": "contract.near", + "mpc_responder_id": "responder-1", + "near_boot_nodes": "boot1", + "rust_backtrace": "1", + "rust_log": "info" + } + }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } +} From e658af4cfdebf8b9a7ccdb3a0b546feb79336b6a Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 00:34:51 +0100 Subject: [PATCH 037/176] add more tests --- crates/tee-launcher/src/docker_types.rs | 104 ++++++++++ crates/tee-launcher/src/main.rs | 250 ++++++++++++++++++++---- 2 files changed, 314 insertions(+), 40 deletions(-) diff --git a/crates/tee-launcher/src/docker_types.rs b/crates/tee-launcher/src/docker_types.rs index 16f0aad59..e48780fd3 100644 --- a/crates/tee-launcher/src/docker_types.rs +++ b/crates/tee-launcher/src/docker_types.rs @@ -44,3 +44,107 @@ pub struct ManifestPlatform { pub struct ManifestConfig { pub digest: DockerSha256Digest, } + +#[cfg(test)] +mod tests { + use assert_matches::assert_matches; + + use super::*; + + fn sample_digest_str() -> String { + format!("sha256:{}", "ab".repeat(32)) + } + + #[test] + fn image_index_deserializes() { + // given + let json = serde_json::json!({ + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "digest": "sha256:abc123", + "platform": { "architecture": "amd64", "os": "linux" } + }, + { + "digest": "sha256:def456", + "platform": { "architecture": "arm64", "os": "linux" } + } + ] + }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(ManifestResponse::ImageIndex { manifests }) => { + assert_eq!(manifests.len(), 2); + assert_eq!(manifests[0].platform, ManifestPlatform { + architecture: "amd64".into(), + os: "linux".into(), + }); + }); + } + + #[test] + fn docker_v2_manifest_deserializes() { + // given + let json = serde_json::json!({ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { "digest": sample_digest_str() } + }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(ManifestResponse::DockerV2 { config }) => { + assert_eq!(config.digest.to_string(), sample_digest_str()); + }); + } + + #[test] + fn oci_manifest_deserializes() { + // given + let json = serde_json::json!({ + "mediaType": "application/vnd.oci.image.manifest.v1+json", + "config": { "digest": sample_digest_str() } + }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(ManifestResponse::OciManifest { config }) => { + assert_eq!(config.digest.to_string(), sample_digest_str()); + }); + } + + #[test] + fn unknown_media_type_is_rejected() { + // given + let json = serde_json::json!({ + "mediaType": "application/vnd.unknown.format", + "config": { "digest": sample_digest_str() } + }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Err(_)); + } + + #[test] + fn docker_token_response_deserializes() { + // given + let json = serde_json::json!({ "token": "abc.def.ghi" }); + + // when + let result = serde_json::from_value::(json); + + // then + assert_matches!(result, Ok(resp) => { + assert_eq!(resp.token, "abc.def.ghi"); + }); + } +} diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 0e7de6ec6..d50982569 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -74,47 +74,31 @@ async fn run() -> Result<(), LauncherError> { source, }); - let image_hash: DockerSha256Digest = { - match approved_hashes_file { - Err(err) => { - let default_image_digest = args.default_image_digest; - tracing::warn!( - ?err, - ?default_image_digest, - "approved hashes file does not exist on disk, falling back to default digest" - ); - default_image_digest - } - Ok(approved_hashes_file) => { - let approved_hashes_on_disk: ApprovedHashesFile = - serde_json::from_reader(approved_hashes_file).map_err(|source| { - LauncherError::JsonParse { - path: IMAGE_DIGEST_FILE.to_string(), - source, - } - })?; - - if let Some(override_image) = &dstack_config.launcher_config.mpc_hash_override { - tracing::info!(?override_image, "override mpc image hash provided"); - - let override_image_is_allowed = approved_hashes_on_disk - .approved_hashes - .contains(override_image); - - if !override_image_is_allowed { - return Err(LauncherError::InvalidHashOverride(format!( - "MPC_HASH_OVERRIDE={override_image} does not match any approved hash", - ))); - } - - override_image.clone() - } else { - approved_hashes_on_disk.newest_approved_hash().clone() - } - } + let approved_hashes_on_disk: Option = match approved_hashes_file { + Err(err) => { + tracing::warn!( + ?err, + default_image_digest = ?args.default_image_digest, + "approved hashes file does not exist on disk, falling back to default digest" + ); + None + } + Ok(file) => { + let parsed: ApprovedHashesFile = + serde_json::from_reader(file).map_err(|source| LauncherError::JsonParse { + path: IMAGE_DIGEST_FILE.to_string(), + source, + })?; + Some(parsed) } }; + let image_hash = select_image_hash( + approved_hashes_on_disk.as_ref(), + &args.default_image_digest, + dstack_config.launcher_config.mpc_hash_override.as_ref(), + )?; + let () = validate_image_hash(&dstack_config.launcher_config, image_hash.clone()).await?; let should_extend_rtmr_3 = args.platform == Platform::Tee; @@ -143,6 +127,39 @@ async fn run() -> Result<(), LauncherError> { Ok(()) } +/// Select which image hash to use, given the approved hashes file (if present), +/// a fallback default digest, and an optional user override. +/// +/// Selection rules: +/// - If the approved hashes file is absent → use `default_digest` +/// - If `override_hash` is set and appears in the approved list → use it +/// - If `override_hash` is set but NOT in the approved list → error +/// - Otherwise → use the newest approved hash (first in the list) +fn select_image_hash( + approved_hashes: Option<&ApprovedHashesFile>, + default_digest: &DockerSha256Digest, + override_hash: Option<&DockerSha256Digest>, +) -> Result { + let Some(approved) = approved_hashes else { + tracing::info!("no approved hashes file, using default digest"); + return Ok(default_digest.clone()); + }; + + if let Some(override_image) = override_hash { + tracing::info!(?override_image, "override mpc image hash provided"); + if !approved.approved_hashes.contains(override_image) { + return Err(LauncherError::InvalidHashOverride(format!( + "MPC_HASH_OVERRIDE={override_image} does not match any approved hash", + ))); + } + return Ok(override_image.clone()); + } + + let selected = approved.newest_approved_hash().clone(); + tracing::info!(?selected, "selected newest approved hash"); + Ok(selected) +} + async fn get_manifest_digest( config: &LauncherConfig, expected_image_digest: &DockerSha256Digest, @@ -442,15 +459,29 @@ mod tests { use std::collections::BTreeMap; use assert_matches::assert_matches; - use launcher_interface::types::DockerSha256Digest; + use bounded_collections::NonEmptyVec; + use launcher_interface::types::{ApprovedHashesFile, DockerSha256Digest}; use crate::constants::*; use crate::docker_run_args; use crate::error::LauncherError; + use crate::select_image_hash; use crate::types::*; + fn digest(hex_char: char) -> DockerSha256Digest { + format!("sha256:{}", std::iter::repeat_n(hex_char, 64).collect::()) + .parse() + .unwrap() + } + fn sample_digest() -> DockerSha256Digest { - format!("sha256:{}", "a".repeat(64)).parse().unwrap() + digest('a') + } + + fn approved_file(hashes: Vec) -> ApprovedHashesFile { + ApprovedHashesFile { + approved_hashes: NonEmptyVec::from_vec(hashes).unwrap(), + } } fn base_mpc_config() -> MpcBinaryConfig { @@ -600,4 +631,143 @@ mod tests { // then assert_matches!(result, Err(LauncherError::UnsafeEnvValue { .. })); } + + // --- select_image_hash --- + + #[test] + fn select_hash_override_present_and_in_approved_list() { + // given + let override_digest = digest('b'); + let approved = approved_file(vec![digest('c'), override_digest.clone(), digest('d')]); + + // when + let result = select_image_hash(Some(&approved), &digest('f'), Some(&override_digest)); + + // then + assert_matches!(result, Ok(selected) => { + assert_eq!(selected, override_digest); + }); + } + + #[test] + fn select_hash_override_not_in_approved_list() { + // given + let override_digest = digest('b'); + let approved = approved_file(vec![digest('c'), digest('d')]); + + // when + let result = select_image_hash(Some(&approved), &digest('f'), Some(&override_digest)); + + // then + assert_matches!(result, Err(LauncherError::InvalidHashOverride(_))); + } + + #[test] + fn select_hash_no_override_picks_newest() { + // given - first entry is "newest" + let newest = digest('a'); + let approved = approved_file(vec![newest.clone(), digest('b'), digest('c')]); + + // when + let result = select_image_hash(Some(&approved), &digest('f'), None); + + // then + assert_matches!(result, Ok(selected) => { + assert_eq!(selected, newest); + }); + } + + #[test] + fn select_hash_missing_file_falls_back_to_default() { + // given + let default = digest('d'); + + // when + let result = select_image_hash(None, &default, None); + + // then + assert_matches!(result, Ok(selected) => { + assert_eq!(selected, default); + }); + } + + #[test] + fn select_hash_missing_file_ignores_override() { + // given - override is set but file is missing, so default wins + let default = digest('d'); + let override_digest = digest('b'); + + // when + let result = select_image_hash(None, &default, Some(&override_digest)); + + // then + assert_matches!(result, Ok(selected) => { + assert_eq!(selected, default); + }); + } + + // --- approved_hashes JSON key alignment --- + + #[test] + fn approved_hashes_json_key_is_approved_hashes() { + // given - the JSON field name must match between launcher and MPC node + let file = approved_file(vec![sample_digest()]); + + // when + let json = serde_json::to_value(&file).unwrap(); + + // then + assert!(json.get("approved_hashes").is_some()); + } +} + +/// Integration tests requiring network access and Docker Hub. +/// Run with: cargo test -p tee-launcher --features integration-test +#[cfg(all(test, feature = "integration-test"))] +mod integration_tests { + use super::*; + + const TEST_DIGEST: &str = + "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372"; + const TEST_TAG: &str = "83b52da4e2270c688cdd30da04f6b9d3565f25bb"; + const TEST_IMAGE_NAME: &str = "nearone/testing"; + const TEST_REGISTRY: &str = "registry.hub.docker.com"; + + fn test_launcher_config() -> LauncherConfig { + LauncherConfig { + image_tags: bounded_collections::NonEmptyVec::from_vec(vec![TEST_TAG.into()]).unwrap(), + image_name: TEST_IMAGE_NAME.into(), + registry: TEST_REGISTRY.into(), + rpc_request_timeout_secs: 10, + rpc_request_interval_secs: 1, + rpc_max_attempts: 20, + mpc_hash_override: None, + } + } + + #[tokio::test] + async fn get_manifest_digest_resolves_known_image() { + // given + let config = test_launcher_config(); + let expected_digest: DockerSha256Digest = TEST_DIGEST.parse().unwrap(); + + // when + let result = get_manifest_digest(&config, &expected_digest).await; + + // then + assert!(result.is_ok(), "get_manifest_digest failed: {result:?}"); + } + + #[tokio::test] + async fn validate_image_hash_succeeds_for_known_image() { + // given + let config = test_launcher_config(); + let expected_digest: DockerSha256Digest = TEST_DIGEST.parse().unwrap(); + + // when + let result = validate_image_hash(&config, expected_digest).await; + + // then + assert!(result.is_ok(), "validate_image_hash failed: {result:?}"); + } } From 4281065823cd73210f740cf2a17b4b0886948ee0 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 11:17:38 +0100 Subject: [PATCH 038/176] rename to ApprovedHashes --- crates/launcher-interface/src/lib.rs | 8 +++---- .../src/tee/allowed_image_hashes_watcher.rs | 4 ++-- crates/tee-launcher/src/main.rs | 23 +++++++++++-------- 3 files changed, 19 insertions(+), 16 deletions(-) diff --git a/crates/launcher-interface/src/lib.rs b/crates/launcher-interface/src/lib.rs index 0d6fd5cb4..17d7e172e 100644 --- a/crates/launcher-interface/src/lib.rs +++ b/crates/launcher-interface/src/lib.rs @@ -9,11 +9,11 @@ pub mod types { /// JSON structure for the approved hashes file written by the MPC node, and read by the launcher. #[derive(Debug, Serialize, Deserialize)] - pub struct ApprovedHashesFile { + pub struct ApprovedHashes { pub approved_hashes: bounded_collections::NonEmptyVec, } - impl ApprovedHashesFile { + impl ApprovedHashes { pub fn newest_approved_hash(&self) -> &DockerSha256Digest { self.approved_hashes.first() } @@ -81,7 +81,7 @@ mod paths {} mod tests { use assert_matches::assert_matches; - use super::types::{ApprovedHashesFile, DockerSha256Digest, DockerDigestParseError}; + use super::types::{ApprovedHashes, DockerDigestParseError, DockerSha256Digest}; use mpc_primitives::hash::MpcDockerImageHash; fn sample_digest() -> DockerSha256Digest { @@ -164,7 +164,7 @@ mod tests { #[test] fn serialize_approved_hashes_file() { - let file = ApprovedHashesFile { + let file = ApprovedHashes { approved_hashes: bounded_collections::NonEmptyVec::from_vec(vec![sample_digest()]) .unwrap(), }; diff --git a/crates/node/src/tee/allowed_image_hashes_watcher.rs b/crates/node/src/tee/allowed_image_hashes_watcher.rs index 32bc37b62..2a5af11ad 100644 --- a/crates/node/src/tee/allowed_image_hashes_watcher.rs +++ b/crates/node/src/tee/allowed_image_hashes_watcher.rs @@ -1,7 +1,7 @@ use bounded_collections::NonEmptyVec; use derive_more::From; use itertools::Itertools; -use launcher_interface::types::{ApprovedHashesFile, DockerSha256Digest}; +use launcher_interface::types::{ApprovedHashes, DockerSha256Digest}; use mpc_contract::tee::proposal::MpcDockerImageHash; use std::{future::Future, io, panic, path::PathBuf}; use thiserror::Error; @@ -42,7 +42,7 @@ impl AllowedImageHashesStorage for AllowedImageHashesFile { "Writing approved MPC image hashes to disk (JSON format)." ); - let approved_hashes = ApprovedHashesFile { + let approved_hashes = ApprovedHashes { approved_hashes: approved_hashes.mapped(DockerSha256Digest::from), }; diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index d50982569..3bb4e8391 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -6,7 +6,7 @@ use std::{collections::VecDeque, time::Duration}; use backon::{ExponentialBuilder, Retryable}; use clap::Parser; use launcher_interface::MPC_IMAGE_HASH_EVENT; -use launcher_interface::types::{ApprovedHashesFile, DockerSha256Digest}; +use launcher_interface::types::{ApprovedHashes, DockerSha256Digest}; use constants::*; use docker_types::*; @@ -74,7 +74,7 @@ async fn run() -> Result<(), LauncherError> { source, }); - let approved_hashes_on_disk: Option = match approved_hashes_file { + let approved_hashes_on_disk: Option = match approved_hashes_file { Err(err) => { tracing::warn!( ?err, @@ -84,7 +84,7 @@ async fn run() -> Result<(), LauncherError> { None } Ok(file) => { - let parsed: ApprovedHashesFile = + let parsed: ApprovedHashes = serde_json::from_reader(file).map_err(|source| LauncherError::JsonParse { path: IMAGE_DIGEST_FILE.to_string(), source, @@ -136,7 +136,7 @@ async fn run() -> Result<(), LauncherError> { /// - If `override_hash` is set but NOT in the approved list → error /// - Otherwise → use the newest approved hash (first in the list) fn select_image_hash( - approved_hashes: Option<&ApprovedHashesFile>, + approved_hashes: Option<&ApprovedHashes>, default_digest: &DockerSha256Digest, override_hash: Option<&DockerSha256Digest>, ) -> Result { @@ -460,7 +460,7 @@ mod tests { use assert_matches::assert_matches; use bounded_collections::NonEmptyVec; - use launcher_interface::types::{ApprovedHashesFile, DockerSha256Digest}; + use launcher_interface::types::{ApprovedHashes, DockerSha256Digest}; use crate::constants::*; use crate::docker_run_args; @@ -469,17 +469,20 @@ mod tests { use crate::types::*; fn digest(hex_char: char) -> DockerSha256Digest { - format!("sha256:{}", std::iter::repeat_n(hex_char, 64).collect::()) - .parse() - .unwrap() + format!( + "sha256:{}", + std::iter::repeat_n(hex_char, 64).collect::() + ) + .parse() + .unwrap() } fn sample_digest() -> DockerSha256Digest { digest('a') } - fn approved_file(hashes: Vec) -> ApprovedHashesFile { - ApprovedHashesFile { + fn approved_file(hashes: Vec) -> ApprovedHashes { + ApprovedHashes { approved_hashes: NonEmptyVec::from_vec(hashes).unwrap(), } } From 154e27b4274be0ebf1d20b776391cc4fdb21c0ec Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 11:26:54 +0100 Subject: [PATCH 039/176] update docker build launcher --- .github/workflows/ci.yml | 12 +++++++++--- .github/workflows/docker_build_launcher.yml | 18 ++++++++++++------ deployment/build-images.sh | 3 +++ 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 20c0333ec..00d0600bb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -64,7 +64,7 @@ jobs: docker-launcher-build-and-verify: name: "Build MPC Launcher Docker image and verify" - runs-on: warp-ubuntu-2404-x64-2x + runs-on: warp-ubuntu-2404-x64-8x timeout-minutes: 60 permissions: contents: read @@ -75,10 +75,16 @@ jobs: with: persist-credentials: false - - name: Install skopeo + - name: Install build dependencies run: | sudo apt-get update - sudo apt-get install -y skopeo + sudo apt-get install -y skopeo liblzma-dev + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 + with: + save-if: ${{ github.ref == 'refs/heads/main' }} + cache-provider: "warpbuild" - name: Build launcher docker image and verify its hash shell: bash diff --git a/.github/workflows/docker_build_launcher.yml b/.github/workflows/docker_build_launcher.yml index 2cab91641..b0d69d76f 100644 --- a/.github/workflows/docker_build_launcher.yml +++ b/.github/workflows/docker_build_launcher.yml @@ -13,7 +13,7 @@ on: jobs: build-and-push-images: name: "Build and push Docker launcher image with commit hash" - runs-on: warp-ubuntu-2404-x64-2x + runs-on: warp-ubuntu-2404-x64-8x permissions: contents: read @@ -23,17 +23,23 @@ jobs: with: persist-credentials: false + - name: Install build dependencies + run: | + sudo apt-get update + sudo apt-get install -y skopeo liblzma-dev + + - name: Cache Rust dependencies + uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 + with: + save-if: ${{ github.ref == 'refs/heads/main' }} + cache-provider: "warpbuild" + - name: Login to Docker Hub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 with: username: ${{ secrets.DOCKERHUB_USERNAME }} password: ${{ secrets.DOCKERHUB_TOKEN }} - - name: Install skopeo - run: | - sudo apt-get update - sudo apt-get install -y skopeo - - name: Build and push launcher image run: | export LAUNCHER_IMAGE_NAME=mpc-launcher diff --git a/deployment/build-images.sh b/deployment/build-images.sh index 933531a66..cc4bfe02e 100755 --- a/deployment/build-images.sh +++ b/deployment/build-images.sh @@ -117,6 +117,8 @@ get_image_hash() { } if $USE_LAUNCHER; then + cargo build -p tee-launcher --release --locked + launcher_binary_hash=$(sha256sum target/release/tee-launcher | cut -d' ' -f1) build_reproducible_image $LAUNCHER_IMAGE_NAME $DOCKERFILE_LAUNCHER launcher_image_hash=$(get_image_hash $LAUNCHER_IMAGE_NAME) fi @@ -182,5 +184,6 @@ if $USE_NODE_GCP; then echo "node gcp docker image hash: $node_gcp_image_hash" fi if $USE_LAUNCHER; then + echo "launcher binary hash: $launcher_binary_hash" echo "launcher docker image hash: $launcher_image_hash" fi From a696478257301e130765290a1693dfce235c0b7b Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 11:28:01 +0100 Subject: [PATCH 040/176] update cargo lock --- Cargo.lock | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 98c4194f2..03775211b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4926,7 +4926,7 @@ dependencies = [ [[package]] name = "launcher-interface" -version = "3.5.1" +version = "3.6.0" dependencies = [ "assert_matches", "bounded-collections", @@ -10593,7 +10593,7 @@ dependencies = [ [[package]] name = "tee-launcher" -version = "3.5.1" +version = "3.6.0" dependencies = [ "assert_matches", "backon", From 98d85fb7784b53bcdbfe63fccd511e1468bdfb3f Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 11:30:26 +0100 Subject: [PATCH 041/176] undo hash.rs change --- crates/primitives/src/hash.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/crates/primitives/src/hash.rs b/crates/primitives/src/hash.rs index 7a65ddee0..4f7c6ba87 100644 --- a/crates/primitives/src/hash.rs +++ b/crates/primitives/src/hash.rs @@ -55,14 +55,6 @@ impl Hash32 { } } -impl MpcDockerImageHash { - /// Converts the hash to a hexadecimal string representation with a `sha256:` prefix - pub fn as_hex_sha256(&self) -> String { - let hex_encoding = self.as_hex(); - format!("sha256:{hex_encoding}") - } -} - #[derive(Error, Debug)] pub enum Hash32ParseError { #[error("not a valid hex string")] From 9d5d521bdd5512943d3c055c57ab7c48bb485b8c Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 13:58:57 +0100 Subject: [PATCH 042/176] use assert_matches! --- crates/tee-launcher/src/main.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 3bb4e8391..e11043af8 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -729,6 +729,7 @@ mod tests { #[cfg(all(test, feature = "integration-test"))] mod integration_tests { use super::*; + use assert_matches::assert_matches; const TEST_DIGEST: &str = "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372"; @@ -771,6 +772,6 @@ mod integration_tests { let result = validate_image_hash(&config, expected_digest).await; // then - assert!(result.is_ok(), "validate_image_hash failed: {result:?}"); + assert_matches!(result, Ok(_)); } } From b25072fcf90d40f8353585cb501557554a5ac48c Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 14:25:36 +0100 Subject: [PATCH 043/176] claude first pass --- crates/node/src/cli.rs | 714 ++++++++++---------------------- crates/node/src/config.rs | 3 + crates/node/src/config/start.rs | 80 ++++ crates/node/src/lib.rs | 1 + crates/node/src/run.rs | 321 ++++++++++++++ libs/nearcore | 2 +- 6 files changed, 629 insertions(+), 492 deletions(-) create mode 100644 crates/node/src/config/start.rs create mode 100644 crates/node/src/run.rs diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 3f976a547..b4ca08405 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -1,60 +1,30 @@ use crate::{ config::{ - generate_and_write_backup_encryption_key_to_disk, load_config_file, BlockArgs, CKDConfig, - ConfigFile, ForeignChainsConfig, IndexerConfig, KeygenConfig, PersistentSecrets, - PresignatureConfig, RespondConfig, SecretsConfig, SignatureConfig, SyncMode, TripleConfig, - }, - coordinator::Coordinator, - db::SecretDB, - indexer::{ - real::spawn_real_indexer, tx_sender::TransactionSender, IndexerAPI, ReadForeignChainPolicy, + BlockArgs, CKDConfig, ConfigFile, ForeignChainsConfig, IndexerConfig, KeygenConfig, + PersistentSecrets, PresignatureConfig, SignatureConfig, StartConfig, SyncMode, + TeeAuthorityStartConfig, TripleConfig, }, keyshare::{ compat::legacy_ecdsa_key_from_keyshares, local::LocalPermanentKeyStorageBackend, permanent::{PermanentKeyStorage, PermanentKeyStorageBackend, PermanentKeyshareData}, - GcpPermanentKeyStorageConfig, KeyStorageConfig, KeyshareStorage, }, - migration_service::spawn_recovery_server_and_run_onboarding, p2p::testing::{generate_test_p2p_configs, PortSeed}, - profiler, - tracking::{self, start_root_task}, - web::{start_web_server, static_web_data, DebugRequest}, }; -use anyhow::{anyhow, Context}; use clap::{Args, Parser, Subcommand, ValueEnum}; use hex::FromHex; -use mpc_attestation::report_data::ReportDataV1; -use mpc_contract::state::ProtocolContractState; use near_account_id::AccountId; use near_indexer_primitives::types::Finality; -use near_time::Clock; use std::{ - collections::BTreeMap, net::{Ipv4Addr, SocketAddr}, - sync::Mutex, -}; -use std::{path::PathBuf, sync::Arc, sync::OnceLock, time::Duration}; -use tee_authority::tee_authority::{ - DstackTeeAuthorityConfig, LocalTeeAuthorityConfig, TeeAuthority, DEFAULT_DSTACK_ENDPOINT, - DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL, + path::PathBuf, }; -use tokio::sync::{broadcast, mpsc, oneshot, watch, RwLock}; -use tokio_util::sync::CancellationToken; +use tee_authority::tee_authority::{DEFAULT_DSTACK_ENDPOINT, DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL}; use url::Url; -use contract_interface::types::Ed25519PublicKey; -use { - crate::tee::{ - monitor_allowed_image_hashes, - remote_attestation::{monitor_attestation_removal, periodic_attestation_submission}, - AllowedImageHashesFile, - }, - mpc_contract::tee::proposal::MpcDockerImageHash, - tracing::info, -}; - -pub const ATTESTATION_RESUBMISSION_INTERVAL: Duration = Duration::from_secs(60 * 60); // 1 hour +// --------------------------------------------------------------------------- +// Top-level CLI +// --------------------------------------------------------------------------- #[derive(Parser, Debug)] #[command(name = "mpc-node")] @@ -77,6 +47,13 @@ pub enum LogFormat { #[derive(Subcommand, Debug)] pub enum CliCommand { + /// Starts the MPC node using a single JSON configuration file instead of + /// environment variables and CLI flags. + StartWithConfigFile { + /// Path to a JSON configuration file containing all settings needed to + /// start the MPC node. + config_path: PathBuf, + }, Start(StartCmd), /// Generates/downloads required files for Near node to run Init(InitConfigArgs), @@ -109,32 +86,9 @@ pub enum CliCommand { }, } -#[derive(Args, Debug)] -pub struct InitConfigArgs { - #[arg(long, env("MPC_HOME_DIR"))] - pub dir: std::path::PathBuf, - /// chain/network id (localnet, testnet, devnet, betanet) - #[arg(long)] - pub chain_id: Option, - /// Genesis file to use when initialize testnet (including downloading) - #[arg(long)] - pub genesis: Option, - /// Download the verified NEAR config file automatically. - #[arg(long)] - pub download_config: bool, - #[arg(long)] - pub download_config_url: Option, - /// Download the verified NEAR genesis file automatically. - #[arg(long)] - pub download_genesis: bool, - /// Specify a custom download URL for the genesis-file. - #[arg(long)] - pub download_genesis_url: Option, - #[arg(long)] - pub download_genesis_records_url: Option, - #[arg(long)] - pub boot_nodes: Option, -} +// --------------------------------------------------------------------------- +// Start subcommand (CLI flags / env vars) +// --------------------------------------------------------------------------- #[derive(Args, Debug)] pub struct StartCmd { @@ -153,17 +107,17 @@ pub struct StartCmd { pub gcp_project_id: Option, /// TEE authority config #[command(subcommand)] - pub tee_authority: TeeAuthorityConfig, + pub tee_authority: CliTeeAuthorityConfig, /// TEE related configuration settings. #[command(flatten)] - pub image_hash_config: MpcImageHashConfig, + pub image_hash_config: CliImageHashConfig, /// Hex-encoded 32 byte AES key for backup encryption. #[arg(env("MPC_BACKUP_ENCRYPTION_KEY_HEX"))] pub backup_encryption_key_hex: Option, } #[derive(Subcommand, Debug, Clone)] -pub enum TeeAuthorityConfig { +pub enum CliTeeAuthorityConfig { Local, Dstack { #[arg(long, env("DSTACK_ENDPOINT"), default_value = DEFAULT_DSTACK_ENDPOINT)] @@ -173,24 +127,8 @@ pub enum TeeAuthorityConfig { }, } -impl TryFrom for TeeAuthority { - type Error = anyhow::Error; - - fn try_from(cmd: TeeAuthorityConfig) -> Result { - let authority_config = match cmd { - TeeAuthorityConfig::Local => LocalTeeAuthorityConfig::default().into(), - TeeAuthorityConfig::Dstack { - dstack_endpoint, - quote_upload_url, - } => DstackTeeAuthorityConfig::new(dstack_endpoint, quote_upload_url).into(), - }; - - Ok(authority_config) - } -} - #[derive(Args, Debug)] -pub struct MpcImageHashConfig { +pub struct CliImageHashConfig { #[arg( long, env("MPC_IMAGE_HASH"), @@ -205,6 +143,65 @@ pub struct MpcImageHashConfig { pub latest_allowed_hash_file: Option, } +impl From for StartConfig { + fn from(cmd: StartCmd) -> Self { + StartConfig { + home_dir: cmd.home_dir, + secret_store_key_hex: cmd.secret_store_key_hex, + gcp_keyshare_secret_id: cmd.gcp_keyshare_secret_id, + gcp_project_id: cmd.gcp_project_id, + tee_authority: match cmd.tee_authority { + CliTeeAuthorityConfig::Local => TeeAuthorityStartConfig::Local, + CliTeeAuthorityConfig::Dstack { + dstack_endpoint, + quote_upload_url, + } => TeeAuthorityStartConfig::Dstack { + dstack_endpoint, + quote_upload_url: quote_upload_url.to_string(), + }, + }, + image_hash: cmd.image_hash_config.image_hash, + latest_allowed_hash_file: cmd.image_hash_config.latest_allowed_hash_file, + backup_encryption_key_hex: cmd.backup_encryption_key_hex, + } + } +} + +// --------------------------------------------------------------------------- +// Init subcommand +// --------------------------------------------------------------------------- + +#[derive(Args, Debug)] +pub struct InitConfigArgs { + #[arg(long, env("MPC_HOME_DIR"))] + pub dir: std::path::PathBuf, + /// chain/network id (localnet, testnet, devnet, betanet) + #[arg(long)] + pub chain_id: Option, + /// Genesis file to use when initialize testnet (including downloading) + #[arg(long)] + pub genesis: Option, + /// Download the verified NEAR config file automatically. + #[arg(long)] + pub download_config: bool, + #[arg(long)] + pub download_config_url: Option, + /// Download the verified NEAR genesis file automatically. + #[arg(long)] + pub download_genesis: bool, + /// Specify a custom download URL for the genesis-file. + #[arg(long)] + pub download_genesis_url: Option, + #[arg(long)] + pub download_genesis_records_url: Option, + #[arg(long)] + pub boot_nodes: Option, +} + +// --------------------------------------------------------------------------- +// Import/Export keyshare subcommands +// --------------------------------------------------------------------------- + #[derive(Args, Debug)] pub struct ImportKeyshareCmd { /// Path to home directory @@ -233,292 +230,17 @@ pub struct ExportKeyshareCmd { pub local_encryption_key_hex: String, } -impl StartCmd { - async fn run(self) -> anyhow::Result<()> { - let root_runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .worker_threads(1) - .build()?; - - let _tokio_enter_guard = root_runtime.enter(); - - // Load configuration and initialize persistent secrets - let home_dir = PathBuf::from(self.home_dir.clone()); - let config = load_config_file(&home_dir)?; - let persistent_secrets = PersistentSecrets::generate_or_get_existing( - &home_dir, - config.number_of_responder_keys, - )?; - - profiler::web_server::start_web_server(config.pprof_bind_address).await?; - root_runtime.spawn(crate::metrics::tokio_task_metrics::run_monitor_loop()); - - // TODO(#1296): Decide if the MPC responder account is actually needed - let respond_config = RespondConfig::from_parts(&config, &persistent_secrets); - - let backup_encryption_key_hex = match &self.backup_encryption_key_hex { - Some(key) => key.clone(), - None => generate_and_write_backup_encryption_key_to_disk(&home_dir)?, - }; - - // Load secrets from configuration and persistent storage - let secrets = SecretsConfig::from_parts( - &self.secret_store_key_hex, - persistent_secrets.clone(), - &backup_encryption_key_hex, - )?; - - // Generate attestation - let tee_authority = TeeAuthority::try_from(self.tee_authority.clone())?; - let tls_public_key = &secrets.persistent_secrets.p2p_private_key.verifying_key(); - - let account_public_key = &secrets.persistent_secrets.near_signer_key.verifying_key(); - - let report_data = ReportDataV1::new( - *Ed25519PublicKey::from(tls_public_key).as_bytes(), - *Ed25519PublicKey::from(account_public_key).as_bytes(), - ) - .into(); - - let attestation = tee_authority.generate_attestation(report_data).await?; - - // Create communication channels and runtime - let (debug_request_sender, _) = tokio::sync::broadcast::channel(10); - let root_task_handle = Arc::new(OnceLock::new()); - - let (protocol_state_sender, protocol_state_receiver) = - watch::channel(ProtocolContractState::NotInitialized); - - let (migration_state_sender, migration_state_receiver) = - watch::channel((0, BTreeMap::new())); - let web_server = root_runtime - .block_on(start_web_server( - root_task_handle.clone(), - debug_request_sender.clone(), - config.web_ui, - static_web_data(&secrets, Some(attestation)), - protocol_state_receiver, - migration_state_receiver, - )) - .context("Failed to create web server.")?; - - let _web_server_join_handle = root_runtime.spawn(web_server); - - // Create Indexer and wait for indexer to be synced. - let (indexer_exit_sender, indexer_exit_receiver) = oneshot::channel(); - let indexer_api = spawn_real_indexer( - home_dir.clone(), - config.indexer.clone(), - config.my_near_account_id.clone(), - persistent_secrets.near_signer_key.clone(), - respond_config, - indexer_exit_sender, - protocol_state_sender, - migration_state_sender, - *tls_public_key, - ); - - let (shutdown_signal_sender, mut shutdown_signal_receiver) = mpsc::channel(1); - let cancellation_token = CancellationToken::new(); - - let image_hash_watcher_handle = if let (Some(image_hash), Some(latest_allowed_hash_file)) = ( - &self.image_hash_config.image_hash, - &self.image_hash_config.latest_allowed_hash_file, - ) { - let current_image_hash_bytes: [u8; 32] = hex::decode(image_hash) - .expect("The currently running image is a hex string.") - .try_into() - .expect("The currently running image hash hex representation is 32 bytes."); - - let allowed_hashes_in_contract = indexer_api.allowed_docker_images_receiver.clone(); - let image_hash_storage = AllowedImageHashesFile::from(latest_allowed_hash_file.clone()); - - Some(root_runtime.spawn(monitor_allowed_image_hashes( - cancellation_token.child_token(), - MpcDockerImageHash::from(current_image_hash_bytes), - allowed_hashes_in_contract, - image_hash_storage, - shutdown_signal_sender.clone(), - ))) - } else { - tracing::info!( - "MPC_IMAGE_HASH and/or MPC_LATEST_ALLOWED_HASH_FILE not set, skipping TEE image hash monitoring" - ); - None - }; - - let root_future = self.create_root_future( - home_dir.clone(), - config.clone(), - secrets.clone(), - indexer_api, - debug_request_sender, - root_task_handle, - tee_authority, - ); - - let root_task = root_runtime.spawn(start_root_task("root", root_future).0); - - let exit_reason = tokio::select! { - root_task_result = root_task => { - root_task_result? - } - indexer_exit_response = indexer_exit_receiver => { - indexer_exit_response.context("Indexer thread dropped response channel.")? - } - Some(()) = shutdown_signal_receiver.recv() => { - Err(anyhow!("TEE allowed image hashes watcher is sending shutdown signal.")) - } - }; - - // Perform graceful shutdown - cancellation_token.cancel(); - - if let Some(handle) = image_hash_watcher_handle { - info!("Waiting for image hash watcher to gracefully exit."); - let exit_result = handle.await; - info!(?exit_result, "Image hash watcher exited."); - } - - exit_reason - } - - #[allow(clippy::too_many_arguments)] - async fn create_root_future( - self, - home_dir: PathBuf, - config: ConfigFile, - secrets: SecretsConfig, - indexer_api: IndexerAPI, - debug_request_sender: broadcast::Sender, - // Cloning a OnceLock returns a new cell, which is why we have to wrap it in an arc. - // Otherwise we would not write to the same cell/lock. - root_task_handle_once_lock: Arc>>, - tee_authority: TeeAuthority, - ) -> anyhow::Result<()> - where - TransactionSenderImpl: TransactionSender + 'static, - ForeignChainPolicyReader: ReadForeignChainPolicy + Clone + Send + Sync + 'static, - { - let root_task_handle = tracking::current_task(); - - root_task_handle_once_lock - .set(root_task_handle.clone()) - .map_err(|_| anyhow!("Root task handle was already set"))?; - - let tls_public_key = - Ed25519PublicKey::from(&secrets.persistent_secrets.p2p_private_key.verifying_key()); - let account_public_key = - Ed25519PublicKey::from(&secrets.persistent_secrets.near_signer_key.verifying_key()); - - let secret_db = SecretDB::new(&home_dir.join("assets"), secrets.local_storage_aes_key)?; - - let key_storage_config = KeyStorageConfig { - home_dir: home_dir.clone(), - local_encryption_key: secrets.local_storage_aes_key, - gcp: if let Some(secret_id) = self.gcp_keyshare_secret_id { - let project_id = self.gcp_project_id.ok_or_else(|| { - anyhow::anyhow!( - "GCP_PROJECT_ID must be specified to use GCP_KEYSHARE_SECRET_ID" - ) - })?; - Some(GcpPermanentKeyStorageConfig { - project_id, - secret_id, - }) - } else { - None - }, - }; - - // Spawn periodic attestation submission task - let tee_authority_clone = tee_authority.clone(); - let tx_sender_clone = indexer_api.txn_sender.clone(); - let tls_public_key_clone = tls_public_key.clone(); - let account_public_key_clone = account_public_key.clone(); - let allowed_docker_images_receiver_clone = - indexer_api.allowed_docker_images_receiver.clone(); - let allowed_launcher_compose_receiver_clone = - indexer_api.allowed_launcher_compose_receiver.clone(); - tokio::spawn(async move { - if let Err(e) = periodic_attestation_submission( - tee_authority_clone, - tx_sender_clone, - tls_public_key_clone, - account_public_key_clone, - allowed_docker_images_receiver_clone, - allowed_launcher_compose_receiver_clone, - tokio::time::interval(ATTESTATION_RESUBMISSION_INTERVAL), - ) - .await - { - tracing::error!( - error = ?e, - "periodic attestation submission task failed" - ); - } - }); - - // Spawn TEE attestation monitoring task - let tx_sender_clone = indexer_api.txn_sender.clone(); - let tee_accounts_receiver = indexer_api.attested_nodes_receiver.clone(); - let account_id_clone = config.my_near_account_id.clone(); - let allowed_docker_images_receiver_clone = - indexer_api.allowed_docker_images_receiver.clone(); - let allowed_launcher_compose_receiver_clone = - indexer_api.allowed_launcher_compose_receiver.clone(); - tokio::spawn(async move { - if let Err(e) = monitor_attestation_removal( - account_id_clone, - tee_authority, - tx_sender_clone, - tls_public_key, - account_public_key, - allowed_docker_images_receiver_clone, - allowed_launcher_compose_receiver_clone, - tee_accounts_receiver, - ) - .await - { - tracing::error!( - error = ?e, - "attestation removal monitoring task failed" - ); - } - }); - - let keyshare_storage: Arc> = - RwLock::new(key_storage_config.create().await?).into(); - - spawn_recovery_server_and_run_onboarding( - config.migration_web_ui, - (&secrets).into(), - config.my_near_account_id.clone(), - keyshare_storage.clone(), - indexer_api.my_migration_info_receiver.clone(), - indexer_api.contract_state_receiver.clone(), - indexer_api.txn_sender.clone(), - ) - .await?; - - let coordinator = Coordinator { - clock: Clock::real(), - config_file: config, - secrets, - secret_db, - keyshare_storage, - indexer: indexer_api, - currently_running_job_name: Arc::new(Mutex::new(String::new())), - debug_request_sender, - }; - coordinator.run().await - } -} +// --------------------------------------------------------------------------- +// Dispatch +// --------------------------------------------------------------------------- impl Cli { pub async fn run(self) -> anyhow::Result<()> { match self.command { - CliCommand::Start(start) => start.run().await, + CliCommand::StartWithConfigFile { config_path } => { + StartConfig::from_json_file(&config_path)?.run().await + } + CliCommand::Start(start) => StartConfig::from(start).run().await, CliCommand::Init(config) => { let (download_config_type, download_config_url) = if config.download_config { ( @@ -565,7 +287,7 @@ impl Cli { participants.len() == responders.len(), "Number of participants must match number of responders" ); - self.run_generate_test_configs( + run_generate_test_configs( output_dir, participants.clone(), responders.clone(), @@ -578,132 +300,12 @@ impl Cli { } } } - - fn duplicate_migrating_accounts( - mut accounts: Vec, - migrating_nodes: &[usize], - ) -> anyhow::Result> { - for migrating_node_idx in migrating_nodes { - let migrating_node_account: AccountId = accounts - .get(*migrating_node_idx) - .ok_or_else(|| { - anyhow::anyhow!("index {} out of bounds for accounts", migrating_node_idx) - })? - .clone(); - - accounts.push(migrating_node_account); - } - Ok(accounts) - } - - #[allow(clippy::too_many_arguments)] - fn run_generate_test_configs( - &self, - output_dir: &str, - participants: Vec, - responders: Vec, - threshold: usize, - desired_triples_to_buffer: usize, - desired_presignatures_to_buffer: usize, - desired_responder_keys_per_participant: usize, - migrating_nodes: &[usize], - ) -> anyhow::Result<()> { - let participants = Self::duplicate_migrating_accounts(participants, migrating_nodes)?; - let responders = Self::duplicate_migrating_accounts(responders, migrating_nodes)?; - - let p2p_key_pairs = participants - .iter() - .enumerate() - .map(|(idx, _account_id)| { - let subdir = PathBuf::from(output_dir).join(idx.to_string()); - PersistentSecrets::generate_or_get_existing( - &subdir, - desired_responder_keys_per_participant, - ) - .map(|secret| secret.p2p_private_key) - }) - .collect::, _>>()?; - let configs = generate_test_p2p_configs( - &participants, - threshold, - PortSeed::CLI_FOR_PYTEST, - Some(p2p_key_pairs), - )?; - let participants_config = configs[0].0.participants.clone(); - for (i, (_config, _p2p_private_key)) in configs.into_iter().enumerate() { - let subdir = format!("{}/{}", output_dir, i); - std::fs::create_dir_all(&subdir)?; - let file_config = self.create_file_config( - &participants[i], - &responders[i], - i, - desired_triples_to_buffer, - desired_presignatures_to_buffer, - )?; - std::fs::write( - format!("{}/config.yaml", subdir), - serde_yaml::to_string(&file_config)?, - )?; - } - std::fs::write( - format!("{}/participants.json", output_dir), - serde_json::to_string(&participants_config)?, - )?; - Ok(()) - } - - fn create_file_config( - &self, - participant: &AccountId, - responder: &AccountId, - index: usize, - desired_triples_to_buffer: usize, - desired_presignatures_to_buffer: usize, - ) -> anyhow::Result { - Ok(ConfigFile { - my_near_account_id: participant.clone(), - near_responder_account_id: responder.clone(), - number_of_responder_keys: 1, - web_ui: SocketAddr::new( - Ipv4Addr::LOCALHOST.into(), - PortSeed::CLI_FOR_PYTEST.web_port(index), - ), - migration_web_ui: SocketAddr::new( - Ipv4Addr::LOCALHOST.into(), - PortSeed::CLI_FOR_PYTEST.migration_web_port(index), - ), - pprof_bind_address: SocketAddr::new( - Ipv4Addr::LOCALHOST.into(), - PortSeed::CLI_FOR_PYTEST.pprof_web_port(index), - ), - indexer: IndexerConfig { - validate_genesis: true, - sync_mode: SyncMode::Block(BlockArgs { height: 0 }), - concurrency: 1.try_into().unwrap(), - mpc_contract_id: "test0".parse().unwrap(), - finality: Finality::None, - port_override: None, - }, - triple: TripleConfig { - concurrency: 2, - desired_triples_to_buffer, - timeout_sec: 60, - parallel_triple_generation_stagger_time_sec: 1, - }, - presignature: PresignatureConfig { - concurrency: 2, - desired_presignatures_to_buffer, - timeout_sec: 60, - }, - signature: SignatureConfig { timeout_sec: 60 }, - ckd: CKDConfig { timeout_sec: 60 }, - keygen: KeygenConfig { timeout_sec: 60 }, - foreign_chains: ForeignChainsConfig::default(), - cores: Some(4), - }) - } } +// --------------------------------------------------------------------------- +// Import/Export keyshare implementations +// --------------------------------------------------------------------------- + impl ImportKeyshareCmd { pub async fn run(&self) -> anyhow::Result<()> { let runtime = tokio::runtime::Runtime::new()?; @@ -801,6 +403,136 @@ impl ExportKeyshareCmd { } } +// --------------------------------------------------------------------------- +// Test config generation +// --------------------------------------------------------------------------- + +fn duplicate_migrating_accounts( + mut accounts: Vec, + migrating_nodes: &[usize], +) -> anyhow::Result> { + for migrating_node_idx in migrating_nodes { + let migrating_node_account: AccountId = accounts + .get(*migrating_node_idx) + .ok_or_else(|| { + anyhow::anyhow!("index {} out of bounds for accounts", migrating_node_idx) + })? + .clone(); + + accounts.push(migrating_node_account); + } + Ok(accounts) +} + +#[allow(clippy::too_many_arguments)] +fn run_generate_test_configs( + output_dir: &str, + participants: Vec, + responders: Vec, + threshold: usize, + desired_triples_to_buffer: usize, + desired_presignatures_to_buffer: usize, + desired_responder_keys_per_participant: usize, + migrating_nodes: &[usize], +) -> anyhow::Result<()> { + let participants = duplicate_migrating_accounts(participants, migrating_nodes)?; + let responders = duplicate_migrating_accounts(responders, migrating_nodes)?; + + let p2p_key_pairs = participants + .iter() + .enumerate() + .map(|(idx, _account_id)| { + let subdir = PathBuf::from(output_dir).join(idx.to_string()); + PersistentSecrets::generate_or_get_existing( + &subdir, + desired_responder_keys_per_participant, + ) + .map(|secret| secret.p2p_private_key) + }) + .collect::, _>>()?; + let configs = generate_test_p2p_configs( + &participants, + threshold, + PortSeed::CLI_FOR_PYTEST, + Some(p2p_key_pairs), + )?; + let participants_config = configs[0].0.participants.clone(); + for (i, (_config, _p2p_private_key)) in configs.into_iter().enumerate() { + let subdir = format!("{}/{}", output_dir, i); + std::fs::create_dir_all(&subdir)?; + let file_config = create_file_config( + &participants[i], + &responders[i], + i, + desired_triples_to_buffer, + desired_presignatures_to_buffer, + ); + std::fs::write( + format!("{}/config.yaml", subdir), + serde_yaml::to_string(&file_config)?, + )?; + } + std::fs::write( + format!("{}/participants.json", output_dir), + serde_json::to_string(&participants_config)?, + )?; + Ok(()) +} + +fn create_file_config( + participant: &AccountId, + responder: &AccountId, + index: usize, + desired_triples_to_buffer: usize, + desired_presignatures_to_buffer: usize, +) -> ConfigFile { + ConfigFile { + my_near_account_id: participant.clone(), + near_responder_account_id: responder.clone(), + number_of_responder_keys: 1, + web_ui: SocketAddr::new( + Ipv4Addr::LOCALHOST.into(), + PortSeed::CLI_FOR_PYTEST.web_port(index), + ), + migration_web_ui: SocketAddr::new( + Ipv4Addr::LOCALHOST.into(), + PortSeed::CLI_FOR_PYTEST.migration_web_port(index), + ), + pprof_bind_address: SocketAddr::new( + Ipv4Addr::LOCALHOST.into(), + PortSeed::CLI_FOR_PYTEST.pprof_web_port(index), + ), + indexer: IndexerConfig { + validate_genesis: true, + sync_mode: SyncMode::Block(BlockArgs { height: 0 }), + concurrency: 1.try_into().unwrap(), + mpc_contract_id: "test0".parse().unwrap(), + finality: Finality::None, + port_override: None, + }, + triple: TripleConfig { + concurrency: 2, + desired_triples_to_buffer, + timeout_sec: 60, + parallel_triple_generation_stagger_time_sec: 1, + }, + presignature: PresignatureConfig { + concurrency: 2, + desired_presignatures_to_buffer, + timeout_sec: 60, + }, + signature: SignatureConfig { timeout_sec: 60 }, + ckd: CKDConfig { timeout_sec: 60 }, + keygen: KeygenConfig { timeout_sec: 60 }, + foreign_chains: ForeignChainsConfig::default(), + cores: Some(4), + } +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + #[cfg(test)] mod tests { use super::*; diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index fdabd98bf..9969e1464 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -13,6 +13,9 @@ use std::{ path::Path, }; +mod start; +pub use start::{StartConfig, TeeAuthorityStartConfig}; + mod foreign_chains; pub use foreign_chains::{ AbstractApiVariant, AbstractChainConfig, AbstractProviderConfig, AuthConfig, BitcoinApiVariant, diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs new file mode 100644 index 000000000..29a6b3a9f --- /dev/null +++ b/crates/node/src/config/start.rs @@ -0,0 +1,80 @@ +use anyhow::Context; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use tee_authority::tee_authority::{ + DstackTeeAuthorityConfig, LocalTeeAuthorityConfig, TeeAuthority, DEFAULT_DSTACK_ENDPOINT, + DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL, +}; +use url::Url; + +/// Configuration for starting the MPC node. This is the canonical type used +/// by the run logic. Both `StartCmd` (CLI flags) and `StartWithConfigFileCmd` +/// (JSON file) convert into this type. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StartConfig { + pub home_dir: String, + /// Hex-encoded 16 byte AES key for local storage encryption. + pub secret_store_key_hex: String, + /// If provided, the root keyshare is stored on GCP. + #[serde(default)] + pub gcp_keyshare_secret_id: Option, + #[serde(default)] + pub gcp_project_id: Option, + /// TEE authority configuration. + pub tee_authority: TeeAuthorityStartConfig, + /// Hex representation of the hash of the running image. Only required in TEE. + #[serde(default)] + pub image_hash: Option, + /// Path to the file where the node writes the latest allowed hash. + /// If not set, assumes running outside of TEE and skips image hash monitoring. + #[serde(default)] + pub latest_allowed_hash_file: Option, + /// Hex-encoded 32 byte AES key for backup encryption. + #[serde(default)] + pub backup_encryption_key_hex: Option, +} + +/// TEE authority configuration for JSON deserialization. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum TeeAuthorityStartConfig { + Local, + Dstack { + #[serde(default = "default_dstack_endpoint")] + dstack_endpoint: String, + #[serde(default = "default_quote_upload_url")] + quote_upload_url: String, + }, +} + +fn default_dstack_endpoint() -> String { + DEFAULT_DSTACK_ENDPOINT.to_string() +} + +fn default_quote_upload_url() -> String { + DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL.to_string() +} + +impl TeeAuthorityStartConfig { + pub fn into_tee_authority(self) -> anyhow::Result { + Ok(match self { + TeeAuthorityStartConfig::Local => LocalTeeAuthorityConfig::default().into(), + TeeAuthorityStartConfig::Dstack { + dstack_endpoint, + quote_upload_url, + } => { + let url: Url = quote_upload_url.parse().context("invalid quote_upload_url")?; + DstackTeeAuthorityConfig::new(dstack_endpoint, url).into() + } + }) + } +} + +impl StartConfig { + pub fn from_json_file(path: &std::path::Path) -> anyhow::Result { + let content = std::fs::read_to_string(path) + .with_context(|| format!("failed to read config file: {}", path.display()))?; + serde_json::from_str(&content) + .with_context(|| format!("failed to parse config file: {}", path.display())) + } +} diff --git a/crates/node/src/lib.rs b/crates/node/src/lib.rs index 71778f6fe..dffd498fe 100644 --- a/crates/node/src/lib.rs +++ b/crates/node/src/lib.rs @@ -39,6 +39,7 @@ mod protocol; mod protocol_version; mod providers; pub mod requests; +mod run; mod runtime; mod storage; pub mod tracing; diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs new file mode 100644 index 000000000..127c749ef --- /dev/null +++ b/crates/node/src/run.rs @@ -0,0 +1,321 @@ +use crate::{ + config::{ + generate_and_write_backup_encryption_key_to_disk, load_config_file, ConfigFile, + PersistentSecrets, RespondConfig, SecretsConfig, StartConfig, + }, + coordinator::Coordinator, + db::SecretDB, + indexer::{ + real::spawn_real_indexer, tx_sender::TransactionSender, IndexerAPI, ReadForeignChainPolicy, + }, + keyshare::{GcpPermanentKeyStorageConfig, KeyStorageConfig, KeyshareStorage}, + migration_service::spawn_recovery_server_and_run_onboarding, + profiler, + tracking::{self, start_root_task}, + web::{start_web_server, static_web_data, DebugRequest}, +}; +use anyhow::{anyhow, Context}; +use contract_interface::types::Ed25519PublicKey; +use mpc_attestation::report_data::ReportDataV1; +use mpc_contract::state::ProtocolContractState; +use mpc_contract::tee::proposal::MpcDockerImageHash; +use near_time::Clock; +use std::{ + collections::BTreeMap, + path::PathBuf, + sync::{Arc, Mutex, OnceLock}, + time::Duration, +}; +use tee_authority::tee_authority::TeeAuthority; +use tokio::sync::{broadcast, mpsc, oneshot, watch, RwLock}; +use tokio_util::sync::CancellationToken; +use tracing::info; + +use crate::tee::{ + monitor_allowed_image_hashes, + remote_attestation::{monitor_attestation_removal, periodic_attestation_submission}, + AllowedImageHashesFile, +}; + +pub const ATTESTATION_RESUBMISSION_INTERVAL: Duration = Duration::from_secs(60 * 60); // 1 hour + +impl StartConfig { + pub async fn run(self) -> anyhow::Result<()> { + let root_runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .worker_threads(1) + .build()?; + + let _tokio_enter_guard = root_runtime.enter(); + + // Load configuration and initialize persistent secrets + let home_dir = PathBuf::from(self.home_dir.clone()); + let config = load_config_file(&home_dir)?; + let persistent_secrets = PersistentSecrets::generate_or_get_existing( + &home_dir, + config.number_of_responder_keys, + )?; + + profiler::web_server::start_web_server(config.pprof_bind_address).await?; + root_runtime.spawn(crate::metrics::tokio_task_metrics::run_monitor_loop()); + + // TODO(#1296): Decide if the MPC responder account is actually needed + let respond_config = RespondConfig::from_parts(&config, &persistent_secrets); + + let backup_encryption_key_hex = match &self.backup_encryption_key_hex { + Some(key) => key.clone(), + None => generate_and_write_backup_encryption_key_to_disk(&home_dir)?, + }; + + // Load secrets from configuration and persistent storage + let secrets = SecretsConfig::from_parts( + &self.secret_store_key_hex, + persistent_secrets.clone(), + &backup_encryption_key_hex, + )?; + + // Generate attestation + let tee_authority = self.tee_authority.clone().into_tee_authority()?; + let tls_public_key = &secrets.persistent_secrets.p2p_private_key.verifying_key(); + + let account_public_key = &secrets.persistent_secrets.near_signer_key.verifying_key(); + + let report_data = ReportDataV1::new( + *Ed25519PublicKey::from(tls_public_key).as_bytes(), + *Ed25519PublicKey::from(account_public_key).as_bytes(), + ) + .into(); + + let attestation = tee_authority.generate_attestation(report_data).await?; + + // Create communication channels and runtime + let (debug_request_sender, _) = tokio::sync::broadcast::channel(10); + let root_task_handle = Arc::new(OnceLock::new()); + + let (protocol_state_sender, protocol_state_receiver) = + watch::channel(ProtocolContractState::NotInitialized); + + let (migration_state_sender, migration_state_receiver) = + watch::channel((0, BTreeMap::new())); + let web_server = root_runtime + .block_on(start_web_server( + root_task_handle.clone(), + debug_request_sender.clone(), + config.web_ui, + static_web_data(&secrets, Some(attestation)), + protocol_state_receiver, + migration_state_receiver, + )) + .context("Failed to create web server.")?; + + let _web_server_join_handle = root_runtime.spawn(web_server); + + // Create Indexer and wait for indexer to be synced. + let (indexer_exit_sender, indexer_exit_receiver) = oneshot::channel(); + let indexer_api = spawn_real_indexer( + home_dir.clone(), + config.indexer.clone(), + config.my_near_account_id.clone(), + persistent_secrets.near_signer_key.clone(), + respond_config, + indexer_exit_sender, + protocol_state_sender, + migration_state_sender, + *tls_public_key, + ); + + let (shutdown_signal_sender, mut shutdown_signal_receiver) = mpsc::channel(1); + let cancellation_token = CancellationToken::new(); + + let image_hash_watcher_handle = + if let (Some(image_hash), Some(latest_allowed_hash_file)) = + (&self.image_hash, &self.latest_allowed_hash_file) + { + let current_image_hash_bytes: [u8; 32] = hex::decode(image_hash) + .expect("The currently running image is a hex string.") + .try_into() + .expect("The currently running image hash hex representation is 32 bytes."); + + let allowed_hashes_in_contract = + indexer_api.allowed_docker_images_receiver.clone(); + let image_hash_storage = + AllowedImageHashesFile::from(latest_allowed_hash_file.clone()); + + Some(root_runtime.spawn(monitor_allowed_image_hashes( + cancellation_token.child_token(), + MpcDockerImageHash::from(current_image_hash_bytes), + allowed_hashes_in_contract, + image_hash_storage, + shutdown_signal_sender.clone(), + ))) + } else { + tracing::info!( + "MPC_IMAGE_HASH and/or MPC_LATEST_ALLOWED_HASH_FILE not set, skipping TEE image hash monitoring" + ); + None + }; + + let root_future = create_root_future( + self, + home_dir.clone(), + config.clone(), + secrets.clone(), + indexer_api, + debug_request_sender, + root_task_handle, + tee_authority, + ); + + let root_task = root_runtime.spawn(start_root_task("root", root_future).0); + + let exit_reason = tokio::select! { + root_task_result = root_task => { + root_task_result? + } + indexer_exit_response = indexer_exit_receiver => { + indexer_exit_response.context("Indexer thread dropped response channel.")? + } + Some(()) = shutdown_signal_receiver.recv() => { + Err(anyhow!("TEE allowed image hashes watcher is sending shutdown signal.")) + } + }; + + // Perform graceful shutdown + cancellation_token.cancel(); + + if let Some(handle) = image_hash_watcher_handle { + info!("Waiting for image hash watcher to gracefully exit."); + let exit_result = handle.await; + info!(?exit_result, "Image hash watcher exited."); + } + + exit_reason + } +} + +#[allow(clippy::too_many_arguments)] +async fn create_root_future( + start_config: StartConfig, + home_dir: PathBuf, + config: ConfigFile, + secrets: SecretsConfig, + indexer_api: IndexerAPI, + debug_request_sender: broadcast::Sender, + // Cloning a OnceLock returns a new cell, which is why we have to wrap it in an arc. + // Otherwise we would not write to the same cell/lock. + root_task_handle_once_lock: Arc>>, + tee_authority: TeeAuthority, +) -> anyhow::Result<()> +where + TransactionSenderImpl: TransactionSender + 'static, + ForeignChainPolicyReader: ReadForeignChainPolicy + Clone + Send + Sync + 'static, +{ + let root_task_handle = tracking::current_task(); + + root_task_handle_once_lock + .set(root_task_handle.clone()) + .map_err(|_| anyhow!("Root task handle was already set"))?; + + let tls_public_key = + Ed25519PublicKey::from(&secrets.persistent_secrets.p2p_private_key.verifying_key()); + let account_public_key = + Ed25519PublicKey::from(&secrets.persistent_secrets.near_signer_key.verifying_key()); + + let secret_db = SecretDB::new(&home_dir.join("assets"), secrets.local_storage_aes_key)?; + + let key_storage_config = KeyStorageConfig { + home_dir: home_dir.clone(), + local_encryption_key: secrets.local_storage_aes_key, + gcp: if let Some(secret_id) = start_config.gcp_keyshare_secret_id { + let project_id = start_config.gcp_project_id.ok_or_else(|| { + anyhow::anyhow!("GCP_PROJECT_ID must be specified to use GCP_KEYSHARE_SECRET_ID") + })?; + Some(GcpPermanentKeyStorageConfig { + project_id, + secret_id, + }) + } else { + None + }, + }; + + // Spawn periodic attestation submission task + let tee_authority_clone = tee_authority.clone(); + let tx_sender_clone = indexer_api.txn_sender.clone(); + let tls_public_key_clone = tls_public_key.clone(); + let account_public_key_clone = account_public_key.clone(); + let allowed_docker_images_receiver_clone = indexer_api.allowed_docker_images_receiver.clone(); + let allowed_launcher_compose_receiver_clone = + indexer_api.allowed_launcher_compose_receiver.clone(); + tokio::spawn(async move { + if let Err(e) = periodic_attestation_submission( + tee_authority_clone, + tx_sender_clone, + tls_public_key_clone, + account_public_key_clone, + allowed_docker_images_receiver_clone, + allowed_launcher_compose_receiver_clone, + tokio::time::interval(ATTESTATION_RESUBMISSION_INTERVAL), + ) + .await + { + tracing::error!( + error = ?e, + "periodic attestation submission task failed" + ); + } + }); + + // Spawn TEE attestation monitoring task + let tx_sender_clone = indexer_api.txn_sender.clone(); + let tee_accounts_receiver = indexer_api.attested_nodes_receiver.clone(); + let account_id_clone = config.my_near_account_id.clone(); + let allowed_docker_images_receiver_clone = indexer_api.allowed_docker_images_receiver.clone(); + let allowed_launcher_compose_receiver_clone = + indexer_api.allowed_launcher_compose_receiver.clone(); + tokio::spawn(async move { + if let Err(e) = monitor_attestation_removal( + account_id_clone, + tee_authority, + tx_sender_clone, + tls_public_key, + account_public_key, + allowed_docker_images_receiver_clone, + allowed_launcher_compose_receiver_clone, + tee_accounts_receiver, + ) + .await + { + tracing::error!( + error = ?e, + "attestation removal monitoring task failed" + ); + } + }); + + let keyshare_storage: Arc> = + RwLock::new(key_storage_config.create().await?).into(); + + spawn_recovery_server_and_run_onboarding( + config.migration_web_ui, + (&secrets).into(), + config.my_near_account_id.clone(), + keyshare_storage.clone(), + indexer_api.my_migration_info_receiver.clone(), + indexer_api.contract_state_receiver.clone(), + indexer_api.txn_sender.clone(), + ) + .await?; + + let coordinator = Coordinator { + clock: Clock::real(), + config_file: config, + secrets, + secret_db, + keyshare_storage, + indexer: indexer_api, + currently_running_job_name: Arc::new(Mutex::new(String::new())), + debug_request_sender, + }; + coordinator.run().await +} diff --git a/libs/nearcore b/libs/nearcore index 8a8c21bc8..3def2f7eb 160000 --- a/libs/nearcore +++ b/libs/nearcore @@ -1 +1 @@ -Subproject commit 8a8c21bc81999af93edd1b6bca5b7c6c6337aa63 +Subproject commit 3def2f7ebb7455199e7b3f7b371e3735c23e2930 From af78d2c518e5f01c1f346081b08856d688937eac Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 14:53:35 +0100 Subject: [PATCH 044/176] testing manually with localnet works --- crates/node/src/cli.rs | 58 ++-- crates/node/src/config.rs | 4 +- crates/node/src/config/foreign_chains/auth.rs | 4 + crates/node/src/config/start.rs | 50 +++- crates/node/src/run.rs | 31 +- docs/localnet/localnet.md | 276 ++++++++++-------- 6 files changed, 255 insertions(+), 168 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index b4ca08405..a21dc1f01 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -1,8 +1,9 @@ use crate::{ config::{ - BlockArgs, CKDConfig, ConfigFile, ForeignChainsConfig, IndexerConfig, KeygenConfig, - PersistentSecrets, PresignatureConfig, SignatureConfig, StartConfig, SyncMode, - TeeAuthorityStartConfig, TripleConfig, + load_config_file, BlockArgs, CKDConfig, ConfigFile, ForeignChainsConfig, GcpStartConfig, + IndexerConfig, KeygenConfig, PersistentSecrets, PresignatureConfig, SecretsStartConfig, + SignatureConfig, StartConfig, SyncMode, TeeAuthorityStartConfig, TeeStartConfig, + TripleConfig, }, keyshare::{ compat::legacy_ecdsa_key_from_keyshares, @@ -143,26 +144,37 @@ pub struct CliImageHashConfig { pub latest_allowed_hash_file: Option, } -impl From for StartConfig { - fn from(cmd: StartCmd) -> Self { +impl StartCmd { + fn into_start_config(self, config: ConfigFile) -> StartConfig { + let gcp = match (self.gcp_keyshare_secret_id, self.gcp_project_id) { + (Some(keyshare_secret_id), Some(project_id)) => Some(GcpStartConfig { + keyshare_secret_id, + project_id, + }), + _ => None, + }; StartConfig { - home_dir: cmd.home_dir, - secret_store_key_hex: cmd.secret_store_key_hex, - gcp_keyshare_secret_id: cmd.gcp_keyshare_secret_id, - gcp_project_id: cmd.gcp_project_id, - tee_authority: match cmd.tee_authority { - CliTeeAuthorityConfig::Local => TeeAuthorityStartConfig::Local, - CliTeeAuthorityConfig::Dstack { - dstack_endpoint, - quote_upload_url, - } => TeeAuthorityStartConfig::Dstack { - dstack_endpoint, - quote_upload_url: quote_upload_url.to_string(), + home_dir: self.home_dir, + secrets: SecretsStartConfig { + secret_store_key_hex: self.secret_store_key_hex, + backup_encryption_key_hex: self.backup_encryption_key_hex, + }, + tee: TeeStartConfig { + authority: match self.tee_authority { + CliTeeAuthorityConfig::Local => TeeAuthorityStartConfig::Local, + CliTeeAuthorityConfig::Dstack { + dstack_endpoint, + quote_upload_url, + } => TeeAuthorityStartConfig::Dstack { + dstack_endpoint, + quote_upload_url: quote_upload_url.to_string(), + }, }, + image_hash: self.image_hash_config.image_hash, + latest_allowed_hash_file: self.image_hash_config.latest_allowed_hash_file, }, - image_hash: cmd.image_hash_config.image_hash, - latest_allowed_hash_file: cmd.image_hash_config.latest_allowed_hash_file, - backup_encryption_key_hex: cmd.backup_encryption_key_hex, + gcp, + node: config, } } } @@ -240,7 +252,11 @@ impl Cli { CliCommand::StartWithConfigFile { config_path } => { StartConfig::from_json_file(&config_path)?.run().await } - CliCommand::Start(start) => StartConfig::from(start).run().await, + CliCommand::Start(start) => { + let home_dir = std::path::Path::new(&start.home_dir); + let config_file = load_config_file(home_dir)?; + start.into_start_config(config_file).run().await + } CliCommand::Init(config) => { let (download_config_type, download_config_url) = if config.download_config { ( diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index 9969e1464..f48c9f922 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -14,7 +14,9 @@ use std::{ }; mod start; -pub use start::{StartConfig, TeeAuthorityStartConfig}; +pub use start::{ + GcpStartConfig, SecretsStartConfig, StartConfig, TeeAuthorityStartConfig, TeeStartConfig, +}; mod foreign_chains; pub use foreign_chains::{ diff --git a/crates/node/src/config/foreign_chains/auth.rs b/crates/node/src/config/foreign_chains/auth.rs index 5269b3130..48d916acd 100644 --- a/crates/node/src/config/foreign_chains/auth.rs +++ b/crates/node/src/config/foreign_chains/auth.rs @@ -52,6 +52,10 @@ pub enum TokenConfig { impl TokenConfig { pub fn resolve(&self) -> anyhow::Result { match self { + // TODO: do not resolve env variables this deep in the binary. + // Should be resolved at start, preferably in the config so we can kill env configs + // + // One option is to have a separate secrets config file. TokenConfig::Env { env } => { std::env::var(env).with_context(|| format!("environment variable {env} is not set")) } diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index 29a6b3a9f..c5ed3d802 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -1,3 +1,4 @@ +use super::ConfigFile; use anyhow::Context; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -13,15 +14,33 @@ use url::Url; #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StartConfig { pub home_dir: String, + /// Encryption keys and backup settings. + pub secrets: SecretsStartConfig, + /// TEE authority and image hash monitoring settings. + pub tee: TeeStartConfig, + /// GCP keyshare storage settings. Optional — omit if not using GCP. + #[serde(default)] + pub gcp: Option, + /// Node configuration (indexer, protocol parameters, etc.). + pub node: ConfigFile, +} + +/// Encryption keys needed at startup. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct SecretsStartConfig { /// Hex-encoded 16 byte AES key for local storage encryption. pub secret_store_key_hex: String, - /// If provided, the root keyshare is stored on GCP. - #[serde(default)] - pub gcp_keyshare_secret_id: Option, + /// Hex-encoded 32 byte AES key for backup encryption. + /// If not provided, a key is generated and written to disk. #[serde(default)] - pub gcp_project_id: Option, + pub backup_encryption_key_hex: Option, +} + +/// TEE-related configuration. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct TeeStartConfig { /// TEE authority configuration. - pub tee_authority: TeeAuthorityStartConfig, + pub authority: TeeAuthorityStartConfig, /// Hex representation of the hash of the running image. Only required in TEE. #[serde(default)] pub image_hash: Option, @@ -29,9 +48,15 @@ pub struct StartConfig { /// If not set, assumes running outside of TEE and skips image hash monitoring. #[serde(default)] pub latest_allowed_hash_file: Option, - /// Hex-encoded 32 byte AES key for backup encryption. - #[serde(default)] - pub backup_encryption_key_hex: Option, +} + +/// GCP keyshare storage configuration. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct GcpStartConfig { + /// GCP secret ID for storing the root keyshare. + pub keyshare_secret_id: String, + /// GCP project ID. + pub project_id: String, } /// TEE authority configuration for JSON deserialization. @@ -74,7 +99,12 @@ impl StartConfig { pub fn from_json_file(path: &std::path::Path) -> anyhow::Result { let content = std::fs::read_to_string(path) .with_context(|| format!("failed to read config file: {}", path.display()))?; - serde_json::from_str(&content) - .with_context(|| format!("failed to parse config file: {}", path.display())) + let config: Self = serde_json::from_str(&content) + .with_context(|| format!("failed to parse config file: {}", path.display()))?; + config + .node + .validate() + .context("invalid node config in config file")?; + Ok(config) } } diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs index 127c749ef..263b30a56 100644 --- a/crates/node/src/run.rs +++ b/crates/node/src/run.rs @@ -1,7 +1,7 @@ use crate::{ config::{ - generate_and_write_backup_encryption_key_to_disk, load_config_file, ConfigFile, - PersistentSecrets, RespondConfig, SecretsConfig, StartConfig, + generate_and_write_backup_encryption_key_to_disk, ConfigFile, PersistentSecrets, + RespondConfig, SecretsConfig, StartConfig, }, coordinator::Coordinator, db::SecretDB, @@ -50,7 +50,7 @@ impl StartConfig { // Load configuration and initialize persistent secrets let home_dir = PathBuf::from(self.home_dir.clone()); - let config = load_config_file(&home_dir)?; + let config = self.node.clone(); let persistent_secrets = PersistentSecrets::generate_or_get_existing( &home_dir, config.number_of_responder_keys, @@ -62,20 +62,20 @@ impl StartConfig { // TODO(#1296): Decide if the MPC responder account is actually needed let respond_config = RespondConfig::from_parts(&config, &persistent_secrets); - let backup_encryption_key_hex = match &self.backup_encryption_key_hex { + let backup_encryption_key_hex = match &self.secrets.backup_encryption_key_hex { Some(key) => key.clone(), None => generate_and_write_backup_encryption_key_to_disk(&home_dir)?, }; // Load secrets from configuration and persistent storage let secrets = SecretsConfig::from_parts( - &self.secret_store_key_hex, + &self.secrets.secret_store_key_hex, persistent_secrets.clone(), &backup_encryption_key_hex, )?; // Generate attestation - let tee_authority = self.tee_authority.clone().into_tee_authority()?; + let tee_authority = self.tee.authority.clone().into_tee_authority()?; let tls_public_key = &secrets.persistent_secrets.p2p_private_key.verifying_key(); let account_public_key = &secrets.persistent_secrets.near_signer_key.verifying_key(); @@ -129,7 +129,7 @@ impl StartConfig { let image_hash_watcher_handle = if let (Some(image_hash), Some(latest_allowed_hash_file)) = - (&self.image_hash, &self.latest_allowed_hash_file) + (&self.tee.image_hash, &self.tee.latest_allowed_hash_file) { let current_image_hash_bytes: [u8; 32] = hex::decode(image_hash) .expect("The currently running image is a hex string.") @@ -150,7 +150,7 @@ impl StartConfig { ))) } else { tracing::info!( - "MPC_IMAGE_HASH and/or MPC_LATEST_ALLOWED_HASH_FILE not set, skipping TEE image hash monitoring" + "image_hash and/or latest_allowed_hash_file not set, skipping TEE image hash monitoring" ); None }; @@ -226,17 +226,10 @@ where let key_storage_config = KeyStorageConfig { home_dir: home_dir.clone(), local_encryption_key: secrets.local_storage_aes_key, - gcp: if let Some(secret_id) = start_config.gcp_keyshare_secret_id { - let project_id = start_config.gcp_project_id.ok_or_else(|| { - anyhow::anyhow!("GCP_PROJECT_ID must be specified to use GCP_KEYSHARE_SECRET_ID") - })?; - Some(GcpPermanentKeyStorageConfig { - project_id, - secret_id, - }) - } else { - None - }, + gcp: start_config.gcp.map(|gcp| GcpPermanentKeyStorageConfig { + project_id: gcp.project_id, + secret_id: gcp.keyshare_secret_id, + }), }; // Spawn periodic attestation submission task diff --git a/docs/localnet/localnet.md b/docs/localnet/localnet.md index 24dc69ce8..fd55b62e4 100644 --- a/docs/localnet/localnet.md +++ b/docs/localnet/localnet.md @@ -191,64 +191,86 @@ Since this is not a validator node, we can remove `validator_key.json` rm ~/.near/mpc-frodo/validator_key.json ``` -Next we'll create a `config.yaml` for the MPC-indexer: - -```shell -cat > ~/.near/mpc-frodo/config.yaml << 'EOF' -my_near_account_id: frodo.test.near -near_responder_account_id: frodo.test.near -number_of_responder_keys: 1 -web_ui: 127.0.0.1:8081 -migration_web_ui: 127.0.0.1:8079 -pprof_bind_address: 127.0.0.1:34001 -triple: - concurrency: 2 - desired_triples_to_buffer: 128 - timeout_sec: 60 - parallel_triple_generation_stagger_time_sec: 1 -presignature: - concurrency: 4 - desired_presignatures_to_buffer: 64 - timeout_sec: 60 -signature: - timeout_sec: 60 -indexer: - validate_genesis: false - sync_mode: Latest - concurrency: 1 - mpc_contract_id: mpc-contract.test.near - finality: optimistic -ckd: - timeout_sec: 60 -cores: 4 -foreign_chains: - bitcoin: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: esplora - rpc_url: "https://bitcoin-rpc.publicnode.com" - auth: - kind: none - abstract: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: standard - rpc_url: "https://api.testnet.abs.xyz" - auth: - kind: none - starknet: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: standard - rpc_url: "https://starknet-rpc.publicnode.com" - auth: - kind: none +Next we'll create a JSON configuration file for Frodo's MPC node. This single file +contains all settings (secrets, TEE config, and node parameters): + +```shell +cat > ~/.near/mpc-frodo/mpc-config.json << EOF +{ + "home_dir": "$HOME/.near/mpc-frodo", + "secrets": { + "secret_store_key_hex": "11111111111111111111111111111111" + }, + "tee": { + "authority": { "type": "local" }, + "image_hash": "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0", + "latest_allowed_hash_file": "/tmp/LATEST_ALLOWED_HASH_FILE.txt" + }, + "node": { + "my_near_account_id": "frodo.test.near", + "near_responder_account_id": "frodo.test.near", + "number_of_responder_keys": 1, + "web_ui": "127.0.0.1:8081", + "migration_web_ui": "127.0.0.1:8079", + "pprof_bind_address": "127.0.0.1:34001", + "triple": { + "concurrency": 2, + "desired_triples_to_buffer": 128, + "timeout_sec": 60, + "parallel_triple_generation_stagger_time_sec": 1 + }, + "presignature": { + "concurrency": 4, + "desired_presignatures_to_buffer": 64, + "timeout_sec": 60 + }, + "signature": { "timeout_sec": 60 }, + "indexer": { + "validate_genesis": false, + "sync_mode": "Latest", + "concurrency": 1, + "mpc_contract_id": "mpc-contract.test.near", + "finality": "optimistic" + }, + "ckd": { "timeout_sec": 60 }, + "cores": 4, + "foreign_chains": { + "bitcoin": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "esplora", + "rpc_url": "https://bitcoin-rpc.publicnode.com", + "auth": { "kind": "none" } + } + } + }, + "abstract": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "standard", + "rpc_url": "https://api.testnet.abs.xyz", + "auth": { "kind": "none" } + } + } + }, + "starknet": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "standard", + "rpc_url": "https://starknet-rpc.publicnode.com", + "auth": { "kind": "none" } + } + } + } + } + } +} EOF ``` @@ -273,79 +295,99 @@ rm ~/.near/mpc-sam/validator_key.json ``` ```shell -cat > ~/.near/mpc-sam/config.yaml << 'EOF' -my_near_account_id: sam.test.near -near_responder_account_id: sam.test.near -number_of_responder_keys: 1 -web_ui: 127.0.0.1:8082 -migration_web_ui: 127.0.0.1:8078 -pprof_bind_address: 127.0.0.1:34002 -triple: - concurrency: 2 - desired_triples_to_buffer: 128 - timeout_sec: 60 - parallel_triple_generation_stagger_time_sec: 1 -presignature: - concurrency: 4 - desired_presignatures_to_buffer: 64 - timeout_sec: 60 -signature: - timeout_sec: 60 -indexer: - validate_genesis: false - sync_mode: Latest - concurrency: 1 - mpc_contract_id: mpc-contract.test.near - finality: optimistic -ckd: - timeout_sec: 60 -cores: 4 -foreign_chains: - bitcoin: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: esplora - rpc_url: "https://bitcoin-rpc.publicnode.com" - auth: - kind: none - abstract: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: standard - rpc_url: "https://api.testnet.abs.xyz" - auth: - kind: none - starknet: - timeout_sec: 30 - max_retries: 3 - providers: - public: - api_variant: standard - rpc_url: "https://starknet-rpc.publicnode.com" - auth: - kind: none +cat > ~/.near/mpc-sam/mpc-config.json << EOF +{ + "home_dir": "$HOME/.near/mpc-sam", + "secrets": { + "secret_store_key_hex": "11111111111111111111111111111111" + }, + "tee": { + "authority": { "type": "local" }, + "image_hash": "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0", + "latest_allowed_hash_file": "/tmp/LATEST_ALLOWED_HASH_FILE.txt" + }, + "node": { + "my_near_account_id": "sam.test.near", + "near_responder_account_id": "sam.test.near", + "number_of_responder_keys": 1, + "web_ui": "127.0.0.1:8082", + "migration_web_ui": "127.0.0.1:8078", + "pprof_bind_address": "127.0.0.1:34002", + "triple": { + "concurrency": 2, + "desired_triples_to_buffer": 128, + "timeout_sec": 60, + "parallel_triple_generation_stagger_time_sec": 1 + }, + "presignature": { + "concurrency": 4, + "desired_presignatures_to_buffer": 64, + "timeout_sec": 60 + }, + "signature": { "timeout_sec": 60 }, + "indexer": { + "validate_genesis": false, + "sync_mode": "Latest", + "concurrency": 1, + "mpc_contract_id": "mpc-contract.test.near", + "finality": "optimistic" + }, + "ckd": { "timeout_sec": 60 }, + "cores": 4, + "foreign_chains": { + "bitcoin": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "esplora", + "rpc_url": "https://bitcoin-rpc.publicnode.com", + "auth": { "kind": "none" } + } + } + }, + "abstract": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "standard", + "rpc_url": "https://api.testnet.abs.xyz", + "auth": { "kind": "none" } + } + } + }, + "starknet": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "standard", + "rpc_url": "https://starknet-rpc.publicnode.com", + "auth": { "kind": "none" } + } + } + } + } + } +} EOF ``` ### Run the MPC binary -In two separate shells run the MPC binary for frodo and sam. Note the last argument repeating (`11111111111111111111111111111111`) is the encryption key for the secret storage, and can be any arbitrary value. +In two separate shells run the MPC binary for Frodo and Sam using their JSON config files: ```shell -RUST_LOG=info mpc-node start --home-dir ~/.near/mpc-sam/ 11111111111111111111111111111111 --image-hash "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0" --latest-allowed-hash-file /temp/LATEST_ALLOWED_HASH_FILE.txt local +RUST_LOG=info mpc-node start-with-config-file ~/.near/mpc-sam/mpc-config.json ``` ```shell -RUST_LOG=info mpc-node start --home-dir ~/.near/mpc-frodo/ 11111111111111111111111111111111 --image-hash "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0" --latest-allowed-hash-file /temp/LATEST_ALLOWED_HASH_FILE.txt local +RUST_LOG=info mpc-node start-with-config-file ~/.near/mpc-frodo/mpc-config.json ``` Notes: -- `8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0` is just an arbitrary hash. - If you get the following error: ```console From ae8e459a986bc3e9745880c555e3e0ef929d3898 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 14:59:38 +0100 Subject: [PATCH 045/176] use envsubst --- docs/localnet/localnet.md | 161 +------------------------ docs/localnet/mpc-config.template.json | 75 ++++++++++++ 2 files changed, 81 insertions(+), 155 deletions(-) create mode 100644 docs/localnet/mpc-config.template.json diff --git a/docs/localnet/localnet.md b/docs/localnet/localnet.md index fd55b62e4..e9c0674ef 100644 --- a/docs/localnet/localnet.md +++ b/docs/localnet/localnet.md @@ -191,87 +191,13 @@ Since this is not a validator node, we can remove `validator_key.json` rm ~/.near/mpc-frodo/validator_key.json ``` -Next we'll create a JSON configuration file for Frodo's MPC node. This single file +Next we'll create a JSON configuration file for Frodo's MPC node using the +shared template at `docs/localnet/mpc-config.template.json`. This single file contains all settings (secrets, TEE config, and node parameters): ```shell -cat > ~/.near/mpc-frodo/mpc-config.json << EOF -{ - "home_dir": "$HOME/.near/mpc-frodo", - "secrets": { - "secret_store_key_hex": "11111111111111111111111111111111" - }, - "tee": { - "authority": { "type": "local" }, - "image_hash": "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0", - "latest_allowed_hash_file": "/tmp/LATEST_ALLOWED_HASH_FILE.txt" - }, - "node": { - "my_near_account_id": "frodo.test.near", - "near_responder_account_id": "frodo.test.near", - "number_of_responder_keys": 1, - "web_ui": "127.0.0.1:8081", - "migration_web_ui": "127.0.0.1:8079", - "pprof_bind_address": "127.0.0.1:34001", - "triple": { - "concurrency": 2, - "desired_triples_to_buffer": 128, - "timeout_sec": 60, - "parallel_triple_generation_stagger_time_sec": 1 - }, - "presignature": { - "concurrency": 4, - "desired_presignatures_to_buffer": 64, - "timeout_sec": 60 - }, - "signature": { "timeout_sec": 60 }, - "indexer": { - "validate_genesis": false, - "sync_mode": "Latest", - "concurrency": 1, - "mpc_contract_id": "mpc-contract.test.near", - "finality": "optimistic" - }, - "ckd": { "timeout_sec": 60 }, - "cores": 4, - "foreign_chains": { - "bitcoin": { - "timeout_sec": 30, - "max_retries": 3, - "providers": { - "public": { - "api_variant": "esplora", - "rpc_url": "https://bitcoin-rpc.publicnode.com", - "auth": { "kind": "none" } - } - } - }, - "abstract": { - "timeout_sec": 30, - "max_retries": 3, - "providers": { - "public": { - "api_variant": "standard", - "rpc_url": "https://api.testnet.abs.xyz", - "auth": { "kind": "none" } - } - } - }, - "starknet": { - "timeout_sec": 30, - "max_retries": 3, - "providers": { - "public": { - "api_variant": "standard", - "rpc_url": "https://starknet-rpc.publicnode.com", - "auth": { "kind": "none" } - } - } - } - } - } -} -EOF +MPC_NODE_ID=mpc-frodo NEAR_ACCOUNT_ID=frodo.test.near WEB_UI_PORT=8081 MIGRATION_WEB_UI_PORT=8079 PPROF_PORT=34001 \ + envsubst < docs/localnet/mpc-config.template.json > ~/.near/mpc-frodo/mpc-config.json ``` ### Initialize Sam's node @@ -295,83 +221,8 @@ rm ~/.near/mpc-sam/validator_key.json ``` ```shell -cat > ~/.near/mpc-sam/mpc-config.json << EOF -{ - "home_dir": "$HOME/.near/mpc-sam", - "secrets": { - "secret_store_key_hex": "11111111111111111111111111111111" - }, - "tee": { - "authority": { "type": "local" }, - "image_hash": "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0", - "latest_allowed_hash_file": "/tmp/LATEST_ALLOWED_HASH_FILE.txt" - }, - "node": { - "my_near_account_id": "sam.test.near", - "near_responder_account_id": "sam.test.near", - "number_of_responder_keys": 1, - "web_ui": "127.0.0.1:8082", - "migration_web_ui": "127.0.0.1:8078", - "pprof_bind_address": "127.0.0.1:34002", - "triple": { - "concurrency": 2, - "desired_triples_to_buffer": 128, - "timeout_sec": 60, - "parallel_triple_generation_stagger_time_sec": 1 - }, - "presignature": { - "concurrency": 4, - "desired_presignatures_to_buffer": 64, - "timeout_sec": 60 - }, - "signature": { "timeout_sec": 60 }, - "indexer": { - "validate_genesis": false, - "sync_mode": "Latest", - "concurrency": 1, - "mpc_contract_id": "mpc-contract.test.near", - "finality": "optimistic" - }, - "ckd": { "timeout_sec": 60 }, - "cores": 4, - "foreign_chains": { - "bitcoin": { - "timeout_sec": 30, - "max_retries": 3, - "providers": { - "public": { - "api_variant": "esplora", - "rpc_url": "https://bitcoin-rpc.publicnode.com", - "auth": { "kind": "none" } - } - } - }, - "abstract": { - "timeout_sec": 30, - "max_retries": 3, - "providers": { - "public": { - "api_variant": "standard", - "rpc_url": "https://api.testnet.abs.xyz", - "auth": { "kind": "none" } - } - } - }, - "starknet": { - "timeout_sec": 30, - "max_retries": 3, - "providers": { - "public": { - "api_variant": "standard", - "rpc_url": "https://starknet-rpc.publicnode.com", - "auth": { "kind": "none" } - } - } - } - } - } -} -EOF +MPC_NODE_ID=mpc-sam NEAR_ACCOUNT_ID=sam.test.near WEB_UI_PORT=8082 MIGRATION_WEB_UI_PORT=8078 PPROF_PORT=34002 \ + envsubst < docs/localnet/mpc-config.template.json > ~/.near/mpc-sam/mpc-config.json ``` ### Run the MPC binary diff --git a/docs/localnet/mpc-config.template.json b/docs/localnet/mpc-config.template.json new file mode 100644 index 000000000..07e15ff27 --- /dev/null +++ b/docs/localnet/mpc-config.template.json @@ -0,0 +1,75 @@ +{ + "home_dir": "$HOME/.near/$MPC_NODE_ID", + "secrets": { + "secret_store_key_hex": "11111111111111111111111111111111" + }, + "tee": { + "authority": { "type": "local" }, + "image_hash": "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0", + "latest_allowed_hash_file": "/tmp/LATEST_ALLOWED_HASH_FILE.txt" + }, + "node": { + "my_near_account_id": "$NEAR_ACCOUNT_ID", + "near_responder_account_id": "$NEAR_ACCOUNT_ID", + "number_of_responder_keys": 1, + "web_ui": "127.0.0.1:$WEB_UI_PORT", + "migration_web_ui": "127.0.0.1:$MIGRATION_WEB_UI_PORT", + "pprof_bind_address": "127.0.0.1:$PPROF_PORT", + "triple": { + "concurrency": 2, + "desired_triples_to_buffer": 128, + "timeout_sec": 60, + "parallel_triple_generation_stagger_time_sec": 1 + }, + "presignature": { + "concurrency": 4, + "desired_presignatures_to_buffer": 64, + "timeout_sec": 60 + }, + "signature": { "timeout_sec": 60 }, + "indexer": { + "validate_genesis": false, + "sync_mode": "Latest", + "concurrency": 1, + "mpc_contract_id": "mpc-contract.test.near", + "finality": "optimistic" + }, + "ckd": { "timeout_sec": 60 }, + "cores": 4, + "foreign_chains": { + "bitcoin": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "esplora", + "rpc_url": "https://bitcoin-rpc.publicnode.com", + "auth": { "kind": "none" } + } + } + }, + "abstract": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "standard", + "rpc_url": "https://api.testnet.abs.xyz", + "auth": { "kind": "none" } + } + } + }, + "starknet": { + "timeout_sec": 30, + "max_retries": 3, + "providers": { + "public": { + "api_variant": "standard", + "rpc_url": "https://starknet-rpc.publicnode.com", + "auth": { "kind": "none" } + } + } + } + } + } +} From a989b042101605fc016cf44d55af3500414ea498 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 14:59:52 +0100 Subject: [PATCH 046/176] make it portable for fish --- docs/localnet/localnet.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/localnet/localnet.md b/docs/localnet/localnet.md index e9c0674ef..90045af21 100644 --- a/docs/localnet/localnet.md +++ b/docs/localnet/localnet.md @@ -196,7 +196,7 @@ shared template at `docs/localnet/mpc-config.template.json`. This single file contains all settings (secrets, TEE config, and node parameters): ```shell -MPC_NODE_ID=mpc-frodo NEAR_ACCOUNT_ID=frodo.test.near WEB_UI_PORT=8081 MIGRATION_WEB_UI_PORT=8079 PPROF_PORT=34001 \ +env MPC_NODE_ID=mpc-frodo NEAR_ACCOUNT_ID=frodo.test.near WEB_UI_PORT=8081 MIGRATION_WEB_UI_PORT=8079 PPROF_PORT=34001 \ envsubst < docs/localnet/mpc-config.template.json > ~/.near/mpc-frodo/mpc-config.json ``` @@ -221,7 +221,7 @@ rm ~/.near/mpc-sam/validator_key.json ``` ```shell -MPC_NODE_ID=mpc-sam NEAR_ACCOUNT_ID=sam.test.near WEB_UI_PORT=8082 MIGRATION_WEB_UI_PORT=8078 PPROF_PORT=34002 \ +env MPC_NODE_ID=mpc-sam NEAR_ACCOUNT_ID=sam.test.near WEB_UI_PORT=8082 MIGRATION_WEB_UI_PORT=8078 PPROF_PORT=34002 \ envsubst < docs/localnet/mpc-config.template.json > ~/.near/mpc-sam/mpc-config.json ``` From 62efec6044f63143c3b1192dd00fb4b2749e8857 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 15:05:55 +0100 Subject: [PATCH 047/176] remove section comments --- crates/node/src/cli.rs | 40 ---------------------------------------- 1 file changed, 40 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index a21dc1f01..0ea6c4811 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -22,11 +22,6 @@ use std::{ }; use tee_authority::tee_authority::{DEFAULT_DSTACK_ENDPOINT, DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL}; use url::Url; - -// --------------------------------------------------------------------------- -// Top-level CLI -// --------------------------------------------------------------------------- - #[derive(Parser, Debug)] #[command(name = "mpc-node")] #[command(about = "MPC Node for Near Protocol")] @@ -86,11 +81,6 @@ pub enum CliCommand { migrating_nodes: Vec, }, } - -// --------------------------------------------------------------------------- -// Start subcommand (CLI flags / env vars) -// --------------------------------------------------------------------------- - #[derive(Args, Debug)] pub struct StartCmd { #[arg(long, env("MPC_HOME_DIR"))] @@ -178,11 +168,6 @@ impl StartCmd { } } } - -// --------------------------------------------------------------------------- -// Init subcommand -// --------------------------------------------------------------------------- - #[derive(Args, Debug)] pub struct InitConfigArgs { #[arg(long, env("MPC_HOME_DIR"))] @@ -209,11 +194,6 @@ pub struct InitConfigArgs { #[arg(long)] pub boot_nodes: Option, } - -// --------------------------------------------------------------------------- -// Import/Export keyshare subcommands -// --------------------------------------------------------------------------- - #[derive(Args, Debug)] pub struct ImportKeyshareCmd { /// Path to home directory @@ -241,11 +221,6 @@ pub struct ExportKeyshareCmd { #[arg(help = "Hex-encoded 16 byte AES key for local storage encryption")] pub local_encryption_key_hex: String, } - -// --------------------------------------------------------------------------- -// Dispatch -// --------------------------------------------------------------------------- - impl Cli { pub async fn run(self) -> anyhow::Result<()> { match self.command { @@ -317,11 +292,6 @@ impl Cli { } } } - -// --------------------------------------------------------------------------- -// Import/Export keyshare implementations -// --------------------------------------------------------------------------- - impl ImportKeyshareCmd { pub async fn run(&self) -> anyhow::Result<()> { let runtime = tokio::runtime::Runtime::new()?; @@ -418,11 +388,6 @@ impl ExportKeyshareCmd { }) } } - -// --------------------------------------------------------------------------- -// Test config generation -// --------------------------------------------------------------------------- - fn duplicate_migrating_accounts( mut accounts: Vec, migrating_nodes: &[usize], @@ -544,11 +509,6 @@ fn create_file_config( cores: Some(4), } } - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - #[cfg(test)] mod tests { use super::*; From 50f27e069a2e3b32b365336002d260b86be6a7dd Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 15:06:15 +0100 Subject: [PATCH 048/176] cargo fmt --- crates/node/src/config/start.rs | 4 ++- crates/node/src/run.rs | 47 +++++++++++++++------------------ 2 files changed, 25 insertions(+), 26 deletions(-) diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index c5ed3d802..8ee8ea9a7 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -88,7 +88,9 @@ impl TeeAuthorityStartConfig { dstack_endpoint, quote_upload_url, } => { - let url: Url = quote_upload_url.parse().context("invalid quote_upload_url")?; + let url: Url = quote_upload_url + .parse() + .context("invalid quote_upload_url")?; DstackTeeAuthorityConfig::new(dstack_endpoint, url).into() } }) diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs index 263b30a56..9c033f305 100644 --- a/crates/node/src/run.rs +++ b/crates/node/src/run.rs @@ -127,33 +127,30 @@ impl StartConfig { let (shutdown_signal_sender, mut shutdown_signal_receiver) = mpsc::channel(1); let cancellation_token = CancellationToken::new(); - let image_hash_watcher_handle = - if let (Some(image_hash), Some(latest_allowed_hash_file)) = - (&self.tee.image_hash, &self.tee.latest_allowed_hash_file) - { - let current_image_hash_bytes: [u8; 32] = hex::decode(image_hash) - .expect("The currently running image is a hex string.") - .try_into() - .expect("The currently running image hash hex representation is 32 bytes."); - - let allowed_hashes_in_contract = - indexer_api.allowed_docker_images_receiver.clone(); - let image_hash_storage = - AllowedImageHashesFile::from(latest_allowed_hash_file.clone()); - - Some(root_runtime.spawn(monitor_allowed_image_hashes( - cancellation_token.child_token(), - MpcDockerImageHash::from(current_image_hash_bytes), - allowed_hashes_in_contract, - image_hash_storage, - shutdown_signal_sender.clone(), - ))) - } else { - tracing::info!( + let image_hash_watcher_handle = if let (Some(image_hash), Some(latest_allowed_hash_file)) = + (&self.tee.image_hash, &self.tee.latest_allowed_hash_file) + { + let current_image_hash_bytes: [u8; 32] = hex::decode(image_hash) + .expect("The currently running image is a hex string.") + .try_into() + .expect("The currently running image hash hex representation is 32 bytes."); + + let allowed_hashes_in_contract = indexer_api.allowed_docker_images_receiver.clone(); + let image_hash_storage = AllowedImageHashesFile::from(latest_allowed_hash_file.clone()); + + Some(root_runtime.spawn(monitor_allowed_image_hashes( + cancellation_token.child_token(), + MpcDockerImageHash::from(current_image_hash_bytes), + allowed_hashes_in_contract, + image_hash_storage, + shutdown_signal_sender.clone(), + ))) + } else { + tracing::info!( "image_hash and/or latest_allowed_hash_file not set, skipping TEE image hash monitoring" ); - None - }; + None + }; let root_future = create_root_future( self, From b5aedee8bddcaeffeecd002c2fc7832ca805cd1b Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 15:20:15 +0100 Subject: [PATCH 049/176] Have run as standalone function --- crates/node/src/cli.rs | 8 +- crates/node/src/run.rs | 285 ++++++++++++++++++++--------------------- 2 files changed, 147 insertions(+), 146 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 0ea6c4811..2b1aa8abf 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -11,6 +11,7 @@ use crate::{ permanent::{PermanentKeyStorage, PermanentKeyStorageBackend, PermanentKeyshareData}, }, p2p::testing::{generate_test_p2p_configs, PortSeed}, + run::run, }; use clap::{Args, Parser, Subcommand, ValueEnum}; use hex::FromHex; @@ -225,12 +226,15 @@ impl Cli { pub async fn run(self) -> anyhow::Result<()> { match self.command { CliCommand::StartWithConfigFile { config_path } => { - StartConfig::from_json_file(&config_path)?.run().await + let node_configuration = StartConfig::from_json_file(&config_path)?; + run(node_configuration).await } CliCommand::Start(start) => { let home_dir = std::path::Path::new(&start.home_dir); let config_file = load_config_file(home_dir)?; - start.into_start_config(config_file).run().await + + let node_configuration = start.into_start_config(config_file); + run(node_configuration).await } CliCommand::Init(config) => { let (download_config_type, download_config_url) = if config.download_config { diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs index 9c033f305..f403a299f 100644 --- a/crates/node/src/run.rs +++ b/crates/node/src/run.rs @@ -39,155 +39,152 @@ use crate::tee::{ pub const ATTESTATION_RESUBMISSION_INTERVAL: Duration = Duration::from_secs(60 * 60); // 1 hour -impl StartConfig { - pub async fn run(self) -> anyhow::Result<()> { - let root_runtime = tokio::runtime::Builder::new_multi_thread() - .enable_all() - .worker_threads(1) - .build()?; - - let _tokio_enter_guard = root_runtime.enter(); - - // Load configuration and initialize persistent secrets - let home_dir = PathBuf::from(self.home_dir.clone()); - let config = self.node.clone(); - let persistent_secrets = PersistentSecrets::generate_or_get_existing( - &home_dir, - config.number_of_responder_keys, - )?; - - profiler::web_server::start_web_server(config.pprof_bind_address).await?; - root_runtime.spawn(crate::metrics::tokio_task_metrics::run_monitor_loop()); - - // TODO(#1296): Decide if the MPC responder account is actually needed - let respond_config = RespondConfig::from_parts(&config, &persistent_secrets); - - let backup_encryption_key_hex = match &self.secrets.backup_encryption_key_hex { - Some(key) => key.clone(), - None => generate_and_write_backup_encryption_key_to_disk(&home_dir)?, - }; - - // Load secrets from configuration and persistent storage - let secrets = SecretsConfig::from_parts( - &self.secrets.secret_store_key_hex, - persistent_secrets.clone(), - &backup_encryption_key_hex, - )?; - - // Generate attestation - let tee_authority = self.tee.authority.clone().into_tee_authority()?; - let tls_public_key = &secrets.persistent_secrets.p2p_private_key.verifying_key(); - - let account_public_key = &secrets.persistent_secrets.near_signer_key.verifying_key(); - - let report_data = ReportDataV1::new( - *Ed25519PublicKey::from(tls_public_key).as_bytes(), - *Ed25519PublicKey::from(account_public_key).as_bytes(), - ) - .into(); - - let attestation = tee_authority.generate_attestation(report_data).await?; - - // Create communication channels and runtime - let (debug_request_sender, _) = tokio::sync::broadcast::channel(10); - let root_task_handle = Arc::new(OnceLock::new()); - - let (protocol_state_sender, protocol_state_receiver) = - watch::channel(ProtocolContractState::NotInitialized); - - let (migration_state_sender, migration_state_receiver) = - watch::channel((0, BTreeMap::new())); - let web_server = root_runtime - .block_on(start_web_server( - root_task_handle.clone(), - debug_request_sender.clone(), - config.web_ui, - static_web_data(&secrets, Some(attestation)), - protocol_state_receiver, - migration_state_receiver, - )) - .context("Failed to create web server.")?; - - let _web_server_join_handle = root_runtime.spawn(web_server); - - // Create Indexer and wait for indexer to be synced. - let (indexer_exit_sender, indexer_exit_receiver) = oneshot::channel(); - let indexer_api = spawn_real_indexer( - home_dir.clone(), - config.indexer.clone(), - config.my_near_account_id.clone(), - persistent_secrets.near_signer_key.clone(), - respond_config, - indexer_exit_sender, - protocol_state_sender, - migration_state_sender, - *tls_public_key, - ); - - let (shutdown_signal_sender, mut shutdown_signal_receiver) = mpsc::channel(1); - let cancellation_token = CancellationToken::new(); - - let image_hash_watcher_handle = if let (Some(image_hash), Some(latest_allowed_hash_file)) = - (&self.tee.image_hash, &self.tee.latest_allowed_hash_file) - { - let current_image_hash_bytes: [u8; 32] = hex::decode(image_hash) - .expect("The currently running image is a hex string.") - .try_into() - .expect("The currently running image hash hex representation is 32 bytes."); - - let allowed_hashes_in_contract = indexer_api.allowed_docker_images_receiver.clone(); - let image_hash_storage = AllowedImageHashesFile::from(latest_allowed_hash_file.clone()); - - Some(root_runtime.spawn(monitor_allowed_image_hashes( - cancellation_token.child_token(), - MpcDockerImageHash::from(current_image_hash_bytes), - allowed_hashes_in_contract, - image_hash_storage, - shutdown_signal_sender.clone(), - ))) - } else { - tracing::info!( +pub async fn run(config: StartConfig) -> anyhow::Result<()> { + let root_runtime = tokio::runtime::Builder::new_multi_thread() + .enable_all() + .worker_threads(1) + .build()?; + + let _tokio_enter_guard = root_runtime.enter(); + + // Load configuration and initialize persistent secrets + let home_dir = PathBuf::from(config.home_dir.clone()); + let node_config = config.node.clone(); + let persistent_secrets = PersistentSecrets::generate_or_get_existing( + &home_dir, + node_config.number_of_responder_keys, + )?; + + profiler::web_server::start_web_server(node_config.pprof_bind_address).await?; + root_runtime.spawn(crate::metrics::tokio_task_metrics::run_monitor_loop()); + + // TODO(#1296): Decide if the MPC responder account is actually needed + let respond_config = RespondConfig::from_parts(&node_config, &persistent_secrets); + + let backup_encryption_key_hex = match &config.secrets.backup_encryption_key_hex { + Some(key) => key.clone(), + None => generate_and_write_backup_encryption_key_to_disk(&home_dir)?, + }; + + // Load secrets from configuration and persistent storage + let secrets = SecretsConfig::from_parts( + &config.secrets.secret_store_key_hex, + persistent_secrets.clone(), + &backup_encryption_key_hex, + )?; + + // Generate attestation + let tee_authority = config.tee.authority.clone().into_tee_authority()?; + let tls_public_key = &secrets.persistent_secrets.p2p_private_key.verifying_key(); + + let account_public_key = &secrets.persistent_secrets.near_signer_key.verifying_key(); + + let report_data = ReportDataV1::new( + *Ed25519PublicKey::from(tls_public_key).as_bytes(), + *Ed25519PublicKey::from(account_public_key).as_bytes(), + ) + .into(); + + let attestation = tee_authority.generate_attestation(report_data).await?; + + // Create communication channels and runtime + let (debug_request_sender, _) = tokio::sync::broadcast::channel(10); + let root_task_handle = Arc::new(OnceLock::new()); + + let (protocol_state_sender, protocol_state_receiver) = + watch::channel(ProtocolContractState::NotInitialized); + + let (migration_state_sender, migration_state_receiver) = watch::channel((0, BTreeMap::new())); + let web_server = root_runtime + .block_on(start_web_server( + root_task_handle.clone(), + debug_request_sender.clone(), + node_config.web_ui, + static_web_data(&secrets, Some(attestation)), + protocol_state_receiver, + migration_state_receiver, + )) + .context("Failed to create web server.")?; + + let _web_server_join_handle = root_runtime.spawn(web_server); + + // Create Indexer and wait for indexer to be synced. + let (indexer_exit_sender, indexer_exit_receiver) = oneshot::channel(); + let indexer_api = spawn_real_indexer( + home_dir.clone(), + node_config.indexer.clone(), + node_config.my_near_account_id.clone(), + persistent_secrets.near_signer_key.clone(), + respond_config, + indexer_exit_sender, + protocol_state_sender, + migration_state_sender, + *tls_public_key, + ); + + let (shutdown_signal_sender, mut shutdown_signal_receiver) = mpsc::channel(1); + let cancellation_token = CancellationToken::new(); + + let image_hash_watcher_handle = if let (Some(image_hash), Some(latest_allowed_hash_file)) = + (&config.tee.image_hash, &config.tee.latest_allowed_hash_file) + { + let current_image_hash_bytes: [u8; 32] = hex::decode(image_hash) + .expect("The currently running image is a hex string.") + .try_into() + .expect("The currently running image hash hex representation is 32 bytes."); + + let allowed_hashes_in_contract = indexer_api.allowed_docker_images_receiver.clone(); + let image_hash_storage = AllowedImageHashesFile::from(latest_allowed_hash_file.clone()); + + Some(root_runtime.spawn(monitor_allowed_image_hashes( + cancellation_token.child_token(), + MpcDockerImageHash::from(current_image_hash_bytes), + allowed_hashes_in_contract, + image_hash_storage, + shutdown_signal_sender.clone(), + ))) + } else { + tracing::info!( "image_hash and/or latest_allowed_hash_file not set, skipping TEE image hash monitoring" ); - None - }; - - let root_future = create_root_future( - self, - home_dir.clone(), - config.clone(), - secrets.clone(), - indexer_api, - debug_request_sender, - root_task_handle, - tee_authority, - ); - - let root_task = root_runtime.spawn(start_root_task("root", root_future).0); - - let exit_reason = tokio::select! { - root_task_result = root_task => { - root_task_result? - } - indexer_exit_response = indexer_exit_receiver => { - indexer_exit_response.context("Indexer thread dropped response channel.")? - } - Some(()) = shutdown_signal_receiver.recv() => { - Err(anyhow!("TEE allowed image hashes watcher is sending shutdown signal.")) - } - }; - - // Perform graceful shutdown - cancellation_token.cancel(); - - if let Some(handle) = image_hash_watcher_handle { - info!("Waiting for image hash watcher to gracefully exit."); - let exit_result = handle.await; - info!(?exit_result, "Image hash watcher exited."); + None + }; + + let root_future = create_root_future( + config, + home_dir.clone(), + node_config.clone(), + secrets.clone(), + indexer_api, + debug_request_sender, + root_task_handle, + tee_authority, + ); + + let root_task = root_runtime.spawn(start_root_task("root", root_future).0); + + let exit_reason = tokio::select! { + root_task_result = root_task => { + root_task_result? + } + indexer_exit_response = indexer_exit_receiver => { + indexer_exit_response.context("Indexer thread dropped response channel.")? } + Some(()) = shutdown_signal_receiver.recv() => { + Err(anyhow!("TEE allowed image hashes watcher is sending shutdown signal.")) + } + }; - exit_reason + // Perform graceful shutdown + cancellation_token.cancel(); + + if let Some(handle) = image_hash_watcher_handle { + info!("Waiting for image hash watcher to gracefully exit."); + let exit_result = handle.await; + info!(?exit_result, "Image hash watcher exited."); } + + exit_reason } #[allow(clippy::too_many_arguments)] From 0636c9292618b5210a3780c971943b4718603f97 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 15:20:41 +0100 Subject: [PATCH 050/176] rename to run_mpc_node --- crates/node/src/cli.rs | 6 +++--- crates/node/src/run.rs | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 2b1aa8abf..5062cc7dd 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -11,7 +11,7 @@ use crate::{ permanent::{PermanentKeyStorage, PermanentKeyStorageBackend, PermanentKeyshareData}, }, p2p::testing::{generate_test_p2p_configs, PortSeed}, - run::run, + run::run_mpc_node, }; use clap::{Args, Parser, Subcommand, ValueEnum}; use hex::FromHex; @@ -227,14 +227,14 @@ impl Cli { match self.command { CliCommand::StartWithConfigFile { config_path } => { let node_configuration = StartConfig::from_json_file(&config_path)?; - run(node_configuration).await + run_mpc_node(node_configuration).await } CliCommand::Start(start) => { let home_dir = std::path::Path::new(&start.home_dir); let config_file = load_config_file(home_dir)?; let node_configuration = start.into_start_config(config_file); - run(node_configuration).await + run_mpc_node(node_configuration).await } CliCommand::Init(config) => { let (download_config_type, download_config_url) = if config.download_config { diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs index f403a299f..dcde5b1db 100644 --- a/crates/node/src/run.rs +++ b/crates/node/src/run.rs @@ -39,7 +39,7 @@ use crate::tee::{ pub const ATTESTATION_RESUBMISSION_INTERVAL: Duration = Duration::from_secs(60 * 60); // 1 hour -pub async fn run(config: StartConfig) -> anyhow::Result<()> { +pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { let root_runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .worker_threads(1) From 599d99900f3c3ff8892fdd03554346088cfc6e58 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 15:26:29 +0100 Subject: [PATCH 051/176] use pathbuf --- crates/node/src/cli.rs | 1 + crates/node/src/config/start.rs | 3 ++- crates/node/src/run.rs | 3 +-- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 5062cc7dd..cf141edfb 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -229,6 +229,7 @@ impl Cli { let node_configuration = StartConfig::from_json_file(&config_path)?; run_mpc_node(node_configuration).await } + // TODO: deprecate this CliCommand::Start(start) => { let home_dir = std::path::Path::new(&start.home_dir); let config_file = load_config_file(home_dir)?; diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index 8ee8ea9a7..ffda2122b 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -13,7 +13,7 @@ use url::Url; /// (JSON file) convert into this type. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StartConfig { - pub home_dir: String, + pub home_dir: PathBuf, /// Encryption keys and backup settings. pub secrets: SecretsStartConfig, /// TEE authority and image hash monitoring settings. @@ -68,6 +68,7 @@ pub enum TeeAuthorityStartConfig { #[serde(default = "default_dstack_endpoint")] dstack_endpoint: String, #[serde(default = "default_quote_upload_url")] + // TODO: use URL type for this type quote_upload_url: String, }, } diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs index dcde5b1db..7dd40d392 100644 --- a/crates/node/src/run.rs +++ b/crates/node/src/run.rs @@ -48,10 +48,9 @@ pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { let _tokio_enter_guard = root_runtime.enter(); // Load configuration and initialize persistent secrets - let home_dir = PathBuf::from(config.home_dir.clone()); let node_config = config.node.clone(); let persistent_secrets = PersistentSecrets::generate_or_get_existing( - &home_dir, + &config.home_dir, node_config.number_of_responder_keys, )?; From ffa3497cd8a1f356043840ba857ef750888c1466 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 15:28:41 +0100 Subject: [PATCH 052/176] add todo issue links --- crates/node/src/cli.rs | 2 +- crates/node/src/config/foreign_chains/auth.rs | 2 +- crates/node/src/config/start.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index cf141edfb..365729816 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -229,7 +229,7 @@ impl Cli { let node_configuration = StartConfig::from_json_file(&config_path)?; run_mpc_node(node_configuration).await } - // TODO: deprecate this + // TODO(#2334): deprecate this CliCommand::Start(start) => { let home_dir = std::path::Path::new(&start.home_dir); let config_file = load_config_file(home_dir)?; diff --git a/crates/node/src/config/foreign_chains/auth.rs b/crates/node/src/config/foreign_chains/auth.rs index 48d916acd..3540fe837 100644 --- a/crates/node/src/config/foreign_chains/auth.rs +++ b/crates/node/src/config/foreign_chains/auth.rs @@ -52,7 +52,7 @@ pub enum TokenConfig { impl TokenConfig { pub fn resolve(&self) -> anyhow::Result { match self { - // TODO: do not resolve env variables this deep in the binary. + // TODO(#2335): do not resolve env variables this deep in the binary. // Should be resolved at start, preferably in the config so we can kill env configs // // One option is to have a separate secrets config file. diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index ffda2122b..78aa8be3d 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -68,7 +68,7 @@ pub enum TeeAuthorityStartConfig { #[serde(default = "default_dstack_endpoint")] dstack_endpoint: String, #[serde(default = "default_quote_upload_url")] - // TODO: use URL type for this type + // TODO(#2333): use URL type for this type quote_upload_url: String, }, } From 0b53d1c0a01a7885c6da1fed6c2ed08bcff3636c Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 15:47:40 +0100 Subject: [PATCH 053/176] fix pathbuf issue --- crates/node/src/cli.rs | 2 +- crates/node/src/run.rs | 5 +++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 365729816..47cefd480 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -85,7 +85,7 @@ pub enum CliCommand { #[derive(Args, Debug)] pub struct StartCmd { #[arg(long, env("MPC_HOME_DIR"))] - pub home_dir: String, + pub home_dir: PathBuf, /// Hex-encoded 16 byte AES key for local storage encryption. /// This key should come from a secure secret storage. /// TODO(#444): After TEE integration decide on what to do with AES encryption key diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs index 7dd40d392..93b9c99fe 100644 --- a/crates/node/src/run.rs +++ b/crates/node/src/run.rs @@ -62,7 +62,7 @@ pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { let backup_encryption_key_hex = match &config.secrets.backup_encryption_key_hex { Some(key) => key.clone(), - None => generate_and_write_backup_encryption_key_to_disk(&home_dir)?, + None => generate_and_write_backup_encryption_key_to_disk(&config.home_dir)?, }; // Load secrets from configuration and persistent storage @@ -110,7 +110,7 @@ pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { // Create Indexer and wait for indexer to be synced. let (indexer_exit_sender, indexer_exit_receiver) = oneshot::channel(); let indexer_api = spawn_real_indexer( - home_dir.clone(), + config.home_dir.clone(), node_config.indexer.clone(), node_config.my_near_account_id.clone(), persistent_secrets.near_signer_key.clone(), @@ -149,6 +149,7 @@ pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { None }; + let home_dir = config.home_dir.clone(); let root_future = create_root_future( config, home_dir.clone(), From 34515c211401108e4f61945a72419051a1d9f4ab Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 15:49:20 +0100 Subject: [PATCH 054/176] update pytests --- pytest/common_lib/shared/__init__.py | 4 ++++ pytest/common_lib/shared/mpc_node.py | 34 +++++++++++++++++++++------- 2 files changed, 30 insertions(+), 8 deletions(-) diff --git a/pytest/common_lib/shared/__init__.py b/pytest/common_lib/shared/__init__.py index 48cb7cb3a..27c391764 100644 --- a/pytest/common_lib/shared/__init__.py +++ b/pytest/common_lib/shared/__init__.py @@ -230,6 +230,7 @@ class ConfigValues: migration_address: str pprof_address: str backup_key: bytes + node_config: dict # JSON-serializable dict matching Rust ConfigFile def generate_mpc_configs( @@ -331,6 +332,7 @@ def generate_mpc_configs( ] backup_key = os.urandom(32) + configs.append( ConfigValues( signer_key, @@ -341,6 +343,7 @@ def generate_mpc_configs( migration_address, pprof_address, backup_key, + node_config=config, ) ) return configs @@ -531,6 +534,7 @@ def start_cluster_with_mpc( pytest_signer_keys=pytest_signer_keys, backup_key=config.backup_key, pprof_address=config.pprof_address, + node_config=config.node_config, ) mpc_node.init_nonces(validators[0]) mpc_node.set_block_ingestion(True) diff --git a/pytest/common_lib/shared/mpc_node.py b/pytest/common_lib/shared/mpc_node.py index 436ded22a..c8b87d5ce 100644 --- a/pytest/common_lib/shared/mpc_node.py +++ b/pytest/common_lib/shared/mpc_node.py @@ -61,6 +61,7 @@ def __init__( p2p_public_key: str, pytest_signer_keys: list[Key], backup_key: bytes, + node_config: dict, ): super().__init__(near_node, signer_key, pytest_signer_keys) self.p2p_url: str = p2p_url @@ -74,6 +75,7 @@ def __init__( self.is_running = False self.metrics = MetricsTracker(near_node) self.backup_key = backup_key + self.node_config = node_config def print(self): if not self.is_running: @@ -127,22 +129,38 @@ def reset_mpc_data(self): for file_path in pathlib.Path(self.home_dir).glob(pattern): file_path.unlink() + def _write_start_config(self) -> str: + """Build a StartConfig JSON file and write it to the node's home dir. + Returns the path to the written config file.""" + start_config = { + "home_dir": self.home_dir, + "secrets": { + "secret_store_key_hex": self.secret_store_key, + "backup_encryption_key_hex": self.backup_key.hex(), + }, + "tee": { + "authority": {"type": "local"}, + "image_hash": DUMMY_MPC_IMAGE_HASH, + "latest_allowed_hash_file": "latest_allowed_hash.txt", + }, + "node": self.node_config, + } + config_path = str(pathlib.Path(self.home_dir) / "start_config.json") + with open(config_path, "w") as f: + json.dump(start_config, f, indent=2) + return config_path + def run(self): assert not self.is_running self.is_running = True + config_path = self._write_start_config() extra_env = { "RUST_LOG": "INFO", # mpc-node produces too much output on DEBUG - "MPC_SECRET_STORE_KEY": self.secret_store_key, - "MPC_IMAGE_HASH": DUMMY_MPC_IMAGE_HASH, - "MPC_LATEST_ALLOWED_HASH_FILE": "latest_allowed_hash.txt", - "MPC_BACKUP_ENCRYPTION_KEY_HEX": self.backup_key.hex(), } cmd = ( MPC_BINARY_PATH, - "start", - "--home-dir", - self.home_dir, - "local", + "start-with-config-file", + config_path, ) self.near_node.run_cmd(cmd=cmd, extra_env=extra_env) From ca80bca948cf4b07981a40fc90daf9b599685189 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 16:08:20 +0100 Subject: [PATCH 055/176] fix: test config was overwriting neard config --- crates/node/src/cli.rs | 4 ++-- pytest/common_lib/shared/__init__.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 47cefd480..f5529b72b 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -454,8 +454,8 @@ fn run_generate_test_configs( desired_presignatures_to_buffer, ); std::fs::write( - format!("{}/config.yaml", subdir), - serde_yaml::to_string(&file_config)?, + format!("{}/mpc_node_config.json", subdir), + serde_json::to_string_pretty(&file_config)?, )?; } std::fs::write( diff --git a/pytest/common_lib/shared/__init__.py b/pytest/common_lib/shared/__init__.py index 27c391764..8fdfdad44 100644 --- a/pytest/common_lib/shared/__init__.py +++ b/pytest/common_lib/shared/__init__.py @@ -42,7 +42,7 @@ dot_near = pathlib.Path.home() / ".near" SECRETS_JSON = "secrets.json" NUMBER_OF_VALIDATORS = 1 -CONFIG_YAML = "config.yaml" +MPC_NODE_CONFIG_JSON = "mpc_node_config.json" def create_function_call_access_key_action( @@ -308,9 +308,9 @@ def generate_mpc_configs( my_port = participant["port"] p2p_url = f"http://{my_addr}:{my_port}" - config_file_path = os.path.join(dot_near, str(idx), CONFIG_YAML) + config_file_path = os.path.join(dot_near, str(idx), MPC_NODE_CONFIG_JSON) with open(config_file_path, "r") as f: - config = yaml.load(f, Loader=SafeLoaderIgnoreUnknown) + config = json.load(f) web_address = config.get("web_ui") migration_address = config.get("migration_web_ui") From 44b9a0e765b9f9937169136b0f975e4caae320f4 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 16:09:10 +0100 Subject: [PATCH 056/176] reset nearcore change --- libs/nearcore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/nearcore b/libs/nearcore index 3def2f7eb..8a8c21bc8 160000 --- a/libs/nearcore +++ b/libs/nearcore @@ -1 +1 @@ -Subproject commit 3def2f7ebb7455199e7b3f7b371e3735c23e2930 +Subproject commit 8a8c21bc81999af93edd1b6bca5b7c6c6337aa63 From bc9b2fcd462946a775b97f1087f4a2ecfd71b024 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 16:23:25 +0100 Subject: [PATCH 057/176] fix yml failure --- pytest/common_lib/shared/foreign_chains.py | 25 +++------------------- 1 file changed, 3 insertions(+), 22 deletions(-) diff --git a/pytest/common_lib/shared/foreign_chains.py b/pytest/common_lib/shared/foreign_chains.py index 5d71cb30c..fa6193b87 100644 --- a/pytest/common_lib/shared/foreign_chains.py +++ b/pytest/common_lib/shared/foreign_chains.py @@ -1,32 +1,13 @@ """Shared helpers for foreign chain configuration and policy tests.""" -import pathlib -import re from typing import Any -import yaml - - -def node_config_path(node) -> pathlib.Path: - return pathlib.Path(node.home_dir) / "config.yaml" - def set_foreign_chains_config(node, foreign_chains: dict[str, Any] | None) -> None: - config_path = node_config_path(node) - - config_text = config_path.read_text(encoding="utf-8") - # Keep generated YAML tags intact by editing only the trailing `foreign_chains` section. - config_text = ( - re.sub(r"\nforeign_chains:[\s\S]*\Z", "\n", config_text).rstrip() + "\n" - ) - if foreign_chains is not None: - foreign_chains_text = yaml.safe_dump( - {"foreign_chains": foreign_chains}, sort_keys=False - ) - config_text += "\n" + foreign_chains_text - - config_path.write_text(config_text, encoding="utf-8") + node.node_config["foreign_chains"] = foreign_chains + else: + node.node_config["foreign_chains"] = {} def normalize_policy(policy: dict[str, Any]) -> list[tuple[str, tuple[str, ...]]]: From 99084d1886a39023f529161a9836531bb9bff979 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 16:51:46 +0100 Subject: [PATCH 058/176] redact secrets --- crates/node/src/config/start.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index 78aa8be3d..963248731 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -26,7 +26,7 @@ pub struct StartConfig { } /// Encryption keys needed at startup. -#[derive(Debug, Clone, Serialize, Deserialize)] +#[derive(Clone, Serialize, Deserialize)] pub struct SecretsStartConfig { /// Hex-encoded 16 byte AES key for local storage encryption. pub secret_store_key_hex: String, @@ -36,6 +36,15 @@ pub struct SecretsStartConfig { pub backup_encryption_key_hex: Option, } +impl std::fmt::Debug for SecretsStartConfig { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("SecretsStartConfig") + .field("secret_store_key_hex", &"[REDACTED]") + .field("backup_encryption_key_hex", &self.backup_encryption_key_hex.as_ref().map(|_| "[REDACTED]")) + .finish() + } +} + /// TEE-related configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct TeeStartConfig { From 6f90606a32a01e045127578371dac62c67f7ef2e Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 16:54:30 +0100 Subject: [PATCH 059/176] fmt --- crates/node/src/config/start.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index 963248731..b2a26e565 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -40,7 +40,13 @@ impl std::fmt::Debug for SecretsStartConfig { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { f.debug_struct("SecretsStartConfig") .field("secret_store_key_hex", &"[REDACTED]") - .field("backup_encryption_key_hex", &self.backup_encryption_key_hex.as_ref().map(|_| "[REDACTED]")) + .field( + "backup_encryption_key_hex", + &self + .backup_encryption_key_hex + .as_ref() + .map(|_| "[REDACTED]"), + ) .finish() } } From 79c001dbd0d17b98e039ed45e3d49004eb9e7e51 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 17:17:15 +0100 Subject: [PATCH 060/176] change to pass forward path instead --- Cargo.lock | 4 - crates/tee-launcher/Cargo.toml | 4 - crates/tee-launcher/src/constants.rs | 3 + crates/tee-launcher/src/env_validation.rs | 183 ---------- crates/tee-launcher/src/error.rs | 12 - crates/tee-launcher/src/main.rs | 150 ++++---- crates/tee-launcher/src/types.rs | 407 +--------------------- libs/nearcore | 2 +- 8 files changed, 88 insertions(+), 677 deletions(-) delete mode 100644 crates/tee-launcher/src/env_validation.rs diff --git a/Cargo.lock b/Cargo.lock index 03775211b..af0abd50a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10600,12 +10600,8 @@ dependencies = [ "bounded-collections", "clap", "dstack-sdk", - "hex", - "itertools 0.14.0", "launcher-interface", - "regex", "reqwest 0.12.28", - "rstest", "serde", "serde_json", "tempfile", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index 9dd627211..da20a9ba4 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -16,10 +16,7 @@ backon = { workspace = true } bounded-collections = { workspace = true } clap = { workspace = true } dstack-sdk = { workspace = true } -hex = { workspace = true } launcher-interface = { workspace = true } -itertools = { workspace = true } -regex = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } @@ -31,7 +28,6 @@ url = { workspace = true, features = ["serde"] } [dev-dependencies] assert_matches = { workspace = true } -rstest = { workspace = true } tempfile = { workspace = true } [lints] diff --git a/crates/tee-launcher/src/constants.rs b/crates/tee-launcher/src/constants.rs index af89e71b3..a158586d1 100644 --- a/crates/tee-launcher/src/constants.rs +++ b/crates/tee-launcher/src/constants.rs @@ -2,3 +2,6 @@ pub(crate) const MPC_CONTAINER_NAME: &str = "mpc-node"; pub(crate) const IMAGE_DIGEST_FILE: &str = "/mnt/shared/image-digest.bin"; pub(crate) const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; pub(crate) const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; + +/// Path inside the container where the MPC config file is bind-mounted. +pub(crate) const MPC_CONFIG_CONTAINER_PATH: &str = "/mnt/shared/mpc-config.json"; diff --git a/crates/tee-launcher/src/env_validation.rs b/crates/tee-launcher/src/env_validation.rs deleted file mode 100644 index 96a3af3cb..000000000 --- a/crates/tee-launcher/src/env_validation.rs +++ /dev/null @@ -1,183 +0,0 @@ -use std::sync::LazyLock; - -use regex::Regex; - -/// Hard caps to prevent DoS via huge env payloads (matching Python launcher). -pub(crate) const MAX_PASSTHROUGH_ENV_VARS: usize = 64; -pub(crate) const MAX_ENV_VALUE_LEN: usize = 1024; -pub(crate) const MAX_TOTAL_ENV_BYTES: usize = 32 * 1024; // 32 KB - -/// Never pass raw private keys via launcher. -const DENIED_CONTAINER_ENV_KEYS: &[&str] = &["MPC_P2P_PRIVATE_KEY", "MPC_ACCOUNT_SK"]; - -/// Matches `MPC_[A-Z0-9_]{1,64}` — same pattern as the Python launcher. -static MPC_ENV_KEY_RE: LazyLock = - LazyLock::new(|| Regex::new(r"^MPC_[A-Z0-9_]{1,64}$").unwrap()); - -/// Non-MPC keys that are explicitly allowed for backwards compatibility. -const COMPAT_ALLOWED_KEYS: &[&str] = &["RUST_LOG", "RUST_BACKTRACE", "NEAR_BOOT_NODES"]; - -// --------------------------------------------------------------------------- -// Key validation -// --------------------------------------------------------------------------- - -/// Validates an extra env key (from the catch-all `extra_env` map). -/// -/// - Must match `MPC_[A-Z0-9_]{1,64}` **or** be in the compat allowlist -/// - Must not be in the deny list -pub(crate) fn validate_env_key(key: &str) -> Result<(), crate::error::LauncherError> { - if DENIED_CONTAINER_ENV_KEYS.contains(&key) { - return Err(crate::error::LauncherError::UnsafeEnvValue { - key: key.to_owned(), - reason: "denied key".into(), - }); - } - if MPC_ENV_KEY_RE.is_match(key) || COMPAT_ALLOWED_KEYS.contains(&key) { - return Ok(()); - } - Err(crate::error::LauncherError::UnsafeEnvValue { - key: key.to_owned(), - reason: "key does not match allowlist".into(), - }) -} - -// --------------------------------------------------------------------------- -// Value validation -// --------------------------------------------------------------------------- - -fn has_control_chars(s: &str) -> bool { - for ch in s.chars() { - if ch == '\n' || ch == '\r' || ch == '\0' { - return true; - } - if (ch as u32) < 0x20 && ch != '\t' { - return true; - } - } - false -} - -/// Validates an env value (applied to ALL vars, typed and extra). -/// -/// - Length <= `MAX_ENV_VALUE_LEN` -/// - No ASCII control characters (except tab) -/// - Does not contain `LD_PRELOAD` -pub(crate) fn validate_env_value( - key: &str, - value: &str, -) -> Result<(), crate::error::LauncherError> { - if value.len() > MAX_ENV_VALUE_LEN { - return Err(crate::error::LauncherError::UnsafeEnvValue { - key: key.to_owned(), - reason: format!("value too long ({} > {MAX_ENV_VALUE_LEN})", value.len()), - }); - } - if has_control_chars(value) { - return Err(crate::error::LauncherError::UnsafeEnvValue { - key: key.to_owned(), - reason: "contains control characters".into(), - }); - } - if value.contains("LD_PRELOAD") { - return Err(crate::error::LauncherError::UnsafeEnvValue { - key: key.to_owned(), - reason: "contains LD_PRELOAD".into(), - }); - } - Ok(()) -} - -#[cfg(test)] -mod tests { - use assert_matches::assert_matches; - use rstest::rstest; - - use super::*; - - #[rstest] - #[case("MPC_FOO")] - #[case("MPC_FOO_123")] - #[case("MPC_A_B_C")] - fn key_allows_mpc_prefix_uppercase(#[case] key: &str) { - assert_matches!(validate_env_key(key), Ok(_)); - } - - #[rstest] - #[case("MPC_foo")] - #[case("MPC-FOO")] - #[case("MPC.FOO")] - #[case("MPC_")] - fn key_rejects_lowercase_or_invalid_format(#[case] key: &str) { - assert_matches!(validate_env_key(key), Err(_)); - } - - #[rstest] - #[case("RUST_LOG")] - #[case("RUST_BACKTRACE")] - #[case("NEAR_BOOT_NODES")] - fn key_allows_compat_non_mpc_keys(#[case] key: &str) { - assert_matches!(validate_env_key(key), Ok(_)); - } - - #[rstest] - #[case("MPC_P2P_PRIVATE_KEY")] - #[case("MPC_ACCOUNT_SK")] - fn key_denies_sensitive_keys(#[case] key: &str) { - assert_matches!(validate_env_key(key), Err(_)); - } - - #[rstest] - #[case("BAD_KEY")] - #[case("HOME")] - fn key_rejects_unknown_non_mpc_key(#[case] key: &str) { - assert_matches!(validate_env_key(key), Err(_)); - } - - #[rstest] - #[case("ok\nno")] - #[case("ok\rno")] - fn value_rejects_control_chars(#[case] value: &str) { - assert_matches!(validate_env_value("K", value), Err(_)); - } - - #[test] - fn value_rejects_control_char_unit_separator() { - assert_matches!(validate_env_value("K", &format!("a{}b", '\x1F')), Err(_)); - } - - #[test] - fn value_allows_tab() { - assert_matches!(validate_env_value("K", "a\tb"), Ok(_)); - } - - #[rstest] - #[case("LD_PRELOAD=/tmp/x.so")] - #[case("foo LD_PRELOAD bar")] - fn value_rejects_ld_preload(#[case] value: &str) { - assert_matches!(validate_env_value("K", value), Err(_)); - } - - #[test] - fn value_rejects_too_long() { - assert_matches!( - validate_env_value("K", &"a".repeat(MAX_ENV_VALUE_LEN + 1)), - Err(_) - ); - } - - #[test] - fn value_accepts_at_length_limit() { - assert_matches!( - validate_env_value("K", &"a".repeat(MAX_ENV_VALUE_LEN)), - Ok(_) - ); - } - - #[rstest] - #[case("hello-world")] - #[case("192.168.1.1")] - #[case("info,mpc_node=debug")] - fn value_accepts_normal(#[case] value: &str) { - assert_matches!(validate_env_value("K", value), Ok(_)); - } -} diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 812f87509..02a45a3ff 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -27,18 +27,6 @@ pub enum LauncherError { output: String, }, - #[error("Too many env vars to pass through (>{0})")] - TooManyEnvVars(usize), - - #[error("Total env payload too large (>{0} bytes)")] - EnvPayloadTooLarge(usize), - - #[error("Env var '{key}' has unsafe value: {reason}")] - UnsafeEnvValue { key: String, reason: String }, - - #[error("Unsafe docker command: LD_PRELOAD detected")] - LdPreloadDetected, - #[error("Failed to read {path}: {source}")] FileRead { path: String, diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index e11043af8..5b5cb480a 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -17,7 +17,6 @@ use url::Url; mod constants; mod docker_types; -mod env_validation; mod error; mod types; @@ -120,7 +119,7 @@ async fn run() -> Result<(), LauncherError> { launch_mpc_container( args.platform, &image_hash, - &dstack_config.mpc_passthrough_env, + &dstack_config.mpc_config_file, &dstack_config.docker_command_config, )?; @@ -355,22 +354,12 @@ async fn validate_image_hash( fn docker_run_args( platform: Platform, - mpc_config: &MpcBinaryConfig, + mpc_config_file: &std::path::Path, docker_flags: &DockerLaunchFlags, image_digest: &DockerSha256Digest, -) -> Result, LauncherError> { +) -> Vec { let mut cmd: Vec = vec![]; - // Required environment variables - cmd.extend([ - "--env".into(), - format!("MPC_IMAGE_HASH={}", image_digest.as_raw_hex()), - ]); - cmd.extend([ - "--env".into(), - format!("MPC_LATEST_ALLOWED_HASH_FILE={IMAGE_DIGEST_FILE}"), - ]); - if platform == Platform::Tee { cmd.extend([ "--env".into(), @@ -382,9 +371,12 @@ fn docker_run_args( ]); } - for (key, value) in mpc_config.env_vars()? { - cmd.extend(["--env".into(), format!("{key}={value}")]); - } + // Mount the MPC config file into the container (read-only) + let host_path = mpc_config_file.display(); + cmd.extend([ + "-v".into(), + format!("{host_path}:{MPC_CONFIG_CONTAINER_PATH}:ro"), + ]); cmd.extend(docker_flags.extra_hosts.docker_args()); cmd.extend(docker_flags.port_mappings.docker_args()); @@ -403,23 +395,21 @@ fn docker_run_args( MPC_CONTAINER_NAME.into(), "--detach".into(), image_digest.to_string(), + // Command for the MPC binary: read config from file + "start-with-config-file".into(), + MPC_CONFIG_CONTAINER_PATH.into(), ]); let docker_command_string = cmd.join(" "); tracing::info!(?docker_command_string, "docker cmd"); - // Final LD_PRELOAD safeguard - if docker_command_string.contains("LD_PRELOAD") { - return Err(LauncherError::LdPreloadDetected); - } - - Ok(cmd) + cmd } fn launch_mpc_container( platform: Platform, valid_hash: &DockerSha256Digest, - mpc_config: &MpcBinaryConfig, + mpc_config_file: &std::path::Path, docker_flags: &DockerLaunchFlags, ) -> Result<(), LauncherError> { tracing::info!("Launching MPC node with validated hash: {valid_hash}",); @@ -429,7 +419,7 @@ fn launch_mpc_container( .args(["rm", "-f", MPC_CONTAINER_NAME]) .output(); - let docker_run_args = docker_run_args(platform, mpc_config, docker_flags, valid_hash)?; + let docker_run_args = docker_run_args(platform, mpc_config_file, docker_flags, valid_hash); let run_output = Command::new("docker") .arg("run") @@ -456,7 +446,7 @@ fn launch_mpc_container( #[cfg(test)] mod tests { - use std::collections::BTreeMap; + use std::path::Path; use assert_matches::assert_matches; use bounded_collections::NonEmptyVec; @@ -468,6 +458,8 @@ mod tests { use crate::select_image_hash; use crate::types::*; + const SAMPLE_CONFIG_PATH: &str = "/tapp/mpc-config.json"; + fn digest(hex_char: char) -> DockerSha256Digest { format!( "sha256:{}", @@ -487,23 +479,6 @@ mod tests { } } - fn base_mpc_config() -> MpcBinaryConfig { - MpcBinaryConfig { - mpc_account_id: "test-account".into(), - mpc_local_address: "127.0.0.1".parse().unwrap(), - mpc_secret_key_store: "secret".into(), - mpc_backup_encryption_key_hex: "0".repeat(64), - mpc_env: MpcEnv::Testnet, - mpc_home_dir: "/data".into(), - mpc_contract_id: "contract.near".into(), - mpc_responder_id: "responder-1".into(), - near_boot_nodes: "boot1,boot2".into(), - rust_backtrace: RustBacktrace::Enabled, - rust_log: RustLog::Level(RustLogLevel::Info), - extra_env: BTreeMap::new(), - } - } - fn empty_docker_flags() -> DockerLaunchFlags { serde_json::from_value(serde_json::json!({ "extra_hosts": {"hosts": []}, @@ -523,12 +498,11 @@ mod tests { #[test] fn tee_mode_includes_dstack_mount() { // given - let config = base_mpc_config(); let flags = empty_docker_flags(); let digest = sample_digest(); // when - let args = docker_run_args(Platform::Tee, &config, &flags, &digest).unwrap(); + let args = docker_run_args(Platform::Tee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); // then let joined = args.join(" "); @@ -539,12 +513,12 @@ mod tests { #[test] fn nontee_mode_excludes_dstack_mount() { // given - let config = base_mpc_config(); let flags = empty_docker_flags(); let digest = sample_digest(); // when - let args = docker_run_args(Platform::NonTee, &config, &flags, &digest).unwrap(); + let args = + docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); // then let joined = args.join(" "); @@ -555,12 +529,12 @@ mod tests { #[test] fn includes_security_opts_and_required_volumes() { // given - let config = base_mpc_config(); let flags = empty_docker_flags(); let digest = sample_digest(); // when - let args = docker_run_args(Platform::NonTee, &config, &flags, &digest).unwrap(); + let args = + docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); // then let joined = args.join(" "); @@ -573,66 +547,94 @@ mod tests { } #[test] - fn image_digest_is_last_argument() { + fn mounts_config_file_read_only() { // given - let config = base_mpc_config(); let flags = empty_docker_flags(); let digest = sample_digest(); // when - let args = docker_run_args(Platform::NonTee, &config, &flags, &digest).unwrap(); + let args = + docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); // then - assert_eq!(args.last().unwrap(), &digest.to_string()); + let joined = args.join(" "); + assert!(joined.contains(&format!( + "{SAMPLE_CONFIG_PATH}:{MPC_CONFIG_CONTAINER_PATH}:ro" + ))); } #[test] - fn includes_ports_and_extra_hosts() { + fn includes_start_with_config_file_command() { // given - let config = base_mpc_config(); - let flags = docker_flags_with_host_and_port(); + let flags = empty_docker_flags(); let digest = sample_digest(); // when - let args = docker_run_args(Platform::NonTee, &config, &flags, &digest).unwrap(); + let args = + docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); // then let joined = args.join(" "); - assert!(joined.contains("--add-host node1:192.168.1.1")); - assert!(joined.contains("-p 11780:11780")); + assert!(joined.contains(&format!( + "start-with-config-file {MPC_CONFIG_CONTAINER_PATH}" + ))); } #[test] - fn includes_mpc_env_vars() { + fn image_digest_appears_before_command() { // given - let config = base_mpc_config(); let flags = empty_docker_flags(); let digest = sample_digest(); // when - let args = docker_run_args(Platform::NonTee, &config, &flags, &digest).unwrap(); + let args = + docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); + + // then - image digest should appear before "start-with-config-file" + let digest_pos = args.iter().position(|a| a == &digest.to_string()).unwrap(); + let cmd_pos = args + .iter() + .position(|a| a == "start-with-config-file") + .unwrap(); + assert!(digest_pos < cmd_pos); + } + + #[test] + fn includes_ports_and_extra_hosts() { + // given + let flags = docker_flags_with_host_and_port(); + let digest = sample_digest(); + + // when + let args = + docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); // then let joined = args.join(" "); - assert!(joined.contains("MPC_ACCOUNT_ID=test-account")); - assert!(joined.contains("MPC_IMAGE_HASH=")); - assert!(joined.contains(&format!("MPC_LATEST_ALLOWED_HASH_FILE={IMAGE_DIGEST_FILE}"))); + assert!(joined.contains("--add-host node1:192.168.1.1")); + assert!(joined.contains("-p 11780:11780")); } #[test] - fn ld_preload_in_typed_field_is_rejected_by_env_validation() { - // given - typed fields are also validated by env_validation::validate_env_value, - // so LD_PRELOAD in any env value is caught before the final safeguard. - let mut config = base_mpc_config(); - config.mpc_account_id = "LD_PRELOAD=/evil.so".into(); + fn no_env_vars_forwarded_for_mpc_config() { + // given let flags = empty_docker_flags(); let digest = sample_digest(); // when - let result = docker_run_args(Platform::NonTee, &config, &flags, &digest); - - // then - assert_matches!(result, Err(LauncherError::UnsafeEnvValue { .. })); + let args = + docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); + + // then - no MPC_* env vars should be present (only DSTACK_ENDPOINT in TEE mode) + let env_args: Vec<&String> = args + .windows(2) + .filter(|w| w[0] == "--env") + .map(|w| &w[1]) + .collect(); + assert!( + env_args.is_empty(), + "expected no --env args in non-TEE mode, got: {env_args:?}" + ); } // --- select_image_hash --- diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 6017d6c23..4d7c6b911 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -1,6 +1,4 @@ -use std::collections::BTreeMap; -use std::fmt; -use std::net::{IpAddr, Ipv4Addr}; +use std::net::Ipv4Addr; use std::num::NonZeroU16; use std::path::PathBuf; @@ -11,8 +9,6 @@ use bounded_collections::NonEmptyVec; use clap::{Parser, ValueEnum}; use serde::{Deserialize, Serialize}; -use crate::env_validation; - /// CLI arguments parsed from environment variables via clap. #[derive(Parser, Debug)] #[command(name = "tee-launcher")] @@ -45,15 +41,14 @@ pub enum Platform { } /// Typed representation of the dstack user config file (`/tapp/user_config`). -/// -/// Launcher-only keys are extracted into typed fields; all remaining keys are -/// kept in `passthrough_env` for forwarding to the MPC container. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Config { pub launcher_config: LauncherConfig, pub docker_command_config: DockerLaunchFlags, - /// Remaining env vars forwarded to the MPC container. - pub mpc_passthrough_env: MpcBinaryConfig, + /// Path to the MPC node JSON config file on the host. + /// This file is mounted into the container and passed via + /// `start-with-config-file ` to the MPC binary. + pub mpc_config_file: PathBuf, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -74,34 +69,6 @@ pub struct LauncherConfig { pub mpc_hash_override: Option, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct MpcBinaryConfig { - // mpc - // TODO: use near type to not accept any string - pub mpc_account_id: String, - pub mpc_local_address: IpAddr, - // TODO: think this is no longer needed with node generated keys - pub mpc_secret_key_store: String, - // TODO: think this is no longer needed with node generated keys - pub mpc_backup_encryption_key_hex: String, - pub mpc_env: MpcEnv, - pub mpc_home_dir: PathBuf, - // TODO: use near type to not accept any string - pub mpc_contract_id: String, - // TODO: use near type to not accept any string - pub mpc_responder_id: String, - // near - pub near_boot_nodes: String, - // rust - pub rust_backtrace: RustBacktrace, - pub rust_log: RustLog, - /// Additional env vars not covered by the typed fields above. - /// Allows operators to pass new `MPC_*` vars without a launcher rebuild. - /// Keys and values are validated at emission time in `env_vars()`. - #[serde(flatten)] - pub extra_env: BTreeMap, -} - #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DockerLaunchFlags { pub extra_hosts: ExtraHosts, @@ -153,184 +120,14 @@ impl PortMappings { } } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum MpcEnv { - Localnet, - Testnet, - Mainnet, -} - -impl fmt::Display for MpcEnv { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - MpcEnv::Localnet => write!(f, "localnet"), - MpcEnv::Testnet => write!(f, "testnet"), - MpcEnv::Mainnet => write!(f, "mainnet"), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub enum RustBacktrace { - #[serde(rename = "0")] - Disabled, - #[serde(rename = "1")] - Enabled, - #[serde(rename = "short")] - Short, - #[serde(rename = "full")] - Full, -} - -impl fmt::Display for RustBacktrace { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RustBacktrace::Disabled => write!(f, "0"), - RustBacktrace::Enabled => write!(f, "1"), - RustBacktrace::Short => write!(f, "short"), - RustBacktrace::Full => write!(f, "full"), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(rename_all = "lowercase")] -pub enum RustLogLevel { - Error, - Warn, - Info, - Debug, - Trace, -} - -impl fmt::Display for RustLogLevel { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RustLogLevel::Error => write!(f, "error"), - RustLogLevel::Warn => write!(f, "warn"), - RustLogLevel::Info => write!(f, "info"), - RustLogLevel::Debug => write!(f, "debug"), - RustLogLevel::Trace => write!(f, "trace"), - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(untagged)] -pub enum RustLog { - Level(RustLogLevel), - Filter(String), -} - -impl fmt::Display for RustLog { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self { - RustLog::Level(level) => level.fmt(f), - RustLog::Filter(filter) => write!(f, "{filter}"), - } - } -} - -impl MpcBinaryConfig { - /// Returns all env vars to pass to the MPC container. - /// - /// Typed fields are emitted first (deterministic order), followed by - /// validated extras from `extra_env`. All keys and values are validated - /// uniformly before returning. - #[cfg(test)] - pub(crate) fn with_extra_env(mut self, extra: std::collections::BTreeMap) -> Self { - self.extra_env = extra; - self - } - - pub fn env_vars(&self) -> Result, crate::error::LauncherError> { - let mut vars: Vec<(String, String)> = vec![ - ("MPC_ACCOUNT_ID".into(), self.mpc_account_id.clone()), - ( - "MPC_LOCAL_ADDRESS".into(), - self.mpc_local_address.to_string(), - ), - ( - "MPC_SECRET_STORE_KEY".into(), - self.mpc_secret_key_store.clone(), - ), - ("MPC_CONTRACT_ID".into(), self.mpc_contract_id.clone()), - ("MPC_ENV".into(), self.mpc_env.to_string()), - ( - "MPC_HOME_DIR".into(), - self.mpc_home_dir.display().to_string(), - ), - ("MPC_RESPONDER_ID".into(), self.mpc_responder_id.clone()), - ( - "MPC_BACKUP_ENCRYPTION_KEY_HEX".into(), - self.mpc_backup_encryption_key_hex.clone(), - ), - ("NEAR_BOOT_NODES".into(), self.near_boot_nodes.clone()), - ("RUST_BACKTRACE".into(), self.rust_backtrace.to_string()), - ("RUST_LOG".into(), self.rust_log.to_string()), - ]; - - // Keys already emitted via typed fields — skip duplicates from extra_env. - let typed_keys: std::collections::HashSet = - vars.iter().map(|(k, _)| k.clone()).collect(); - - if self.extra_env.len() > env_validation::MAX_PASSTHROUGH_ENV_VARS { - return Err(crate::error::LauncherError::TooManyEnvVars( - env_validation::MAX_PASSTHROUGH_ENV_VARS, - )); - } - - // BTreeMap iteration is sorted, giving deterministic output. - for (key, value) in &self.extra_env { - if typed_keys.contains(key.as_str()) { - continue; - } - env_validation::validate_env_key(key)?; - vars.push((key.clone(), value.clone())); - } - - // Validate ALL env vars uniformly (typed + extra) and enforce aggregate caps. - let mut total_bytes: usize = 0; - for (key, value) in &vars { - env_validation::validate_env_value(key, value)?; - total_bytes += key.len() + 1 + value.len(); - } - if total_bytes > env_validation::MAX_TOTAL_ENV_BYTES { - return Err(crate::error::LauncherError::EnvPayloadTooLarge( - env_validation::MAX_TOTAL_ENV_BYTES, - )); - } - - Ok(vars) - } -} - #[cfg(test)] mod tests { use assert_matches::assert_matches; - use std::collections::BTreeMap; use std::net::Ipv4Addr; use std::num::NonZeroU16; use super::*; - fn base_mpc_config() -> MpcBinaryConfig { - MpcBinaryConfig { - mpc_account_id: "test-account".into(), - mpc_local_address: "127.0.0.1".parse().unwrap(), - mpc_secret_key_store: "secret".into(), - mpc_backup_encryption_key_hex: "0".repeat(64), - mpc_env: MpcEnv::Testnet, - mpc_home_dir: "/data".into(), - mpc_contract_id: "contract.near".into(), - mpc_responder_id: "responder-1".into(), - near_boot_nodes: "boot1,boot2".into(), - rust_backtrace: RustBacktrace::Enabled, - rust_log: RustLog::Level(RustLogLevel::Info), - extra_env: BTreeMap::new(), - } - } - // --- HostEntry deserialization --- #[test] @@ -471,170 +268,6 @@ mod tests { assert_eq!(args, vec!["-p", "11780:11780"]); } - // --- MpcBinaryConfig::env_vars --- - - #[test] - fn env_vars_includes_all_typed_fields() { - // given - let config = base_mpc_config(); - - // when - let vars = config.env_vars().unwrap(); - - // then - let keys: Vec<&str> = vars.iter().map(|(k, _)| k.as_str()).collect(); - assert!(keys.contains(&"MPC_ACCOUNT_ID")); - assert!(keys.contains(&"MPC_LOCAL_ADDRESS")); - assert!(keys.contains(&"MPC_SECRET_STORE_KEY")); - assert!(keys.contains(&"MPC_CONTRACT_ID")); - assert!(keys.contains(&"MPC_ENV")); - assert!(keys.contains(&"MPC_HOME_DIR")); - assert!(keys.contains(&"MPC_RESPONDER_ID")); - assert!(keys.contains(&"MPC_BACKUP_ENCRYPTION_KEY_HEX")); - assert!(keys.contains(&"NEAR_BOOT_NODES")); - assert!(keys.contains(&"RUST_BACKTRACE")); - assert!(keys.contains(&"RUST_LOG")); - } - - #[test] - fn env_vars_passes_valid_extra_mpc_key() { - // given - let mut extra = BTreeMap::new(); - extra.insert("MPC_NEW_FEATURE".into(), "enabled".into()); - let config = base_mpc_config().with_extra_env(extra); - - // when - let vars = config.env_vars().unwrap(); - - // then - assert!(vars.iter().any(|(k, v)| k == "MPC_NEW_FEATURE" && v == "enabled")); - } - - #[test] - fn env_vars_deduplicates_typed_key_from_extra() { - // given - let mut extra = BTreeMap::new(); - extra.insert("MPC_ACCOUNT_ID".into(), "duplicate".into()); - let config = base_mpc_config().with_extra_env(extra); - - // when - let vars = config.env_vars().unwrap(); - - // then - let account_values: Vec<&str> = vars - .iter() - .filter(|(k, _)| k == "MPC_ACCOUNT_ID") - .map(|(_, v)| v.as_str()) - .collect(); - assert_eq!(account_values.len(), 1); - assert_eq!(account_values[0], "test-account"); - } - - #[test] - fn env_vars_rejects_sensitive_key_in_extra() { - // given - let mut extra = BTreeMap::new(); - extra.insert("MPC_P2P_PRIVATE_KEY".into(), "secret".into()); - let config = base_mpc_config().with_extra_env(extra); - - // when - let result = config.env_vars(); - - // then - assert_matches!(result, Err(crate::error::LauncherError::UnsafeEnvValue { .. })); - } - - #[test] - fn env_vars_rejects_account_sk_in_extra() { - // given - let mut extra = BTreeMap::new(); - extra.insert("MPC_ACCOUNT_SK".into(), "secret".into()); - let config = base_mpc_config().with_extra_env(extra); - - // when - let result = config.env_vars(); - - // then - assert_matches!(result, Err(crate::error::LauncherError::UnsafeEnvValue { .. })); - } - - #[test] - fn env_vars_rejects_value_with_newline() { - // given - let mut extra = BTreeMap::new(); - extra.insert("MPC_INJECTED".into(), "ok\nbad".into()); - let config = base_mpc_config().with_extra_env(extra); - - // when - let result = config.env_vars(); - - // then - assert_matches!(result, Err(crate::error::LauncherError::UnsafeEnvValue { .. })); - } - - #[test] - fn env_vars_rejects_value_containing_ld_preload() { - // given - let mut extra = BTreeMap::new(); - extra.insert("MPC_INJECTED".into(), "LD_PRELOAD=/tmp/x.so".into()); - let config = base_mpc_config().with_extra_env(extra); - - // when - let result = config.env_vars(); - - // then - assert_matches!(result, Err(crate::error::LauncherError::UnsafeEnvValue { .. })); - } - - #[test] - fn env_vars_rejects_too_many_extra_vars() { - // given - let mut extra = BTreeMap::new(); - for i in 0..=crate::env_validation::MAX_PASSTHROUGH_ENV_VARS { - extra.insert(format!("MPC_X_{i}"), "1".into()); - } - let config = base_mpc_config().with_extra_env(extra); - - // when - let result = config.env_vars(); - - // then - assert_matches!(result, Err(crate::error::LauncherError::TooManyEnvVars(_))); - } - - #[test] - fn env_vars_rejects_total_bytes_exceeded() { - // given - let mut extra = BTreeMap::new(); - for i in 0..40 { - extra.insert( - format!("MPC_BIG_{i}"), - "a".repeat(crate::env_validation::MAX_ENV_VALUE_LEN), - ); - } - let config = base_mpc_config().with_extra_env(extra); - - // when - let result = config.env_vars(); - - // then - assert_matches!(result, Err(crate::error::LauncherError::EnvPayloadTooLarge(_))); - } - - #[test] - fn env_vars_rejects_unknown_non_mpc_key() { - // given - let mut extra = BTreeMap::new(); - extra.insert("BAD_KEY".into(), "value".into()); - let config = base_mpc_config().with_extra_env(extra); - - // when - let result = config.env_vars(); - - // then - assert_matches!(result, Err(crate::error::LauncherError::UnsafeEnvValue { .. })); - } - // --- Config full deserialization --- #[test] @@ -654,19 +287,7 @@ mod tests { "extra_hosts": {"hosts": [{"hostname": {"Domain": "node1"}, "ip": "192.168.1.1"}]}, "port_mappings": {"ports": [{"src": 11780, "dst": 11780}]} }, - "mpc_passthrough_env": { - "mpc_account_id": "account123", - "mpc_local_address": "127.0.0.1", - "mpc_secret_key_store": "secret", - "mpc_backup_encryption_key_hex": "0000000000000000000000000000000000000000000000000000000000000000", - "mpc_env": "Testnet", - "mpc_home_dir": "/data", - "mpc_contract_id": "contract.near", - "mpc_responder_id": "responder-1", - "near_boot_nodes": "boot1", - "rust_backtrace": "1", - "rust_log": "info" - } + "mpc_config_file": "/tapp/mpc-config.json" }); // when @@ -674,14 +295,14 @@ mod tests { // then assert_matches!(result, Ok(config) => { - assert_eq!(config.mpc_passthrough_env.mpc_account_id, "account123"); assert_eq!(config.launcher_config.image_name, "nearone/mpc-node"); + assert_eq!(config.mpc_config_file, PathBuf::from("/tapp/mpc-config.json")); }); } #[test] fn config_rejects_missing_required_field() { - // given - mpc_account_id is missing + // given - mpc_config_file is missing let json = serde_json::json!({ "launcher_config": { "image_tags": ["tag1"], @@ -695,18 +316,6 @@ mod tests { "docker_command_config": { "extra_hosts": {"hosts": []}, "port_mappings": {"ports": []} - }, - "mpc_passthrough_env": { - "mpc_local_address": "127.0.0.1", - "mpc_secret_key_store": "secret", - "mpc_backup_encryption_key_hex": "0000000000000000000000000000000000000000000000000000000000000000", - "mpc_env": "Testnet", - "mpc_home_dir": "/data", - "mpc_contract_id": "contract.near", - "mpc_responder_id": "responder-1", - "near_boot_nodes": "boot1", - "rust_backtrace": "1", - "rust_log": "info" } }); diff --git a/libs/nearcore b/libs/nearcore index 8a8c21bc8..3def2f7eb 160000 --- a/libs/nearcore +++ b/libs/nearcore @@ -1 +1 @@ -Subproject commit 8a8c21bc81999af93edd1b6bca5b7c6c6337aa63 +Subproject commit 3def2f7ebb7455199e7b3f7b371e3735c23e2930 From 3812aa4d634f32ee6156e6f3ad394bea45fde23f Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 17:38:43 +0100 Subject: [PATCH 061/176] fmt --- crates/tee-launcher/src/main.rs | 63 ++++++++++++++++++++++++-------- crates/tee-launcher/src/types.rs | 6 +-- 2 files changed, 50 insertions(+), 19 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 5b5cb480a..757000c77 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -502,7 +502,12 @@ mod tests { let digest = sample_digest(); // when - let args = docker_run_args(Platform::Tee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); + let args = docker_run_args( + Platform::Tee, + Path::new(SAMPLE_CONFIG_PATH), + &flags, + &digest, + ); // then let joined = args.join(" "); @@ -517,8 +522,12 @@ mod tests { let digest = sample_digest(); // when - let args = - docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); + let args = docker_run_args( + Platform::NonTee, + Path::new(SAMPLE_CONFIG_PATH), + &flags, + &digest, + ); // then let joined = args.join(" "); @@ -533,8 +542,12 @@ mod tests { let digest = sample_digest(); // when - let args = - docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); + let args = docker_run_args( + Platform::NonTee, + Path::new(SAMPLE_CONFIG_PATH), + &flags, + &digest, + ); // then let joined = args.join(" "); @@ -553,8 +566,12 @@ mod tests { let digest = sample_digest(); // when - let args = - docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); + let args = docker_run_args( + Platform::NonTee, + Path::new(SAMPLE_CONFIG_PATH), + &flags, + &digest, + ); // then let joined = args.join(" "); @@ -570,8 +587,12 @@ mod tests { let digest = sample_digest(); // when - let args = - docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); + let args = docker_run_args( + Platform::NonTee, + Path::new(SAMPLE_CONFIG_PATH), + &flags, + &digest, + ); // then let joined = args.join(" "); @@ -587,8 +608,12 @@ mod tests { let digest = sample_digest(); // when - let args = - docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); + let args = docker_run_args( + Platform::NonTee, + Path::new(SAMPLE_CONFIG_PATH), + &flags, + &digest, + ); // then - image digest should appear before "start-with-config-file" let digest_pos = args.iter().position(|a| a == &digest.to_string()).unwrap(); @@ -606,8 +631,12 @@ mod tests { let digest = sample_digest(); // when - let args = - docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); + let args = docker_run_args( + Platform::NonTee, + Path::new(SAMPLE_CONFIG_PATH), + &flags, + &digest, + ); // then let joined = args.join(" "); @@ -622,8 +651,12 @@ mod tests { let digest = sample_digest(); // when - let args = - docker_run_args(Platform::NonTee, Path::new(SAMPLE_CONFIG_PATH), &flags, &digest); + let args = docker_run_args( + Platform::NonTee, + Path::new(SAMPLE_CONFIG_PATH), + &flags, + &digest, + ); // then - no MPC_* env vars should be present (only DSTACK_ENDPOINT in TEE mode) let env_args: Vec<&String> = args diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 4d7c6b911..352ed1979 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -133,8 +133,7 @@ mod tests { #[test] fn host_entry_valid_deserialization() { // given - let json = - serde_json::json!({"hostname": {"Domain": "node.local"}, "ip": "192.168.1.1"}); + let json = serde_json::json!({"hostname": {"Domain": "node.local"}, "ip": "192.168.1.1"}); // when let result = serde_json::from_value::(json); @@ -148,8 +147,7 @@ mod tests { #[test] fn host_entry_rejects_invalid_ip() { // given - let json = - serde_json::json!({"hostname": {"Domain": "node.local"}, "ip": "not-an-ip"}); + let json = serde_json::json!({"hostname": {"Domain": "node.local"}, "ip": "not-an-ip"}); // when let result = serde_json::from_value::(json); From 5fa48e5a1e4cf05687b6e57952d0a6d7ac5aec36 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 17:39:39 +0100 Subject: [PATCH 062/176] shear and sort --- Cargo.lock | 1 - Cargo.toml | 3 +-- crates/launcher-interface/Cargo.toml | 3 +-- crates/mpc-attestation/Cargo.toml | 2 +- crates/node/Cargo.toml | 2 +- crates/node/src/tee/allowed_image_hashes_watcher.rs | 2 +- crates/tee-launcher/Cargo.toml | 1 - 7 files changed, 5 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index af0abd50a..50b4e2ec6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10604,7 +10604,6 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", - "tempfile", "thiserror 2.0.18", "tokio", "tracing", diff --git a/Cargo.toml b/Cargo.toml index d4a3addde..27e118ab3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -45,11 +45,11 @@ contract-interface = { path = "crates/contract-interface" } foreign-chain-inspector = { path = "crates/foreign-chain-inspector" } foreign-chain-rpc-interfaces = { path = "crates/foreign-chain-rpc-interfaces" } include-measurements = { path = "crates/include-measurements" } +launcher-interface = { path = "crates/launcher-interface" } mpc-attestation = { path = "crates/mpc-attestation" } mpc-contract = { path = "crates/contract", features = ["dev-utils"] } mpc-crypto-types = { path = "crates/crypto-types" } mpc-node = { path = "crates/node" } -launcher-interface = { path = "crates/launcher-interface" } mpc-primitives = { path = "crates/primitives", features = ["abi"] } mpc-tls = { path = "crates/tls" } near-mpc-sdk = { path = "crates/near-mpc-sdk" } @@ -97,7 +97,6 @@ derive_more = { version = "2.1.1", features = [ "into", ] } digest = "0.10.7" -dotenvy = "0.15" dstack-sdk = { version = "0.1.2" } dstack-sdk-types = { version = "0.1.2", features = ["borsh"] } ecdsa = { version = "0.16.9", features = ["digest", "hazmat"] } diff --git a/crates/launcher-interface/Cargo.toml b/crates/launcher-interface/Cargo.toml index 56f46ec29..b559eed81 100644 --- a/crates/launcher-interface/Cargo.toml +++ b/crates/launcher-interface/Cargo.toml @@ -5,13 +5,12 @@ edition.workspace = true license.workspace = true [dependencies] -derive_more = { workspace = true } bounded-collections = { workspace = true } +derive_more = { workspace = true } mpc-primitives = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } - [dev-dependencies] assert_matches = { workspace = true } insta = { workspace = true } diff --git a/crates/mpc-attestation/Cargo.toml b/crates/mpc-attestation/Cargo.toml index 7af910854..7922a8230 100644 --- a/crates/mpc-attestation/Cargo.toml +++ b/crates/mpc-attestation/Cargo.toml @@ -13,12 +13,12 @@ borsh = { workspace = true } derive_more = { workspace = true } hex = { workspace = true } include-measurements = { workspace = true } +launcher-interface = { workspace = true } mpc-primitives = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } sha2 = { workspace = true } sha3 = { workspace = true } -launcher-interface = { workspace = true } [dev-dependencies] assert_matches = { workspace = true } diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 0e682ba0d..af793fcd1 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -17,7 +17,6 @@ backon = { workspace = true } base64 = { workspace = true } borsh = { workspace = true } bounded-collections = { workspace = true } -launcher-interface = { workspace = true } bs58 = { workspace = true } bytes = { workspace = true } clap = { workspace = true } @@ -35,6 +34,7 @@ humantime = { workspace = true } hyper = { workspace = true } itertools = { workspace = true } k256 = { workspace = true } +launcher-interface = { workspace = true } lru = { workspace = true } mpc-attestation = { workspace = true } mpc-contract = { workspace = true } diff --git a/crates/node/src/tee/allowed_image_hashes_watcher.rs b/crates/node/src/tee/allowed_image_hashes_watcher.rs index 2a5af11ad..de2ba5d53 100644 --- a/crates/node/src/tee/allowed_image_hashes_watcher.rs +++ b/crates/node/src/tee/allowed_image_hashes_watcher.rs @@ -47,7 +47,7 @@ impl AllowedImageHashesStorage for AllowedImageHashesFile { }; let json = serde_json::to_string_pretty(&approved_hashes) - .expect("previous json! macro would also panic. TODO figure out what to return"); + .expect("previous json! macro would also panic. figure out what to return"); tracing::debug!(?approved_hashes, "writing approved hashes to disk"); diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index da20a9ba4..32a65aad0 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -28,7 +28,6 @@ url = { workspace = true, features = ["serde"] } [dev-dependencies] assert_matches = { workspace = true } -tempfile = { workspace = true } [lints] workspace = true From a5cbac2a1a8688bc729f52a7474f8a27caeac839 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 17:41:53 +0100 Subject: [PATCH 063/176] make check all fast pass --- crates/tee-launcher/src/docker_types.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tee-launcher/src/docker_types.rs b/crates/tee-launcher/src/docker_types.rs index e48780fd3..6c3bf27ce 100644 --- a/crates/tee-launcher/src/docker_types.rs +++ b/crates/tee-launcher/src/docker_types.rs @@ -1,7 +1,7 @@ use launcher_interface::types::DockerSha256Digest; use serde::{Deserialize, Serialize}; -/// Partial response https://auth.docker.io/token +/// Partial response #[derive(Debug, Deserialize, Serialize)] pub struct DockerTokenResponse { pub token: String, From 3138c557bc5bf37f06e4539ea9549e4935f0fcbc Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 17:46:34 +0100 Subject: [PATCH 064/176] undo nearcore --- libs/nearcore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/nearcore b/libs/nearcore index 3def2f7eb..8a8c21bc8 160000 --- a/libs/nearcore +++ b/libs/nearcore @@ -1 +1 @@ -Subproject commit 3def2f7ebb7455199e7b3f7b371e3735c23e2930 +Subproject commit 8a8c21bc81999af93edd1b6bca5b7c6c6337aa63 From d8f44a65f2e7f8a38107a0ec9311060dc168dbb4 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 6 Mar 2026 17:46:51 +0100 Subject: [PATCH 065/176] undo launcher --- tee_launcher/launcher.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tee_launcher/launcher.py b/tee_launcher/launcher.py index e8beeb9c2..a0dd34958 100644 --- a/tee_launcher/launcher.py +++ b/tee_launcher/launcher.py @@ -147,7 +147,7 @@ class Platform(Enum): ALLOWED_MPC_ENV_VARS = { "MPC_ACCOUNT_ID", # ID of the MPC account on the network "MPC_LOCAL_ADDRESS", # Local IP address or hostname used by the MPC node - "MPC_SECRET_STORE_KEY", # Key used to encrypt/decrypt secrets // Isn't this deprecated?, + "MPC_SECRET_STORE_KEY", # Key used to encrypt/decrypt secrets "MPC_CONTRACT_ID", # Contract ID associated with the MPC node "MPC_ENV", # Environment (e.g., 'testnet', 'mainnet') "MPC_HOME_DIR", # Home directory for the MPC node @@ -242,7 +242,6 @@ def is_safe_port_mapping(mapping: str) -> bool: def remove_existing_container(): - # changed in rust, no point checking current container exists. Just send shutdown signal to MPC_CONTAINER_NAME """Stop and remove the MPC container if it exists.""" try: containers = check_output( @@ -261,7 +260,6 @@ class ImageSpec: image_name: str registry: str - # TODO: This post validation is not covered def __post_init__(self): if not self.tags or not all(is_non_empty_and_cleaned(tag) for tag in self.tags): raise ValueError( From 2ebb35f03eb55178e45169cc5958f2805f7972a2 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Mon, 9 Mar 2026 13:44:22 +0100 Subject: [PATCH 066/176] oneshot change to toml --- Cargo.lock | 50 +++++++++++++++-- Cargo.toml | 1 + crates/node/Cargo.toml | 1 + crates/node/src/cli.rs | 6 +- crates/node/src/config/start.rs | 8 +-- docs/localnet/localnet.md | 14 ++--- docs/localnet/mpc-config.template.json | 75 ------------------------- docs/localnet/mpc-config.template.toml | 77 ++++++++++++++++++++++++++ libs/nearcore | 2 +- pytest/common_lib/shared/mpc_node.py | 10 ++-- pytest/requirements.txt | 1 + 11 files changed, 146 insertions(+), 99 deletions(-) delete mode 100644 docs/localnet/mpc-config.template.json create mode 100644 docs/localnet/mpc-config.template.toml diff --git a/Cargo.lock b/Cargo.lock index 43161c697..c6466809f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1640,7 +1640,7 @@ dependencies = [ "serde-untagged", "serde-value", "thiserror 2.0.18", - "toml", + "toml 0.8.23", "unicode-xid", "url", ] @@ -5554,6 +5554,7 @@ dependencies = [ "tokio-rustls", "tokio-stream", "tokio-util", + "toml 1.0.6+spec-1.1.0", "tower", "tracing", "tracing-subscriber", @@ -9950,6 +9951,15 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_spanned" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +dependencies = [ + "serde_core", +] + [[package]] name = "serde_urlencoded" version = "0.7.1" @@ -10963,11 +10973,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dc1beb996b9d83529a9e75c17a1686767d148d70663143c7854d8b4a09ced362" dependencies = [ "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_edit 0.22.27", ] +[[package]] +name = "toml" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "399b1124a3c9e16766831c6bba21e50192572cdd98706ea114f9502509686ffc" +dependencies = [ + "indexmap 2.13.0", + "serde_core", + "serde_spanned 1.0.4", + "toml_datetime 1.0.0+spec-1.1.0", + "toml_parser", + "toml_writer", + "winnow", +] + [[package]] name = "toml_datetime" version = "0.6.11" @@ -10986,6 +11011,15 @@ dependencies = [ "serde_core", ] +[[package]] +name = "toml_datetime" +version = "1.0.0+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32c2555c699578a4f59f0cc68e5116c8d7cabbd45e1409b989d4be085b53f13e" +dependencies = [ + "serde_core", +] + [[package]] name = "toml_edit" version = "0.22.27" @@ -10994,7 +11028,7 @@ checksum = "41fe8c660ae4257887cf66394862d21dbca4a6ddd26f04a3560410406a2f819a" dependencies = [ "indexmap 2.13.0", "serde", - "serde_spanned", + "serde_spanned 0.6.9", "toml_datetime 0.6.11", "toml_write", "winnow", @@ -11014,9 +11048,9 @@ dependencies = [ [[package]] name = "toml_parser" -version = "1.0.7+spec-1.1.0" +version = "1.0.9+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "247eaa3197818b831697600aadf81514e577e0cba5eab10f7e064e78ae154df1" +checksum = "702d4415e08923e7e1ef96cd5727c0dfed80b4d2fa25db9647fe5eb6f7c5a4c4" dependencies = [ "winnow", ] @@ -11027,6 +11061,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" +[[package]] +name = "toml_writer" +version = "1.0.6+spec-1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" + [[package]] name = "tonic" version = "0.13.1" diff --git a/Cargo.toml b/Cargo.toml index 459adf731..81f61d02d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,6 +166,7 @@ rustls = { version = "0.23.36", default-features = false, features = ["std"] } serde = { version = "1.0", features = ["derive"] } serde_bytes = "0.11.19" serde_json = "1.0" +toml = "1.0.6" serde_repr = "0.1.20" serde_with = { version = "3.16.1", features = ["hex"] } serial_test = "3.4.0" diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 65233197f..1f3a02a20 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -59,6 +59,7 @@ rustls = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_with = { workspace = true } +toml = { workspace = true } serde_yaml = { workspace = true } sha3 = { workspace = true } socket2 = { workspace = true } diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index f5529b72b..514691aec 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -44,10 +44,10 @@ pub enum LogFormat { #[derive(Subcommand, Debug)] pub enum CliCommand { - /// Starts the MPC node using a single JSON configuration file instead of + /// Starts the MPC node using a single TOML configuration file instead of /// environment variables and CLI flags. StartWithConfigFile { - /// Path to a JSON configuration file containing all settings needed to + /// Path to a TOML configuration file containing all settings needed to /// start the MPC node. config_path: PathBuf, }, @@ -226,7 +226,7 @@ impl Cli { pub async fn run(self) -> anyhow::Result<()> { match self.command { CliCommand::StartWithConfigFile { config_path } => { - let node_configuration = StartConfig::from_json_file(&config_path)?; + let node_configuration = StartConfig::from_toml_file(&config_path)?; run_mpc_node(node_configuration).await } // TODO(#2334): deprecate this diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index b2a26e565..dea203f84 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -10,7 +10,7 @@ use url::Url; /// Configuration for starting the MPC node. This is the canonical type used /// by the run logic. Both `StartCmd` (CLI flags) and `StartWithConfigFileCmd` -/// (JSON file) convert into this type. +/// (TOML file) convert into this type. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct StartConfig { pub home_dir: PathBuf, @@ -74,7 +74,7 @@ pub struct GcpStartConfig { pub project_id: String, } -/// TEE authority configuration for JSON deserialization. +/// TEE authority configuration for deserialization. #[derive(Debug, Clone, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum TeeAuthorityStartConfig { @@ -114,10 +114,10 @@ impl TeeAuthorityStartConfig { } impl StartConfig { - pub fn from_json_file(path: &std::path::Path) -> anyhow::Result { + pub fn from_toml_file(path: &std::path::Path) -> anyhow::Result { let content = std::fs::read_to_string(path) .with_context(|| format!("failed to read config file: {}", path.display()))?; - let config: Self = serde_json::from_str(&content) + let config: Self = toml::from_str(&content) .with_context(|| format!("failed to parse config file: {}", path.display()))?; config .node diff --git a/docs/localnet/localnet.md b/docs/localnet/localnet.md index 90045af21..14a8d81db 100644 --- a/docs/localnet/localnet.md +++ b/docs/localnet/localnet.md @@ -191,13 +191,13 @@ Since this is not a validator node, we can remove `validator_key.json` rm ~/.near/mpc-frodo/validator_key.json ``` -Next we'll create a JSON configuration file for Frodo's MPC node using the -shared template at `docs/localnet/mpc-config.template.json`. This single file +Next we'll create a TOML configuration file for Frodo's MPC node using the +shared template at `docs/localnet/mpc-config.template.toml`. This single file contains all settings (secrets, TEE config, and node parameters): ```shell env MPC_NODE_ID=mpc-frodo NEAR_ACCOUNT_ID=frodo.test.near WEB_UI_PORT=8081 MIGRATION_WEB_UI_PORT=8079 PPROF_PORT=34001 \ - envsubst < docs/localnet/mpc-config.template.json > ~/.near/mpc-frodo/mpc-config.json + envsubst < docs/localnet/mpc-config.template.toml > ~/.near/mpc-frodo/mpc-config.toml ``` ### Initialize Sam's node @@ -222,19 +222,19 @@ rm ~/.near/mpc-sam/validator_key.json ```shell env MPC_NODE_ID=mpc-sam NEAR_ACCOUNT_ID=sam.test.near WEB_UI_PORT=8082 MIGRATION_WEB_UI_PORT=8078 PPROF_PORT=34002 \ - envsubst < docs/localnet/mpc-config.template.json > ~/.near/mpc-sam/mpc-config.json + envsubst < docs/localnet/mpc-config.template.toml > ~/.near/mpc-sam/mpc-config.toml ``` ### Run the MPC binary -In two separate shells run the MPC binary for Frodo and Sam using their JSON config files: +In two separate shells run the MPC binary for Frodo and Sam using their TOML config files: ```shell -RUST_LOG=info mpc-node start-with-config-file ~/.near/mpc-sam/mpc-config.json +RUST_LOG=info mpc-node start-with-config-file ~/.near/mpc-sam/mpc-config.toml ``` ```shell -RUST_LOG=info mpc-node start-with-config-file ~/.near/mpc-frodo/mpc-config.json +RUST_LOG=info mpc-node start-with-config-file ~/.near/mpc-frodo/mpc-config.toml ``` Notes: diff --git a/docs/localnet/mpc-config.template.json b/docs/localnet/mpc-config.template.json deleted file mode 100644 index 07e15ff27..000000000 --- a/docs/localnet/mpc-config.template.json +++ /dev/null @@ -1,75 +0,0 @@ -{ - "home_dir": "$HOME/.near/$MPC_NODE_ID", - "secrets": { - "secret_store_key_hex": "11111111111111111111111111111111" - }, - "tee": { - "authority": { "type": "local" }, - "image_hash": "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0", - "latest_allowed_hash_file": "/tmp/LATEST_ALLOWED_HASH_FILE.txt" - }, - "node": { - "my_near_account_id": "$NEAR_ACCOUNT_ID", - "near_responder_account_id": "$NEAR_ACCOUNT_ID", - "number_of_responder_keys": 1, - "web_ui": "127.0.0.1:$WEB_UI_PORT", - "migration_web_ui": "127.0.0.1:$MIGRATION_WEB_UI_PORT", - "pprof_bind_address": "127.0.0.1:$PPROF_PORT", - "triple": { - "concurrency": 2, - "desired_triples_to_buffer": 128, - "timeout_sec": 60, - "parallel_triple_generation_stagger_time_sec": 1 - }, - "presignature": { - "concurrency": 4, - "desired_presignatures_to_buffer": 64, - "timeout_sec": 60 - }, - "signature": { "timeout_sec": 60 }, - "indexer": { - "validate_genesis": false, - "sync_mode": "Latest", - "concurrency": 1, - "mpc_contract_id": "mpc-contract.test.near", - "finality": "optimistic" - }, - "ckd": { "timeout_sec": 60 }, - "cores": 4, - "foreign_chains": { - "bitcoin": { - "timeout_sec": 30, - "max_retries": 3, - "providers": { - "public": { - "api_variant": "esplora", - "rpc_url": "https://bitcoin-rpc.publicnode.com", - "auth": { "kind": "none" } - } - } - }, - "abstract": { - "timeout_sec": 30, - "max_retries": 3, - "providers": { - "public": { - "api_variant": "standard", - "rpc_url": "https://api.testnet.abs.xyz", - "auth": { "kind": "none" } - } - } - }, - "starknet": { - "timeout_sec": 30, - "max_retries": 3, - "providers": { - "public": { - "api_variant": "standard", - "rpc_url": "https://starknet-rpc.publicnode.com", - "auth": { "kind": "none" } - } - } - } - } - } -} diff --git a/docs/localnet/mpc-config.template.toml b/docs/localnet/mpc-config.template.toml new file mode 100644 index 000000000..c9f7d6d4d --- /dev/null +++ b/docs/localnet/mpc-config.template.toml @@ -0,0 +1,77 @@ +home_dir = "$HOME/.near/$MPC_NODE_ID" + +[secrets] +secret_store_key_hex = "11111111111111111111111111111111" + +[tee] +image_hash = "8b40f81f77b8c22d6c777a6e14d307a1d11cb55ab83541fbb8575d02d86a74b0" +latest_allowed_hash_file = "/tmp/LATEST_ALLOWED_HASH_FILE.txt" + +[tee.authority] +type = "local" + +[node] +my_near_account_id = "$NEAR_ACCOUNT_ID" +near_responder_account_id = "$NEAR_ACCOUNT_ID" +number_of_responder_keys = 1 +web_ui = "127.0.0.1:$WEB_UI_PORT" +migration_web_ui = "127.0.0.1:$MIGRATION_WEB_UI_PORT" +pprof_bind_address = "127.0.0.1:$PPROF_PORT" +cores = 4 + +[node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[node.signature] +timeout_sec = 60 + +[node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "mpc-contract.test.near" +finality = "optimistic" + +[node.ckd] +timeout_sec = 60 + +[node.foreign_chains.bitcoin] +timeout_sec = 30 +max_retries = 3 + +[node.foreign_chains.bitcoin.providers.public] +api_variant = "esplora" +rpc_url = "https://bitcoin-rpc.publicnode.com" + +[node.foreign_chains.bitcoin.providers.public.auth] +kind = "none" + +[node.foreign_chains.abstract] +timeout_sec = 30 +max_retries = 3 + +[node.foreign_chains.abstract.providers.public] +api_variant = "standard" +rpc_url = "https://api.testnet.abs.xyz" + +[node.foreign_chains.abstract.providers.public.auth] +kind = "none" + +[node.foreign_chains.starknet] +timeout_sec = 30 +max_retries = 3 + +[node.foreign_chains.starknet.providers.public] +api_variant = "standard" +rpc_url = "https://starknet-rpc.publicnode.com" + +[node.foreign_chains.starknet.providers.public.auth] +kind = "none" diff --git a/libs/nearcore b/libs/nearcore index 8a8c21bc8..3def2f7eb 160000 --- a/libs/nearcore +++ b/libs/nearcore @@ -1 +1 @@ -Subproject commit 8a8c21bc81999af93edd1b6bca5b7c6c6337aa63 +Subproject commit 3def2f7ebb7455199e7b3f7b371e3735c23e2930 diff --git a/pytest/common_lib/shared/mpc_node.py b/pytest/common_lib/shared/mpc_node.py index c8b87d5ce..fcbb007b3 100644 --- a/pytest/common_lib/shared/mpc_node.py +++ b/pytest/common_lib/shared/mpc_node.py @@ -6,6 +6,8 @@ import time from typing import cast +import tomli_w + from key import Key @@ -130,7 +132,7 @@ def reset_mpc_data(self): file_path.unlink() def _write_start_config(self) -> str: - """Build a StartConfig JSON file and write it to the node's home dir. + """Build a StartConfig TOML file and write it to the node's home dir. Returns the path to the written config file.""" start_config = { "home_dir": self.home_dir, @@ -145,9 +147,9 @@ def _write_start_config(self) -> str: }, "node": self.node_config, } - config_path = str(pathlib.Path(self.home_dir) / "start_config.json") - with open(config_path, "w") as f: - json.dump(start_config, f, indent=2) + config_path = str(pathlib.Path(self.home_dir) / "start_config.toml") + with open(config_path, "wb") as f: + tomli_w.dump(start_config, f) return config_path def run(self): diff --git a/pytest/requirements.txt b/pytest/requirements.txt index 1f5f14c5e..1bbc919bd 100644 --- a/pytest/requirements.txt +++ b/pytest/requirements.txt @@ -4,3 +4,4 @@ blspy py-arkworks-bls12381 pytest==8.3.4 gitpython +tomli_w From 7d5cc0951aedc8f730beed2e8d60b25e3b804de4 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Mon, 9 Mar 2026 13:51:10 +0100 Subject: [PATCH 067/176] sort toml declaration --- Cargo.toml | 2 +- crates/node/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 81f61d02d..85d9c5b67 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,7 +166,6 @@ rustls = { version = "0.23.36", default-features = false, features = ["std"] } serde = { version = "1.0", features = ["derive"] } serde_bytes = "0.11.19" serde_json = "1.0" -toml = "1.0.6" serde_repr = "0.1.20" serde_with = { version = "3.16.1", features = ["hex"] } serial_test = "3.4.0" @@ -184,6 +183,7 @@ tokio-metrics = { version = "0.4.8" } tokio-rustls = { version = "0.26.4", default-features = false } tokio-stream = { version = "0.1" } tokio-util = { version = "0.7.12", features = ["time"] } +toml = "1.0.6" tower = "0.5.3" tracing = "0.1.44" tracing-subscriber = { version = "0.3.22", features = [ diff --git a/crates/node/Cargo.toml b/crates/node/Cargo.toml index 1f3a02a20..2ce8b5e54 100644 --- a/crates/node/Cargo.toml +++ b/crates/node/Cargo.toml @@ -59,7 +59,6 @@ rustls = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } serde_with = { workspace = true } -toml = { workspace = true } serde_yaml = { workspace = true } sha3 = { workspace = true } socket2 = { workspace = true } @@ -74,6 +73,7 @@ tokio-metrics = { workspace = true } tokio-rustls = { workspace = true } tokio-stream = { workspace = true } tokio-util = { workspace = true } +toml = { workspace = true } tower = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } From ee45ca9d523cb1083ea092e945f6a9a6bc62cb4f Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Mon, 9 Mar 2026 13:55:51 +0100 Subject: [PATCH 068/176] revert nearcore change --- libs/nearcore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/libs/nearcore b/libs/nearcore index 3def2f7eb..8a8c21bc8 160000 --- a/libs/nearcore +++ b/libs/nearcore @@ -1 +1 @@ -Subproject commit 3def2f7ebb7455199e7b3f7b371e3735c23e2930 +Subproject commit 8a8c21bc81999af93edd1b6bca5b7c6c6337aa63 From 3b4dcbbacc0dc708672cb7348e99a280bab10610 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Mon, 9 Mar 2026 14:13:47 +0100 Subject: [PATCH 069/176] pytest, dont incldue None fields --- pytest/common_lib/shared/mpc_node.py | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pytest/common_lib/shared/mpc_node.py b/pytest/common_lib/shared/mpc_node.py index fcbb007b3..9f6fad0d1 100644 --- a/pytest/common_lib/shared/mpc_node.py +++ b/pytest/common_lib/shared/mpc_node.py @@ -131,6 +131,15 @@ def reset_mpc_data(self): for file_path in pathlib.Path(self.home_dir).glob(pattern): file_path.unlink() + @staticmethod + def _strip_none(obj): + """Recursively remove keys with None values since TOML has no null.""" + if isinstance(obj, dict): + return {k: MpcNode._strip_none(v) for k, v in obj.items() if v is not None} + if isinstance(obj, list): + return [MpcNode._strip_none(v) for v in obj] + return obj + def _write_start_config(self) -> str: """Build a StartConfig TOML file and write it to the node's home dir. Returns the path to the written config file.""" @@ -149,7 +158,7 @@ def _write_start_config(self) -> str: } config_path = str(pathlib.Path(self.home_dir) / "start_config.toml") with open(config_path, "wb") as f: - tomli_w.dump(start_config, f) + tomli_w.dump(self._strip_none(start_config), f) return config_path def run(self): From 7d45c50ee93c953fd3f01e844b5723cf7b96c074 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 13 Mar 2026 13:11:19 +0100 Subject: [PATCH 070/176] remove extra hosts functionality, see #2438 --- crates/tee-launcher/src/main.rs | 10 ++----- crates/tee-launcher/src/types.rs | 49 -------------------------------- 2 files changed, 3 insertions(+), 56 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 8e60a8bcc..0df736096 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -378,7 +378,6 @@ fn docker_run_args( format!("{host_path}:{MPC_CONFIG_CONTAINER_PATH}:ro"), ]); - cmd.extend(docker_flags.extra_hosts.docker_args()); cmd.extend(docker_flags.port_mappings.docker_args()); // Container run configuration @@ -481,15 +480,13 @@ mod tests { fn empty_docker_flags() -> DockerLaunchFlags { serde_json::from_value(serde_json::json!({ - "extra_hosts": {"hosts": []}, "port_mappings": {"ports": []} })) .unwrap() } - fn docker_flags_with_host_and_port() -> DockerLaunchFlags { + fn docker_flags_with_port() -> DockerLaunchFlags { serde_json::from_value(serde_json::json!({ - "extra_hosts": {"hosts": [{"hostname": {"Domain": "node1"}, "ip": "192.168.1.1"}]}, "port_mappings": {"ports": [{"src": 11780, "dst": 11780}]} })) .unwrap() @@ -625,9 +622,9 @@ mod tests { } #[test] - fn includes_ports_and_extra_hosts() { + fn includes_ports() { // given - let flags = docker_flags_with_host_and_port(); + let flags = docker_flags_with_port(); let digest = sample_digest(); // when @@ -640,7 +637,6 @@ mod tests { // then let joined = args.join(" "); - assert!(joined.contains("--add-host node1:192.168.1.1")); assert!(joined.contains("-p 11780:11780")); } diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 29b6510e0..bfd2f29cb 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -71,27 +71,9 @@ pub struct LauncherConfig { #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DockerLaunchFlags { - pub extra_hosts: ExtraHosts, pub port_mappings: PortMappings, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct ExtraHosts { - hosts: Vec, -} - -impl ExtraHosts { - /// Returns `["--add-host", "h1:ip1", "--add-host", "h2:ip2", ...]`. - pub fn docker_args(&self) -> Vec { - self.hosts - .iter() - .flat_map(|HostEntry { hostname, ip }| { - ["--add-host".into(), format!("{hostname}:{ip}")] - }) - .collect() - } -} - /// A `--add-host` entry: `hostname:IPv4`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct HostEntry { @@ -220,35 +202,6 @@ mod tests { // --- docker_args output format --- - #[test] - fn extra_hosts_docker_args_format() { - // given - let hosts = ExtraHosts { - hosts: vec![HostEntry { - hostname: url::Host::Domain("node.local".into()), - ip: Ipv4Addr::new(192, 168, 1, 1), - }], - }; - - // when - let args = hosts.docker_args(); - - // then - assert_eq!(args, vec!["--add-host", "node.local:192.168.1.1"]); - } - - #[test] - fn empty_extra_hosts_produces_no_docker_args() { - // given - let hosts = ExtraHosts { hosts: vec![] }; - - // when - let args = hosts.docker_args(); - - // then - assert!(args.is_empty()); - } - #[test] fn port_mappings_docker_args_format() { // given @@ -282,7 +235,6 @@ mod tests { "mpc_hash_override": null }, "docker_command_config": { - "extra_hosts": {"hosts": [{"hostname": {"Domain": "node1"}, "ip": "192.168.1.1"}]}, "port_mappings": {"ports": [{"src": 11780, "dst": 11780}]} }, "mpc_config_file": "/tapp/mpc-config.json" @@ -312,7 +264,6 @@ mod tests { "mpc_hash_override": null }, "docker_command_config": { - "extra_hosts": {"hosts": []}, "port_mappings": {"ports": []} } }); From 4fcfdc6213b989ba221784a844ad77763173034a Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 13 Mar 2026 21:39:54 +0100 Subject: [PATCH 071/176] use compose tempalte file --- Cargo.lock | 1 + crates/tee-launcher/Cargo.toml | 1 + .../docker-compose.tee.template.yml | 20 ++ .../tee-launcher/docker-compose.template.yml | 17 ++ crates/tee-launcher/src/error.rs | 9 + crates/tee-launcher/src/main.rs | 228 +++++++----------- crates/tee-launcher/src/types.rs | 29 +-- 7 files changed, 146 insertions(+), 159 deletions(-) create mode 100644 crates/tee-launcher/docker-compose.tee.template.yml create mode 100644 crates/tee-launcher/docker-compose.template.yml diff --git a/Cargo.lock b/Cargo.lock index 28413386c..4013b8aba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10658,6 +10658,7 @@ dependencies = [ "reqwest 0.12.28", "serde", "serde_json", + "tempfile", "thiserror 2.0.18", "tokio", "tracing", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index a9e4e2cb9..cf93a02eb 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -20,6 +20,7 @@ launcher-interface = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } tracing = { workspace = true } diff --git a/crates/tee-launcher/docker-compose.tee.template.yml b/crates/tee-launcher/docker-compose.tee.template.yml new file mode 100644 index 000000000..6312625e1 --- /dev/null +++ b/crates/tee-launcher/docker-compose.tee.template.yml @@ -0,0 +1,20 @@ +services: + mpc-node: + image: "{{IMAGE}}" + container_name: "{{CONTAINER_NAME}}" + security_opt: + - no-new-privileges:true + ports: {{PORTS}} + environment: + - "DSTACK_ENDPOINT={{DSTACK_UNIX_SOCKET}}" + volumes: + - "{{MPC_CONFIG_HOST_PATH}}:{{MPC_CONFIG_CONTAINER_PATH}}:ro" + - /tapp:/tapp:ro + - shared-volume:/mnt/shared + - mpc-data:/data + - "{{DSTACK_UNIX_SOCKET}}:{{DSTACK_UNIX_SOCKET}}" + command: ["start-with-config-file", "{{MPC_CONFIG_CONTAINER_PATH}}"] + +volumes: + shared-volume: + mpc-data: diff --git a/crates/tee-launcher/docker-compose.template.yml b/crates/tee-launcher/docker-compose.template.yml new file mode 100644 index 000000000..29b44651e --- /dev/null +++ b/crates/tee-launcher/docker-compose.template.yml @@ -0,0 +1,17 @@ +services: + mpc-node: + image: "{{IMAGE}}" + container_name: "{{CONTAINER_NAME}}" + security_opt: + - no-new-privileges:true + ports: {{PORTS}} + volumes: + - "{{MPC_CONFIG_HOST_PATH}}:{{MPC_CONFIG_CONTAINER_PATH}}:ro" + - /tapp:/tapp:ro + - shared-volume:/mnt/shared + - mpc-data:/data + command: ["start-with-config-file", "{{MPC_CONFIG_CONTAINER_PATH}}"] + +volumes: + shared-volume: + mpc-data: diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 02a45a3ff..9ce1c75a2 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -33,6 +33,15 @@ pub enum LauncherError { source: std::io::Error, }, + #[error("Failed to write {path}: {source}")] + FileWrite { + path: String, + source: std::io::Error, + }, + + #[error("Failed to create temp file: {0}")] + TempFileCreate(std::io::Error), + #[error("Failed to parse {path}: {source}")] JsonParse { path: String, diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 0df736096..3206b0022 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -1,5 +1,6 @@ // A rewrite of launcher.py +use std::io::Write; use std::process::Command; use std::{collections::VecDeque, time::Duration}; @@ -20,6 +21,9 @@ mod docker_types; mod error; mod types; +const COMPOSE_TEMPLATE: &str = include_str!("../docker-compose.template.yml"); +const COMPOSE_TEE_TEMPLATE: &str = include_str!("../docker-compose.tee.template.yml"); + const DOCKER_AUTH_ACCEPT_HEADER_VALUE: HeaderValue = HeaderValue::from_static("application/vnd.docker.distribution.manifest.v2+json"); @@ -352,57 +356,47 @@ async fn validate_image_hash( Ok(()) } -fn docker_run_args( +fn render_compose_file( platform: Platform, mpc_config_file: &std::path::Path, docker_flags: &DockerLaunchFlags, image_digest: &DockerSha256Digest, -) -> Vec { - let mut cmd: Vec = vec![]; +) -> Result { + let template = match platform { + Platform::Tee => COMPOSE_TEE_TEMPLATE, + Platform::NonTee => COMPOSE_TEMPLATE, + }; - if platform == Platform::Tee { - cmd.extend([ - "--env".into(), - format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"), - ]); - cmd.extend([ - "-v".into(), - format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"), - ]); - } + let ports: Vec = docker_flags + .port_mappings + .ports + .iter() + .map(|p| p.docker_compose_value()) + .collect(); + let ports_json = serde_json::to_string(&ports).expect("port list is serializable"); + + let rendered = template + .replace("{{IMAGE}}", &image_digest.to_string()) + .replace("{{CONTAINER_NAME}}", MPC_CONTAINER_NAME) + .replace( + "{{MPC_CONFIG_HOST_PATH}}", + &mpc_config_file.display().to_string(), + ) + .replace("{{MPC_CONFIG_CONTAINER_PATH}}", MPC_CONFIG_CONTAINER_PATH) + .replace("{{DSTACK_UNIX_SOCKET}}", DSTACK_UNIX_SOCKET) + .replace("{{PORTS}}", &ports_json); + + tracing::info!(compose = %rendered, "rendered docker-compose file"); - // Mount the MPC config file into the container (read-only) - let host_path = mpc_config_file.display(); - cmd.extend([ - "-v".into(), - format!("{host_path}:{MPC_CONFIG_CONTAINER_PATH}:ro"), - ]); - - cmd.extend(docker_flags.port_mappings.docker_args()); - - // Container run configuration - cmd.extend([ - "--security-opt".into(), - "no-new-privileges:true".into(), - "-v".into(), - "/tapp:/tapp:ro".into(), - "-v".into(), - "shared-volume:/mnt/shared".into(), - "-v".into(), - "mpc-data:/data".into(), - "--name".into(), - MPC_CONTAINER_NAME.into(), - "--detach".into(), - image_digest.to_string(), - // Command for the MPC binary: read config from file - "start-with-config-file".into(), - MPC_CONFIG_CONTAINER_PATH.into(), - ]); - - let docker_command_string = cmd.join(" "); - tracing::info!(?docker_command_string, "docker cmd"); - - cmd + let mut file = + tempfile::NamedTempFile::new().map_err(|source| LauncherError::TempFileCreate(source))?; + file.write_all(rendered.as_bytes()) + .map_err(|source| LauncherError::FileWrite { + path: file.path().display().to_string(), + source, + })?; + + Ok(file) } fn launch_mpc_container( @@ -413,16 +407,17 @@ fn launch_mpc_container( ) -> Result<(), LauncherError> { tracing::info!("Launching MPC node with validated hash: {valid_hash}",); - // shutdown container if one is already running + let compose_file = + render_compose_file(platform, mpc_config_file, docker_flags, valid_hash)?; + let compose_path = compose_file.path().display().to_string(); + + // Remove any existing container from a previous run (by name, independent of compose file) let _ = Command::new("docker") .args(["rm", "-f", MPC_CONTAINER_NAME]) .output(); - let docker_run_args = docker_run_args(platform, mpc_config_file, docker_flags, valid_hash); - let run_output = Command::new("docker") - .arg("run") - .args(&docker_run_args) + .args(["compose", "-f", &compose_path, "up", "-d"]) .output() .map_err(|inner| LauncherError::DockerRunFailed { image_hash: valid_hash.clone(), @@ -432,7 +427,7 @@ fn launch_mpc_container( if !run_output.status.success() { let stderr = String::from_utf8_lossy(&run_output.stderr); let stdout = String::from_utf8_lossy(&run_output.stdout); - tracing::error!(%stderr, %stdout, "docker run failed"); + tracing::error!(%stderr, %stdout, "docker compose up failed"); return Err(LauncherError::DockerRunFailedExitStatus { image_hash: valid_hash.clone(), output: stderr.into_owned(), @@ -452,13 +447,23 @@ mod tests { use near_mpc_bounded_collections::NonEmptyVec; use crate::constants::*; - use crate::docker_run_args; use crate::error::LauncherError; + use crate::render_compose_file; use crate::select_image_hash; use crate::types::*; const SAMPLE_CONFIG_PATH: &str = "/tapp/mpc-config.json"; + fn render( + platform: Platform, + config_path: &str, + flags: &DockerLaunchFlags, + digest: &DockerSha256Digest, + ) -> String { + let file = render_compose_file(platform, Path::new(config_path), flags, digest).unwrap(); + std::fs::read_to_string(file.path()).unwrap() + } + fn digest(hex_char: char) -> DockerSha256Digest { format!( "sha256:{}", @@ -493,43 +498,31 @@ mod tests { } #[test] - fn tee_mode_includes_dstack_mount() { + fn tee_mode_includes_dstack_env_and_volume() { // given let flags = empty_docker_flags(); let digest = sample_digest(); // when - let args = docker_run_args( - Platform::Tee, - Path::new(SAMPLE_CONFIG_PATH), - &flags, - &digest, - ); + let rendered = render(Platform::Tee, SAMPLE_CONFIG_PATH, &flags, &digest); // then - let joined = args.join(" "); - assert!(joined.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); - assert!(joined.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); + assert!(rendered.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); + assert!(rendered.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); } #[test] - fn nontee_mode_excludes_dstack_mount() { + fn nontee_mode_excludes_dstack() { // given let flags = empty_docker_flags(); let digest = sample_digest(); // when - let args = docker_run_args( - Platform::NonTee, - Path::new(SAMPLE_CONFIG_PATH), - &flags, - &digest, - ); + let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); // then - let joined = args.join(" "); - assert!(!joined.contains("DSTACK_ENDPOINT=")); - assert!(!joined.contains(&format!("{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"))); + assert!(!rendered.contains("DSTACK_ENDPOINT")); + assert!(!rendered.contains(DSTACK_UNIX_SOCKET)); } #[test] @@ -539,21 +532,14 @@ mod tests { let digest = sample_digest(); // when - let args = docker_run_args( - Platform::NonTee, - Path::new(SAMPLE_CONFIG_PATH), - &flags, - &digest, - ); + let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); // then - let joined = args.join(" "); - assert!(joined.contains("--security-opt no-new-privileges:true")); - assert!(joined.contains("/tapp:/tapp:ro")); - assert!(joined.contains("shared-volume:/mnt/shared")); - assert!(joined.contains("mpc-data:/data")); - assert!(joined.contains(&format!("--name {MPC_CONTAINER_NAME}"))); - assert!(joined.contains("--detach")); + assert!(rendered.contains("no-new-privileges:true")); + assert!(rendered.contains("/tapp:/tapp:ro")); + assert!(rendered.contains("shared-volume:/mnt/shared")); + assert!(rendered.contains("mpc-data:/data")); + assert!(rendered.contains(&format!("container_name: \"{MPC_CONTAINER_NAME}\""))); } #[test] @@ -563,16 +549,10 @@ mod tests { let digest = sample_digest(); // when - let args = docker_run_args( - Platform::NonTee, - Path::new(SAMPLE_CONFIG_PATH), - &flags, - &digest, - ); + let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); // then - let joined = args.join(" "); - assert!(joined.contains(&format!( + assert!(rendered.contains(&format!( "{SAMPLE_CONFIG_PATH}:{MPC_CONFIG_CONTAINER_PATH}:ro" ))); } @@ -584,41 +564,24 @@ mod tests { let digest = sample_digest(); // when - let args = docker_run_args( - Platform::NonTee, - Path::new(SAMPLE_CONFIG_PATH), - &flags, - &digest, - ); + let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); // then - let joined = args.join(" "); - assert!(joined.contains(&format!( - "start-with-config-file {MPC_CONFIG_CONTAINER_PATH}" - ))); + assert!(rendered.contains("start-with-config-file")); + assert!(rendered.contains(MPC_CONFIG_CONTAINER_PATH)); } #[test] - fn image_digest_appears_before_command() { + fn image_is_set() { // given let flags = empty_docker_flags(); let digest = sample_digest(); // when - let args = docker_run_args( - Platform::NonTee, - Path::new(SAMPLE_CONFIG_PATH), - &flags, - &digest, - ); + let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); - // then - image digest should appear before "start-with-config-file" - let digest_pos = args.iter().position(|a| a == &digest.to_string()).unwrap(); - let cmd_pos = args - .iter() - .position(|a| a == "start-with-config-file") - .unwrap(); - assert!(digest_pos < cmd_pos); + // then + assert!(rendered.contains(&format!("image: \"{digest}\""))); } #[test] @@ -628,42 +591,23 @@ mod tests { let digest = sample_digest(); // when - let args = docker_run_args( - Platform::NonTee, - Path::new(SAMPLE_CONFIG_PATH), - &flags, - &digest, - ); + let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); // then - let joined = args.join(" "); - assert!(joined.contains("-p 11780:11780")); + assert!(rendered.contains("11780:11780")); } #[test] - fn no_env_vars_forwarded_for_mpc_config() { + fn no_env_section_in_nontee_mode() { // given let flags = empty_docker_flags(); let digest = sample_digest(); // when - let args = docker_run_args( - Platform::NonTee, - Path::new(SAMPLE_CONFIG_PATH), - &flags, - &digest, - ); + let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); - // then - no MPC_* env vars should be present (only DSTACK_ENDPOINT in TEE mode) - let env_args: Vec<&String> = args - .windows(2) - .filter(|w| w[0] == "--env") - .map(|w| &w[1]) - .collect(); - assert!( - env_args.is_empty(), - "expected no --env args in non-TEE mode, got: {env_args:?}" - ); + // then + assert!(!rendered.contains("environment:")); } // --- select_image_hash --- diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index bfd2f29cb..09e196020 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -82,7 +82,7 @@ pub struct HostEntry { } #[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct PortMappings { +pub struct PortMappings { pub ports: Vec, } @@ -92,13 +92,10 @@ pub struct PortMapping { dst: NonZeroU16, } -impl PortMappings { - /// Returns `["-p", "src1:dst1", "-p", "src2:dst2", ...]`. - pub fn docker_args(&self) -> Vec { - self.ports - .iter() - .flat_map(|PortMapping { src, dst }| ["-p".into(), format!("{src}:{dst}")]) - .collect() +impl PortMapping { + /// Returns e.g. `"11780:11780"` for use in docker-compose port lists. + pub fn docker_compose_value(&self) -> String { + format!("{}:{}", self.src, self.dst) } } @@ -200,23 +197,21 @@ mod tests { assert_matches!(result, Err(_)); } - // --- docker_args output format --- + // --- docker_compose_value output format --- #[test] - fn port_mappings_docker_args_format() { + fn port_mapping_docker_compose_value() { // given - let mappings = PortMappings { - ports: vec![PortMapping { - src: NonZeroU16::new(11780).unwrap(), - dst: NonZeroU16::new(11780).unwrap(), - }], + let mapping = PortMapping { + src: NonZeroU16::new(11780).unwrap(), + dst: NonZeroU16::new(11780).unwrap(), }; // when - let args = mappings.docker_args(); + let value = mapping.docker_compose_value(); // then - assert_eq!(args, vec!["-p", "11780:11780"]); + assert_eq!(value, "11780:11780"); } // --- Config full deserialization --- From eca6da3b68b8e7bbd7285e6eb60d5b15e30f37ef Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 13 Mar 2026 22:01:05 +0100 Subject: [PATCH 072/176] create file and forward content in the file --- crates/tee-launcher/src/constants.rs | 2 +- crates/tee-launcher/src/main.rs | 13 +++++++++---- crates/tee-launcher/src/types.rs | 17 ++++++++--------- 3 files changed, 18 insertions(+), 14 deletions(-) diff --git a/crates/tee-launcher/src/constants.rs b/crates/tee-launcher/src/constants.rs index a158586d1..9cdf69794 100644 --- a/crates/tee-launcher/src/constants.rs +++ b/crates/tee-launcher/src/constants.rs @@ -4,4 +4,4 @@ pub(crate) const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; pub(crate) const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; /// Path inside the container where the MPC config file is bind-mounted. -pub(crate) const MPC_CONFIG_CONTAINER_PATH: &str = "/mnt/shared/mpc-config.json"; +pub(crate) const MPC_CONFIG_CONTAINER_PATH: &str = "/mnt/shared/mpc-config"; diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 3206b0022..e135f0ee2 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -13,6 +13,7 @@ use constants::*; use docker_types::*; use error::*; use reqwest::header::{ACCEPT, AUTHORIZATION, HeaderMap, HeaderValue}; +use tempfile::NamedTempFile; use types::*; use url::Url; @@ -120,10 +121,15 @@ async fn run() -> Result<(), LauncherError> { .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; } + let mut mpc_binary_config_file = NamedTempFile::new().expect("file creation works"); + mpc_binary_config_file + .write(dstack_config.mpc_config_content.as_bytes()) + .expect("writing to file works"); + launch_mpc_container( args.platform, &image_hash, - &dstack_config.mpc_config_file, + mpc_binary_config_file.path(), &dstack_config.docker_command_config, )?; @@ -371,7 +377,7 @@ fn render_compose_file( .port_mappings .ports .iter() - .map(|p| p.docker_compose_value()) + .map(PortMapping::docker_compose_value) .collect(); let ports_json = serde_json::to_string(&ports).expect("port list is serializable"); @@ -407,8 +413,7 @@ fn launch_mpc_container( ) -> Result<(), LauncherError> { tracing::info!("Launching MPC node with validated hash: {valid_hash}",); - let compose_file = - render_compose_file(platform, mpc_config_file, docker_flags, valid_hash)?; + let compose_file = render_compose_file(platform, mpc_config_file, docker_flags, valid_hash)?; let compose_path = compose_file.path().display().to_string(); // Remove any existing container from a previous run (by name, independent of compose file) diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 09e196020..82089acb7 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -1,6 +1,5 @@ use std::net::Ipv4Addr; use std::num::NonZeroU16; -use std::path::PathBuf; use launcher_interface::types::DockerSha256Digest; use url::Host; @@ -45,10 +44,10 @@ pub enum Platform { pub struct Config { pub launcher_config: LauncherConfig, pub docker_command_config: DockerLaunchFlags, - /// Path to the MPC node JSON config file on the host. - /// This file is mounted into the container and passed via - /// `start-with-config-file ` to the MPC binary. - pub mpc_config_file: PathBuf, + /// Inline MPC node config content (opaque to the launcher). + /// Written to a temporary file on disk, mounted into the container, + /// and passed via `start-with-config-file ` to the MPC binary. + pub mpc_config_content: String, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -88,8 +87,8 @@ pub struct PortMappings { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub struct PortMapping { - src: NonZeroU16, - dst: NonZeroU16, + pub(crate) src: NonZeroU16, + pub(crate) dst: NonZeroU16, } impl PortMapping { @@ -232,7 +231,7 @@ mod tests { "docker_command_config": { "port_mappings": {"ports": [{"src": 11780, "dst": 11780}]} }, - "mpc_config_file": "/tapp/mpc-config.json" + "mpc_config_file": "[some_config = true]" }); // when @@ -241,7 +240,7 @@ mod tests { // then assert_matches!(result, Ok(config) => { assert_eq!(config.launcher_config.image_name, "nearone/mpc-node"); - assert_eq!(config.mpc_config_file, PathBuf::from("/tapp/mpc-config.json")); + assert_eq!(config.mpc_config_content, "[some_config = true]"); }); } From 75343925d4f595bbc6c6e982634fc169771d32e0 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 13 Mar 2026 22:07:34 +0100 Subject: [PATCH 073/176] remove rewrite comment --- crates/tee-launcher/src/main.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index e135f0ee2..98182d8c8 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -1,5 +1,3 @@ -// A rewrite of launcher.py - use std::io::Write; use std::process::Command; use std::{collections::VecDeque, time::Duration}; From 515d4257f41863c9975ac9049f8115fcfe0a8726 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 13 Mar 2026 22:11:27 +0100 Subject: [PATCH 074/176] use write_all --- crates/tee-launcher/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 98182d8c8..cfdd3897e 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -121,7 +121,7 @@ async fn run() -> Result<(), LauncherError> { let mut mpc_binary_config_file = NamedTempFile::new().expect("file creation works"); mpc_binary_config_file - .write(dstack_config.mpc_config_content.as_bytes()) + .write_all(dstack_config.mpc_config_content.as_bytes()) .expect("writing to file works"); launch_mpc_container( From 12d7e478ad2938977ee83df07432ddcd9a64ff9f Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 13 Mar 2026 22:13:37 +0100 Subject: [PATCH 075/176] dont use tempfile for passed config, since it gets dropped --- crates/tee-launcher/src/main.rs | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index cfdd3897e..a3b94ecfd 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -11,7 +11,7 @@ use constants::*; use docker_types::*; use error::*; use reqwest::header::{ACCEPT, AUTHORIZATION, HeaderMap, HeaderValue}; -use tempfile::NamedTempFile; + use types::*; use url::Url; @@ -119,15 +119,17 @@ async fn run() -> Result<(), LauncherError> { .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; } - let mut mpc_binary_config_file = NamedTempFile::new().expect("file creation works"); - mpc_binary_config_file - .write_all(dstack_config.mpc_config_content.as_bytes()) - .expect("writing to file works"); + let mpc_binary_config_path = std::path::Path::new("/tmp/mpc-config"); + std::fs::write(mpc_binary_config_path, dstack_config.mpc_config_content.as_bytes()) + .map_err(|source| LauncherError::FileWrite { + path: mpc_binary_config_path.display().to_string(), + source, + })?; launch_mpc_container( args.platform, &image_hash, - mpc_binary_config_file.path(), + mpc_binary_config_path, &dstack_config.docker_command_config, )?; From 1e7ec24d0fffaf8be83ed13107cb37fde107a3d2 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 13 Mar 2026 22:37:37 +0100 Subject: [PATCH 076/176] change to toml and forward toml file --- Cargo.lock | 1 + crates/tee-launcher/Cargo.toml | 1 + crates/tee-launcher/src/error.rs | 6 ++ crates/tee-launcher/src/main.rs | 25 +++---- crates/tee-launcher/src/types.rs | 108 ++++++++++++++++++++----------- deployment/localnet/tee/sam.json | 36 ----------- deployment/localnet/tee/sam.toml | 56 ++++++++++++++++ deployment/testnet/sam.json | 37 ----------- deployment/testnet/sam.toml | 57 ++++++++++++++++ 9 files changed, 203 insertions(+), 124 deletions(-) delete mode 100644 deployment/localnet/tee/sam.json create mode 100644 deployment/localnet/tee/sam.toml delete mode 100644 deployment/testnet/sam.json create mode 100644 deployment/testnet/sam.toml diff --git a/Cargo.lock b/Cargo.lock index 4013b8aba..2cbcd7bee 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10661,6 +10661,7 @@ dependencies = [ "tempfile", "thiserror 2.0.18", "tokio", + "toml 1.0.6+spec-1.1.0", "tracing", "tracing-subscriber", "url", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index cf93a02eb..a06018d7a 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -20,6 +20,7 @@ launcher-interface = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +toml = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 9ce1c75a2..03295a6c8 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -48,6 +48,12 @@ pub enum LauncherError { source: serde_json::Error, }, + #[error("Failed to parse {path}: {source}")] + TomlParse { + path: String, + source: toml::de::Error, + }, + #[error("HTTP error: {0}")] Http(#[from] reqwest::Error), diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index a3b94ecfd..cb670a47d 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -53,17 +53,16 @@ async fn run() -> Result<(), LauncherError> { tracing::info!(platform = ?args.platform, "starting launcher"); - // Load dstack user config - let config_file = std::fs::OpenOptions::new() - .read(true) - .open(DSTACK_USER_CONFIG_FILE) - .map_err(|source| LauncherError::FileRead { + // Load dstack user config (TOML) + let config_contents = std::fs::read_to_string(DSTACK_USER_CONFIG_FILE).map_err(|source| { + LauncherError::FileRead { path: DSTACK_USER_CONFIG_FILE.to_string(), source, - })?; + } + })?; let dstack_config: Config = - serde_json::from_reader(config_file).map_err(|source| LauncherError::JsonParse { + toml::from_str(&config_contents).map_err(|source| LauncherError::TomlParse { path: DSTACK_USER_CONFIG_FILE.to_string(), source, })?; @@ -120,11 +119,14 @@ async fn run() -> Result<(), LauncherError> { } let mpc_binary_config_path = std::path::Path::new("/tmp/mpc-config"); - std::fs::write(mpc_binary_config_path, dstack_config.mpc_config_content.as_bytes()) - .map_err(|source| LauncherError::FileWrite { + let mpc_config_toml = toml::to_string(&dstack_config.mpc_config) + .expect("re-serializing a toml::Table always succeeds"); + std::fs::write(mpc_binary_config_path, mpc_config_toml.as_bytes()).map_err(|source| { + LauncherError::FileWrite { path: mpc_binary_config_path.display().to_string(), source, - })?; + } + })?; launch_mpc_container( args.platform, @@ -394,8 +396,7 @@ fn render_compose_file( tracing::info!(compose = %rendered, "rendered docker-compose file"); - let mut file = - tempfile::NamedTempFile::new().map_err(|source| LauncherError::TempFileCreate(source))?; + let mut file = tempfile::NamedTempFile::new().map_err(LauncherError::TempFileCreate)?; file.write_all(rendered.as_bytes()) .map_err(|source| LauncherError::FileWrite { path: file.path().display().to_string(), diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 82089acb7..c85480eb3 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -44,10 +44,11 @@ pub enum Platform { pub struct Config { pub launcher_config: LauncherConfig, pub docker_command_config: DockerLaunchFlags, - /// Inline MPC node config content (opaque to the launcher). - /// Written to a temporary file on disk, mounted into the container, - /// and passed via `start-with-config-file ` to the MPC binary. - pub mpc_config_content: String, + /// Opaque MPC node configuration table. + /// The launcher does not interpret these fields — they are re-serialized + /// to a TOML string, written to a file on disk, and mounted into the + /// container for the MPC binary to consume via `start-with-config-file`. + pub mpc_config: toml::Table, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -213,57 +214,86 @@ mod tests { assert_eq!(value, "11780:11780"); } - // --- Config full deserialization --- + // --- Config full deserialization (TOML) --- #[test] - fn config_deserializes_valid_json() { + fn config_deserializes_valid_toml() { // given - let json = serde_json::json!({ - "launcher_config": { - "image_tags": ["tag1"], - "image_name": "nearone/mpc-node", - "registry": "registry.hub.docker.com", - "rpc_request_timeout_secs": 10, - "rpc_request_interval_secs": 1, - "rpc_max_attempts": 20, - "mpc_hash_override": null - }, - "docker_command_config": { - "port_mappings": {"ports": [{"src": 11780, "dst": 11780}]} - }, - "mpc_config_file": "[some_config = true]" - }); + let toml_str = r#" +[launcher_config] +image_tags = ["tag1"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +[docker_command_config.port_mappings] +ports = [{ src = 11780, dst = 11780 }] + +[mpc_config] +home_dir = "/data" +some_opaque_field = true +"#; // when - let result = serde_json::from_value::(json); + let result = toml::from_str::(toml_str); // then assert_matches!(result, Ok(config) => { assert_eq!(config.launcher_config.image_name, "nearone/mpc-node"); - assert_eq!(config.mpc_config_content, "[some_config = true]"); + assert_eq!(config.mpc_config["home_dir"].as_str(), Some("/data")); + assert_eq!(config.mpc_config["some_opaque_field"].as_bool(), Some(true)); }); } + #[test] + fn config_mpc_config_round_trips_to_toml_string() { + // given + let toml_str = r#" +[launcher_config] +image_tags = ["tag1"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +[docker_command_config.port_mappings] +ports = [{ src = 11780, dst = 11780 }] + +[mpc_config] +home_dir = "/data" +arbitrary_key = "arbitrary_value" +"#; + let config: Config = toml::from_str(toml_str).unwrap(); + + // when — re-serialize the opaque table (what the launcher writes to disk) + let serialized = toml::to_string(&config.mpc_config).unwrap(); + + // then + assert!(serialized.contains("home_dir")); + assert!(serialized.contains("arbitrary_key")); + } + #[test] fn config_rejects_missing_required_field() { - // given - mpc_config_file is missing - let json = serde_json::json!({ - "launcher_config": { - "image_tags": ["tag1"], - "image_name": "nearone/mpc-node", - "registry": "registry.hub.docker.com", - "rpc_request_timeout_secs": 10, - "rpc_request_interval_secs": 1, - "rpc_max_attempts": 20, - "mpc_hash_override": null - }, - "docker_command_config": { - "port_mappings": {"ports": []} - } - }); + // given - mpc_config is missing + let toml_str = r#" +[launcher_config] +image_tags = ["tag1"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +[docker_command_config.port_mappings] +ports = [] +"#; // when - let result = serde_json::from_value::(json); + let result = toml::from_str::(toml_str); // then assert_matches!(result, Err(_)); diff --git a/deployment/localnet/tee/sam.json b/deployment/localnet/tee/sam.json deleted file mode 100644 index a2ab8f528..000000000 --- a/deployment/localnet/tee/sam.json +++ /dev/null @@ -1,36 +0,0 @@ -{ - "launcher_config": { - "image_tags": ["main-260e88b"], - "image_name": "nearone/mpc-node", - "registry": "registry.hub.docker.com", - "rpc_request_timeout_secs": 10, - "rpc_request_interval_secs": 1, - "rpc_max_attempts": 20, - "mpc_hash_override": null - }, - "docker_command_config": { - "extra_hosts": { - "hosts": [] - }, - "port_mappings": { - "ports": [ - { "src": 8080, "dst": 8080 }, - { "src": 24566, "dst": 24566 }, - { "src": 13002, "dst": 13002 } - ] - } - }, - "mpc_passthrough_env": { - "mpc_account_id": "sam.test.near", - "mpc_local_address": "127.0.0.1", - "mpc_secret_key_store": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "mpc_backup_encryption_key_hex": "0000000000000000000000000000000000000000000000000000000000000000", - "mpc_env": "Localnet", - "mpc_home_dir": "/data", - "mpc_contract_id": "mpc-contract.test.near", - "mpc_responder_id": "sam.test.near", - "near_boot_nodes": "ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566", - "rust_backtrace": "full", - "rust_log": "info" - } -} diff --git a/deployment/localnet/tee/sam.toml b/deployment/localnet/tee/sam.toml new file mode 100644 index 000000000..f9bc42440 --- /dev/null +++ b/deployment/localnet/tee/sam.toml @@ -0,0 +1,56 @@ +[launcher_config] +image_tags = ["main-260e88b"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +[docker_command_config.port_mappings] +ports = [ + { src = 8080, dst = 8080 }, + { src = 24566, dst = 24566 }, + { src = 13002, dst = 13002 }, +] + +[mpc_config] +home_dir = "/data" + +[mpc_config.secrets] +secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_config.tee.authority] +type = "local" + +[mpc_config.node] +my_near_account_id = "sam.test.near" +near_responder_account_id = "sam.test.near" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +cores = 4 + +[mpc_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "mpc-contract.test.near" +finality = "optimistic" + +[mpc_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_config.node.signature] +timeout_sec = 60 + +[mpc_config.node.ckd] +timeout_sec = 60 diff --git a/deployment/testnet/sam.json b/deployment/testnet/sam.json deleted file mode 100644 index 9b96a09a1..000000000 --- a/deployment/testnet/sam.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "launcher_config": { - "image_tags": ["barak-doc-update_localnet_guide-b12bc7d"], - "image_name": "nearone/mpc-node", - "registry": "registry.hub.docker.com", - "rpc_request_timeout_secs": 10, - "rpc_request_interval_secs": 1, - "rpc_max_attempts": 20, - "mpc_hash_override": null - }, - "docker_command_config": { - "extra_hosts": { - "hosts": [] - }, - "port_mappings": { - "ports": [ - { "src": 8080, "dst": 8080 }, - { "src": 24567, "dst": 24567 }, - { "src": 13002, "dst": 13002 }, - { "src": 80, "dst": 80 } - ] - } - }, - "mpc_passthrough_env": { - "mpc_account_id": "$SAM_ACCOUNT", - "mpc_local_address": "127.0.0.1", - "mpc_secret_key_store": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "mpc_backup_encryption_key_hex": "0000000000000000000000000000000000000000000000000000000000000000", - "mpc_env": "Testnet", - "mpc_home_dir": "/data", - "mpc_contract_id": "$MPC_CONTRACT_ACCOUNT", - "mpc_responder_id": "$SAM_ACCOUNT", - "near_boot_nodes": "$BOOTNODES", - "rust_backtrace": "full", - "rust_log": "info" - } -} diff --git a/deployment/testnet/sam.toml b/deployment/testnet/sam.toml new file mode 100644 index 000000000..5d9c9586c --- /dev/null +++ b/deployment/testnet/sam.toml @@ -0,0 +1,57 @@ +[launcher_config] +image_tags = ["barak-doc-update_localnet_guide-b12bc7d"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +[docker_command_config.port_mappings] +ports = [ + { src = 8080, dst = 8080 }, + { src = 24567, dst = 24567 }, + { src = 13002, dst = 13002 }, + { src = 80, dst = 80 }, +] + +[mpc_config] +home_dir = "/data" + +[mpc_config.secrets] +secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_config.tee.authority] +type = "local" + +[mpc_config.node] +my_near_account_id = "$SAM_ACCOUNT" +near_responder_account_id = "$SAM_ACCOUNT" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +cores = 4 + +[mpc_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "$MPC_CONTRACT_ACCOUNT" +finality = "optimistic" + +[mpc_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_config.node.signature] +timeout_sec = 60 + +[mpc_config.node.ckd] +timeout_sec = 60 From e7f69b7309513c96f39fb26a9f3dd9cacac2a1c9 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 13 Mar 2026 22:46:46 +0100 Subject: [PATCH 077/176] update launcher hash :) --- tee_launcher/launcher_docker_compose.yaml | 2 +- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tee_launcher/launcher_docker_compose.yaml b/tee_launcher/launcher_docker_compose.yaml index db24475aa..b59b44b32 100644 --- a/tee_launcher/launcher_docker_compose.yaml +++ b/tee_launcher/launcher_docker_compose.yaml @@ -2,7 +2,7 @@ version: '3.8' services: launcher: - image: nearone/mpc-launcher@sha256:84c7537a2f84d3477eac2e5ef3ba0765b5d688f86096947eea4744ce25b27054 + image: nearone/mpc-launcher@sha256:85a4fa6d1eec05e8f43dba17d3f4368f89719a2a06b9e2051d84813c3f651068 container_name: launcher diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 48b0bc4fc..e3fa51311 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:84c7537a2f84d3477eac2e5ef3ba0765b5d688f86096947eea4744ce25b27054 + image: nearone/mpc-launcher@sha256:85a4fa6d1eec05e8f43dba17d3f4368f89719a2a06b9e2051d84813c3f651068 container_name: "${LAUNCHER_IMAGE_NAME}" environment: From 70989692ee464540863d8b28be30de1fc583300b Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Fri, 13 Mar 2026 22:47:27 +0100 Subject: [PATCH 078/176] sort deps --- crates/launcher-interface/Cargo.toml | 2 +- crates/tee-launcher/Cargo.toml | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/launcher-interface/Cargo.toml b/crates/launcher-interface/Cargo.toml index 85572724e..db901977d 100644 --- a/crates/launcher-interface/Cargo.toml +++ b/crates/launcher-interface/Cargo.toml @@ -5,9 +5,9 @@ edition.workspace = true license.workspace = true [dependencies] -near-mpc-bounded-collections = { workspace = true } derive_more = { workspace = true } mpc-primitives = { workspace = true } +near-mpc-bounded-collections = { workspace = true } serde = { workspace = true } thiserror = { workspace = true } diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index a06018d7a..e0bc44094 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -13,17 +13,17 @@ integration-test = [] [dependencies] backon = { workspace = true } -near-mpc-bounded-collections = { workspace = true } clap = { workspace = true } dstack-sdk = { workspace = true } launcher-interface = { workspace = true } +near-mpc-bounded-collections = { workspace = true } reqwest = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } -toml = { workspace = true } tempfile = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true } +toml = { workspace = true } tracing = { workspace = true } tracing-subscriber = { workspace = true } url = { workspace = true, features = ["serde"] } From f5d9d5c11e163616df6a0e281ff2aac597292e1b Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 14:26:33 +0100 Subject: [PATCH 079/176] chore: add frodo.toml file --- deployment/testnet/frodo.toml | 57 +++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 deployment/testnet/frodo.toml diff --git a/deployment/testnet/frodo.toml b/deployment/testnet/frodo.toml new file mode 100644 index 000000000..e3a7599ce --- /dev/null +++ b/deployment/testnet/frodo.toml @@ -0,0 +1,57 @@ +[launcher_config] +image_tags = ["barak-doc-update_localnet_guide-b12bc7d"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +[docker_command_config.port_mappings] +ports = [ + { src = 8080, dst = 8080 }, + { src = 24567, dst = 24567 }, + { src = 13001, dst = 13001 }, + { src = 80, dst = 80 }, +] + +[mpc_config] +home_dir = "/data" + +[mpc_config.secrets] +secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_config.tee.authority] +type = "local" + +[mpc_config.node] +my_near_account_id = "$FRODO_ACCOUNT" +near_responder_account_id = "$FRODO_ACCOUNT" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +cores = 4 + +[mpc_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "$MPC_CONTRACT_ACCOUNT" +finality = "optimistic" + +[mpc_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_config.node.signature] +timeout_sec = 60 + +[mpc_config.node.ckd] +timeout_sec = 60 From 1ee4ba436a2d375547cd090dea62fca12b72c816 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 15:15:41 +0100 Subject: [PATCH 080/176] fix: install openssl --- deployment/Dockerfile-launcher | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Dockerfile-launcher b/deployment/Dockerfile-launcher index 30bb734e4..b39c42608 100644 --- a/deployment/Dockerfile-launcher +++ b/deployment/Dockerfile-launcher @@ -8,7 +8,7 @@ RUN \ --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ repro-sources-list.sh && \ apt-get update && \ - apt-get install -y --no-install-recommends docker.io && \ + apt-get install -y --no-install-recommends docker.io libssl3 ca-certificates && \ : "Clean up for improving reproducibility" && \ rm -rf /var/log/* /var/cache/ldconfig/aux-cache From 104eaf7a652a80a1bee0e1d4e6fca8cf8ac5e758 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 15:23:17 +0100 Subject: [PATCH 081/176] update compose file to use new image and binary with config file accepting --- tee_launcher/launcher_docker_compose_nontee.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index e3fa51311..fd7101b55 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,12 +1,12 @@ services: launcher: - image: nearone/mpc-launcher@sha256:85a4fa6d1eec05e8f43dba17d3f4368f89719a2a06b9e2051d84813c3f651068 + image: nearone/mpc-launcher@sha256:70e6d08328123b44406523af3147aebad37a9472839b8ebf0a303cecd7174fb0 container_name: "${LAUNCHER_IMAGE_NAME}" environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:9143bc98aaae3408c14cf4490d7b0e96a5a32d989ec865a0cf8dde391831a7a9 # 3.6.0 release + - DEFAULT_IMAGE_DIGEST=sha256:e7d1df7453b9bb9e89969f02a0ae59c3c2743cd895963e97a8e7666defbf4dab # latest main with config file support volumes: - /var/run/docker.sock:/var/run/docker.sock From fcc6072def6603c666ef19d6e0f3f88754468bd6 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 15:43:05 +0100 Subject: [PATCH 082/176] static musl build + reproducibie --- Cargo.toml | 2 +- deployment/Dockerfile-launcher | 4 ++-- deployment/build-images.sh | 6 +++--- repro-env.toml | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 343e0a959..fc1ae1b44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -166,7 +166,7 @@ reddsa = { git = "https://github.com/near/reddsa", rev = "c7cd92a55f7399d8d7f8c0 ] } regex = "1.12.3" # TODO(#2053): upgrading this leads to errors -reqwest = { version = "0.12.28", features = ["multipart", "json"] } +reqwest = { version = "0.12.28", default-features = false, features = ["charset", "http2", "rustls-tls", "multipart", "json"] } rmp-serde = "1.3.1" rstest = { version = "0.26.1" } rustls = { version = "0.23.37", default-features = false, features = ["std"] } diff --git a/deployment/Dockerfile-launcher b/deployment/Dockerfile-launcher index b39c42608..fa798df37 100644 --- a/deployment/Dockerfile-launcher +++ b/deployment/Dockerfile-launcher @@ -8,10 +8,10 @@ RUN \ --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ repro-sources-list.sh && \ apt-get update && \ - apt-get install -y --no-install-recommends docker.io libssl3 ca-certificates && \ + apt-get install -y --no-install-recommends docker.io && \ : "Clean up for improving reproducibility" && \ rm -rf /var/log/* /var/cache/ldconfig/aux-cache -COPY --chmod=0755 target/release/tee-launcher /usr/local/bin/tee-launcher +COPY --chmod=0755 target/x86_64-unknown-linux-musl/reproducible/tee-launcher /usr/local/bin/tee-launcher RUN mkdir -p /app-data && mkdir -p /mnt/shared CMD ["tee-launcher"] diff --git a/deployment/build-images.sh b/deployment/build-images.sh index cc4bfe02e..2e8b7bd76 100755 --- a/deployment/build-images.sh +++ b/deployment/build-images.sh @@ -61,7 +61,7 @@ require_cmds() { require_cmds docker jq git find touch -if $USE_NODE; then +if $USE_NODE || $USE_LAUNCHER; then require_cmds repro-env podman fi @@ -117,8 +117,8 @@ get_image_hash() { } if $USE_LAUNCHER; then - cargo build -p tee-launcher --release --locked - launcher_binary_hash=$(sha256sum target/release/tee-launcher | cut -d' ' -f1) + SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH repro-env build --env SOURCE_DATE_EPOCH -- sh -c "rustup target add x86_64-unknown-linux-musl && cargo build -p tee-launcher --profile reproducible --locked --target x86_64-unknown-linux-musl" + launcher_binary_hash=$(sha256sum target/x86_64-unknown-linux-musl/reproducible/tee-launcher | cut -d' ' -f1) build_reproducible_image $LAUNCHER_IMAGE_NAME $DOCKERFILE_LAUNCHER launcher_image_hash=$(get_image_hash $LAUNCHER_IMAGE_NAME) fi diff --git a/repro-env.toml b/repro-env.toml index ea1899e49..621894d05 100644 --- a/repro-env.toml +++ b/repro-env.toml @@ -3,4 +3,4 @@ image = "docker.io/rust:1.86-slim-bookworm" [packages] system = "debian" -dependencies = ["make", "git", "libssl-dev", "pkg-config", "clang"] \ No newline at end of file +dependencies = ["make", "git", "libssl-dev", "pkg-config", "clang", "musl-tools"] \ No newline at end of file From 6943356419a6948e2a90fe4b038d2aa3b672b4a1 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 15:54:39 +0100 Subject: [PATCH 083/176] use same pattern for building launcher as node --- .github/workflows/ci.yml | 11 ++++++++++- .github/workflows/docker_build_launcher.yml | 11 ++++++++++- deployment/build-images.sh | 2 +- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7f30b0802..fc26450fd 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,10 +75,19 @@ jobs: with: persist-credentials: false + - name: Allow unprivileged user namespaces (needed by repro-env/podman on Ubuntu 24.04) + run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - name: Install build dependencies run: | sudo apt-get update - sudo apt-get install -y skopeo liblzma-dev + sudo apt-get install -y skopeo liblzma-dev podman + + - name: Install repro-env + run: | + wget 'https://github.com/kpcyrd/repro-env/releases/download/v0.4.3/repro-env' + echo '2a00b21ac5e990e0c6a0ccbf3b91e34a073660d1f4553b5f3cda2b09cc4d4d8a repro-env' | sha256sum -c - + sudo install -m755 repro-env -t /usr/bin - name: Cache Rust dependencies uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 diff --git a/.github/workflows/docker_build_launcher.yml b/.github/workflows/docker_build_launcher.yml index b0d69d76f..3b8e80da8 100644 --- a/.github/workflows/docker_build_launcher.yml +++ b/.github/workflows/docker_build_launcher.yml @@ -23,10 +23,19 @@ jobs: with: persist-credentials: false + - name: Allow unprivileged user namespaces (needed by repro-env/podman on Ubuntu 24.04) + run: sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0 + - name: Install build dependencies run: | sudo apt-get update - sudo apt-get install -y skopeo liblzma-dev + sudo apt-get install -y skopeo liblzma-dev podman + + - name: Install repro-env + run: | + wget 'https://github.com/kpcyrd/repro-env/releases/download/v0.4.3/repro-env' + echo '2a00b21ac5e990e0c6a0ccbf3b91e34a073660d1f4553b5f3cda2b09cc4d4d8a repro-env' | sha256sum -c - + sudo install -m755 repro-env -t /usr/bin - name: Cache Rust dependencies uses: Swatinem/rust-cache@f13886b937689c021905a6b90929199931d60db1 # v2.8.1 diff --git a/deployment/build-images.sh b/deployment/build-images.sh index 2e8b7bd76..344f6c4d4 100755 --- a/deployment/build-images.sh +++ b/deployment/build-images.sh @@ -2,7 +2,7 @@ # Script to reproducibly the docker images for the node and launcher # # Requirements: docker, docker-buildx, jq, git, find, touch -# Extra requirements if using --node: repro-env, podman +# Extra requirements if using --node or --launcher: repro-env, podman # # Usage: # ./deployment/build-images.sh [--node] [--launcher] [--push] From f120573faa8573db0c0cf4cfae7e7d4ace1ec1ab Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 15:58:58 +0100 Subject: [PATCH 084/176] remove musl --- deployment/Dockerfile-launcher | 2 +- deployment/build-images.sh | 4 ++-- repro-env.toml | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/Dockerfile-launcher b/deployment/Dockerfile-launcher index fa798df37..357adf674 100644 --- a/deployment/Dockerfile-launcher +++ b/deployment/Dockerfile-launcher @@ -12,6 +12,6 @@ RUN \ : "Clean up for improving reproducibility" && \ rm -rf /var/log/* /var/cache/ldconfig/aux-cache -COPY --chmod=0755 target/x86_64-unknown-linux-musl/reproducible/tee-launcher /usr/local/bin/tee-launcher +COPY --chmod=0755 target/reproducible/tee-launcher /usr/local/bin/tee-launcher RUN mkdir -p /app-data && mkdir -p /mnt/shared CMD ["tee-launcher"] diff --git a/deployment/build-images.sh b/deployment/build-images.sh index 344f6c4d4..7e80e3407 100755 --- a/deployment/build-images.sh +++ b/deployment/build-images.sh @@ -117,8 +117,8 @@ get_image_hash() { } if $USE_LAUNCHER; then - SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH repro-env build --env SOURCE_DATE_EPOCH -- sh -c "rustup target add x86_64-unknown-linux-musl && cargo build -p tee-launcher --profile reproducible --locked --target x86_64-unknown-linux-musl" - launcher_binary_hash=$(sha256sum target/x86_64-unknown-linux-musl/reproducible/tee-launcher | cut -d' ' -f1) + SOURCE_DATE_EPOCH=$SOURCE_DATE_EPOCH repro-env build --env SOURCE_DATE_EPOCH -- cargo build -p tee-launcher --profile reproducible --locked + launcher_binary_hash=$(sha256sum target/reproducible/tee-launcher | cut -d' ' -f1) build_reproducible_image $LAUNCHER_IMAGE_NAME $DOCKERFILE_LAUNCHER launcher_image_hash=$(get_image_hash $LAUNCHER_IMAGE_NAME) fi diff --git a/repro-env.toml b/repro-env.toml index 621894d05..ea1899e49 100644 --- a/repro-env.toml +++ b/repro-env.toml @@ -3,4 +3,4 @@ image = "docker.io/rust:1.86-slim-bookworm" [packages] system = "debian" -dependencies = ["make", "git", "libssl-dev", "pkg-config", "clang", "musl-tools"] \ No newline at end of file +dependencies = ["make", "git", "libssl-dev", "pkg-config", "clang"] \ No newline at end of file From 13f39744b9b11a226ce3d8e45269645cc10b0286 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 16:04:12 +0100 Subject: [PATCH 085/176] update image for ci --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index fd7101b55..885d3a3f5 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:70e6d08328123b44406523af3147aebad37a9472839b8ebf0a303cecd7174fb0 + image: nearone/mpc-launcher@sha256:a4d870b0eee3a55b3c5e94b3707e85fb50e5de64eb6be65ef2d27426616c95f9 container_name: "${LAUNCHER_IMAGE_NAME}" environment: From c2f713385b4f55f8a99a7c36f6fe41eebf9495f0 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 16:10:04 +0100 Subject: [PATCH 086/176] create toml file instead of env --- .../launcher_docker_compose_nontee.yaml | 2 +- tee_launcher/user-config.conf | 18 ------ tee_launcher/user-config.toml | 56 +++++++++++++++++++ 3 files changed, 57 insertions(+), 19 deletions(-) delete mode 100644 tee_launcher/user-config.conf create mode 100644 tee_launcher/user-config.toml diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 885d3a3f5..91ab2f556 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -10,7 +10,7 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock - - ./user-config.conf:/tapp/user_config:ro + - ./user-config.toml:/tapp/user_config:ro - shared-volume:/mnt/shared - mpc-data:/data diff --git a/tee_launcher/user-config.conf b/tee_launcher/user-config.conf deleted file mode 100644 index 0c8980d62..000000000 --- a/tee_launcher/user-config.conf +++ /dev/null @@ -1,18 +0,0 @@ -# Optional override parameters to find fetch the MPC docker image. -MPC_IMAGE_NAME=nearone/mpc-node -MPC_IMAGE_TAGS=3.6.0 -MPC_REGISTRY=registry.hub.docker.com - -MPC_ACCOUNT_ID=mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet -MPC_LOCAL_ADDRESS=127.0.0.1 -MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -MPC_CONTRACT_ID=mpc-contract-barak-launch1-4c5e2fe1fb42.5035bf56abb0.testnet -MPC_ENV=testnet -MPC_HOME_DIR=/data -RUST_BACKTRACE=full -RUST_LOG=mpc=debug,info - -NEAR_BOOT_NODES=ed25519:9qyu1RaJ5shX6UEb7UooPQYVXCC1tNHCiDPPxJ8Pv1UJ@116.202.220.238:34567,ed25519:8mzYnfuT5zQYqV99CfYAX6XoRmNxVJ1nAZHXXW4GrFD@34.221.144.70:24567,ed25519:B87Qq34LbWadFx2dq5bwUEtB5KBgr8ZhsoEpAiSP2qVX@142.132.203.80:24567,ed25519:EufXMhFVixgFpg2bBaHGL4Zrks1DDrhAZTQYwbjRTAUX@65.109.25.109:24567,ed25519:HJJde5skATXLA4wGk8P9awvfzaW47tCU2EsRXnMoFRA9@129.150.39.19:24567,ed25519:BavpjuYrnXRFQVWjLdx9vx9vAvanit9NhhcPeM6gjAkE@95.217.198.233:24567,ed25519:81zk9MvvoxB1AzTW721o9m2NeYx3pDFDZyRJUQej65uc@195.14.6.172:24567,ed25519:E4gQXBovauvqxx85TdemezhkDDsAsqEL7ZJ4cp5Cdhsb@129.80.119.109:24567,ed25519:6cWtXFAzqpZ8D7EpLGYBmkw95oKYkzN8i99UcRgsyRMy@164.132.247.155:24567,ed25519:CLnWy9xv2GUqfgepzLwpv4bozj3H3kgzjbVREyS6wcqq@47.242.112.172:24567,ed25519:2NmT9Wy9HGBmH8sTWSq2QfaMk4R8ZHBEhk8ZH4g4f1Qk@65.109.88.175:24567,ed25519:9dhPYd1ArZ6mTMP7nnRzm8JBPwKCaBxiYontS5KfXz5h@34.239.1.54:24567,ed25519:8iiQH4vtqsqWgsm4ypCJQQwqJR3AGp9o7F69YRaCHKxA@141.95.204.11:24567,ed25519:4L97JnFFFVbfE8M3tY9bRtgV5376y5dFH8cSaoBDRWnK@5.199.170.103:24567,ed25519:DGJ91V2wJ8NFpkqZvphtSeM4CBeiLsrHGdinTugiRoFF@52.35.74.212:24567,ed25519:B9LSvCTimoEUtuUvpfu1S54an54uTetVabmkT5dELUCN@91.134.22.129:24567,ed25519:cRGmtzkkSZT6wXNjbthSXMD6dHrEgSeDtiEJAcnLLxH@15.204.213.166:24567 -# needed: Port forwarding - telemetry. -PORTS=8080:8080,3030:3030,80:80,24567:24567 - diff --git a/tee_launcher/user-config.toml b/tee_launcher/user-config.toml new file mode 100644 index 000000000..6ba4e5aa3 --- /dev/null +++ b/tee_launcher/user-config.toml @@ -0,0 +1,56 @@ +[launcher_config] +image_tags = ["3.6.0"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +[docker_command_config.port_mappings] +ports = [ + { src = 8080, dst = 8080 }, + { src = 3030, dst = 3030 }, + { src = 80, dst = 80 }, + { src = 24567, dst = 24567 }, +] + +[mpc_config] +home_dir = "/data" + +[mpc_config.secrets] +secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" + +[mpc_config.tee.authority] +type = "local" + +[mpc_config.node] +my_near_account_id = "mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet" +near_responder_account_id = "mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +cores = 4 + +[mpc_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "mpc-contract-barak-launch1-4c5e2fe1fb42.5035bf56abb0.testnet" +finality = "optimistic" + +[mpc_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_config.node.signature] +timeout_sec = 60 + +[mpc_config.node.ckd] +timeout_sec = 60 From 498caf6faa449bafbff7a7c6d78c49fb989ccb19 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 16:15:17 +0100 Subject: [PATCH 087/176] change image tags --- tee_launcher/user-config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/user-config.toml b/tee_launcher/user-config.toml index 6ba4e5aa3..c88191ff3 100644 --- a/tee_launcher/user-config.toml +++ b/tee_launcher/user-config.toml @@ -1,5 +1,5 @@ [launcher_config] -image_tags = ["3.6.0"] +image_tags = ["main-6ee90e0"] image_name = "nearone/mpc-node" registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 From 919919548391c4ad34cf3429a7e33ea1987e6a0f Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 16:16:58 +0100 Subject: [PATCH 088/176] update compose file --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 91ab2f556..ae60d296f 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:e7d1df7453b9bb9e89969f02a0ae59c3c2743cd895963e97a8e7666defbf4dab # latest main with config file support + - DEFAULT_IMAGE_DIGEST=sha256:689ad0f37a5a9db75eeb93cb15ad4bb507883412f891a4fd19448ada575c8237 # nearone/mpc-node:main-6ee90e0 volumes: - /var/run/docker.sock:/var/run/docker.sock From b2b2cd7f4753bb3f5203fc2b839856a6cf3951f0 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 16:21:10 +0100 Subject: [PATCH 089/176] revert image digest change --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index ae60d296f..91ab2f556 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:689ad0f37a5a9db75eeb93cb15ad4bb507883412f891a4fd19448ada575c8237 # nearone/mpc-node:main-6ee90e0 + - DEFAULT_IMAGE_DIGEST=sha256:e7d1df7453b9bb9e89969f02a0ae59c3c2743cd895963e97a8e7666defbf4dab # latest main with config file support volumes: - /var/run/docker.sock:/var/run/docker.sock From 5113f37680f3eed114fb1dd2ac50193f615fa5db Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 16:23:46 +0100 Subject: [PATCH 090/176] try new image again --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 91ab2f556..ae60d296f 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:e7d1df7453b9bb9e89969f02a0ae59c3c2743cd895963e97a8e7666defbf4dab # latest main with config file support + - DEFAULT_IMAGE_DIGEST=sha256:689ad0f37a5a9db75eeb93cb15ad4bb507883412f891a4fd19448ada575c8237 # nearone/mpc-node:main-6ee90e0 volumes: - /var/run/docker.sock:/var/run/docker.sock From bcca994c18b6c2ae2f0b88c9adba36102446bc02 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 16:26:04 +0100 Subject: [PATCH 091/176] use docker compose plugin --- deployment/Dockerfile-launcher | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Dockerfile-launcher b/deployment/Dockerfile-launcher index 357adf674..77debe54d 100644 --- a/deployment/Dockerfile-launcher +++ b/deployment/Dockerfile-launcher @@ -8,7 +8,7 @@ RUN \ --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ repro-sources-list.sh && \ apt-get update && \ - apt-get install -y --no-install-recommends docker.io && \ + apt-get install -y --no-install-recommends docker.io docker-compose-plugin && \ : "Clean up for improving reproducibility" && \ rm -rf /var/log/* /var/cache/ldconfig/aux-cache From 7c0051075e24a4a3922b3d2065cf6a165816176d Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 16:32:06 +0100 Subject: [PATCH 092/176] . --- deployment/Dockerfile-launcher | 2 +- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/Dockerfile-launcher b/deployment/Dockerfile-launcher index 77debe54d..357adf674 100644 --- a/deployment/Dockerfile-launcher +++ b/deployment/Dockerfile-launcher @@ -8,7 +8,7 @@ RUN \ --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ repro-sources-list.sh && \ apt-get update && \ - apt-get install -y --no-install-recommends docker.io docker-compose-plugin && \ + apt-get install -y --no-install-recommends docker.io && \ : "Clean up for improving reproducibility" && \ rm -rf /var/log/* /var/cache/ldconfig/aux-cache diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index ae60d296f..91ab2f556 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:689ad0f37a5a9db75eeb93cb15ad4bb507883412f891a4fd19448ada575c8237 # nearone/mpc-node:main-6ee90e0 + - DEFAULT_IMAGE_DIGEST=sha256:e7d1df7453b9bb9e89969f02a0ae59c3c2743cd895963e97a8e7666defbf4dab # latest main with config file support volumes: - /var/run/docker.sock:/var/run/docker.sock From ce6289eb5c4a8cf15eedfb861e0840b0fa5e8ad4 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 16:56:42 +0100 Subject: [PATCH 093/176] include image name in template --- .../docker-compose.tee.template.yml | 2 +- .../tee-launcher/docker-compose.template.yml | 2 +- crates/tee-launcher/src/main.rs | 20 ++++++++++++++++--- 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/crates/tee-launcher/docker-compose.tee.template.yml b/crates/tee-launcher/docker-compose.tee.template.yml index 6312625e1..5bea15363 100644 --- a/crates/tee-launcher/docker-compose.tee.template.yml +++ b/crates/tee-launcher/docker-compose.tee.template.yml @@ -1,6 +1,6 @@ services: mpc-node: - image: "{{IMAGE}}" + image: "{{IMAGE_NAME}}@{{IMAGE}}" container_name: "{{CONTAINER_NAME}}" security_opt: - no-new-privileges:true diff --git a/crates/tee-launcher/docker-compose.template.yml b/crates/tee-launcher/docker-compose.template.yml index 29b44651e..7bc1e7593 100644 --- a/crates/tee-launcher/docker-compose.template.yml +++ b/crates/tee-launcher/docker-compose.template.yml @@ -1,6 +1,6 @@ services: mpc-node: - image: "{{IMAGE}}" + image: "{{IMAGE_NAME}}@{{IMAGE}}" container_name: "{{CONTAINER_NAME}}" security_opt: - no-new-privileges:true diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index cb670a47d..abfddbf39 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -131,6 +131,7 @@ async fn run() -> Result<(), LauncherError> { launch_mpc_container( args.platform, &image_hash, + &dstack_config.launcher_config.image_name, mpc_binary_config_path, &dstack_config.docker_command_config, )?; @@ -368,6 +369,7 @@ fn render_compose_file( platform: Platform, mpc_config_file: &std::path::Path, docker_flags: &DockerLaunchFlags, + image_name: &str, image_digest: &DockerSha256Digest, ) -> Result { let template = match platform { @@ -384,6 +386,7 @@ fn render_compose_file( let ports_json = serde_json::to_string(&ports).expect("port list is serializable"); let rendered = template + .replace("{{IMAGE_NAME}}", image_name) .replace("{{IMAGE}}", &image_digest.to_string()) .replace("{{CONTAINER_NAME}}", MPC_CONTAINER_NAME) .replace( @@ -409,12 +412,14 @@ fn render_compose_file( fn launch_mpc_container( platform: Platform, valid_hash: &DockerSha256Digest, + image_name: &str, mpc_config_file: &std::path::Path, docker_flags: &DockerLaunchFlags, ) -> Result<(), LauncherError> { tracing::info!("Launching MPC node with validated hash: {valid_hash}",); - let compose_file = render_compose_file(platform, mpc_config_file, docker_flags, valid_hash)?; + let compose_file = + render_compose_file(platform, mpc_config_file, docker_flags, image_name, valid_hash)?; let compose_path = compose_file.path().display().to_string(); // Remove any existing container from a previous run (by name, independent of compose file) @@ -460,13 +465,22 @@ mod tests { const SAMPLE_CONFIG_PATH: &str = "/tapp/mpc-config.json"; + const SAMPLE_IMAGE_NAME: &str = "nearone/mpc-node"; + fn render( platform: Platform, config_path: &str, flags: &DockerLaunchFlags, digest: &DockerSha256Digest, ) -> String { - let file = render_compose_file(platform, Path::new(config_path), flags, digest).unwrap(); + let file = render_compose_file( + platform, + Path::new(config_path), + flags, + SAMPLE_IMAGE_NAME, + digest, + ) + .unwrap(); std::fs::read_to_string(file.path()).unwrap() } @@ -587,7 +601,7 @@ mod tests { let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); // then - assert!(rendered.contains(&format!("image: \"{digest}\""))); + assert!(rendered.contains(&format!("image: \"{SAMPLE_IMAGE_NAME}@{digest}\""))); } #[test] From 4f3f327b52d337c6be4ad8637cddf435bd239121 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:01:50 +0100 Subject: [PATCH 094/176] use new launcher --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 91ab2f556..1e282476b 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:a4d870b0eee3a55b3c5e94b3707e85fb50e5de64eb6be65ef2d27426616c95f9 + image: nearone/mpc-launcher@sha256:7055bdcaae4004819973381303a48ac75d95c2d5b72ed5c97fbdad0c652cb6e2 container_name: "${LAUNCHER_IMAGE_NAME}" environment: From 717e3fb314a4e341329b71685ac2db840dc4dee4 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:03:51 +0100 Subject: [PATCH 095/176] try claude's suggestion of image digest --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 1e282476b..8ad461c6e 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:e7d1df7453b9bb9e89969f02a0ae59c3c2743cd895963e97a8e7666defbf4dab # latest main with config file support + - DEFAULT_IMAGE_DIGEST=sha256:689ad0f37a5a9db75eeb93cb15ad4bb507883412f891a4fd19448ada575c8237 # config digest for nearone/mpc-node:main-6ee90e0 volumes: - /var/run/docker.sock:/var/run/docker.sock From a7c7fede43f9370126870775913b84e9ee80e113 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:23:26 +0100 Subject: [PATCH 096/176] use manifest digest --- crates/tee-launcher/src/main.rs | 32 +++++++++++++++++++------------- 1 file changed, 19 insertions(+), 13 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index abfddbf39..67eb7ab2a 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -100,7 +100,8 @@ async fn run() -> Result<(), LauncherError> { dstack_config.launcher_config.mpc_hash_override.as_ref(), )?; - let () = validate_image_hash(&dstack_config.launcher_config, image_hash.clone()).await?; + let manifest_digest = + validate_image_hash(&dstack_config.launcher_config, image_hash.clone()).await?; let should_extend_rtmr_3 = args.platform == Platform::Tee; @@ -130,7 +131,7 @@ async fn run() -> Result<(), LauncherError> { launch_mpc_container( args.platform, - &image_hash, + &manifest_digest, &dstack_config.launcher_config.image_name, mpc_binary_config_path, &dstack_config.docker_command_config, @@ -307,7 +308,7 @@ async fn get_manifest_digest( async fn validate_image_hash( launcher_config: &LauncherConfig, image_hash: DockerSha256Digest, -) -> Result<(), ImageDigestValidationFailed> { +) -> Result { let manifest_digest = get_manifest_digest(launcher_config, &image_hash) .await .map_err(|e| ImageDigestValidationFailed::ManifestDigestLookupFailed(e.to_string()))?; @@ -362,7 +363,7 @@ async fn validate_image_hash( ); } - Ok(()) + Ok(pulled_digest) } fn render_compose_file( @@ -370,7 +371,7 @@ fn render_compose_file( mpc_config_file: &std::path::Path, docker_flags: &DockerLaunchFlags, image_name: &str, - image_digest: &DockerSha256Digest, + manifest_digest: &DockerSha256Digest, ) -> Result { let template = match platform { Platform::Tee => COMPOSE_TEE_TEMPLATE, @@ -387,7 +388,7 @@ fn render_compose_file( let rendered = template .replace("{{IMAGE_NAME}}", image_name) - .replace("{{IMAGE}}", &image_digest.to_string()) + .replace("{{IMAGE}}", &manifest_digest.to_string()) .replace("{{CONTAINER_NAME}}", MPC_CONTAINER_NAME) .replace( "{{MPC_CONFIG_HOST_PATH}}", @@ -411,15 +412,20 @@ fn render_compose_file( fn launch_mpc_container( platform: Platform, - valid_hash: &DockerSha256Digest, + manifest_digest: &DockerSha256Digest, image_name: &str, mpc_config_file: &std::path::Path, docker_flags: &DockerLaunchFlags, ) -> Result<(), LauncherError> { - tracing::info!("Launching MPC node with validated hash: {valid_hash}",); - - let compose_file = - render_compose_file(platform, mpc_config_file, docker_flags, image_name, valid_hash)?; + tracing::info!(?manifest_digest, "launching MPC node"); + + let compose_file = render_compose_file( + platform, + mpc_config_file, + docker_flags, + image_name, + manifest_digest, + )?; let compose_path = compose_file.path().display().to_string(); // Remove any existing container from a previous run (by name, independent of compose file) @@ -431,7 +437,7 @@ fn launch_mpc_container( .args(["compose", "-f", &compose_path, "up", "-d"]) .output() .map_err(|inner| LauncherError::DockerRunFailed { - image_hash: valid_hash.clone(), + image_hash: manifest_digest.clone(), inner, })?; @@ -440,7 +446,7 @@ fn launch_mpc_container( let stdout = String::from_utf8_lossy(&run_output.stdout); tracing::error!(%stderr, %stdout, "docker compose up failed"); return Err(LauncherError::DockerRunFailedExitStatus { - image_hash: valid_hash.clone(), + image_hash: manifest_digest.clone(), output: stderr.into_owned(), }); } From 4b1b97d1274a0f50b6512487455510977eaf1822 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:30:22 +0100 Subject: [PATCH 097/176] return manifest_digest --- crates/tee-launcher/src/main.rs | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 67eb7ab2a..f937b1bb0 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -176,7 +176,7 @@ fn select_image_hash( async fn get_manifest_digest( config: &LauncherConfig, expected_image_digest: &DockerSha256Digest, -) -> Result { +) -> Result { let mut tags: VecDeque = config.image_tags.iter().cloned().collect(); // We need an authorization token to fetch manifests. @@ -295,7 +295,12 @@ async fn get_manifest_digest( continue; }; - return Ok(content_digest); + return content_digest.parse().map_err(|_| { + LauncherError::RegistryResponseParse(format!( + "failed to parse manifest digest: {}", + content_digest + )) + }); } } } @@ -363,7 +368,7 @@ async fn validate_image_hash( ); } - Ok(pulled_digest) + Ok(manifest_digest) } fn render_compose_file( From 4a46c27a86f2f856317ed927b4342d905856a8f4 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:34:22 +0100 Subject: [PATCH 098/176] try new launcher --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 8ad461c6e..21f54d73f 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:7055bdcaae4004819973381303a48ac75d95c2d5b72ed5c97fbdad0c652cb6e2 + image: nearone/mpc-launcher@sha256:81aa4df17574e657bcc1e84af3c78d3d4a6d9bb89d546d067ed4fd457c7f90fc container_name: "${LAUNCHER_IMAGE_NAME}" environment: From 6579da8efd5022e1f8e9c62c8ed52c8beedabec7 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:38:08 +0100 Subject: [PATCH 099/176] . --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 21f54d73f..e59e0ecf9 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:689ad0f37a5a9db75eeb93cb15ad4bb507883412f891a4fd19448ada575c8237 # config digest for nearone/mpc-node:main-6ee90e0 + - DEFAULT_IMAGE_DIGEST=sha256:e7d1df7453b9bb9e89969f02a0ae59c3c2743cd895963e97a8e7666defbf4dab # latest main with config file support volumes: - /var/run/docker.sock:/var/run/docker.sock From 6817d8101198be3886b12a0323a4e69a3f9985b0 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:43:07 +0100 Subject: [PATCH 100/176] revert to manifest digest --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index e59e0ecf9..21f54d73f 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:e7d1df7453b9bb9e89969f02a0ae59c3c2743cd895963e97a8e7666defbf4dab # latest main with config file support + - DEFAULT_IMAGE_DIGEST=sha256:689ad0f37a5a9db75eeb93cb15ad4bb507883412f891a4fd19448ada575c8237 # config digest for nearone/mpc-node:main-6ee90e0 volumes: - /var/run/docker.sock:/var/run/docker.sock From e7d97a17f808cf663036f486e1f85ee6b9a86514 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:44:16 +0100 Subject: [PATCH 101/176] add more logs for docker run --- crates/tee-launcher/src/main.rs | 31 ++++++++++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index f937b1bb0..eff67809f 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -274,7 +274,16 @@ async fn get_manifest_digest( match manifest { ManifestResponse::ImageIndex { manifests } => { - // Multi-platform manifest; scan for amd64/linux + let platform_digests: Vec<_> = manifests + .iter() + .filter(|m| m.platform.architecture == AMD64 && m.platform.os == LINUX) + .map(|m| m.digest.as_str()) + .collect(); + tracing::info!( + ?tag, + ?platform_digests, + "received multi-platform image index, queuing amd64/linux manifests" + ); manifests .into_iter() .filter(|manifest| { @@ -284,6 +293,12 @@ async fn get_manifest_digest( } ManifestResponse::DockerV2 { config } | ManifestResponse::OciManifest { config } => { if config.digest != *expected_image_digest { + tracing::warn!( + ?tag, + actual_config_digest = %config.digest, + expected_config_digest = %expected_image_digest, + "config digest mismatch, skipping tag" + ); continue; } @@ -292,9 +307,18 @@ async fn get_manifest_digest( .and_then(|v| v.to_str().ok()) .map(|s| s.to_string()) else { + tracing::warn!( + ?tag, + "manifest matched but Docker-Content-Digest header missing, skipping" + ); continue; }; + tracing::info!( + ?tag, + %content_digest, + "config digest matched, resolved manifest digest" + ); return content_digest.parse().map_err(|_| { LauncherError::RegistryResponseParse(format!( "failed to parse manifest digest: {}", @@ -305,6 +329,11 @@ async fn get_manifest_digest( } } + tracing::error!( + ?expected_image_digest, + tags = ?config.image_tags, + "no tag produced a manifest with matching config digest" + ); Err(LauncherError::ImageHashNotFoundAmongTags) } From 2c906b90582a40f3415f9fb8265c3ac9df55b398 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:48:13 +0100 Subject: [PATCH 102/176] run with logs --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 21f54d73f..03478aee3 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:81aa4df17574e657bcc1e84af3c78d3d4a6d9bb89d546d067ed4fd457c7f90fc + image: nearone/mpc-launcher@sha256:83319115964430be6759328ece9eda424b306798c03ab852aaad107c7deffad8 container_name: "${LAUNCHER_IMAGE_NAME}" environment: From b4f3b65e684e59a8d102edba88b5cb52793919a3 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:52:20 +0100 Subject: [PATCH 103/176] remove tail for debugiggng --- scripts/check-mpc-node-docker-starts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-mpc-node-docker-starts.sh b/scripts/check-mpc-node-docker-starts.sh index 79b24d386..5f59d8184 100755 --- a/scripts/check-mpc-node-docker-starts.sh +++ b/scripts/check-mpc-node-docker-starts.sh @@ -25,7 +25,7 @@ if $USE_LAUNCHER; then export LAUNCHER_IMAGE_NAME docker compose -f launcher_docker_compose_nontee.yaml up -d sleep 10 - launcher_logs=$(docker logs --tail 10 "$LAUNCHER_IMAGE_NAME" 2>&1) + launcher_logs=$(docker logs "$LAUNCHER_IMAGE_NAME" 2>&1) if ! echo "$launcher_logs" | grep "MPC launched successfully."; then echo "MPC launcher image did not start properly" echo "$launcher_logs" From bfc7d94971253f0d41a847be42ac44193ad79458 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 17:55:21 +0100 Subject: [PATCH 104/176] add docker compose plugin --- deployment/Dockerfile-launcher | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/Dockerfile-launcher b/deployment/Dockerfile-launcher index 357adf674..77debe54d 100644 --- a/deployment/Dockerfile-launcher +++ b/deployment/Dockerfile-launcher @@ -8,7 +8,7 @@ RUN \ --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ repro-sources-list.sh && \ apt-get update && \ - apt-get install -y --no-install-recommends docker.io && \ + apt-get install -y --no-install-recommends docker.io docker-compose-plugin && \ : "Clean up for improving reproducibility" && \ rm -rf /var/log/* /var/cache/ldconfig/aux-cache From 940bd5087a760961e9476298cb7c0ecaa3d86ad3 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 18:02:36 +0100 Subject: [PATCH 105/176] download docker-compose --- deployment/Dockerfile-launcher | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/deployment/Dockerfile-launcher b/deployment/Dockerfile-launcher index 77debe54d..33a96871a 100644 --- a/deployment/Dockerfile-launcher +++ b/deployment/Dockerfile-launcher @@ -1,3 +1,9 @@ +FROM debian:bookworm-slim@sha256:acd98e6cfc42813a4db9ca54ed79b6f702830bfc2fa43a2c2e87517371d82edb AS download +ARG COMPOSE_VERSION=v2.37.0 +ARG COMPOSE_SHA256=e6e471b1e7bf0443592d3987dea6073f08db3e48ba0580199109aa7a44257e54 +ADD https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-linux-x86_64 /docker-compose +RUN echo "${COMPOSE_SHA256} /docker-compose" | sha256sum -c - + FROM debian:bookworm-slim@sha256:acd98e6cfc42813a4db9ca54ed79b6f702830bfc2fa43a2c2e87517371d82edb ENV DEBIAN_FRONTEND=noninteractive @@ -8,10 +14,12 @@ RUN \ --mount=type=bind,source=./deployment/repro-sources-list.sh,target=/usr/local/bin/repro-sources-list.sh \ repro-sources-list.sh && \ apt-get update && \ - apt-get install -y --no-install-recommends docker.io docker-compose-plugin && \ + apt-get install -y --no-install-recommends docker.io && \ : "Clean up for improving reproducibility" && \ rm -rf /var/log/* /var/cache/ldconfig/aux-cache +COPY --from=download --chmod=0755 /docker-compose /usr/local/lib/docker/cli-plugins/docker-compose + COPY --chmod=0755 target/reproducible/tee-launcher /usr/local/bin/tee-launcher RUN mkdir -p /app-data && mkdir -p /mnt/shared CMD ["tee-launcher"] From 2e5201f27e35bea666a006b606aa87a00b4d6e1f Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 18:06:36 +0100 Subject: [PATCH 106/176] . --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 03478aee3..9b8ec313f 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:83319115964430be6759328ece9eda424b306798c03ab852aaad107c7deffad8 + image: nearone/mpc-launcher@sha256:479a538078af69691b2b5e7cbcd904d6397c89e3d3962aa0d0160f9ca43a661e container_name: "${LAUNCHER_IMAGE_NAME}" environment: From 793b940dba120f3da85ff470663ece842217a04d Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 18:12:52 +0100 Subject: [PATCH 107/176] add mpc-node invokation in the command --- crates/tee-launcher/docker-compose.tee.template.yml | 2 +- crates/tee-launcher/docker-compose.template.yml | 2 +- crates/tee-launcher/src/main.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/tee-launcher/docker-compose.tee.template.yml b/crates/tee-launcher/docker-compose.tee.template.yml index 5bea15363..f409458c7 100644 --- a/crates/tee-launcher/docker-compose.tee.template.yml +++ b/crates/tee-launcher/docker-compose.tee.template.yml @@ -13,7 +13,7 @@ services: - shared-volume:/mnt/shared - mpc-data:/data - "{{DSTACK_UNIX_SOCKET}}:{{DSTACK_UNIX_SOCKET}}" - command: ["start-with-config-file", "{{MPC_CONFIG_CONTAINER_PATH}}"] + command: ["/app/mpc-node", "start-with-config-file", "{{MPC_CONFIG_CONTAINER_PATH}}"] volumes: shared-volume: diff --git a/crates/tee-launcher/docker-compose.template.yml b/crates/tee-launcher/docker-compose.template.yml index 7bc1e7593..3810960ab 100644 --- a/crates/tee-launcher/docker-compose.template.yml +++ b/crates/tee-launcher/docker-compose.template.yml @@ -10,7 +10,7 @@ services: - /tapp:/tapp:ro - shared-volume:/mnt/shared - mpc-data:/data - command: ["start-with-config-file", "{{MPC_CONFIG_CONTAINER_PATH}}"] + command: ["/app/mpc-node", "start-with-config-file", "{{MPC_CONFIG_CONTAINER_PATH}}"] volumes: shared-volume: diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index eff67809f..a03398a03 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -627,7 +627,7 @@ mod tests { let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); // then - assert!(rendered.contains("start-with-config-file")); + assert!(rendered.contains("/app/mpc-node")); assert!(rendered.contains(MPC_CONFIG_CONTAINER_PATH)); } From f8daedd1e2a0f029c768160f09af6a3038d81ff1 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 18:22:45 +0100 Subject: [PATCH 108/176] update launcher again --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 9b8ec313f..fe7b46ec2 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:479a538078af69691b2b5e7cbcd904d6397c89e3d3962aa0d0160f9ca43a661e + image: nearone/mpc-launcher@sha256:8fc1fa432a9a2b82ae86f733511808c4e82e91b27d6ba34a9d239e3849986941 container_name: "${LAUNCHER_IMAGE_NAME}" environment: From 2744500f6dbdc49fc923334f675dd98eef16ffa9 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 18:49:12 +0100 Subject: [PATCH 109/176] use tmp path for config path --- crates/tee-launcher/src/constants.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tee-launcher/src/constants.rs b/crates/tee-launcher/src/constants.rs index 9cdf69794..cc7dddf8e 100644 --- a/crates/tee-launcher/src/constants.rs +++ b/crates/tee-launcher/src/constants.rs @@ -4,4 +4,4 @@ pub(crate) const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; pub(crate) const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; /// Path inside the container where the MPC config file is bind-mounted. -pub(crate) const MPC_CONFIG_CONTAINER_PATH: &str = "/mnt/shared/mpc-config"; +pub(crate) const MPC_CONFIG_CONTAINER_PATH: &str = "/tmp/mpc-config"; From 9a902d05295bb6cb9e391a932be79726ba24adf1 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 19:17:28 +0100 Subject: [PATCH 110/176] update launcher --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index fe7b46ec2..5eb7951f2 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:8fc1fa432a9a2b82ae86f733511808c4e82e91b27d6ba34a9d239e3849986941 + image: nearone/mpc-launcher@sha256:fe8f892f9ac0f876ff0df262b631e638b238c1f2541bad5f3d23d32bad74cc51 container_name: "${LAUNCHER_IMAGE_NAME}" environment: From efc2cbb0d662687bb417f51bc42f8c2c55682200 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 19:23:22 +0100 Subject: [PATCH 111/176] update to write file not directory --- crates/tee-launcher/src/constants.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tee-launcher/src/constants.rs b/crates/tee-launcher/src/constants.rs index cc7dddf8e..607943ee2 100644 --- a/crates/tee-launcher/src/constants.rs +++ b/crates/tee-launcher/src/constants.rs @@ -4,4 +4,4 @@ pub(crate) const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; pub(crate) const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; /// Path inside the container where the MPC config file is bind-mounted. -pub(crate) const MPC_CONFIG_CONTAINER_PATH: &str = "/tmp/mpc-config"; +pub(crate) const MPC_CONFIG_CONTAINER_PATH: &str = "/tmp/mpc-config.toml"; From 05031a0f56b597dc4626dc31058e0f319bd80217 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 19:30:54 +0100 Subject: [PATCH 112/176] update launcher yet again --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 5eb7951f2..4a2f2f768 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:fe8f892f9ac0f876ff0df262b631e638b238c1f2541bad5f3d23d32bad74cc51 + image: nearone/mpc-launcher@sha256:962603dcf6ec638eb541df199b54526c9d5876c58235a1a208b973128fcd1c64 container_name: "${LAUNCHER_IMAGE_NAME}" environment: From c8046406c7148bd6ce2e489136451125970335f2 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 20:10:00 +0100 Subject: [PATCH 113/176] fix docker in docker issue for path --- .../docker-compose.tee.template.yml | 3 +- .../tee-launcher/docker-compose.template.yml | 3 +- crates/tee-launcher/src/constants.rs | 5 ++- crates/tee-launcher/src/main.rs | 42 ++++++------------- 4 files changed, 18 insertions(+), 35 deletions(-) diff --git a/crates/tee-launcher/docker-compose.tee.template.yml b/crates/tee-launcher/docker-compose.tee.template.yml index f409458c7..14134f58b 100644 --- a/crates/tee-launcher/docker-compose.tee.template.yml +++ b/crates/tee-launcher/docker-compose.tee.template.yml @@ -8,12 +8,11 @@ services: environment: - "DSTACK_ENDPOINT={{DSTACK_UNIX_SOCKET}}" volumes: - - "{{MPC_CONFIG_HOST_PATH}}:{{MPC_CONFIG_CONTAINER_PATH}}:ro" - /tapp:/tapp:ro - shared-volume:/mnt/shared - mpc-data:/data - "{{DSTACK_UNIX_SOCKET}}:{{DSTACK_UNIX_SOCKET}}" - command: ["/app/mpc-node", "start-with-config-file", "{{MPC_CONFIG_CONTAINER_PATH}}"] + command: ["/app/mpc-node", "start-with-config-file", "{{MPC_CONFIG_SHARED_PATH}}"] volumes: shared-volume: diff --git a/crates/tee-launcher/docker-compose.template.yml b/crates/tee-launcher/docker-compose.template.yml index 3810960ab..4e641c607 100644 --- a/crates/tee-launcher/docker-compose.template.yml +++ b/crates/tee-launcher/docker-compose.template.yml @@ -6,11 +6,10 @@ services: - no-new-privileges:true ports: {{PORTS}} volumes: - - "{{MPC_CONFIG_HOST_PATH}}:{{MPC_CONFIG_CONTAINER_PATH}}:ro" - /tapp:/tapp:ro - shared-volume:/mnt/shared - mpc-data:/data - command: ["/app/mpc-node", "start-with-config-file", "{{MPC_CONFIG_CONTAINER_PATH}}"] + command: ["/app/mpc-node", "start-with-config-file", "{{MPC_CONFIG_SHARED_PATH}}"] volumes: shared-volume: diff --git a/crates/tee-launcher/src/constants.rs b/crates/tee-launcher/src/constants.rs index 607943ee2..6a6b7623b 100644 --- a/crates/tee-launcher/src/constants.rs +++ b/crates/tee-launcher/src/constants.rs @@ -3,5 +3,6 @@ pub(crate) const IMAGE_DIGEST_FILE: &str = "/mnt/shared/image-digest.bin"; pub(crate) const DSTACK_UNIX_SOCKET: &str = "/var/run/dstack.sock"; pub(crate) const DSTACK_USER_CONFIG_FILE: &str = "/tapp/user_config"; -/// Path inside the container where the MPC config file is bind-mounted. -pub(crate) const MPC_CONFIG_CONTAINER_PATH: &str = "/tmp/mpc-config.toml"; +/// Path on the shared volume where the launcher writes the MPC config and the +/// MPC container reads it. Both containers mount `shared-volume` at `/mnt/shared`. +pub(crate) const MPC_CONFIG_SHARED_PATH: &str = "/mnt/shared/mpc-config.toml"; diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index a03398a03..8942e84fe 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -119,7 +119,7 @@ async fn run() -> Result<(), LauncherError> { .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; } - let mpc_binary_config_path = std::path::Path::new("/tmp/mpc-config"); + let mpc_binary_config_path = std::path::Path::new(MPC_CONFIG_SHARED_PATH); let mpc_config_toml = toml::to_string(&dstack_config.mpc_config) .expect("re-serializing a toml::Table always succeeds"); std::fs::write(mpc_binary_config_path, mpc_config_toml.as_bytes()).map_err(|source| { @@ -133,7 +133,6 @@ async fn run() -> Result<(), LauncherError> { args.platform, &manifest_digest, &dstack_config.launcher_config.image_name, - mpc_binary_config_path, &dstack_config.docker_command_config, )?; @@ -402,7 +401,6 @@ async fn validate_image_hash( fn render_compose_file( platform: Platform, - mpc_config_file: &std::path::Path, docker_flags: &DockerLaunchFlags, image_name: &str, manifest_digest: &DockerSha256Digest, @@ -424,11 +422,7 @@ fn render_compose_file( .replace("{{IMAGE_NAME}}", image_name) .replace("{{IMAGE}}", &manifest_digest.to_string()) .replace("{{CONTAINER_NAME}}", MPC_CONTAINER_NAME) - .replace( - "{{MPC_CONFIG_HOST_PATH}}", - &mpc_config_file.display().to_string(), - ) - .replace("{{MPC_CONFIG_CONTAINER_PATH}}", MPC_CONFIG_CONTAINER_PATH) + .replace("{{MPC_CONFIG_SHARED_PATH}}", MPC_CONFIG_SHARED_PATH) .replace("{{DSTACK_UNIX_SOCKET}}", DSTACK_UNIX_SOCKET) .replace("{{PORTS}}", &ports_json); @@ -448,14 +442,12 @@ fn launch_mpc_container( platform: Platform, manifest_digest: &DockerSha256Digest, image_name: &str, - mpc_config_file: &std::path::Path, docker_flags: &DockerLaunchFlags, ) -> Result<(), LauncherError> { tracing::info!(?manifest_digest, "launching MPC node"); let compose_file = render_compose_file( platform, - mpc_config_file, docker_flags, image_name, manifest_digest, @@ -491,8 +483,6 @@ fn launch_mpc_container( #[cfg(test)] mod tests { - use std::path::Path; - use assert_matches::assert_matches; use launcher_interface::types::{ApprovedHashes, DockerSha256Digest}; use near_mpc_bounded_collections::NonEmptyVec; @@ -503,19 +493,15 @@ mod tests { use crate::select_image_hash; use crate::types::*; - const SAMPLE_CONFIG_PATH: &str = "/tapp/mpc-config.json"; - const SAMPLE_IMAGE_NAME: &str = "nearone/mpc-node"; fn render( platform: Platform, - config_path: &str, flags: &DockerLaunchFlags, digest: &DockerSha256Digest, ) -> String { let file = render_compose_file( platform, - Path::new(config_path), flags, SAMPLE_IMAGE_NAME, digest, @@ -564,7 +550,7 @@ mod tests { let digest = sample_digest(); // when - let rendered = render(Platform::Tee, SAMPLE_CONFIG_PATH, &flags, &digest); + let rendered = render(Platform::Tee, &flags, &digest); // then assert!(rendered.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); @@ -578,7 +564,7 @@ mod tests { let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); + let rendered = render(Platform::NonTee, &flags, &digest); // then assert!(!rendered.contains("DSTACK_ENDPOINT")); @@ -592,7 +578,7 @@ mod tests { let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); + let rendered = render(Platform::NonTee, &flags, &digest); // then assert!(rendered.contains("no-new-privileges:true")); @@ -609,12 +595,10 @@ mod tests { let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); + let rendered = render(Platform::NonTee, &flags, &digest); - // then - assert!(rendered.contains(&format!( - "{SAMPLE_CONFIG_PATH}:{MPC_CONFIG_CONTAINER_PATH}:ro" - ))); + // then — config is on the shared volume, referenced in the command + assert!(rendered.contains(MPC_CONFIG_SHARED_PATH)); } #[test] @@ -624,11 +608,11 @@ mod tests { let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); + let rendered = render(Platform::NonTee, &flags, &digest); // then assert!(rendered.contains("/app/mpc-node")); - assert!(rendered.contains(MPC_CONFIG_CONTAINER_PATH)); + assert!(rendered.contains(MPC_CONFIG_SHARED_PATH)); } #[test] @@ -638,7 +622,7 @@ mod tests { let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); + let rendered = render(Platform::NonTee, &flags, &digest); // then assert!(rendered.contains(&format!("image: \"{SAMPLE_IMAGE_NAME}@{digest}\""))); @@ -651,7 +635,7 @@ mod tests { let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); + let rendered = render(Platform::NonTee, &flags, &digest); // then assert!(rendered.contains("11780:11780")); @@ -664,7 +648,7 @@ mod tests { let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, SAMPLE_CONFIG_PATH, &flags, &digest); + let rendered = render(Platform::NonTee, &flags, &digest); // then assert!(!rendered.contains("environment:")); From 220c5b37e69ff407f749bda78f449ee8a5013dc9 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 20:13:28 +0100 Subject: [PATCH 114/176] update launcher --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 4a2f2f768..e3a115cb4 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:962603dcf6ec638eb541df199b54526c9d5876c58235a1a208b973128fcd1c64 + image: nearone/mpc-launcher@sha256:4caed9923a56ad5561db6008e213caca375d934c9ab8ed03481a0945b2e8df82 container_name: "${LAUNCHER_IMAGE_NAME}" environment: From 2122c29ce7263c5d722a72ef3ef6009ebaf45a73 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 20:30:12 +0100 Subject: [PATCH 115/176] remove head for logs --- scripts/check-mpc-node-docker-starts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-mpc-node-docker-starts.sh b/scripts/check-mpc-node-docker-starts.sh index 5f59d8184..6bdff676b 100755 --- a/scripts/check-mpc-node-docker-starts.sh +++ b/scripts/check-mpc-node-docker-starts.sh @@ -64,7 +64,7 @@ echo "Container started: $CONTAINER_ID" # Check if container is actually running sleep 60 if [ -z "$(docker ps --filter "id=$CONTAINER_ID" --format "{{.ID}}")" ]; then - docker logs "$CONTAINER_ID" 2>&1 | head -50 + docker logs "$CONTAINER_ID" 2>&1 echo "❌ Container cannot initialize/start properly" exit 1 fi From e796d295e8afa6f58c8fd4fce587ed226e86f3e6 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 20:32:28 +0100 Subject: [PATCH 116/176] add names for the shared volumes --- crates/tee-launcher/docker-compose.tee.template.yml | 2 ++ crates/tee-launcher/docker-compose.template.yml | 2 ++ 2 files changed, 4 insertions(+) diff --git a/crates/tee-launcher/docker-compose.tee.template.yml b/crates/tee-launcher/docker-compose.tee.template.yml index 14134f58b..ef3c06fb4 100644 --- a/crates/tee-launcher/docker-compose.tee.template.yml +++ b/crates/tee-launcher/docker-compose.tee.template.yml @@ -16,4 +16,6 @@ services: volumes: shared-volume: + name: shared-volume mpc-data: + name: mpc-data diff --git a/crates/tee-launcher/docker-compose.template.yml b/crates/tee-launcher/docker-compose.template.yml index 4e641c607..b6e3e5979 100644 --- a/crates/tee-launcher/docker-compose.template.yml +++ b/crates/tee-launcher/docker-compose.template.yml @@ -13,4 +13,6 @@ services: volumes: shared-volume: + name: shared-volume mpc-data: + name: mpc-data From 2140318ab74749ecf9da3d84004766e612ffe214 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 20:46:47 +0100 Subject: [PATCH 117/176] . --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index e3a115cb4..120aa53cd 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:4caed9923a56ad5561db6008e213caca375d934c9ab8ed03481a0945b2e8df82 + image: nearone/mpc-launcher@sha256:56881591b91d324d78392f805752ecb640c773a4d62c7ae43b5fd671f6296555 container_name: "${LAUNCHER_IMAGE_NAME}" environment: From fc09fee7cc535aecc11ab67082a57ad851ddb907 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 21:49:34 +0100 Subject: [PATCH 118/176] add near init config --- crates/node/src/cli.rs | 53 +++++----- crates/node/src/config.rs | 3 +- crates/node/src/config/start.rs | 173 +++++++++++++++++++++++++++++++- 3 files changed, 197 insertions(+), 32 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 427a35aba..b4b31b2f2 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -1,7 +1,7 @@ use crate::{ config::{ - load_config_file, ConfigFile, GcpStartConfig, SecretsStartConfig, StartConfig, - TeeAuthorityStartConfig, TeeStartConfig, + load_config_file, ConfigFile, GcpStartConfig, NearInitConfig, SecretsStartConfig, + StartConfig, TeeAuthorityStartConfig, TeeStartConfig, }, keyshare::{ compat::legacy_ecdsa_key_from_keyshares, @@ -143,6 +143,7 @@ impl StartCmd { secret_store_key_hex: self.secret_store_key_hex, backup_encryption_key_hex: self.backup_encryption_key_hex, }, + near_init: None, tee: TeeStartConfig { authority: match self.tee_authority { CliTeeAuthorityConfig::Local => TeeAuthorityStartConfig::Local, @@ -188,6 +189,22 @@ pub struct InitConfigArgs { #[arg(long)] pub boot_nodes: Option, } + +impl InitConfigArgs { + pub fn into_near_init_config(self) -> NearInitConfig { + NearInitConfig { + chain_id: self.chain_id.unwrap_or_default(), + boot_nodes: self.boot_nodes.unwrap_or_default(), + genesis_path: self.genesis.map(PathBuf::from), + download_config: Some(self.download_config), + download_config_url: self.download_config_url, + download_genesis: Some(self.download_genesis), + download_genesis_url: self.download_genesis_url, + download_genesis_records_url: self.download_genesis_records_url, + } + } +} + #[derive(Args, Debug)] pub struct ImportKeyshareCmd { /// Path to home directory @@ -220,6 +237,7 @@ impl Cli { match self.command { CliCommand::StartWithConfigFile { config_path } => { let node_configuration = StartConfig::from_toml_file(&config_path)?; + node_configuration.ensure_near_initialized()?; run_mpc_node(node_configuration).await } // TODO(#2334): deprecate this @@ -231,34 +249,9 @@ impl Cli { run_mpc_node(node_configuration).await } CliCommand::Init(config) => { - let (download_config_type, download_config_url) = if config.download_config { - ( - Some(near_config_utils::DownloadConfigType::RPC), - config.download_config_url.as_ref().map(AsRef::as_ref), - ) - } else { - (None, None) - }; - near_indexer::init_configs( - &config.dir, - config.chain_id, - None, - None, - 1, - false, - config.genesis.as_ref().map(AsRef::as_ref), - config.download_genesis, - config.download_genesis_url.as_ref().map(AsRef::as_ref), - config - .download_genesis_records_url - .as_ref() - .map(AsRef::as_ref), - download_config_type, - download_config_url, - config.boot_nodes.as_ref().map(AsRef::as_ref), - None, - None, - ) + let dir = config.dir.clone(); + let near_init = config.into_near_init_config(); + near_init.run_init(&dir) } CliCommand::ImportKeyshare(cmd) => cmd.run().await, CliCommand::ExportKeyshare(cmd) => cmd.run().await, diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index f48c9f922..24afbacca 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -15,7 +15,8 @@ use std::{ mod start; pub use start::{ - GcpStartConfig, SecretsStartConfig, StartConfig, TeeAuthorityStartConfig, TeeStartConfig, + GcpStartConfig, NearInitConfig, SecretsStartConfig, StartConfig, TeeAuthorityStartConfig, + TeeStartConfig, }; mod foreign_chains; diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index dea203f84..00dc06ce6 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -1,7 +1,7 @@ use super::ConfigFile; use anyhow::Context; use serde::{Deserialize, Serialize}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; use tee_authority::tee_authority::{ DstackTeeAuthorityConfig, LocalTeeAuthorityConfig, TeeAuthority, DEFAULT_DSTACK_ENDPOINT, DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL, @@ -21,10 +21,116 @@ pub struct StartConfig { /// GCP keyshare storage settings. Optional — omit if not using GCP. #[serde(default)] pub gcp: Option, + /// NEAR node initialization settings. Required for `start-with-config-file` + /// so the node can self-initialize when `config.json` is absent. + /// When using the legacy `start` command (behind `start.sh`), this is + /// `None` because `start.sh` already ran `mpc-node init`. + #[serde(default)] + pub near_init: Option, /// Node configuration (indexer, protocol parameters, etc.). pub node: ConfigFile, } +/// NEAR node initialization configuration. Controls how the NEAR node's +/// genesis and config files are bootstrapped when they don't yet exist. +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct NearInitConfig { + /// NEAR chain / network ID (e.g. "mainnet", "testnet", "mpc-localnet"). + pub chain_id: String, + /// Comma-separated NEAR boot nodes. + pub boot_nodes: String, + /// Path to a local genesis file. When set the genesis is copied from this + /// path instead of being downloaded. Typically used for localnet. + #[serde(default)] + pub genesis_path: Option, + /// Whether to download the NEAR config file. Defaults to `true` for + /// non-localnet chains when not specified. + #[serde(default)] + pub download_config: Option, + /// Custom URL to download the NEAR config file from. + #[serde(default)] + pub download_config_url: Option, + /// Whether to download the NEAR genesis file. Defaults to `true` for + /// non-localnet chains when not specified. + #[serde(default)] + pub download_genesis: Option, + /// Custom URL to download the genesis file from. + #[serde(default)] + pub download_genesis_url: Option, + /// Custom URL to download the genesis records from. + #[serde(default)] + pub download_genesis_records_url: Option, +} + +impl NearInitConfig { + /// Runs `near_indexer::init_configs` to create the NEAR data directory. + pub fn run_init(&self, home_dir: &Path) -> anyhow::Result<()> { + let is_localnet = self.chain_id == "mpc-localnet"; + + let genesis_arg = self.genesis_path.as_deref().and_then(Path::to_str); + + let should_download_genesis = self.download_genesis.unwrap_or(!is_localnet); + let should_download_config = self.download_config.unwrap_or(!is_localnet); + + let download_config_type = if should_download_config { + Some(near_config_utils::DownloadConfigType::RPC) + } else { + None + }; + + let chain_id_arg = if self.chain_id.is_empty() { + None + } else { + Some(self.chain_id.clone()) + }; + let boot_nodes_arg = if self.boot_nodes.is_empty() { + None + } else { + Some(self.boot_nodes.as_str()) + }; + + near_indexer::init_configs( + home_dir, + chain_id_arg, + None, + None, + 1, + false, + genesis_arg, + should_download_genesis, + self.download_genesis_url.as_deref(), + self.download_genesis_records_url.as_deref(), + download_config_type, + self.download_config_url.as_deref(), + boot_nodes_arg, + None, // max_gas_burnt_view + None, // state_sync_bucket + ) + .context("failed to initialize NEAR node")?; + + // For localnet, overwrite the genesis file with the original (init + // modifies it) and remove the unnecessary validator_key.json. + if is_localnet { + if let Some(genesis_src) = &self.genesis_path { + let genesis_dst = home_dir.join("genesis.json"); + std::fs::copy(genesis_src, &genesis_dst).with_context(|| { + format!( + "failed to copy genesis from {} to {}", + genesis_src.display(), + genesis_dst.display() + ) + })?; + } + let validator_key = home_dir.join("validator_key.json"); + if validator_key.exists() { + std::fs::remove_file(&validator_key).ok(); + } + } + + Ok(()) + } +} + /// Encryption keys needed at startup. #[derive(Clone, Serialize, Deserialize)] pub struct SecretsStartConfig { @@ -125,4 +231,69 @@ impl StartConfig { .context("invalid node config in config file")?; Ok(config) } + + /// Ensures the NEAR node data directory is initialized. + /// + /// When `near_init` is `Some` and `home_dir/config.json` does not yet + /// exist, this runs the equivalent of `mpc-node init` followed by the + /// config-patching that `start.sh` performs (tracked shards, state sync, + /// etc.). If `config.json` already exists the method is a no-op. + /// + /// When `near_init` is `None` (legacy `start` command) this is always a + /// no-op — `start.sh` is expected to have handled initialization. + pub fn ensure_near_initialized(&self) -> anyhow::Result<()> { + let Some(near_init) = &self.near_init else { + return Ok(()); + }; + + let near_config_path = self.home_dir.join("config.json"); + if near_config_path.exists() { + tracing::info!("NEAR node already initialized, skipping init"); + return Ok(()); + } + + tracing::info!(chain_id = %near_init.chain_id, "initializing NEAR node"); + near_init.run_init(&self.home_dir)?; + + // Patch the NEAR node config the same way start.sh does. + Self::patch_near_config(&near_config_path, &near_init.chain_id, &self.node)?; + + Ok(()) + } + + /// Applies post-init patches to the NEAR node `config.json`, matching the + /// behaviour of `update_near_node_config()` in `start.sh`. + fn patch_near_config( + config_path: &Path, + chain_id: &str, + node_config: &ConfigFile, + ) -> anyhow::Result<()> { + let raw = std::fs::read_to_string(config_path) + .with_context(|| format!("failed to read {}", config_path.display()))?; + let mut config: serde_json::Value = + serde_json::from_str(&raw).context("failed to parse NEAR config.json")?; + + // store.load_mem_tries_for_tracked_shards = true + config["store"]["load_mem_tries_for_tracked_shards"] = serde_json::Value::Bool(true); + + let is_localnet = chain_id == "mpc-localnet"; + if is_localnet { + config["state_sync_enabled"] = serde_json::Value::Bool(false); + } else { + config["state_sync"]["sync"]["ExternalStorage"] + ["external_storage_fallback_threshold"] = serde_json::json!(0); + } + + // Track the shard that hosts the MPC contract. + let contract_id = node_config.indexer.mpc_contract_id.to_string(); + config["tracked_shards_config"] = serde_json::json!({ "Accounts": [contract_id] }); + + let patched = serde_json::to_string_pretty(&config) + .context("failed to re-serialize NEAR config.json")?; + std::fs::write(config_path, patched) + .with_context(|| format!("failed to write {}", config_path.display()))?; + + tracing::info!("NEAR node config.json patched successfully"); + Ok(()) + } } From 140ba523d49c486e0cbc1e1c416bdaf2f57e71d5 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 21:57:16 +0100 Subject: [PATCH 119/176] add near_init --- deployment/localnet/tee/sam.toml | 5 +++++ deployment/testnet/frodo.toml | 4 ++++ deployment/testnet/sam.toml | 4 ++++ docs/localnet/mpc-config.template.toml | 5 +++++ tee_launcher/user-config.toml | 4 ++++ 5 files changed, 22 insertions(+) diff --git a/deployment/localnet/tee/sam.toml b/deployment/localnet/tee/sam.toml index f9bc42440..9584468b9 100644 --- a/deployment/localnet/tee/sam.toml +++ b/deployment/localnet/tee/sam.toml @@ -16,6 +16,11 @@ ports = [ [mpc_config] home_dir = "/data" +[mpc_config.near_init] +chain_id = "mpc-localnet" +boot_nodes = "" +genesis_path = "/app/localnet-genesis.json" + [mpc_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" diff --git a/deployment/testnet/frodo.toml b/deployment/testnet/frodo.toml index e3a7599ce..f47e9352b 100644 --- a/deployment/testnet/frodo.toml +++ b/deployment/testnet/frodo.toml @@ -17,6 +17,10 @@ ports = [ [mpc_config] home_dir = "/data" +[mpc_config.near_init] +chain_id = "testnet" +boot_nodes = "" + [mpc_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" diff --git a/deployment/testnet/sam.toml b/deployment/testnet/sam.toml index 5d9c9586c..fc0ff0273 100644 --- a/deployment/testnet/sam.toml +++ b/deployment/testnet/sam.toml @@ -17,6 +17,10 @@ ports = [ [mpc_config] home_dir = "/data" +[mpc_config.near_init] +chain_id = "testnet" +boot_nodes = "" + [mpc_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" diff --git a/docs/localnet/mpc-config.template.toml b/docs/localnet/mpc-config.template.toml index c9f7d6d4d..fb1e4c780 100644 --- a/docs/localnet/mpc-config.template.toml +++ b/docs/localnet/mpc-config.template.toml @@ -1,5 +1,10 @@ home_dir = "$HOME/.near/$MPC_NODE_ID" +[near_init] +chain_id = "mpc-localnet" +boot_nodes = "" +genesis_path = "/app/localnet-genesis.json" + [secrets] secret_store_key_hex = "11111111111111111111111111111111" diff --git a/tee_launcher/user-config.toml b/tee_launcher/user-config.toml index c88191ff3..eb5da0aaf 100644 --- a/tee_launcher/user-config.toml +++ b/tee_launcher/user-config.toml @@ -17,6 +17,10 @@ ports = [ [mpc_config] home_dir = "/data" +[mpc_config.near_init] +chain_id = "testnet" +boot_nodes = "" + [mpc_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" From 6b7645ab1028dcb8ffc9704dbe9276af9880c912 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 22:14:41 +0100 Subject: [PATCH 120/176] update mpc image --- deployment/localnet/tee/sam.toml | 2 +- deployment/testnet/frodo.toml | 2 +- deployment/testnet/sam.toml | 2 +- tee_launcher/launcher_docker_compose.yaml | 2 +- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- tee_launcher/user-config.toml | 2 +- 6 files changed, 6 insertions(+), 6 deletions(-) diff --git a/deployment/localnet/tee/sam.toml b/deployment/localnet/tee/sam.toml index 9584468b9..03affa5b8 100644 --- a/deployment/localnet/tee/sam.toml +++ b/deployment/localnet/tee/sam.toml @@ -1,5 +1,5 @@ [launcher_config] -image_tags = ["main-260e88b"] +image_tags = ["2262-port-node-launcher-to-rust-v2-140ba52"] image_name = "nearone/mpc-node" registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 diff --git a/deployment/testnet/frodo.toml b/deployment/testnet/frodo.toml index f47e9352b..3dcccbb22 100644 --- a/deployment/testnet/frodo.toml +++ b/deployment/testnet/frodo.toml @@ -1,5 +1,5 @@ [launcher_config] -image_tags = ["barak-doc-update_localnet_guide-b12bc7d"] +image_tags = ["2262-port-node-launcher-to-rust-v2-140ba52"] image_name = "nearone/mpc-node" registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 diff --git a/deployment/testnet/sam.toml b/deployment/testnet/sam.toml index fc0ff0273..5901e6328 100644 --- a/deployment/testnet/sam.toml +++ b/deployment/testnet/sam.toml @@ -1,5 +1,5 @@ [launcher_config] -image_tags = ["barak-doc-update_localnet_guide-b12bc7d"] +image_tags = ["2262-port-node-launcher-to-rust-v2-140ba52"] image_name = "nearone/mpc-node" registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 diff --git a/tee_launcher/launcher_docker_compose.yaml b/tee_launcher/launcher_docker_compose.yaml index b59b44b32..0ecbef41f 100644 --- a/tee_launcher/launcher_docker_compose.yaml +++ b/tee_launcher/launcher_docker_compose.yaml @@ -9,7 +9,7 @@ services: environment: - PLATFORM=TEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:07bba7c60565750f6d5fe6800cd73513dd2d0d02e6893184064e209ff37c25a2 + - DEFAULT_IMAGE_DIGEST=sha256:952e056156102f614db979a812ec10ed98187a206533ffc1740008768e889c15 volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 120aa53cd..535062e1c 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:689ad0f37a5a9db75eeb93cb15ad4bb507883412f891a4fd19448ada575c8237 # config digest for nearone/mpc-node:main-6ee90e0 + - DEFAULT_IMAGE_DIGEST=sha256:952e056156102f614db979a812ec10ed98187a206533ffc1740008768e889c15 # config digest for nearone/mpc-node:2262-port-node-launcher-to-rust-v2-140ba52 volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/tee_launcher/user-config.toml b/tee_launcher/user-config.toml index eb5da0aaf..0b22d94a9 100644 --- a/tee_launcher/user-config.toml +++ b/tee_launcher/user-config.toml @@ -1,5 +1,5 @@ [launcher_config] -image_tags = ["main-6ee90e0"] +image_tags = ["2262-port-node-launcher-to-rust-v2-140ba52"] image_name = "nearone/mpc-node" registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 From bf1e9722030a4ed9073abf3ce9e65d942f429305 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 22:23:25 +0100 Subject: [PATCH 121/176] . --- tee_launcher/launcher_docker_compose.yaml | 2 +- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tee_launcher/launcher_docker_compose.yaml b/tee_launcher/launcher_docker_compose.yaml index 0ecbef41f..90cafc8b2 100644 --- a/tee_launcher/launcher_docker_compose.yaml +++ b/tee_launcher/launcher_docker_compose.yaml @@ -9,7 +9,7 @@ services: environment: - PLATFORM=TEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:952e056156102f614db979a812ec10ed98187a206533ffc1740008768e889c15 + - DEFAULT_IMAGE_DIGEST=sha256:ab42ed7ec5aa743668abd3d9606ac7f556cb1f2c57b963d29bacd573d20a924a volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 535062e1c..2e345035d 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:952e056156102f614db979a812ec10ed98187a206533ffc1740008768e889c15 # config digest for nearone/mpc-node:2262-port-node-launcher-to-rust-v2-140ba52 + - DEFAULT_IMAGE_DIGEST=sha256:ab42ed7ec5aa743668abd3d9606ac7f556cb1f2c57b963d29bacd573d20a924a # config digest for nearone/mpc-node:2262-port-node-launcher-to-rust-v2-140ba52 volumes: - /var/run/docker.sock:/var/run/docker.sock From 2ff46458b6882188656efcd7e9b7c4b3a09341cc Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 22:35:22 +0100 Subject: [PATCH 122/176] add more logs --- scripts/check-mpc-node-docker-starts.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/check-mpc-node-docker-starts.sh b/scripts/check-mpc-node-docker-starts.sh index 6bdff676b..9f9cbe077 100755 --- a/scripts/check-mpc-node-docker-starts.sh +++ b/scripts/check-mpc-node-docker-starts.sh @@ -64,7 +64,12 @@ echo "Container started: $CONTAINER_ID" # Check if container is actually running sleep 60 if [ -z "$(docker ps --filter "id=$CONTAINER_ID" --format "{{.ID}}")" ]; then - docker logs "$CONTAINER_ID" 2>&1 + echo "=== Container inspect ===" + docker inspect "$CONTAINER_ID" --format '{{.State.Status}} exit={{.State.ExitCode}}' 2>&1 || true + echo "=== Container logs ===" + docker logs "$CONTAINER_ID" 2>&1 || true + echo "=== docker ps -a ===" + docker ps -a 2>&1 || true echo "❌ Container cannot initialize/start properly" exit 1 fi From b264cb6ccd2bec00c14aedd1efd32181b8bd78b6 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Sun, 15 Mar 2026 22:42:34 +0100 Subject: [PATCH 123/176] incraese resoruces --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fc26450fd..fe0eff55a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: launcher-nontee-check: name: "MPC Launcher non-tee check" - runs-on: warp-ubuntu-2404-x64-2x + runs-on: warp-ubuntu-2404-x64-8x # was OOMing with 2x timeout-minutes: 60 permissions: contents: read From a076a0931b1a1590ea6a5f2b3d3ebe2e7eeedfb0 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:06:33 +0100 Subject: [PATCH 124/176] update launcher image and default mpc digest --- tee_launcher/launcher_docker_compose.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tee_launcher/launcher_docker_compose.yaml b/tee_launcher/launcher_docker_compose.yaml index 90cafc8b2..4b950710f 100644 --- a/tee_launcher/launcher_docker_compose.yaml +++ b/tee_launcher/launcher_docker_compose.yaml @@ -2,14 +2,14 @@ version: '3.8' services: launcher: - image: nearone/mpc-launcher@sha256:85a4fa6d1eec05e8f43dba17d3f4368f89719a2a06b9e2051d84813c3f651068 + image: nearone/mpc-launcher@sha256:5973f1b0510f580ec1cada64310a430f4d7a380a69e57493b055bbb9cdd6ae63 container_name: launcher environment: - PLATFORM=TEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:ab42ed7ec5aa743668abd3d9606ac7f556cb1f2c57b963d29bacd573d20a924a + - DEFAULT_IMAGE_DIGEST=sha256:a9a874255739ab610513a570e8af9daf4f0a774781a0e13120d9b09c4106ce65 # 3.7.0 volumes: - /var/run/docker.sock:/var/run/docker.sock From 1643c6e7d09969155834c7d0eee7613633b61f5d Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:10:20 +0100 Subject: [PATCH 125/176] update nontee yaml --- tee_launcher/launcher_docker_compose_nontee.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 2e345035d..b0b53f742 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -1,12 +1,12 @@ services: launcher: - image: nearone/mpc-launcher@sha256:56881591b91d324d78392f805752ecb640c773a4d62c7ae43b5fd671f6296555 + image: nearone/mpc-launcher@sha256:5973f1b0510f580ec1cada64310a430f4d7a380a69e57493b055bbb9cdd6ae63 container_name: "${LAUNCHER_IMAGE_NAME}" environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:ab42ed7ec5aa743668abd3d9606ac7f556cb1f2c57b963d29bacd573d20a924a # config digest for nearone/mpc-node:2262-port-node-launcher-to-rust-v2-140ba52 + - DEFAULT_IMAGE_DIGEST=sha256:a9a874255739ab610513a570e8af9daf4f0a774781a0e13120d9b09c4106ce65 # 3.7.0 volumes: - /var/run/docker.sock:/var/run/docker.sock From 6d1664693625e868f49f252a433681132fa51295 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:18:46 +0100 Subject: [PATCH 126/176] . --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index b0b53f742..1d235a1f2 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:a9a874255739ab610513a570e8af9daf4f0a774781a0e13120d9b09c4106ce65 # 3.7.0 + - DEFAULT_IMAGE_DIGEST=sha256:a9a874255739ab610513a570e8af9daf4f0a774781a0e13120d9b09c4106ce65 # 3.7.0 volumes: - /var/run/docker.sock:/var/run/docker.sock From db0e20ea0972f1c3f7eb31d76ff5ac4bbb9a37cb Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:18:57 +0100 Subject: [PATCH 127/176] retrigger ci --- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index 1d235a1f2..b0b53f742 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:a9a874255739ab610513a570e8af9daf4f0a774781a0e13120d9b09c4106ce65 # 3.7.0 + - DEFAULT_IMAGE_DIGEST=sha256:a9a874255739ab610513a570e8af9daf4f0a774781a0e13120d9b09c4106ce65 # 3.7.0 volumes: - /var/run/docker.sock:/var/run/docker.sock From b4a67b40061b5b672ca84fff0633848684bb3c86 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:26:07 +0100 Subject: [PATCH 128/176] use the image id --- tee_launcher/launcher_docker_compose.yaml | 2 +- tee_launcher/launcher_docker_compose_nontee.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tee_launcher/launcher_docker_compose.yaml b/tee_launcher/launcher_docker_compose.yaml index 4b950710f..50f9b7820 100644 --- a/tee_launcher/launcher_docker_compose.yaml +++ b/tee_launcher/launcher_docker_compose.yaml @@ -9,7 +9,7 @@ services: environment: - PLATFORM=TEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:a9a874255739ab610513a570e8af9daf4f0a774781a0e13120d9b09c4106ce65 # 3.7.0 + - DEFAULT_IMAGE_DIGEST=sha256:e2ef71c220158f9ee19a265d583647eedb4e0cd7ca37021fbf0ab34e3d214ed0 # 3.7.0 volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/tee_launcher/launcher_docker_compose_nontee.yaml index b0b53f742..42ec10b8d 100644 --- a/tee_launcher/launcher_docker_compose_nontee.yaml +++ b/tee_launcher/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:a9a874255739ab610513a570e8af9daf4f0a774781a0e13120d9b09c4106ce65 # 3.7.0 + - DEFAULT_IMAGE_DIGEST=sha256:e2ef71c220158f9ee19a265d583647eedb4e0cd7ca37021fbf0ab34e3d214ed0 # 3.7.0 volumes: - /var/run/docker.sock:/var/run/docker.sock From 4688f25b37c6c9ee40473532a09c42c5bc9fec6f Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:29:48 +0100 Subject: [PATCH 129/176] update image tags in user config --- tee_launcher/user-config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tee_launcher/user-config.toml b/tee_launcher/user-config.toml index 0b22d94a9..fcd6d055d 100644 --- a/tee_launcher/user-config.toml +++ b/tee_launcher/user-config.toml @@ -1,5 +1,5 @@ [launcher_config] -image_tags = ["2262-port-node-launcher-to-rust-v2-140ba52"] +image_tags = ["3.7.0"] image_name = "nearone/mpc-node" registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 From aea06b57d44828ff6727beebd2e90c0943ef8ead Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:39:39 +0100 Subject: [PATCH 130/176] add check that near_init is set --- crates/node/src/cli.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 5f07960e5..da62862b8 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -253,6 +253,12 @@ impl Cli { match self.command { CliCommand::StartWithConfigFile { config_path } => { let node_configuration = StartConfig::from_toml_file(&config_path)?; + // TODO(#2334): make near_init field non optional + anyhow::ensure!( + node_configuration.near_init.is_some(), + "[near_init] table must be set" + ); + node_configuration.ensure_near_initialized()?; run_mpc_node(node_configuration).await } From 24bf7435d74ec5fd76060a8ff7f4ffc5431221eb Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:43:13 +0100 Subject: [PATCH 131/176] added missing near_init fields --- tee_launcher/user-config.toml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tee_launcher/user-config.toml b/tee_launcher/user-config.toml index fcd6d055d..c8583d282 100644 --- a/tee_launcher/user-config.toml +++ b/tee_launcher/user-config.toml @@ -20,6 +20,8 @@ home_dir = "/data" [mpc_config.near_init] chain_id = "testnet" boot_nodes = "" +download_genesis = true +download_config = "rpc" [mpc_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" From fcfb62ec1ec010f4f8dbeccb8068691f5689a416 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:48:49 +0100 Subject: [PATCH 132/176] remove duplicate snapshots --- .../launcher_interface__tests__approved_hashes_file.snap | 9 --------- .../launcher_interface__tests__docker_digest.snap | 5 ----- ...launcher_interface__tests__docker_digest_display.snap | 5 ----- ...uncher_interface__tests__docker_digest_roundtrip.snap | 5 ----- docs/localnet/mpc-config.template.toml | 1 + 5 files changed, 1 insertion(+), 24 deletions(-) delete mode 100644 crates/launcher-interface/src/snapshots/launcher_interface__tests__approved_hashes_file.snap delete mode 100644 crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest.snap delete mode 100644 crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_display.snap delete mode 100644 crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_roundtrip.snap diff --git a/crates/launcher-interface/src/snapshots/launcher_interface__tests__approved_hashes_file.snap b/crates/launcher-interface/src/snapshots/launcher_interface__tests__approved_hashes_file.snap deleted file mode 100644 index 61f7f301c..000000000 --- a/crates/launcher-interface/src/snapshots/launcher_interface__tests__approved_hashes_file.snap +++ /dev/null @@ -1,9 +0,0 @@ ---- -source: crates/launcher-interface/src/lib.rs -expression: json ---- -{ - "approved_hashes": [ - "sha256:abababababababababababababababababababababababababababababababab" - ] -} diff --git a/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest.snap b/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest.snap deleted file mode 100644 index 268452a91..000000000 --- a/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/launcher-interface/src/lib.rs -expression: json ---- -"sha256:abababababababababababababababababababababababababababababababab" diff --git a/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_display.snap b/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_display.snap deleted file mode 100644 index 44b276fe5..000000000 --- a/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_display.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/launcher-interface/src/lib.rs -expression: digest.to_string() ---- -sha256:abababababababababababababababababababababababababababababababab diff --git a/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_roundtrip.snap b/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_roundtrip.snap deleted file mode 100644 index 568aec643..000000000 --- a/crates/launcher-interface/src/snapshots/launcher_interface__tests__docker_digest_roundtrip.snap +++ /dev/null @@ -1,5 +0,0 @@ ---- -source: crates/launcher-interface/src/lib.rs -expression: "serde_json::to_value(&deserialized).unwrap()" ---- -"sha256:abababababababababababababababababababababababababababababababab" diff --git a/docs/localnet/mpc-config.template.toml b/docs/localnet/mpc-config.template.toml index b6e030e20..bdfda022e 100644 --- a/docs/localnet/mpc-config.template.toml +++ b/docs/localnet/mpc-config.template.toml @@ -4,6 +4,7 @@ home_dir = "$HOME/.near/$MPC_NODE_ID" chain_id = "mpc-localnet" boot_nodes = "$NEAR_BOOT_NODES" genesis_path = "$HOME/.near/mpc-localnet/genesis.json" +download_genesis = false rpc_addr = "0.0.0.0:$RPC_PORT" network_addr = "0.0.0.0:$INDEXER_PORT" From e61d8f90ba99330efd4fcd95d2b0769c47e71c3d Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:55:19 +0100 Subject: [PATCH 133/176] add readme.md --- crates/tee-launcher/README.md | 114 ++++++++++++++++++++++++++++++++++ 1 file changed, 114 insertions(+) create mode 100644 crates/tee-launcher/README.md diff --git a/crates/tee-launcher/README.md b/crates/tee-launcher/README.md new file mode 100644 index 000000000..177387f09 --- /dev/null +++ b/crates/tee-launcher/README.md @@ -0,0 +1,114 @@ +# TEE Launcher (Rust) + +Secure launcher for initializing and attesting a Docker-based MPC node inside a TEE-enabled environment (e.g., Intel TDX via dstack). + +Replaces the previous Python launcher (`tee_launcher/launcher.py`). + +## What it does + +1. Loads a TOML configuration file from `/tapp/user_config` +2. Selects an approved MPC image hash (from on-disk approved list, override, or default) +3. Validates the image by resolving it through the Docker registry and pulling by digest +4. In TEE mode: extends RTMR3 by emitting the image digest to dstack +5. Writes the MPC node config to a shared volume +6. Launches the MPC container via `docker compose up -d` + +## CLI Arguments + +All arguments are read from environment variables (set via docker-compose `environment`): + +| Variable | Required | Description | +|----------|----------|-------------| +| `PLATFORM` | Yes | `TEE` or `NONTEE` | +| `DOCKER_CONTENT_TRUST` | Yes | Must be `1` | +| `DEFAULT_IMAGE_DIGEST` | Yes | Fallback `sha256:...` digest when the approved-hashes file is absent | + +## Configuration (TOML) + +The launcher reads its configuration from `/tapp/user_config` as a TOML file. This is a change from the previous Python launcher which used a `.env`-style file. + +```toml +[launcher_config] +image_tags = ["latest"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 +# Optional: force selection of a specific digest (must be in approved list) +# mpc_hash_override = "sha256:abcd..." + +[docker_command_config.port_mappings] +ports = [ + { src = 11780, dst = 11780 }, + { src = 2200, dst = 2200 }, +] + +# Opaque MPC node configuration. +# The launcher does not interpret these fields — they are re-serialized +# to TOML and mounted into the container at /mnt/shared/mpc-config.toml +# for the MPC binary to consume via `start-with-config-file`. +[mpc_config] +# ... any fields the MPC node expects +``` + +### `[launcher_config]` + +| Field | Default | Description | +|-------|---------|-------------| +| `image_tags` | `["latest"]` | Comma-separated Docker image tags to search | +| `image_name` | `nearone/mpc-node` | Docker image name | +| `registry` | `registry.hub.docker.com` | Docker registry hostname | +| `rpc_request_timeout_secs` | `10` | Per-request timeout for registry API calls | +| `rpc_request_interval_secs` | `1` | Initial retry interval for registry API calls | +| `rpc_max_attempts` | `20` | Maximum registry API retry attempts | +| `mpc_hash_override` | (none) | Optional: force a specific `sha256:` digest (must appear in approved list) | + +### `[docker_command_config.port_mappings]` + +Port mappings forwarded to the MPC container. + +### `[mpc_config]` + +Arbitrary TOML table passed through to the MPC node. The launcher writes this verbatim to `/mnt/shared/mpc-config.toml`, which the container reads on startup. + +## Image Hash Selection + +Priority order: +1. If the approved hashes file (`/mnt/shared/image-digest.bin`) exists and `mpc_hash_override` is set: use the override (must be in the approved list) +2. If the approved hashes file exists: use the newest approved hash (first in list) +3. If the file is absent: fall back to `DEFAULT_IMAGE_DIGEST` + +## File Locations + +| Path | Description | +|------|-------------| +| `/tapp/user_config` | TOML configuration file | +| `/mnt/shared/image-digest.bin` | JSON file with approved image hashes (written by the MPC node) | +| `/mnt/shared/mpc-config.toml` | MPC node config (written by the launcher) | +| `/var/run/dstack.sock` | dstack unix socket (TEE mode only) | + +## Key Differences from the Python Launcher + +| Aspect | Python (`launcher.py`) | Rust (`tee-launcher`) | +|--------|----------------------|----------------------| +| Config format | `.env` key-value file | TOML | +| MPC node config | Environment variables passed to container | TOML file mounted into container | +| Container launch | `docker run` with flags | `docker compose up -d` with rendered template | +| RTMR3 extension | `curl` to unix socket | `dstack-sdk` native client | + +## Building + +```bash +cargo build -p tee-launcher --release +``` + +## Testing + +```bash +# Unit tests +cargo nextest run -p tee-launcher + +# Integration tests (requires network access and Docker Hub) +cargo nextest run -p tee-launcher --features integration-test +``` From d7ba5f8ac905e2ed43d590af7e425517706323be Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 12:55:44 +0100 Subject: [PATCH 134/176] added readme --- crates/tee-launcher/Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index e0bc44094..8aa46ef75 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -1,5 +1,6 @@ [package] name = "tee-launcher" +readme = "README.md" version = { workspace = true } license = { workspace = true } edition = { workspace = true } From 9f96eb82c2454dcd10ff0b49f04a696063d545dd Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 13:04:24 +0100 Subject: [PATCH 135/176] delete conf files --- deployment/localnet/tee/frodo.conf | 19 -------- deployment/localnet/tee/frodo.toml | 62 +++++++++++++++++++++++++ deployment/localnet/tee/frodo_conf.json | 21 --------- deployment/localnet/tee/sam.conf | 19 -------- deployment/localnet/tee/sam.toml | 1 + deployment/localnet/tee/sam_conf.json | 21 --------- 6 files changed, 63 insertions(+), 80 deletions(-) delete mode 100644 deployment/localnet/tee/frodo.conf create mode 100644 deployment/localnet/tee/frodo.toml delete mode 100644 deployment/localnet/tee/frodo_conf.json delete mode 100644 deployment/localnet/tee/sam.conf delete mode 100644 deployment/localnet/tee/sam_conf.json diff --git a/deployment/localnet/tee/frodo.conf b/deployment/localnet/tee/frodo.conf deleted file mode 100644 index 8d75a8465..000000000 --- a/deployment/localnet/tee/frodo.conf +++ /dev/null @@ -1,19 +0,0 @@ -# MPC Docker image override -MPC_IMAGE_NAME=nearone/mpc-node -MPC_IMAGE_TAGS=main-260e88b -MPC_REGISTRY=registry.hub.docker.com - -# MPC node settings -MPC_ACCOUNT_ID=frodo.test.near -MPC_LOCAL_ADDRESS=127.0.0.1 -MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -MPC_CONTRACT_ID=mpc-contract.test.near -MPC_ENV=mpc-localnet -MPC_HOME_DIR=/data -RUST_BACKTRACE=full -RUST_LOG=info - -NEAR_BOOT_NODES=ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566 - -# Port forwarding -PORTS=8080:8080,24566:24566,13001:13001 \ No newline at end of file diff --git a/deployment/localnet/tee/frodo.toml b/deployment/localnet/tee/frodo.toml new file mode 100644 index 000000000..cf9a8ebde --- /dev/null +++ b/deployment/localnet/tee/frodo.toml @@ -0,0 +1,62 @@ +[launcher_config] +image_tags = ["2262-port-node-launcher-to-rust-v2-140ba52"] +image_name = "nearone/mpc-node" +registry = "registry.hub.docker.com" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +[docker_command_config.port_mappings] +ports = [ + { src = 8080, dst = 8080 }, + { src = 24566, dst = 24566 }, + { src = 13001, dst = 13001 }, +] + +[mpc_config] +home_dir = "/data" + +[mpc_config.near_init] +chain_id = "mpc-localnet" +boot_nodes = "" +genesis_path = "/app/localnet-genesis.json" +download_genesis = false + +[mpc_config.secrets] +secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_config.tee.authority] +type = "local" + +[mpc_config.node] +my_near_account_id = "frodo.test.near" +near_responder_account_id = "frodo.test.near" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +cores = 4 + +[mpc_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "mpc-contract.test.near" +finality = "optimistic" + +[mpc_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_config.node.signature] +timeout_sec = 60 + +[mpc_config.node.ckd] +timeout_sec = 60 diff --git a/deployment/localnet/tee/frodo_conf.json b/deployment/localnet/tee/frodo_conf.json deleted file mode 100644 index 081733e2e..000000000 --- a/deployment/localnet/tee/frodo_conf.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "MPC_IMAGE_NAME": "nearone/mpc-node", - "MPC_IMAGE_TAGS": "main-260e88b", - "MPC_REGISTRY": "registry.hub.docker.com", - "MPC_ACCOUNT_ID": "frodo.test.near", - "MPC_LOCAL_ADDRESS": "127.0.0.1", - "MPC_SECRET_STORE_KEY": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "MPC_CONTRACT_ID": "mpc-contract.test.near", - "MPC_ENV": "mpc-localnet", - "MPC_HOME_DIR": "/data", - "RUST_BACKTRACE": "full", - "RUST_LOG": "info", - "NEAR_BOOT_NODES": [ - "ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566" - ], - "PORTS": [ - "8080:8080", - "24566:24566", - "13001:13001" - ] -} \ No newline at end of file diff --git a/deployment/localnet/tee/sam.conf b/deployment/localnet/tee/sam.conf deleted file mode 100644 index 507084bf2..000000000 --- a/deployment/localnet/tee/sam.conf +++ /dev/null @@ -1,19 +0,0 @@ -# MPC Docker image override -MPC_IMAGE_NAME=nearone/mpc-node -MPC_IMAGE_TAGS=main-260e88b -MPC_REGISTRY=registry.hub.docker.com - -# MPC node settings -MPC_ACCOUNT_ID=sam.test.near -MPC_LOCAL_ADDRESS=127.0.0.1 -MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -MPC_CONTRACT_ID=mpc-contract.test.near -MPC_ENV=mpc-localnet -MPC_HOME_DIR=/data -RUST_BACKTRACE=full -RUST_LOG=info - -NEAR_BOOT_NODES=ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566 - -# Port forwarding -PORTS=8080:8080,24566:24566,13002:13002 \ No newline at end of file diff --git a/deployment/localnet/tee/sam.toml b/deployment/localnet/tee/sam.toml index 03affa5b8..c4e16983c 100644 --- a/deployment/localnet/tee/sam.toml +++ b/deployment/localnet/tee/sam.toml @@ -20,6 +20,7 @@ home_dir = "/data" chain_id = "mpc-localnet" boot_nodes = "" genesis_path = "/app/localnet-genesis.json" +download_genesis = false [mpc_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" diff --git a/deployment/localnet/tee/sam_conf.json b/deployment/localnet/tee/sam_conf.json deleted file mode 100644 index 8c307acd1..000000000 --- a/deployment/localnet/tee/sam_conf.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "MPC_IMAGE_NAME": "nearone/mpc-node", - "MPC_IMAGE_TAGS": "main-260e88b", - "MPC_REGISTRY": "registry.hub.docker.com", - "MPC_ACCOUNT_ID": "sam.test.near", - "MPC_LOCAL_ADDRESS": "127.0.0.1", - "MPC_SECRET_STORE_KEY": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "MPC_CONTRACT_ID": "mpc-contract.test.near", - "MPC_ENV": "mpc-localnet", - "MPC_HOME_DIR": "/data", - "RUST_BACKTRACE": "full", - "RUST_LOG": "info", - "NEAR_BOOT_NODES": [ - "ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566" - ], - "PORTS": [ - "8080:8080", - "24566:24566", - "13002:13002" - ] -} \ No newline at end of file From 1bc4f8545255b43adbb2b6e130549ed64712aea9 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 13:12:26 +0100 Subject: [PATCH 136/176] ude toml files for testnet --- deployment/testnet/frodo.conf | 19 ------------------ deployment/testnet/frodo.json | 37 ----------------------------------- deployment/testnet/frodo.toml | 14 ++++++++----- deployment/testnet/sam.conf | 19 ------------------ deployment/testnet/sam.toml | 14 ++++++++----- 5 files changed, 18 insertions(+), 85 deletions(-) delete mode 100644 deployment/testnet/frodo.conf delete mode 100644 deployment/testnet/frodo.json delete mode 100644 deployment/testnet/sam.conf diff --git a/deployment/testnet/frodo.conf b/deployment/testnet/frodo.conf deleted file mode 100644 index a6705e1ce..000000000 --- a/deployment/testnet/frodo.conf +++ /dev/null @@ -1,19 +0,0 @@ -# MPC Docker image override -MPC_IMAGE_NAME=nearone/mpc-node -MPC_IMAGE_TAGS=barak-doc-update_localnet_guide-b12bc7d -MPC_REGISTRY=registry.hub.docker.com - -# MPC node settings -MPC_ACCOUNT_ID=$FRODO_ACCOUNT -MPC_LOCAL_ADDRESS=127.0.0.1 -MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -MPC_CONTRACT_ID=$MPC_CONTRACT_ACCOUNT -MPC_ENV=testnet -MPC_HOME_DIR=/data -RUST_BACKTRACE=full -RUST_LOG=info - -NEAR_BOOT_NODES=$BOOTNODES - -# Port forwarding -PORTS=8080:8080,24567:24567,13001:13001,80:80 \ No newline at end of file diff --git a/deployment/testnet/frodo.json b/deployment/testnet/frodo.json deleted file mode 100644 index 4827afc67..000000000 --- a/deployment/testnet/frodo.json +++ /dev/null @@ -1,37 +0,0 @@ -{ - "launcher_config": { - "image_tags": ["barak-doc-update_localnet_guide-b12bc7d"], - "image_name": "nearone/mpc-node", - "registry": "registry.hub.docker.com", - "rpc_request_timeout_secs": 10, - "rpc_request_interval_secs": 1, - "rpc_max_attempts": 20, - "mpc_hash_override": null - }, - "docker_command_config": { - "extra_hosts": { - "hosts": [] - }, - "port_mappings": { - "ports": [ - { "src": 8080, "dst": 8080 }, - { "src": 24567, "dst": 24567 }, - { "src": 13001, "dst": 13001 }, - { "src": 80, "dst": 80 } - ] - } - }, - "mpc_passthrough_env": { - "mpc_account_id": "$FRODO_ACCOUNT", - "mpc_local_address": "127.0.0.1", - "mpc_secret_key_store": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", - "mpc_backup_encryption_key_hex": "0000000000000000000000000000000000000000000000000000000000000000", - "mpc_env": "Testnet", - "mpc_home_dir": "/data", - "mpc_contract_id": "$MPC_CONTRACT_ACCOUNT", - "mpc_responder_id": "$FRODO_ACCOUNT", - "near_boot_nodes": "$BOOTNODES", - "rust_backtrace": "full", - "rust_log": "info" - } -} diff --git a/deployment/testnet/frodo.toml b/deployment/testnet/frodo.toml index 3dcccbb22..d9241c86e 100644 --- a/deployment/testnet/frodo.toml +++ b/deployment/testnet/frodo.toml @@ -20,6 +20,8 @@ home_dir = "/data" [mpc_config.near_init] chain_id = "testnet" boot_nodes = "" +download_genesis = true +download_config = "rpc" [mpc_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" @@ -31,10 +33,11 @@ type = "local" [mpc_config.node] my_near_account_id = "$FRODO_ACCOUNT" near_responder_account_id = "$FRODO_ACCOUNT" -number_of_responder_keys = 1 +number_of_responder_keys = 50 web_ui = "0.0.0.0:8080" migration_web_ui = "0.0.0.0:8078" -cores = 4 +pprof_bind_address = "0.0.0.0:34001" +cores = 12 [mpc_config.node.indexer] validate_genesis = false @@ -42,16 +45,17 @@ sync_mode = "Latest" concurrency = 1 mpc_contract_id = "$MPC_CONTRACT_ACCOUNT" finality = "optimistic" +port_override = 80 [mpc_config.node.triple] concurrency = 2 -desired_triples_to_buffer = 128 +desired_triples_to_buffer = 1000000 timeout_sec = 60 parallel_triple_generation_stagger_time_sec = 1 [mpc_config.node.presignature] -concurrency = 4 -desired_presignatures_to_buffer = 64 +concurrency = 16 +desired_presignatures_to_buffer = 8192 timeout_sec = 60 [mpc_config.node.signature] diff --git a/deployment/testnet/sam.conf b/deployment/testnet/sam.conf deleted file mode 100644 index 4e63db03b..000000000 --- a/deployment/testnet/sam.conf +++ /dev/null @@ -1,19 +0,0 @@ -# MPC Docker image override -MPC_IMAGE_NAME=nearone/mpc-node -MPC_IMAGE_TAGS=barak-doc-update_localnet_guide-b12bc7d -MPC_REGISTRY=registry.hub.docker.com - -# MPC node settings -MPC_ACCOUNT_ID=$SAM_ACCOUNT -MPC_LOCAL_ADDRESS=127.0.0.1 -MPC_SECRET_STORE_KEY=AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA -MPC_CONTRACT_ID=$MPC_CONTRACT_ACCOUNT -MPC_ENV=testnet -MPC_HOME_DIR=/data -RUST_BACKTRACE=full -RUST_LOG=info - -NEAR_BOOT_NODES=$BOOTNODES - -# Port forwarding -PORTS=8080:8080,24567:24567,13002:13002,80:80 \ No newline at end of file diff --git a/deployment/testnet/sam.toml b/deployment/testnet/sam.toml index 5901e6328..01fc88d7d 100644 --- a/deployment/testnet/sam.toml +++ b/deployment/testnet/sam.toml @@ -20,6 +20,8 @@ home_dir = "/data" [mpc_config.near_init] chain_id = "testnet" boot_nodes = "" +download_genesis = true +download_config = "rpc" [mpc_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" @@ -31,10 +33,11 @@ type = "local" [mpc_config.node] my_near_account_id = "$SAM_ACCOUNT" near_responder_account_id = "$SAM_ACCOUNT" -number_of_responder_keys = 1 +number_of_responder_keys = 50 web_ui = "0.0.0.0:8080" migration_web_ui = "0.0.0.0:8078" -cores = 4 +pprof_bind_address = "0.0.0.0:34001" +cores = 12 [mpc_config.node.indexer] validate_genesis = false @@ -42,16 +45,17 @@ sync_mode = "Latest" concurrency = 1 mpc_contract_id = "$MPC_CONTRACT_ACCOUNT" finality = "optimistic" +port_override = 80 [mpc_config.node.triple] concurrency = 2 -desired_triples_to_buffer = 128 +desired_triples_to_buffer = 1000000 timeout_sec = 60 parallel_triple_generation_stagger_time_sec = 1 [mpc_config.node.presignature] -concurrency = 4 -desired_presignatures_to_buffer = 64 +concurrency = 16 +desired_presignatures_to_buffer = 8192 timeout_sec = 60 [mpc_config.node.signature] From 67ed302d7e485eb47ee9ef2adc7ca6dbfbd182fb Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 13:15:03 +0100 Subject: [PATCH 137/176] remove OOM comment --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe0eff55a..f58d0cc89 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,7 +47,7 @@ jobs: launcher-nontee-check: name: "MPC Launcher non-tee check" - runs-on: warp-ubuntu-2404-x64-8x # was OOMing with 2x + runs-on: warp-ubuntu-2404-x64-8x timeout-minutes: 60 permissions: contents: read From 62b31a675ca9f240c1503531a00eea47c62749eb Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 13:16:19 +0100 Subject: [PATCH 138/176] no pub in tee-launcher --- crates/tee-launcher/src/docker_types.rs | 22 +++++------ crates/tee-launcher/src/error.rs | 4 +- crates/tee-launcher/src/types.rs | 50 ++++++++++++------------- 3 files changed, 38 insertions(+), 38 deletions(-) diff --git a/crates/tee-launcher/src/docker_types.rs b/crates/tee-launcher/src/docker_types.rs index 6c3bf27ce..ac9321b3e 100644 --- a/crates/tee-launcher/src/docker_types.rs +++ b/crates/tee-launcher/src/docker_types.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; /// Partial response #[derive(Debug, Deserialize, Serialize)] -pub struct DockerTokenResponse { - pub token: String, +pub(crate) struct DockerTokenResponse { + pub(crate) token: String, } /// Response from `GET /v2/{name}/manifests/{reference}`. @@ -14,7 +14,7 @@ pub struct DockerTokenResponse { /// - Docker V2 / OCI manifest → single-platform manifest with a config digest #[derive(Debug, Deserialize, Serialize)] #[serde(tag = "mediaType")] -pub enum ManifestResponse { +pub(crate) enum ManifestResponse { /// Multi-platform manifest (OCI image index). #[serde(rename = "application/vnd.oci.image.index.v1+json")] ImageIndex { manifests: Vec }, @@ -29,20 +29,20 @@ pub enum ManifestResponse { } #[derive(Debug, Deserialize, Serialize)] -pub struct ManifestEntry { - pub digest: String, - pub platform: ManifestPlatform, +pub(crate) struct ManifestEntry { + pub(crate) digest: String, + pub(crate) platform: ManifestPlatform, } #[derive(Debug, Deserialize, Serialize, PartialEq, Eq)] -pub struct ManifestPlatform { - pub architecture: String, - pub os: String, +pub(crate) struct ManifestPlatform { + pub(crate) architecture: String, + pub(crate) os: String, } #[derive(Debug, Deserialize, Serialize)] -pub struct ManifestConfig { - pub digest: DockerSha256Digest, +pub(crate) struct ManifestConfig { + pub(crate) digest: DockerSha256Digest, } #[cfg(test)] diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 03295a6c8..229fa5940 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -2,7 +2,7 @@ use launcher_interface::types::DockerSha256Digest; use thiserror::Error; #[derive(Error, Debug)] -pub enum LauncherError { +pub(crate) enum LauncherError { #[error("EmitEvent failed while extending RTMR3: {0}")] DstackEmitEventFailed(String), @@ -68,7 +68,7 @@ pub enum LauncherError { } #[derive(Error, Debug)] -pub enum ImageDigestValidationFailed { +pub(crate) enum ImageDigestValidationFailed { #[error("manifest digest lookup failed: {0}")] ManifestDigestLookupFailed(String), #[error("docker pull failed for {0}")] diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index c85480eb3..34deb7966 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -11,10 +11,10 @@ use serde::{Deserialize, Serialize}; /// CLI arguments parsed from environment variables via clap. #[derive(Parser, Debug)] #[command(name = "tee-launcher")] -pub struct CliArgs { +pub(crate) struct CliArgs { /// Platform mode: TEE or NONTEE #[arg(long, env = "PLATFORM")] - pub platform: Platform, + pub(crate) platform: Platform, #[arg(long, env = "DOCKER_CONTENT_TRUST")] // ensure that `docker_content_trust` is enabled. @@ -22,7 +22,7 @@ pub struct CliArgs { /// Fallback image digest when the approved-hashes file is absent #[arg(long, env = "DEFAULT_IMAGE_DIGEST")] - pub default_image_digest: DockerSha256Digest, + pub(crate) default_image_digest: DockerSha256Digest, } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] @@ -32,7 +32,7 @@ enum DockerContentTrust { } #[derive(Debug, Clone, Copy, PartialEq, Eq, ValueEnum)] -pub enum Platform { +pub(crate) enum Platform { #[value(name = "TEE")] Tee, #[value(name = "NONTEE")] @@ -41,60 +41,60 @@ pub enum Platform { /// Typed representation of the dstack user config file (`/tapp/user_config`). #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct Config { - pub launcher_config: LauncherConfig, - pub docker_command_config: DockerLaunchFlags, +pub(crate) struct Config { + pub(crate) launcher_config: LauncherConfig, + pub(crate) docker_command_config: DockerLaunchFlags, /// Opaque MPC node configuration table. /// The launcher does not interpret these fields — they are re-serialized /// to a TOML string, written to a file on disk, and mounted into the /// container for the MPC binary to consume via `start-with-config-file`. - pub mpc_config: toml::Table, + pub(crate) mpc_config: toml::Table, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LauncherConfig { +pub(crate) struct LauncherConfig { /// Docker image tags to search (from `MPC_IMAGE_TAGS`, comma-separated). - pub image_tags: NonEmptyVec, + pub(crate) image_tags: NonEmptyVec, /// Docker image name (from `MPC_IMAGE_NAME`). - pub image_name: String, + pub(crate) image_name: String, /// Docker registry (from `MPC_REGISTRY`). - pub registry: String, + pub(crate) registry: String, /// Per-request timeout for registry RPC calls (from `RPC_REQUEST_TIMEOUT_SECS`). - pub rpc_request_timeout_secs: u64, + pub(crate) rpc_request_timeout_secs: u64, /// Delay between registry RPC retries (from `RPC_REQUEST_INTERVAL_SECS`). - pub rpc_request_interval_secs: u64, + pub(crate) rpc_request_interval_secs: u64, /// Maximum registry RPC attempts (from `RPC_MAX_ATTEMPTS`). - pub rpc_max_attempts: u32, + pub(crate) rpc_max_attempts: u32, /// Optional hash override that bypasses registry lookup (from `MPC_HASH_OVERRIDE`). - pub mpc_hash_override: Option, + pub(crate) mpc_hash_override: Option, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DockerLaunchFlags { - pub port_mappings: PortMappings, +pub(crate) struct DockerLaunchFlags { + pub(crate) port_mappings: PortMappings, } /// A `--add-host` entry: `hostname:IPv4`. #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct HostEntry { - pub hostname: Host, - pub ip: Ipv4Addr, +pub(crate) struct HostEntry { + pub(crate) hostname: Host, + pub(crate) ip: Ipv4Addr, } #[derive(Debug, Clone, Serialize, Deserialize)] -pub struct PortMappings { - pub ports: Vec, +pub(crate) struct PortMappings { + pub(crate) ports: Vec, } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -pub struct PortMapping { +pub(crate) struct PortMapping { pub(crate) src: NonZeroU16, pub(crate) dst: NonZeroU16, } impl PortMapping { /// Returns e.g. `"11780:11780"` for use in docker-compose port lists. - pub fn docker_compose_value(&self) -> String { + pub(crate) fn docker_compose_value(&self) -> String { format!("{}:{}", self.src, self.dst) } } From 8ccfccb1e80e5b6fcfb13e4ed0ea5fb9dbfe2e7f Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 13:22:41 +0100 Subject: [PATCH 139/176] add issues to todos --- crates/mpc-attestation/src/attestation.rs | 1 + crates/tee-launcher/src/main.rs | 5 ++--- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/crates/mpc-attestation/src/attestation.rs b/crates/mpc-attestation/src/attestation.rs index b1fec58ed..2f616add9 100644 --- a/crates/mpc-attestation/src/attestation.rs +++ b/crates/mpc-attestation/src/attestation.rs @@ -141,6 +141,7 @@ impl Attestation { .get_single_event(MPC_IMAGE_HASH_EVENT)? .event_payload; + // TODO(#2478): decode raw bytes let mpc_image_hash_bytes: Vec = hex::decode(mpc_image_hash_payload) .map_err(|err| { VerificationError::Custom(format!( diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 955c99bf6..9e63b4223 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -112,8 +112,7 @@ async fn run() -> Result<(), LauncherError> { dstack_client .emit_event( MPC_IMAGE_HASH_EVENT.to_string(), - // TODO: mpc binary has to go back from back hex as well. Just send the raw bytes as payload. - image_hash.as_hex().as_bytes().to_vec(), + image_hash.as_ref().to_vec(), ) .await .map_err(|e| LauncherError::DstackEmitEventFailed(e.to_string()))?; @@ -179,7 +178,7 @@ async fn get_manifest_digest( let mut tags: VecDeque = config.image_tags.iter().cloned().collect(); // We need an authorization token to fetch manifests. - // TODO: this still has the registry hard-coded in the url. also, if we use a different registry, we need a different auth-endpoint + // TODO(#2479): this still has the registry hard-coded in the url. also, if we use a different registry, we need a different auth-endpoint let token_url = format!( "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{}:pull", config.image_name From 9109c580e38f6330009c319b062ea7cbf22dfc3f Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 13:23:40 +0100 Subject: [PATCH 140/176] remove spacing in comment --- crates/tee-launcher/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 9e63b4223..cbe195783 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -336,7 +336,7 @@ async fn get_manifest_digest( } /// Returns if the given image digest is valid (pull + manifest + digest match). -/// Does NOT extend RTMR3 and does NOT run the container. +/// Does NOT extend RTMR3 and does NOT run the container. async fn validate_image_hash( launcher_config: &LauncherConfig, image_hash: DockerSha256Digest, From 2dae82ebc5e331a6ddf832be73ed23e68e84ff01 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 13:34:27 +0100 Subject: [PATCH 141/176] inline portmappngs --- crates/tee-launcher/README.md | 8 +-- crates/tee-launcher/src/main.rs | 81 ++++++++++++++---------------- crates/tee-launcher/src/types.rs | 21 ++------ deployment/localnet/tee/frodo.toml | 4 +- deployment/localnet/tee/sam.toml | 4 +- deployment/testnet/frodo.toml | 4 +- deployment/testnet/sam.toml | 4 +- tee_launcher/user-config.toml | 4 +- 8 files changed, 50 insertions(+), 80 deletions(-) diff --git a/crates/tee-launcher/README.md b/crates/tee-launcher/README.md index 177387f09..e157484f1 100644 --- a/crates/tee-launcher/README.md +++ b/crates/tee-launcher/README.md @@ -37,9 +37,7 @@ rpc_request_interval_secs = 1 rpc_max_attempts = 20 # Optional: force selection of a specific digest (must be in approved list) # mpc_hash_override = "sha256:abcd..." - -[docker_command_config.port_mappings] -ports = [ +port_mappings = [ { src = 11780, dst = 11780 }, { src = 2200, dst = 2200 }, ] @@ -64,9 +62,7 @@ ports = [ | `rpc_max_attempts` | `20` | Maximum registry API retry attempts | | `mpc_hash_override` | (none) | Optional: force a specific `sha256:` digest (must appear in approved list) | -### `[docker_command_config.port_mappings]` - -Port mappings forwarded to the MPC container. +| `port_mappings` | `[]` | Port mappings forwarded to the MPC container (`{ src, dst }` pairs) | ### `[mpc_config]` diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index cbe195783..44789f7d2 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -61,7 +61,7 @@ async fn run() -> Result<(), LauncherError> { } })?; - let dstack_config: Config = + let config: Config = toml::from_str(&config_contents).map_err(|source| LauncherError::TomlParse { path: DSTACK_USER_CONFIG_FILE.to_string(), source, @@ -97,11 +97,10 @@ async fn run() -> Result<(), LauncherError> { let image_hash = select_image_hash( approved_hashes_on_disk.as_ref(), &args.default_image_digest, - dstack_config.launcher_config.mpc_hash_override.as_ref(), + config.launcher_config.mpc_hash_override.as_ref(), )?; - let manifest_digest = - validate_image_hash(&dstack_config.launcher_config, image_hash.clone()).await?; + let manifest_digest = validate_image_hash(&config.launcher_config, image_hash.clone()).await?; let should_extend_rtmr_3 = args.platform == Platform::Tee; @@ -119,8 +118,8 @@ async fn run() -> Result<(), LauncherError> { } let mpc_binary_config_path = std::path::Path::new(MPC_CONFIG_SHARED_PATH); - let mpc_config_toml = toml::to_string(&dstack_config.mpc_config) - .expect("re-serializing a toml::Table always succeeds"); + let mpc_config_toml = + toml::to_string(&config.mpc_config).expect("re-serializing a toml::Table always succeeds"); std::fs::write(mpc_binary_config_path, mpc_config_toml.as_bytes()).map_err(|source| { LauncherError::FileWrite { path: mpc_binary_config_path.display().to_string(), @@ -131,8 +130,8 @@ async fn run() -> Result<(), LauncherError> { launch_mpc_container( args.platform, &manifest_digest, - &dstack_config.launcher_config.image_name, - &dstack_config.docker_command_config, + &config.launcher_config.image_name, + &config.launcher_config.port_mappings, )?; Ok(()) @@ -400,7 +399,7 @@ async fn validate_image_hash( fn render_compose_file( platform: Platform, - docker_flags: &DockerLaunchFlags, + port_mappings: &[PortMapping], image_name: &str, manifest_digest: &DockerSha256Digest, ) -> Result { @@ -409,9 +408,7 @@ fn render_compose_file( Platform::NonTee => COMPOSE_TEMPLATE, }; - let ports: Vec = docker_flags - .port_mappings - .ports + let ports: Vec = port_mappings .iter() .map(PortMapping::docker_compose_value) .collect(); @@ -441,11 +438,11 @@ fn launch_mpc_container( platform: Platform, manifest_digest: &DockerSha256Digest, image_name: &str, - docker_flags: &DockerLaunchFlags, + port_mappings: &[PortMapping], ) -> Result<(), LauncherError> { tracing::info!(?manifest_digest, "launching MPC node"); - let compose_file = render_compose_file(platform, docker_flags, image_name, manifest_digest)?; + let compose_file = render_compose_file(platform, port_mappings, image_name, manifest_digest)?; let compose_path = compose_file.path().display().to_string(); // Remove any existing container from a previous run (by name, independent of compose file) @@ -477,6 +474,8 @@ fn launch_mpc_container( #[cfg(test)] mod tests { + use std::num::NonZeroU16; + use assert_matches::assert_matches; use launcher_interface::types::{ApprovedHashes, DockerSha256Digest}; use near_mpc_bounded_collections::NonEmptyVec; @@ -491,10 +490,10 @@ mod tests { fn render( platform: Platform, - flags: &DockerLaunchFlags, + port_mappings: &[PortMapping], digest: &DockerSha256Digest, ) -> String { - let file = render_compose_file(platform, flags, SAMPLE_IMAGE_NAME, digest).unwrap(); + let file = render_compose_file(platform, port_mappings, SAMPLE_IMAGE_NAME, digest).unwrap(); std::fs::read_to_string(file.path()).unwrap() } @@ -517,28 +516,25 @@ mod tests { } } - fn empty_docker_flags() -> DockerLaunchFlags { - serde_json::from_value(serde_json::json!({ - "port_mappings": {"ports": []} - })) - .unwrap() + fn empty_port_mappings() -> Vec { + vec![] } - fn docker_flags_with_port() -> DockerLaunchFlags { - serde_json::from_value(serde_json::json!({ - "port_mappings": {"ports": [{"src": 11780, "dst": 11780}]} - })) - .unwrap() + fn port_mappings_with_port() -> Vec { + vec![PortMapping { + src: NonZeroU16::new(11780).unwrap(), + dst: NonZeroU16::new(11780).unwrap(), + }] } #[test] fn tee_mode_includes_dstack_env_and_volume() { // given - let flags = empty_docker_flags(); + let port_mappings = empty_port_mappings(); let digest = sample_digest(); // when - let rendered = render(Platform::Tee, &flags, &digest); + let rendered = render(Platform::Tee, &port_mappings, &digest); // then assert!(rendered.contains(&format!("DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"))); @@ -548,11 +544,11 @@ mod tests { #[test] fn nontee_mode_excludes_dstack() { // given - let flags = empty_docker_flags(); + let port_mappings = empty_port_mappings(); let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, &flags, &digest); + let rendered = render(Platform::NonTee, &port_mappings, &digest); // then assert!(!rendered.contains("DSTACK_ENDPOINT")); @@ -562,11 +558,11 @@ mod tests { #[test] fn includes_security_opts_and_required_volumes() { // given - let flags = empty_docker_flags(); + let port_mappings = empty_port_mappings(); let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, &flags, &digest); + let rendered = render(Platform::NonTee, &port_mappings, &digest); // then assert!(rendered.contains("no-new-privileges:true")); @@ -579,11 +575,11 @@ mod tests { #[test] fn mounts_config_file_read_only() { // given - let flags = empty_docker_flags(); + let port_mappings = empty_port_mappings(); let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, &flags, &digest); + let rendered = render(Platform::NonTee, &port_mappings, &digest); // then — config is on the shared volume, referenced in the command assert!(rendered.contains(MPC_CONFIG_SHARED_PATH)); @@ -592,11 +588,11 @@ mod tests { #[test] fn includes_start_with_config_file_command() { // given - let flags = empty_docker_flags(); + let port_mappings = empty_port_mappings(); let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, &flags, &digest); + let rendered = render(Platform::NonTee, &port_mappings, &digest); // then assert!(rendered.contains("/app/mpc-node")); @@ -606,11 +602,11 @@ mod tests { #[test] fn image_is_set() { // given - let flags = empty_docker_flags(); + let port_mappings = empty_port_mappings(); let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, &flags, &digest); + let rendered = render(Platform::NonTee, &port_mappings, &digest); // then assert!(rendered.contains(&format!("image: \"{SAMPLE_IMAGE_NAME}@{digest}\""))); @@ -619,11 +615,11 @@ mod tests { #[test] fn includes_ports() { // given - let flags = docker_flags_with_port(); + let port_mappings = port_mappings_with_port(); let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, &flags, &digest); + let rendered = render(Platform::NonTee, &port_mappings, &digest); // then assert!(rendered.contains("11780:11780")); @@ -632,11 +628,11 @@ mod tests { #[test] fn no_env_section_in_nontee_mode() { // given - let flags = empty_docker_flags(); + let port_mappings = empty_port_mappings(); let digest = sample_digest(); // when - let rendered = render(Platform::NonTee, &flags, &digest); + let rendered = render(Platform::NonTee, &port_mappings, &digest); // then assert!(!rendered.contains("environment:")); @@ -754,6 +750,7 @@ mod integration_tests { rpc_request_interval_secs: 1, rpc_max_attempts: 20, mpc_hash_override: None, + port_mappings: vec![], } } diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 34deb7966..36380a732 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -43,7 +43,6 @@ pub(crate) enum Platform { #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct Config { pub(crate) launcher_config: LauncherConfig, - pub(crate) docker_command_config: DockerLaunchFlags, /// Opaque MPC node configuration table. /// The launcher does not interpret these fields — they are re-serialized /// to a TOML string, written to a file on disk, and mounted into the @@ -67,11 +66,7 @@ pub(crate) struct LauncherConfig { pub(crate) rpc_max_attempts: u32, /// Optional hash override that bypasses registry lookup (from `MPC_HASH_OVERRIDE`). pub(crate) mpc_hash_override: Option, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct DockerLaunchFlags { - pub(crate) port_mappings: PortMappings, + pub(crate) port_mappings: Vec, } /// A `--add-host` entry: `hostname:IPv4`. @@ -81,11 +76,6 @@ pub(crate) struct HostEntry { pub(crate) ip: Ipv4Addr, } -#[derive(Debug, Clone, Serialize, Deserialize)] -pub(crate) struct PortMappings { - pub(crate) ports: Vec, -} - #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct PortMapping { pub(crate) src: NonZeroU16, @@ -228,8 +218,7 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 -[docker_command_config.port_mappings] -ports = [{ src = 11780, dst = 11780 }] +port_mappings = [{ src = 11780, dst = 11780 }] [mpc_config] home_dir = "/data" @@ -259,8 +248,7 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 -[docker_command_config.port_mappings] -ports = [{ src = 11780, dst = 11780 }] +port_mappings = [{ src = 11780, dst = 11780 }] [mpc_config] home_dir = "/data" @@ -288,8 +276,7 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 -[docker_command_config.port_mappings] -ports = [] +port_mappings = [] "#; // when diff --git a/deployment/localnet/tee/frodo.toml b/deployment/localnet/tee/frodo.toml index cf9a8ebde..c9203ff13 100644 --- a/deployment/localnet/tee/frodo.toml +++ b/deployment/localnet/tee/frodo.toml @@ -5,9 +5,7 @@ registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 - -[docker_command_config.port_mappings] -ports = [ +port_mappings = [ { src = 8080, dst = 8080 }, { src = 24566, dst = 24566 }, { src = 13001, dst = 13001 }, diff --git a/deployment/localnet/tee/sam.toml b/deployment/localnet/tee/sam.toml index c4e16983c..f8857d089 100644 --- a/deployment/localnet/tee/sam.toml +++ b/deployment/localnet/tee/sam.toml @@ -5,9 +5,7 @@ registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 - -[docker_command_config.port_mappings] -ports = [ +port_mappings = [ { src = 8080, dst = 8080 }, { src = 24566, dst = 24566 }, { src = 13002, dst = 13002 }, diff --git a/deployment/testnet/frodo.toml b/deployment/testnet/frodo.toml index d9241c86e..18a8d6f9c 100644 --- a/deployment/testnet/frodo.toml +++ b/deployment/testnet/frodo.toml @@ -5,9 +5,7 @@ registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 - -[docker_command_config.port_mappings] -ports = [ +port_mappings = [ { src = 8080, dst = 8080 }, { src = 24567, dst = 24567 }, { src = 13001, dst = 13001 }, diff --git a/deployment/testnet/sam.toml b/deployment/testnet/sam.toml index 01fc88d7d..ac587eb1f 100644 --- a/deployment/testnet/sam.toml +++ b/deployment/testnet/sam.toml @@ -5,9 +5,7 @@ registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 - -[docker_command_config.port_mappings] -ports = [ +port_mappings = [ { src = 8080, dst = 8080 }, { src = 24567, dst = 24567 }, { src = 13002, dst = 13002 }, diff --git a/tee_launcher/user-config.toml b/tee_launcher/user-config.toml index c8583d282..0b0692cff 100644 --- a/tee_launcher/user-config.toml +++ b/tee_launcher/user-config.toml @@ -5,9 +5,7 @@ registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 - -[docker_command_config.port_mappings] -ports = [ +port_mappings = [ { src = 8080, dst = 8080 }, { src = 3030, dst = 3030 }, { src = 80, dst = 80 }, From 820cab0b9a012a34fa4604103e795b9a69eff1e2 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 13:36:14 +0100 Subject: [PATCH 142/176] use host/container --- crates/tee-launcher/README.md | 6 +++--- crates/tee-launcher/src/main.rs | 4 ++-- crates/tee-launcher/src/types.rs | 20 ++++++++++---------- deployment/localnet/tee/frodo.toml | 6 +++--- deployment/localnet/tee/sam.toml | 6 +++--- deployment/testnet/frodo.toml | 8 ++++---- deployment/testnet/sam.toml | 8 ++++---- tee_launcher/user-config.toml | 8 ++++---- 8 files changed, 33 insertions(+), 33 deletions(-) diff --git a/crates/tee-launcher/README.md b/crates/tee-launcher/README.md index e157484f1..425d1fd0d 100644 --- a/crates/tee-launcher/README.md +++ b/crates/tee-launcher/README.md @@ -38,8 +38,8 @@ rpc_max_attempts = 20 # Optional: force selection of a specific digest (must be in approved list) # mpc_hash_override = "sha256:abcd..." port_mappings = [ - { src = 11780, dst = 11780 }, - { src = 2200, dst = 2200 }, + { host = 11780, container = 11780 }, + { host = 2200, container = 2200 }, ] # Opaque MPC node configuration. @@ -62,7 +62,7 @@ port_mappings = [ | `rpc_max_attempts` | `20` | Maximum registry API retry attempts | | `mpc_hash_override` | (none) | Optional: force a specific `sha256:` digest (must appear in approved list) | -| `port_mappings` | `[]` | Port mappings forwarded to the MPC container (`{ src, dst }` pairs) | +| `port_mappings` | `[]` | Port mappings forwarded to the MPC container (`{ host, container }` pairs) | ### `[mpc_config]` diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 44789f7d2..def49fa58 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -522,8 +522,8 @@ mod tests { fn port_mappings_with_port() -> Vec { vec![PortMapping { - src: NonZeroU16::new(11780).unwrap(), - dst: NonZeroU16::new(11780).unwrap(), + host: NonZeroU16::new(11780).unwrap(), + container: NonZeroU16::new(11780).unwrap(), }] } diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 36380a732..0beddf549 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -78,14 +78,14 @@ pub(crate) struct HostEntry { #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] pub(crate) struct PortMapping { - pub(crate) src: NonZeroU16, - pub(crate) dst: NonZeroU16, + pub(crate) host: NonZeroU16, + pub(crate) container: NonZeroU16, } impl PortMapping { /// Returns e.g. `"11780:11780"` for use in docker-compose port lists. pub(crate) fn docker_compose_value(&self) -> String { - format!("{}:{}", self.src, self.dst) + format!("{}:{}", self.host, self.container) } } @@ -154,7 +154,7 @@ mod tests { #[test] fn port_mapping_valid_deserialization() { // given - let json = serde_json::json!({"src": 11780, "dst": 11780}); + let json = serde_json::json!({"host": 11780, "container": 11780}); // when let result = serde_json::from_value::(json); @@ -166,7 +166,7 @@ mod tests { #[test] fn port_mapping_rejects_zero_port() { // given - let json = serde_json::json!({"src": 0, "dst": 11780}); + let json = serde_json::json!({"host": 0, "container": 11780}); // when let result = serde_json::from_value::(json); @@ -178,7 +178,7 @@ mod tests { #[test] fn port_mapping_rejects_out_of_range_port() { // given - let json = serde_json::json!({"src": 65536, "dst": 11780}); + let json = serde_json::json!({"host": 65536, "container": 11780}); // when let result = serde_json::from_value::(json); @@ -193,8 +193,8 @@ mod tests { fn port_mapping_docker_compose_value() { // given let mapping = PortMapping { - src: NonZeroU16::new(11780).unwrap(), - dst: NonZeroU16::new(11780).unwrap(), + host: NonZeroU16::new(11780).unwrap(), + container: NonZeroU16::new(11780).unwrap(), }; // when @@ -218,7 +218,7 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 -port_mappings = [{ src = 11780, dst = 11780 }] +port_mappings = [{ host = 11780, container = 11780 }] [mpc_config] home_dir = "/data" @@ -248,7 +248,7 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 -port_mappings = [{ src = 11780, dst = 11780 }] +port_mappings = [{ host = 11780, container = 11780 }] [mpc_config] home_dir = "/data" diff --git a/deployment/localnet/tee/frodo.toml b/deployment/localnet/tee/frodo.toml index c9203ff13..f66150cbe 100644 --- a/deployment/localnet/tee/frodo.toml +++ b/deployment/localnet/tee/frodo.toml @@ -6,9 +6,9 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 port_mappings = [ - { src = 8080, dst = 8080 }, - { src = 24566, dst = 24566 }, - { src = 13001, dst = 13001 }, + { host =8080, container =8080 }, + { host =24566, container =24566 }, + { host =13001, container =13001 }, ] [mpc_config] diff --git a/deployment/localnet/tee/sam.toml b/deployment/localnet/tee/sam.toml index f8857d089..8c9c1a8c4 100644 --- a/deployment/localnet/tee/sam.toml +++ b/deployment/localnet/tee/sam.toml @@ -6,9 +6,9 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 port_mappings = [ - { src = 8080, dst = 8080 }, - { src = 24566, dst = 24566 }, - { src = 13002, dst = 13002 }, + { host =8080, container =8080 }, + { host =24566, container =24566 }, + { host =13002, container =13002 }, ] [mpc_config] diff --git a/deployment/testnet/frodo.toml b/deployment/testnet/frodo.toml index 18a8d6f9c..fdfd18d61 100644 --- a/deployment/testnet/frodo.toml +++ b/deployment/testnet/frodo.toml @@ -6,10 +6,10 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 port_mappings = [ - { src = 8080, dst = 8080 }, - { src = 24567, dst = 24567 }, - { src = 13001, dst = 13001 }, - { src = 80, dst = 80 }, + { host =8080, container =8080 }, + { host =24567, container =24567 }, + { host =13001, container =13001 }, + { host =80, container =80 }, ] [mpc_config] diff --git a/deployment/testnet/sam.toml b/deployment/testnet/sam.toml index ac587eb1f..587b9b101 100644 --- a/deployment/testnet/sam.toml +++ b/deployment/testnet/sam.toml @@ -6,10 +6,10 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 port_mappings = [ - { src = 8080, dst = 8080 }, - { src = 24567, dst = 24567 }, - { src = 13002, dst = 13002 }, - { src = 80, dst = 80 }, + { host =8080, container =8080 }, + { host =24567, container =24567 }, + { host =13002, container =13002 }, + { host =80, container =80 }, ] [mpc_config] diff --git a/tee_launcher/user-config.toml b/tee_launcher/user-config.toml index 0b0692cff..4bffc67b4 100644 --- a/tee_launcher/user-config.toml +++ b/tee_launcher/user-config.toml @@ -6,10 +6,10 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 port_mappings = [ - { src = 8080, dst = 8080 }, - { src = 3030, dst = 3030 }, - { src = 80, dst = 80 }, - { src = 24567, dst = 24567 }, + { host =8080, container =8080 }, + { host =3030, container =3030 }, + { host =80, container =80 }, + { host =24567, container =24567 }, ] [mpc_config] From faba6a2ba96659932ca0ba4b562e09e6b8f0689a Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 13:57:45 +0100 Subject: [PATCH 143/176] use httpmock to test get_manifest_digest --- Cargo.lock | 1 + crates/tee-launcher/Cargo.toml | 1 + crates/tee-launcher/src/main.rs | 287 +++++++++++++++++++++++++++++--- 3 files changed, 269 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index a24eef4ec..694217a31 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10664,6 +10664,7 @@ dependencies = [ "backon", "clap", "dstack-sdk", + "httpmock", "launcher-interface", "near-mpc-bounded-collections", "reqwest 0.12.28", diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index 8aa46ef75..e18667b89 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -31,6 +31,7 @@ url = { workspace = true, features = ["serde"] } [dev-dependencies] assert_matches = { workspace = true } +httpmock = { workspace = true } [lints] workspace = true diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index def49fa58..d0e42f029 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -170,21 +170,54 @@ fn select_image_hash( Ok(selected) } +/// Provides the URLs needed to interact with a container registry. +trait RegistryInfo { + fn token_url(&self, image_name: &str) -> String; + fn manifest_url(&self, image_name: &str, tag: &str) -> Result; +} + +/// Production registry info for Docker Hub. +struct DockerRegistry { + registry_base_url: String, +} + +impl DockerRegistry { + fn new(config: &LauncherConfig) -> Self { + Self { + registry_base_url: format!("https://{}", config.registry), + } + } +} + +impl RegistryInfo for DockerRegistry { + // TODO(#2479): if we use a different registry, we need a different auth-endpoint + fn token_url(&self, image_name: &str) -> String { + format!( + "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{image_name}:pull", + ) + } + + fn manifest_url(&self, image_name: &str, tag: &str) -> Result { + let url_string = format!("{}/v2/{image_name}/manifests/{tag}", self.registry_base_url); + + url_string + .parse() + .map_err(|_| LauncherError::InvalidManifestUrl(url_string)) + } +} + async fn get_manifest_digest( + registry: &dyn RegistryInfo, config: &LauncherConfig, expected_image_digest: &DockerSha256Digest, ) -> Result { let mut tags: VecDeque = config.image_tags.iter().cloned().collect(); - // We need an authorization token to fetch manifests. - // TODO(#2479): this still has the registry hard-coded in the url. also, if we use a different registry, we need a different auth-endpoint - let token_url = format!( - "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{}:pull", - config.image_name - ); - let reqwest_client = reqwest::Client::new(); + // We need an authorization token to fetch manifests. + let token_url = registry.token_url(&config.image_name); + let token_request_response = reqwest_client .get(token_url) .send() @@ -204,17 +237,7 @@ async fn get_manifest_digest( .map_err(|e| LauncherError::RegistryAuthFailed(e.to_string()))?; while let Some(tag) = tags.pop_front() { - let manifest_url: Url = format!( - "https://{}/v2/{}/manifests/{tag}", - config.registry, config.image_name - ) - .parse() - .map_err(|_| { - LauncherError::InvalidManifestUrl(format!( - "https://{}/v2/{}/manifests/{tag}", - config.registry, config.image_name - )) - })?; + let manifest_url = registry.manifest_url(&config.image_name, &tag)?; let authorization_value: HeaderValue = format!("Bearer {}", token_response.token) .parse() @@ -340,7 +363,8 @@ async fn validate_image_hash( launcher_config: &LauncherConfig, image_hash: DockerSha256Digest, ) -> Result { - let manifest_digest = get_manifest_digest(launcher_config, &image_hash) + let registry = DockerRegistry::new(launcher_config); + let manifest_digest = get_manifest_digest(®istry, launcher_config, &image_hash) .await .map_err(|e| ImageDigestValidationFailed::ManifestDigestLookupFailed(e.to_string()))?; let image_name = &launcher_config.image_name; @@ -477,14 +501,17 @@ mod tests { use std::num::NonZeroU16; use assert_matches::assert_matches; + use httpmock::prelude::*; use launcher_interface::types::{ApprovedHashes, DockerSha256Digest}; use near_mpc_bounded_collections::NonEmptyVec; use crate::constants::*; use crate::error::LauncherError; + use crate::get_manifest_digest; use crate::render_compose_file; use crate::select_image_hash; use crate::types::*; + use crate::RegistryInfo; const SAMPLE_IMAGE_NAME: &str = "nearone/mpc-node"; @@ -516,6 +543,46 @@ mod tests { } } + struct MockRegistry { + base_url: String, + } + + impl RegistryInfo for MockRegistry { + fn token_url(&self, _image_name: &str) -> String { + format!("{}/token", self.base_url) + } + + fn manifest_url( + &self, + image_name: &str, + tag: &str, + ) -> Result { + let raw = format!("{}/v2/{image_name}/manifests/{tag}", self.base_url); + raw.parse() + .map_err(|_| crate::error::LauncherError::InvalidManifestUrl(raw)) + } + } + + fn mock_launcher_config(tag: &str) -> LauncherConfig { + LauncherConfig { + image_tags: near_mpc_bounded_collections::NonEmptyVec::from_vec(vec![tag.into()]) + .unwrap(), + image_name: "test/image".into(), + registry: "unused".into(), + rpc_request_timeout_secs: 5, + rpc_request_interval_secs: 1, + rpc_max_attempts: 1, + mpc_hash_override: None, + port_mappings: vec![], + } + } + + fn mock_registry(server: &MockServer) -> MockRegistry { + MockRegistry { + base_url: server.base_url(), + } + } + fn empty_port_mappings() -> Vec { vec![] } @@ -725,6 +792,185 @@ mod tests { // then assert!(json.get("approved_hashes").is_some()); } + + // --- get_manifest_digest (mocked registry) --- + + #[tokio::test] + async fn get_manifest_digest_resolves_docker_v2() { + // given + let server = MockServer::start(); + let expected_image_digest = sample_digest(); + let manifest_digest = digest('b'); + + server.mock(|when, then| { + when.method(GET).path("/token"); + then.status(200) + .json_body(serde_json::json!({ "token": "test-token" })); + }); + + let manifest_body = serde_json::json!({ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { "digest": expected_image_digest.to_string() } + }); + + server.mock(|when, then| { + when.method(GET).path("/v2/test/image/manifests/v1.0"); + then.status(200) + .header("Docker-Content-Digest", &manifest_digest.to_string()) + .json_body(manifest_body); + }); + + let registry = mock_registry(&server); + let config = mock_launcher_config("v1.0"); + + // when + let result = get_manifest_digest(®istry, &config, &expected_image_digest).await; + + // then + assert_matches!(result, Ok(d) => { + assert_eq!(d, manifest_digest); + }); + } + + #[tokio::test] + async fn get_manifest_digest_follows_image_index_to_amd64_manifest() { + // given + let server = MockServer::start(); + let expected_image_digest = sample_digest(); + let manifest_digest = digest('c'); + let amd64_ref = "sha256:amd64ref"; + + server.mock(|when, then| { + when.method(GET).path("/token"); + then.status(200) + .json_body(serde_json::json!({ "token": "test-token" })); + }); + + // First request: image index pointing to amd64 manifest + server.mock(|when, then| { + when.method(GET).path("/v2/test/image/manifests/latest"); + then.status(200).json_body(serde_json::json!({ + "mediaType": "application/vnd.oci.image.index.v1+json", + "manifests": [ + { + "digest": amd64_ref, + "platform": { "architecture": "amd64", "os": "linux" } + }, + { + "digest": "sha256:armref", + "platform": { "architecture": "arm64", "os": "linux" } + } + ] + })); + }); + + // Second request: the resolved amd64 manifest + server.mock(|when, then| { + when.method(GET) + .path(format!("/v2/test/image/manifests/{amd64_ref}")); + then.status(200) + .header("Docker-Content-Digest", &manifest_digest.to_string()) + .json_body(serde_json::json!({ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { "digest": expected_image_digest.to_string() } + })); + }); + + let registry = mock_registry(&server); + let config = mock_launcher_config("latest"); + + // when + let result = get_manifest_digest(®istry, &config, &expected_image_digest).await; + + // then + assert_matches!(result, Ok(d) => { + assert_eq!(d, manifest_digest); + }); + } + + #[tokio::test] + async fn get_manifest_digest_skips_mismatched_config_digest() { + // given + let server = MockServer::start(); + let expected_image_digest = sample_digest(); + let wrong_digest = digest('f'); + + server.mock(|when, then| { + when.method(GET).path("/token"); + then.status(200) + .json_body(serde_json::json!({ "token": "test-token" })); + }); + + server.mock(|when, then| { + when.method(GET).path("/v2/test/image/manifests/v1.0"); + then.status(200) + .header("Docker-Content-Digest", "sha256:doesntmatter") + .json_body(serde_json::json!({ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { "digest": wrong_digest.to_string() } + })); + }); + + let registry = mock_registry(&server); + let config = mock_launcher_config("v1.0"); + + // when + let result = get_manifest_digest(®istry, &config, &expected_image_digest).await; + + // then + assert_matches!(result, Err(LauncherError::ImageHashNotFoundAmongTags)); + } + + #[tokio::test] + async fn get_manifest_digest_errors_on_auth_failure() { + // given + let server = MockServer::start(); + + server.mock(|when, then| { + when.method(GET).path("/token"); + then.status(403); + }); + + let registry = mock_registry(&server); + let config = mock_launcher_config("v1.0"); + + // when + let result = get_manifest_digest(®istry, &config, &sample_digest()).await; + + // then + assert_matches!(result, Err(LauncherError::RegistryAuthFailed(_))); + } + + #[tokio::test] + async fn get_manifest_digest_missing_content_digest_header_skips_tag() { + // given + let server = MockServer::start(); + let expected_image_digest = sample_digest(); + + server.mock(|when, then| { + when.method(GET).path("/token"); + then.status(200) + .json_body(serde_json::json!({ "token": "test-token" })); + }); + + // Manifest matches but no Docker-Content-Digest header + server.mock(|when, then| { + when.method(GET).path("/v2/test/image/manifests/v1.0"); + then.status(200).json_body(serde_json::json!({ + "mediaType": "application/vnd.docker.distribution.manifest.v2+json", + "config": { "digest": expected_image_digest.to_string() } + })); + }); + + let registry = mock_registry(&server); + let config = mock_launcher_config("v1.0"); + + // when + let result = get_manifest_digest(®istry, &config, &expected_image_digest).await; + + // then - tag is skipped, no more tags → error + assert_matches!(result, Err(LauncherError::ImageHashNotFoundAmongTags)); + } } /// Integration tests requiring network access and Docker Hub. @@ -761,7 +1007,8 @@ mod integration_tests { let expected_digest: DockerSha256Digest = TEST_DIGEST.parse().unwrap(); // when - let result = get_manifest_digest(&config, &expected_digest).await; + let registry = DockerRegistry::new(&config); + let result = get_manifest_digest(®istry, &config, &expected_digest).await; // then assert!(result.is_ok(), "get_manifest_digest failed: {result:?}"); From 21dea253fc1175c7f328c019c21dcc0e5d3ef5e1 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 14:02:53 +0100 Subject: [PATCH 144/176] use dockerregistry trait --- crates/tee-launcher/Cargo.toml | 2 +- crates/tee-launcher/src/main.rs | 44 +++++++++++++++++---------------- 2 files changed, 24 insertions(+), 22 deletions(-) diff --git a/crates/tee-launcher/Cargo.toml b/crates/tee-launcher/Cargo.toml index e18667b89..23ee283af 100644 --- a/crates/tee-launcher/Cargo.toml +++ b/crates/tee-launcher/Cargo.toml @@ -10,7 +10,7 @@ name = "tee-launcher" path = "src/main.rs" [features] -integration-test = [] +external-services-tests = [] [dependencies] backon = { workspace = true } diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index d0e42f029..70bfba3c7 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -172,33 +172,39 @@ fn select_image_hash( /// Provides the URLs needed to interact with a container registry. trait RegistryInfo { - fn token_url(&self, image_name: &str) -> String; - fn manifest_url(&self, image_name: &str, tag: &str) -> Result; + fn token_url(&self) -> String; + fn manifest_url(&self, tag: &str) -> Result; } /// Production registry info for Docker Hub. struct DockerRegistry { registry_base_url: String, + image_name: String, } impl DockerRegistry { fn new(config: &LauncherConfig) -> Self { Self { registry_base_url: format!("https://{}", config.registry), + image_name: config.image_name.clone(), } } } impl RegistryInfo for DockerRegistry { // TODO(#2479): if we use a different registry, we need a different auth-endpoint - fn token_url(&self, image_name: &str) -> String { + fn token_url(&self) -> String { format!( - "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{image_name}:pull", + "https://auth.docker.io/token?service=registry.docker.io&scope=repository:{}:pull", + self.image_name, ) } - fn manifest_url(&self, image_name: &str, tag: &str) -> Result { - let url_string = format!("{}/v2/{image_name}/manifests/{tag}", self.registry_base_url); + fn manifest_url(&self, tag: &str) -> Result { + let url_string = format!( + "{}/v2/{}/manifests/{tag}", + self.registry_base_url, self.image_name + ); url_string .parse() @@ -216,7 +222,7 @@ async fn get_manifest_digest( let reqwest_client = reqwest::Client::new(); // We need an authorization token to fetch manifests. - let token_url = registry.token_url(&config.image_name); + let token_url = registry.token_url(); let token_request_response = reqwest_client .get(token_url) @@ -237,7 +243,7 @@ async fn get_manifest_digest( .map_err(|e| LauncherError::RegistryAuthFailed(e.to_string()))?; while let Some(tag) = tags.pop_front() { - let manifest_url = registry.manifest_url(&config.image_name, &tag)?; + let manifest_url = registry.manifest_url(&tag)?; let authorization_value: HeaderValue = format!("Bearer {}", token_response.token) .parse() @@ -505,13 +511,13 @@ mod tests { use launcher_interface::types::{ApprovedHashes, DockerSha256Digest}; use near_mpc_bounded_collections::NonEmptyVec; + use crate::RegistryInfo; use crate::constants::*; use crate::error::LauncherError; use crate::get_manifest_digest; use crate::render_compose_file; use crate::select_image_hash; use crate::types::*; - use crate::RegistryInfo; const SAMPLE_IMAGE_NAME: &str = "nearone/mpc-node"; @@ -545,19 +551,16 @@ mod tests { struct MockRegistry { base_url: String, + image_name: String, } impl RegistryInfo for MockRegistry { - fn token_url(&self, _image_name: &str) -> String { + fn token_url(&self) -> String { format!("{}/token", self.base_url) } - fn manifest_url( - &self, - image_name: &str, - tag: &str, - ) -> Result { - let raw = format!("{}/v2/{image_name}/manifests/{tag}", self.base_url); + fn manifest_url(&self, tag: &str) -> Result { + let raw = format!("{}/v2/{}/manifests/{tag}", self.base_url, self.image_name); raw.parse() .map_err(|_| crate::error::LauncherError::InvalidManifestUrl(raw)) } @@ -580,6 +583,7 @@ mod tests { fn mock_registry(server: &MockServer) -> MockRegistry { MockRegistry { base_url: server.base_url(), + image_name: "test/image".into(), } } @@ -793,8 +797,6 @@ mod tests { assert!(json.get("approved_hashes").is_some()); } - // --- get_manifest_digest (mocked registry) --- - #[tokio::test] async fn get_manifest_digest_resolves_docker_v2() { // given @@ -816,7 +818,7 @@ mod tests { server.mock(|when, then| { when.method(GET).path("/v2/test/image/manifests/v1.0"); then.status(200) - .header("Docker-Content-Digest", &manifest_digest.to_string()) + .header("Docker-Content-Digest", manifest_digest.to_string()) .json_body(manifest_body); }); @@ -869,7 +871,7 @@ mod tests { when.method(GET) .path(format!("/v2/test/image/manifests/{amd64_ref}")); then.status(200) - .header("Docker-Content-Digest", &manifest_digest.to_string()) + .header("Docker-Content-Digest", manifest_digest.to_string()) .json_body(serde_json::json!({ "mediaType": "application/vnd.docker.distribution.manifest.v2+json", "config": { "digest": expected_image_digest.to_string() } @@ -975,7 +977,7 @@ mod tests { /// Integration tests requiring network access and Docker Hub. /// Run with: cargo test -p tee-launcher --features integration-test -#[cfg(all(test, feature = "integration-test"))] +#[cfg(all(test, feature = "external-services-tests"))] mod integration_tests { use super::*; use assert_matches::assert_matches; From e4e544564134eb7401742789e6d7230203703dec Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 14:11:41 +0100 Subject: [PATCH 145/176] use image_id to be explicit --- crates/tee-launcher/src/error.rs | 6 +++--- crates/tee-launcher/src/main.rs | 12 +++++++----- 2 files changed, 10 insertions(+), 8 deletions(-) diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 229fa5940..5f35773ac 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -76,10 +76,10 @@ pub(crate) enum ImageDigestValidationFailed { #[error("docker inspect failed for {0}")] DockerInspectFailed(String), #[error( - "pulled image has mismatching digest. pulled: {pulled_digest}, expected: {expected_digest}" + "pulled image has mismatching image ID. pulled: {pulled_image_id}, expected: {expected_image_id}" )] PulledImageHasMismatchedDigest { - expected_digest: DockerSha256Digest, - pulled_digest: DockerSha256Digest, + expected_image_id: DockerSha256Digest, + pulled_image_id: DockerSha256Digest, }, } diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 70bfba3c7..2879edd2a 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -390,7 +390,9 @@ async fn validate_image_hash( )); } - // Verify digest + // Verify that the pulled image ID matches the expected config digest. + // `docker inspect .ID` returns the image ID, which equals the config digest + // (i.e. the sha256 of the image config blob). let inspect = Command::new("docker") .args([ "image", @@ -409,17 +411,17 @@ async fn validate_image_hash( )); } - let pulled_digest = String::from_utf8_lossy(&inspect.stdout) + let pulled_image_id: DockerSha256Digest = String::from_utf8_lossy(&inspect.stdout) .trim() .to_string() .parse() .expect("is valid digest"); - if pulled_digest != image_hash { + if pulled_image_id != image_hash { return Err( ImageDigestValidationFailed::PulledImageHasMismatchedDigest { - pulled_digest, - expected_digest: image_hash, + pulled_image_id, + expected_image_id: image_hash, }, ); } From 13237ee3b50e8f7f4d4f591822be5bfe973d36f4 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 14:14:56 +0100 Subject: [PATCH 146/176] feature gate it to linux --- crates/tee-launcher/src/main.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 2879edd2a..25eb9ed80 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -977,8 +977,7 @@ mod tests { } } -/// Integration tests requiring network access and Docker Hub. -/// Run with: cargo test -p tee-launcher --features integration-test +/// Tests requiring network access and Docker Hub. #[cfg(all(test, feature = "external-services-tests"))] mod integration_tests { use super::*; @@ -1018,6 +1017,11 @@ mod integration_tests { assert!(result.is_ok(), "get_manifest_digest failed: {result:?}"); } + // `validate_image_hash` compares the output of `docker inspect .ID` against + // the expected config digest. On native Linux, `.ID` returns the config digest + // (sha256 of the image config blob), but on macOS, Docker Desktop's containerd + // image store returns the manifest digest instead, causing a spurious mismatch. + #[cfg(target_os = "linux")] #[tokio::test] async fn validate_image_hash_succeeds_for_known_image() { // given From a10b829a7c74c53a97ef983d5189421664a6c87e Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 15:42:18 +0100 Subject: [PATCH 147/176] delete launcher/ folder and move out files that are still relevant --- ... mpc-node-docker-compose.tee.template.yml} | 0 ...l => mpc-node-docker-compose.template.yml} | 0 crates/tee-launcher/src/main.rs | 9 +- .../cvm-deployment}/configs/kms.env | 0 .../cvm-deployment}/configs/sgx.env | 0 .../cvm-deployment}/default.env | 0 .../cvm-deployment}/deploy-launcher-guide.md | 0 .../cvm-deployment}/deploy-launcher.sh | 0 .../launcher_docker_compose.yaml | 0 .../launcher_docker_compose_nontee.yaml | 0 .../cvm-deployment}/user-config.toml | 15 +- {tee_launcher => docs}/UPDATING_LAUNCHER.md | 0 .../using-the-launcher-in-nontee-setup.md | 0 libs/nearcore | 2 +- tee_launcher/__init__.py | 0 tee_launcher/launcher-test-image/Dockerfile | 3 - tee_launcher/launcher.md | 102 -- tee_launcher/launcher.py | 873 ------------------ tee_launcher/requirements.txt | 2 - tee_launcher/test_launcher.py | 32 - tee_launcher/test_launcher_config.py | 844 ----------------- 21 files changed, 18 insertions(+), 1864 deletions(-) rename crates/tee-launcher/{docker-compose.tee.template.yml => mpc-node-docker-compose.tee.template.yml} (100%) rename crates/tee-launcher/{docker-compose.template.yml => mpc-node-docker-compose.template.yml} (100%) rename {tee_launcher => deployment/cvm-deployment}/configs/kms.env (100%) rename {tee_launcher => deployment/cvm-deployment}/configs/sgx.env (100%) rename {tee_launcher => deployment/cvm-deployment}/default.env (100%) rename {tee_launcher => deployment/cvm-deployment}/deploy-launcher-guide.md (100%) rename {tee_launcher => deployment/cvm-deployment}/deploy-launcher.sh (100%) rename {tee_launcher => deployment/cvm-deployment}/launcher_docker_compose.yaml (100%) rename {tee_launcher => deployment/cvm-deployment}/launcher_docker_compose_nontee.yaml (100%) rename {tee_launcher => deployment/cvm-deployment}/user-config.toml (80%) rename {tee_launcher => docs}/UPDATING_LAUNCHER.md (100%) rename {tee_launcher => docs}/using-the-launcher-in-nontee-setup.md (100%) delete mode 100644 tee_launcher/__init__.py delete mode 100644 tee_launcher/launcher-test-image/Dockerfile delete mode 100644 tee_launcher/launcher.md delete mode 100644 tee_launcher/launcher.py delete mode 100644 tee_launcher/requirements.txt delete mode 100644 tee_launcher/test_launcher.py delete mode 100644 tee_launcher/test_launcher_config.py diff --git a/crates/tee-launcher/docker-compose.tee.template.yml b/crates/tee-launcher/mpc-node-docker-compose.tee.template.yml similarity index 100% rename from crates/tee-launcher/docker-compose.tee.template.yml rename to crates/tee-launcher/mpc-node-docker-compose.tee.template.yml diff --git a/crates/tee-launcher/docker-compose.template.yml b/crates/tee-launcher/mpc-node-docker-compose.template.yml similarity index 100% rename from crates/tee-launcher/docker-compose.template.yml rename to crates/tee-launcher/mpc-node-docker-compose.template.yml diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 25eb9ed80..811ae8c2f 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -20,8 +20,8 @@ mod docker_types; mod error; mod types; -const COMPOSE_TEMPLATE: &str = include_str!("../docker-compose.template.yml"); -const COMPOSE_TEE_TEMPLATE: &str = include_str!("../docker-compose.tee.template.yml"); +const COMPOSE_TEMPLATE: &str = include_str!("../mpc-node-docker-compose.template.yml"); +const COMPOSE_TEE_TEMPLATE: &str = include_str!("../mpc-node-docker-compose.tee.template.yml"); const DOCKER_AUTH_ACCEPT_HEADER_VALUE: HeaderValue = HeaderValue::from_static("application/vnd.docker.distribution.manifest.v2+json"); @@ -983,6 +983,11 @@ mod integration_tests { use super::*; use assert_matches::assert_matches; + // # Dockerfile + // FROM alpine@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 + // CMD ["true"] + // TODO: Look into reusing this image, as its small and will be faster on CI + const TEST_DIGEST: &str = "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372"; const TEST_TAG: &str = "83b52da4e2270c688cdd30da04f6b9d3565f25bb"; diff --git a/tee_launcher/configs/kms.env b/deployment/cvm-deployment/configs/kms.env similarity index 100% rename from tee_launcher/configs/kms.env rename to deployment/cvm-deployment/configs/kms.env diff --git a/tee_launcher/configs/sgx.env b/deployment/cvm-deployment/configs/sgx.env similarity index 100% rename from tee_launcher/configs/sgx.env rename to deployment/cvm-deployment/configs/sgx.env diff --git a/tee_launcher/default.env b/deployment/cvm-deployment/default.env similarity index 100% rename from tee_launcher/default.env rename to deployment/cvm-deployment/default.env diff --git a/tee_launcher/deploy-launcher-guide.md b/deployment/cvm-deployment/deploy-launcher-guide.md similarity index 100% rename from tee_launcher/deploy-launcher-guide.md rename to deployment/cvm-deployment/deploy-launcher-guide.md diff --git a/tee_launcher/deploy-launcher.sh b/deployment/cvm-deployment/deploy-launcher.sh similarity index 100% rename from tee_launcher/deploy-launcher.sh rename to deployment/cvm-deployment/deploy-launcher.sh diff --git a/tee_launcher/launcher_docker_compose.yaml b/deployment/cvm-deployment/launcher_docker_compose.yaml similarity index 100% rename from tee_launcher/launcher_docker_compose.yaml rename to deployment/cvm-deployment/launcher_docker_compose.yaml diff --git a/tee_launcher/launcher_docker_compose_nontee.yaml b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml similarity index 100% rename from tee_launcher/launcher_docker_compose_nontee.yaml rename to deployment/cvm-deployment/launcher_docker_compose_nontee.yaml diff --git a/tee_launcher/user-config.toml b/deployment/cvm-deployment/user-config.toml similarity index 80% rename from tee_launcher/user-config.toml rename to deployment/cvm-deployment/user-config.toml index 4bffc67b4..377541d18 100644 --- a/tee_launcher/user-config.toml +++ b/deployment/cvm-deployment/user-config.toml @@ -6,10 +6,15 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 port_mappings = [ - { host =8080, container =8080 }, - { host =3030, container =3030 }, - { host =80, container =80 }, - { host =24567, container =24567 }, + # debug/metrics endpoint + { host = 8080, container = 8080 }, + { host = 3030, container = 3030 }, + # MPC - P2P + { host = 80, container = 80 }, + # neard + { host = 24567, container = 24567 }, + # migration + { host = 8079, container = 8079 }, ] [mpc_config] @@ -32,7 +37,7 @@ my_near_account_id = "mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet" near_responder_account_id = "mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet" number_of_responder_keys = 1 web_ui = "0.0.0.0:8080" -migration_web_ui = "0.0.0.0:8078" +migration_web_ui = "0.0.0.0:8079" cores = 4 [mpc_config.node.indexer] diff --git a/tee_launcher/UPDATING_LAUNCHER.md b/docs/UPDATING_LAUNCHER.md similarity index 100% rename from tee_launcher/UPDATING_LAUNCHER.md rename to docs/UPDATING_LAUNCHER.md diff --git a/tee_launcher/using-the-launcher-in-nontee-setup.md b/docs/using-the-launcher-in-nontee-setup.md similarity index 100% rename from tee_launcher/using-the-launcher-in-nontee-setup.md rename to docs/using-the-launcher-in-nontee-setup.md diff --git a/libs/nearcore b/libs/nearcore index 9d43667a7..96100bbf3 160000 --- a/libs/nearcore +++ b/libs/nearcore @@ -1 +1 @@ -Subproject commit 9d43667a71f64b7efa6c9f897eef61983d373977 +Subproject commit 96100bbf3b5de7eed46cab7bfe446e5002e19c11 diff --git a/tee_launcher/__init__.py b/tee_launcher/__init__.py deleted file mode 100644 index e69de29bb..000000000 diff --git a/tee_launcher/launcher-test-image/Dockerfile b/tee_launcher/launcher-test-image/Dockerfile deleted file mode 100644 index b49bc4b74..000000000 --- a/tee_launcher/launcher-test-image/Dockerfile +++ /dev/null @@ -1,3 +0,0 @@ -# Dockerfile -FROM alpine@sha256:765942a4039992336de8dd5db680586e1a206607dd06170ff0a37267a9e01958 -CMD ["true"] \ No newline at end of file diff --git a/tee_launcher/launcher.md b/tee_launcher/launcher.md deleted file mode 100644 index a21904a80..000000000 --- a/tee_launcher/launcher.md +++ /dev/null @@ -1,102 +0,0 @@ -# Deploy and Upgrade an MPC Node on dstack - -The launcher is a single Python script: [launcher.py](launcher.py) - -This is a secure launcher script for initializing and attesting a Docker-based MPC node. -It is designed to run inside a TEE-enabled environment (e.g., Intel TDX) to add and ensures the integrity and trustworthiness of the image before launching it. - - -## 🔐 Features - -- Pull an MPC docker image. -- Compares the MPC image digest against expected values -- Extends RTMR3 with the verified image digest -- prints remote attestation and quote generation information to log -- Starts the MPC node container with secure mount and network settings - -## Usage - -The launcher script is designed to run inside a confidential TDX VM managed by Dstack VMM. - -launcher-docker-compose.yaml — Docker Compose file used to start the launcher and supporting containers. -config.txt — File containing trusted environment variables used by the launcher and MPC node. -It should be uploaded to: /tapp/.host-shared/.user-config - -## 🧩 Environment Variables - -- `DOCKER_CONTENT_TRUST=1`: Must be enabled -- `DEFAULT_IMAGE_DIGEST`: The expected hash of the Docker image (e.g., `sha256:...`) - -## 📁 File Locations - -- `/tapp/user_config"`: Optional `.env` file for overriding defaults -- `/mnt/shared/image-digest`: Optional override of image digest (written by external components) -- `/var/run/dstack.sock`: Unix socket used to communicate with `dstack` - -## 🔧 Configuration (via user-config) - -## 🖼️ Image selection - -| Variable | Description | -|----------|-------------| -| `MPC_IMAGE_NAME` | Name of the MPC docker image (default: `nearone/mpc-node`) | -| `MPC_REGISTRY` | Registry hostname (default: `registry.hub.docker.com`) | -| `MPC_IMAGE_TAGS` | Comma-separated tags to try (default: `latest`) | -| `MPC_HASH_OVERRIDE` | Optional: force a slection of specific sha256 digest (must be in approved list) | -| `RPC_REQUEST_TIMEOUT_SECS` | Per-request timeout for dockerhub | `10` | -| `RPC_REQUEST_INTERVAL_SECS` | Initial retry interval (seconds) for dockerhub | `1.0` | -| `RPC_MAX_ATTEMPTS` | Max attempts before failure for dockerhub | `20` | - -The launcher supports the following environment variables via `/tapp/user_config`: - -Example values (for [user-config.conf](./user-config.conf)) - -```bash -LAUNCHER_IMAGE_NAME=nearone/mpc-node -LAUNCHER_IMAGE_TAGS=latest -LAUNCHER_REGISTRY=registry.hub.docker.com -MPC_HASH_OVERRIDE=sha256:xyz... -RPC_REQUEST_TIMEOUT_SECS =10 -RPC_REQUEST_INTERVAL_SECS =1 -RPC_MAX_ATTEMPTS =20 -``` - -## Reproducible builds -from: tee_launcher folder run: -docker build -t barakeinavnear/launcher:latest -f development/Dockerfile.launcher . - -- [Dockerfile-node](../deployment/Dockerfile-node) Dockerfile with all dependencies pinned to specific versions, e.g., other Dockerfile via sha256 digests and Linux distribution packages via explicit version strings -- [build-images.sh](../deployment/build-images.sh) drives the build process - -For example, I ran `../deployment/build-images.sh` on the git commit [ef3f1e7...](https://github.com/Near-One/mpc/commit/ef3f1e7f862d447de60e91d32dadf68696eb6a58). The resulting Docker image digest was - -``` -sha256:dcbd3b8c8ae35d2ba63b25d6b617ce8b7faabb0af96ffa2e35b08a50258ebfa4 -``` - -and the MPC binary digest was - -``` -5dd1a80f842d08753184334466e97d55e5caa3dbcd93af27097d11e926d7f823 -``` - -The respective commands to find either are - -``` -docker image inspect mpc-node-gcp:latest | jq '.[0].Id' -``` - -Note, the image digest used with `docker run` is the output of the `docker image inspect ...` command. - -``` -docker run --rm dcbd3b8c8ae35d2ba63b25d6b617ce8b7faabb0af96ffa2e35b08a50258ebfa4 cat /app/mpc-node | sha256sum -``` - -Opens: write a script utilizing `vmm-cli.py` from Dstack to deploy an mpc node - -- Artifacts to deploy a node - - Scripts to a) reproducibly build the mpc binary and b) reproducibly build a docker image containing the mpc binary -- Actual upgrade procedure - - Write new image hash to /mnt/shared/image-digest - - Shut down cvm - - Amend `LAUNCHER_IMAGE_TAGS` if necessary; can be done from host by editing ./meta-dstack/build/run/*/shared/.user-config diff --git a/tee_launcher/launcher.py b/tee_launcher/launcher.py deleted file mode 100644 index 4f75fb762..000000000 --- a/tee_launcher/launcher.py +++ /dev/null @@ -1,873 +0,0 @@ -from collections import deque -import logging -import os -import stat -from typing import Dict, Union -from enum import Enum -import requests -from subprocess import CompletedProcess, run, check_output -import sys -import time -import traceback -from dataclasses import dataclass -import re -import json -from typing import NamedTuple - - -from requests.models import Response - -MPC_CONTAINER_NAME = "mpc-node" - -# The volume where this file resides is shared between launcher and app. -# To avoid concurrent modifications, the launcher mounts the volume read-only! -# the contents of this file are generated by the node itself and fetched from the contract. -IMAGE_DIGEST_FILE = "/mnt/shared/image-digest.bin" - - -# docker-hub defaults -RPC_REQUEST_TIMEOUT_SECS = 10 -RPC_REQUEST_INTERVAL_SECS = 1.0 -RPC_MAX_ATTEMPTS = 20 - - -class RpcTimingConfig(NamedTuple): - rpc_request_timeout_secs: float - rpc_request_interval_secs: float - rpc_max_attempts: int - - -# -# Platform mode (MUST come from measured docker-compose env in TEE deployments) -# -ENV_VAR_PLATFORM = "PLATFORM" - - -# do not change the string values - they are used in docker-compose files. -class Platform(Enum): - TEE = "TEE" - NONTEE = "NONTEE" - - -# only considered if `IMAGE_DIGEST_FILE` does not exist. -ENV_VAR_DEFAULT_IMAGE_DIGEST = "DEFAULT_IMAGE_DIGEST" - -# optional - the time to wait between rpc requests, in seconds. Defaults to 1.0 seconds. -ENV_VAR_RPC_REQUEST_INTERVAL_SECS = "RPC_REQUEST_INTERVAL_SECS" -# optional - the maximum time to wait for an rpc response. Defaults to 10 seconds. -ENV_VAR_RPC_REQUEST_TIMEOUT_SECS = "RPC_REQUEST_TIMEOUT_SECS" -# optional - the maximum number of attempts for rpc requests until we raise an exception -ENV_VAR_RPC_MAX_ATTEMPTS = "RPC_MAX_ATTEMPTS" -# MUST be set to 1. -OS_ENV_DOCKER_CONTENT_TRUST = "DOCKER_CONTENT_TRUST" - - -# dstack user configuration flags -DSTACK_USER_CONFIG_FILE = "/tapp/user_config" - -# dstack user config. Read from `DSTACK_USER_CONFIG_FILE` -DSTACK_USER_CONFIG_MPC_IMAGE_TAGS = "MPC_IMAGE_TAGS" -DSTACK_USER_CONFIG_MPC_IMAGE_NAME = "MPC_IMAGE_NAME" -DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY = "MPC_REGISTRY" - -# optional - if set, overrides the approved hashes list. -# format: sha256:... -ENV_VAR_MPC_HASH_OVERRIDE = "MPC_HASH_OVERRIDE" - -# Default values for dstack user config file. -DEFAULT_MPC_IMAGE_NAME = "nearone/mpc-node" -DEFAULT_MPC_REGISTRY = "registry.hub.docker.com" -DEFAULT_MPC_IMAGE_TAG = "latest" - -# Environment variables that configure the launcher itself. -# These are read from the user config file but should NEVER be passed to the MPC container. -ALLOWED_LAUNCHER_ENV_VARS = { - DSTACK_USER_CONFIG_MPC_IMAGE_TAGS, - DSTACK_USER_CONFIG_MPC_IMAGE_NAME, - DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY, - ENV_VAR_MPC_HASH_OVERRIDE, - ENV_VAR_RPC_REQUEST_TIMEOUT_SECS, - ENV_VAR_RPC_REQUEST_INTERVAL_SECS, - ENV_VAR_RPC_MAX_ATTEMPTS, -} - - -# the unix socket to communicate with dstack -DSTACK_UNIX_SOCKET = "/var/run/dstack.sock" - -SHA256_PREFIX = "sha256:" -SHA256_REGEX = re.compile(r"^sha256:[0-9a-f]{64}$") - -# JSON key used inside image-digest.bin -# IMPORTANT: Must stay aligned with the MPC node implementation in: -# crates/node/src/tee/allowed_image_hashes_watcher.rs -JSON_KEY_APPROVED_HASHES = "approved_hashes" - - -# -------------------------------------------------------------------------------------- -# Security policy for env passthrough -# -------------------------------------------------------------------------------------- - -# Allow all MPC_* keys, but keep validation strict. -MPC_ENV_KEY_RE = re.compile(r"^MPC_[A-Z0-9_]{1,64}$") - - -# Hard caps to prevent DoS via huge env payloads. -MAX_PASSTHROUGH_ENV_VARS = 64 -MAX_ENV_VALUE_LEN = 1024 -MAX_TOTAL_ENV_BYTES = 32 * 1024 # 32KB total across passed envs - -# Never pass raw private keys via launcher (any platform) -DENIED_CONTAINER_ENV_KEYS = { - "MPC_P2P_PRIVATE_KEY", - "MPC_ACCOUNT_SK", -} - -# Example of .user-config file format: -# -# MPC_ACCOUNT_ID=mpc-user-123 -# MPC_LOCAL_ADDRESS=127.0.0.1 -# MPC_SECRET_STORE_KEY=secret -# MPC_CONTRACT_ID=mpc-contract -# MPC_ENV=testnet -# MPC_HOME_DIR=/data -# NEAR_BOOT_NODES=boot1,boot2 -# RUST_BACKTRACE=1 -# RUST_LOG=info -# MPC_RESPONDER_ID=responder-xyz -# PORTS=11780:11780,2200:2200 - -# Define an allow-list of permitted environment variables that will be passed to MPC container. -# Note - extra hosts and port forwarding are explicitly defined in the docker run command generation. -# NOTE: Kept for backwards compatibility and for documentation purposes; the effective policy is: -# - allow MPC_* keys that match MPC_ENV_KEY_RE -# - plus existing non-MPC keys below (RUST_LOG / RUST_BACKTRACE / NEAR_BOOT_NODES) -ALLOWED_MPC_ENV_VARS = { - "MPC_ACCOUNT_ID", # ID of the MPC account on the network - "MPC_LOCAL_ADDRESS", # Local IP address or hostname used by the MPC node - "MPC_SECRET_STORE_KEY", # Key used to encrypt/decrypt secrets - "MPC_CONTRACT_ID", # Contract ID associated with the MPC node - "MPC_ENV", # Environment (e.g., 'testnet', 'mainnet') - "MPC_HOME_DIR", # Home directory for the MPC node - "NEAR_BOOT_NODES", # Comma-separated list of boot nodes - "RUST_BACKTRACE", # Enables backtraces for Rust errors - "RUST_LOG", # Logging level for Rust code - "MPC_RESPONDER_ID", # Unique responder ID for MPC communication - "MPC_BACKUP_ENCRYPTION_KEY_HEX", # encryption key for backups -} - -# Regex: hostnames must be alphanum + dash/dot, IPs must be valid IPv4 -HOST_ENTRY_RE = re.compile(r"^[a-zA-Z0-9\-\.]+:\d{1,3}(\.\d{1,3}){3}$") -PORT_MAPPING_RE = re.compile(r"^(\d{1,5}):(\d{1,5})$") - -# Updated regex to block any entry starting with '-' (including '--') and other unsafe characters -INVALID_HOST_ENTRY_PATTERN = re.compile(r"^[;&|`$\\<>-]|^--") - - -def _has_control_chars(s: str) -> bool: - # Disallow NUL + CR/LF at minimum; also block other ASCII control chars (< 0x20) except tab. - for ch in s: - oc = ord(ch) - if ch in ("\n", "\r", "\x00"): - return True - if oc < 0x20 and ch != "\t": - return True - return False - - -def is_safe_env_value(value: str) -> bool: - """ - Validates that an env value contains no unsafe control characters (CR/LF/NUL), - does not include LD_PRELOAD, and is within size limits to prevent injection or DoS. - """ - if not isinstance(value, str): - return False - if len(value) > MAX_ENV_VALUE_LEN: - return False - if _has_control_chars(value): - return False - if "LD_PRELOAD" in value: - return False - return True - - -def is_valid_port_mapping(entry: str) -> bool: - match = PORT_MAPPING_RE.match(entry) - if not match: - return False - host_port, container_port = map(int, match.groups()) - return 0 < host_port <= 65535 and 0 < container_port <= 65535 - - -def is_non_empty_and_cleaned(val: str) -> bool: - if not isinstance(val, str): - return False - if not val.strip(): - return False - return val.strip() == val - - -def is_safe_port_mapping(mapping: str) -> bool: - """Ensure that the port mapping does not contain unsafe characters or start with '--' or '-'.""" - return not INVALID_HOST_ENTRY_PATTERN.search(mapping) - - -def remove_existing_container(): - """Stop and remove the MPC container if it exists.""" - try: - containers = check_output( - ["docker", "ps", "-a", "--format", "{{.Names}}"], text=True - ).splitlines() - if MPC_CONTAINER_NAME in containers: - logging.info(f"Removing existing container: {MPC_CONTAINER_NAME}") - run(["docker", "rm", "-f", MPC_CONTAINER_NAME], check=False) - except Exception as e: - logging.warning(f"Failed to check/remove container {MPC_CONTAINER_NAME}: {e}") - - -@dataclass(frozen=True) -class ImageSpec: - tags: list[str] - image_name: str - registry: str - - def __post_init__(self): - if not self.tags or not all(is_non_empty_and_cleaned(tag) for tag in self.tags): - raise ValueError( - "tags must be a non-empty list of non-empty strings without whitespaces." - ) - - if not is_non_empty_and_cleaned(self.image_name): - raise ValueError( - "image_name must be a non-empty string without whitespaces." - ) - - if not is_non_empty_and_cleaned(self.registry): - raise ValueError("registry must be a non-empty string without whitespaces.") - - -@dataclass(frozen=True) -class ResolvedImage: - spec: ImageSpec - digest: str - - def __post_init__(self): - if not is_non_empty_and_cleaned(self.digest): - raise ValueError( - "image digest must be a non-empty string without whitespaces" - ) - # should we require specific lengths? - - def name(self) -> str: - return self.spec.image_name - - def tags(self) -> list[str]: - return self.spec.tags - - def registry(self) -> str: - return self.spec.registry - - -def parse_env_lines(lines: list[str]) -> dict: - env = {} - for line in lines: - line = line.strip() - if not line or line.startswith("#") or "=" not in line: - continue - key, value = line.split("=", 1) - key = key.strip() - value = value.strip() - if not key: - continue - env[key] = value - return env - - -# Parses a .env-style file into a dictionary of key-value pairs. -def parse_env_file(path: str) -> dict: - with open(path, "r") as f: - return parse_env_lines(f.readlines()) - - -def is_unix_socket(path: str) -> bool: - try: - st = os.stat(path) - return stat.S_ISSOCK(st.st_mode) - except FileNotFoundError: - return False - except Exception: - return False - - -def parse_platform() -> Platform: - """ - Platform selection MUST be a measured input in TEE deployments. - Therefore, we only read it from process env (docker-compose 'environment'), - and never from /tapp/user_config. - """ - raw = os.environ.get(ENV_VAR_PLATFORM) - if raw is None: - raise RuntimeError( - f"{ENV_VAR_PLATFORM} must be set to one of {[p.value for p in Platform]}" - ) - - val = raw.strip() - try: - return Platform(val) - except ValueError as e: - allowed = ", ".join(p.value for p in Platform) - raise RuntimeError( - f"Invalid {ENV_VAR_PLATFORM}={raw!r}. Expected one of: {allowed}" - ) from e - - -def load_rpc_timing_config(dstack_config: dict[str, str]) -> RpcTimingConfig: - """ - Loads dockerhub RPC timing configuration from dstack_config, - falling back to defaults if not provided by user. - """ - - timeout_secs = float( - dstack_config.get(ENV_VAR_RPC_REQUEST_TIMEOUT_SECS, RPC_REQUEST_TIMEOUT_SECS) - ) - interval_secs = float( - dstack_config.get(ENV_VAR_RPC_REQUEST_INTERVAL_SECS, RPC_REQUEST_INTERVAL_SECS) - ) - max_attempts = int(dstack_config.get(ENV_VAR_RPC_MAX_ATTEMPTS, RPC_MAX_ATTEMPTS)) - - return RpcTimingConfig( - rpc_request_timeout_secs=timeout_secs, - rpc_request_interval_secs=interval_secs, - rpc_max_attempts=max_attempts, - ) - - -def get_image_spec(dstack_config: dict[str, str]) -> ImageSpec: - tags_values: list[str] = dstack_config.get( - DSTACK_USER_CONFIG_MPC_IMAGE_TAGS, DEFAULT_MPC_IMAGE_TAG - ).split(",") - tags = [tag.strip() for tag in tags_values if tag.strip()] - logging.info(f"Using tags {tags} to find matching MPC node docker image.") - - image_name: str = dstack_config.get( - DSTACK_USER_CONFIG_MPC_IMAGE_NAME, DEFAULT_MPC_IMAGE_NAME - ) - logging.info(f"Using image name {image_name}.") - - registry: str = dstack_config.get( - DSTACK_USER_CONFIG_MPC_IMAGE_REGISTRY, DEFAULT_MPC_REGISTRY - ) - logging.info(f"Using registry {registry}.") - - return ImageSpec(tags=tags, image_name=image_name, registry=registry) - - -def load_and_select_hash(dstack_config: dict) -> str: - """ - Load the approved MPC image hashes and deterministically select a single hash. - - Selection rules: - - If MPC_HASH_OVERRIDE is set → use it (after basic format validation) - - Else → use the newest approved hash (already first in the list) - - This function does NOT validate the hash — only selects it. - """ - - # IMAGE_DIGEST_FILE missing → use DEFAULT_IMAGE_DIGEST - if not os.path.isfile(IMAGE_DIGEST_FILE): - fallback = os.environ[ENV_VAR_DEFAULT_IMAGE_DIGEST].strip() - - if not fallback.startswith(SHA256_PREFIX): - fallback = SHA256_PREFIX + fallback - if not SHA256_REGEX.match(fallback): - raise RuntimeError(f"DEFAULT_IMAGE_DIGEST invalid: {fallback}") - - logging.info( - f"{IMAGE_DIGEST_FILE} missing → fallback to DEFAULT_IMAGE_DIGEST={fallback}" - ) - approved_hashes = [fallback] - - else: - # Load JSON with approved hashes - try: - with open(IMAGE_DIGEST_FILE, "r") as f: - data = json.load(f) - except Exception as e: - raise RuntimeError(f"Failed to parse {IMAGE_DIGEST_FILE}: {e}") - - hashes = data.get(JSON_KEY_APPROVED_HASHES) - - if not isinstance(hashes, list) or not hashes: - raise RuntimeError( - f"Invalid JSON in {IMAGE_DIGEST_FILE}: approved_hashes missing or empty" - ) - - # Hashes from the node are already ordered newest → oldest - approved_hashes = hashes - - # Print allowed hashes for operator UX - logging.info("Approved MPC image hashes (newest → oldest):") - for h in approved_hashes: - logging.info(f" - {h}") - - # --- Optional override --- - override = dstack_config.get(ENV_VAR_MPC_HASH_OVERRIDE) - - if override: - if not SHA256_REGEX.match(override): - raise RuntimeError(f"MPC_HASH_OVERRIDE invalid: {override}") - else: - if override not in approved_hashes: - logging.error( - f"MPC_HASH_OVERRIDE={override} does NOT match any approved hash!" - ) - raise RuntimeError(f"MPC_HASH_OVERRIDE invalid: {override}") - logging.info(f"MPC_HASH_OVERRIDE provided → selecting: {override}") - return override - - # No override → select newest - selected = approved_hashes[0] - logging.info(f"Selected MPC hash (newest allowed): {selected}") - return selected - - -def validate_image_hash( - image_digest: str, - dstack_config: dict, - rpc_request_timeout_secs: float, - rpc_request_interval_secs: float, - rpc_max_attempts: int, -) -> bool: - """ - Returns True if the given image digest is valid (pull + manifest + digest match). - Does NOT extend RTMR3 and does NOT run the container. - """ - try: - logging.info(f"Validating MPC hash: {image_digest}") - - image_spec = get_image_spec(dstack_config) - docker_image = ResolvedImage(spec=image_spec, digest=image_digest) - - manifest_digest = get_manifest_digest( - docker_image, - rpc_request_timeout_secs, - rpc_request_interval_secs, - rpc_max_attempts, - ) - - name_and_digest = f"{image_spec.image_name}@{manifest_digest}" - - # Pull - proc = run(["docker", "pull", name_and_digest], capture_output=True) - if proc.returncode != 0: - logging.error( - f"docker pull failed for {image_digest} using {name_and_digest}" - ) - logging.error( - f"stdout:\n{proc.stdout}", - ) - logging.error( - f"stderr:\n{proc.stderr}", - ) - return False - - # Verify digest - proc = run( - [ - "docker", - "image", - "inspect", - "--format", - "{{index .ID}}", - name_and_digest, - ], - capture_output=True, - ) - if proc.returncode != 0: - logging.error(f"docker inspect failed for {image_digest}") - return False - - pulled_digest = proc.stdout.decode("utf-8").strip() - if pulled_digest != image_digest: - logging.error(f"digest mismatch: {pulled_digest} != {image_digest}") - return False - logging.info(f"MPC hash {image_digest} validated successfully.") - return True - - except Exception as e: - logging.error(f"Validation failed for {image_digest}: {e}") - return False - - -def extend_rtmr3(platform: Platform, valid_hash: str) -> None: - if platform == Platform.NONTEE: - logging.info("PLATFORM=NONTEE → skipping RTMR3 extension step.") - return - - if not is_unix_socket(DSTACK_UNIX_SOCKET): - raise RuntimeError( - f"PLATFORM=TEE requires dstack unix socket at {DSTACK_UNIX_SOCKET}." - ) - - bare = get_bare_digest(valid_hash) - logging.info(f"Extending RTMR3 with validated hash: {bare}") - - # GetQuote first - proc = curl_unix_socket_post( - endpoint="GetQuote", payload='{"report_data": ""}', capture_output=True - ) - if proc.returncode: - raise RuntimeError("GetQuote failed before extending RTMR3") - - payload_json = json.dumps({"event": "mpc-image-digest", "payload": bare}) - - proc = curl_unix_socket_post( - endpoint="EmitEvent", payload=payload_json, capture_output=True - ) - if proc.returncode: - raise RuntimeError("EmitEvent failed while extending RTMR3") - - -def launch_mpc_container(platform: Platform, valid_hash: str, user_env: dict) -> None: - logging.info(f"Launching MPC node with validated hash: {valid_hash}") - - remove_existing_container() - docker_cmd = build_docker_cmd(platform, user_env, valid_hash) - - proc = run(docker_cmd) - if proc.returncode != 0: - raise RuntimeError(f"docker run failed for validated hash={valid_hash}") - - logging.info("MPC launched successfully.") - - -def curl_unix_socket_post( - endpoint: str, payload: Union[str, bytes], capture_output: bool = False -) -> CompletedProcess: - """ - Send a POST request via curl using the DSTACK UNIX socket. - Python's requests package cannot natively talk HTTP over a unix socket (which is the API - exposed by dstack's guest agent). To avoid installing another Python depdendency, namely - requests-unixsocket, we just use curl. - - Args: - endpoint: Path after `http://dstack/`, e.g. 'GetQuote', 'EmitEvent' - payload: JSON string or bytes to send as the request body - capture_output: Whether to capture stdout/stderr (default: False) - - Returns: - subprocess.CompletedProcess result - """ - url = f"http://dstack/{endpoint}" - cmd = [ - "curl", - "--unix-socket", - DSTACK_UNIX_SOCKET, - "-X", - "POST", - url, - "-H", - "Content-Type: application/json", - "-d", - payload, - ] - return run(cmd, capture_output=capture_output) - - -def main(): - logging.info("start") - - platform = parse_platform() - logging.info(f"Launcher platform: {platform.value}") - - if platform is Platform.TEE and not is_unix_socket(DSTACK_UNIX_SOCKET): - raise RuntimeError( - f"PLATFORM=TEE requires dstack unix socket at {DSTACK_UNIX_SOCKET}." - ) - - # DOCKER_CONTENT_TRUST must be enabled - if os.environ.get(OS_ENV_DOCKER_CONTENT_TRUST, "0") != "1": - raise RuntimeError( - "Environment variable DOCKER_CONTENT_TRUST must be set to 1." - ) - - # Load dstack configuration (tags, registry, image name) - # In dstack, /tapp/user_config provides unmeasured data to the CVM. - # We use this interface to make some aspects of the launcher configurable. - # *** Only security-irrelevant parts *** may be made configurable in this way, e.g., the specific image tag(s) we look up. - dstack_config: dict[str, str] = ( - parse_env_file(DSTACK_USER_CONFIG_FILE) - if os.path.isfile(DSTACK_USER_CONFIG_FILE) - else {} - ) - - rpc_cfg = load_rpc_timing_config(dstack_config) - - rpc_request_timeout_secs = rpc_cfg.rpc_request_timeout_secs - rpc_request_interval_secs = rpc_cfg.rpc_request_interval_secs - rpc_max_attempts = rpc_cfg.rpc_max_attempts - - # Choose exactly one allowed hash (override or newest) - selected_hash = load_and_select_hash(dstack_config) - - if not validate_image_hash( - selected_hash, - dstack_config, - rpc_request_timeout_secs, - rpc_request_interval_secs, - rpc_max_attempts, - ): - raise RuntimeError(f"MPC image hash validation failed: {selected_hash}") - - # Continue with validated hash - logging.info(f"MPC image hash validated successfully: {selected_hash}") - - extend_rtmr3(platform, selected_hash) - - launch_mpc_container(platform, selected_hash, dstack_config) - - -def request_until_success( - url: str, - headers: Dict[str, str], - rpc_request_timeout_secs: float, - rpc_request_interval_secs: float, - rpc_max_attempts: int, -) -> Response: - """ - Repeatedly sends a GET request to the specified URL until a successful (200 OK) response is received. - - Args: - url (str): The URL to request. - headers (Dict[str, str]): Optional headers to include in the request. - rpc_request_interval_secs (float): Time in seconds to wait between retries on failure. - rpc_request_timeout_secs (float): Maximum time in seconds to wait for a request to succeed. - - Returns: - Response: The successful HTTP response object with status code 200. - - Notes: - - Retries indefinitely until the request succeeds. - - Prints a warning with the response content on each failure. - """ - for attempt in range(1, rpc_max_attempts + 1): - # Ensure that we respect the backoff time. Performance is not a priority in this case. - time.sleep(rpc_request_interval_secs) - rpc_request_interval_secs = min(max(rpc_request_interval_secs, 1.0) * 1.5, 60.0) - try: - manifest_resp = requests.get( - url, headers=headers, timeout=rpc_request_timeout_secs - ) - except requests.exceptions.Timeout: - print( - f"[Warning] Attempt {attempt}/{rpc_max_attempts}: Failed to fetch {url} for headers {headers}. " - f"Status: Timeout" - ) - continue - if manifest_resp.status_code != 200: - print( - f"[Warning] Attempt {attempt}/{rpc_max_attempts}: Failed to fetch {url} for headers {headers}. " - f"Status: {manifest_resp.text} {manifest_resp.headers}" - ) - continue - else: - return manifest_resp - - raise RuntimeError( - f"Failed to get successful response from {url} after {rpc_max_attempts} attempts." - ) - - -def get_manifest_digest( - docker_image: ResolvedImage, - rpc_request_timeout_secs: float, - rpc_request_interval_secs: float, - rpc_max_attempts: int, -) -> str: - """ - Given an `image_digest` returns a manifest digest. - `docker pull` requires a manifest digest. This function translates an image digest into a manifest digest by talking to the Docker registry. - API doc for image registry https://distribution.github.io/distribution/spec/api/ - """ - if not docker_image.tags(): - raise Exception(f"No tags found for image {docker_image.spec.image_name}") - - # We need an authorization token to fetch manifests. - # TODO: this still has the registry hard-coded in the url. also, if we use a different registry, we need a different auth-endpoint. - token_resp = requests.get( - f"https://auth.docker.io/token?service=registry.docker.io&scope=repository:{docker_image.name()}:pull" - ) - token_resp.raise_for_status() - token = token_resp.json().get("token", []) - - tags = deque(docker_image.tags()) - - while tags: - tag = tags.popleft() - - manifest_url = f"https://{docker_image.registry()}/v2/{docker_image.name()}/manifests/{tag}" - headers = { - "Accept": "application/vnd.docker.distribution.manifest.v2+json", - "Authorization": f"Bearer {token}", - } - try: - manifest_resp = request_until_success( - url=manifest_url, - headers=headers, - rpc_request_timeout_secs=rpc_request_timeout_secs, - rpc_request_interval_secs=rpc_request_interval_secs, - rpc_max_attempts=rpc_max_attempts, - ) - manifest = manifest_resp.json() - match manifest["mediaType"]: - case "application/vnd.oci.image.index.v1+json": - # Multi-platform manifest; we scan for amd64/linux images and add them to `tags` - for image_manifest in manifest.get("manifests", []): - platform = image_manifest.get("platform", []) - if ( - platform.get("architecture") == "amd64" - and platform.get("os") == "linux" - ): - tags.append(image_manifest["digest"]) - case ( - "application/vnd.docker.distribution.manifest.v2+json" - | "application/vnd.oci.image.manifest.v1+json" - ): - config_digest = manifest["config"]["digest"] - if config_digest == docker_image.digest: - return manifest_resp.headers["Docker-Content-Digest"] - except RuntimeError as e: - print( - f"[Warning] {e}: Exceeded number of maximum RPC requests for any given attempt. Will continue in the hopes of finding the matching image hash among remaining tags" - ) - # Q: Do we expect all requests to succeed? - raise Exception("Image hash not found among tags.") - - -def get_bare_digest(full_digest: str) -> str: - """ - Extracts and returns the bare digest (without the sha256: prefix). - - Example: - 'sha256:abcd...' -> 'abcd...' - """ - if not full_digest.startswith("sha256:"): - raise ValueError(f"Invalid digest (missing sha256: prefix): {full_digest}") - - return full_digest.split(":", 1)[1] - - -def is_allowed_container_env_key(key: str) -> bool: - if key in DENIED_CONTAINER_ENV_KEYS: - return False - # Allow MPC_* keys with strict regex - if MPC_ENV_KEY_RE.match(key): - return True - # Keep allowlist - if key in ALLOWED_MPC_ENV_VARS: - return True - return False - - -def build_docker_cmd( - platform: Platform, user_env: dict[str, str], image_digest: str -) -> list[str]: - bare_digest = get_bare_digest(image_digest) - - docker_cmd = ["docker", "run"] - - # Required environment variables - docker_cmd += ["--env", f"MPC_IMAGE_HASH={bare_digest}"] - docker_cmd += ["--env", f"MPC_LATEST_ALLOWED_HASH_FILE={IMAGE_DIGEST_FILE}"] - - if platform is Platform.TEE: - docker_cmd += ["--env", f"DSTACK_ENDPOINT={DSTACK_UNIX_SOCKET}"] - docker_cmd += ["-v", f"{DSTACK_UNIX_SOCKET}:{DSTACK_UNIX_SOCKET}"] - - # Track env passthrough size/caps - passed_env_count = 0 - total_env_bytes = 0 - - # Deterministic iteration for stable logs/behavior - for key in sorted(user_env.keys()): - value = user_env[key] - - if key in ALLOWED_LAUNCHER_ENV_VARS: - # launcher-only env vars: never pass to container - continue - - if key == "PORTS": - for port_pair in value.split(","): - clean_host = port_pair.strip() - if is_safe_port_mapping(clean_host) and is_valid_port_mapping( - clean_host - ): - docker_cmd += ["-p", clean_host] - else: - logging.warning( - f"Ignoring invalid or unsafe PORTS entry: {clean_host}" - ) - continue - - if not is_allowed_container_env_key(key): - logging.warning(f"Ignoring unknown or unapproved env var: {key}") - continue - - if not is_safe_env_value(value): - logging.warning(f"Ignoring env var with unsafe value: {key}") - continue - - # Enforce caps - passed_env_count += 1 - if passed_env_count > MAX_PASSTHROUGH_ENV_VARS: - raise RuntimeError( - f"Too many env vars to pass through (>{MAX_PASSTHROUGH_ENV_VARS})." - ) - - # Approximate byte accounting (key=value plus overhead) - total_env_bytes += len(key) + 1 + len(value) - if total_env_bytes > MAX_TOTAL_ENV_BYTES: - raise RuntimeError( - f"Total env payload too large (>{MAX_TOTAL_ENV_BYTES} bytes)." - ) - - docker_cmd += ["--env", f"{key}={value}"] - - # Container run configuration - docker_cmd += [ - "--security-opt", - "no-new-privileges:true", - "-v", - "/tapp:/tapp:ro", - "-v", - "shared-volume:/mnt/shared", - "-v", - "mpc-data:/data", - "--name", - MPC_CONTAINER_NAME, - "--detach", - image_digest, # IMPORTANT: Docker must get the FULL digest - ] - - logging.info("docker cmd %s", " ".join(docker_cmd)) - - # Final LD_PRELOAD safeguard - docker_cmd_str = " ".join(docker_cmd) - if "LD_PRELOAD" in docker_cmd_str: - raise RuntimeError("Unsafe docker command: LD_PRELOAD detected.") - - return docker_cmd - - -if __name__ == "__main__": - try: - logging.basicConfig( - level=logging.INFO, format="%(asctime)s [%(levelname)s] %(message)s" - ) - - main() - sys.exit(0) - except Exception as e: - print("Error:", str(e), file=sys.stderr) - traceback.print_exc(file=sys.stderr) - sys.exit(1) diff --git a/tee_launcher/requirements.txt b/tee_launcher/requirements.txt deleted file mode 100644 index 61a0f40c5..000000000 --- a/tee_launcher/requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -pytest==8.3.4 -requests==2.32.4 diff --git a/tee_launcher/test_launcher.py b/tee_launcher/test_launcher.py deleted file mode 100644 index 18d2960ad..000000000 --- a/tee_launcher/test_launcher.py +++ /dev/null @@ -1,32 +0,0 @@ -import unittest - -from launcher import ImageSpec, ResolvedImage, get_manifest_digest - - -class TestLauncher(unittest.TestCase): - def test_get_manifest_digest(self): - # Use a recent (at the time of writing) tag/image digest/manifest digest combination for testing - registry_url = "registry.hub.docker.com" - image_name = "nearone/mpc-node-gcp" - image_hash = ( - "sha256:7e5a6bcb6707d134fc479cc293830c05ce45891f0977d467362cbb7f55cde46b" - ) - expected_manifest_digest = ( - "sha256:005943bccdd401e71c5408d65cf301eeb8bfc3926fe346023912412aafda2490" - ) - tags = ["8805536ab98d924d980a58ecc0518a8c90204bec"] - image = ResolvedImage( - ImageSpec(tags=tags, image_name=image_name, registry=registry_url), - digest=image_hash, - ) - result = get_manifest_digest( - image, - rpc_request_timeout_secs=10, - rpc_request_interval_secs=0.5, - rpc_max_attempts=10, - ) - self.assertEqual(result, expected_manifest_digest) - - -if __name__ == "__main__": - unittest.main() diff --git a/tee_launcher/test_launcher_config.py b/tee_launcher/test_launcher_config.py deleted file mode 100644 index fd33007ec..000000000 --- a/tee_launcher/test_launcher_config.py +++ /dev/null @@ -1,844 +0,0 @@ -# test_launcher_config.py - -import inspect -import json -import os -import tempfile -import tee_launcher.launcher as launcher - - -import pytest -from unittest.mock import mock_open - -from tee_launcher.launcher import ( - load_and_select_hash, - validate_image_hash, - parse_env_lines, - build_docker_cmd, - is_valid_port_mapping, - Platform, - is_safe_env_value, - _has_control_chars, - is_allowed_container_env_key, - MAX_ENV_VALUE_LEN, - MAX_PASSTHROUGH_ENV_VARS, -) -from tee_launcher.launcher import ( - JSON_KEY_APPROVED_HASHES, - ENV_VAR_MPC_HASH_OVERRIDE, - ENV_VAR_DEFAULT_IMAGE_DIGEST, -) - - -# Test constants for user_config content -TEST_MPC_ACCOUNT_ID = "mpc-user-123" - -TEST_PORTS_WITH_INJECTION = "11780:11780,--env BAD=1" - - -def make_digest_json(hashes): - return json.dumps({JSON_KEY_APPROVED_HASHES: hashes}) - - -def parse_env_string(text: str) -> dict: - return parse_env_lines(text.splitlines()) - - -def test_parse_env_lines_basic(): - lines = [ - "# a comment", - "KEY1=value1", - " KEY2 = value2 ", - "", - "INVALIDLINE", - "EMPTY_KEY=", - ] - env = parse_env_lines(lines) - assert env == {"KEY1": "value1", "KEY2": "value2", "EMPTY_KEY": ""} - - -# test user config loading and parsing -def write_temp_config(content: str) -> str: - tmp = tempfile.NamedTemporaryFile(mode="w", delete=False) - tmp.write(content) - tmp.close() - return tmp.name - - -def test_valid_user_config_parsing(): - config_str = """ - MPC_ACCOUNT_ID=account123 - MPC_LOCAL_ADDRESS=127.0.0.1 - # A comment - MPC_ENV=testnet - """ - env = parse_env_string(config_str) - - assert env["MPC_ACCOUNT_ID"] == "account123" - assert env["MPC_LOCAL_ADDRESS"] == "127.0.0.1" - assert env["MPC_ENV"] == "testnet" - - -def test_config_ignores_blank_lines_and_comments(): - config_str = """ - - # This is a comment - MPC_SECRET_STORE_KEY=topsecret - - """ - env = parse_env_string(config_str) - - assert env["MPC_SECRET_STORE_KEY"] == "topsecret" - assert len(env) == 1 - - -def test_config_skips_malformed_lines(): - config_str = """ - GOOD_KEY=value - bad_line_without_equal - ANOTHER_GOOD=ok - = - """ - env = parse_env_string(config_str) - - assert "GOOD_KEY" in env - assert "ANOTHER_GOOD" in env - assert "bad_line_without_equal" not in env - assert "" not in env # ensure empty keys are skipped - - -def test_config_overrides_duplicate_keys(): - config_str = """ - MPC_ACCOUNT_ID=first - MPC_ACCOUNT_ID=second - """ - env = parse_env_string(config_str) - - assert env["MPC_ACCOUNT_ID"] == "second" # last one wins - - -# test valid and invalid host entries and port mappings - - -def test_valid_port_mapping(): - assert is_valid_port_mapping("11780:11780") - assert not is_valid_port_mapping("65536:11780") - assert not is_valid_port_mapping("--volume /:/mnt") - - -def test_build_docker_cmd_sanitizes_ports_and_hosts(): - env = { - "PORTS": TEST_PORTS_WITH_INJECTION, - "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, - } - cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") - - func_name = inspect.currentframe().f_code.co_name - print(f"[{func_name}] CMD:", " ".join(cmd)) - - assert "--env" in cmd - assert f"MPC_ACCOUNT_ID={TEST_MPC_ACCOUNT_ID}" in cmd - assert "-p" in cmd - assert "11780:11780" in cmd - - # Make sure injection strings were filtered - assert not any("BAD=1" in arg for arg in cmd) - assert not any("/:/mnt" in arg for arg in cmd) - - -def test_ports_does_not_allow_volume_injection(): - env = { - "PORTS": "2200:2200,--volume /:/mnt", - "MPC_ACCOUNT_ID": "safe", - } - cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") - - assert "2200:2200" in cmd - assert not any("/:/mnt" in arg for arg in cmd) - - -def test_invalid_env_key_is_ignored(): - env = { - "BAD_KEY": "should_not_be_used", - "MPC_ACCOUNT_ID": "safe", - } - cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") - - assert "should_not_be_used" not in " ".join(cmd) - assert "MPC_ACCOUNT_ID=safe" in cmd - - -def test_mpc_backup_encryption_key_is_allowed(): - env = { - "MPC_BACKUP_ENCRYPTION_KEY_HEX": "0000000000000000000000000000000000000000000000000000000000000000", - } - cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") - - assert ( - "MPC_BACKUP_ENCRYPTION_KEY_HEX=0000000000000000000000000000000000000000000000000000000000000000" - in " ".join(cmd) - ) - - -def test_env_value_with_shell_injection_is_handled_safely(): - env = { - "MPC_ACCOUNT_ID": "safe; rm -rf /", - } - cmd = build_docker_cmd(launcher.Platform.TEE, env, "sha256:abc123") - - assert "--env" in cmd - assert "MPC_ACCOUNT_ID=safe; rm -rf /" in cmd - - -def test_parse_and_build_docker_cmd_full_flow(): - config_str = """ - # Valid entries - MPC_ACCOUNT_ID=test-user - PORTS=11780:11780, --env BAD=oops - IMAGE_HASH=sha256:abc123 - """ - - env = parse_env_string(config_str) - image_hash = env.get("IMAGE_HASH", "sha256:default") - - cmd = build_docker_cmd(launcher.Platform.TEE, env, image_hash) - - print(f"[{inspect.currentframe().f_code.co_name}] CMD: {' '.join(cmd)}") - - assert "--env" in cmd - assert "MPC_ACCOUNT_ID=test-user" in cmd - assert "-p" in cmd - assert "11780:11780" in cmd - - # Confirm malicious injection is blocked - assert not any("--env BAD=oops" in s or "oops" in s for s in cmd) - - -# Test that ensures LD_PRELOAD cannot be injected into the docker command -def test_ld_preload_injection_blocked1(): - # Set up the environment variable with a dangerous LD_PRELOAD value - malicious_env = { - "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, - "--env LD_PRELOAD": "/path/to/my/malloc.so", # The dangerous value - } - - # Call build_docker_cmd to generate the docker command - docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") - - # Check that LD_PRELOAD is not included in the command - assert "--env" in docker_cmd # Ensure there is an env var - assert ( - "LD_PRELOAD" not in docker_cmd - ) # Make sure LD_PRELOAD is not in the generated command - - # Alternatively, if you're using a regex to ensure safe environment variables - assert not any( - "-e " in arg for arg in docker_cmd - ) # Ensure no CLI injection for LD_PRELOAD - - -# Additional tests can go here for host/port validation - - -# Test that ensures LD_PRELOAD cannot be injected through ports -def test_ld_preload_in_ports1(): - # Set up environment with malicious PORTS containing LD_PRELOAD - malicious_env = { - "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, - "PORTS": "11780:11780,--env LD_PRELOAD=/path/to/my/malloc.so", - } - - # Call build_docker_cmd to generate the docker command - docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") - - # Check that LD_PRELOAD is not part of the port mappings in the docker command - assert "-p" in docker_cmd # Ensure port mappings are included - assert "LD_PRELOAD" not in docker_cmd # Ensure LD_PRELOAD is NOT in the command - - # Check that there are no malicious injections - assert not any( - "--env LD_PRELOAD" in arg for arg in docker_cmd - ) # No environment injection - - -# Additional tests could go here to check other edge cases - - -# Test that ensures LD_PRELOAD cannot be injected through mpc account id -def test_ld_preload_in_mpc_account_id(): - # Set up environment containing LD_PRELOAD - malicious_env = { - "MPC_ACCOUNT_ID": f"{TEST_MPC_ACCOUNT_ID}, --env LD_PRELOAD=/path/to/my/malloc.so", - } - - # Call build_docker_cmd to generate the docker command - docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") - - # Check that LD_PRELOAD is not part of the docker command - assert "LD_PRELOAD" not in docker_cmd # Ensure LD_PRELOAD is NOT in the command - - # Check that there are no malicious injections - print(docker_cmd) - assert not any( - "--env LD_PRELOAD" in arg for arg in docker_cmd - ) # No environment injection - - -# Test that ensures LD_PRELOAD cannot be injected into the docker command -def test_ld_preload_injection_blocked2(): - # Set up the environment variable with a dangerous LD_PRELOAD value - malicious_env = { - "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, - "-e LD_PRELOAD": "/path/to/my/malloc.so", # The dangerous value - } - - # Call build_docker_cmd to generate the docker command - docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") - - assert ( - "-e LD_PRELOAD" not in docker_cmd - ) # Make sure LD_PRELOAD is not in the generated command - - -# Additional tests can go here for host/port validation - - -# Test that ensures LD_PRELOAD cannot be injected through ports -def test_ld_preload_in_ports2(): - # Set up environment with malicious PORTS containing LD_PRELOAD - malicious_env = { - "MPC_ACCOUNT_ID": TEST_MPC_ACCOUNT_ID, - "PORTS": "11780:11780,-e LD_PRELOAD=/path/to/my/malloc.so", - } - - # Call build_docker_cmd to generate the docker command - docker_cmd = build_docker_cmd(launcher.Platform.TEE, malicious_env, "sha256:abc123") - - # Check that LD_PRELOAD is not part of the port mappings in the docker command - assert "-p" in docker_cmd # Ensure port mappings are included - assert "LD_PRELOAD" not in docker_cmd # Ensure LD_PRELOAD is NOT in the command - - -def test_json_key_matches_node(): - """ - Ensure the JSON key used by the launcher to read approved image hashes - stays aligned with the MPC node implementation. - mpc/crates/node/src/tee/allowed_image_hashes_watcher.rs -> JSON_KEY_APPROVED_HASHES - - If this test fails, it means the launcher and MPC node are using different - JSON field names, which would break MPC hash propagation. - """ - assert launcher.JSON_KEY_APPROVED_HASHES == "approved_hashes" - - -def test_override_present(monkeypatch): - override_value = "sha256:" + "a" * 64 - approved = ["sha256:" + "b" * 64, override_value, "sha256:" + "c" * 64] - - fake_json = make_digest_json(approved) - - monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: True) - monkeypatch.setattr("builtins.open", mock_open(read_data=fake_json)) - - dstack_config = {ENV_VAR_MPC_HASH_OVERRIDE: override_value} - - selected = load_and_select_hash(dstack_config) - assert selected == override_value - - -def test_override_not_present(monkeypatch): - approved = ["sha256:aaa", "sha256:bbb"] - fake_json = make_digest_json(approved) - - monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: True) - monkeypatch.setattr("builtins.open", mock_open(read_data=fake_json)) - - dstack_config = { - ENV_VAR_MPC_HASH_OVERRIDE: "sha256:xyz" # NOT in list - } - - with pytest.raises(RuntimeError): - load_and_select_hash(dstack_config) - - -def test_no_override_picks_newest(monkeypatch): - approved = ["sha256:newest", "sha256:older", "sha256:oldest"] - fake_json = make_digest_json(approved) - - monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: True) - monkeypatch.setattr("builtins.open", mock_open(read_data=fake_json)) - - selected = load_and_select_hash({}) - assert selected == "sha256:newest" - - -def test_missing_file_fallback(monkeypatch): - # Pretend file does NOT exist - monkeypatch.setattr("tee_launcher.launcher.os.path.isfile", lambda _: False) - - # Valid fallback digest (64 hex chars) - monkeypatch.setenv(ENV_VAR_DEFAULT_IMAGE_DIGEST, "a" * 64) - - selected = load_and_select_hash({}) - assert selected == "sha256:" + "a" * 64 - - -TEST_DIGEST = "sha256:f2472280c437efc00fa25a030a24990ae16c4fbec0d74914e178473ce4d57372" -# Important: ensure the config matches your test image -DSTACK_CONFIG = { - "MPC_IMAGE_TAGS": "83b52da4e2270c688cdd30da04f6b9d3565f25bb", - "MPC_IMAGE_NAME": "nearone/testing", - "MPC_REGISTRY": "registry.hub.docker.com", -} - -# Launcher defaults -RPC_REQUEST_TIMEOUT_SECS = 10.0 -RPC_REQUEST_INTERVAL_SECS = 1.0 -RPC_MAX_ATTEMPTS = 20 - - -# ------------------------------------------------------------------------------------ -# NOTE: Integration Test (External Dependency) -# -# This test validates that `validate_image_hash()` correctly: -# - contacts the real Docker registry, -# - resolves the manifest digest, -# - pulls the remote image, -# - and verifies that its sha256 digest matches the expected immutable value. -# -# The test image is a **pre-built, minimal Docker image containing only a tiny -# binary**, created intentionally for performance and fast pulls. -# This image is uploaded to Docker Hub together. -# -# IMPORTANT: -# • The digest in this test corresponds EXACTLY to that pre-built image. -# • Dockerfile used to build the image can be found at mpc/tee_launcher/launcher-test-image/Dockerfile -# • If the test image is rebuilt, the digest MUST be updated here. -# • If the registry is unavailable or slow, this test may fail. -# • CI will run this only if explicitly enabled. -# -# Please read that file before modifying the digest, registry, or test behavior. -# ------------------------------------------------------------------------------------ -def test_validate_image_hash(): - result = validate_image_hash( - TEST_DIGEST, - DSTACK_CONFIG, - RPC_REQUEST_TIMEOUT_SECS, - RPC_REQUEST_INTERVAL_SECS, - RPC_MAX_ATTEMPTS, - ) - assert result is True, "validate_image_hash() failed for test image" - - -# test launcher support for non TEE images. - - -class DummyProc: - def __init__(self, returncode=0, stdout=b"", stderr=b""): - self.returncode = returncode - self.stdout = stdout - self.stderr = stderr - - -@pytest.fixture -def base_env(monkeypatch): - # Required by launcher - monkeypatch.setenv(launcher.OS_ENV_DOCKER_CONTENT_TRUST, "1") - monkeypatch.setenv(launcher.ENV_VAR_DEFAULT_IMAGE_DIGEST, "sha256:" + "a" * 64) - - -def test_parse_platform_missing(monkeypatch, base_env): - monkeypatch.delenv(launcher.ENV_VAR_PLATFORM, raising=False) - with pytest.raises(RuntimeError): - launcher.parse_platform() - - -@pytest.mark.parametrize("val", ["", "foo", "TEE as", "NON_TEE", "1", "tee", "nontee"]) -def test_parse_platform_invalid(monkeypatch, base_env, val): - monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, val) - with pytest.raises(RuntimeError): - launcher.parse_platform() - - -@pytest.mark.parametrize( - "val,expected", - [ - ("TEE", launcher.Platform.TEE), - ("NONTEE", launcher.Platform.NONTEE), - ], -) -def test_parse_platform_valid(monkeypatch, base_env, val, expected): - monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, val) - assert launcher.parse_platform() is expected - - -def test_extend_rtmr3_nontee_skips_dstack(monkeypatch, base_env): - called = {"count": 0} - - def fake_curl(*args, **kwargs): - called["count"] += 1 - return DummyProc(returncode=0) - - monkeypatch.setattr(launcher, "curl_unix_socket_post", fake_curl) - - launcher.extend_rtmr3(launcher.Platform.NONTEE, "sha256:" + "b" * 64) - assert called["count"] == 0 - - -def test_extend_rtmr3_tee_requires_socket(monkeypatch, base_env): - monkeypatch.setattr(launcher, "is_unix_socket", lambda p: False) - with pytest.raises(RuntimeError): - launcher.extend_rtmr3(launcher.Platform.TEE, "sha256:" + "b" * 64) - - -def test_extend_rtmr3_tee_getquote_fail(monkeypatch, base_env): - monkeypatch.setattr(launcher, "is_unix_socket", lambda p: True) - - def fake_curl(endpoint, payload, capture_output=False): - # Fail only GetQuote - if endpoint == "GetQuote": - return DummyProc(returncode=7) - return DummyProc(returncode=0) - - monkeypatch.setattr(launcher, "curl_unix_socket_post", fake_curl) - with pytest.raises(RuntimeError, match="GetQuote failed"): - launcher.extend_rtmr3(launcher.Platform.TEE, "sha256:" + "b" * 64) - - -def test_extend_rtmr3_tee_emitevent_fail(monkeypatch, base_env): - monkeypatch.setattr(launcher, "is_unix_socket", lambda p: True) - - def fake_curl(endpoint, payload, capture_output=False): - if endpoint == "GetQuote": - return DummyProc(returncode=0) - if endpoint == "EmitEvent": - return DummyProc(returncode=55) - return DummyProc(returncode=0) - - monkeypatch.setattr(launcher, "curl_unix_socket_post", fake_curl) - with pytest.raises(RuntimeError, match="EmitEvent failed"): - launcher.extend_rtmr3(launcher.Platform.TEE, "sha256:" + "b" * 64) - - -def test_build_docker_cmd_nontee_no_dstack_mount(base_env): - env = { - "MPC_ACCOUNT_ID": "x", - # launcher-only env should be ignored - launcher.ENV_VAR_RPC_MAX_ATTEMPTS: "5", - } - cmd = launcher.build_docker_cmd(launcher.Platform.NONTEE, env, "sha256:" + "c" * 64) - s = " ".join(cmd) - - assert "DSTACK_ENDPOINT=" not in s - assert f"{launcher.DSTACK_UNIX_SOCKET}:{launcher.DSTACK_UNIX_SOCKET}" not in s - - -def test_build_docker_cmd_tee_has_dstack_mount(base_env): - env = {"MPC_ACCOUNT_ID": "x"} - cmd = launcher.build_docker_cmd(launcher.Platform.TEE, env, "sha256:" + "c" * 64) - s = " ".join(cmd) - - assert f"DSTACK_ENDPOINT={launcher.DSTACK_UNIX_SOCKET}" in s - assert f"{launcher.DSTACK_UNIX_SOCKET}:{launcher.DSTACK_UNIX_SOCKET}" in s - - -def test_main_tee_fails_closed_before_launch(monkeypatch, base_env): - monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, launcher.Platform.TEE.value) - - monkeypatch.setattr(launcher, "is_unix_socket", lambda p: False) - - # prevent any real docker/network - monkeypatch.setattr( - launcher, "load_and_select_hash", lambda cfg: "sha256:" + "d" * 64 - ) - monkeypatch.setattr(launcher, "validate_image_hash", lambda *a, **k: True) - monkeypatch.setattr( - launcher, - "launch_mpc_container", - lambda *a, **k: (_ for _ in ()).throw(AssertionError("should not launch")), - ) - - with pytest.raises(RuntimeError, match="requires dstack unix socket"): - launcher.main() - - -def test_main_nontee_skips_extend_and_launches(monkeypatch, base_env): - monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, "NONTEE") - monkeypatch.setattr( - launcher, "is_unix_socket", lambda p: False - ) # should not matter - - monkeypatch.setattr( - launcher, "load_and_select_hash", lambda cfg: "sha256:" + "d" * 64 - ) - monkeypatch.setattr(launcher, "validate_image_hash", lambda *a, **k: True) - - called = {"extend": 0, "launch": 0} - monkeypatch.setattr( - launcher, - "extend_rtmr3", - lambda platform, h: called.__setitem__("extend", called["extend"] + 1), - ) - monkeypatch.setattr( - launcher, - "launch_mpc_container", - lambda platform, h, cfg: called.__setitem__("launch", called["launch"] + 1), - ) - - launcher.main() - assert called["extend"] == 1 - assert called["launch"] == 1 - - -def assert_subsequence(seq, subseq): - it = iter(seq) - for x in subseq: - for y in it: - if y == x: - break - else: - raise AssertionError(f"Missing subsequence item: {x}\nseq={seq}") - - -def test_main_nontee_builds_expected_mpc_docker_cmd(monkeypatch, tmp_path): - """ - Verify that launcher.main() builds the correct MPC docker command in NONTEE mode. - - Steps: - 1. Configure the launcher to run with PLATFORM=NONTEE. - 2. Set required environment variables (DOCKER_CONTENT_TRUST, DEFAULT_IMAGE_DIGEST). - 3. Create a temporary user_config file with MPC env vars, ports, and extra hosts. - 4. Simulate a missing IMAGE_DIGEST_FILE so the launcher falls back to DEFAULT_IMAGE_DIGEST. - 5. Stub image validation and docker interactions to avoid real network or docker usage. - 6. Invoke launcher.main(). - 7. Capture the docker run command used to start the MPC container. - 8. Assert that the command: - - Includes expected MPC configuration (env vars, ports, hosts, volumes). - - Does NOT include dstack socket mounts or DSTACK_ENDPOINT. - - Filters out injection attempts in ports and hosts. - - Uses the expected full image digest. - """ - # --- Arrange: environment (NONTEE) --- - monkeypatch.setenv(launcher.ENV_VAR_PLATFORM, launcher.Platform.NONTEE.value) - monkeypatch.setenv(launcher.OS_ENV_DOCKER_CONTENT_TRUST, "1") - - default_digest = "sha256:" + "a" * 64 - monkeypatch.setenv(launcher.ENV_VAR_DEFAULT_IMAGE_DIGEST, default_digest) - - # Provide a temp user config file so main() passes env into build_docker_cmd() - user_config = tmp_path / "user_config" - user_config.write_text( - "\n".join( - [ - f"MPC_ACCOUNT_ID={TEST_MPC_ACCOUNT_ID}", - f"PORTS={TEST_PORTS_WITH_INJECTION}", # injection should be ignored - ] - ) - + "\n" - ) - - # Point launcher at our temp config - monkeypatch.setattr(launcher, "DSTACK_USER_CONFIG_FILE", str(user_config)) - - # Make IMAGE_DIGEST_FILE "missing" so DEFAULT_IMAGE_DIGEST is used - def fake_isfile(path: str) -> bool: - if path == launcher.IMAGE_DIGEST_FILE: - return False - if path == str(user_config): - return True - return os.path.isfile(path) - - monkeypatch.setattr(launcher.os.path, "isfile", fake_isfile) - - # Avoid network/docker verification in validate_image_hash - monkeypatch.setattr(launcher, "validate_image_hash", lambda *a, **k: True) - - # Avoid remove_existing_container touching real docker - monkeypatch.setattr(launcher, "check_output", lambda *a, **k: "") - - # Capture the docker run command used to launch MPC - captured = {"docker_run_cmd": None} - - def fake_run(cmd, *args, **kwargs): - # cmd is a list[str] - if ( - isinstance(cmd, list) - and len(cmd) >= 2 - and cmd[0] == "docker" - and cmd[1] == "run" - ): - captured["docker_run_cmd"] = cmd - return DummyProc(returncode=0) - return DummyProc(returncode=0) - - monkeypatch.setattr(launcher, "run", fake_run) - - # --- Act --- - launcher.main() - - # --- Assert --- - cmd = captured["docker_run_cmd"] - assert cmd is not None, "Expected launcher to invoke 'docker run' for MPC container" - - cmd_str = " ".join(cmd) - - # NONTEE invariants - assert "DSTACK_ENDPOINT=" not in cmd_str - assert f"{launcher.DSTACK_UNIX_SOCKET}:{launcher.DSTACK_UNIX_SOCKET}" not in cmd_str - - # Expected env propagation + sanitization - assert f"MPC_ACCOUNT_ID={TEST_MPC_ACCOUNT_ID}" in cmd_str - assert "-p" in cmd and "11780:11780" in cmd_str - - # Injection strings filtered out - assert "BAD=1" not in cmd_str - assert "/:/mnt" not in cmd_str - - # Required mounts / flags from build_docker_cmd - assert "--security-opt" in cmd_str - assert "no-new-privileges:true" in cmd_str - assert "/tapp:/tapp:ro" in cmd_str - assert "shared-volume:/mnt/shared" in cmd_str - assert "mpc-data:/data" in cmd_str - assert f"--name {launcher.MPC_CONTAINER_NAME}" in cmd_str - - # Image digest should be the final argument and should be the FULL digest - assert cmd[-1] == default_digest - - expected_core = [ - "docker", - "run", - "--security-opt", - "no-new-privileges:true", - "-v", - "/tapp:/tapp:ro", - "-v", - "shared-volume:/mnt/shared", - "-v", - "mpc-data:/data", - "--name", - launcher.MPC_CONTAINER_NAME, - "--detach", - ] - assert_subsequence(cmd, expected_core) - - -def _base_env(): - # Minimal env for build_docker_cmd (launcher will add required MPC_IMAGE_HASH etc itself) - return { - "MPC_ACCOUNT_ID": "mpc-user-123", - "MPC_CONTRACT_ID": "contract.near", - "MPC_ENV": "testnet", - "MPC_HOME_DIR": "/data", - "NEAR_BOOT_NODES": "boot1,boot2", - "RUST_LOG": "info", - } - - -def test_has_control_chars_rejects_newline_and_cr(): - assert _has_control_chars("a\nb") is True - assert _has_control_chars("a\rb") is True - - -def test_has_control_chars_rejects_other_control_chars_but_allows_tab(): - # tab is allowed by the Python helper in the patched launcher - assert _has_control_chars("a\tb") is False - # ASCII control char 0x1F should be rejected - assert _has_control_chars("a" + chr(0x1F) + "b") is True - - -def test_is_safe_env_value_rejects_control_chars(): - assert is_safe_env_value("ok\nno") is False - assert is_safe_env_value("ok\rno") is False - assert is_safe_env_value("ok" + chr(0x1F) + "no") is False - - -def test_is_safe_env_value_rejects_ld_preload_substring(): - assert is_safe_env_value("LD_PRELOAD=/tmp/x.so") is False - assert is_safe_env_value("foo LD_PRELOAD bar") is False - - -def test_is_safe_env_value_rejects_too_long_value(): - assert is_safe_env_value("a" * (MAX_ENV_VALUE_LEN + 1)) is False - assert is_safe_env_value("a" * MAX_ENV_VALUE_LEN) is True - - -def testis_allowed_container_env_key_allows_mpc_prefix_uppercase(): - assert is_allowed_container_env_key("MPC_FOO") is True - assert is_allowed_container_env_key("MPC_FOO_123") is True - assert is_allowed_container_env_key("MPC_A_B_C") is True - - -def testis_allowed_container_env_key_rejects_lowercase_or_invalid_chars(): - assert is_allowed_container_env_key("MPC_foo") is False - assert is_allowed_container_env_key("MPC-FOO") is False - assert is_allowed_container_env_key("MPC.FOO") is False - assert is_allowed_container_env_key("MPC_") is False - - -def testis_allowed_container_env_key_allows_compat_non_mpc_keys(): - assert is_allowed_container_env_key("RUST_LOG") is True - assert is_allowed_container_env_key("RUST_BACKTRACE") is True - assert is_allowed_container_env_key("NEAR_BOOT_NODES") is True - - -def testis_allowed_container_env_key_denies_sensitive_keys(): - assert is_allowed_container_env_key("MPC_P2P_PRIVATE_KEY") is False - assert is_allowed_container_env_key("MPC_ACCOUNT_SK") is False - - -def test_build_docker_cmd_allows_arbitrary_mpc_prefix_env_vars(): - env = _base_env() - env["MPC_NEW_FEATURE_FLAG"] = "1" - env["MPC_SOME_CONFIG"] = "value" - - cmd = build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) - - cmd_str = " ".join(cmd) - assert "--env MPC_NEW_FEATURE_FLAG=1" in cmd_str - assert "--env MPC_SOME_CONFIG=value" in cmd_str - - -def test_build_docker_cmd_blocks_sensitive_mpc_private_keys(): - env = _base_env() - env["MPC_P2P_PRIVATE_KEY"] = "supersecret" - env["MPC_ACCOUNT_SK"] = "supersecret2" - - cmd = build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) - cmd_str = " ".join(cmd) - - assert "MPC_P2P_PRIVATE_KEY" not in cmd_str - assert "MPC_ACCOUNT_SK" not in cmd_str - - -def test_build_docker_cmd_rejects_env_value_with_newline(): - env = _base_env() - env["MPC_NEW_FEATURE_FLAG"] = "ok\nbad" - - cmd = build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) - cmd_str = " ".join(cmd) - - # It should be ignored (not passed) - assert "MPC_NEW_FEATURE_FLAG" not in cmd_str - - -def test_build_docker_cmd_enforces_max_env_count_cap(): - env = _base_env() - # add many MPC_* keys to exceed cap - for i in range(MAX_PASSTHROUGH_ENV_VARS + 1): - env[f"MPC_X_{i}"] = "1" - - with pytest.raises(RuntimeError, match="Too many env vars"): - build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) - - -def test_build_docker_cmd_enforces_total_env_bytes_cap(): - env = _base_env() - - # Each env contributes ~ len(key)+1+MAX_ENV_VALUE_LEN bytes. - # With MAX_ENV_VALUE_LEN=1024 and MAX_TOTAL_ENV_BYTES=32768, ~35-40 vars will exceed the cap. - for i in range(40): - env[f"MPC_BIG_{i}"] = "a" * MAX_ENV_VALUE_LEN - - with pytest.raises(RuntimeError, match="Total env payload too large"): - build_docker_cmd(Platform.NONTEE, env, "sha256:" + "a" * 64) From 58b2d4c4f8ad593c33566c8fa2d0c079e1fc074a Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 15:52:19 +0100 Subject: [PATCH 148/176] add rw to allow launcher to create config file --- crates/contract/assets/launcher_docker_compose.yaml.template | 2 +- deployment/cvm-deployment/launcher_docker_compose.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/contract/assets/launcher_docker_compose.yaml.template b/crates/contract/assets/launcher_docker_compose.yaml.template index 612afb397..bcf258c4f 100644 --- a/crates/contract/assets/launcher_docker_compose.yaml.template +++ b/crates/contract/assets/launcher_docker_compose.yaml.template @@ -15,7 +15,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - /var/run/dstack.sock:/var/run/dstack.sock - /tapp:/tapp:ro - - shared-volume:/mnt/shared:ro + - shared-volume:/mnt/shared:rw security_opt: - no-new-privileges:true diff --git a/deployment/cvm-deployment/launcher_docker_compose.yaml b/deployment/cvm-deployment/launcher_docker_compose.yaml index 50f9b7820..a5813e80a 100644 --- a/deployment/cvm-deployment/launcher_docker_compose.yaml +++ b/deployment/cvm-deployment/launcher_docker_compose.yaml @@ -15,7 +15,7 @@ services: - /var/run/docker.sock:/var/run/docker.sock - /var/run/dstack.sock:/var/run/dstack.sock - /tapp:/tapp:ro - - shared-volume:/mnt/shared:ro + - shared-volume:/mnt/shared:rw security_opt: - no-new-privileges:true From 6e66e17f43c8954f1ae66a11a5a8540db5ab6146 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 15:53:32 +0100 Subject: [PATCH 149/176] explicit false write open option --- crates/tee-launcher/src/main.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 811ae8c2f..23866f2dd 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -69,6 +69,7 @@ async fn run() -> Result<(), LauncherError> { let approved_hashes_file = std::fs::OpenOptions::new() .read(true) + .write(false) .open(IMAGE_DIGEST_FILE) .map_err(|source| LauncherError::FileRead { path: IMAGE_DIGEST_FILE.to_string(), From 949d3b5358820bbbd93e641648a1c1bbd07bcf27 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 16:22:44 +0100 Subject: [PATCH 150/176] remove tee-launcher tests --- .github/workflows/ci.yml | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f58d0cc89..894dd3b95 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -255,37 +255,6 @@ jobs: # `neard` is already downloaded in the step, `download-neard`, above pytest -m "not ci_excluded" -s -x --skip-nearcore-build - tee-launcher-tests: - name: "TEE Launcher: pytests" - runs-on: warp-ubuntu-2404-x64-2x - timeout-minutes: 60 - permissions: - contents: read - - steps: - - name: Checkout repository - uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - with: - persist-credentials: false - - - name: Setup python - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 - with: - python-version: "3.11" - - - name: Setup virtualenv - run: | - python3 -m venv tee_launcher/venv - source tee_launcher/venv/bin/activate - cd tee_launcher - pip install -r requirements.txt - - - name: Run pytest - run: | - source tee_launcher/venv/bin/activate - cd tee_launcher - PYTHONPATH=. pytest -vsx - fast-ci-checks: name: "Fast CI checks" runs-on: warp-ubuntu-2404-x64-8x From 353572ea100b6f01681d928421b396d2bdfb3078 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 16:27:23 +0100 Subject: [PATCH 151/176] fix path to launcher docker compose --- scripts/check-mpc-node-docker-starts.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/check-mpc-node-docker-starts.sh b/scripts/check-mpc-node-docker-starts.sh index 9f9cbe077..6fb35d18f 100755 --- a/scripts/check-mpc-node-docker-starts.sh +++ b/scripts/check-mpc-node-docker-starts.sh @@ -21,7 +21,7 @@ done : "${LAUNCHER_IMAGE_NAME:=mpc-launcher-nontee}" if $USE_LAUNCHER; then - cd tee_launcher + cd ../deployment/cvm-deployment export LAUNCHER_IMAGE_NAME docker compose -f launcher_docker_compose_nontee.yaml up -d sleep 10 From dbfb3b392b7f8905110cc813258324bb8e56afc4 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 16:35:14 +0100 Subject: [PATCH 152/176] use relative path of the git reo --- scripts/check-mpc-node-docker-starts.sh | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/check-mpc-node-docker-starts.sh b/scripts/check-mpc-node-docker-starts.sh index 6fb35d18f..a711bdd68 100755 --- a/scripts/check-mpc-node-docker-starts.sh +++ b/scripts/check-mpc-node-docker-starts.sh @@ -2,6 +2,9 @@ set -euo pipefail +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" + USE_LAUNCHER=false for arg in "$@"; do @@ -21,7 +24,7 @@ done : "${LAUNCHER_IMAGE_NAME:=mpc-launcher-nontee}" if $USE_LAUNCHER; then - cd ../deployment/cvm-deployment + cd "$REPO_ROOT/deployment/cvm-deployment" export LAUNCHER_IMAGE_NAME docker compose -f launcher_docker_compose_nontee.yaml up -d sleep 10 @@ -36,6 +39,8 @@ else touch /tmp/image-digest.bin # Test container startup - fail if container can't start # Start container in background and check status after 60 seconds + # + # TODO: REMOVE ALL ENVS PASSED CONTAINER_ID=$(docker run -d \ -v /tmp/:/data \ -e MPC_HOME_DIR="/data" \ From 09d527145f670f7b792418a511ee5aaf677f0038 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 17:04:32 +0100 Subject: [PATCH 153/176] update launcher hash to current branch --- deployment/cvm-deployment/launcher_docker_compose.yaml | 2 +- deployment/cvm-deployment/launcher_docker_compose_nontee.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/cvm-deployment/launcher_docker_compose.yaml b/deployment/cvm-deployment/launcher_docker_compose.yaml index a5813e80a..9e9ec0fda 100644 --- a/deployment/cvm-deployment/launcher_docker_compose.yaml +++ b/deployment/cvm-deployment/launcher_docker_compose.yaml @@ -2,7 +2,7 @@ version: '3.8' services: launcher: - image: nearone/mpc-launcher@sha256:5973f1b0510f580ec1cada64310a430f4d7a380a69e57493b055bbb9cdd6ae63 + image: nearone/mpc-launcher@sha256:sha256:e0816222685dadecec3a70c303dc05dbe098aa006d1236748ebd6701afce50de container_name: launcher diff --git a/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml index 42ec10b8d..e9cdaf9d5 100644 --- a/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml +++ b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml @@ -1,6 +1,6 @@ services: launcher: - image: nearone/mpc-launcher@sha256:5973f1b0510f580ec1cada64310a430f4d7a380a69e57493b055bbb9cdd6ae63 + image: nearone/mpc-launcher@sha256:e0816222685dadecec3a70c303dc05dbe098aa006d1236748ebd6701afce50de container_name: "${LAUNCHER_IMAGE_NAME}" environment: From e4f44aa3d16bc5cdf072864361cbc74668cf0d79 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 17:07:00 +0100 Subject: [PATCH 154/176] add docs/UPDATING_LAUNCHER.md to exempt kebab case --- scripts/check-kebab-case-files.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/scripts/check-kebab-case-files.sh b/scripts/check-kebab-case-files.sh index eda77900a..d75ba6752 100755 --- a/scripts/check-kebab-case-files.sh +++ b/scripts/check-kebab-case-files.sh @@ -30,6 +30,7 @@ EXEMPT_FILES=( launcher_docker_compose.yaml launcher_docker_compose_nontee.yaml launcher_image_compose.yaml + UPDATING_LAUNCHER.md ) # Build a regex that matches any of the allowed non-kebab-case filenames From 88564aeb482909fc230ff111b6839b847d4869e8 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 17:07:54 +0100 Subject: [PATCH 155/176] update paths to docs launcher --- crates/test-utils/assets/README.md | 5 +---- localnet/tee/scripts/single-node-readme.md | 23 +++++++++++++++++----- 2 files changed, 19 insertions(+), 9 deletions(-) diff --git a/crates/test-utils/assets/README.md b/crates/test-utils/assets/README.md index 15c781905..c0445a918 100644 --- a/crates/test-utils/assets/README.md +++ b/crates/test-utils/assets/README.md @@ -1,7 +1,6 @@ # Updating Test Assets -Updating test assets is needed when updating launcher code (or when updating other measured components). See [UPDATING_LAUNCHER.md](../../../tee_launcher/UPDATING_LAUNCHER.md) - +Updating test assets is needed when updating launcher code (or when updating other measured components). See [UPDATING_LAUNCHER.md](../../../docs/UPDATING_LAUNCHER.md) To update the test asset files, fetch `/public_data` from the MPC node’s public HTTP endpoint and save the response to a JSON file. @@ -12,11 +11,9 @@ Example: curl http://:/public_data -o public_data.json ``` - See [single-node-readme.md](../../../localnet/tee/scripts/single-node-readme.md) for automation script that will launch a TEE MPC node, collect the attestation, and save the public data into /tmp/%user/public_data.json - ## Steps 1. Change into the `crates/test_utils/assets` directory: diff --git a/localnet/tee/scripts/single-node-readme.md b/localnet/tee/scripts/single-node-readme.md index 506712e66..bea9fcfa2 100644 --- a/localnet/tee/scripts/single-node-readme.md +++ b/localnet/tee/scripts/single-node-readme.md @@ -1,14 +1,16 @@ # Run a Single MPC Node on Localnet (dstack CVM) This script: + - Creates/reuses one NEAR account on localnet -- Deploys one MPC node into dstack CVM (node is not guaranteed to be fully functional) +- Deploys one MPC node into dstack CVM (node is not guaranteed to be fully functional) - Fetches `/public_data` and saves it to JSON It is used to generate real attestation data for testing only: -See [UPDATING_LAUNCHER.md](../../../tee_launcher/UPDATING_LAUNCHER.md) +See [UPDATING_LAUNCHER.md](../../../docs/UPDATING_LAUNCHER.md) ## Prerequisites + - Local NEAR network running: `NEAR_ENV=mpc-localnet neard --home ~/.near/mpc-localnet run` - `mpc-localnet` configured in `near` CLI - dstack running (`http://127.0.0.1:10000`) @@ -17,47 +19,58 @@ See [UPDATING_LAUNCHER.md](../../../tee_launcher/UPDATING_LAUNCHER.md) ## Setup variables ### Required + ```bash # dstack base path (the folder containing vmm or vmm-data folder) export BASE_PATH=/path/to/dstack # external machine IP (you can use: # ip -4 -o addr show scope global | awk '{print $4}' | cut -d/ -f1 | grep -Ev '^(10\.|192\.168\.|172\.(1[6-9]|2[0-9]|3[0-1])\.)' export MACHINE_IP= -# the mpc docker image tag. +# the mpc docker image tag. # 1. make sure it is available on docker hub. -# 2. make sure that DEFAULT_IMAGE_DIGEST=sha256: in mpc/tee_launcher/launcher_docker_compose.yaml corresponds to that tag, by calling -# `docker pull nearone/mpc-node:$MPC_IMAGE_TAGS` and then +# 2. make sure that DEFAULT_IMAGE_DIGEST=sha256: in mpc/tee_launcher/launcher_docker_compose.yaml corresponds to that tag, by calling +# `docker pull nearone/mpc-node:$MPC_IMAGE_TAGS` and then # `docker inspect --format='{{.Id}}' nearone/mpc-node:$MPC_IMAGE_TAGS` export MPC_IMAGE_TAGS=3.3.0 ``` ### dstack port + If dstack VMM is not on port 10000: + ```bash export VMM_RPC=http://127.0.0.1: ``` ### Optional + If you want to use specific NEAR accounts name instead of defaults: + ```bash export NODE_ACCOUNT=frodo.test.near export CONTRACT_ACCOUNT=mpc-contract.test.near ``` ## Run + From the MPC repo root: + ```bash bash ./localnet/tee/scripts/single-node.sh ``` ## Output + - The script prints the work directory and all assigned ports at startup - Public endpoint: `http://:/public_data` - Saved JSON: `/public_data.json` (path printed by the script) ## Cleanup + To remove the CVM after you're done: + ```bash BASE_PATH=/path/to/dstack bash ./localnet/tee/scripts/single-node.sh --cleanup ``` + The exact command is printed at the end of a successful run. From 4a60f4c46b90acd3f46b16a213626a9b919da48e Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 17:10:07 +0100 Subject: [PATCH 156/176] fix broken links --- docs/localnet/tee-localnet.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/localnet/tee-localnet.md b/docs/localnet/tee-localnet.md index 3d6b523e1..5fe315e63 100644 --- a/docs/localnet/tee-localnet.md +++ b/docs/localnet/tee-localnet.md @@ -61,7 +61,7 @@ If any of these ports are already in use on your system, you will need to update The most important ports are the Frodo/Sam TLS ports: 13001 / 13002. These appear in the following files: -- frodo/sam.conf +- frodo/sam.toml - frodo/sam.env - init_tee.json @@ -72,8 +72,8 @@ There are additional ports defined in frodo/sam.env, but you may change those to Those are the recommended configuration settings: you will need the following files: -* [docker-compose.yml](../../tee_launcher/launcher_docker_compose.yaml) -* [frodo.conf](../../deployment/localnet/tee/frodo.conf) / [sam.conf](../../deployment/localnet/tee/sam.conf) +* [docker-compose.yml](../../deployment/cvm-deployment/launcher_docker_compose.yaml) +* [frodo.toml](../../deployment/localnet/tee/frodo.toml) / [sam.toml](../../deployment/localnet/tee/sam.toml) * [frodo.env](../../deployment/localnet/tee/frodo.env)/ [sam.env](../../deployment/localnet/tee/sam.env) - if you use the deployment script @@ -105,9 +105,9 @@ Define the machine's external IP once export MACHINE_IP=$(curl -4 -s ifconfig.me) # or use known IP for the machine ``` -#### Environment File (`frodo/sam.conf`, `frodo/sam.env`) ) +#### Environment File (`frodo/sam.toml`, `frodo/sam.env`) ) -Update Sam/Frodo.conf fields: +Update Sam/Frodo.toml fields: ```env @@ -127,7 +127,7 @@ $Docker inspect nearone/mpc-node:main_3.0.3 | grep "Id" You can start the nodes **manually** as described in the Operator Guide, or you can start them using the `deploy-launcher.sh` script as shown below. -Once all paths and configuration files (`*.env` and `*.conf`) are prepared, you can launch each MPC node (Frodo and Sam) using the `deploy-launcher.sh` helper script. +Once all paths and configuration files (`*.env` and `*.toml`) are prepared, you can launch each MPC node (Frodo and Sam) using the `deploy-launcher.sh` helper script. #### 1. Move into the `tee_launcher` Directory @@ -153,11 +153,11 @@ export BASE_PATH="dstask base path" #### 4. Replace ${MACHINE_IP} inside the config files ```bash -envsubst '${MACHINE_IP}' < deployment/localnet/tee/frodo.conf > "/tmp/$USER/frodo.conf" +envsubst '${MACHINE_IP}' < deployment/localnet/tee/frodo.toml > "/tmp/$USER/frodo.toml" ``` ```bash -envsubst '${MACHINE_IP}' < deployment/localnet/tee/sam.conf > "/tmp/$USER/sam.conf" +envsubst '${MACHINE_IP}' < deployment/localnet/tee/sam.toml > "/tmp/$USER/sam.toml" ``` #### 5. Start the Frodo MPC Node From baf1e1684ac2e151d74e6a63b92d29952bbdf08c Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 17:16:48 +0100 Subject: [PATCH 157/176] update script --- scripts/build-and-verify-launcher-docker-image.sh | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/scripts/build-and-verify-launcher-docker-image.sh b/scripts/build-and-verify-launcher-docker-image.sh index 1f802e934..fc54247d4 100755 --- a/scripts/build-and-verify-launcher-docker-image.sh +++ b/scripts/build-and-verify-launcher-docker-image.sh @@ -2,6 +2,9 @@ set -euo pipefail +LAUNCHER_COMPOSE="deployment/cvm-deployment/launcher_docker_compose.yaml" +LAUNCHER_TEMPLATE="crates/contract/assets/launcher_docker_compose.yaml.template" + ./deployment/build-images.sh --launcher # Step 1: Get the built launcher image's manifest hash @@ -14,8 +17,8 @@ built_launcher_hash=$(sha256sum $temp_dir/manifest.json | cut -d' ' -f1) echo "Built launcher image hash: $built_launcher_hash" # Step 2: Extract the launcher and MPC hashes from the deployment compose file -deployed_launcher_hash=$(grep -o 'nearone/mpc-launcher@sha256:.*' tee_launcher/launcher_docker_compose.yaml | grep -o '@sha256:.*' | cut -c 9-) -deployed_mpc_hash=$(grep 'DEFAULT_IMAGE_DIGEST=sha256:' tee_launcher/launcher_docker_compose.yaml | grep -o 'sha256:.*' | cut -c 8-) +deployed_launcher_hash=$(grep -o 'nearone/mpc-launcher@sha256:.*' $LAUNCHER_COMPOSE | grep -o '@sha256:.*' | cut -c 9-) +deployed_mpc_hash=$(grep 'DEFAULT_IMAGE_DIGEST=sha256:' $LAUNCHER_COMPOSE | grep -o 'sha256:.*' | cut -c 8-) # Step 3: Fill the contract template with the deployment compose hashes and compare # This verifies both: @@ -24,12 +27,12 @@ deployed_mpc_hash=$(grep 'DEFAULT_IMAGE_DIGEST=sha256:' tee_launcher/launcher_do filled_template=$(sed \ -e "s/{{LAUNCHER_IMAGE_HASH}}/${deployed_launcher_hash}/" \ -e "s/{{DEFAULT_IMAGE_DIGEST_HASH}}/${deployed_mpc_hash}/" \ - crates/contract/assets/launcher_docker_compose.yaml.template) + $LAUNCHER_TEMPLATE) -if ! diff <(echo "$filled_template") tee_launcher/launcher_docker_compose.yaml > /dev/null; then +if ! diff <(echo "$filled_template") $LAUNCHER_COMPOSE > /dev/null; then echo "Template structure verification failed" echo "The contract template (filled with deployment hashes) does not match the deployment compose file." - diff <(echo "$filled_template") tee_launcher/launcher_docker_compose.yaml || true + diff <(echo "$filled_template") $LAUNCHER_COMPOSE || true exit 1 fi echo "Template structure verified: contract template matches deployment compose" From 96c21c890cdc496c1b475505ead8efca84c5609a Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Tue, 17 Mar 2026 17:52:26 +0100 Subject: [PATCH 158/176] add rw for nontee as well --- deployment/cvm-deployment/launcher_docker_compose_nontee.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml index e9cdaf9d5..167ac7ef6 100644 --- a/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml +++ b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml @@ -11,7 +11,7 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock - ./user-config.toml:/tapp/user_config:ro - - shared-volume:/mnt/shared + - shared-volume:/mnt/shared:rw - mpc-data:/data security_opt: From 88ec98a26f642f658e35f45c073eb9833a1b2964 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Wed, 18 Mar 2026 11:14:35 +0100 Subject: [PATCH 159/176] launcher intercepts tee authority config --- crates/launcher-interface/src/lib.rs | 3 ++ crates/launcher-interface/src/types.rs | 18 +++++++- crates/node/src/cli.rs | 23 +++------- crates/node/src/config.rs | 3 +- crates/node/src/config/start.rs | 51 +++++----------------- crates/node/src/run.rs | 24 +++++----- crates/primitives/src/hash.rs | 4 ++ crates/tee-launcher/src/main.rs | 33 ++++++++++++-- crates/tee-launcher/src/types.rs | 8 ++-- deployment/cvm-deployment/user-config.toml | 3 -- deployment/localnet/tee/frodo.toml | 3 -- deployment/localnet/tee/sam.toml | 9 ++-- deployment/testnet/frodo.toml | 3 -- deployment/testnet/sam.toml | 3 -- libs/nearcore | 2 +- 15 files changed, 90 insertions(+), 100 deletions(-) diff --git a/crates/launcher-interface/src/lib.rs b/crates/launcher-interface/src/lib.rs index ab9651008..4c5a17883 100644 --- a/crates/launcher-interface/src/lib.rs +++ b/crates/launcher-interface/src/lib.rs @@ -2,3 +2,6 @@ pub mod types; /// event name for image digest pub const MPC_IMAGE_HASH_EVENT: &str = "mpc-image-digest"; + +pub const DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL: &str = + "https://cloud-api.phala.network/api/v1/attestations/verify"; diff --git a/crates/launcher-interface/src/types.rs b/crates/launcher-interface/src/types.rs index 2aff4eddd..7c1a06477 100644 --- a/crates/launcher-interface/src/types.rs +++ b/crates/launcher-interface/src/types.rs @@ -1,11 +1,27 @@ -use std::fmt; use std::str::FromStr; +use std::{fmt, path::PathBuf}; use mpc_primitives::hash::MpcDockerImageHash; use serde::{Deserialize, Serialize}; const SHA256_PREFIX: &str = "sha256:"; +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum TeeAuthorityConfig { + Local, + Dstack { + dstack_endpoint: String, + // TODO(#2333): use URL type for this type + quote_upload_url: String, + /// Hex representation of the hash of the running image. Only required in TEE. + image_hash: DockerSha256Digest, + /// Path to the file where the node writes the latest allowed hash. + /// If not set, assumes running outside of TEE and skips image hash monitoring. + latest_allowed_hash_file_path: PathBuf, + }, +} + #[derive(Debug, Serialize, Deserialize)] pub struct ApprovedHashes { pub approved_hashes: near_mpc_bounded_collections::NonEmptyVec, diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index da62862b8..e6ca9edfd 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -1,7 +1,7 @@ use crate::{ config::{ load_config_file, ChainId, ConfigFile, DownloadConfigType, GcpStartConfig, NearInitConfig, - SecretsStartConfig, StartConfig, TeeAuthorityStartConfig, TeeStartConfig, + SecretsStartConfig, StartConfig, }, keyshare::{ compat::legacy_ecdsa_key_from_keyshares, @@ -12,6 +12,7 @@ use crate::{ }; use clap::{Args, Parser, Subcommand, ValueEnum}; use hex::FromHex; +use launcher_interface::types::TeeAuthorityConfig; use std::path::PathBuf; use tee_authority::tee_authority::{DEFAULT_DSTACK_ENDPOINT, DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL}; use url::Url; @@ -90,9 +91,6 @@ pub struct StartCmd { pub gcp_keyshare_secret_id: Option, #[arg(env("GCP_PROJECT_ID"))] pub gcp_project_id: Option, - /// TEE authority config - #[command(subcommand)] - pub tee_authority: CliTeeAuthorityConfig, /// TEE related configuration settings. #[command(flatten)] pub image_hash_config: CliImageHashConfig, @@ -144,20 +142,9 @@ impl StartCmd { backup_encryption_key_hex: self.backup_encryption_key_hex, }, near_init: None, - tee: TeeStartConfig { - authority: match self.tee_authority { - CliTeeAuthorityConfig::Local => TeeAuthorityStartConfig::Local, - CliTeeAuthorityConfig::Dstack { - dstack_endpoint, - quote_upload_url, - } => TeeAuthorityStartConfig::Dstack { - dstack_endpoint, - quote_upload_url: quote_upload_url.to_string(), - }, - }, - image_hash: self.image_hash_config.image_hash, - latest_allowed_hash_file: self.image_hash_config.latest_allowed_hash_file, - }, + // dstack and TEE is not supported with StartCmd, as it will be removed + // in #2334, and not used by the rust launcher. + tee: TeeAuthorityConfig::Local, gcp, node: config, } diff --git a/crates/node/src/config.rs b/crates/node/src/config.rs index 9c2296898..af7226f25 100644 --- a/crates/node/src/config.rs +++ b/crates/node/src/config.rs @@ -13,10 +13,9 @@ use std::{ path::Path, }; -mod start; +pub(crate) mod start; pub use start::{ ChainId, DownloadConfigType, GcpStartConfig, NearInitConfig, SecretsStartConfig, StartConfig, - TeeAuthorityStartConfig, TeeStartConfig, }; mod foreign_chains; diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index a1b906a8c..2073de1bd 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -1,10 +1,10 @@ use super::ConfigFile; use anyhow::Context; +use launcher_interface::types::TeeAuthorityConfig; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tee_authority::tee_authority::{ - DstackTeeAuthorityConfig, LocalTeeAuthorityConfig, TeeAuthority, DEFAULT_DSTACK_ENDPOINT, - DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL, + DstackTeeAuthorityConfig, LocalTeeAuthorityConfig, TeeAuthority, }; use url::Url; @@ -17,7 +17,7 @@ pub struct StartConfig { /// Encryption keys and backup settings. pub secrets: SecretsStartConfig, /// TEE authority and image hash monitoring settings. - pub tee: TeeStartConfig, + pub tee: TeeAuthorityConfig, /// GCP keyshare storage settings. Optional — omit if not using GCP. pub gcp: Option, /// NEAR node initialization settings. Required for `start-with-config-file` @@ -136,20 +136,6 @@ impl std::fmt::Debug for SecretsStartConfig { } } -/// TEE-related configuration. -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct TeeStartConfig { - /// TEE authority configuration. - pub authority: TeeAuthorityStartConfig, - /// Hex representation of the hash of the running image. Only required in TEE. - #[serde(default)] - pub image_hash: Option, - /// Path to the file where the node writes the latest allowed hash. - /// If not set, assumes running outside of TEE and skips image hash monitoring. - #[serde(default)] - pub latest_allowed_hash_file: Option, -} - /// GCP keyshare storage configuration. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct GcpStartConfig { @@ -159,35 +145,18 @@ pub struct GcpStartConfig { pub project_id: String, } -/// TEE authority configuration for deserialization. -#[derive(Debug, Clone, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -pub enum TeeAuthorityStartConfig { - Local, - Dstack { - #[serde(default = "default_dstack_endpoint")] - dstack_endpoint: String, - #[serde(default = "default_quote_upload_url")] - // TODO(#2333): use URL type for this type - quote_upload_url: String, - }, -} - -fn default_dstack_endpoint() -> String { - DEFAULT_DSTACK_ENDPOINT.to_string() -} - -fn default_quote_upload_url() -> String { - DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL.to_string() +pub trait TeeAuthorityImpl { + fn into_tee_authority(self) -> anyhow::Result; } -impl TeeAuthorityStartConfig { - pub fn into_tee_authority(self) -> anyhow::Result { +impl TeeAuthorityImpl for TeeAuthorityConfig { + fn into_tee_authority(self) -> anyhow::Result { Ok(match self { - TeeAuthorityStartConfig::Local => LocalTeeAuthorityConfig::default().into(), - TeeAuthorityStartConfig::Dstack { + TeeAuthorityConfig::Local => LocalTeeAuthorityConfig::default().into(), + TeeAuthorityConfig::Dstack { dstack_endpoint, quote_upload_url, + .. } => { let url: Url = quote_upload_url .parse() diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs index 2702749ff..b8a36d179 100644 --- a/crates/node/src/run.rs +++ b/crates/node/src/run.rs @@ -1,7 +1,7 @@ use crate::{ config::{ - generate_and_write_backup_encryption_key_to_disk, ConfigFile, PersistentSecrets, - RespondConfig, SecretsConfig, StartConfig, + generate_and_write_backup_encryption_key_to_disk, start::TeeAuthorityImpl as _, ConfigFile, + PersistentSecrets, RespondConfig, SecretsConfig, StartConfig, }, coordinator::Coordinator, db::SecretDB, @@ -15,6 +15,7 @@ use crate::{ web::{start_web_server, static_web_data, DebugRequest}, }; use anyhow::{anyhow, Context}; +use launcher_interface::types::TeeAuthorityConfig; use mpc_attestation::report_data::ReportDataV1; use mpc_contract::state::ProtocolContractState; use mpc_contract::tee::proposal::MpcDockerImageHash; @@ -73,7 +74,7 @@ pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { )?; // Generate attestation - let tee_authority = config.tee.authority.clone().into_tee_authority()?; + let tee_authority = config.tee.clone().into_tee_authority()?; let tls_public_key = &secrets.persistent_secrets.p2p_private_key.verifying_key(); let account_public_key = &secrets.persistent_secrets.near_signer_key.verifying_key(); @@ -125,20 +126,19 @@ pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { let (shutdown_signal_sender, mut shutdown_signal_receiver) = mpsc::channel(1); let cancellation_token = CancellationToken::new(); - let image_hash_watcher_handle = if let (Some(image_hash), Some(latest_allowed_hash_file)) = - (&config.tee.image_hash, &config.tee.latest_allowed_hash_file) + let image_hash_watcher_handle = if let TeeAuthorityConfig::Dstack { + image_hash, + latest_allowed_hash_file_path, + .. + } = &config.tee { - let current_image_hash_bytes: [u8; 32] = hex::decode(image_hash) - .expect("The currently running image is a hex string.") - .try_into() - .expect("The currently running image hash hex representation is 32 bytes."); - let allowed_hashes_in_contract = indexer_api.allowed_docker_images_receiver.clone(); - let image_hash_storage = AllowedImageHashesFile::from(latest_allowed_hash_file.clone()); + let image_hash_storage = + AllowedImageHashesFile::from(latest_allowed_hash_file_path.clone()); Some(root_runtime.spawn(monitor_allowed_image_hashes( cancellation_token.child_token(), - MpcDockerImageHash::from(current_image_hash_bytes), + MpcDockerImageHash::from(image_hash.as_bytes()), allowed_hashes_in_contract, image_hash_storage, shutdown_signal_sender.clone(), diff --git a/crates/primitives/src/hash.rs b/crates/primitives/src/hash.rs index c2ea22a91..02c92cea7 100644 --- a/crates/primitives/src/hash.rs +++ b/crates/primitives/src/hash.rs @@ -53,6 +53,10 @@ impl Hash32 { pub fn as_hex(&self) -> String { hex::encode(self.as_ref()) } + + pub fn as_bytes(&self) -> [u8; 32] { + self.bytes + } } #[derive(Error, Debug)] diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 23866f2dd..c00b2e204 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -4,8 +4,8 @@ use std::{collections::VecDeque, time::Duration}; use backon::{ExponentialBuilder, Retryable}; use clap::Parser; -use launcher_interface::MPC_IMAGE_HASH_EVENT; -use launcher_interface::types::{ApprovedHashes, DockerSha256Digest}; +use launcher_interface::types::{ApprovedHashes, DockerSha256Digest, TeeAuthorityConfig}; +use launcher_interface::{DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL, MPC_IMAGE_HASH_EVENT}; use constants::*; use docker_types::*; @@ -119,8 +119,35 @@ async fn run() -> Result<(), LauncherError> { } let mpc_binary_config_path = std::path::Path::new(MPC_CONFIG_SHARED_PATH); + + let mut mpc_node_config = config.mpc_node_config; + + let tee_config = match args.platform { + Platform::Tee => TeeAuthorityConfig::Dstack { + dstack_endpoint: DSTACK_UNIX_SOCKET.to_string(), + quote_upload_url: DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL.to_string(), + image_hash: image_hash.clone(), + latest_allowed_hash_file_path: IMAGE_DIGEST_FILE + .parse() + .expect("image digest file has a valid path"), + }, + Platform::NonTee => TeeAuthorityConfig::Local, + }; + + match mpc_node_config.entry("tee") { + toml::map::Entry::Vacant(vacant_entry) => { + let tee_config_serialized = + toml::Value::try_from(&tee_config).expect("TeeAuthorityConfig serializes to TOML"); + vacant_entry.insert(tee_config_serialized); + } + toml::map::Entry::Occupied(_) => { + panic!("[tee] config table is not configurable by the user. please remove this field") + } + }; + let mpc_config_toml = - toml::to_string(&config.mpc_config).expect("re-serializing a toml::Table always succeeds"); + toml::to_string(&mpc_node_config).expect("re-serializing a toml::Table always succeeds"); + std::fs::write(mpc_binary_config_path, mpc_config_toml.as_bytes()).map_err(|source| { LauncherError::FileWrite { path: mpc_binary_config_path.display().to_string(), diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 0beddf549..61a22b176 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -47,7 +47,7 @@ pub(crate) struct Config { /// The launcher does not interpret these fields — they are re-serialized /// to a TOML string, written to a file on disk, and mounted into the /// container for the MPC binary to consume via `start-with-config-file`. - pub(crate) mpc_config: toml::Table, + pub(crate) mpc_node_config: toml::Table, } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -231,8 +231,8 @@ some_opaque_field = true // then assert_matches!(result, Ok(config) => { assert_eq!(config.launcher_config.image_name, "nearone/mpc-node"); - assert_eq!(config.mpc_config["home_dir"].as_str(), Some("/data")); - assert_eq!(config.mpc_config["some_opaque_field"].as_bool(), Some(true)); + assert_eq!(config.mpc_node_config["home_dir"].as_str(), Some("/data")); + assert_eq!(config.mpc_node_config["some_opaque_field"].as_bool(), Some(true)); }); } @@ -257,7 +257,7 @@ arbitrary_key = "arbitrary_value" let config: Config = toml::from_str(toml_str).unwrap(); // when — re-serialize the opaque table (what the launcher writes to disk) - let serialized = toml::to_string(&config.mpc_config).unwrap(); + let serialized = toml::to_string(&config.mpc_node_config).unwrap(); // then assert!(serialized.contains("home_dir")); diff --git a/deployment/cvm-deployment/user-config.toml b/deployment/cvm-deployment/user-config.toml index 377541d18..68baee94e 100644 --- a/deployment/cvm-deployment/user-config.toml +++ b/deployment/cvm-deployment/user-config.toml @@ -29,9 +29,6 @@ download_config = "rpc" [mpc_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" -[mpc_config.tee.authority] -type = "local" - [mpc_config.node] my_near_account_id = "mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet" near_responder_account_id = "mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet" diff --git a/deployment/localnet/tee/frodo.toml b/deployment/localnet/tee/frodo.toml index f66150cbe..270931953 100644 --- a/deployment/localnet/tee/frodo.toml +++ b/deployment/localnet/tee/frodo.toml @@ -24,9 +24,6 @@ download_genesis = false secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" -[mpc_config.tee.authority] -type = "local" - [mpc_config.node] my_near_account_id = "frodo.test.near" near_responder_account_id = "frodo.test.near" diff --git a/deployment/localnet/tee/sam.toml b/deployment/localnet/tee/sam.toml index 8c9c1a8c4..c424e66fb 100644 --- a/deployment/localnet/tee/sam.toml +++ b/deployment/localnet/tee/sam.toml @@ -6,9 +6,9 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 port_mappings = [ - { host =8080, container =8080 }, - { host =24566, container =24566 }, - { host =13002, container =13002 }, + { host = 8080, container = 8080 }, + { host = 24566, container = 24566 }, + { host = 13002, container = 13002 }, ] [mpc_config] @@ -24,9 +24,6 @@ download_genesis = false secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" -[mpc_config.tee.authority] -type = "local" - [mpc_config.node] my_near_account_id = "sam.test.near" near_responder_account_id = "sam.test.near" diff --git a/deployment/testnet/frodo.toml b/deployment/testnet/frodo.toml index fdfd18d61..a5a937915 100644 --- a/deployment/testnet/frodo.toml +++ b/deployment/testnet/frodo.toml @@ -25,9 +25,6 @@ download_config = "rpc" secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" -[mpc_config.tee.authority] -type = "local" - [mpc_config.node] my_near_account_id = "$FRODO_ACCOUNT" near_responder_account_id = "$FRODO_ACCOUNT" diff --git a/deployment/testnet/sam.toml b/deployment/testnet/sam.toml index 587b9b101..f0b4dcf87 100644 --- a/deployment/testnet/sam.toml +++ b/deployment/testnet/sam.toml @@ -25,9 +25,6 @@ download_config = "rpc" secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" -[mpc_config.tee.authority] -type = "local" - [mpc_config.node] my_near_account_id = "$SAM_ACCOUNT" near_responder_account_id = "$SAM_ACCOUNT" diff --git a/libs/nearcore b/libs/nearcore index 96100bbf3..9d43667a7 160000 --- a/libs/nearcore +++ b/libs/nearcore @@ -1 +1 @@ -Subproject commit 96100bbf3b5de7eed46cab7bfe446e5002e19c11 +Subproject commit 9d43667a71f64b7efa6c9f897eef61983d373977 From 6d11ede1b7d96daf577de39d275574bedebaeed5 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Wed, 18 Mar 2026 11:29:14 +0100 Subject: [PATCH 160/176] also intercept teeconfig --- crates/launcher-interface/src/types.rs | 14 ++++++--- crates/node/src/cli.rs | 15 ++++++++- crates/node/src/config/start.rs | 4 ++- crates/node/src/run.rs | 43 +++++++++----------------- crates/primitives/src/hash.rs | 12 ++++--- crates/tee-launcher/src/main.rs | 29 ++++++++++++++--- 6 files changed, 72 insertions(+), 45 deletions(-) diff --git a/crates/launcher-interface/src/types.rs b/crates/launcher-interface/src/types.rs index 7c1a06477..e3194c5a2 100644 --- a/crates/launcher-interface/src/types.rs +++ b/crates/launcher-interface/src/types.rs @@ -14,14 +14,18 @@ pub enum TeeAuthorityConfig { dstack_endpoint: String, // TODO(#2333): use URL type for this type quote_upload_url: String, - /// Hex representation of the hash of the running image. Only required in TEE. - image_hash: DockerSha256Digest, - /// Path to the file where the node writes the latest allowed hash. - /// If not set, assumes running outside of TEE and skips image hash monitoring. - latest_allowed_hash_file_path: PathBuf, }, } +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct ImageConfig { + /// Hex representation of the hash of the running image. Only required in TEE. + pub image_hash: DockerSha256Digest, + /// Path to the file where the node writes the latest allowed hash. + /// If not set, assumes running outside of TEE and skips image hash monitoring. + pub latest_allowed_hash_file_path: PathBuf, +} + #[derive(Debug, Serialize, Deserialize)] pub struct ApprovedHashes { pub approved_hashes: near_mpc_bounded_collections::NonEmptyVec, diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index e6ca9edfd..e4d8b3f2a 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -12,10 +12,15 @@ use crate::{ }; use clap::{Args, Parser, Subcommand, ValueEnum}; use hex::FromHex; -use launcher_interface::types::TeeAuthorityConfig; +use launcher_interface::types::{ImageConfig, TeeAuthorityConfig}; +use mpc_contract::tee::proposal::MpcDockerImageHash; use std::path::PathBuf; use tee_authority::tee_authority::{DEFAULT_DSTACK_ENDPOINT, DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL}; use url::Url; + +const DUMMY_ALLOWED_HASH: MpcDockerImageHash = MpcDockerImageHash::new([0; 32]); +const ALLOWED_IMAGE_HASHES_FILE_PATH: &str = "/tmp/allowed_image_hashes.json"; + #[derive(Parser, Debug)] #[command(name = "mpc-node")] #[command(about = "MPC Node for Near Protocol")] @@ -147,6 +152,14 @@ impl StartCmd { tee: TeeAuthorityConfig::Local, gcp, node: config, + // Use dummy values as we don't want a breaking change, and + // this start command will be deprecated in #2334 + image_config: ImageConfig { + image_hash: DUMMY_ALLOWED_HASH.into(), + latest_allowed_hash_file_path: ALLOWED_IMAGE_HASHES_FILE_PATH + .parse() + .expect("dummy allowed image hashes is valid path"), + }, } } } diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index 2073de1bd..cb80fa78b 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -1,6 +1,6 @@ use super::ConfigFile; use anyhow::Context; -use launcher_interface::types::TeeAuthorityConfig; +use launcher_interface::types::{ImageConfig, TeeAuthorityConfig}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; use tee_authority::tee_authority::{ @@ -18,6 +18,8 @@ pub struct StartConfig { pub secrets: SecretsStartConfig, /// TEE authority and image hash monitoring settings. pub tee: TeeAuthorityConfig, + /// Configuration of the image hash running and where to write allowed image hashes + pub image_config: ImageConfig, /// GCP keyshare storage settings. Optional — omit if not using GCP. pub gcp: Option, /// NEAR node initialization settings. Required for `start-with-config-file` diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs index b8a36d179..8d907632b 100644 --- a/crates/node/src/run.rs +++ b/crates/node/src/run.rs @@ -15,7 +15,6 @@ use crate::{ web::{start_web_server, static_web_data, DebugRequest}, }; use anyhow::{anyhow, Context}; -use launcher_interface::types::TeeAuthorityConfig; use mpc_attestation::report_data::ReportDataV1; use mpc_contract::state::ProtocolContractState; use mpc_contract::tee::proposal::MpcDockerImageHash; @@ -126,29 +125,17 @@ pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { let (shutdown_signal_sender, mut shutdown_signal_receiver) = mpsc::channel(1); let cancellation_token = CancellationToken::new(); - let image_hash_watcher_handle = if let TeeAuthorityConfig::Dstack { - image_hash, - latest_allowed_hash_file_path, - .. - } = &config.tee - { - let allowed_hashes_in_contract = indexer_api.allowed_docker_images_receiver.clone(); - let image_hash_storage = - AllowedImageHashesFile::from(latest_allowed_hash_file_path.clone()); - - Some(root_runtime.spawn(monitor_allowed_image_hashes( - cancellation_token.child_token(), - MpcDockerImageHash::from(image_hash.as_bytes()), - allowed_hashes_in_contract, - image_hash_storage, - shutdown_signal_sender.clone(), - ))) - } else { - tracing::info!( - "image_hash and/or latest_allowed_hash_file not set, skipping TEE image hash monitoring" - ); - None - }; + let allowed_hashes_in_contract = indexer_api.allowed_docker_images_receiver.clone(); + let image_hash_storage = + AllowedImageHashesFile::from(config.image_config.latest_allowed_hash_file_path.clone()); + + let image_hash_watcher_handle = root_runtime.spawn(monitor_allowed_image_hashes( + cancellation_token.child_token(), + MpcDockerImageHash::from(config.image_config.image_hash.as_bytes()), + allowed_hashes_in_contract, + image_hash_storage, + shutdown_signal_sender.clone(), + )); let home_dir = config.home_dir.clone(); let root_future = create_root_future( @@ -179,11 +166,9 @@ pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { // Perform graceful shutdown cancellation_token.cancel(); - if let Some(handle) = image_hash_watcher_handle { - info!("Waiting for image hash watcher to gracefully exit."); - let exit_result = handle.await; - info!(?exit_result, "Image hash watcher exited."); - } + info!("Waiting for image hash watcher to gracefully exit."); + let exit_result = image_hash_watcher_handle.await; + info!(?exit_result, "Image hash watcher exited."); exit_reason } diff --git a/crates/primitives/src/hash.rs b/crates/primitives/src/hash.rs index 02c92cea7..3f789eef8 100644 --- a/crates/primitives/src/hash.rs +++ b/crates/primitives/src/hash.rs @@ -41,10 +41,7 @@ pub struct Hash32 { impl From<[u8; 32]> for Hash32 { fn from(bytes: [u8; 32]) -> Self { - Self { - bytes, - _marker: PhantomData, - } + Self::new(bytes) } } @@ -57,6 +54,13 @@ impl Hash32 { pub fn as_bytes(&self) -> [u8; 32] { self.bytes } + + pub const fn new(bytes: [u8; 32]) -> Self { + Self { + bytes, + _marker: PhantomData, + } + } } #[derive(Error, Debug)] diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index c00b2e204..1772bacec 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -4,7 +4,9 @@ use std::{collections::VecDeque, time::Duration}; use backon::{ExponentialBuilder, Retryable}; use clap::Parser; -use launcher_interface::types::{ApprovedHashes, DockerSha256Digest, TeeAuthorityConfig}; +use launcher_interface::types::{ + ApprovedHashes, DockerSha256Digest, ImageConfig, TeeAuthorityConfig, +}; use launcher_interface::{DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL, MPC_IMAGE_HASH_EVENT}; use constants::*; @@ -126,14 +128,17 @@ async fn run() -> Result<(), LauncherError> { Platform::Tee => TeeAuthorityConfig::Dstack { dstack_endpoint: DSTACK_UNIX_SOCKET.to_string(), quote_upload_url: DEFAULT_PHALA_TDX_QUOTE_UPLOAD_URL.to_string(), - image_hash: image_hash.clone(), - latest_allowed_hash_file_path: IMAGE_DIGEST_FILE - .parse() - .expect("image digest file has a valid path"), }, Platform::NonTee => TeeAuthorityConfig::Local, }; + let image_hash_config = ImageConfig { + image_hash: image_hash.clone(), + latest_allowed_hash_file_path: IMAGE_DIGEST_FILE + .parse() + .expect("image digest file has a valid path"), + }; + match mpc_node_config.entry("tee") { toml::map::Entry::Vacant(vacant_entry) => { let tee_config_serialized = @@ -145,6 +150,19 @@ async fn run() -> Result<(), LauncherError> { } }; + match mpc_node_config.entry("image_config") { + toml::map::Entry::Vacant(vacant_entry) => { + let tee_config_serialized = + toml::Value::try_from(&image_hash_config).expect("ImageConfig serializes to TOML"); + vacant_entry.insert(tee_config_serialized); + } + toml::map::Entry::Occupied(_) => { + panic!( + "[ImageConfig] config table is not configurable by the user. please remove this field" + ) + } + }; + let mpc_config_toml = toml::to_string(&mpc_node_config).expect("re-serializing a toml::Table always succeeds"); @@ -1009,6 +1027,7 @@ mod tests { #[cfg(all(test, feature = "external-services-tests"))] mod integration_tests { use super::*; + #[cfg(target_os = "linux")] use assert_matches::assert_matches; // # Dockerfile From 0bcaff9f6b38a1aa211f2ff94d9e137a34205da6 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Wed, 18 Mar 2026 11:46:37 +0100 Subject: [PATCH 161/176] test interception of configs --- crates/tee-launcher/src/error.rs | 3 + crates/tee-launcher/src/main.rs | 319 +++++++++++++++++++++++++++---- crates/tee-launcher/src/types.rs | 4 +- 3 files changed, 290 insertions(+), 36 deletions(-) diff --git a/crates/tee-launcher/src/error.rs b/crates/tee-launcher/src/error.rs index 5f35773ac..0b3283e71 100644 --- a/crates/tee-launcher/src/error.rs +++ b/crates/tee-launcher/src/error.rs @@ -63,6 +63,9 @@ pub(crate) enum LauncherError { #[error("Invalid manifest URL: {0}")] InvalidManifestUrl(String), + #[error("User config contains reserved key [{0}] — remove it from mpc_node_config")] + ReservedConfigKey(String), + #[error("The selected image failed digest validation: {0}")] ImageDigestValidationFailed(#[from] ImageDigestValidationFailed), } diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index 1772bacec..a759657cf 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -122,8 +122,6 @@ async fn run() -> Result<(), LauncherError> { let mpc_binary_config_path = std::path::Path::new(MPC_CONFIG_SHARED_PATH); - let mut mpc_node_config = config.mpc_node_config; - let tee_config = match args.platform { Platform::Tee => TeeAuthorityConfig::Dstack { dstack_endpoint: DSTACK_UNIX_SOCKET.to_string(), @@ -139,29 +137,8 @@ async fn run() -> Result<(), LauncherError> { .expect("image digest file has a valid path"), }; - match mpc_node_config.entry("tee") { - toml::map::Entry::Vacant(vacant_entry) => { - let tee_config_serialized = - toml::Value::try_from(&tee_config).expect("TeeAuthorityConfig serializes to TOML"); - vacant_entry.insert(tee_config_serialized); - } - toml::map::Entry::Occupied(_) => { - panic!("[tee] config table is not configurable by the user. please remove this field") - } - }; - - match mpc_node_config.entry("image_config") { - toml::map::Entry::Vacant(vacant_entry) => { - let tee_config_serialized = - toml::Value::try_from(&image_hash_config).expect("ImageConfig serializes to TOML"); - vacant_entry.insert(tee_config_serialized); - } - toml::map::Entry::Occupied(_) => { - panic!( - "[ImageConfig] config table is not configurable by the user. please remove this field" - ) - } - }; + let mpc_node_config = + intercept_node_config(config.mpc_node_config, &tee_config, &image_hash_config)?; let mpc_config_toml = toml::to_string(&mpc_node_config).expect("re-serializing a toml::Table always succeeds"); @@ -183,6 +160,46 @@ async fn run() -> Result<(), LauncherError> { Ok(()) } +/// Inject launcher-controlled config sections (`tee` and `image_config`) into +/// the user-provided MPC node config table. Returns an error if the user +/// config already contains either reserved key. +fn intercept_node_config( + mut node_config: toml::Table, + tee_config: &TeeAuthorityConfig, + image_config: &ImageConfig, +) -> Result { + insert_reserved( + &mut node_config, + "tee", + toml::Value::try_from(tee_config).expect("TeeAuthorityConfig serializes to TOML"), + )?; + insert_reserved( + &mut node_config, + "image_config", + toml::Value::try_from(image_config).expect("ImageConfig serializes to TOML"), + )?; + Ok(node_config) +} + +/// Inject launcher-controlled config sections (`tee` and `image_config`) into +/// the user-provided MPC node config table. Returns an error if the user +/// config already contains either reserved key. +/// Insert `value` under `key` in `table`, returning an error if the key +/// already exists. +fn insert_reserved( + table: &mut toml::Table, + key: &str, + value: toml::Value, +) -> Result<(), LauncherError> { + match table.entry(key) { + toml::map::Entry::Vacant(vacant) => { + vacant.insert(value); + Ok(()) + } + toml::map::Entry::Occupied(_) => Err(LauncherError::ReservedConfigKey(key.to_string())), + } +} + /// Select which image hash to use, given the approved hashes file (if present), /// a fallback default digest, and an optional user override. /// @@ -554,19 +571,18 @@ fn launch_mpc_container( mod tests { use std::num::NonZeroU16; + use crate::{ + RegistryInfo, constants::*, error::LauncherError, get_manifest_digest, + intercept_node_config, render_compose_file, select_image_hash, types::*, + }; + use assert_matches::assert_matches; use httpmock::prelude::*; - use launcher_interface::types::{ApprovedHashes, DockerSha256Digest}; + use launcher_interface::types::{ + ApprovedHashes, DockerSha256Digest, ImageConfig, TeeAuthorityConfig, + }; use near_mpc_bounded_collections::NonEmptyVec; - use crate::RegistryInfo; - use crate::constants::*; - use crate::error::LauncherError; - use crate::get_manifest_digest; - use crate::render_compose_file; - use crate::select_image_hash; - use crate::types::*; - const SAMPLE_IMAGE_NAME: &str = "nearone/mpc-node"; fn render( @@ -757,6 +773,241 @@ mod tests { assert!(!rendered.contains("environment:")); } + fn sample_tee_config() -> TeeAuthorityConfig { + TeeAuthorityConfig::Dstack { + dstack_endpoint: "/var/run/dstack.sock".to_string(), + quote_upload_url: "https://example.com/quote".to_string(), + } + } + + fn sample_image_config() -> ImageConfig { + ImageConfig { + image_hash: sample_digest(), + latest_allowed_hash_file_path: "/mnt/shared/image-digest.bin".into(), + } + } + + #[test] + fn intercept_config_injects_tee_and_image_config() { + // given + let config: toml::Table = toml::from_str(r#"home_dir = "/data""#).unwrap(); + + // when + let result = + intercept_node_config(config, &sample_tee_config(), &sample_image_config()).unwrap(); + + // then + assert!(result.contains_key("tee")); + assert!(result.contains_key("image_config")); + assert_eq!(result["home_dir"].as_str(), Some("/data")); + } + + #[test] + fn intercept_config_rejects_user_provided_tee_key() { + // given + let config: toml::Table = toml::from_str( + r#"[tee] +type = "Local" +"#, + ) + .unwrap(); + + // when + let result = intercept_node_config(config, &sample_tee_config(), &sample_image_config()); + + // then + assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { + assert_eq!(key, "tee"); + }); + } + + #[test] + fn intercept_config_rejects_user_provided_image_config_key() { + // given + let config: toml::Table = toml::from_str( + r#"[image_config] +image_hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000" +latest_allowed_hash_file_path = "/evil" +"#, + ) + .unwrap(); + + // when + let result = intercept_node_config(config, &sample_tee_config(), &sample_image_config()); + + // then + assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { + assert_eq!(key, "image_config"); + }); + } + + #[test] + fn intercept_config_rejects_both_reserved_keys_reports_tee_first() { + // given — both reserved keys present + let config: toml::Table = toml::from_str( + r#" +[tee] +type = "Local" +[image_config] +image_hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000" +latest_allowed_hash_file_path = "/evil" +"#, + ) + .unwrap(); + + // when + let result = intercept_node_config(config, &sample_tee_config(), &sample_image_config()); + + // then — tee is checked first + assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { + assert_eq!(key, "tee"); + }); + } + + #[test] + fn intercept_config_empty_table_gets_both_keys() { + // given + let config = toml::Table::new(); + + // when + let result = + intercept_node_config(config, &sample_tee_config(), &sample_image_config()).unwrap(); + + // then + assert!(result.contains_key("tee")); + assert!(result.contains_key("image_config")); + assert_eq!(result.len(), 2); + } + + #[test] + fn intercept_config_preserves_all_existing_keys() { + // given + let config: toml::Table = toml::from_str( + r#" +home_dir = "/data" +port = 8080 +[nested] +key = "value" +"#, + ) + .unwrap(); + + // when + let result = + intercept_node_config(config, &sample_tee_config(), &sample_image_config()).unwrap(); + + // then + assert_eq!(result["home_dir"].as_str(), Some("/data")); + assert_eq!(result["port"].as_integer(), Some(8080)); + assert_eq!(result["nested"]["key"].as_str(), Some("value")); + assert!(result.contains_key("tee")); + assert!(result.contains_key("image_config")); + } + + #[test] + fn intercept_config_dstack_tee_config_serializes_correctly() { + // given + let config = toml::Table::new(); + let tee = TeeAuthorityConfig::Dstack { + dstack_endpoint: "/my/socket".to_string(), + quote_upload_url: "https://example.com".to_string(), + }; + + // when + let result = intercept_node_config(config, &tee, &sample_image_config()).unwrap(); + + // then + let tee_table = result["tee"].as_table().unwrap(); + assert_eq!(tee_table["dstack_endpoint"].as_str(), Some("/my/socket")); + assert_eq!( + tee_table["quote_upload_url"].as_str(), + Some("https://example.com") + ); + } + + #[test] + fn intercept_config_local_tee_config_serializes_correctly() { + // given + let config = toml::Table::new(); + + // when + let result = + intercept_node_config(config, &TeeAuthorityConfig::Local, &sample_image_config()) + .unwrap(); + + // then — Local variant is a unit variant; just verify the key exists + assert!(result.contains_key("tee")); + // re-serialize the whole thing to verify it round-trips + let toml_str = toml::to_string(&result).unwrap(); + assert!(toml_str.contains("tee")); + } + + #[test] + fn intercept_config_image_config_contains_expected_fields() { + // given + let config = toml::Table::new(); + let image_cfg = ImageConfig { + image_hash: digest('b'), + latest_allowed_hash_file_path: "/some/path".into(), + }; + + // when + let result = intercept_node_config(config, &sample_tee_config(), &image_cfg).unwrap(); + + // then + let ic = result["image_config"].as_table().unwrap(); + assert!(ic["image_hash"].as_str().unwrap().contains("bbbb")); + assert_eq!( + ic["latest_allowed_hash_file_path"].as_str(), + Some("/some/path") + ); + } + + #[test] + fn intercept_config_output_re_serializes_to_valid_toml() { + // given + let config: toml::Table = toml::from_str(r#"home_dir = "/data""#).unwrap(); + + // when + let result = + intercept_node_config(config, &sample_tee_config(), &sample_image_config()).unwrap(); + let toml_str = toml::to_string(&result).unwrap(); + + // then — the output can be parsed back + let reparsed: toml::Table = toml::from_str(&toml_str).unwrap(); + assert!(reparsed.contains_key("tee")); + assert!(reparsed.contains_key("image_config")); + assert_eq!(reparsed["home_dir"].as_str(), Some("/data")); + } + + #[test] + fn intercept_config_tee_as_non_table_value_is_rejected() { + // given — tee exists but as a string, not a table + let config: toml::Table = toml::from_str(r#"tee = "sneaky""#).unwrap(); + + // when + let result = intercept_node_config(config, &sample_tee_config(), &sample_image_config()); + + // then — any occupied entry is rejected regardless of value type + assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { + assert_eq!(key, "tee"); + }); + } + + #[test] + fn intercept_config_image_config_as_non_table_value_is_rejected() { + // given + let config: toml::Table = toml::from_str(r#"image_config = 42"#).unwrap(); + + // when + let result = intercept_node_config(config, &sample_tee_config(), &sample_image_config()); + + // then + assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { + assert_eq!(key, "image_config"); + }); + } + // --- select_image_hash --- #[test] diff --git a/crates/tee-launcher/src/types.rs b/crates/tee-launcher/src/types.rs index 61a22b176..0f122f4b7 100644 --- a/crates/tee-launcher/src/types.rs +++ b/crates/tee-launcher/src/types.rs @@ -220,7 +220,7 @@ rpc_max_attempts = 20 port_mappings = [{ host = 11780, container = 11780 }] -[mpc_config] +[mpc_node_config] home_dir = "/data" some_opaque_field = true "#; @@ -250,7 +250,7 @@ rpc_max_attempts = 20 port_mappings = [{ host = 11780, container = 11780 }] -[mpc_config] +[mpc_node_config] home_dir = "/data" arbitrary_key = "arbitrary_value" "#; From 3232e15a279327d5f541509931874b3d814e3e3d Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Wed, 18 Mar 2026 13:09:32 +0100 Subject: [PATCH 162/176] wip logging --- crates/node/src/cli.rs | 21 +++++++++---------- crates/node/src/config/start.rs | 27 +++++++++++++++++++++++++ crates/node/src/main.rs | 1 - crates/node/src/run.rs | 3 +++ crates/node/src/tracing.rs | 36 ++++++++++++++++++++++++--------- 5 files changed, 67 insertions(+), 21 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index e4d8b3f2a..7112f20e1 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -1,6 +1,8 @@ use crate::{ config::{ - load_config_file, ChainId, ConfigFile, DownloadConfigType, GcpStartConfig, NearInitConfig, + load_config_file, + start::{LogConfig, LogFormat}, + ChainId, ConfigFile, DownloadConfigType, GcpStartConfig, NearInitConfig, SecretsStartConfig, StartConfig, }, keyshare::{ @@ -10,7 +12,7 @@ use crate::{ }, run::run_mpc_node, }; -use clap::{Args, Parser, Subcommand, ValueEnum}; +use clap::{Args, Parser, Subcommand}; use hex::FromHex; use launcher_interface::types::{ImageConfig, TeeAuthorityConfig}; use mpc_contract::tee::proposal::MpcDockerImageHash; @@ -26,20 +28,13 @@ const ALLOWED_IMAGE_HASHES_FILE_PATH: &str = "/tmp/allowed_image_hashes.json"; #[command(about = "MPC Node for Near Protocol")] #[command(version = env!("CARGO_PKG_VERSION"))] pub struct Cli { + // TODO(#2334): can be removed when deprecating StartCmd as it's part of config file #[arg(long, value_enum, env("MPC_LOG_FORMAT"), default_value = "plain")] pub log_format: LogFormat, #[clap(subcommand)] pub command: CliCommand, } -#[derive(Copy, Clone, Debug, ValueEnum)] -pub enum LogFormat { - /// Plaintext logs - Plain, - /// JSON logs - Json, -} - #[derive(Subcommand, Debug)] pub enum CliCommand { /// Starts the MPC node using a single TOML configuration file instead of @@ -132,7 +127,7 @@ pub struct CliImageHashConfig { } impl StartCmd { - fn into_start_config(self, config: ConfigFile) -> StartConfig { + fn into_start_config(self, config: ConfigFile, log_format: LogFormat) -> StartConfig { let gcp = match (self.gcp_keyshare_secret_id, self.gcp_project_id) { (Some(keyshare_secret_id), Some(project_id)) => Some(GcpStartConfig { keyshare_secret_id, @@ -160,6 +155,10 @@ impl StartCmd { .parse() .expect("dummy allowed image hashes is valid path"), }, + log_config: LogConfig { + log_format, + log_level: None, + }, } } } diff --git a/crates/node/src/config/start.rs b/crates/node/src/config/start.rs index cb80fa78b..b44512562 100644 --- a/crates/node/src/config/start.rs +++ b/crates/node/src/config/start.rs @@ -1,5 +1,6 @@ use super::ConfigFile; use anyhow::Context; +use clap::ValueEnum; use launcher_interface::types::{ImageConfig, TeeAuthorityConfig}; use serde::{Deserialize, Serialize}; use std::path::{Path, PathBuf}; @@ -29,6 +30,32 @@ pub struct StartConfig { pub near_init: Option, /// Node configuration (indexer, protocol parameters, etc.). pub node: ConfigFile, + pub log_config: LogConfig, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LogConfig { + // TODO(#2334): make non optional + pub log_level: Option, + pub log_format: LogFormat, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(tag = "type", rename_all = "snake_case")] +pub enum LogLevel { + Trace, + Debug, + Info, + Warn, + Error, +} + +#[derive(Copy, Clone, Debug, ValueEnum, Serialize, Deserialize)] +pub enum LogFormat { + /// Plaintext logs + Plain, + /// JSON logs + Json, } /// NEAR node initialization configuration. Controls how the NEAR node's diff --git a/crates/node/src/main.rs b/crates/node/src/main.rs index f3f546b35..5d60ae090 100644 --- a/crates/node/src/main.rs +++ b/crates/node/src/main.rs @@ -12,6 +12,5 @@ fn main() -> anyhow::Result<()> { let cli = cli::Cli::parse(); mpc_node::metrics::init_build_info_metric(); - mpc_node::tracing::init_logging(cli.log_format); futures::executor::block_on(cli.run()) } diff --git a/crates/node/src/run.rs b/crates/node/src/run.rs index 8d907632b..b1626b9a3 100644 --- a/crates/node/src/run.rs +++ b/crates/node/src/run.rs @@ -11,6 +11,7 @@ use crate::{ keyshare::{GcpPermanentKeyStorageConfig, KeyStorageConfig, KeyshareStorage}, migration_service::spawn_recovery_server_and_run_onboarding, profiler, + tracing::init_logging, tracking::{self, start_root_task}, web::{start_web_server, static_web_data, DebugRequest}, }; @@ -40,6 +41,8 @@ use crate::tee::{ pub const ATTESTATION_RESUBMISSION_INTERVAL: Duration = Duration::from_secs(60 * 60); // 1 hour pub async fn run_mpc_node(config: StartConfig) -> anyhow::Result<()> { + init_logging(&config.log_config); + let root_runtime = tokio::runtime::Builder::new_multi_thread() .enable_all() .worker_threads(1) diff --git a/crates/node/src/tracing.rs b/crates/node/src/tracing.rs index 3a5c58233..0bdb1dca4 100644 --- a/crates/node/src/tracing.rs +++ b/crates/node/src/tracing.rs @@ -1,23 +1,41 @@ -use crate::cli::LogFormat; +use tracing::Level; +use tracing_subscriber::EnvFilter; -pub fn init_logging(log_format: LogFormat) { - match log_format { - LogFormat::Json => init_json_logging(), - LogFormat::Plain => init_plain_logging(), +use crate::config::start::{LogConfig, LogFormat, LogLevel}; + +pub fn init_logging(log_config: &LogConfig) { + let log_level = log_config.log_level.as_ref().map(|l| match l { + LogLevel::Trace => Level::TRACE, + LogLevel::Debug => Level::DEBUG, + LogLevel::Info => Level::INFO, + LogLevel::Warn => Level::WARN, + LogLevel::Error => Level::ERROR, + }); + + match log_config.log_format { + LogFormat::Json => init_json_logging(log_level), + LogFormat::Plain => init_plain_logging(log_level), + } +} + +fn env_filter(log_level: Option) -> EnvFilter { + match log_level { + Some(level) => EnvFilter::new(level.as_str()), + None => EnvFilter::from_default_env(), } } -fn init_plain_logging() { +fn init_plain_logging(log_level: Option) { tracing_subscriber::fmt() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_env_filter(env_filter(log_level)) .try_init() .ok(); } -fn init_json_logging() { +fn init_json_logging(log_level: Option) { tracing_subscriber::fmt() .json() - .with_env_filter(tracing_subscriber::EnvFilter::from_default_env()) + .with_env_filter(env_filter(log_level)) .try_init() .ok(); } From 90fd7013a7cfef871831e4c2151b59096b04d062 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Wed, 18 Mar 2026 13:22:11 +0100 Subject: [PATCH 163/176] use initlogging in tests --- crates/node/src/cli.rs | 2 +- crates/node/src/p2p.rs | 11 +++----- crates/node/src/providers/ecdsa/triple.rs | 5 +--- crates/node/src/requests/queue.rs | 32 +++++++---------------- 4 files changed, 15 insertions(+), 35 deletions(-) diff --git a/crates/node/src/cli.rs b/crates/node/src/cli.rs index 7112f20e1..7e2dca41a 100644 --- a/crates/node/src/cli.rs +++ b/crates/node/src/cli.rs @@ -266,7 +266,7 @@ impl Cli { let home_dir = std::path::Path::new(&start.home_dir); let config_file = load_config_file(home_dir)?; - let node_configuration = start.into_start_config(config_file); + let node_configuration = start.into_start_config(config_file, self.log_format); run_mpc_node(node_configuration).await } CliCommand::Init(config) => { diff --git a/crates/node/src/p2p.rs b/crates/node/src/p2p.rs index 68654af32..3fd827917 100644 --- a/crates/node/src/p2p.rs +++ b/crates/node/src/p2p.rs @@ -1039,7 +1039,6 @@ pub mod testing { #[cfg(test)] mod tests { - use crate::cli::LogFormat; use crate::config::MpcConfig; use crate::network::conn::ConnectionVersion; use crate::network::{MeshNetworkTransportReceiver, MeshNetworkTransportSender}; @@ -1048,7 +1047,6 @@ mod tests { ChannelId, MpcMessage, MpcStartMessage, MpcTaskId, ParticipantId, PeerMessage, UniqueId, }; use crate::providers::EcdsaTaskId; - use crate::tracing::init_logging; use crate::tracking::testing::start_root_task_with_periodic_dump; use mpc_contract::primitives::domain::DomainId; use mpc_contract::primitives::key_state::{AttemptId, EpochId, KeyEventId}; @@ -1056,9 +1054,8 @@ mod tests { use std::time::Duration; use tokio::time::timeout; - #[tokio::test] + #[test_log::test(tokio::test)] async fn test_basic_tls_mesh_network() { - init_logging(LogFormat::Plain); let configs = generate_test_p2p_configs( &["test0".parse().unwrap(), "test1".parse().unwrap()], 2, @@ -1158,9 +1155,8 @@ mod tests { result } - #[tokio::test] + #[test_log::test(tokio::test)] async fn test_wait_for_ready() { - init_logging(LogFormat::Plain); let mut configs = generate_test_p2p_configs( &[ "test0".parse().unwrap(), @@ -1288,9 +1284,8 @@ mod tests { ids } - #[tokio::test] + #[test_log::test(tokio::test)] async fn test_receiver_does_not_accept_new_connection_if_connected() { - init_logging(LogFormat::Plain); let mut configs = generate_test_p2p_configs( &["test0".parse().unwrap(), "test1".parse().unwrap()], 2, diff --git a/crates/node/src/providers/ecdsa/triple.rs b/crates/node/src/providers/ecdsa/triple.rs index 05cd7a8d1..375f82d7f 100644 --- a/crates/node/src/providers/ecdsa/triple.rs +++ b/crates/node/src/providers/ecdsa/triple.rs @@ -312,14 +312,12 @@ pub fn participants_from_triples( #[cfg(test)] mod tests { use super::{ManyTripleGenerationComputation, PairedTriple}; - use crate::cli::LogFormat; use crate::network::computation::MpcLeaderCentricComputation; use crate::network::testing::run_test_clients; use crate::network::{MeshNetworkClient, NetworkTaskChannel}; use crate::primitives::{MpcTaskId, UniqueId}; use crate::providers::ecdsa::EcdsaTaskId; use crate::tests::into_participant_ids; - use crate::tracing::init_logging; use crate::tracking; use futures::{stream, StreamExt}; use std::collections::HashMap; @@ -334,9 +332,8 @@ mod tests { const TRIPLES_PER_BATCH: usize = 10; const BATCHES_TO_GENERATE_PER_CLIENT: usize = 10; - #[tokio::test(flavor = "multi_thread")] + #[test_log::test(tokio::test(flavor = "multi_thread"))] async fn test_many_triple_generation() { - init_logging(LogFormat::Plain); tracking::testing::start_root_task_with_periodic_dump(async { let all_triples = run_test_clients( into_participant_ids(&TestGenerators::new(NUM_PARTICIPANTS, THRESHOLD.into())), diff --git a/crates/node/src/requests/queue.rs b/crates/node/src/requests/queue.rs index a8c1053ae..d5095b2fc 100644 --- a/crates/node/src/requests/queue.rs +++ b/crates/node/src/requests/queue.rs @@ -550,7 +550,6 @@ impl #[cfg(test)] mod tests { use super::{NetworkAPIForRequests, PendingRequests, QueuedRequest}; - use crate::cli::LogFormat; use crate::indexer::types::{ChainCKDRespondArgs, ChainSignatureRespondArgs}; use crate::primitives::ParticipantId; use crate::requests::queue::{ @@ -559,7 +558,6 @@ mod tests { }; use crate::requests::recent_blocks_tracker::tests::TestBlockMaker; use crate::tests::into_participant_ids; - use crate::tracing::init_logging; use crate::types::{CKDRequest, SignatureRequest}; use mpc_contract::primitives::domain::DomainId; use mpc_contract::primitives::signature::{Payload, Tweak}; @@ -676,9 +674,8 @@ mod tests { } } - #[test] + #[test_log::test] fn test_pending_ckd_requests_leader_retry() { - init_logging(LogFormat::Plain); let clock = FakeClock::default(); let participants = into_participant_ids(&TestGenerators::new_contiguous_participant_ids(4, 3.into())); @@ -775,9 +772,8 @@ mod tests { assert_eq!(pending_requests.get_requests_to_attempt().len(), 0); } - #[test] + #[test_log::test] fn test_pending_signature_requests_leader_retry() { - init_logging(LogFormat::Plain); let clock = FakeClock::default(); let participants = into_participant_ids(&TestGenerators::new_contiguous_participant_ids(4, 3.into())); @@ -875,9 +871,8 @@ mod tests { assert_eq!(pending_requests.get_requests_to_attempt().len(), 0); } - #[test] + #[test_log::test] fn test_pending_ckd_requests_abort_after_maximum_attempts() { - init_logging(LogFormat::Plain); let clock = FakeClock::default(); let participants = into_participant_ids(&TestGenerators::new_contiguous_participant_ids(4, 3.into())); @@ -913,9 +908,8 @@ mod tests { assert_eq!(pending_requests.get_requests_to_attempt().len(), 0); } - #[test] + #[test_log::test] fn test_pending_signature_requests_abort_after_maximum_attempts() { - init_logging(LogFormat::Plain); let clock = FakeClock::default(); let participants = into_participant_ids(&TestGenerators::new_contiguous_participant_ids(4, 3.into())); @@ -952,9 +946,8 @@ mod tests { assert_eq!(pending_requests.get_requests_to_attempt().len(), 0); } - #[test] + #[test_log::test] fn test_pending_ckd_requests_discard_old_and_non_canonical_requests() { - init_logging(LogFormat::Plain); let clock = FakeClock::default(); let participants = into_participant_ids(&TestGenerators::new_contiguous_participant_ids(4, 3.into())); @@ -1032,9 +1025,8 @@ mod tests { )); } - #[test] + #[test_log::test] fn test_pending_signature_requests_discard_old_and_non_canonical_requests() { - init_logging(LogFormat::Plain); let clock = FakeClock::default(); let participants = into_participant_ids(&TestGenerators::new_contiguous_participant_ids(4, 3.into())); @@ -1113,9 +1105,8 @@ mod tests { )); } - #[test] + #[test_log::test] fn test_pending_ckd_requests_fallback_leader() { - init_logging(LogFormat::Plain); let clock = FakeClock::default(); let participants = into_participant_ids(&TestGenerators::new_contiguous_participant_ids(4, 3.into())); @@ -1196,9 +1187,8 @@ mod tests { assert_eq!(to_attempt3[0].request.id, req2.id); } - #[test] + #[test_log::test] fn test_pending_signature_requests_fallback_leader() { - init_logging(LogFormat::Plain); let clock = FakeClock::default(); let participants = into_participant_ids(&TestGenerators::new_contiguous_participant_ids(4, 3.into())); @@ -1280,9 +1270,8 @@ mod tests { assert_eq!(to_attempt3[0].request.id, req2.id); } - #[test] + #[test_log::test] fn test_ckd_request_latency_debug() { - init_logging(LogFormat::Plain); let clock = FakeClock::default(); let participants = into_participant_ids(&TestGenerators::new_contiguous_participant_ids(4, 3.into())); @@ -1325,9 +1314,8 @@ mod tests { ); } - #[test] + #[test_log::test] fn test_signature_request_latency_debug() { - init_logging(LogFormat::Plain); let clock = FakeClock::default(); let participants = into_participant_ids(&TestGenerators::new_contiguous_participant_ids(4, 3.into())); From 9f26a38ad9049d35cad50f3b08fd84f2d7b83181 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 19 Mar 2026 13:13:20 +0100 Subject: [PATCH 164/176] fix remaining tests --- crates/tee-launcher/src/main.rs | 152 ++++++++++---------------------- 1 file changed, 48 insertions(+), 104 deletions(-) diff --git a/crates/tee-launcher/src/main.rs b/crates/tee-launcher/src/main.rs index f2f5f7c8e..dff322660 100644 --- a/crates/tee-launcher/src/main.rs +++ b/crates/tee-launcher/src/main.rs @@ -160,9 +160,9 @@ async fn run() -> Result<(), LauncherError> { Ok(()) } -/// Inject launcher-controlled config sections (`tee` and `image_config`) into -/// the user-provided MPC node config table. Returns an error if the user -/// config already contains either reserved key. +/// Inject launcher-controlled config section (`tee`) into the user-provided +/// MPC node config table. Returns an error if the user config already +/// contains the reserved key. fn intercept_node_config( mut node_config: toml::Table, tee_config: &TeeConfig, @@ -175,9 +175,9 @@ fn intercept_node_config( Ok(node_config) } -/// Inject launcher-controlled config sections (`tee` and `image_config`) into -/// the user-provided MPC node config table. Returns an error if the user -/// config already contains either reserved key. +/// Inject launcher-controlled config section (`tee`) into the user-provided +/// MPC node config table. Returns an error if the user config already +/// contains the reserved key. /// Insert `value` under `key` in `table`, returning an error if the key /// already exists. fn insert_reserved( @@ -573,7 +573,7 @@ mod tests { use assert_matches::assert_matches; use httpmock::prelude::*; use launcher_interface::types::{ - ApprovedHashes, DockerSha256Digest, ImageConfig, TeeAuthorityConfig, + ApprovedHashes, DockerSha256Digest, TeeAuthorityConfig, TeeConfig, }; use near_mpc_bounded_collections::NonEmptyVec; @@ -767,32 +767,27 @@ mod tests { assert!(!rendered.contains("environment:")); } - fn sample_tee_config() -> TeeAuthorityConfig { - TeeAuthorityConfig::Dstack { - dstack_endpoint: "/var/run/dstack.sock".to_string(), - quote_upload_url: "https://example.com/quote".to_string(), - } - } - - fn sample_image_config() -> ImageConfig { - ImageConfig { + fn sample_tee_config() -> TeeConfig { + TeeConfig { + authority: TeeAuthorityConfig::Dstack { + dstack_endpoint: "/var/run/dstack.sock".to_string(), + quote_upload_url: "https://example.com/quote".to_string(), + }, image_hash: sample_digest(), latest_allowed_hash_file_path: "/mnt/shared/image-digest.bin".into(), } } #[test] - fn intercept_config_injects_tee_and_image_config() { + fn intercept_config_injects_tee_config() { // given let config: toml::Table = toml::from_str(r#"home_dir = "/data""#).unwrap(); // when - let result = - intercept_node_config(config, &sample_tee_config(), &sample_image_config()).unwrap(); + let result = intercept_node_config(config, &sample_tee_config()).unwrap(); // then assert!(result.contains_key("tee")); - assert!(result.contains_key("image_config")); assert_eq!(result["home_dir"].as_str(), Some("/data")); } @@ -807,7 +802,7 @@ type = "Local" .unwrap(); // when - let result = intercept_node_config(config, &sample_tee_config(), &sample_image_config()); + let result = intercept_node_config(config, &sample_tee_config()); // then assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { @@ -816,61 +811,16 @@ type = "Local" } #[test] - fn intercept_config_rejects_user_provided_image_config_key() { - // given - let config: toml::Table = toml::from_str( - r#"[image_config] -image_hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000" -latest_allowed_hash_file_path = "/evil" -"#, - ) - .unwrap(); - - // when - let result = intercept_node_config(config, &sample_tee_config(), &sample_image_config()); - - // then - assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { - assert_eq!(key, "image_config"); - }); - } - - #[test] - fn intercept_config_rejects_both_reserved_keys_reports_tee_first() { - // given — both reserved keys present - let config: toml::Table = toml::from_str( - r#" -[tee] -type = "Local" -[image_config] -image_hash = "sha256:0000000000000000000000000000000000000000000000000000000000000000" -latest_allowed_hash_file_path = "/evil" -"#, - ) - .unwrap(); - - // when - let result = intercept_node_config(config, &sample_tee_config(), &sample_image_config()); - - // then — tee is checked first - assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { - assert_eq!(key, "tee"); - }); - } - - #[test] - fn intercept_config_empty_table_gets_both_keys() { + fn intercept_config_empty_table_gets_tee_key() { // given let config = toml::Table::new(); // when - let result = - intercept_node_config(config, &sample_tee_config(), &sample_image_config()).unwrap(); + let result = intercept_node_config(config, &sample_tee_config()).unwrap(); // then assert!(result.contains_key("tee")); - assert!(result.contains_key("image_config")); - assert_eq!(result.len(), 2); + assert_eq!(result.len(), 1); } #[test] @@ -887,34 +837,37 @@ key = "value" .unwrap(); // when - let result = - intercept_node_config(config, &sample_tee_config(), &sample_image_config()).unwrap(); + let result = intercept_node_config(config, &sample_tee_config()).unwrap(); // then assert_eq!(result["home_dir"].as_str(), Some("/data")); assert_eq!(result["port"].as_integer(), Some(8080)); assert_eq!(result["nested"]["key"].as_str(), Some("value")); assert!(result.contains_key("tee")); - assert!(result.contains_key("image_config")); } #[test] fn intercept_config_dstack_tee_config_serializes_correctly() { // given let config = toml::Table::new(); - let tee = TeeAuthorityConfig::Dstack { - dstack_endpoint: "/my/socket".to_string(), - quote_upload_url: "https://example.com".to_string(), + let tee = TeeConfig { + authority: TeeAuthorityConfig::Dstack { + dstack_endpoint: "/my/socket".to_string(), + quote_upload_url: "https://example.com".to_string(), + }, + image_hash: sample_digest(), + latest_allowed_hash_file_path: "/mnt/shared/image-digest.bin".into(), }; // when - let result = intercept_node_config(config, &tee, &sample_image_config()).unwrap(); + let result = intercept_node_config(config, &tee).unwrap(); // then let tee_table = result["tee"].as_table().unwrap(); - assert_eq!(tee_table["dstack_endpoint"].as_str(), Some("/my/socket")); + let authority = tee_table["authority"].as_table().unwrap(); + assert_eq!(authority["dstack_endpoint"].as_str(), Some("/my/socket")); assert_eq!( - tee_table["quote_upload_url"].as_str(), + authority["quote_upload_url"].as_str(), Some("https://example.com") ); } @@ -923,11 +876,14 @@ key = "value" fn intercept_config_local_tee_config_serializes_correctly() { // given let config = toml::Table::new(); + let tee = TeeConfig { + authority: TeeAuthorityConfig::Local, + image_hash: sample_digest(), + latest_allowed_hash_file_path: "/mnt/shared/image-digest.bin".into(), + }; // when - let result = - intercept_node_config(config, &TeeAuthorityConfig::Local, &sample_image_config()) - .unwrap(); + let result = intercept_node_config(config, &tee).unwrap(); // then — Local variant is a unit variant; just verify the key exists assert!(result.contains_key("tee")); @@ -940,19 +896,23 @@ key = "value" fn intercept_config_image_config_contains_expected_fields() { // given let config = toml::Table::new(); - let image_cfg = ImageConfig { + let tee = TeeConfig { + authority: TeeAuthorityConfig::Dstack { + dstack_endpoint: "/var/run/dstack.sock".to_string(), + quote_upload_url: "https://example.com/quote".to_string(), + }, image_hash: digest('b'), latest_allowed_hash_file_path: "/some/path".into(), }; // when - let result = intercept_node_config(config, &sample_tee_config(), &image_cfg).unwrap(); + let result = intercept_node_config(config, &tee).unwrap(); // then - let ic = result["image_config"].as_table().unwrap(); - assert!(ic["image_hash"].as_str().unwrap().contains("bbbb")); + let tee_table = result["tee"].as_table().unwrap(); + assert!(tee_table["image_hash"].as_str().unwrap().contains("bbbb")); assert_eq!( - ic["latest_allowed_hash_file_path"].as_str(), + tee_table["latest_allowed_hash_file_path"].as_str(), Some("/some/path") ); } @@ -963,14 +923,12 @@ key = "value" let config: toml::Table = toml::from_str(r#"home_dir = "/data""#).unwrap(); // when - let result = - intercept_node_config(config, &sample_tee_config(), &sample_image_config()).unwrap(); + let result = intercept_node_config(config, &sample_tee_config()).unwrap(); let toml_str = toml::to_string(&result).unwrap(); // then — the output can be parsed back let reparsed: toml::Table = toml::from_str(&toml_str).unwrap(); assert!(reparsed.contains_key("tee")); - assert!(reparsed.contains_key("image_config")); assert_eq!(reparsed["home_dir"].as_str(), Some("/data")); } @@ -980,7 +938,7 @@ key = "value" let config: toml::Table = toml::from_str(r#"tee = "sneaky""#).unwrap(); // when - let result = intercept_node_config(config, &sample_tee_config(), &sample_image_config()); + let result = intercept_node_config(config, &sample_tee_config()); // then — any occupied entry is rejected regardless of value type assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { @@ -988,20 +946,6 @@ key = "value" }); } - #[test] - fn intercept_config_image_config_as_non_table_value_is_rejected() { - // given - let config: toml::Table = toml::from_str(r#"image_config = 42"#).unwrap(); - - // when - let result = intercept_node_config(config, &sample_tee_config(), &sample_image_config()); - - // then - assert_matches!(result, Err(LauncherError::ReservedConfigKey(key)) => { - assert_eq!(key, "image_config"); - }); - } - // --- select_image_hash --- #[test] From 52f7edb179e5997290cbd0eaf8956ad70b8220f5 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 19 Mar 2026 13:21:09 +0100 Subject: [PATCH 165/176] update digests --- deployment/cvm-deployment/launcher_docker_compose.yaml | 4 ++-- deployment/cvm-deployment/launcher_docker_compose_nontee.yaml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/deployment/cvm-deployment/launcher_docker_compose.yaml b/deployment/cvm-deployment/launcher_docker_compose.yaml index 9e9ec0fda..342f76bbf 100644 --- a/deployment/cvm-deployment/launcher_docker_compose.yaml +++ b/deployment/cvm-deployment/launcher_docker_compose.yaml @@ -2,14 +2,14 @@ version: '3.8' services: launcher: - image: nearone/mpc-launcher@sha256:sha256:e0816222685dadecec3a70c303dc05dbe098aa006d1236748ebd6701afce50de + image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316 container_name: launcher environment: - PLATFORM=TEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:e2ef71c220158f9ee19a265d583647eedb4e0cd7ca37021fbf0ab34e3d214ed0 # 3.7.0 + - DEFAULT_IMAGE_DIGEST=sha256:6027170974405afd60446b5079060d166e1231a2c8a4fb818088e7ba5b6171cd volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml index 167ac7ef6..56fea1fb5 100644 --- a/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml +++ b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml @@ -1,12 +1,12 @@ services: launcher: - image: nearone/mpc-launcher@sha256:e0816222685dadecec3a70c303dc05dbe098aa006d1236748ebd6701afce50de + image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316 container_name: "${LAUNCHER_IMAGE_NAME}" environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:e2ef71c220158f9ee19a265d583647eedb4e0cd7ca37021fbf0ab34e3d214ed0 # 3.7.0 + - DEFAULT_IMAGE_DIGEST=sha256:6027170974405afd60446b5079060d166e1231a2c8a4fb818088e7ba5b6171cd volumes: - /var/run/docker.sock:/var/run/docker.sock From 5f8647fa8d4ce8653300a2dd0c0c3335286aafec Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 19 Mar 2026 13:37:53 +0100 Subject: [PATCH 166/176] update toml tiles --- deployment/localnet/tee/frodo.toml | 28 ++++++++++++++++------------ deployment/localnet/tee/sam.toml | 22 +++++++++++++--------- deployment/testnet/frodo.toml | 30 +++++++++++++++++------------- deployment/testnet/sam.toml | 30 +++++++++++++++++------------- 4 files changed, 63 insertions(+), 47 deletions(-) diff --git a/deployment/localnet/tee/frodo.toml b/deployment/localnet/tee/frodo.toml index 270931953..458354508 100644 --- a/deployment/localnet/tee/frodo.toml +++ b/deployment/localnet/tee/frodo.toml @@ -6,25 +6,29 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 port_mappings = [ - { host =8080, container =8080 }, - { host =24566, container =24566 }, - { host =13001, container =13001 }, + { host = 8080, container = 8080 }, + { host = 24566, container = 24566 }, + { host = 13001, container = 13001 }, ] -[mpc_config] +[mpc_node_config] home_dir = "/data" -[mpc_config.near_init] +[mpc_node_config.log] +format = "plain" +filter = "info" + +[mpc_node_config.near_init] chain_id = "mpc-localnet" boot_nodes = "" genesis_path = "/app/localnet-genesis.json" download_genesis = false -[mpc_config.secrets] +[mpc_node_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" -[mpc_config.node] +[mpc_node_config.node] my_near_account_id = "frodo.test.near" near_responder_account_id = "frodo.test.near" number_of_responder_keys = 1 @@ -32,26 +36,26 @@ web_ui = "0.0.0.0:8080" migration_web_ui = "0.0.0.0:8078" cores = 4 -[mpc_config.node.indexer] +[mpc_node_config.node.indexer] validate_genesis = false sync_mode = "Latest" concurrency = 1 mpc_contract_id = "mpc-contract.test.near" finality = "optimistic" -[mpc_config.node.triple] +[mpc_node_config.node.triple] concurrency = 2 desired_triples_to_buffer = 128 timeout_sec = 60 parallel_triple_generation_stagger_time_sec = 1 -[mpc_config.node.presignature] +[mpc_node_config.node.presignature] concurrency = 4 desired_presignatures_to_buffer = 64 timeout_sec = 60 -[mpc_config.node.signature] +[mpc_node_config.node.signature] timeout_sec = 60 -[mpc_config.node.ckd] +[mpc_node_config.node.ckd] timeout_sec = 60 diff --git a/deployment/localnet/tee/sam.toml b/deployment/localnet/tee/sam.toml index c424e66fb..a691a6414 100644 --- a/deployment/localnet/tee/sam.toml +++ b/deployment/localnet/tee/sam.toml @@ -11,20 +11,24 @@ port_mappings = [ { host = 13002, container = 13002 }, ] -[mpc_config] +[mpc_node_config] home_dir = "/data" -[mpc_config.near_init] +[mpc_node_config.log] +format = "plain" +filter = "info" + +[mpc_node_config.near_init] chain_id = "mpc-localnet" boot_nodes = "" genesis_path = "/app/localnet-genesis.json" download_genesis = false -[mpc_config.secrets] +[mpc_node_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" -[mpc_config.node] +[mpc_node_config.node] my_near_account_id = "sam.test.near" near_responder_account_id = "sam.test.near" number_of_responder_keys = 1 @@ -32,26 +36,26 @@ web_ui = "0.0.0.0:8080" migration_web_ui = "0.0.0.0:8078" cores = 4 -[mpc_config.node.indexer] +[mpc_node_config.node.indexer] validate_genesis = false sync_mode = "Latest" concurrency = 1 mpc_contract_id = "mpc-contract.test.near" finality = "optimistic" -[mpc_config.node.triple] +[mpc_node_config.node.triple] concurrency = 2 desired_triples_to_buffer = 128 timeout_sec = 60 parallel_triple_generation_stagger_time_sec = 1 -[mpc_config.node.presignature] +[mpc_node_config.node.presignature] concurrency = 4 desired_presignatures_to_buffer = 64 timeout_sec = 60 -[mpc_config.node.signature] +[mpc_node_config.node.signature] timeout_sec = 60 -[mpc_config.node.ckd] +[mpc_node_config.node.ckd] timeout_sec = 60 diff --git a/deployment/testnet/frodo.toml b/deployment/testnet/frodo.toml index a5a937915..cb6bc96f2 100644 --- a/deployment/testnet/frodo.toml +++ b/deployment/testnet/frodo.toml @@ -6,26 +6,30 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 port_mappings = [ - { host =8080, container =8080 }, - { host =24567, container =24567 }, - { host =13001, container =13001 }, - { host =80, container =80 }, + { host = 8080, container = 8080 }, + { host = 24567, container = 24567 }, + { host = 13001, container = 13001 }, + { host = 80, container = 80 }, ] -[mpc_config] +[mpc_node_config] home_dir = "/data" -[mpc_config.near_init] +[mpc_node_config.log] +format = "json" +filter = "info" + +[mpc_node_config.near_init] chain_id = "testnet" boot_nodes = "" download_genesis = true download_config = "rpc" -[mpc_config.secrets] +[mpc_node_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" -[mpc_config.node] +[mpc_node_config.node] my_near_account_id = "$FRODO_ACCOUNT" near_responder_account_id = "$FRODO_ACCOUNT" number_of_responder_keys = 50 @@ -34,7 +38,7 @@ migration_web_ui = "0.0.0.0:8078" pprof_bind_address = "0.0.0.0:34001" cores = 12 -[mpc_config.node.indexer] +[mpc_node_config.node.indexer] validate_genesis = false sync_mode = "Latest" concurrency = 1 @@ -42,19 +46,19 @@ mpc_contract_id = "$MPC_CONTRACT_ACCOUNT" finality = "optimistic" port_override = 80 -[mpc_config.node.triple] +[mpc_node_config.node.triple] concurrency = 2 desired_triples_to_buffer = 1000000 timeout_sec = 60 parallel_triple_generation_stagger_time_sec = 1 -[mpc_config.node.presignature] +[mpc_node_config.node.presignature] concurrency = 16 desired_presignatures_to_buffer = 8192 timeout_sec = 60 -[mpc_config.node.signature] +[mpc_node_config.node.signature] timeout_sec = 60 -[mpc_config.node.ckd] +[mpc_node_config.node.ckd] timeout_sec = 60 diff --git a/deployment/testnet/sam.toml b/deployment/testnet/sam.toml index f0b4dcf87..40e9a00aa 100644 --- a/deployment/testnet/sam.toml +++ b/deployment/testnet/sam.toml @@ -6,26 +6,30 @@ rpc_request_timeout_secs = 10 rpc_request_interval_secs = 1 rpc_max_attempts = 20 port_mappings = [ - { host =8080, container =8080 }, - { host =24567, container =24567 }, - { host =13002, container =13002 }, - { host =80, container =80 }, + { host = 8080, container = 8080 }, + { host = 24567, container = 24567 }, + { host = 13002, container = 13002 }, + { host = 80, container = 80 }, ] -[mpc_config] +[mpc_node_config] home_dir = "/data" -[mpc_config.near_init] +[mpc_node_config.log] +format = "json" +filter = "info" + +[mpc_node_config.near_init] chain_id = "testnet" boot_nodes = "" download_genesis = true download_config = "rpc" -[mpc_config.secrets] +[mpc_node_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" -[mpc_config.node] +[mpc_node_config.node] my_near_account_id = "$SAM_ACCOUNT" near_responder_account_id = "$SAM_ACCOUNT" number_of_responder_keys = 50 @@ -34,7 +38,7 @@ migration_web_ui = "0.0.0.0:8078" pprof_bind_address = "0.0.0.0:34001" cores = 12 -[mpc_config.node.indexer] +[mpc_node_config.node.indexer] validate_genesis = false sync_mode = "Latest" concurrency = 1 @@ -42,19 +46,19 @@ mpc_contract_id = "$MPC_CONTRACT_ACCOUNT" finality = "optimistic" port_override = 80 -[mpc_config.node.triple] +[mpc_node_config.node.triple] concurrency = 2 desired_triples_to_buffer = 1000000 timeout_sec = 60 parallel_triple_generation_stagger_time_sec = 1 -[mpc_config.node.presignature] +[mpc_node_config.node.presignature] concurrency = 16 desired_presignatures_to_buffer = 8192 timeout_sec = 60 -[mpc_config.node.signature] +[mpc_node_config.node.signature] timeout_sec = 60 -[mpc_config.node.ckd] +[mpc_node_config.node.ckd] timeout_sec = 60 From b67c455f68d220324bef398ed49533357f17dd7b Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 19 Mar 2026 13:40:47 +0100 Subject: [PATCH 167/176] fix user config --- deployment/cvm-deployment/user-config.toml | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/deployment/cvm-deployment/user-config.toml b/deployment/cvm-deployment/user-config.toml index 68baee94e..d6728db9d 100644 --- a/deployment/cvm-deployment/user-config.toml +++ b/deployment/cvm-deployment/user-config.toml @@ -17,19 +17,23 @@ port_mappings = [ { host = 8079, container = 8079 }, ] -[mpc_config] +[mpc_node_config] home_dir = "/data" -[mpc_config.near_init] +[mpc_node_config.log] +format = "json" +filter = "info" + +[mpc_node_config.near_init] chain_id = "testnet" boot_nodes = "" download_genesis = true download_config = "rpc" -[mpc_config.secrets] +[mpc_node_config.secrets] secret_store_key_hex = "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA" -[mpc_config.node] +[mpc_node_config.node] my_near_account_id = "mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet" near_responder_account_id = "mpc-3-barak-launch1-b654bfa0a52e.5035bf56abb0.testnet" number_of_responder_keys = 1 @@ -37,26 +41,26 @@ web_ui = "0.0.0.0:8080" migration_web_ui = "0.0.0.0:8079" cores = 4 -[mpc_config.node.indexer] +[mpc_node_config.node.indexer] validate_genesis = false sync_mode = "Latest" concurrency = 1 mpc_contract_id = "mpc-contract-barak-launch1-4c5e2fe1fb42.5035bf56abb0.testnet" finality = "optimistic" -[mpc_config.node.triple] +[mpc_node_config.node.triple] concurrency = 2 desired_triples_to_buffer = 128 timeout_sec = 60 parallel_triple_generation_stagger_time_sec = 1 -[mpc_config.node.presignature] +[mpc_node_config.node.presignature] concurrency = 4 desired_presignatures_to_buffer = 64 timeout_sec = 60 -[mpc_config.node.signature] +[mpc_node_config.node.signature] timeout_sec = 60 -[mpc_config.node.ckd] +[mpc_node_config.node.ckd] timeout_sec = 60 From 6bd0ba92a04a91f31bbda47ed6536e5fe80fe9e1 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 19 Mar 2026 13:43:06 +0100 Subject: [PATCH 168/176] update image tags --- deployment/cvm-deployment/user-config.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/cvm-deployment/user-config.toml b/deployment/cvm-deployment/user-config.toml index d6728db9d..eb89dcc32 100644 --- a/deployment/cvm-deployment/user-config.toml +++ b/deployment/cvm-deployment/user-config.toml @@ -1,5 +1,5 @@ [launcher_config] -image_tags = ["3.7.0"] +image_tags = ["main-9515e18"] image_name = "nearone/mpc-node" registry = "registry.hub.docker.com" rpc_request_timeout_secs = 10 From 43a0fc1e9fb1d4569b8a553b0a0e6f6d8b606602 Mon Sep 17 00:00:00 2001 From: Daniel Sharifi Date: Thu, 19 Mar 2026 13:50:09 +0100 Subject: [PATCH 169/176] . --- deployment/cvm-deployment/launcher_docker_compose.yaml | 2 +- deployment/cvm-deployment/launcher_docker_compose_nontee.yaml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/deployment/cvm-deployment/launcher_docker_compose.yaml b/deployment/cvm-deployment/launcher_docker_compose.yaml index 342f76bbf..25a19acff 100644 --- a/deployment/cvm-deployment/launcher_docker_compose.yaml +++ b/deployment/cvm-deployment/launcher_docker_compose.yaml @@ -9,7 +9,7 @@ services: environment: - PLATFORM=TEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:6027170974405afd60446b5079060d166e1231a2c8a4fb818088e7ba5b6171cd + - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980 volumes: - /var/run/docker.sock:/var/run/docker.sock diff --git a/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml index 56fea1fb5..8fd42c74e 100644 --- a/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml +++ b/deployment/cvm-deployment/launcher_docker_compose_nontee.yaml @@ -6,7 +6,7 @@ services: environment: - PLATFORM=NONTEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:6027170974405afd60446b5079060d166e1231a2c8a4fb818088e7ba5b6171cd + - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980 volumes: - /var/run/docker.sock:/var/run/docker.sock From 2c4f2a05c7108dc1587eee0cd86d6144f6312aa5 Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Sun, 15 Mar 2026 14:31:38 +0000 Subject: [PATCH 170/176] update localnet scripts to generate TOML config for Rust launcher The tee-launcher now expects a structured TOML config instead of flat key=value env files. Update deploy-tee-localnet.sh and single-node.sh to render TOML configs matching the new Config struct, and rename the old template to .bak. Co-Authored-By: Claude Opus 4.6 (1M context) --- localnet/tee/scripts/deploy-tee-localnet.sh | 20 ++++++- .../how-to-run-localnet-tee-setup-script.md | 2 +- .../tee/scripts/node.conf.localnet.toml.tpl | 53 +++++++++++++++++++ ...ocalnet.tpl => node.conf.localnet.tpl.bak} | 0 localnet/tee/scripts/single-node.sh | 19 ++++++- 5 files changed, 89 insertions(+), 5 deletions(-) create mode 100644 localnet/tee/scripts/node.conf.localnet.toml.tpl rename localnet/tee/scripts/{node.conf.localnet.tpl => node.conf.localnet.tpl.bak} (100%) diff --git a/localnet/tee/scripts/deploy-tee-localnet.sh b/localnet/tee/scripts/deploy-tee-localnet.sh index 31345f765..ce5c00fe3 100644 --- a/localnet/tee/scripts/deploy-tee-localnet.sh +++ b/localnet/tee/scripts/deploy-tee-localnet.sh @@ -144,11 +144,25 @@ MODE="${MODE:-testnet}" # testnet|localnet # templates live here (UPDATED for move to localnet/tee/scripts) ENV_TPL="$REPO_ROOT/localnet/tee/scripts/node.env.tpl" if [ "$MODE" = "localnet" ]; then - CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.tpl" + CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.toml.tpl" else CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.tpl" fi +# Convert comma-separated "src:dst" port string to TOML inline table array entries. +# E.g. "8080:8080,24566:24566" -> " { src = 8080, dst = 8080 },\n { src = 24566, dst = 24566 }," +ports_to_toml() { + local ports="$1" result="" + IFS=',' read -ra pairs <<< "$ports" + for pair in "${pairs[@]}"; do + local src="${pair%%:*}" + local dst="${pair##*:}" + result+=" { src = $src, dst = $dst }, +" + done + echo -n "$result" +} + WORKDIR="/tmp/$USER/mpc_testnet_scale/$MPC_NETWORK_NAME" mkdir -p "$WORKDIR" @@ -714,7 +728,7 @@ render_node_files_range() { local env_out conf_out env_out="$WORKDIR/node${i}.env" - conf_out="$WORKDIR/node${i}.conf" + conf_out="$WORKDIR/node${i}.toml" export APP_NAME="$app_name" export VMM_RPC @@ -750,6 +764,8 @@ render_node_files_range() { export MPC_SECRET_STORE_KEY="$(printf '%032x' "$i")" export MPC_CONTRACT_ID="$MPC_CONTRACT_ACCOUNT" export PORTS="8080:8080,24566:24566,${future_port}:${future_port}" + export PORTS_TOML + PORTS_TOML="$(ports_to_toml "$PORTS")" export NEAR_BOOT_NODES="ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566" envsubst <"$ENV_TPL" >"$env_out" diff --git a/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md b/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md index 15b7b5bee..770964e9c 100644 --- a/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md +++ b/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md @@ -153,7 +153,7 @@ All generated files are stored under: ``` Important artifacts: -- `node{i}.conf`, `node{i}.env` +- `node{i}.toml`, `node{i}.env` - `keys.json` - `init_args.json` diff --git a/localnet/tee/scripts/node.conf.localnet.toml.tpl b/localnet/tee/scripts/node.conf.localnet.toml.tpl new file mode 100644 index 000000000..2c1b44ab0 --- /dev/null +++ b/localnet/tee/scripts/node.conf.localnet.toml.tpl @@ -0,0 +1,53 @@ +[launcher_config] +image_tags = ["${MPC_IMAGE_TAGS}"] +image_name = "${MPC_IMAGE_NAME}" +registry = "${MPC_REGISTRY}" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 + +[docker_command_config.port_mappings] +ports = [ +${PORTS_TOML}] + +[mpc_config] +home_dir = "/data" + +[mpc_config.secrets] +secret_store_key_hex = "${MPC_SECRET_STORE_KEY}" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_config.tee.authority] +type = "local" + +[mpc_config.node] +my_near_account_id = "${MPC_ACCOUNT_ID}" +near_responder_account_id = "${MPC_ACCOUNT_ID}" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +cores = 4 + +[mpc_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "${MPC_CONTRACT_ID}" +finality = "optimistic" + +[mpc_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_config.node.signature] +timeout_sec = 60 + +[mpc_config.node.ckd] +timeout_sec = 60 diff --git a/localnet/tee/scripts/node.conf.localnet.tpl b/localnet/tee/scripts/node.conf.localnet.tpl.bak similarity index 100% rename from localnet/tee/scripts/node.conf.localnet.tpl rename to localnet/tee/scripts/node.conf.localnet.tpl.bak diff --git a/localnet/tee/scripts/single-node.sh b/localnet/tee/scripts/single-node.sh index 13b62d135..c1a0eb435 100755 --- a/localnet/tee/scripts/single-node.sh +++ b/localnet/tee/scripts/single-node.sh @@ -132,13 +132,26 @@ DISK="${DISK:-500G}" REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" TEE_LAUNCHER_DIR="$REPO_ROOT/tee_launcher" ENV_TPL="${ENV_TPL:-$REPO_ROOT/localnet/tee/scripts/node.env.tpl}" -CONF_TPL="${CONF_TPL:-$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.tpl}" +CONF_TPL="${CONF_TPL:-$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.toml.tpl}" + +# Convert comma-separated "src:dst" port string to TOML inline table array entries. +ports_to_toml() { + local ports="$1" result="" + IFS=',' read -ra pairs <<< "$ports" + for pair in "${pairs[@]}"; do + local src="${pair%%:*}" + local dst="${pair##*:}" + result+=" { src = $src, dst = $dst }, +" + done + echo -n "$result" +} WORKDIR="${WORKDIR:-$(mktemp -d /tmp/mpc_localnet_one_node.XXXXXX)}" mkdir -p "$WORKDIR" log "Work directory: $WORKDIR" ENV_OUT="$WORKDIR/node.env" -CONF_OUT="$WORKDIR/node.conf" +CONF_OUT="$WORKDIR/node.toml" PUBLIC_DATA_JSON_OUT="${PUBLIC_DATA_JSON_OUT:-$WORKDIR/public_data.json}" near_account_exists() { @@ -193,6 +206,8 @@ render_env_and_conf() { export MPC_CONTRACT_ID="$CONTRACT_ACCOUNT" export MPC_SECRET_STORE_KEY="${MPC_SECRET_STORE_KEY:-00000000000000000000000000000000}" export PORTS="${PORTS:-8080:8080,24566:24566,${FUTURE_PORT}:${FUTURE_PORT}}" + export PORTS_TOML + PORTS_TOML="$(ports_to_toml "$PORTS")" envsubst <"$ENV_TPL" >"$ENV_OUT" envsubst <"$CONF_TPL" >"$CONF_OUT" From 0b9c46213880dc7972010ff98d14f5b3a82b3e2e Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Mon, 16 Mar 2026 08:06:09 +0000 Subject: [PATCH 171/176] add near_init section to TOML template Match the updated sam.toml which now includes [mpc_config.near_init] with chain_id, boot_nodes, and genesis_path fields. Co-Authored-By: Claude Opus 4.6 (1M context) --- localnet/tee/scripts/node.conf.localnet.toml.tpl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/localnet/tee/scripts/node.conf.localnet.toml.tpl b/localnet/tee/scripts/node.conf.localnet.toml.tpl index 2c1b44ab0..0a584c6e5 100644 --- a/localnet/tee/scripts/node.conf.localnet.toml.tpl +++ b/localnet/tee/scripts/node.conf.localnet.toml.tpl @@ -13,6 +13,11 @@ ${PORTS_TOML}] [mpc_config] home_dir = "/data" +[mpc_config.near_init] +chain_id = "mpc-localnet" +boot_nodes = "${NEAR_BOOT_NODES}" +genesis_path = "/app/localnet-genesis.json" + [mpc_config.secrets] secret_store_key_hex = "${MPC_SECRET_STORE_KEY}" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" From 971a9d6b7372cca01d71bb4d8a4cdffca7461324 Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Thu, 19 Mar 2026 16:47:45 +0000 Subject: [PATCH 172/176] update test-utils attestation assets from localnet run --- crates/test-utils/assets/app_compose.json | 4 +- crates/test-utils/assets/collateral.json | 12 +-- .../assets/launcher_image_compose.yaml | 6 +- crates/test-utils/assets/mpc_image_digest.txt | 2 +- .../assets/near_account_public_key.pub | 2 +- .../test-utils/assets/near_p2p_public_key.pub | 2 +- crates/test-utils/assets/public_data.json | 87 ++++--------------- crates/test-utils/assets/quote.json | 2 +- crates/test-utils/assets/tcb_info.json | 18 ++-- 9 files changed, 43 insertions(+), 92 deletions(-) diff --git a/crates/test-utils/assets/app_compose.json b/crates/test-utils/assets/app_compose.json index a1c06b183..7119afab6 100644 --- a/crates/test-utils/assets/app_compose.json +++ b/crates/test-utils/assets/app_compose.json @@ -1,8 +1,8 @@ { "manifest_version": 2, - "name": "mpc-localnet-one-node-1771748631", + "name": "mpc-local-node0-testnet-tee", "runner": "docker-compose", - "docker_compose_file": "version: '3.8'\n\nservices:\n launcher:\n image: nearone/mpc-launcher@sha256:e28cb0425db06255fe5fc7aadb79534ac63c94c7a721f75c1af1e934d2eb0701\n\n container_name: launcher\n\n environment:\n - PLATFORM=TEE\n - DOCKER_CONTENT_TRUST=1\n - DEFAULT_IMAGE_DIGEST=sha256:6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45\n\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n - /var/run/dstack.sock:/var/run/dstack.sock\n - /tapp:/tapp:ro\n - shared-volume:/mnt/shared:ro\n\n security_opt:\n - no-new-privileges:true\n\n read_only: true\n\n tmpfs:\n - /tmp\n\nvolumes:\n shared-volume:\n name: shared-volume\n", + "docker_compose_file": "version: '3.8'\n\nservices:\n launcher:\n image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316\n\n container_name: launcher\n\n environment:\n - PLATFORM=TEE\n - DOCKER_CONTENT_TRUST=1\n - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980\n\n volumes:\n - /var/run/docker.sock:/var/run/docker.sock\n - /var/run/dstack.sock:/var/run/dstack.sock\n - /tapp:/tapp:ro\n - shared-volume:/mnt/shared:rw\n\n security_opt:\n - no-new-privileges:true\n\n read_only: true\n\n tmpfs:\n - /tmp\n\nvolumes:\n shared-volume:\n name: shared-volume\n", "kms_enabled": false, "gateway_enabled": false, "local_key_provider_enabled": true, diff --git a/crates/test-utils/assets/collateral.json b/crates/test-utils/assets/collateral.json index a4e0b9e3f..3a22de564 100644 --- a/crates/test-utils/assets/collateral.json +++ b/crates/test-utils/assets/collateral.json @@ -1,11 +1,11 @@ { "pck_crl_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICljCCAj2gAwIBAgIVAJVvXc29G+HpQEnJ1PQzzgFXC95UMAoGCCqGSM49BAMC\nMGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD\nb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw\nCQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHAxIjAg\nBgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoMEUludGVs\nIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0Ex\nCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENSB/7t21lXSO\n2Cuzpxw74eJB72EyDGgW5rXCtx2tVTLq6hKk6z+UiRZCnqR7psOvgqFeSxlmTlJl\neTmi2WYz3qOBuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBS\nBgNVHR8ESzBJMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2Vy\ndmljZXMuaW50ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUlW9d\nzb0b4elAScnU9DPOAVcL3lQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB\nAf8CAQAwCgYIKoZIzj0EAwIDRwAwRAIgXsVki0w+i6VYGW3UF/22uaXe0YJDj1Ue\nnA+TjD1ai5cCICYb1SAmD5xkfTVpvo4UoyiSYxrDWLmUR4CI9NKyfPN+\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "root_ca_crl": "308201203081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3235303332303131323135375a170d3236303430333131323135375aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020347003044022030c9fce1438da0a94e4fffdd46c9650e393be6e5a7862d4e4e73527932d04af302206539efe3f734c3d7df20d9dfc4630e1c7ff0439a0f8ece101f15b5eaff9b4f33", - "pck_crl": "30820d1730820cbd020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303232313133343330335a170d3236303332333133343330335a30820be9303302146fc34e5023e728923435d61aa4b83c618166ad35170d3236303232313133343330335a300c300a0603551d1504030a01013034021500efae6e9715fca13b87e333e8261ed6d990a926ad170d3236303232313133343330335a300c300a0603551d1504030a01013034021500fd608648629cba73078b4d492f4b3ea741ad08cd170d3236303232313133343330335a300c300a0603551d1504030a010130340215008af924184e1d5afddd73c3d63a12f5e8b5737e56170d3236303232313133343330335a300c300a0603551d1504030a01013034021500b1257978cfa9ccdd0759abf8c5ca72fae3a78a9b170d3236303232313133343330335a300c300a0603551d1504030a01013033021474fea614a972be0e2843f2059835811ed872f9b3170d3236303232313133343330335a300c300a0603551d1504030a01013034021500f9c4ef56b3ab48d577e108baedf4bf88014214b9170d3236303232313133343330335a300c300a0603551d1504030a010130330214071de0778f9e5fc4f2878f30d6b07c9a30e6b30b170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cde2424f972cea94ff239937f4d80c25029dd60b170d3236303232313133343330335a300c300a0603551d1504030a0101303302146c3319e5109b64507d3cf1132ce00349ef527319170d3236303232313133343330335a300c300a0603551d1504030a01013034021500df08d756b66a7497f43b5bb58ada04d3f4f7a937170d3236303232313133343330335a300c300a0603551d1504030a01013033021428af485b6cf67e409a39d5cb5aee4598f7a8fa7b170d3236303232313133343330335a300c300a0603551d1504030a01013034021500fb8b2daec092cada8aa9bc4ff2f1c20d0346668c170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cd4850ac52bdcc69a6a6f058c8bc57bbd0b5f864170d3236303232313133343330335a300c300a0603551d1504030a01013034021500994dd3666f5275fb805f95dd02bd50cb2679d8ad170d3236303232313133343330335a300c300a0603551d1504030a0101303302140702136900252274d9035eedf5457462fad0ef4c170d3236303232313133343330335a300c300a0603551d1504030a01013033021461f2bf73e39b4e04aa27d801bd73d24319b5bf80170d3236303232313133343330335a300c300a0603551d1504030a0101303302143992be851b96902eff38959e6c2eff1b0651a4b5170d3236303232313133343330335a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3236303232313133343330335a300c300a0603551d1504030a010130330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3236303232313133343330335a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3236303232313133343330335a300c300a0603551d1504030a0101303302147ae37748a9f912f4c63ba7ab07c593ce1d1d1181170d3236303232313133343330335a300c300a0603551d1504030a01013033021413884b33269938c195aa170fca75da177538df0b170d3236303232313133343330335a300c300a0603551d1504030a0101303402150085d3c9381b77a7e04d119c9e5ad6749ff3ffab87170d3236303232313133343330335a300c300a0603551d1504030a0101303402150093887ca4411e7a923bd1fed2819b2949f201b5b4170d3236303232313133343330335a300c300a0603551d1504030a0101303302142498dc6283930996fd8bf23a37acbe26a3bed457170d3236303232313133343330335a300c300a0603551d1504030a010130340215008a66f1a749488667689cc3903ac54c662b712e73170d3236303232313133343330335a300c300a0603551d1504030a01013034021500afc13610bdd36cb7985d106481a880d3a01fda07170d3236303232313133343330335a300c300a0603551d1504030a01013034021500efe04b2c33d036aac96ca673bf1e9a47b64d5cbb170d3236303232313133343330335a300c300a0603551d1504030a0101303402150083d9ac8d8bb509d1c6c809ad712e8430559ed7f3170d3236303232313133343330335a300c300a0603551d1504030a0101303302147931fd50b5071c1bbfc5b7b6ded8b45b9d8b8529170d3236303232313133343330335a300c300a0603551d1504030a0101303302141fa20e2970bde5d57f7b8ddf8339484e1f1d0823170d3236303232313133343330335a300c300a0603551d1504030a0101303302141e87b2c3b32d8d23e411cef34197b95af0c8adf5170d3236303232313133343330335a300c300a0603551d1504030a010130340215009afd2ee90a473550a167d996911437c7502d1f09170d3236303232313133343330335a300c300a0603551d1504030a0101303302144481b0f11728a13b696d3ea9c770a0b15ec58dda170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a7859f57982ef0e67d37bc8ef2ef5ac835ff1aa9170d3236303232313133343330335a300c300a0603551d1504030a010130340215009d67753b81e47090aea763fbec4c4549bcdb9933170d3236303232313133343330335a300c300a0603551d1504030a01013033021434bfbb7a1d9c568147e118b614f7b76ed3ef68df170d3236303232313133343330335a300c300a0603551d1504030a0101303302142c3cc6fe9279db1516d5ce39f2a898cda5a175e1170d3236303232313133343330335a300c300a0603551d1504030a010130330214717948687509234be979e4b7dce6f31bef64b68c170d3236303232313133343330335a300c300a0603551d1504030a010130340215009d76ef2c39c136e8658b6e7396b1d7445a27631f170d3236303232313133343330335a300c300a0603551d1504030a01013034021500c3e025fca995f36f59b48467939e3e34e6361a6f170d3236303232313133343330335a300c300a0603551d1504030a010130340215008c5f6b3257da05b17429e2e61ba965d67330606a170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a17c51722ec1e0c3278fe8bdf052059cbec4e648170d3236303232313133343330335a300c300a0603551d1504030a01013033021411c943b866fa04944e3057e5a67146596475a023170d3236303232313133343330335a300c300a0603551d1504030a01013034021500be6913785406155454a28885a515b3da5767d3a9170d3236303232313133343330335a300c300a0603551d1504030a0101303302140ac5ec91bd934c07b9ea41625e9cc09681002eb0170d3236303232313133343330335a300c300a0603551d1504030a0101303302146d51a0eabc1f9a1e9ddd5b36bdda1631ae6c182a170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a52c5d71c4166b4fc0ded8b679951e5ee9193de5170d3236303232313133343330335a300c300a0603551d1504030a010130330214249779aedd85fcac93c8853516be5428c26b3bf8170d3236303232313133343330335a300c300a0603551d1504030a01013033021434ba4fd76bde5309210cf1dd1ffb494c638a9157170d3236303232313133343330335a300c300a0603551d1504030a010130330214043e04919daae13443248395094d2a2eacfc76fe170d3236303232313133343330335a300c300a0603551d1504030a01013033021447fc577d2d094cbdf270715ed6848a93855ad34b170d3236303232313133343330335a300c300a0603551d1504030a0101303302147d62a2f5e6f386e469653fffff045d0a8178e8e7170d3236303232313133343330335a300c300a0603551d1504030a01013034021500c4ed45fe026bb6a47eaec35ea80b7ef407ce062c170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cf9831077a3ca4f1a2c56867bf55b18eccbeffd8170d3236303232313133343330335a300c300a0603551d1504030a0101303302146c2b81d7ea2e436720ce29f1d0b1ccb7a218600f170d3236303232313133343330335a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d040302034800304502203bd1f94fcc12868b9760bcf134ce34b844713b667fcb9e4d2207fc0cd81566de022100ef021d2ca3ff8bd81861b3a3722f955703b69f5133cfe5fea3d4a1098922f103", + "root_ca_crl": "308201223081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303232363133303430305a170d3237303232363133303430305aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020349003046022100c252ed59c795ba2b11496a4a99758bb8cbc380a1ebbb0865be69f2c4b38bb6400221009a7d8b03602a9ee2d62322d759166d6933d24d9dfa01ab3fde4520691d715bd7", + "pck_crl": "30820d1830820cbd020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303331383136303130355a170d3236303431373136303130355a30820be9303302146fc34e5023e728923435d61aa4b83c618166ad35170d3236303331383136303130355a300c300a0603551d1504030a01013034021500efae6e9715fca13b87e333e8261ed6d990a926ad170d3236303331383136303130355a300c300a0603551d1504030a01013034021500fd608648629cba73078b4d492f4b3ea741ad08cd170d3236303331383136303130355a300c300a0603551d1504030a010130340215008af924184e1d5afddd73c3d63a12f5e8b5737e56170d3236303331383136303130355a300c300a0603551d1504030a01013034021500b1257978cfa9ccdd0759abf8c5ca72fae3a78a9b170d3236303331383136303130355a300c300a0603551d1504030a01013033021474fea614a972be0e2843f2059835811ed872f9b3170d3236303331383136303130355a300c300a0603551d1504030a01013034021500f9c4ef56b3ab48d577e108baedf4bf88014214b9170d3236303331383136303130355a300c300a0603551d1504030a010130330214071de0778f9e5fc4f2878f30d6b07c9a30e6b30b170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cde2424f972cea94ff239937f4d80c25029dd60b170d3236303331383136303130355a300c300a0603551d1504030a0101303302146c3319e5109b64507d3cf1132ce00349ef527319170d3236303331383136303130355a300c300a0603551d1504030a01013034021500df08d756b66a7497f43b5bb58ada04d3f4f7a937170d3236303331383136303130355a300c300a0603551d1504030a01013033021428af485b6cf67e409a39d5cb5aee4598f7a8fa7b170d3236303331383136303130355a300c300a0603551d1504030a01013034021500fb8b2daec092cada8aa9bc4ff2f1c20d0346668c170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cd4850ac52bdcc69a6a6f058c8bc57bbd0b5f864170d3236303331383136303130355a300c300a0603551d1504030a01013034021500994dd3666f5275fb805f95dd02bd50cb2679d8ad170d3236303331383136303130355a300c300a0603551d1504030a0101303302140702136900252274d9035eedf5457462fad0ef4c170d3236303331383136303130355a300c300a0603551d1504030a01013033021461f2bf73e39b4e04aa27d801bd73d24319b5bf80170d3236303331383136303130355a300c300a0603551d1504030a0101303302143992be851b96902eff38959e6c2eff1b0651a4b5170d3236303331383136303130355a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3236303331383136303130355a300c300a0603551d1504030a010130330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3236303331383136303130355a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3236303331383136303130355a300c300a0603551d1504030a0101303302147ae37748a9f912f4c63ba7ab07c593ce1d1d1181170d3236303331383136303130355a300c300a0603551d1504030a01013033021413884b33269938c195aa170fca75da177538df0b170d3236303331383136303130355a300c300a0603551d1504030a0101303402150085d3c9381b77a7e04d119c9e5ad6749ff3ffab87170d3236303331383136303130355a300c300a0603551d1504030a0101303402150093887ca4411e7a923bd1fed2819b2949f201b5b4170d3236303331383136303130355a300c300a0603551d1504030a0101303302142498dc6283930996fd8bf23a37acbe26a3bed457170d3236303331383136303130355a300c300a0603551d1504030a010130340215008a66f1a749488667689cc3903ac54c662b712e73170d3236303331383136303130355a300c300a0603551d1504030a01013034021500afc13610bdd36cb7985d106481a880d3a01fda07170d3236303331383136303130355a300c300a0603551d1504030a01013034021500efe04b2c33d036aac96ca673bf1e9a47b64d5cbb170d3236303331383136303130355a300c300a0603551d1504030a0101303402150083d9ac8d8bb509d1c6c809ad712e8430559ed7f3170d3236303331383136303130355a300c300a0603551d1504030a0101303302147931fd50b5071c1bbfc5b7b6ded8b45b9d8b8529170d3236303331383136303130355a300c300a0603551d1504030a0101303302141fa20e2970bde5d57f7b8ddf8339484e1f1d0823170d3236303331383136303130355a300c300a0603551d1504030a0101303302141e87b2c3b32d8d23e411cef34197b95af0c8adf5170d3236303331383136303130355a300c300a0603551d1504030a010130340215009afd2ee90a473550a167d996911437c7502d1f09170d3236303331383136303130355a300c300a0603551d1504030a0101303302144481b0f11728a13b696d3ea9c770a0b15ec58dda170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a7859f57982ef0e67d37bc8ef2ef5ac835ff1aa9170d3236303331383136303130355a300c300a0603551d1504030a010130340215009d67753b81e47090aea763fbec4c4549bcdb9933170d3236303331383136303130355a300c300a0603551d1504030a01013033021434bfbb7a1d9c568147e118b614f7b76ed3ef68df170d3236303331383136303130355a300c300a0603551d1504030a0101303302142c3cc6fe9279db1516d5ce39f2a898cda5a175e1170d3236303331383136303130355a300c300a0603551d1504030a010130330214717948687509234be979e4b7dce6f31bef64b68c170d3236303331383136303130355a300c300a0603551d1504030a010130340215009d76ef2c39c136e8658b6e7396b1d7445a27631f170d3236303331383136303130355a300c300a0603551d1504030a01013034021500c3e025fca995f36f59b48467939e3e34e6361a6f170d3236303331383136303130355a300c300a0603551d1504030a010130340215008c5f6b3257da05b17429e2e61ba965d67330606a170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a17c51722ec1e0c3278fe8bdf052059cbec4e648170d3236303331383136303130355a300c300a0603551d1504030a01013033021411c943b866fa04944e3057e5a67146596475a023170d3236303331383136303130355a300c300a0603551d1504030a01013034021500be6913785406155454a28885a515b3da5767d3a9170d3236303331383136303130355a300c300a0603551d1504030a0101303302140ac5ec91bd934c07b9ea41625e9cc09681002eb0170d3236303331383136303130355a300c300a0603551d1504030a0101303302146d51a0eabc1f9a1e9ddd5b36bdda1631ae6c182a170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a52c5d71c4166b4fc0ded8b679951e5ee9193de5170d3236303331383136303130355a300c300a0603551d1504030a010130330214249779aedd85fcac93c8853516be5428c26b3bf8170d3236303331383136303130355a300c300a0603551d1504030a01013033021434ba4fd76bde5309210cf1dd1ffb494c638a9157170d3236303331383136303130355a300c300a0603551d1504030a010130330214043e04919daae13443248395094d2a2eacfc76fe170d3236303331383136303130355a300c300a0603551d1504030a01013033021447fc577d2d094cbdf270715ed6848a93855ad34b170d3236303331383136303130355a300c300a0603551d1504030a0101303302147d62a2f5e6f386e469653fffff045d0a8178e8e7170d3236303331383136303130355a300c300a0603551d1504030a01013034021500c4ed45fe026bb6a47eaec35ea80b7ef407ce062c170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cf9831077a3ca4f1a2c56867bf55b18eccbeffd8170d3236303331383136303130355a300c300a0603551d1504030a0101303302146c2b81d7ea2e436720ce29f1d0b1ccb7a218600f170d3236303331383136303130355a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d0403020349003046022100c71b1e53814b3437773403491a1e10a53043880e98868ee957bfa044031c3bf3022100d12d92717d516bd91b8a0c275fce3dc886f4cbc8a2300275fcf01a78691eeefd", "tcb_info_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG\nA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw\nb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD\nVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv\nP+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju\nypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f\nBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz\nLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK\nQEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG\nSM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh\nAKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "tcb_info": "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2026-02-21T13:31:57Z\",\"nextUpdate\":\"2026-03-23T13:31:57Z\",\"fmspc\":\"b0c06f000000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":18,\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\"},\"tdxModuleIdentities\":[{\"id\":\"TDX_03\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":3},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]},{\"id\":\"TDX_01\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":6},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]},{\"tcb\":{\"isvsvn\":2},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]}]}],\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":3,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":4,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":5,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2018-01-04T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00106\",\"INTEL-SA-00115\",\"INTEL-SA-00135\",\"INTEL-SA-00203\",\"INTEL-SA-00220\",\"INTEL-SA-00233\",\"INTEL-SA-00270\",\"INTEL-SA-00293\",\"INTEL-SA-00320\",\"INTEL-SA-00329\",\"INTEL-SA-00381\",\"INTEL-SA-00389\",\"INTEL-SA-00477\",\"INTEL-SA-00837\",\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]}]}", - "tcb_info_signature": "6cb0f2b2d37350c8d25f478cac1b5a341cc1d6deb8379f0aa5c8708ebf00fbd7fb39b146ebd086593f7b86f37c8cb82a0e3df178857625ef95d64f636167c5eb", + "tcb_info": "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2026-03-18T15:43:27Z\",\"nextUpdate\":\"2026-04-17T15:43:27Z\",\"fmspc\":\"b0c06f000000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":18,\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\"},\"tdxModuleIdentities\":[{\"id\":\"TDX_03\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":3},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]},{\"id\":\"TDX_01\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":6},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]},{\"tcb\":{\"isvsvn\":2},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]}]}],\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":3,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":4,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":5,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2018-01-04T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00106\",\"INTEL-SA-00115\",\"INTEL-SA-00135\",\"INTEL-SA-00203\",\"INTEL-SA-00220\",\"INTEL-SA-00233\",\"INTEL-SA-00270\",\"INTEL-SA-00293\",\"INTEL-SA-00320\",\"INTEL-SA-00329\",\"INTEL-SA-00381\",\"INTEL-SA-00389\",\"INTEL-SA-00477\",\"INTEL-SA-00837\",\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]}]}", + "tcb_info_signature": "77bd013b1fbb1162604d1b76e2ead05315b61963d15c34e6c8dfbec009930dcaa55e2026eb3befad7df463210a85c392d9b77caf72c76e4ec03f02c71855a95c", "qe_identity_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG\nA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw\nb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD\nVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv\nP+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju\nypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f\nBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz\nLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK\nQEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG\nSM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh\nAKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "qe_identity": "{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2026-02-21T13:43:47Z\",\"nextUpdate\":\"2026-03-23T13:43:47Z\",\"tcbEvaluationDataNumber\":18,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"11000000000000000000000000000000\",\"attributesMask\":\"FBFFFFFFFFFFFFFF0000000000000000\",\"mrsigner\":\"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]}", - "qe_identity_signature": "8d31db300d8fbd61c3525f177a5963ef729139e358b9b572822f7a72812d3eff83fc06a97b0cc9d921a3a12ad63f547266aff1629b9bae4ec31d69305bdb6c52" + "qe_identity": "{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2026-03-18T15:16:33Z\",\"nextUpdate\":\"2026-04-17T15:16:33Z\",\"tcbEvaluationDataNumber\":18,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"11000000000000000000000000000000\",\"attributesMask\":\"FBFFFFFFFFFFFFFF0000000000000000\",\"mrsigner\":\"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]}", + "qe_identity_signature": "a0c628117e0d40c9b2682dff38cd21283c70bc319546e3a05c530458c8a532f3bc67d5c66196dd012f9fe8490da0648b9ec0bbeb43ed48662a969871567abec4" } diff --git a/crates/test-utils/assets/launcher_image_compose.yaml b/crates/test-utils/assets/launcher_image_compose.yaml index 314e066f2..25a19acff 100644 --- a/crates/test-utils/assets/launcher_image_compose.yaml +++ b/crates/test-utils/assets/launcher_image_compose.yaml @@ -2,20 +2,20 @@ version: '3.8' services: launcher: - image: nearone/mpc-launcher@sha256:e28cb0425db06255fe5fc7aadb79534ac63c94c7a721f75c1af1e934d2eb0701 + image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316 container_name: launcher environment: - PLATFORM=TEE - DOCKER_CONTENT_TRUST=1 - - DEFAULT_IMAGE_DIGEST=sha256:6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45 + - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980 volumes: - /var/run/docker.sock:/var/run/docker.sock - /var/run/dstack.sock:/var/run/dstack.sock - /tapp:/tapp:ro - - shared-volume:/mnt/shared:ro + - shared-volume:/mnt/shared:rw security_opt: - no-new-privileges:true diff --git a/crates/test-utils/assets/mpc_image_digest.txt b/crates/test-utils/assets/mpc_image_digest.txt index 9b36d86aa..ea6e0fda2 100644 --- a/crates/test-utils/assets/mpc_image_digest.txt +++ b/crates/test-utils/assets/mpc_image_digest.txt @@ -1 +1 @@ -6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45 \ No newline at end of file +6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980 \ No newline at end of file diff --git a/crates/test-utils/assets/near_account_public_key.pub b/crates/test-utils/assets/near_account_public_key.pub index 48fca4efd..2f1c3c5c4 100644 --- a/crates/test-utils/assets/near_account_public_key.pub +++ b/crates/test-utils/assets/near_account_public_key.pub @@ -1 +1 @@ -ed25519:7DeYpZ9UDY9t6hgw9zakSu2VrseuVpwhaTwLfWUNppqF \ No newline at end of file +ed25519:3QLLYQfJcDv5UD9Spo8dR32XrWiZdTZrA93KCJ7DMcN3 \ No newline at end of file diff --git a/crates/test-utils/assets/near_p2p_public_key.pub b/crates/test-utils/assets/near_p2p_public_key.pub index 9c9f00845..cacb92286 100644 --- a/crates/test-utils/assets/near_p2p_public_key.pub +++ b/crates/test-utils/assets/near_p2p_public_key.pub @@ -1 +1 @@ -ed25519:os7wBGxbPavhE9F56eSU9G2TqDT3YFdLsZqeSijqAT4 \ No newline at end of file +ed25519:9vx7F6MxKXcvT4rpxrtTdowAUA2gDvNssHymSWan8oAt \ No newline at end of file diff --git a/crates/test-utils/assets/public_data.json b/crates/test-utils/assets/public_data.json index 16c8d6d65..0709587f2 100644 --- a/crates/test-utils/assets/public_data.json +++ b/crates/test-utils/assets/public_data.json @@ -1,82 +1,33 @@ { - "near_signer_public_key": "ed25519:7DeYpZ9UDY9t6hgw9zakSu2VrseuVpwhaTwLfWUNppqF", - "near_p2p_public_key": "ed25519:os7wBGxbPavhE9F56eSU9G2TqDT3YFdLsZqeSijqAT4", + "near_signer_public_key": "ed25519:3QLLYQfJcDv5UD9Spo8dR32XrWiZdTZrA93KCJ7DMcN3", + "near_p2p_public_key": "ed25519:9vx7F6MxKXcvT4rpxrtTdowAUA2gDvNssHymSWan8oAt", "near_responder_public_keys": [ - "ed25519:Ko4VqMsrDPapf3M3isuu4nyycU4ZJdhAVxF46SAaiyU", - "ed25519:J3qZzxjRCQeQnQwr4P9E18Au69CNabBYBX5stc9BijaL", - "ed25519:A3LveKbnJUtQES94HU6sapE61hPC7pFErWXmBTEWqRJ6", - "ed25519:EZJ8k8VVa7XDRkXpYDJ1muJU4Zt3W8emcuFAQennTSPF", - "ed25519:BJKEe7FvsEYbAqYoAnuZcCroYnoN7byUC35kPnB6Kt8a", - "ed25519:B1e1eCns6FipzA1EpqYsGEmW5FshJ1VG9p38wjoLXoU5", - "ed25519:DSbs39dUxCkr76joLopC19wX22Z7mkTtyWqFfqfjBHuc", - "ed25519:De8qFXwVzdMkrxjsvQuzdAQrukjdykvigsBABjQK6h2m", - "ed25519:7RJA77oDDvTApLQCS6eDJwZiZzZzYvrNQKgdQvWTBWRR", - "ed25519:DMAYtSo9DfpQuZQCmwYkfbbDiteU4bvvTnFYREfuGbpR", - "ed25519:289rt9nZTEVzUrA9N4txXWSUCMfthoT6WEWgSiKUXx2D", - "ed25519:Dtf6K5Bnv6BYEw5vuA2jgfPm1B6XfJGcRcMCWnH6RAuX", - "ed25519:hSmYb4czfXMxdAUVhiAUTRfJJh8vMvyUzS5QcXEUGyz", - "ed25519:41rhaYLfd7QtYKogU49a2hWETfHkFYUfiTfzsB6iF5an", - "ed25519:63HvyThe2VeZJgsFDKUxBboxmgyzrNhyovPvVacso8HC", - "ed25519:Do2HSNDBS1k9RAr3KHNyTuXa68QciP4PNy8oNyNKng1", - "ed25519:7p7ptby14Wo5k6iCwwNZjJcsSLCJx39pc38riabktri1", - "ed25519:72FNME7ZZA7C7YtaoYfGSDAweP3GcR5BEC2xKWAqJ1r7", - "ed25519:BwbS1W9PygQLKpnrARAQDFDUeuepLUXSY5D7fVn1ByiA", - "ed25519:Aa6evgmAi2JSkskYrvD1cMTNSeBAFR8MQDbrw4J1q9hK", - "ed25519:7HWJCvJvRyW6Evi4jhBBKi5g1PWVDDgFVv2wFYngmwi4", - "ed25519:FRr8TDyHEVcGiTDBT7hasRCJpQiMe2J4beh9f3R2o15c", - "ed25519:G8iGeBgatiMB8QPzqBwSPdyPpYUd8EQ87vjLJQFFdD2Q", - "ed25519:AcFS33c45SiyhGjq7vAFNY9L9Fa5eLfcp12pWMNK6jyk", - "ed25519:7qmeGN3zVxYrZZEC52AkRKakm3L77NiVKd1NPPkCdqAr", - "ed25519:3drp9wPWoKgCFWER9pvy6psyqfgEYmfsFmCgoTaYP1mz", - "ed25519:2zx3A1noqdWQFxX8JczMUfm1cya434qDLaCUbaP7Xexo", - "ed25519:HSuYfvb98M7wpaVjFhP2aoW7Kv1VpDxU6NcWRd7taKTS", - "ed25519:GEW3FMMkGtesgqS5CiCw2WzYkt7tNZFPc37FEZTFDZDq", - "ed25519:8XwzZaRNxJDrmqwkz3Q6V7x91nVjhM4H3q1d69pDVzxT", - "ed25519:5Y8JwpvzoDDgRxdc9MpVUjC6BZ5AWNAgVNkXQCK6nvEB", - "ed25519:HmPSb6ZMyHdSjGW8Jzwuwp2KMJ6y4oBgbdGD4SgYviK2", - "ed25519:DMSKnq9htoLTFYrx3MVKbHLTpsJqPD5eMmF4FsuUWQxZ", - "ed25519:Hg2EfJSGMyxY9utafnFXiZBhzLHiqzaGBhfgxpJEcEFX", - "ed25519:7Q4afPMBCNATdibmnRWYtBmpLwr7ybDRcFfpTa8hxkNK", - "ed25519:3NvXx3CPsYJNMg4r9J5GbFfU5hf8xaKnYjd18nLWSF3t", - "ed25519:FaVhEYnD1VsYows1FqqwQFzWw9svirb6R7is8ZA2fYKT", - "ed25519:89FdAs8nEkn74SRsRuNierfkj4LfWtRsKBryLskuaBVE", - "ed25519:FucAj3Fv9X7Ri7TrrpoGQm2mk4x1Zx45p4rFe3ovUREY", - "ed25519:2KSeFPShZbRXcdxWBpU4gEfhQBcgyJfvPWB8TSMNhnzh", - "ed25519:nVqZNPk5nTN1MMJdSY97v1qQYnqGRsvKuf9iQB4PYs9", - "ed25519:5ZNs2mqURhwaF9HCrxx6uKR7GAR4Gp8hXEskm3LSLodU", - "ed25519:CTQSM5AVVRGjrSJ1duoofYPnJbqJNiAwmVyqAtSvsRUS", - "ed25519:5T3wY4pB84QfAeUFwPGGYwGVN542nWXxDEJU4oYJAVL3", - "ed25519:BikJcvyjmUDM4q1tfzU5rmocxZgZvyd7TwRyktPSbmAD", - "ed25519:6WbYqApHTHC33uPkJg88i7YHkKBRABEYq2b8snHcFXCC", - "ed25519:BhpGVA3SnXdLEJxWY1y74G4NA2jJM4SCxbsuv2Hmg8Rn", - "ed25519:Dgu2PJfqSkf6w4DuB8UsJZS8RfPiaDwBDWAfpNj3sVVr", - "ed25519:WVjzNCCt5ogksujdh8XcrgkJu97uPZsD5wE1A7WvW4g", - "ed25519:G89VLPw46CxgdV99DexVt4zviTqy1WD1G6Y2XPZxuc7o" + "ed25519:DG5cLS9eQBta3JzGAtsc7CJYviC7ktvUhWds8XCzsfdG" ], "tee_participant_info": { "Dstack": { - "quote": [4, 0, 2, 0, 129, 0, 0, 0, 0, 0, 0, 0, 147, 154, 114, 51, 247, 156, 76, 169, 148, 10, 13, 179, 149, 127, 6, 7, 61, 153, 138, 108, 16, 87, 107, 253, 246, 246, 237, 142, 155, 133, 233, 50, 0, 0, 0, 0, 11, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 240, 99, 40, 14, 148, 251, 5, 31, 93, 215, 177, 252, 89, 206, 154, 172, 66, 187, 150, 29, 248, 212, 75, 112, 156, 155, 15, 248, 122, 123, 77, 246, 72, 101, 123, 166, 209, 24, 149, 137, 254, 171, 29, 90, 60, 154, 157, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 231, 2, 6, 0, 0, 0, 0, 0, 240, 109, 253, 166, 220, 225, 207, 144, 77, 78, 43, 171, 29, 195, 112, 99, 76, 249, 92, 239, 162, 206, 178, 222, 46, 238, 18, 124, 147, 130, 105, 128, 144, 215, 164, 161, 62, 20, 197, 54, 236, 108, 156, 60, 143, 168, 112, 119, 1, 110, 105, 242, 11, 52, 142, 61, 104, 116, 87, 244, 229, 124, 63, 254, 60, 251, 113, 56, 246, 146, 168, 139, 218, 191, 234, 199, 146, 253, 217, 141, 72, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 115, 190, 47, 112, 190, 239, 183, 11, 72, 166, 16, 158, 237, 71, 21, 215, 39, 13, 70, 131, 179, 191, 53, 111, 162, 95, 175, 191, 26, 167, 110, 57, 233, 18, 126, 110, 104, 140, 205, 169, 139, 218, 177, 212, 212, 127, 70, 167, 181, 35, 39, 141, 79, 145, 78, 232, 223, 14, 200, 12, 209, 195, 212, 152, 203, 241, 21, 43, 12, 94, 175, 101, 186, 217, 66, 80, 114, 135, 74, 63, 207, 137, 30, 139, 1, 113, 61, 61, 153, 55, 227, 224, 210, 108, 21, 219, 244, 146, 76, 7, 245, 6, 111, 61, 198, 133, 152, 68, 24, 67, 68, 48, 106, 163, 38, 56, 23, 21, 61, 202, 238, 133, 175, 151, 210, 62, 12, 11, 150, 239, 224, 115, 29, 136, 101, 168, 116, 126, 81, 185, 227, 81, 172, 201, 31, 132, 227, 182, 188, 148, 117, 137, 112, 2, 51, 248, 229, 0, 217, 195, 123, 45, 121, 115, 131, 137, 239, 97, 209, 209, 129, 132, 245, 11, 172, 178, 11, 152, 186, 41, 211, 45, 250, 26, 147, 59, 17, 246, 215, 102, 85, 0, 1, 195, 223, 53, 227, 107, 60, 23, 144, 43, 16, 55, 239, 206, 246, 210, 246, 13, 185, 162, 204, 96, 156, 224, 219, 17, 240, 182, 48, 192, 132, 93, 226, 121, 108, 93, 157, 145, 44, 64, 158, 145, 229, 42, 58, 76, 114, 127, 244, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 204, 16, 0, 0, 10, 124, 154, 105, 114, 192, 236, 4, 179, 208, 82, 53, 162, 157, 20, 21, 152, 133, 71, 85, 85, 21, 59, 229, 138, 60, 128, 119, 31, 41, 74, 151, 97, 88, 211, 84, 31, 214, 163, 180, 43, 240, 136, 29, 116, 231, 138, 255, 148, 105, 65, 17, 10, 5, 221, 116, 226, 119, 8, 76, 218, 67, 115, 16, 142, 188, 151, 224, 189, 139, 21, 116, 87, 28, 157, 151, 225, 226, 250, 218, 147, 80, 231, 144, 252, 223, 62, 103, 176, 31, 51, 101, 181, 44, 82, 180, 72, 148, 151, 10, 88, 144, 81, 87, 230, 135, 174, 102, 165, 143, 241, 229, 60, 148, 151, 208, 187, 151, 100, 64, 82, 171, 52, 1, 40, 143, 31, 160, 6, 0, 70, 16, 0, 0, 4, 4, 25, 27, 4, 255, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 231, 0, 0, 0, 0, 0, 0, 0, 229, 163, 167, 181, 216, 48, 194, 149, 59, 152, 83, 76, 108, 89, 163, 163, 79, 220, 52, 233, 51, 247, 245, 137, 143, 10, 133, 207, 8, 132, 107, 202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 158, 42, 124, 111, 148, 143, 23, 71, 78, 52, 167, 252, 67, 237, 3, 15, 124, 21, 99, 241, 186, 189, 223, 99, 64, 200, 46, 14, 84, 168, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 215, 98, 66, 167, 203, 62, 30, 139, 2, 195, 192, 57, 51, 40, 206, 207, 77, 159, 207, 61, 144, 77, 52, 130, 175, 199, 184, 139, 101, 54, 6, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 180, 243, 159, 196, 112, 17, 166, 99, 200, 182, 156, 136, 179, 201, 15, 15, 47, 98, 23, 37, 24, 68, 126, 153, 62, 34, 18, 139, 203, 16, 236, 185, 233, 46, 116, 134, 201, 84, 179, 122, 113, 148, 250, 173, 226, 9, 104, 70, 208, 7, 5, 223, 11, 239, 88, 182, 207, 12, 115, 185, 54, 184, 154, 234, 32, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 5, 0, 94, 14, 0, 0, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 69, 56, 84, 67, 67, 66, 74, 97, 103, 65, 119, 73, 66, 65, 103, 73, 85, 102, 50, 83, 98, 121, 119, 107, 77, 86, 84, 74, 75, 85, 53, 55, 47, 74, 119, 66, 112, 56, 69, 100, 104, 80, 48, 52, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 119, 10, 99, 68, 69, 105, 77, 67, 65, 71, 65, 49, 85, 69, 65, 119, 119, 90, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 66, 68, 83, 121, 66, 81, 98, 71, 70, 48, 90, 109, 57, 121, 98, 83, 66, 68, 81, 84, 69, 97, 77, 66, 103, 71, 65, 49, 85, 69, 67, 103, 119, 82, 10, 83, 87, 53, 48, 90, 87, 119, 103, 81, 50, 57, 121, 99, 71, 57, 121, 89, 88, 82, 112, 98, 50, 52, 120, 70, 68, 65, 83, 66, 103, 78, 86, 66, 65, 99, 77, 67, 49, 78, 104, 98, 110, 82, 104, 73, 69, 78, 115, 89, 88, 74, 104, 77, 81, 115, 119, 67, 81, 89, 68, 86, 81, 81, 73, 10, 68, 65, 74, 68, 81, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 66, 104, 77, 67, 86, 86, 77, 119, 72, 104, 99, 78, 77, 106, 85, 120, 77, 84, 65, 50, 77, 68, 99, 122, 78, 122, 77, 48, 87, 104, 99, 78, 77, 122, 73, 120, 77, 84, 65, 50, 77, 68, 99, 122, 78, 122, 77, 48, 10, 87, 106, 66, 119, 77, 83, 73, 119, 73, 65, 89, 68, 86, 81, 81, 68, 68, 66, 108, 74, 98, 110, 82, 108, 98, 67, 66, 84, 82, 49, 103, 103, 85, 69, 78, 76, 73, 69, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 77, 82, 111, 119, 71, 65, 89, 68, 86, 81, 81, 75, 10, 68, 66, 70, 74, 98, 110, 82, 108, 98, 67, 66, 68, 98, 51, 74, 119, 98, 51, 74, 104, 100, 71, 108, 118, 98, 106, 69, 85, 77, 66, 73, 71, 65, 49, 85, 69, 66, 119, 119, 76, 85, 50, 70, 117, 100, 71, 69, 103, 81, 50, 120, 104, 99, 109, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 10, 66, 65, 103, 77, 65, 107, 78, 66, 77, 81, 115, 119, 67, 81, 89, 68, 86, 81, 81, 71, 69, 119, 74, 86, 85, 122, 66, 90, 77, 66, 77, 71, 66, 121, 113, 71, 83, 77, 52, 57, 65, 103, 69, 71, 67, 67, 113, 71, 83, 77, 52, 57, 65, 119, 69, 72, 65, 48, 73, 65, 66, 71, 112, 118, 10, 48, 89, 117, 89, 114, 113, 65, 117, 83, 75, 66, 122, 75, 108, 117, 98, 54, 109, 76, 43, 114, 118, 102, 68, 53, 65, 106, 89, 79, 51, 81, 78, 103, 102, 87, 122, 116, 103, 52, 101, 109, 49, 69, 71, 66, 86, 107, 71, 108, 87, 118, 100, 117, 66, 48, 88, 81, 83, 69, 47, 115, 120, 71, 68, 10, 109, 83, 118, 75, 111, 57, 116, 51, 67, 114, 79, 80, 67, 52, 83, 85, 54, 88, 54, 106, 103, 103, 77, 77, 77, 73, 73, 68, 67, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 83, 86, 98, 49, 51, 78, 118, 82, 118, 104, 54, 85, 66, 74, 121, 100, 84, 48, 10, 77, 56, 52, 66, 86, 119, 118, 101, 86, 68, 66, 114, 66, 103, 78, 86, 72, 82, 56, 69, 90, 68, 66, 105, 77, 71, 67, 103, 88, 113, 66, 99, 104, 108, 112, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 70, 119, 97, 83, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 10, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 90, 87, 119, 117, 89, 50, 57, 116, 76, 51, 78, 110, 101, 67, 57, 106, 90, 88, 74, 48, 97, 87, 90, 112, 89, 50, 70, 48, 97, 87, 57, 117, 76, 51, 89, 48, 76, 51, 66, 106, 97, 50, 78, 121, 98, 68, 57, 106, 89, 84, 49, 119, 10, 98, 71, 70, 48, 90, 109, 57, 121, 98, 83, 90, 108, 98, 109, 78, 118, 90, 71, 108, 117, 90, 122, 49, 107, 90, 88, 73, 119, 72, 81, 89, 68, 86, 82, 48, 79, 66, 66, 89, 69, 70, 71, 51, 110, 54, 83, 43, 75, 120, 78, 54, 116, 43, 72, 73, 56, 71, 112, 57, 54, 80, 107, 117, 90, 10, 105, 87, 115, 90, 77, 65, 52, 71, 65, 49, 85, 100, 68, 119, 69, 66, 47, 119, 81, 69, 65, 119, 73, 71, 119, 68, 65, 77, 66, 103, 78, 86, 72, 82, 77, 66, 65, 102, 56, 69, 65, 106, 65, 65, 77, 73, 73, 67, 79, 81, 89, 74, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 10, 66, 73, 73, 67, 75, 106, 67, 67, 65, 105, 89, 119, 72, 103, 89, 75, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 65, 81, 81, 81, 48, 103, 106, 102, 115, 81, 65, 106, 82, 113, 52, 98, 116, 79, 56, 113, 80, 65, 86, 83, 107, 106, 67, 67, 65, 87, 77, 71, 67, 105, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 119, 103, 103, 70, 84, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 66, 65, 103, 69, 69, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 67, 65, 103, 69, 69, 10, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 68, 65, 103, 69, 67, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 69, 65, 103, 69, 67, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 10, 65, 81, 73, 70, 65, 103, 69, 69, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 71, 65, 103, 69, 66, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 72, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 73, 65, 103, 69, 70, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 74, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 75, 65, 103, 69, 65, 10, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 76, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 77, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 10, 65, 81, 73, 78, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 79, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 80, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 81, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 82, 65, 103, 69, 76, 77, 66, 56, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 83, 66, 66, 65, 69, 10, 66, 65, 73, 67, 66, 65, 69, 65, 66, 81, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 77, 66, 65, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 77, 69, 65, 103, 65, 65, 77, 66, 81, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 81, 69, 10, 66, 114, 68, 65, 98, 119, 65, 65, 65, 68, 65, 80, 66, 103, 111, 113, 104, 107, 105, 71, 43, 69, 48, 66, 68, 81, 69, 70, 67, 103, 69, 66, 77, 66, 52, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 89, 69, 69, 68, 97, 57, 104, 116, 65, 56, 65, 74, 47, 90, 10, 50, 70, 109, 97, 76, 53, 74, 113, 47, 75, 69, 119, 82, 65, 89, 75, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 66, 122, 65, 50, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 66, 65, 81, 72, 47, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 67, 65, 81, 72, 47, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 68, 65, 81, 72, 47, 77, 65, 111, 71, 67, 67, 113, 71, 83, 77, 52, 57, 66, 65, 77, 67, 65, 48, 107, 65, 77, 69, 89, 67, 10, 73, 81, 67, 70, 71, 49, 89, 65, 98, 51, 101, 88, 70, 116, 101, 56, 53, 51, 67, 108, 86, 66, 110, 104, 108, 67, 102, 68, 121, 99, 53, 55, 50, 90, 88, 69, 113, 97, 120, 52, 85, 99, 99, 83, 97, 119, 73, 104, 65, 79, 110, 48, 86, 78, 75, 84, 90, 109, 65, 120, 85, 70, 52, 110, 10, 119, 82, 107, 83, 70, 104, 52, 113, 70, 74, 51, 97, 85, 108, 122, 70, 111, 80, 81, 84, 51, 120, 73, 102, 55, 107, 70, 68, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 67, 108, 106, 67, 67, 65, 106, 50, 103, 65, 119, 73, 66, 65, 103, 73, 86, 65, 74, 86, 118, 88, 99, 50, 57, 71, 43, 72, 112, 81, 69, 110, 74, 49, 80, 81, 122, 122, 103, 70, 88, 67, 57, 53, 85, 77, 65, 111, 71, 67, 67, 113, 71, 83, 77, 52, 57, 66, 65, 77, 67, 10, 77, 71, 103, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 77, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 70, 78, 72, 87, 67, 66, 83, 98, 50, 57, 48, 73, 69, 78, 66, 77, 82, 111, 119, 71, 65, 89, 68, 86, 81, 81, 75, 68, 66, 70, 74, 98, 110, 82, 108, 98, 67, 66, 68, 10, 98, 51, 74, 119, 98, 51, 74, 104, 100, 71, 108, 118, 98, 106, 69, 85, 77, 66, 73, 71, 65, 49, 85, 69, 66, 119, 119, 76, 85, 50, 70, 117, 100, 71, 69, 103, 81, 50, 120, 104, 99, 109, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 103, 77, 65, 107, 78, 66, 77, 81, 115, 119, 10, 67, 81, 89, 68, 86, 81, 81, 71, 69, 119, 74, 86, 85, 122, 65, 101, 70, 119, 48, 120, 79, 68, 65, 49, 77, 106, 69, 120, 77, 68, 85, 119, 77, 84, 66, 97, 70, 119, 48, 122, 77, 122, 65, 49, 77, 106, 69, 120, 77, 68, 85, 119, 77, 84, 66, 97, 77, 72, 65, 120, 73, 106, 65, 103, 10, 66, 103, 78, 86, 66, 65, 77, 77, 71, 85, 108, 117, 100, 71, 86, 115, 73, 70, 78, 72, 87, 67, 66, 81, 81, 48, 115, 103, 85, 71, 120, 104, 100, 71, 90, 118, 99, 109, 48, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 10, 73, 69, 78, 118, 99, 110, 66, 118, 99, 109, 70, 48, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 10, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 89, 84, 65, 108, 86, 84, 77, 70, 107, 119, 69, 119, 89, 72, 75, 111, 90, 73, 122, 106, 48, 67, 65, 81, 89, 73, 75, 111, 90, 73, 122, 106, 48, 68, 65, 81, 99, 68, 81, 103, 65, 69, 78, 83, 66, 47, 55, 116, 50, 49, 108, 88, 83, 79, 10, 50, 67, 117, 122, 112, 120, 119, 55, 52, 101, 74, 66, 55, 50, 69, 121, 68, 71, 103, 87, 53, 114, 88, 67, 116, 120, 50, 116, 86, 84, 76, 113, 54, 104, 75, 107, 54, 122, 43, 85, 105, 82, 90, 67, 110, 113, 82, 55, 112, 115, 79, 118, 103, 113, 70, 101, 83, 120, 108, 109, 84, 108, 74, 108, 10, 101, 84, 109, 105, 50, 87, 89, 122, 51, 113, 79, 66, 117, 122, 67, 66, 117, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 81, 105, 90, 81, 122, 87, 87, 112, 48, 48, 105, 102, 79, 68, 116, 74, 86, 83, 118, 49, 65, 98, 79, 83, 99, 71, 114, 68, 66, 83, 10, 66, 103, 78, 86, 72, 82, 56, 69, 83, 122, 66, 74, 77, 69, 101, 103, 82, 97, 66, 68, 104, 107, 70, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 99, 121, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 10, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 90, 87, 119, 117, 89, 50, 57, 116, 76, 48, 108, 117, 100, 71, 86, 115, 85, 48, 100, 89, 85, 109, 57, 118, 100, 69, 78, 66, 76, 109, 82, 108, 99, 106, 65, 100, 66, 103, 78, 86, 72, 81, 52, 69, 70, 103, 81, 85, 108, 87, 57, 100, 10, 122, 98, 48, 98, 52, 101, 108, 65, 83, 99, 110, 85, 57, 68, 80, 79, 65, 86, 99, 76, 51, 108, 81, 119, 68, 103, 89, 68, 86, 82, 48, 80, 65, 81, 72, 47, 66, 65, 81, 68, 65, 103, 69, 71, 77, 66, 73, 71, 65, 49, 85, 100, 69, 119, 69, 66, 47, 119, 81, 73, 77, 65, 89, 66, 10, 65, 102, 56, 67, 65, 81, 65, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 68, 82, 119, 65, 119, 82, 65, 73, 103, 88, 115, 86, 107, 105, 48, 119, 43, 105, 54, 86, 89, 71, 87, 51, 85, 70, 47, 50, 50, 117, 97, 88, 101, 48, 89, 74, 68, 106, 49, 85, 101, 10, 110, 65, 43, 84, 106, 68, 49, 97, 105, 53, 99, 67, 73, 67, 89, 98, 49, 83, 65, 109, 68, 53, 120, 107, 102, 84, 86, 112, 118, 111, 52, 85, 111, 121, 105, 83, 89, 120, 114, 68, 87, 76, 109, 85, 82, 52, 67, 73, 57, 78, 75, 121, 102, 80, 78, 43, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 67, 106, 122, 67, 67, 65, 106, 83, 103, 65, 119, 73, 66, 65, 103, 73, 85, 73, 109, 85, 77, 49, 108, 113, 100, 78, 73, 110, 122, 103, 55, 83, 86, 85, 114, 57, 81, 71, 122, 107, 110, 66, 113, 119, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 119, 10, 97, 68, 69, 97, 77, 66, 103, 71, 65, 49, 85, 69, 65, 119, 119, 82, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 74, 118, 98, 51, 81, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 69, 78, 118, 10, 99, 110, 66, 118, 99, 109, 70, 48, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 67, 122, 65, 74, 10, 66, 103, 78, 86, 66, 65, 89, 84, 65, 108, 86, 84, 77, 66, 52, 88, 68, 84, 69, 52, 77, 68, 85, 121, 77, 84, 69, 119, 78, 68, 85, 120, 77, 70, 111, 88, 68, 84, 81, 53, 77, 84, 73, 122, 77, 84, 73, 122, 78, 84, 107, 49, 79, 86, 111, 119, 97, 68, 69, 97, 77, 66, 103, 71, 10, 65, 49, 85, 69, 65, 119, 119, 82, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 74, 118, 98, 51, 81, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 69, 78, 118, 99, 110, 66, 118, 99, 109, 70, 48, 10, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 89, 84, 10, 65, 108, 86, 84, 77, 70, 107, 119, 69, 119, 89, 72, 75, 111, 90, 73, 122, 106, 48, 67, 65, 81, 89, 73, 75, 111, 90, 73, 122, 106, 48, 68, 65, 81, 99, 68, 81, 103, 65, 69, 67, 54, 110, 69, 119, 77, 68, 73, 89, 90, 79, 106, 47, 105, 80, 87, 115, 67, 122, 97, 69, 75, 105, 55, 10, 49, 79, 105, 79, 83, 76, 82, 70, 104, 87, 71, 106, 98, 110, 66, 86, 74, 102, 86, 110, 107, 89, 52, 117, 51, 73, 106, 107, 68, 89, 89, 76, 48, 77, 120, 79, 52, 109, 113, 115, 121, 89, 106, 108, 66, 97, 108, 84, 86, 89, 120, 70, 80, 50, 115, 74, 66, 75, 53, 122, 108, 75, 79, 66, 10, 117, 122, 67, 66, 117, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 81, 105, 90, 81, 122, 87, 87, 112, 48, 48, 105, 102, 79, 68, 116, 74, 86, 83, 118, 49, 65, 98, 79, 83, 99, 71, 114, 68, 66, 83, 66, 103, 78, 86, 72, 82, 56, 69, 83, 122, 66, 74, 10, 77, 69, 101, 103, 82, 97, 66, 68, 104, 107, 70, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 99, 121, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 10, 90, 87, 119, 117, 89, 50, 57, 116, 76, 48, 108, 117, 100, 71, 86, 115, 85, 48, 100, 89, 85, 109, 57, 118, 100, 69, 78, 66, 76, 109, 82, 108, 99, 106, 65, 100, 66, 103, 78, 86, 72, 81, 52, 69, 70, 103, 81, 85, 73, 109, 85, 77, 49, 108, 113, 100, 78, 73, 110, 122, 103, 55, 83, 86, 10, 85, 114, 57, 81, 71, 122, 107, 110, 66, 113, 119, 119, 68, 103, 89, 68, 86, 82, 48, 80, 65, 81, 72, 47, 66, 65, 81, 68, 65, 103, 69, 71, 77, 66, 73, 71, 65, 49, 85, 100, 69, 119, 69, 66, 47, 119, 81, 73, 77, 65, 89, 66, 65, 102, 56, 67, 65, 81, 69, 119, 67, 103, 89, 73, 10, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 68, 83, 81, 65, 119, 82, 103, 73, 104, 65, 79, 87, 47, 53, 81, 107, 82, 43, 83, 57, 67, 105, 83, 68, 99, 78, 111, 111, 119, 76, 117, 80, 82, 76, 115, 87, 71, 102, 47, 89, 105, 55, 71, 83, 88, 57, 52, 66, 103, 119, 84, 119, 103, 10, 65, 105, 69, 65, 52, 74, 48, 108, 114, 72, 111, 77, 115, 43, 88, 111, 53, 111, 47, 115, 88, 54, 79, 57, 81, 87, 120, 72, 82, 65, 118, 90, 85, 71, 79, 100, 82, 81, 55, 99, 118, 113, 82, 88, 97, 113, 73, 61, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], + "quote": [4, 0, 2, 0, 129, 0, 0, 0, 0, 0, 0, 0, 147, 154, 114, 51, 247, 156, 76, 169, 148, 10, 13, 179, 149, 127, 6, 7, 61, 153, 138, 108, 16, 87, 107, 253, 246, 246, 237, 142, 155, 133, 233, 50, 0, 0, 0, 0, 11, 1, 4, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 123, 240, 99, 40, 14, 148, 251, 5, 31, 93, 215, 177, 252, 89, 206, 154, 172, 66, 187, 150, 29, 248, 212, 75, 112, 156, 155, 15, 248, 122, 123, 77, 246, 72, 101, 123, 166, 209, 24, 149, 137, 254, 171, 29, 90, 60, 154, 157, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 16, 0, 0, 0, 0, 231, 2, 6, 0, 0, 0, 0, 0, 240, 109, 253, 166, 220, 225, 207, 144, 77, 78, 43, 171, 29, 195, 112, 99, 76, 249, 92, 239, 162, 206, 178, 222, 46, 238, 18, 124, 147, 130, 105, 128, 144, 215, 164, 161, 62, 20, 197, 54, 236, 108, 156, 60, 143, 168, 112, 119, 1, 83, 236, 221, 225, 0, 154, 247, 12, 125, 232, 120, 198, 48, 97, 92, 131, 215, 76, 192, 223, 245, 128, 116, 216, 4, 194, 52, 38, 59, 232, 158, 20, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 230, 115, 190, 47, 112, 190, 239, 183, 11, 72, 166, 16, 158, 237, 71, 21, 215, 39, 13, 70, 131, 179, 191, 53, 111, 162, 95, 175, 191, 26, 167, 110, 57, 233, 18, 126, 110, 104, 140, 205, 169, 139, 218, 177, 212, 212, 127, 70, 167, 181, 35, 39, 141, 79, 145, 78, 232, 223, 14, 200, 12, 209, 195, 212, 152, 203, 241, 21, 43, 12, 94, 175, 101, 186, 217, 66, 80, 114, 135, 74, 63, 207, 137, 30, 139, 1, 113, 61, 61, 153, 55, 227, 224, 210, 108, 21, 219, 244, 146, 76, 7, 245, 6, 111, 61, 198, 133, 152, 68, 24, 67, 68, 48, 106, 163, 38, 56, 23, 21, 61, 202, 238, 133, 175, 151, 210, 62, 12, 11, 150, 239, 224, 115, 29, 136, 101, 168, 116, 126, 81, 185, 227, 81, 172, 218, 10, 68, 24, 210, 32, 87, 249, 132, 115, 76, 139, 18, 47, 196, 30, 6, 46, 191, 7, 36, 203, 162, 85, 86, 86, 236, 144, 73, 229, 115, 173, 70, 197, 32, 31, 176, 14, 71, 252, 120, 207, 34, 229, 187, 182, 191, 54, 0, 1, 230, 96, 24, 18, 213, 192, 100, 70, 58, 213, 78, 59, 53, 17, 217, 163, 54, 112, 232, 23, 160, 220, 235, 179, 37, 49, 79, 190, 82, 213, 203, 190, 225, 212, 81, 68, 83, 229, 207, 194, 138, 138, 24, 32, 234, 212, 207, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 204, 16, 0, 0, 72, 83, 124, 99, 30, 47, 37, 1, 239, 147, 205, 111, 190, 223, 244, 63, 24, 79, 122, 117, 181, 21, 208, 192, 202, 124, 19, 125, 86, 114, 223, 16, 236, 95, 243, 28, 111, 185, 165, 202, 147, 55, 87, 91, 132, 48, 117, 139, 38, 49, 119, 42, 244, 99, 183, 237, 161, 13, 98, 224, 148, 222, 245, 106, 142, 188, 151, 224, 189, 139, 21, 116, 87, 28, 157, 151, 225, 226, 250, 218, 147, 80, 231, 144, 252, 223, 62, 103, 176, 31, 51, 101, 181, 44, 82, 180, 72, 148, 151, 10, 88, 144, 81, 87, 230, 135, 174, 102, 165, 143, 241, 229, 60, 148, 151, 208, 187, 151, 100, 64, 82, 171, 52, 1, 40, 143, 31, 160, 6, 0, 70, 16, 0, 0, 4, 4, 25, 27, 4, 255, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 21, 0, 0, 0, 0, 0, 0, 0, 231, 0, 0, 0, 0, 0, 0, 0, 229, 163, 167, 181, 216, 48, 194, 149, 59, 152, 83, 76, 108, 89, 163, 163, 79, 220, 52, 233, 51, 247, 245, 137, 143, 10, 133, 207, 8, 132, 107, 202, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 220, 158, 42, 124, 111, 148, 143, 23, 71, 78, 52, 167, 252, 67, 237, 3, 15, 124, 21, 99, 241, 186, 189, 223, 99, 64, 200, 46, 14, 84, 168, 197, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, 0, 6, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 215, 98, 66, 167, 203, 62, 30, 139, 2, 195, 192, 57, 51, 40, 206, 207, 77, 159, 207, 61, 144, 77, 52, 130, 175, 199, 184, 139, 101, 54, 6, 152, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 150, 43, 224, 72, 222, 235, 58, 201, 147, 41, 73, 102, 159, 167, 17, 139, 183, 255, 92, 219, 174, 234, 152, 2, 188, 66, 241, 7, 248, 149, 104, 181, 57, 202, 96, 91, 236, 128, 245, 224, 94, 102, 1, 108, 69, 117, 32, 200, 63, 232, 4, 154, 218, 62, 108, 62, 37, 222, 11, 49, 235, 195, 58, 169, 32, 0, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, 25, 26, 27, 28, 29, 30, 31, 5, 0, 94, 14, 0, 0, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 69, 56, 84, 67, 67, 66, 74, 97, 103, 65, 119, 73, 66, 65, 103, 73, 85, 102, 50, 83, 98, 121, 119, 107, 77, 86, 84, 74, 75, 85, 53, 55, 47, 74, 119, 66, 112, 56, 69, 100, 104, 80, 48, 52, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 119, 10, 99, 68, 69, 105, 77, 67, 65, 71, 65, 49, 85, 69, 65, 119, 119, 90, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 66, 68, 83, 121, 66, 81, 98, 71, 70, 48, 90, 109, 57, 121, 98, 83, 66, 68, 81, 84, 69, 97, 77, 66, 103, 71, 65, 49, 85, 69, 67, 103, 119, 82, 10, 83, 87, 53, 48, 90, 87, 119, 103, 81, 50, 57, 121, 99, 71, 57, 121, 89, 88, 82, 112, 98, 50, 52, 120, 70, 68, 65, 83, 66, 103, 78, 86, 66, 65, 99, 77, 67, 49, 78, 104, 98, 110, 82, 104, 73, 69, 78, 115, 89, 88, 74, 104, 77, 81, 115, 119, 67, 81, 89, 68, 86, 81, 81, 73, 10, 68, 65, 74, 68, 81, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 66, 104, 77, 67, 86, 86, 77, 119, 72, 104, 99, 78, 77, 106, 85, 120, 77, 84, 65, 50, 77, 68, 99, 122, 78, 122, 77, 48, 87, 104, 99, 78, 77, 122, 73, 120, 77, 84, 65, 50, 77, 68, 99, 122, 78, 122, 77, 48, 10, 87, 106, 66, 119, 77, 83, 73, 119, 73, 65, 89, 68, 86, 81, 81, 68, 68, 66, 108, 74, 98, 110, 82, 108, 98, 67, 66, 84, 82, 49, 103, 103, 85, 69, 78, 76, 73, 69, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 77, 82, 111, 119, 71, 65, 89, 68, 86, 81, 81, 75, 10, 68, 66, 70, 74, 98, 110, 82, 108, 98, 67, 66, 68, 98, 51, 74, 119, 98, 51, 74, 104, 100, 71, 108, 118, 98, 106, 69, 85, 77, 66, 73, 71, 65, 49, 85, 69, 66, 119, 119, 76, 85, 50, 70, 117, 100, 71, 69, 103, 81, 50, 120, 104, 99, 109, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 10, 66, 65, 103, 77, 65, 107, 78, 66, 77, 81, 115, 119, 67, 81, 89, 68, 86, 81, 81, 71, 69, 119, 74, 86, 85, 122, 66, 90, 77, 66, 77, 71, 66, 121, 113, 71, 83, 77, 52, 57, 65, 103, 69, 71, 67, 67, 113, 71, 83, 77, 52, 57, 65, 119, 69, 72, 65, 48, 73, 65, 66, 71, 112, 118, 10, 48, 89, 117, 89, 114, 113, 65, 117, 83, 75, 66, 122, 75, 108, 117, 98, 54, 109, 76, 43, 114, 118, 102, 68, 53, 65, 106, 89, 79, 51, 81, 78, 103, 102, 87, 122, 116, 103, 52, 101, 109, 49, 69, 71, 66, 86, 107, 71, 108, 87, 118, 100, 117, 66, 48, 88, 81, 83, 69, 47, 115, 120, 71, 68, 10, 109, 83, 118, 75, 111, 57, 116, 51, 67, 114, 79, 80, 67, 52, 83, 85, 54, 88, 54, 106, 103, 103, 77, 77, 77, 73, 73, 68, 67, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 83, 86, 98, 49, 51, 78, 118, 82, 118, 104, 54, 85, 66, 74, 121, 100, 84, 48, 10, 77, 56, 52, 66, 86, 119, 118, 101, 86, 68, 66, 114, 66, 103, 78, 86, 72, 82, 56, 69, 90, 68, 66, 105, 77, 71, 67, 103, 88, 113, 66, 99, 104, 108, 112, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 70, 119, 97, 83, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 10, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 90, 87, 119, 117, 89, 50, 57, 116, 76, 51, 78, 110, 101, 67, 57, 106, 90, 88, 74, 48, 97, 87, 90, 112, 89, 50, 70, 48, 97, 87, 57, 117, 76, 51, 89, 48, 76, 51, 66, 106, 97, 50, 78, 121, 98, 68, 57, 106, 89, 84, 49, 119, 10, 98, 71, 70, 48, 90, 109, 57, 121, 98, 83, 90, 108, 98, 109, 78, 118, 90, 71, 108, 117, 90, 122, 49, 107, 90, 88, 73, 119, 72, 81, 89, 68, 86, 82, 48, 79, 66, 66, 89, 69, 70, 71, 51, 110, 54, 83, 43, 75, 120, 78, 54, 116, 43, 72, 73, 56, 71, 112, 57, 54, 80, 107, 117, 90, 10, 105, 87, 115, 90, 77, 65, 52, 71, 65, 49, 85, 100, 68, 119, 69, 66, 47, 119, 81, 69, 65, 119, 73, 71, 119, 68, 65, 77, 66, 103, 78, 86, 72, 82, 77, 66, 65, 102, 56, 69, 65, 106, 65, 65, 77, 73, 73, 67, 79, 81, 89, 74, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 10, 66, 73, 73, 67, 75, 106, 67, 67, 65, 105, 89, 119, 72, 103, 89, 75, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 65, 81, 81, 81, 48, 103, 106, 102, 115, 81, 65, 106, 82, 113, 52, 98, 116, 79, 56, 113, 80, 65, 86, 83, 107, 106, 67, 67, 65, 87, 77, 71, 67, 105, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 119, 103, 103, 70, 84, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 66, 65, 103, 69, 69, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 67, 65, 103, 69, 69, 10, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 68, 65, 103, 69, 67, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 69, 65, 103, 69, 67, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 10, 65, 81, 73, 70, 65, 103, 69, 69, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 71, 65, 103, 69, 66, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 72, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 73, 65, 103, 69, 70, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 74, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 75, 65, 103, 69, 65, 10, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 76, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 77, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 10, 65, 81, 73, 78, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 79, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 80, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 81, 65, 103, 69, 65, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 82, 65, 103, 69, 76, 77, 66, 56, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 73, 83, 66, 66, 65, 69, 10, 66, 65, 73, 67, 66, 65, 69, 65, 66, 81, 65, 65, 65, 65, 65, 65, 65, 65, 65, 65, 77, 66, 65, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 77, 69, 65, 103, 65, 65, 77, 66, 81, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 81, 69, 10, 66, 114, 68, 65, 98, 119, 65, 65, 65, 68, 65, 80, 66, 103, 111, 113, 104, 107, 105, 71, 43, 69, 48, 66, 68, 81, 69, 70, 67, 103, 69, 66, 77, 66, 52, 71, 67, 105, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 89, 69, 69, 68, 97, 57, 104, 116, 65, 56, 65, 74, 47, 90, 10, 50, 70, 109, 97, 76, 53, 74, 113, 47, 75, 69, 119, 82, 65, 89, 75, 75, 111, 90, 73, 104, 118, 104, 78, 65, 81, 48, 66, 66, 122, 65, 50, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 66, 65, 81, 72, 47, 77, 66, 65, 71, 67, 121, 113, 71, 10, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 67, 65, 81, 72, 47, 77, 66, 65, 71, 67, 121, 113, 71, 83, 73, 98, 52, 84, 81, 69, 78, 65, 81, 99, 68, 65, 81, 72, 47, 77, 65, 111, 71, 67, 67, 113, 71, 83, 77, 52, 57, 66, 65, 77, 67, 65, 48, 107, 65, 77, 69, 89, 67, 10, 73, 81, 67, 70, 71, 49, 89, 65, 98, 51, 101, 88, 70, 116, 101, 56, 53, 51, 67, 108, 86, 66, 110, 104, 108, 67, 102, 68, 121, 99, 53, 55, 50, 90, 88, 69, 113, 97, 120, 52, 85, 99, 99, 83, 97, 119, 73, 104, 65, 79, 110, 48, 86, 78, 75, 84, 90, 109, 65, 120, 85, 70, 52, 110, 10, 119, 82, 107, 83, 70, 104, 52, 113, 70, 74, 51, 97, 85, 108, 122, 70, 111, 80, 81, 84, 51, 120, 73, 102, 55, 107, 70, 68, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 67, 108, 106, 67, 67, 65, 106, 50, 103, 65, 119, 73, 66, 65, 103, 73, 86, 65, 74, 86, 118, 88, 99, 50, 57, 71, 43, 72, 112, 81, 69, 110, 74, 49, 80, 81, 122, 122, 103, 70, 88, 67, 57, 53, 85, 77, 65, 111, 71, 67, 67, 113, 71, 83, 77, 52, 57, 66, 65, 77, 67, 10, 77, 71, 103, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 77, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 70, 78, 72, 87, 67, 66, 83, 98, 50, 57, 48, 73, 69, 78, 66, 77, 82, 111, 119, 71, 65, 89, 68, 86, 81, 81, 75, 68, 66, 70, 74, 98, 110, 82, 108, 98, 67, 66, 68, 10, 98, 51, 74, 119, 98, 51, 74, 104, 100, 71, 108, 118, 98, 106, 69, 85, 77, 66, 73, 71, 65, 49, 85, 69, 66, 119, 119, 76, 85, 50, 70, 117, 100, 71, 69, 103, 81, 50, 120, 104, 99, 109, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 103, 77, 65, 107, 78, 66, 77, 81, 115, 119, 10, 67, 81, 89, 68, 86, 81, 81, 71, 69, 119, 74, 86, 85, 122, 65, 101, 70, 119, 48, 120, 79, 68, 65, 49, 77, 106, 69, 120, 77, 68, 85, 119, 77, 84, 66, 97, 70, 119, 48, 122, 77, 122, 65, 49, 77, 106, 69, 120, 77, 68, 85, 119, 77, 84, 66, 97, 77, 72, 65, 120, 73, 106, 65, 103, 10, 66, 103, 78, 86, 66, 65, 77, 77, 71, 85, 108, 117, 100, 71, 86, 115, 73, 70, 78, 72, 87, 67, 66, 81, 81, 48, 115, 103, 85, 71, 120, 104, 100, 71, 90, 118, 99, 109, 48, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 10, 73, 69, 78, 118, 99, 110, 66, 118, 99, 109, 70, 48, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 10, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 89, 84, 65, 108, 86, 84, 77, 70, 107, 119, 69, 119, 89, 72, 75, 111, 90, 73, 122, 106, 48, 67, 65, 81, 89, 73, 75, 111, 90, 73, 122, 106, 48, 68, 65, 81, 99, 68, 81, 103, 65, 69, 78, 83, 66, 47, 55, 116, 50, 49, 108, 88, 83, 79, 10, 50, 67, 117, 122, 112, 120, 119, 55, 52, 101, 74, 66, 55, 50, 69, 121, 68, 71, 103, 87, 53, 114, 88, 67, 116, 120, 50, 116, 86, 84, 76, 113, 54, 104, 75, 107, 54, 122, 43, 85, 105, 82, 90, 67, 110, 113, 82, 55, 112, 115, 79, 118, 103, 113, 70, 101, 83, 120, 108, 109, 84, 108, 74, 108, 10, 101, 84, 109, 105, 50, 87, 89, 122, 51, 113, 79, 66, 117, 122, 67, 66, 117, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 81, 105, 90, 81, 122, 87, 87, 112, 48, 48, 105, 102, 79, 68, 116, 74, 86, 83, 118, 49, 65, 98, 79, 83, 99, 71, 114, 68, 66, 83, 10, 66, 103, 78, 86, 72, 82, 56, 69, 83, 122, 66, 74, 77, 69, 101, 103, 82, 97, 66, 68, 104, 107, 70, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 99, 121, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 10, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 90, 87, 119, 117, 89, 50, 57, 116, 76, 48, 108, 117, 100, 71, 86, 115, 85, 48, 100, 89, 85, 109, 57, 118, 100, 69, 78, 66, 76, 109, 82, 108, 99, 106, 65, 100, 66, 103, 78, 86, 72, 81, 52, 69, 70, 103, 81, 85, 108, 87, 57, 100, 10, 122, 98, 48, 98, 52, 101, 108, 65, 83, 99, 110, 85, 57, 68, 80, 79, 65, 86, 99, 76, 51, 108, 81, 119, 68, 103, 89, 68, 86, 82, 48, 80, 65, 81, 72, 47, 66, 65, 81, 68, 65, 103, 69, 71, 77, 66, 73, 71, 65, 49, 85, 100, 69, 119, 69, 66, 47, 119, 81, 73, 77, 65, 89, 66, 10, 65, 102, 56, 67, 65, 81, 65, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 68, 82, 119, 65, 119, 82, 65, 73, 103, 88, 115, 86, 107, 105, 48, 119, 43, 105, 54, 86, 89, 71, 87, 51, 85, 70, 47, 50, 50, 117, 97, 88, 101, 48, 89, 74, 68, 106, 49, 85, 101, 10, 110, 65, 43, 84, 106, 68, 49, 97, 105, 53, 99, 67, 73, 67, 89, 98, 49, 83, 65, 109, 68, 53, 120, 107, 102, 84, 86, 112, 118, 111, 52, 85, 111, 121, 105, 83, 89, 120, 114, 68, 87, 76, 109, 85, 82, 52, 67, 73, 57, 78, 75, 121, 102, 80, 78, 43, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 45, 45, 45, 45, 45, 66, 69, 71, 73, 78, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 77, 73, 73, 67, 106, 122, 67, 67, 65, 106, 83, 103, 65, 119, 73, 66, 65, 103, 73, 85, 73, 109, 85, 77, 49, 108, 113, 100, 78, 73, 110, 122, 103, 55, 83, 86, 85, 114, 57, 81, 71, 122, 107, 110, 66, 113, 119, 119, 67, 103, 89, 73, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 119, 10, 97, 68, 69, 97, 77, 66, 103, 71, 65, 49, 85, 69, 65, 119, 119, 82, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 74, 118, 98, 51, 81, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 69, 78, 118, 10, 99, 110, 66, 118, 99, 109, 70, 48, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 67, 122, 65, 74, 10, 66, 103, 78, 86, 66, 65, 89, 84, 65, 108, 86, 84, 77, 66, 52, 88, 68, 84, 69, 52, 77, 68, 85, 121, 77, 84, 69, 119, 78, 68, 85, 120, 77, 70, 111, 88, 68, 84, 81, 53, 77, 84, 73, 122, 77, 84, 73, 122, 78, 84, 107, 49, 79, 86, 111, 119, 97, 68, 69, 97, 77, 66, 103, 71, 10, 65, 49, 85, 69, 65, 119, 119, 82, 83, 87, 53, 48, 90, 87, 119, 103, 85, 48, 100, 89, 73, 70, 74, 118, 98, 51, 81, 103, 81, 48, 69, 120, 71, 106, 65, 89, 66, 103, 78, 86, 66, 65, 111, 77, 69, 85, 108, 117, 100, 71, 86, 115, 73, 69, 78, 118, 99, 110, 66, 118, 99, 109, 70, 48, 10, 97, 87, 57, 117, 77, 82, 81, 119, 69, 103, 89, 68, 86, 81, 81, 72, 68, 65, 116, 84, 89, 87, 53, 48, 89, 83, 66, 68, 98, 71, 70, 121, 89, 84, 69, 76, 77, 65, 107, 71, 65, 49, 85, 69, 67, 65, 119, 67, 81, 48, 69, 120, 67, 122, 65, 74, 66, 103, 78, 86, 66, 65, 89, 84, 10, 65, 108, 86, 84, 77, 70, 107, 119, 69, 119, 89, 72, 75, 111, 90, 73, 122, 106, 48, 67, 65, 81, 89, 73, 75, 111, 90, 73, 122, 106, 48, 68, 65, 81, 99, 68, 81, 103, 65, 69, 67, 54, 110, 69, 119, 77, 68, 73, 89, 90, 79, 106, 47, 105, 80, 87, 115, 67, 122, 97, 69, 75, 105, 55, 10, 49, 79, 105, 79, 83, 76, 82, 70, 104, 87, 71, 106, 98, 110, 66, 86, 74, 102, 86, 110, 107, 89, 52, 117, 51, 73, 106, 107, 68, 89, 89, 76, 48, 77, 120, 79, 52, 109, 113, 115, 121, 89, 106, 108, 66, 97, 108, 84, 86, 89, 120, 70, 80, 50, 115, 74, 66, 75, 53, 122, 108, 75, 79, 66, 10, 117, 122, 67, 66, 117, 68, 65, 102, 66, 103, 78, 86, 72, 83, 77, 69, 71, 68, 65, 87, 103, 66, 81, 105, 90, 81, 122, 87, 87, 112, 48, 48, 105, 102, 79, 68, 116, 74, 86, 83, 118, 49, 65, 98, 79, 83, 99, 71, 114, 68, 66, 83, 66, 103, 78, 86, 72, 82, 56, 69, 83, 122, 66, 74, 10, 77, 69, 101, 103, 82, 97, 66, 68, 104, 107, 70, 111, 100, 72, 82, 119, 99, 122, 111, 118, 76, 50, 78, 108, 99, 110, 82, 112, 90, 109, 108, 106, 89, 88, 82, 108, 99, 121, 53, 48, 99, 110, 86, 122, 100, 71, 86, 107, 99, 50, 86, 121, 100, 109, 108, 106, 90, 88, 77, 117, 97, 87, 53, 48, 10, 90, 87, 119, 117, 89, 50, 57, 116, 76, 48, 108, 117, 100, 71, 86, 115, 85, 48, 100, 89, 85, 109, 57, 118, 100, 69, 78, 66, 76, 109, 82, 108, 99, 106, 65, 100, 66, 103, 78, 86, 72, 81, 52, 69, 70, 103, 81, 85, 73, 109, 85, 77, 49, 108, 113, 100, 78, 73, 110, 122, 103, 55, 83, 86, 10, 85, 114, 57, 81, 71, 122, 107, 110, 66, 113, 119, 119, 68, 103, 89, 68, 86, 82, 48, 80, 65, 81, 72, 47, 66, 65, 81, 68, 65, 103, 69, 71, 77, 66, 73, 71, 65, 49, 85, 100, 69, 119, 69, 66, 47, 119, 81, 73, 77, 65, 89, 66, 65, 102, 56, 67, 65, 81, 69, 119, 67, 103, 89, 73, 10, 75, 111, 90, 73, 122, 106, 48, 69, 65, 119, 73, 68, 83, 81, 65, 119, 82, 103, 73, 104, 65, 79, 87, 47, 53, 81, 107, 82, 43, 83, 57, 67, 105, 83, 68, 99, 78, 111, 111, 119, 76, 117, 80, 82, 76, 115, 87, 71, 102, 47, 89, 105, 55, 71, 83, 88, 57, 52, 66, 103, 119, 84, 119, 103, 10, 65, 105, 69, 65, 52, 74, 48, 108, 114, 72, 111, 77, 115, 43, 88, 111, 53, 111, 47, 115, 88, 54, 79, 57, 81, 87, 120, 72, 82, 65, 118, 90, 85, 71, 79, 100, 82, 81, 55, 99, 118, 113, 82, 88, 97, 113, 73, 61, 10, 45, 45, 45, 45, 45, 69, 78, 68, 32, 67, 69, 82, 84, 73, 70, 73, 67, 65, 84, 69, 45, 45, 45, 45, 45, 10, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], "collateral": { "pck_crl_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICljCCAj2gAwIBAgIVAJVvXc29G+HpQEnJ1PQzzgFXC95UMAoGCCqGSM49BAMC\nMGgxGjAYBgNVBAMMEUludGVsIFNHWCBSb290IENBMRowGAYDVQQKDBFJbnRlbCBD\nb3Jwb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQsw\nCQYDVQQGEwJVUzAeFw0xODA1MjExMDUwMTBaFw0zMzA1MjExMDUwMTBaMHAxIjAg\nBgNVBAMMGUludGVsIFNHWCBQQ0sgUGxhdGZvcm0gQ0ExGjAYBgNVBAoMEUludGVs\nIENvcnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0Ex\nCzAJBgNVBAYTAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAENSB/7t21lXSO\n2Cuzpxw74eJB72EyDGgW5rXCtx2tVTLq6hKk6z+UiRZCnqR7psOvgqFeSxlmTlJl\neTmi2WYz3qOBuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBS\nBgNVHR8ESzBJMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2Vy\ndmljZXMuaW50ZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUlW9d\nzb0b4elAScnU9DPOAVcL3lQwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYB\nAf8CAQAwCgYIKoZIzj0EAwIDRwAwRAIgXsVki0w+i6VYGW3UF/22uaXe0YJDj1Ue\nnA+TjD1ai5cCICYb1SAmD5xkfTVpvo4UoyiSYxrDWLmUR4CI9NKyfPN+\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "root_ca_crl": "308201203081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3235303332303131323135375a170d3236303430333131323135375aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020347003044022030c9fce1438da0a94e4fffdd46c9650e393be6e5a7862d4e4e73527932d04af302206539efe3f734c3d7df20d9dfc4630e1c7ff0439a0f8ece101f15b5eaff9b4f33", - "pck_crl": "30820d1730820cbd020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303232313133343330335a170d3236303332333133343330335a30820be9303302146fc34e5023e728923435d61aa4b83c618166ad35170d3236303232313133343330335a300c300a0603551d1504030a01013034021500efae6e9715fca13b87e333e8261ed6d990a926ad170d3236303232313133343330335a300c300a0603551d1504030a01013034021500fd608648629cba73078b4d492f4b3ea741ad08cd170d3236303232313133343330335a300c300a0603551d1504030a010130340215008af924184e1d5afddd73c3d63a12f5e8b5737e56170d3236303232313133343330335a300c300a0603551d1504030a01013034021500b1257978cfa9ccdd0759abf8c5ca72fae3a78a9b170d3236303232313133343330335a300c300a0603551d1504030a01013033021474fea614a972be0e2843f2059835811ed872f9b3170d3236303232313133343330335a300c300a0603551d1504030a01013034021500f9c4ef56b3ab48d577e108baedf4bf88014214b9170d3236303232313133343330335a300c300a0603551d1504030a010130330214071de0778f9e5fc4f2878f30d6b07c9a30e6b30b170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cde2424f972cea94ff239937f4d80c25029dd60b170d3236303232313133343330335a300c300a0603551d1504030a0101303302146c3319e5109b64507d3cf1132ce00349ef527319170d3236303232313133343330335a300c300a0603551d1504030a01013034021500df08d756b66a7497f43b5bb58ada04d3f4f7a937170d3236303232313133343330335a300c300a0603551d1504030a01013033021428af485b6cf67e409a39d5cb5aee4598f7a8fa7b170d3236303232313133343330335a300c300a0603551d1504030a01013034021500fb8b2daec092cada8aa9bc4ff2f1c20d0346668c170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cd4850ac52bdcc69a6a6f058c8bc57bbd0b5f864170d3236303232313133343330335a300c300a0603551d1504030a01013034021500994dd3666f5275fb805f95dd02bd50cb2679d8ad170d3236303232313133343330335a300c300a0603551d1504030a0101303302140702136900252274d9035eedf5457462fad0ef4c170d3236303232313133343330335a300c300a0603551d1504030a01013033021461f2bf73e39b4e04aa27d801bd73d24319b5bf80170d3236303232313133343330335a300c300a0603551d1504030a0101303302143992be851b96902eff38959e6c2eff1b0651a4b5170d3236303232313133343330335a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3236303232313133343330335a300c300a0603551d1504030a010130330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3236303232313133343330335a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3236303232313133343330335a300c300a0603551d1504030a0101303302147ae37748a9f912f4c63ba7ab07c593ce1d1d1181170d3236303232313133343330335a300c300a0603551d1504030a01013033021413884b33269938c195aa170fca75da177538df0b170d3236303232313133343330335a300c300a0603551d1504030a0101303402150085d3c9381b77a7e04d119c9e5ad6749ff3ffab87170d3236303232313133343330335a300c300a0603551d1504030a0101303402150093887ca4411e7a923bd1fed2819b2949f201b5b4170d3236303232313133343330335a300c300a0603551d1504030a0101303302142498dc6283930996fd8bf23a37acbe26a3bed457170d3236303232313133343330335a300c300a0603551d1504030a010130340215008a66f1a749488667689cc3903ac54c662b712e73170d3236303232313133343330335a300c300a0603551d1504030a01013034021500afc13610bdd36cb7985d106481a880d3a01fda07170d3236303232313133343330335a300c300a0603551d1504030a01013034021500efe04b2c33d036aac96ca673bf1e9a47b64d5cbb170d3236303232313133343330335a300c300a0603551d1504030a0101303402150083d9ac8d8bb509d1c6c809ad712e8430559ed7f3170d3236303232313133343330335a300c300a0603551d1504030a0101303302147931fd50b5071c1bbfc5b7b6ded8b45b9d8b8529170d3236303232313133343330335a300c300a0603551d1504030a0101303302141fa20e2970bde5d57f7b8ddf8339484e1f1d0823170d3236303232313133343330335a300c300a0603551d1504030a0101303302141e87b2c3b32d8d23e411cef34197b95af0c8adf5170d3236303232313133343330335a300c300a0603551d1504030a010130340215009afd2ee90a473550a167d996911437c7502d1f09170d3236303232313133343330335a300c300a0603551d1504030a0101303302144481b0f11728a13b696d3ea9c770a0b15ec58dda170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a7859f57982ef0e67d37bc8ef2ef5ac835ff1aa9170d3236303232313133343330335a300c300a0603551d1504030a010130340215009d67753b81e47090aea763fbec4c4549bcdb9933170d3236303232313133343330335a300c300a0603551d1504030a01013033021434bfbb7a1d9c568147e118b614f7b76ed3ef68df170d3236303232313133343330335a300c300a0603551d1504030a0101303302142c3cc6fe9279db1516d5ce39f2a898cda5a175e1170d3236303232313133343330335a300c300a0603551d1504030a010130330214717948687509234be979e4b7dce6f31bef64b68c170d3236303232313133343330335a300c300a0603551d1504030a010130340215009d76ef2c39c136e8658b6e7396b1d7445a27631f170d3236303232313133343330335a300c300a0603551d1504030a01013034021500c3e025fca995f36f59b48467939e3e34e6361a6f170d3236303232313133343330335a300c300a0603551d1504030a010130340215008c5f6b3257da05b17429e2e61ba965d67330606a170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a17c51722ec1e0c3278fe8bdf052059cbec4e648170d3236303232313133343330335a300c300a0603551d1504030a01013033021411c943b866fa04944e3057e5a67146596475a023170d3236303232313133343330335a300c300a0603551d1504030a01013034021500be6913785406155454a28885a515b3da5767d3a9170d3236303232313133343330335a300c300a0603551d1504030a0101303302140ac5ec91bd934c07b9ea41625e9cc09681002eb0170d3236303232313133343330335a300c300a0603551d1504030a0101303302146d51a0eabc1f9a1e9ddd5b36bdda1631ae6c182a170d3236303232313133343330335a300c300a0603551d1504030a01013034021500a52c5d71c4166b4fc0ded8b679951e5ee9193de5170d3236303232313133343330335a300c300a0603551d1504030a010130330214249779aedd85fcac93c8853516be5428c26b3bf8170d3236303232313133343330335a300c300a0603551d1504030a01013033021434ba4fd76bde5309210cf1dd1ffb494c638a9157170d3236303232313133343330335a300c300a0603551d1504030a010130330214043e04919daae13443248395094d2a2eacfc76fe170d3236303232313133343330335a300c300a0603551d1504030a01013033021447fc577d2d094cbdf270715ed6848a93855ad34b170d3236303232313133343330335a300c300a0603551d1504030a0101303302147d62a2f5e6f386e469653fffff045d0a8178e8e7170d3236303232313133343330335a300c300a0603551d1504030a01013034021500c4ed45fe026bb6a47eaec35ea80b7ef407ce062c170d3236303232313133343330335a300c300a0603551d1504030a01013034021500cf9831077a3ca4f1a2c56867bf55b18eccbeffd8170d3236303232313133343330335a300c300a0603551d1504030a0101303302146c2b81d7ea2e436720ce29f1d0b1ccb7a218600f170d3236303232313133343330335a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d040302034800304502203bd1f94fcc12868b9760bcf134ce34b844713b667fcb9e4d2207fc0cd81566de022100ef021d2ca3ff8bd81861b3a3722f955703b69f5133cfe5fea3d4a1098922f103", + "root_ca_crl": "308201223081c8020101300a06082a8648ce3d0403023068311a301806035504030c11496e74656c2053475820526f6f74204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303232363133303430305a170d3237303232363133303430305aa02f302d300a0603551d140403020101301f0603551d2304183016801422650cd65a9d3489f383b49552bf501b392706ac300a06082a8648ce3d0403020349003046022100c252ed59c795ba2b11496a4a99758bb8cbc380a1ebbb0865be69f2c4b38bb6400221009a7d8b03602a9ee2d62322d759166d6933d24d9dfa01ab3fde4520691d715bd7", + "pck_crl": "30820d1830820cbd020101300a06082a8648ce3d04030230703122302006035504030c19496e74656c205347582050434b20506c6174666f726d204341311a3018060355040a0c11496e74656c20436f72706f726174696f6e3114301206035504070c0b53616e746120436c617261310b300906035504080c024341310b3009060355040613025553170d3236303331383136303130355a170d3236303431373136303130355a30820be9303302146fc34e5023e728923435d61aa4b83c618166ad35170d3236303331383136303130355a300c300a0603551d1504030a01013034021500efae6e9715fca13b87e333e8261ed6d990a926ad170d3236303331383136303130355a300c300a0603551d1504030a01013034021500fd608648629cba73078b4d492f4b3ea741ad08cd170d3236303331383136303130355a300c300a0603551d1504030a010130340215008af924184e1d5afddd73c3d63a12f5e8b5737e56170d3236303331383136303130355a300c300a0603551d1504030a01013034021500b1257978cfa9ccdd0759abf8c5ca72fae3a78a9b170d3236303331383136303130355a300c300a0603551d1504030a01013033021474fea614a972be0e2843f2059835811ed872f9b3170d3236303331383136303130355a300c300a0603551d1504030a01013034021500f9c4ef56b3ab48d577e108baedf4bf88014214b9170d3236303331383136303130355a300c300a0603551d1504030a010130330214071de0778f9e5fc4f2878f30d6b07c9a30e6b30b170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cde2424f972cea94ff239937f4d80c25029dd60b170d3236303331383136303130355a300c300a0603551d1504030a0101303302146c3319e5109b64507d3cf1132ce00349ef527319170d3236303331383136303130355a300c300a0603551d1504030a01013034021500df08d756b66a7497f43b5bb58ada04d3f4f7a937170d3236303331383136303130355a300c300a0603551d1504030a01013033021428af485b6cf67e409a39d5cb5aee4598f7a8fa7b170d3236303331383136303130355a300c300a0603551d1504030a01013034021500fb8b2daec092cada8aa9bc4ff2f1c20d0346668c170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cd4850ac52bdcc69a6a6f058c8bc57bbd0b5f864170d3236303331383136303130355a300c300a0603551d1504030a01013034021500994dd3666f5275fb805f95dd02bd50cb2679d8ad170d3236303331383136303130355a300c300a0603551d1504030a0101303302140702136900252274d9035eedf5457462fad0ef4c170d3236303331383136303130355a300c300a0603551d1504030a01013033021461f2bf73e39b4e04aa27d801bd73d24319b5bf80170d3236303331383136303130355a300c300a0603551d1504030a0101303302143992be851b96902eff38959e6c2eff1b0651a4b5170d3236303331383136303130355a300c300a0603551d1504030a0101303302140fda43a00b68ea79b7c2deaeac0b498bdfb2af90170d3236303331383136303130355a300c300a0603551d1504030a010130330214639f139a5040fdcff191e8a4fb1bf086ed603971170d3236303331383136303130355a300c300a0603551d1504030a01013034021500959d533f9249dc1e513544cdc830bf19b7f1f301170d3236303331383136303130355a300c300a0603551d1504030a0101303302147ae37748a9f912f4c63ba7ab07c593ce1d1d1181170d3236303331383136303130355a300c300a0603551d1504030a01013033021413884b33269938c195aa170fca75da177538df0b170d3236303331383136303130355a300c300a0603551d1504030a0101303402150085d3c9381b77a7e04d119c9e5ad6749ff3ffab87170d3236303331383136303130355a300c300a0603551d1504030a0101303402150093887ca4411e7a923bd1fed2819b2949f201b5b4170d3236303331383136303130355a300c300a0603551d1504030a0101303302142498dc6283930996fd8bf23a37acbe26a3bed457170d3236303331383136303130355a300c300a0603551d1504030a010130340215008a66f1a749488667689cc3903ac54c662b712e73170d3236303331383136303130355a300c300a0603551d1504030a01013034021500afc13610bdd36cb7985d106481a880d3a01fda07170d3236303331383136303130355a300c300a0603551d1504030a01013034021500efe04b2c33d036aac96ca673bf1e9a47b64d5cbb170d3236303331383136303130355a300c300a0603551d1504030a0101303402150083d9ac8d8bb509d1c6c809ad712e8430559ed7f3170d3236303331383136303130355a300c300a0603551d1504030a0101303302147931fd50b5071c1bbfc5b7b6ded8b45b9d8b8529170d3236303331383136303130355a300c300a0603551d1504030a0101303302141fa20e2970bde5d57f7b8ddf8339484e1f1d0823170d3236303331383136303130355a300c300a0603551d1504030a0101303302141e87b2c3b32d8d23e411cef34197b95af0c8adf5170d3236303331383136303130355a300c300a0603551d1504030a010130340215009afd2ee90a473550a167d996911437c7502d1f09170d3236303331383136303130355a300c300a0603551d1504030a0101303302144481b0f11728a13b696d3ea9c770a0b15ec58dda170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a7859f57982ef0e67d37bc8ef2ef5ac835ff1aa9170d3236303331383136303130355a300c300a0603551d1504030a010130340215009d67753b81e47090aea763fbec4c4549bcdb9933170d3236303331383136303130355a300c300a0603551d1504030a01013033021434bfbb7a1d9c568147e118b614f7b76ed3ef68df170d3236303331383136303130355a300c300a0603551d1504030a0101303302142c3cc6fe9279db1516d5ce39f2a898cda5a175e1170d3236303331383136303130355a300c300a0603551d1504030a010130330214717948687509234be979e4b7dce6f31bef64b68c170d3236303331383136303130355a300c300a0603551d1504030a010130340215009d76ef2c39c136e8658b6e7396b1d7445a27631f170d3236303331383136303130355a300c300a0603551d1504030a01013034021500c3e025fca995f36f59b48467939e3e34e6361a6f170d3236303331383136303130355a300c300a0603551d1504030a010130340215008c5f6b3257da05b17429e2e61ba965d67330606a170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a17c51722ec1e0c3278fe8bdf052059cbec4e648170d3236303331383136303130355a300c300a0603551d1504030a01013033021411c943b866fa04944e3057e5a67146596475a023170d3236303331383136303130355a300c300a0603551d1504030a01013034021500be6913785406155454a28885a515b3da5767d3a9170d3236303331383136303130355a300c300a0603551d1504030a0101303302140ac5ec91bd934c07b9ea41625e9cc09681002eb0170d3236303331383136303130355a300c300a0603551d1504030a0101303302146d51a0eabc1f9a1e9ddd5b36bdda1631ae6c182a170d3236303331383136303130355a300c300a0603551d1504030a01013034021500a52c5d71c4166b4fc0ded8b679951e5ee9193de5170d3236303331383136303130355a300c300a0603551d1504030a010130330214249779aedd85fcac93c8853516be5428c26b3bf8170d3236303331383136303130355a300c300a0603551d1504030a01013033021434ba4fd76bde5309210cf1dd1ffb494c638a9157170d3236303331383136303130355a300c300a0603551d1504030a010130330214043e04919daae13443248395094d2a2eacfc76fe170d3236303331383136303130355a300c300a0603551d1504030a01013033021447fc577d2d094cbdf270715ed6848a93855ad34b170d3236303331383136303130355a300c300a0603551d1504030a0101303302147d62a2f5e6f386e469653fffff045d0a8178e8e7170d3236303331383136303130355a300c300a0603551d1504030a01013034021500c4ed45fe026bb6a47eaec35ea80b7ef407ce062c170d3236303331383136303130355a300c300a0603551d1504030a01013034021500cf9831077a3ca4f1a2c56867bf55b18eccbeffd8170d3236303331383136303130355a300c300a0603551d1504030a0101303302146c2b81d7ea2e436720ce29f1d0b1ccb7a218600f170d3236303331383136303130355a300c300a0603551d1504030a0101a02f302d300a0603551d140403020101301f0603551d23041830168014956f5dcdbd1be1e94049c9d4f433ce01570bde54300a06082a8648ce3d0403020349003046022100c71b1e53814b3437773403491a1e10a53043880e98868ee957bfa044031c3bf3022100d12d92717d516bd91b8a0c275fce3dc886f4cbc8a2300275fcf01a78691eeefd", "tcb_info_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG\nA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw\nb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD\nVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv\nP+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju\nypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f\nBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz\nLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK\nQEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG\nSM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh\nAKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "tcb_info": "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2026-02-21T13:31:57Z\",\"nextUpdate\":\"2026-03-23T13:31:57Z\",\"fmspc\":\"b0c06f000000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":18,\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\"},\"tdxModuleIdentities\":[{\"id\":\"TDX_03\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":3},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]},{\"id\":\"TDX_01\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":6},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]},{\"tcb\":{\"isvsvn\":2},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]}]}],\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":3,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":4,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":5,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2018-01-04T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00106\",\"INTEL-SA-00115\",\"INTEL-SA-00135\",\"INTEL-SA-00203\",\"INTEL-SA-00220\",\"INTEL-SA-00233\",\"INTEL-SA-00270\",\"INTEL-SA-00293\",\"INTEL-SA-00320\",\"INTEL-SA-00329\",\"INTEL-SA-00381\",\"INTEL-SA-00389\",\"INTEL-SA-00477\",\"INTEL-SA-00837\",\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]}]}", - "tcb_info_signature": "6cb0f2b2d37350c8d25f478cac1b5a341cc1d6deb8379f0aa5c8708ebf00fbd7fb39b146ebd086593f7b86f37c8cb82a0e3df178857625ef95d64f636167c5eb", + "tcb_info": "{\"id\":\"TDX\",\"version\":3,\"issueDate\":\"2026-03-18T15:43:27Z\",\"nextUpdate\":\"2026-04-17T15:43:27Z\",\"fmspc\":\"b0c06f000000\",\"pceId\":\"0000\",\"tcbType\":0,\"tcbEvaluationDataNumber\":18,\"tdxModule\":{\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\"},\"tdxModuleIdentities\":[{\"id\":\"TDX_03\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":3},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]},{\"id\":\"TDX_01\",\"mrsigner\":\"000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\",\"attributes\":\"0000000000000000\",\"attributesMask\":\"FFFFFFFFFFFFFFFF\",\"tcbLevels\":[{\"tcb\":{\"isvsvn\":6},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]},{\"tcb\":{\"isvsvn\":2},\"tcbDate\":\"2023-08-09T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01099\"]}]}],\"tcbLevels\":[{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":3,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":4,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":3,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":11,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2024-03-13T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]},{\"tcb\":{\"sgxtcbcomponents\":[{\"svn\":2,\"category\":\"BIOS\",\"type\":\"Early Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"SGX Late Microcode Update\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TXT SINIT\"},{\"svn\":2,\"category\":\"BIOS\"},{\"svn\":3,\"category\":\"BIOS\"},{\"svn\":1,\"category\":\"BIOS\"},{\"svn\":0},{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"SEAMLDR ACM\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}],\"pcesvn\":5,\"tdxtcbcomponents\":[{\"svn\":5,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":0,\"category\":\"OS/VMM\",\"type\":\"TDX Module\"},{\"svn\":2,\"category\":\"OS/VMM\",\"type\":\"TDX Late Microcode Update\"},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0},{\"svn\":0}]},\"tcbDate\":\"2018-01-04T00:00:00Z\",\"tcbStatus\":\"OutOfDate\",\"advisoryIDs\":[\"INTEL-SA-00106\",\"INTEL-SA-00115\",\"INTEL-SA-00135\",\"INTEL-SA-00203\",\"INTEL-SA-00220\",\"INTEL-SA-00233\",\"INTEL-SA-00270\",\"INTEL-SA-00293\",\"INTEL-SA-00320\",\"INTEL-SA-00329\",\"INTEL-SA-00381\",\"INTEL-SA-00389\",\"INTEL-SA-00477\",\"INTEL-SA-00837\",\"INTEL-SA-01036\",\"INTEL-SA-01079\",\"INTEL-SA-01099\",\"INTEL-SA-01103\",\"INTEL-SA-01111\"]}]}", + "tcb_info_signature": "77bd013b1fbb1162604d1b76e2ead05315b61963d15c34e6c8dfbec009930dcaa55e2026eb3befad7df463210a85c392d9b77caf72c76e4ec03f02c71855a95c", "qe_identity_issuer_chain": "-----BEGIN CERTIFICATE-----\nMIICjTCCAjKgAwIBAgIUfjiC1ftVKUpASY5FhAPpFJG99FUwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTI1MDUwNjA5MjUwMFoXDTMyMDUwNjA5MjUwMFowbDEeMBwG\nA1UEAwwVSW50ZWwgU0dYIFRDQiBTaWduaW5nMRowGAYDVQQKDBFJbnRlbCBDb3Jw\nb3JhdGlvbjEUMBIGA1UEBwwLU2FudGEgQ2xhcmExCzAJBgNVBAgMAkNBMQswCQYD\nVQQGEwJVUzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABENFG8xzydWRfK92bmGv\nP+mAh91PEyV7Jh6FGJd5ndE9aBH7R3E4A7ubrlh/zN3C4xvpoouGlirMba+W2lju\nypajgbUwgbIwHwYDVR0jBBgwFoAUImUM1lqdNInzg7SVUr9QGzknBqwwUgYDVR0f\nBEswSTBHoEWgQ4ZBaHR0cHM6Ly9jZXJ0aWZpY2F0ZXMudHJ1c3RlZHNlcnZpY2Vz\nLmludGVsLmNvbS9JbnRlbFNHWFJvb3RDQS5kZXIwHQYDVR0OBBYEFH44gtX7VSlK\nQEmORYQD6RSRvfRVMA4GA1UdDwEB/wQEAwIGwDAMBgNVHRMBAf8EAjAAMAoGCCqG\nSM49BAMCA0kAMEYCIQDdmmRuAo3qCO8TC1IoJMITAoOEw4dlgEBHzSz1TuMSTAIh\nAKVTqOkt59+co0O3m3hC+v5Fb00FjYWcgeu3EijOULo5\n-----END CERTIFICATE-----\n-----BEGIN CERTIFICATE-----\nMIICjzCCAjSgAwIBAgIUImUM1lqdNInzg7SVUr9QGzknBqwwCgYIKoZIzj0EAwIw\naDEaMBgGA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENv\ncnBvcmF0aW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJ\nBgNVBAYTAlVTMB4XDTE4MDUyMTEwNDUxMFoXDTQ5MTIzMTIzNTk1OVowaDEaMBgG\nA1UEAwwRSW50ZWwgU0dYIFJvb3QgQ0ExGjAYBgNVBAoMEUludGVsIENvcnBvcmF0\naW9uMRQwEgYDVQQHDAtTYW50YSBDbGFyYTELMAkGA1UECAwCQ0ExCzAJBgNVBAYT\nAlVTMFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEC6nEwMDIYZOj/iPWsCzaEKi7\n1OiOSLRFhWGjbnBVJfVnkY4u3IjkDYYL0MxO4mqsyYjlBalTVYxFP2sJBK5zlKOB\nuzCBuDAfBgNVHSMEGDAWgBQiZQzWWp00ifODtJVSv1AbOScGrDBSBgNVHR8ESzBJ\nMEegRaBDhkFodHRwczovL2NlcnRpZmljYXRlcy50cnVzdGVkc2VydmljZXMuaW50\nZWwuY29tL0ludGVsU0dYUm9vdENBLmRlcjAdBgNVHQ4EFgQUImUM1lqdNInzg7SV\nUr9QGzknBqwwDgYDVR0PAQH/BAQDAgEGMBIGA1UdEwEB/wQIMAYBAf8CAQEwCgYI\nKoZIzj0EAwIDSQAwRgIhAOW/5QkR+S9CiSDcNoowLuPRLsWGf/Yi7GSX94BgwTwg\nAiEA4J0lrHoMs+Xo5o/sX6O9QWxHRAvZUGOdRQ7cvqRXaqI=\n-----END CERTIFICATE-----\n", - "qe_identity": "{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2026-02-21T13:43:47Z\",\"nextUpdate\":\"2026-03-23T13:43:47Z\",\"tcbEvaluationDataNumber\":18,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"11000000000000000000000000000000\",\"attributesMask\":\"FBFFFFFFFFFFFFFF0000000000000000\",\"mrsigner\":\"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]}", - "qe_identity_signature": "8d31db300d8fbd61c3525f177a5963ef729139e358b9b572822f7a72812d3eff83fc06a97b0cc9d921a3a12ad63f547266aff1629b9bae4ec31d69305bdb6c52" + "qe_identity": "{\"id\":\"TD_QE\",\"version\":2,\"issueDate\":\"2026-03-18T15:16:33Z\",\"nextUpdate\":\"2026-04-17T15:16:33Z\",\"tcbEvaluationDataNumber\":18,\"miscselect\":\"00000000\",\"miscselectMask\":\"FFFFFFFF\",\"attributes\":\"11000000000000000000000000000000\",\"attributesMask\":\"FBFFFFFFFFFFFFFF0000000000000000\",\"mrsigner\":\"DC9E2A7C6F948F17474E34A7FC43ED030F7C1563F1BABDDF6340C82E0E54A8C5\",\"isvprodid\":2,\"tcbLevels\":[{\"tcb\":{\"isvsvn\":4},\"tcbDate\":\"2024-11-13T00:00:00Z\",\"tcbStatus\":\"UpToDate\"}]}", + "qe_identity_signature": "a0c628117e0d40c9b2682dff38cd21283c70bc319546e3a05c530458c8a532f3bc67d5c66196dd012f9fe8490da0648b9ec0bbeb43ed48662a969871567abec4" }, "tcb_info": { "mrtd": "f06dfda6dce1cf904d4e2bab1dc370634cf95cefa2ceb2de2eee127c9382698090d7a4a13e14c536ec6c9c3c8fa87077", "rtmr0": "e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46", "rtmr1": "a7b523278d4f914ee8df0ec80cd1c3d498cbf1152b0c5eaf65bad9425072874a3fcf891e8b01713d3d9937e3e0d26c15", "rtmr2": "dbf4924c07f5066f3dc6859844184344306aa3263817153dcaee85af97d23e0c0b96efe0731d8865a8747e51b9e351ac", - "rtmr3": "c91f84e3b6bc947589700233f8e500d9c37b2d79738389ef61d1d18184f50bacb20b98ba29d32dfa1a933b11f6d76655", + "rtmr3": "da0a4418d22057f984734c8b122fc41e062ebf0724cba2555656ec9049e573ad46c5201fb00e47fc78cf22e5bbb6bf36", "os_image_hash": "", - "compose_hash": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f692a88bdabfeac792fdd98d48", + "compose_hash": "53ecdde1009af70c7de878c630615c83d74cc0dff58074d804c234263be89e14", "device_id": "7a82191bd4dedb9d716e3aa422963cf1009f36e3068404a0322feca1ce517dc9", - "app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-localnet-one-node-1771748631\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:e28cb0425db06255fe5fc7aadb79534ac63c94c7a721f75c1af1e934d2eb0701\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45\\n\\n volumes:\\n - /var/run/docker.sock:/var/run/docker.sock\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n - /tapp:/tapp:ro\\n - shared-volume:/mnt/shared:ro\\n\\n security_opt:\\n - no-new-privileges:true\\n\\n read_only: true\\n\\n tmpfs:\\n - /tmp\\n\\nvolumes:\\n shared-volume:\\n name: shared-volume\\n\",\n \"kms_enabled\": false,\n \"gateway_enabled\": false,\n \"local_key_provider_enabled\": true,\n \"key_provider_id\": \"\",\n \"public_logs\": true,\n \"public_sysinfo\": true,\n \"allowed_envs\": [],\n \"no_instance_id\": true,\n \"secure_time\": false\n}", + "app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-local-node0-testnet-tee\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980\\n\\n volumes:\\n - /var/run/docker.sock:/var/run/docker.sock\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n - /tapp:/tapp:ro\\n - shared-volume:/mnt/shared:rw\\n\\n security_opt:\\n - no-new-privileges:true\\n\\n read_only: true\\n\\n tmpfs:\\n - /tmp\\n\\nvolumes:\\n shared-volume:\\n name: shared-volume\\n\",\n \"kms_enabled\": false,\n \"gateway_enabled\": false,\n \"local_key_provider_enabled\": true,\n \"key_provider_id\": \"\",\n \"public_logs\": true,\n \"public_sysinfo\": true,\n \"allowed_envs\": [],\n \"no_instance_id\": true,\n \"secure_time\": false\n}", "event_log": [ { "imr": 0, @@ -228,16 +179,16 @@ { "imr": 3, "event_type": 134217729, - "digest": "749f2448e65b5beae9ee03fcbcf0debb84bddb024c5d7859d98c8d25a5361630b13653e0c0feba4b172a83c33d967ebc", + "digest": "40c2b270b217916105b5d8502aeaa68f451b8e23d4c9d284a027b790168fc2d7db5d91ea0948a1aaf0618d7b2f982112", "event": "app-id", - "event_payload": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f6" + "event_payload": "53ecdde1009af70c7de878c630615c83d74cc0df" }, { "imr": 3, "event_type": 134217729, - "digest": "d322210028255abc72dc77012047a9d497cc0252d0f07f86d98290675ca66abfb9afc78785c6038357426af4e7dfd26f", + "digest": "2e7f3eddf530256b99ca3b3dea6c7ff3bbdcb251a4228d4f54d04b05277d6a46ec04bdf08aac771f2579e07271f22192", "event": "compose-hash", - "event_payload": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f692a88bdabfeac792fdd98d48" + "event_payload": "53ecdde1009af70c7de878c630615c83d74cc0dff58074d804c234263be89e14" }, { "imr": 3, @@ -270,9 +221,9 @@ { "imr": 3, "event_type": 134217729, - "digest": "a098bd7250ced8cc8340120c5452dcab7297a994c102b5c40c8e1c0ba2ba53d3b3c4342298d34e5587f753d750a9faad", + "digest": "9aed81f5b1af85f768ef6873ed6f997f55f37de951cca18f5daa35890ab9e5573314d2e0cd188a6913dd4ab6f5455678", "event": "mpc-image-digest", - "event_payload": "6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45" + "event_payload": "6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980" } ] } diff --git a/crates/test-utils/assets/quote.json b/crates/test-utils/assets/quote.json index 887cc7852..1886ba77e 100644 --- a/crates/test-utils/assets/quote.json +++ b/crates/test-utils/assets/quote.json @@ -1 +1 @@ -[4,0,2,0,129,0,0,0,0,0,0,0,147,154,114,51,247,156,76,169,148,10,13,179,149,127,6,7,61,153,138,108,16,87,107,253,246,246,237,142,155,133,233,50,0,0,0,0,11,1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,123,240,99,40,14,148,251,5,31,93,215,177,252,89,206,154,172,66,187,150,29,248,212,75,112,156,155,15,248,122,123,77,246,72,101,123,166,209,24,149,137,254,171,29,90,60,154,157,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,231,2,6,0,0,0,0,0,240,109,253,166,220,225,207,144,77,78,43,171,29,195,112,99,76,249,92,239,162,206,178,222,46,238,18,124,147,130,105,128,144,215,164,161,62,20,197,54,236,108,156,60,143,168,112,119,1,110,105,242,11,52,142,61,104,116,87,244,229,124,63,254,60,251,113,56,246,146,168,139,218,191,234,199,146,253,217,141,72,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,230,115,190,47,112,190,239,183,11,72,166,16,158,237,71,21,215,39,13,70,131,179,191,53,111,162,95,175,191,26,167,110,57,233,18,126,110,104,140,205,169,139,218,177,212,212,127,70,167,181,35,39,141,79,145,78,232,223,14,200,12,209,195,212,152,203,241,21,43,12,94,175,101,186,217,66,80,114,135,74,63,207,137,30,139,1,113,61,61,153,55,227,224,210,108,21,219,244,146,76,7,245,6,111,61,198,133,152,68,24,67,68,48,106,163,38,56,23,21,61,202,238,133,175,151,210,62,12,11,150,239,224,115,29,136,101,168,116,126,81,185,227,81,172,201,31,132,227,182,188,148,117,137,112,2,51,248,229,0,217,195,123,45,121,115,131,137,239,97,209,209,129,132,245,11,172,178,11,152,186,41,211,45,250,26,147,59,17,246,215,102,85,0,1,195,223,53,227,107,60,23,144,43,16,55,239,206,246,210,246,13,185,162,204,96,156,224,219,17,240,182,48,192,132,93,226,121,108,93,157,145,44,64,158,145,229,42,58,76,114,127,244,0,0,0,0,0,0,0,0,0,0,0,0,0,0,204,16,0,0,10,124,154,105,114,192,236,4,179,208,82,53,162,157,20,21,152,133,71,85,85,21,59,229,138,60,128,119,31,41,74,151,97,88,211,84,31,214,163,180,43,240,136,29,116,231,138,255,148,105,65,17,10,5,221,116,226,119,8,76,218,67,115,16,142,188,151,224,189,139,21,116,87,28,157,151,225,226,250,218,147,80,231,144,252,223,62,103,176,31,51,101,181,44,82,180,72,148,151,10,88,144,81,87,230,135,174,102,165,143,241,229,60,148,151,208,187,151,100,64,82,171,52,1,40,143,31,160,6,0,70,16,0,0,4,4,25,27,4,255,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,21,0,0,0,0,0,0,0,231,0,0,0,0,0,0,0,229,163,167,181,216,48,194,149,59,152,83,76,108,89,163,163,79,220,52,233,51,247,245,137,143,10,133,207,8,132,107,202,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,220,158,42,124,111,148,143,23,71,78,52,167,252,67,237,3,15,124,21,99,241,186,189,223,99,64,200,46,14,84,168,197,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,215,98,66,167,203,62,30,139,2,195,192,57,51,40,206,207,77,159,207,61,144,77,52,130,175,199,184,139,101,54,6,152,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,180,243,159,196,112,17,166,99,200,182,156,136,179,201,15,15,47,98,23,37,24,68,126,153,62,34,18,139,203,16,236,185,233,46,116,134,201,84,179,122,113,148,250,173,226,9,104,70,208,7,5,223,11,239,88,182,207,12,115,185,54,184,154,234,32,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,5,0,94,14,0,0,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,69,56,84,67,67,66,74,97,103,65,119,73,66,65,103,73,85,102,50,83,98,121,119,107,77,86,84,74,75,85,53,55,47,74,119,66,112,56,69,100,104,80,48,52,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,119,10,99,68,69,105,77,67,65,71,65,49,85,69,65,119,119,90,83,87,53,48,90,87,119,103,85,48,100,89,73,70,66,68,83,121,66,81,98,71,70,48,90,109,57,121,98,83,66,68,81,84,69,97,77,66,103,71,65,49,85,69,67,103,119,82,10,83,87,53,48,90,87,119,103,81,50,57,121,99,71,57,121,89,88,82,112,98,50,52,120,70,68,65,83,66,103,78,86,66,65,99,77,67,49,78,104,98,110,82,104,73,69,78,115,89,88,74,104,77,81,115,119,67,81,89,68,86,81,81,73,10,68,65,74,68,81,84,69,76,77,65,107,71,65,49,85,69,66,104,77,67,86,86,77,119,72,104,99,78,77,106,85,120,77,84,65,50,77,68,99,122,78,122,77,48,87,104,99,78,77,122,73,120,77,84,65,50,77,68,99,122,78,122,77,48,10,87,106,66,119,77,83,73,119,73,65,89,68,86,81,81,68,68,66,108,74,98,110,82,108,98,67,66,84,82,49,103,103,85,69,78,76,73,69,78,108,99,110,82,112,90,109,108,106,89,88,82,108,77,82,111,119,71,65,89,68,86,81,81,75,10,68,66,70,74,98,110,82,108,98,67,66,68,98,51,74,119,98,51,74,104,100,71,108,118,98,106,69,85,77,66,73,71,65,49,85,69,66,119,119,76,85,50,70,117,100,71,69,103,81,50,120,104,99,109,69,120,67,122,65,74,66,103,78,86,10,66,65,103,77,65,107,78,66,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,86,85,122,66,90,77,66,77,71,66,121,113,71,83,77,52,57,65,103,69,71,67,67,113,71,83,77,52,57,65,119,69,72,65,48,73,65,66,71,112,118,10,48,89,117,89,114,113,65,117,83,75,66,122,75,108,117,98,54,109,76,43,114,118,102,68,53,65,106,89,79,51,81,78,103,102,87,122,116,103,52,101,109,49,69,71,66,86,107,71,108,87,118,100,117,66,48,88,81,83,69,47,115,120,71,68,10,109,83,118,75,111,57,116,51,67,114,79,80,67,52,83,85,54,88,54,106,103,103,77,77,77,73,73,68,67,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,83,86,98,49,51,78,118,82,118,104,54,85,66,74,121,100,84,48,10,77,56,52,66,86,119,118,101,86,68,66,114,66,103,78,86,72,82,56,69,90,68,66,105,77,71,67,103,88,113,66,99,104,108,112,111,100,72,82,119,99,122,111,118,76,50,70,119,97,83,53,48,99,110,86,122,100,71,86,107,99,50,86,121,10,100,109,108,106,90,88,77,117,97,87,53,48,90,87,119,117,89,50,57,116,76,51,78,110,101,67,57,106,90,88,74,48,97,87,90,112,89,50,70,48,97,87,57,117,76,51,89,48,76,51,66,106,97,50,78,121,98,68,57,106,89,84,49,119,10,98,71,70,48,90,109,57,121,98,83,90,108,98,109,78,118,90,71,108,117,90,122,49,107,90,88,73,119,72,81,89,68,86,82,48,79,66,66,89,69,70,71,51,110,54,83,43,75,120,78,54,116,43,72,73,56,71,112,57,54,80,107,117,90,10,105,87,115,90,77,65,52,71,65,49,85,100,68,119,69,66,47,119,81,69,65,119,73,71,119,68,65,77,66,103,78,86,72,82,77,66,65,102,56,69,65,106,65,65,77,73,73,67,79,81,89,74,75,111,90,73,104,118,104,78,65,81,48,66,10,66,73,73,67,75,106,67,67,65,105,89,119,72,103,89,75,75,111,90,73,104,118,104,78,65,81,48,66,65,81,81,81,48,103,106,102,115,81,65,106,82,113,52,98,116,79,56,113,80,65,86,83,107,106,67,67,65,87,77,71,67,105,113,71,10,83,73,98,52,84,81,69,78,65,81,73,119,103,103,70,84,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,66,65,103,69,69,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,67,65,103,69,69,10,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,68,65,103,69,67,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,69,65,103,69,67,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,10,65,81,73,70,65,103,69,69,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,71,65,103,69,66,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,72,65,103,69,65,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,73,73,65,103,69,70,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,74,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,75,65,103,69,65,10,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,76,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,77,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,10,65,81,73,78,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,79,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,80,65,103,69,65,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,73,81,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,82,65,103,69,76,77,66,56,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,83,66,66,65,69,10,66,65,73,67,66,65,69,65,66,81,65,65,65,65,65,65,65,65,65,65,77,66,65,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,77,69,65,103,65,65,77,66,81,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,81,69,10,66,114,68,65,98,119,65,65,65,68,65,80,66,103,111,113,104,107,105,71,43,69,48,66,68,81,69,70,67,103,69,66,77,66,52,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,89,69,69,68,97,57,104,116,65,56,65,74,47,90,10,50,70,109,97,76,53,74,113,47,75,69,119,82,65,89,75,75,111,90,73,104,118,104,78,65,81,48,66,66,122,65,50,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,99,66,65,81,72,47,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,99,67,65,81,72,47,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,99,68,65,81,72,47,77,65,111,71,67,67,113,71,83,77,52,57,66,65,77,67,65,48,107,65,77,69,89,67,10,73,81,67,70,71,49,89,65,98,51,101,88,70,116,101,56,53,51,67,108,86,66,110,104,108,67,102,68,121,99,53,55,50,90,88,69,113,97,120,52,85,99,99,83,97,119,73,104,65,79,110,48,86,78,75,84,90,109,65,120,85,70,52,110,10,119,82,107,83,70,104,52,113,70,74,51,97,85,108,122,70,111,80,81,84,51,120,73,102,55,107,70,68,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,108,106,67,67,65,106,50,103,65,119,73,66,65,103,73,86,65,74,86,118,88,99,50,57,71,43,72,112,81,69,110,74,49,80,81,122,122,103,70,88,67,57,53,85,77,65,111,71,67,67,113,71,83,77,52,57,66,65,77,67,10,77,71,103,120,71,106,65,89,66,103,78,86,66,65,77,77,69,85,108,117,100,71,86,115,73,70,78,72,87,67,66,83,98,50,57,48,73,69,78,66,77,82,111,119,71,65,89,68,86,81,81,75,68,66,70,74,98,110,82,108,98,67,66,68,10,98,51,74,119,98,51,74,104,100,71,108,118,98,106,69,85,77,66,73,71,65,49,85,69,66,119,119,76,85,50,70,117,100,71,69,103,81,50,120,104,99,109,69,120,67,122,65,74,66,103,78,86,66,65,103,77,65,107,78,66,77,81,115,119,10,67,81,89,68,86,81,81,71,69,119,74,86,85,122,65,101,70,119,48,120,79,68,65,49,77,106,69,120,77,68,85,119,77,84,66,97,70,119,48,122,77,122,65,49,77,106,69,120,77,68,85,119,77,84,66,97,77,72,65,120,73,106,65,103,10,66,103,78,86,66,65,77,77,71,85,108,117,100,71,86,115,73,70,78,72,87,67,66,81,81,48,115,103,85,71,120,104,100,71,90,118,99,109,48,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,10,73,69,78,118,99,110,66,118,99,109,70,48,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,10,67,122,65,74,66,103,78,86,66,65,89,84,65,108,86,84,77,70,107,119,69,119,89,72,75,111,90,73,122,106,48,67,65,81,89,73,75,111,90,73,122,106,48,68,65,81,99,68,81,103,65,69,78,83,66,47,55,116,50,49,108,88,83,79,10,50,67,117,122,112,120,119,55,52,101,74,66,55,50,69,121,68,71,103,87,53,114,88,67,116,120,50,116,86,84,76,113,54,104,75,107,54,122,43,85,105,82,90,67,110,113,82,55,112,115,79,118,103,113,70,101,83,120,108,109,84,108,74,108,10,101,84,109,105,50,87,89,122,51,113,79,66,117,122,67,66,117,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,81,105,90,81,122,87,87,112,48,48,105,102,79,68,116,74,86,83,118,49,65,98,79,83,99,71,114,68,66,83,10,66,103,78,86,72,82,56,69,83,122,66,74,77,69,101,103,82,97,66,68,104,107,70,111,100,72,82,119,99,122,111,118,76,50,78,108,99,110,82,112,90,109,108,106,89,88,82,108,99,121,53,48,99,110,86,122,100,71,86,107,99,50,86,121,10,100,109,108,106,90,88,77,117,97,87,53,48,90,87,119,117,89,50,57,116,76,48,108,117,100,71,86,115,85,48,100,89,85,109,57,118,100,69,78,66,76,109,82,108,99,106,65,100,66,103,78,86,72,81,52,69,70,103,81,85,108,87,57,100,10,122,98,48,98,52,101,108,65,83,99,110,85,57,68,80,79,65,86,99,76,51,108,81,119,68,103,89,68,86,82,48,80,65,81,72,47,66,65,81,68,65,103,69,71,77,66,73,71,65,49,85,100,69,119,69,66,47,119,81,73,77,65,89,66,10,65,102,56,67,65,81,65,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,68,82,119,65,119,82,65,73,103,88,115,86,107,105,48,119,43,105,54,86,89,71,87,51,85,70,47,50,50,117,97,88,101,48,89,74,68,106,49,85,101,10,110,65,43,84,106,68,49,97,105,53,99,67,73,67,89,98,49,83,65,109,68,53,120,107,102,84,86,112,118,111,52,85,111,121,105,83,89,120,114,68,87,76,109,85,82,52,67,73,57,78,75,121,102,80,78,43,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,106,122,67,67,65,106,83,103,65,119,73,66,65,103,73,85,73,109,85,77,49,108,113,100,78,73,110,122,103,55,83,86,85,114,57,81,71,122,107,110,66,113,119,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,119,10,97,68,69,97,77,66,103,71,65,49,85,69,65,119,119,82,83,87,53,48,90,87,119,103,85,48,100,89,73,70,74,118,98,51,81,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,73,69,78,118,10,99,110,66,118,99,109,70,48,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,67,122,65,74,10,66,103,78,86,66,65,89,84,65,108,86,84,77,66,52,88,68,84,69,52,77,68,85,121,77,84,69,119,78,68,85,120,77,70,111,88,68,84,81,53,77,84,73,122,77,84,73,122,78,84,107,49,79,86,111,119,97,68,69,97,77,66,103,71,10,65,49,85,69,65,119,119,82,83,87,53,48,90,87,119,103,85,48,100,89,73,70,74,118,98,51,81,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,73,69,78,118,99,110,66,118,99,109,70,48,10,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,67,122,65,74,66,103,78,86,66,65,89,84,10,65,108,86,84,77,70,107,119,69,119,89,72,75,111,90,73,122,106,48,67,65,81,89,73,75,111,90,73,122,106,48,68,65,81,99,68,81,103,65,69,67,54,110,69,119,77,68,73,89,90,79,106,47,105,80,87,115,67,122,97,69,75,105,55,10,49,79,105,79,83,76,82,70,104,87,71,106,98,110,66,86,74,102,86,110,107,89,52,117,51,73,106,107,68,89,89,76,48,77,120,79,52,109,113,115,121,89,106,108,66,97,108,84,86,89,120,70,80,50,115,74,66,75,53,122,108,75,79,66,10,117,122,67,66,117,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,81,105,90,81,122,87,87,112,48,48,105,102,79,68,116,74,86,83,118,49,65,98,79,83,99,71,114,68,66,83,66,103,78,86,72,82,56,69,83,122,66,74,10,77,69,101,103,82,97,66,68,104,107,70,111,100,72,82,119,99,122,111,118,76,50,78,108,99,110,82,112,90,109,108,106,89,88,82,108,99,121,53,48,99,110,86,122,100,71,86,107,99,50,86,121,100,109,108,106,90,88,77,117,97,87,53,48,10,90,87,119,117,89,50,57,116,76,48,108,117,100,71,86,115,85,48,100,89,85,109,57,118,100,69,78,66,76,109,82,108,99,106,65,100,66,103,78,86,72,81,52,69,70,103,81,85,73,109,85,77,49,108,113,100,78,73,110,122,103,55,83,86,10,85,114,57,81,71,122,107,110,66,113,119,119,68,103,89,68,86,82,48,80,65,81,72,47,66,65,81,68,65,103,69,71,77,66,73,71,65,49,85,100,69,119,69,66,47,119,81,73,77,65,89,66,65,102,56,67,65,81,69,119,67,103,89,73,10,75,111,90,73,122,106,48,69,65,119,73,68,83,81,65,119,82,103,73,104,65,79,87,47,53,81,107,82,43,83,57,67,105,83,68,99,78,111,111,119,76,117,80,82,76,115,87,71,102,47,89,105,55,71,83,88,57,52,66,103,119,84,119,103,10,65,105,69,65,52,74,48,108,114,72,111,77,115,43,88,111,53,111,47,115,88,54,79,57,81,87,120,72,82,65,118,90,85,71,79,100,82,81,55,99,118,113,82,88,97,113,73,61,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] +[4,0,2,0,129,0,0,0,0,0,0,0,147,154,114,51,247,156,76,169,148,10,13,179,149,127,6,7,61,153,138,108,16,87,107,253,246,246,237,142,155,133,233,50,0,0,0,0,11,1,4,0,0,0,0,0,0,0,0,0,0,0,0,0,123,240,99,40,14,148,251,5,31,93,215,177,252,89,206,154,172,66,187,150,29,248,212,75,112,156,155,15,248,122,123,77,246,72,101,123,166,209,24,149,137,254,171,29,90,60,154,157,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,16,0,0,0,0,231,2,6,0,0,0,0,0,240,109,253,166,220,225,207,144,77,78,43,171,29,195,112,99,76,249,92,239,162,206,178,222,46,238,18,124,147,130,105,128,144,215,164,161,62,20,197,54,236,108,156,60,143,168,112,119,1,83,236,221,225,0,154,247,12,125,232,120,198,48,97,92,131,215,76,192,223,245,128,116,216,4,194,52,38,59,232,158,20,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,230,115,190,47,112,190,239,183,11,72,166,16,158,237,71,21,215,39,13,70,131,179,191,53,111,162,95,175,191,26,167,110,57,233,18,126,110,104,140,205,169,139,218,177,212,212,127,70,167,181,35,39,141,79,145,78,232,223,14,200,12,209,195,212,152,203,241,21,43,12,94,175,101,186,217,66,80,114,135,74,63,207,137,30,139,1,113,61,61,153,55,227,224,210,108,21,219,244,146,76,7,245,6,111,61,198,133,152,68,24,67,68,48,106,163,38,56,23,21,61,202,238,133,175,151,210,62,12,11,150,239,224,115,29,136,101,168,116,126,81,185,227,81,172,218,10,68,24,210,32,87,249,132,115,76,139,18,47,196,30,6,46,191,7,36,203,162,85,86,86,236,144,73,229,115,173,70,197,32,31,176,14,71,252,120,207,34,229,187,182,191,54,0,1,230,96,24,18,213,192,100,70,58,213,78,59,53,17,217,163,54,112,232,23,160,220,235,179,37,49,79,190,82,213,203,190,225,212,81,68,83,229,207,194,138,138,24,32,234,212,207,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,204,16,0,0,72,83,124,99,30,47,37,1,239,147,205,111,190,223,244,63,24,79,122,117,181,21,208,192,202,124,19,125,86,114,223,16,236,95,243,28,111,185,165,202,147,55,87,91,132,48,117,139,38,49,119,42,244,99,183,237,161,13,98,224,148,222,245,106,142,188,151,224,189,139,21,116,87,28,157,151,225,226,250,218,147,80,231,144,252,223,62,103,176,31,51,101,181,44,82,180,72,148,151,10,88,144,81,87,230,135,174,102,165,143,241,229,60,148,151,208,187,151,100,64,82,171,52,1,40,143,31,160,6,0,70,16,0,0,4,4,25,27,4,255,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,21,0,0,0,0,0,0,0,231,0,0,0,0,0,0,0,229,163,167,181,216,48,194,149,59,152,83,76,108,89,163,163,79,220,52,233,51,247,245,137,143,10,133,207,8,132,107,202,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,220,158,42,124,111,148,143,23,71,78,52,167,252,67,237,3,15,124,21,99,241,186,189,223,99,64,200,46,14,84,168,197,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,6,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,215,98,66,167,203,62,30,139,2,195,192,57,51,40,206,207,77,159,207,61,144,77,52,130,175,199,184,139,101,54,6,152,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,150,43,224,72,222,235,58,201,147,41,73,102,159,167,17,139,183,255,92,219,174,234,152,2,188,66,241,7,248,149,104,181,57,202,96,91,236,128,245,224,94,102,1,108,69,117,32,200,63,232,4,154,218,62,108,62,37,222,11,49,235,195,58,169,32,0,0,1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30,31,5,0,94,14,0,0,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,69,56,84,67,67,66,74,97,103,65,119,73,66,65,103,73,85,102,50,83,98,121,119,107,77,86,84,74,75,85,53,55,47,74,119,66,112,56,69,100,104,80,48,52,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,119,10,99,68,69,105,77,67,65,71,65,49,85,69,65,119,119,90,83,87,53,48,90,87,119,103,85,48,100,89,73,70,66,68,83,121,66,81,98,71,70,48,90,109,57,121,98,83,66,68,81,84,69,97,77,66,103,71,65,49,85,69,67,103,119,82,10,83,87,53,48,90,87,119,103,81,50,57,121,99,71,57,121,89,88,82,112,98,50,52,120,70,68,65,83,66,103,78,86,66,65,99,77,67,49,78,104,98,110,82,104,73,69,78,115,89,88,74,104,77,81,115,119,67,81,89,68,86,81,81,73,10,68,65,74,68,81,84,69,76,77,65,107,71,65,49,85,69,66,104,77,67,86,86,77,119,72,104,99,78,77,106,85,120,77,84,65,50,77,68,99,122,78,122,77,48,87,104,99,78,77,122,73,120,77,84,65,50,77,68,99,122,78,122,77,48,10,87,106,66,119,77,83,73,119,73,65,89,68,86,81,81,68,68,66,108,74,98,110,82,108,98,67,66,84,82,49,103,103,85,69,78,76,73,69,78,108,99,110,82,112,90,109,108,106,89,88,82,108,77,82,111,119,71,65,89,68,86,81,81,75,10,68,66,70,74,98,110,82,108,98,67,66,68,98,51,74,119,98,51,74,104,100,71,108,118,98,106,69,85,77,66,73,71,65,49,85,69,66,119,119,76,85,50,70,117,100,71,69,103,81,50,120,104,99,109,69,120,67,122,65,74,66,103,78,86,10,66,65,103,77,65,107,78,66,77,81,115,119,67,81,89,68,86,81,81,71,69,119,74,86,85,122,66,90,77,66,77,71,66,121,113,71,83,77,52,57,65,103,69,71,67,67,113,71,83,77,52,57,65,119,69,72,65,48,73,65,66,71,112,118,10,48,89,117,89,114,113,65,117,83,75,66,122,75,108,117,98,54,109,76,43,114,118,102,68,53,65,106,89,79,51,81,78,103,102,87,122,116,103,52,101,109,49,69,71,66,86,107,71,108,87,118,100,117,66,48,88,81,83,69,47,115,120,71,68,10,109,83,118,75,111,57,116,51,67,114,79,80,67,52,83,85,54,88,54,106,103,103,77,77,77,73,73,68,67,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,83,86,98,49,51,78,118,82,118,104,54,85,66,74,121,100,84,48,10,77,56,52,66,86,119,118,101,86,68,66,114,66,103,78,86,72,82,56,69,90,68,66,105,77,71,67,103,88,113,66,99,104,108,112,111,100,72,82,119,99,122,111,118,76,50,70,119,97,83,53,48,99,110,86,122,100,71,86,107,99,50,86,121,10,100,109,108,106,90,88,77,117,97,87,53,48,90,87,119,117,89,50,57,116,76,51,78,110,101,67,57,106,90,88,74,48,97,87,90,112,89,50,70,48,97,87,57,117,76,51,89,48,76,51,66,106,97,50,78,121,98,68,57,106,89,84,49,119,10,98,71,70,48,90,109,57,121,98,83,90,108,98,109,78,118,90,71,108,117,90,122,49,107,90,88,73,119,72,81,89,68,86,82,48,79,66,66,89,69,70,71,51,110,54,83,43,75,120,78,54,116,43,72,73,56,71,112,57,54,80,107,117,90,10,105,87,115,90,77,65,52,71,65,49,85,100,68,119,69,66,47,119,81,69,65,119,73,71,119,68,65,77,66,103,78,86,72,82,77,66,65,102,56,69,65,106,65,65,77,73,73,67,79,81,89,74,75,111,90,73,104,118,104,78,65,81,48,66,10,66,73,73,67,75,106,67,67,65,105,89,119,72,103,89,75,75,111,90,73,104,118,104,78,65,81,48,66,65,81,81,81,48,103,106,102,115,81,65,106,82,113,52,98,116,79,56,113,80,65,86,83,107,106,67,67,65,87,77,71,67,105,113,71,10,83,73,98,52,84,81,69,78,65,81,73,119,103,103,70,84,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,66,65,103,69,69,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,67,65,103,69,69,10,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,68,65,103,69,67,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,69,65,103,69,67,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,10,65,81,73,70,65,103,69,69,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,71,65,103,69,66,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,72,65,103,69,65,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,73,73,65,103,69,70,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,74,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,75,65,103,69,65,10,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,76,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,77,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,10,65,81,73,78,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,79,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,80,65,103,69,65,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,73,81,65,103,69,65,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,82,65,103,69,76,77,66,56,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,73,83,66,66,65,69,10,66,65,73,67,66,65,69,65,66,81,65,65,65,65,65,65,65,65,65,65,77,66,65,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,77,69,65,103,65,65,77,66,81,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,81,69,10,66,114,68,65,98,119,65,65,65,68,65,80,66,103,111,113,104,107,105,71,43,69,48,66,68,81,69,70,67,103,69,66,77,66,52,71,67,105,113,71,83,73,98,52,84,81,69,78,65,81,89,69,69,68,97,57,104,116,65,56,65,74,47,90,10,50,70,109,97,76,53,74,113,47,75,69,119,82,65,89,75,75,111,90,73,104,118,104,78,65,81,48,66,66,122,65,50,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,99,66,65,81,72,47,77,66,65,71,67,121,113,71,10,83,73,98,52,84,81,69,78,65,81,99,67,65,81,72,47,77,66,65,71,67,121,113,71,83,73,98,52,84,81,69,78,65,81,99,68,65,81,72,47,77,65,111,71,67,67,113,71,83,77,52,57,66,65,77,67,65,48,107,65,77,69,89,67,10,73,81,67,70,71,49,89,65,98,51,101,88,70,116,101,56,53,51,67,108,86,66,110,104,108,67,102,68,121,99,53,55,50,90,88,69,113,97,120,52,85,99,99,83,97,119,73,104,65,79,110,48,86,78,75,84,90,109,65,120,85,70,52,110,10,119,82,107,83,70,104,52,113,70,74,51,97,85,108,122,70,111,80,81,84,51,120,73,102,55,107,70,68,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,108,106,67,67,65,106,50,103,65,119,73,66,65,103,73,86,65,74,86,118,88,99,50,57,71,43,72,112,81,69,110,74,49,80,81,122,122,103,70,88,67,57,53,85,77,65,111,71,67,67,113,71,83,77,52,57,66,65,77,67,10,77,71,103,120,71,106,65,89,66,103,78,86,66,65,77,77,69,85,108,117,100,71,86,115,73,70,78,72,87,67,66,83,98,50,57,48,73,69,78,66,77,82,111,119,71,65,89,68,86,81,81,75,68,66,70,74,98,110,82,108,98,67,66,68,10,98,51,74,119,98,51,74,104,100,71,108,118,98,106,69,85,77,66,73,71,65,49,85,69,66,119,119,76,85,50,70,117,100,71,69,103,81,50,120,104,99,109,69,120,67,122,65,74,66,103,78,86,66,65,103,77,65,107,78,66,77,81,115,119,10,67,81,89,68,86,81,81,71,69,119,74,86,85,122,65,101,70,119,48,120,79,68,65,49,77,106,69,120,77,68,85,119,77,84,66,97,70,119,48,122,77,122,65,49,77,106,69,120,77,68,85,119,77,84,66,97,77,72,65,120,73,106,65,103,10,66,103,78,86,66,65,77,77,71,85,108,117,100,71,86,115,73,70,78,72,87,67,66,81,81,48,115,103,85,71,120,104,100,71,90,118,99,109,48,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,10,73,69,78,118,99,110,66,118,99,109,70,48,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,10,67,122,65,74,66,103,78,86,66,65,89,84,65,108,86,84,77,70,107,119,69,119,89,72,75,111,90,73,122,106,48,67,65,81,89,73,75,111,90,73,122,106,48,68,65,81,99,68,81,103,65,69,78,83,66,47,55,116,50,49,108,88,83,79,10,50,67,117,122,112,120,119,55,52,101,74,66,55,50,69,121,68,71,103,87,53,114,88,67,116,120,50,116,86,84,76,113,54,104,75,107,54,122,43,85,105,82,90,67,110,113,82,55,112,115,79,118,103,113,70,101,83,120,108,109,84,108,74,108,10,101,84,109,105,50,87,89,122,51,113,79,66,117,122,67,66,117,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,81,105,90,81,122,87,87,112,48,48,105,102,79,68,116,74,86,83,118,49,65,98,79,83,99,71,114,68,66,83,10,66,103,78,86,72,82,56,69,83,122,66,74,77,69,101,103,82,97,66,68,104,107,70,111,100,72,82,119,99,122,111,118,76,50,78,108,99,110,82,112,90,109,108,106,89,88,82,108,99,121,53,48,99,110,86,122,100,71,86,107,99,50,86,121,10,100,109,108,106,90,88,77,117,97,87,53,48,90,87,119,117,89,50,57,116,76,48,108,117,100,71,86,115,85,48,100,89,85,109,57,118,100,69,78,66,76,109,82,108,99,106,65,100,66,103,78,86,72,81,52,69,70,103,81,85,108,87,57,100,10,122,98,48,98,52,101,108,65,83,99,110,85,57,68,80,79,65,86,99,76,51,108,81,119,68,103,89,68,86,82,48,80,65,81,72,47,66,65,81,68,65,103,69,71,77,66,73,71,65,49,85,100,69,119,69,66,47,119,81,73,77,65,89,66,10,65,102,56,67,65,81,65,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,68,82,119,65,119,82,65,73,103,88,115,86,107,105,48,119,43,105,54,86,89,71,87,51,85,70,47,50,50,117,97,88,101,48,89,74,68,106,49,85,101,10,110,65,43,84,106,68,49,97,105,53,99,67,73,67,89,98,49,83,65,109,68,53,120,107,102,84,86,112,118,111,52,85,111,121,105,83,89,120,114,68,87,76,109,85,82,52,67,73,57,78,75,121,102,80,78,43,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,45,45,45,45,45,66,69,71,73,78,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,77,73,73,67,106,122,67,67,65,106,83,103,65,119,73,66,65,103,73,85,73,109,85,77,49,108,113,100,78,73,110,122,103,55,83,86,85,114,57,81,71,122,107,110,66,113,119,119,67,103,89,73,75,111,90,73,122,106,48,69,65,119,73,119,10,97,68,69,97,77,66,103,71,65,49,85,69,65,119,119,82,83,87,53,48,90,87,119,103,85,48,100,89,73,70,74,118,98,51,81,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,73,69,78,118,10,99,110,66,118,99,109,70,48,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,67,122,65,74,10,66,103,78,86,66,65,89,84,65,108,86,84,77,66,52,88,68,84,69,52,77,68,85,121,77,84,69,119,78,68,85,120,77,70,111,88,68,84,81,53,77,84,73,122,77,84,73,122,78,84,107,49,79,86,111,119,97,68,69,97,77,66,103,71,10,65,49,85,69,65,119,119,82,83,87,53,48,90,87,119,103,85,48,100,89,73,70,74,118,98,51,81,103,81,48,69,120,71,106,65,89,66,103,78,86,66,65,111,77,69,85,108,117,100,71,86,115,73,69,78,118,99,110,66,118,99,109,70,48,10,97,87,57,117,77,82,81,119,69,103,89,68,86,81,81,72,68,65,116,84,89,87,53,48,89,83,66,68,98,71,70,121,89,84,69,76,77,65,107,71,65,49,85,69,67,65,119,67,81,48,69,120,67,122,65,74,66,103,78,86,66,65,89,84,10,65,108,86,84,77,70,107,119,69,119,89,72,75,111,90,73,122,106,48,67,65,81,89,73,75,111,90,73,122,106,48,68,65,81,99,68,81,103,65,69,67,54,110,69,119,77,68,73,89,90,79,106,47,105,80,87,115,67,122,97,69,75,105,55,10,49,79,105,79,83,76,82,70,104,87,71,106,98,110,66,86,74,102,86,110,107,89,52,117,51,73,106,107,68,89,89,76,48,77,120,79,52,109,113,115,121,89,106,108,66,97,108,84,86,89,120,70,80,50,115,74,66,75,53,122,108,75,79,66,10,117,122,67,66,117,68,65,102,66,103,78,86,72,83,77,69,71,68,65,87,103,66,81,105,90,81,122,87,87,112,48,48,105,102,79,68,116,74,86,83,118,49,65,98,79,83,99,71,114,68,66,83,66,103,78,86,72,82,56,69,83,122,66,74,10,77,69,101,103,82,97,66,68,104,107,70,111,100,72,82,119,99,122,111,118,76,50,78,108,99,110,82,112,90,109,108,106,89,88,82,108,99,121,53,48,99,110,86,122,100,71,86,107,99,50,86,121,100,109,108,106,90,88,77,117,97,87,53,48,10,90,87,119,117,89,50,57,116,76,48,108,117,100,71,86,115,85,48,100,89,85,109,57,118,100,69,78,66,76,109,82,108,99,106,65,100,66,103,78,86,72,81,52,69,70,103,81,85,73,109,85,77,49,108,113,100,78,73,110,122,103,55,83,86,10,85,114,57,81,71,122,107,110,66,113,119,119,68,103,89,68,86,82,48,80,65,81,72,47,66,65,81,68,65,103,69,71,77,66,73,71,65,49,85,100,69,119,69,66,47,119,81,73,77,65,89,66,65,102,56,67,65,81,69,119,67,103,89,73,10,75,111,90,73,122,106,48,69,65,119,73,68,83,81,65,119,82,103,73,104,65,79,87,47,53,81,107,82,43,83,57,67,105,83,68,99,78,111,111,119,76,117,80,82,76,115,87,71,102,47,89,105,55,71,83,88,57,52,66,103,119,84,119,103,10,65,105,69,65,52,74,48,108,114,72,111,77,115,43,88,111,53,111,47,115,88,54,79,57,81,87,120,72,82,65,118,90,85,71,79,100,82,81,55,99,118,113,82,88,97,113,73,61,10,45,45,45,45,45,69,78,68,32,67,69,82,84,73,70,73,67,65,84,69,45,45,45,45,45,10,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0] diff --git a/crates/test-utils/assets/tcb_info.json b/crates/test-utils/assets/tcb_info.json index 772640d15..368169ea2 100644 --- a/crates/test-utils/assets/tcb_info.json +++ b/crates/test-utils/assets/tcb_info.json @@ -3,11 +3,11 @@ "rtmr0": "e673be2f70beefb70b48a6109eed4715d7270d4683b3bf356fa25fafbf1aa76e39e9127e6e688ccda98bdab1d4d47f46", "rtmr1": "a7b523278d4f914ee8df0ec80cd1c3d498cbf1152b0c5eaf65bad9425072874a3fcf891e8b01713d3d9937e3e0d26c15", "rtmr2": "dbf4924c07f5066f3dc6859844184344306aa3263817153dcaee85af97d23e0c0b96efe0731d8865a8747e51b9e351ac", - "rtmr3": "c91f84e3b6bc947589700233f8e500d9c37b2d79738389ef61d1d18184f50bacb20b98ba29d32dfa1a933b11f6d76655", + "rtmr3": "da0a4418d22057f984734c8b122fc41e062ebf0724cba2555656ec9049e573ad46c5201fb00e47fc78cf22e5bbb6bf36", "os_image_hash": "", - "compose_hash": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f692a88bdabfeac792fdd98d48", + "compose_hash": "53ecdde1009af70c7de878c630615c83d74cc0dff58074d804c234263be89e14", "device_id": "7a82191bd4dedb9d716e3aa422963cf1009f36e3068404a0322feca1ce517dc9", - "app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-localnet-one-node-1771748631\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:e28cb0425db06255fe5fc7aadb79534ac63c94c7a721f75c1af1e934d2eb0701\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45\\n\\n volumes:\\n - /var/run/docker.sock:/var/run/docker.sock\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n - /tapp:/tapp:ro\\n - shared-volume:/mnt/shared:ro\\n\\n security_opt:\\n - no-new-privileges:true\\n\\n read_only: true\\n\\n tmpfs:\\n - /tmp\\n\\nvolumes:\\n shared-volume:\\n name: shared-volume\\n\",\n \"kms_enabled\": false,\n \"gateway_enabled\": false,\n \"local_key_provider_enabled\": true,\n \"key_provider_id\": \"\",\n \"public_logs\": true,\n \"public_sysinfo\": true,\n \"allowed_envs\": [],\n \"no_instance_id\": true,\n \"secure_time\": false\n}", + "app_compose": "{\n \"manifest_version\": 2,\n \"name\": \"mpc-local-node0-testnet-tee\",\n \"runner\": \"docker-compose\",\n \"docker_compose_file\": \"version: '3.8'\\n\\nservices:\\n launcher:\\n image: nearone/mpc-launcher@sha256:02ba809689637d13a9e28b31a9e00de4fef776f3d604bdd51c5a03ee756eb316\\n\\n container_name: launcher\\n\\n environment:\\n - PLATFORM=TEE\\n - DOCKER_CONTENT_TRUST=1\\n - DEFAULT_IMAGE_DIGEST=sha256:6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980\\n\\n volumes:\\n - /var/run/docker.sock:/var/run/docker.sock\\n - /var/run/dstack.sock:/var/run/dstack.sock\\n - /tapp:/tapp:ro\\n - shared-volume:/mnt/shared:rw\\n\\n security_opt:\\n - no-new-privileges:true\\n\\n read_only: true\\n\\n tmpfs:\\n - /tmp\\n\\nvolumes:\\n shared-volume:\\n name: shared-volume\\n\",\n \"kms_enabled\": false,\n \"gateway_enabled\": false,\n \"local_key_provider_enabled\": true,\n \"key_provider_id\": \"\",\n \"public_logs\": true,\n \"public_sysinfo\": true,\n \"allowed_envs\": [],\n \"no_instance_id\": true,\n \"secure_time\": false\n}", "event_log": [ { "imr": 0, @@ -159,16 +159,16 @@ { "imr": 3, "event_type": 134217729, - "digest": "749f2448e65b5beae9ee03fcbcf0debb84bddb024c5d7859d98c8d25a5361630b13653e0c0feba4b172a83c33d967ebc", + "digest": "40c2b270b217916105b5d8502aeaa68f451b8e23d4c9d284a027b790168fc2d7db5d91ea0948a1aaf0618d7b2f982112", "event": "app-id", - "event_payload": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f6" + "event_payload": "53ecdde1009af70c7de878c630615c83d74cc0df" }, { "imr": 3, "event_type": 134217729, - "digest": "d322210028255abc72dc77012047a9d497cc0252d0f07f86d98290675ca66abfb9afc78785c6038357426af4e7dfd26f", + "digest": "2e7f3eddf530256b99ca3b3dea6c7ff3bbdcb251a4228d4f54d04b05277d6a46ec04bdf08aac771f2579e07271f22192", "event": "compose-hash", - "event_payload": "6e69f20b348e3d687457f4e57c3ffe3cfb7138f692a88bdabfeac792fdd98d48" + "event_payload": "53ecdde1009af70c7de878c630615c83d74cc0dff58074d804c234263be89e14" }, { "imr": 3, @@ -201,9 +201,9 @@ { "imr": 3, "event_type": 134217729, - "digest": "a098bd7250ced8cc8340120c5452dcab7297a994c102b5c40c8e1c0ba2ba53d3b3c4342298d34e5587f753d750a9faad", + "digest": "9aed81f5b1af85f768ef6873ed6f997f55f37de951cca18f5daa35890ab9e5573314d2e0cd188a6913dd4ab6f5455678", "event": "mpc-image-digest", - "event_payload": "6e5b08f91752fd7cb10349de45f74272f340fe42a172d2cbc237f3f1d5527a45" + "event_payload": "6a5700fccbb3facddd1f3934f4976c4dcefc176c4aac28cd2fd035984b368980" } ] } From f1f35f57a5ff21ff9909b70b17cddd1092434a0e Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Thu, 19 Mar 2026 16:55:52 +0000 Subject: [PATCH 173/176] update VALID_ATTESTATION_TIMESTAMP --- crates/test-utils/src/attestation.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/test-utils/src/attestation.rs b/crates/test-utils/src/attestation.rs index 1a7a05341..85ce9a5c0 100644 --- a/crates/test-utils/src/attestation.rs +++ b/crates/test-utils/src/attestation.rs @@ -18,10 +18,10 @@ pub const TEST_MPC_IMAGE_DIGEST_HEX: &str = include_str!("../assets/mpc_image_di pub const TEST_LAUNCHER_IMAGE_COMPOSE_STRING: &str = include_str!("../assets/launcher_image_compose.yaml"); -/// Unix time as of 2026/02/18, represents a date where +/// Unix time as of 2026/03/20, represents a date where /// the measurements stored in ../assets are valid. When these measurements are /// modified, this value should be updated as well -pub const VALID_ATTESTATION_TIMESTAMP: u64 = 1771750692; +pub const VALID_ATTESTATION_TIMESTAMP: u64 = 1774018367; pub fn launcher_compose_digest() -> LauncherDockerComposeHash { let digest: [u8; 32] = Sha256::digest(TEST_LAUNCHER_IMAGE_COMPOSE_STRING).into(); From c6451a732c71dc76f8118b848dac877b2cd8532e Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Thu, 19 Mar 2026 17:06:33 +0000 Subject: [PATCH 174/176] Revert "add near_init section to TOML template" This reverts commit 0b9c46213880dc7972010ff98d14f5b3a82b3e2e. --- localnet/tee/scripts/node.conf.localnet.toml.tpl | 5 ----- 1 file changed, 5 deletions(-) diff --git a/localnet/tee/scripts/node.conf.localnet.toml.tpl b/localnet/tee/scripts/node.conf.localnet.toml.tpl index 0a584c6e5..2c1b44ab0 100644 --- a/localnet/tee/scripts/node.conf.localnet.toml.tpl +++ b/localnet/tee/scripts/node.conf.localnet.toml.tpl @@ -13,11 +13,6 @@ ${PORTS_TOML}] [mpc_config] home_dir = "/data" -[mpc_config.near_init] -chain_id = "mpc-localnet" -boot_nodes = "${NEAR_BOOT_NODES}" -genesis_path = "/app/localnet-genesis.json" - [mpc_config.secrets] secret_store_key_hex = "${MPC_SECRET_STORE_KEY}" backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" From 61cdfd0e46e06ea0040be985c30dd8f253b32da6 Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Thu, 19 Mar 2026 17:06:33 +0000 Subject: [PATCH 175/176] Revert "update localnet scripts to generate TOML config for Rust launcher" This reverts commit 2c4f2a05c7108dc1587eee0cd86d6144f6312aa5. --- localnet/tee/scripts/deploy-tee-localnet.sh | 20 +------ .../how-to-run-localnet-tee-setup-script.md | 2 +- .../tee/scripts/node.conf.localnet.toml.tpl | 53 ------------------- ...ocalnet.tpl.bak => node.conf.localnet.tpl} | 0 localnet/tee/scripts/single-node.sh | 19 +------ 5 files changed, 5 insertions(+), 89 deletions(-) delete mode 100644 localnet/tee/scripts/node.conf.localnet.toml.tpl rename localnet/tee/scripts/{node.conf.localnet.tpl.bak => node.conf.localnet.tpl} (100%) diff --git a/localnet/tee/scripts/deploy-tee-localnet.sh b/localnet/tee/scripts/deploy-tee-localnet.sh index ce5c00fe3..31345f765 100644 --- a/localnet/tee/scripts/deploy-tee-localnet.sh +++ b/localnet/tee/scripts/deploy-tee-localnet.sh @@ -144,25 +144,11 @@ MODE="${MODE:-testnet}" # testnet|localnet # templates live here (UPDATED for move to localnet/tee/scripts) ENV_TPL="$REPO_ROOT/localnet/tee/scripts/node.env.tpl" if [ "$MODE" = "localnet" ]; then - CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.toml.tpl" + CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.tpl" else CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.tpl" fi -# Convert comma-separated "src:dst" port string to TOML inline table array entries. -# E.g. "8080:8080,24566:24566" -> " { src = 8080, dst = 8080 },\n { src = 24566, dst = 24566 }," -ports_to_toml() { - local ports="$1" result="" - IFS=',' read -ra pairs <<< "$ports" - for pair in "${pairs[@]}"; do - local src="${pair%%:*}" - local dst="${pair##*:}" - result+=" { src = $src, dst = $dst }, -" - done - echo -n "$result" -} - WORKDIR="/tmp/$USER/mpc_testnet_scale/$MPC_NETWORK_NAME" mkdir -p "$WORKDIR" @@ -728,7 +714,7 @@ render_node_files_range() { local env_out conf_out env_out="$WORKDIR/node${i}.env" - conf_out="$WORKDIR/node${i}.toml" + conf_out="$WORKDIR/node${i}.conf" export APP_NAME="$app_name" export VMM_RPC @@ -764,8 +750,6 @@ render_node_files_range() { export MPC_SECRET_STORE_KEY="$(printf '%032x' "$i")" export MPC_CONTRACT_ID="$MPC_CONTRACT_ACCOUNT" export PORTS="8080:8080,24566:24566,${future_port}:${future_port}" - export PORTS_TOML - PORTS_TOML="$(ports_to_toml "$PORTS")" export NEAR_BOOT_NODES="ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566" envsubst <"$ENV_TPL" >"$env_out" diff --git a/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md b/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md index 770964e9c..15b7b5bee 100644 --- a/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md +++ b/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md @@ -153,7 +153,7 @@ All generated files are stored under: ``` Important artifacts: -- `node{i}.toml`, `node{i}.env` +- `node{i}.conf`, `node{i}.env` - `keys.json` - `init_args.json` diff --git a/localnet/tee/scripts/node.conf.localnet.toml.tpl b/localnet/tee/scripts/node.conf.localnet.toml.tpl deleted file mode 100644 index 2c1b44ab0..000000000 --- a/localnet/tee/scripts/node.conf.localnet.toml.tpl +++ /dev/null @@ -1,53 +0,0 @@ -[launcher_config] -image_tags = ["${MPC_IMAGE_TAGS}"] -image_name = "${MPC_IMAGE_NAME}" -registry = "${MPC_REGISTRY}" -rpc_request_timeout_secs = 10 -rpc_request_interval_secs = 1 -rpc_max_attempts = 20 - -[docker_command_config.port_mappings] -ports = [ -${PORTS_TOML}] - -[mpc_config] -home_dir = "/data" - -[mpc_config.secrets] -secret_store_key_hex = "${MPC_SECRET_STORE_KEY}" -backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" - -[mpc_config.tee.authority] -type = "local" - -[mpc_config.node] -my_near_account_id = "${MPC_ACCOUNT_ID}" -near_responder_account_id = "${MPC_ACCOUNT_ID}" -number_of_responder_keys = 1 -web_ui = "0.0.0.0:8080" -migration_web_ui = "0.0.0.0:8078" -cores = 4 - -[mpc_config.node.indexer] -validate_genesis = false -sync_mode = "Latest" -concurrency = 1 -mpc_contract_id = "${MPC_CONTRACT_ID}" -finality = "optimistic" - -[mpc_config.node.triple] -concurrency = 2 -desired_triples_to_buffer = 128 -timeout_sec = 60 -parallel_triple_generation_stagger_time_sec = 1 - -[mpc_config.node.presignature] -concurrency = 4 -desired_presignatures_to_buffer = 64 -timeout_sec = 60 - -[mpc_config.node.signature] -timeout_sec = 60 - -[mpc_config.node.ckd] -timeout_sec = 60 diff --git a/localnet/tee/scripts/node.conf.localnet.tpl.bak b/localnet/tee/scripts/node.conf.localnet.tpl similarity index 100% rename from localnet/tee/scripts/node.conf.localnet.tpl.bak rename to localnet/tee/scripts/node.conf.localnet.tpl diff --git a/localnet/tee/scripts/single-node.sh b/localnet/tee/scripts/single-node.sh index c1a0eb435..13b62d135 100755 --- a/localnet/tee/scripts/single-node.sh +++ b/localnet/tee/scripts/single-node.sh @@ -132,26 +132,13 @@ DISK="${DISK:-500G}" REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" TEE_LAUNCHER_DIR="$REPO_ROOT/tee_launcher" ENV_TPL="${ENV_TPL:-$REPO_ROOT/localnet/tee/scripts/node.env.tpl}" -CONF_TPL="${CONF_TPL:-$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.toml.tpl}" - -# Convert comma-separated "src:dst" port string to TOML inline table array entries. -ports_to_toml() { - local ports="$1" result="" - IFS=',' read -ra pairs <<< "$ports" - for pair in "${pairs[@]}"; do - local src="${pair%%:*}" - local dst="${pair##*:}" - result+=" { src = $src, dst = $dst }, -" - done - echo -n "$result" -} +CONF_TPL="${CONF_TPL:-$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.tpl}" WORKDIR="${WORKDIR:-$(mktemp -d /tmp/mpc_localnet_one_node.XXXXXX)}" mkdir -p "$WORKDIR" log "Work directory: $WORKDIR" ENV_OUT="$WORKDIR/node.env" -CONF_OUT="$WORKDIR/node.toml" +CONF_OUT="$WORKDIR/node.conf" PUBLIC_DATA_JSON_OUT="${PUBLIC_DATA_JSON_OUT:-$WORKDIR/public_data.json}" near_account_exists() { @@ -206,8 +193,6 @@ render_env_and_conf() { export MPC_CONTRACT_ID="$CONTRACT_ACCOUNT" export MPC_SECRET_STORE_KEY="${MPC_SECRET_STORE_KEY:-00000000000000000000000000000000}" export PORTS="${PORTS:-8080:8080,24566:24566,${FUTURE_PORT}:${FUTURE_PORT}}" - export PORTS_TOML - PORTS_TOML="$(ports_to_toml "$PORTS")" envsubst <"$ENV_TPL" >"$ENV_OUT" envsubst <"$CONF_TPL" >"$CONF_OUT" From 9e1f5554871baa1314e0b60d5f022283e9f2d76b Mon Sep 17 00:00:00 2001 From: Barakeinav1 Date: Thu, 19 Mar 2026 17:07:35 +0000 Subject: [PATCH 176/176] update localnet deploy scripts for Rust launcher and new contract votes --- localnet/tee/scripts/deploy-tee-localnet.sh | 82 +++++++++++++++++-- .../how-to-run-localnet-tee-setup-script.md | 3 +- .../tee/scripts/node.conf.localnet.toml.tpl | 58 +++++++++++++ localnet/tee/scripts/set-localnet-env.sh | 31 +++++++ localnet/tee/scripts/single-node.sh | 21 ++++- 5 files changed, 185 insertions(+), 10 deletions(-) create mode 100644 localnet/tee/scripts/node.conf.localnet.toml.tpl create mode 100644 localnet/tee/scripts/set-localnet-env.sh diff --git a/localnet/tee/scripts/deploy-tee-localnet.sh b/localnet/tee/scripts/deploy-tee-localnet.sh index 31345f765..9223e9e9a 100644 --- a/localnet/tee/scripts/deploy-tee-localnet.sh +++ b/localnet/tee/scripts/deploy-tee-localnet.sh @@ -135,7 +135,7 @@ VMM_RPC="${VMM_RPC:-http://127.0.0.1:10000}" # Repo-relative paths (assumes you're running from repo root) REPO_ROOT="$(pwd)" -TEE_LAUNCHER_DIR="$REPO_ROOT/tee_launcher" +TEE_LAUNCHER_DIR="$REPO_ROOT/deployment/cvm-deployment" COMPOSE_YAML="$TEE_LAUNCHER_DIR/launcher_docker_compose.yaml" ADD_DOMAIN_JSON="$REPO_ROOT/docs/localnet/args/add_domain.json" @@ -144,11 +144,25 @@ MODE="${MODE:-testnet}" # testnet|localnet # templates live here (UPDATED for move to localnet/tee/scripts) ENV_TPL="$REPO_ROOT/localnet/tee/scripts/node.env.tpl" if [ "$MODE" = "localnet" ]; then - CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.tpl" + CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.toml.tpl" else CONF_TPL="$REPO_ROOT/localnet/tee/scripts/node.conf.tpl" fi +# Convert comma-separated "host:container" port string to TOML inline table array entries. +# E.g. "8080:8080,24566:24566" -> " { host = 8080, container = 8080 },\n..." +ports_to_toml() { + local ports="$1" result="" + IFS=',' read -ra pairs <<< "$ports" + for pair in "${pairs[@]}"; do + local host_port="${pair%%:*}" + local container_port="${pair##*:}" + result+=" { host =$host_port, container =$container_port }, +" + done + echo -n "$result" +} + WORKDIR="/tmp/$USER/mpc_testnet_scale/$MPC_NETWORK_NAME" mkdir -p "$WORKDIR" @@ -278,9 +292,10 @@ phase_rank() { init_args) echo 75 ;; near_keys) echo 80 ;; near_init) echo 90 ;; - near_vote_hash) echo 95 ;; - near_vote_launcher_hash) echo 96 ;; - near_vote_domain) echo 97 ;; + near_vote_hash) echo 93 ;; + near_vote_launcher_hash) echo 94 ;; + near_vote_measurement) echo 95 ;; + near_vote_domain) echo 96 ;; near_vote_new_params) echo 98 ;; near_vote_new_params_votes) echo 99 ;; @@ -714,7 +729,7 @@ render_node_files_range() { local env_out conf_out env_out="$WORKDIR/node${i}.env" - conf_out="$WORKDIR/node${i}.conf" + conf_out="$WORKDIR/node${i}.toml" export APP_NAME="$app_name" export VMM_RPC @@ -750,6 +765,8 @@ render_node_files_range() { export MPC_SECRET_STORE_KEY="$(printf '%032x' "$i")" export MPC_CONTRACT_ID="$MPC_CONTRACT_ACCOUNT" export PORTS="8080:8080,24566:24566,${future_port}:${future_port}" + export PORTS_TOML + PORTS_TOML="$(ports_to_toml "$PORTS")" export NEAR_BOOT_NODES="ed25519:BGa4WiBj43Mr66f9Ehf6swKtR6wZmWuwCsV3s4PSR3nx@${MACHINE_IP}:24566" envsubst <"$ENV_TPL" >"$env_out" @@ -1112,6 +1129,47 @@ vote_add_launcher_hash_threshold() { done } +## Extract OS measurements from a tcb_info JSON file. +## Outputs a JSON object: {"mrtd":"","rtmr0":"","rtmr1":"","rtmr2":"","key_provider_event_digest":""} +extract_measurement_from_tcb_info() { + local tcb_info_file="$1" + if [ ! -f "$tcb_info_file" ]; then + err "tcb_info file not found: $tcb_info_file" + exit 1 + fi + local mrtd rtmr0 rtmr1 rtmr2 kp_digest + mrtd="$(jq -r '.mrtd' "$tcb_info_file")" + rtmr0="$(jq -r '.rtmr0' "$tcb_info_file")" + rtmr1="$(jq -r '.rtmr1' "$tcb_info_file")" + rtmr2="$(jq -r '.rtmr2' "$tcb_info_file")" + kp_digest="$(jq -r '.event_log[] | select(.event == "key-provider") | .digest' "$tcb_info_file")" + + if [ -z "$mrtd" ] || [ -z "$rtmr0" ] || [ -z "$rtmr1" ] || [ -z "$rtmr2" ] || [ -z "$kp_digest" ]; then + err "Could not extract all measurement fields from $tcb_info_file" + exit 1 + fi + + printf '{"mrtd":"%s","rtmr0":"%s","rtmr1":"%s","rtmr2":"%s","key_provider_event_digest":"%s"}' \ + "$mrtd" "$rtmr0" "$rtmr1" "$rtmr2" "$kp_digest" +} + +vote_add_os_measurement_threshold() { + local threshold="$1" + local measurement_json="$2" + log "Voting OS measurement with threshold=$threshold" + for i in $(seq 0 $((threshold-1))); do + local acct + acct="$(node_account_for_i "$i")" + log "vote_add_os_measurement as $acct" + near_tx_retry "vote_add_os_measurement by $acct" \ + near contract call-function as-transaction "$MPC_CONTRACT_ACCOUNT" vote_add_os_measurement \ + json-args "{\"measurement\": $measurement_json}" prepaid-gas '100.0 Tgas' \ + attached-deposit '0 NEAR' sign-as "$acct" \ + network-config "$NEAR_NETWORK_CONFIG" sign-with-keychain send + near_sleep "vote_add_os_measurement by $acct" + done +} + vote_add_domains() { @@ -1554,6 +1612,18 @@ main() { maybe_stop_after_phase near_vote_launcher_hash fi + if should_run_from_start near_vote_measurement; then + pause_phase "NEAR: vote add OS measurements" + local tcb_info_dir="$REPO_ROOT/crates/mpc-attestation/assets" + for tcb_file in "$tcb_info_dir"/tcb_info*.json; do + local measurement_json + measurement_json="$(extract_measurement_from_tcb_info "$tcb_file")" + log "Voting measurement from $(basename "$tcb_file"): $measurement_json" + vote_add_os_measurement_threshold "$threshold" "$measurement_json" + done + maybe_stop_after_phase near_vote_measurement + fi + if should_run_from_start near_vote_domain; then pause_phase "NEAR: vote add domain" vote_add_domains diff --git a/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md b/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md index 15b7b5bee..6bac7b983 100644 --- a/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md +++ b/localnet/tee/scripts/how-to-run-localnet-tee-setup-script.md @@ -17,6 +17,7 @@ The script automates the end‑to‑end setup of an MPC network on localnet: - Initializes the MPC contract - Votes for the MPC Docker image hash - Votes for the launcher image hash +- Votes for OS measurements (from compiled-in `tcb_info.json` files) - Votes to add signing domains (all nodes vote) - Leaves the network ready to process `sign` requests @@ -96,7 +97,7 @@ export FUNDER_PRIVATE_KEY=$(jq -r '.secret_key' ~/.near/mpc-localnet/validator_k ### Optional Control Variables ```bash -export START_FROM_PHASE=render|deploy|init_args|near_keys|near_init|near_vote_hash|near_vote_launcher_hash|near_vote_domain +export START_FROM_PHASE=render|deploy|init_args|near_keys|near_init|near_vote_hash|near_vote_launcher_hash|near_vote_measurement|near_vote_domain export STOP_AFTER_PHASE= export RESUME=1 export FORCE_REDEPLOY=1 diff --git a/localnet/tee/scripts/node.conf.localnet.toml.tpl b/localnet/tee/scripts/node.conf.localnet.toml.tpl new file mode 100644 index 000000000..292b628d2 --- /dev/null +++ b/localnet/tee/scripts/node.conf.localnet.toml.tpl @@ -0,0 +1,58 @@ +[launcher_config] +image_tags = ["${MPC_IMAGE_TAGS}"] +image_name = "${MPC_IMAGE_NAME}" +registry = "${MPC_REGISTRY}" +rpc_request_timeout_secs = 10 +rpc_request_interval_secs = 1 +rpc_max_attempts = 20 +port_mappings = [ +${PORTS_TOML}] + +[mpc_node_config] +home_dir = "/data" + +[mpc_node_config.log] +format = "plain" +filter = "info" + +[mpc_node_config.near_init] +chain_id = "mpc-localnet" +boot_nodes = "${NEAR_BOOT_NODES}" +genesis_path = "/app/localnet-genesis.json" +download_genesis = false + +[mpc_node_config.secrets] +secret_store_key_hex = "${MPC_SECRET_STORE_KEY}" +backup_encryption_key_hex = "0000000000000000000000000000000000000000000000000000000000000000" + +[mpc_node_config.node] +my_near_account_id = "${MPC_ACCOUNT_ID}" +near_responder_account_id = "${MPC_ACCOUNT_ID}" +number_of_responder_keys = 1 +web_ui = "0.0.0.0:8080" +migration_web_ui = "0.0.0.0:8078" +cores = 4 + +[mpc_node_config.node.indexer] +validate_genesis = false +sync_mode = "Latest" +concurrency = 1 +mpc_contract_id = "${MPC_CONTRACT_ID}" +finality = "optimistic" + +[mpc_node_config.node.triple] +concurrency = 2 +desired_triples_to_buffer = 128 +timeout_sec = 60 +parallel_triple_generation_stagger_time_sec = 1 + +[mpc_node_config.node.presignature] +concurrency = 4 +desired_presignatures_to_buffer = 64 +timeout_sec = 60 + +[mpc_node_config.node.signature] +timeout_sec = 60 + +[mpc_node_config.node.ckd] +timeout_sec = 60 diff --git a/localnet/tee/scripts/set-localnet-env.sh b/localnet/tee/scripts/set-localnet-env.sh new file mode 100644 index 000000000..176098bab --- /dev/null +++ b/localnet/tee/scripts/set-localnet-env.sh @@ -0,0 +1,31 @@ +#!/usr/bin/env bash +# Source this file before running deploy-tee-localnet.sh: +# source localnet/tee/scripts/set-localnet-env.sh + +export HOST_PROFILE=alice +export MODE=localnet +export MPC_NETWORK_BASE_NAME=mpc-local +export REUSE_NETWORK_NAME=mpc-local +export N=2 + +export MACHINE_IP=51.68.219.1 +export BASE_PATH=/mnt/data/barak/dstack +export VMM_RPC=http://127.0.0.1:10000 + +export MPC_IMAGE_NAME=nearone/mpc-node +export MPC_IMAGE_TAGS=main-9515e18 +export MPC_REGISTRY=registry.hub.docker.com + +export NEAR_NETWORK_CONFIG=mpc-localnet +export NEAR_RPC_URL=http://127.0.0.1:3030 +export ACCOUNT_SUFFIX=.test.near + +export FUNDER_ACCOUNT=test.near +export FUNDER_PRIVATE_KEY="$(jq -r '.secret_key' ~/.near/mpc-localnet/validator_key.json)" + +export MAX_NODES_TO_FUND=2 + +export NEAR_TX_SLEEP_SEC=1 +export NEAR_RETRY_SLEEP_SEC=2 + +export RESUME=0 diff --git a/localnet/tee/scripts/single-node.sh b/localnet/tee/scripts/single-node.sh index 13b62d135..9227260d9 100755 --- a/localnet/tee/scripts/single-node.sh +++ b/localnet/tee/scripts/single-node.sh @@ -130,15 +130,28 @@ DISK="${DISK:-500G}" # Paths REPO_ROOT="${REPO_ROOT:-$(git rev-parse --show-toplevel 2>/dev/null || pwd)}" -TEE_LAUNCHER_DIR="$REPO_ROOT/tee_launcher" +TEE_LAUNCHER_DIR="$REPO_ROOT/deployment/cvm-deployment" ENV_TPL="${ENV_TPL:-$REPO_ROOT/localnet/tee/scripts/node.env.tpl}" -CONF_TPL="${CONF_TPL:-$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.tpl}" +CONF_TPL="${CONF_TPL:-$REPO_ROOT/localnet/tee/scripts/node.conf.localnet.toml.tpl}" + +# Convert comma-separated "host:container" port string to TOML inline table array entries. +ports_to_toml() { + local ports="$1" result="" + IFS=',' read -ra pairs <<< "$ports" + for pair in "${pairs[@]}"; do + local host_port="${pair%%:*}" + local container_port="${pair##*:}" + result+=" { host =$host_port, container =$container_port }, +" + done + echo -n "$result" +} WORKDIR="${WORKDIR:-$(mktemp -d /tmp/mpc_localnet_one_node.XXXXXX)}" mkdir -p "$WORKDIR" log "Work directory: $WORKDIR" ENV_OUT="$WORKDIR/node.env" -CONF_OUT="$WORKDIR/node.conf" +CONF_OUT="$WORKDIR/node.toml" PUBLIC_DATA_JSON_OUT="${PUBLIC_DATA_JSON_OUT:-$WORKDIR/public_data.json}" near_account_exists() { @@ -193,6 +206,8 @@ render_env_and_conf() { export MPC_CONTRACT_ID="$CONTRACT_ACCOUNT" export MPC_SECRET_STORE_KEY="${MPC_SECRET_STORE_KEY:-00000000000000000000000000000000}" export PORTS="${PORTS:-8080:8080,24566:24566,${FUTURE_PORT}:${FUTURE_PORT}}" + export PORTS_TOML + PORTS_TOML="$(ports_to_toml "$PORTS")" envsubst <"$ENV_TPL" >"$ENV_OUT" envsubst <"$CONF_TPL" >"$CONF_OUT"