ic-backup 0.64.1

Host-side snapshot backup and same-release recovery for Internet Computer canisters
Documentation
//! Original allowance thresholds, canonical complete extents and exact learned binding.
use super::*;
use crate::tests::support::encode_fixture;
use crate::tests::support::ic_snapshot_metadata::values;
use crate::{
    model::ic_snapshot_metadata::{IcSnapshotMetadataReply, IcSnapshotMetadataRequest},
    tests::support::ic_snapshot_upload::{
        SOURCE_ID, TARGET, data_workflow, source_plan, unhex, upload_plan,
    },
};
use ic_platform::management_canister_types::{ChunkHash, ReadCanisterSnapshotMetadataResult};

fn with_values(
    values: ReadCanisterSnapshotMetadataResult,
    check: impl FnOnce(&IcSnapshotUploadRequest<'_>),
) {
    let source = source_plan();
    let request = IcSnapshotMetadataRequest::new(TARGET, SOURCE_ID).unwrap();
    let metadata =
        IcSnapshotMetadataReply::decode(&request, &encode_fixture(values).unwrap()).unwrap();
    let upload = IcSnapshotUploadRequest::metadata(
        &source,
        &metadata,
        &ArtifactChecksumRecord::from_bytes(b"original tree"),
    )
    .unwrap();
    check(&upload);
}
fn allocation<'a, 'b>(payload: &'a IcSnapshotUploadRequest<'b>) -> IcSnapshotUploadReply<'a, 'b> {
    IcSnapshotUploadReply::decode(
        payload,
        &unhex("4449444c026c01b6b897890f016d7b0100031500ff"),
    )
    .unwrap()
}
#[test]
fn complete_exact_extents_keep_stage_headroom_and_allocation_reply_binding() {
    let mut values = values("upload-source");
    values.wasm_module_size = 5;
    values.wasm_memory_size = 3;
    values.stable_memory_size = 0;
    values.wasm_chunk_store = vec![
        ChunkHash { hash: vec![1; 32] },
        ChunkHash { hash: vec![2; 32] },
    ];
    with_values(values, |metadata| {
        let workflow = data_workflow(metadata, 12);
        let reply = allocation(metadata);
        assert!(matches!(
            DataUploadPreparation::new(&data_workflow(metadata, 4), 7, &reply, 4),
            Err(SnapshotDataPlanningError::InsufficientAllowance {
                required: 5,
                original: 4
            })
        ));
        let preparation = DataUploadPreparation::new(&workflow, 7, &reply, 4).unwrap();
        let kinds = preparation.kinds();
        let expected = vec![
            SnapshotDataKind::WasmModule { offset: 0, size: 4 },
            SnapshotDataKind::WasmModule { offset: 4, size: 1 },
            SnapshotDataKind::WasmMemory { offset: 0, size: 3 },
            SnapshotDataKind::WasmChunk { hash: vec![1; 32] },
            SnapshotDataKind::WasmChunk { hash: vec![2; 32] },
        ];
        assert_eq!(
            encode_fixture(kinds).unwrap(),
            encode_fixture(expected).unwrap()
        );
        let bindings: Vec<_> = (0..kinds.len())
            .map(|i| ArtifactChecksumRecord::from_bytes(&i.to_le_bytes()))
            .collect();
        let data = preparation.finish(bindings.clone()).unwrap();
        assert_eq!(data.destination(), &[21, 0, 255]);
        let plan = data.plan().unwrap();
        assert_eq!(plan.budget().mutations(), 12);
        assert_eq!(plan.budget().observations(), 1);
        assert_eq!(plan.allocated_attempts().mutations, 5);
        assert_eq!(plan.allocated_attempts().observations, 0);
        for (i, binding) in bindings.iter().enumerate() {
            let id = u64::try_from(i).unwrap();
            assert_eq!(plan.operation(id).unwrap().request(), binding.hash());
            assert_eq!(
                plan.operation(id).unwrap().depends_on(),
                id.checked_sub(1).into_iter().collect::<Vec<_>>()
            );
        }
        let mut original = serde_json::to_value(upload_plan(metadata)).unwrap();
        original["operations"][0]["operation_sequence"] = serde_json::json!(0);
        original["operations"][0]["budget"]["observations"] = serde_json::json!(0);
        original["budget"]["observations"] = serde_json::json!(0);
        let original: OperationPlanRecord = serde_json::from_value(original).unwrap();
        let binding = ExecutionStageBindingRecord::new(&workflow, 0, &original, vec![]).unwrap();
        let predecessor = ExecutionStagePredecessorRecord::new(
            0,
            binding.digest(),
            ArtifactChecksumRecord::from_bytes(b"qualified settlement"),
            reply.digest(),
        );
        let bound = data.bind(&binding, &original, vec![predecessor]).unwrap();
        data.validate_binding(&bound).unwrap();
        assert!(data.bind(&binding, &original, vec![]).is_err());
        let wrong = ExecutionStagePredecessorRecord::new(
            0,
            binding.digest(),
            ArtifactChecksumRecord::from_bytes(b"qualified settlement"),
            ArtifactChecksumRecord::from_bytes(b"other reply"),
        );
        assert!(matches!(
            data.bind(&binding, &original, vec![wrong]),
            Err(SnapshotDataPlanningError::OriginalMismatch)
        ));
        assert!(
            DataUploadPreparation::new(&workflow, 7, &reply, 4)
                .unwrap()
                .finish(bindings[..4].to_vec())
                .is_err()
        );
    });
}
#[test]
fn unknown_stage_rejects_before_upload_planning() {
    with_values(values("upload-source"), |metadata| {
        assert!(matches!(
            DataUploadPreparation::new(&data_workflow(metadata, 2), 99, &allocation(metadata), 4),
            Err(SnapshotDataPlanningError::Plan(_))
        ));
    });
}
#[test]
fn empty_regions_finish_without_a_placeholder_plan() {
    let mut input = values("upload-source");
    input.wasm_module_size = 0;
    input.wasm_memory_size = 0;
    input.stable_memory_size = 0;
    input.wasm_chunk_store.clear();
    with_values(input, |metadata| {
        let reply = allocation(metadata);
        let workflow = data_workflow(metadata, 1);
        let plan = DataUploadPreparation::new(&workflow, 7, &reply, 4)
            .unwrap()
            .finish(vec![])
            .unwrap();
        assert!(plan.kinds().is_empty());
        assert!(plan.plan().is_none());
    });
}
#[test]
fn changed_original_context_and_data_acknowledgement_cannot_plan_allocation() {
    let mut values = values("upload-source");
    values.wasm_module_size = 8;
    values.wasm_memory_size = 8;
    values.stable_memory_size = 8;
    with_values(values, |metadata| {
        let workflow = data_workflow(metadata, 100);
        let mut caller_fields = serde_json::to_value(&workflow).unwrap();
        caller_fields["allocation"]["context"]["caller"] = serde_json::json!(TARGET);
        let another_caller = serde_json::from_value(caller_fields).unwrap();
        assert!(
            DataUploadPreparation::new(&another_caller, 7, &allocation(metadata), 1024 * 1024)
                .is_ok()
        );
        let mut fields = serde_json::to_value(&workflow).unwrap();
        fields["allocation"]["context"]["release"] = serde_json::json!("99".repeat(32));
        let changed = serde_json::from_value(fields).unwrap();
        assert!(matches!(
            DataUploadPreparation::new(&changed, 7, &allocation(metadata), 1024 * 1024),
            Err(SnapshotDataPlanningError::OriginalMismatch)
        ));
        let data = IcSnapshotUploadRequest::data(
            metadata,
            &[21, 0, 255],
            SnapshotDataKind::WasmModule { offset: 0, size: 1 },
            &[7],
        )
        .unwrap();
        let reply = IcSnapshotUploadReply::decode(
            &data,
            crate::model::ic_lifecycle_reply::EMPTY_CANDID_REPLY,
        )
        .unwrap();
        assert!(matches!(
            DataUploadPreparation::new(&workflow, 7, &reply, 1024 * 1024),
            Err(SnapshotDataPlanningError::OriginalMismatch)
        ));
    });
}