ic-backup 0.4.0

Host-side snapshot backup and same-release recovery for Internet Computer canisters
Documentation
use super::*;
use crate::{
    model::{
        artifacts::ArtifactChecksumRecord,
        ic_mutation::IcMutationAcknowledgementInput,
        ic_snapshot_metadata::{IcSnapshotMetadataReply, IcSnapshotMetadataRequest},
        ic_snapshot_upload::{IcSnapshotUploadReplyKind, IcSnapshotUploadRequest},
    },
    test_support::ic_snapshot_upload::{
        DESTINATION_ID, SOURCE_ID, TARGET, raw, source_plan, upload_plan,
    },
};
use ic_management_canister_types::{SnapshotDataKind, UploadCanisterSnapshotMetadataResult};

fn input(
    request: &IcSnapshotUploadAttempt<'_, '_>,
    reply: Vec<u8>,
) -> IcMutationAcknowledgementInput {
    IcMutationAcknowledgementInput {
        authority: request.authority().digest(),
        mutation_attempt: request.mutation_attempt(),
        context: request.plan().context().clone(),
        target: request.payload().target().into(),
        reply,
        evidence: ArtifactChecksumRecord::from_bytes(b"passive provider declaration"),
    }
}

#[test]
fn metadata_and_data_association_preserve_pending_spending_and_original_evidence() {
    let source_plan = source_plan();
    let metadata_request = IcSnapshotMetadataRequest::new(TARGET, SOURCE_ID).unwrap();
    let metadata =
        IcSnapshotMetadataReply::decode(&metadata_request, &raw("upload-source")).unwrap();
    let checksum = ArtifactChecksumRecord::from_bytes(b"declared tree");
    let upload = IcSnapshotUploadRequest::metadata(&source_plan, &metadata, &checksum).unwrap();
    let data = IcSnapshotUploadRequest::data(
        &upload,
        DESTINATION_ID,
        SnapshotDataKind::WasmModule { offset: 5, size: 3 },
        SOURCE_ID,
    )
    .unwrap();
    for payload in [&upload, &data] {
        let plan = upload_plan(payload);
        let mut journal = AttemptJournalRecord::new(plan.attempt_authority(7).unwrap());
        journal.reserve_mutation().unwrap();
        let before = journal.clone();
        let request = IcSnapshotUploadAttempt::new(&plan, 7, &journal, payload).unwrap();
        let bytes = if matches!(
            payload.kind(),
            crate::model::ic_snapshot_upload::IcSnapshotUploadKind::Metadata
        ) {
            candid::encode_one(UploadCanisterSnapshotMetadataResult {
                snapshot_id: DESTINATION_ID.to_vec(),
            })
            .unwrap()
        } else {
            b"DIDL\0\0".to_vec()
        };
        let acknowledgement = IcMutationAcknowledgement::new(input(&request, bytes)).unwrap();
        let view = validate_acknowledgement(&request, &journal, &acknowledgement).unwrap();
        assert_eq!(
            view.acknowledgement().input().authority,
            acknowledgement.input().authority
        );
        assert_eq!(
            view.acknowledgement().input().evidence,
            acknowledgement.input().evidence
        );
        assert_eq!(
            view.acknowledgement().input().reply,
            acknowledgement.input().reply
        );
        assert_eq!(
            view.reply().payload_checksum(),
            &ArtifactChecksumRecord::from_bytes(&acknowledgement.input().reply)
        );
        let expected = if matches!(
            payload.kind(),
            crate::model::ic_snapshot_upload::IcSnapshotUploadKind::Metadata
        ) {
            IcSnapshotUploadReplyKind::Metadata {
                snapshot_id: DESTINATION_ID.to_vec(),
            }
        } else {
            IcSnapshotUploadReplyKind::DataAcknowledgement
        };
        assert_eq!(view.reply().kind(), &expected);
        assert_eq!(journal, before);
        assert_eq!(journal.view().pending_mutation, Some(1));
        assert_eq!(journal.view().mutations_remaining, 0);
        assert!(!journal.view().applied);
        assert!(journal.reserve_mutation().is_err());
        assert_eq!(journal, before);
    }
}

#[test]
fn actual_claim_mismatches_invalid_wire_and_lost_recovery_reply_stay_pending() {
    let source_plan = source_plan();
    let metadata_request = IcSnapshotMetadataRequest::new(TARGET, SOURCE_ID).unwrap();
    let metadata =
        IcSnapshotMetadataReply::decode(&metadata_request, &raw("upload-source-absent")).unwrap();
    let upload = IcSnapshotUploadRequest::metadata(
        &source_plan,
        &metadata,
        &ArtifactChecksumRecord::from_bytes(b"tree"),
    )
    .unwrap();
    let plan = upload_plan(&upload);
    let mut journal = AttemptJournalRecord::new(plan.attempt_authority(7).unwrap());
    journal.reserve_mutation().unwrap();
    let request = IcSnapshotUploadAttempt::new(&plan, 7, &journal, &upload).unwrap();
    let reply = candid::encode_one(UploadCanisterSnapshotMetadataResult {
        snapshot_id: DESTINATION_ID.to_vec(),
    })
    .unwrap();
    for change in 0..7 {
        let before = journal.clone();
        let mut fields = input(&request, reply.clone());
        match change {
            0 => fields.authority = ArtifactChecksumRecord::from_bytes(b"wrong"),
            1 => fields.mutation_attempt = 2,
            2..=4 => {
                let mut context = serde_json::to_value(&fields.context).unwrap();
                let (field, value) = match change {
                    2 => ("network", "12".repeat(32)),
                    3 => ("caller", "aaaaa-aa".into()),
                    _ => ("release", "12".repeat(32)),
                };
                context[field] = serde_json::json!(value);
                fields.context = serde_json::from_value(context).unwrap();
            }
            5 => fields.target = "aaaaa-aa".into(),
            _ => fields.reply = b"DIDL\0\0".to_vec(),
        }
        let acknowledgement = IcMutationAcknowledgement::new(fields).unwrap();
        let error = validate_acknowledgement(&request, &journal, &acknowledgement).unwrap_err();
        assert!(match change {
            0 => matches!(error, IcSnapshotUploadAssociationError::AuthorityMismatch),
            1 => matches!(error, IcSnapshotUploadAssociationError::AttemptMismatch),
            2..=4 => matches!(error, IcSnapshotUploadAssociationError::ContextMismatch),
            5 => matches!(error, IcSnapshotUploadAssociationError::TargetMismatch),
            _ => matches!(error, IcSnapshotUploadAssociationError::Reply(_)),
        });
        assert_eq!(journal, before);
    }
    let acknowledgement = IcMutationAcknowledgement::new(input(&request, reply)).unwrap();
    journal
        .reserve_observation(
            1,
            ArtifactChecksumRecord::from_bytes(b"original observation").hash(),
        )
        .unwrap();
    let before = journal.clone();
    assert!(matches!(
        validate_acknowledgement(&request, &journal, &acknowledgement),
        Err(IcSnapshotUploadAssociationError::Reservation(
            IcSnapshotUploadAttemptError::ObservationPending
        ))
    ));
    assert_eq!(journal, before);
    assert_eq!(journal.view().pending_observation, Some(2));
    assert_eq!(journal.view().observations_remaining, 0);
}