Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Finalizer based on LMD votes, not connected to actual balances yet #52

Draft
wants to merge 10 commits into
base: master
Choose a base branch
from
203 changes: 203 additions & 0 deletions services/blocks/standard/lmdfinalizer/finalizer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,203 @@
// Package lmdfinalizer establishes which blocks are finalized from its LDM votes and the LDM votes of its children blocks.
// Abbreviation: LFB means Latest Finalized Block.
package lmdfinalizer

import (
"encoding/hex"
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/pkg/errors"
"github.com/rs/zerolog"
"github.com/wealdtech/chaind/services/blocks/standard/lmdfinalizer/tree"
"github.com/wealdtech/chaind/services/chaindb"
)

// PreBalanceThreshold is a mock threshold used until we connect the finalizer with the validator balances.
const PreBalanceThreshold = 10000

// LMDFinalizer is a beacon chain finalizer based on LMD votes.
type LMDFinalizer interface {
// AddBlock to finalizer to be candidate for finalization and use its included attestations as votes for
// other blocks.
AddBlock(dbblock *chaindb.Block, attestations []*chaindb.Attestation)
}

// newLFBHandler event handler to be triggered when a new LFB is finalized.
type newLFBHandler func(phase0.Root, phase0.Slot)
mcdee marked this conversation as resolved.
Show resolved Hide resolved

// finalizer is the implementation of LMDFinalizer.
type finalizer struct {
tree tree.Tree
votes lmdVotes

log zerolog.Logger

onAddNode chan *tree.Node

newLFBHandler newLFBHandler
}

// New LMDFinalizer with logger `log`.
func New(latestFinalized *chaindb.Block, log zerolog.Logger, handler newLFBHandler) LMDFinalizer {
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is already starting to be overloaded with parameters. Using the .WithX() parameter system that is used in other modules in chaind would make this easier to use.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The first two are kind of mandatory. I guess I can make log optional. Do you want me to add a WithX() for latestFinalized?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, ideally everything but the context would be a parameter as it makes future changes significantly easier to manager (for example, if we wanted to allow multiple handlers we could just add a WithHandlers() alongside the additional WithHandler() and it would not break any existing code.

LFB := tree.NewNode(latestFinalized, nil)

f := &finalizer{
tree: tree.New(LFB),
votes: newLMDVotes(),

log: log.With().Str("subservice", "LMD finalizer").Logger(),

onAddNode: make(chan *tree.Node, 1000), // TODO: size of channel

newLFBHandler: handler,
}

go f.mainLoop()

return f
}

// AddBlock to finalizer to be candidate for finalization and use its included attestations as votes for
// other blocks.
func (f *finalizer) AddBlock(dbblock *chaindb.Block, attestations []*chaindb.Attestation) {
node := tree.NewNode(dbblock, attestations)

f.onAddNode <- node
}

// mainLoop receives via channels commands and executed them, it is run in its own goroutine so public functions.
func (f *finalizer) mainLoop() {
for {
node := <-f.onAddNode
err := f.addNode(node)
if err != nil {
f.log.Error().Err(err).Msg("error adding block")
}
}
}

// addNode to the finalizer.
func (f *finalizer) addNode(node *tree.Node) error {
if f.tree.IsOldBlock(node) {
return errors.New("adding block that is not in the future of latest finalized block")
}

f.tree.Insert(node)

// Finalizer works even if blocks come out of order.
children := f.tree.FindOrphans(node)
f.adopt(node, children)

f.attestationsToVotes(node)

newLFB := f.countVotes()
if newLFB != nil {
// we have a new LFB
f.onNewLatestFinalizedBlock(newLFB)
}

return nil
}

// attestationsToVotes takes all the attestations included in a node block and convert them to votes and includes them
// to be counted.
func (f *finalizer) attestationsToVotes(node *tree.Node) {
for _, attestation := range node.Attestations() {
if f.tree.IsOldSlot(attestation.Slot) {
// We should ignore this attestation, as it refers to a block that is not later than the
// latest LDM finalized block.
continue
}

votes := f.attestationToVotes(attestation)
f.votes.insert(votes)
}

// memory optimization
node.RemoveAttestations()
}

// attestationToVotes returns an array of votes from an attestation.
func (finalizer) attestationToVotes(attestation *chaindb.Attestation) []lmdVote {
return []lmdVote{
{
slot: attestation.Slot,
root: attestation.BeaconBlockRoot,
weight: voteWeight(len(attestation.AggregationIndices)),
},
}
}

// adopt children nodes by parent node.
func (f *finalizer) adopt(parent *tree.Node, children []*tree.Node) {
f.tree.Adopt(parent, children)
votes := []lmdVote{}
for _, child := range children {
votes = append(votes, lmdVote{
root: parent.Root(),
slot: parent.Slot(),
weight: child.CurrenVote(),
})
}
f.votes.insert(votes)
}

// countVotes check votes that have not been counted yet, and count them if their referred block exists in the tree.
func (f *finalizer) countVotes() *tree.Node {
var newLFB *tree.Node

f.votes.tryUncounted(func(vote lmdVote) bool {
if newLFB != nil {
// Skip counting this vote as we found a new LFB.
return false
}

block := f.tree.GetByRoot(vote.root)
if block == nil {
// Cannot count this vote as we do not have the block it counts into.
return false
}

newLFB = f.countVote(block, vote.weight)

return true
})

return newLFB
}

// countVote for block and its ancestors.
// Returns a block if its weight reached threshold, otherwise nil.
func (f *finalizer) countVote(block *tree.Node, vote voteWeight) *tree.Node {
var newLFB *tree.Node

f.tree.Climb(block, func(block *tree.Node) bool {
block.CountVote(vote)

if f.threshold(block) {
newLFB = block
return false // Do not climb anymore, as this is the new latest finalized block.
}
return true
})

return newLFB
}

// threshold returns true if a block votes have reach the threshold to become finalized.
func (finalizer) threshold(block *tree.Node) bool {
return block.CurrenVote() > PreBalanceThreshold
}

// onNewLatestFinalizedBlock is called when the finalizer find a new LFB. It handles the transition to a new LFB,
// and calls the event handler.
func (f *finalizer) onNewLatestFinalizedBlock(newLFB *tree.Node) {
f.tree.OnNewLatestFinalizedBlock(newLFB)
f.votes.newLatestFinalizedBlockSlot(newLFB.Slot())

root := newLFB.Root()
f.log.Info().Str("root", hex.EncodeToString(root[:])).Uint64("slot", uint64(newLFB.Slot())).Msg("new finalized block")

if f.newLFBHandler != nil {
f.newLFBHandler(newLFB.Root(), newLFB.Slot())
}
}
185 changes: 185 additions & 0 deletions services/blocks/standard/lmdfinalizer/finalizer_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
package lmdfinalizer_test

import (
"github.com/attestantio/go-eth2-client/spec/phase0"
"github.com/rs/zerolog"
zerologger "github.com/rs/zerolog/log"
"github.com/stretchr/testify/assert"
"github.com/wealdtech/chaind/services/blocks/standard/lmdfinalizer"
"github.com/wealdtech/chaind/services/blocks/standard/lmdfinalizer/mock"
"sync"
"testing"
)

var genesis, _ = mock.MockBlock(0, 0, 0, nil)

func TestFinalizer_SimpleRun(t *testing.T) {
log := zerologger.With().Logger().Level(zerolog.ErrorLevel)
count := 0
var wg sync.WaitGroup
wg.Add(2)

f := lmdfinalizer.New(genesis, log, func(root phase0.Root, slot phase0.Slot) {
wg.Done()
count++

switch count {
case 1:
assert.Equal(t, phase0.Slot(2), slot)
assert.EqualValues(t, mock.MockRoot(1), root)
case 2:
assert.Equal(t, phase0.Slot(100), slot)
assert.EqualValues(t, mock.MockRoot(3), root)
default:
assert.Fail(t, "should there be only 2")
}
})

f.AddBlock(mock.MockBlock(2, 1, 0, nil)) // 1: child of genesis
f.AddBlock(mock.MockBlock(2, 2, 0, nil)) // 2: child of genesis
f.AddBlock(mock.MockBlock(100, 3, 1, nil)) // 3: child of 1
f.AddBlock(mock.MockBlock(10, 4, 1000, nil)) // 4: child of none
f.AddBlock(mock.MockBlock(101, 5, 3, []mock.MockAttestation{
{
Slot: 2,
Root: 1,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 100,
Root: 3,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
})) // 5: child of 3
f.AddBlock(mock.MockBlock(104, 6, 1000, nil)) // 6: child of none
f.AddBlock(mock.MockBlock(110, 7, 1000, []mock.MockAttestation{
{
Slot: 2,
Root: 1,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 100,
Root: 3,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 101,
Root: 5,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
})) // 7: child of 5
f.AddBlock(mock.MockBlock(112, 8, 7, []mock.MockAttestation{
{
Slot: 2,
Root: 1,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 100,
Root: 3,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 101,
Root: 5,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 110,
Root: 7,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
})) // 8: child of 7
f.AddBlock(mock.MockBlock(113, 9, 8, nil)) // 8: child of 8

wg.Wait()

assert.Equal(t, 2, count)
}

func TestFinalizer_SimpleRunOutOfOrder(t *testing.T) {
log := zerologger.With().Logger().Level(zerolog.ErrorLevel)
count := 0
var wg sync.WaitGroup
wg.Add(2)

f := lmdfinalizer.New(genesis, log, func(root phase0.Root, slot phase0.Slot) {
wg.Done()
count++

switch count {
case 1:
assert.Equal(t, phase0.Slot(2), slot)
assert.EqualValues(t, mock.MockRoot(1), root)
case 2:
assert.Equal(t, phase0.Slot(100), slot)
assert.EqualValues(t, mock.MockRoot(3), root)
default:
assert.Fail(t, "should there be only 2")
}
})

f.AddBlock(mock.MockBlock(100, 3, 1, nil)) // 3: child of 1
f.AddBlock(mock.MockBlock(10, 4, 1000, nil)) // 4: child of none
// 5: child of 3
f.AddBlock(mock.MockBlock(104, 6, 1000, nil)) // 6: child of none
f.AddBlock(mock.MockBlock(110, 7, 1000, []mock.MockAttestation{
{
Slot: 2,
Root: 1,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 100,
Root: 3,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 101,
Root: 5,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
})) // 7: child of 5
f.AddBlock(mock.MockBlock(101, 5, 3, []mock.MockAttestation{
{
Slot: 2,
Root: 1,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 100,
Root: 3,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
}))
f.AddBlock(mock.MockBlock(2, 1, 0, nil)) // 1: child of genesis
f.AddBlock(mock.MockBlock(2, 2, 0, nil)) // 2: child of genesis
f.AddBlock(mock.MockBlock(112, 8, 7, []mock.MockAttestation{
{
Slot: 2,
Root: 1,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 100,
Root: 3,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 101,
Root: 5,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
{
Slot: 110,
Root: 7,
NumIndices: lmdfinalizer.PreBalanceThreshold / 4,
},
})) // 8: child of 7
f.AddBlock(mock.MockBlock(113, 9, 8, nil)) // 8: child of 8

wg.Wait()

assert.Equal(t, 2, count)
}
Loading