ic-backup 0.63.2

Host-side snapshot backup and same-release recovery for Internet Computer canisters
Documentation
use super::*;
use crate::model::attempt_journal::{ObservationOutcomeRecord, ObservationReservationError};
use crate::tests::support::encode_fixture;
use crate::tests::support::ic_observation::snapshot;
use crate::tests::support::ic_snapshot_upload::{SOURCE_ID, observation_input, with_observation};
use ic_platform::management_canister_types::Snapshot;

fn response(
    request: &IcSnapshotUploadObservationRequest<'_, '_>,
    snapshots: Vec<Snapshot>,
) -> IcObservationResponse {
    let mut input = observation_input(request);
    input.reply = encode_fixture(snapshots).unwrap();
    IcObservationResponse::new(input).unwrap()
}
fn claims(
    request: &IcSnapshotUploadObservationRequest<'_, '_>,
    baseline: &IcSnapshotReply<'_>,
    response: &IcObservationResponse,
    attribution: IcInventoryAttribution,
) -> IcInventorySettlement {
    IcInventorySettlement {
        context: crate::model::settlement::SettlementContext {
            authority: request.authority().digest(),
            mutation_attempt: 1,
            observation_attempt: 2,
            challenge: ArtifactChecksumRecord::from_bytes(b"fresh challenge"),
            observation_evidence: response.input().evidence.clone(),
            evidence: ArtifactChecksumRecord::from_bytes(b"synthetic local qualification only"),
        },

        baseline: baseline.digest(),
        inventory: IcSnapshotReply::decode(request.payload(), &response.input().reply)
            .unwrap()
            .digest(),

        attribution,
    }
}
fn applied(id: &[u8]) -> IcInventoryAttribution {
    IcInventoryAttribution::Applied {
        snapshot_id: id.into(),
        attribution: ArtifactChecksumRecord::from_bytes(b"exclusive allocation declaration"),
    }
}
fn unresolved() -> IcInventoryAttribution {
    IcInventoryAttribution::Unresolved {
        uncertainty: ArtifactChecksumRecord::from_bytes(
            b"settled authenticated uncertainty declaration",
        ),
    }
}

#[test]
fn explicit_attribution_selects_one_of_several_new_ids_and_retains_exact_originals() {
    with_observation(|plan, upload, list, journal| {
        let request =
            IcSnapshotUploadObservationRequest::new(plan, 7, journal, upload, list).unwrap();
        let bytes = encode_fixture(vec![snapshot(&[1])]).unwrap();
        let baseline = IcSnapshotReply::decode(list, &bytes).unwrap();
        let response = response(
            &request,
            vec![snapshot(&[3]), snapshot(&[1]), snapshot(&[2])],
        );
        let claims = claims(&request, &baseline, &response, applied(&[3]));
        let before = journal.digest();
        let view = validate_settlement(
            &request,
            journal,
            &baseline,
            &response,
            &claims.context.challenge,
            &claims,
        )
        .unwrap();
        assert_eq!(view.outcome(), ObservationOutcomeRecord::Applied);
        assert_eq!(view.snapshot().unwrap().id(), &[3]);
        assert_eq!(view.baseline().digest(), baseline.digest());
        assert_eq!(
            view.observation().response().input().evidence,
            claims.context.observation_evidence
        );
        assert!(std::ptr::eq(view.settlement(), &raw const claims));
        assert_eq!(journal.digest(), before);
        assert_eq!(journal.view().pending_mutation, Some(1));
        assert_eq!(journal.view().pending_observation, Some(2));
        let mut changed = claims.clone();
        changed.context.authority = ArtifactChecksumRecord::from_bytes(b"other original authority");
        assert!(matches!(
            validate_settlement(
                &request,
                journal,
                &baseline,
                &response,
                &claims.context.challenge,
                &changed
            ),
            Err(IcSnapshotUploadSettlementError::Identity(
                InventorySettlementMismatch::Authority
            ))
        ));
        assert_eq!(journal.digest(), before);
    });
}

#[test]
fn applied_requires_new_candidate_and_canonical_destination_bounds() {
    with_observation(|plan, upload, list, journal| {
        let request =
            IcSnapshotUploadObservationRequest::new(plan, 7, journal, upload, list).unwrap();
        let bytes = encode_fixture(vec![snapshot(&[1])]).unwrap();
        let baseline = IcSnapshotReply::decode(list, &bytes).unwrap();
        let response = response(
            &request,
            vec![
                snapshot(&[1]),
                snapshot(SOURCE_ID),
                snapshot(&[2]),
                snapshot(&[9; 256]),
            ],
        );
        // Closed-baseline admission precedes both changed authority and source-ID exclusion.
        let missing = self::response(&request, vec![snapshot(SOURCE_ID)]);
        let mut invalid = claims(&request, &baseline, &missing, applied(SOURCE_ID));
        invalid.context.authority = ArtifactChecksumRecord::from_bytes(b"wrong authority");
        assert!(matches!(
            validate_settlement(
                &request,
                journal,
                &baseline,
                &missing,
                &invalid.context.challenge,
                &invalid
            ),
            Err(IcSnapshotUploadSettlementError::Baseline(
                SnapshotInventoryDeltaError::LostBaseline
            ))
        ));
        for id in [vec![], vec![9; 257], SOURCE_ID.to_vec()] {
            let claims = claims(&request, &baseline, &response, applied(&id));
            assert!(matches!(
                validate_settlement(
                    &request,
                    journal,
                    &baseline,
                    &response,
                    &claims.context.challenge,
                    &claims
                ),
                Err(IcSnapshotUploadSettlementError::Destination(_))
            ));
        }
        for id in [&[1][..], &[8][..]] {
            let claims = claims(&request, &baseline, &response, applied(id));
            assert!(matches!(
                validate_settlement(
                    &request,
                    journal,
                    &baseline,
                    &response,
                    &claims.context.challenge,
                    &claims
                ),
                Err(IcSnapshotUploadSettlementError::NotNewCandidate)
            ));
        }
        let claims = claims(&request, &baseline, &response, applied(&[9; 256]));
        assert_eq!(
            validate_settlement(
                &request,
                journal,
                &baseline,
                &response,
                &claims.context.challenge,
                &claims
            )
            .unwrap()
            .snapshot()
            .unwrap()
            .id(),
            &[9; 256]
        );
    });
}

#[test]
fn malformed_or_lost_observation_cannot_become_settled_uncertainty() {
    with_observation(|plan, upload, list, journal| {
        let request =
            IcSnapshotUploadObservationRequest::new(plan, 7, journal, upload, list).unwrap();
        let bytes = encode_fixture(Vec::<Snapshot>::new()).unwrap();
        let baseline = IcSnapshotReply::decode(list, &bytes).unwrap();
        let valid = response(&request, vec![]);
        let claims = claims(&request, &baseline, &valid, unresolved());
        let before = journal.digest();
        let empty = AttemptJournalRecord::new(journal.authority().clone());
        assert!(matches!(
            validate_settlement(
                &request,
                &empty,
                &baseline,
                &valid,
                &claims.context.challenge,
                &claims
            ),
            Err(IcSnapshotUploadSettlementError::Observation(
                IcObservationAssociationError::Reservation(
                    ObservationReservationError::MutationMismatch
                )
            ))
        ));
        for raw in [
            vec![],
            b"lost reply".to_vec(),
            encode_fixture(vec![snapshot(&[1]), snapshot(&[1])]).unwrap(),
        ] {
            let mut input = observation_input(&request);
            input.reply = raw;
            let malformed = IcObservationResponse::new(input).unwrap();
            assert!(matches!(
                validate_settlement(
                    &request,
                    journal,
                    &baseline,
                    &malformed,
                    &claims.context.challenge,
                    &claims
                ),
                Err(IcSnapshotUploadSettlementError::Observation(_))
            ));
            assert_eq!(journal.digest(), before);
            assert_eq!(journal.view().pending_observation, Some(2));
        }
    });
}

#[test]
fn allocation_claim_diagnostics_redact_raw_identifiers() {
    let attribution = applied(b"secret-destination-identity");
    let debug = format!("{attribution:?}");
    assert!(debug.contains("snapshot_id_bytes: 27"));
    assert!(!debug.contains("secret-destination-identity"));
    assert!(!debug.contains("115, 101, 99"));
}