Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 51 additions & 5 deletions core/station/impl/src/services/cycle_manager.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,9 @@ use canfund::api::ledger::{CyclesLedgerCanister, IcLedgerCanister};
use canfund::manager::options::{FundManagerOptions, ObtainCyclesOptions};
use canfund::manager::record::CanisterRecord;
use canfund::manager::RegisterOpts;
use canfund::operations::obtain::{MintCycles, WithdrawFromCyclesLedger};
use canfund::operations::obtain::{
MintCycles, ObtainCycles, ObtainCyclesError, WithdrawFromCyclesLedger,
};
use canfund::FundManager;
use ic_cdk::api::management_canister::main::CanisterId;
use ic_cdk::print;
Expand Down Expand Up @@ -64,11 +66,36 @@ impl CycleManager {
manager.borrow_mut().register(canister_id, register_opts);
});

ic_cdk::print(format!(
crate::core::ic_cdk::api::print(format!(
"Cycle manager: canister {canister_id} added to cycle monitoring."
));
}

/// Registers an external canister for monitoring.
///
/// External canisters can only be funded from the station's own cycles balance. They are
/// registered with explicit obtain options that never obtain cycles, so canfund does not
/// fall back to the station's global obtain strategy (e.g. minting from a treasury account).
pub fn add_external_canister(
&self,
canister_id: CanisterId,
fund_strategy: MonitorExternalCanisterStrategy,
) {
let register_opts = RegisterOpts::new()
.with_strategy(fund_strategy.into())
.with_obtain_cycles_options(ObtainCyclesOptions {
obtain_cycles: Arc::new(NoObtainCycles),
});

FUND_MANAGER.with(|manager| {
manager.borrow_mut().register(canister_id, register_opts);
});

crate::core::ic_cdk::api::print(format!(
"Cycle manager: external canister {canister_id} added to cycle monitoring."
));
}

pub fn get_canister(&self, canister_id: &CanisterId) -> Option<CanisterRecord> {
FUND_MANAGER.with(|manager| manager.borrow().get_canisters().get(canister_id).cloned())
}
Expand All @@ -78,7 +105,7 @@ impl CycleManager {
manager.borrow_mut().unregister(canister_id);
});

ic_cdk::print(format!(
crate::core::ic_cdk::api::print(format!(
"Cycle manager: canister {canister_id} removed from cycle monitoring."
));
}
Expand All @@ -88,7 +115,7 @@ impl CycleManager {
manager.borrow_mut().start();
});

ic_cdk::print("Cycle manager: monitoring started.");
crate::core::ic_cdk::api::print("Cycle manager: monitoring started.");
}

pub fn set_global_cycle_obtain_strategy(&self, strategy: &CycleObtainStrategy) {
Expand All @@ -99,12 +126,31 @@ impl CycleManager {
fund_manager.with_options(options);
});

ic_cdk::print(format!(
crate::core::ic_cdk::api::print(format!(
"Cycle manager: obtain cycles strategy changed to {strategy:?}."
));
}
}

/// Obtain cycles operation that never obtains cycles.
struct NoObtainCycles;

#[async_trait::async_trait]
impl ObtainCycles for NoObtainCycles {
async fn obtain_cycles(
&self,
_amount: u128,
target_canister_id: Principal,
) -> Result<u128, ObtainCyclesError> {
Err(ObtainCyclesError {
details: format!(
"Obtaining cycles is not supported for external canister {target_canister_id}"
),
can_retry: false,
})
}
}

fn get_obtain_cycle_config(strategy: &CycleObtainStrategy) -> Option<ObtainCyclesOptions> {
match strategy {
CycleObtainStrategy::Disabled => None,
Expand Down
112 changes: 107 additions & 5 deletions core/station/impl/src/services/external_canister.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1038,10 +1038,9 @@ impl ExternalCanisterService {
pub fn canister_monitor_restart(&self) {
for canister in self.external_canister_repository.find_all() {
if let Some(monitoring) = &canister.monitoring {
self.cycle_manager.add_canister(
self.cycle_manager.add_external_canister(
canister.canister_id,
monitoring.funding_strategy.clone(),
monitoring.cycle_obtain_strategy,
);
Comment thread
aterga marked this conversation as resolved.
}
}
Expand All @@ -1053,10 +1052,13 @@ impl ExternalCanisterService {
strategy: MonitorExternalCanisterStrategy,
cycle_obtain_strategy: Option<CycleObtainStrategy>,
) -> ServiceResult<()> {
if let Some(CycleObtainStrategy::MintFromNativeToken { .. }) = cycle_obtain_strategy {
if !matches!(
cycle_obtain_strategy,
None | Some(CycleObtainStrategy::Disabled)
) {
Err(ExternalCanisterError::Failed {
reason: format!(
"Minting from native token is not yet supported for canister {}",
"Obtaining cycles is not supported for canister {}",
canister_id.to_text()
),
})?;
Expand All @@ -1073,7 +1075,7 @@ impl ExternalCanisterService {
}

self.cycle_manager
.add_canister(canister_id, strategy.clone(), cycle_obtain_strategy);
.add_external_canister(canister_id, strategy.clone());

external_canister.monitoring = Some(ExternalCanisterMonitoring {
funding_strategy: strategy,
Expand Down Expand Up @@ -2360,6 +2362,106 @@ mod tests {
})
);
}

#[test]
fn canister_monitor_start_rejects_cycle_obtain_strategies() {
setup();
let account = crate::models::account_test_utils::mock_account();
crate::repositories::ACCOUNT_REPOSITORY.insert(account.to_key(), account.clone());
let external_canister =
crate::models::external_canister_test_utils::mock_external_canister();
EXTERNAL_CANISTER_REPOSITORY.insert(external_canister.key(), external_canister.clone());

for cycle_obtain_strategy in [
CycleObtainStrategy::MintFromNativeToken {
account_id: account.id,
},
CycleObtainStrategy::WithdrawFromCyclesLedger {
account_id: account.id,
},
] {
let result = EXTERNAL_CANISTER_SERVICE.canister_monitor_start(
external_canister.canister_id,
MonitorExternalCanisterStrategy::Always(1_000_000_000_000),
Some(cycle_obtain_strategy),
);

assert!(result.is_err());
assert!(CYCLE_MANAGER
.get_canister(&external_canister.canister_id)
.is_none());
}

for cycle_obtain_strategy in [None, Some(CycleObtainStrategy::Disabled)] {
let external_canister =
crate::models::external_canister_test_utils::mock_external_canister();
EXTERNAL_CANISTER_REPOSITORY.insert(external_canister.key(), external_canister.clone());

EXTERNAL_CANISTER_SERVICE
.canister_monitor_start(
external_canister.canister_id,
MonitorExternalCanisterStrategy::Always(1_000_000_000_000),
cycle_obtain_strategy,
)
.expect("Failed to start monitoring the external canister");
}
}

#[tokio::test]
async fn monitored_external_canisters_never_obtain_cycles() {
setup();
let account = crate::models::account_test_utils::mock_account();
crate::repositories::ACCOUNT_REPOSITORY.insert(account.to_key(), account.clone());
CYCLE_MANAGER.set_global_cycle_obtain_strategy(&CycleObtainStrategy::MintFromNativeToken {
account_id: account.id,
});

// a monitor started through the service
let started = crate::models::external_canister_test_utils::mock_external_canister();
EXTERNAL_CANISTER_REPOSITORY.insert(started.key(), started.clone());
EXTERNAL_CANISTER_SERVICE
.canister_monitor_start(
started.canister_id,
MonitorExternalCanisterStrategy::Always(1_000_000_000_000),
None,
)
.expect("Failed to start monitoring the external canister");

// a monitor restored from storage with a per-canister obtain strategy
let mut restored = crate::models::external_canister_test_utils::mock_external_canister();
restored.monitoring = Some(ExternalCanisterMonitoring {
funding_strategy: MonitorExternalCanisterStrategy::Always(1_000_000_000_000),
cycle_obtain_strategy: Some(CycleObtainStrategy::WithdrawFromCyclesLedger {
account_id: account.id,
}),
});
EXTERNAL_CANISTER_REPOSITORY.insert(restored.key(), restored.clone());
EXTERNAL_CANISTER_SERVICE.canister_monitor_restart();

// changing the global strategy afterwards must not affect the external canisters
CYCLE_MANAGER.set_global_cycle_obtain_strategy(
&CycleObtainStrategy::WithdrawFromCyclesLedger {
account_id: account.id,
},
);

for canister_id in [started.canister_id, restored.canister_id] {
let record = CYCLE_MANAGER
.get_canister(&canister_id)
.expect("External canister is not monitored");
let obtain_cycles_options = record
.get_obtain_cycles_options()
.clone()
.expect("External canister has no obtain cycles options");

let error = obtain_cycles_options
.obtain_cycles
.obtain_cycles(1_000_000_000_000, canister_id)
.await
.expect_err("External canister must not obtain cycles");
assert!(!error.can_retry);
}
}
}

#[cfg(feature = "canbench")]
Expand Down
Loading
Loading