Skip to content

Match each frozen record file to a released suite of its own - #241

Merged
thedavidmeister merged 2 commits into
mainfrom
fix-frozen-record-shared-bytecode
Sep 20, 2026
Merged

thedavidmeister merged 2 commits into
mainfrom
fix-frozen-record-shared-bytecode

Conversation

@thedavidmeister

@thedavidmeister thedavidmeister commented Sep 20, 2026 •

Copy link
Copy Markdown
Contributor

checkFrozenSnapshotsReleased asked, for each file in the frozen record, whether SOME
declared released suite derives the address that file records. The derived address is a
pure function of the creation code, so that question cannot tell two tags that froze
identical bytes apart — and identical bytes are not exotic: a release that changes only
dependencies, metadata or documentation compiles to the same creation code. This repo's
own record holds two such pairs (0_1_7/0_1_8 and 0_1_10/0_1_11), and so does
every other deploy repo in the org.

Either half of such a pair could be dropped from releasedSuites() while its own record
file went on being "declared" by its twin: the dropped key stops being dispatchable, its
frozen dependency list stops being checked, and the whole suite stays green. That is the
state the record check exists to make impossible.

The match is now one for one: N record files recording an address need N released suites
deriving it. Still by derived address and by nothing else — DeploySuite.suite is a
free-form key on a virtual declaration, so matching on a tag spelled inside it would make
this pass or fail on the shape of a string and would refuse every consumer that spells
its keys some other way. Counting needs no convention at all.

The inequality runs one way only: more declarations than record files at an address stays
passing, because a release deployed before a repo adopted this machinery has no frozen
record and never will.

FrozenSnapshotsOutnumberReleases is new and is raised only where
FrozenSnapshotNotReleased never could be: there ARE matching entries and there are not
enough of them.

No breaking change to DeploySuite — struct, fields, key alphabet and
releasedSuites() are untouched. The only API addition is the new error.

Known limit, documented in the source: an entry RENAMED rather than dropped is still
not caught — the counts are unchanged. Nothing in a DeploySuite says which tag it was
cut from.

Consumers

Checked against the new rule rather than assumed. All satisfy it as they stand; a bump
needs no source edit.

  • rain.factory.deploy — records 0_1_1/0_1_9/0_1_10, declares three entries; twin
    pair 0_1_9/0_1_10 is 2 records / 2 entries → passes, and issue docs: say what findDeployBlock, isStartBlock and the deploy actually do #33's mutants are
    both killed on bump.
  • rain.math.float.deploy — 4 record files, 4 entries; both contracts are twin pairs
    across those tags → 2/2 at each address → passes.
  • rain.tofu.erc20-decimals.deploy — 1 record file, 1 entry → passes.
  • rain.deploy itself — 8 record files, 8 entries, two twin pairs per contract → passes.

A consumer whose record holds more files recording one address than its releasedSuites()
names entries deriving it now goes red. For a repo generating its declaration with
writeReleasedSuitesLib, that state IS the defect and the fix is to regenerate.

QA

  • Discriminating tests: testFrozenSnapshotTwinReleaseDroppedFromTheDeclarationReverts
    fails on base — written first with a deliberately loose vm.expectRevert() so the
    first run was a real assertion failure rather than a compile error, and observed as
    [FAIL: next call did not revert as expected] against unmodified source while its
    three siblings passed in the same run. Controls that must NOT fire:
    testFrozenSnapshotTwinReleasesBothDeclaredPasses,
    testFrozenSnapshotMoreDeclarationsThanRecordFilesPasses (pins the one-way
    inequality), and testTwinReleasesFrozeTheSameCreationCode, which asserts the fixture's
    premise so it cannot go vacuous.
  • Mutations applied: end-to-end in this repo's own record — drop address-registry@0_1_11
    from the generated src/lib/LibAddressRegistryReleased.sol (array 4→3, entry removed),
    the M45 shape from the issue. Pre-fix source: testEveryFrozenSnapshotIsReleased
    PASSES (mutant survives). Fixed source, same mutation: FAILS with
    FrozenSnapshotsOutnumberReleases("src/generated/0_1_11/AddressRegistry.sol", 0xDBb7Cf1cba967d2D577045E138eEd359675a6fa7, 2, 1).
    Mutation reverted; tree clean.
  • Oracle: the frozen record on disk — the count of files recording an address, which is
    independent of the declaration being checked. The twin fixture uses the REAL append-only
    files src/generated/0_1_10/AddressRegistry.sol and 0_1_11/AddressRegistry.sol rather
    than a synthetic pair, so the premise is the repo's actual history.
  • Category check: Frozen-record check cannot see a release dropped from the declaration when two releases share bytecode rain.factory.deploy#33 asks that a release dropped from the
    declaration be caught regardless of whether another release shares its bytecode, without
    breaking consumers that declare arbitrary keys. Both covered. The issue suggested keying
    on the tag; that is deliberately NOT what this does, and the reason is in the body.

Suite: 490 → 494 passing from forge clean builds, with the same 76 failures before and
after, all vm.createFork/createSelectFork: environment variable '<NETWORK>_RPC_URL' not found (fork tests needing RPC env not present on the build machine). No existing test was
weakened, changed or deleted. forge fmt --check and forge lint -D warnings clean.

Closes rainlanguage/rain.factory.deploy#33

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Bug Fixes

    • Strengthened frozen snapshot verification to require one matching declaration for every released suite at each deployed address.
    • Prevented a single matching release from incorrectly validating multiple snapshot records.
    • Preserved distinct handling for snapshots with no released declaration.
  • Tests

    • Added coverage for duplicate deployment addresses and matching creation code.
    • Verified that all required declarations are enforced while additional valid declarations remain accepted.

`checkFrozenSnapshotsReleased` asked, for each file in the frozen record,
whether SOME declared released suite derives the address that file records.
The derived address is a pure function of the creation code, so that question
cannot tell two tags that froze identical bytes apart -- and identical bytes
are not exotic: a release that changes only dependencies, metadata or
documentation compiles to the same creation code, which is how `0_1_7`/`0_1_8`
and `0_1_10`/`0_1_11` came to be two such pairs in this repo`s own record.

Either half of such a pair could therefore be dropped from `releasedSuites()`
and its own record file would go on being "declared" by its twin: the dropped
key stops being dispatchable, the dropped entry`s frozen dependency list stops
being checked, and the whole suite stays green. That is the state the record
check exists to make impossible.

The match is now one for one: N record files recording an address need N
released suites deriving it. Still by derived address and by nothing else --
`DeploySuite.suite` is a free-form key on a virtual declaration, so matching
on a tag spelled inside it would make this pass or fail on the shape of a
string and would refuse every consumer that spells its keys some other way.
Counting needs no convention at all.

The inequality runs one way only. More declarations than record files at an
address stays passing, because a release deployed before a repo adopted this
machinery has no frozen record and never will.

`FrozenSnapshotsOutnumberReleases` is new and is raised only where
`FrozenSnapshotNotReleased` never could be: there ARE matching entries and
there are not enough of them, so a reader told "not released" would go looking
for an entry that is already there.

Reported against a consumer in rainlanguage/rain.factory.deploy#33.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 20, 2026 •

Copy link
Copy Markdown

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: ASSERTIVE

Plan: Advanced

Run ID: f17e638f-6c8a-41d1-bb02-6745c6e0b62d

📥 Commits

Reviewing files that changed from the base of the PR and between d2b541e and 2b80301.

📒 Files selected for processing (2)
  • src/abstract/RainDeployVerifySnapshotBase.sol
  • test/src/abstract/RainDeployVerifySnapshotBase.t.sol
💤 Files with no reviewable changes (1)
  • test/src/abstract/RainDeployVerifySnapshotBase.t.sol

Included review availability: Your plan provides up to 1 included review per hour; 0 remain after this review.


Walkthrough

The snapshot check now enforces one-to-one accounting between frozen record files and released suites that derive the same deployed address. It adds an error for excess records and tests twin releases with shared creation code.

Changes

Frozen snapshot accounting

Layer / File(s) Summary
Release accounting contract
src/abstract/RainDeployVerifySnapshotBase.sol
The contract documents the one-for-one release rule and adds FrozenSnapshotsOutnumberReleases for excess record files.
Release accounting implementation
src/abstract/RainDeployVerifySnapshotBase.sol
checkFrozenSnapshotsReleased counts released suites and earlier record files for each deployed address. It reverts when records outnumber declarations.
Duplicate-address release validation
test/src/abstract/RainDeployVerifySnapshotBase.t.sol
Tests cover twin releases with identical creation code, missing and matching declarations, and extra declarations.

Priority: ➖ Normal

Estimated code review effort: 3 (Moderate) | ~25 minutes

Change: Bug fix · Severity of issue fixed: Medium

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: matching each frozen record file to an individual released suite. It aligns with the implementation and PR objectives.
Linked Issues check ✅ Passed Issue #33 requires each frozen record to be covered by its own released declaration, including releases with identical derived addresses. checkFrozenSnapshotsReleased counts record files and release…
Out of Scope Changes check ✅ Passed The production changes implement the per-address accounting required by issue #33. The new error documentation and renamed-entry limitation describe the implemented behavior. The tests directly cover …
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check. Docstring coverage is scoped to functions touched by this diff. Analyzed 0 functions across 0…
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@thedavidmeister
thedavidmeister merged commit cdf7675 into main Sep 20, 2026
6 checks passed
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.

Frozen-record check cannot see a release dropped from the declaration when two releases share bytecode

1 participant