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:

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.