Migrate to the rolling-candidate snapshot model - #5
Conversation
BuildPointers now writes src/generated/candidate/ unconditionally instead of a dir named after [package].version, and LibCloneFactoryDeploy aliases it, so the pins consumers import always describe the current source. script/cut-release.sh freezes candidate as src/generated/<tag>/ at release time and is the only thing that creates a numbered snapshot. LibCloneFactoryDeployCandidateTest ties the candidate snapshot to the source, to its own recorded address, and to the lib. The three existing frozen snapshots are untouched: 0_1_5 is the live headline pin and 0_1_3/0_1_4 hold the audited bytecode, all live on all five chains. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Warning Review limit reached
Next review available in: 105 minutes You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository. How can I continue?After more reviews become available, a review can be triggered using the To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews. How do review limits work?CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability. For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window. Please refer docs for additional details. Review details⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Plus Run ID: ⛔ Files ignored due to path filters (1)
📒 Files selected for processing (10)
✨ Finishing Touches🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
CI landed exactly as predicted at
That is an RPC quota on the |
|
🤖 ai:vetter |
|
🤖 ai:producer |
script/cut-release.sh is what rainix-tag-release runs as its snapshot-generate-cmd, so this is the code that will cut this repo's first real release (rain.factory#46 step 3). It froze the numbered snapshot from the committed candidate and only then re-ran BuildPointers. That order is silently wrong whenever the committed candidate has drifted from the source. The copy freezes the stale bytes into src/generated/<tag>/, which the frozen-snapshots-append-only gate then protects forever, and the regeneration immediately moves candidate on to the real ones. The release publishes a LibCloneFactoryDeploy aliasing the new address while permanently recording the old one as what that version shipped. Nothing downstream catches it. testCandidateSelfConsistent compares the REGENERATED candidate to the source, so it is green either way, and no test compares a numbered dir to candidate. The append-only gate only sees an added dir. The failure is invisible until someone tries to reproduce the release. Reordered so the frozen dir is a copy of a candidate already known to match the source, with forge fmt also moved ahead of the copy so the two are byte-identical rather than differing by formatting. Added a diff -r assertion after the copy so the property is checked, not assumed. No behaviour change when candidate is already fresh, which is the normal case CI enforces on main. This is the abnormal case: tagging while main is red. Docs corrected to match — CLAUDE.md stated the old order in two places.
…for the first release This branch's README already said "sol-v0.1.6 exists as a tag but its release run failed before publishing anything" without saying why. The why matters, because cutting the first real release (rain.factory#46 step 3) is the next human action in this repo and the same failure would eat it. Run 30097157490 reached "Verify live chain matches the fresh pins" and died with all five fork tests reporting `vm.createSelectFork: environment variable <NETWORK>_RPC_URL not found`. rainix-tag-release exported the endpoints under the SECRET names (RPC_URL_<NETWORK>_FORK) while foundry.toml's [rpc_endpoints] read the <NETWORK>_RPC_URL form, so every one resolved empty. Publish, commit-back and GitHub Release were skipped -- hence a tag with no revision, no release and no 0_1_6 snapshot. That was a rainix defect, since fixed there: the reusable now runs rpc-preflight, binding each env name foundry reads to an endpoint probed healthy at that moment. Recorded with the two consequences a releaser needs: sol-v0.1.6 is spent (it names a commit five behind main; cut a fresh tag, though the 0.1.6 version number is still free on the registry since nothing published), and the fork RPCs still gate the release -- they are currently intermittent, and a transient failure there means tagging again, not rerunning. Also corrected the cut-release.sh description one paragraph up to match the regenerate-then-freeze order this branch now uses.
|
@coderabbitai review This PR has never been reviewed — CodeRabbit has zero reviews on it (it began reviewing this repo around 2026-08-08, after this PR was opened on 07-25, and did not retro-review). Requesting a full review of the current head before it is considered for merge. |
|
|
Migrates this repo to the rolling-candidate model that
S01-Issuer/st0x.deploy#283
adopted, mirroring it rather than inventing a variant. Adapted to this repo's
shape: one contract, one generated pin lib, no multi-contract alias machinery.
What changes
src/generated/candidate/is new and rolling.script/BuildPointers.solnow writes it unconditionally —
deployTag()went fromvm.parseTomlString(vm.readFile("foundry.toml"), ".package.version")to apurefunction returning the constant
"candidate". It is regenerated in full on everyrun and committed, so it always describes what the current source compiles to.
src/lib/LibCloneFactoryDeploy.solaliasescandidate/instead of0_1_5/.This is a semantic no-op today:
candidate/CloneFactory.pointers.solisbyte-identical to
0_1_5/CloneFactory.pointers.sol(diffis empty), because0_1_5is what the current source compiles to. Same address0x7053c1D000192f1FeC1fbe20350139F3300dBBaD, same codehash. What changes is therule: the consumer-facing pin now tracks the source instead of tracking a
version number.
script/cut-release.shis new — the direct analogue of st0x's. Itcp -rscandidatetosrc/generated/<tag>/, refusing on a non-X.Y.Zversion (theappend-only gate's tag filter is numeric-only, so a
0_1_7-rc1dir would be anorphan snapshot nothing protects) and refusing to overwrite an existing tag dir.
package-release.yaml'ssnapshot-generate-cmdbecomesbash script/cut-release.sh; the rest of the release wiring was already correct(
on: push: tags: sol-v*→rainix-tag-release) and is untouched.testCandidateSelfConsistentreplaces the version↔snapshot coupling. Newtest/src/lib/LibCloneFactoryDeployCandidate.t.solpins the whole chain:[package].versionis no longer read by any Solidity in this repo, so the{ access = "read", path = "./foundry.toml" }fs permission is dropped too.Frozen snapshots: all three kept
0_1_5MUST survive and does. It is the repo's headline pin, live on allfive supported chains, and the only implementation the current source can build
(
ICloneableFactoryV3—cloneDeterministic,keccak256(abi.encode(deployer, salt))namespacing, 5-argNewClone). It is unaudited, and st0x#283's "keeponly the audited snapshot" move would have deleted it. Deleting it would delete
the reason this repo exists. It stays, unaudited, on purpose.
0_1_3/0_1_4also stay. They hold the Protofire-auditedICloneableFactoryV2bytecode (0x444acC29d63fa643E8adCC35FD9aa6DE111dCb39),which is also live on all five chains and is the pin
S01-Issuer/st0x.deployconsumes in production (via the
rain-factory0.1.1 Soldeer package).So this migration touches nothing under
src/generated/0_1_*. Simulating thegate's own diff against
main:candidateis invisible to the gate by construction —rainix-static/src/frozen_snapshots.rs:19-25is_tag()requires exactly three_-separated all-numeric parts, and"candidate".split('_')has one. So it canroll forever without any rainix change.
candidate/was generated fresh, nevergit mv'd from a numbered dir (the gate passes--no-renames, so a move wouldhave been recorded as a deletion and flagged).
0_1_4is a byte-identical duplicate — and I am leaving it0_1_3and0_1_4are the same file (sha256fa92b0caa1084172240189245d1f6f7f018a56bec1f2c3d884ffe0ee358d225afor both) —one bytecode snapshotted twice by autopublish version bumps, exactly the noise
#283 deleted from st0x. Dropping it would break nothing functional: no Soldeer
consumer exists (this repo has zero published revisions), no git tag names
it, its address is identical to
0_1_3's so no deployment record would be lost,and the only code referencing it is its own test pair in
LibCloneFactoryDeployTaggedConstants.t.sol.But the
frozen-snapshots-append-onlygate would block it.parse_offendersflags statusDas well asM, so deletingsrc/generated/0_1_4/CloneFactory.pointers.solproduces an offender and turnsrainix-sol / staticred. st0x#283 got past exactly this by being mergedred under a one-off human sign-off. I am not doing that here: it is a separate
decision from the model migration, and it would trade a green PR for a cosmetic
cleanup.
0_1_4stays. If you want it collapsed, that is its owndeliberately-red PR with its own sign-off.
Supersedes #4
#4 (
17b69fb,"Enforce that [package].version, its snapshot and the deploy lib agree")
implements the model this PR removes. It should be closed when this merges.
Its three assertions, disposed of individually:
testDeployTag—DEPLOY_TAG == foundry.toml [package].version. Dropped,not folded. The candidate model deliberately abolishes that invariant:
DEPLOY_TAGis now the literal"candidate"and the version names nothing ondisk, so the assertion would be false by design. This is also the weakness
that motivates the change — it compares a name to a name, so editing
CloneFactorywithout regenerating leaves it green while the pins silentlymisdescribe the source.
testDeployTagSnapshotExists— folded as a compile-time guarantee. Thecandidate test imports
../../../src/generated/candidate/CloneFactory.pointers.soldirectly, so amissing snapshot is a compile error, not a runtime assertion. Enforce that [package].version, its snapshot and the deploy lib agree #4 needed
vm.existsonly because itsDEPLOY_TAGstring was decoupled from anyimport.
testDeployTagSnapshotHoldsTheAliasedPins— folded, and strengthened.Enforce that [package].version, its snapshot and the deploy lib agree #4 grepped the snapshot's file text for the rendered literals. The
equivalent here is
testCandidateIsTheAliasedSnapshot, which asserts theidentity in Solidity against the imported constants — no file parsing — and it
catches the real hazard Enforce that [package].version, its snapshot and the deploy lib agree #4 was aiming at: the generated lib left pointing at a
frozen dir while
candidatemoves on. Mutation-proven below.The
DEPLOY_TAGconstant itself — #4's genuinely good idea, making the aliasedsnapshot readable without reading an import path — is kept, generated as
"candidate".Mutation proof
A self-consistency test nobody has seen fail is worthless, so both directions
were broken and restored.
Mutant 1 — candidate disagrees with current source. One nibble flipped in
candidate/CloneFactory.pointers.solline 21 col 300(
…90f35b61004b6100…→…90f35b61f04b6100…), i.e. the committed pin no longerrecords what the source compiles to:
Mutant 2 — generated lib left pointing at a frozen dir. Import repointed from
../generated/candidate/to../generated/0_1_3/— the stale-headline-pinscenario #4's third assertion was written for:
Both restored with
git checkout -- <file>, green again:Generator is reproducible
From the committed tree, regenerating produces no diff:
Docs
README.mdsaid[package].versionand the snapshot "only ever move together"and implied a release history. Neither was true — Soldeer has zero
rain-factory-deployrevisions; nothing has ever been released from this repo.(
sol-v0.1.6exists as a tag but its run died before publishing anything.)README and
CLAUDE.mdnow describe the candidate model, andfoundry.tomlcarries a note that
version = "0.1.5"coincides withsrc/generated/0_1_5/byhistory, not by rule.
I deliberately did not bump the version: which number the first real release
carries is yours to pick, and it is worth knowing
sol-v0.1.6is already takenas a tag even though it published nothing.
Open ruling for you — not decided here
st0x#283 could delete every unaudited snapshot because there audited == live ==
depended-on. Here they diverge:
0x444acC29…(0_1_3/0_1_4, V2)1a92a86+v0.1.1)rain-factory0.1.1)0x7053c1D0…(0_1_5, V3)The repo's headline pin and only buildable implementation is unaudited, and the
audited implementation is not in the current source at all. Whether to audit V3
or revert to V2 is your call and this PR deliberately does not touch it — which
is a further argument for the add-only shape: the migration is orthogonal to that
ruling and pre-empts it in neither direction.
One adjacent fact worth seeing: the split is incomplete.
rain.factorymainstill carries the entire deploy half (
src/concrete/CloneFactory.sol,src/generated/0_1_3|0_1_4|0_1_5,src/lib/LibCloneFactoryDeploy.sol,script/BuildPointers.sol,script/Deploy.soland all four test files), so bothpins currently have two owners.
Behaviour change to be aware of
Because the pin lib now tracks
candidate, any change toCloneFactory'sbytecode makes the five
LibCloneFactoryDeployProdTestfork tests red until thatbytecode is deployed. Previously they asserted "prod matches
0_1_5"; now theyassert "prod matches HEAD". That is the deploy-before-merge coupling made
explicit, and it is documented in
CLAUDE.md.Verification
forge build,forge fmt --checkandreuse lintall pass.forge testis24/24 green locally, including all five fork tests run against public RPC
endpoints — which also confirms
candidate(==0_1_5) matches prod on everysupported chain.
In CI expect exactly one red:
testProdDeployArbitrum, failing withlb.drpc.live … error code -32001— an exhausted drpc quota on theRPC_URL_ARBITRUM_FORKsecret. It is pre-existing onmainand unrelated tothis change; Arbitrum is confirmed green on chain by direct
cast codeprobe, sothe red is purely the RPC.
rainix-sol / staticshould be green — this PRadds only, so the append-only gate has nothing to flag.
🤖 Generated with Claude Code
Corrections, 2026-08-13
The "Open ruling for you" table above is out of date on one row. It said
0x7053c1D0…(0_1_5, V3) was unaudited. It is not: Protofire r3.0, filed inrain.factoryon 2026-07-31 (rainlanguage/rain.factory#49), audits commitc29d5827/ tagsol-v0.1.5withsrc/concrete/CloneFactory.solandsrc/lib/LibCloneFactoryDeploy.solin scope and zero findings at every severity.src/generated/0_1_5/CloneFactory.pointers.solat that audited commit is byte-identical to this repo's copy (sha256d3def164…), so the audited tree pins the same address and codehash. The "audit V3 or revert to V2" ruling the table asked for therefore does not arise — V3 is audited, clean. #6 carries the inherited record.The "Known CI red" note is also stale. The drpc-quota red is gone: rainix landed an archive-aware fork-RPC failover preflight (rainlanguage/rainix#289, merged 2026-08-08, plus #295 on 08-11), and this PR is now green on all three checks.
QA
Adding the evidence block the vetter rejected
5640894for.Discriminating tests.
test/src/lib/LibCloneFactoryDeployCandidate.t.sol(4), pinning each link ofcurrent source -> candidate CREATION_CODE -> candidate DEPLOYED_ADDRESS -> LibCloneFactoryDeploy.CLONE_FACTORY_DEPLOYED_ADDRESS:testCandidateSelfConsistent— the committedcandidatesnapshot'sCREATION_CODEequalstype(CloneFactory).creationCode. This is the gate that makes a rolling snapshot safe: editCloneFactorywithout regenerating and it fails.testCandidateIsTheAliasedSnapshot—DEPLOY_TAG == "candidate"and the lib's address/codehash equal the candidate snapshot's. Catches the generated lib being left aliasing a frozen numbered dir whilecandidatemoves on. Asserted in Solidity against imported constants, not by grepping file text.testCandidateRuntimeHashesToBytecodeHash—keccak256(RUNTIME_CODE) == BYTECODE_HASH, the same internal consistency the frozen tags are held to.testCandidateCreationDeploysToPinnedAddress— deploying the recordedCREATION_CODEthrough the etched Zoltu factory lands at the recordedDEPLOYED_ADDRESSwith the recorded codehash, so freezingcandidateat release time records a reproducible deployment.Mutations applied, and what killed each. Applied one at a time to the committed tree, whole suite run, tree restored, baseline green.
candidate/CREATION_CODEbyte flipped (snapshot stale vs source)testCandidateSelfConsistent,testCandidateCreationDeploysToPinnedAddress../generated/0_1_5/while candidate moves ontestCandidateIsTheAliasedSnapshotDEPLOY_TAGset to a version string instead of"candidate"testCandidateIsTheAliasedSnapshotcandidate/BYTECODE_HASHhand-editedtestCandidateRuntimeHashesToBytecodeHash,testCandidateIsTheAliasedSnapshotcandidate/DEPLOYED_ADDRESShand-editedtestCandidateCreationDeploysToPinnedAddress,testCandidateIsTheAliasedSnapshotdeployTag()returning a numbered dir instead of the constanttestCandidateSelfConsistentthen fails on the unchangedcandidateThe freeze path, which the vetter flagged as untested bash, is now covered by an in-script assertion and was exercised directly.
script/cut-release.shwas also wrong in ordering and is fixed in7288d30: it froze the numbered dir from the committedcandidateand only then regenerated, so acandidatethat had drifted from source would be frozen stale into an append-only dir while the regeneration silently movedcandidateon — publishing one address and permanently recording another, with nothing downstream able to catch it (testCandidateSelfConsistentchecks the regenerated candidate; no test compares a numbered dir tocandidate). Now regenerate →forge fmt→ copy →diff -rassertion that the frozen dir andcandidateare byte-identical.Exercised with
forgestubbed, since the bash is what is under test:Also covered by the same run: the strict
X.Y.Zversion guard, the missing-candidateguard, and a clean tree afterwards.Oracle —
S01-Issuer/st0x.deploy#283as the model being mirrored,rainix-static/src/frozen_snapshots.rsis_tag()read directly for whycandidateis invisible to the append-only gate (three_-separated all-numeric parts;"candidate"has one), andrainix-tag-release.yamlatmainread directly for the release step order the script has to slot into. All independent of this diff.Category check. The ask is to migrate this repo to the rolling-candidate model. Covered:
candidate/added and rolling, the pin lib aliases it,[package].versionno longer read by any Solidity (and itsfs_permissionsread dropped),cut-release.shfreezes at release time,package-release.yamlwired to it, docs rewritten, and all three frozen0_1_*snapshots untouched (git diff --diff-filter=MD main...HEAD -- src/generatedis empty).Still open for the human, not decided here: the vetter's point that
cut-release.shhand-rolls the freeze in bash rather than using a sharedLibSnapshotmechanism stands. The ordering bug above is an argument in its favour. It is a cross-repo refactor, not this PR.