Skip to main content

canic_host/install_root/deployment_registration/
mod.rs

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