Initializations: version-addressed init contracts run atomically at publish - #86
Initializations: version-addressed init contracts run atomically at publish#86charlesHetterich wants to merge 5 commits into
Conversation
|
Revised the Solidity convention per review: initializations are now addressed by (contract, version) uniformly — Rust |
…les; initialize() entry point
|
Second revision, per review — net +4 lines while adding the shim build path:
|
Constructors are dead behind per-name proxies: delegatecall enters the callee's
callexport, neverdeploy, so constructor writes land in the implementation's own storage which nothing reads. #76 shipped "storage zero-defaults are your initialization" as the interim rule. This PR gives contracts real initialization: custom logic that runs exactly once, atomically, in the same transaction that publishes a version, directly against the name's proxy storage — one concept covering both first-publish setup and upgrade-time storage transformation.The model
An initialization is addressed by (contract, version). Rust:
<crate>/initializations/<version>.rs— the crate names the contract. Solidity:initializations/<Contract>/<version>.sol— the directory names the contract. One rule, two idiomatic projections — the same split the publish version already has (Cargo.toml[package].versionvs the@custom:cdmcolon suffix). The file addressed to a version runs exactly when that version is published.1.3.0; 1.4's init seesfrom = 1.2.Each initialization exposes one conventional entry point —
initialize(uint128 from, address owner)(0x3a67c2f8, pinned incontract-registry-coreand mirrored + drift-tested inproxy.ts) — wherefromis the previously-latest version key (0 on first publish) andowneris the name's registry owner. If it reverts, the entire publish rolls back (Utility.batch_all+ revert bubbling).Rust initializations are manifest-free: the user creates
initializations/<version>.rsand nothing else —cdm deploysynthesizes a throwaway shim crate (edition + dependencies copied from the contract's resolved cargo metadata, one[[bin]]pointing at the file) and builds it with cargo-pvm-contract. Each file is fully self-contained, embedding its own copy of the storage layout it operates on: published initializations become frozen text the evolving contract can never break, while the deploy-time layout guard keeps the CURRENT initialization honest against the CURRENT implementation. Solidity initializations live in a per-contract subdirectory whose name is the router (initializations/Counter/1.3.0.sol), and the file's contract must inherit that contract (contract Init_1_3_0 is Counter) — inheritance demoted to validation, which is also what guarantees the shared storage layout. The subdirectory is mandatory even for single-contract projects: a flat form would plant a rename trap the day a second contract appears, and two contracts can legitimately need an initialization at the same version. A flat-placed file errors with a message that teaches the layout; a directory naming no detected contract errors listing the known names.Frozen proxy: one new meta op
callCode(address,bytes)(0xd74c1f04, admin-gated): delegatecall an arbitrary address against the proxy's storage, bubbling return/revert verbatim. Deliberately live while the proxy is frozen — freeze halts the plain/versioned delegation planes only, so the freeze → publish-with-initialization → unfreeze migration window works end to end.This required regenerating the frozen per-name proxy blob — the point of landing this before the first paseo deploy of the versioned-proxy generation:
Fresh
deploy-registryruns upload the new blob and setsetProxyCodeHash;--upgradepushes it to live registries via the existingensureProxyCodeHashpath.Structural authorization: initialization contracts are never registered versions, so their selectors are unreachable through the proxy's plain/versioned planes.
callCode— registry-only — is the single path into proxy storage, which is why user initializations need no access-control boilerplate insideinitialize.Registry
publishWithInit(name, key, target, metadataUri, initTarget)— validate + record the version + repoint latest exactly likepublish, then composecallCode(initTarget, [initialize selector][from][owner])and call the proxy. NewInitialized(string indexed, uint128, address)event,InvalidInitTarget()error. The registry→proxy calldata is byte-locked by in-file dispatch tests on both sides of the wire (and mirrored in the TS drift tests);getAddress's frozen 64-byte format is untouched.Pipeline / CLI
X.Y.Z, errors on malformed spellings), Solidity directory routing with inheritance validation. Rust initializations build on demand through the generated shim crate — no manifest requirements to detect or enforce.batch_allchunk as its implementation (the chunker weighs impl+init pairs as one item so pairs never split), registered viapublishWithInit; salted<package>#initat the publish version.extra_output = ["storageLayout"], hardhat the equivalentoutputSelection.+initmarker on the publishing row's version cell.Templates
shared-counterkeeps its storage in one inline#[storage]struct anchored at#[slot(0)](packs identically to bare auto-numbered fields, and makes the build emit the storage layout the guard checks) and ships a workinginitializations/0.1.0.rs;foundry-countershipsinitializations/CounterA/0.1.0.sol. Empty constructors are gone —#[contract]emits a default deploy export. Both templates double as docs; the README gains an Initializations section.e2e (local PPN)
New
initializations.e2e.test.ts(Rust matrix, against the freshly built registry + refrozen blob):from=0read back through the proxy;from= previous latest, old versions read the transformed state via versioned calls;callCodeat the proxy is registry-only; zero init target rejected;solidity.e2e.test.tsnow drives the full pipeline over the template's realinitializations/CounterA/0.1.0.sol(owner set through the proxy, cross-VM) and adds the reverting-initialization rollback case. The full pre-existing e2e suite stays green alongside.Closes #77