Skip to content

Commit 408bb06

Browse files
committed
feat: lotus-shed: finality calculator
doesn't quite work
1 parent 77ae5af commit 408bb06

File tree

4 files changed

+260
-0
lines changed

4 files changed

+260
-0
lines changed

cmd/lotus-shed/finality.go

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
package main
2+
3+
import (
4+
"bufio"
5+
"fmt"
6+
"math"
7+
"os"
8+
"strconv"
9+
10+
"github.com/dreading/gospecfunc/bessel"
11+
"github.com/filecoin-project/lotus/build"
12+
"github.com/urfave/cli/v2"
13+
"golang.org/x/exp/constraints"
14+
"gonum.org/v1/gonum/stat/distuv"
15+
)
16+
17+
var finalityCmd = &cli.Command{
18+
Name: "finality-calculator",
19+
Description: "Calculate the finality probability of at a tipset",
20+
Flags: []cli.Flag{
21+
&cli.StringFlag{
22+
Name: "repo",
23+
Value: "~/.lotus",
24+
},
25+
&cli.StringFlag{
26+
Name: "input",
27+
},
28+
},
29+
ArgsUsage: "[inputFile]",
30+
Action: func(cctx *cli.Context) error {
31+
input := cctx.Args().Get(0)
32+
file, err := os.Open(input)
33+
if err != nil {
34+
return err
35+
}
36+
defer file.Close()
37+
38+
var chain []int
39+
scanner := bufio.NewScanner(file)
40+
for scanner.Scan() {
41+
num, err := strconv.Atoi(scanner.Text())
42+
if err != nil {
43+
return err
44+
}
45+
chain = append(chain, num)
46+
}
47+
48+
if err := scanner.Err(); err != nil {
49+
return err
50+
}
51+
52+
blocksPerEpoch := 5.0 // Expected number of blocks per epoch
53+
byzantineFraction := 0.3 // Upper bound on the fraction of malicious nodes in the network
54+
currentEpoch := len(chain) - 1 // Current epoch (end of history)
55+
targetEpoch := currentEpoch - 30 // Target epoch for which finality is calculated
56+
57+
finality := FinalityCalcValidator(chain, blocksPerEpoch, byzantineFraction, currentEpoch, targetEpoch)
58+
59+
fmt.Fprintf(cctx.App.Writer, "Finality probability: %f\n", finality)
60+
61+
return nil
62+
},
63+
}
64+
65+
// FinalityCalcValidator computes the probability that a previous blockchain tipset gets replaced.
66+
//
67+
// Based on https://github.com/consensus-shipyard/ec-finality-calculator
68+
func FinalityCalcValidator(chain []int, blocksPerEpoch float64, byzantineFraction float64, currentEpoch int, targetEpoch int) float64 {
69+
// Threshold at which the probability of an event is considered negligible
70+
const negligibleThreshold = 1e-25
71+
72+
maxKL := 400 // Max k for which to calculate Pr(L=k)
73+
maxKB := int((currentEpoch - targetEpoch) * int(blocksPerEpoch)) // Max k for which to calculate Pr(B=k)
74+
maxKM := 400 // Max k for which to calculate Pr(M=k)
75+
maxIM := 100 // Maximum number of epochs for the calculation (after which the pr become negligible)
76+
77+
rateMaliciousBlocks := blocksPerEpoch * byzantineFraction // upper bound
78+
rateHonestBlocks := blocksPerEpoch - rateMaliciousBlocks // lower bound
79+
80+
// Compute L
81+
prL := make([]float64, maxKL+1)
82+
83+
for k := 0; k <= maxKL; k++ {
84+
sumExpectedAdversarialBlocksI := 0.0
85+
sumChainBlocksI := 0
86+
87+
for i := targetEpoch; i > currentEpoch-int(build.Finality); i-- {
88+
sumExpectedAdversarialBlocksI += rateMaliciousBlocks
89+
sumChainBlocksI += chain[i-1]
90+
// Poisson(k=k, lambda=sum(f*e))
91+
prLi := distuv.Poisson{Lambda: sumExpectedAdversarialBlocksI}.Prob(float64(k + sumChainBlocksI))
92+
prL[k] = math.Max(prL[k], prLi)
93+
94+
// Break if prL[k] becomes negligible
95+
if k > 1 && prL[k] < negligibleThreshold && prL[k] < prL[k-1] {
96+
maxKL = k
97+
prL = prL[:k+1]
98+
break
99+
}
100+
}
101+
}
102+
103+
// As the adversarial lead is never negative, the missing probability is added to k=0
104+
prL[0] += 1 - sum(prL)
105+
106+
// Compute B
107+
prB := make([]float64, maxKB+1)
108+
109+
// Calculate Pr(B=k) for each value of k
110+
for k := 0; k <= maxKB; k++ {
111+
prB[k] = distuv.Poisson{Lambda: float64(currentEpoch-targetEpoch) * rateMaliciousBlocks}.Prob(float64(k))
112+
113+
// Break if prB[k] becomes negligible
114+
if k > 1 && prB[k] < negligibleThreshold && prB[k] < prB[k-1] {
115+
maxKB = k
116+
prB = prB[:k+1]
117+
break
118+
}
119+
}
120+
121+
// Compute M
122+
prHgt0 := 1 - distuv.Poisson{Lambda: rateHonestBlocks}.Prob(0)
123+
124+
expZ := 0.0
125+
for k := 0; k < int(4*blocksPerEpoch); k++ {
126+
pmf := distuv.Poisson{Lambda: rateMaliciousBlocks}.Prob(float64(k))
127+
expZ += ((rateHonestBlocks + float64(k)) / math.Pow(2, float64(k))) * pmf
128+
}
129+
130+
ratePublicChain := prHgt0 * expZ
131+
132+
prM := make([]float64, maxKM+1)
133+
for k := 0; k <= maxKM; k++ {
134+
for i := maxIM; i > 0; i-- {
135+
probMI := SkellamPMF(k, float64(i)*rateMaliciousBlocks, float64(i)*ratePublicChain)
136+
137+
// Break if probMI becomes negligible
138+
if probMI < negligibleThreshold && probMI < prM[k] {
139+
break
140+
}
141+
prM[k] = math.Max(prM[k], probMI)
142+
}
143+
144+
// Break if prM[k] becomes negligible
145+
if k > 1 && prM[k] < negligibleThreshold && prM[k] < prM[k-1] {
146+
maxKM = k
147+
prM = prM[:k+1]
148+
break
149+
}
150+
}
151+
152+
prM[0] += 1 - sum(prM)
153+
154+
// Compute error probability upper bound
155+
cumsumL := cumsum(prL)
156+
cumsumB := cumsum(prB)
157+
cumsumM := cumsum(prM)
158+
159+
k := sum(chain[targetEpoch:currentEpoch])
160+
161+
sumLgeK := cumsumL[len(cumsumL)-1]
162+
if k > 0 {
163+
sumLgeK -= cumsumL[min(k-1, maxKL)]
164+
}
165+
166+
doubleSum := 0.0
167+
168+
for l := 0; l < k; l++ {
169+
sumBgeKminL := cumsumB[len(cumsumB)-1]
170+
if k-l-1 > 0 {
171+
sumBgeKminL -= cumsumB[min(k-l-1, maxKB)]
172+
}
173+
doubleSum += prL[min(l, maxKL)] * sumBgeKminL
174+
175+
for b := 0; b < k-l; b++ {
176+
sumMgeKminLminB := cumsumM[len(cumsumM)-1]
177+
if k-l-b-1 > 0 {
178+
sumMgeKminLminB -= cumsumM[min(k-l-b-1, maxKM)]
179+
}
180+
doubleSum += prL[min(l, maxKL)] * prB[min(b, maxKB)] * sumMgeKminLminB
181+
}
182+
}
183+
184+
prError := sumLgeK + doubleSum
185+
186+
return math.Min(prError, 1.0)
187+
}
188+
189+
func sum[T constraints.Integer | constraints.Float](s []T) T {
190+
var total T
191+
for _, v := range s {
192+
total += v
193+
}
194+
return total
195+
}
196+
197+
func cumsum(arr []float64) []float64 {
198+
cumsums := make([]float64, len(arr))
199+
cumSum := 0.0
200+
for i, value := range arr {
201+
cumSum += value
202+
cumsums[i] = cumSum
203+
}
204+
return cumsums
205+
}
206+
207+
func min(a, b int) int {
208+
if a < b {
209+
return a
210+
}
211+
return b
212+
}
213+
214+
// SkellamPMF calculates the probability mass function (PMF) of a Skellam distribution.
215+
//
216+
// The Skellam distribution is the probability distribution of the difference
217+
// of two independent Poisson random variables.
218+
//
219+
// Arguments:
220+
// * k - The difference of two Poisson random variables.
221+
// * mu1 - The expected value of the first Poisson distribution.
222+
// * mu2 - The expected value of the second Poisson distribution.
223+
//
224+
// Returns:
225+
// * A float64 representing the PMF of the Skellam distribution at k.
226+
func SkellamPMF(k int, mu1 float64, mu2 float64) float64 {
227+
// Based on https://github.com/jsoares/rusty-skellam/blob/main/src/lib.rs
228+
229+
// Return NaN if parameters outside range
230+
if math.IsNaN(mu1) || mu1 <= 0 || math.IsNaN(mu2) || mu2 <= 0 {
231+
return math.NaN()
232+
}
233+
234+
// Parameterise and compute the Modified Bessel function of the first kind
235+
nu := float64(k)
236+
z := complex(2.0*math.Sqrt(mu1*mu2), 0)
237+
besselResult := bessel.I(nu, z)
238+
239+
// Compute the pmf
240+
return math.Exp(-(mu1 + mu2)) * math.Pow(mu1/mu2, nu/2.0) * real(besselResult)
241+
}
242+
243+
/*
244+
func main() {
245+
seed := rand.NewSource(1)
246+
random := rand.New(seed)
247+
chain := make([]int, 1000)
248+
for i := range chain {
249+
chain[i] = random.Intn(5) + 1
250+
}
251+
252+
errorProbability := FinalityCalcValidator(chain, 5.0, 0.3, 1000, 900)
253+
fmt.Printf("Error probability: %f\n", errorProbability)
254+
}
255+
*/

cmd/lotus-shed/main.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,7 @@ func main() {
9191
mismatchesCmd,
9292
blockCmd,
9393
adlCmd,
94+
finalityCmd,
9495
}
9596

9697
app := &cli.App{

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -181,6 +181,7 @@ require (
181181
github.com/dgraph-io/ristretto v0.1.1 // indirect
182182
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
183183
github.com/drand/kyber-bls12381 v0.3.1 // indirect
184+
github.com/dreading/gospecfunc v0.0.0-20191105042551-e794f60da5c3 // indirect
184185
github.com/elastic/go-windows v1.0.0 // indirect
185186
github.com/etclabscore/go-jsonschema-walk v0.0.6 // indirect
186187
github.com/filecoin-project/go-amt-ipld/v2 v2.1.0 // indirect

go.sum

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,8 @@ github.com/drand/kyber v1.3.0 h1:TVd7+xoRgKQ4Ck1viNLPFy6IWhuZM36Bq6zDXD8Asls=
216216
github.com/drand/kyber v1.3.0/go.mod h1:f+mNHjiGT++CuueBrpeMhFNdKZAsy0tu03bKq9D5LPA=
217217
github.com/drand/kyber-bls12381 v0.3.1 h1:KWb8l/zYTP5yrvKTgvhOrk2eNPscbMiUOIeWBnmUxGo=
218218
github.com/drand/kyber-bls12381 v0.3.1/go.mod h1:H4y9bLPu7KZA/1efDg+jtJ7emKx+ro3PU7/jWUVt140=
219+
github.com/dreading/gospecfunc v0.0.0-20191105042551-e794f60da5c3 h1:oOp1la+wHlyd3ODqW2CbFj8w6Lod4gPMzHFbD0rbp88=
220+
github.com/dreading/gospecfunc v0.0.0-20191105042551-e794f60da5c3/go.mod h1:lkytgpljbGOM3VZj4Fm7FkGy/oUInQFklbkHBVAvJEg=
219221
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
220222
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
221223
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
@@ -1755,6 +1757,7 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
17551757
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
17561758
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
17571759
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
1760+
golang.org/x/tools v0.0.0-20191022213345-0bbdf54effa2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
17581761
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
17591762
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
17601763
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=

0 commit comments

Comments
 (0)