mirror of
https://github.com/OffchainLabs/prysm.git
synced 2026-01-09 13:28:01 -05:00
Track unrealized justification/finalization in forkchoice (#10658)
* Track unrealized justification/finalization in forkchoice * add missing files * mod tidy Co-authored-by: terencechain <terence@prysmaticlabs.com>
This commit is contained in:
@@ -12,6 +12,7 @@ go_library(
|
||||
"proposer_boost.go",
|
||||
"store.go",
|
||||
"types.go",
|
||||
"unrealized_justification.go",
|
||||
],
|
||||
importpath = "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice/doubly-linked-tree",
|
||||
visibility = [
|
||||
@@ -43,6 +44,7 @@ go_test(
|
||||
"optimistic_sync_test.go",
|
||||
"proposer_boost_test.go",
|
||||
"store_test.go",
|
||||
"unrealized_justification_test.go",
|
||||
"vote_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
@@ -54,5 +56,6 @@ go_test(
|
||||
"//encoding/bytesutil:go_default_library",
|
||||
"//testing/assert:go_default_library",
|
||||
"//testing/require:go_default_library",
|
||||
"@org_golang_x_net//context:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -9,3 +9,5 @@ var errUnknownFinalizedRoot = errors.New("unknown finalized root")
|
||||
var errUnknownJustifiedRoot = errors.New("unknown justified root")
|
||||
var errInvalidOptimisticStatus = errors.New("invalid optimistic status")
|
||||
var errUnknownPayloadHash = errors.New("unknown payload hash")
|
||||
var errInvalidUnrealizedJustifiedEpoch = errors.New("invalid unrealized justified epoch")
|
||||
var errInvalidUnrealizedFinalizedEpoch = errors.New("invalid unrealized finalized epoch")
|
||||
|
||||
@@ -72,7 +72,7 @@ func (f *ForkChoice) Head(
|
||||
return [32]byte{}, errors.Wrap(err, "could not apply weight changes")
|
||||
}
|
||||
|
||||
if err := f.store.treeRootNode.updateBestDescendant(ctx, justifiedEpoch, finalizedEpoch); err != nil {
|
||||
if err := f.store.treeRootNode.updateBestDescendant(ctx, f.store.justifiedEpoch, f.store.finalizedEpoch); err != nil {
|
||||
return [32]byte{}, errors.Wrap(err, "could not update best descendant")
|
||||
}
|
||||
|
||||
|
||||
@@ -112,13 +112,15 @@ func (s *Store) insert(ctx context.Context,
|
||||
parent := s.nodeByRoot[parentRoot]
|
||||
|
||||
n := &Node{
|
||||
slot: slot,
|
||||
root: root,
|
||||
parent: parent,
|
||||
justifiedEpoch: justifiedEpoch,
|
||||
finalizedEpoch: finalizedEpoch,
|
||||
optimistic: true,
|
||||
payloadHash: payloadHash,
|
||||
slot: slot,
|
||||
root: root,
|
||||
parent: parent,
|
||||
justifiedEpoch: justifiedEpoch,
|
||||
unrealizedJustifiedEpoch: justifiedEpoch,
|
||||
finalizedEpoch: finalizedEpoch,
|
||||
unrealizedFinalizedEpoch: finalizedEpoch,
|
||||
optimistic: true,
|
||||
payloadHash: payloadHash,
|
||||
}
|
||||
|
||||
s.nodeByPayload[payloadHash] = n
|
||||
@@ -145,8 +147,12 @@ func (s *Store) insert(ctx context.Context,
|
||||
|
||||
// updateCheckpoints Update the justified / finalized epochs in store if necessary.
|
||||
func (s *Store) updateCheckpoints(justifiedEpoch, finalizedEpoch types.Epoch) {
|
||||
s.justifiedEpoch = justifiedEpoch
|
||||
s.finalizedEpoch = finalizedEpoch
|
||||
if s.justifiedEpoch < justifiedEpoch {
|
||||
s.justifiedEpoch = justifiedEpoch
|
||||
}
|
||||
if s.finalizedEpoch < finalizedEpoch {
|
||||
s.finalizedEpoch = finalizedEpoch
|
||||
}
|
||||
}
|
||||
|
||||
// pruneFinalizedNodeByRootMap prunes the `nodeByRoot` map
|
||||
|
||||
@@ -35,17 +35,19 @@ type Store struct {
|
||||
// Node defines the individual block which includes its block parent, ancestor and how much weight accounted for it.
|
||||
// This is used as an array based stateful DAG for efficient fork choice look up.
|
||||
type Node struct {
|
||||
slot types.Slot // slot of the block converted to the node.
|
||||
root [fieldparams.RootLength]byte // root of the block converted to the node.
|
||||
payloadHash [fieldparams.RootLength]byte // payloadHash of the block converted to the node.
|
||||
parent *Node // parent index of this node.
|
||||
children []*Node // the list of direct children of this Node
|
||||
justifiedEpoch types.Epoch // justifiedEpoch of this node.
|
||||
finalizedEpoch types.Epoch // finalizedEpoch of this node.
|
||||
balance uint64 // the balance that voted for this node directly
|
||||
weight uint64 // weight of this node: the total balance including children
|
||||
bestDescendant *Node // bestDescendant node of this node.
|
||||
optimistic bool // whether the block has been fully validated or not
|
||||
slot types.Slot // slot of the block converted to the node.
|
||||
root [fieldparams.RootLength]byte // root of the block converted to the node.
|
||||
payloadHash [fieldparams.RootLength]byte // payloadHash of the block converted to the node.
|
||||
parent *Node // parent index of this node.
|
||||
children []*Node // the list of direct children of this Node
|
||||
justifiedEpoch types.Epoch // justifiedEpoch of this node.
|
||||
unrealizedJustifiedEpoch types.Epoch // the epoch that would be justified if the block would be advanced to the next epoch.
|
||||
finalizedEpoch types.Epoch // finalizedEpoch of this node.
|
||||
unrealizedFinalizedEpoch types.Epoch // the epoch that would be finalized if the block would be advanced to the next epoch.
|
||||
balance uint64 // the balance that voted for this node directly
|
||||
weight uint64 // weight of this node: the total balance including children
|
||||
bestDescendant *Node // bestDescendant node of this node.
|
||||
optimistic bool // whether the block has been fully validated or not
|
||||
}
|
||||
|
||||
// Vote defines an individual validator's vote.
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package doublylinkedtree
|
||||
|
||||
import types "github.com/prysmaticlabs/prysm/consensus-types/primitives"
|
||||
|
||||
func (s *Store) setUnrealizedJustifiedEpoch(root [32]byte, epoch types.Epoch) error {
|
||||
s.nodesLock.Lock()
|
||||
defer s.nodesLock.Unlock()
|
||||
|
||||
node, ok := s.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
return ErrNilNode
|
||||
}
|
||||
if epoch < node.unrealizedJustifiedEpoch {
|
||||
return errInvalidUnrealizedJustifiedEpoch
|
||||
}
|
||||
node.unrealizedJustifiedEpoch = epoch
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) setUnrealizedFinalizedEpoch(root [32]byte, epoch types.Epoch) error {
|
||||
s.nodesLock.Lock()
|
||||
defer s.nodesLock.Unlock()
|
||||
|
||||
node, ok := s.nodeByRoot[root]
|
||||
if !ok || node == nil {
|
||||
return ErrNilNode
|
||||
}
|
||||
if epoch < node.unrealizedFinalizedEpoch {
|
||||
return errInvalidUnrealizedFinalizedEpoch
|
||||
}
|
||||
node.unrealizedFinalizedEpoch = epoch
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUnrealizedCheckpoints "realizes" the unrealized justified and finalized
|
||||
// epochs stored within nodes. It should be called at the beginning of each
|
||||
// epoch
|
||||
func (f *ForkChoice) UpdateUnrealizedCheckpoints() {
|
||||
f.store.nodesLock.Lock()
|
||||
defer f.store.nodesLock.Unlock()
|
||||
for _, node := range f.store.nodeByRoot {
|
||||
node.justifiedEpoch = node.unrealizedJustifiedEpoch
|
||||
node.finalizedEpoch = node.unrealizedFinalizedEpoch
|
||||
if node.justifiedEpoch > f.store.justifiedEpoch {
|
||||
f.store.justifiedEpoch = node.justifiedEpoch
|
||||
}
|
||||
if node.finalizedEpoch > f.store.finalizedEpoch {
|
||||
f.store.finalizedEpoch = node.finalizedEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
package doublylinkedtree
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/config/params"
|
||||
types "github.com/prysmaticlabs/prysm/consensus-types/primitives"
|
||||
"github.com/prysmaticlabs/prysm/testing/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestStore_SetUnrealizedEpochs(t *testing.T) {
|
||||
f := setup(1, 1)
|
||||
ctx := context.Background()
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1))
|
||||
f.store.nodesLock.RLock()
|
||||
require.Equal(t, types.Epoch(1), f.store.nodeByRoot[[32]byte{'b'}].unrealizedJustifiedEpoch)
|
||||
require.Equal(t, types.Epoch(1), f.store.nodeByRoot[[32]byte{'b'}].unrealizedFinalizedEpoch)
|
||||
f.store.nodesLock.RUnlock()
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'b'}, 2))
|
||||
require.NoError(t, f.store.setUnrealizedFinalizedEpoch([32]byte{'b'}, 2))
|
||||
f.store.nodesLock.RLock()
|
||||
require.Equal(t, types.Epoch(2), f.store.nodeByRoot[[32]byte{'b'}].unrealizedJustifiedEpoch)
|
||||
require.Equal(t, types.Epoch(2), f.store.nodeByRoot[[32]byte{'b'}].unrealizedFinalizedEpoch)
|
||||
f.store.nodesLock.RUnlock()
|
||||
|
||||
require.ErrorIs(t, errInvalidUnrealizedJustifiedEpoch, f.store.setUnrealizedJustifiedEpoch([32]byte{'b'}, 0))
|
||||
require.ErrorIs(t, errInvalidUnrealizedFinalizedEpoch, f.store.setUnrealizedFinalizedEpoch([32]byte{'b'}, 0))
|
||||
}
|
||||
|
||||
func TestStore_UpdateUnrealizedCheckpoints(t *testing.T) {
|
||||
f := setup(1, 1)
|
||||
ctx := context.Background()
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1))
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// Epoch 2 | Epoch 3
|
||||
// |
|
||||
// C |
|
||||
// / |
|
||||
// A <-- B |
|
||||
// \ |
|
||||
// ---- D
|
||||
//
|
||||
// B is the first block that justifies A.
|
||||
//
|
||||
func TestStore_LongFork(t *testing.T) {
|
||||
f := setup(1, 1)
|
||||
ctx := context.Background()
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'b'}, 2))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'c'}, 2))
|
||||
|
||||
// Add an attestation to c, it is head
|
||||
f.ProcessAttestation(ctx, []uint64{0}, [32]byte{'c'}, 1)
|
||||
headRoot, err := f.Head(ctx, 1, [32]byte{}, []uint64{100}, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'c'}, headRoot)
|
||||
|
||||
// D is head even though its weight is lower.
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 103, [32]byte{'d'}, [32]byte{'b'}, [32]byte{'D'}, 2, 1))
|
||||
headRoot, err = f.Head(ctx, 2, [32]byte{}, []uint64{100}, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'d'}, headRoot)
|
||||
require.Equal(t, uint64(0), f.store.nodeByRoot[[32]byte{'d'}].weight)
|
||||
require.Equal(t, uint64(100), f.store.nodeByRoot[[32]byte{'c'}].weight)
|
||||
|
||||
// Update unrealized justification, c becomes head
|
||||
f.UpdateUnrealizedCheckpoints()
|
||||
headRoot, err = f.Head(ctx, 2, [32]byte{}, []uint64{100}, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'c'}, headRoot)
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// Epoch 1 Epoch 2 Epoch 3
|
||||
// | |
|
||||
// | |
|
||||
// A <-- B <-- C <-- D <-- E <-- F <-- G <-- H |
|
||||
// | \ |
|
||||
// | --------------- I
|
||||
// | |
|
||||
//
|
||||
// E justifies A. G justifies E.
|
||||
//
|
||||
func TestStore_NoDeadLock(t *testing.T) {
|
||||
f := setup(0, 0)
|
||||
ctx := context.Background()
|
||||
|
||||
// Epoch 1 blocks
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{'D'}, 0, 0))
|
||||
|
||||
// Epoch 2 Blocks
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 104, [32]byte{'e'}, [32]byte{'d'}, [32]byte{'E'}, 0, 0))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'e'}, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{'F'}, 0, 0))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'f'}, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 106, [32]byte{'g'}, [32]byte{'f'}, [32]byte{'G'}, 0, 0))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'g'}, 2))
|
||||
require.NoError(t, f.store.setUnrealizedFinalizedEpoch([32]byte{'g'}, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 107, [32]byte{'h'}, [32]byte{'g'}, [32]byte{'H'}, 0, 0))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'h'}, 2))
|
||||
require.NoError(t, f.store.setUnrealizedFinalizedEpoch([32]byte{'h'}, 1))
|
||||
// Add an attestation for h
|
||||
f.ProcessAttestation(ctx, []uint64{0}, [32]byte{'h'}, 1)
|
||||
|
||||
// Epoch 3
|
||||
// Current Head is H
|
||||
headRoot, err := f.Head(ctx, 0, [32]byte{}, []uint64{100}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'h'}, headRoot)
|
||||
require.Equal(t, types.Epoch(0), f.JustifiedEpoch())
|
||||
|
||||
// Insert Block I, it becomes Head
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 108, [32]byte{'i'}, [32]byte{'f'}, [32]byte{'I'}, 1, 0))
|
||||
headRoot, err = f.Head(ctx, 1, [32]byte{}, []uint64{100}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'i'}, headRoot)
|
||||
require.Equal(t, types.Epoch(1), f.JustifiedEpoch())
|
||||
require.Equal(t, types.Epoch(0), f.FinalizedEpoch())
|
||||
|
||||
// Realized Justified checkpoints, H becomes head
|
||||
f.UpdateUnrealizedCheckpoints()
|
||||
headRoot, err = f.Head(ctx, 1, [32]byte{}, []uint64{100}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'h'}, headRoot)
|
||||
require.Equal(t, types.Epoch(2), f.JustifiedEpoch())
|
||||
require.Equal(t, types.Epoch(1), f.FinalizedEpoch())
|
||||
}
|
||||
|
||||
// Epoch 1 | Epoch 2
|
||||
// |
|
||||
// -- D (late)
|
||||
// / |
|
||||
// A <- B <- C |
|
||||
// \ |
|
||||
// -- -- -- E <- F <- G <- H
|
||||
// |
|
||||
//
|
||||
// D justifies and comes late.
|
||||
//
|
||||
func TestStore_ForkNextEpoch(t *testing.T) {
|
||||
f := setup(0, 0)
|
||||
ctx := context.Background()
|
||||
|
||||
// Epoch 1 blocks (D does not arrive)
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 0, 0))
|
||||
|
||||
// Epoch 2 blocks
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 104, [32]byte{'e'}, [32]byte{'c'}, [32]byte{'E'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{'F'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 106, [32]byte{'g'}, [32]byte{'f'}, [32]byte{'G'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 107, [32]byte{'h'}, [32]byte{'g'}, [32]byte{'H'}, 0, 0))
|
||||
|
||||
// Insert an attestation to H, H is head
|
||||
f.ProcessAttestation(ctx, []uint64{0}, [32]byte{'h'}, 1)
|
||||
headRoot, err := f.Head(ctx, 0, [32]byte{}, []uint64{100}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'h'}, headRoot)
|
||||
require.Equal(t, types.Epoch(0), f.JustifiedEpoch())
|
||||
|
||||
// D arrives late, D is head
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{'D'}, 0, 0))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'d'}, 1))
|
||||
f.UpdateUnrealizedCheckpoints()
|
||||
headRoot, err = f.Head(ctx, 0, [32]byte{}, []uint64{100}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'d'}, headRoot)
|
||||
require.Equal(t, types.Epoch(1), f.JustifiedEpoch())
|
||||
require.Equal(t, uint64(0), f.store.nodeByRoot[[32]byte{'d'}].weight)
|
||||
require.Equal(t, uint64(100), f.store.nodeByRoot[[32]byte{'h'}].weight)
|
||||
}
|
||||
@@ -12,6 +12,7 @@ go_library(
|
||||
"proposer_boost.go",
|
||||
"store.go",
|
||||
"types.go",
|
||||
"unrealized_justification.go",
|
||||
],
|
||||
importpath = "github.com/prysmaticlabs/prysm/beacon-chain/forkchoice/protoarray",
|
||||
visibility = [
|
||||
@@ -44,6 +45,7 @@ go_test(
|
||||
"optimistic_sync_test.go",
|
||||
"proposer_boost_test.go",
|
||||
"store_test.go",
|
||||
"unrealized_justification_test.go",
|
||||
"vote_test.go",
|
||||
],
|
||||
embed = [":go_default_library"],
|
||||
@@ -55,5 +57,6 @@ go_test(
|
||||
"//encoding/bytesutil:go_default_library",
|
||||
"//testing/assert:go_default_library",
|
||||
"//testing/require:go_default_library",
|
||||
"@org_golang_x_net//context:go_default_library",
|
||||
],
|
||||
)
|
||||
|
||||
@@ -13,3 +13,5 @@ var errInvalidParentDelta = errors.New("parent delta is invalid")
|
||||
var errInvalidNodeDelta = errors.New("node delta is invalid")
|
||||
var errInvalidDeltaLength = errors.New("delta length is invalid")
|
||||
var errInvalidOptimisticStatus = errors.New("invalid optimistic status")
|
||||
var errInvalidUnrealizedJustifiedEpoch = errors.New("invalid unrealized justified epoch")
|
||||
var errInvalidUnrealizedFinalizedEpoch = errors.New("invalid unrealized finalized epoch")
|
||||
|
||||
@@ -337,15 +337,17 @@ func (s *Store) insert(ctx context.Context,
|
||||
}
|
||||
|
||||
n := &Node{
|
||||
slot: slot,
|
||||
root: root,
|
||||
parent: parentIndex,
|
||||
justifiedEpoch: justifiedEpoch,
|
||||
finalizedEpoch: finalizedEpoch,
|
||||
bestChild: NonExistentNode,
|
||||
bestDescendant: NonExistentNode,
|
||||
weight: 0,
|
||||
payloadHash: payloadHash,
|
||||
slot: slot,
|
||||
root: root,
|
||||
parent: parentIndex,
|
||||
justifiedEpoch: justifiedEpoch,
|
||||
unrealizedJustifiedEpoch: justifiedEpoch,
|
||||
finalizedEpoch: finalizedEpoch,
|
||||
unrealizedFinalizedEpoch: finalizedEpoch,
|
||||
bestChild: NonExistentNode,
|
||||
bestDescendant: NonExistentNode,
|
||||
weight: 0,
|
||||
payloadHash: payloadHash,
|
||||
}
|
||||
|
||||
s.nodesIndices[root] = index
|
||||
@@ -382,7 +384,7 @@ func (s *Store) applyWeightChanges(
|
||||
}
|
||||
|
||||
// Update the justified / finalized epochs in store if necessary.
|
||||
if s.justifiedEpoch != justifiedEpoch || s.finalizedEpoch != finalizedEpoch {
|
||||
if s.justifiedEpoch < justifiedEpoch || s.finalizedEpoch < finalizedEpoch {
|
||||
s.justifiedEpoch = justifiedEpoch
|
||||
s.finalizedEpoch = finalizedEpoch
|
||||
}
|
||||
|
||||
@@ -36,16 +36,18 @@ type Store struct {
|
||||
// Node defines the individual block which includes its block parent, ancestor and how much weight accounted for it.
|
||||
// This is used as an array based stateful DAG for efficient fork choice look up.
|
||||
type Node struct {
|
||||
slot types.Slot // slot of the block converted to the node.
|
||||
root [fieldparams.RootLength]byte // root of the block converted to the node.
|
||||
payloadHash [fieldparams.RootLength]byte // payloadHash of the block converted to the node.
|
||||
parent uint64 // parent index of this node.
|
||||
justifiedEpoch types.Epoch // justifiedEpoch of this node.
|
||||
finalizedEpoch types.Epoch // finalizedEpoch of this node.
|
||||
weight uint64 // weight of this node.
|
||||
bestChild uint64 // bestChild index of this node.
|
||||
bestDescendant uint64 // bestDescendant of this node.
|
||||
status status // optimistic status of this node
|
||||
slot types.Slot // slot of the block converted to the node.
|
||||
root [fieldparams.RootLength]byte // root of the block converted to the node.
|
||||
payloadHash [fieldparams.RootLength]byte // payloadHash of the block converted to the node.
|
||||
parent uint64 // parent index of this node.
|
||||
justifiedEpoch types.Epoch // justifiedEpoch of this node.
|
||||
unrealizedJustifiedEpoch types.Epoch // the epoch that would be justified if the block would be advanced to the next epoch.
|
||||
finalizedEpoch types.Epoch // finalizedEpoch of this node.
|
||||
unrealizedFinalizedEpoch types.Epoch // the epoch that would be finalized if the block would be advanced to the next epoch.
|
||||
weight uint64 // weight of this node.
|
||||
bestChild uint64 // bestChild index of this node.
|
||||
bestDescendant uint64 // bestDescendant of this node.
|
||||
status status // optimistic status of this node
|
||||
}
|
||||
|
||||
// enum used as optimistic status of a node
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
package protoarray
|
||||
|
||||
import (
|
||||
types "github.com/prysmaticlabs/prysm/consensus-types/primitives"
|
||||
)
|
||||
|
||||
func (s *Store) setUnrealizedJustifiedEpoch(root [32]byte, epoch types.Epoch) error {
|
||||
s.nodesLock.Lock()
|
||||
defer s.nodesLock.Unlock()
|
||||
index, ok := s.nodesIndices[root]
|
||||
if !ok {
|
||||
return ErrUnknownNodeRoot
|
||||
}
|
||||
if index >= uint64(len(s.nodes)) {
|
||||
return errInvalidNodeIndex
|
||||
}
|
||||
node := s.nodes[index]
|
||||
if node == nil {
|
||||
return errInvalidNodeIndex
|
||||
}
|
||||
if epoch < node.unrealizedJustifiedEpoch {
|
||||
return errInvalidUnrealizedJustifiedEpoch
|
||||
}
|
||||
node.unrealizedJustifiedEpoch = epoch
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Store) setUnrealizedFinalizedEpoch(root [32]byte, epoch types.Epoch) error {
|
||||
s.nodesLock.Lock()
|
||||
defer s.nodesLock.Unlock()
|
||||
index, ok := s.nodesIndices[root]
|
||||
if !ok {
|
||||
return ErrUnknownNodeRoot
|
||||
}
|
||||
if index >= uint64(len(s.nodes)) {
|
||||
return errInvalidNodeIndex
|
||||
}
|
||||
node := s.nodes[index]
|
||||
if node == nil {
|
||||
return errInvalidNodeIndex
|
||||
}
|
||||
if epoch < node.unrealizedFinalizedEpoch {
|
||||
return errInvalidUnrealizedFinalizedEpoch
|
||||
}
|
||||
node.unrealizedFinalizedEpoch = epoch
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateUnrealizedCheckpoints "realizes" the unrealized justified and finalized
|
||||
// epochs stored within nodes. It should be called at the beginning of each
|
||||
// epoch
|
||||
func (f *ForkChoice) UpdateUnrealizedCheckpoints() {
|
||||
f.store.nodesLock.Lock()
|
||||
defer f.store.nodesLock.Unlock()
|
||||
for _, node := range f.store.nodes {
|
||||
node.justifiedEpoch = node.unrealizedJustifiedEpoch
|
||||
node.finalizedEpoch = node.unrealizedFinalizedEpoch
|
||||
if node.justifiedEpoch > f.store.justifiedEpoch {
|
||||
f.store.justifiedEpoch = node.justifiedEpoch
|
||||
}
|
||||
if node.finalizedEpoch > f.store.finalizedEpoch {
|
||||
f.store.finalizedEpoch = node.finalizedEpoch
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,187 @@
|
||||
package protoarray
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/prysmaticlabs/prysm/config/params"
|
||||
types "github.com/prysmaticlabs/prysm/consensus-types/primitives"
|
||||
"github.com/prysmaticlabs/prysm/testing/require"
|
||||
"golang.org/x/net/context"
|
||||
)
|
||||
|
||||
func TestStore_SetUnrealizedEpochs(t *testing.T) {
|
||||
f := setup(1, 1)
|
||||
ctx := context.Background()
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1))
|
||||
f.store.nodesLock.RLock()
|
||||
require.Equal(t, types.Epoch(1), f.store.nodes[2].unrealizedJustifiedEpoch)
|
||||
require.Equal(t, types.Epoch(1), f.store.nodes[2].unrealizedFinalizedEpoch)
|
||||
f.store.nodesLock.RUnlock()
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'b'}, 2))
|
||||
require.NoError(t, f.store.setUnrealizedFinalizedEpoch([32]byte{'b'}, 2))
|
||||
f.store.nodesLock.RLock()
|
||||
require.Equal(t, types.Epoch(2), f.store.nodes[2].unrealizedJustifiedEpoch)
|
||||
require.Equal(t, types.Epoch(2), f.store.nodes[2].unrealizedFinalizedEpoch)
|
||||
f.store.nodesLock.RUnlock()
|
||||
|
||||
require.ErrorIs(t, errInvalidUnrealizedJustifiedEpoch, f.store.setUnrealizedJustifiedEpoch([32]byte{'b'}, 0))
|
||||
require.ErrorIs(t, errInvalidUnrealizedFinalizedEpoch, f.store.setUnrealizedFinalizedEpoch([32]byte{'b'}, 0))
|
||||
}
|
||||
|
||||
func TestStore_UpdateUnrealizedCheckpoints(t *testing.T) {
|
||||
f := setup(1, 1)
|
||||
ctx := context.Background()
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1))
|
||||
|
||||
}
|
||||
|
||||
//
|
||||
// Epoch 2 | Epoch 3
|
||||
// |
|
||||
// C |
|
||||
// / |
|
||||
// A <-- B |
|
||||
// \ |
|
||||
// ---- D
|
||||
//
|
||||
// B is the first block that justifies A.
|
||||
//
|
||||
func TestStore_LongFork(t *testing.T) {
|
||||
f := setup(1, 1)
|
||||
ctx := context.Background()
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 1, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 1, 1))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'b'}, 2))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 1, 1))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'c'}, 2))
|
||||
|
||||
// Add an attestation to c, it is head
|
||||
f.ProcessAttestation(ctx, []uint64{0}, [32]byte{'c'}, 1)
|
||||
headRoot, err := f.Head(ctx, 1, [32]byte{}, []uint64{100}, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'c'}, headRoot)
|
||||
|
||||
// D is head even though its weight is lower.
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 103, [32]byte{'d'}, [32]byte{'b'}, [32]byte{'D'}, 2, 1))
|
||||
headRoot, err = f.Head(ctx, 2, [32]byte{}, []uint64{100}, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'d'}, headRoot)
|
||||
require.Equal(t, uint64(0), f.store.nodes[4].weight)
|
||||
require.Equal(t, uint64(100), f.store.nodes[3].weight)
|
||||
|
||||
// Update unrealized justification, c becomes head
|
||||
f.UpdateUnrealizedCheckpoints()
|
||||
headRoot, err = f.Head(ctx, 2, [32]byte{}, []uint64{100}, 1)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'c'}, headRoot)
|
||||
}
|
||||
|
||||
//
|
||||
//
|
||||
// Epoch 1 Epoch 2 Epoch 3
|
||||
// | |
|
||||
// | |
|
||||
// A <-- B <-- C <-- D <-- E <-- F <-- G <-- H |
|
||||
// | \ |
|
||||
// | --------------- I
|
||||
// | |
|
||||
//
|
||||
// E justifies A. G justifies E.
|
||||
//
|
||||
func TestStore_NoDeadLock(t *testing.T) {
|
||||
f := setup(0, 0)
|
||||
ctx := context.Background()
|
||||
|
||||
// Epoch 1 blocks
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{'D'}, 0, 0))
|
||||
|
||||
// Epoch 2 Blocks
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 104, [32]byte{'e'}, [32]byte{'d'}, [32]byte{'E'}, 0, 0))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'e'}, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{'F'}, 0, 0))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'f'}, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 106, [32]byte{'g'}, [32]byte{'f'}, [32]byte{'G'}, 0, 0))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'g'}, 2))
|
||||
require.NoError(t, f.store.setUnrealizedFinalizedEpoch([32]byte{'g'}, 1))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 107, [32]byte{'h'}, [32]byte{'g'}, [32]byte{'H'}, 0, 0))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'h'}, 2))
|
||||
require.NoError(t, f.store.setUnrealizedFinalizedEpoch([32]byte{'h'}, 1))
|
||||
// Add an attestation for h
|
||||
f.ProcessAttestation(ctx, []uint64{0}, [32]byte{'h'}, 1)
|
||||
|
||||
// Epoch 3
|
||||
// Current Head is H
|
||||
headRoot, err := f.Head(ctx, 0, [32]byte{}, []uint64{100}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'h'}, headRoot)
|
||||
require.Equal(t, types.Epoch(0), f.JustifiedEpoch())
|
||||
|
||||
// Insert Block I, it becomes Head
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 108, [32]byte{'i'}, [32]byte{'f'}, [32]byte{'I'}, 1, 0))
|
||||
headRoot, err = f.Head(ctx, 1, [32]byte{}, []uint64{100}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'i'}, headRoot)
|
||||
require.Equal(t, types.Epoch(1), f.JustifiedEpoch())
|
||||
require.Equal(t, types.Epoch(0), f.FinalizedEpoch())
|
||||
|
||||
// Realized Justified checkpoints, H becomes head
|
||||
f.UpdateUnrealizedCheckpoints()
|
||||
headRoot, err = f.Head(ctx, 1, [32]byte{}, []uint64{100}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'h'}, headRoot)
|
||||
require.Equal(t, types.Epoch(2), f.JustifiedEpoch())
|
||||
require.Equal(t, types.Epoch(1), f.FinalizedEpoch())
|
||||
}
|
||||
|
||||
// Epoch 1 | Epoch 2
|
||||
// |
|
||||
// -- D (late) --
|
||||
// / |
|
||||
// A <- B <- C |
|
||||
// \ |
|
||||
// -- -- -- E <- F <- G <- H
|
||||
// |
|
||||
//
|
||||
// D justifies and comes late.
|
||||
//
|
||||
func TestStore_ForkNextEpoch(t *testing.T) {
|
||||
f := setup(0, 0)
|
||||
ctx := context.Background()
|
||||
|
||||
// Epoch 1 blocks (D does not arrive)
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 100, [32]byte{'a'}, params.BeaconConfig().ZeroHash, [32]byte{'A'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 101, [32]byte{'b'}, [32]byte{'a'}, [32]byte{'B'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 102, [32]byte{'c'}, [32]byte{'b'}, [32]byte{'C'}, 0, 0))
|
||||
|
||||
// Epoch 2 blocks
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 104, [32]byte{'e'}, [32]byte{'c'}, [32]byte{'E'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 105, [32]byte{'f'}, [32]byte{'e'}, [32]byte{'F'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 106, [32]byte{'g'}, [32]byte{'f'}, [32]byte{'G'}, 0, 0))
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 107, [32]byte{'h'}, [32]byte{'g'}, [32]byte{'H'}, 0, 0))
|
||||
|
||||
// Insert an attestation to H, H is head
|
||||
f.ProcessAttestation(ctx, []uint64{0}, [32]byte{'h'}, 1)
|
||||
headRoot, err := f.Head(ctx, 0, [32]byte{}, []uint64{100}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'h'}, headRoot)
|
||||
require.Equal(t, types.Epoch(0), f.JustifiedEpoch())
|
||||
|
||||
// D arrives late, D is head
|
||||
require.NoError(t, f.InsertOptimisticBlock(ctx, 103, [32]byte{'d'}, [32]byte{'c'}, [32]byte{'D'}, 0, 0))
|
||||
require.NoError(t, f.store.setUnrealizedJustifiedEpoch([32]byte{'d'}, 1))
|
||||
f.UpdateUnrealizedCheckpoints()
|
||||
headRoot, err = f.Head(ctx, 0, [32]byte{}, []uint64{100}, 0)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, [32]byte{'d'}, headRoot)
|
||||
require.Equal(t, types.Epoch(1), f.JustifiedEpoch())
|
||||
// nodes[8] = D since it's late!
|
||||
require.Equal(t, uint64(0), f.store.nodes[8].weight)
|
||||
require.Equal(t, uint64(100), f.store.nodes[7].weight)
|
||||
}
|
||||
2
go.mod
2
go.mod
@@ -88,6 +88,7 @@ require (
|
||||
golang.org/x/crypto v0.0.0-20220321153916-2c7772ba3064
|
||||
golang.org/x/exp v0.0.0-20200513190911-00229845015e
|
||||
golang.org/x/mod v0.6.0-dev.0.20220106191415-9b9b3d81d5e3
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
|
||||
golang.org/x/tools v0.1.10
|
||||
google.golang.org/genproto v0.0.0-20210426193834-eac7f76ac494
|
||||
@@ -237,7 +238,6 @@ require (
|
||||
go.uber.org/atomic v1.9.0 // indirect
|
||||
go.uber.org/multierr v1.8.0 // indirect
|
||||
go.uber.org/zap v1.21.0 // indirect
|
||||
golang.org/x/net v0.0.0-20220225172249-27dd8689420f // indirect
|
||||
golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c // indirect
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211 // indirect
|
||||
golang.org/x/text v0.3.7 // indirect
|
||||
|
||||
Reference in New Issue
Block a user