Skip to content

Commit

Permalink
Respect IL freeze deadline, use forkchoice store for proposer and att…
Browse files Browse the repository at this point in the history
…ester head
  • Loading branch information
terencechain committed Feb 4, 2025
1 parent 626972e commit 38f1586
Show file tree
Hide file tree
Showing 29 changed files with 193 additions and 135 deletions.
35 changes: 1 addition & 34 deletions beacon-chain/blockchain/chain_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ type ForkchoiceFetcher interface {
Ancestor(context.Context, []byte, primitives.Slot) ([]byte, error)
CachedHeadRoot() [32]byte
GetProposerHead() [32]byte
GetAttesterHead() [32]byte
SetForkChoiceGenesisTime(uint64)
UpdateHead(context.Context, primitives.Slot)
HighestReceivedBlockSlot() primitives.Slot
Expand Down Expand Up @@ -69,7 +70,6 @@ type GenesisFetcher interface {
type HeadFetcher interface {
HeadSlot() primitives.Slot
HeadRoot(ctx context.Context) ([]byte, error)
FilteredHeadRoot(ctx context.Context) ([32]byte, error)
HeadBlock(ctx context.Context) (interfaces.ReadOnlySignedBeaconBlock, error)
HeadState(ctx context.Context) (state.BeaconState, error)
HeadStateReadOnly(ctx context.Context) (state.ReadOnlyBeaconState, error)
Expand Down Expand Up @@ -181,39 +181,6 @@ func (s *Service) HeadRoot(ctx context.Context) ([]byte, error) {
return r[:], nil
}

// FilteredHeadRoot returns the filtered head root of the chain.
// If the head root does not satisfy the inclusion list constraint,
// its parent root is returned.
func (s *Service) FilteredHeadRoot(ctx context.Context) ([32]byte, error) {
s.headLock.RLock()
defer s.headLock.RUnlock()

if s.head != nil && s.head.root != params.BeaconConfig().ZeroHash {
if s.head.root == s.badInclusionListBlock {
return s.head.block.Block().ParentRoot(), nil
}
return s.head.root, nil
}

headBlock, err := s.cfg.BeaconDB.HeadBlock(ctx)
if err != nil {
return [32]byte{}, err
}
if headBlock == nil || headBlock.IsNil() {
return params.BeaconConfig().ZeroHash, nil
}

root, err := headBlock.Block().HashTreeRoot()
if err != nil {
return [32]byte{}, err
}
if root == s.badInclusionListBlock {
return headBlock.Block().ParentRoot(), nil
}

return root, nil
}

// HeadBlock returns the head block of the chain.
// If the head is nil from service struct,
// it will attempt to get the head block from DB.
Expand Down
7 changes: 7 additions & 0 deletions beacon-chain/blockchain/chain_info_forkchoice.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,13 @@ func (s *Service) GetProposerHead() [32]byte {
return s.cfg.ForkChoiceStore.GetProposerHead()
}

// GetAttesterHead returns the corresponding value from forkchoice
func (s *Service) GetAttesterHead() [32]byte {
s.cfg.ForkChoiceStore.RLock()
defer s.cfg.ForkChoiceStore.RUnlock()
return s.cfg.ForkChoiceStore.GetAttesterHead()
}

// SetForkChoiceGenesisTime sets the genesis time in Forkchoice
func (s *Service) SetForkChoiceGenesisTime(timestamp uint64) {
s.cfg.ForkChoiceStore.Lock()
Expand Down
6 changes: 1 addition & 5 deletions beacon-chain/blockchain/execution_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -289,11 +289,7 @@ func (s *Service) notifyNewPayload(ctx context.Context, preStateVersion int,
"slot": blk.Block().Slot(),
"parentRoot": fmt.Sprintf("%#x", parentRoot),
}).Info("Called new payload but inclusion list didn't satisfy")
r, err := blk.Block().HashTreeRoot()
if err != nil {
return false, errors.Wrap(err, "could not get block hash tree root")
}
s.badInclusionListBlock = r // Cache the block root that fails to satisfy the inclusion list constraint.
blk.Block().MarkInclusionListNotSatisfied() // Cache the block root that fails to satisfy the inclusion list constraint.
return true, nil
case errors.Is(err, execution.ErrAcceptedSyncingPayloadStatus):
newPayloadOptimisticNodeCount.Inc()
Expand Down
39 changes: 19 additions & 20 deletions beacon-chain/blockchain/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,26 +47,25 @@ import (
// Service represents a service that handles the internal
// logic of managing the full PoS beacon chain.
type Service struct {
cfg *config
ctx context.Context
cancel context.CancelFunc
genesisTime time.Time
head *head
headLock sync.RWMutex
originBlockRoot [32]byte // genesis root, or weak subjectivity checkpoint root, depending on how the node is initialized
boundaryRoots [][32]byte
checkpointStateCache *cache.CheckpointStateCache
initSyncBlocks map[[32]byte]interfaces.ReadOnlySignedBeaconBlock
initSyncBlocksLock sync.RWMutex
wsVerifier *WeakSubjectivityVerifier
clockSetter startup.ClockSetter
clockWaiter startup.ClockWaiter
syncComplete chan struct{}
blobNotifiers *blobNotifierMap
blockBeingSynced *currentlySyncingBlock
blobStorage *filesystem.BlobStorage
inclusionListCache *cache.InclusionLists
badInclusionListBlock [32]byte
cfg *config
ctx context.Context
cancel context.CancelFunc
genesisTime time.Time
head *head
headLock sync.RWMutex
originBlockRoot [32]byte // genesis root, or weak subjectivity checkpoint root, depending on how the node is initialized
boundaryRoots [][32]byte
checkpointStateCache *cache.CheckpointStateCache
initSyncBlocks map[[32]byte]interfaces.ReadOnlySignedBeaconBlock
initSyncBlocksLock sync.RWMutex
wsVerifier *WeakSubjectivityVerifier
clockSetter startup.ClockSetter
clockWaiter startup.ClockWaiter
syncComplete chan struct{}
blobNotifiers *blobNotifierMap
blockBeingSynced *currentlySyncingBlock
blobStorage *filesystem.BlobStorage
inclusionListCache *cache.InclusionLists
}

// config options for the service.
Expand Down
17 changes: 10 additions & 7 deletions beacon-chain/blockchain/testing/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,13 +348,6 @@ func (s *ChainService) HeadRoot(_ context.Context) ([]byte, error) {
return make([]byte, 32), nil
}

func (s *ChainService) FilteredHeadRoot(_ context.Context) ([32]byte, error) {
if len(s.Root) > 0 {
return bytesutil.ToBytes32(s.Root), nil
}
return [32]byte{}, nil
}

// HeadBlock mocks HeadBlock method in chain service.
func (s *ChainService) HeadBlock(context.Context) (interfaces.ReadOnlySignedBeaconBlock, error) {
return s.Block, nil
Expand Down Expand Up @@ -633,6 +626,16 @@ func (s *ChainService) GetProposerHead() [32]byte {
return [32]byte{}
}

// GetAttesterHead mocks the same method in the chain service
func (s *ChainService) GetAttesterHead() [32]byte {
if s.ForkChoiceStore != nil {
return s.ForkChoiceStore.GetAttesterHead()
}
var rootArr [32]byte
copy(rootArr[:], s.Root)
return rootArr
}

// SetForkChoiceGenesisTime mocks the same method in the chain service
func (s *ChainService) SetForkChoiceGenesisTime(timestamp uint64) {
if s.ForkChoiceStore != nil {
Expand Down
21 changes: 14 additions & 7 deletions beacon-chain/cache/inclusion_list.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,30 +10,33 @@ import (
type InclusionLists struct {
mu sync.RWMutex
ils map[primitives.Slot]map[primitives.ValidatorIndex]struct {
txs [][]byte
seenTwice bool
txs [][]byte
seenTwice bool
isBeforeFreezeDeadline bool
}
}

// NewInclusionLists initializes a new InclusionLists instance.
func NewInclusionLists() *InclusionLists {
return &InclusionLists{
ils: make(map[primitives.Slot]map[primitives.ValidatorIndex]struct {
txs [][]byte
seenTwice bool
txs [][]byte
seenTwice bool
isBeforeFreezeDeadline bool
}),
}
}

// Add adds a set of transactions for a specific slot and validator index.
func (i *InclusionLists) Add(slot primitives.Slot, validatorIndex primitives.ValidatorIndex, txs [][]byte) {
func (i *InclusionLists) Add(slot primitives.Slot, validatorIndex primitives.ValidatorIndex, txs [][]byte, isBeforeFreezeDeadline bool) {
i.mu.Lock()
defer i.mu.Unlock()

if _, ok := i.ils[slot]; !ok {
i.ils[slot] = make(map[primitives.ValidatorIndex]struct {
txs [][]byte
seenTwice bool
txs [][]byte
seenTwice bool
isBeforeFreezeDeadline bool
})
}

Expand All @@ -44,6 +47,7 @@ func (i *InclusionLists) Add(slot primitives.Slot, validatorIndex primitives.Val

if entry.txs == nil {
entry.txs = txs
entry.isBeforeFreezeDeadline = isBeforeFreezeDeadline
} else {
entry.seenTwice = true
entry.txs = nil // Clear transactions to save space if seen twice.
Expand All @@ -64,6 +68,9 @@ func (i *InclusionLists) Get(slot primitives.Slot) [][]byte {
var uniqueTxs [][]byte
seen := make(map[[32]byte]struct{})
for _, entry := range ils {
if !entry.isBeforeFreezeDeadline {
continue
}
for _, tx := range entry.txs {
hash := sha256.Sum256(tx)
if _, duplicate := seen[hash]; !duplicate {
Expand Down
6 changes: 3 additions & 3 deletions beacon-chain/cache/inclusion_list_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,23 +17,23 @@ func TestInclusionLists(t *testing.T) {
{
name: "Add single validator with unique transactions",
actions: func() {
il.Add(1, 1, [][]byte{[]byte("tx1"), []byte("tx2")})
il.Add(1, 1, [][]byte{[]byte("tx1"), []byte("tx2")}, true)
},
expectedGet: [][]byte{[]byte("tx1"), []byte("tx2")},
expectedTwice: false,
},
{
name: "Add duplicate transactions for second validator",
actions: func() {
il.Add(1, 2, [][]byte{[]byte("tx1"), []byte("tx3")})
il.Add(1, 2, [][]byte{[]byte("tx1"), []byte("tx3")}, true)
},
expectedGet: [][]byte{[]byte("tx1"), []byte("tx2"), []byte("tx3")},
expectedTwice: false,
},
{
name: "Mark validator as seen twice",
actions: func() {
il.Add(1, 1, [][]byte{[]byte("tx4")})
il.Add(1, 1, [][]byte{[]byte("tx4")}, true)
},
expectedGet: [][]byte{[]byte("tx1"), []byte("tx3")},
expectedTwice: true,
Expand Down
1 change: 1 addition & 0 deletions beacon-chain/forkchoice/doubly-linked-tree/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ load("@prysm//tools/go:def.bzl", "go_library", "go_test")
go_library(
name = "go_default_library",
srcs = [
"attester_head.go",
"doc.go",
"errors.go",
"forkchoice.go",
Expand Down
18 changes: 18 additions & 0 deletions beacon-chain/forkchoice/doubly-linked-tree/attester_head.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package doublylinkedtree

// GetAttesterHead returns the attester head root given inclusion list satisfaction.
func (f *ForkChoice) GetAttesterHead() [32]byte {
head := f.store.headNode
if head == nil {
return [32]byte{}
}

parent := head.parent
if parent == nil {
return head.root
}
if head.notSatisfyingInclusionList {
return parent.root
}
return head.root
}
23 changes: 18 additions & 5 deletions beacon-chain/forkchoice/doubly-linked-tree/reorg_late_blocks.go
Original file line number Diff line number Diff line change
Expand Up @@ -147,11 +147,6 @@ func (f *ForkChoice) GetProposerHead() [32]byte {
return head.root
}

// Only orphan a block if the parent LMD vote is strong
if parent.weight*100 < f.store.committeeWeight*params.BeaconConfig().ReorgParentWeightThreshold {
return head.root
}

// Only reorg if we are proposing early
secs, err := slots.SecondsSinceSlotStart(head.slot+1, f.store.genesisTime, uint64(time.Now().Unix()))
if err != nil {
Expand All @@ -161,5 +156,23 @@ func (f *ForkChoice) GetProposerHead() [32]byte {
if secs >= orphanLateBlockProposingEarly {
return head.root
}

// Newly added in EIP-7805
// reorg_prerequisites = all([shuffling_stable, ffg_competitive, finalization_ok,
// proposing_on_time, single_slot_reorg, head_weak, parent_strong])
//
// # Check that the head block is in the unsatisfied inclusion list blocks
// inclusion_list_not_satisfied = head_root in store.unsatisfied_inclusion_list_blocks # [New in EIP-7805]
//
// if reorg_prerequisites and (head_late or inclusion_list_not_satisfied):
// return parent_root
// else:
// return head_root

// Only orphan a block if the parent LMD vote is strong and satisfies inclusion list
if parent.weight*100 < f.store.committeeWeight*params.BeaconConfig().ReorgParentWeightThreshold && !head.notSatisfyingInclusionList {
return head.root
}

return parent.root
}
21 changes: 11 additions & 10 deletions beacon-chain/forkchoice/doubly-linked-tree/store.go
Original file line number Diff line number Diff line change
Expand Up @@ -90,16 +90,17 @@ func (s *Store) insert(ctx context.Context,

parent := s.nodeByRoot[parentRoot]
n := &Node{
slot: slot,
root: root,
parent: parent,
justifiedEpoch: justifiedEpoch,
unrealizedJustifiedEpoch: justifiedEpoch,
finalizedEpoch: finalizedEpoch,
unrealizedFinalizedEpoch: finalizedEpoch,
optimistic: true,
payloadHash: payloadHash,
timestamp: uint64(time.Now().Unix()),
slot: slot,
root: root,
parent: parent,
justifiedEpoch: justifiedEpoch,
unrealizedJustifiedEpoch: justifiedEpoch,
finalizedEpoch: finalizedEpoch,
unrealizedFinalizedEpoch: finalizedEpoch,
optimistic: true,
payloadHash: payloadHash,
timestamp: uint64(time.Now().Unix()),
notSatisfyingInclusionList: roblock.Block().NotSatisfyingInclusionList(),
}

// Set the node's target checkpoint
Expand Down
31 changes: 16 additions & 15 deletions beacon-chain/forkchoice/doubly-linked-tree/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -46,21 +46,22 @@ 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 primitives.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.
target *Node // target checkpoint for
children []*Node // the list of direct children of this Node
justifiedEpoch primitives.Epoch // justifiedEpoch of this node.
unrealizedJustifiedEpoch primitives.Epoch // the epoch that would be justified if the block would be advanced to the next epoch.
finalizedEpoch primitives.Epoch // finalizedEpoch of this node.
unrealizedFinalizedEpoch primitives.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
timestamp uint64 // The timestamp when the node was inserted.
slot primitives.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.
target *Node // target checkpoint for
children []*Node // the list of direct children of this Node
justifiedEpoch primitives.Epoch // justifiedEpoch of this node.
unrealizedJustifiedEpoch primitives.Epoch // the epoch that would be justified if the block would be advanced to the next epoch.
finalizedEpoch primitives.Epoch // finalizedEpoch of this node.
unrealizedFinalizedEpoch primitives.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
notSatisfyingInclusionList bool // whether the node is not satisfying the inclusion list
timestamp uint64 // The timestamp when the node was inserted.
}

// Vote defines an individual validator's vote.
Expand Down
1 change: 1 addition & 0 deletions beacon-chain/forkchoice/interfaces.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type RLocker interface {
type HeadRetriever interface {
Head(context.Context) ([32]byte, error)
GetProposerHead() [32]byte
GetAttesterHead() [32]byte
CachedHeadRoot() [32]byte
}

Expand Down
6 changes: 2 additions & 4 deletions beacon-chain/rpc/core/validator.go
Original file line number Diff line number Diff line change
Expand Up @@ -541,10 +541,8 @@ func (s *Service) GetAttestationData(
return nil, &RpcError{Reason: Unavailable, Err: errOptimisticMode}
}

headRoot, err := s.HeadFetcher.FilteredHeadRoot(ctx) // Attesters vote based on IL constrained head root.
if err != nil {
return nil, &RpcError{Reason: Internal, Err: errors.Wrap(err, "could not get head root")}
}
headRoot := s.ChainInfoFetcher.GetAttesterHead() // Attesters vote based on IL constrained head root.

targetEpoch := slots.ToEpoch(req.Slot)
targetRoot, err := s.HeadFetcher.TargetRootForEpoch(headRoot, targetEpoch)
if err != nil {
Expand Down
Loading

0 comments on commit 38f1586

Please sign in to comment.