Skip to content

Versioned per-name proxies: one stable address per contract, semver implementations behind it - #76

Open
charlesHetterich wants to merge 25 commits into
mainfrom
versioned-proxies
Open

Versioned per-name proxies: one stable address per contract, semver implementations behind it#76
charlesHetterich wants to merge 25 commits into
mainfrom
versioned-proxies

Conversation

@charlesHetterich

@charlesHetterich charlesHetterich commented Aug 12, 2026

Copy link
Copy Markdown
Collaborator

Every @org/name now has one permanent address. The registry acts as a factory: a name's first publish instantiates a minimal per-name proxy that owns the address, storage, and balance forever; semver versions are implementation contracts behind it, and any published version can be called over the same shared state.

Breaking — clean break with v1. Versions are semver strings everywhere; numeric indices are gone. The CLI and migration tooling don't read v1 registries; old numeric cdm.json pins fail with a reinstall hint. (paseo-next was reset; nothing live depends on v1.)

Closes #77, #81.

How it works

  • Routing (per-name proxy, no methods of its own): plain calldata → latest version; [MAGIC][versionKey][calldata] → that exact version; [MAGIC][0][selector] → CDM meta ops (implOf, latest, minSupported, frozen, admin; registry-only publish, setMinSupported, freeze/unfreeze, callCode).
  • Versions come from Cargo.toml [package].version (Rust) or /// @custom:cdm @org/name:X.Y.Z (Solidity). Publishes must strictly increase; cdm deploy skips versions already on-chain, so it's idempotent.
  • Owner controls: setMinSupported ratchets a floor below which pinned calls revert; freeze/unfreeze halts all delegation for migrations.
  • Initializations (Initializations: version-addressed init contracts run atomically at publish #86): initializations/<version>.{rs,sol} runs once, atomically, when that version publishes — first-publish setup and upgrade migrations in one mechanism.
  • Install: cdm i @org/pkg:1.2.3 / :^1.2 / latest, resolved to an exact version; ABI is version-exact.
  • Addresses are CREATE2 from (registry, keccak(name), frozen proxy blob) — offline-predictable, identical across chains with the same registry address.

Proxy surface

Every name's proxy exposes the same frozen surface, reached via [MAGIC][0][selector]:

Functions
Queries (open) implOf(uint128), latest(), minSupported(), frozen(), admin()
Registry-only publish(uint128, address), setMinSupported(uint128), freeze(), unfreeze(), setAdmin(address), callCode(address, bytes)

Owners drive these through the registry: publish / publishWithInit, setMinSupported(name, key), freezeContract(name) / unfreezeContract(name). Registry queries: getAddress, getProxy, getLatestKey, getMinSupported, getVersionCount, getVersionAt. Registry admin: setCode, setProxyCodeHash, freeze / unfreeze, adminImportContracts.

On-chain

  • contract-registry v2: publish / publishWithInit, factory instantiation, setMinSupported, freezeContract, setProxyCodeHash, version getters. Upgradable in place via setCode.
  • contract-proxy (per-name, frozen blob 0x9c918a8b…542e86) and contract-registry-proxy, both on the bump allocator.
  • contract-registry-core: wire format and selectors, hex-pinned and drift-tested against the TS mirror.

Tooling

Builder (proxy.ts wire mirror, deploy pipeline, install resolution, storage-layout metadata, initialization shim builds, layout guard), CLI (version column, up-to-date state, +init marker), frontend (semver display, min-supported), cdm::import! (semver cdm.json), migrations (cdm.registry.v2), deploy-registry --upgrade. Solidity builds target the EVM backend (forge build, no resolc).

Not in this PR

Consumer-side version pinning (#78 TS, #79 Solidity, #80 Rust — wire format ships now), cdm deploy --dry-run (#85), initialization scaffolding (#88), source-drift warning (#84).

Verification

Rust host tests, TS unit suite, and five e2e suites against a local PPN (make start EPHEMERAL=1), including shared-storage-across-versions, ratchet/freeze, Solidity-through-proxy cross-VM calls, and initializations. CI e2e needs PPN_GITHUB_TOKEN.

Rollout

Fresh v2 deploy to reset paseo-next (same CREATE3 registry address) and extensive testing there before merging to main; PCF devnet bootstraps fresh on v2.

The registry becomes a factory: a name's first publish CREATE2-instantiates
a minimal per-name proxy (salt = keccak256(name), frozen blob, zero-arg
constructor so the address commits only to registry+name+blob) that
permanently owns the name's address, storage, and balance. Versions are
implementation contracts the proxy delegate-calls — plain calls route to
latest, [MAGIC][u128 key] calldata routes to an exact version, and a
[MAGIC][0][selector] meta plane serves CDM queries plus the registry-only
publish/setMinSupported/setAdmin ops. Version keys pack semver as
major<<64|minor<<32|patch, strictly increasing per name, so the version
list is sorted by construction; owners can ratchet a min-supported floor
below which pinned calls revert UnsupportedVersion().

The v1→v2 registry storage layout is append-only (info.proxy lands in a
previously-unwritten slot; key_of is a new mapping beside the untouched v1
tables), so setCode upgrades need no data migration: v1 records read back
as legacy entries with keys derived as 0.0.(index+1), resolve exactly as
before, and upgrade onto a proxy at their next publish. The getAddress
64-byte wire format is unchanged and now returns the stable proxy address.

cdm::import! accepts semver version strings in cdm.json (legacy numeric
indices still parse); the registry proxy crate moves to
src/contract/registry-proxy so contract-proxy can take src/contract/proxy.
… upgrades

proxy.ts mirrors the per-name proxy wire format (magic prefix, meta
selectors, slots, key packing) with every constant drift-tested against the
Rust pins, exposed browser-safe via the ./proxy subpath. Deploys read each
crate's Cargo.toml version, skip anything at or below the registry's latest
key (idempotent re-runs), salt implementation addresses by semver, publish
through the new registry ABI, and bake the stable per-name address into
Solidity imports. Installs resolve latest/exact/npm-range specs against the
on-chain version list (numeric pins keep resolving as legacy v1 indices)
and pin the resolved semver plus stable address in cdm.json. Metadata
gains the implementation's storage layout for future compat checks.

The contract-proxy blob is frozen under src/contract/proxy/artifacts with
a hash-verified loader (regenerate via freeze-proxy-artifact.ts); fresh
registry deploys upload it and set proxyCodeHash, and deploy-registry.ts
--upgrade performs in-place setCode upgrades with consumed-salt probing.
Migration snapshots gain a cdm.registry.v2 schema (version keys + per-name
proxies); v1 snapshots stay readable and import with derived legacy keys.
cdm deploy shows each crate's version and renders already-published crates
as up-to-date skips; the stable per-name address replaces the per-version
implementation address in tables and summaries. cdm install accepts exact
versions and npm-style ranges after the colon (the old parser silently
truncated 1.2.3 to index 1), passes cdm.json specs through verbatim, and
renders resolved semver. The frontend lists versions by key with their
implementation targets, marks releases below the owner's min-supported
floor, and labels the package's stable proxy address.
Resolves the harness and registry-suite conflicts by keeping the PPN
connect-only harness from main and re-applying the versioned-proxy
semantics: the harness additionally checks the contract-proxy blob, builds
the shared-counter template, and deploys raw blobs without dry-runs (the
Node/Bun instantiate divergence); the registry suite publishes semver keys
and asserts stable proxy addresses with per-run unique names; the new
proxy suite exercises multi-version shared state, the meta plane, and the
min-supported ratchet; the bulletin suite publishes with the v2 signature.
The proxy suite's raw versioned/meta calls (no ABI — the wire format is a
calldata prefix) now mirror product-sdk's exact papi shapes: the unsafe
api (generated descriptors lag PPN's runtime for these entries), dest as
a hex string, calldata as Uint8Array, and Revive.call's weight_limit
field (not gas_limit). Wire helpers import through the browser-safe
/proxy subpath — the package root loses named exports to the resolver's
tree-shaking under vitest, same story as /abi.

Template workspaces bump polkavm-derive 0.31 → 0.35 to match
cargo-pvm-contract main's polkavm line; 0.31's picosimd pin conflicts
with the current SDK resolution and broke template builds.

Full run against an ephemeral local PPN: 4 suites, 47/47 — including
live proof of two implementation versions sharing one proxy's storage,
the meta plane byte-for-byte, and the min-supported ratchet.
@github-actions

github-actions Bot commented Aug 12, 2026

Copy link
Copy Markdown
Contributor

CDM CLI dev release

This PR includes a @parity/cdm-cli changeset, so CI published a dev CLI release for this branch.

Install it:

curl -fsSL https://raw.githubusercontent.com/paritytech/contract-dependency-manager/ee8c189b85cede149402ffe9b16d57da2fb3f838/install.sh | CDM_TAG=cdm-cli-dev-pr-76 bash

Update an existing install:

cdm update --tag cdm-cli-dev-pr-76

Release tag: cdm-cli-dev-pr-76
Commit: ee8c189b85cede149402ffe9b16d57da2fb3f838

The proxy-based versioned system is now the only system. Versions are
semver strings everywhere: the registry drops the legacy branches,
derived 0.0.(index+1) keys, and the index-based getters (getVersionAt
covers enumeration); version rows merge into one VersionRecord mapping;
install/CLI/migrations lose the numeric-pin paths (old cdm.json pins fail
with reinstall instructions); v1 snapshots are neither exported nor
imported. Nothing live depends on v1 — paseo-next was reset and the PCF
devnet bootstraps fresh.

The per-name proxy keeps only what routing needs: the enumeration meta
ops (versionCount/versionAt/resolveMax) and their duplicate sorted key
list leave the frozen blob (27.5 → 24.1 kB); the O(1) point queries
(implOf/latest/minSupported/admin) stay, riding on state the proxy holds
anyway, with the latest key at a dedicated slot. The registry remains the
catalog for enumeration.

Also: cdm deploy now warns when a published method's selector collides
with the proxy call prefix, and the two changesets merge into one with an
explicit breaking notice.
… routers

Every CDM contract now inherits an owner-controlled pause:
freezeContract(name) halts all delegation at the per-name proxy (plain and
versioned calls revert ContractFrozen()) while the meta plane and registry
operations stay live — so a storage-reshaping migration is freeze →
publish → unfreeze → ratchet minSupported, with no window where old code
writes mid-reshape. New cdm.proxy.frozen slot and freeze/unfreeze/frozen
meta ops, registry forwarding with owner auth, wire mirror, and live e2e.

cargo-pvm-contract main replaced Mapping::view/view_mut with the #133
chained get()/entry() accessors; the registry adopts them (this is also
what CI compiles against — it floats on branch main by design, so the
Rust/TypeScript/Templates jobs red on the previous push go green here).

Both routers switch to the SDK's bump allocator: passthrough frames make
a handful of one-shot allocations, so a real malloc buys nothing — the
per-name proxy blob drops 26.1 → 13.7 kB and the registry proxy 15.4 →
2.5 kB, recurring per-call code-loading savings on every CDM call. The
registry implementation keeps picoalloc for its allocation-churning
paging and import paths.
.gitignore's blanket *.polkavm rule swallowed the frozen artifact the
loader hash-verifies against — the manifest was tracked, the blob wasn't,
which is exactly the CI TypeScript failure. Carve out the proxy artifacts
directory the same way create3's is.
@socket-security

socket-security Bot commented Sep 3, 2026

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Addednpm/​@​types/​semver@​7.8.01001007487100

View full report

Trim narrative and rationale comments to constraint-bearing content across the
per-name proxy, registry, and CDM tooling; drop dead exports and duplicate
tests; fix docs that contradicted the code (EVM backend, CREATE3 registry
deploy, proxy constructor args, import payloads).
Cleanup pass over the versioned-proxies branch
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Constructors are effectively ignored behind per-name proxies — initializer primitive + constructor sugar

1 participant