Skip to content
1 change: 1 addition & 0 deletions capsules/object-provider/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions elastos/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions elastos/crates/elastos-server/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ tokio = { version = "1.0", features = ["full"] }
# Serialization
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
semver = "1.0"
wasmparser = "=0.236.1"

# Async trait support
Expand Down
71 changes: 45 additions & 26 deletions elastos/crates/elastos-server/src/carrier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1610,33 +1610,11 @@ async fn handle_file_stream(
if head_path.is_file() {
let content = tokio::fs::read(&head_path).await?;
let head: serde_json::Value = serde_json::from_slice(&content)?;
// Extract fields the client expects in flat format
let head_cid = head["payload"]["latest_release_cid"]
.as_str()
.unwrap_or_default();
let version = head["payload"]["version"].as_str().unwrap_or_default();
let channel = head["payload"]["channel"].as_str().unwrap_or("stable");
let signer_did = head["signer_did"].as_str().unwrap_or_default();
// Read release_cid from publish state if available
let release_cid = if let Ok(state_bytes) = tokio::fs::read(&state_path).await {
serde_json::from_slice::<serde_json::Value>(&state_bytes)
.ok()
.and_then(|s| s["last_release_cid"].as_str().map(|s| s.to_string()))
.unwrap_or_default()
} else {
String::new()
let publish_state = match tokio::fs::read(&state_path).await {
Ok(bytes) => serde_json::from_slice::<serde_json::Value>(&bytes).ok(),
Err(_) => None,
};

let response = serde_json::json!({
"ok": true,
"release": {
"head_cid": head_cid,
"release_cid": release_cid,
"version": version,
"channel": channel,
"signer_did": signer_did,
}
});
let response = release_head_announcement(&head, publish_state.as_ref());
send_json(send, &response).await?;
} else {
send_json(
Expand Down Expand Up @@ -1854,6 +1832,29 @@ async fn handle_file_stream(
Ok(())
}

fn release_head_announcement(
head: &serde_json::Value,
publish_state: Option<&serde_json::Value>,
) -> serde_json::Value {
// Both CIDs come from the publisher's completed publish receipt.
let head_cid = publish_state
.and_then(|state| state["last_head_cid"].as_str())
.unwrap_or_default();
let release_cid = publish_state
.and_then(|state| state["last_release_cid"].as_str())
.unwrap_or_default();
serde_json::json!({
"ok": true,
"release": {
"head_cid": head_cid,
"release_cid": release_cid,
"version": head["payload"]["version"].as_str().unwrap_or_default(),
"channel": head["payload"]["channel"].as_str().unwrap_or("stable"),
"signer_did": head["signer_did"].as_str().unwrap_or_default(),
}
})
}

async fn carrier_content_fetch_bytes(
registry: &ProviderRegistry,
cid: &str,
Expand Down Expand Up @@ -15316,6 +15317,24 @@ pub(crate) mod tests {
shutdown_test_carrier_node(node).await;
}

#[test]
fn release_announcement_uses_distinct_published_head_and_release_cids() {
let head = serde_json::json!({
"signer_did": "did:key:fixture",
"payload": {"version": "0.7.1", "channel": "stable", "latest_release_cid": "release-cid"}
});
let state = serde_json::json!({
"last_head_cid": "head-cid", "last_release_cid": "release-cid"
});
let announcement = release_head_announcement(&head, Some(&state));
assert_eq!(announcement["release"]["head_cid"], "head-cid");
assert_eq!(announcement["release"]["release_cid"], "release-cid");

let legacy = release_head_announcement(&head, None);
assert_eq!(legacy["release"]["head_cid"], "");
assert_eq!(legacy["release"]["version"], "0.7.1");
}

#[test]
fn test_cached_release_metadata_does_not_mark_trusted_source_runtime() {
let dir = tempfile::tempdir().unwrap();
Expand Down
74 changes: 29 additions & 45 deletions elastos/crates/elastos-server/src/operator_control.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
use std::fs::OpenOptions;
use std::io::Write as _;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::{Duration, SystemTime, UNIX_EPOCH};

Expand Down Expand Up @@ -451,34 +450,20 @@ pub async fn gather_local_update_check(data_dir: &Path) -> Result<OperatorUpdate
source.name
)
})?;
let release = client.release_head().await.with_context(|| {
format!(
"Carrier discovery from trusted source '{}' failed to return a release head.",
source.name
)
})?;
let release = release.ok_or_else(|| {
anyhow::anyhow!(
"Carrier discovery from trusted source '{}' returned no release head.",
source.name
)
})?;
let head_cid = release
.get("head_cid")
.and_then(|value| value.as_str())
.map(|value| value.to_string())
.filter(|value| !value.is_empty());
let head_bytes = client
.fetch_file("release-head.json")
let discovered = crate::update::discover_carrier_release_head(&client, &source)
.await
.with_context(|| {
format!(
"Carrier fetch of release-head.json from trusted source '{}' failed.",
"Carrier release discovery for trusted source '{}' failed",
source.name
)
})?;

let head = verify_release_envelope(&head_bytes, "elastos.release.head.v1", &primary_publisher)?;
let head_cid = discovered.head_cid;
let head = verify_release_envelope(
&discovered.bytes,
"elastos.release.head.v1",
&primary_publisher,
)?;
let latest_version = head["payload"]["version"]
.as_str()
.unwrap_or("unknown")
Expand All @@ -494,7 +479,17 @@ pub async fn gather_local_update_check(data_dir: &Path) -> Result<OperatorUpdate
channel,
current_version,
latest_version: latest_version.clone(),
update_available: latest_version != source.installed_version,
update_available: match crate::update::compare_release_versions(
&source.installed_version,
&latest_version,
)? {
std::cmp::Ordering::Greater => true,
std::cmp::Ordering::Equal => false,
std::cmp::Ordering::Less => anyhow::bail!(
"Signed release {latest_version} is older than installed release {}",
source.installed_version
),
},
discovery: "Carrier".to_string(),
working_gateway: None,
head_cid,
Expand Down Expand Up @@ -522,24 +517,10 @@ pub async fn apply_local_update(data_dir: &Path) -> Result<OperatorUpdateApply>
})?,
);

let platform = crate::update::detect_release_platform().to_string();
let fetch_counter = Arc::new(AtomicUsize::new(0));
let carrier_for_fetch = carrier_client.clone();
let fetch_fn: crate::update::FetchFn = Box::new(move |_cid, _gateways| {
let fetch_fn: crate::update::FetchFn = Box::new(move |cid, _gateways| {
let client = carrier_for_fetch.clone();
let counter = fetch_counter.clone();
let platform = platform.clone();
Box::pin(async move {
let n = counter.fetch_add(1, Ordering::SeqCst);
let path = match n {
0 => "release-head.json".to_string(),
1 => "release.json".to_string(),
2 => format!("elastos-{}", platform),
3 => format!("components-{}.json", platform),
_ => return Err(anyhow::anyhow!("unexpected operator update fetch #{}", n)),
};
client.fetch_file(&path).await
})
Box::pin(async move { client.fetch_content(&cid, None).await })
});

let try_p2p: crate::update::TryP2pFn = Box::new(|source, publisher_did| {
Expand Down Expand Up @@ -896,15 +877,18 @@ fn normalized_channel(source: &TrustedSource) -> String {
async fn try_operator_p2p_discovery(
source: &TrustedSource,
_publisher_did: &str,
) -> Option<String> {
let client = crate::carrier::CarrierClient::connect_trusted_source(
) -> Result<Option<crate::update::DiscoveredHead>> {
let Ok(client) = crate::carrier::CarrierClient::connect_trusted_source(
source,
OPERATOR_CONNECT_TIMEOUT_SECS + 5,
)
.await
.ok()?;
let release = client.release_head().await.ok()??;
release["head_cid"].as_str().map(|value| value.to_string())
else {
return Ok(None);
};
crate::update::discover_carrier_release_head(&client, source)
.await
.map(Some)
}

fn now_ts() -> u64 {
Expand Down
31 changes: 12 additions & 19 deletions elastos/crates/elastos-server/src/release_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -45,29 +45,16 @@ pub async fn run_update_command(
None
};

let platform = update::detect_release_platform().to_string();
let gateway_only_fetch = no_p2p;
let fetch_counter = Arc::new(std::sync::atomic::AtomicUsize::new(0));
let carrier_for_fetch = carrier_client.clone();
let fetch_fn: update::FetchFn = Box::new(move |cid, gateways| {
let client = carrier_for_fetch.clone();
let counter = fetch_counter.clone();
let platform = platform.clone();
let gateway_only_fetch = gateway_only_fetch;
Box::pin(async move {
let mut carrier_error: Option<anyhow::Error> = None;

if !gateway_only_fetch {
if let Some(client) = client.as_ref() {
let n = counter.fetch_add(1, std::sync::atomic::Ordering::SeqCst);
let path = match n {
0 => "release-head.json".to_string(),
1 => "release.json".to_string(),
2 => format!("elastos-{}", platform),
3 => format!("components-{}.json", platform),
_ => return Err(anyhow::anyhow!("unexpected fetch #{}", n)),
};
match client.fetch_file(&path).await {
match client.fetch_content(&cid, None).await {
Ok(bytes) => return Ok(bytes),
Err(err) => carrier_error = Some(err),
}
Expand Down Expand Up @@ -106,10 +93,16 @@ pub async fn run_update_command(
.await
}

async fn try_p2p_discovery(source: &TrustedSource, _publisher_did: &str) -> Option<String> {
let client = elastos_server::carrier::CarrierClient::connect_trusted_source(source, 15)
async fn try_p2p_discovery(
source: &TrustedSource,
_publisher_did: &str,
) -> anyhow::Result<Option<update::DiscoveredHead>> {
let Ok(client) =
elastos_server::carrier::CarrierClient::connect_trusted_source(source, 15).await
else {
return Ok(None);
};
update::discover_carrier_release_head(&client, source)
.await
.ok()?;
let release = client.release_head().await.ok()??;
release["head_cid"].as_str().map(|s| s.to_string())
.map(Some)
}
44 changes: 33 additions & 11 deletions elastos/crates/elastos-server/src/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2079,15 +2079,6 @@ pub(crate) fn install_signed_model_catalog(
Ok(())
}

pub fn write_installed_manifest_bytes(
data_dir: &Path,
manifest_bytes: &[u8],
platform: &str,
) -> anyhow::Result<Vec<String>> {
let manifest: ComponentsManifest = serde_json::from_slice(manifest_bytes)?;
write_installed_manifest(data_dir, &manifest, platform)
}

// ── List mode ───────────────────────────────────────────────────────

fn list_components(manifest: &ComponentsManifest, data_dir: &Path, platform: &str) {
Expand Down Expand Up @@ -2408,6 +2399,8 @@ fn required_release_artifact_checksum<'a>(
}
}

/// Refresh support assets after the updater installs the verified manifest bytes.
/// The publisher owns that manifest; setup's local stamping stays separate.
pub async fn refresh_installed_components_for_update(
data_dir: &Path,
old_components: Option<&[u8]>,
Expand Down Expand Up @@ -2527,7 +2520,6 @@ pub async fn refresh_installed_components_for_update(

refreshed.sort();
refreshed.dedup();
write_installed_manifest_bytes(data_dir, new_components, platform)?;
Ok(refreshed)
}

Expand Down Expand Up @@ -5688,6 +5680,26 @@ mod tests {
assert!(err.to_string().contains("missing checksum"));
}

#[tokio::test]
async fn update_refresh_preserves_manifest_bytes_without_asset_changes() {
let new_bytes = b"{ \n \"profiles\": {}, \"external\":{}, \"capsules\": {}, \"publisher_extension\":true }\n";
let old_bytes = br#"{"external":{},"capsules":{},"profiles":{}}"#;
for old in [None, Some(old_bytes.as_slice()), Some(new_bytes.as_slice())] {
let tmp = tempfile::tempdir().unwrap();
let path = tmp.path().join("components.json");
fs::write(&path, new_bytes).unwrap();
let expected_hash = sha2::Sha256::digest(new_bytes);
let refreshed =
refresh_installed_components_for_update(tmp.path(), old, new_bytes, "x86_64-linux")
.await
.unwrap();
assert!(refreshed.is_empty());
let installed = fs::read(&path).unwrap();
assert_eq!(sha2::Sha256::digest(&installed), expected_hash);
assert_eq!(installed, new_bytes);
}
}

#[tokio::test]
async fn test_refresh_installed_components_for_update_refreshes_changed_local_copy() {
let tmp = tempfile::tempdir().unwrap();
Expand Down Expand Up @@ -5736,17 +5748,27 @@ mod tests {
"profiles": {}
});

// The updater installs the publisher's bytes before refreshing assets.
let mut new_bytes = b" \n".to_vec();
new_bytes.extend(serde_json::to_vec(&new_manifest).unwrap());
new_bytes.extend(b"\n ");
let manifest_path = data_dir.join("components.json");
fs::write(&manifest_path, &new_bytes).unwrap();
let expected_hash = sha2::Sha256::digest(&new_bytes);
let refreshed = refresh_installed_components_for_update(
data_dir,
Some(&serde_json::to_vec(&old_manifest).unwrap()),
&serde_json::to_vec(&new_manifest).unwrap(),
&new_bytes,
"x86_64-linux",
)
.await
.unwrap();

assert_eq!(refreshed, vec!["localhost-provider".to_string()]);
assert_eq!(fs::read(&install_path).unwrap(), b"new-binary");
let installed = fs::read(&manifest_path).unwrap();
assert_eq!(sha2::Sha256::digest(&installed), expected_hash);
assert_eq!(installed, new_bytes);
}

#[tokio::test]
Expand Down
Loading
Loading