Skip to content

Commit

Permalink
Implement EIP7805: Fork-choice enforced Inclusion Lists
Browse files Browse the repository at this point in the history
  • Loading branch information
terencechain committed Dec 28, 2024
1 parent 6ce6b86 commit 2671fcf
Show file tree
Hide file tree
Showing 66 changed files with 3,564 additions and 1,653 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ The format is based on Keep a Changelog, and this project adheres to Semantic Ve
- Log before blob filesystem cache warm-up.
- New design for the attestation pool. [PR](https://github.com/prysmaticlabs/prysm/pull/14324)
- Add field param placeholder for Electra blob target and max to pass spec tests.
- Placeholder for FOCIL to make CI happy : )

### Changed

Expand Down
1 change: 1 addition & 0 deletions beacon-chain/blockchain/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ go_library(
"forkchoice_update_execution.go",
"head.go",
"head_sync_committee_info.go",
"inclusion_list.go",
"init_sync_process_block.go",
"log.go",
"merge_ascii_art.go",
Expand Down
34 changes: 34 additions & 0 deletions beacon-chain/blockchain/chain_info.go
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,7 @@ 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 @@ -180,6 +181,39 @@ 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
19 changes: 18 additions & 1 deletion beacon-chain/blockchain/execution_engine.go
Original file line number Diff line number Diff line change
Expand Up @@ -269,12 +269,29 @@ func (s *Service) notifyNewPayload(ctx context.Context, preStateVersion int,
return false, errors.Wrap(err, "could not get execution requests")
}
}
lastValidHash, err = s.cfg.ExecutionEngineCaller.NewPayload(ctx, payload, versionedHashes, parentRoot, requests)

txs := make([][]byte, 0)
// Post-FOCIL, only consider the inclusion list constraint if it matches the current slot.
if slots.ToEpoch(s.CurrentSlot()) >= params.BeaconConfig().FocilForkEpoch && s.CurrentSlot() == blk.Block().Slot() {
txs = s.inclusionListCache.Get(blk.Block().Slot())
}
lastValidHash, err = s.cfg.ExecutionEngineCaller.NewPayload(ctx, payload, versionedHashes, parentRoot, requests, txs)

switch {
case err == nil:
newPayloadValidNodeCount.Inc()
return true, nil
case errors.Is(err, execution.ErrBadInclusionListPayloadStatus):
log.WithFields(logrus.Fields{
"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.
return true, nil
case errors.Is(err, execution.ErrAcceptedSyncingPayloadStatus):
newPayloadOptimisticNodeCount.Inc()
log.WithFields(logrus.Fields{
Expand Down
59 changes: 59 additions & 0 deletions beacon-chain/blockchain/inclusion_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package blockchain

import (
"context"
"time"

"github.com/prysmaticlabs/prysm/v5/config/params"
"github.com/prysmaticlabs/prysm/v5/time/slots"
)

const updateInclusionListBlockInterval = time.Second

// Routine that updates block building with inclusion lists one second before the slot starts.
func (s *Service) updateBlockWithInclusionListRoutine() {
if err := s.waitForSync(); err != nil {
log.WithError(err).Error("Failed to wait for initial sync")
return
}

interval := time.Second*time.Duration(params.BeaconConfig().SecondsPerSlot) - updateInclusionListBlockInterval
ticker := slots.NewSlotTickerWithIntervals(s.genesisTime, []time.Duration{interval})

for {
select {
case <-s.ctx.Done():
return
case <-ticker.C():
s.updateBlockWithInclusionList(context.Background())
}
}
}

// Updates block building with inclusion lists, the current payload ID, and the new upload ID.
func (s *Service) updateBlockWithInclusionList(ctx context.Context) {
currentSlot := s.CurrentSlot()

// Skip update if not in or past the FOCIL fork epoch.
if slots.ToEpoch(currentSlot) < params.BeaconConfig().FocilForkEpoch {
return
}

s.cfg.ForkChoiceStore.RLock()
defer s.cfg.ForkChoiceStore.RUnlock()

headRoot := s.headRoot()
id, found := s.cfg.PayloadIDCache.PayloadID(currentSlot+1, headRoot)
if !found {
return
}

txs := s.inclusionListCache.Get(currentSlot)
newID, err := s.cfg.ExecutionEngineCaller.UpdatePayloadWithInclusionList(ctx, id, txs)
if err != nil {
log.WithError(err).Error("Failed to update block with inclusion list")
return
}

s.cfg.PayloadIDCache.Set(currentSlot+1, headRoot, *newID)
}
39 changes: 21 additions & 18 deletions beacon-chain/blockchain/service.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,24 +47,26 @@ 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
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[*ethpb.InclusionList]
badInclusionListBlock [32]byte
}

// config options for the service.
Expand Down Expand Up @@ -215,6 +217,7 @@ func (s *Service) Start() {
}
s.spawnProcessAttestationsRoutine()
go s.runLateBlockTasks()
go s.updateBlockWithInclusionListRoutine()
}

// Stop the blockchain service's main event loop and associated goroutines.
Expand Down
7 changes: 7 additions & 0 deletions beacon-chain/blockchain/testing/mock.go
Original file line number Diff line number Diff line change
Expand Up @@ -348,6 +348,13 @@ 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
1 change: 1 addition & 0 deletions beacon-chain/cache/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ go_library(
"common.go",
"doc.go",
"error.go",
"inclusion_list.go",
"interfaces.go",
"payload_id.go",
"proposer_indices.go",
Expand Down
104 changes: 104 additions & 0 deletions beacon-chain/cache/inclusion_list.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
package cache

import (
"crypto/sha256"
"sync"

"github.com/pkg/errors"
"github.com/prysmaticlabs/prysm/v5/consensus-types/primitives"
ethpb "github.com/prysmaticlabs/prysm/v5/proto/prysm/v1alpha1"
)

// InclusionLists maintains a thread-safe map of ValidatorIndex to generic types.
type InclusionLists[T any] struct {
mu sync.Mutex
ils map[primitives.ValidatorIndex]*T
}

// NewInclusionLists initializes a new InclusionLists instance.
func NewInclusionLists[T any]() *InclusionLists[T] {
return &InclusionLists[T]{
ils: make(map[primitives.ValidatorIndex]*T),
}
}

// Add adds or updates an entry in the map with a given InclusionList.
// It tracks if >1 inclusion list is seen for a given validator index by converting the value to a Slot type.
func (c *InclusionLists[T]) Add(il *ethpb.InclusionList) error {
c.mu.Lock()
defer c.mu.Unlock()

c.prune(il.Slot)

if current, exists := c.ils[il.ValidatorIndex]; exists {
if existingIL, ok := any(current).(*ethpb.InclusionList); ok && existingIL.Slot == il.Slot {
if slot, ok := any(il.Slot).(T); ok {
c.ils[il.ValidatorIndex] = &slot
return nil
}
return errors.New("type mismatch: expected Slot type")
}
}

if value, ok := any(il).(T); ok {
c.ils[il.ValidatorIndex] = &value
return nil
}
return errors.New("type mismatch: expected InclusionList type")
}

// Get retrieves all unique transactions for the given slot.
func (c *InclusionLists[T]) Get(slot primitives.Slot) [][]byte {
c.mu.Lock()
defer c.mu.Unlock()

c.prune(slot)

seen := make(map[[32]byte]struct{})
var txs [][]byte

for _, entry := range c.ils {
if il, ok := any(entry).(*ethpb.InclusionList); ok && il != nil {
for _, tx := range il.Transactions {
hash := sha256.Sum256(tx)
if _, found := seen[hash]; found {
continue
}
seen[hash] = struct{}{}
txs = append(txs, tx)
}
}
}

return txs
}

// SeenTwice checks if a validator index is associated with a Slot type value.
func (c *InclusionLists[T]) SeenTwice(idx primitives.ValidatorIndex) bool {
c.mu.Lock()
defer c.mu.Unlock()

entry, exists := c.ils[idx]
if !exists {
return false
}

_, isSlot := any(entry).(primitives.Slot)
return isSlot
}

// prune removes outdated entries from the map based on the slot.
func (c *InclusionLists[T]) prune(slot primitives.Slot) {
for idx, entry := range c.ils {
switch v := any(entry).(type) {
case *ethpb.InclusionList:
if v != nil && v.Slot+2 <= slot {
delete(c.ils, idx)
}
case primitives.Slot:
if v+2 <= slot {
delete(c.ils, idx)
}
}
}
}
2 changes: 2 additions & 0 deletions beacon-chain/core/helpers/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ go_library(
"beacon_committee.go",
"block.go",
"genesis.go",
"inclusion_list.go",
"metrics.go",
"randao.go",
"rewards_penalties.go",
Expand All @@ -20,6 +21,7 @@ go_library(
visibility = ["//visibility:public"],
deps = [
"//beacon-chain/cache:go_default_library",
"//beacon-chain/core/signing:go_default_library",
"//beacon-chain/core/time:go_default_library",
"//beacon-chain/forkchoice/types:go_default_library",
"//beacon-chain/state:go_default_library",
Expand Down
23 changes: 20 additions & 3 deletions beacon-chain/core/helpers/beacon_committee.go
Original file line number Diff line number Diff line change
Expand Up @@ -210,9 +210,10 @@ func BeaconCommittee(

// CommitteeAssignment represents committee list, committee index, and to be attested slot for a given epoch.
type CommitteeAssignment struct {
Committee []primitives.ValidatorIndex
AttesterSlot primitives.Slot
CommitteeIndex primitives.CommitteeIndex
Committee []primitives.ValidatorIndex
AttesterSlot primitives.Slot
CommitteeIndex primitives.CommitteeIndex
InclusionListCommitteeSlot primitives.Slot
}

// verifyAssignmentEpoch verifies if the given epoch is valid for assignment based on the provided state.
Expand Down Expand Up @@ -322,6 +323,22 @@ func CommitteeAssignments(ctx context.Context, state state.BeaconState, epoch pr
assignments[vIndex].CommitteeIndex = primitives.CommitteeIndex(j)
}
}
if state.Version() >= version.FOCIL {
// Retrieve inclusion list committee assignments for the slot and update the assignments map.
indices, err := GetInclusionListCommittee(ctx, state, slot)
if err != nil {
return nil, errors.Wrap(err, "could not get inclusion list committee")
}
for _, vIndex := range indices {
if _, exists := vals[vIndex]; !exists {
continue
}
if _, exists := assignments[vIndex]; !exists {
assignments[vIndex] = &CommitteeAssignment{}
}
assignments[vIndex].InclusionListCommitteeSlot = slot
}
}
}
return assignments, nil
}
Expand Down
Loading

0 comments on commit 2671fcf

Please sign in to comment.