Skip to main content

canic_host/install_root/deployment_registration/
mod.rs

1use super::root_verification::{
2    RootVerificationReceiptInput, deployment_root_verification_state, file_sha256_hex,
3    root_verification_receipt_from_report, verified_root_state_transition,
4    write_verified_root_state_if_unchanged,
5};
6use super::state::{
7    INSTALL_STATE_SCHEMA_VERSION, InstallState, RootVerificationStatus,
8    deployment_install_state_path, read_deployment_install_state, validate_network_name,
9    validate_state_name, write_install_state,
10};
11use crate::deployment_truth::{
12    DeploymentCheckV1, DeploymentRootVerificationEvidenceStatusV1,
13    DeploymentRootVerificationReceiptV1, DeploymentRootVerificationRequestV1,
14    DeploymentRootVerificationSourceV1, DeploymentRootVerificationStateV1,
15    deployment_root_verification_report_from_check, validate_deployment_root_verification_report,
16};
17use crate::release_set::{
18    icp_root, resolve_artifact_root, root_release_set_manifest_path, workspace_root,
19};
20use canic_core::cdk::types::Principal;
21use std::path::{Path, PathBuf};
22
23///
24/// RegisterDeploymentStateOptions
25///
26
27#[derive(Clone, Debug, Eq, PartialEq)]
28pub struct RegisterDeploymentStateOptions {
29    pub deployment_name: String,
30    pub fleet_template: String,
31    pub root_canister_id: String,
32    pub network: String,
33    pub allow_unverified: bool,
34    pub icp_root: Option<PathBuf>,
35    pub workspace_root: Option<PathBuf>,
36}
37
38///
39/// VerifyDeploymentRootOptions
40///
41
42#[derive(Clone, Debug, Eq, PartialEq)]
43pub struct VerifyDeploymentRootOptions {
44    pub deployment_name: String,
45    pub network: String,
46    pub deployment_check: DeploymentCheckV1,
47    pub verified_at_unix_secs: Option<u64>,
48    pub icp_root: Option<PathBuf>,
49}
50
51/// Register minimal local deployment-target state for an existing root canister.
52///
53/// Registration is an explicit operator recovery path after the 0.46 hard cut.
54/// It does not migrate legacy fleet state, verify live inventory, copy receipts,
55/// or claim artifact/controller truth.
56pub fn register_deployment_state(
57    options: RegisterDeploymentStateOptions,
58) -> Result<PathBuf, Box<dyn std::error::Error>> {
59    validate_state_name(&options.deployment_name)?;
60    validate_state_name(&options.fleet_template)?;
61    validate_network_name(&options.network)?;
62    if !options.allow_unverified {
63        return Err(
64            "deployment registration requires explicit unverified-root acknowledgement; pass --allow-unverified"
65                .into(),
66        );
67    }
68    Principal::from_text(&options.root_canister_id).map_err(|err| {
69        format!(
70            "invalid root principal for deployment {}: {err}",
71            options.deployment_name
72        )
73    })?;
74
75    let workspace_root = match options.workspace_root {
76        Some(path) => path,
77        None => workspace_root()?,
78    };
79    let icp_root = match options.icp_root {
80        Some(path) => path,
81        None => icp_root()?,
82    };
83    let release_set_manifest_path =
84        registered_deployment_release_set_manifest_path(&icp_root, &options.network);
85    let timestamp = super::current_unix_secs()?;
86    let state = InstallState {
87        schema_version: INSTALL_STATE_SCHEMA_VERSION,
88        deployment_name: options.deployment_name,
89        fleet_template: options.fleet_template.clone(),
90        created_at_unix_secs: timestamp,
91        updated_at_unix_secs: timestamp,
92        network: options.network.clone(),
93        root_target: options.root_canister_id.clone(),
94        root_canister_id: options.root_canister_id,
95        root_verification: RootVerificationStatus::NotVerified,
96        root_build_target: "root".to_string(),
97        workspace_root: workspace_root.display().to_string(),
98        icp_root: icp_root.display().to_string(),
99        config_path: workspace_root
100            .join("fleets")
101            .join(&options.fleet_template)
102            .join("canic.toml")
103            .display()
104            .to_string(),
105        release_set_manifest_path: release_set_manifest_path.display().to_string(),
106    };
107
108    write_install_state(&icp_root, &options.network, &state)
109}
110
111/// Promote an explicitly registered deployment root from `not_verified` to
112/// `verified` using bound deployment-truth evidence.
113pub fn verify_registered_deployment_root(
114    options: VerifyDeploymentRootOptions,
115) -> Result<DeploymentRootVerificationReceiptV1, Box<dyn std::error::Error>> {
116    validate_state_name(&options.deployment_name)?;
117    validate_network_name(&options.network)?;
118    let verified_at_unix_secs = match options.verified_at_unix_secs {
119        Some(value) => value,
120        None => super::current_unix_secs()?,
121    };
122    let icp_root = match options.icp_root {
123        Some(path) => path,
124        None => icp_root()?,
125    };
126    let state_path =
127        deployment_install_state_path(&icp_root, &options.network, &options.deployment_name);
128    let state =
129        read_deployment_install_state(&icp_root, &options.network, &options.deployment_name)?
130            .ok_or_else(|| {
131                format!(
132                    "no local deployment state exists for {}; run canic deploy register first",
133                    options.deployment_name
134                )
135            })?;
136    let state_fleet_template = state.fleet_template.clone();
137    let state_root_canister_id = state.root_canister_id.clone();
138    let local_state_digest_before = file_sha256_hex(&state_path)?;
139    let previous_root_verification = deployment_root_verification_state(&state.root_verification);
140    let report =
141        deployment_root_verification_report_from_check(DeploymentRootVerificationRequestV1 {
142            report_id: format!(
143                "local:{}:{}:root-verification-report",
144                options.network, options.deployment_name
145            ),
146            requested_at: format!("unix:{verified_at_unix_secs}"),
147            deployment_name: options.deployment_name.clone(),
148            network: options.network.clone(),
149            expected_fleet_template: state.fleet_template.clone(),
150            expected_root_principal: state.root_canister_id.clone(),
151            current_root_verification: previous_root_verification,
152            source: DeploymentRootVerificationSourceV1::DeploymentTruthCheck,
153            deployment_check: options.deployment_check,
154        });
155    validate_deployment_root_verification_report(&report)?;
156    if report.evidence_status != DeploymentRootVerificationEvidenceStatusV1::EvidenceSatisfied {
157        return Err(format!(
158            "deployment root verification failed for {}: {} blocker(s)",
159            options.deployment_name,
160            report.blockers.len()
161        )
162        .into());
163    }
164    let state_transition = verified_root_state_transition(previous_root_verification);
165    let local_state_digest_after = match previous_root_verification {
166        DeploymentRootVerificationStateV1::NotVerified => {
167            let mut verified_state = state;
168            verified_state.root_verification = RootVerificationStatus::Verified;
169            verified_state.updated_at_unix_secs = verified_at_unix_secs;
170            write_verified_root_state_if_unchanged(
171                &icp_root,
172                &options.network,
173                &verified_state,
174                &local_state_digest_before,
175            )?
176        }
177        DeploymentRootVerificationStateV1::Verified => file_sha256_hex(&state_path)?,
178    };
179
180    root_verification_receipt_from_report(RootVerificationReceiptInput {
181        deployment_name: options.deployment_name,
182        network: options.network,
183        fleet_template: state_fleet_template,
184        root_principal: state_root_canister_id,
185        previous_root_verification,
186        state_transition,
187        report,
188        verified_at_unix_secs,
189        local_state_path: state_path.display().to_string(),
190        local_state_digest_before,
191        local_state_digest_after,
192    })
193}
194
195fn registered_deployment_release_set_manifest_path(icp_root: &Path, network: &str) -> PathBuf {
196    let artifact_root = resolve_artifact_root(icp_root, network)
197        .unwrap_or_else(|_| icp_root.join(".icp").join(network).join("canisters"));
198    root_release_set_manifest_path(&artifact_root)
199        .unwrap_or_else(|_| artifact_root.join("root").join("root.release-set.json"))
200}