Summary
CellScript currently propagates an environment name string to transitive dependencies without preserving or validating the selected CKB chain identity.
This creates two problems:
- a dependency can apply its own override named mainnet even if that label denotes a different chain_id or genesis_hash; and
- 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:
|
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"):
|
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:
|
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:
- Inherit by chain identity: locate exactly one dependency environment with matching chain_id and genesis_hash.
- Map by name explicitly: the depender names the dependency's environment, then the resolver verifies its chain identity matches the root policy.
- 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
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.
Summary
CellScript currently propagates an environment name string to transitive dependencies without preserving or validating the selected CKB chain identity.
This creates two problems:
Environment names are local aliases. They are not chain identities and must not be ambient across package boundaries.
Code evidence
CellScript/src/package/mod.rs
Lines 584 to 595 in 8ae6dc4
CellScript/src/package/mod.rs
Lines 1114 to 1155 in 8ae6dc4
CellScript/src/package/mod.rs
Lines 1335 to 1348 in 8ae6dc4
CellScript/src/package/mod.rs
Lines 2116 to 2124 in 8ae6dc4
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:
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:
For every dependency edge, choose one explicit policy:
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
Acceptance criteria
Non-goals