Skip to content

[Toolchain][CKB environment][P0] Make transitive dependency selection chain-identity-safe #17

Description

@a19q3

Summary

CellScript currently propagates an environment name string to transitive dependencies without preserving or validating the selected CKB chain identity.

This creates two problems:

  1. a dependency can apply its own override named mainnet even if that label denotes a different chain_id or genesis_hash; and
  2. a transitive external resolver can panic when the inherited environment label is not declared in the dependency's manifest.

Environment names are local aliases. They are not chain identities and must not be ambient across package boundaries.

Code evidence

  • Dependency child options copy only parent.environment, which is Option:
    fn dependency_resolution_options(dependency: &Dependency, parent: &ResolutionOptions) -> ResolutionOptions {
    let mut options = ResolutionOptions {
    scope: DependencyScope::Runtime,
    environment: parent.environment.clone(),
    offline: parent.offline,
    ..ResolutionOptions::default()
    };
    if let Dependency::Detailed(detail) = dependency {
    options.features.extend(detail.features.iter().cloned());
    options.no_default_features = !detail.default_features;
    }
    options
  • selected_dependencies requires the environment declaration only for root packages, but applies a matching dependency override for transitive packages as well:

    CellScript/src/package/mod.rs

    Lines 1114 to 1155 in 8ae6dc4

    fn selected_dependencies(
    &self,
    manifest: &PackageManifest,
    options: &ResolutionOptions,
    root: bool,
    ) -> Result<BTreeMap<String, Dependency>> {
    let mut dependencies: BTreeMap<String, Dependency> =
    manifest.dependencies.iter().map(|(name, dep)| (name.clone(), dep.clone())).collect();
    if options.scope == DependencyScope::Test && root {
    for (name, dep) in &manifest.dev_dependencies {
    if dependencies.insert(name.clone(), dep.clone()).is_some() {
    return Err(CompileError::without_span(format!(
    "dependency alias '{}' is declared in both [dependencies] and [dev_dependencies]",
    name
    )));
    }
    }
    }
    if let Some(environment) = options.environment.as_deref() {
    if root && !manifest.environments.contains_key(environment) {
    return Err(CompileError::without_span(format!(
    "unknown package environment '{}'; declare [environments.{}] with chain_id and genesis_hash",
    environment, environment
    )));
    }
    if let Some(overrides) = manifest.dependency_overrides.get(environment) {
    for (alias, dependency) in overrides {
    if !dependencies.contains_key(alias) {
    return Err(CompileError::without_span(format!(
    "environment '{}' overrides unknown dependency alias '{}'",
    environment, alias
    )));
    }
    dependencies.insert(alias.clone(), dependency.clone());
    }
    }
    } else if root && !manifest.dependency_overrides.is_empty() {
    return Err(CompileError::without_span(
    "Cell.toml declares environment-specific dependency overrides; select one explicitly with --environment",
    ));
    }
  • the external resolver request uses owner_manifest.environments.get(name).expect("selected environment was validated"):

    CellScript/src/package/mod.rs

    Lines 1335 to 1348 in 8ae6dc4

    resolver_name, expected_digest, actual_digest
    )));
    }
    let environment = options.environment.as_deref().map(|name| {
    let config = owner_manifest.environments.get(name).expect("selected environment was validated");
    ExternalResolverEnvironment { name, chain_id: &config.chain_id, genesis_hash: &config.genesis_hash }
    });
    let request = ExternalResolverRequest {
    schema: EXTERNAL_RESOLVER_REQUEST_SCHEMA,
    alias,
    package: package_name,
    version_requirement: &dependency.version,
    environment,
  • Cell.lock binds chain_id and genesis_hash only on the selected root environment table:

    CellScript/src/package/mod.rs

    Lines 2116 to 2124 in 8ae6dc4

    pub struct LockedEnvironment {
    pub chain_id: String,
    pub genesis_hash: String,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub dependencies: BTreeMap<String, String>,
    #[serde(default, skip_serializing_if = "BTreeMap::is_empty")]
    pub dev_dependencies: BTreeMap<String, String>,
    }

The expect is reachable for a transitive dependency that declares an external resolver but does not declare the inherited root environment name.

Threat model

Assume application A selects:

  • environment name mainnet;
  • chain_id ckb;
  • the CKB mainnet genesis hash.

Dependency B may independently use the name mainnet for another CKB fork, a private network, or malformed configuration. Applying B's mainnet override solely because the strings match can select different code, schemas, or deployment identities while the root lock still reports A's chain identity.

This is not merely a UX problem. Dependency selection can change verifier source and generated artifacts.

Reference model

Move package-alt treats dependency environment selection as an explicit edge mapping. Its lock entry records use_environment, described as the environment selected from the dependency's own manifest:

https://github.com/MystenLabs/sui/blob/5a9f37431c473fa2f6d49abecbcc6a6d7190f533/external-crates/move/crates/move-package-alt/src/schema/lockfile.rs#L35-L63

The manifest replacement carries an explicit use_environment field:

https://github.com/MystenLabs/sui/blob/5a9f37431c473fa2f6d49abecbcc6a6d7190f533/external-crates/move/crates/move-package-alt/src/schema/manifest.rs#L88-L101

CellScript additionally has CKB's concrete chain identity, so it should bind both the local name and chain_id/genesis_hash.

Proposed contract

Replace ambient Option propagation with a typed selected environment context:

  • root local name;
  • chain_id;
  • normalized 32-byte genesis_hash;
  • dependency-local environment mapping, if any;
  • source span/manifest edge that selected the mapping.

For every dependency edge, choose one explicit policy:

  1. Inherit by chain identity: locate exactly one dependency environment with matching chain_id and genesis_hash.
  2. Map by name explicitly: the depender names the dependency's environment, then the resolver verifies its chain identity matches the root policy.
  3. Environment-independent dependency: do not apply any dependency override.

Never inherit by equal display name alone.

The lockfile must record the selected dependency-local environment or environment-independent decision on each affected node/edge. Locked/frozen builds must validate the manifest digest and exact mapping without guessing.

All failed lookups must return a stable CompileError; resolver code must not use expect for user-controlled manifest relationships.

Acceptance matrix

Case Expected
root and dependency use different names for the same chain identity with explicit mapping pass
same environment name and same chain identity pass
same name but different genesis hash fail before dependency materialization
same name but different chain_id fail
dependency has no matching environment and is declared environment-independent pass without applying overrides
dependency has environment overrides but no explicit/inferred identity match fail
transitive external resolver lacks the inherited name stable diagnostic, never panic
frozen lock records a mapping no longer present in the dependency manifest fail
two dependency environments match the same chain identity fail as ambiguous

Acceptance criteria

  • No transitive environment decision is made from a label alone.
  • Selected chain identity is preserved through the complete graph.
  • Each transitive override has a recorded dependency-local environment mapping.
  • External resolver requests receive validated chain identity or no environment.
  • All user-controlled mismatches produce stable errors, not panics.
  • Cell.lock schema and consistency checks cover per-node/per-edge environment selection.
  • Tests include at least a three-package chain and a diamond with different local environment names.
  • Offline/frozen materialization validates the same mapping without network or resolver execution.

Non-goals

  • assuming mainnet and testnet have universal meanings;
  • allowing dependencies to change the root chain identity;
  • invoking external resolvers during locked builds;
  • copying Move's published-address semantics into CKB.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingenhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions