Versioned per-name proxies: one stable address per contract, semver implementations behind it - #76
Open
charlesHetterich wants to merge 8 commits into
Open
Versioned per-name proxies: one stable address per contract, semver implementations behind it#76charlesHetterich wants to merge 8 commits into
charlesHetterich wants to merge 8 commits into
Conversation
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.
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Contributor
CDM CLI dev releaseThis PR includes a Install it: curl -fsSL https://raw.githubusercontent.com/paritytech/contract-dependency-manager/c2d9ac4fcc7c6fbfeae37ce18174a54d37f9de24/install.sh | CDM_TAG=cdm-cli-dev-pr-76 bashUpdate an existing install: cdm update --tag cdm-cli-dev-pr-76Release tag: |
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Today every version of
@org/nameis a separate contract at its own address with its own storage — so "upgrading" means abandoning state, and a consumer's version pin is really just a pointer to one frozen deployment. This PR makes the registry a factory: each name's first publish instantiates a minimal per-name proxy that permanently owns the name's address, storage, and balance. Versions become semver-keyed implementation contracts behind it, and the proxy can route any call to any published version — multiple versions live simultaneously over one shared state.(This is the direct answer to the "what are the guarantees for CDM consumers" thread: a consumer now gets one permanent address, real logic pinning — a call pinned to
1.2.3executes1.2.3's code — and state continuity across upgrades, with the owner'sminSupportedratchet as the explicit, auditable storage-migration escape hatch instead of silent breakage.)This is a clean break with the v1 era. Versions are semver strings everywhere; numeric indices no longer exist. The registry, CLI, and migration tooling do not read v1 registries, and legacy numeric
cdm.jsonpins fail with instructions to reinstall. (paseo-next was reset mid-development, so no live deployment depends on v1 anymore; PCF's devnet registry was already condemned and bootstraps fresh on v2.)The model
The per-name proxy (
contract-proxy,src/contract/proxy/) declares no methods — the contract's own ABI passes through untouched — and reserves exactly one calldata pattern:getAddress);[MAGIC][u128 versionKey][inner]→ delegate-call that exact version; below the owner's floor →UnsupportedVersion();[MAGIC][0][selector][args]→ the CDM meta plane: O(1) point queries riding on state the proxy needs anyway (implOf,latest,minSupported,frozen,admin) plus registry-onlypublish/setMinSupported/freeze/unfreeze/setAdmin. Enumeration is deliberately NOT in the frozen blob — that's the registry's catalog job (getVersionCount/getVersionAt).Every CDM contract also inherits an owner-controlled pause switch:
freezeContract(name)halts all delegation (plain and versioned revertContractFrozen()) while the meta plane and registry operations stay live, so the storage-migration flow is freeze → publish the reshaping version → unfreeze → ratchetminSupported— no window where old code can write mid-reshape.Because every call passes through CDM-owned proxy code, this doubles as an inheritance layer: behavior added to the proxy blob is inherited by every contract published under it, in a namespace (the magic prefix) that can never collide with the developer's own functions. The blob is frozen per name at first publish, so its surface is deliberately minimal; future blob generations (via
setProxyCodeHash) extend the inherited surface for names published after them.Both routers use the SDK's bump allocator (pure passthrough frames make a handful of one-shot allocations — no free/reuse to gain from a real malloc), which cuts the per-name proxy blob to 13.7 kB and the registry proxy to 2.5 kB; blob size is per-call code-loading weight, so this is recurring savings on every CDM call. The registry implementation keeps picoalloc (its paging/import paths churn allocations under a fixed arena). The contracts track cargo-pvm-contract
main(currently the #133 nested-storage API — chainedget()/entry()).Version keys pack semver as
major<<64 | minor<<32 | patch(u128), publishes must be strictly increasing per name, and a version can never be republished. The version's source of truth is the crate'sCargo.tomlpackage.version, which makescdm deployidempotent: anything at or below the registry's latest key is skipped as up-to-date.Proxies are CREATE2-instantiated by the registry itself (
salt = keccak256(name), zero-arg constructor, frozen blob atsrc/contract/proxy/artifacts/), so a name's address is a pure function of(registry, name, proxy blob)— offline-predictable, and identical across chains running the same registry address and blob generation. The registry stays the proxy's admin, so ownership/auth logic remains upgradable in the registry without touching deployed proxies.What changed, per layer
contract-registry-core— the wire format (versioning.rs): magic prefix, key packing, calldata routing, meta selectors; every constant hex-pinned by tests and mirrored (drift-tested) in TS.contract-registry—publish(name, key, target, uri)replacespublishLatest; factory instantiation;setMinSupportedforwarding + mirror; getters (getProxy,getLatestKey,getMinSupported,getVersionAt);setProxyCodeHashadmin surface; v2adminImportContracts(records only, proxy required); dispatch tests additionally lock the registry→proxy meta calldata bytes.@parity/cdm-builder—proxy.tswire mirror (browser-safe/proxysubpath); deploy pipeline reads Cargo versions, skips published keys, bakes stable addresses into Solidity imports, and warns if a published method's selector collides with the proxy call prefix; installs resolvelatest/exact/npm-range specs; metadata gains the implementation'sstorage_layout; frozen-artifact loader;deploy-registry.ts --upgrade(in-placesetCodewith consumed-salt probing); migration snapshots use thecdm.registry.v2schema.@org/pkg:1.2.3and@org/pkg:^1.2(the old parser silently truncated1.2.3to index1).cdm::import!— cdm.json versions are semver strings. The consumer call path is intentionally unchanged (runtimegetAddress); contract-side version pinning needs a small generic calldata-preamble hook in cargo-pvm-contract'sCallBuilderand is deferred — the wire format ships now, so pinned Rust calls activate later with no on-chain changes.Verification
getAddressand the meta wire format on both sides of the Rust/TS boundary.pnpm checkgreen end to end.make start EPHEMERAL=1): 4 suites, 47/47 — includingproxy.e2e.test.ts, which publishes the shared-counter blob twice and proves on-chain that a counter written through v1.1.0, written again through the superseded v1.0.0 via the calldata prefix, and read through both pinned versions is one and the same storage; plus meta-plane byte-exact queries, the ratchet and the freeze switch blocking calls while plain traffic/meta respectively continue, and monotonicity enforcement.Rollout
Fresh v2 deploy on the reset paseo-next (same CREATE3 registry address under the registry-deployer key; the deploy flow uploads the per-name proxy blob and sets
proxyCodeHash). PCF's devnet bootstraps fresh on v2 whenever they schedule it.Parked follow-ups: upstream
CallBuilderpreamble → pinned Rust consumer calls + baked proxy addresses;#[cdm::init]initializer sugar (v1 rule: storage defaults are your initialization); publish-time storage-layout diff warnings (the metadata field ships now); Solidity versioned-call wrapper.