canic-core 0.110.36

Canic — a canister orchestration and management toolkit for the Internet Computer
Documentation
use super::*;
use crate::{
    model::replay::{OperationId, RecoveryReason, ReplayReceiptStatus},
    ops::cost_guard::CostGuardRequest,
    replay_policy::CostClass,
};

fn p(value: u8) -> Principal {
    Principal::from_slice(&[value])
}

fn pending(child: Principal, id: u8) -> ReplayReceiptRecord {
    ReplayReceiptRecord {
        schema_version: 1,
        command_kind: CHILD_FUNDING_COMMAND_KIND.into(),
        operation_id: [id; 32],
        actor: ReplayActor::direct_caller(child),
        payload_hash_schema_version: 1,
        payload_hash: [3; 32],
        status: ReplayReceiptStatus::ExternalEffectInFlight,
        created_at_ns: 1,
        updated_at_ns: 1,
        expires_at_ns: Some(2),
        response_schema_version: None,
        response_bytes: None,
        staged_response_schema_version: None,
        staged_response_bytes: None,
        cost_guard_settlement: None,
        effect: Some(ExternalEffectDescriptor::ManagementCall {
            canister: child,
            method: "deposit_cycles".into(),
        }),
    }
}

fn retain(record: ReplayReceiptRecord) {
    let command = CommandKind::new(&record.command_kind).unwrap();
    let key = ReplayReceiptOps::slot_key(&command, OperationId::from_bytes(record.operation_id));
    ReplayReceiptOps::upsert(key, record);
}

fn reset() {
    CostGuardOps::reset_for_tests();
    ReplayReceiptOps::reset_for_tests();
    CyclesFundingLedgerOps::reset_for_tests();
}

#[test]
fn child_usage_preserves_charges_and_pending_reservations_across_replay_expiry() {
    reset();
    let parent = p(1);
    let child = p(2);
    let permit = CostGuardOps::reserve(CostGuardRequest {
        cost_class: CostClass::ValueTransfer,
        command_kind: CommandKind::new(CHILD_FUNDING_COMMAND_KIND).unwrap(),
        quota_subject: child,
        payer: parent,
        now_secs: 10,
        quota_window_secs: 60,
        max_operations_per_window: 60,
        current_cycle_balance: 1_000,
        cycle_reservation_cycles: 30,
        min_cycles_after_reservation: 1,
    })
    .unwrap();
    let mut record = pending(child, 1);
    record.cost_guard_settlement = Some(permit.replay_settlement());
    retain(record.clone());
    CyclesFundingLedgerOps::record_child_grant(child, 130, 10);
    let before = snapshot(parent, child, 11_000_000_000).unwrap();
    assert_eq!(before.accounted_cycles.to_u128(), 130);
    assert_eq!(before.pending_operations, 1);
    assert_eq!(before.reserved_cycles, Some(30.into()));
    assert_eq!(
        snapshot(p(9), child, 11_000_000_000)
            .unwrap()
            .reserved_cycles,
        None
    );
    assert_eq!(
        snapshot(parent, child, 3_610_000_000_000)
            .unwrap()
            .reserved_cycles,
        None
    );
    assert_eq!(snapshot(parent, child, 11_000_000_000).unwrap(), before);
    CostGuardOps::complete(&permit, 12).unwrap();
    record.status = ReplayReceiptStatus::RecoveryRequired {
        reason: RecoveryReason::ResponseCommitFailed,
    };
    retain(record.clone());
    let recovery = snapshot(parent, child, 13_000_000_000).unwrap();
    assert_eq!(recovery.pending_operations, 1);
    assert_eq!(recovery.reserved_cycles, Some(0.into()));
    assert_eq!(recovery.accounted_cycles, before.accounted_cycles);
    record.status = ReplayReceiptStatus::Committed;
    retain(record);
    let completed = snapshot(parent, child, 14_000_000_000).unwrap();
    assert_eq!(completed.pending_operations, 0);
    assert_eq!(completed.reserved_cycles, Some(0.into()));
    assert_eq!(completed.accounted_cycles, before.accounted_cycles);
}

#[test]
fn child_usage_does_not_assume_missing_evidence_or_other_children_are_unused() {
    reset();
    retain(pending(p(2), 1));
    retain(pending(p(3), 2));
    let observed = snapshot(p(1), p(2), 10).unwrap();
    assert_eq!(observed.pending_operations, 1);
    assert_eq!(observed.reserved_cycles, None);
    assert_eq!(snapshot(p(1), p(4), 10).unwrap().pending_operations, 0);
    let bytes = candid::encode_one(&observed).unwrap();
    assert_eq!(
        candid::decode_one::<ChildFundingUsage>(&bytes).unwrap(),
        observed
    );
}