use super::{
RootCapability, RootContext, nonroot_cycles, nonroot_cycles::AuthorizedCyclesGrant, replay,
};
use crate::{
InternalError, InternalErrorOrigin,
cdk::types::Principal,
dto::error::Error,
dto::rpc::{
AcknowledgePlacementReceiptRequest, CreateCanisterRequest, CreateCanisterResponse,
RecycleCanisterRequest, Response, UpgradeCanisterRequest,
},
log,
log::Topic,
model::replay::{CommandKind, ExternalEffectDescriptor, OperationId, RecoveryReason},
ops::{
ic::IcOps,
replay::{
acknowledge_root_placement_receipt,
guard::{ReplayPending, secs_to_ns},
receipt::PlacementReceiptAcknowledgementDecision,
},
},
workflow::{
canister_lifecycle::{
CanisterLifecycleEvent, CanisterLifecycleWorkflow, CanisterUpgradeCostContext,
},
pool::PoolWorkflow,
replay::mark_recovery_required_after_failure,
rpc::{
RootCapabilityAuthority, RootCapabilityLifecycleExecutor,
RootComponentChildProvisionRequest,
},
},
};
pub(super) async fn execute_root_capability(
ctx: &RootContext,
pending: &ReplayPending,
capability: RootCapability,
authorized_cycles: Option<AuthorizedCyclesGrant>,
authority: &RootCapabilityAuthority,
lifecycle: &dyn RootCapabilityLifecycleExecutor,
) -> Result<Response, InternalError> {
let descriptor = capability.descriptor();
let capability_name = descriptor.name;
let result = match capability {
RootCapability::AcknowledgePlacementReceipt(_) => {
unreachable!("receipt acknowledgement bypasses replay execution")
}
RootCapability::AllocatePlacementChild(req) | RootCapability::ProvisionCanister(req) => {
execute_provision(
ctx,
pending,
&req,
descriptor.command_kind,
authority,
lifecycle,
)
.await
}
RootCapability::UpgradeCanister(req) => execute_upgrade(ctx, pending, &req).await,
RootCapability::RecycleCanister(req) => execute_recycle(pending, &req).await,
RootCapability::RequestCycles(req) => {
let response = if let Some(grant) = authorized_cycles {
nonroot_cycles::execute_authorized_request_cycles(ctx, pending, grant).await
} else if ctx.is_root_env {
nonroot_cycles::execute_root_request_cycles(ctx, pending, &req, authority).await
} else {
nonroot_cycles::execute_request_cycles(ctx, pending, &req).await
}?;
Ok(Response::Cycles(response))
}
};
if let Err(err) = &result {
log!(
Topic::Rpc,
Warn,
"execute_root_capability failed (capability={capability_name}, caller={}, subnet={}, now={}): {err}",
ctx.caller,
ctx.subnet_id,
ctx.now
);
}
result
}
pub(super) fn execute_placement_receipt_acknowledgement(
ctx: &RootContext,
req: &AcknowledgePlacementReceiptRequest,
) -> Result<Response, InternalError> {
let operation_id = OperationId::from_bytes(req.operation_id);
match acknowledge_root_placement_receipt(operation_id, ctx.caller)
.map_err(replay::map_replay_store_error)?
{
PlacementReceiptAcknowledgementDecision::Acknowledged
| PlacementReceiptAcknowledgementDecision::AlreadyAbsent => {}
PlacementReceiptAcknowledgementDecision::ActorMismatch => {
return Err(InternalError::public(Error::forbidden(format!(
"placement receipt {operation_id} is not owned by caller",
))));
}
PlacementReceiptAcknowledgementDecision::NotCommitted => {
return Err(InternalError::public(Error::conflict(format!(
"placement receipt {operation_id} is not committed",
))));
}
PlacementReceiptAcknowledgementDecision::NotPlacementEffect => {
return Err(InternalError::public(Error::conflict(format!(
"placement receipt {operation_id} does not contain a placement-child effect",
))));
}
}
let response = Response::AcknowledgePlacementReceipt;
Ok(response)
}
async fn execute_provision(
ctx: &RootContext,
pending: &ReplayPending,
req: &CreateCanisterRequest,
command_kind: &'static str,
authority: &RootCapabilityAuthority,
lifecycle: &dyn RootCapabilityLifecycleExecutor,
) -> Result<Response, InternalError> {
let parent_pid = resolve_provision_parent(authority)?;
mark_root_provision_external_effect(pending, ctx, req, parent_pid, command_kind)?;
let provision = component_child_provision_request(pending, req, authority)?;
let new_canister_pid = match lifecycle.provision_component_child(provision).await {
Ok(pid) => pid,
Err(err) => {
return Err(preserve_root_provision_recovery_required(
pending,
ctx,
req,
parent_pid,
err,
command_kind,
RecoveryReason::ComponentChildLifecycleInterrupted,
));
}
};
let response = Response::CreateCanister(CreateCanisterResponse { new_canister_pid });
if let Err(err) = replay::stage_response(pending, &response) {
return Err(preserve_root_provision_recovery_required(
pending,
ctx,
req,
parent_pid,
err,
command_kind,
RecoveryReason::ResponseCommitFailed,
));
}
Ok(response)
}
fn component_child_provision_request(
pending: &ReplayPending,
req: &CreateCanisterRequest,
authority: &RootCapabilityAuthority,
) -> Result<RootComponentChildProvisionRequest, InternalError> {
let component = authority.caller_component().ok_or_else(|| {
InternalError::public(Error::forbidden(
"Fleet Subnet Root cannot own an application Component Child operation",
))
})?;
let expected_registry = authority.caller_registry().cloned().ok_or_else(|| {
InternalError::invariant(
InternalErrorOrigin::Workflow,
"authorized Component caller has no protected Registry head",
)
})?;
Ok(RootComponentChildProvisionRequest {
operation_id: pending.receipt_token.receipt().operation_id.into_bytes(),
component,
expected_registry,
child_role: req.canister_role.clone(),
application_init_args: req.extra_arg.clone(),
})
}
fn resolve_provision_parent(
authority: &RootCapabilityAuthority,
) -> Result<crate::cdk::types::Principal, InternalError> {
authority.provision_parent_canister_id().ok_or_else(|| {
InternalError::invariant(
crate::InternalErrorOrigin::Workflow,
"authorized provision request has no protected parent authority",
)
})
}
fn root_provision_command_kind(command_kind: &'static str) -> CommandKind {
CommandKind::new(command_kind).expect("root provision command kind is a valid static label")
}
pub(super) fn mark_root_provision_external_effect(
pending: &ReplayPending,
ctx: &RootContext,
req: &CreateCanisterRequest,
parent_pid: Principal,
command_kind: &'static str,
) -> Result<(), InternalError> {
replay::mark_external_effect_in_flight(
pending,
ExternalEffectDescriptor::ManagementCreateCanister {
command_kind: root_provision_command_kind(command_kind),
},
)?;
log!(
Topic::Rpc,
Info,
"root provision replay effect marked effect=provision_canister command_kind={} caller={} role={} parent={}",
command_kind,
ctx.caller,
req.canister_role,
parent_pid
);
Ok(())
}
fn preserve_root_provision_recovery_required(
pending: &ReplayPending,
ctx: &RootContext,
req: &CreateCanisterRequest,
parent_pid: Principal,
err: InternalError,
command_kind: &'static str,
reason: RecoveryReason,
) -> InternalError {
let (error_class, error_origin) = err.log_fields();
let err = mark_recovery_required_after_failure(
&pending.receipt_token,
reason,
secs_to_ns(IcOps::now_secs()),
err,
"root provision replay recovery marker failed",
);
log!(
Topic::Rpc,
Error,
"root provision replay recovery required effect=provision_canister command_kind={} caller={} role={} parent={} error_class={} error_origin={}",
command_kind,
ctx.caller,
req.canister_role,
parent_pid,
error_class,
error_origin
);
err
}
async fn execute_upgrade(
ctx: &RootContext,
pending: &ReplayPending,
req: &UpgradeCanisterRequest,
) -> Result<Response, InternalError> {
let response = Response::UpgradeCanister;
replay::stage_response(pending, &response)?;
let event = CanisterLifecycleEvent::Upgrade {
cost_context: CanisterUpgradeCostContext {
quota_subject: ctx.caller,
payer: ctx.self_pid,
now_secs: ctx.now,
},
pid: req.canister_pid,
replay_pending: pending,
};
CanisterLifecycleWorkflow::apply(event).await?;
Ok(response)
}
async fn execute_recycle(
pending: &ReplayPending,
req: &RecycleCanisterRequest,
) -> Result<Response, InternalError> {
let response = Response::RecycleCanister;
replay::stage_response(pending, &response)?;
PoolWorkflow::pool_recycle_canister(req.canister_pid).await?;
Ok(response)
}