Skip to content

Commit

Permalink
feat: lotus-shed: finality calculator
Browse files Browse the repository at this point in the history
doesn't quite work
  • Loading branch information
rvagg committed Jun 14, 2024
1 parent 77ae5af commit 408bb06
Show file tree
Hide file tree
Showing 4 changed files with 260 additions and 0 deletions.
255 changes: 255 additions & 0 deletions cmd/lotus-shed/finality.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,255 @@
package main

import (
"bufio"
"fmt"
"math"
"os"
"strconv"

"github.com/dreading/gospecfunc/bessel"
"github.com/filecoin-project/lotus/build"
"github.com/urfave/cli/v2"
"golang.org/x/exp/constraints"
"gonum.org/v1/gonum/stat/distuv"
)

var finalityCmd = &cli.Command{
Name: "finality-calculator",
Description: "Calculate the finality probability of at a tipset",
Flags: []cli.Flag{
&cli.StringFlag{
Name: "repo",
Value: "~/.lotus",
},
&cli.StringFlag{
Name: "input",
},
},
ArgsUsage: "[inputFile]",
Action: func(cctx *cli.Context) error {
input := cctx.Args().Get(0)
file, err := os.Open(input)
if err != nil {
return err
}
defer file.Close()

Check failure on line 36 in cmd/lotus-shed/finality.go

View workflow job for this annotation

GitHub Actions / Check (lint-all)

Error return value of `file.Close` is not checked (errcheck)

var chain []int
scanner := bufio.NewScanner(file)
for scanner.Scan() {
num, err := strconv.Atoi(scanner.Text())
if err != nil {
return err
}
chain = append(chain, num)
}

if err := scanner.Err(); err != nil {
return err
}

blocksPerEpoch := 5.0 // Expected number of blocks per epoch
byzantineFraction := 0.3 // Upper bound on the fraction of malicious nodes in the network
currentEpoch := len(chain) - 1 // Current epoch (end of history)
targetEpoch := currentEpoch - 30 // Target epoch for which finality is calculated

finality := FinalityCalcValidator(chain, blocksPerEpoch, byzantineFraction, currentEpoch, targetEpoch)

fmt.Fprintf(cctx.App.Writer, "Finality probability: %f\n", finality)

Check failure on line 59 in cmd/lotus-shed/finality.go

View workflow job for this annotation

GitHub Actions / Check (lint-all)

Error return value of `fmt.Fprintf` is not checked (errcheck)

return nil
},
}

// FinalityCalcValidator computes the probability that a previous blockchain tipset gets replaced.
//
// Based on https://github.com/consensus-shipyard/ec-finality-calculator
func FinalityCalcValidator(chain []int, blocksPerEpoch float64, byzantineFraction float64, currentEpoch int, targetEpoch int) float64 {
// Threshold at which the probability of an event is considered negligible
const negligibleThreshold = 1e-25

maxKL := 400 // Max k for which to calculate Pr(L=k)
maxKB := int((currentEpoch - targetEpoch) * int(blocksPerEpoch)) // Max k for which to calculate Pr(B=k)

Check failure on line 73 in cmd/lotus-shed/finality.go

View workflow job for this annotation

GitHub Actions / Check (lint-all)

unnecessary conversion (unconvert)
maxKM := 400 // Max k for which to calculate Pr(M=k)
maxIM := 100 // Maximum number of epochs for the calculation (after which the pr become negligible)

rateMaliciousBlocks := blocksPerEpoch * byzantineFraction // upper bound
rateHonestBlocks := blocksPerEpoch - rateMaliciousBlocks // lower bound

// Compute L
prL := make([]float64, maxKL+1)

for k := 0; k <= maxKL; k++ {
sumExpectedAdversarialBlocksI := 0.0
sumChainBlocksI := 0

for i := targetEpoch; i > currentEpoch-int(build.Finality); i-- {
sumExpectedAdversarialBlocksI += rateMaliciousBlocks
sumChainBlocksI += chain[i-1]
// Poisson(k=k, lambda=sum(f*e))
prLi := distuv.Poisson{Lambda: sumExpectedAdversarialBlocksI}.Prob(float64(k + sumChainBlocksI))
prL[k] = math.Max(prL[k], prLi)

// Break if prL[k] becomes negligible
if k > 1 && prL[k] < negligibleThreshold && prL[k] < prL[k-1] {
maxKL = k
prL = prL[:k+1]
break
}
}
}

// As the adversarial lead is never negative, the missing probability is added to k=0
prL[0] += 1 - sum(prL)

// Compute B
prB := make([]float64, maxKB+1)

// Calculate Pr(B=k) for each value of k
for k := 0; k <= maxKB; k++ {
prB[k] = distuv.Poisson{Lambda: float64(currentEpoch-targetEpoch) * rateMaliciousBlocks}.Prob(float64(k))

// Break if prB[k] becomes negligible
if k > 1 && prB[k] < negligibleThreshold && prB[k] < prB[k-1] {
maxKB = k
prB = prB[:k+1]
break
}
}

// Compute M
prHgt0 := 1 - distuv.Poisson{Lambda: rateHonestBlocks}.Prob(0)

expZ := 0.0
for k := 0; k < int(4*blocksPerEpoch); k++ {
pmf := distuv.Poisson{Lambda: rateMaliciousBlocks}.Prob(float64(k))
expZ += ((rateHonestBlocks + float64(k)) / math.Pow(2, float64(k))) * pmf
}

ratePublicChain := prHgt0 * expZ

prM := make([]float64, maxKM+1)
for k := 0; k <= maxKM; k++ {
for i := maxIM; i > 0; i-- {
probMI := SkellamPMF(k, float64(i)*rateMaliciousBlocks, float64(i)*ratePublicChain)

// Break if probMI becomes negligible
if probMI < negligibleThreshold && probMI < prM[k] {
break
}
prM[k] = math.Max(prM[k], probMI)
}

// Break if prM[k] becomes negligible
if k > 1 && prM[k] < negligibleThreshold && prM[k] < prM[k-1] {
maxKM = k
prM = prM[:k+1]
break
}
}

prM[0] += 1 - sum(prM)

// Compute error probability upper bound
cumsumL := cumsum(prL)
cumsumB := cumsum(prB)
cumsumM := cumsum(prM)

k := sum(chain[targetEpoch:currentEpoch])

sumLgeK := cumsumL[len(cumsumL)-1]
if k > 0 {
sumLgeK -= cumsumL[min(k-1, maxKL)]
}

doubleSum := 0.0

for l := 0; l < k; l++ {
sumBgeKminL := cumsumB[len(cumsumB)-1]
if k-l-1 > 0 {
sumBgeKminL -= cumsumB[min(k-l-1, maxKB)]
}
doubleSum += prL[min(l, maxKL)] * sumBgeKminL

for b := 0; b < k-l; b++ {
sumMgeKminLminB := cumsumM[len(cumsumM)-1]
if k-l-b-1 > 0 {
sumMgeKminLminB -= cumsumM[min(k-l-b-1, maxKM)]
}
doubleSum += prL[min(l, maxKL)] * prB[min(b, maxKB)] * sumMgeKminLminB
}
}

prError := sumLgeK + doubleSum

return math.Min(prError, 1.0)
}

func sum[T constraints.Integer | constraints.Float](s []T) T {
var total T
for _, v := range s {
total += v
}
return total
}

func cumsum(arr []float64) []float64 {
cumsums := make([]float64, len(arr))
cumSum := 0.0
for i, value := range arr {
cumSum += value
cumsums[i] = cumSum
}
return cumsums
}

func min(a, b int) int {
if a < b {
return a
}
return b
}

// SkellamPMF calculates the probability mass function (PMF) of a Skellam distribution.
//
// The Skellam distribution is the probability distribution of the difference
// of two independent Poisson random variables.
//
// Arguments:
// * k - The difference of two Poisson random variables.
// * mu1 - The expected value of the first Poisson distribution.
// * mu2 - The expected value of the second Poisson distribution.
//
// Returns:
// * A float64 representing the PMF of the Skellam distribution at k.
func SkellamPMF(k int, mu1 float64, mu2 float64) float64 {
// Based on https://github.com/jsoares/rusty-skellam/blob/main/src/lib.rs

// Return NaN if parameters outside range
if math.IsNaN(mu1) || mu1 <= 0 || math.IsNaN(mu2) || mu2 <= 0 {
return math.NaN()
}

// Parameterise and compute the Modified Bessel function of the first kind
nu := float64(k)
z := complex(2.0*math.Sqrt(mu1*mu2), 0)
besselResult := bessel.I(nu, z)

// Compute the pmf
return math.Exp(-(mu1 + mu2)) * math.Pow(mu1/mu2, nu/2.0) * real(besselResult)
}

/*
func main() {
seed := rand.NewSource(1)
random := rand.New(seed)
chain := make([]int, 1000)
for i := range chain {
chain[i] = random.Intn(5) + 1
}
errorProbability := FinalityCalcValidator(chain, 5.0, 0.3, 1000, 900)
fmt.Printf("Error probability: %f\n", errorProbability)
}
*/
1 change: 1 addition & 0 deletions cmd/lotus-shed/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,7 @@ func main() {
mismatchesCmd,
blockCmd,
adlCmd,
finalityCmd,
}

app := &cli.App{
Expand Down
1 change: 1 addition & 0 deletions go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -181,6 +181,7 @@ require (
github.com/dgraph-io/ristretto v0.1.1 // indirect
github.com/dgryski/go-farm v0.0.0-20200201041132-a6ae2369ad13 // indirect
github.com/drand/kyber-bls12381 v0.3.1 // indirect
github.com/dreading/gospecfunc v0.0.0-20191105042551-e794f60da5c3 // indirect
github.com/elastic/go-windows v1.0.0 // indirect
github.com/etclabscore/go-jsonschema-walk v0.0.6 // indirect
github.com/filecoin-project/go-amt-ipld/v2 v2.1.0 // indirect
Expand Down
3 changes: 3 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,8 @@ github.com/drand/kyber v1.3.0 h1:TVd7+xoRgKQ4Ck1viNLPFy6IWhuZM36Bq6zDXD8Asls=
github.com/drand/kyber v1.3.0/go.mod h1:f+mNHjiGT++CuueBrpeMhFNdKZAsy0tu03bKq9D5LPA=
github.com/drand/kyber-bls12381 v0.3.1 h1:KWb8l/zYTP5yrvKTgvhOrk2eNPscbMiUOIeWBnmUxGo=
github.com/drand/kyber-bls12381 v0.3.1/go.mod h1:H4y9bLPu7KZA/1efDg+jtJ7emKx+ro3PU7/jWUVt140=
github.com/dreading/gospecfunc v0.0.0-20191105042551-e794f60da5c3 h1:oOp1la+wHlyd3ODqW2CbFj8w6Lod4gPMzHFbD0rbp88=
github.com/dreading/gospecfunc v0.0.0-20191105042551-e794f60da5c3/go.mod h1:lkytgpljbGOM3VZj4Fm7FkGy/oUInQFklbkHBVAvJEg=
github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk=
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
Expand Down Expand Up @@ -1755,6 +1757,7 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn
golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191022213345-0bbdf54effa2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
Expand Down

0 comments on commit 408bb06

Please sign in to comment.