|
| 1 | +/- |
| 2 | +Incremental (proto-array-style) weight maintenance agrees with the |
| 3 | +naive re-tally. |
| 4 | +
|
| 5 | +Upstream's `_compute_lmd_ghost_head` re-tallies every vote on every call |
| 6 | +(`_accumulate_ancestor_weights` folded over the full LMD view). |
| 7 | +Production clients instead maintain the weight map incrementally: when |
| 8 | +the vote set changes, only the delta (votes added, votes removed is |
| 9 | +handled by re-extraction here — the LMD view is always a function of the |
| 10 | +pool) is credited. Issue #67 asks for the refinement theorem making that |
| 11 | +optimization a conformance target. |
| 12 | +
|
| 13 | +This file proves the algebraic core of that refinement: |
| 14 | +
|
| 15 | + - `Weights.Equiv` — extensional equality of weight maps (`get`-equal; |
| 16 | + association lists differing in entry order or zero entries are the |
| 17 | + same weight function). |
| 18 | + - `creditChain_get_add` / `accumulateAncestorWeights_append` — the |
| 19 | + tally is **pointwise additive**: crediting a batch is the pointwise |
| 20 | + sum of crediting its parts. This is exactly the property that makes |
| 21 | + per-vote weight *deltas* well-defined. |
| 22 | + - `accumulateAncestorWeights_perm` — the tally is **order-free**: any |
| 23 | + permutation of the vote list yields the same weight function, so an |
| 24 | + incremental maintainer may apply vote updates in any order. |
| 25 | + - `ghostWalk_congr_weights` / `computeLmdGhostHead_congr_weights` — |
| 26 | + the GHOST descent reads weights only through `get`, so extensionally |
| 27 | + equal weight maps select the same head (tie-break included). |
| 28 | +
|
| 29 | +Together: a client that maintains weights incrementally — in any order, |
| 30 | +batching however it likes — computes the same head as the naive spec |
| 31 | +walk, provided its maintained map is `get`-equal to the full tally. |
| 32 | +The best-child/best-descendant cache layer of proto-array sits on top |
| 33 | +and is follow-up work; its correctness reduces to the walk over the |
| 34 | +same weight function proved here. |
| 35 | +-/ |
| 36 | + |
| 37 | +import LeanSpec.Forks.Lstar.Store.Ancestry |
| 38 | + |
| 39 | +namespace LeanSpec.Forks.Lstar |
| 40 | +namespace Store |
| 41 | + |
| 42 | +namespace Weights |
| 43 | + |
| 44 | +/-- Extensional equality of weight maps: equal weight on every root. |
| 45 | +Association lists differing in order or explicit zeros are identified. -/ |
| 46 | +def Equiv (w w' : Weights) : Prop := ∀ r : Root, w.get r = w'.get r |
| 47 | + |
| 48 | +theorem Equiv.refl (w : Weights) : Equiv w w := fun _ => rfl |
| 49 | + |
| 50 | +theorem Equiv.symm {w w' : Weights} (h : Equiv w w') : Equiv w' w := |
| 51 | + fun r => (h r).symm |
| 52 | + |
| 53 | +theorem Equiv.trans {w₁ w₂ w₃ : Weights} (h1 : Equiv w₁ w₂) |
| 54 | + (h2 : Equiv w₂ w₃) : Equiv w₁ w₃ := |
| 55 | + fun r => (h1 r).trans (h2 r) |
| 56 | + |
| 57 | +/-- `find?` skips entries whose key differs from the searched root. -/ |
| 58 | +private theorem find?_filter_ne (w : Weights) (r x : Root) (hxr : ¬x = r) : |
| 59 | + (w.filter (fun q => !(q.1 == r))).find? (fun p => p.1 == x) |
| 60 | + = w.find? (fun p => p.1 == x) := by |
| 61 | + induction w with |
| 62 | + | nil => rfl |
| 63 | + | cons a t ih => |
| 64 | + by_cases har : a.1 = r |
| 65 | + · have hnax : ¬a.1 = x := fun hax => hxr (by rw [← hax, har]) |
| 66 | + rw [List.filter_cons] |
| 67 | + rw [if_neg (by simp [har])] |
| 68 | + rw [List.find?_cons_of_neg (by simp [hnax]), ih] |
| 69 | + · rw [List.filter_cons] |
| 70 | + rw [if_pos (by simp [har])] |
| 71 | + by_cases hax : a.1 = x |
| 72 | + · rw [List.find?_cons_of_pos (by simp [hax]), |
| 73 | + List.find?_cons_of_pos (by simp [hax])] |
| 74 | + · rw [List.find?_cons_of_neg (by simp [hax]), |
| 75 | + List.find?_cons_of_neg (by simp [hax]), ih] |
| 76 | + |
| 77 | +/-- `bump` adds exactly one to the bumped root and nothing elsewhere. -/ |
| 78 | +theorem get_bump (w : Weights) (r x : Root) : |
| 79 | + (w.bump r).get x = w.get x + (if x = r then 1 else 0) := by |
| 80 | + unfold bump |
| 81 | + cases hf : w.find? (fun p => p.1 == r) with |
| 82 | + | some p => |
| 83 | + by_cases hxr : x = r |
| 84 | + · subst hxr |
| 85 | + unfold get |
| 86 | + rw [List.find?_cons_of_pos (by simp)] |
| 87 | + simp only [Option.map_some, Option.getD_some] |
| 88 | + rw [hf]; rfl |
| 89 | + · unfold get |
| 90 | + have hnrx : ¬(r = x) := fun h => hxr h.symm |
| 91 | + rw [List.find?_cons_of_neg (by simp [hnrx]), |
| 92 | + find?_filter_ne w r x hxr] |
| 93 | + simp [hxr] |
| 94 | + | none => |
| 95 | + by_cases hxr : x = r |
| 96 | + · subst hxr |
| 97 | + unfold get |
| 98 | + rw [List.find?_cons_of_pos (by simp)] |
| 99 | + simp only [Option.map_some, Option.getD_some] |
| 100 | + rw [hf]; rfl |
| 101 | + · unfold get |
| 102 | + have hnrx : ¬(r = x) := fun h => hxr h.symm |
| 103 | + rw [List.find?_cons_of_neg (by simp [hnrx])] |
| 104 | + simp [hxr] |
| 105 | + |
| 106 | +end Weights |
| 107 | + |
| 108 | +/-- The chain credit is pointwise additive over its accumulator: crediting |
| 109 | +on top of `w` reads as `w` plus crediting from empty. This is the exact |
| 110 | +algebraic fact that makes per-vote weight deltas well-defined. -/ |
| 111 | +theorem creditChain_get_add (st : Store) (s : Slot) : |
| 112 | + ∀ (fuel : Nat) (r : Root) (w : Weights) (x : Root), |
| 113 | + (creditChain st s fuel r w).get x |
| 114 | + = w.get x + (creditChain st s fuel r []).get x |
| 115 | + | 0, _, _, _ => rfl |
| 116 | + | fuel + 1, r, w, x => by |
| 117 | + have h0 : Weights.get ([] : Weights) x = 0 := rfl |
| 118 | + unfold creditChain |
| 119 | + cases hb : st.getBlock? r with |
| 120 | + | none => dsimp only; omega |
| 121 | + | some b => |
| 122 | + dsimp only |
| 123 | + by_cases hle : b.slot ≤ s |
| 124 | + · rw [if_pos hle, if_pos hle]; omega |
| 125 | + · rw [if_neg hle, if_neg hle] |
| 126 | + have h1 := creditChain_get_add st s fuel b.parentRoot (w.bump r) x |
| 127 | + have h2 := creditChain_get_add st s fuel b.parentRoot |
| 128 | + (Weights.bump [] r) x |
| 129 | + have h3 := Weights.get_bump w r x |
| 130 | + have h4 := Weights.get_bump [] r x |
| 131 | + omega |
| 132 | + |
| 133 | +/-- Folding the credit step over a batch on top of any accumulator reads |
| 134 | +as the accumulator plus folding from empty. -/ |
| 135 | +private theorem tallyFold_get_add (st : Store) (s : Slot) : |
| 136 | + ∀ (l : List (Nat × AttestationData)) (w : Weights) (x : Root), |
| 137 | + (l.foldl (fun w att => |
| 138 | + creditChain st s (st.blocks.length + 1) att.2.head.root w) w).get x |
| 139 | + = w.get x + (l.foldl (fun w att => |
| 140 | + creditChain st s (st.blocks.length + 1) att.2.head.root w) []).get x |
| 141 | + | [], w, x => by |
| 142 | + have h0 : Weights.get ([] : Weights) x = 0 := rfl |
| 143 | + dsimp only [List.foldl_nil] |
| 144 | + omega |
| 145 | + | v :: t, w, x => by |
| 146 | + rw [List.foldl_cons, List.foldl_cons] |
| 147 | + have h1 := tallyFold_get_add st s t |
| 148 | + (creditChain st s (st.blocks.length + 1) v.2.head.root w) x |
| 149 | + have h2 := tallyFold_get_add st s t |
| 150 | + (creditChain st s (st.blocks.length + 1) v.2.head.root []) x |
| 151 | + have h3 := creditChain_get_add st s (st.blocks.length + 1) |
| 152 | + v.2.head.root w x |
| 153 | + omega |
| 154 | + |
| 155 | +/-- The batch tally is pointwise additive: tallying `a ++ b` is the |
| 156 | +pointwise sum of tallying each part. An incremental maintainer may |
| 157 | +therefore credit any new batch on top of an existing tally — the |
| 158 | +algebraic fact that makes per-vote weight *deltas* well-defined. -/ |
| 159 | +theorem accumulateAncestorWeights_append (st : Store) |
| 160 | + (a b : List (Nat × AttestationData)) (s : Slot) (x : Root) : |
| 161 | + (accumulateAncestorWeights st (a ++ b) s).get x |
| 162 | + = (accumulateAncestorWeights st a s).get x |
| 163 | + + (accumulateAncestorWeights st b s).get x := by |
| 164 | + unfold accumulateAncestorWeights |
| 165 | + rw [List.foldl_append] |
| 166 | + exact tallyFold_get_add st s b _ x |
| 167 | + |
| 168 | +/-- Prepending one vote adds its chain credit pointwise. -/ |
| 169 | +private theorem accumulate_cons_get (st : Store) |
| 170 | + (v : Nat × AttestationData) (l : List (Nat × AttestationData)) |
| 171 | + (s : Slot) (x : Root) : |
| 172 | + (accumulateAncestorWeights st (v :: l) s).get x |
| 173 | + = (accumulateAncestorWeights st [v] s).get x |
| 174 | + + (accumulateAncestorWeights st l s).get x := by |
| 175 | + have h := accumulateAncestorWeights_append st [v] l s x |
| 176 | + simpa using h |
| 177 | + |
| 178 | +/-- The tally is order-free: permuting the vote list leaves the weight of |
| 179 | +every root unchanged, so incremental updates may be applied in any |
| 180 | +order. -/ |
| 181 | +theorem accumulateAncestorWeights_perm (st : Store) |
| 182 | + {a b : List (Nat × AttestationData)} (hperm : a.Perm b) (s : Slot) |
| 183 | + (x : Root) : |
| 184 | + (accumulateAncestorWeights st a s).get x |
| 185 | + = (accumulateAncestorWeights st b s).get x := by |
| 186 | + induction hperm with |
| 187 | + | nil => rfl |
| 188 | + | cons v _ ih => |
| 189 | + rename_i l₁ l₂ _ |
| 190 | + have h1 := accumulate_cons_get st v l₁ s x |
| 191 | + have h2 := accumulate_cons_get st v l₂ s x |
| 192 | + omega |
| 193 | + | swap u v l => |
| 194 | + have h1 := accumulate_cons_get st v (u :: l) s x |
| 195 | + have h2 := accumulate_cons_get st u l s x |
| 196 | + have h3 := accumulate_cons_get st u (v :: l) s x |
| 197 | + have h4 := accumulate_cons_get st v l s x |
| 198 | + omega |
| 199 | + | trans _ _ ih1 ih2 => exact ih1.trans ih2 |
| 200 | + |
| 201 | +/-! ## The GHOST walk reads weights only through `get` -/ |
| 202 | + |
| 203 | +/-- Eligible children agree between extensionally equal weight maps. -/ |
| 204 | +theorem childrenOf_congr_weights (st : Store) {w w' : Weights} |
| 205 | + (h : Weights.Equiv w w') (minScore : Option Nat) (parent : Root) : |
| 206 | + childrenOf st w minScore parent = childrenOf st w' minScore parent := by |
| 207 | + unfold childrenOf |
| 208 | + congr 1 |
| 209 | + apply List.filter_congr |
| 210 | + intro p _ |
| 211 | + cases minScore with |
| 212 | + | none => rfl |
| 213 | + | some m => simp only [h p.1] |
| 214 | + |
| 215 | +/-- The child comparison agrees between extensionally equal weight maps. -/ |
| 216 | +theorem beats_congr_weights {w w' : Weights} (h : Weights.Equiv w w') |
| 217 | + (best cand : Root) : beats w best cand = beats w' best cand := by |
| 218 | + unfold beats |
| 219 | + rw [h best, h cand] |
| 220 | + |
| 221 | +/-- Folding a pick with pointwise-equal comparisons picks the same. -/ |
| 222 | +private theorem foldl_pick_congr {f g : Root → Root → Bool} |
| 223 | + (h : ∀ a b, f a b = g a b) : |
| 224 | + ∀ (cs : List Root) (a : Root), |
| 225 | + cs.foldl (fun best cand => if f best cand then cand else best) a |
| 226 | + = cs.foldl (fun best cand => if g best cand then cand else best) a |
| 227 | + | [], _ => rfl |
| 228 | + | c :: cs, a => by |
| 229 | + rw [List.foldl_cons, List.foldl_cons, h a c] |
| 230 | + exact foldl_pick_congr h cs _ |
| 231 | + |
| 232 | +/-- The winning child agrees between extensionally equal weight maps. -/ |
| 233 | +theorem maxChild_congr_weights {w w' : Weights} (h : Weights.Equiv w w') |
| 234 | + (cs : List Root) : maxChild w cs = maxChild w' cs := by |
| 235 | + cases cs with |
| 236 | + | nil => rfl |
| 237 | + | cons c t => |
| 238 | + unfold maxChild |
| 239 | + dsimp only |
| 240 | + rw [foldl_pick_congr (beats_congr_weights h) t c] |
| 241 | + |
| 242 | +/-- The GHOST descent agrees between extensionally equal weight maps: |
| 243 | +every step (child enumeration, threshold filter, comparison, tie-break) |
| 244 | +reads weights only through `get`. -/ |
| 245 | +theorem ghostWalk_congr_weights (st : Store) {w w' : Weights} |
| 246 | + (h : Weights.Equiv w w') (minScore : Option Nat) : |
| 247 | + ∀ (fuel : Nat) (head : Root), |
| 248 | + ghostWalk st w minScore fuel head |
| 249 | + = ghostWalk st w' minScore fuel head |
| 250 | + | 0, _ => rfl |
| 251 | + | fuel + 1, head => by |
| 252 | + unfold ghostWalk |
| 253 | + rw [childrenOf_congr_weights st h, maxChild_congr_weights h] |
| 254 | + cases maxChild w' (childrenOf st w' minScore head) with |
| 255 | + | none => rfl |
| 256 | + | some best => exact ghostWalk_congr_weights st h minScore fuel best |
| 257 | + |
| 258 | +/-- #67 (weight-delta layer): head selection depends on the vote tally |
| 259 | +only extensionally. A client maintaining the weight map incrementally — |
| 260 | +crediting batches in any order (`accumulateAncestorWeights_append`, |
| 261 | +`accumulateAncestorWeights_perm`) — selects exactly the head of the |
| 262 | +naive spec walk, tie-break included, as long as its maintained map is |
| 263 | +`get`-equal to the full tally. -/ |
| 264 | +theorem computeLmdGhostHead_incremental (st : Store) (startRoot : Root) |
| 265 | + (attestations : List (Nat × AttestationData)) |
| 266 | + {anchor : Block} (hanchor : st.getBlock? startRoot = some anchor) |
| 267 | + {w : Weights} |
| 268 | + (hw : Weights.Equiv w |
| 269 | + (accumulateAncestorWeights st attestations anchor.slot)) |
| 270 | + (minScore : Option Nat) : |
| 271 | + ghostWalk st w minScore (st.blocks.length + 1) startRoot |
| 272 | + = computeLmdGhostHead st startRoot attestations minScore := by |
| 273 | + unfold computeLmdGhostHead |
| 274 | + rw [hanchor] |
| 275 | + exact ghostWalk_congr_weights st hw minScore (st.blocks.length + 1) |
| 276 | + startRoot |
| 277 | + |
| 278 | +end Store |
| 279 | +end LeanSpec.Forks.Lstar |
0 commit comments