refactor: enforce frozen natively in StrictBaseModel#845
Merged
tcoratger merged 1 commit intoJun 6, 2026
Merged
Conversation
Restore the frozen constraint that leanEthereum#789 removed, this time enforced once in the base model instead of per class. Every spec type is now immutable by default, with no opt-outs: the State accumulator and the fork-choice Store are frozen too, and every remaining in-place mutation site is converted back to the model_copy(update=...) functional style. Follow-up to leanEthereum#842/leanEthereum#843, as discussed in the leanEthereum#843 review thread. Changes: - base.py: add frozen to StrictBaseModel; restore the pre-leanEthereum#789 docstring. - Delete the 17 per-class model_config | {"frozen": True} overrides (block, checkpoint, attestation, aggregation, validator, xmss, eth2) now that the base enforces them. - state_transition.py: process_slots rebinds through model_copy (the deepcopy barrier is no longer needed), process_block_header applies its updates atomically in one final copy, process_attestations returns a new state. - fork_choice.py, timeline.py, validator_duties.py, aggregation.py: every store update flows through model_copy; dicts and inner sets are shallow-copied before growing so the caller's store is left untouched. - node/chain/service.py, node/sync/service.py: rebind the store instead of patching it in place. - xmss/interface.py: advance_preparation returns a rebuilt secret key. - enr.py: from_rlp rebuilds the record with the computed node id. - packages/testing + tests: all fixture-setup mutations converted to model_copy rebinding; helpers that mutated arguments now return the new instance. - Restore test_frozen_rejects_assignment and the immutability wording in the SSZ patterns rule; add mirrored immutability tests for State and Store. Validation: - just check passes (ruff lint + format, ty, codespell, mdformat, lock) - Unit suites: lstar spec 257 passed, node 1682 passed, crypto/ssz/enr/containers/base 1660 passed Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
8c53875 to
1e22b1e
Compare
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.
cc @alexanderlhicks
What
Enforce immutability once, in the base model, instead of per class:
StrictBaseModelnow carries"frozen": Truenatively (restoring the pre-refactor: drop model_copy in favour of in-place mutation #789 constraint and docstring).model_config = Container.model_config | {"frozen": True}overrides from refactor(lstar): freeze constructed-once SSZ containers #842/refactor(lstar): freeze the Block family #843 are deleted — subclasses inherit the constraint.StateandStoreare frozen too, and every remaining in-place mutation site is converted back to themodel_copy(update=...)functional style.Follow-up to #842/#843, as discussed in the #843 review thread.
Why
Immutability is groundwork for formal verification: a block, checkpoint, state, or store never changes once it exists, so instances are safe to share by reference across fork choice, the chain, and attestations. Enforcing it in the base keeps the spec legible — the constraint lives in one place rather than being repeated on every container.
Migrated mutation sites
Spec (
src/lean_spec/spec/):state_transition.py—process_slotsrebinds throughmodel_copy(thedeepcopybarrier is no longer needed),process_block_headerapplies its updates atomically in one final copy,process_attestationsreturns a new state.fork_choice.py,timeline.py,validator_duties.py,aggregation.py— every store update flows throughmodel_copy; dicts and inner sets are shallow-copied before growing so the caller's store is left untouched.xmss/interface.py—advance_preparationreturns a rebuilt secret key instead of rotating the bottom trees in place.Node (
src/lean_spec/node/):chain/service.py,sync/service.py— rebind the store instead of patching it in place.networking/enr/enr.py—from_rlprebuilds the record with the computed node id.Test framework + tests:
packages/testingfixtures and ~110 test-side fixture-setup mutations converted tomodel_copyrebinding; helpers that mutated arguments now return the new instance.test_frozen_rejects_assignment(dropped in refactor: drop model_copy in favour of in-place mutation #789) and the immutability wording in the SSZ patterns rule; adds mirrored immutability tests forStateand the newcontainers/test_store.py.Caveat
Unchanged from #842/#843:
frozenis an enforcement aid, not a hard immutability guarantee (pydantic#12361).Validation
just check— clean (ruff lint + format, ty, codespell, mdformat, lock)hash_tree_root, so generated fixtures should be byte-identical; CI'sfillrun will confirm.🤖 Generated with Claude Code