Skip to content

refactor: enforce frozen natively in StrictBaseModel#845

Merged
tcoratger merged 1 commit into
leanEthereum:mainfrom
tcoratger:refactor/frozen-by-default-strict-base-model
Jun 6, 2026
Merged

refactor: enforce frozen natively in StrictBaseModel#845
tcoratger merged 1 commit into
leanEthereum:mainfrom
tcoratger:refactor/frozen-by-default-strict-base-model

Conversation

@tcoratger
Copy link
Copy Markdown
Collaborator

@tcoratger tcoratger commented Jun 5, 2026

cc @alexanderlhicks

What

Enforce immutability once, in the base model, instead of per class:

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.pyprocess_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.
  • xmss/interface.pyadvance_preparation returns 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.pyfrom_rlp rebuilds the record with the computed node id.

Test framework + tests:

  • packages/testing fixtures and ~110 test-side fixture-setup mutations converted to model_copy rebinding; helpers that mutated arguments now return the new instance.
  • Restores 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 for State and the new containers/test_store.py.

Caveat

Unchanged from #842/#843: frozen is an enforcement aid, not a hard immutability guarantee (pydantic#12361).

Validation

  • just check — clean (ruff lint + format, ty, codespell, mdformat, lock)
  • Unit suites: lstar spec 257 passed, node 1682 passed, crypto/ssz/enr/containers/base 1660 passed
  • Every migration preserves hash_tree_root, so generated fixtures should be byte-identical; CI's fill run will confirm.

🤖 Generated with Claude Code

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>
@tcoratger tcoratger force-pushed the refactor/frozen-by-default-strict-base-model branch from 8c53875 to 1e22b1e Compare June 6, 2026 08:20
@tcoratger tcoratger merged commit 5cee882 into leanEthereum:main Jun 6, 2026
13 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.

1 participant