ic-backup 0.3.6

Host-side snapshot backup and same-release recovery for Internet Computer canisters
Documentation
//! Pure actual consistency matching; native evidence never proves an active application fence.

use super::*;
use crate::{
    model::{
        consistency::{ConsistencyBoundary, ConsistencyRequirementRecord},
        operation_plan::{PlanContextRecord, PlanContextRequest},
    },
    test_support::{
        consistency::{fence, input, parameters},
        membership::{hash, plan},
    },
};

#[test]
fn admits_only_original_lane_and_requires_every_exact_target_stopped() {
    let plan = plan();
    for guarantee in [
        ConsistencyGuaranteeRecord::PerCanister,
        ConsistencyGuaranteeRecord::ApplicationCoordinated,
    ] {
        let requirement = ConsistencyRequirementRecord::new(&plan, guarantee);
        let request = ConsistencyRequest::new(&plan, &requirement, parameters(guarantee)).unwrap();
        let observation = ConsistencyObservation::new(input(&request)).unwrap();
        let view = validate(&request, &observation).unwrap();
        assert_eq!(view.request(), &request.digest());
        assert_eq!(view.requirement(), &requirement.digest());
        assert_eq!(view.targets()[0].stopped_and_drained, hash("ef"));
        assert_eq!(view.evidence(), &hash("34"));
        assert_eq!(view.remote_observations(), 1);
        assert_eq!(
            view.fence().map(|fence| &fence.identity),
            request.expected_fence().map(|fence| &fence.identity)
        );
        for state in [CaptureState::Running, CaptureState::Stopping] {
            let mut data = input(&request);
            data.targets[0].state = state;
            assert_eq!(
                validate(&request, &ConsistencyObservation::new(data).unwrap()).unwrap_err(),
                ConsistencyError::TargetNotStopped
            );
        }
        let mut data = input(&request);
        data.consistency = match guarantee {
            ConsistencyGuaranteeRecord::PerCanister => {
                ConsistencyEvidence::ApplicationCoordinated(Box::new(fence()))
            }
            ConsistencyGuaranteeRecord::ApplicationCoordinated => ConsistencyEvidence::PerCanister,
        };
        assert_eq!(
            validate(&request, &ConsistencyObservation::new(data).unwrap()).unwrap_err(),
            ConsistencyError::GuaranteeMismatch
        );
    }
}

#[test]
fn rejects_stale_boundary_challenge_or_fence_and_changed_actual_revision() {
    let plan = plan();
    let guarantee = ConsistencyGuaranteeRecord::ApplicationCoordinated;
    let requirement = ConsistencyRequirementRecord::new(&plan, guarantee);
    let request = ConsistencyRequest::new(&plan, &requirement, parameters(guarantee)).unwrap();
    let observation = ConsistencyObservation::new(input(&request)).unwrap();
    for variant in 0..4 {
        let mut params = parameters(guarantee);
        match variant {
            0 => params.challenge = hash("34"),
            1 => params.boundary = ConsistencyBoundary::AfterCapture,
            2 => params.expected_fence.as_mut().unwrap().identity = hash("90"),
            _ => params.expected_fence.as_mut().unwrap().membership_revision = hash("90"),
        }
        let changed = ConsistencyRequest::new(&plan, &requirement, params).unwrap();
        assert_eq!(
            validate(&changed, &observation).unwrap_err(),
            ConsistencyError::RequestMismatch
        );
    }
    let mut data = input(&request);
    let mut changed = fence();
    changed.identity = hash("90");
    data.consistency = ConsistencyEvidence::ApplicationCoordinated(Box::new(changed));
    assert_eq!(
        validate(&request, &ConsistencyObservation::new(data).unwrap()).unwrap_err(),
        ConsistencyError::FenceMismatch
    );
    for revision in [None, Some(hash("90"))] {
        let mut data = input(&request);
        data.membership_revision = revision;
        assert_eq!(
            validate(&request, &ConsistencyObservation::new(data).unwrap()).unwrap_err(),
            ConsistencyError::MembershipRevisionMismatch
        );
    }
    let mut data = input(&request);
    let mut inactive = fence();
    inactive.state = ApplicationFenceState::Inactive;
    data.consistency = ConsistencyEvidence::ApplicationCoordinated(Box::new(inactive));
    assert_eq!(
        validate(&request, &ConsistencyObservation::new(data).unwrap()).unwrap_err(),
        ConsistencyError::FenceNotActive
    );
    let mut data = input(&request);
    let mut changed = fence();
    changed.membership_revision = hash("90");
    data.membership_revision = Some(hash("90"));
    data.consistency = ConsistencyEvidence::ApplicationCoordinated(Box::new(changed));
    assert_eq!(
        validate(&request, &ConsistencyObservation::new(data).unwrap()).unwrap_err(),
        ConsistencyError::MembershipRevisionMismatch
    );
}

#[test]
fn rejects_actual_context_full_inventory_and_missing_or_extra_selected_targets() {
    let plan = plan();
    let requirement =
        ConsistencyRequirementRecord::new(&plan, ConsistencyGuaranteeRecord::PerCanister);
    let request =
        ConsistencyRequest::new(&plan, &requirement, parameters(requirement.guarantee())).unwrap();
    for field in ["network", "caller", "release"] {
        let mut data = input(&request);
        data.context = PlanContextRecord::new(&PlanContextRequest {
            network: if field == "network" {
                "90".repeat(32)
            } else {
                plan.context().network().into()
            },
            caller: if field == "caller" {
                "aaaaa-aa".into()
            } else {
                plan.context().caller().into()
            },
            release: if field == "release" {
                "90".repeat(32)
            } else {
                plan.context().release().into()
            },
        })
        .unwrap();
        assert_eq!(
            validate(&request, &ConsistencyObservation::new(data).unwrap()).unwrap_err(),
            ConsistencyError::ContextMismatch(field)
        );
    }
    let mut data = input(&request);
    let mut inventory = serde_json::to_value(&data.inventory).unwrap();
    inventory["targets"][0]["role"] = serde_json::json!("changed-unselected-parent");
    data.inventory = serde_json::from_value(inventory).unwrap();
    assert_eq!(
        validate(&request, &ConsistencyObservation::new(data).unwrap()).unwrap_err(),
        ConsistencyError::InventoryMismatch
    );
    for extra in [false, true] {
        let mut data = input(&request);
        let mut target = data.targets[0].clone();
        target.target = "aaaaa-aa".into();
        if extra {
            data.targets.push(target);
        } else {
            data.targets = vec![target];
        }
        assert_eq!(
            validate(&request, &ConsistencyObservation::new(data).unwrap()).unwrap_err(),
            ConsistencyError::SelectionMismatch
        );
    }
}

#[test]
fn zero_and_max_call_reporting_do_not_change_original_allowances() {
    let plan = plan();
    let requirement =
        ConsistencyRequirementRecord::new(&plan, ConsistencyGuaranteeRecord::PerCanister);
    for limit in [0, 1024] {
        let mut params = parameters(requirement.guarantee());
        params.max_remote_observations = limit;
        let request = ConsistencyRequest::new(&plan, &requirement, params).unwrap();
        let mut data = input(&request);
        data.remote_observations = limit;
        let observation = ConsistencyObservation::new(data.clone()).unwrap();
        assert_eq!(
            validate(&request, &observation)
                .unwrap()
                .remote_observations(),
            limit
        );
        data.remote_observations = limit + 1;
        assert_eq!(
            validate(&request, &ConsistencyObservation::new(data).unwrap()).unwrap_err(),
            ConsistencyError::ObservationLimitExceeded {
                limit,
                reported: limit + 1
            }
        );
        assert_eq!(
            plan.attempt_authority(7).unwrap().budget().observations(),
            1
        );
    }
}

#[test]
fn every_member_of_a_multi_target_selection_requires_stopped_evidence() {
    let mut value = serde_json::to_value(plan()).unwrap();
    value["selected_targets"] =
        serde_json::json!(["aaaaa-aa", crate::test_support::membership::APP]);
    value["operations"][0]["target"] = serde_json::json!("aaaaa-aa");
    let plan = serde_json::from_value(value).unwrap();
    for guarantee in [
        ConsistencyGuaranteeRecord::PerCanister,
        ConsistencyGuaranteeRecord::ApplicationCoordinated,
    ] {
        let requirement = ConsistencyRequirementRecord::new(&plan, guarantee);
        let request = ConsistencyRequest::new(&plan, &requirement, parameters(guarantee)).unwrap();
        let original = input(&request);
        assert_eq!(original.targets.len(), 2);
        assert_eq!(
            validate(
                &request,
                &ConsistencyObservation::new(original.clone()).unwrap()
            )
            .unwrap()
            .targets()
            .len(),
            2
        );
        for index in 0..original.targets.len() {
            let mut changed = original.clone();
            changed.targets[index].state = CaptureState::Stopping;
            assert_eq!(
                validate(&request, &ConsistencyObservation::new(changed).unwrap()).unwrap_err(),
                ConsistencyError::TargetNotStopped
            );
        }
    }
}