Skip to content

Commit 8b6a7d7

Browse files
committed
feat: add isolated Pudge Registry sandbox
1 parent 9ed6be8 commit 8b6a7d7

18 files changed

Lines changed: 1112 additions & 127 deletions

‎CHANGELOG.md‎

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,16 @@
22

33
## Unreleased
44

5+
- Add an isolated Pudge Testnet Registry Sandbox. Its API, Postgres database,
6+
object volume, signing origin, RPC identity, website build, wallet storage,
7+
and deployment evidence are separate from production. Sandbox releases are
8+
hidden 72 hours after admission; version JSON is deleted at expiry and source
9+
objects are deleted after a 24-hour grace period, while minimal audit
10+
tombstones remain. The API rejects a wrong-network RPC and cross-environment
11+
deployment payloads. `cellc artifact record-deployment --network testnet`
12+
defaults to the Pudge Registry API, and `cell-dep` revalidates liveness on the
13+
network recorded in accepted evidence. Pudge chain history remains immutable:
14+
expiry removes Registry indexing and off-chain objects, not on-chain Cells.
515
- Complete the Registry's generalized artifact and chain-evidence path. Rust,
616
C, JavaScript, and other CKB artifacts now keep explicit source, build,
717
deployment, TCB, and copy-only identities instead of being presented as
@@ -53,8 +63,9 @@
5363
localized statuses and copyable audit values. Publisher authorisation now
5464
accepts both JoyID
5565
(`joyid_ckb`) and standard CKB secp256k1 (`ckb_secp256k1`) principals through
56-
the CCC CKB-signer boundary on mainnet. The chooser no longer exposes a
57-
testnet option or constructs a testnet client. The frontend never accepts
66+
the CCC CKB-signer boundary. Production exposes only mainnet; the separately
67+
built Pudge Sandbox constructs a testnet client without adding a network
68+
selector to either environment. The frontend never accepts
5869
mnemonic words; traditional recovery phrases remain inside the wallet. CLI
5970
auth commands use `--wallet-signature`, with `--joyid-signature` retained as
6071
a visible compatibility alias, and the API adds the corresponding typed

‎docs/wiki/Tutorial-12-Phase1-Registry-End-to-End.md‎

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,11 @@ CellScript path first, then the generic artifact path.
1111
## 1. Connect a CKB wallet
1212

1313
Open `https://cellscript.dev/registry/submit`. The page does not expose a
14-
network selector: Registry authorisation and deployment evidence are CKB
15-
mainnet-only.
14+
network selector. The production Registry is CKB mainnet-only. Pudge testing
15+
uses `https://testnet.cellscript.dev/registry`, with a different API origin,
16+
database, object store, wallet connection state, and testnet-only evidence.
17+
Sandbox records disappear from discovery after 72 hours and their source bytes
18+
are purged after a 24-hour grace period; this does not erase Pudge chain history.
1619

1720
Choose a detected wallet from the modal. Wallets listed without an active
1821
connector link to their official installation page. The wallet signs only the
@@ -202,7 +205,7 @@ canonical SHA-256 and threshold in the accepted evidence. Only then does
202205
verification become `verified`; a reproducible executable cannot be recorded
203206
as deployed before this transition.
204207

205-
## 5. Record a mainnet deployment
208+
## 5. Record a deployment on the Registry's fixed network
206209

207210
The deployment request is a signed
208211
`cellscript-registry-deployment` / `record_deployment` payload sent to:
@@ -212,11 +215,12 @@ POST /v1/artifacts/acme/vault-lock/releases/1.0.0/deployments
212215
```
213216

214217
It includes the published `artifact_hash`, equal `data_hash`, `code_hash`,
215-
`hash_type`, `dep_type`, and the mainnet OutPoint. The API requires the same
218+
`hash_type`, `dep_type`, and the environment's OutPoint. The API requires the same
216219
namespace capability used for publishing and prior verified-build evidence.
217220

218-
The API calls mainnet `get_live_cell`. It rejects a dead or missing Cell, a
219-
data-hash mismatch, a Type Script hash mismatch, a non-mainnet network, or an
221+
The API first verifies the configured RPC chain identity, then calls
222+
`get_live_cell`. It rejects a dead or missing Cell, a data-hash mismatch, a
223+
Type Script hash mismatch, a network mismatch, or an
220224
OutPoint that is not bound to the published executable. A successful request
221225
appends deployment evidence and changes only `deployment_status` to
222226
`chain_verified`.
@@ -252,7 +256,7 @@ cellc artifact fetch acme/vault-lock@1.0.0 --output vault-lock.bundle.json
252256
cellc artifact verify --bundle vault-lock.bundle.json --receipt vault-lock.bundle.json.receipt.json
253257
cellc artifact pin acme/vault-lock@1.0.0 --output Artifacts.lock --accept-hash-bound
254258
cellc artifact reproduction-evidence acme/vault-lock@1.0.0 --report builder-a.json --report builder-b.json --output reproduced-build-promotion.json
255-
cellc artifact record-deployment acme/vault-lock@1.0.0 --code-hash <hash> --hash-type data1 --dep-type code --tx-hash <tx_hash> --index 0 --capability-key-id <key_id>
259+
cellc artifact record-deployment acme/vault-lock@1.0.0 --network mainnet --code-hash <hash> --hash-type data1 --dep-type code --tx-hash <tx_hash> --index 0 --capability-key-id <key_id>
256260
cellc artifact cell-dep acme/vault-lock@1.0.0 --output CellDep.json --accept-hash-bound --rpc-url https://mainnet.ckb.dev/rpc
257261
cellc artifact set-availability acme/vault-lock@1.0.0 --status yanked --reason "security advisory" --capability-key-id <key_id>
258262
cellc artifact commitment acme/vault-lock@1.0.0 --output RegistryCommitment.json
@@ -278,6 +282,20 @@ The transaction-intent and scanner code is implemented, but production does
278282
not claim a chain commitment until operators deploy and configure the canonical
279283
mainnet Registry Type Script, commitment custody Lock, and both code CellDeps.
280284

285+
For the isolated Pudge flow, use:
286+
287+
```bash
288+
cellc publish --api-url https://api.testnet.registry.cellscript.dev
289+
cellc artifact record-deployment acme/vault-lock@1.0.0 \
290+
--network testnet \
291+
--api-url https://api.testnet.registry.cellscript.dev \
292+
--code-hash <hash> --hash-type data1 --dep-type code \
293+
--tx-hash <testnet_tx_hash> --index 0 --capability-key-id <key_id>
294+
```
295+
296+
`cell-dep` reads the accepted evidence network and defaults to the matching
297+
official RPC; an explicit `--rpc-url` still has to report the same chain.
298+
281299
## 7. Other artifact kinds
282300

283301
- `runtime_verifier`: `ckb_executable` bundle with source, executable, and ABI;

‎services/registry-api/README.md‎

Lines changed: 44 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,18 @@ adapter.
99
- `https://registry.cellscript.dev` serves immutable bundles and static release
1010
JSON independently from the write database.
1111

12+
The Pudge test environment is a separate, ephemeral service:
13+
14+
- `https://api.testnet.registry.cellscript.dev` is the sandbox API;
15+
- `https://objects.testnet.registry.cellscript.dev` is its object origin;
16+
- `https://testnet.cellscript.dev/registry` is its `noindex` UI.
17+
18+
It uses a different Postgres volume, object volume, signing origin, wallet
19+
storage key, RPC identity, and Compose project. Do not put a network selector in
20+
the production Registry. `REGISTRY_ENVIRONMENT=testnet-sandbox` requires the
21+
dedicated origins and accepts only a Pudge/Testnet RPC. Unknown environments
22+
fail closed.
23+
1224
Postgres is authoritative for publisher capabilities, namespace ownership,
1325
artifact releases, orthogonal release states, evidence, jobs, idempotency, and
1426
audit events. R2 or the filesystem adapter stores immutable content and static
@@ -155,8 +167,29 @@ The browser wallet directory lists Neuron, JoyID, imToken, CKBull, SafePal,
155167
Ledger, imKey, OneKey, UTXO Global, Rei Wallet, Gate, and QuantumPurse. Runtime
156168
connectivity is determined by CCC discovery. Directory entries without a live
157169
connector use the external signed-payload handoff and never bypass backend
158-
signature verification. The service accepts no testnet authorisation or
159-
deployment mode.
170+
signature verification. Production accepts only mainnet authorisation and
171+
deployment evidence. The isolated Pudge Sandbox accepts only testnet evidence;
172+
the two origins make wallet challenges and capability signatures non-replayable
173+
across environments.
174+
175+
## Pudge Sandbox Retention
176+
177+
Every sandbox release stores `registry_environment = testnet-sandbox`,
178+
`network = testnet`, `expires_at = created_at + 72h`, and
179+
`purge_after = expires_at + 24h`. Public SQL and in-memory reads filter by
180+
`expires_at` even if maintenance is delayed. At expiry, the version-addressed
181+
static JSON is deleted; after the grace period, a source object is deleted only
182+
when no non-expired release references its snapshot hash. Database identity and
183+
audit rows remain as tombstones so abuse and replay investigations are not
184+
erased. Reads never extend TTL.
185+
186+
The sandbox additionally limits a wallet principal to 20 accepted publish
187+
attempts per 24 hours and one package coordinate to five; the ordinary IP,
188+
capability, namespace-cooldown, request-size, and snapshot-size controls still
189+
apply.
190+
191+
This policy cannot delete Pudge chain history or consume a deployed code Cell.
192+
It only removes the Registry index and its off-chain object bytes.
160193

161194
## Release Admission
162195

@@ -198,7 +231,7 @@ cellc publish --artifact-manifest Artifact.toml
198231
`CELLSCRIPT_CAPABILITY_PRIVATE_KEY_PKCS8_B64` supplies the delegated key in CI.
199232
`CELLSCRIPT_REGISTRY_IDEMPOTENCY_KEY` pins the exact retry key.
200233

201-
## Mainnet Deployment Evidence
234+
## Network-Bound Deployment Evidence
202235

203236
Executable publication begins at `deployment_status = undeployed`. A publisher
204237
records a deployment by signing canonical JSON for:
@@ -207,19 +240,21 @@ records a deployment by signing canonical JSON for:
207240
cellscript-registry-deployment / record_deployment
208241
```
209242

210-
The request must identify `network = mainnet`, the published executable hash,
211-
equal Cell data hash, code hash, hash type, dep type, and OutPoint. Prior
212-
verified-build evidence is mandatory.
243+
The request must identify the network fixed by the Registry environment
244+
(`mainnet` in production, `testnet` in the Pudge Sandbox), the published
245+
executable hash, equal Cell data hash, code hash, hash type, dep type, and
246+
OutPoint. Prior verified-build evidence is mandatory.
213247

214-
The API calls CKB mainnet `get_live_cell(out_point, true, false)` and fails
248+
The API confirms the configured RPC chain identity, calls
249+
`get_live_cell(out_point, true, false)`, and fails
215250
closed unless the Cell is live and its data hash equals the published
216251
executable. For `hash_type = type`, it serializes the returned Type Script with
217252
Molecule and verifies its CKB Script hash against `code_hash`. Data-hash modes
218253
require `code_hash` to equal the data hash. Success appends hash-addressed
219254
evidence and sets only `deployment_status = chain_verified`.
220255

221-
`CKB_MAINNET_RPC_URL` may override the default official mainnet RPC endpoint.
222-
No testnet network value is accepted.
256+
`CKB_RPC_URL` configures the environment RPC. `CKB_MAINNET_RPC_URL` remains a
257+
production compatibility alias.
223258

224259
## Registry Chain Commitments
225260

‎services/registry-api/deploy/docker-compose.production.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ services:
5454
REGISTRY_ORIGIN: ${REGISTRY_ORIGIN:-https://api.registry.cellscript.dev}
5555
STATIC_REGISTRY_ORIGIN: ${STATIC_REGISTRY_ORIGIN:-https://registry.cellscript.dev}
5656
CKB_MAINNET_RPC_URL: ${CKB_MAINNET_RPC_URL:-https://mainnet.ckb.dev/rpc}
57+
CKB_RPC_URL: ${CKB_RPC_URL:-https://mainnet.ckb.dev/rpc}
5758
REGISTRY_TYPE_SCRIPT_JSON: ${REGISTRY_TYPE_SCRIPT_JSON:-}
5859
REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON: ${REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON:-}
5960
REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON: ${REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON:-}
@@ -62,6 +63,7 @@ services:
6263
CKB_REGISTRY_SCAN_MAX_CELLS: ${CKB_REGISTRY_SCAN_MAX_CELLS:-1000}
6364
CKB_MIN_CONFIRMATIONS: ${CKB_MIN_CONFIRMATIONS:-24}
6465
ENVIRONMENT: production
66+
REGISTRY_ENVIRONMENT: production
6567
MAX_INCOMING_BODY_BYTES: "7340032"
6668
MAX_JSON_BODY_BYTES: "6291456"
6769
MAX_SNAPSHOT_BYTES: "5242880"
Lines changed: 140 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,140 @@
1+
name: cellscript-registry-testnet
2+
3+
services:
4+
postgres:
5+
image: postgres:17-alpine
6+
restart: unless-stopped
7+
environment:
8+
POSTGRES_DB: cellscript_registry_testnet
9+
POSTGRES_USER: cellscript_registry_testnet
10+
POSTGRES_PASSWORD: ${REGISTRY_TESTNET_DB_PASSWORD:?REGISTRY_TESTNET_DB_PASSWORD is required}
11+
volumes:
12+
- registry-testnet-postgres:/var/lib/postgresql/data
13+
networks: [registry-testnet-internal]
14+
healthcheck:
15+
test: ["CMD-SHELL", "pg_isready -U cellscript_registry_testnet -d cellscript_registry_testnet"]
16+
interval: 10s
17+
timeout: 5s
18+
retries: 10
19+
start_period: 20s
20+
security_opt: [no-new-privileges:true]
21+
22+
object-store-init:
23+
image: alpine:3.22
24+
command: ["sh", "-c", "chown -R 1000:101 /objects && find /objects -type d -exec chmod 2750 '{}' ';' && find /objects -type f -exec chmod 0640 '{}' ';'"]
25+
volumes:
26+
- registry-testnet-objects:/objects
27+
restart: "no"
28+
security_opt: [no-new-privileges:true]
29+
30+
api:
31+
image: ${REGISTRY_API_IMAGE:-cellscript-registry-api:latest}
32+
build:
33+
context: ..
34+
dockerfile: Dockerfile
35+
restart: unless-stopped
36+
depends_on:
37+
postgres: { condition: service_healthy }
38+
object-store-init: { condition: service_completed_successfully }
39+
environment:
40+
PORT: "8787"
41+
DATABASE_URL: postgresql://cellscript_registry_testnet:${REGISTRY_TESTNET_DB_PASSWORD}@postgres:5432/cellscript_registry_testnet
42+
REGISTRY_OBJECTS_DIR: /objects
43+
REGISTRY_ADMIN_TOKEN: ${REGISTRY_TESTNET_ADMIN_TOKEN:?REGISTRY_TESTNET_ADMIN_TOKEN is required}
44+
REGISTRY_ORIGIN: ${REGISTRY_TESTNET_ORIGIN:-https://api.testnet.registry.cellscript.dev}
45+
STATIC_REGISTRY_ORIGIN: ${STATIC_REGISTRY_TESTNET_ORIGIN:-https://objects.testnet.registry.cellscript.dev}
46+
CKB_RPC_URL: ${CKB_TESTNET_RPC_URL:-https://testnet.ckb.dev/rpc}
47+
CKB_MIN_CONFIRMATIONS: "4"
48+
REGISTRY_TYPE_SCRIPT_JSON: ${REGISTRY_TESTNET_TYPE_SCRIPT_JSON:-}
49+
REGISTRY_TYPE_SCRIPT_CELL_DEP_JSON: ${REGISTRY_TESTNET_TYPE_SCRIPT_CELL_DEP_JSON:-}
50+
REGISTRY_COMMITMENT_LOCK_SCRIPT_JSON: ${REGISTRY_TESTNET_COMMITMENT_LOCK_SCRIPT_JSON:-}
51+
REGISTRY_COMMITMENT_LOCK_CELL_DEP_JSON: ${REGISTRY_TESTNET_COMMITMENT_LOCK_CELL_DEP_JSON:-}
52+
REGISTRY_REPRODUCER_POLICY_JSON: ${REGISTRY_REPRODUCER_POLICY_JSON:-}
53+
CKB_REGISTRY_SCAN_MAX_CELLS: "1000"
54+
ENVIRONMENT: testnet-sandbox
55+
REGISTRY_ENVIRONMENT: testnet-sandbox
56+
MAX_INCOMING_BODY_BYTES: "7340032"
57+
MAX_JSON_BODY_BYTES: "6291456"
58+
MAX_SNAPSHOT_BYTES: "5242880"
59+
REQUIRE_REGISTRY_VERIFIER_READY: "true"
60+
REGISTRY_VERIFIER_SHARED_HEARTBEAT: /objects/.health/verifier-ready
61+
REGISTRY_VERIFIER_HEARTBEAT_MAX_AGE_SECONDS: "120"
62+
VIRTUAL_HOST: ${REGISTRY_TESTNET_API_HOST:-api.testnet.registry.cellscript.dev}
63+
VIRTUAL_PORT: "8787"
64+
expose: ["8787"]
65+
volumes:
66+
- registry-testnet-objects:/objects
67+
networks: [registry-testnet-internal, stack-network]
68+
read_only: true
69+
tmpfs: [/tmp:size=32m,mode=1777]
70+
security_opt: [no-new-privileges:true]
71+
healthcheck:
72+
test: ["CMD", "node", "-e", "fetch('http://127.0.0.1:8787/ready').then(r=>{if(!r.ok)process.exit(1)}).catch(()=>process.exit(1))"]
73+
interval: 15s
74+
timeout: 5s
75+
retries: 10
76+
start_period: 30s
77+
78+
verifier:
79+
image: ${REGISTRY_VERIFIER_IMAGE:-cellscript-registry-verifier:latest}
80+
build:
81+
context: ${CELLSCRIPT_REGISTRY_SOURCE_ROOT:-../../..}
82+
dockerfile: services/registry-api/Dockerfile.verifier
83+
restart: unless-stopped
84+
init: true
85+
depends_on:
86+
api: { condition: service_started }
87+
postgres: { condition: service_healthy }
88+
object-store-init: { condition: service_completed_successfully }
89+
environment:
90+
DATABASE_URL: postgresql://cellscript_registry_testnet:${REGISTRY_TESTNET_DB_PASSWORD}@postgres:5432/cellscript_registry_testnet
91+
REGISTRY_OBJECTS_DIR: /objects
92+
STATIC_REGISTRY_ORIGIN: ${STATIC_REGISTRY_TESTNET_ORIGIN:-https://objects.testnet.registry.cellscript.dev}
93+
CELLSCRIPT_REGISTRY_API_URL: ${REGISTRY_TESTNET_ORIGIN:-https://api.testnet.registry.cellscript.dev}
94+
ENVIRONMENT: testnet-sandbox
95+
REGISTRY_VERIFIER_POLL_INTERVAL_MS: "2000"
96+
REGISTRY_VERIFIER_JOB_TIMEOUT_SECONDS: "240"
97+
REGISTRY_VERIFIER_LEASE_SECONDS: "300"
98+
REGISTRY_VERIFIER_HEALTH_FILE: /tmp/registry-verifier-ready
99+
REGISTRY_VERIFIER_SHARED_HEARTBEAT: /objects/.health/verifier-ready
100+
volumes:
101+
- registry-testnet-objects:/objects
102+
networks: [registry-testnet-internal, stack-network]
103+
read_only: true
104+
tmpfs: [/tmp:size=512m,mode=1777]
105+
pids_limit: 128
106+
mem_limit: 1g
107+
cpus: 1.0
108+
cap_drop: [ALL]
109+
security_opt: [no-new-privileges:true]
110+
111+
static-registry:
112+
image: nginx:1.27-alpine
113+
restart: unless-stopped
114+
depends_on:
115+
object-store-init: { condition: service_completed_successfully }
116+
environment:
117+
VIRTUAL_HOST: ${STATIC_REGISTRY_TESTNET_HOST:-objects.testnet.registry.cellscript.dev}
118+
VIRTUAL_PORT: "8080"
119+
expose: ["8080"]
120+
volumes:
121+
- registry-testnet-objects:/srv/registry:ro
122+
- ./registry-static.nginx.conf:/etc/nginx/conf.d/default.conf:ro
123+
networks: [stack-network]
124+
read_only: true
125+
tmpfs:
126+
- /var/cache/nginx:size=16m
127+
- /var/run:size=1m
128+
- /tmp:size=4m
129+
security_opt: [no-new-privileges:true]
130+
131+
volumes:
132+
registry-testnet-postgres:
133+
registry-testnet-objects:
134+
135+
networks:
136+
registry-testnet-internal:
137+
internal: true
138+
stack-network:
139+
external: true
140+
name: stack-network
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
alter table package_versions
2+
add column if not exists registry_environment text not null default 'production',
3+
add column if not exists chain_network text not null default 'mainnet',
4+
add column if not exists expires_at timestamptz,
5+
add column if not exists expired_at timestamptz,
6+
add column if not exists purge_after timestamptz,
7+
add column if not exists static_purged_at timestamptz,
8+
add column if not exists source_purged_at timestamptz;
9+
10+
alter table package_versions
11+
add constraint package_versions_registry_environment_check
12+
check (registry_environment in ('production', 'testnet-sandbox')),
13+
add constraint package_versions_chain_network_check
14+
check (chain_network in ('mainnet', 'testnet')),
15+
add constraint package_versions_environment_network_check
16+
check (
17+
(registry_environment = 'production' and chain_network = 'mainnet'
18+
and expires_at is null and purge_after is null)
19+
or
20+
(registry_environment = 'testnet-sandbox' and chain_network = 'testnet'
21+
and expires_at is not null and purge_after is not null and purge_after > expires_at)
22+
);
23+
24+
create index if not exists package_versions_expiry_idx
25+
on package_versions(expires_at)
26+
where expires_at is not null;
27+
28+
create index if not exists package_versions_object_purge_idx
29+
on package_versions(purge_after, snapshot_hash)
30+
where purge_after is not null and source_purged_at is null;

0 commit comments

Comments
 (0)