From fd8accb42e4fc52441fd23f7a08b867d57c719f2 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Fri, 26 Sep 2025 17:28:31 -0600 Subject: [PATCH 01/29] Implemented certificate rule --- analysis/markov/.gitignore | 1 + analysis/markov/Linleios.lean | 2 + analysis/markov/Linleios/Evolve.lean | 74 ++++++++++++++++++++++ analysis/markov/Main.lean | 11 ++++ analysis/markov/lake-manifest.json | 95 ++++++++++++++++++++++++++++ analysis/markov/lakefile.toml | 26 ++++++++ analysis/markov/lean-toolchain | 1 + 7 files changed, 210 insertions(+) create mode 100644 analysis/markov/.gitignore create mode 100644 analysis/markov/Linleios.lean create mode 100644 analysis/markov/Linleios/Evolve.lean create mode 100644 analysis/markov/Main.lean create mode 100644 analysis/markov/lake-manifest.json create mode 100644 analysis/markov/lakefile.toml create mode 100644 analysis/markov/lean-toolchain diff --git a/analysis/markov/.gitignore b/analysis/markov/.gitignore new file mode 100644 index 000000000..bfb30ec8c --- /dev/null +++ b/analysis/markov/.gitignore @@ -0,0 +1 @@ +/.lake diff --git a/analysis/markov/Linleios.lean b/analysis/markov/Linleios.lean new file mode 100644 index 000000000..7fe097c8a --- /dev/null +++ b/analysis/markov/Linleios.lean @@ -0,0 +1,2 @@ + +import Linleios.Evolve diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean new file mode 100644 index 000000000..12f844b3b --- /dev/null +++ b/analysis/markov/Linleios/Evolve.lean @@ -0,0 +1,74 @@ + +import Std.Data.HashMap +import Batteries.Lean.HashMap + +open Std (HashMap) + + +abbrev Probability := Float + + +structure Environment where + activeSlotCoefficient : Probability + Lheader : Nat + Lvote : Nat + Ldiff : Nat + pSpacingOkay : Probability + +def makeEnvironment (activeSlotCoefficient : Float) (Lheader Lvote Ldiff : Nat) : Environment := + { + activeSlotCoefficient := activeSlotCoefficient + Lheader := Lheader + Lvote := Lvote + Ldiff := Ldiff + pSpacingOkay := (1 - activeSlotCoefficient).pow (3 * Lheader + Lvote + Ldiff - 1).toFloat + } + + +structure State where + rbCount : Nat + ebCount : Nat +deriving Repr, BEq, Hashable, Inhabited + +example : (default : State).rbCount = 0 := rfl + +example : (default : State).ebCount = 0 := rfl + + +def Probabilities := HashMap State Probability +deriving Repr, EmptyCollection + +instance : Inhabited Probabilities where + default := (∅ : Probabilities).insert Inhabited.default 1 + + +def forge {env : Environment} (state : State) : List (State × Probability) := + [ + ⟨ + { + state with + rbCount := state.rbCount + 1 + } + , env.pSpacingOkay + ⟩ + , ⟨ + { + state with + rbCount := state.rbCount + 1 + ebCount := state.ebCount + 1 + } + , env.pSpacingOkay + ⟩ + ] + + +def evolve (transition : State → List (State × Probability)) : Probabilities → Probabilities := + HashMap.fold + ( + fun acc state p => + HashMap.mergeWith (fun _ => Add.add) acc + ∘ HashMap.map (fun _ => Mul.mul p ∘ List.sum ∘ List.map Prod.snd) + ∘ List.groupByKey Prod.fst + $ transition state + ) + ∅ diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean new file mode 100644 index 000000000..e0fed88ae --- /dev/null +++ b/analysis/markov/Main.lean @@ -0,0 +1,11 @@ + +import Linleios + + +def env : Environment := makeEnvironment 0.05 1 4 7 + +def s0 : Probabilities := default +def s1 := evolve (@forge env) s0 + +def main : IO Unit := + IO.println $ (reprPrec s1 0).pretty diff --git a/analysis/markov/lake-manifest.json b/analysis/markov/lake-manifest.json new file mode 100644 index 000000000..2ccb685d7 --- /dev/null +++ b/analysis/markov/lake-manifest.json @@ -0,0 +1,95 @@ +{"version": "1.1.0", + "packagesDir": ".lake/packages", + "packages": + [{"url": "https://github.com/leanprover-community/mathlib4", + "type": "git", + "subDir": null, + "scope": "leanprover-community", + "rev": "c211948581bde9846a99e32d97a03f0d5307c31e", + "name": "mathlib", + "manifestFile": "lake-manifest.json", + "inputRev": "v4.20.0", + "inherited": false, + "configFile": "lakefile.lean"}, + {"url": "https://github.com/leanprover-community/plausible", + "type": "git", + "subDir": null, + "scope": "leanprover-community", + "rev": "2ac43674e92a695e96caac19f4002b25434636da", + "name": "plausible", + "manifestFile": "lake-manifest.json", + "inputRev": "main", + "inherited": true, + "configFile": "lakefile.toml"}, + {"url": "https://github.com/leanprover-community/LeanSearchClient", + "type": "git", + "subDir": null, + "scope": "leanprover-community", + "rev": "6c62474116f525d2814f0157bb468bf3a4f9f120", + "name": "LeanSearchClient", + "manifestFile": "lake-manifest.json", + "inputRev": "main", + "inherited": true, + "configFile": "lakefile.toml"}, + {"url": "https://github.com/leanprover-community/import-graph", + "type": "git", + "subDir": null, + "scope": "leanprover-community", + "rev": "a11bcb5238149ae5d8a0aa5e2f8eddf8a3a9b27d", + "name": "importGraph", + "manifestFile": "lake-manifest.json", + "inputRev": "main", + "inherited": true, + "configFile": "lakefile.toml"}, + {"url": "https://github.com/leanprover-community/ProofWidgets4", + "type": "git", + "subDir": null, + "scope": "leanprover-community", + "rev": "21e6a0522cd2ae6cf88e9da99a1dd010408ab306", + "name": "proofwidgets", + "manifestFile": "lake-manifest.json", + "inputRev": "v0.0.60", + "inherited": true, + "configFile": "lakefile.lean"}, + {"url": "https://github.com/leanprover-community/aesop", + "type": "git", + "subDir": null, + "scope": "leanprover-community", + "rev": "ddfca7829bf8aa4083cdf9633935dddbb28b7b2a", + "name": "aesop", + "manifestFile": "lake-manifest.json", + "inputRev": "master", + "inherited": true, + "configFile": "lakefile.toml"}, + {"url": "https://github.com/leanprover-community/quote4", + "type": "git", + "subDir": null, + "scope": "leanprover-community", + "rev": "2865ea099ab1dd8d6fc93381d77a4ac87a85527a", + "name": "Qq", + "manifestFile": "lake-manifest.json", + "inputRev": "master", + "inherited": true, + "configFile": "lakefile.toml"}, + {"url": "https://github.com/leanprover-community/batteries", + "type": "git", + "subDir": null, + "scope": "leanprover-community", + "rev": "7a0d63fbf8fd350e891868a06d9927efa545ac1e", + "name": "batteries", + "manifestFile": "lake-manifest.json", + "inputRev": "main", + "inherited": true, + "configFile": "lakefile.toml"}, + {"url": "https://github.com/leanprover/lean4-cli", + "type": "git", + "subDir": null, + "scope": "leanprover", + "rev": "f9e25dcbed001489c53bceeb1f1d50bbaf7451d4", + "name": "Cli", + "manifestFile": "lake-manifest.json", + "inputRev": "main", + "inherited": true, + "configFile": "lakefile.toml"}], + "name": "linleios", + "lakeDir": ".lake"} diff --git a/analysis/markov/lakefile.toml b/analysis/markov/lakefile.toml new file mode 100644 index 000000000..3bc2fd8ad --- /dev/null +++ b/analysis/markov/lakefile.toml @@ -0,0 +1,26 @@ +name = "linleios" +version = "0.1.0" +keywords = ["math"] +defaultTargets = ["linleios"] + +[[lean_lib]] +name = "Linleios" + +[[lean_exe]] +name = "linleios" +root = "Main" + +[leanOptions] +pp.unicode.fun = true # pretty-prints `fun a ↦ b` +autoImplicit = false + +[[require]] +name = "mathlib" +scope = "leanprover-community" +version = "git#v4.20.0" + +[lean] +server-options = [ + ["checkBinderAnnotations", "false"], + ["diagnostics", "true"] +] diff --git a/analysis/markov/lean-toolchain b/analysis/markov/lean-toolchain new file mode 100644 index 000000000..52fe77473 --- /dev/null +++ b/analysis/markov/lean-toolchain @@ -0,0 +1 @@ +leanprover/lean4:v4.20.0 From b2d0fc18e3899637da1f96d6886f0127932163df Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Fri, 26 Sep 2025 17:34:11 -0600 Subject: [PATCH 02/29] Added Lean4 workflow --- .github/workflows/lean_action_ci.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 .github/workflows/lean_action_ci.yml diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml new file mode 100644 index 000000000..82dbb7176 --- /dev/null +++ b/.github/workflows/lean_action_ci.yml @@ -0,0 +1,19 @@ +name: Lean Action CI + +on: + push: + pull_request: + workflow_dispatch: + +jobs: + build: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v4 + with: + path: analsys/markov + + - uses: leanprover/lean-action@v1 + with: + path: analsys/markov From 042362e2e1bd1099336d551eced38020bfd3e341 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Fri, 26 Sep 2025 17:35:50 -0600 Subject: [PATCH 03/29] Revised Lean4 action --- .github/workflows/lean_action_ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml index 82dbb7176..24328298b 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -12,8 +12,8 @@ jobs: steps: - uses: actions/checkout@v4 with: - path: analsys/markov + lake-package-directory: analsys/markov - uses: leanprover/lean-action@v1 with: - path: analsys/markov + lake-package-directory: analsys/markov From 11c36e40ca7d95581a726fd323fff8155e39d990 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Fri, 26 Sep 2025 17:39:57 -0600 Subject: [PATCH 04/29] Fix to CI --- .github/workflows/lean_action_ci.yml | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml index 24328298b..29383cebd 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -1,9 +1,14 @@ name: Lean Action CI on: - push: pull_request: - workflow_dispatch: + paths: + - "analysis/markov/**" + push: + branches: + - main + paths: + - "analysis/markov/**" jobs: build: @@ -11,9 +16,6 @@ jobs: steps: - uses: actions/checkout@v4 - with: - lake-package-directory: analsys/markov - - uses: leanprover/lean-action@v1 with: lake-package-directory: analsys/markov From e8888215de06800c0f82ad044985229816272f59 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Fri, 26 Sep 2025 17:41:06 -0600 Subject: [PATCH 05/29] Fixed typo --- .github/workflows/lean_action_ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml index 29383cebd..3b6e9f012 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -18,4 +18,4 @@ jobs: - uses: actions/checkout@v4 - uses: leanprover/lean-action@v1 with: - lake-package-directory: analsys/markov + lake-package-directory: analysis/markov From c4415463afb05441b8aa39b73e65e0785f457c99 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Mon, 29 Sep 2025 09:11:29 -0600 Subject: [PATCH 06/29] Implemented simulation function --- .github/workflows/lean_action_ci.yml | 1 - analysis/markov/Linleios/Evolve.lean | 9 ++++++++- analysis/markov/Main.lean | 7 +++++-- 3 files changed, 13 insertions(+), 4 deletions(-) diff --git a/.github/workflows/lean_action_ci.yml b/.github/workflows/lean_action_ci.yml index 3b6e9f012..58c72ad02 100644 --- a/.github/workflows/lean_action_ci.yml +++ b/.github/workflows/lean_action_ci.yml @@ -13,7 +13,6 @@ on: jobs: build: runs-on: ubuntu-latest - steps: - uses: actions/checkout@v4 - uses: leanprover/lean-action@v1 diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index 12f844b3b..ede4d3e3e 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -49,7 +49,7 @@ def forge {env : Environment} (state : State) : List (State × Probability) := state with rbCount := state.rbCount + 1 } - , env.pSpacingOkay + , 1 - env.pSpacingOkay ⟩ , ⟨ { @@ -72,3 +72,10 @@ def evolve (transition : State → List (State × Probability)) : Probabilities $ transition state ) ∅ + +def simulate (transition : State → List (State × Probability)) (start : Probabilities) : Nat → Probabilities +| 0 => start +| n + 1 => simulate transition (evolve transition start) n + +def totalProbability (states : Probabilities) : Probability := + states.values.sum diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index e0fed88ae..d93471314 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -5,7 +5,10 @@ import Linleios def env : Environment := makeEnvironment 0.05 1 4 7 def s0 : Probabilities := default -def s1 := evolve (@forge env) s0 +def sn := simulate (@forge env) s0 5 +def pn := totalProbability sn def main : IO Unit := - IO.println $ (reprPrec s1 0).pretty + do + IO.println $ (reprPrec sn 0).pretty + IO.println $ (reprPrec pn 0).pretty From 97dcb663e5f1cad643fd89ce722bf2a41dc1b4b0 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Mon, 29 Sep 2025 10:18:25 -0600 Subject: [PATCH 07/29] Implemented pruning --- analysis/markov/Linleios/Evolve.lean | 7 +++++-- analysis/markov/Main.lean | 7 ++++--- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index ede4d3e3e..5cd8aa61d 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -74,8 +74,11 @@ def evolve (transition : State → List (State × Probability)) : Probabilities ∅ def simulate (transition : State → List (State × Probability)) (start : Probabilities) : Nat → Probabilities -| 0 => start -| n + 1 => simulate transition (evolve transition start) n + | 0 => start + | n + 1 => simulate transition (evolve transition start) n + +def prune (ε : Float) : Probabilities → Probabilities := + HashMap.filter (fun _ p => p ≥ ε) def totalProbability (states : Probabilities) : Probability := states.values.sum diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index d93471314..2a44670d9 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -5,10 +5,11 @@ import Linleios def env : Environment := makeEnvironment 0.05 1 4 7 def s0 : Probabilities := default -def sn := simulate (@forge env) s0 5 +def sn := simulate (@forge env) s0 50 def pn := totalProbability sn def main : IO Unit := do - IO.println $ (reprPrec sn 0).pretty - IO.println $ (reprPrec pn 0).pretty + let print {α : Type} [Repr α] (x : α) : IO Unit := IO.println $ (reprPrec x 0).pretty + print sn + print pn From 0a986963e7d079601440b7f81d7696650bf5e087 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Mon, 29 Sep 2025 15:12:26 -0600 Subject: [PATCH 08/29] Work in progress on probability of quorum --- analysis/markov/Linleios/Evolve.lean | 71 +++++++++++++++++++--------- analysis/markov/Main.lean | 8 ++-- analysis/sims/kernels.nix | 1 + 3 files changed, 54 insertions(+), 26 deletions(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index 5cd8aa61d..63e221981 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -8,26 +8,40 @@ open Std (HashMap) abbrev Probability := Float +def approxCommittee (committeeSize : Float) : Float × Float := + let nPools : Nat := 2500 + let stakes : List Float := (List.range nPools).map (fun k => ((k + 1).toFloat / nPools.toFloat)^10 - (k.toFloat / nPools.toFloat)^10) + let ps : List Float := stakes.map (fun s => 1 - Float.exp (- committeeSize * s)) + let μ : Float := ps.sum + let σ := (ps.map (fun p => p * (1 - p))).sum.sqrt + ⟨ μ , σ ⟩ + + structure Environment where activeSlotCoefficient : Probability Lheader : Nat Lvote : Nat Ldiff : Nat pSpacingOkay : Probability + pRbHeaderArrives : Probability + pEbValidates : Probability -def makeEnvironment (activeSlotCoefficient : Float) (Lheader Lvote Ldiff : Nat) : Environment := +def makeEnvironment (activeSlotCoefficient pRbHeaderArrives pEbValidates : Float) (Lheader Lvote Ldiff : Nat) : Environment := { activeSlotCoefficient := activeSlotCoefficient Lheader := Lheader Lvote := Lvote Ldiff := Ldiff pSpacingOkay := (1 - activeSlotCoefficient).pow (3 * Lheader + Lvote + Ldiff - 1).toFloat + pRbHeaderArrives := pRbHeaderArrives + pEbValidates := pEbValidates } structure State where rbCount : Nat ebCount : Nat + canCertify : Bool deriving Repr, BEq, Hashable, Inhabited example : (default : State).rbCount = 0 := rfl @@ -42,27 +56,34 @@ instance : Inhabited Probabilities where default := (∅ : Probabilities).insert Inhabited.default 1 -def forge {env : Environment} (state : State) : List (State × Probability) := +abbrev Outcomes := List (State × Probability) + + +def forge {env : Environment} (state : State) : Outcomes := + let state' := + { + state with + rbCount := state.rbCount + 1 + canCertify := true + } + if state.canCertify + then [ + ⟨{state' with ebCount := state.ebCount + 1}, env.pSpacingOkay⟩ + , ⟨state', 1 - env.pSpacingOkay⟩ + ] + else [(state', 1)] + +def validate {env : Environment} (state : State) : Outcomes := [ - ⟨ - { - state with - rbCount := state.rbCount + 1 - } - , 1 - env.pSpacingOkay - ⟩ - , ⟨ - { - state with - rbCount := state.rbCount + 1 - ebCount := state.ebCount + 1 - } - , env.pSpacingOkay - ⟩ + (state, env.pRbHeaderArrives * env.pEbValidates) + , ({state with canCertify := false}, 1 - env.pRbHeaderArrives * env.pEbValidates) ] +def step {env : Environment} : List (State → Outcomes) := + [@forge env, @validate env] + -def evolve (transition : State → List (State × Probability)) : Probabilities → Probabilities := +def evolve (transition : State → Outcomes) : Probabilities → Probabilities := HashMap.fold ( fun acc state p => @@ -73,12 +94,18 @@ def evolve (transition : State → List (State × Probability)) : Probabilities ) ∅ -def simulate (transition : State → List (State × Probability)) (start : Probabilities) : Nat → Probabilities - | 0 => start - | n + 1 => simulate transition (evolve transition start) n +def chain (transitions : List (State → Outcomes)) : Probabilities → Probabilities := + match transitions with + | [] => id + | (t :: ts) => chain ts ∘ evolve t + +def simulate (env : Environment) (start : Probabilities) : Nat → Probabilities +| 0 => start +| n + 1 => let state' := chain (@step env) start + simulate env state' n def prune (ε : Float) : Probabilities → Probabilities := - HashMap.filter (fun _ p => p ≥ ε) + HashMap.filter (fun _ p => p > ε) def totalProbability (states : Probabilities) : Probability := states.values.sum diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index 2a44670d9..ef7b6bf01 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -2,14 +2,14 @@ import Linleios -def env : Environment := makeEnvironment 0.05 1 4 7 +def env : Environment := makeEnvironment 0.05 0.95 0.90 1 4 7 def s0 : Probabilities := default -def sn := simulate (@forge env) s0 50 +def sn := simulate env s0 10 def pn := totalProbability sn def main : IO Unit := do - let print {α : Type} [Repr α] (x : α) : IO Unit := IO.println $ (reprPrec x 0).pretty - print sn + let print {α : Type} [Repr α] (x : α) : IO Unit := IO.println (reprPrec x 0).pretty + print $ prune 0 sn print pn diff --git a/analysis/sims/kernels.nix b/analysis/sims/kernels.nix index 5846a1e15..0fe971a14 100644 --- a/analysis/sims/kernels.nix +++ b/analysis/sims/kernels.nix @@ -33,6 +33,7 @@ in igraph lubridate mongolite + poibin quantreg RPostgres R_utils From aeadb5035b8edc6a0e2a3afe2a7a9e1a21a0f16a Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Tue, 30 Sep 2025 13:55:54 -0600 Subject: [PATCH 09/29] Computation of quorum probability --- analysis/markov/Linleios/Evolve.lean | 59 +++++++++++++++++++++++++--- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index 63e221981..9bd84d66d 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -5,17 +5,66 @@ import Batteries.Lean.HashMap open Std (HashMap) +private partial def erf (x : Float) : Float := + if x < 0 + then - erf (- x) + else + let p := 0.3275911 + let a₁ := 0.254829592 + let a₂ := -0.284496736 + let a₃ := 1.421413741 + let a₄ := -1.453152027 + let a₅ := 1.061405429 + let t := 1 / (1 + p * x) + 1 - (a₁ * t + a₂ * t^2 + a₃ * t^3 + a₄ * t^4 + a₅ * t^5) * Float.exp (- x^2) + +private def cdfGaussian (x μ σ : Float) : Float := + (1 + erf ((x - μ) / σ / Float.sqrt 2)) / 2 + +private def bisectionSearch (f : Float → Float) (low high : Float) (ε : Float) (maxIter : Nat) : Float := + match maxIter with + | 0 => (low + high) / 2 + | maxIter' + 1 => + let mid := (low + high) / 2 + let fmid := f mid + if high - low < ε || Float.abs fmid < ε then + mid + else if f low * fmid < 0 then + bisectionSearch f low mid ε maxIter' + else + bisectionSearch f mid high ε maxIter' +termination_by maxIter + + abbrev Probability := Float -def approxCommittee (committeeSize : Float) : Float × Float := - let nPools : Nat := 2500 - let stakes : List Float := (List.range nPools).map (fun k => ((k + 1).toFloat / nPools.toFloat)^10 - (k.toFloat / nPools.toFloat)^10) - let ps : List Float := stakes.map (fun s => 1 - Float.exp (- committeeSize * s)) - let μ : Float := ps.sum +def nPools : Nat := 2500 + +def stakeDistribution (nPools : Nat) : List Float := + (List.range nPools).map (fun k => ((k + 1).toFloat / nPools.toFloat)^10 - (k.toFloat / nPools.toFloat)^10) + +private def calibrateCommittee(committeeSize : Float) : Float := + let stakes : List Float := stakeDistribution nPools + let f (m : Float) : Float := + let ps : List Float := stakes.map (fun s => 1 - Float.exp (- m * s)) + ps.sum - committeeSize + bisectionSearch f committeeSize nPools.toFloat 0.5 10 + +private def committeeDistribution (pSuccessfulVote committeeSize : Float) : Float × Float := + let stakes : List Float := stakeDistribution nPools + let m := calibrateCommittee committeeSize + let ps : List Float := stakes.map (fun s => pSuccessfulVote * (1 - Float.exp (- m * s))) + let μ := ps.sum let σ := (ps.map (fun p => p * (1 - p))).sum.sqrt ⟨ μ , σ ⟩ +def pQuorum (pSuccessfulVote committeeSize τ : Float) : Float := + let ⟨ μ , σ ⟩ := committeeDistribution pSuccessfulVote committeeSize + 1 - cdfGaussian (τ * committeeSize) μ σ + +#eval pQuorum 0.90 600 0.75 + structure Environment where activeSlotCoefficient : Probability From a500ee54031a8424e3843aa46a0e20c4c0a07fa2 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Tue, 30 Sep 2025 14:10:30 -0600 Subject: [PATCH 10/29] Implemented quorum propagation --- analysis/markov/Linleios/Evolve.lean | 27 ++++++++++++--------------- analysis/markov/Main.lean | 4 ++-- 2 files changed, 14 insertions(+), 17 deletions(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index 9bd84d66d..9a22f9268 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -63,8 +63,6 @@ def pQuorum (pSuccessfulVote committeeSize τ : Float) : Float := let ⟨ μ , σ ⟩ := committeeDistribution pSuccessfulVote committeeSize 1 - cdfGaussian (τ * committeeSize) μ σ -#eval pQuorum 0.90 600 0.75 - structure Environment where activeSlotCoefficient : Probability @@ -72,18 +70,16 @@ structure Environment where Lvote : Nat Ldiff : Nat pSpacingOkay : Probability - pRbHeaderArrives : Probability - pEbValidates : Probability + pQuorum : Probability -def makeEnvironment (activeSlotCoefficient pRbHeaderArrives pEbValidates : Float) (Lheader Lvote Ldiff : Nat) : Environment := +def makeEnvironment (activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize τ : Float) (Lheader Lvote Ldiff : Nat) : Environment := { activeSlotCoefficient := activeSlotCoefficient Lheader := Lheader Lvote := Lvote Ldiff := Ldiff pSpacingOkay := (1 - activeSlotCoefficient).pow (3 * Lheader + Lvote + Ldiff - 1).toFloat - pRbHeaderArrives := pRbHeaderArrives - pEbValidates := pEbValidates + pQuorum := pQuorum (pRbHeaderArrives * pEbValidates) committeeSize τ } @@ -93,9 +89,10 @@ structure State where canCertify : Bool deriving Repr, BEq, Hashable, Inhabited -example : (default : State).rbCount = 0 := rfl - -example : (default : State).ebCount = 0 := rfl +theorem genesis : (default : State).rbCount = 0 ∧ (default : State).ebCount = 0 := by + constructor + rfl + rfl def Probabilities := HashMap State Probability @@ -108,7 +105,7 @@ instance : Inhabited Probabilities where abbrev Outcomes := List (State × Probability) -def forge {env : Environment} (state : State) : Outcomes := +def certify {env : Environment} (state : State) : Outcomes := let state' := { state with @@ -122,14 +119,14 @@ def forge {env : Environment} (state : State) : Outcomes := ] else [(state', 1)] -def validate {env : Environment} (state : State) : Outcomes := +def vote {env : Environment} (state : State) : Outcomes := [ - (state, env.pRbHeaderArrives * env.pEbValidates) - , ({state with canCertify := false}, 1 - env.pRbHeaderArrives * env.pEbValidates) + (state, env.pQuorum) + , ({state with canCertify := false}, 1 - env.pQuorum) ] def step {env : Environment} : List (State → Outcomes) := - [@forge env, @validate env] + [@certify env, @vote env] def evolve (transition : State → Outcomes) : Probabilities → Probabilities := diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index ef7b6bf01..c7ec17c98 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -2,10 +2,10 @@ import Linleios -def env : Environment := makeEnvironment 0.05 0.95 0.90 1 4 7 +def env : Environment := makeEnvironment 0.05 0.95 0.90 600 0.75 1 4 7 def s0 : Probabilities := default -def sn := simulate env s0 10 +def sn := simulate env s0 2 def pn := totalProbability sn def main : IO Unit := From 7e08726021ffefbf7d6db5d3a20a8ff1c1a4050f Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Tue, 30 Sep 2025 14:14:45 -0600 Subject: [PATCH 11/29] Incorporated pruning into simulation --- analysis/markov/Linleios/Evolve.lean | 12 ++++++------ analysis/markov/Main.lean | 6 +++--- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index 9a22f9268..56b3d5c29 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -129,6 +129,9 @@ def step {env : Environment} : List (State → Outcomes) := [@certify env, @vote env] +def prune (ε : Float) : Probabilities → Probabilities := + HashMap.filter (fun _ p => p > ε) + def evolve (transition : State → Outcomes) : Probabilities → Probabilities := HashMap.fold ( @@ -145,13 +148,10 @@ def chain (transitions : List (State → Outcomes)) : Probabilities → Probabil | [] => id | (t :: ts) => chain ts ∘ evolve t -def simulate (env : Environment) (start : Probabilities) : Nat → Probabilities +def simulate (env : Environment) (start : Probabilities) (ε : Float) : Nat → Probabilities | 0 => start -| n + 1 => let state' := chain (@step env) start - simulate env state' n - -def prune (ε : Float) : Probabilities → Probabilities := - HashMap.filter (fun _ p => p > ε) +| n + 1 => let state' := prune ε $ chain (@step env) start + simulate env state' ε n def totalProbability (states : Probabilities) : Probability := states.values.sum diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index c7ec17c98..c1b4a74e0 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -5,11 +5,11 @@ import Linleios def env : Environment := makeEnvironment 0.05 0.95 0.90 600 0.75 1 4 7 def s0 : Probabilities := default -def sn := simulate env s0 2 +def sn := simulate env s0 1e-6 2 def pn := totalProbability sn def main : IO Unit := do let print {α : Type} [Repr α] (x : α) : IO Unit := IO.println (reprPrec x 0).pretty - print $ prune 0 sn - print pn + print sn + print $ 1 - pn From 85264045d4e92ea27836581426e3491f767634d9 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Tue, 30 Sep 2025 14:21:21 -0600 Subject: [PATCH 12/29] Refactored file structure --- analysis/markov/Linleios/Evolve.lean | 101 +--------------------- analysis/markov/Linleios/Probability.lean | 27 ++++++ analysis/markov/Linleios/Types.lean | 51 +++++++++++ analysis/markov/Linleios/Util.lean | 30 +++++++ 4 files changed, 110 insertions(+), 99 deletions(-) create mode 100644 analysis/markov/Linleios/Probability.lean create mode 100644 analysis/markov/Linleios/Types.lean create mode 100644 analysis/markov/Linleios/Util.lean diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index 56b3d5c29..5d4c790b6 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -2,107 +2,10 @@ import Std.Data.HashMap import Batteries.Lean.HashMap -open Std (HashMap) - - -private partial def erf (x : Float) : Float := - if x < 0 - then - erf (- x) - else - let p := 0.3275911 - let a₁ := 0.254829592 - let a₂ := -0.284496736 - let a₃ := 1.421413741 - let a₄ := -1.453152027 - let a₅ := 1.061405429 - let t := 1 / (1 + p * x) - 1 - (a₁ * t + a₂ * t^2 + a₃ * t^3 + a₄ * t^4 + a₅ * t^5) * Float.exp (- x^2) - -private def cdfGaussian (x μ σ : Float) : Float := - (1 + erf ((x - μ) / σ / Float.sqrt 2)) / 2 - -private def bisectionSearch (f : Float → Float) (low high : Float) (ε : Float) (maxIter : Nat) : Float := - match maxIter with - | 0 => (low + high) / 2 - | maxIter' + 1 => - let mid := (low + high) / 2 - let fmid := f mid - if high - low < ε || Float.abs fmid < ε then - mid - else if f low * fmid < 0 then - bisectionSearch f low mid ε maxIter' - else - bisectionSearch f mid high ε maxIter' -termination_by maxIter - - -abbrev Probability := Float - - -def nPools : Nat := 2500 - -def stakeDistribution (nPools : Nat) : List Float := - (List.range nPools).map (fun k => ((k + 1).toFloat / nPools.toFloat)^10 - (k.toFloat / nPools.toFloat)^10) - -private def calibrateCommittee(committeeSize : Float) : Float := - let stakes : List Float := stakeDistribution nPools - let f (m : Float) : Float := - let ps : List Float := stakes.map (fun s => 1 - Float.exp (- m * s)) - ps.sum - committeeSize - bisectionSearch f committeeSize nPools.toFloat 0.5 10 - -private def committeeDistribution (pSuccessfulVote committeeSize : Float) : Float × Float := - let stakes : List Float := stakeDistribution nPools - let m := calibrateCommittee committeeSize - let ps : List Float := stakes.map (fun s => pSuccessfulVote * (1 - Float.exp (- m * s))) - let μ := ps.sum - let σ := (ps.map (fun p => p * (1 - p))).sum.sqrt - ⟨ μ , σ ⟩ +import Linleios.Types -def pQuorum (pSuccessfulVote committeeSize τ : Float) : Float := - let ⟨ μ , σ ⟩ := committeeDistribution pSuccessfulVote committeeSize - 1 - cdfGaussian (τ * committeeSize) μ σ - -structure Environment where - activeSlotCoefficient : Probability - Lheader : Nat - Lvote : Nat - Ldiff : Nat - pSpacingOkay : Probability - pQuorum : Probability - -def makeEnvironment (activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize τ : Float) (Lheader Lvote Ldiff : Nat) : Environment := - { - activeSlotCoefficient := activeSlotCoefficient - Lheader := Lheader - Lvote := Lvote - Ldiff := Ldiff - pSpacingOkay := (1 - activeSlotCoefficient).pow (3 * Lheader + Lvote + Ldiff - 1).toFloat - pQuorum := pQuorum (pRbHeaderArrives * pEbValidates) committeeSize τ - } - - -structure State where - rbCount : Nat - ebCount : Nat - canCertify : Bool -deriving Repr, BEq, Hashable, Inhabited - -theorem genesis : (default : State).rbCount = 0 ∧ (default : State).ebCount = 0 := by - constructor - rfl - rfl - - -def Probabilities := HashMap State Probability -deriving Repr, EmptyCollection - -instance : Inhabited Probabilities where - default := (∅ : Probabilities).insert Inhabited.default 1 - - -abbrev Outcomes := List (State × Probability) +open Std (HashMap) def certify {env : Environment} (state : State) : Outcomes := diff --git a/analysis/markov/Linleios/Probability.lean b/analysis/markov/Linleios/Probability.lean new file mode 100644 index 000000000..5c061ca71 --- /dev/null +++ b/analysis/markov/Linleios/Probability.lean @@ -0,0 +1,27 @@ + +import Linleios.Util + + +def nPools : Nat := 2500 + +def stakeDistribution (nPools : Nat) : List Float := + (List.range nPools).map (fun k => ((k + 1).toFloat / nPools.toFloat)^10 - (k.toFloat / nPools.toFloat)^10) + +private def calibrateCommittee(committeeSize : Float) : Float := + let stakes : List Float := stakeDistribution nPools + let f (m : Float) : Float := + let ps : List Float := stakes.map (fun s => 1 - Float.exp (- m * s)) + ps.sum - committeeSize + bisectionSearch f committeeSize nPools.toFloat 0.5 10 + +private def committeeDistribution (pSuccessfulVote committeeSize : Float) : Float × Float := + let stakes : List Float := stakeDistribution nPools + let m := calibrateCommittee committeeSize + let ps : List Float := stakes.map (fun s => pSuccessfulVote * (1 - Float.exp (- m * s))) + let μ := ps.sum + let σ := (ps.map (fun p => p * (1 - p))).sum.sqrt + ⟨ μ , σ ⟩ + +def pQuorum (pSuccessfulVote committeeSize τ : Float) : Float := + let ⟨ μ , σ ⟩ := committeeDistribution pSuccessfulVote committeeSize + 1 - cdfGaussian (τ * committeeSize) μ σ diff --git a/analysis/markov/Linleios/Types.lean b/analysis/markov/Linleios/Types.lean new file mode 100644 index 000000000..8f35bcb58 --- /dev/null +++ b/analysis/markov/Linleios/Types.lean @@ -0,0 +1,51 @@ +import Std.Data.HashMap +import Batteries.Lean.HashMap + +import Linleios.Probability + + +open Std (HashMap) + + +abbrev Probability := Float + + +structure Environment where + activeSlotCoefficient : Probability + Lheader : Nat + Lvote : Nat + Ldiff : Nat + pSpacingOkay : Probability + pQuorum : Probability + +def makeEnvironment (activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize τ : Float) (Lheader Lvote Ldiff : Nat) : Environment := + { + activeSlotCoefficient := activeSlotCoefficient + Lheader := Lheader + Lvote := Lvote + Ldiff := Ldiff + pSpacingOkay := (1 - activeSlotCoefficient).pow (3 * Lheader + Lvote + Ldiff - 1).toFloat + pQuorum := pQuorum (pRbHeaderArrives * pEbValidates) committeeSize τ + } + + +structure State where + rbCount : Nat + ebCount : Nat + canCertify : Bool +deriving Repr, BEq, Hashable, Inhabited + +theorem genesis : (default : State).rbCount = 0 ∧ (default : State).ebCount = 0 := by + constructor + rfl + rfl + + +def Probabilities := HashMap State Probability +deriving Repr, EmptyCollection + +instance : Inhabited Probabilities where + default := (∅ : Probabilities).insert Inhabited.default 1 + + +abbrev Outcomes := List (State × Probability) diff --git a/analysis/markov/Linleios/Util.lean b/analysis/markov/Linleios/Util.lean new file mode 100644 index 000000000..9c55bd4e7 --- /dev/null +++ b/analysis/markov/Linleios/Util.lean @@ -0,0 +1,30 @@ + +partial def erf (x : Float) : Float := + if x < 0 + then - erf (- x) + else + let p := 0.3275911 + let a₁ := 0.254829592 + let a₂ := -0.284496736 + let a₃ := 1.421413741 + let a₄ := -1.453152027 + let a₅ := 1.061405429 + let t := 1 / (1 + p * x) + 1 - (a₁ * t + a₂ * t^2 + a₃ * t^3 + a₄ * t^4 + a₅ * t^5) * Float.exp (- x^2) + +def cdfGaussian (x μ σ : Float) : Float := + (1 + erf ((x - μ) / σ / Float.sqrt 2)) / 2 + +def bisectionSearch (f : Float → Float) (low high : Float) (ε : Float) (maxIter : Nat) : Float := + match maxIter with + | 0 => (low + high) / 2 + | maxIter' + 1 => + let mid := (low + high) / 2 + let fmid := f mid + if high - low < ε || Float.abs fmid < ε then + mid + else if f low * fmid < 0 then + bisectionSearch f low mid ε maxIter' + else + bisectionSearch f mid high ε maxIter' +termination_by maxIter From 36044894bca10316e8ada5d7fede80148f62d139 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Tue, 30 Sep 2025 14:40:45 -0600 Subject: [PATCH 13/29] Implemented computation of eb distribution --- analysis/markov/Linleios/Evolve.lean | 9 +++++++++ analysis/markov/Main.lean | 10 ++++++---- 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index 5d4c790b6..e4df54515 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -58,3 +58,12 @@ def simulate (env : Environment) (start : Probabilities) (ε : Float) : Nat → def totalProbability (states : Probabilities) : Probability := states.values.sum + +def ebDistribution : Probabilities → HashMap Nat Probability := + HashMap.fold + ( + fun acc state p => + HashMap.mergeWith (fun _ => Add.add) acc + $ singleton ⟨ state.ebCount , p ⟩ + ) + ∅ diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index c1b4a74e0..beb6064a5 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -4,12 +4,14 @@ import Linleios def env : Environment := makeEnvironment 0.05 0.95 0.90 600 0.75 1 4 7 -def s0 : Probabilities := default -def sn := simulate env s0 1e-6 2 -def pn := totalProbability sn +def sn := simulate env default 1e-6 10 def main : IO Unit := do let print {α : Type} [Repr α] (x : α) : IO Unit := IO.println (reprPrec x 0).pretty + IO.println "" print sn - print $ 1 - pn + IO.println "" + print $ ebDistribution sn + IO.println "" + print $ 1 - totalProbability sn From 7bd9473a27b6c52c67b6b4b710cd163cbb0a31f7 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Tue, 30 Sep 2025 14:50:54 -0600 Subject: [PATCH 14/29] Implemented efficiecy computation --- analysis/markov/Linleios/Evolve.lean | 9 +++++++++ analysis/markov/Main.lean | 4 +++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index e4df54515..65610411f 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -67,3 +67,12 @@ def ebDistribution : Probabilities → HashMap Nat Probability := $ singleton ⟨ state.ebCount , p ⟩ ) ∅ + +def ebEfficiency (states : Probabilities) : Float := + let rbCount := states.keys.head!.rbCount + let ebCount := + HashMap.fold + (fun acc state p =>acc + state.ebCount.toFloat * p) + 0 + states + ebCount / (rbCount.toFloat - 1) diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index beb6064a5..6d2f2a7e6 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -4,7 +4,7 @@ import Linleios def env : Environment := makeEnvironment 0.05 0.95 0.90 600 0.75 1 4 7 -def sn := simulate env default 1e-6 10 +def sn := simulate env default 1e-6 25 def main : IO Unit := do @@ -14,4 +14,6 @@ def main : IO Unit := IO.println "" print $ ebDistribution sn IO.println "" + print $ ebEfficiency sn + IO.println "" print $ 1 - totalProbability sn From 349458b1b0f28595389d1d919027bfdafd59c2d6 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Wed, 1 Oct 2025 09:59:56 -0600 Subject: [PATCH 15/29] Added CLI --- analysis/markov/Linleios.lean | 1 - analysis/markov/Main.lean | 60 +++++++++++++++++++++++++++-- analysis/markov/lake-manifest.json | 62 ++++++++++++++++++++---------- analysis/markov/lakefile.toml | 10 +++++ 4 files changed, 108 insertions(+), 25 deletions(-) diff --git a/analysis/markov/Linleios.lean b/analysis/markov/Linleios.lean index 7fe097c8a..3920332dc 100644 --- a/analysis/markov/Linleios.lean +++ b/analysis/markov/Linleios.lean @@ -1,2 +1 @@ - import Linleios.Evolve diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index 6d2f2a7e6..e5c6f1f39 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -1,13 +1,33 @@ +import Cli import Linleios +import Parser.Char.Numeric +import Parser.Stream +open Cli -def env : Environment := makeEnvironment 0.05 0.95 0.90 600 0.75 1 4 7 -def sn := simulate env default 1e-6 25 +instance : ParseableType Float where + name := "Float" + parse? x := match (Parser.Char.ASCII.parseFloat : SimpleParser Substring Char Float).run x with + | .ok _ y => some y + | _ => none -def main : IO Unit := + +def runMarkovCmd (p : Parsed) : IO UInt32 := do + let activeSlotCoefficient : Float := p.flag! "active-slot-coefficient" |>.as! Float + let Lheader : Nat := p.flag! "l-header" |>.as! Nat + let Lvote : Nat := p.flag! "l-vote" |>.as! Nat + let Ldiff : Nat := p.flag! "l-diff" |>.as! Nat + let committeeSize : Nat := p.flag! "committee-size" |>.as! Nat + let τ : Float := p.flag! "quorum-fraction" |>.as! Float + let pRbHeaderArrives : Float := p.flag! "p-rb-header-arrives" |>.as! Float + let pEbValidates : Float := p.flag! "p-eb-validates" |>.as! Float + let ε : Float := p.flag! "tolerance" |>.as! Float + let rbCount : Nat := p.flag! "rb-count" |>.as! Nat + let env := makeEnvironment activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize.toFloat τ Lheader Lvote Ldiff + let sn := simulate env default ε rbCount let print {α : Type} [Repr α] (x : α) : IO Unit := IO.println (reprPrec x 0).pretty IO.println "" print sn @@ -17,3 +37,37 @@ def main : IO Unit := print $ ebEfficiency sn IO.println "" print $ 1 - totalProbability sn + pure 0 + +def markovCmd : Cmd := `[Cli| + markovCmd VIA runMarkovCmd; ["0.0.1"] + "Run a Markov simulation of Linear Leios." + FLAGS: + "active-slot-coefficient" : Float ; "The active slot coefficient for RBs, in probability per slot." + "l-header" : Nat ; "L_header protocol parameter, in slots." + "l-vote" : Nat ; "L_vote protocol parameter, in slots." + "l-diff" : Nat ; "L_diff protocol parameter, in slots." + "committee-size" : Nat ; "Expected number of voters in the committee." + "quorum-fraction" : Float ; "τ protocol parameter, in %/100." + "p-rb-header-arrives" : Float ; "Probability that the RB header arrives before L_header." + "p-eb-validates" : Float ; "Probability that the EB is fully validated before 3 L_header + L_vote." + "tolerance" : Float ; "Discard states with less than this probability." + "rb-count" : Nat ; "Number of RBs to simulate." + EXTENSIONS: + author "bwbush"; + defaultValues! #[ + ("active-slot-coefficient", "0.05") + , ("l-header" , "1" ) + , ("l-vote" , "4" ) + , ("l-diff" , "7" ) + , ("committee-size" , "600" ) + , ("quorum-fraction" , "0.75") + , ("p-rb-header-arrives" , "0.95") + , ("p-eb-validates" , "0.90") + , ("tolerance" , "1e-9") + , ("rb-count" , "100" ) + ] +] + +def main (args : List String) : IO UInt32 := + markovCmd.validate args diff --git a/analysis/markov/lake-manifest.json b/analysis/markov/lake-manifest.json index 2ccb685d7..e8e8006b8 100644 --- a/analysis/markov/lake-manifest.json +++ b/analysis/markov/lake-manifest.json @@ -1,7 +1,27 @@ {"version": "1.1.0", "packagesDir": ".lake/packages", "packages": - [{"url": "https://github.com/leanprover-community/mathlib4", + [{"url": "https://github.com/mhuisi/lean4-cli", + "type": "git", + "subDir": null, + "scope": "", + "rev": "f9e25dcbed001489c53bceeb1f1d50bbaf7451d4", + "name": "Cli", + "manifestFile": "lake-manifest.json", + "inputRev": "v4.20.0", + "inherited": false, + "configFile": "lakefile.toml"}, + {"url": "https://github.com/fgdorais/lean4-parser", + "type": "git", + "subDir": null, + "scope": "", + "rev": "26d5ce4d60195a869b1fdb100b442794ea63e1ad", + "name": "Parser", + "manifestFile": "lake-manifest.json", + "inputRev": "26d5ce4d60195a869b1fdb100b442794ea63e1ad", + "inherited": false, + "configFile": "lakefile.toml"}, + {"url": "https://github.com/leanprover-community/mathlib4", "type": "git", "subDir": null, "scope": "leanprover-community", @@ -11,6 +31,26 @@ "inputRev": "v4.20.0", "inherited": false, "configFile": "lakefile.lean"}, + {"url": "https://github.com/fgdorais/lean4-unicode-basic", + "type": "git", + "subDir": null, + "scope": "", + "rev": "45c426d1cb016fcb4fcbe043f1cd2d97acb2dbc3", + "name": "UnicodeBasic", + "manifestFile": "lake-manifest.json", + "inputRev": "main", + "inherited": true, + "configFile": "lakefile.lean"}, + {"url": "https://github.com/leanprover-community/batteries", + "type": "git", + "subDir": null, + "scope": "", + "rev": "7a0d63fbf8fd350e891868a06d9927efa545ac1e", + "name": "batteries", + "manifestFile": "lake-manifest.json", + "inputRev": "main", + "inherited": true, + "configFile": "lakefile.toml"}, {"url": "https://github.com/leanprover-community/plausible", "type": "git", "subDir": null, @@ -70,26 +110,6 @@ "manifestFile": "lake-manifest.json", "inputRev": "master", "inherited": true, - "configFile": "lakefile.toml"}, - {"url": "https://github.com/leanprover-community/batteries", - "type": "git", - "subDir": null, - "scope": "leanprover-community", - "rev": "7a0d63fbf8fd350e891868a06d9927efa545ac1e", - "name": "batteries", - "manifestFile": "lake-manifest.json", - "inputRev": "main", - "inherited": true, - "configFile": "lakefile.toml"}, - {"url": "https://github.com/leanprover/lean4-cli", - "type": "git", - "subDir": null, - "scope": "leanprover", - "rev": "f9e25dcbed001489c53bceeb1f1d50bbaf7451d4", - "name": "Cli", - "manifestFile": "lake-manifest.json", - "inputRev": "main", - "inherited": true, "configFile": "lakefile.toml"}], "name": "linleios", "lakeDir": ".lake"} diff --git a/analysis/markov/lakefile.toml b/analysis/markov/lakefile.toml index 3bc2fd8ad..b2a3c08ff 100644 --- a/analysis/markov/lakefile.toml +++ b/analysis/markov/lakefile.toml @@ -19,6 +19,16 @@ name = "mathlib" scope = "leanprover-community" version = "git#v4.20.0" +[[require]] +name = "Parser" +git = "https://github.com/fgdorais/lean4-parser" +rev = "26d5ce4d60195a869b1fdb100b442794ea63e1ad" + +[[require]] +name = "Cli" +git = "https://github.com/mhuisi/lean4-cli" +rev = "v4.20.0" + [lean] server-options = [ ["checkBinderAnnotations", "false"], From 925e2309b916ee11c30fb1447475e92b806e72f5 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Wed, 1 Oct 2025 10:52:26 -0600 Subject: [PATCH 16/29] Added JSON output --- analysis/markov/Linleios/Evolve.lean | 6 +++++ analysis/markov/Main.lean | 37 +++++++++++++--------------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index 65610411f..534315564 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -1,10 +1,13 @@ import Std.Data.HashMap import Batteries.Lean.HashMap +import Lean.Data.Json.FromToJson import Linleios.Types +open Lean (Json) +open Lean.ToJson (toJson) open Std (HashMap) @@ -68,6 +71,9 @@ def ebDistribution : Probabilities → HashMap Nat Probability := ) ∅ +def ebDistributionJson : Probabilities → Json := + Json.mkObj ∘ List.map (fun ⟨k, v⟩ => ⟨toString k, toJson v⟩) ∘ HashMap.toList ∘ ebDistribution + def ebEfficiency (states : Probabilities) : Float := let rbCount := states.keys.head!.rbCount let ebCount := diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index e5c6f1f39..763366627 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -5,6 +5,7 @@ import Parser.Char.Numeric import Parser.Stream open Cli +open Lean (Json) instance : ParseableType Float where @@ -28,31 +29,27 @@ def runMarkovCmd (p : Parsed) : IO UInt32 := let rbCount : Nat := p.flag! "rb-count" |>.as! Nat let env := makeEnvironment activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize.toFloat τ Lheader Lvote Ldiff let sn := simulate env default ε rbCount - let print {α : Type} [Repr α] (x : α) : IO Unit := IO.println (reprPrec x 0).pretty - IO.println "" - print sn - IO.println "" - print $ ebDistribution sn - IO.println "" - print $ ebEfficiency sn - IO.println "" - print $ 1 - totalProbability sn + if p.hasFlag "output-file" + then IO.FS.writeFile (p.flag! "output-file" |>.as! String) (Json.pretty $ ebDistributionJson sn) + IO.println s!"Efficiency: {(reprPrec (ebEfficiency sn) 0).pretty}" + IO.eprintln s!"Missing probability: {1 - totalProbability sn}" pure 0 def markovCmd : Cmd := `[Cli| markovCmd VIA runMarkovCmd; ["0.0.1"] "Run a Markov simulation of Linear Leios." FLAGS: - "active-slot-coefficient" : Float ; "The active slot coefficient for RBs, in probability per slot." - "l-header" : Nat ; "L_header protocol parameter, in slots." - "l-vote" : Nat ; "L_vote protocol parameter, in slots." - "l-diff" : Nat ; "L_diff protocol parameter, in slots." - "committee-size" : Nat ; "Expected number of voters in the committee." - "quorum-fraction" : Float ; "τ protocol parameter, in %/100." - "p-rb-header-arrives" : Float ; "Probability that the RB header arrives before L_header." - "p-eb-validates" : Float ; "Probability that the EB is fully validated before 3 L_header + L_vote." - "tolerance" : Float ; "Discard states with less than this probability." - "rb-count" : Nat ; "Number of RBs to simulate." + "active-slot-coefficient" : Float ; "The active slot coefficient for RBs, in probability per slot." + "l-header" : Nat ; "L_header protocol parameter, in slots." + "l-vote" : Nat ; "L_vote protocol parameter, in slots." + "l-diff" : Nat ; "L_diff protocol parameter, in slots." + "committee-size" : Nat ; "Expected number of voters in the committee." + "quorum-fraction" : Float ; "τ protocol parameter, in %/100." + "p-rb-header-arrives" : Float ; "Probability that the RB header arrives before L_header." + "p-eb-validates" : Float ; "Probability that the EB is fully validated before 3 L_header + L_vote." + "tolerance" : Float ; "Discard states with less than this probability." + "rb-count" : Nat ; "Number of RBs to simulate." + "output-file" : String ; "Path to the JSON output file for the EB distribution." EXTENSIONS: author "bwbush"; defaultValues! #[ @@ -64,7 +61,7 @@ def markovCmd : Cmd := `[Cli| , ("quorum-fraction" , "0.75") , ("p-rb-header-arrives" , "0.95") , ("p-eb-validates" , "0.90") - , ("tolerance" , "1e-9") + , ("tolerance" , "1e-8") , ("rb-count" , "100" ) ] ] From 093356c835671ea8a14d7d9766189be4a58e28e6 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Wed, 1 Oct 2025 11:54:02 -0600 Subject: [PATCH 17/29] Created read-me file with instructions for building and running --- analysis/markov/Main.lean | 7 +-- analysis/markov/ReadMe.md | 123 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 126 insertions(+), 4 deletions(-) create mode 100644 analysis/markov/ReadMe.md diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index 763366627..f6c4868c3 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -35,8 +35,8 @@ def runMarkovCmd (p : Parsed) : IO UInt32 := IO.eprintln s!"Missing probability: {1 - totalProbability sn}" pure 0 -def markovCmd : Cmd := `[Cli| - markovCmd VIA runMarkovCmd; ["0.0.1"] +def markov : Cmd := `[Cli| + markov VIA runMarkovCmd; ["0.0.1"] "Run a Markov simulation of Linear Leios." FLAGS: "active-slot-coefficient" : Float ; "The active slot coefficient for RBs, in probability per slot." @@ -51,7 +51,6 @@ def markovCmd : Cmd := `[Cli| "rb-count" : Nat ; "Number of RBs to simulate." "output-file" : String ; "Path to the JSON output file for the EB distribution." EXTENSIONS: - author "bwbush"; defaultValues! #[ ("active-slot-coefficient", "0.05") , ("l-header" , "1" ) @@ -67,4 +66,4 @@ def markovCmd : Cmd := `[Cli| ] def main (args : List String) : IO UInt32 := - markovCmd.validate args + markov.validate args diff --git a/analysis/markov/ReadMe.md b/analysis/markov/ReadMe.md new file mode 100644 index 000000000..c7cb6dcbb --- /dev/null +++ b/analysis/markov/ReadMe.md @@ -0,0 +1,123 @@ +# Markov-model simulation for Linear Leios + + +## Example + +```console +$ lake exe linleios \ + --l-header 1 \ + --l-vote 4 \ + --l-diff 5 \ + --committee-size 600 \ + --quorum-fraction 0.80 \ + --p-rb-header-arrives 0.95 \ + --p-eb-validates 0.85 \ + --output-file tmp.json + +Efficiency: 0.358416 +Missing probability: 0.000001 + +$ json2yaml tmp.json + +'61': 0 +'60': 0 +'59': 1e-06 +'58': 2e-06 +'57': 5e-06 +'56': 1.2e-05 +'55': 2.8e-05 +'54': 6.2e-05 +'53': 0.00013 +'52': 0.000263 +'51': 0.00051 +'50': 0.00095 +'49': 0.0017 +'48': 0.002924 +'47': 0.004832 +'46': 0.00767 +'45': 0.011695 +'44': 0.017128 +'43': 0.024091 +'42': 0.032532 +'41': 0.042169 +'40': 0.052455 +'39': 0.062599 +'38': 0.071642 +'37': 0.0786 +'36': 0.082632 +'35': 0.083203 +'34': 0.080197 +'33': 0.073954 +'32': 0.065202 +'31': 0.054925 +'30': 0.044172 +'29': 0.033887 +'28': 0.024777 +'27': 0.017248 +'26': 0.011419 +'25': 0.007182 +'24': 0.004285 +'23': 0.002422 +'22': 0.001295 +'21': 0.000654 +'20': 0.000311 +'19': 0.000139 +'18': 5.8e-05 +'17': 2.3e-05 +'16': 8e-06 +'15': 3e-06 +'14': 1e-06 +'13': 0 +'12': 0 +``` + + +## Usage + +```console +$ lake exe linleios --help + +markov [0.0.1] +Run a Markov simulation of Linear Leios. + +USAGE: + markov [FLAGS] + +FLAGS: + -h, --help Prints this message. + --version Prints the version. + --active-slot-coefficient : Float The active slot coefficient for RBs, in + probability per slot. [Default: `0.05`] + --l-header : Nat L_header protocol parameter, in slots. + [Default: `1`] + --l-vote : Nat L_vote protocol parameter, in slots. + [Default: `4`] + --l-diff : Nat L_diff protocol parameter, in slots. + [Default: `7`] + --committee-size : Nat Expected number of voters in the + committee. [Default: `600`] + --quorum-fraction : Float τ protocol parameter, in %/100. [Default: + `0.75`] + --p-rb-header-arrives : Float Probability that the RB header arrives + before L_header. [Default: `0.95`] + --p-eb-validates : Float Probability that the EB is fully + validated before 3 L_header + L_vote. + [Default: `0.90`] + --tolerance : Float Discard states with less than this + probability. [Default: `1e-8`] + --rb-count : Nat Number of RBs to simulate. [Default: + `100`] + --output-file : String Path to the JSON output file for the EB + distribution. +``` + + +## Building + +```console +$ nix-shell -p lean4 elan + +$ lake build +``` + +The executable program will reside at `.lake/build/bin/linleios`. From a48ae52b30f5cfa8131ad1ab7c86523a1f9f662b Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Wed, 1 Oct 2025 12:02:53 -0600 Subject: [PATCH 18/29] Completed basic documentation of the executable program --- analysis/markov/ReadMe.md | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/analysis/markov/ReadMe.md b/analysis/markov/ReadMe.md index c7cb6dcbb..c466c8616 100644 --- a/analysis/markov/ReadMe.md +++ b/analysis/markov/ReadMe.md @@ -1,18 +1,18 @@ # Markov-model simulation for Linear Leios +This Markovian model of Linear Leios computes the probability of EB certifications as RBs are produced. + ## Example +The `linleios` program executes the Markov model for EB production in Linear Leios. The protocol parameters and network characteristic are specified as flags on the command line. The program outputs the following information: + +- The efficiency of EB production, defined as the expected number of certified EBs per RB. +- The "missing probability" resulting from the finite-resolution arithmetic of the computations. +- Optionally, a JSON file containing the probabilities of the given number of certified EBs. + ```console -$ lake exe linleios \ - --l-header 1 \ - --l-vote 4 \ - --l-diff 5 \ - --committee-size 600 \ - --quorum-fraction 0.80 \ - --p-rb-header-arrives 0.95 \ - --p-eb-validates 0.85 \ - --output-file tmp.json +$ lake exe linleios --l-header 1 --l-vote 4 --l-diff 5 --committee-size 600 --quorum-fraction 0.80 --p-rb-header-arrives 0.95 --p-eb-validates 0.85 --rb-count 100 --output-file tmp.json Efficiency: 0.358416 Missing probability: 0.000001 From a371ceb122bdfbb60ea68eae951c32642b147ce1 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Wed, 1 Oct 2025 12:04:08 -0600 Subject: [PATCH 19/29] Tweaked documentation --- analysis/markov/ReadMe.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analysis/markov/ReadMe.md b/analysis/markov/ReadMe.md index c466c8616..d77617aec 100644 --- a/analysis/markov/ReadMe.md +++ b/analysis/markov/ReadMe.md @@ -7,8 +7,8 @@ This Markovian model of Linear Leios computes the probability of EB certificatio The `linleios` program executes the Markov model for EB production in Linear Leios. The protocol parameters and network characteristic are specified as flags on the command line. The program outputs the following information: -- The efficiency of EB production, defined as the expected number of certified EBs per RB. -- The "missing probability" resulting from the finite-resolution arithmetic of the computations. +- The efficiency of EB production, defined as the expected number of certified EBs per RB, on `/dev/stdout`. +- The "missing probability" resulting from the finite-resolution arithmetic of the computations, on `/dev/stderr`. - Optionally, a JSON file containing the probabilities of the given number of certified EBs. ```console From 9ed8c5324db1664ffcd9a5181959ba02301d3ccf Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Wed, 1 Oct 2025 12:36:20 -0600 Subject: [PATCH 20/29] Documented total number of stakepools --- analysis/markov/Main.lean | 2 +- analysis/markov/ReadMe.md | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index f6c4868c3..a35509811 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -43,7 +43,7 @@ def markov : Cmd := `[Cli| "l-header" : Nat ; "L_header protocol parameter, in slots." "l-vote" : Nat ; "L_vote protocol parameter, in slots." "l-diff" : Nat ; "L_diff protocol parameter, in slots." - "committee-size" : Nat ; "Expected number of voters in the committee." + "committee-size" : Nat ; "Expected number of voters in the committee, out of 2500 stakepools total." "quorum-fraction" : Float ; "τ protocol parameter, in %/100." "p-rb-header-arrives" : Float ; "Probability that the RB header arrives before L_header." "p-eb-validates" : Float ; "Probability that the EB is fully validated before 3 L_header + L_vote." diff --git a/analysis/markov/ReadMe.md b/analysis/markov/ReadMe.md index d77617aec..70d7c2261 100644 --- a/analysis/markov/ReadMe.md +++ b/analysis/markov/ReadMe.md @@ -95,7 +95,8 @@ FLAGS: --l-diff : Nat L_diff protocol parameter, in slots. [Default: `7`] --committee-size : Nat Expected number of voters in the - committee. [Default: `600`] + committee, out of 2500 stakepools total. + [Default: `600`] --quorum-fraction : Float τ protocol parameter, in %/100. [Default: `0.75`] --p-rb-header-arrives : Float Probability that the RB header arrives From d0cea6eadf74624874873da0c760c440ebd74759 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Thu, 2 Oct 2025 07:52:44 -0600 Subject: [PATCH 21/29] Implemented adversarial stake --- analysis/markov/Linleios/Evolve.lean | 30 ++++++++++++++++++---------- analysis/markov/Linleios/Types.lean | 10 ++++++++-- analysis/markov/Main.lean | 7 +++++-- 3 files changed, 32 insertions(+), 15 deletions(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index 534315564..af049f1ba 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -11,28 +11,36 @@ open Lean.ToJson (toJson) open Std (HashMap) -def certify {env : Environment} (state : State) : Outcomes := +def forge {env : Environment} (state : State) : Outcomes := let state' := { state with - rbCount := state.rbCount + 1 - canCertify := true + clock := state.clock + 1 } + let p := 1 - env.fAdversary + [ + ({state' with rbCount := state.rbCount + 1}, p) + , (state', 1 - p) + ] + +def certify {env : Environment} (state : State) : Outcomes := + let p := env.pSpacingOkay * (1 - env.fAdversary) if state.canCertify then [ - ⟨{state' with ebCount := state.ebCount + 1}, env.pSpacingOkay⟩ - , ⟨state', 1 - env.pSpacingOkay⟩ + ⟨{state with ebCount := state.ebCount + 1}, p⟩ + , ⟨state, 1 - p⟩ ] - else [(state', 1)] + else [(state, 1)] def vote {env : Environment} (state : State) : Outcomes := + let p := env.pQuorum * (1 - env.fAdversary) [ - (state, env.pQuorum) - , ({state with canCertify := false}, 1 - env.pQuorum) + ({state with canCertify := true}, p) + , ({state with canCertify := false}, 1 - p) ] def step {env : Environment} : List (State → Outcomes) := - [@certify env, @vote env] + [@forge env, @certify env, @vote env] def prune (ε : Float) : Probabilities → Probabilities := @@ -75,10 +83,10 @@ def ebDistributionJson : Probabilities → Json := Json.mkObj ∘ List.map (fun ⟨k, v⟩ => ⟨toString k, toJson v⟩) ∘ HashMap.toList ∘ ebDistribution def ebEfficiency (states : Probabilities) : Float := - let rbCount := states.keys.head!.rbCount + let clock := states.keys.head!.clock let ebCount := HashMap.fold (fun acc state p =>acc + state.ebCount.toFloat * p) 0 states - ebCount / (rbCount.toFloat - 1) + ebCount / (clock.toFloat - 1) diff --git a/analysis/markov/Linleios/Types.lean b/analysis/markov/Linleios/Types.lean index 8f35bcb58..457d11fbe 100644 --- a/analysis/markov/Linleios/Types.lean +++ b/analysis/markov/Linleios/Types.lean @@ -17,8 +17,10 @@ structure Environment where Ldiff : Nat pSpacingOkay : Probability pQuorum : Probability + fAdversary : Float +deriving Repr -def makeEnvironment (activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize τ : Float) (Lheader Lvote Ldiff : Nat) : Environment := +def makeEnvironment (activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize τ fAdversary : Float) (Lheader Lvote Ldiff : Nat) : Environment := { activeSlotCoefficient := activeSlotCoefficient Lheader := Lheader @@ -26,16 +28,20 @@ def makeEnvironment (activeSlotCoefficient pRbHeaderArrives pEbValidates committ Ldiff := Ldiff pSpacingOkay := (1 - activeSlotCoefficient).pow (3 * Lheader + Lvote + Ldiff - 1).toFloat pQuorum := pQuorum (pRbHeaderArrives * pEbValidates) committeeSize τ + fAdversary := fAdversary } structure State where + clock : Nat rbCount : Nat ebCount : Nat canCertify : Bool deriving Repr, BEq, Hashable, Inhabited -theorem genesis : (default : State).rbCount = 0 ∧ (default : State).ebCount = 0 := by +theorem genesis : (default : State).clock = 0 ∧ (default : State).rbCount = 0 ∧ (default : State).ebCount = 0 := by + constructor + rfl constructor rfl rfl diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index a35509811..c7db26341 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -25,9 +25,10 @@ def runMarkovCmd (p : Parsed) : IO UInt32 := let τ : Float := p.flag! "quorum-fraction" |>.as! Float let pRbHeaderArrives : Float := p.flag! "p-rb-header-arrives" |>.as! Float let pEbValidates : Float := p.flag! "p-eb-validates" |>.as! Float + let fAdversary : Float := p.flag! "adversary-fraction" |>.as! Float let ε : Float := p.flag! "tolerance" |>.as! Float let rbCount : Nat := p.flag! "rb-count" |>.as! Nat - let env := makeEnvironment activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize.toFloat τ Lheader Lvote Ldiff + let env := makeEnvironment activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize.toFloat τ fAdversary Lheader Lvote Ldiff let sn := simulate env default ε rbCount if p.hasFlag "output-file" then IO.FS.writeFile (p.flag! "output-file" |>.as! String) (Json.pretty $ ebDistributionJson sn) @@ -47,8 +48,9 @@ def markov : Cmd := `[Cli| "quorum-fraction" : Float ; "τ protocol parameter, in %/100." "p-rb-header-arrives" : Float ; "Probability that the RB header arrives before L_header." "p-eb-validates" : Float ; "Probability that the EB is fully validated before 3 L_header + L_vote." + "adversary-fraction" : Float ; "Fraction of stake that is adversarial." "tolerance" : Float ; "Discard states with less than this probability." - "rb-count" : Nat ; "Number of RBs to simulate." + "rb-count" : Nat ; "Number of potential RBs to simulate." "output-file" : String ; "Path to the JSON output file for the EB distribution." EXTENSIONS: defaultValues! #[ @@ -60,6 +62,7 @@ def markov : Cmd := `[Cli| , ("quorum-fraction" , "0.75") , ("p-rb-header-arrives" , "0.95") , ("p-eb-validates" , "0.90") + , ("adversary-fraction" , "0" ) , ("tolerance" , "1e-8") , ("rb-count" , "100" ) ] From 5b0934de9106a350911fa4e88024fb7553c92776 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Thu, 2 Oct 2025 08:08:44 -0600 Subject: [PATCH 22/29] Expanded efficiency measures --- analysis/markov/Linleios/Evolve.lean | 15 +++++++++++++++ analysis/markov/Main.lean | 4 +++- 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index af049f1ba..b5248075b 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -82,6 +82,15 @@ def ebDistribution : Probabilities → HashMap Nat Probability := def ebDistributionJson : Probabilities → Json := Json.mkObj ∘ List.map (fun ⟨k, v⟩ => ⟨toString k, toJson v⟩) ∘ HashMap.toList ∘ ebDistribution +def rbEfficiency (states : Probabilities) : Float := + let clock := states.keys.head!.clock + let rbCount := + HashMap.fold + (fun acc state p =>acc + state.rbCount.toFloat * p) + 0 + states + rbCount / clock.toFloat + def ebEfficiency (states : Probabilities) : Float := let clock := states.keys.head!.clock let ebCount := @@ -90,3 +99,9 @@ def ebEfficiency (states : Probabilities) : Float := 0 states ebCount / (clock.toFloat - 1) + +def efficiency (states : Probabilities) : Float := + let rb := rbEfficiency states + let eb := ebEfficiency states + let ρ := 12.5 / 0.9 + (rb + ρ * eb) / (1 + ρ) diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index c7db26341..37471a0e4 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -32,7 +32,9 @@ def runMarkovCmd (p : Parsed) : IO UInt32 := let sn := simulate env default ε rbCount if p.hasFlag "output-file" then IO.FS.writeFile (p.flag! "output-file" |>.as! String) (Json.pretty $ ebDistributionJson sn) - IO.println s!"Efficiency: {(reprPrec (ebEfficiency sn) 0).pretty}" + IO.println s!"RB efficiency: {(reprPrec (rbEfficiency sn) 0).pretty}" + IO.println s!"EB efficiency: {(reprPrec (ebEfficiency sn) 0).pretty}" + IO.println s!"Overall efficiency: {(reprPrec (efficiency sn) 0).pretty}" IO.eprintln s!"Missing probability: {1 - totalProbability sn}" pure 0 From 1fbd9cefcbd3bb5efc793829040656da7927d9a3 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Thu, 2 Oct 2025 08:54:57 -0600 Subject: [PATCH 23/29] Updated instructions and example in readme file --- analysis/markov/Linleios/Evolve.lean | 6 +- analysis/markov/Main.lean | 2 +- analysis/markov/ReadMe.md | 142 +++++++++++++++------------ 3 files changed, 87 insertions(+), 63 deletions(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index b5248075b..eac2dfb87 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -80,7 +80,11 @@ def ebDistribution : Probabilities → HashMap Nat Probability := ∅ def ebDistributionJson : Probabilities → Json := - Json.mkObj ∘ List.map (fun ⟨k, v⟩ => ⟨toString k, toJson v⟩) ∘ HashMap.toList ∘ ebDistribution + Json.mkObj + ∘ List.map (fun ⟨k, v⟩ => ⟨toString k, toJson v⟩) + ∘ (fun z => z.mergeSort (by exact fun x y => x.fst ≤ y.fst)) + ∘ HashMap.toList + ∘ ebDistribution def rbEfficiency (states : Probabilities) : Float := let clock := states.keys.head!.clock diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index 37471a0e4..a12df0b5b 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -50,7 +50,7 @@ def markov : Cmd := `[Cli| "quorum-fraction" : Float ; "τ protocol parameter, in %/100." "p-rb-header-arrives" : Float ; "Probability that the RB header arrives before L_header." "p-eb-validates" : Float ; "Probability that the EB is fully validated before 3 L_header + L_vote." - "adversary-fraction" : Float ; "Fraction of stake that is adversarial." + "adversary-fraction" : Float ; "Fraction of stake that is adversarial: the adversary never produces RBs or EBs and never votes." "tolerance" : Float ; "Discard states with less than this probability." "rb-count" : Nat ; "Number of potential RBs to simulate." "output-file" : String ; "Path to the JSON output file for the EB distribution." diff --git a/analysis/markov/ReadMe.md b/analysis/markov/ReadMe.md index 70d7c2261..a7926a091 100644 --- a/analysis/markov/ReadMe.md +++ b/analysis/markov/ReadMe.md @@ -7,68 +7,86 @@ This Markovian model of Linear Leios computes the probability of EB certificatio The `linleios` program executes the Markov model for EB production in Linear Leios. The protocol parameters and network characteristic are specified as flags on the command line. The program outputs the following information: -- The efficiency of EB production, defined as the expected number of certified EBs per RB, on `/dev/stdout`. +- The efficiencies, on `/dev/stdout`. + - RB efficiency: the fraction of possible RBs that were actually produced. + - EB efficiency: the fraction of possible EBs that were actually produced. + - Efficiency: the fraction o possible payload bytes that were actual produced. - The "missing probability" resulting from the finite-resolution arithmetic of the computations, on `/dev/stderr`. - Optionally, a JSON file containing the probabilities of the given number of certified EBs. +```bash +lake exe linleios \ + --l-header 1 \ + --l-vote 4 \ + --l-diff 5 \ + --committee-size 600 \ + --quorum-fraction 0.80 \ + --p-rb-header-arrives 0.95 \ + --p-eb-validates 0.85 \ + --rb-count 100 \ + --adversary-fraction 0.1 \ + --output-file tmp.json +``` + ```console -$ lake exe linleios --l-header 1 --l-vote 4 --l-diff 5 --committee-size 600 --quorum-fraction 0.80 --p-rb-header-arrives 0.95 --p-eb-validates 0.85 --rb-count 100 --output-file tmp.json - -Efficiency: 0.358416 -Missing probability: 0.000001 - -$ json2yaml tmp.json - -'61': 0 -'60': 0 -'59': 1e-06 -'58': 2e-06 -'57': 5e-06 -'56': 1.2e-05 -'55': 2.8e-05 -'54': 6.2e-05 -'53': 0.00013 -'52': 0.000263 -'51': 0.00051 -'50': 0.00095 -'49': 0.0017 -'48': 0.002924 -'47': 0.004832 -'46': 0.00767 -'45': 0.011695 -'44': 0.017128 -'43': 0.024091 -'42': 0.032532 -'41': 0.042169 -'40': 0.052455 -'39': 0.062599 -'38': 0.071642 -'37': 0.0786 -'36': 0.082632 -'35': 0.083203 -'34': 0.080197 -'33': 0.073954 -'32': 0.065202 -'31': 0.054925 -'30': 0.044172 -'29': 0.033887 -'28': 0.024777 -'27': 0.017248 -'26': 0.011419 -'25': 0.007182 -'24': 0.004285 -'23': 0.002422 -'22': 0.001295 -'21': 0.000654 -'20': 0.000311 -'19': 0.000139 -'18': 5.8e-05 -'17': 2.3e-05 -'16': 8e-06 -'15': 3e-06 -'14': 1e-06 -'13': 0 -'12': 0 +RB efficiency: 0.899977 +EB efficiency: 0.290308 +Overall efficiency: 0.331256 +Missing probability: 0.000028 +``` + +```bash +jq 'to_entries | sort_by(.key | tonumber) | from_entries' tmp.json +``` + +```json +{ + "8": 0, + "9": 0.000001, + "10": 0.000003, + "11": 0.000012, + "12": 0.000036, + "13": 0.0001, + "14": 0.000251, + "15": 0.000582, + "16": 0.001251, + "17": 0.0025, + "18": 0.004659, + "19": 0.008126, + "20": 0.013297, + "21": 0.020463, + "22": 0.02968, + "23": 0.040648, + "24": 0.052656, + "25": 0.064622, + "26": 0.07524, + "27": 0.083218, + "28": 0.087538, + "29": 0.087673, + "30": 0.083686, + "31": 0.076199, + "32": 0.066239, + "33": 0.055015, + "34": 0.043687, + "35": 0.03319, + "36": 0.024137, + "37": 0.016812, + "38": 0.011221, + "39": 0.00718, + "40": 0.004405, + "41": 0.002593, + "42": 0.001464, + "43": 0.000794, + "44": 0.000413, + "45": 0.000206, + "46": 0.000098, + "47": 0.000045, + "48": 0.000019, + "49": 0.000008, + "50": 0.000003, + "51": 0.000001, + "52": 0 +} ``` @@ -83,7 +101,6 @@ Run a Markov simulation of Linear Leios. USAGE: markov [FLAGS] -FLAGS: -h, --help Prints this message. --version Prints the version. --active-slot-coefficient : Float The active slot coefficient for RBs, in @@ -104,10 +121,13 @@ FLAGS: --p-eb-validates : Float Probability that the EB is fully validated before 3 L_header + L_vote. [Default: `0.90`] + --adversary-fraction : Float Fraction of stake that is adversarial: + the adversary never produces RBs or EBs + and never votes. [Default: `0`] --tolerance : Float Discard states with less than this probability. [Default: `1e-8`] - --rb-count : Nat Number of RBs to simulate. [Default: - `100`] + --rb-count : Nat Number of potential RBs to simulate. + [Default: `100`] --output-file : String Path to the JSON output file for the EB distribution. ``` From 9e4d3c7c5412fa96e125838aaa5d895a841b09b9 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Thu, 2 Oct 2025 09:09:34 -0600 Subject: [PATCH 24/29] Documented parameters --- analysis/markov/ReadMe.md | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/analysis/markov/ReadMe.md b/analysis/markov/ReadMe.md index a7926a091..4148d2094 100644 --- a/analysis/markov/ReadMe.md +++ b/analysis/markov/ReadMe.md @@ -3,6 +3,20 @@ This Markovian model of Linear Leios computes the probability of EB certifications as RBs are produced. +## Parameters + +| Symbol | Flag | Default | Description | +|-----------------|-------------------------|--------:|-------------------------------------------------------------------------------------| +| $L_\text{hdr}$ | `--l-header` | 1 | Constraint on header diffusion time. | +| $L_\text{vote}$ | `--l-vote` | 4 | Constraint on voting time. | +| $L_\text{diff}$ | `--l-diff` | 7 | Constraint on diffusion time. | +| $m$ | `--committee-size` | 600 | Number of members on the voting committee. | +| $\tau$ | `--quorum-fraction` | 0.75 | Stake-weighted fraction of committee's votes required to produce a certificate. | +| $p_\text{rb}$ | `--p-rb-header-arrives` | 0.95 | Probability that the RB header arrives at the node before $L_\text{hdr}$ seconds. | +| $p_\text{eb}$ | `--p-eb-validates` | 0.90 | Probability that the EB is fully validated before $3 L_\text{hdr} + L_\text{vote}$. | +| $f_\text{adv}$ | `--adversary-fraction` | 0.00 | Fraction of stake held by adversaries. | + + ## Example The `linleios` program executes the Markov model for EB production in Linear Leios. The protocol parameters and network characteristic are specified as flags on the command line. The program outputs the following information: @@ -35,11 +49,9 @@ Overall efficiency: 0.331256 Missing probability: 0.000028 ``` -```bash -jq 'to_entries | sort_by(.key | tonumber) | from_entries' tmp.json -``` +```console +$ jq 'to_entries | sort_by(.key | tonumber) | from_entries' tmp.json -```json { "8": 0, "9": 0.000001, From f6856d6a24f22faec3755de3c3cb9f220446c740 Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Thu, 2 Oct 2025 09:15:03 -0600 Subject: [PATCH 25/29] Added figure --- analysis/markov/ReadMe.md | 8 ++++++-- analysis/markov/example-results.png | Bin 0 -> 35523 bytes 2 files changed, 6 insertions(+), 2 deletions(-) create mode 100644 analysis/markov/example-results.png diff --git a/analysis/markov/ReadMe.md b/analysis/markov/ReadMe.md index 4148d2094..f6cbbee6d 100644 --- a/analysis/markov/ReadMe.md +++ b/analysis/markov/ReadMe.md @@ -49,9 +49,11 @@ Overall efficiency: 0.331256 Missing probability: 0.000028 ``` -```console -$ jq 'to_entries | sort_by(.key | tonumber) | from_entries' tmp.json +```bash +jq 'to_entries | sort_by(.key | tonumber) | from_entries' tmp.json +``` +```console { "8": 0, "9": 0.000001, @@ -101,6 +103,8 @@ $ jq 'to_entries | sort_by(.key | tonumber) | from_entries' tmp.json } ``` +![Example results](example-results.png) + ## Usage diff --git a/analysis/markov/example-results.png b/analysis/markov/example-results.png new file mode 100644 index 0000000000000000000000000000000000000000..f9df52e1a4df2879f491602fb6fca202d589d51e GIT binary patch literal 35523 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
    B>s^b0 z{tieuwvnUalahJ>FIb=H2GK|ZDzRJomBUp%2e{zi;J^uL3h2UvWME5Mo6a1E3Czks zdlK*1YY3wc%j)060@zSw4xSrH?4D z;^62L%>q&a+y;l3>*m$~k|ll={4!SyT0XkFo0n)g975o%nv5w}>417%)RzQGAYDDZ zUr-AGQZ!eE78hJGRFs#u=&*sGtgo-bT)lM&RGVR%&M&1u0`P`lDZ>ZE0UXYUvuBIQ2H*$S1Cqn<%qo{NK5jDZ z3L$&wZ*7lfvtidAgG3-)#1!V|v#_%-b*Hq#7L-*;i;IJYekm-}LB{aA#HuR&Y5cT- z=aItNQRAoJqww*>f++4U?=@XydudnPK%aC%BG_aIZTVZ0CG6u0I4 zD=n?b-@jQvhH7L)EhXxBy$%PC1GGg=eq|316&Yuh>!+lJsxhs>6GCr5LnF4^LP%4y z1}sk0@AgEoTIrkDV7O2&Ks&8we-dkT;VR-Ai_b^9Bw2!U2v Date: Thu, 2 Oct 2025 09:17:18 -0600 Subject: [PATCH 26/29] Added table of contents --- analysis/markov/ReadMe.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/analysis/markov/ReadMe.md b/analysis/markov/ReadMe.md index f6cbbee6d..3d8b6fc72 100644 --- a/analysis/markov/ReadMe.md +++ b/analysis/markov/ReadMe.md @@ -2,6 +2,19 @@ This Markovian model of Linear Leios computes the probability of EB certifications as RBs are produced. +## Contents + +- [Approach](#approach) +- [Parameters](#parameters) +- [Example](#example) +- [Usage](#usage) +- [Building](#building) + + +## Approach + +(to be written) + ## Parameters From 61447daa4dfc0f58a6f20878497c65e2e972d01b Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Thu, 2 Oct 2025 12:59:19 -0600 Subject: [PATCH 27/29] Documented model --- analysis/markov/Linleios/Evolve.lean | 35 +++-- analysis/markov/Linleios/Types.lean | 7 +- analysis/markov/Main.lean | 5 +- analysis/markov/ReadMe.md | 188 ++++++++++++++++++--------- analysis/markov/example-results.png | Bin 35523 -> 38028 bytes 5 files changed, 155 insertions(+), 80 deletions(-) diff --git a/analysis/markov/Linleios/Evolve.lean b/analysis/markov/Linleios/Evolve.lean index eac2dfb87..498207ca6 100644 --- a/analysis/markov/Linleios/Evolve.lean +++ b/analysis/markov/Linleios/Evolve.lean @@ -11,7 +11,7 @@ open Lean.ToJson (toJson) open Std (HashMap) -def forge {env : Environment} (state : State) : Outcomes := +def forgeRb {env : Environment} (state : State) : Outcomes := let state' := { state with @@ -19,28 +19,39 @@ def forge {env : Environment} (state : State) : Outcomes := } let p := 1 - env.fAdversary [ - ({state' with rbCount := state.rbCount + 1}, p) - , (state', 1 - p) + ({state' with hasRb := true, rbCount := state.rbCount + 1}, p) + , ({state' with hasRb := false, canCertify := false}, 1 - p) ] def certify {env : Environment} (state : State) : Outcomes := - let p := env.pSpacingOkay * (1 - env.fAdversary) - if state.canCertify - then [ + if state.hasRb && state.canCertify + then let p := env.pSpacingOkay + [ ⟨{state with ebCount := state.ebCount + 1}, p⟩ , ⟨state, 1 - p⟩ ] else [(state, 1)] +def forgeEb {env : Environment} (state : State) : Outcomes := + if state.hasRb + then let p := 1 - env.pLate + [ + ({state with canCertify := true}, p) + , ({state with canCertify := false}, 1 - p) + ] + else [(state, 1)] + def vote {env : Environment} (state : State) : Outcomes := - let p := env.pQuorum * (1 - env.fAdversary) - [ - ({state with canCertify := true}, p) - , ({state with canCertify := false}, 1 - p) - ] + if state.hasRb + then let p := env.pQuorum + [ + (state, p) + , ({state with canCertify := false}, 1 - p) + ] + else [(state, 1)] def step {env : Environment} : List (State → Outcomes) := - [@forge env, @certify env, @vote env] + [@forgeRb env, @certify env, @forgeEb env, @vote env] def prune (ε : Float) : Probabilities → Probabilities := diff --git a/analysis/markov/Linleios/Types.lean b/analysis/markov/Linleios/Types.lean index 457d11fbe..f1051b0a9 100644 --- a/analysis/markov/Linleios/Types.lean +++ b/analysis/markov/Linleios/Types.lean @@ -17,17 +17,19 @@ structure Environment where Ldiff : Nat pSpacingOkay : Probability pQuorum : Probability + pLate : Probability fAdversary : Float deriving Repr -def makeEnvironment (activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize τ fAdversary : Float) (Lheader Lvote Ldiff : Nat) : Environment := +def makeEnvironment (Lheader Lvote Ldiff : Nat) (activeSlotCoefficient committeeSize τ pRbHeaderArrives pEbValidates pEbUnvalidated fAdversary : Float) : Environment := { activeSlotCoefficient := activeSlotCoefficient Lheader := Lheader Lvote := Lvote Ldiff := Ldiff pSpacingOkay := (1 - activeSlotCoefficient).pow (3 * Lheader + Lvote + Ldiff - 1).toFloat - pQuorum := pQuorum (pRbHeaderArrives * pEbValidates) committeeSize τ + pQuorum := pQuorum (pRbHeaderArrives * pEbValidates * (1 - fAdversary)) committeeSize τ + pLate := pEbUnvalidated * (1 - committeeSize / nPools.toFloat) fAdversary := fAdversary } @@ -36,6 +38,7 @@ structure State where clock : Nat rbCount : Nat ebCount : Nat + hasRb : Bool canCertify : Bool deriving Repr, BEq, Hashable, Inhabited diff --git a/analysis/markov/Main.lean b/analysis/markov/Main.lean index a12df0b5b..08487ae68 100644 --- a/analysis/markov/Main.lean +++ b/analysis/markov/Main.lean @@ -25,10 +25,11 @@ def runMarkovCmd (p : Parsed) : IO UInt32 := let τ : Float := p.flag! "quorum-fraction" |>.as! Float let pRbHeaderArrives : Float := p.flag! "p-rb-header-arrives" |>.as! Float let pEbValidates : Float := p.flag! "p-eb-validates" |>.as! Float + let pLateDiffusion : Float := p.flag! "p-late-diffusion" |>.as! Float let fAdversary : Float := p.flag! "adversary-fraction" |>.as! Float let ε : Float := p.flag! "tolerance" |>.as! Float let rbCount : Nat := p.flag! "rb-count" |>.as! Nat - let env := makeEnvironment activeSlotCoefficient pRbHeaderArrives pEbValidates committeeSize.toFloat τ fAdversary Lheader Lvote Ldiff + let env := makeEnvironment Lheader Lvote Ldiff activeSlotCoefficient committeeSize.toFloat τ pRbHeaderArrives pEbValidates pLateDiffusion fAdversary let sn := simulate env default ε rbCount if p.hasFlag "output-file" then IO.FS.writeFile (p.flag! "output-file" |>.as! String) (Json.pretty $ ebDistributionJson sn) @@ -50,6 +51,7 @@ def markov : Cmd := `[Cli| "quorum-fraction" : Float ; "τ protocol parameter, in %/100." "p-rb-header-arrives" : Float ; "Probability that the RB header arrives before L_header." "p-eb-validates" : Float ; "Probability that the EB is fully validated before 3 L_header + L_vote." + "p-late-diffusion" : Float ; "Probability than a RB producer hasn't validated the previous EB." "adversary-fraction" : Float ; "Fraction of stake that is adversarial: the adversary never produces RBs or EBs and never votes." "tolerance" : Float ; "Discard states with less than this probability." "rb-count" : Nat ; "Number of potential RBs to simulate." @@ -64,6 +66,7 @@ def markov : Cmd := `[Cli| , ("quorum-fraction" , "0.75") , ("p-rb-header-arrives" , "0.95") , ("p-eb-validates" , "0.90") + , ("p-late-diffusion" , "0.05") , ("adversary-fraction" , "0" ) , ("tolerance" , "1e-8") , ("rb-count" , "100" ) diff --git a/analysis/markov/ReadMe.md b/analysis/markov/ReadMe.md index 3d8b6fc72..53a93b2a3 100644 --- a/analysis/markov/ReadMe.md +++ b/analysis/markov/ReadMe.md @@ -1,33 +1,85 @@ -# Markov-model simulation for Linear Leios +# Markovian simulation of Linear Leios + +This Markovian simulation of Linear Leios computes the probability of EB certifications as RBs are produced. -This Markovian model of Linear Leios computes the probability of EB certifications as RBs are produced. ## Contents -- [Approach](#approach) +- [Model](#model) - [Parameters](#parameters) - [Example](#example) - [Usage](#usage) - [Building](#building) -## Approach +## Model + +The protocol state is represented by three quantities. + +- The number of RBs that have been produced. +- The number of EBs that have been produced. +- Whether an honest RB was produced. +- Whether a certificate is ready for inclusion in the next RB. + +Time is tracked in terms of block-forging opportunties instead of in terms of slots. + +Transitions occur in several substeps: + +1. *Forge RB:* create a new RB. +2. *Certify:* include a certificate in the RB. +3. *Forge EB:* create a new EB. +4. *Vote:* cast votes to reach a quorum. + + +### Substep 1: Forge RB + +The adversarial model assumes that adversaries obstain from producing RBs, EBs, and votes. Let $f_\text{adv}$ be the fraction of stake held by the adversary. Two transitions are possible: + +- *Forge a new RB:* probability $1 - f_\text{adv}$. +- *Abstain from forging a new RB:* probability $f_\text{adv}$. + + +### Substep 2: Certify + +Provided that a quorum of votes have endorsed the EB, the following conditions are required for certifying it in the RB that was just forged. + +- *RB spacing:* The previous RB must have been forged at least $3 L_\text{hdr} + L_\text{vote} + L_\text{diff}$ slots previously. + - The distribution of gaps between RBs follows the negative-binomial distribution. +- *Quorum:* The voting yielded a quorum. +- *Not an adversary:* There is no RB if an adversary is the designated block producer. + + +### Substep 3: Forge EB + +Provided that an honest RB exists, an EB can be forged if the node has received the previous EB and computed the ledger state. + +- Because of their membership in the previous vote, a fraction $n_\text{comm} / n_\text{pools}$ of the pools have already updated their ledger state. +- Of the pools not having voted on the block we define $p_\text{late}$ as the probability that the EB has arrived too late to compute the ledger state needed to produce the next EB. + + +### Substep 4: Vote + +Obtaining a successful quorum of votes is distributed according to bernoulli trials where the expected number of successes is the mean committee size and the success probabilities of individual pools accord with the stake distribution. Those success probabilities are modified in several ways: -(to be written) +- *Adversaries do not vote:* probability $f_\text{adv}$. +- *RB header arrives within $L_\text{hdr}$ slots:* probability $p_\text{rb}$. +- *EB is fully validated within $3 L_\text{hdr} + L_\text{vote}$ slots*: probability $p_\text{eb}$. ## Parameters -| Symbol | Flag | Default | Description | -|-----------------|-------------------------|--------:|-------------------------------------------------------------------------------------| -| $L_\text{hdr}$ | `--l-header` | 1 | Constraint on header diffusion time. | -| $L_\text{vote}$ | `--l-vote` | 4 | Constraint on voting time. | -| $L_\text{diff}$ | `--l-diff` | 7 | Constraint on diffusion time. | -| $m$ | `--committee-size` | 600 | Number of members on the voting committee. | -| $\tau$ | `--quorum-fraction` | 0.75 | Stake-weighted fraction of committee's votes required to produce a certificate. | -| $p_\text{rb}$ | `--p-rb-header-arrives` | 0.95 | Probability that the RB header arrives at the node before $L_\text{hdr}$ seconds. | -| $p_\text{eb}$ | `--p-eb-validates` | 0.90 | Probability that the EB is fully validated before $3 L_\text{hdr} + L_\text{vote}$. | -| $f_\text{adv}$ | `--adversary-fraction` | 0.00 | Fraction of stake held by adversaries. | +| Symbol | Flag | Default | Description | +|------------------|-------------------------|--------:|------------------------------------------------------------------------------------------------------| +| $L_\text{hdr}$ | `--l-header` | 1 | Constraint on header diffusion time. | +| $L_\text{vote}$ | `--l-vote` | 4 | Constraint on voting time. | +| $L_\text{diff}$ | `--l-diff` | 7 | Constraint on diffusion time. | +| $n_\text{comm}$ | | 2500 | Number of stakepools (constant). | +| $n_\text{pools}$ | `--committee-size` | 600 | Number of members on the voting committee. | +| $\tau$ | `--quorum-fraction` | 0.75 | Stake-weighted fraction of committee's votes required to produce a certificate. | +| $p_\text{rb}$ | `--p-rb-header-arrives` | 0.95 | Probability that the RB header arrives at a node before $L_\text{hdr}$ seconds. | +| $p_\text{eb}$ | `--p-eb-validates` | 0.90 | Probability that the EB is fully validated by a node before $3 L_\text{hdr} + L_\text{vote}$. | +| $p_\text{late}$ | `--p-late-diffusion` | 0.05 | Probability that an RB producer hasn't yet validated the EB by the time they need to forge their RB. | +| $f_\text{adv}$ | `--adversary-fraction` | 0.00 | Fraction of stake held by adversaries. | ## Example @@ -49,17 +101,18 @@ lake exe linleios \ --committee-size 600 \ --quorum-fraction 0.80 \ --p-rb-header-arrives 0.95 \ - --p-eb-validates 0.85 \ + --p-eb-validates 0.95 \ + --p-late-diffusion 0.05 \ --rb-count 100 \ --adversary-fraction 0.1 \ --output-file tmp.json ``` ```console -RB efficiency: 0.899977 -EB efficiency: 0.290308 -Overall efficiency: 0.331256 -Missing probability: 0.000028 +RB efficiency: 0.899974 +EB efficiency: 0.313049 +Overall efficiency: 0.352469 +Missing probability: 0.000030 ``` ```bash @@ -68,51 +121,53 @@ jq 'to_entries | sort_by(.key | tonumber) | from_entries' tmp.json ```console { - "8": 0, - "9": 0.000001, - "10": 0.000003, - "11": 0.000012, - "12": 0.000036, - "13": 0.0001, - "14": 0.000251, - "15": 0.000582, - "16": 0.001251, - "17": 0.0025, - "18": 0.004659, - "19": 0.008126, - "20": 0.013297, - "21": 0.020463, - "22": 0.02968, - "23": 0.040648, - "24": 0.052656, - "25": 0.064622, - "26": 0.07524, - "27": 0.083218, - "28": 0.087538, - "29": 0.087673, - "30": 0.083686, - "31": 0.076199, - "32": 0.066239, - "33": 0.055015, - "34": 0.043687, - "35": 0.03319, - "36": 0.024137, - "37": 0.016812, - "38": 0.011221, - "39": 0.00718, - "40": 0.004405, - "41": 0.002593, - "42": 0.001464, - "43": 0.000794, - "44": 0.000413, - "45": 0.000206, - "46": 0.000098, - "47": 0.000045, - "48": 0.000019, - "49": 0.000008, - "50": 0.000003, - "51": 0.000001, - "52": 0 + "10": 0, + "11": 0.000003, + "12": 0.00001, + "13": 0.000031, + "14": 0.000081, + "15": 0.000196, + "16": 0.00044, + "17": 0.000919, + "18": 0.001799, + "19": 0.003308, + "20": 0.005731, + "21": 0.009381, + "22": 0.014533, + "23": 0.021353, + "24": 0.029805, + "25": 0.039585, + "26": 0.050095, + "27": 0.060484, + "28": 0.069757, + "29": 0.07693, + "30": 0.081208, + "31": 0.082129, + "32": 0.079641, + "33": 0.074106, + "34": 0.066214, + "35": 0.056847, + "36": 0.04692, + "37": 0.037253, + "38": 0.028464, + "39": 0.020939, + "40": 0.014837, + "41": 0.010129, + "42": 0.006664, + "43": 0.004227, + "44": 0.002586, + "45": 0.001525, + "46": 0.000868, + "47": 0.000476, + "48": 0.000252, + "49": 0.000128, + "50": 0.000063, + "51": 0.00003, + "52": 0.000013, + "53": 0.000005, + "54": 0.000002, + "55": 0.000001, + "56": 0 } ``` @@ -150,6 +205,9 @@ USAGE: --p-eb-validates : Float Probability that the EB is fully validated before 3 L_header + L_vote. [Default: `0.90`] + --p-late-diffusion : Float Probability than a RB producer hasn't + validated the previous EB. [Default: + `0.05`] --adversary-fraction : Float Fraction of stake that is adversarial: the adversary never produces RBs or EBs and never votes. [Default: `0`] diff --git a/analysis/markov/example-results.png b/analysis/markov/example-results.png index f9df52e1a4df2879f491602fb6fca202d589d51e..9efe11f0511e9c9665fd5f7af86357ae466ac946 100644 GIT binary patch literal 38028 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

    )ml$GCKRxJN0*1$KI zL&Fb1!3gOzxC4xCuj|Cq7;^38Je3y`30$$Wk@^6i8K2ACfGmY$V;~5ZpAY*eMKC*m z2fuD-(IUu@0DRaY8N9{@Z6Va${EiR9d-rm6k#KRJXarBcNHkx?zo9WY;ZCmQ{uKgV z=TLvkzuiazvuY%o+zmT>9~<~d7$<-&{w@yA_t&6$pw~AyH#4CN@80gX0A0+6V$b*{ zT(wO=m=H-&$n+Es|X z1moQKC+D;D9V3RJCIJdL>+)ZbYB%Xsm|isa_sR*JW~3#c`qgLBP~tYM6O<@QwSOqJ z`EZ-^%{7AklG8cP>cf-m&I!kvV7JqwG4N|>XsCGHY58b(S-Dg6b9H;H{8L|T?D@E4 zC;a^S(s@c|oLCN>`s@A4*f@u91p6UO=(;Bdn>O??vV#Hy=FzW(}|N*7^JJaJbv-?7IVMmJjaVS1Ymlw8R|S z_jd%H)ZVr~UO!&h{G)DT%0d=OV8B0DhL935^;DVA+oESyl!|699d(sDovg>8+ya?JWY)Yw>8?c+x%fnyz z9}C`GsY~v=n3dvgoV;KMF{sT%h<+WTK8nNqd9F`#2+vYp6Im>`P3jU67Qufg%Wu&c zy>=|%wtL{V_yjZUoN(Ih#0SgnH~yUBU`dn7j|e)~ zNFY@BDl0lyH1a()On7Bbe%=O5y4b z5mQpGPfe3$0kOfh*XscFy|d$kIg*{Vc3^I1yL$k8Z_nER9Aa|KQdYU9@R*6d6(m>? zM^5MA7nS2%B)837G8gP;TER4lN<7hDfhYKp41TQ-prNEg>F-|{X#ccmR)68Nog7

    )O3{={1Au zghu@G$uCQ*NhoJpni8wu=q>nLPO4_evPx~{*G7sN>FA8X&48>FmjXm& zx;#X0p_UpAvz;u;spP_fgC!>2ll}dlPgdjW+Km@ad?jbn_86A*_wL%|Au<_R=mtD- zf4}28pW`}7w1~*PO@-)?^o)#BkOXaPY!np~fPYz*uNK9`5cdn?7^i%KlllN((*V?- z6b{$Cq8<`$wDhxxh*(SmG8H82B2e30fxQORZDcGgEJQ`WgFnQ-eVhOC*AF0fRjGO% zEBBsRh7}vxarNd)=ZhB>W}R>&F^u=otOPIu-l?+w_7ofM(7{p`5Ivk8eM*AYJR=Xd zs1(0B+oVBvEDh_XTMjUl0)uw)vkkw*(-lK3{)5RFc2o5FD0b7xrS9k5e_37so6(?8 zxhvgee$gC&eJ}wsI=X`sn=h)mGa@0AR~dWpAW_5*9QeWBh-K%GA0}tfZEZllQn?$v zkTga8pBXs4|k=bKI)NCp3-Zg!|sSDiDUIA zhfxx!dpv?=J-ckK5^BHmJ)J8CZ!Vyuq@)gtODv~l5xptMK!X|%8n0d*3S;qoc4~Zz z=p}e14RMLh*aSQ&z?biWfmE?O8E&$0a3#8l;xdRKvh?oL2bK*XXQ2aIen63ejojck zf&_F;CiR{Oz)kGz4i0qVoHCb;CE}6D8``3|=1D7dBSEXv%nTtT_bbT+Dpy!Wkhk~tyg%Bm3LQ`2lo#B z12o98OtVYHc9E@tdN?WR#m`(FCwGSn(T+=OsgXRt_QKI^iDoVtHR4bKGGSlsQztM> zYh^69<{9}1p7Oq^_r-8OUDj%CZN(zx>e`zBz^IjNLPcp#uW9yZ8Eur#UNP;(sZ(3x zdiOobWNLNV;U&yiEbsr`I))SW*$Q+;&isk*w$ulLW0 zee*!RekaFIR6~~*_j37b@wnZfMy5EgIuqf;DQ~=Q)iE*s8e-Z_l`L3Oz}Hgc~6i(sN=$b!1I}w zG9Sh(F$INQ2knYih3Gipu@J(VZ4L)-w=!ON+z@~_;eLL4KfB}w02Aac8?_M&@1sQU zt067k&r&>?iK)Db{}YK_=~<%gEPY6(y~(|HD;&a!xB;ZjD|yw2?UofTS>iz4|GOrv zHx3o(5a!Y&c<2*B-QSZZAD!r%0KqBG!66&t?TrW|E1mrHHJ`h{B^ zZ0(S^0unrCxX$Pl+XF^&3qN@JA-F9jo71)l(G~Q7+8~4WnEo`m#Qv;jol(rXJ)b2x z$838um9viaR+nq%OVoZ-{E*1B(6VAC#LL~BFjD_hy2Kb8X73r>)FWM6>+?ykR1%7Z zum52GX|iGvUF(3sSsN}i-R`tq9ky!>BD(+d%M-2};W?rL9gNJ~kEPlCFoYr5gmR~d z=F62lLT32XfNN-w_~$bwXqxO6d#I_Yf8{0uz$z&*8DAxG3%?TaMI-uha_9iiC%-gJ zTi=@dU8}%11~OJ1th=8(w_0+jM(!TWglaiX`JkqmyuXln`0AvSHK^jU>{^(aHPrdQ zK*+Fln5FI;i*a{znG?h4RbN*+LsNr47?-eOm`?1*jT_z7=~-FjTR-j}@2&Eh4tyCM z9c_>0JU`p7o+z_h2BFX9xG||xrKF*u;pF6GZf*{iI^NyEJFI_mgOK+opVP-o)RV>= zw*vGjI0@8>!00h{+?9qB!wSg3&+iar0axcZ=kCw{vb#LUsG3i8#M&)z5(nBQ&$g?HK8L^>D$G;ILt35f? zR2_Zww@b})0?mqYC^lznzWwSjIBD|VD;tf$k^Q#)TYVWkhDB5qy`LHvrq6bI9oM*L ze}Ti@OtbwbNOjro9vzZDh)Pw}&=@N)lslVxpI*;lR+B0NG>`|IAV+p)WncG@KnzEI z{P>BA|Eq1=Gj9#bqSo!(MiI!69jV6uF-q6AT z7>Ull0-5w zclD zbIrurUNNxxR?&!sA}?Whb;a(@I-Q+mKb?9IwN-E9Mr}E_(HPSq+6*iz&DaC*Gj7w# zS3LSs%HDp@I4vHCD(!k@ILBP~$3AGe0cHWy@b56DFQ577b~Bw~S6qtzoP%VuZQ15q zDcEGnhmU(s8h>(@-226kAXYj7Kmrj7I%^*vA8QSoda%7iiKC~lGcQZ8@ur~jDIdn6 zj4%QV>*nqtU=bi>?(5Ivd7)%?9!+pNTAZGq24)xq&lk#nw7|DNIy00tawlBE*f`^Y zY3s|`Ob*Spq3P+ho{L6uSEz1#3Oy4ubKW9?V!!5tKmKH89XcZCw)GB!gadR-Pp>96 z8LGN~MUs5imC{NfkfS;W)&an$oeJA3UKyF$0-<~DWjhVVT4cqwUPzWh`J%h{B4-0z z2#Ntb+M`^6y2{MTb18+m=h3?Fd=^*_H)^1~|Ah9l6E!c<5=3>Ct zsUW9UJqNslOa(W)1%OFFq6ZT2vto-e;FD@+Uw{1g0RX${zK%+yvX)jN*nrvB0GXZL z^kjy7bK*SMrlVFT(f{M`XHB#Ijv90;aBXb!*9Qq{+cmoo|0~Sio?$KdgwNw#9i&*_ zsZfCpLSKrhVoz`BcH&Um_#lI5aY~Bb_(s4dtE&FYLy(Hp-qK6c`g-G^_L7TUC3g}O z35{wuSJ3))y6qK2vl?b5q~8KW(CN{xurE5wb+1fA>o9NWc;c+A%~~XwRqZvu^9S)P zVB8Mwu8j>OG9cvvhx;exX@X9QbKFsPsV@Tnb*F{S-6yMfTa!ST zKFr3Tj@;WINi$V412hoBdPM+P@dg8NTw7wO>#@3H;nROh`4@xE1U~cOZ@_1{ovc+f zHOIfG7PxH&YLVJU^WqeKJ%kR-vX2%Fk?!5!Y$TD9v!|i#A)K%dRkpU?)c>F{;3Cxo zeigZ&pY`-86e{8zN^u2!4+Oh_dhZ^=2(-{QhFC&c;SngLCNO6o)Z}?882=v2%;J>nllPh%z7Q;;$Yl z>+@5`8H~PuPpr(ll)={4q5XJr4?j5}Qo0Aat@ShnNIV9D#YHfui?f1%ECnrt9S8%4 z9IUy0_<w|e^of%4_(pcYuj^B|9UEc=FGt@Xo9Rt|)RX3@b&d$xpg{1Mgb zPr}UF9!v5PS_s!c--Q#PJ^m0PiMhR?*;MVv-w!t!+)f@f%%El1C@Yluwo!`l+UQ@- z_iEcwGQ#&dNHJi?>Ls;UumT^3@z;FZrdvq$H3V9>dDVcJ2p{2cf?YtLm7OhnED$T% zykj@>=0SABXgaz(H~Z%k9cBuzIwDBjJA1oz8Y%T{7DE}bujl*w@~S{* zMoC!?jir!I=LLS2-Jql6IyM81H2TmG*S0jwaZ?ns!cZ``FH~ZHI=Nu@7MmKOz5>|~PJ4s{|0X3qq)TKjK%mmgG+*gQGmO8+n~qxdy; zRj4G*A2{ua=-$D_18gSky=j|Z{|f)hEio{a$;|vMc&Rc~uU!1l=60n8AlBEHkZl|; zyb$~_o3B>2LTXA)BlRNm;9wX~q`iF`m0*g`$f(C7bF~ix=1KIe07dp(ETOs%9;_5V z=zZD=;k!fhEr6iVf(s1mnPTPx7E8r8w}euAujDDv;I5q#=i&K7m^#23@)EhgjM*0R z;9(`8e-WVB0PqyIc)_+QSUd%;r0sS6mP^wumuwsyW6wWhtmkLNQsTHQr~QPLvU5p+ zVNrw=j62T^{6HiK>B`IZ-e8eu<{zYG&}_Dix!M-IbgN*W`h_tPJ3GhuTR}w0PQ-5? z;Mc`*0|`^4PvRN=`TBVVnZJLP*I&C`O|$q-)Jw4K?3!C@vD~otb9Uto3?-U(BynNQWNDuWtP+*@|Yq&%V5#h4%^*jE0 z542g5DEwzX=WMf4dsZ-Df>+ZM34K~fNtzA4&f&TRmlTNuIo_Ljd%+I zO?cNQLJnyl26&+Ea0>?PYGMkxU?0kFQH_B?>IHDxk%EBF{p?3gcXy=>zWkDT6 zZf@@1S#SpQUOTxVvYIqAo6_arU=5O?1ZD@|G7mKkQ`y$3OD3fCd!} zF4jVR*DN*oAA2y6V*Xyq>;rCjvAe=~apw=&$|T752kR#g>0JG=6FIasE5Dzfe*+gS_da|RQ5pw{I* zhQ1c0ZZgZ0rTZX5BOv8Ev5N&Q91LzcrariM5krz!HYOL_?ib4O?O{v#5$M@xOmlR= z&DKNZlfkx2Li;zcJsirpx8$?f>nS^tw6rO$_BRFw|5cDDK&8Xx@K^4y>TAD0x6a8i zkyK_V*HrDl&&n(JH$BaHpWJ*OwpiP^WC9r8pF0nuX|W7|_oOzFGzHgtz@=x^2Qu1& zEtrSBH?Q{hfzQYzRUS|hlQ6$PypHvh4^=)69&SA8vB+6HAT430$PIva$om24JzD%WyM+d&t9_>mJq9f(iot=)v1hAz=IMqfgk;{Mq8T}S&-Z?j6ear1#ARD14ou}7n)X&DBm-s$ zj_w!eUb@qvQZ%%+(+<#h`h_a1vKOTP)ZDJQuY5mvLD~&lOm4L4{0^4<_AT0C2`+N9 z-(Q&`w20aIv?= zP#{@O#`2MT3hVLoU%>fXwKb3;bmQDMz z53v*Pl+{g7VPMD6-$%il&kwmbM~oIAp{(+}p+VEd#oGjCFwWZW{%NxI;`I<1$`^zY zD`*=H3LeClc$hZbcu&Q?jX!4ZbW&pVPp{5@v@3D)PTIA8st?oSxv%W2@Kt})YW1zb2gMx6*(v&}ZTM{YJKppJ4H@7a~7#=t#={MlMVR zap_XIAIPlSW*Yfu)Ms_V?$;ogA^Y2BLz!NpQBg$LXyym?5)!Em-aHl@BY}h!+c0ry zNp*T;hE};M1Vc{CxUvFrK&Ya7=6){`%of(5!Kk_v8yUvozgzl)m^R~Qj6%qt84xA$ z3vWKll}J_5g_aL+272CCqZuKKB;)dLQ;(uz=#I-Phy=#=dd1Pe`-Iu7Tn@X;&Fwh> z*4nfS?Wv%{s9XQU@Jp`$cJ%-BJFz^u#g=RbUIIKR-Cle@A>iJKGz3CV*q0-7A zKYGx5lnRl}zbZBmCrv9Ujv*D=@a7&$6!JxSmD5;g2_nKdtP`kyKCnMwOw=tf16=rF zLTQef#|)~etQto9_9%o6BV+$DV<<^ZS zC+(KIDcc{r@plv0CT1tJTQD(y!XW$>Caii_e6E_a4zl3@Fof3(y2t@-TLk?JivEB} zIZCK1GB5}%y~)|Vs{0q}UiZUW>O41QX)&a|`gOy*-v2jq35sXK`#v;~MYSyYb3l7x znCNJ>MvQGtL?i^#DK6LO&^zO%^-nF&0%@%Olu=M96%4_kjoSyqu^SLv70~hI>z#~+ zvBOnV_fvuE+$A*V!JqpMEn+gcHLJ**0YL81N}C6%uO`$ScH>QMZf?9r<+IMq>hVf< zei(zbJ!XzLd%RAT)Hl&IbkNbD+A!@c|GJC*Z~7aCZ!ii2smD1x;81wUV>c5BXz?f> zaZiiF5AT_U(J#vo@(ie)*P_zLzu82fOW-%G_}tPWb<}f;_VIB=Rc*OEN@oZ1xi|^q z^+p%H}O7f z;>gJnoMd^uocj0cDFFnRVAwX4{~CiN*yvQQhu9n+h|E{ypm%T>(B!KZq6gTl8n5KZ z#$7W(f{6L}`twg=G|~?mJn63G{&%zbj|a1jgcu(It+IChh`01T(_(h2tu3y{lx(=W z6T{m)4@_yK{+#=}vG<;N2Kujod9<5^hIdBs3N=%Y^_Cp6*u?b4wWXMtULU{CyjM8TW0zwm&u3Q}* z5s5*x$88xJ0fA==uchh{zJjb zR4^c+@6J9j+0S`)ehynojL{8-IgC%eGP9bP*nA3WYIdMk|Ev`dA(jH&*pRRKZKQRK znS4GV*tE`}wbI%g#)pTWEn|gB-)sMyzBVbU#r{oFi|i<^NepVv<1&inC;18ii~yl1 zoRoO|kW6fdy~ea-yju6n^p88kfiM2v2o!SnGfXn_75bREVZPvO@oz-xW|giFFOQhJ zRb{rUQC=Rg>P>I-UV|f-R(*~zH;L#%S~BI#2OT~x>64Ks7^n0vpwC%5_mu>%vq zsMsNln-YhHhMK!YM;X5H9#(F+hoaazYh^nKQ4|+pU(Qs7%=dj3y`>GH5_;Jt0;2rA)B_JS?WEJ2n=X& z-+=@YDzy7Ooj5vHX+`F>FORl2u~zKNzOy23S29x|BKLbiQm78PZy$4P)F!-yomW+V zUt#GIHRBmwR^au6!J=X-GCzW5eEv5y-%l%C77XvjuF!t0H~Z?Ri}#QVgTD>mwsH6> zegVZczX) z{V5OiJYsiYX2z&nuxnhO_Mvq*Vu1n~GBuF9zVWHiKLMe&q=HTNPAXKGuB*xxUEc#8 z&1&dl%w*@Y2n|zHXg*9={+47zM9b;K=R>qVLUN`aL7 zm1`F!?s^NAjAAomnv7=T370^OeKS&OB(p`go0n2iMI}lqGqRef(TFamj(h&Q;gTv^ z!YzEKpGz4xkB$CZL_&dzDqo)vx;$pPD422k>2LX~#A&v*owFRjXxP>fDmgaZ6;jq~D^KFBzd(`?}tuFN%hyIH@1K_cXwuX=3cZM8y{%sz7x z97(4dADw#5fc+(ka9ZiZoo@okqM>o@zqC^(i~k7FSs_*WHW~qq#BDUMeN3rin~!_S z9kS)0W%4>yu|a-S&!3+|Dw9*DuEEH8A}h6 zWt*`PW_J}w+}n9$7ta$a6`h>Sb|~Mb6|f&fQSs>0OP=D=##fRD1@b+bGG0?iz~9)=j}&i zSBHs$67W@_!Dvg!4ExqU>(EjVtAEugFBJ|WLsw$6TzF&NeM0Ghn#uLsWXjPj#N+|T zhkcnBc2=U{F(zzpGbi$XD$urdZ0di#zAf?aI#N8FBRInHMdB-*44zf{mMsr;^|k=iFelgUs9+S@+#{-X=zY?9-6@RP-5R`^sB+Q+i0%qXl`yYa!5^bBcr!( z8Z@NEnW@+wK1AnRwa@7@w;r#gJCmYTUM zB9juxd96c!^rNS{*z{)IL1umbk$X6wtEYE47KzPeKTz43tdbk^TYmPYA%S-^$vN~n zswu9RD3n@!YWZg~Qc|R!BQBL&nSG4f4)asFrP;>hUiLQR{Ng!FBLPuS8M)_sQ;D zXGb{lVkp-1bXzBe@<5#fwJS|z%6kQOmRc8VQElBLmZpAB7uB+6+gALBUaMJNdepH4f9(fp;-A z5v4?1m=rwRMARW>Jy5KP<19M(vK{#MJ+VET@bH`8nV>;ytn=ECw&4?rf7KJz{K=E* zWqnmcI#VqQT%6t-e(MS33oK~C$;pXQea+8?$M7|v9VxZQJRidheG-UpbPU_2m^!(B zDoVK#13G%#H+GConj46g@Yc2Z#%==l-G)zhmY!qNU1_xgO#JLw+h@t1R2kXG>c5aO zd?>%tvJ}i&Y=|L1Fy$326`ED(N2H>alLL3Gt7~h6xm4F9P1N@XufXwXvD5jS$S0Um zF&BB2)F`gYb^Wg!iszuYK-?hxq3;heT=9#yjNV0DdJ<}3{KE5a{orS%2Zf;_6O-$& zngPPUS7@$Aaj_S3VD5@;J%N}Q;)%~QjFQJmt2Nn|ks$;7TZ_HuBO|u9x2-u4B9|xv zdzEuLuQf&dE97}#{d`yNlkA%?N>Ta_ge)EYZ9#)qP3qPk>vQwAg3a11jW0A}QhGEH zs*?I8kb6#en)Lps5}z*i(Vr=?0#~Ngi)R>iKQ8O&mbmlB$mO#3v;MIJc*G8ClMCLV zTL=MytpyldWexdPleum<%>Q+Zl#~=oi`LdC-RP^x(+I8&X6c#jxbovIosTNdKqave zZ{1;-_6KsbTh=~lX(%I#_p$%J%L!F09iLjF5r`K5Oti_t!NJR%bn%{D3FKww8S7hH z6ubc#M3vc6-G%v{~o=G4#6KBsjis zDJ0$2y*L-RmX9wTbKw14fz$=GGHZ4xMJ)QY43ZKea2Gcfx_Xr*q#MVmeJewltnPcdxP zw+9j4Cc}(N?<4bl!X+{Ro0Ajl4#Cy+l?s#%$we7yidH+PrGdy}hPlmPw-rjriZ4&# z;^ICUdA?}QYlVf2Tc^d-)ReBlnfsH6d>`K=R}*zGWiIu1;syp{<4Nez9PGAsy}Y>{ zV)D1*3p^n#cpD!ZYhq)Q_VT5LnHi%Kd)Dawz8TE%rXm2;IIrdW8ofwYJz&|D8p;4Y z91|{3zlSyqjiQEr;>bvB{Mc$yB+K(Z_FO%$NT=!=I5QLC=!FtTW?-d@W;7oogTPN=}jL#?8BqD=X~o!EtiMD>f~VK zv4($$+EZUZ3lGr?Sm@EKpC;OIabe?Pw2FAk4-i}>q9tTymF>LdL;gB}9C{k&9=f_2 zHepi>`7D~F6_+(Zo37~9ZB_BNJeEN$J}2u@Jmn8kVq&htGYI7Xa-rsG=P+^S9r&b3585j*3b@MrsIh-r$J~DGD$``QdxI#_z5CGo>#PzwpDiI~8pS z6_36FUh%u0QBf+zuQ1g9exDH>M0lFF)Y$vQKJneE=-F@I8SZKM5WSoc*k1AtCcAV6 zlaW!U#;^EhnN4_ea4<*l#zx;((3(#K%Wd!se$88Z-^t)lE>n_5*Vx!EQRAgA8gUzT zD?H39UxR6%9D55unL20Ozd4`Q(&fT?JB#K)qVMCmMsinO^tKG%SRDexk;F2rryQ zWU9f=d|;8mN}5umMGgQUU~010>ezg%m)~@>=w|jYkpwO6)T`FNHB?A#%@7X=Y3j>F zGcbJCm&s-9x0;lpK1YzJD#67R$cv*RBll1aDk&rj3XoSRO$twOPfOT{+1dy z2h7iYj9Ef}?ISdzUIx;70Rt6U5Ib3aG2Kvs_{yltpd>b3HB$W2ReT#8rr7lzDFwWi zkWb4am5vv^PXcrAPtW@?-YvGHuXcPEP2A96JADoZhlGryH#Xl#6*otP>~_G$C3U^2 z3PL+OIP1Q?+0@ZdD&I{mA=^5A{I2n;y3n^~xH<6J0L5oxBt1-_ERU)=Qn2Ce{u}noOm>+_9$KD)#%b}Q80vHh+ z%0X`*OH~V$BOz2mW51BA?-#(_rA0=nMJ8Hd9U4rwv8>gHK0m*{D;-X=*oX1!XS}_K zLRTl58=DxVoS-4f=g{pYh>BL*Y_T2L_s{E_W7rI8YNDwEg^Td=8NA;X=;|ZxIngX z^kzLv@tg*j&Nsun)h`^dj5)-H9kH#t%>}cCR}`X za(SdL(lyCkT%ZTB$Fe5!PtDDDvkTGoeGr5$>8JjLkqH6OjCPyXFB3<`{yK8^-MYF~ zAMS^LuYzM~kacI|6FSoh88_WrHo(dj_8UN{$r2S^Tl=B1%AxaUut?o_>=quC`nJm2 z{uVYa?3%*7pfqtr@b!W#)!&NPLJ?@O}MeYIk{|Cm9eV1dx3rhoe^D zCJp{VGv_TXleqJ2RRs27HSEYgI@b6JJvUd9-?x7Y+}336#;8?s1?TsJ;UMSzZ4LBD z)0?W=hb3~3cS+I*O=^E$@e^yzN-sf?4j(@ z+~U5QuHHCA>g9z^*c!#Kw;`=l8s))o-PxNSPWRe1IE)%QGaxKqDDLuy+=Hg`GBbI9PwI3Z{lwJp6(du>7?R4WSkXg<8(3Ql^nw(6dI2E=Tggt4D|u4jrcbN#W&&Hd#A8cV1drK=8^T+>4BZ+_qWj#f+P%u<#hX zF{8QwD_#H0H)G#2QfD&W?{Qf~$OhI&Y;6Ur4-cL|Ur~%;C;T9x;(O-UE)mbgI zD9dvD%2CLJ2iG`zZtv;}v9dCe^z`rt(4HP;x?gYo)OsN&N0Qdmw5L8S_(k`x)f+cp z?kp}&((HZ{p`rZRq>K`W4X5?u6)JqW0`+*l&#Fp)6pdH81?L#ak=J=aSyOZS+#@BW zzJ+9JJle1%{@|-?%OPCG%F1Z5OZyDBXw zw#Z&Oi3YP`Tis_0V%IVyiRt(f|F{>|a-7wVQH_I#hxj(MnoQF)j3q4y&@df*695Ez zGE%xgTUFH~wz;`y`q?pjcV}LWrLH)FlWlI$Qmo4b}*$GoVm-GBc4Rrt}-0&TXa6~C4G@=M*rOpE^bvh#w1&7yKNr!xsfZ*UT? z31+nDo>N=rc@P-({-}QbZTqL03RicluFr#$la#Yd&8$AYCr@7|a5o^OQbQ94uNzC= ztx-KHdTuYGOSyd)yOH)$MBKN*d*IavD&^BHy`)%2`_vbQjrasLLDa7MnfO6{xAAy0D$FpFjO-1%8qJ9`PU z)br9rJrf~}GBPq+Y>guNpW;^^3qcA?aAL*~14eA??#5Dnt#maP7bR-!NIhrg_qDaP z18U^x57ea2QHIXLB*Xjn@BQ>*nORx$lv0FH&ulk#^nX4x0EnB2w3L*U`S~2>-lLWH zOrF)t%1k6h^(RkaMUqoebe#_MEwTTXFN*MdsoL4h6tg~i-q>8{ATMyXJEQXo^W7<| zPWVkv6%-VXj*k@;O+*=kPze6~nM$6eHqZ2m$=%V8vQOsjy=4g}SxRGDOO!yRuUbI=BNa(U(m{ z^U={|zyu$NZFO}O-qdn8O*FO`A@W6bw9?Mle+I6ST~PdGb8{13@}i@w3+Xsvm(}Ei z1g6!Fwl;D>`>dp-q!%y54Gh3hEnLsv8jOu~oNn+H%B=D_eqteCkPJT;iEmU^RcUE! zA08g23D_aW8A>zQSaTowsc0)Hbsz0-?JkedTe}@>TS!Rw)YQ}z6g+;dlydp<<*Ym< z8%HDSvG%OmmO6+pm9%K zAy!TeRDERywxmKP&+-&@+zRh5c(S|dd2+N569Kr|M#jeC;^G(>7@4nLfk4&sHSIR% z+963e__fFN$8~llrb_qS)dpW&c#RDwjj^#Yz3#K3Ed6?~3xqVuFydcRa|px1Fq&_F zu>IqPVj|+ncH7jH7$c(@40<^`J44ZR>1)ZXP5trw!UCgHd#tMZmw^EYOogED|2E>? zoflY_hqNr9(O=l>Xb-ySVGyU>xHSw&ZcjE^Y3;9es6=;|GL_;fSF>v$^o8h%$qM7D(YSE#uE$P#84{! zp(1_#uS&sP<=h-Gsq60T0wPnB8pFD{x-n}V9cbhketZTHrZ&qZR`b!=SRjb)Ww zRo!aGjg5_ad4o~0hd6o>WiXgZ%%~JoCM43<*-0hl<)NdKYoAAeQ z%CLlI43JV$Sy@}J&dgkRbu3uooxaYD%UR9qmGVFs;DQ6V&H)?_xw|IjOQ<2 zFtE$nh@R{j+t{o{jjfGW8CzHgnsrm#b;2l`Kp3&NB1QZ=qmzT5MQBhsR{h0rsWv^A zo7>RLtOABUVMzguWjo6w(UFlrPMQ|j%4_lEVOs=U%ZZ9pdloRTqAd;PwQhabsLN-z zb#zpalf%Kszha%3kl^a>{yHZI)WK%7%mT(UV4A#vUmCu`1Ddx8`{Udc<~-f?9694aQgcC8X6ia#?PNWKLOemI1(Ce zP6t`hywJ0ltE!ZA>+{gi(3dZpFt979q!b7C1D+?mKnYan1m>sh?d_W;Wxg#eXymCE zytqIpr`;Co_CWsgz z+3)-@^wMD-Fk%MNnjF@$^74m=YxPc2{ey$$lEI`c!ifyV=VTNWYr*b-hQ-QdmzHjQ z$(DD^^dwGzL(v*ed$_;P94qJUei#sdol&kRBeVD=JGlEw#hp8Mq{FDWTCghe6LNEN z!4|^28T|XctPBqq7hW6{U#+Qv4lrP7qQ*sD&u(UV8vQSJcE#t<@p97B;XMayo8LQy zfd!$`Zgyp?LfC03CNh%FN%zT37?%WLLFVHCToalc!5gq{2&wxD2YVaFR8%m#TL~{* z5^>)tvzhXO`7}6qvDF_taB*-p*Vi@cJocYs;yx(1WSGqukYy>&7>7fYA?^=$Cr26K zJ;`RpD}q3TIP%Je=JuHnSUP!lc(}Um!NeRi#PK-HcY#{SYcAv|=c>Z?V8&Sf(W7M; z5FXCg;<2BTi8;>FQdYJ-J@I5_@nkS*R?Srff2p3U+Sk_y>lSog)*LR>-P+vDDvXaw z8gOt5NR^b5!r`e0qY0+dX$N>DE`1CnX3Szz>}kqE^qhhc1A2sV!`Zan)G? zhshUAgT7aih#nYB-bDl2A;&O_iRxc(?qz1ZQA#U|-b9@4ZOnop3UF_1Xh0wk*I5(^ zyHiD|$jJCz*A0A;m#$uAM~R@L)11j>ywcFhs>busdD;}l-+bGrz*U^HJvnkqnMF-L zS4?@;1Os$))qGzWY8u@4Pjym=fAR zZDcn>F|xk1L#jXJ=;XBWwIp-h$MORJnwGGu!a%yBRceY(G%p@c-3#-2l!xX^*D;kd zrSKswy73hFfpT(kDA(kf>gwvAJ$nX^f2IF*k2J04b<}oos_?ROR8ykiaetm8R!ql&-D@$m6I9(kAT zS_;2K^nkgp&SzAF>OUjn>$e5>2s?rfAa5=%E+HyK1%>O(Qk(?T0_&eH&~E=6UhCwnCwY%@#-pPL zK)doqbQumWkjuloYwRnjW2TqeGtI%k=wsJTA8LKuEp=F{r{?}^C9sy8R2}c4z|n=% zig|4>_U-Mt4tu8vIW582c5!k7>jG2uy%e9p7sFIlniPK`XMo%C*AnLYIWfr#j~&G? zegk?X;4*PP+AHaHCMG8K_C6K&C;0KfL=#Lu=tc;~4_u={z1oAeNP30}4K<6oSXuXm zw8a#GXA+tO_Uqq1%@hWcadsF8%0wpcvaxk_cFHHSI6!3-4yZ|Ioa_VL?^lJLyGBQ= zHH!3Dz{C)wnl&Cl6=4e4IA?XmBRJ`Ei zKWOQstQ-*@PRLpF8~AKaH~Jq2qejba-v*Y2!!!~1_x9g`Roc@aTn((PPhdDTKwG`w0bpqRwMh{}L75d@fGE6Q zzW=39R0``qIXR&WmHO&6-$mhz+-M{elTl^Py=giAUCMoa7c@{oQBfR;L=l}lY`+YO z0cr~_N?tpoEAT&YYq7(`rNSs?=kELa=x{eWLw zo~qYhdcq3drUBrDOUkwfgY!>zmfP*d!^KY8I<1-ZxuZHPn4HY-o^<#0fX6P4j_wC? zC|ES<<;zBvmep`*WpM`xR}$bN?~98Q1RdsgR>r_}pnDwu8je!eaFdT?yT?QV01HJQ z1`iN)7$*N2mJc>NIR(S0gd7)98}E9FLV8^{xui^*5gvQRRaN_AHube|NScGmz;2ep z1z_A(Wmvu=n9`-H&wAV~6&!GJFDaM`GeCk;qup_G!asnsYSMF87YNR!rCZn6*F!?^ zEUfa%Krv%Iw7MoHbiXD=Msk4>CL<#Qf8;Xd2@tq6iu6!UFJc?4765TLqF;H_gfaHB@>1`rxm&6?EFgq}2cQM)q967N z_mfqBsb|runl}gn$knbWRLR}`_3Idjpsus8s6GB#oN+Hx3JRZa`hmovf3h478fX)Y zst*?FdjpY}P>}~E3J!tWhf|=+5?feU7=1&LyIRAaymdpWhJ^KZAhVnveE;IvfB!Qi z`ZuKc-y>wAtD;z>RAxvLyiumK{_n1hiyb*v+6~91*PyL_0xo13hQVX=;)r! zR@R@MXzROZda=Z!h_eU?2w)rl3TFO0g}~BMc;4IF+pq{uQe_`77J%A-U3zog4Dr?(@YE8t+%)LC~S64ZfuXx{jS6T}1e=CKk zSr=&H2GXt}vHMV61x?BT`N23NAWVV%%IF5408(%7>EXO~?NvSo_)sz;BIPBHyLWpc zB8aAc!n$rf9+i-kgj@|DIQyfcBXH?hc;pcg5q~}EYHz3DvmCfU>rwo8?3SUtt*tHS z9mHTc3NO!n>w3x48OJ^bFbarP(D2A_er#iLC*-N!5KAZ*&%*#VSD#;Hf* zvVo|7^2-t&c(#gGkzOsBnz`@aFV?^&MUS`p0a`*(7B(w4l$M$*YSe_y&(9BLKuoM2 z*j8c+VJcp8 z+wUE*U?GOVl{7b-bR}@zO+{SV1tD*PN?Q))zYGj)k7L*NSph4O0!adRg;Y|~7=-r# z7@CC#%y~QGId`|WK~KOY?}JHYT4fO=wg5>}w_YJ7#o-4nprWSMfsh$&(bK0-XIsOA zrS+=p8LnS%`t<43VaC;Qfx5@`}8GD+Br9@2t6gH#iu=fZ^xw4xrafPMh?GAbpzqCmm&a@pA8=Jn5DsUa#uW+d>KDFNDn zSWpCLN+CJX1&QgsNmx$#5&5B1@OCqVL$s{6FvnO+ z6U-Xu0DzW6UW?1EcHr?ne~1$yH9rv}1JHf^{o(Sd3;RHVE-YLFnJw%wNOfGLpm-S- zb?zI8{7Sf(<08>h_~ixytdd!_3cz%h;~tgc%pi^XEZ5acaQw zQr&t)PD0YObp#X;U7tVqqzLcs>7NBQ>)-CAF2H#Mb{!rb9{GZLfbzip;SF%N(&X3D zAdc@0Ks;%6GYrz%agpgH0M(YivenFtKZYKce(O$iJRd(U|tN>l& zPCE82n@POe*(w`|wb|KzNLn2oc^JmaWiNp_R#3p}*#cKV4LW7ao{^COu64%E3xY{B zpd4vBBAQDdPWVZ{zi4S{#(XeyaoGhOyr_oqZ*V5ye8?&&%*h{)+~nd43=b!( zD*+n?SC}~lvy0&8-)&~brc=ns<@O8N_ck(5I)xo@Y$QrjxbF#6G zL-rs`HX1T2bk@{>he3-w9sJaa0RV_U0D#Q4N(m>ScmqOJ?P7yZuOx$g7~}xLrlzIQ z0u-a7g4w_5mQX5TVd3}>X46h+8>YYt2)P%yGwTU%K#?TcS8x}ZnVCCU+7~YP1GU@D z8#i)_i*Me#HPqg2G4STzj2^f+5MC{>uBGJ?Ko&lSc?Agk41;rXf57K^^E5bP<$l3) zwi$c>`HsATLdbeg9r#J0rag~^739JPoKEjJ^u6wUfz5y`XdW5S@(6&a3*P;tQ)MqH zAb>y|Wi#WM?QecZszHpl2lDsco4zn@c6Xfh2(78Lb@xX91Q}x~ed?7tEl&^Jv6VJ2YL3w^bL3zahypEii zS$1!9a#AnD)k_6cSg`r9;kV}>nhx~En3$Nb1~8=nJcdUgYM+24Prbkdh)Q`zUk~g! zMyy;p&>y9Uq4~T8FYHwJ6La|DhrfQnCnh0D5!Ek)5D`=##zaHIW|WnKo|`WYi}l)6Sie+V!{W>VDQMXd}P5y4S$`XY9P?5J6Ott2F&0* zOOP=-QBm7@SoaILDQ`KsAZN~ms3?f5mKGNmmzO=@PM}MHX1?g=yp$_-?^OX1fjEUq zDF-Zn0T&oB;cER5>ihx`2iVY4&?*4B@T{C19UURD7fCM-#e+$3t^h=S>Ig&V6N&O~ zeK}cKkO7N%vH-gV{<|Sr&*K9p&{6LQmc@zu79|}DYLY^oE#l}e@PrW)ZKUQ@X!We51{AK z{_)wF8A@LBzw&>ruC3L3pL!YIs^38ezs3HGz_Njtn7Zt6+k-tVy@7sA>S4Rg5E=}z` zh@SW-mkb5jb*oju{^~Ex&E;!Xtfsh3a`Eus5)gPn!2w7zIcQ!+MDzmYK~Vti5kQg` z;&Gs&c36QLc9H%mXw!-o6Dm%v2iE~c!3HGT;Byg)T6hXDRNLEcHu@7fh4u9G=$Bi@ zx^96Hhg1#Rf#X*xN8_A)ZE1R65!*5L6pH5*O>`wZI5 z&0JL$yK)J@rl57e#+H?ye*5by^}GxCl-ny6Q;A7Q^~XCXhj)d_y!^pca`H+@8lVA2 z1S5#DwzL~bHg$ced#c@8@JqlejCWH>b~!YQ(?P304B3l+C%b%E1-vi#xgc#|<}HIf zFGMTY{9vU$p@?fYO^K%c{!ta!O0X3AwiP(OMgTCtcR>0~PpcIiCY<%&{(fS3KlpvX zVAvnKyRBh%5^R5;-Wz`+p>i-~s*f(U9f9S>Oa8TM6(37O!@Y3SM+& zZA}bL25<`jegwC5SF^uZEl(Z%eJH#qMM()o-3OwDK&r;^$F=;NO(Wp%gI->%!MB4D zEkKbFg%4r<5U`!pjrwC#Lmi)&(TOLM6Gf)zM*x`|-<%3MA zP>9*Uw*VFp=HYRGI0kYdy{UZHi_6P(%Pj}Nxq~94MnzrI$q|GR2bE6qFInw?QXqlt z4R{amc}PeIEE$rJz`m)n$4CiOaA44S(!{3V1VWHkpjQi2h<**bLqPTnaV{!7wzqBw z)~)X3z-r|?GpN-R98oAng;4UzWhg(IsOAHFxag=+s51g2jQ~m&5_x4cy*5yx2=wMY zg{ThPmGJfJbC5yW-q-*q3)$%Cbuy$Zr;HGQU?_CGhbRGX*RSnuaQxrEfx*?ZmO4^7 zO&&k4h4nxH2pr{bCDfs8iF5JHZaRp$bp0faaUa4xxq$ODs4Vuw>v28aM!jKrpa}c?r_6egs#S zU=GZ}0wUHUJ3$q;p@|8$40WvG0_}=u#=Ai-N$_HDQquWv-~L(# z+ZR*G%E^feDd5A^P(lMqC?yMQ0E&;Te%pl`1r_tySU!s{S#U74A3QiXIPijM%;@RK zs1<4K+qwEdNNPguJHxKyASR6eKF?+Gq$z8 z4H<#H{gInM9N3@p87gp1^pLzBEjAQ)aM(ien85>>35O)pI-+MI_K8{~G+A5+@Cs@B zmUr`UZ5M@QFVbZtn}_|srjio>?a$!(uly1!*YtlhVfBCMf~SO7kN2b^kmnzx!H=w@ Ll0+fW;Q9Xp`;len literal 35523 zcmeEu^;?wf7VanlDiSJ!qJ&aPN=r*iD=7^s(p?UXNQty`iF6|!L#c$+NRBj0w=lo} z!@zk4(C^!OpTFQ-#~+wWyxw`ASaGj=-OHfIN-~5ODK0`F5JK5Uk}41g&H@B-_UZYv z;5VM`I`-fL$3a|H?fm)kqd%3WAP@p=SGA{(Dn>3ew)QqJ%`A;+9HF+xG-8h*b3h*H34ztyWFY7tnxy6SkpRnztAE78QW^;e|uXvOzd zZ4QZ#Q0wR_FAO>rg=$1sP3|&FZ0S%sy^mK}THI#5HyddW+dSBJYm}SQ`x#O%8jHve zwa)ddv2M^ux%ISOV@e1)LJ)3n>mvukj5B{5hQ@nVI=`i9Ecl73S*rLEyD`+yoVSD7 zb#GJaE3lJ-hM~edyeu2#)CUucBmb38za1fX+X|< zP}f9IZx*ksDM*h{Piq;hZ|)n78LZ148KrE-_iys>%?cBzPloVCZu9U}cugGj!%-=o z#iBS)8n`<9?(M0XvnZ$FI@(fc_igC;b%XU0Wgl{ zo5eivv6N3eh-Bq}HV>-E!uKlj-}IRfg3k|c$a)r=!)tqLh4~R`b9uu@M`3O*8H_U2 z(tRmx?et7!f|;QkUFzGpsC@0snVt07$s-RnPR@ed#;!@*f z37C1s0|#a^v=)?eGkBt;%+d>B9k+iLO4{g9W<;~U@yTnEV~=`M<=i2-b{L96JZT(? z4Lo#GzQNx}8=A8s)&XC;7m*g|w4oC8HtueNXfM%+??G#LJ7@dR!$-_j5yN!kx<$Xr z2KgV}R-R6(RdDZ0}RyhIvkS&3eR8&S+mi9$& zWno}dwc2#h0=oB%E4}%PthR`Q&U7E^da#4w;MzVcQU7j~t}+_~8ROojw(?+XcbU~& z>pBlx!Z*ty7)@DM#g8t{UgG!;HEaInR9)+P0%%8n@?FY2#@BDer5GecBl1* zCJiweTq36Wt-O_Ch2Snz&EoKjQAeZ?;m(J-*4L6*Z?7~q5*$7sgG5Ve|*t0ZHdK2EiYNcLeJNG{iU+{Tn+8=*!|+g_VKxTzBhB_T5j3+ zykE)?Blb3{i|K_aqfC`=a)-Y0Dp6S2yv+7d(o)Q|$z>@-uPEectSasH-5jMmH}F#F z=^G6l?ID@0o!>tQU&9FBSzn2Fp}goF{X%y-(N4Kc*!pP@tcZLl{a{%A%r?*Wd*dO& z`)5eiWW^(Wp5~8n1Y3T!eQoetvii%ATCsqh zJEM(bGY{{uaz~`tkdL2e zV^Ufk9i_F*-N28VXPD7w4l1~FUY|0f(=S_r-+*t&U_HS)CUXT^boT{j^Nckpu*P6R@ zXGyVb8)rzmzeXX<>S2e6?fvf$=4-hkz6l6Rl;D5vjev;fXGS3#7H!|SvQF{c3>ShJ zr3$?-!h1z_$I;)If~gngTNuil?#qbmo?z$`>0Hxyzy3yvD;4u0)mY&}9{*)ClNeqibtZA^7xk4eIW+`S zChcnYEjhRnQHQvdlTlB8nJRB-g`VGpMm3!?F-K(0YC6tsh6g+s%V|RQsUAznKiEln z!oh@*KI;3ObFP$UN|sHB`^@6iZ|rWDAYEr+c1^dskU7Hhk9PAu}j_N`LJl>@JbB?|1-;*-9sf;QgaCN1xNK}jK}-F z>fk+}J%C=4+2?&EKekJiRpE%GKS+`S6ir`-a#Pel^Eby0WE-gm1~))Ye}6=5$z@1G~Vppw<>%qr#w~=eUf^ zmr`fmC-UFgp5Wf4$TqpZ^81TeE@u$sJ&*1Wzm=Guq{(*&YmAG2<{DpQmusP!$tkli zA{WiET@{L87eEoJ5J;6t#+f|U%*yT-@HAgYZEN^ZcNU)@MjJFMXTjhFk#Z8ByeQ@3j3_yjR}e{B@<^X4|8PQ?2QPrclSGrz zju6vC-+rxgbv(j{p!~%w8zF}MATImh*Z_RQ@^8dL0s%{lQZ|Vc~_ioYB<4<(;|5Iee9Ap0x#TKaHQ_PF4tVKknVOp zrevgh=vma$PY$k0W)G<;y?@!}8A|X-+BvGl(!RSx(ZOLVM|6AcR>l3h*YbUgyL%l1 z2cBCaTXoAu-q_GE)in-^f^2=X>c()#0r27@8)GJ-y;D0vvwe$*+^f&PZ{N|_;{dZR1TNm$`CS7=w^V`k( zF?`yUp-tLU>{mTLJUz7z_nIyJWketc!c_Rd>nMn%CCljI$KeNh#zdN3bNAERlns+u z$kv~Uh>>;7cZHibpny zh@9!Haqc;nde_5gckO`N_gdH$gv8(D*3a@Bh`j669|i5gztq^1 ze;hUZ{FeIT<;!P!Ms4w%9tAB_o3{XAFugPD_<~MJ6!12L*mnrbpQ5#3@ zZ}$(hu9fm{9hkER$+G+~ejDOty~<*y56C16s~J|#eZOm(`IG3iX8xqSiZ;o`$hcnr zU?q1nvGxV@7v7v3h7br1L{?Hv?N!psIMiKZHu>lXRU`52EQ6`;Q@Wg-4|g-z25Z$= zncDBjf7BZ=Oa3%%YG-CO=)A;DJL$ZyT06Y>z&WXRX`tdt;d!>|fx_x*5$_&-q`4CF zn2i4Ng$Q)gB()308HwukF%&<7AF=H!vj6J+*BO!2D(W}NJsx_9jn_0J0Qq~rl8tlpZ>s_7H)uR`gfH6EvtEqvuhu_Z1e+0pE!RhGi zr03z|(^OM?EC5&x8U23dQnV4?vsrtg4X^XdYgBA4aYGU}5S*Us<$@n*VbNY_adIDxCt$s*M6}%(p>?oCmVkJ0)OYs)1r)wuZ|ZY?i7!= zTL>fWL?Nt;W~I(uAG4Z6drC<7uA{REg3d!%rS+F<$l_tn4y^1SoBywNZ=3h-%##$R zX|yL(wEYWt!GAwvQ*4)~pj16=T@xBg)K4$@_TP&qzien*6~B%S2?<%VG-Ldag>YXV zI|E$roS`i~IL|BI&ExjqxU%E}N&o%z?01@SIq^z-D1iT6lH&grTG#!W#?;EnY2Jiu zb0h>dmv`^ux<++y(ihaG1g?wrb)VM@r#05Fg9nr3FXi&ySQ z>DTc`@^J;arM#vY{7y13l$Rx~uD-uJ-E(9$0sF$PQ5=)eB{xD7K5y1~X>cHpuvb2` zyqe%-h2E;hmke=uys6YQ^V7uk4u*BL^H_B*YLY`Y2R|@6=mWtTCLvcWQ9>P)TUNcnKdB7Py0~I65DJsgg$<6I8(4r95&de0p zd_;KtLxqL9YI>T>O51@n--J)c*DojyK7D(%CnHYrII0#>~u?^NxpmuE_4s*C-h3bPlwRJ@_&qo=YOG*!RA^UpYdel&Q znE87Bb_er*fY(&`4ag&2uqJ4Vrk2(sG-Xlzj<|lERy-=+L;%rxFvlmb^Mms6N!lir zbKJ{rl~j>hls;yL5!&#yy1>$o`1I0~n4Uv!=A3z?T_Gn!UFfvz*eg|warWCsp{=SG zBjxE9eefM8pttqUL+n34et&hXdbA6LxyGzfU6z>)b1q}CkqPJ#R9+s+t<^r-pdMFP z_3K859p$XM%S1M85Y`{Mis_gBX0h0?jyMiP!2hkt&Na7&+#H9h`eDCdy#ir==)S#P zj8CmL*)Z-d){N{)%Dr&4+&Gv)c_>fa&vj6|w4kL9IAKi;c`pbY-Z|r5jn)lXk!!W` z0aO&dQo(N>*XsqA?iOmLe~YSsI!GdLA*2_<5lSwtRyl37L~i*~OJWrI-(KuaDa7pb zzs41qzND_1U!X8Jm{%NDehKn$9k@^z66S-0Uwg7KS{t22bMta&S?jyrcQ=GRj2}UX z1s)L#XNY)gbuep}KU!TiH+^hn#q!ccP&-NZI0gseWp`X}!ZUB;+1{@GiO0iZ7A9P7 z7KKMVbkoN42oo~M<(QL@D2n-xB!hL~`i7^Qr6zkDp4Vs`&YbdDK=ZExol zs!RBej8>RM(J7`jpmT)>V9P_Rzl}(8K5@KTo?)nAU86jGuW#Kf$Px;msM{Q2O@rE0 zi>06zRpo`1iG{)k`TS#Dc7Ib6eb^ETxqi7X(?04u8H(aGIspg%JUEcCilA%#x#O%i z-a|h)7@v;^38w^s^1S9H8ym!c=@1L0=d1YuzR3c!{&!^OAnj*;>$-*D`FVNvCfLLP zmODB*(NhrrZEnZ;2h_rRYiBesHzLE|Kb$Qxai+4`*Euii}cC%Z+Cu3Fl)pK&Ek2NU4Lu;nbIXOvwx9ZKIX*Q`#8+#artN{d7hV7f98rfaIhsN+5HywuUU+cKrY^F3X)-wcgJ`F00qk7 zK_&e8az)^pNHDU_;%#vNrdsOQ_c8@kTHtZ>4Uwfx|gpK}F%ih0oLb4Jc zH}~n;=}v?aVGv%jeOrt5dpm(heWz%Zd&1lA$yndcy&iw08;q3v<6%`v7G5vH5!T;2 zbPS3sEO<;GuPa-HajyH|FO50S_w$WM4gNYf@n--m&d`v?tJ1L7NVc?O$$wLt(`|QK z>rLRvl0O4+#O8;K>)m|Yeez5FGO@bPYTp+m+rCy3(MomigI*zm&{#iEZ}q;b9BpP} z!_La;^upm1*kb`KR`=tmb$@JQ0eXFXmihWOu#gdLb&b3lEf2J`^RUu4bGyUi+uEVX z?wA2#J%;(-J&u#B&X~bW`St|d*tXU&k6l12#eeiA9%>D#MfL<1kKMb+%0_zPH4tFL z?vpmAroTNVPm<=9_b8Xf{x)ihfx8|X!S!C3X`#Vyw^nWB``Ep57mgGeb6Rl4{+trN zxA>;VxP35i&LX-72|)CJIK*uC2qti~Io^YKQ-Y%6;qC6kY?n!ODL=OACdYgArSmEK z*FQSF;Y;FlY4x8=f%XOo>Y8dTl`SDVzBc!OxME1j%aouh!p9EI9twyuFj!+A16h~y_WPhL-xa;XdbcK3thjye_#h^5 z2C2`S2Y~@p6eo#X%)y~d&?XQ3ilYnNE0e5zjdki?xZbnqs$8=G zV&8`$A%2Z=1$Sh;|0y);9Z}=vR)c8)<||M8Gv8QZGgH?F$V^5>6cF2MAZyPQH0Tcw z)~;~M3e1vbS{|jB7b)qX2)QiAmY1;EN2wHJb8|jAHa79(WE3`s5c!oFgoMduE9;)* zL-&|DzA@)!NteAeYbTLqVpaF3&r&3q72gQDMR@A@CwZEtH!oi_d4wLCA{{pL*-zTy z?bli-iU*GyiZkVTEaQO5JoWht(OFqpPpX+;QB}FJqmvuw&9Yy*Z#oa#hH9EiY+CTb_EbJp$cjyWLVP4^&FL44} z%NxR*+=kwLW<0#+ySDv%sSvwcXk*(n{Y$(wlX>|6Y780499?d4)n3E%~j}Lp~|Q!XJbD$yTq% znDimmfLHIW8MXU|GdNq{7`0(fWo{YOQb2-QPMm@zS&3b*mOEJ~k&W&SINDpAFUhK9 z%9I$0pl&6o2|okYtH^d-nLpd8($&#PJgFAryX(xNQlQcok7(N2IM!{h=Nmtxs+sx+ zUW7;2IBb7o6gaR2c3=DcRUD^OON;9=YyEZ%fEP`K*uW9pB@o|kUDYr_LnwfHAtC0b_w_D9euM&;;5_{^fZnW+?izU_8@(qJ6SpT! zZ2@Qe3?#K>%DPOAiO2Fs$$KurMeur6sBcr3Sonn{n>3A1y1Vvbry&VI2?+0FoC}+C ziTh*oYT9|*%Y((Ki{{UA3l-6;*@Ze4wrP3!ieSS}up1_RjomOIA>oN%E6jEmMBNbN zUCI>}!?3a=-ds?b$Tz-1T0`^CjvZFRE&>03r0+|tp!nE}phBBLEP~+mX)Ow9l)9}6 zH&>)$rb+_$H^+M$yk?Pd^4n{5+ST8IDI2l_Q~r?&?p0NJumPRGMnADLrh1i8E(!Ru z(KFjBEv9G+j%RO<^DZAPw2mr{qWB$vft2Tdsyh)TI`IXBc9PPu(Ogd3cT^s_x zo7#)^a=&g@88r4JL+bTeqpI7XCszk}1)PT{PJ%JTackv|;>p;ELk0r0V|t+?a!%Ht z=(R%5g{uQq#WC!N<>Ew;!Y#joGZ=q$U}rU+CqdL3Ap`cvzrQocKmQ23$DhB?PV~Oh zeW8pZeBFe?s4>k#^>fJIUH}XFbqZrp4cYNrwAI>tS4JlHT?dEE7ap@36zLKuo6hJT z?`G5@f>6qNDzt7^KtSNM;wgnuQYLW8aMmj^1Ysye^)dTqEf?$e&21WlgCdY8i0!{g zsgGUgY*Dj*2Eb^ryWf7D@5G*lVP#&{Wkm*v&K%TV2pO~ZoBts{wB~B$RkH$3vbWBf zbV43J+LgZmah^i&PgBzqyEZa+#@o%KpcuwvWa+;HRit3d3#xsiDrBLH0weP=zSMXp1;*%#4 z((fMYC7YWoAkNXmoR&v`kQfVdxm@rfJi`@zAdZeyN}zP3@P@=;%<)kv*6At9(E zJuqvH%)JqAK7aDCHP<96mQ`o2C&n2Zege#VDu6sFW>SuViuYl@bP-=*x}HihihQn{JryPnz4g?41D(M zCq8}d)TcML5+@hlGCOX5bKQ{Y&!jW(s4uD(&)3X15XGz?Z8G3_ubo!XNDr?s{)B5Q z?*IuV-VN%8C38KeXgW$))9ae;T7768)pTXafatn>AxvH_;VKoX zBN>k4mW`zH7;vP77~%q_FS0Q2+u>_@P*>v3>5Cq#8L8|B@gUqCdnWAcrzL9mX6w$1 zIKjT#ub{kw0eouH!-Il`iP!D?U^Q4TKE=7u=*wD#Y0F+_HKeFMzBu=pzzH>8yQ4k+ zDjlSbuf!5hu)Hj|UL9@ivEe>Ybo#|sWl7JcFw5q?mktZ<9I#gODl?d~R-9ou0sN;> zPF_Cv1j?n&>3tH9w60SmAa8wU9xH{RmcLqO>twrV5#S0MJ<|%6~IFgB(xqcPi zSprxHGX04*>e!4jINL74d`s5hOFWb)M%Kzo*dtysqU^NVno_%g^W!1mR~Zu1--Z?C zrV~QR(5yvsmTTt#7qH%tuT`d|`W&QgyZ;MEMR0HZqm;SXlgS2w+4r9tY#S_>isu)T z9O%q;R#FfV zlnh$+&&4N^!Ex7i_-9Gg)lA20dvM$#kb2NcYSN?j$RIa(|KL2QJ3W2!Q6#?(9>g`| zzd*}%XsU-ssyntsH43=)Tbi@ z@1?{-}9GC1D3R_WIhSZIWN*%NhxkF7zqt`H-P zaok?c9?Y+>{n>NWKGa@!3R?r04Yclt@9a3AG&7hDCnQ;Ht4aY=EU?UCMy~v<^zXW` z^iWdS%a93o)s*Z{&lO{TWsFk+(>_j(^Fn(Wbd+udJEL&NdrArS#Ul0H6j@mTn)jA8 zI!W-nmT`h`9fsoZi*Z#JK5j``W0PXOTYGT%zOfoX_laNayCQnPV&Nk9MfG8Gi(-LHy$Ax{5PnVISPz_|2}mb)C(U&k2cAzetdX)G1*pJ zZ0TY!c;?a?a?T9h?dB|V$nwv%>!N8v_lfz@L>H9PBqjIPuLA=XT{0AEA)V({39rqN z1AO&2G?>=qJfBWcrYCW?J^C@aho_E1{7z6Vb<)kGa z?)UJeG5r_0qY(}x7Rf>O^#2T4UacZ{s(rU$eH~CPsU8CpMgDj|89DQCCiyt}uU54C z-w^}^m_0o`{Yj4~#%rSMEGjOQvA<>QaAy@5Lk6X0=MT!gvJ-y!;L)Tt%j!-Z!aqX@0>@e+$Xsr-*S6$x^8fq~Op^+OixwB^ zsH@xd_a%R6I zWfO!)qucfV6|S1jqJBJ!7lgbB0x=ph;bkBnlauqvB+00j)V)>9B)9tMIxIblp+6Jy z5XJ$TG&wo#CrujHWIl&F41vLSeYb0+DJA@Ulfbq-Yw5bS=S-&QbR{n)(3K39n z(1nE~Ce)M;3fGeM_4AyP*YXnSxyZA!DWHM7w4{I_z2KsqB&JWJe8S53w2ygIvB4e>4jz{U z3r{@i*&3cwd*)zJh{7uK5$=B6GlA#+=5apDA5%R&*EdHBU7%6uCN;!9fG`F}BeC4J zhEweWYudrz2xh`SS`j1R=Hsh7&Rs*nu!c$gYDPPsItxrRx3zA#9Bwcf6maWo^jC^1 z;$ROC0df0u88EYm#ykVjWcSiVm2f|go!*7sMEdSz3xHw+=>c#V>PTYbdihRHP9gU7 zN|L=?eufM+j~+hc>lKhL0IjWf(sDq+zUfV+{M7cM51~$t0T;Hv`@BC>@^b}Q6nXpA zQ)Fwoy}jQXwl$;^xkY`HX5-Q6DAuk-vhcyT{Fxu1!LBkU6yZJGJq5OV4U^bT+c7*v zA2&k=f3+l2E2S3g@7r1s_=0WCpbtkXSg$gi1F4KdPjOe*f&=Pvz;WCQjfo*UfpMj9 zw=aa7LQWoOpgH^QU|`x55>)Hb>CZnjbLI{ zbtt2tz*4asUqCeA{SwfL6v1|Hu!uH1y_7TA&VQ!W{05}`!aqC=wNU{`+{UR@Mb-gM zoMInpYfoS59KkiJQYcE=ck;$Rhtfusk4rF+9R^aRlT!k3H#ih2hcbU6b*9V;i;DGz zto$=149852ut_4H4X>VfrTe78r)PdADRc#CJ)TDo`gGff3R@$UY-hxN{^}h~b+awg zJLx~FIab}9%%=|0{R*;S_oK++QyhHrt%dU- zRajsKLn9a!BP|W){@9nWiKaeuHa$$lb*on<_0_K+mkFqen_J!DTgVCxvT5Mc^Oj$I zGaW1rUZDCLeTg+7LOfIwX17gLzrlU)d6=;CcQJfD=V4=CeBy?synHej;bmNJv-oC@ zu?PK5G{k_A0rnR_y#)8dqKL0}zHTjIyL2b;h+-C%4cs?co}^1ByDsIT z#vGJoWC9kKL}sBAO+Gl|$6(hu-eudIRhtgpuVo^os`*rRx!C!gu3E;%G<`li#;&ks z26wYHz*KhUV5IvYmuqZM!H|Nw#pvzRr1^vJ*l_E!= zlj!7Jk5ZQF`_yLMi20bqBviDmnHpq?;dDW0E9ldB9e6HvRz*~*lU&FVKh%~PDkJj*Y@az(B} z#~xg}X14K+Rp(`8QAzdCv(2-6&u!tunX&o;rr$q!fgS<{x9Rt*%0j&Z16$+ko>ir# zDk@Oup1P*q;Ex{<7LsP3#kG;eDMB#f$3L_x#i$;}2e3!6C1 zb=B-I<;Such*%62aKVMjMvM8;D`m7r7rg-;S#IRZ#I)td4oG@GLk!m5WvTmd>EPhh z_+I^<>g(%U6|M2a?1zaSzFPR6mX&vw#z~}j{vDT^u4OGL>F2LOwR050=mL@bM{a9o z0H)s$5nSUJTq*gI(q-v2+5`A0>9%z(cF^WhSFbtlzjiVd^AL>9>U$09&3qw7lo%~7 zeD7DjS-{NlCwe)6HnP$YrS*8N)8WCMv9WQM+(@( zP~9a$xELv`ph0zU@x&u0m4|D8`ria~I`|_5PZV%6?{(haS#=w=s?u}l=E<@c(-KJ5 zFMSbo;%Vb^9{XmQ{S+MH_>`nZT4{ELjU!SsMaFA8!u%5_K5a2p#sl4)xh>&;PR#Zj zeXK&N2*x25t&0$4Vq+^h{=@rpqNu#8>Xf>ML%9gXR;$Ocp%d)1UrDvm6F^%VSlaVY z4i&Y))G^6VJ`@e`S4o0{ROXlG(|?B}o%nIzhht$%3X1zp4~=f0 zxYIoD&MN9c+XpdUUYiM+(HDPTVv*|6{$wGz&ZlQmyzGO(7Ec6Bpo>XbtI+ghx@{TA zG#s)TP{RG0H0O>(pko_l?ZO=ZI_};D(eAgeVdccD>5!@k*Zk`0SF;__-Q(Zv&BIg# zlC4cmZI2nsA&Ci4S)h_$AkMmgs7FO>(i8K^vf`ZuD+7DDC?3iZP{SpFD|Niaf5O{Y zX+jNNiRn~=8j{jU?dvta&f`sS`J$tg^U`ViK<+*^jUb-2VS()dRTnf5T=dBOTr}i| zEZ{T3pz*2nSDMtZWl1uWn_y# z#r97hcyv@=UY>F<0$}Y?^@(U^b#;1pf2|Y1D5`_kQb5REy8}U1?1-5+wT+q z304zq{fVFF_`-m@e=_cj$yLqXDWPIWoaMg!Ps`I9DT1v#ykP2}uLq1hECMbd4;bZn zN+3em{e3t1V#M*OFoIL@m}NE?hCz`sK>M`6nC+&|x32aog}ln@Zo@ig7r&$6$-^Q( z)+eAWc8*32;b+wj8LYC|OZh1YtAiqs-+|@EnBHlwHCYC-rSVeZPAa#RjUK`ExFXz# z*^b(P9poii+bxid62M2TyJH)ohG%`z!34xzfsLp;?-3VPgl|=Ahy2JxJ+IFd0L<&TGn}>j@Qs|Dz@#B`>j@U|&s-mK z;I;ep6}?txldXLCli2IJ$#UKHLl#`03dZVmllVbShq<^S9LODzMOlw*UiKu_SPqxk zcp?+m>rwh7=fKYi`dHmQkeTugFddE|0cK&v>q10`oauC~+{|17<3cJ$7%AC-vRg7zr*?%M#S2dwn7Ey4v!V?RF)!8B#l?IrL7kXu)gsbb0 zv+8tc-I)lCZM;(UO$rL>5Q^FMI;Z2MR#`qV&Kw!t9@3%U?{hT%i8TcJ&6_q0anEO< zpeG%8IWJ#TQIVU6EE0RhwJnq=&?~FwoA5-h>R8ug=H@O&DT@prBH=>Fce#;}Y#~(A z$SBW?z|aSu9q-*Y8s?O}covZ4{&;}m9x9OR!G>+#?75ZMSObxrAJ0?04)@gaHH@k1 zw>}fF7ig8e#L7_vHQ6Gkb51F3ulz`-`cz-#jxW8+5aP0*rm zyQZnTvDL2(4BZQ4NCV80ref?+$Uj9(hXv*=>JkW>1+;uTr=p_w{|bQoqb`#kUWdq3 zFVJ9MXJ&T$-Hgwum_WC9glRlNP5BZx?9GAx)7j1%Z})v+`Y#yT5;R!sQj%MDNN~?ZVW!b)JyDWL1Cpq{3WUB%s>e<+d&8?{TyljJp*?`(5`>_oi+vDu{eB*= zm5Om@s9+B;i^7lMpt&haER5Al1fFvlb4$k}JAq^>0mp?fuY>6aqB?1RHItzHqIUlt z&}uG56922|Qu z%yWvL%UXiTsl5(1K)Z0Sh24|GZD%F*=wKd5ez6~uo@lH7%vI2H!q#EOstF*{)raj= zAkNA@Z%Zg*a0fS0{}wjsx&Mni^F`4=z`P8E%{6ZvqNgQ>&0D}w6yj|6W@7VUr=3LT~J_| ztsD~_y}kHF$$*Asds|?CVSqMFO>Sf@`{hH4fTikb(sQmem$9+49mLL^S$40z#v^oN zl^2rv{OeJhMhtil4~X7CF9-VcLhxg$5g;$rb@GqUqOvKZU2$pquKhfI@Z>}1MW>mr z{@5$1x|7m5wQVV%1=JNR$l#%&Ap&-oEeHb$J(uxjeCh*i<^>wj?(S}{-7nOk4}Jw& z>gNHOx!2KQBCi!l;^)g=G+n{^;_lXE)L&mDVU&+q2&7fhEpS>MXlw-bI`bN+94@U5 zgJ7#C%IAWS?{sHCrp-ea9R(6&->#r?*3rzGMYlL#JYt&ts>3`}p{^A_GU~FcV3u2H z$)TpGHnwOJ>wbsY?hOznjk_!#Aiv!GlO}!Ll%B>U7Xl9$a=gG#@Y-;?H3Yxf77{?W zIbyNHh@dp_dt!uQfK)9 z<|MbW5{Os;zoMyWQcm*~UT%hc6f{;!rBeAtAP|=3ufsQiWP@DSRt!9eM@TVHY|w~= zi+X`VGAsCO0njiZ)Y~>o1l;)=488TGEyRA2UXjSV3 zF{_cvFW}4WS5#m3{4sJM>eI9TMh`MW*K%=V147@;SftFXKMM}RZFaKrh*i~iOgD20 zF;4q87?eU#0|(D^+x*jg!n(~d2LX&DP{CyawE*A+!YQ#x*~PF2U=N3uYnqOVtC9Pt zby1J~tr~}Uih?|yMMu}gzD%tYr{`xjN0^X?$+A%gs0oKVHvZU^narpv^?IVsrlp0C z+>Mt14Hrhr&6>Q=2zuCqpTL+nRg~d>E6VKUptvXoT(+6<6bopy!OX`ypqUARmRiZ* z`~hl8Y^4i>uXut=JzF~TmRSZ>lXWl|=*vb&FTUdfbkdJbz!faq-fO^i1^}JPkM414 zK-NuS)?#$57e}aUmkX;7*IXLWJ5`NVmG_(E02vF)oCXg?@T}nUrRZ3TZS(q_gujn3 zX*TY&A8oQ@d5WoKU*!is--!9*>3QzWiXQwVhKEf)7yU=GTKN9P7^AW?y$v~HGmj(! z^+N*m;V-T7 z?M4&Meut*xLYlex722yLUSMG1=eJybEYy?^qr%j{J=hL|NBS4H1lM5>KR}b`+O_y1 z`inCa@boabz6*?|O%TRSYj_2W>G90QbVVDLD@qSA#-Rdv-q^C>7{Or+x?jQU09;fl zYDN_P=~KikYBNR|c}yf%$7uQm>gpGj^+aB=uRN)M0M<{UwEKE_z4534Os}9lwjxqI zwMVRN@k=ABWpd6wi~-@wc{+se@E3Vw&iwj0nirrsu&hCNGwVmOoCFMLG*s>Z`95lb z3(zs7q`q!FMPR;w<~&GhutN-*csydU&pAde6QJ=J`?(t%GZkIV^WPp3&5Q?GK-H_; z(QEnrN>R@mwNKA9QPshDt!Ca;pHpdiLkONLJTg-uD==Qe<<#-M2mT`&Ri@jS|5`eQ zR}pl10;u{1HOXhTkBcBIzbTB5Er79AD@Z-W6x<6s0B-Xjbf4FL9DkJEA56|p#c!WM zi4XJvu`S*H@jOfs z)YJeUHv&)tQqFJ8y4O+dauJu_z*;6CXs`n=L+u4o@%P1AT2ejxe!<4=S&J`{*W}5i z9>HEe^g4Jhrq6ytO{7#$rxf0_t~Od4$OYw!;oLn_TIqMyYqiUVd#Oh?AUFdU$5P%e z7XGlv+Qp7qS@_3r&JvQB5NDb36{u@83NqoTiDE3%SJ)iZf26kw9{as23RjNUt=Q zK0H9eO^_-;X^BCjCd(pJ&4_QHzW{qW2!3>xo{^ zuXPQ`rCjh!4;um^WejS2CMs0~4EaR{gX-L6%r0Q@3WU)%<gv5$|qe~idt3DRcYf3YY+P-*Z;{rhL%LQi{$S3nZsR}5tRl&GDB_-u?`zneJ zRIEu3&M6~YE1jcUh)t$9?NU++0_tyvVvtm_~4!bdI@~b#$od zQKnxwA-7Ih{HZdHA~p4BA!Q2$U_ya-tnRp(QF1IP{a&Dz+%E+Aquu{UoT6AhTt>f` z5#`KZp%deRHNX3`laBp6nh{u5{G2_;6R<`*AvwA#aHDe*$oQexUe07? zQHopwtr@a|JNyo(@PMcZR>=$8;46;m<E|*GYJ&k37HZ^5gBT7 zsBy%e8pHGn4`+8GG_I_&O2i`?@F#R#<4<STKW*AG(Cq9mM;=Ib3r<@A33JEYR5O?kb9c&lN_dO2-f$TS1B1vz6$5ddw=9s_nX3SKPY8(RW8pQ6R8fPRpyI?Tj8Ap6ibvhvO_4TZ{lC@jTMt)wIv`jCq7snT7-MW4DdhN?s^U52i|_?-9d=WqrP>oVyn^5 z4Qk5&-NXUK1D_q%#Q_7Vu3mv=uRK}ZdD>Ku%AIjdV1^U0686EXe^24vPoW#vpQwC9 z!ecW8*SWDr@ERP!neA;bowmIlSw5cvYE(B)lTM+7l9Tuu&huY$R1+CF#hO-yfP6%L zu_#(hnsNgpF7?ye`cgOd;+{LsL;zux*RI4l&E?Ys9X6=hYhh)zI@x%XrI^UX#H4^Y zd;PIqp+BAzW_TJ3#^(sgDGD#F)hsgTxg;y`OR>WYYe}1ouwVg9H~!PnNhDQN961aH zvv;5DfbQz_tyFuT38Li|TgWRM&A7?O;!k-U>_A$H-Ly2e)n%h{i5i4=;T<5Q{)Aqa zv!0m19&eb?tCAie)yPAXS9;A<&!6MP3DnjbCt7~-`k|APG{Nh9PW;+Qr`T%di!_%( zV^DH+t3MhVXDAya9j1X*Pjl&AP2zUS>*(AD^2SAaY_UUjtmXLAc$vs}HFYMlKd)$L zsPJSmrv~cQRsrL@Tw`O9?2__g;o)hg~&K z)=nFotN^sV#OfsZ4#BLQEGW_mDc&Pd3>-Ehx}D5dBUJBU#wTU{H9C(81t^&5AxWlL z5OQVbVmf2@8|z~*sCNAb$OhkOK`*oyI(rRhVj4Vv9&b^_yODqEak68iyMfE36)4vi zF8{|&)HJ(8U453c+gkkJ7p6yWfm4QNe-tWi{(~~H<6_mh=@KA&02IFQrZtpDkFX?} zSahJt2Ww+{!J+)E;56j_IgLN#P#mZq&qu1C_~HRL?TTCt>P%!rY<8H=)bI4YzZdC< z(CgJ3IC%|L*)uwN+^TKW0z&G&c^47h3i8NkKChzL=-&2t);!c$y0S>n+Q&ah=#1n~ zUt(^z%CAt)z?$0N)?Ujoj* zZ_yFVIAcT2O`XQ0db*Gs>9-|ZkM3OP&>;bdP3Vq^b*+p;rNXUE>7e5tIP3ff^^KX3 zG@4z zv;l^1NlaYdpSem%Rj|Fx<5JWb24BuReg6;1iBc(G5&aAxwKZ&IlCYCT!6JZ4721&j>6RoP}v0Ajz<~GOXR$cpr=z8DYNbcwUK;SuLgFT|zX8a1LeCg1EUKz0U zb}7|E<&s^Shu6%i0%b$4iCtu+_7*25*gfvb5&#d`#=~`tX^__>y2IQXL_p%f1;eU_A($W#zQJSVbFQUh&de^S!CLXY|mw-hoe-Xr> z9FrjxEM6m7uqo|*BAg(dc#Z3dwu+7c^KbxU&HPNTpj#}e8vuL&DFjTYlK6vMdQbIh zq!iuAH^yZGgU2!fp2(XZY=W7yZP0E!RtC2=S6YFY++;K|*Ju%tZ zSu&5UCqE5Dt)X9}|LR7Js@kaf-&`265iQit{PhfsXza;mJMI1i_2RgDSrM!c#0jVW z(FKPD0lj&#XGN2$nocM@FMz(;G z_x$pUUnh%uM{D310Wxl9>~b?c!CvX7?wePR8`JoYAAwPuPO^~eY15$qUp%HUHm*Ie zmrdYT9TUYh1ZqHU@g;$VWE0%a4E@<)>Xv|PLD`EFB<29Dyr>+&Qtba%pvA1YG;Bh@ z_?kc*im9F@1pjNowP|b-*$85H0)JUJ`^{0(5WXe!BqBF^sb(IC zG)PD4*1_Zo5O7d94m<($h~@tdH{lKbpW5C!tm9fpooB!fFd9vjiAzkbV)5G zmF}f_3`Mz`RJw^?CMIk;R(7IY+!>%<;afKdW4Y zfBzko?``51YON5i^RVs9_bBYsjf|)k^!XXu{+Jp%CV{5_fyWFu;y~bult&W~A97f( z4p;0b_w^H=(r+}AUIQz+5|CFW61Z9>nx#zeyKgeCB)EqN#yzK^ND7dzlG{HGsgTNuCvw$eYzWE-QQnQ)oYJ2F~TI2-Q?d<+13$^nO!S7IP7U~qa=vjgPpyF!H~5#FDG2tjrGf5wddH7=~ccPtm^{R#RIB)m_uGkAfx zG@+tFeSg!9(C8Z?M=j%MwROoVFt9}K&vT;3jtUD4OVMLTz5~TMTb7m3PyIvB&30H} zZ6+m#J$f7F325lGcYoC#ve8s``~qP!UClb;aHl8n+20nN zTJ7)a@q6zDAd=n}N2(=2>j!y3SQi-Z?I?-vwg~9cqS-B)c@()? zAv+hBSFr}8DE=@0KPZj#3sSSMpfgjJLfy#IYhYRTxY1C9%b%RrNJ9czky#cTIf#1A0utB%sQfNo*nSQUk~olE~oXAgK; zrD8KmE#9wAIQncMZzqswaBI72O}x9E%XsORS`C+-9k3+sxctRt0haw5SrP13Ep+x1 z7!b2ktt<@&SRm$r-8ff)Qo$b*LKp_sKQn4Dby{V)&~5p|wA|m*pNoXTd7zB`_tE(0 z?egt@`zyX`@`%2)~_x=5!doG^#@PNL5y$BDU{;X(Em1?ySAqV>*m_%38rPWIcy;;lW>an%JFG8Rly5^=-75 z@jzfK7ZOIBfPZs`0jmA8{m(F00Efp49lphJO$fO)r>%fwvtg3$)gIiuck(3hIa%)X zffG%_)$q;~g=9C5OFB%yA}@Snf*MpZMIPoFBvaHD*zD?p%0`_}4@jPH08!@isY+*L zQ<4Fo1|+&vf241q)?zTPbfWKO$Sm=XR#fhs4t z(}+$~|G~gQiRpDH-k6WJz?NMByWxDscc9~RvUK%4h#$17XL8Q8WBwWMQ|&0?XKPY@ zlZAv1E7d~zQf+nb>QBw3LHvW%D8=Uq{hIwScEwcFm|8gqs09jE1n)s#UhmHYC^?$% zL*x|;K6e2?3)RW*OGEkJEgvGXVc7Ro7zDnOF?#WFXqU>nUX0{ZPZ9x8ezNWXTl5O! z|24-&xQ7sE28L2ZRAODcx;BYj1mC+MTa>`5)94;1@wFJ9Vz8{=v^SOo0SR)Tk1o?bvTTZ z|Eh!$_d70tPpyBUWgfcz4me(*8i^M?>~{%CUUShTYzeX{tAen|OKw^7by=#xw<0{v$g z&kc)|mdN|wCzi%5xvL7abex<%vUg{}7dM|>-E!X83>i@moU7xa!3nECAq0$HD-)9& zm@5;PsWF8}JTsTz-pmx1x&9C(v$v)&S{@uFDIFzm`)Fr1r&iQfh)DB0p5yep3x+?g z+pqCHvGPuqpO{m6Unxh|Mz3&hV2bGtv@smR;-5FEI>fo4e?*;oaHz<^f#pn#>d&GA zmg@KSmzx3tbhMu8cXrRV>q>r%DWROkNT=pleM%G7svs|4ZvjjAHAdmmwKtB3d$N9k zm^fqIPYE%_VEg{F2};wtchHA;dpTl#i(XK}bx3IHIzspA)%hSPzZ7<=`k zk)}iTk=X!~#O7{L>x|9X7(pl9O4E^^*(iO~f>!g*uSH6NuZ{k` z$olQwhK#Cbn=|#3#G0W4oA34F-(I@$uq;}{FZuoX#cntE>oXNV^tp_4-~FyvvWez{ zx~{f}W95s8p>w|6uIV|NvOhit1>okWOYU3qBib)tPvv!J8jacBo__KK8<*^nz@y~z z2#qVg72le5*s3MFXs2@mwk;$1VmyIWWYi__kUE0^y=QojABCSDE5wG|Zgt%)^Sk~` zG)#Jl%H*F_VZH(*{C&~LmXrm1XIcF1+uqb`F;AX_7h$_HXCZqAzEA~pRXgG1*V{Zq zM7pJ>i{$@MZ<;ng2)Zv@Z=r|1`Ku{c9iLFt-X3Nvtu0s3R;sGQw3ZgvJ@BQT1)oGh zT--4%g)@;%vl{E1px_jf4tCv0g^er^MdqEgC5O$AG*Wjc4>u)^#?%_zGOnN(eCs9a z+E?A)GWIH4K|#UodED`k43F?1gaS%kr58MZ2GhZpz)9j43$lG8*hY!s42z}D~H zRaC6E5k#yjKb^XM=ZObS&#xR`uD-&nJG>5!l2(}> z8yM-l98lMD*)WMyF?U)8iP2KN?|+YoSHZ-@#}o4Nvs}6bZ$?e+NO%!}x`h5#n=QRE zVtOJGxwx7OmjivTH8chW;F3Lt=W2wVT(=g0<=phDZ)uUB3|Lq9biadAdhj6FOiVf| zGmC!b`~KC~0m>S-ytMl#+jzJ?y$K4fC{?3#;n%wDHMQSFa?56SeoIOZ@L<@;DtT zqRlWbFh|v}jfgVQu*aaEsodOcb8Df6AZK~#$`r$Stxw01cGNdpI+QCzieokX{1q0gV8Q|j=IzBG9My$+ECLY%e zwE4WbjOltg;VmJXm#~(xF+;4vP(WCSzZ~a=g#idZ3CSlMGynLzf6im;>tFBbJi?)n zf5TE`o6xwaczp^>&LLdM{nDd%B)xNc;~A=4@Ob_=z(6gn4wx<@6z|fwQ&Q4WyG3%Q z_>md5lhD&s;^27uQs2K{{pmtF2FJ69l)+AKHx79%TpdZyPrv-y%~6>L&@D= zdokfzBn_aKa)&kG{CV2@9#1UeosPt4rYi0r$A zLu#vdv^7?C@8I_ZNKPun$wD4Yy}@f=dqtwB7-B1_s+;JW8A9ZRFPe_aun{->P#7+@ z+{?|ps>u3pHk)1*P87)ls2hlZjq$@BVC59y7fFFR;^Tk*Fj#7rY}RtVOQ>l zAGp%GeHVV;<=s-OmxhK5z{W(c!FIE;UQX;XH8rc7o1&VOFo&-9dWH3d53{6+ow#Fw zQdIwN6q8!Ka?X4+-sT>#@|t^su&=@Yw6qjAFBiG}HNW#nIk|6;CaD7}E#?A+pJFTQ zUda}I%AmZi8sP4!#sRdVuE_E6>SuI1##oi$l%>O%X6)HL#f=cuWli!}{*S7OtEMK#X1oyk9G$Q^9oKvBQAswTp zjF}l#SePnPfLu>1Panh!Vy};13tAap6O}v1q!}RuQot%A6zNR|@)H#4L!aNW9-G!N z8jq!sO|9fHx1y#D6eHTzbWd1+6WiK@c>Njm?C)Z$E18&*Il{!B}aq+$4CMaR9{x1K8~HOm=4?Kx_^DW+}fBU zGQ;^P*?6WWBeNIt*TYSBk1(>kLfcfy*r|A#oEj z`TI79)!Riz&n#x;-m>n@3pO`%=wXYe;NO-CV+x7okF#8m&mbzdzB#|IEcEBN`og## zg`W(^qTf6|oCuJ^)ywgdS}N7iBo7l0wid=+luq;WT>m+=`^90{cW2sLlQI;V`@?cI zQp&9ikF33YQx11pP46XF=3THuCxAacG*>O**bzdJ{a)%VM%ugYoKHej3ge@1qci15 zxbyxy35nkA{g66%hMOcdt|dPM?oF@CNQT|4>-zmYI9logCM`wbRfOEV(^Jb0%4ecM z1?;+d-RaM#Cj^i7@T$FV1yli!*fP}D2Ro^HN0C~sN~xF28PPv}(*6BIp%ueR)h%@j z#O3#j5+5*s`jQr!5QAa&=PBfAY@Z%+xSXo)xk)4B0zQ7H7$Qmgg4)w1N{x>fpb7Gt zo+espZ*|7Y6qYxu0ZuE23tvQ}vK_-p`;neMI=M&eRSpgozPZb&E^%q82sf; zzBh&6UL^@laV1JhTRMOx{_Wg58d96T$apg|p~`M_g!XZnTK`xn>uC9ox2hp(jO#-s zI@Y7a$F?nD|I=h!8BIpQ&BhcrChSn+8os!yV_fcyRU&WFef*neHj|U7%DPK-E{FY^ zVWdkC@_=!ba%<@WqMoBYO(%<-R#nL+bj&*roh~ZL({Ya&E@>W&NR>_dM5tta@mMtw zsf~u17ks%9IDtW+k7;h1Y&L2zCo@yDPos$FO7_+qpbF{cKaQt{6|dCt$2qZ)hG&KCoRkxhKL2!#c&&oPA=zeoYZu!>BA_mlo7vmafU3136 z_q?m;L0+~Vf>wjWSoQ<&&k)O7)8L?SP3X_hW^6Im zdV1kf_gK~T`rU;Ex;$ygq{E#I4c^7EnA$oz&QLbC7SkJeLK+%6CJ%NtZ~k1hV4Su)we|ddb@Tt5E}+ zf%}xkh8pbb=2l8$1x!H!e%EOS81R;<9P+@n2ir-F3+-le7oj;2IcJO;?gP9Wp-cEkaTM{+w@XM+Z&f^0G-0UrQ{n zPep~AW=-#?dbNJ1vFX~x_a+@5k4s4@!+`$OI;Q8?Ka;Z>|6&pL5#HP!|IDX#xcz#l zSeJz3l@6QxJORHvU4bRt)(;9PGqaE#TYEM(;;Dt^`ufW9Q!Y7q`M~h-7xMCnp$;xb zcm7~;+f94h;HqwOemy;@Eaf(s-#TmvJz?e0DYtxmxT`DTr;{qHKijUIm>529F#GM? z?6kPvg8WR_HnQ5d@)}B*Z&!K=_V!P=@phJ9&g>rT_5A+c zGHg*H*PE&{9 zVqen7i}u^|GxS~`d3feqLi(#15D3<+{m^=8dsR7sg$2ts@{=u`(F)|Q#k`zp&9XQ$ zYAB}LRibz#O?cKs8$bu49N zWl%^_Mi^uNNn+1J0Mb)ZhLdv5kJ`}jsL7u!rFE-@3qS4X0F&l*kr^D^J}%qrjNv8` zyQ8rHlPRa~m5F51gpD zCH*&SK>;Pb7oupgt*&O!dW)Wx_Ncj&*~aeIgP9WSj9^Uc_`pC3XYWr7U==p3ULQ#~ zvybt#%c5g>GLpq2mS^sFds9eQAjkFCh-c=qmO6fx_FtBfA)p~7oI$d3aXoR^oxzki zvT<~rA9^^m(iz8G;yWo_S+%g(XFgm}(UT%M!*`@GQazbvwBu`@G91T^oZ0pAY8mE2 z$q0EUmpL`i;cD1z8oXiYus)CI`j{!>oFmZ?wvDQxrii028Lw79V5}RBNleNb(V#}X zemHvC$=}-U8E>1QlqE6tzN}=~+Xd6s0l9fAZ$C8CGKkYOxCAv+*fJ^Dv(H_rQOWU! zyfKz{`SBC;)$zs7&&Q~vurs5%-MpxbtC3>g2n5RX|+4d1tBr*-q}~ z%37M@rl!`Uk`=}UMn}oMfoevM)rar3N9+kk zd$eaGBjZ<2kzQE14a236A3p|ZsYU1Pl5?9^NjK3o`Mtb+bV}A9zU#=b5eW`=hl?xB z6AGk#ri-u|R~PVOkl%JhCSb5+VdG4j+lh#{XJllQ>wC2{H@|)JrWDRVtfN=tKhD5O z8@q0Lc6MlRP(0<4^HE>^kC;pH8HF(DPU7$gp!@SGqdR%|chMo7Z4;ettBIG5U7Yr( zFwt8efCW$#{fDmePk_KXQGqFU4+ahS`T5n=zud-i`_E$`aSc-VXX~$r{P{!I`Tuqp z`Nd|VixU%fgPKXW%qnd+w93>vY@RL3%gM2rPm8Ta8Q83jEUc~q33H(I?Nn2f*gFj5 zRfnzF_W1Zauw+Te)~>EDA0KSzR)vK3@89R==fnIoF)69d^1#MOnZ@!ZRL>VNFkY&t zsJwjX=I)-FmbRsumZ=z0lAr%(cX`my&ksgK$HvAsHa3obWzgH%+2PSh3eeR#sMedU^t#?98e!4Og3y?fW6r#V-`6Qa=4 z%F@=IA{oVDNEy^TGBUzxI#h3L6@Nl>BN66gZ~hEt)h;$2-aVL$TUuJ`j9~XzOYEJT zZI2=+Cl}U1p*mUGR!1wm=BGaM&%W=B<&9b+Q4*nBp8E0G)Wk%k$fyViuMdrX0aXNk z;orKosh(=Iy*${{+iN*iX~%!Etso%anvgKjmxTl_mPU=U6Bkzrh;3$TR1GyWq`aMK z#K6FCB+NP=GOJ6ZSMP}ntH;H~{rU6fg-h3iB~ofmc1gz@G6g=9^JZsfQ{KA=jK-Tc zZrllK{>WPmuldMb?G5ww;=)26*P)y!EgfiWZT<7q)hGa9m%jmOEm&)1)G5P1hR;HDA1Zxq*zQSHiB)YbW95tTRx&&%tun~#)~kcfO}yxV*~4h$hpv&Na2n0R>;G!1S(H!*qO5|SyR zWT#nan~;=*M?lbEdI0B*AfaYU?aELsZflp4(3UG?bu3=&mb(LEg zJ9}Rc{5FzUyA31qDlcBVK>}d}>*`h8Y*Fd9wl+?)k$vERF}=wDJ@7?a7|kC6&wNW7 z9v%*isSV{g2_atT#hJ9~^B26C9+xy!Ji+!1p*ERAgN@v*!z6s95c4sfWzKuL{tZ3oEtlEI z<*QfC-@LKin8X;z$kcfL97fEp(a1zMG1~1ezm%7k2YXIR(uM}s=c0@cT4VnH{xs5& z(U0Cz^75X*hJ;C^9PO>mRS1MMx3^16N&0PIem`oA0*|)#y?hoq$?ovVMzEuj?>#>GB3jxgS6N5&$ufxX( zp&B8z3rtS(M{n-1u|dnwe1CnykuXI>!q-7hZx*_Fgv=TyuV4Qx(3d60Qnv}*%j>iYXjz}glT790uFwT%07sT1wCX6cxYfx~OFGSmkV3kZB8)lN}) zL%o8mC}@=dpBp^2#HcS50#S3&{iVgls|#(#pm6cy#}6E0HevV}508EJS~AU)%Jbyo z6*g;;k&zHBL#kB1yyb@sQtfn9ZnMS|)C__QbwXbBO44JnQgHnT-~u490j%T@2xQ>A zb`1E16eKq1z&-=uXhGYel-lDu871Ya(7Y?(QK&1c&`i=h>D8pPd5dG;z z^}y`YN=w7j&iCm88HtIBNdn7_tNJ!)p4^WTm^^{tuU=u@e*F3KXIXGiHpoE|62ZkL zKzJ4qKxn393O}^VQ!9HqGlvZyEDy$jHdd z=Fwggmz1n<*kiIOf`}@dTq-OU7!>5>s&2`Y5f{Fd2K<`>b za(w)62rVO+Y!Pk4!>TM(s;nqDQ-B3gQKTi(VCAtqcAM)HLa|1Q*W6zZ;AKB1dtxaj zElsevuN;C*pK>_dUVzIX8Y-&Zqt1h^IXGmcJdLW}-d?=>GH+BRQo46nhK-MIAcMk2 z?UJ{x)zs7+k9JK@7co}c8`WK5+A=*W%VD|+*Q^;1&8w-Q!L25dV;UIqm$9&>a=)hS za-qheYrK2YfoTTxR!}{8omUM`etfX?o_Ge%YHzVG3qk?7>*VR9mxTr(#p2xL)Y)60 z-zgLGgxY5#hXd7qb>8MK6@T3-MR9Tre%O~x@?0Gk!m3N!V%ls*}Z)EG9@*2Q$7A_a%yU1R8)T! z5(Ppn4rf9B{_E4tc$Y4DKrDmQY!6J)_9!k8R*{gDoT?Zb%A`lVefuRZFR!TR;ZcJW zPcH#c{L)a=qyl)wKsCc^1`Hl;bRSo-HCI&r)XoRCRCpHEv`+r`CY!fHk8 z0Q$_&0K8ly;b385vE3WB0ZAFAuvWXA>xEGt4^WegXuOKBHJ%{)ANWs({>r~$+5ZAc z|F3V6`viw8P8A5s7<7z~qEA6gMsbJshaXvx7*z>sT)B^|x*DQ11QCFo5Uclq*5{oC zF`0^B|KQ+@aZF51ncLyvAs-*#B^>fz$CExH9$OYZ+<74Xb);iG;MjnBqE8+@ zqGjT-UVZ^TTUuH=COW#dwpIi1ie;A7(l79IK)DrVWq>JjGr5tswG#2%`kmFUa#zG+OM)5jW$MHEW_QNIX-AqDa;xhA@7KrhX*3cN1fGeEa zCfCE`k`2hGL{^NzqX3x^_xanmZ!Ik?U%mQ`UXj&e^1H9^JmK5>(q!Z6>xCU09L!!r zXn6DH4W!M)#Ki8N9!NEg0IW4#PWrxma~*xv=tB&wZDMZg?9ZRkk>-alsm;;u3glIea%J2^x#>}wj=;f&?yj!=F`!LZ||Gm9D%aBi>Bq5Xyw0{5o-4vjvxA)VjTw*|H zu;m>c9SI2uu%p_9vokZk%##pLCiQZNNQqDz1L^#S zFg^(hz^(&;lG|(JH6WBgK=JIYC#1C1)zxaaZ&H zOYr{!4^~GYU}D$3DFMzk8JAf!kgeeZ!2p@(s#i4TXc6fRe$|D~=?ZDq0e~M32P`Of zuA*gd@BnDw*RZe(ii<ARtP0Y^QB44Ses*A~$O^%v-@JAxb+4Gj&T{5a>sg{`^H zvLf(qFlEa>&6~kOL%a${*kIV~#>Pf#YioGcHmCP3QqErEgc=PEU(|g!v+LA2MLz4{N(}$Sj?kG0|Nsu z7V;_B*$bcyiG6I53boKkErdc|$NhCcUsItRs4+)yLGTE@yxf-sjeUJq0Hj7R>pXC5 ztgO@v^rTZ77`_%4uMT`kn(UE(4x$D+Y{W+w{aa`LMj`)Z9Z+2Y2%A@Ffw)1i3^x04 z5%Qy$SW~?h9-J;XvgVn4&GBX{pyj&STA0lSaTp3BB50saE-gXc9!e7Z?7Y!H2*Bg+ z-Mh<;klKp#^IsGi0MxKmeB+LQ055ynd_NG5yK1T}v3)%#ETm4N? zGCr|cU7no<8x0hK+}h zZwVzH07Izw0U*rQ;qY^?vn!Y+Y2G({{rYv9OiZ%;!t!82PIfkvTIo7?%UF$z%dhO0 z%ci2A$yea4AQ5=mw60*y-R8D#Yi-SfEbfivus#0;iVfJ#$B!PtKV(CR>h1CyG6q!j zYl8)IAvNh>%3xy9_=r@k<>fLrH`i;Aqy|(0!yUq~b7!+rD6sF5(NQ>Pevqj55@rKc zIwapkgeS%YL_8)y8q@ww0>6(64wEHatA+;o*^%ntJnRUd?HX=^5l_?gba`Te)u8Lns`BaSDX5(;XF{g}pvd<8&kwJ|;iI7z zmX@SH~g+nG4J+mhSTP>#JP}7wTA`eaDX5@Z*QY zUh^s)#S=>d-#{jf%6&*TPV2wV?cIP}#mdRq(cZ4>ayAO_+R_q#ZG3DjwrT`6^Y`yp zDc1&^rkB4#FM;m#`b%(ifK>!*gU!u`pkZL51bK6{KQGSDuLC3z!dqfV!Fj+Rm#;yx z7|T)RT6Kn?8xt3&>2wtW#BKTuh(%p}eK6L_^mN){QAcSqZtH0hwOtOrT}A%TsHkCJ zGnZw7V%l$Oy{PbTspv-=>l1Yi4Gj>i0sLraXoQ|YpW-guQ!#xZ1y>txYs;QY3JVTi zhShf`i^GnL)Iy{KC|cD)uB@!1Gre{d`XY@vB!D|A)LSeuCPu~;Fv~~?y4BTu6~ag)Iw}A>geV7nwDnL%)dvLi z_4WBRyAY+}Z`Mn{d@Hg**iO;v(EOn|Kt9w+>1%E6I^KGS|67}zW+SEF&3G*5yO61J zanKel&ID69gP;WatQY4nqOyAh8Ug6s>WyeSzSkDJhlYlZ+=M<8=oqaU!E`bKY8Z?$Ffb4@yuO|u z%%&z!7CMdWdhcb-Z%qAw1Cg)+;JmcF932~b8i~up%bOLc__Mj$OSKQ|;rRF%8k8Vg zI8awN1j4pmfgFs_!EOL_)p(0@0+(glCMMaX$pF8^#l@F(Ei5cDwr6A_pveiv z>naYZGPDiA3&{DMqeDVwrl*q&i{X3WCuH%d>*>)b?Y9@ezX2-aL5=aG@FNB?A>am^ zDYe_3@1tsOYg-;JK{eEg!8d?&Nhsl%r9d}?NwYe;kcE-a*K;63AO3y{;zQn~v>lR3 z1|Z-A<%gJ<*bC4KfE#&uD1v@^KCa`F6K?C}{XV278Hk0qg|mXGjk}zk0FLwZ@!>n& zp8|NPWcA<{!t(~S-{rV76w9qwR8T0CnesJWUS7({jwY%^*vXoW69`=NyYJB*?DBGm z?wp*QfO3q6iy2Fh;hxjxWm=l5bO8O)WpNS-lt^c=JV3i#xdrsly#YbSEeamnS18ol z$_lzE2qy#u+($PztCgW0h^rEF9bjfG%*?W1k{|^Gj>_5YSXo})g-r`S1jGh_9Nrc{ z&DrtR=Wdl=CeSJMW3da?04wqF`Ra1I6h~1%fH-@y- zFqtK8q^%tk7KU69%J+5f1*mU2QpySmWrJmJe}5m0v%sS2=->dVa8KSy&9@)mO_BB< z%j0_Rl}8}r2s(%mYCBKa7y>0yR)&hE_?n3!xw@Xa00{zoDpe{1F2a%RA2$$pIMKM0 z<9lnXF*rDc(PhJUSUOa#Bj4Gv>lE#8P8fr(LxLKUKJ(nnOpZ#?B4oGTo*rm6+QVLX z6EKuAekS9dU0jR=kaR+}xVX5x*k|0}P2hb$Cs(8D5Nz37G$Fl(x#n04z=5`Q0UYHO zJeqtA%heG(kbeQ(AREi`e5e|_=%DyA(gz_Mtl$nUZF)C1&Xp^6(5ac6zCSoP2sP_k zUshOdt{H@408%dd6PL_ZyzuUqHov}2M&{*7nhGsevtnUr(?Bl`dOz{-Nww_vR4Jnd&vN}^A=-d@869J&Ce0$<6P zHA|6S0A4i|IuD`umRSssd$3Y64ld(4U(m^?TI0-%8xau!4eH&ce%ZH)

      B>s^b0 z{tieuwvnUalahJ>FIb=H2GK|ZDzRJomBUp%2e{zi;J^uL3h2UvWME5Mo6a1E3Czks zdlK*1YY3wc%j)060@zSw4xSrH?4D z;^62L%>q&a+y;l3>*m$~k|ll={4!SyT0XkFo0n)g975o%nv5w}>417%)RzQGAYDDZ zUr-AGQZ!eE78hJGRFs#u=&*sGtgo-bT)lM&RGVR%&M&1u0`P`lDZ>ZE0UXYUvuBIQ2H*$S1Cqn<%qo{NK5jDZ z3L$&wZ*7lfvtidAgG3-)#1!V|v#_%-b*Hq#7L-*;i;IJYekm-}LB{aA#HuR&Y5cT- z=aItNQRAoJqww*>f++4U?=@XydudnPK%aC%BG_aIZTVZ0CG6u0I4 zD=n?b-@jQvhH7L)EhXxBy$%PC1GGg=eq|316&Yuh>!+lJsxhs>6GCr5LnF4^LP%4y z1}sk0@AgEoTIrkDV7O2&Ks&8we-dkT;VR-Ai_b^9Bw2!U2v Date: Thu, 2 Oct 2025 13:00:35 -0600 Subject: [PATCH 28/29] Fixed typography --- analysis/markov/ReadMe.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/analysis/markov/ReadMe.md b/analysis/markov/ReadMe.md index 53a93b2a3..c7c26dd88 100644 --- a/analysis/markov/ReadMe.md +++ b/analysis/markov/ReadMe.md @@ -62,8 +62,8 @@ Provided that an honest RB exists, an EB can be forged if the node has received Obtaining a successful quorum of votes is distributed according to bernoulli trials where the expected number of successes is the mean committee size and the success probabilities of individual pools accord with the stake distribution. Those success probabilities are modified in several ways: - *Adversaries do not vote:* probability $f_\text{adv}$. -- *RB header arrives within $L_\text{hdr}$ slots:* probability $p_\text{rb}$. -- *EB is fully validated within $3 L_\text{hdr} + L_\text{vote}$ slots*: probability $p_\text{eb}$. +- *RB header arrives within* $L_\text{hdr}$ *slots:* probability $p_\text{rb}$. +- *EB is fully validated within* $3 L_\text{hdr} + L_\text{vote}$ *slots*: probability $p_\text{eb}$. ## Parameters From e5bb11c184563914c391c1654fe7f54e99517d0e Mon Sep 17 00:00:00 2001 From: Brian W Bush Date: Thu, 2 Oct 2025 15:36:52 -0600 Subject: [PATCH 29/29] Updated logbook --- Logbook.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/Logbook.md b/Logbook.md index fc917341e..6b6d90f79 100644 --- a/Logbook.md +++ b/Logbook.md @@ -1,5 +1,18 @@ # Leios logbook +## 2025-10-02 + +### Markovian simulator for Linear Leios + +A new [Markovian simulation of Linear Leios](analysis/markov/ReadMe.md) computes the probability of EB certifications as RBs are produced. It probabilistically models four key transitions in Linear Leios and computes the efficiency of RB and EB production under potential adversarial conditions. + +1. *Forge RB:* create a new RB. +2. *Certify:* include a certificate in the RB. +3. *Forge EB:* create a new EB. +4. *Vote:* cast votes to reach a quorum. + +![Example results](analysis/markov/example-results.png) + ## 2025-09-19 ### Antithesis configuration for Rust simulator