CellScript Package and Deployment Registry: Early Design Discussion

Discussion: Package Provenance and Deployment Identity for CellScript on CKB

CellScript 0.12 coming this or next week is moving toward what I would consider the first practically stable developer-facing release.

The goal of 0.12 is not just to make the bundled examples compile. It is to make CellScript contracts inspectable and safer to author: clearer runtime error codes, better CKB authoring surfaces, witness ABI documentation, mutate/replacement-output documentation, explicit collection boundaries, and stronger release gates, etc.

Before the package and deployment model becomes too baked into the toolchain, I would like to open an early discussion about registry design.

I currently do not think this should block 0.12 , or that we need to over-design the full ecosystem before real usage appears. But I do think package provenance and deployment identity are important enough to discuss early with the community.

The problem

For ordinary development, a package registry can look like crates.io or npm: resolve a package name and version, download source, build it, and use it.

For smart contracts, that is not enough.

A production CellScript dependency eventually needs to answer questions such as:

  • Which source package was used?
  • Which compiler version produced the artifact?
  • What schema and ABI commitments were used?
  • What constraints report was generated?
  • What exact RISC-V artifact was deployed?
  • Which CKB CellDep, OutPoint, data_hash, dep_type, lock/type identity, or type-id lineage corresponds to that artifact?
  • Can a wallet or builder verify that the package used in a transaction is the same one the developer intended?

In other words, a source package version is useful for development, but production use also needs deployment truth.

My current intuition

My current intuition is that CellScript may need a two-layer package model:

  1. An off-chain source registry for .cell packages, interface packages, schemas, docs, examples, tests, and reproducible build metadata.

  2. A compact on-chain or chain-indexed deployment registry for deployed artifact identity, code cell identity, schema/ABI commitments, type-id lineage, and CKB-specific deployment facts.

The Cell.lock file would bind these two layers cryptographically.

In short:

Source packages should be distributed off-chain, but production deployments should be verifiable against chain deployment identity.

Why not pure on-chain packages?

It is unlikely that publishing every CellScript source package directly to CKB is the right default.

Source archives, docs, examples, tests, schema manifests, and editor metadata are development artifacts, not consensus-critical state. Frequent package releases would create unnecessary permanent state churn, and CKB capacity costs make source-package storage especially unattractive.

The chain should probably record compact deployment facts and commitments, not replace the whole source distribution system.

Why not pure off-chain packages?

A pure off-chain registry also seems insufficient.

For production CKB contracts, builders and wallets need concrete deployment identity: CellDep, OutPoint, data_hash, dep_type, script/code hash checks, schema/ABI commitments, and ideally provenance back to the source package, compiler version, and constraints report.

A compromised or stale source registry should not be enough to trick a production builder into using the wrong deployed artifact.

Possible shape

A source-facing Cell.toml could describe the package, dependencies, target profile, and optional deployment sections.

A production Cell.lock could pin:

  • source hash
  • compiler version
  • metadata schema
  • schema hash
  • ABI hash
  • artifact hash
  • constraints hash
  • CKB CellDep
  • OutPoint
  • data_hash
  • dep_type
  • type-id or lineage information where applicable

The builder should not accept a package by name alone. It should verify that the resolved source package, build artifact, constraints report, and CKB deployment identity all match.

Package states: source-only vs deployment-bound

One important distinction is that a CellScript package can exist in at least two states:

  1. Source-only / undeployed package

    This is a normal development package. It may contain .cell source files, interfaces, schemas, docs, tests, examples, and reproducible build metadata. It can be imported, compiled, tested, audited, and used as a library dependency.

    However, it does not by itself claim any production deployment identity on CKB.

  2. Deployment-bound package

    This is a package version whose built artifact has been deployed, and whose deployment identity can be verified. For CKB, that means binding the package version to facts such as CellDep, OutPoint, data_hash, dep_type, script/code hash, schema/ABI commitments, constraints report, compiler version, and possibly type-id lineage.

    A deployment-bound package is what wallets and production builders should rely on when constructing real transactions.

In other words, a package can be useful before it is deployed, but it should not be treated as a production dependency until its deployment identity is bound and verifiable.

The same source package version may have zero, one, or many deployment bindings.

For example, [email protected] may start as a source-only package, later gain a CKB testnet deployment, then eventually a CKB mainnet deployment. These should be separate deployment records attached to the same source/package identity, not separate source packages.

So I think the model should distinguish:

  • package identity: namespace / name / version / source hash
  • build identity: compiler version / metadata schema / schema hash / ABI hash / artifact hash / constraints hash
  • deployment identity: chain / network / CellDep or code cell / OutPoint / data_hash / dep_type / type-id lineage where applicable

Questions for the CKB community

I would really appreciate thoughts on:

  1. Should CellScript eventually have its own source registry, or reuse/adapt an existing registry protocol with CellScript-specific metadata?

  2. What is the minimal useful CKB deployment record without wasting capacity?

  3. Should deployment records live under one global registry type script, namespace-specific type scripts, or mostly off-chain with chain-indexed commitments?

  4. Which fields should be considered essential for CKB deployment identity: OutPoint, data_hash, dep_type, type-id lineage, schema hash, ABI hash, constraints hash?

  5. How should wallets and transaction builders verify CellScript dependencies before constructing production transactions?

  6. Who should own namespaces and maintainer keys?

  7. Should reproducible build proofs or audit signatures be required before a package is considered production-ready?

  8. How should yanking, supersession, and maintainer rotation work?

Current position

My current position is:

CellScript packages should be distributed like development packages, but verified like smart-contract deployments.

The off-chain registry should optimize for source distribution and developer experience. CKB should record only compact, verifiable deployment truth where it is actually useful. The lockfile should bind the two.

I also do not think CellScript should remain merely a personal language project if it becomes useful infrastructure for the CKB ecosystem. I am happy to keep driving the early implementation, but package provenance, deployment identity, registry policy, builder verification, and production-readiness standards are ecosystem-level concerns.

That is why I would like to make this design surface public early, before too many assumptions become hard-coded into the toolchain. I would especially welcome feedback from CKB developers, wallet and builder maintainers, and anyone with strong views on capacity cost, package governance, or smart-contract supply chains.

3 Likes

Drawing from the Move/Sui experience, they normally separate source package metadata, dependency pinning, and publication metadata through Move.toml, Move.lock, and Published.toml.

CellScript likely woyld need a similar separation, but adapted to CKB’s ‘CellDep’/‘OutPoint’-based deployment model rather than Sui’s native package-object model.

1 Like

For anyone else who is not up to speed, this is CellScript context:

1 Like

Thanks for adding the context;). I forgot to link the original CellScript thread. This helps.

1 Like

CellScript Registry Phase 1: A Go-Style, GitHub-Based Package Registry for CKB Smart Contracts

Publishing and consuming smart contract libraries shouldn’t require standing up a custom API server, maintaining a specialized database, or paying on-chain storage costs for source code that only developers need. CellScript’s Phase 1 registry takes a deliberately minimalist approach: everything is Git, everything is on GitHub, and the chain only records what actually matters at runtime.

This post walks through the design, explains why we chose this model, and shows how to use it end to end.

The Core Idea: Convention Over Configuration

Cellscript registry has two tiers, both backed by Git repositories on GitHub — but the discovery tier is optional, not mandatory.

The first tier is a discovery index — a lightweight Git repo that maps namespace/name to a source repository URL. Think of it as a phone book with overrides. It only gets updated when a package’s source location doesn’t follow the standard convention.

The second tier is a per-package version index called registry.json, which lives inside each source repository right next to Cell.toml. When you run cellc publish, it computes a source hash, reads your build artifacts, and appends a new version entry to this file. Then you commit, tag, and push. That’s it. No PR to any external index, no API call, no server to maintain.

The key insight is the Go-style convention: if no explicit discovery entry exists, cellscript/amm automatically resolves to github.com/cellscript/amm. You don’t need to register anything. You just push your repo to the conventional location, and it works. The discovery index only exists for packages that break the convention — repos hosted elsewhere, or where the repo name doesn’t match the package name.

This is exactly how Go modules work. go get github.com/cellscript/amm doesn’t need any index — it just clones that URL. The discovery index is an equivalent of GONOSUMCHECK or a GOPRIVATE override: a way to handle exceptions, not a mandatory gate.

Why This Works for Smart Contracts

There’s a subtlety here that’s easy to miss. In a traditional package registry, the package is the unit of identity. You install [email protected], and that’s the end of the story. For smart contracts, the package is only the first layer.

CellScript uses what we call a three-layer identity model. A package exists in three distinct identity scopes, and each one answers a different question:

Package Identity answers “what source code was written?” It’s carried by Cell.toml and the registry index, verified at compile time. The key fields are namespace, name, version, and source_hash.

Build Identity answers “what did the compiler produce?” It’s carried by Cell.lock, verified at build time. The key fields are compiler_version, artifact_hash, metadata_hash, schema_hash, abi_hash, and constraints_hash.

Deployment Identity answers “which cell on which chain?” It’s carried by Deployed.toml, verified at runtime. The key fields are network, chain_id, tx_hash, output_index, code_hash, hash_type, data_hash, out_point, dep_type, type_id, and script_role.

Each layer is independently meaningful but cryptographically bound to the layers above and below through the lockfile. If someone tampers with the source code after publishing, the source_hash won’t match. If someone swaps the artifact, the artifact_hash won’t match. If someone points to the wrong on-chain cell, the data_hash won’t match the on-chain reality. The system fails closed.

This is why we can get away with a Git-based registry. The registry doesn’t need to be a trust anchor. The trust anchors are the cryptographic hashes, and they’re verified independently at each layer. The registry is just a discovery mechanism — a way to find the source code. Once you’ve found it, you verify it.

The Three Files

CellScript uses three files to separate concerns. This is inspired by Move/Sui’s Move.toml / Move.lock / Published.toml split, but adapted for CKB’s CellDep and OutPoint model instead of Sui’s native package-object model.

Cell.toml — Deployment Intents

Cell.toml is the source package declaration. It describes what the developer intends to deploy, not what was actually deployed. The key addition for the registry is the namespace field:

[package]
name = "amm_pool"
version = "1.2.0"
namespace = "cellscript"

[dependencies]
token = { version = "0.3.0", namespace = "cellscript" }

[build]
target_profile = "ckb"

Dependencies can be resolved from the registry (by namespace and version), from a local path, or from a git URL. Resolution priority is path > git > registry, which means you can always override a registry dependency with a local checkout for development without changing any configuration.

Cell.lock — Build Identity

Cell.lock is the cryptographic bind point between source and deployment. It records exact dependency versions, git revisions, source hashes, and build hashes. It’s self-sufficient for re-verification — the url and revision fields let you re-clone the exact source commit without re-querying the discovery index.

version = 1

[package]
name = "amm_pool"
version = "1.2.0"
namespace = "cellscript"
source_hash = "blake2b:0xabcd..."

[package.build]
compiler_version = "0.19.0"
target_profile = "ckb"
artifact_hash = "blake2b:0x1234..."

[dependencies.token]
version = "0.3.2"
namespace = "cellscript"
source = { registry = "cellscript/token", url = "https://github.com/cellscript/token", revision = "f7e8d9c0..." }
source_hash = "blake2b:0x2222..."

[deployment.ckb.aggron4]
status = "deployed"
record = "ckb-testnet:0xaaaa..."

This is analogous to go.sum — it pins exact versions with their hashes, making the build independently reproducible.

Deployed.toml — Deployment Facts

Deployed.toml records immutable deployment facts derived from the chain. It’s generated automatically after a deployment transaction is confirmed, and it must not be edited by hand.

version = 1

[package]
name = "amm_pool"
version = "1.2.0"
source_hash = "blake2b:0xabcd..."

[build]
compiler_version = "0.19.0"
artifact_hash = "blake2b:0x1234..."

[[deployments]]
network = "aggron4"
chain_id = "ckb-testnet"
script_role = "type"
tx_hash = "0xaaaa..."
output_index = 0
code_hash = "0xbbbb..."
hash_type = "data1"
dep_type = "code"
out_point = "0xaaaa...:0"
data_hash = "0xcccc..."
type_id = "0xdddd..."

The separation matters. Cell.toml says “I want hash_type = data1.” Deployed.toml says “the cell at 0xaaaa…:0 actually has hash_type = data1, and here’s the on-chain proof.” One is intent, the other is fact. Confusing the two leads to exactly the kind of supply-chain vulnerabilities that smart contract systems should avoid.

Tutorial: End to End

Let’s walk through the complete lifecycle of a package, from authoring to verified on-chain deployment.

Step 1: Create a Package

cellc init amm_pool --namespace cellscript

This generates a Cell.toml with namespace = "cellscript" and a starter source file. At this point, there’s no Cell.lock, no registry.json, no Deployed.toml. The package is purely local.

Step 2: Add Dependencies

Edit Cell.toml to add a registry dependency:

[dependencies]
token = { version = "0.3.0", namespace = "cellscript" }

When you build, the resolver kicks in:

image

The discovery index tells the resolver where to find the source. The registry.json inside the source repo provides version metadata. The source_hash in that metadata is verified against the actual source tree. If anything has been tampered with, the build fails.

Step 3: Publish

cellc publish

This computes a source hash from your current source tree, reads build artifacts for their hashes, and appends a new version entry to registry.json. Then you commit and push:

git add registry.json
git commit -m "publish v1.2.0"
git tag v1.2.0
git push --tags

Notice what didn’t happen: you didn’t open a PR against any discovery index, you didn’t call an API, and you didn’t upload anything to a server. The version metadata lives in your source repo. And since cellscript/amm_pool automatically resolves to github.com/cellscript/amm_pool via convention, you didn’t even need to register with the discovery index. The index is only for packages that break the convention — hosted on a different platform, or where the repo name doesn’t match the package name.

Step 4: Deploy to CKB

This is where the toolchain gets real. We don’t just push data to the chain — we go through a verified pipeline.

The build step produces a real RISC-V ELF binary. cellc ckb-hash computes the CKB Blake2b hash of that binary. cellc deploy-plan generates a deployment plan. cellc verify-deploy validates the plan. Then build_deploy_transaction() from the cellscript-ckb-adapter crate constructs a proper CKB transaction with TYPE_ID, occupied capacity calculation, and change output — all computed headlessly, without needing an RPC connection for the construction itself.

After the transaction is submitted and committed, Deployed.toml is generated from the locally-computed evidence plus the on-chain tx_hash. No on-chain re-derivation is needed for generation — the adapter already knows all the hash fields. Verification (a separate step) is where on-chain reads happen.

Step 5: Cross-Verify All Three Layers

After deployment, you can verify the full identity chain:

cellc package verify   # source_hash matches
cellc verify-artifact  # artifact_hash matches the real binary
cellc registry verify  # data_hash matches on-chain cell

Or programmatically, as my end-to-end tests do:

// Package Identity: source_hash
let computed = compute_source_hash(&pkg_dir).unwrap();
assert_eq!(computed, read_lock.package.source_hash.as_deref().unwrap());

// Build Identity: artifact_hash
let lock_artifact = read_lock.package_build.as_ref().unwrap().artifact_hash.as_ref().unwrap();
let deployed_artifact = read_deployed.build.as_ref().unwrap().artifact_hash.as_ref().unwrap();
assert_eq!(lock_artifact, deployed_artifact);

// Deployment Identity: on-chain data_hash
let on_chain_data_hash = live_cell["cell"]["data"]["hash"].as_str().unwrap();
let computed_data_hash = format!("0x{}", hex::encode(ckb_data_hash(&artifact_binary)));
assert_eq!(on_chain_data_hash, computed_data_hash);

The three assertions verify three different things: that the source hasn’t changed since publishing, that the build artifact matches what was compiled, and that the on-chain cell contains the exact binary that was deployed. Any break in this chain means something is wrong, and the system fails closed rather than silently accepting a mismatch.

Design Rationale: Why Git, Why GitHub, Why Now

A few design decisions deserve more explanation.

Why Git, not a custom API? Because Git already solves the problems we need solved: content-addressed storage, cryptographic integrity via commit hashes, offline caching via local clones, and a workflow that every developer already knows. Building a custom API server would solve the same problems but add operational burden, authentication complexity, and a single point of failure — all for a registry that currently serves a small ecosystem.

Why a two-tier model instead of a single monorepo index? Because publishing a new version should be a git push to your own repo, not a PR to someone else’s. A monorepo index (like crates.io’s index repo) requires every version publish to update a shared repository. That creates friction: CI conflicts, merge races, permission management. CellScript’s model lets version metadata travel with the source, like Go’s go.mod. And thanks to the Go-style convention fallback (github.com/<namespace>/<name>), the discovery index doesn’t even need to be updated when registering a new package — only when a package’s source location doesn’t follow the convention. For the majority of packages, the discovery index is never consulted at all.

Why GitHub specifically? We’re not locked into GitHub. The discovery index maps to source URLs, and those URLs can point to any Git host. But GitHub is where CKB ecosystem development already happens, and it provides free repository hosting, reliable availability, and a familiar workflow. If someone wants to self-host their source, they can — the discovery index just needs a URL that git clone can reach.

Why off-chain deployment records instead of on-chain? CKB capacity costs make on-chain source-package storage unattractive. A 5KB RISC-V ELF binary requires about 541 CKB of capacity just for the code cell. Storing version metadata, schema manifests, and ABI indices on-chain would multiply that cost for no consensus benefit — these are developer artifacts, not runtime state. The chain should record compact deployment facts (CellDep, OutPoint, data_hash), not replace the entire source distribution system.

What about the proxy? Phase 3 can add an optional caching layer like proxy.golang.org, but the Git-based path is the permanent canonical mechanism, not a temporary placeholder. A proxy would be a transparent cache for faster installs and availability guarantees, not a replacement. If the proxy is down, cellc install falls back to direct Git cloning.

The End-to-End Test Suite

We didn’t just design this — we tested it thoroughly. The test suite in tests/e2e_registry_devnet.rs covers 13 scenarios across three layers:

Offline Git registry (6 tests): Two-tier discovery, registry.json append/update idempotency, Go-style namespace isolation, version upgrade and yank, multi-package dependency chains, discovery index add/update flow.

Headless CKB deploy (5 tests): Deploy transaction construction with TYPE_ID, Deployed.toml three-layer identity, cell deps and multi-network records, fail-closed hash mismatch rejection, source hash cross-platform determinism.

Live devnet deploy (2 tests, #[ignore] by default): Real CKB devnet from ../ckb, cellc build producing actual RISC-V ELF artifacts, cellc deploy-plan and cellc verify-deploy, build_deploy_transaction() from the adapter, transaction submission and on-chain commitment, get_live_cell verification of data_hash against the real binary, and full cross-verification of all three identity layers.

The live devnet tests are the real proof. They don’t use fake artifacts or manual JSON construction. They go through the complete toolchain: cellc build → cellc ckb-hash → cellc deploy-plan → cellc verify-deploy → build_deploy_transaction() → submit to devnet → wait for commitment → verify on-chain → write Deployed.toml + Cell.lock → cross-verify source ↔ artifact ↔ deployment identities. Every hash is computed from real data, every verification is against real on-chain state.

What Comes Next

Phase 1 is deliberately minimal. We’re shipping the two-tier Git registry, the three-file separation, and the three-layer identity model. Here’s what we’re not shipping yet, and why:

On-chain type script index (Phase 2): An on-chain script that indexes deployments by code_hash or TYPE_ID. Useful for wallets and builders that want to discover deployments without reading off-chain files. But the CKB ecosystem hasn’t demonstrated demand for this yet, and the capacity costs are real. We’ll build it when it’s needed.

Registry proxy (Phase 3): A caching layer like proxy.golang.org for faster installs and guaranteed availability. The Git-based path always remains the primary resolution mechanism. The proxy is a transparent cache, not a replacement.

Audit signatures and publisher identity (Phase 2): Packages can currently carry optional audit report hashes and acceptance gate status. Requiring cryptographic signatures from auditors before marking a deployment as production-ready is a natural extension, but it needs a key management story first.

Yanking and supersession: The yanked flag is already in the registry.json schema. Phase 1 records it; Phase 2 enforces it at the resolver level.

The important thing is that none of these future additions require changing the fundamental architecture. Adding a proxy doesn’t change the discovery index schema. Adding on-chain indexing doesn’t change how Deployed.toml is generated. The two-tier Git model is the permanent canonical path, and everything else layers on top of it.


CellScript is a domain-specific language for Nervos CKB smart contracts. The registry implementation lives in src/package/registry.rs and the deployment adapter in crates/cellscript-ckb-adapter/. The full design document is at docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md.

2 Likes