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:
Potuz
2022-05-21 10:27:07 -03:00
committed by GitHub
parent 244e670b71
commit e7991b9d7b
14 changed files with 553 additions and 42 deletions

View File

@@ -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",
],
)

View File

@@ -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")

View File

@@ -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")
}

View File

@@ -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

View File

@@ -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.

View File

@@ -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
}
}
}

View File

@@ -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)
}

View File

@@ -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",
],
)

View File

@@ -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")

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
}
}
}

View File

@@ -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
View File

@@ -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