Skip to content

Commit 4495b1e

Browse files
authored
Markovian model for Leios efficiency (#559)
* Implemented model * Added documentation * Ran example * Updated logbook
1 parent 171ccbf commit 4495b1e

File tree

15 files changed

+735
-0
lines changed

15 files changed

+735
-0
lines changed
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
name: Lean Action CI
2+
3+
on:
4+
pull_request:
5+
paths:
6+
- "analysis/markov/**"
7+
push:
8+
branches:
9+
- main
10+
paths:
11+
- "analysis/markov/**"
12+
13+
jobs:
14+
build:
15+
runs-on: ubuntu-latest
16+
steps:
17+
- uses: actions/checkout@v4
18+
- uses: leanprover/lean-action@v1
19+
with:
20+
lake-package-directory: analysis/markov

Logbook.md

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,18 @@
11
# Leios logbook
22

3+
## 2025-10-02
4+
5+
### Markovian simulator for Linear Leios
6+
7+
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.
8+
9+
1. *Forge RB:* create a new RB.
10+
2. *Certify:* include a certificate in the RB.
11+
3. *Forge EB:* create a new EB.
12+
4. *Vote:* cast votes to reach a quorum.
13+
14+
![Example results](analysis/markov/example-results.png)
15+
316
## 2025-09-19
417

518
### Antithesis configuration for Rust simulator

analysis/markov/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
/.lake

analysis/markov/Linleios.lean

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
import Linleios.Evolve
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
2+
import Std.Data.HashMap
3+
import Batteries.Lean.HashMap
4+
import Lean.Data.Json.FromToJson
5+
6+
import Linleios.Types
7+
8+
9+
open Lean (Json)
10+
open Lean.ToJson (toJson)
11+
open Std (HashMap)
12+
13+
14+
def forgeRb {env : Environment} (state : State) : Outcomes :=
15+
let state' :=
16+
{
17+
state with
18+
clock := state.clock + 1
19+
}
20+
let p := 1 - env.fAdversary
21+
[
22+
({state' with hasRb := true, rbCount := state.rbCount + 1}, p)
23+
, ({state' with hasRb := false, canCertify := false}, 1 - p)
24+
]
25+
26+
def certify {env : Environment} (state : State) : Outcomes :=
27+
if state.hasRb && state.canCertify
28+
then let p := env.pSpacingOkay
29+
[
30+
⟨{state with ebCount := state.ebCount + 1}, p⟩
31+
, ⟨state, 1 - p⟩
32+
]
33+
else [(state, 1)]
34+
35+
def forgeEb {env : Environment} (state : State) : Outcomes :=
36+
if state.hasRb
37+
then let p := 1 - env.pLate
38+
[
39+
({state with canCertify := true}, p)
40+
, ({state with canCertify := false}, 1 - p)
41+
]
42+
else [(state, 1)]
43+
44+
def vote {env : Environment} (state : State) : Outcomes :=
45+
if state.hasRb
46+
then let p := env.pQuorum
47+
[
48+
(state, p)
49+
, ({state with canCertify := false}, 1 - p)
50+
]
51+
else [(state, 1)]
52+
53+
def step {env : Environment} : List (State → Outcomes) :=
54+
[@forgeRb env, @certify env, @forgeEb env, @vote env]
55+
56+
57+
def prune (ε : Float) : Probabilities → Probabilities :=
58+
HashMap.filter (fun _ p => p > ε)
59+
60+
def evolve (transition : State → Outcomes) : Probabilities → Probabilities :=
61+
HashMap.fold
62+
(
63+
fun acc state p =>
64+
HashMap.mergeWith (fun _ => Add.add) acc
65+
∘ HashMap.map (fun _ => Mul.mul p ∘ List.sum ∘ List.map Prod.snd)
66+
∘ List.groupByKey Prod.fst
67+
$ transition state
68+
)
69+
70+
71+
def chain (transitions : List (State → Outcomes)) : Probabilities → Probabilities :=
72+
match transitions with
73+
| [] => id
74+
| (t :: ts) => chain ts ∘ evolve t
75+
76+
def simulate (env : Environment) (start : Probabilities) (ε : Float) : Nat → Probabilities
77+
| 0 => start
78+
| n + 1 => let state' := prune ε $ chain (@step env) start
79+
simulate env state' ε n
80+
81+
def totalProbability (states : Probabilities) : Probability :=
82+
states.values.sum
83+
84+
def ebDistribution : Probabilities → HashMap Nat Probability :=
85+
HashMap.fold
86+
(
87+
fun acc state p =>
88+
HashMap.mergeWith (fun _ => Add.add) acc
89+
$ singleton ⟨ state.ebCount , p ⟩
90+
)
91+
92+
93+
def ebDistributionJson : Probabilities → Json :=
94+
Json.mkObj
95+
∘ List.map (fun ⟨k, v⟩ => ⟨toString k, toJson v⟩)
96+
∘ (fun z => z.mergeSort (by exact fun x y => x.fst ≤ y.fst))
97+
∘ HashMap.toList
98+
∘ ebDistribution
99+
100+
def rbEfficiency (states : Probabilities) : Float :=
101+
let clock := states.keys.head!.clock
102+
let rbCount :=
103+
HashMap.fold
104+
(fun acc state p =>acc + state.rbCount.toFloat * p)
105+
0
106+
states
107+
rbCount / clock.toFloat
108+
109+
def ebEfficiency (states : Probabilities) : Float :=
110+
let clock := states.keys.head!.clock
111+
let ebCount :=
112+
HashMap.fold
113+
(fun acc state p =>acc + state.ebCount.toFloat * p)
114+
0
115+
states
116+
ebCount / (clock.toFloat - 1)
117+
118+
def efficiency (states : Probabilities) : Float :=
119+
let rb := rbEfficiency states
120+
let eb := ebEfficiency states
121+
let ρ := 12.5 / 0.9
122+
(rb + ρ * eb) / (1 + ρ)
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
2+
import Linleios.Util
3+
4+
5+
def nPools : Nat := 2500
6+
7+
def stakeDistribution (nPools : Nat) : List Float :=
8+
(List.range nPools).map (fun k => ((k + 1).toFloat / nPools.toFloat)^10 - (k.toFloat / nPools.toFloat)^10)
9+
10+
private def calibrateCommittee(committeeSize : Float) : Float :=
11+
let stakes : List Float := stakeDistribution nPools
12+
let f (m : Float) : Float :=
13+
let ps : List Float := stakes.map (fun s => 1 - Float.exp (- m * s))
14+
ps.sum - committeeSize
15+
bisectionSearch f committeeSize nPools.toFloat 0.5 10
16+
17+
private def committeeDistribution (pSuccessfulVote committeeSize : Float) : Float × Float :=
18+
let stakes : List Float := stakeDistribution nPools
19+
let m := calibrateCommittee committeeSize
20+
let ps : List Float := stakes.map (fun s => pSuccessfulVote * (1 - Float.exp (- m * s)))
21+
let μ := ps.sum
22+
let σ := (ps.map (fun p => p * (1 - p))).sum.sqrt
23+
⟨ μ , σ ⟩
24+
25+
def pQuorum (pSuccessfulVote committeeSize τ : Float) : Float :=
26+
let ⟨ μ , σ ⟩ := committeeDistribution pSuccessfulVote committeeSize
27+
1 - cdfGaussian (τ * committeeSize) μ σ
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import Std.Data.HashMap
2+
import Batteries.Lean.HashMap
3+
4+
import Linleios.Probability
5+
6+
7+
open Std (HashMap)
8+
9+
10+
abbrev Probability := Float
11+
12+
13+
structure Environment where
14+
activeSlotCoefficient : Probability
15+
Lheader : Nat
16+
Lvote : Nat
17+
Ldiff : Nat
18+
pSpacingOkay : Probability
19+
pQuorum : Probability
20+
pLate : Probability
21+
fAdversary : Float
22+
deriving Repr
23+
24+
def makeEnvironment (Lheader Lvote Ldiff : Nat) (activeSlotCoefficient committeeSize τ pRbHeaderArrives pEbValidates pEbUnvalidated fAdversary : Float) : Environment :=
25+
{
26+
activeSlotCoefficient := activeSlotCoefficient
27+
Lheader := Lheader
28+
Lvote := Lvote
29+
Ldiff := Ldiff
30+
pSpacingOkay := (1 - activeSlotCoefficient).pow (3 * Lheader + Lvote + Ldiff - 1).toFloat
31+
pQuorum := pQuorum (pRbHeaderArrives * pEbValidates * (1 - fAdversary)) committeeSize τ
32+
pLate := pEbUnvalidated * (1 - committeeSize / nPools.toFloat)
33+
fAdversary := fAdversary
34+
}
35+
36+
37+
structure State where
38+
clock : Nat
39+
rbCount : Nat
40+
ebCount : Nat
41+
hasRb : Bool
42+
canCertify : Bool
43+
deriving Repr, BEq, Hashable, Inhabited
44+
45+
theorem genesis : (default : State).clock = 0 ∧ (default : State).rbCount = 0 ∧ (default : State).ebCount = 0 := by
46+
constructor
47+
rfl
48+
constructor
49+
rfl
50+
rfl
51+
52+
53+
def Probabilities := HashMap State Probability
54+
deriving Repr, EmptyCollection
55+
56+
instance : Inhabited Probabilities where
57+
default := (∅ : Probabilities).insert Inhabited.default 1
58+
59+
60+
abbrev Outcomes := List (State × Probability)

analysis/markov/Linleios/Util.lean

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
2+
partial def erf (x : Float) : Float :=
3+
if x < 0
4+
then - erf (- x)
5+
else
6+
let p := 0.3275911
7+
let a₁ := 0.254829592
8+
let a₂ := -0.284496736
9+
let a₃ := 1.421413741
10+
let a₄ := -1.453152027
11+
let a₅ := 1.061405429
12+
let t := 1 / (1 + p * x)
13+
1 - (a₁ * t + a₂ * t^2 + a₃ * t^3 + a₄ * t^4 + a₅ * t^5) * Float.exp (- x^2)
14+
15+
def cdfGaussian (x μ σ : Float) : Float :=
16+
(1 + erf ((x - μ) / σ / Float.sqrt 2)) / 2
17+
18+
def bisectionSearch (f : Float → Float) (low high : Float) (ε : Float) (maxIter : Nat) : Float :=
19+
match maxIter with
20+
| 0 => (low + high) / 2
21+
| maxIter' + 1 =>
22+
let mid := (low + high) / 2
23+
let fmid := f mid
24+
if high - low < ε || Float.abs fmid < ε then
25+
mid
26+
else if f low * fmid < 0 then
27+
bisectionSearch f low mid ε maxIter'
28+
else
29+
bisectionSearch f mid high ε maxIter'
30+
termination_by maxIter

analysis/markov/Main.lean

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
2+
import Cli
3+
import Linleios
4+
import Parser.Char.Numeric
5+
import Parser.Stream
6+
7+
open Cli
8+
open Lean (Json)
9+
10+
11+
instance : ParseableType Float where
12+
name := "Float"
13+
parse? x := match (Parser.Char.ASCII.parseFloat : SimpleParser Substring Char Float).run x with
14+
| .ok _ y => some y
15+
| _ => none
16+
17+
18+
def runMarkovCmd (p : Parsed) : IO UInt32 :=
19+
do
20+
let activeSlotCoefficient : Float := p.flag! "active-slot-coefficient" |>.as! Float
21+
let Lheader : Nat := p.flag! "l-header" |>.as! Nat
22+
let Lvote : Nat := p.flag! "l-vote" |>.as! Nat
23+
let Ldiff : Nat := p.flag! "l-diff" |>.as! Nat
24+
let committeeSize : Nat := p.flag! "committee-size" |>.as! Nat
25+
let τ : Float := p.flag! "quorum-fraction" |>.as! Float
26+
let pRbHeaderArrives : Float := p.flag! "p-rb-header-arrives" |>.as! Float
27+
let pEbValidates : Float := p.flag! "p-eb-validates" |>.as! Float
28+
let pLateDiffusion : Float := p.flag! "p-late-diffusion" |>.as! Float
29+
let fAdversary : Float := p.flag! "adversary-fraction" |>.as! Float
30+
let ε : Float := p.flag! "tolerance" |>.as! Float
31+
let rbCount : Nat := p.flag! "rb-count" |>.as! Nat
32+
let env := makeEnvironment Lheader Lvote Ldiff activeSlotCoefficient committeeSize.toFloat τ pRbHeaderArrives pEbValidates pLateDiffusion fAdversary
33+
let sn := simulate env default ε rbCount
34+
if p.hasFlag "output-file"
35+
then IO.FS.writeFile (p.flag! "output-file" |>.as! String) (Json.pretty $ ebDistributionJson sn)
36+
IO.println s!"RB efficiency: {(reprPrec (rbEfficiency sn) 0).pretty}"
37+
IO.println s!"EB efficiency: {(reprPrec (ebEfficiency sn) 0).pretty}"
38+
IO.println s!"Overall efficiency: {(reprPrec (efficiency sn) 0).pretty}"
39+
IO.eprintln s!"Missing probability: {1 - totalProbability sn}"
40+
pure 0
41+
42+
def markov : Cmd := `[Cli|
43+
markov VIA runMarkovCmd; ["0.0.1"]
44+
"Run a Markov simulation of Linear Leios."
45+
FLAGS:
46+
"active-slot-coefficient" : Float ; "The active slot coefficient for RBs, in probability per slot."
47+
"l-header" : Nat ; "L_header protocol parameter, in slots."
48+
"l-vote" : Nat ; "L_vote protocol parameter, in slots."
49+
"l-diff" : Nat ; "L_diff protocol parameter, in slots."
50+
"committee-size" : Nat ; "Expected number of voters in the committee, out of 2500 stakepools total."
51+
"quorum-fraction" : Float ; "τ protocol parameter, in %/100."
52+
"p-rb-header-arrives" : Float ; "Probability that the RB header arrives before L_header."
53+
"p-eb-validates" : Float ; "Probability that the EB is fully validated before 3 L_header + L_vote."
54+
"p-late-diffusion" : Float ; "Probability than a RB producer hasn't validated the previous EB."
55+
"adversary-fraction" : Float ; "Fraction of stake that is adversarial: the adversary never produces RBs or EBs and never votes."
56+
"tolerance" : Float ; "Discard states with less than this probability."
57+
"rb-count" : Nat ; "Number of potential RBs to simulate."
58+
"output-file" : String ; "Path to the JSON output file for the EB distribution."
59+
EXTENSIONS:
60+
defaultValues! #[
61+
("active-slot-coefficient", "0.05")
62+
, ("l-header" , "1" )
63+
, ("l-vote" , "4" )
64+
, ("l-diff" , "7" )
65+
, ("committee-size" , "600" )
66+
, ("quorum-fraction" , "0.75")
67+
, ("p-rb-header-arrives" , "0.95")
68+
, ("p-eb-validates" , "0.90")
69+
, ("p-late-diffusion" , "0.05")
70+
, ("adversary-fraction" , "0" )
71+
, ("tolerance" , "1e-8")
72+
, ("rb-count" , "100" )
73+
]
74+
]
75+
76+
def main (args : List String) : IO UInt32 :=
77+
markov.validate args

0 commit comments

Comments
 (0)