From 49a5f0222d6ddcf5006883be4ed0e5c496bcd77e Mon Sep 17 00:00:00 2001 From: Arthur Date: Mon, 17 Aug 2026 23:58:29 +0800 Subject: [PATCH] Fix 0.24 audit blockers --- AGENTS.md | 7 +- CHANGELOG.md | 12 +- Cargo.lock | 12 +- Cargo.toml | 4 +- README.md | 20 +- contracts/registry-type-script/Cargo.lock | 2 +- contracts/registry-type-script/Cargo.toml | 2 +- contracts/registry-type-script/README.md | 3 +- .../v0.23.0/cellscript-registry-type-script | Bin 0 -> 3352 bytes .../v0.24.0/cellscript-registry-type-script | Bin 0 -> 3352 bytes .../release-manifest.json | 10 +- .../registry-type-script/tests/ckb_vm.rs | 2 +- crates/cellscript-artifact-checker/Cargo.toml | 2 +- crates/cellscript-ckb-adapter/Cargo.toml | 2 +- crates/cellscript-fiber-adapter/Cargo.toml | 2 +- crates/cellscript-tools/Cargo.toml | 2 +- .../cellscript-tools/src/tooling_release.rs | 13 ++ crates/cellscript-wasm/Cargo.toml | 2 +- docs/CELLSCRIPT_CKB_ADAPTER.md | 2 +- docs/CELLSCRIPT_GATE_POLICY.md | 12 ++ docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md | 4 +- ...KAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md | 4 +- .../releases/CELLSCRIPT_0_24_RELEASE_NOTES.md | 20 +- .../Tutorial-12-Phase1-Registry-End-to-End.md | 4 +- docs/wiki/_Sidebar.md | 1 + editors/vscode-cellscript | 2 +- proposals/novaseal | 2 +- scripts/cellscript_ckb_stateful_scenarios.sh | 8 + scripts/cellscript_gate.sh | 55 ++++- services/registry-api/README.md | 6 +- services/registry-api/src/index.ts | 6 +- .../registry-api/test/registry-api.test.ts | 26 ++- services/registry-api/wrangler.example.toml | 2 +- .../registry-artifact-verifier/Cargo.lock | 6 +- .../registry-artifact-verifier/Cargo.toml | 2 +- services/registry-verifier/Cargo.lock | 6 +- services/registry-verifier/Cargo.toml | 2 +- src/lib.rs | 5 + src/package/mod.rs | 161 +++++++++++++- src/package/registry.rs | 203 +++++++++++++++--- 40 files changed, 536 insertions(+), 100 deletions(-) create mode 100755 contracts/registry-type-script/artifacts/v0.23.0/cellscript-registry-type-script create mode 100755 contracts/registry-type-script/artifacts/v0.24.0/cellscript-registry-type-script diff --git a/AGENTS.md b/AGENTS.md index 1b42d4f9..555f7d87 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -315,8 +315,11 @@ Existing command families to be aware of: - `proposals/novaseal` is a submodule (`NovaSeal.git`, branch `main`). Same for `proposals/evolving-dob/evolving-dob-profile-v1`. - `tools/ckb-tx-measure` depends on `../ckb/util/jsonrpc-types` and - `../ckb/util/types`; the gate builds the helper with CellScript's pinned - Rust 1.97.1 toolchain so its declared `rust-version` remains enforceable. + `../ckb/util/types`; when release validation receives `--ckb-repo`, the gate + stages the helper's tracked workspace under `target/` so those same relative + paths resolve to the explicit checkout. The gate builds the helper with + CellScript's pinned Rust 1.97.1 toolchain so its declared `rust-version` + remains enforceable. - `--primitive-strict 0.16` is the current production assurance gate; the README mentions it and the policy lives in `docs/`. diff --git a/CHANGELOG.md b/CHANGELOG.md index 9f945f50..05aab5a1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,16 @@ # Changelog -## Unreleased +## 0.24.0 - 2026-08-17 + +- Close the 0.24 audit blockers: reject transitive executable resolvers and + unsafe Git refs, bound and origin-bind Registry downloads, preserve exact + LS-IDL Type-script identity, and stop exposing unexpected service errors. + Restore a deliberate Clippy boundary for legacy explicit-context helpers, + align package identities with 0.24, and carry the closed 0.23 release and + NovaSeal evidence forward. +- Isolate every CKB-dependent gate check. The backend stateful wrapper accepts + `CELLSCRIPT_CKB_REPO`, and release modes propagate `--ckb-repo` to the staged + `ckb-tx-measure` workspace instead of silently using another sibling checkout. - Remove the unreachable external RISC-V toolchain fallback and make the audited internal assembler the sole ELF-emission path. Reassign `E2400` to diff --git a/Cargo.lock b/Cargo.lock index b92e72b7..1392a0df 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -314,7 +314,7 @@ dependencies = [ [[package]] name = "cellscript" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "base64 0.22.1", @@ -354,7 +354,7 @@ dependencies = [ [[package]] name = "cellscript-artifact-checker" -version = "0.22.0" +version = "0.24.0" dependencies = [ "blake2b_simd", "clap", @@ -365,7 +365,7 @@ dependencies = [ [[package]] name = "cellscript-ckb-adapter" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "ckb-hash", @@ -388,7 +388,7 @@ dependencies = [ [[package]] name = "cellscript-fiber-adapter" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "camino", @@ -408,7 +408,7 @@ dependencies = [ [[package]] name = "cellscript-tools" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "blake2b-ref", @@ -431,7 +431,7 @@ dependencies = [ [[package]] name = "cellscript-wasm" -version = "0.22.0" +version = "0.24.0" dependencies = [ "cellscript", "serde", diff --git a/Cargo.toml b/Cargo.toml index 124516cd..b3566a61 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,7 +21,7 @@ resolver = "3" [package] name = "cellscript" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" autobins = false @@ -82,7 +82,7 @@ blake2b_simd = "1.0" toml = "0.8" hex = "0.4" semver = "1.0" -cellscript-artifact-checker = { version = "=0.22.0", path = "crates/cellscript-artifact-checker" } +cellscript-artifact-checker = { version = "=0.24.0", path = "crates/cellscript-artifact-checker" } indexmap = "=2.2.6" diff --git a/README.md b/README.md index 08cfc346..1891ec9d 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@

[![CellScript CI](https://github.com/CellScript-Labs/CellScript/actions/workflows/ci.yml/badge.svg)](https://github.com/CellScript-Labs/CellScript/actions/workflows/ci.yml) -[![Release: v0.22.0](https://img.shields.io/badge/release-v0.22.0-2f6f4e.svg)](https://github.com/CellScript-Labs/CellScript/releases/tag/v0.22.0) +[![Release: v0.23.0](https://img.shields.io/badge/release-v0.23.0-2f6f4e.svg)](https://github.com/CellScript-Labs/CellScript/releases/tag/v0.23.0) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE-MIT) [![Rust 1.97.1](https://img.shields.io/badge/rust-1.97.1-orange.svg)](Cargo.toml) [![Targets: CKB](https://img.shields.io/badge/targets-CKB-2f6f4e.svg)](#target-profiles) @@ -20,8 +20,8 @@ artifacts, together with typed metadata for auditing, policy checks, schema binding, and scheduler-aware execution. The current stable release is -[CellScript v0.22.0](https://github.com/CellScript-Labs/CellScript/releases/tag/v0.22.0). -See the [0.22 release notes](docs/releases/CELLSCRIPT_0_22_RELEASE_NOTES.md) +[CellScript v0.23.0](https://github.com/CellScript-Labs/CellScript/releases/tag/v0.23.0). +See the [0.23 release notes](docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md) for its shipped surface, evidence boundaries, and migration checklist. The completed 0.23 implementation scope is tracked in the [0.23 release notes](docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md); those @@ -122,7 +122,7 @@ curl -fsSL https://raw.githubusercontent.com/CellScript-Labs/CellScript/main/scr Or pin a specific version: ```bash -CELLSCRIPT_VERSION=0.22.0 curl -fsSL https://raw.githubusercontent.com/CellScript-Labs/CellScript/main/scripts/install.sh | sh +CELLSCRIPT_VERSION=0.23.0 curl -fsSL https://raw.githubusercontent.com/CellScript-Labs/CellScript/main/scripts/install.sh | sh ``` The release page publishes `SHA256SUMS` alongside all four platform archives. @@ -130,7 +130,7 @@ The release page publishes `SHA256SUMS` alongside all four platform archives. Build the exact published source instead: ```bash -git clone --branch v0.22.0 --depth 1 https://github.com/CellScript-Labs/CellScript.git +git clone --branch v0.23.0 --depth 1 https://github.com/CellScript-Labs/CellScript.git cd CellScript cargo install --locked --path . ``` @@ -732,7 +732,7 @@ policy defaults: [package] edition = "2026" name = "token" -version = "0.22.0" +version = "0.24.0" entry = "src/main.cell" source_roots = ["src"] @@ -916,9 +916,11 @@ the manual, CI, recovery, and external-wallet path. - The default source-package authority is `https://api.registry.cellscript.dev`; only publicly accepted statuses enter ordinary version selection. `CELLSCRIPT_REGISTRY_API_URL` changes that API - origin, while `CELLSCRIPT_REGISTRY_URL` explicitly selects the legacy - Git/offline discovery authority. An unavailable production API does not - silently downgrade to Git discovery. + origin, and `CELLSCRIPT_REGISTRY_STATIC_ORIGIN` explicitly trusts a separate + immutable-object origin for a private Registry. Snapshot downloads are + origin-bound; a lockfile cannot redirect the client to an unrelated host. + `CELLSCRIPT_REGISTRY_URL` selects the legacy Git/offline discovery authority. + An unavailable production API does not silently downgrade to Git discovery. - Registry discovery may grow to include CellScript packages, verifier artifacts, deployed artifact records, reproducible artifacts, and external CKB tooling artifacts. Dependency resolution stays narrower than discovery. diff --git a/contracts/registry-type-script/Cargo.lock b/contracts/registry-type-script/Cargo.lock index 8e1964bd..2218782d 100644 --- a/contracts/registry-type-script/Cargo.lock +++ b/contracts/registry-type-script/Cargo.lock @@ -176,7 +176,7 @@ dependencies = [ [[package]] name = "cellscript-registry-type-script" -version = "0.22.0" +version = "0.24.0" dependencies = [ "ckb-hash", "ckb-std", diff --git a/contracts/registry-type-script/Cargo.toml b/contracts/registry-type-script/Cargo.toml index de75b24d..5be5777b 100644 --- a/contracts/registry-type-script/Cargo.toml +++ b/contracts/registry-type-script/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-registry-type-script" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" publish = false diff --git a/contracts/registry-type-script/README.md b/contracts/registry-type-script/README.md index 6c26ae72..0926bcaa 100644 --- a/contracts/registry-type-script/README.md +++ b/contracts/registry-type-script/README.md @@ -43,7 +43,8 @@ Reproduce the canonical Linux artifact with the pinned container digest: contracts/registry-type-script/build_canonical_container.sh ``` -The deployable artifact is tracked under `artifacts/v0.22.0` and was produced +The current deployable artifact is tracked under `artifacts/v0.24.0`; the +identical historical release bytes remain under their versioned directories. It was produced for the `x86_64-unknown-linux-gnu` host with the builder image digest recorded in `release-manifest.json`. Rust/LLVM may order identical RISC-V functions differently on another build host, so the script claims a byte-for-byte diff --git a/contracts/registry-type-script/artifacts/v0.23.0/cellscript-registry-type-script b/contracts/registry-type-script/artifacts/v0.23.0/cellscript-registry-type-script new file mode 100755 index 0000000000000000000000000000000000000000..9a756b8aba6aeb32444c747fdadeb54e29aa4bfe GIT binary patch literal 3352 zcmbtXeQXrR6`$Fi**$;M+&rl48i@~iF0JLu|EL(y@F)@$&?I!q9xQ3JEZZFbc3H zF-+Iz$Gn-1?!QhK$dne{ET(0CUpK?h@LSF68dD;xzGhYwx`~_nH094k-OiNX^kaMN zh}ZQUIeI)(8uX?)GAG5;{xatGzc$!@H9KFo5pTYKqXC%GJbl52^{&E(JTpAPJJuuo zI>wJBaxp9&{0`X=GHF-CdO7sB02^i`j-*?7V-UA-JNf-2Lf;ZP#RHauab5AhOW+UV zp;Lc3d+w9-7e+6B>PP}>lmUGk^i&BUKS=Oj=hof1Env;8*xhtMF9(}cW&>>%^mm|d z0W5=q35&b_yK&=r!aWKKWDdxqg)X6u!b0-V}8I+17_wZyX!Wu=^m)E(c1NMl64$bue!#(Gj$!xTf_^@9o3aKe}>y`t10|2b;cl z^^W&;`Sl|^bAHQ>MG|bHlo+%~aW$0s9{lp)ore!2@gvRBF5g&uNayJOK~9TA_aRBB z=7KIAJMp|FJz`+Jt((HZ!i{aZCL`Zb|F%=LbxPiPiFQ6vC7R4Em$=_{t~VIRTsT`z zqt>?c-q5C2W2ecy3yW1(_>lypjXZ|3j>4Hh``KbT5JKCBpU5Gej1KNS{zlWD!$oV+-!%_Ys-&kUmmFWNM>Q z3Bi4Fw}~7-PWs}H5;?&`A|)-TPYe>-a+LI0dWal1MB3oi5O>+tJx<(j z8`m2Vw)Cr& z;Uld%E!-40%U$Dcg({X=m&e$pwzTHV; z9!5!UzmPa#nUQE7%-2yfOG=-hQ_bO_d_fJ>UJMf6xsULLJ%o21CH&eU zqOCnc_!q~C_S4%$&yeV3JyNcmM4A&5U?adrfQ|H8hB+^vMI54+Dzg?G-Le5iy`N)6 zoko129hi%O)kCzx8q%BOSTGT%^C>q@@W7&A9<)b|Wl%3sDxnIAVV9NEjI{Gz=%<$OE0(7xx`wIjC%KL6l8bxA3qC*QN^ z_jzI{Qh#TG&8Z#=k+^SZGEOj!x<5pkzmkZ}(x0w-pb7Y$F*V*EW1qqkXu;Rtv#>nL zGIBN}0cU%@$n4JXE&4bGSY4&x%~Y19*{<`)?Y7~zW6il*PTR^U_EuUPV(%i`$03&* zH;5oBzdwi|(^mAOr~Bm<7L?ha_(1qQ>SjI3O z+-gAeliwT|a;s$*gs9M+ zFWqlIib%1#eHsmi2U_L8`{+ShNSJ=82!cJ*(E9L?2h_SzPU;Hr$J4CV{bD36T9sAz zxk#FYi(VG-4RLMfr9hXpU z*s97aPNU2u*umCIb02g{4R4~@E@o#?UcVk&-2u4_H_?-D|KYvyhuo zc+no=f=FtVP;`RB-8p?=?m#DBm5~IzWho>1(K17 zvsC3`GLG>!hIu7z5;S6r=j(f!yf$;gcAS5gN6-A(R>0Uw{;q^o#HWiUlU2lVGK=8) ztm(MeWOP#;g@9~1wYh@n42j#{_ec%7@_(LFIB?l|5SnDuq^bQ6>Gs;|j0iyvw zS}!s%)4$8`?=lSEnddP7-kF+pv-UOXYqRctR9_nr%{ayq{n+p~%6Mef*=GG6tL02% pkn`iS^RXKG8z!--Sx-04Q_MRK*|U zo*%PjI=cNjoq$O#x>`)i?7ka@qG7k1*Ci$etG;JigiaGT^EBx%L|xCMkJ_{!;7GcKHU>~Dw~If3BjmS2hj`F(D6TX9cM1FvGI`eAvz%h7l(Mmk&UfkDO0f)% zGAx(%-_IqU$K1n2XaEPTqlHxc5}j*O=%kd|TDhX6SnT_B>!&!=VdHT+6X{tQ7W<8; zu9JmvB*sdK$(K`k^?#8wn>AFvg~~H1&j!P`P;5KJ8e)i!`6zapVrGr9yKbe}5XJf_ zWqHPPLH!VP%ej^Vav!4HW&f6?5zAjm9p#H_TJH4RK63rjE2pQn3DsP{W%yrX zNsky*NjHc(j!mAy!1FJi8joWZg%hBe&0Aa13%2Ha^Q4|ob?PY`mo+ll&Y!@FE68GsI_ zt!uw?Y3OKmlJ7?8+RCGqNx#0j^SL+r--v(n>h>y6(EmzJ%Vf``rQfa$A8pBL<|err z?izP1RI$vuJmy_$O$)XawmFUaUzSx*J@W(HY>ApYz&Yws3c4srVM)tRaF{&7$w2| zeB!ueS|WLrUq>F+%M+yBf+7sHxcxDS8x;l$4S!2^m&Lgkxzv{7#Z4R41H+5^%OatH zZ`8viT`NFDbJ+;*&Lo;UkMKoyq7@Yr&EX+@K@HJf2w>j1AM=IXn0Fn+{MtdRtv!SJ z7sjymv)fp&mFPr0C|6E`=0uo!!_*t5-f)j)i1YGU*dcm}GGjr}ts5Zf{R%1S6!3+1 z>baPD^}5cfo`O8Qt^`YRx;eA}nFWZ7CBW>J5``+W%j^673>cjiQC8dO(eBYwqXU1Tp z{>}oMQ#~BQasMXqf5ar}fe;RUD`A_ZFI~5Qar!$WV!S=_zo5^e1zmsN!m2mRsNReO zl7Z{_Aau07ILX^13>HZ zhXX)sZbctF-6yZGKxSW7nR>T}N;13t;IehimN;5i9obRP&)#GK7jg$;6~p|XF1t;b z%(?`|dHo~v_H&Cq)!c->I|!(q(-0y}sf0A$MXsP<2~Fn*{ey0`^t=!iy7Hy_4WNhw zsXL}%DBRy7`#*r$w2(0MKoJCcq@m@(9}lW^qny+k;!mVmtp~(NShOmu?sJhe3m3gC zqQ@sP#fjgH&Mn!kginHcZyK17rqqy7HW&2wwP}kA^d_28Wk0gJV0UQjAVQ6wn| z$t!Nqb4`q0oI?fOyp3k6*3V$+UN&U@L%Np&aew)Jgg?d8FTYYz0WydvOI0o;qnO{u zFt4OdP>C4x^WD8nT${dOJHfxl!!v)f6)uAE_R^w@ z4Lkcl*H+f44S=>&7F$Ve!%u%)xw1NMbJfb~6`MVbQyJ&Yqsz{!s`~9Q#^b4aO=q^( z)~PX0^~8wRAi9B9mwC4tw2tZk)+-8RXV& zt@G9TJYK5xTB*n9f!x@t=l%b#KcWs;%fVVJHrkCIy`6z{7;Ub;p{oHu+%GaP)4tQN z?=%$Ooo6ur-kF$9v-dUoYqRfu*k2nF%{ayueqz`gZ9JHLw%LEjdO1^>()sb(`B)GA hh)Rq#`{~Acf_cZG^%=9Drv;E^{EvBdo@BoLzW|Qn5oG`X literal 0 HcmV?d00001 diff --git a/contracts/registry-type-script/release-manifest.json b/contracts/registry-type-script/release-manifest.json index 42689997..73466ccc 100644 --- a/contracts/registry-type-script/release-manifest.json +++ b/contracts/registry-type-script/release-manifest.json @@ -1,15 +1,15 @@ { "schema": "cellscript-registry-type-script-release-v1", - "version": "0.22.0", + "version": "0.24.0", "target": "riscv64imac-unknown-none-elf", - "artifact": "artifacts/v0.22.0/cellscript-registry-type-script", + "artifact": "artifacts/v0.24.0/cellscript-registry-type-script", "canonical_build_host": "x86_64-unknown-linux-gnu", "canonical_builder_image": "rust@sha256:77fac8b98f9f46062bb680b6d25d5bcaabfc400143952ebc572e924bcbedc3fa", "artifact_bytes": 3352, - "sha256": "6a7ab8eaa2281fe77ca8c7b092006c52f96006ac2c7e4b013f8f88b7bf1f742a", - "ckb_data_hash": "0x8b6de99567accdca438818a55c16534ed10fc335f117709b1487fd2666808bfb", + "sha256": "0f48a8736360c121f6ae0f04ab4b0496834f6715d47e3284a0a07add609dede9", + "ckb_data_hash": "0x0dd596ade29e06e5bcc00f56abf36ecbe9afaa09f1b26a64436aa37854da622b", "script_template": { - "code_hash": "0x8b6de99567accdca438818a55c16534ed10fc335f117709b1487fd2666808bfb", + "code_hash": "0x0dd596ade29e06e5bcc00f56abf36ecbe9afaa09f1b26a64436aa37854da622b", "hash_type": "data1", "args_schema": "ckb_script_hash(custody_lock)", "args_bytes": 32 diff --git a/contracts/registry-type-script/tests/ckb_vm.rs b/contracts/registry-type-script/tests/ckb_vm.rs index 31c02d11..03226587 100644 --- a/contracts/registry-type-script/tests/ckb_vm.rs +++ b/contracts/registry-type-script/tests/ckb_vm.rs @@ -22,7 +22,7 @@ struct Scripts { } fn contract_binary() -> Bytes { - let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("artifacts/v0.22.0/cellscript-registry-type-script"); + let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("artifacts/v0.24.0/cellscript-registry-type-script"); std::fs::read(&path).unwrap_or_else(|error| panic!("read tracked canonical artifact {}: {error}", path.display())).into() } diff --git a/crates/cellscript-artifact-checker/Cargo.toml b/crates/cellscript-artifact-checker/Cargo.toml index 42affa58..16696998 100644 --- a/crates/cellscript-artifact-checker/Cargo.toml +++ b/crates/cellscript-artifact-checker/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-artifact-checker" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" description = "Bounded independent verifier for CellScript lowering records and CKB RISC-V artifacts" diff --git a/crates/cellscript-ckb-adapter/Cargo.toml b/crates/cellscript-ckb-adapter/Cargo.toml index 97f72ab1..5af32a89 100644 --- a/crates/cellscript-ckb-adapter/Cargo.toml +++ b/crates/cellscript-ckb-adapter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-ckb-adapter" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" publish = false diff --git a/crates/cellscript-fiber-adapter/Cargo.toml b/crates/cellscript-fiber-adapter/Cargo.toml index b488bd48..0ec961fb 100644 --- a/crates/cellscript-fiber-adapter/Cargo.toml +++ b/crates/cellscript-fiber-adapter/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-fiber-adapter" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" publish = false diff --git a/crates/cellscript-tools/Cargo.toml b/crates/cellscript-tools/Cargo.toml index f3f71157..93664e1f 100644 --- a/crates/cellscript-tools/Cargo.toml +++ b/crates/cellscript-tools/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-tools" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" publish = false diff --git a/crates/cellscript-tools/src/tooling_release.rs b/crates/cellscript-tools/src/tooling_release.rs index 2b740b4b..fdfc0036 100644 --- a/crates/cellscript-tools/src/tooling_release.rs +++ b/crates/cellscript-tools/src/tooling_release.rs @@ -470,6 +470,19 @@ pub fn run(root: &Path) -> Result<()> { !tx_measure_gate.contains("RUSTUP_TOOLCHAIN"), "CKB transaction measure tooling must use CellScript's pinned Rust toolchain", )?; + for token in [ + "release_ckb_repo_from_args() {", + "staging_dir=\"$(mktemp -d \"$ROOT_DIR/target/cellscript-ckb-tx-measure.XXXXXX\")\"", + "cp tools/ckb-tx-measure/Cargo.toml tools/ckb-tx-measure/Cargo.lock", + "cp src/bin/ckb_tx_measure.rs", + "ln -s \"$ckb_repo\" \"$staging_dir/ckb\"", + ] { + require(gate_script.contains(token), format!("release CKB checkout propagation is missing '{token}'"))?; + } + require( + gate_script.matches("run_release_auxiliary_checks \"$ckb_repo\"").count() == 2, + "release and release-quick must both propagate the selected CKB checkout to auxiliary checks", + )?; require( gate_script.contains("--root \"$ROOT_DIR\" workspace-version"), "release source identity must read the root package version from Cargo.toml", diff --git a/crates/cellscript-wasm/Cargo.toml b/crates/cellscript-wasm/Cargo.toml index 1afa4081..4533776d 100644 --- a/crates/cellscript-wasm/Cargo.toml +++ b/crates/cellscript-wasm/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-wasm" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" publish = false diff --git a/docs/CELLSCRIPT_CKB_ADAPTER.md b/docs/CELLSCRIPT_CKB_ADAPTER.md index 9955bab7..9e3d202b 100644 --- a/docs/CELLSCRIPT_CKB_ADAPTER.md +++ b/docs/CELLSCRIPT_CKB_ADAPTER.md @@ -524,7 +524,7 @@ cargo build -p cellscript-ckb-adapter --bin cellscript-deploy # Build the canonical Registry Type Script deployment for external signing export LOCK_ARG=0x$(cat ~/.ckb/default-lock-arg) # your secp256k1 lock arg cellscript-deploy --rpc http://127.0.0.1:8114 --json build-deploy \ - --artifact contracts/registry-type-script/artifacts/v0.22.0/cellscript-registry-type-script \ + --artifact contracts/registry-type-script/artifacts/v0.24.0/cellscript-registry-type-script \ --lock-arg $LOCK_ARG \ --name cellscript-registry-type-script \ --hash-type data1 \ diff --git a/docs/CELLSCRIPT_GATE_POLICY.md b/docs/CELLSCRIPT_GATE_POLICY.md index b88ed5f1..893c5be6 100644 --- a/docs/CELLSCRIPT_GATE_POLICY.md +++ b/docs/CELLSCRIPT_GATE_POLICY.md @@ -165,6 +165,18 @@ and validates every step's commit, spent-input liveness, live outputs, cycles, serialized size, and occupied capacity. `--stateful-scenarios` remains only as an explicit option for bounded runs. +The backend gate normally resolves that checkout as the sibling `../ckb` +directory. When that path is occupied by another development worktree, set +`CELLSCRIPT_CKB_REPO` to a separate clean checkout at the exact pinned revision; +the stateful wrapper forwards it as the acceptance harness's `--ckb-repo`. +This avoids modifying or stashing an unrelated CKB worktree during validation. + +For `release` and `release-quick`, pass the same checkout with `--ckb-repo`. +The release gate stages the independent `ckb-tx-measure` workspace under +`target/` with its tracked manifest, lockfile, and source so its relative CKB +dependencies resolve against that explicit checkout too. The default remains +the sibling `../ckb`; the tracked lockfile remains bound to the release pin. + The transaction matrix is produced by the native Rust acceptance harness and is intentionally labelled as recipe-replayer evidence, not generated-builder output. Separately, the gate runs the public `cellc action build` and diff --git a/docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md b/docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md index c877cbc6..70256812 100644 --- a/docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md +++ b/docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md @@ -107,10 +107,12 @@ uniquely identify executable data. More than one matching deployment returns The compatibility route is: ```text -GET /idl/:code_hash +GET /idl/:code_hash[?data_hash=0x...] ``` It is retained for existing LS-IDL clients and returns the same original +bytes for immutable `data`/`data1`/`data2` identities. A Type-hash candidate +still requires `data_hash`; code hash alone never selects upgradeable code bytes. New integrations should use the canonical route so network, hash type, and data-hash identity are explicit. diff --git a/docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md b/docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md index b3265c13..5bc7ad92 100644 --- a/docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md +++ b/docs/CELLSCRIPT_PACKAGE_PROVENANCE_AND_DEPLOYMENT_IDENTITY.md @@ -454,7 +454,7 @@ source_hash = "blake2b:0xabcd..." [package_build] edition = "2026" compatibility_profile_hash = "blake2b:0xprofile..." -compiler_version = "0.22.0" +compiler_version = "0.24.0" target_profile = "ckb" artifact_hash = "blake2b:0x1234..." metadata_hash = "blake2b:0x5678..." @@ -840,7 +840,7 @@ for audit, offline fixtures, and direct-Git fallback: "version": "1.2.0", "tag": "v1.2.0", "source_hash": "blake2b:0xabcd...", - "cellscript_version": "0.22.0", + "cellscript_version": "0.24.0", "dependencies": { "token": { "namespace": "cellscript", "version": "0.3.0" } }, diff --git a/docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md b/docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md index 8d9ac847..497fb65c 100644 --- a/docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md +++ b/docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md @@ -1,9 +1,9 @@ # CellScript 0.24 Development Release Notes -**Status**: implementation-complete merge candidate; `dev`, `ci`, and -`backend` passed on 2026-08-10. The refreshed iCKB evidence submodule commit -`0e18ccd97bd75cac7de9211dc8d344c0bc08942f` is published and bound by the -parent gitlink; the full release gate remains required before production claims +**Status**: audit-remediation merge candidate. The refreshed iCKB evidence +submodule commit `0e18ccd97bd75cac7de9211dc8d344c0bc08942f` is published and +bound by the parent gitlink. The merge gates must pass for the exact candidate, +and the full release gate remains required before production claims. **Source edition**: 2026 @@ -301,13 +301,11 @@ or conversion of executable/copy artifacts into source dependencies. ## Validation -The package/Registry closure passed `dev` and `ci` on 2026-08-10, with the CI -website phase using the required Node 22 toolchain. The complete `backend` gate -then passed from an isolated clean checkout containing the refreshed iCKB -differential evidence, pinned CKB revision -`f7fa4436737756f97a24e254f22c13a36316ecea`, and CKB SDK `v5.1.0`. This -covered the compiler tests, Clippy, full strict backend audit, all 218 iCKB -differential cases, and the production stateful CKB scenario harness: +The 2026-08-10 `dev`, `ci`, and `backend` results predated later changes on the +0.24 branch and are not evidence for this merge candidate. The exact audit-fix +snapshot must pass all three commands below in an isolated checkout using Node +22, CKB revision `f7fa4436737756f97a24e254f22c13a36316ecea`, and CKB SDK +`v5.1.0` before merge: ```bash ./scripts/cellscript_gate.sh dev diff --git a/docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md b/docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md index 7331f6ba..8e6dd282 100644 --- a/docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md +++ b/docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md @@ -294,7 +294,9 @@ curl --fail \ --output idl.json ``` -The compatibility route `/idl/:code_hash` returns the same original bytes. +The compatibility route `/idl/:code_hash` returns the same original bytes for +immutable hash types. Type-hash deployments require `?data_hash=0x...` even on +that route so an upgrade cannot resolve by code hash alone. The Registry proves the document schema, raw-byte digest, executable suffix, and deployment identity. It does not prove that the Lock Script correctly implements the interface, and it is not a security audit. See the diff --git a/docs/wiki/_Sidebar.md b/docs/wiki/_Sidebar.md index b82ceafa..94e80852 100644 --- a/docs/wiki/_Sidebar.md +++ b/docs/wiki/_Sidebar.md @@ -20,6 +20,7 @@ - [Spore and RGB++ Interoperability Boundaries](https://github.com/CellScript-Labs/CellScript/wiki/Spore-and-RGBPP-Interop-Boundaries) - [BIP340 Verifier CellDep ABI](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.24/docs/CELLSCRIPT_SIGNATURE_VERIFIER_ABI.md) - [LS-IDL Registry Profile](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.24/docs/CELLSCRIPT_LS_IDL_REGISTRY_PROFILE.md) +- [CellScript 0.23 Release Notes](https://github.com/CellScript-Labs/CellScript/blob/v0.23.0/docs/releases/CELLSCRIPT_0_23_RELEASE_NOTES.md) - [CellScript 0.22 Release Notes](https://github.com/CellScript-Labs/CellScript/blob/v0.22.0/docs/releases/CELLSCRIPT_0_22_RELEASE_NOTES.md) - [CellScript 0.24 Development Release Notes](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.24/docs/releases/CELLSCRIPT_0_24_RELEASE_NOTES.md) - [Bounded Fiber Interoperability Guide](https://github.com/CellScript-Labs/CellScript/blob/nightly-0.24/examples/fiber/README.md) diff --git a/editors/vscode-cellscript b/editors/vscode-cellscript index 54bd33d2..0c9f426b 160000 --- a/editors/vscode-cellscript +++ b/editors/vscode-cellscript @@ -1 +1 @@ -Subproject commit 54bd33d2de2e9fe508818cad622a00e96a69f47f +Subproject commit 0c9f426bdc617c4847f38da68fc300586bcc1428 diff --git a/proposals/novaseal b/proposals/novaseal index 919f042f..e2ce6737 160000 --- a/proposals/novaseal +++ b/proposals/novaseal @@ -1 +1 @@ -Subproject commit 919f042f6e0c08aab31dd63fc99aec5d49e4e04d +Subproject commit e2ce6737741c7b13c682ad278bba19644aeed93f diff --git a/scripts/cellscript_ckb_stateful_scenarios.sh b/scripts/cellscript_ckb_stateful_scenarios.sh index 99e21945..625c86de 100755 --- a/scripts/cellscript_ckb_stateful_scenarios.sh +++ b/scripts/cellscript_ckb_stateful_scenarios.sh @@ -3,4 +3,12 @@ set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +if [[ -n "${CELLSCRIPT_CKB_REPO:-}" ]]; then + exec "$SCRIPT_DIR/ckb_cellscript_acceptance.sh" \ + --production \ + --stateful-scenarios \ + --ckb-repo "$CELLSCRIPT_CKB_REPO" \ + "$@" +fi + exec "$SCRIPT_DIR/ckb_cellscript_acceptance.sh" --production --stateful-scenarios "$@" diff --git a/scripts/cellscript_gate.sh b/scripts/cellscript_gate.sh index 574a9d94..4878be33 100755 --- a/scripts/cellscript_gate.sh +++ b/scripts/cellscript_gate.sh @@ -477,8 +477,50 @@ check_wasm_release_bundle() { fi } +release_ckb_repo_from_args() { + local ckb_repo="$ROOT_DIR/../ckb" + while (($# > 0)); do + case "$1" in + --ckb-repo) + if (($# < 2)); then + printf 'missing value for --ckb-repo\n' >&2 + return 2 + fi + ckb_repo="$2" + shift 2 + ;; + *) + shift + ;; + esac + done + printf '%s\n' "$ckb_repo" +} + check_ckb_tx_measure_tool() { - run cargo test --manifest-path tools/ckb-tx-measure/Cargo.toml --locked + local ckb_repo="$1" + local default_ckb_repo="$ROOT_DIR/../ckb" + if [[ ! -d "$ckb_repo" ]]; then + printf 'CKB checkout does not exist: %s\n' "$ckb_repo" >&2 + return 1 + fi + ckb_repo="$(cd "$ckb_repo" && pwd -P)" + if [[ -d "$default_ckb_repo" ]]; then + default_ckb_repo="$(cd "$default_ckb_repo" && pwd -P)" + if [[ "$ckb_repo" == "$default_ckb_repo" ]]; then + run cargo test --manifest-path tools/ckb-tx-measure/Cargo.toml --locked + return + fi + fi + + local staging_dir + staging_dir="$(mktemp -d "$ROOT_DIR/target/cellscript-ckb-tx-measure.XXXXXX")" + mkdir -p "$staging_dir/cellscript/tools/ckb-tx-measure" "$staging_dir/cellscript/src/bin" + cp tools/ckb-tx-measure/Cargo.toml tools/ckb-tx-measure/Cargo.lock \ + "$staging_dir/cellscript/tools/ckb-tx-measure/" + cp src/bin/ckb_tx_measure.rs "$staging_dir/cellscript/src/bin/" + ln -s "$ckb_repo" "$staging_dir/ckb" + run cargo test --manifest-path "$staging_dir/cellscript/tools/ckb-tx-measure/Cargo.toml" --locked } check_novaseal_rust_tooling() { @@ -608,6 +650,7 @@ run_backend_gate() { } run_release_auxiliary_checks() { + local ckb_repo="$1" require_cmd npm run cargo run --quiet --locked -p cellscript-tools --bin cellscript-tools -- \ @@ -616,7 +659,7 @@ run_release_auxiliary_checks() { check_ckb_release_docs check_ckb_acceptance_boundaries check_novaseal_acceptance_boundaries - check_ckb_tx_measure_tool + check_ckb_tx_measure_tool "$ckb_repo" check_novaseal_rust_tooling check_novaseal_verifier_pinning check_wasm_release_bundle @@ -628,18 +671,22 @@ run_release_auxiliary_checks() { } run_release_quick_gate() { + local ckb_repo + ckb_repo="$(release_ckb_repo_from_args "$@")" check_release_source_identity run_ci_gate - run_release_auxiliary_checks + run_release_auxiliary_checks "$ckb_repo" run ./scripts/ckb_cellscript_acceptance.sh --compile-only --production "$@" printf '\nCellScript backend shape report: %s\n' "$CELLSCRIPT_BACKEND_SHAPE_REPORT" printf 'CellScript Molecule schema manifest report: %s\n' "$CELLSCRIPT_MOLECULE_SCHEMA_MANIFEST_REPORT" } run_release_gate() { + local ckb_repo + ckb_repo="$(release_ckb_repo_from_args "$@")" check_release_source_identity run_ci_gate - run_release_auxiliary_checks + run_release_auxiliary_checks "$ckb_repo" run ./scripts/ckb_cellscript_acceptance.sh --production --stateful-scenarios "$@" printf '\nCellScript backend shape report: %s\n' "$CELLSCRIPT_BACKEND_SHAPE_REPORT" printf 'CellScript Molecule schema manifest report: %s\n' "$CELLSCRIPT_MOLECULE_SCHEMA_MANIFEST_REPORT" diff --git a/services/registry-api/README.md b/services/registry-api/README.md index 81731554..51c5a352 100644 --- a/services/registry-api/README.md +++ b/services/registry-api/README.md @@ -174,7 +174,8 @@ The canonical LS-IDL lookup returns `application/vnd.ckb.ls-idl+json` plus digest, coordinate, commitment, and verification headers. `data_hash` is required for `hash_type=type`; ambiguous matches return `409`. `/idl/:code_hash` is a compatibility route for existing -clients and returns the same exact raw bytes. +clients and returns the same exact raw bytes for immutable hash types; a +Type-hash candidate still requires `?data_hash=0x...`. ## Publisher Authorisation @@ -325,6 +326,9 @@ cellc publish --artifact-manifest Artifact.toml ``` `CELLSCRIPT_REGISTRY_API_URL` overrides the API base URL. +`CELLSCRIPT_REGISTRY_STATIC_ORIGIN` declares a separate trusted object origin +for private registries; source snapshots are rejected unless their origin is +the configured API/static origin or the production/testnet static origin. `CELLSCRIPT_CAPABILITY_PRIVATE_KEY_PKCS8_B64` supplies the delegated key in CI. `CELLSCRIPT_REGISTRY_IDEMPOTENCY_KEY` pins the exact retry key. diff --git a/services/registry-api/src/index.ts b/services/registry-api/src/index.ts index af3155b6..54851a8c 100644 --- a/services/registry-api/src/index.ts +++ b/services/registry-api/src/index.ts @@ -201,7 +201,7 @@ export function registryRuntimeConfig(env: Env): RegistryRuntimeConfig { throw new ApiError(503, "invalid_registry_environment", "REGISTRY_ENVIRONMENT must be production or testnet-sandbox"); } export const CANONICAL_REGISTRY_TYPE_SCRIPT = Object.freeze({ - code_hash: "0x8b6de99567accdca438818a55c16534ed10fc335f117709b1487fd2666808bfb", + code_hash: "0x0dd596ade29e06e5bcc00f56abf36ecbe9afaa09f1b26a64436aa37854da622b", hash_type: "data1", }); export const CKB_MAINNET_SIGHASH_LOCK = Object.freeze({ @@ -722,7 +722,7 @@ async function handleLsIdlRead( } const candidate = candidates[0]!; const deployment = candidate.deployment.evidence; - if (!compatibilityRoute && deployment["hash_type"] === "type" && !dataHash) { + if (deployment["hash_type"] === "type" && !dataHash) { throw new ApiError( 409, "ls_idl_data_hash_required", @@ -4700,7 +4700,7 @@ function errorResponse(error: unknown, requestId: string): Response { const headers = corsHeaders(requestId); const status = error instanceof ApiError ? error.status : 500; const code = error instanceof ApiError ? error.code : "internal_error"; - const message = error instanceof Error ? error.message : "internal error"; + const message = error instanceof ApiError ? error.message : "internal server error"; return json({ request_id: requestId, error: { code, message } }, status, headers); } diff --git a/services/registry-api/test/registry-api.test.ts b/services/registry-api/test/registry-api.test.ts index 8f375c5d..d427890c 100644 --- a/services/registry-api/test/registry-api.test.ts +++ b/services/registry-api/test/registry-api.test.ts @@ -667,7 +667,7 @@ async function completeBrowserAuthorisationSession( }, {}, { authorization: `Bearer ${browserToken}` }); } -async function lsIdlLookupApp(idlBytes: Uint8Array) { +async function lsIdlLookupApp(idlBytes: Uint8Array, hashType: "data1" | "type" = "data1") { const store = new MemoryRegistryStore(); const idl = new TextDecoder().decode(idlBytes); const digest = await sha256Hex(idlBytes); @@ -718,7 +718,7 @@ async function lsIdlLookupApp(idlBytes: Uint8Array) { network: "mainnet", code_hash: codeHash, data_hash: codeHash, - hash_type: "data1", + hash_type: hashType, dep_type: "code", }, request_id: "test", @@ -773,6 +773,23 @@ describe("registry api", () => { expect(await formal.text()).toBe(idl); }); + it("requires exact code-cell data identity for Type-hash LS-IDL compatibility reads", async () => { + const idl = "{\"witness\":[]}\n"; + const { app, codeHash } = await lsIdlLookupApp(new TextEncoder().encode(idl), "type"); + + const ambiguous = await get(app, `/idl/${codeHash.slice(2)}`); + expect(ambiguous.status).toBe(409); + expect((await ambiguous.json() as any).error.code).toBe("ls_idl_data_hash_required"); + + const compatibility = await get(app, `/idl/${codeHash.slice(2)}?data_hash=${codeHash}`); + expect(compatibility.status).toBe(200); + expect(await compatibility.text()).toBe(idl); + + const formal = await get(app, `/v1/ckb/scripts/${codeHash}/interfaces/ls-idl?hash_type=type&data_hash=${codeHash}`); + expect(formal.status).toBe(200); + expect(await formal.text()).toBe(idl); + }); + it.runIf(Boolean(process.env.CELLSCRIPT_CKB_IDL_CLIENT_REPO))( "interoperates with the pinned upstream Rust client over the compatibility route", async () => { @@ -2803,7 +2820,10 @@ describe("registry api", () => { ); expect(response.status).toBe(500); - expect((await response.json() as any).error.code).toBe("internal_error"); + const internalError = await response.json() as any; + expect(internalError.error.code).toBe("internal_error"); + expect(internalError.error.message).toBe("internal server error"); + expect(JSON.stringify(internalError)).not.toContain("static registry object write failed"); expect(store.packageVersions.get("cellscript/demo@1.2.3")?.availability_status).toBe("active"); expect(store.auditEvents.some((event) => event.event_type === "admin.package_version.status_updated")).toBe(false); const staticEntryWrites = snapshots.filter((snapshot) => snapshot.key === "artifacts/cellscript/demo/releases/1.2.3.json"); diff --git a/services/registry-api/wrangler.example.toml b/services/registry-api/wrangler.example.toml index f17f8445..9852a727 100644 --- a/services/registry-api/wrangler.example.toml +++ b/services/registry-api/wrangler.example.toml @@ -24,7 +24,7 @@ CLEANUP_QUOTA_EVENT_RETENTION_HOURS = "48" NAMESPACE_CLAIM_COOLDOWN_SECONDS = "3600" CKB_REGISTRY_SCAN_MAX_CELLS = "1000" # Enable only after the canonical mainnet Registry Type Script is deployed. -# REGISTRY_TYPE_SCRIPT_JSON = '{"code_hash":"0x8b6de99567accdca438818a55c16534ed10fc335f117709b1487fd2666808bfb","hash_type":"data1","args":"0x"}' +# REGISTRY_TYPE_SCRIPT_JSON = '{"code_hash":"0x0dd596ade29e06e5bcc00f56abf36ecbe9afaa09f1b26a64436aa37854da622b","hash_type":"data1","args":"0x"}' # REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON = '{"out_point":{"tx_hash":"0x","index":"0x0"},"dep_type":"code"}' # REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON = '{"code_hash":"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8","hash_type":"type","args":"0x"}' # REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON = '{"out_point":{"tx_hash":"0x71a7ba8fc96349fea0ed3a5c47992e3b4084b031a42264a018e0072e8172e46c","index":"0x0"},"dep_type":"dep_group"}' diff --git a/services/registry-artifact-verifier/Cargo.lock b/services/registry-artifact-verifier/Cargo.lock index 1b7980c1..82c49ff2 100644 --- a/services/registry-artifact-verifier/Cargo.lock +++ b/services/registry-artifact-verifier/Cargo.lock @@ -205,7 +205,7 @@ dependencies = [ [[package]] name = "cellscript" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "base64", @@ -235,7 +235,7 @@ dependencies = [ [[package]] name = "cellscript-artifact-checker" -version = "0.22.0" +version = "0.24.0" dependencies = [ "blake2b_simd", "clap", @@ -245,7 +245,7 @@ dependencies = [ [[package]] name = "cellscript-registry-artifact-verifier" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "base64", diff --git a/services/registry-artifact-verifier/Cargo.toml b/services/registry-artifact-verifier/Cargo.toml index 77adbda0..a3035f6f 100644 --- a/services/registry-artifact-verifier/Cargo.toml +++ b/services/registry-artifact-verifier/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-registry-artifact-verifier" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" publish = false diff --git a/services/registry-verifier/Cargo.lock b/services/registry-verifier/Cargo.lock index a0569ead..fd7c8784 100644 --- a/services/registry-verifier/Cargo.lock +++ b/services/registry-verifier/Cargo.lock @@ -205,7 +205,7 @@ dependencies = [ [[package]] name = "cellscript" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "base64", @@ -235,7 +235,7 @@ dependencies = [ [[package]] name = "cellscript-artifact-checker" -version = "0.22.0" +version = "0.24.0" dependencies = [ "blake2b_simd", "clap", @@ -245,7 +245,7 @@ dependencies = [ [[package]] name = "cellscript-registry-verifier" -version = "0.22.0" +version = "0.24.0" dependencies = [ "anyhow", "base64", diff --git a/services/registry-verifier/Cargo.toml b/services/registry-verifier/Cargo.toml index bcf30747..1d964853 100644 --- a/services/registry-verifier/Cargo.toml +++ b/services/registry-verifier/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cellscript-registry-verifier" -version = "0.22.0" +version = "0.24.0" edition = "2024" rust-version = "1.97.1" publish = false diff --git a/src/lib.rs b/src/lib.rs index b6cfe7f3..14a20ecc 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,6 +1,11 @@ //! CellScript - Domain-specific language compiler for CKB blockchain //! Currently the backend can output RISC-V assembly or ELF artifacts. +// The compiler's lowering and backend helpers intentionally carry explicit +// context parameters. Keep their semantic refactors separate from release +// hardening so Clippy does not force high-risk positional rewrites. +#![allow(clippy::too_many_arguments)] + pub(crate) mod aggregate_lowering; pub mod assumptions; pub mod ast; diff --git a/src/package/mod.rs b/src/package/mod.rs index 399d8c66..6aecb00a 100644 --- a/src/package/mod.rs +++ b/src/package/mod.rs @@ -550,6 +550,36 @@ fn sanitize_node_component(value: &str) -> String { .collect() } +fn validate_git_dependency_ref(kind: &str, value: &str) -> Result<()> { + if kind == "rev" { + if value.len() == 40 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Ok(()); + } + return Err(CompileError::without_span("git dependency rev must be a full 40-hex commit identity")); + } + + let invalid_character = value + .chars() + .any(|character| character.is_ascii_control() || character.is_ascii_whitespace() || "~^:?*[\\".contains(character)); + let invalid_component = value.split('/').any(|component| { + component.is_empty() || component.starts_with('.') || component.ends_with('.') || component.ends_with(".lock") + }); + if value.is_empty() + || value.starts_with('-') + || value == "@" + || value.contains("..") + || value.contains("@{") + || invalid_character + || invalid_component + { + return Err(CompileError::without_span(format!( + "invalid git dependency {kind} '{}'; expected a canonical Git reference name", + value + ))); + } + Ok(()) +} + fn package_node_id(package: &ResolvedPackage, options: &ResolutionOptions) -> String { let source = match &package.source { PackageSource::Local(path) => format!("path:{}", path.to_string_lossy().replace('\\', "/")), @@ -793,7 +823,7 @@ dist/ let dependencies = self.selected_dependencies(&manifest, options, true)?; for (alias, dep) in dependencies { let node_id = - self.resolve_dependency_from_root(&alias, &dep, &self.root.clone(), options, &mut Vec::new(), &mut Vec::new())?; + self.resolve_dependency_from_root(&alias, &dep, &self.root.clone(), options, true, &mut Vec::new(), &mut Vec::new())?; self.root_dependencies.insert(alias, node_id); } @@ -1186,6 +1216,7 @@ dist/ dep: &Dependency, base_root: &Path, parent_options: &ResolutionOptions, + resolver_owner_is_root: bool, stack_ids: &mut Vec, stack_labels: &mut Vec, ) -> Result { @@ -1196,6 +1227,12 @@ dist/ } Dependency::Detailed(detailed) => { if detailed.resolver.is_some() { + if !resolver_owner_is_root { + return Err(CompileError::without_span(format!( + "transitive dependency '{}' cannot invoke an external resolver; only the root package may declare executable resolution policy", + alias + ))); + } let normalized = self.resolve_external_dependency(alias, &package_name, detailed, base_root, parent_options)?; let resolved = if let Some(git) = &normalized.git { self.resolve_from_git_with_manifest(&package_name, git, &normalized)? @@ -1275,8 +1312,15 @@ dist/ let child_dependencies = self.selected_dependencies(&manifest, &child_options, false)?; let mut child_edges = BTreeMap::new(); for (child_alias, child_dep) in child_dependencies { - let child_id = - self.resolve_dependency_from_root(&child_alias, &child_dep, &resolved.path, &child_options, stack_ids, stack_labels)?; + let child_id = self.resolve_dependency_from_root( + &child_alias, + &child_dep, + &resolved.path, + &child_options, + false, + stack_ids, + stack_labels, + )?; child_edges.insert(child_alias, child_id); } stack_ids.pop(); @@ -1780,7 +1824,24 @@ dist/ CompileError::without_span(format!("failed to create git cache directory '{}': {}", cache_dir.display(), e)) })?; - let requested_ref = detailed.rev.as_ref().or(detailed.tag.as_ref()).or(detailed.branch.as_ref()); + let requested_ref = if let Some(revision) = detailed.rev.as_ref() { + validate_git_dependency_ref("rev", revision).map_err(|error| { + CompileError::without_span(format!("git dependency '{}' from '{}' is invalid: {}", name, url, error.message)) + })?; + Some(revision) + } else if let Some(tag) = detailed.tag.as_ref() { + validate_git_dependency_ref("tag", tag).map_err(|error| { + CompileError::without_span(format!("git dependency '{}' from '{}' is invalid: {}", name, url, error.message)) + })?; + Some(tag) + } else if let Some(branch) = detailed.branch.as_ref() { + validate_git_dependency_ref("branch", branch).map_err(|error| { + CompileError::without_span(format!("git dependency '{}' from '{}' is invalid: {}", name, url, error.message)) + })?; + Some(branch) + } else { + None + }; let cache_key = format!("{}#{}", url, requested_ref.map(String::as_str).unwrap_or("HEAD")); let cache_name = format!("{}-{:016x}", name, simple_hash(&cache_key)); let clone_dir = cache_dir.join(&cache_name); @@ -3460,6 +3521,98 @@ resolver = "local" assert_eq!(locked.get_resolved()[target].version, "1.2.3"); } + #[cfg(unix)] + #[test] + fn transitive_dependency_cannot_invoke_external_resolver() { + let temp = tempdir().unwrap(); + let root = temp.path(); + write_path_package(root, "child", "child", "1.0.0"); + let marker = root.join("transitive-resolver-ran"); + let shell_digest = sha256_file(Path::new("/bin/sh")).unwrap(); + std::fs::write( + root.join("child/Cell.toml"), + format!( + r#" +[package] +edition = "2026" +name = "child" +version = "1.0.0" + +[resolvers.untrusted] +command = "/bin/sh" +sha256 = "sha256:{shell_digest}" +args = ["-c", "touch '{}'"] + +[dependencies.grandchild] +version = "*" +resolver = "untrusted" +"#, + marker.display() + ), + ) + .unwrap(); + std::fs::write( + root.join("Cell.toml"), + r#" +[package] +edition = "2026" +name = "app" +version = "0.1.0" + +[dependencies.child] +path = "child" +"#, + ) + .unwrap(); + + let error = PackageManager::new(root).resolve_dependencies().unwrap_err(); + assert!(error.message.contains("transitive dependency 'grandchild' cannot invoke an external resolver")); + assert!(!marker.exists(), "transitive resolver command must never start"); + } + + #[cfg(unix)] + #[test] + fn git_dependency_tag_rejects_option_injection_before_git_runs() { + let temp = tempdir().unwrap(); + let root = temp.path(); + let dependency_repo = root.join("git-package"); + write_path_package(root, "git-package", "git_package", "1.0.0"); + for arguments in [ + vec!["init", "-q"], + vec!["config", "user.email", "tests@cellscript.dev"], + vec!["config", "user.name", "CellScript Tests"], + vec!["add", "."], + vec!["commit", "-q", "-m", "initial"], + ] { + let status = std::process::Command::new("git").args(arguments).current_dir(&dependency_repo).status().unwrap(); + assert!(status.success()); + } + let marker = root.join("git-option-injection-ran"); + std::fs::write( + root.join("Cell.toml"), + format!( + r#" +[package] +edition = "2026" +name = "app" +version = "0.1.0" + +[dependencies.remote] +package = "git_package" +git = "{}" +tag = "--upload-pack=/bin/sh -c 'touch {}'" +"#, + dependency_repo.display(), + marker.display() + ), + ) + .unwrap(); + + let error = PackageManager::new(root).resolve_dependencies().unwrap_err(); + assert!(error.message.contains("invalid git dependency tag"), "{}", error.message); + assert!(!marker.exists(), "invalid Git ref must be rejected before Git executes"); + } + #[test] fn moving_git_branch_changes_only_after_explicit_repin() { let temp = tempdir().unwrap(); diff --git a/src/package/registry.rs b/src/package/registry.rs index 4b0d4c4b..dbcbf940 100644 --- a/src/package/registry.rs +++ b/src/package/registry.rs @@ -29,8 +29,12 @@ use std::path::{Path, PathBuf}; pub const DEFAULT_REGISTRY_URL: &str = "https://github.com/cellscript/cellscript-registry"; pub const REGISTRY_URL_ENV: &str = "CELLSCRIPT_REGISTRY_URL"; pub const DEFAULT_PUBLIC_REGISTRY_ORIGIN: &str = "https://api.registry.cellscript.dev"; +pub const DEFAULT_PUBLIC_REGISTRY_STATIC_ORIGIN: &str = "https://registry.cellscript.dev"; +pub const DEFAULT_TESTNET_REGISTRY_ORIGIN: &str = "https://api.testnet.registry.cellscript.dev"; +pub const DEFAULT_TESTNET_REGISTRY_STATIC_ORIGIN: &str = "https://objects.testnet.registry.cellscript.dev"; pub const REGISTRY_API_URL_ENV: &str = "CELLSCRIPT_REGISTRY_API_URL"; pub const REGISTRY_ORIGIN_ENV: &str = "CELLSCRIPT_REGISTRY_ORIGIN"; +pub const REGISTRY_STATIC_ORIGIN_ENV: &str = "CELLSCRIPT_REGISTRY_STATIC_ORIGIN"; pub const REGISTRY_AUTH_PROTOCOL: &str = "cellscript-registry-auth-v1"; pub const AUTHORIZE_CAPABILITY_ACTION: &str = "authorize_capability"; pub const REVOKE_CAPABILITY_ACTION: &str = "revoke_capability"; @@ -496,8 +500,18 @@ fn lookup_public_registry( if !response.status().is_success() { return Err(CompileError::without_span(format!("public registry request '{}' returned HTTP {}", url, response.status()))); } - let payload: PublicRegistryPackage = response - .json() + if response.content_length().is_some_and(|length| length == 0 || length > MAX_PUBLIC_REGISTRY_METADATA_BYTES) { + return Err(CompileError::without_span("public registry metadata Content-Length exceeds the bounded response contract")); + } + let mut bytes = Vec::new(); + response + .take(MAX_PUBLIC_REGISTRY_METADATA_BYTES + 1) + .read_to_end(&mut bytes) + .map_err(|error| CompileError::without_span(format!("failed to read public registry response '{url}': {error}")))?; + if bytes.is_empty() || bytes.len() as u64 > MAX_PUBLIC_REGISTRY_METADATA_BYTES { + return Err(CompileError::without_span("public registry metadata exceeds the bounded response contract")); + } + let payload: PublicRegistryPackage = serde_json::from_slice(&bytes) .map_err(|error| CompileError::without_span(format!("public registry response '{}' is invalid: {error}", url)))?; payload.into_resolution(namespace, name) } @@ -563,10 +577,21 @@ impl PublicRegistryPackage { "artifact '{expected_namespace}/{expected_name}' is not a resolver-safe CellScript dependency" ))); } + if self.releases.len() > MAX_PUBLIC_REGISTRY_RELEASES { + return Err(CompileError::without_span(format!( + "public registry package '{expected_namespace}/{expected_name}' exceeds the {MAX_PUBLIC_REGISTRY_RELEASES}-release response limit" + ))); + } let source = self.repository.filter(|value| !value.trim().is_empty()).unwrap_or_default(); let mut versions = Vec::with_capacity(self.releases.len()); let mut source_snapshots = BTreeMap::new(); for public_version in self.releases { + if public_version.registry_entry.versions.len() > MAX_PUBLIC_REGISTRY_RELEASES { + return Err(CompileError::without_span(format!( + "public registry version '{}' exceeds the bounded signed-version list", + public_version.release + ))); + } if public_version.registry_entry.schema_version != RegistryIndex::CURRENT_SCHEMA_VERSION || public_version.registry_entry.namespace != expected_namespace || public_version.registry_entry.name != expected_name @@ -644,9 +669,70 @@ fn public_registry_release_status(verification: &str, availability: &str) -> Res } } +#[cfg(feature = "cli")] +const MAX_PUBLIC_REGISTRY_METADATA_BYTES: u64 = 1024 * 1024; +#[cfg(feature = "cli")] +const MAX_PUBLIC_REGISTRY_RELEASES: usize = 1024; #[cfg(feature = "cli")] const MAX_PUBLIC_SOURCE_SNAPSHOT_BYTES: u64 = 5 * 1024 * 1024; +#[cfg(feature = "cli")] +fn canonical_trusted_registry_origin(value: &str, label: &str) -> Result { + let url = reqwest::Url::parse(value).map_err(|error| CompileError::without_span(format!("{label} is invalid: {error}")))?; + let Some(host) = url.host_str() else { + return Err(CompileError::without_span(format!("{label} has no host"))); + }; + let host = host.trim_start_matches('[').trim_end_matches(']'); + let loopback = + host.eq_ignore_ascii_case("localhost") || host.parse::().is_ok_and(|address| address.is_loopback()); + if url.scheme() != "https" && !(url.scheme() == "http" && loopback) { + return Err(CompileError::without_span(format!( + "{label} must use HTTPS; plaintext HTTP is allowed only for an explicitly configured loopback Registry" + ))); + } + if !url.username().is_empty() || url.password().is_some() || url.fragment().is_some() { + return Err(CompileError::without_span(format!("{label} must not contain credentials or a fragment"))); + } + Ok(url.origin().ascii_serialization()) +} + +#[cfg(feature = "cli")] +fn trusted_registry_snapshot_origins() -> Result> { + let api_origin = canonical_trusted_registry_origin(&resolver_registry_url(), "Registry API origin")?; + let mut origins = vec![api_origin.clone()]; + if api_origin == DEFAULT_PUBLIC_REGISTRY_ORIGIN { + origins.push(DEFAULT_PUBLIC_REGISTRY_STATIC_ORIGIN.to_string()); + } else if api_origin == DEFAULT_TESTNET_REGISTRY_ORIGIN { + origins.push(DEFAULT_TESTNET_REGISTRY_STATIC_ORIGIN.to_string()); + } + if let Some(configured) = + std::env::var(REGISTRY_STATIC_ORIGIN_ENV).ok().map(|value| value.trim().to_string()).filter(|value| !value.is_empty()) + { + origins.push(canonical_trusted_registry_origin(&configured, "Registry static origin")?); + } + origins.sort(); + origins.dedup(); + Ok(origins) +} + +#[cfg(feature = "cli")] +fn validate_registry_snapshot_url_against(url: &str, trusted_origins: &[String], label: &str) -> Result { + let parsed = reqwest::Url::parse(url).map_err(|error| CompileError::without_span(format!("{label} URL is invalid: {error}")))?; + if !parsed.username().is_empty() || parsed.password().is_some() || parsed.fragment().is_some() { + return Err(CompileError::without_span(format!("{label} URL must not contain credentials or a fragment"))); + } + let origin = canonical_trusted_registry_origin(parsed.as_str(), &format!("{label} origin"))?; + if !trusted_origins.iter().any(|trusted| trusted == &origin) { + return Err(CompileError::without_span(format!("{label} URL origin '{origin}' is not a configured Registry object origin"))); + } + Ok(parsed) +} + +#[cfg(feature = "cli")] +fn validate_registry_snapshot_url(url: &str, label: &str) -> Result { + validate_registry_snapshot_url_against(url, &trusted_registry_snapshot_origins()?, label) +} + #[cfg(feature = "cli")] #[derive(Debug, Deserialize)] struct GeneratedSourceSnapshot { @@ -684,7 +770,28 @@ pub fn materialize_public_source_snapshot( version: &str, expected_source_hash: &str, ) -> Result { - validate_public_source_snapshot_descriptor(snapshot, expected_source_hash)?; + materialize_public_source_snapshot_against( + snapshot, + cache_root, + namespace, + name, + version, + expected_source_hash, + &trusted_registry_snapshot_origins()?, + ) +} + +#[cfg(feature = "cli")] +fn materialize_public_source_snapshot_against( + snapshot: &PublicRegistrySourceSnapshot, + cache_root: &Path, + namespace: &str, + name: &str, + version: &str, + expected_source_hash: &str, + trusted_origins: &[String], +) -> Result { + validate_public_source_snapshot_descriptor_against(snapshot, expected_source_hash, trusted_origins)?; let bytes = download_public_source_snapshot(snapshot)?; std::fs::create_dir_all(cache_root).map_err(|error| { CompileError::without_span(format!("failed to create source snapshot cache '{}': {error}", cache_root.display())) @@ -730,15 +837,7 @@ pub fn materialize_locked_public_source_snapshot( if digest.len() != 64 || !digest.bytes().all(|byte| byte.is_ascii_hexdigit()) { return Err(CompileError::without_span("locked Registry snapshot hash must contain 32 bytes of hex")); } - let parsed = reqwest::Url::parse(url) - .map_err(|error| CompileError::without_span(format!("locked Registry snapshot URL is invalid: {error}")))?; - if !matches!(parsed.scheme(), "http" | "https") - || !parsed.username().is_empty() - || parsed.password().is_some() - || parsed.fragment().is_some() - { - return Err(CompileError::without_span("locked Registry snapshot URL must be HTTP(S) without credentials or a fragment")); - } + let _parsed = validate_registry_snapshot_url(url, "locked Registry snapshot")?; std::fs::create_dir_all(cache_root)?; let target = cache_root.join(format!("{name}-snapshot-{digest}")); if target.exists() { @@ -851,7 +950,11 @@ pub fn materialize_public_source_snapshot( } #[cfg(feature = "cli")] -fn validate_public_source_snapshot_descriptor(snapshot: &PublicRegistrySourceSnapshot, expected_source_hash: &str) -> Result<()> { +fn validate_public_source_snapshot_descriptor_against( + snapshot: &PublicRegistrySourceSnapshot, + expected_source_hash: &str, + trusted_origins: &[String], +) -> Result<()> { if snapshot.schema != "cellscript-registry-immutable-bundle" { return Err(CompileError::without_span(format!("unsupported public registry source snapshot schema '{}'", snapshot.schema))); } @@ -876,14 +979,7 @@ fn validate_public_source_snapshot_descriptor(snapshot: &PublicRegistrySourceSna snapshot.content_type ))); } - let url = reqwest::Url::parse(&snapshot.url) - .map_err(|error| CompileError::without_span(format!("public registry source snapshot URL is invalid: {error}")))?; - if !matches!(url.scheme(), "http" | "https") || !url.username().is_empty() || url.password().is_some() || url.fragment().is_some() - { - return Err(CompileError::without_span( - "public registry source snapshot URL must be an HTTP(S) URL without credentials or a fragment", - )); - } + let _url = validate_registry_snapshot_url_against(&snapshot.url, trusted_origins, "public Registry source snapshot")?; Ok(()) } @@ -2049,10 +2145,60 @@ left = "a" size_bytes: 42, content_type: "application/x-tar".to_string(), }; - let error = validate_public_source_snapshot_descriptor(&snapshot, "source-hash").unwrap_err(); + let error = validate_public_source_snapshot_descriptor_against( + &snapshot, + "source-hash", + &[DEFAULT_PUBLIC_REGISTRY_STATIC_ORIGIN.to_string()], + ) + .unwrap_err(); assert!(error.to_string().contains("does not support source snapshot content type")); } + #[cfg(feature = "cli")] + #[test] + fn registry_snapshot_urls_are_bound_to_explicit_trusted_origins() { + let production = vec![DEFAULT_PUBLIC_REGISTRY_STATIC_ORIGIN.to_string()]; + assert!(validate_registry_snapshot_url_against( + "https://registry.cellscript.dev/source-snapshots/demo.json", + &production, + "snapshot", + ) + .is_ok()); + let internal = + validate_registry_snapshot_url_against("http://169.254.169.254/latest/meta-data/", &production, "snapshot").unwrap_err(); + assert!(internal.message.contains("must use HTTPS") || internal.message.contains("not a configured Registry object origin")); + + let development = vec!["http://127.0.0.1:8787".to_string()]; + assert!(validate_registry_snapshot_url_against("http://127.0.0.1:8787/source-snapshots/demo.json", &development, "snapshot",) + .is_ok()); + assert!(validate_registry_snapshot_url_against("http://127.0.0.1:8788/source-snapshots/demo.json", &development, "snapshot",) + .is_err()); + } + + #[cfg(feature = "cli")] + #[test] + fn public_registry_metadata_rejects_oversized_content_length() { + use std::io::{Read as _, Write as _}; + + let listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let address = listener.local_addr().unwrap(); + let server = std::thread::spawn(move || { + let (mut stream, _) = listener.accept().unwrap(); + let mut request = [0_u8; 4096]; + let _ = stream.read(&mut request).unwrap(); + write!( + stream, + "HTTP/1.1 200 OK\r\nContent-Length: {}\r\nContent-Type: application/json\r\nConnection: close\r\n\r\n", + MAX_PUBLIC_REGISTRY_METADATA_BYTES + 1 + ) + .unwrap(); + }); + + let error = lookup_public_registry(&format!("http://{address}"), "cellscript", "demo").unwrap_err(); + server.join().unwrap(); + assert!(error.message.contains("Content-Length exceeds the bounded response contract")); + } + #[cfg(feature = "cli")] #[test] fn public_source_snapshot_download_is_hash_bound_and_materialized_without_git() { @@ -2103,8 +2249,17 @@ left = "a" content_type: "application/vnd.cellscript.source-snapshot+json".to_string(), }; let cache = tempfile::tempdir().unwrap(); - let materialized = - materialize_public_source_snapshot(&descriptor, cache.path(), "cellscript", "demo", "1.2.3", &source_hash).unwrap(); + let trusted_origins = vec![format!("http://{address}")]; + let materialized = materialize_public_source_snapshot_against( + &descriptor, + cache.path(), + "cellscript", + "demo", + "1.2.3", + &source_hash, + &trusted_origins, + ) + .unwrap(); server.join().unwrap(); assert_eq!(compute_source_hash(&materialized).unwrap(), source_hash); assert_eq!(std::fs::read(materialized.join("src/main.cell")).unwrap(), source);