ic-backup 0.63.1

Host-side snapshot backup and same-release recovery for Internet Computer canisters
Documentation
use super::*;
use crate::model::settlement::EffectAttribution;
use crate::tests::support::encode_fixture;
use crate::{
    model::{
        attempt_journal::ObservationReceiptRequest,
        ic_lifecycle_reply::IcLifecycleReply,
        ic_observation::{IcObservationRequest, IcObservationResponseInput},
    },
    tests::support::{
        ic_observation::{self, original_for_mutation as original},
        operation_plan::hash,
        response_fault::ObservationFault,
    },
};
use ic_platform::management_canister_types::CanisterStatusType as Status;

struct Settings {
    controllers: Vec<ic_platform::Principal>,
}
ic_platform::candid_record!(Settings {
    570_880_087 => controllers: Vec<ic_platform::Principal>,
});

struct WireStatus {
    status: Status,
    settings: Settings,
    marker: u64,
}
ic_platform::candid_record!(WireStatus {
    100_394_802 => status: Status,
    2_336_062_691 => settings: Settings,
    3_127_932_346 => marker: u64,
});

fn input(
    request: &IcObservationRequest<'_>,
    status: Status,
    marker: u64,
) -> IcObservationResponseInput {
    let mut input = ic_observation::input(request);
    input.reply = encode_fixture(WireStatus {
        status,
        settings: Settings {
            controllers: vec![ic_platform::Principal::anonymous()],
        },
        marker,
    })
    .unwrap();
    input
}
fn settlement(
    request: &IcObservationRequest<'_>,
    response: &IcObservationResponse,
) -> IcLifecycleSettlement {
    IcLifecycleSettlement {
        context: crate::model::settlement::SettlementContext {
            authority: request.authority().digest(),
            mutation_attempt: request.mutation_attempt(),
            observation_attempt: request.observation_attempt(),
            challenge: hash("56"),
            observation_evidence: response.input().evidence.clone(),
            evidence: hash("90"),
        },

        status: IcLifecycleReply::decode(request.payload(), &response.input().reply)
            .unwrap()
            .digest(),

        attribution: EffectAttribution::Unresolved {
            uncertainty: hash("78"),
        },
    }
}

#[test]
fn statuses_never_infer_original_outcomes_or_change_spending() {
    for method in [
        Method::StopCanister,
        Method::StartCanister,
        Method::LoadCanisterSnapshot,
    ] {
        let original = original(method, Method::CanisterStatus);
        let before = original.journal.clone();
        let request = original.request();
        for status in [Status::Stopped, Status::Running, Status::Stopping] {
            let response = IcObservationResponse::new(input(&request, status, 42)).unwrap();
            let mut claim = settlement(&request, &response);
            for (attribution, outcome) in [
                (
                    EffectAttribution::Applied {
                        attribution: hash("78"),
                    },
                    ObservationOutcomeRecord::Applied,
                ),
                (
                    EffectAttribution::NotApplied {
                        exclusion: hash("78"),
                    },
                    ObservationOutcomeRecord::NotApplied,
                ),
                (
                    EffectAttribution::Unresolved {
                        uncertainty: hash("78"),
                    },
                    ObservationOutcomeRecord::Uncertain,
                ),
            ] {
                claim.attribution = attribution;
                let view = validate_lifecycle_settlement(
                    &request,
                    &original.journal,
                    &response,
                    &hash("56"),
                    &claim,
                )
                .unwrap();
                assert_eq!(view.outcome(), outcome);
                assert!(std::ptr::eq(view.settlement(), &raw const claim));
                assert!(std::ptr::eq(
                    view.observation().response(),
                    &raw const response
                ));
                assert_eq!(original.journal, before);
            }
        }
    }
}

#[test]
fn capture_and_inventory_have_no_lifecycle_settlement_lane() {
    for (method, observation) in [
        (Method::TakeCanisterSnapshot, Method::CanisterStatus),
        (Method::StopCanister, Method::ListCanisterSnapshots),
        (Method::StartCanister, Method::ListCanisterSnapshots),
        (Method::LoadCanisterSnapshot, Method::ListCanisterSnapshots),
    ] {
        let original = original(method, observation);
        let request = original.request();
        let response = IcObservationResponse::new(ic_observation::input(&request)).unwrap();
        let claim = IcLifecycleSettlement {
            context: crate::model::settlement::SettlementContext {
                authority: request.authority().digest(),
                mutation_attempt: 1,
                observation_attempt: 2,
                challenge: hash("56"),
                observation_evidence: hash("34"),
                evidence: hash("90"),
            },

            status: hash("78"),

            attribution: EffectAttribution::Unresolved {
                uncertainty: hash("78"),
            },
        };
        assert!(matches!(
            validate_lifecycle_settlement(
                &request,
                &original.journal,
                &response,
                &hash("56"),
                &claim
            ),
            Err(IcLifecycleSettlementError::UnsupportedMethod)
        ));
    }
}

#[test]
fn stale_challenge_precedes_status_mismatch_and_exact_raw_evidence_remains_bound() {
    let original = original(Method::LoadCanisterSnapshot, Method::CanisterStatus);
    let request = original.request();
    let response = IcObservationResponse::new(input(&request, Status::Stopped, 42)).unwrap();
    let initial = settlement(&request, &response);
    let before = original.journal.clone();
    let mut stale = initial.clone();
    stale.context.challenge = hash("12");
    stale.status = hash("12");
    assert!(matches!(
        validate_lifecycle_settlement(&request, &original.journal, &response, &hash("56"), &stale),
        Err(IcLifecycleSettlementError::Identity(
            SettlementIdentityMismatch::Challenge
        ))
    ));
    // Equal projected status/controllers cannot substitute for different raw metadata.
    let changed = IcObservationResponse::new(input(&request, Status::Stopped, 43)).unwrap();
    assert!(matches!(
        validate_lifecycle_settlement(&request, &original.journal, &changed, &hash("56"), &initial),
        Err(IcLifecycleSettlementError::StatusMismatch)
    ));
    let mut fields = response.input().clone();
    fields.evidence = hash("12");
    let changed = IcObservationResponse::new(fields).unwrap();
    assert!(matches!(
        validate_lifecycle_settlement(&request, &original.journal, &changed, &hash("56"), &initial),
        Err(IcLifecycleSettlementError::ObservationEvidenceMismatch)
    ));
    assert_eq!(original.journal, before);
}

#[test]
fn existing_association_wire_and_current_journal_admission_remain_mandatory() {
    let original = original(Method::StopCanister, Method::CanisterStatus);
    let request = original.request();
    let response = IcObservationResponse::new(input(&request, Status::Stopped, 42)).unwrap();
    let claim = settlement(&request, &response);
    for fault in [
        ObservationFault::Authority,
        ObservationFault::Request,
        ObservationFault::Target,
        ObservationFault::BothAttempts,
        ObservationFault::Network,
    ] {
        let mut fields = response.input().clone();
        fault.apply(&mut fields, hash("12"), hash("12"));
        let changed = IcObservationResponse::new(fields).unwrap();
        let error = validate_lifecycle_settlement(
            &request,
            &original.journal,
            &changed,
            &hash("56"),
            &claim,
        )
        .unwrap_err();
        let IcLifecycleSettlementError::Observation(error) = error else {
            panic!("existing association denial");
        };
        fault.assert_error(&error);
    }
    let mut fields = response.input().clone();
    fields.reply = b"DIDL\0\0".to_vec();
    let changed = IcObservationResponse::new(fields).unwrap();
    assert!(matches!(
        validate_lifecycle_settlement(&request, &original.journal, &changed, &hash("56"), &claim,),
        Err(IcLifecycleSettlementError::Observation(
            IcObservationAssociationError::Status(_)
        ))
    ));
    let mut settled = original.journal.clone();
    settled
        .record_observation(ObservationReceiptRequest {
            attempt: 2,
            request: request.payload().digest(),
            outcome: ObservationOutcomeRecord::Uncertain,
            evidence: hash("90").clone(),
        })
        .unwrap();
    assert!(matches!(
        validate_lifecycle_settlement(&request, &settled, &response, &hash("56"), &claim),
        Err(IcLifecycleSettlementError::Observation(
            IcObservationAssociationError::Reservation(_)
        ))
    ));
    assert_eq!(settled.view().pending_mutation, Some(1));
    assert_eq!(
        settled.view().observations_used,
        original.journal.view().observations_used
    );
}