Skip to content

Commit ef9f229

Browse files
authored
feat(stor): track upstream #1182 block-horizon guards in insertBlock (#57)
Mirror the pending leanEthereum/leanSpec#1182 (head 5e1b7b51, fixing issue #1171): on_block bounds a block's slot right after the parent lookup, before the state transition's empty-slot loop can run - at most HISTORICAL_ROOTS_LIMIT beyond the parent state (BLOCK_SLOT_GAP_TOO_LARGE) and at most one slot past the store clock (BLOCK_TOO_FAR_IN_FUTURE). STError gains both reasons and Config gains HISTORICAL_ROOTS_LIMIT. insertBlock now reads the parent *state*, exactly as on_block does; parentsPresent_insertBlock therefore takes the #1176 M-4 blocks-states alignment as a hypothesis to carry presence over to the block map. insertBlock_slot_gap_bounded and insertBlock_within_horizon extract the two bounds from a successful insertion - the formal content of issue #1171's fix. Hold this PR until upstream #1182 merges; re-verify the mirrored guards against the merged diff then.
1 parent ac99516 commit ef9f229

4 files changed

Lines changed: 124 additions & 34 deletions

File tree

LeanSpec/Forks/Lstar/Config.lean

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,4 +26,9 @@ deliberately not a whole slot (`GOSSIP_DISPARITY_INTERVALS`, a `Uint64`
2626
upstream). -/
2727
def GOSSIP_DISPARITY_INTERVALS : Nat := 1
2828

29+
/-- SSZ limit on the historical-roots list, doubling as the bound on how
30+
far a block's slot may run beyond its parent
31+
(`HISTORICAL_ROOTS_LIMIT`, a `Uint64` upstream). -/
32+
def HISTORICAL_ROOTS_LIMIT : Nat := 2 ^ 18
33+
2934
end LeanSpec.Forks.Lstar

LeanSpec/Forks/Lstar/Errors.lean

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ function and fork-choice attestation validation:
3232
- `attestationSlotBeforeHead` ↔ `ATTESTATION_SLOT_BEFORE_HEAD`
3333
- `attestationTooFarInFuture` ↔ `ATTESTATION_TOO_FAR_IN_FUTURE`
3434
- `unknownParentBlock` ↔ `UNKNOWN_PARENT_BLOCK`
35+
- `blockSlotGapTooLarge` ↔ `BLOCK_SLOT_GAP_TOO_LARGE`
36+
- `blockTooFarInFuture` ↔ `BLOCK_TOO_FAR_IN_FUTURE`
37+
(both added by the pending
38+
leanEthereum/leanSpec#1182,
39+
fixing issue #1171)
3540
3641
The `STError` name is historical — the state-transition function was
3742
modeled first; the type now carries every modeled rejection reason, like
@@ -84,6 +89,8 @@ inductive STError where
8489
| attestationSlotBeforeHead (slot head : Slot) : STError
8590
| attestationTooFarInFuture (slot maxAdmissible : Nat) : STError
8691
| unknownParentBlock (root : Root) : STError
92+
| blockSlotGapTooLarge (slot parentSlot : Slot) : STError
93+
| blockTooFarInFuture (slot : Slot) (maxAdmissible : Nat) : STError
8794
deriving Repr, BEq, Inhabited
8895

8996
/-- Result of a fallible state-transition step. -/

LeanSpec/Storage/Blocks.lean

Lines changed: 111 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,47 @@
11
/-
2-
Block-store chain structure: every stored block's parent is stored.
2+
Block-store chain structure: every stored block's parent is stored, and
3+
block acceptance is horizon-bounded.
34
45
The catalog sources STOR-1 to a `Database.add_block` parent-existence
56
precondition; in current leanSpec no such database method exists — the
67
gate lives in fork choice: `on_block`
7-
(`src/lean_spec/spec/forks/lstar/fork_choice.py`) rejects a block whose
8-
parent state is not in the store (`UNKNOWN_PARENT_BLOCK`) before
8+
(`src/lean_spec/spec/forks/lstar/fork_choice.py`) looks up the parent
9+
*state* and rejects `UNKNOWN_PARENT_BLOCK` when it is absent, before
910
`SyncService._persist_block` writes anything, so a block reaches the
1011
block map only when its parent is already there. The exception is the
1112
chain anchor: `create_store` seeds the map with a block whose parent is
1213
outside the tree — the zero hash for a genesis anchor, an absent block
1314
for a checkpoint-sync anchor.
1415
15-
Modeled here as the guarded insertion `insertBlock` (the parent gate of
16-
`on_block`, with the upstream `dict` replace-by-key) on the fork-choice
17-
`Store`, and the invariant `ParentsPresent` it maintains.
16+
This file also tracks the pending leanEthereum/leanSpec#1182 (head
17+
`5e1b7b51`, fixing issue #1171): right after the parent lookup,
18+
`on_block` bounds the block's slot before the empty-slot loop of the
19+
state transition runs — the slot may run at most
20+
`HISTORICAL_ROOTS_LIMIT` beyond the parent state
21+
(`BLOCK_SLOT_GAP_TOO_LARGE`), and at most one slot past the store clock
22+
(`BLOCK_TOO_FAR_IN_FUTURE`). Re-verify the mirrored guards against the
23+
merged diff when #1182 lands.
1824
19-
Proves STOR-1 from `docs/lean4-proof-propositions.md`:
25+
Modeled as the guarded insertion `insertBlock` (the `on_block` gate
26+
sequence at the point a block enters the block map, with the upstream
27+
`dict` replace-by-key) on the fork-choice `Store`, and the invariant
28+
`ParentsPresent` it maintains. The gate reads the *states* map exactly
29+
as upstream does; carrying its presence over to the *blocks* map is the
30+
blocks-states alignment invariant (`Store.WellFormed.blocksStatesAligned`,
31+
issue #1176 M-4), which enters the preservation theorem as a
32+
hypothesis.
33+
34+
Proves STOR-1 from `docs/lean4-proof-propositions.md` (and the horizon
35+
bounds of issue #1171):
2036
- STOR-1: every non-anchor block has its parent in the store —
2137
anchoring establishes the invariant (`parentsPresent_anchor`),
2238
guarded insertion preserves it (`parentsPresent_insertBlock`), and
23-
on a genesis-anchored store it takes the catalog's form: a stored
24-
block's parent is the zero hash or itself stored
39+
on a genesis-anchored store it takes the catalog's form
2540
(`parent_exists_or_genesis`).
41+
- An accepted block sits at most `HISTORICAL_ROOTS_LIMIT` beyond its
42+
parent state (`insertBlock_slot_gap_bounded`) and at most one slot
43+
past the store clock (`insertBlock_within_horizon`) — the formal
44+
content of the #1182 fix.
2645
-/
2746

2847
import LeanSpec.Forks.Lstar.Store.Ancestry
@@ -36,17 +55,29 @@ namespace Store
3655

3756
open LeanSpec.Forks.Lstar.Store
3857

39-
/-- Insert a block under its root, gated on the parent being known —
40-
the `UNKNOWN_PARENT_BLOCK` rejection of `on_block`, at the point where
41-
the block enters the store's block map (upstream `dict` assignment,
42-
replacing any entry with the same root). -/
58+
/-- Insert a block under its root, gated as `on_block` gates it at the
59+
point where the block enters the store's block map: the parent *state*
60+
must be known (`UNKNOWN_PARENT_BLOCK`), the slot may run at most
61+
`HISTORICAL_ROOTS_LIMIT` beyond the parent (`BLOCK_SLOT_GAP_TOO_LARGE`
62+
— the empty-slot loop in the transition runs once per slot from the
63+
parent to the block), and at most one slot past the store clock
64+
(`BLOCK_TOO_FAR_IN_FUTURE`), per the pending leanEthereum/leanSpec#1182.
65+
The insertion is the upstream `dict` assignment, replacing any entry
66+
with the same root. Python's negative slot gap and the truncated `Nat`
67+
subtraction both pass the gap guard. -/
4368
def insertBlock (st : LeanSpec.Forks.Lstar.Store) (root : Root)
4469
(b : Block) : ST.Result LeanSpec.Forks.Lstar.Store :=
45-
if (st.getBlock? b.parentRoot).isSome then
46-
.ok { st with
47-
blocks := (root, b) :: st.blocks.filter (fun p => !(p.1 == root)) }
48-
else
49-
.error (.unknownParentBlock b.parentRoot)
70+
match st.getState? b.parentRoot with
71+
| none => .error (.unknownParentBlock b.parentRoot)
72+
| some parentState =>
73+
if HISTORICAL_ROOTS_LIMIT < b.slot.toNat - parentState.slot.toNat then
74+
.error (.blockSlotGapTooLarge b.slot parentState.slot)
75+
else if st.time.toNat / INTERVALS_PER_SLOT + 1 < b.slot.toNat then
76+
.error (.blockTooFarInFuture b.slot
77+
(st.time.toNat / INTERVALS_PER_SLOT + 1))
78+
else
79+
.ok { st with
80+
blocks := (root, b) :: st.blocks.filter (fun p => !(p.1 == root)) }
5081

5182
/-- STOR-1 invariant: every stored block is the chain anchor or has its
5283
parent stored (the anchor's parent is outside the tree — the zero hash
@@ -113,31 +144,42 @@ private theorem getBlock?_isSome_insert
113144
exact this st.blocks h
114145

115146
/-- STOR-1, preservation: the parent-gated insertion keeps every stored
116-
block's parent stored — the new block's parent was required present,
147+
block's parent stored. The gate reads the states map, as upstream does;
148+
the blocks-states alignment (`Store.WellFormed.blocksStatesAligned`,
149+
issue #1176 M-4) carries the parent's presence over to the block map,
117150
and a replace-by-key insertion never makes a present root absent. -/
118151
theorem parentsPresent_insertBlock
119152
(st st' : LeanSpec.Forks.Lstar.Store) (anchorRoot root : Root)
120153
(b : Block)
154+
(halign : ∀ r : Root,
155+
(st.getBlock? r).isSome ↔ (st.getState? r).isSome)
121156
(hpp : ParentsPresent st anchorRoot)
122157
(h : insertBlock st root b = .ok st') :
123158
ParentsPresent st' anchorRoot := by
124159
unfold insertBlock at h
125160
split at h
126-
· next hparent =>
127-
injection h with h'
128-
subst h'
129-
intro p hp
130-
cases List.mem_cons.mp hp with
131-
| inl hnew =>
132-
subst hnew
133-
exact .inr (getBlock?_isSome_insert st root b _ hparent)
134-
| inr hold =>
135-
have hmem := (List.mem_filter.mp hold).1
136-
cases hpp p hmem with
137-
| inl hanchor => exact .inl hanchor
138-
| inr hpresent =>
139-
exact .inr (getBlock?_isSome_insert st root b _ hpresent)
140161
· simp at h
162+
· next parentState hparent =>
163+
split at h
164+
· simp at h
165+
· split at h
166+
· simp at h
167+
· injection h with h'
168+
subst h'
169+
intro p hp
170+
cases List.mem_cons.mp hp with
171+
| inl hnew =>
172+
subst hnew
173+
have hstate : (st.getState? b.parentRoot).isSome := by
174+
rw [hparent]; rfl
175+
exact .inr (getBlock?_isSome_insert st root b _
176+
((halign b.parentRoot).mpr hstate))
177+
| inr hold =>
178+
have hmem := (List.mem_filter.mp hold).1
179+
cases hpp p hmem with
180+
| inl hanchor => exact .inl hanchor
181+
| inr hpresent =>
182+
exact .inr (getBlock?_isSome_insert st root b _ hpresent)
141183

142184
/-- STOR-1, catalog form: on a genesis-anchored store — the invariant
143185
plus an anchor whose block carries the zero-hash parent — every stored
@@ -155,5 +197,41 @@ theorem parent_exists_or_genesis (st : LeanSpec.Forks.Lstar.Store)
155197
| inl hroot => exact .inl (hanchor p hp hroot)
156198
| inr hpresent => exact .inr hpresent
157199

200+
/-! ## Horizon bounds (issue #1171, pending fix leanEthereum/leanSpec#1182) -/
201+
202+
/-- An accepted block names a stored parent state and sits at most
203+
`HISTORICAL_ROOTS_LIMIT` beyond it — the empty-slot loop the state
204+
transition runs from the parent to the block is bounded. -/
205+
theorem insertBlock_slot_gap_bounded
206+
(st st' : LeanSpec.Forks.Lstar.Store) (root : Root) (b : Block)
207+
(h : insertBlock st root b = .ok st') :
208+
∃ parentState, st.getState? b.parentRoot = some parentState ∧
209+
b.slot.toNat - parentState.slot.toNat ≤ HISTORICAL_ROOTS_LIMIT := by
210+
unfold insertBlock at h
211+
split at h
212+
· simp at h
213+
· next parentState hparent =>
214+
split at h
215+
· simp at h
216+
· next hgap =>
217+
split at h
218+
· simp at h
219+
· exact ⟨parentState, hparent, Nat.le_of_not_lt hgap⟩
220+
221+
/-- An accepted block sits at most one slot past the store clock — the
222+
future-slot horizon issue #1171 asked for. -/
223+
theorem insertBlock_within_horizon
224+
(st st' : LeanSpec.Forks.Lstar.Store) (root : Root) (b : Block)
225+
(h : insertBlock st root b = .ok st') :
226+
b.slot.toNat ≤ st.time.toNat / INTERVALS_PER_SLOT + 1 := by
227+
unfold insertBlock at h
228+
split at h
229+
· simp at h
230+
· split at h
231+
· simp at h
232+
· split at h
233+
· simp at h
234+
· next hhorizon => exact Nat.le_of_not_lt hhorizon
235+
158236
end Store
159237
end LeanSpec.Storage

docs/lean4-proof-propositions.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -494,7 +494,7 @@ The propositions here guarantee **chain-structure consistency and write atomicit
494494
- [x] **STOR-1: Every non-genesis Block has its parent in the store**
495495
- Source: `Database.add_block` (parent-existence precondition; no such database method exists in current leanSpec — the gate is `on_block`'s `UNKNOWN_PARENT_BLOCK` rejection in `src/lean_spec/spec/forks/lstar/fork_choice.py`, which runs before `SyncService._persist_block` writes anything)
496496
- Note: Each block in the store has a parent block root (`parent_root`); for non-genesis blocks the parent must exist in `store.blocks` (the `block_root → Block` map). The exception generalizes beyond genesis: `create_store` seeds the map with a chain anchor whose parent is outside the tree (zero hash for genesis, an absent block for a checkpoint-sync anchor), so the invariant is stated relative to the anchor root.
497-
- Proved at: `LeanSpec/Storage/Blocks.lean` (`Store.parentsPresent_anchor` establishes the invariant at anchoring, `Store.parentsPresent_insertBlock` shows the parent-gated insertion preserves it, and `Store.parent_exists_or_genesis` is the catalog form on a genesis-anchored store)
497+
- Proved at: `LeanSpec/Storage/Blocks.lean` (`Store.parentsPresent_anchor` establishes the invariant at anchoring, `Store.parentsPresent_insertBlock` shows the parent-gated insertion preserves it — the gate reads the states map as upstream does, so the #1176 M-4 blocks-states alignment enters as a hypothesis — and `Store.parent_exists_or_genesis` is the catalog form on a genesis-anchored store). `insertBlock` also carries the horizon guards of the pending leanEthereum/leanSpec#1182 (fixing issue #1171): `insertBlock_slot_gap_bounded` and `insertBlock_within_horizon` bound an accepted block's slot against the parent and the store clock.
498498
- Sample code:
499499
500500
```lean

0 commit comments

Comments
 (0)