#![cfg(unix)]
mod support;
mod upload_data_observation;
mod upload_observation;
mod upload_provider;
use ic_backup::{
model::{
artifacts::ArtifactChecksumRecord,
download_journal::DownloadArtifactRequest,
ic_mutation::{IcMutationAcknowledgement, IcMutationAcknowledgementInput},
ic_snapshot_data::{IcSnapshotDataReply, IcSnapshotDataRequest},
ic_snapshot_metadata::{IcSnapshotMetadataReply, IcSnapshotMetadataRequest},
ic_snapshot_upload::{IcSnapshotUploadAttempt, IcSnapshotUploadReplyKind},
operation_plan::OperationPlanRecord,
},
ops::persistence::{
AttemptJournalGuard, BackupLayoutGuard, DownloadJournalGuard, MeasurementHistogram,
MeasurementSummary, create_operation_plan, read_operation_plan,
},
policy::ic_snapshot_upload::validate_acknowledgement,
ports::{ic_mutation::IcMutationProviderError, ic_snapshot_upload::IcSnapshotUploadProvider},
};
use ic_management_canister_types::{
ReadCanisterSnapshotDataResult, ReadCanisterSnapshotMetadataResult, SnapshotDataKind,
UploadCanisterSnapshotDataArgs, UploadCanisterSnapshotMetadataResult,
};
use serde_json::json;
use std::{fs, path::PathBuf};
const TARGET: &str = "renrk-eyaaa-aaaaa-aaada-cai";
const TOKEN: &str = "original-source-token";
const DESTINATION: &[u8] = &[21, 0, 255];
fn root(lane: &str) -> PathBuf {
let root = support::temp_root(&format!("ic-backup-public-upload-{lane}"));
fs::create_dir_all(root.join("artifacts")).unwrap();
root
}
fn plan(request: &str) -> OperationPlanRecord {
serde_json::from_value(json!({
"version":1,"context":{"network":"ab".repeat(32),"caller":"2vxsx-fae","release":"cd".repeat(32)},
"inventory":{"version":1,"targets":[{"canister_id":TARGET,"parent_canister_id":null,"role":null,"module_hash":null}]},
"selected_targets":[TARGET],"graph":{"version":1,"nodes":[{"operation_sequence":7,"depends_on":[]}]},
"operations":[{"operation_sequence":7,"target":TARGET,"request":request,"budget":{"mutations":1,"observations":1}}],
"budget":{"mutations":1,"observations":1}
})).unwrap()
}
#[test]
#[expect(
clippy::too_many_lines,
reason = "one public source/upload reopen journey checks unchanged retained evidence at every boundary"
)]
fn exact_source_new_destination_and_independent_pending_attempts_survive_reopen() {
let source_root = root("source");
let metadata_root = root("metadata");
let data_root = root("data");
let source_layout = BackupLayoutGuard::acquire(&source_root).unwrap();
let original = plan(&"ef".repeat(32));
create_operation_plan(&source_layout, &original).unwrap();
let cases: Vec<serde_json::Value> = serde_json::from_str(include_str!(
"../src/model/ic_snapshot_metadata/tests/golden.json"
))
.unwrap();
let fixture = cases
.iter()
.find(|case| case["name"] == "upload-source-absent")
.unwrap();
let bytes = fixture["reply_hex"]
.as_str()
.unwrap()
.as_bytes()
.as_chunks::<2>()
.0
.iter()
.map(|pair| u8::from_str_radix(std::str::from_utf8(pair).unwrap(), 16).unwrap())
.collect::<Vec<_>>();
let mut values: ReadCanisterSnapshotMetadataResult = candid::decode_one(&bytes).unwrap();
values.wasm_module_size = 3;
values.wasm_memory_size = 0;
values.stable_memory_size = 0;
values.wasm_chunk_store.clear();
let raw = candid::encode_one(values).unwrap();
let metadata_request = IcSnapshotMetadataRequest::new(TARGET, &[0, 255, 17]).unwrap();
let metadata = IcSnapshotMetadataReply::decode(&metadata_request, &raw).unwrap();
let mut downloads = DownloadJournalGuard::create(
&source_layout,
original.digest().hash(),
vec![DownloadArtifactRequest {
canister_id: TARGET.into(),
snapshot_id: TOKEN.into(),
snapshot_taken_at_timestamp: metadata.metadata().taken_at_timestamp,
snapshot_total_size_bytes: u64::MAX,
}],
)
.unwrap();
let writer = downloads
.stage_ic_snapshot_artifact(TOKEN, &metadata, &raw)
.unwrap();
let read = IcSnapshotDataRequest::new(
&metadata,
SnapshotDataKind::WasmModule { offset: 0, size: 3 },
)
.unwrap();
let reply = IcSnapshotDataReply::decode(
&read,
&candid::encode_one(ReadCanisterSnapshotDataResult { chunk: vec![42; 3] }).unwrap(),
)
.unwrap();
writer.append(&reply).unwrap().finish().unwrap();
let upload = downloads
.prepare_ic_snapshot_upload_metadata(&original, TOKEN, &metadata)
.unwrap();
let upload_plan = plan(upload.binding_digest().hash());
let upload_layout = BackupLayoutGuard::acquire(&metadata_root).unwrap();
create_operation_plan(&upload_layout, &upload_plan).unwrap();
source_layout
.retain_restore(
&metadata_root.join("operation-plan.json"),
upload_plan.digest().hash(),
)
.unwrap();
let source_plan_bytes = fs::read(source_root.join("operation-plan.json")).unwrap();
let source_journal_bytes = fs::read(downloads.path()).unwrap();
let references = fs::read(source_root.join("restore-references.json")).unwrap();
upload_observation::retain_original_inventory(&upload_layout, &upload);
let mut attempts =
AttemptJournalGuard::create(&upload_layout, upload_plan.attempt_authority(7).unwrap())
.unwrap();
attempts.reserve_mutation().unwrap();
let attempt_bytes = fs::read(attempts.path()).unwrap();
let retained_attempt = attempts.record().unwrap().clone();
let attempt =
IcSnapshotUploadAttempt::new(&upload_plan, 7, &retained_attempt, &upload).unwrap();
let mut metadata_provider =
upload_provider::RefusingProvider::new(IcMutationProviderError::Unavailable);
assert_eq!(
metadata_provider.submit_upload(&attempt).unwrap_err(),
IcMutationProviderError::Unavailable
);
assert_eq!(fs::read(attempts.path()).unwrap(), attempt_bytes);
let acknowledgement = IcMutationAcknowledgement::new(IcMutationAcknowledgementInput {
authority: attempt.authority().digest(),
mutation_attempt: 1,
context: upload_plan.context().clone(),
target: TARGET.into(),
reply: candid::encode_one(UploadCanisterSnapshotMetadataResult {
snapshot_id: DESTINATION.to_vec(),
})
.unwrap(),
evidence: ArtifactChecksumRecord::from_bytes(b"passive allocation declaration"),
})
.unwrap();
let view = validate_acknowledgement(&attempt, &retained_attempt, &acknowledgement).unwrap();
assert_eq!(
view.reply().kind(),
&IcSnapshotUploadReplyKind::Metadata {
snapshot_id: DESTINATION.to_vec()
}
);
assert_eq!(fs::read(attempts.path()).unwrap(), attempt_bytes);
assert!(!attempts.record().unwrap().view().applied);
assert!(attempts.reserve_mutation().is_err());
let data = downloads
.prepare_ic_snapshot_upload_data(
&original,
TOKEN,
&upload,
DESTINATION,
SnapshotDataKind::WasmModule { offset: 1, size: 2 },
)
.unwrap();
let args: UploadCanisterSnapshotDataArgs = candid::decode_one(data.arguments()).unwrap();
assert_eq!(args.chunk, [42; 2]);
assert_eq!(args.snapshot_id, DESTINATION);
assert!(IcSnapshotUploadAttempt::new(&upload_plan, 7, &retained_attempt, &data).is_err());
let data_plan = plan(data.binding_digest().hash());
let data_layout = BackupLayoutGuard::acquire(&data_root).unwrap();
create_operation_plan(&data_layout, &data_plan).unwrap();
let mut data_attempts =
AttemptJournalGuard::create(&data_layout, data_plan.attempt_authority(7).unwrap()).unwrap();
data_attempts.reserve_mutation().unwrap();
let data_attempt_bytes = fs::read(data_attempts.path()).unwrap();
let record = data_attempts.record().unwrap();
let attempt = IcSnapshotUploadAttempt::new(&data_plan, 7, record, &data).unwrap();
let mut data_provider =
upload_provider::RefusingProvider::new(IcMutationProviderError::Indeterminate);
assert_eq!(
data_provider.submit_upload(&attempt).unwrap_err(),
IcMutationProviderError::Indeterminate
);
assert_eq!(fs::read(data_attempts.path()).unwrap(), data_attempt_bytes);
let acknowledgement = IcMutationAcknowledgement::new(IcMutationAcknowledgementInput {
authority: attempt.authority().digest(),
mutation_attempt: 1,
context: data_plan.context().clone(),
target: TARGET.into(),
reply: b"DIDL\0\0".to_vec(),
evidence: ArtifactChecksumRecord::from_bytes(b"passive write declaration"),
})
.unwrap();
assert_eq!(
validate_acknowledgement(&attempt, record, &acknowledgement)
.unwrap()
.reply()
.kind(),
&IcSnapshotUploadReplyKind::DataAcknowledgement
);
assert_eq!(fs::read(data_attempts.path()).unwrap(), data_attempt_bytes);
assert_eq!(fs::read(downloads.path()).unwrap(), source_journal_bytes);
assert_eq!(
fs::read(source_root.join("operation-plan.json")).unwrap(),
source_plan_bytes
);
assert_eq!(
fs::read(source_root.join("restore-references.json")).unwrap(),
references
);
let payload_digest = data.binding_digest();
drop(data_attempts);
drop(data_layout);
drop(attempts);
drop(upload_layout);
drop(downloads);
drop(source_layout);
let source_layout = BackupLayoutGuard::acquire(&source_root).unwrap();
let retained = read_operation_plan(&source_layout, &original.digest()).unwrap();
let downloads = DownloadJournalGuard::open(&source_layout, retained.digest().hash()).unwrap();
assert_eq!(
downloads
.ic_snapshot_metrics()
.upload_metadata_success_ns()
.samples(),
0
);
assert_eq!(
downloads
.ic_snapshot_metrics()
.prepared_chunk_bytes()
.latest(),
None
);
let data_layout = BackupLayoutGuard::acquire(&data_root).unwrap();
let retained_data = read_operation_plan(&data_layout, &data_plan.digest()).unwrap();
let mut data_attempts =
AttemptJournalGuard::open(&data_layout, &retained_data.attempt_authority(7).unwrap())
.unwrap();
let upload = downloads
.prepare_ic_snapshot_upload_metadata(&retained, TOKEN, &metadata)
.unwrap();
let data = downloads
.prepare_ic_snapshot_upload_data(
&retained,
TOKEN,
&upload,
DESTINATION,
SnapshotDataKind::WasmModule { offset: 1, size: 2 },
)
.unwrap();
assert_eq!(data.binding_digest(), payload_digest);
let measurements = downloads.ic_snapshot_metrics();
let prepared: MeasurementSummary = measurements.prepared_chunk_bytes();
let canonical: ic_metrics::MeasurementSummary = prepared;
assert_eq!(canonical, measurements.prepared_chunk_bytes());
let histogram: MeasurementHistogram<4> = measurements.prepared_chunk_bytes_histogram();
let canonical_histogram: ic_metrics::MeasurementHistogram<4> = histogram;
assert_eq!(canonical_histogram.summary(), canonical);
assert_eq!(histogram.bucket_counts(), &[0, 1, 0, 0]);
assert_eq!(histogram.overflow(), 0);
assert_eq!(measurements.upload_metadata_success_ns().samples(), 1);
assert_eq!(measurements.upload_data_success_ns().samples(), 1);
assert_eq!(measurements.verification_success_ns().samples(), 3);
assert_eq!(measurements.prepared_chunk_bytes().total(), 2);
assert_eq!(
data_attempts.record().unwrap().view().pending_mutation,
Some(1)
);
assert!(data_attempts.reserve_mutation().is_err());
data_attempts = upload_data_observation::retained_readback(
&retained_data,
&data,
&data_layout,
data_attempts,
);
let before = fs::read(data_attempts.path()).unwrap();
assert!(
IcSnapshotUploadAttempt::new(&retained_data, 7, data_attempts.record().unwrap(), &data)
.is_err()
);
assert_eq!(fs::read(data_attempts.path()).unwrap(), before);
assert_eq!(
data_attempts.record().unwrap().view().pending_observation,
Some(2)
);
assert_eq!(
data_attempts.record().unwrap().view().mutations_remaining,
0
);
assert_eq!(
data_attempts
.record()
.unwrap()
.view()
.observations_remaining,
0
);
assert_eq!(
fs::read(source_root.join("restore-references.json")).unwrap(),
references
);
assert_eq!(fs::read(downloads.path()).unwrap(), source_journal_bytes);
let upload_layout = BackupLayoutGuard::acquire(&metadata_root).unwrap();
let retained_metadata = read_operation_plan(&upload_layout, &upload_plan.digest()).unwrap();
let attempts = AttemptJournalGuard::open(
&upload_layout,
&retained_metadata.attempt_authority(7).unwrap(),
)
.unwrap();
assert_eq!(fs::read(attempts.path()).unwrap(), attempt_bytes);
assert_eq!(attempts.record().unwrap().view().pending_mutation, Some(1));
assert_eq!(downloads.ic_snapshot_metrics(), measurements);
assert_eq!(metadata_provider.calls, 1);
assert_eq!(data_provider.calls, 1);
upload_observation::retained_inventory_observation(
&retained_metadata,
&upload,
&upload_layout,
attempts,
);
assert_eq!(fs::read(downloads.path()).unwrap(), source_journal_bytes);
assert_eq!(
fs::read(source_root.join("operation-plan.json")).unwrap(),
source_plan_bytes
);
assert_eq!(
fs::read(source_root.join("restore-references.json")).unwrap(),
references
);
assert_eq!(downloads.ic_snapshot_metrics(), measurements);
upload_data_observation::retained_settled_readback(
&retained_data,
&data,
&data_layout,
data_attempts,
);
assert_eq!(fs::read(downloads.path()).unwrap(), source_journal_bytes);
assert_eq!(
fs::read(source_root.join("restore-references.json")).unwrap(),
references
);
assert_eq!(downloads.ic_snapshot_metrics(), measurements);
}