package blockchain

import (
	"bytes"
	"context"
	"fmt"
	"time"

	"github.com/OffchainLabs/prysm/v7/beacon-chain/core/electra"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed"
	statefeed "github.com/OffchainLabs/prysm/v7/beacon-chain/core/feed/state"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/core/helpers"
	coreTime "github.com/OffchainLabs/prysm/v7/beacon-chain/core/time"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/core/transition"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/das"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/slasher/types"
	"github.com/OffchainLabs/prysm/v7/beacon-chain/state"
	"github.com/OffchainLabs/prysm/v7/config/features"
	"github.com/OffchainLabs/prysm/v7/consensus-types/blocks"
	"github.com/OffchainLabs/prysm/v7/consensus-types/interfaces"
	"github.com/OffchainLabs/prysm/v7/consensus-types/primitives"
	"github.com/OffchainLabs/prysm/v7/encoding/bytesutil"
	"github.com/OffchainLabs/prysm/v7/monitoring/tracing"
	"github.com/OffchainLabs/prysm/v7/monitoring/tracing/trace"
	ethpbv1 "github.com/OffchainLabs/prysm/v7/proto/eth/v1"
	ethpb "github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1"
	"github.com/OffchainLabs/prysm/v7/proto/prysm/v1alpha1/attestation"
	"github.com/OffchainLabs/prysm/v7/runtime/version"
	"github.com/OffchainLabs/prysm/v7/time/slots"
	"github.com/pkg/errors"
	"golang.org/x/sync/errgroup"
)

// This defines how many epochs since finality the run time will begin to save hot state on to the DB.
var epochsSinceFinalitySaveHotStateDB = primitives.Epoch(100)

// This defines how many epochs since finality the run time will begin to expand our respective cache sizes.
var epochsSinceFinalityExpandCache = primitives.Epoch(4)

// BlockReceiver interface defines the methods of chain service for receiving and processing new blocks.
type BlockReceiver interface {
	ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [32]byte, avs das.AvailabilityStore) error
	ReceiveBlockBatch(ctx context.Context, blocks []blocks.ROBlock, avs das.AvailabilityStore) error
	HasBlock(ctx context.Context, root [32]byte) bool
	RecentBlockSlot(root [32]byte) (primitives.Slot, error)
	BlockBeingSynced([32]byte) bool
}

// BlobReceiver interface defines the methods of chain service for receiving new
// blobs
type BlobReceiver interface {
	ReceiveBlob(context.Context, blocks.VerifiedROBlob) error
}

// DataColumnReceiver interface defines the methods of chain service for receiving new
// data columns
type DataColumnReceiver interface {
	ReceiveDataColumn(blocks.VerifiedRODataColumn) error
	ReceiveDataColumns([]blocks.VerifiedRODataColumn) error
}

// SlashingReceiver interface defines the methods of chain service for receiving validated slashing over the wire.
type SlashingReceiver interface {
	ReceiveAttesterSlashing(ctx context.Context, slashing ethpb.AttSlashing)
}

// ReceiveBlock is a function that defines the operations (minus pubsub)
// that are performed on a received block. The operations consist of:
//  1. Validate block, apply state transition and update checkpoints
//  2. Apply fork choice to the processed block
//  3. Save latest head info
func (s *Service) ReceiveBlock(ctx context.Context, block interfaces.ReadOnlySignedBeaconBlock, blockRoot [32]byte, avs das.AvailabilityStore) error {
	ctx, span := trace.StartSpan(ctx, "blockChain.ReceiveBlock")
	defer span.End()
	// Return early if the block is blacklisted
	if features.BlacklistedBlock(blockRoot) {
		return errBlacklistedRoot
	}
	// Return early if the block has been synced
	if s.InForkchoice(blockRoot) {
		log.WithField("blockRoot", fmt.Sprintf("%#x", blockRoot)).Debug("Ignoring already synced block")
		return nil
	}

	receivedTime := time.Now()
	err := s.blockBeingSynced.set(blockRoot)
	if errors.Is(err, errBlockBeingSynced) {
		log.WithField("blockRoot", fmt.Sprintf("%#x", blockRoot)).Debug("Ignoring block currently being synced")
		return nil
	}
	defer s.blockBeingSynced.unset(blockRoot)

	blockCopy, err := block.Copy()
	if err != nil {
		return errors.Wrap(err, "block copy")
	}

	preState, err := s.getBlockPreState(ctx, blockCopy.Block())
	if err != nil {
		return errors.Wrap(err, "could not get block's prestate")
	}

	currentCheckpoints := s.saveCurrentCheckpoints(preState)
	roblock, err := blocks.NewROBlockWithRoot(blockCopy, blockRoot)
	if err != nil {
		return errors.Wrap(err, "new ro block with root")
	}

	postState, isValidPayload, err := s.validateExecutionAndConsensus(ctx, preState, roblock)
	if err != nil {
		return errors.Wrap(err, "validator execution and consensus")
	}

	daWaitedTime, err := s.handleDA(ctx, avs, roblock)
	if err != nil {
		return errors.Wrap(err, "handle da")
	}

	// Defragment the state before continuing block processing.
	s.defragmentState(postState)

	// The rest of block processing takes a lock on forkchoice.
	s.cfg.ForkChoiceStore.Lock()
	defer s.cfg.ForkChoiceStore.Unlock()
	if err := s.savePostStateInfo(ctx, blockRoot, blockCopy, postState); err != nil {
		return errors.Wrap(err, "could not save post state info")
	}
	args := &postBlockProcessConfig{
		ctx:            ctx,
		roblock:        roblock,
		postState:      postState,
		isValidPayload: isValidPayload,
	}
	if err := s.postBlockProcess(args); err != nil {
		err := errors.Wrap(err, "could not process block")
		tracing.AnnotateError(span, err)
		return errors.Wrap(err, "post block process")
	}
	if err := s.updateCheckpoints(ctx, currentCheckpoints, preState, postState, blockRoot); err != nil {
		return errors.Wrap(err, "update checkpoints")
	}
	// If slasher is configured, forward the attestations in the block via an event feed for processing.
	if s.slasherEnabled {
		go s.sendBlockAttestationsToSlasher(blockCopy, preState)
	}

	// Handle post block operations such as pruning exits and bls messages if incoming block is the head
	if err := s.prunePostBlockOperationPools(ctx, blockCopy, blockRoot); err != nil {
		log.WithError(err).Error("Could not prune canonical objects from pool ")
	}

	// Have we been finalizing? Should we start saving hot states to db?
	if err := s.checkSaveHotStateDB(ctx); err != nil {
		return errors.Wrap(err, "check save hot state db")
	}

	// We apply the same heuristic to some of our more important caches.
	if err := s.handleCaches(); err != nil {
		return errors.Wrap(err, "handle caches")
	}
	s.reportPostBlockProcessing(blockCopy, blockRoot, receivedTime, daWaitedTime)
	return nil
}

type ffgCheckpoints struct {
	j, f, c primitives.Epoch
}

func (s *Service) saveCurrentCheckpoints(state state.BeaconState) (cp ffgCheckpoints) {
	// Save current justified and finalized epochs for future use.
	cp.j = s.CurrentJustifiedCheckpt().Epoch
	cp.f = s.FinalizedCheckpt().Epoch
	cp.c = coreTime.CurrentEpoch(state)
	return
}

func (s *Service) updateCheckpoints(
	ctx context.Context,
	cp ffgCheckpoints,
	preState, postState state.BeaconState,
	blockRoot [32]byte,
) error {
	if coreTime.CurrentEpoch(postState) > cp.c && s.cfg.ForkChoiceStore.IsCanonical(blockRoot) {
		headSt, err := s.HeadState(ctx)
		if err != nil {
			return errors.Wrap(err, "could not get head state")
		}
		if err := reportEpochMetrics(ctx, postState, headSt); err != nil {
			log.WithError(err).Error("Could not report epoch metrics")
		}
	}
	if err := s.updateJustificationOnBlock(ctx, preState, postState, cp.j); err != nil {
		return errors.Wrap(err, "could not update justified checkpoint")
	}

	newFinalized, err := s.updateFinalizationOnBlock(ctx, preState, postState, cp.f)
	if err != nil {
		return errors.Wrap(err, "could not update finalized checkpoint")
	}
	// Send finalized events and finalized deposits in the background
	if newFinalized {
		// hook to process all post state finalization tasks
		s.executePostFinalizationTasks(ctx, postState)
	}
	return nil
}

func (s *Service) validateExecutionAndConsensus(
	ctx context.Context,
	preState state.BeaconState,
	block blocks.ROBlock,
) (state.BeaconState, bool, error) {
	preStateVersion, preStateHeader, err := getStateVersionAndPayload(preState)
	if err != nil {
		return nil, false, err
	}
	eg, _ := errgroup.WithContext(ctx)
	var postState state.BeaconState
	eg.Go(func() error {
		var err error
		postState, err = s.validateStateTransition(ctx, preState, block)
		if errors.Is(err, ErrNotDescendantOfFinalized) {
			return invalidBlock{error: err, root: block.Root()}
		}
		if err != nil {
			return errors.Wrap(err, "failed to validate consensus state transition function")
		}
		return nil
	})
	var isValidPayload bool
	eg.Go(func() error {
		var err error
		isValidPayload, err = s.validateExecutionOnBlock(ctx, preStateVersion, preStateHeader, block)
		if err != nil {
			return errors.Wrap(err, "could not notify the engine of the new payload")
		}
		return nil
	})
	if err := eg.Wait(); err != nil {
		return nil, false, err
	}
	return postState, isValidPayload, nil
}

func (s *Service) handleDA(ctx context.Context, avs das.AvailabilityStore, block blocks.ROBlock) (time.Duration, error) {
	var err error
	start := time.Now()
	if avs != nil {
		err = avs.IsDataAvailable(ctx, s.CurrentSlot(), block)
	} else {
		err = s.isDataAvailable(ctx, block)
	}
	elapsed := time.Since(start)
	if err == nil {
		dataAvailWaitedTime.Observe(float64(elapsed.Milliseconds()))
	}
	return elapsed, err
}

func (s *Service) reportPostBlockProcessing(
	block interfaces.SignedBeaconBlock,
	blockRoot [32]byte,
	receivedTime time.Time,
	daWaitedTime time.Duration,
) {
	// Reports on block and fork choice metrics.
	cp := s.cfg.ForkChoiceStore.FinalizedCheckpoint()
	finalized := &ethpb.Checkpoint{Epoch: cp.Epoch, Root: bytesutil.SafeCopyBytes(cp.Root[:])}
	reportSlotMetrics(block.Block().Slot(), s.HeadSlot(), s.CurrentSlot(), finalized)

	// Log block sync status.
	cp = s.cfg.ForkChoiceStore.JustifiedCheckpoint()
	justified := &ethpb.Checkpoint{Epoch: cp.Epoch, Root: bytesutil.SafeCopyBytes(cp.Root[:])}
	if err := logBlockSyncStatus(block.Block(), blockRoot, justified, finalized, receivedTime, s.genesisTime, daWaitedTime); err != nil {
		log.WithError(err).Error("Unable to log block sync status")
	}
	// Log payload data
	if err := logPayload(block.Block()); err != nil {
		log.WithError(err).Error("Unable to log debug block payload data")
	}
	// Log state transition data.
	if err := logStateTransitionData(block.Block()); err != nil {
		log.WithError(err).Error("Unable to log state transition data")
	}
	timeWithoutDaWait := time.Since(receivedTime) - daWaitedTime
	chainServiceProcessingTime.Observe(float64(timeWithoutDaWait.Milliseconds()))
}

func (s *Service) executePostFinalizationTasks(ctx context.Context, finalizedState state.BeaconState) {
	finalized := s.cfg.ForkChoiceStore.FinalizedCheckpoint()

	// Send finalization event
	go func() {
		s.sendNewFinalizedEvent(ctx, finalizedState)
	}()

	// Insert finalized deposits into finalized deposit trie
	depCtx, cancel := context.WithTimeout(context.Background(), depositDeadline)
	go func() {
		s.insertFinalizedDepositsAndPrune(depCtx, finalized.Root)
		cancel()
	}()

	if features.Get().EnableLightClient {
		// Save a light client bootstrap for the finalized checkpoint
		go func() {
			st, err := s.cfg.StateGen.StateByRoot(ctx, finalized.Root)
			if err != nil {
				log.WithError(err).Error("Could not retrieve state for finalized root to save light client bootstrap")
				return
			}
			err = s.lcStore.SaveLightClientBootstrap(s.ctx, finalized.Root, st)
			if err != nil {
				log.WithError(err).Error("Could not save light client bootstrap by block root")
			} else {
				log.Debugf("Saved light client bootstrap for finalized root %#x", finalized.Root)
			}
		}()

		// Clean up the light client store caches
		go func() {
			err := s.lcStore.MigrateToCold(s.ctx, finalized.Root)
			if err != nil {
				log.WithError(err).Error("Could not migrate light client store to cold storage")
			} else {
				log.Debugf("Migrated light client store to cold storage for finalized root %#x", finalized.Root)
			}
		}()
	}
}

// ReceiveBlockBatch processes the whole block batch at once, assuming the block batch is linear ,transitioning
// the state, performing batch verification of all collected signatures and then performing the appropriate
// actions for a block post-transition.
func (s *Service) ReceiveBlockBatch(ctx context.Context, blocks []blocks.ROBlock, avs das.AvailabilityStore) error {
	ctx, span := trace.StartSpan(ctx, "blockChain.ReceiveBlockBatch")
	defer span.End()

	s.cfg.ForkChoiceStore.Lock()
	defer s.cfg.ForkChoiceStore.Unlock()

	// Apply state transition on the incoming newly received block batches, one by one.
	if err := s.onBlockBatch(ctx, blocks, avs); err != nil {
		err := errors.Wrap(err, "could not process block in batch")
		tracing.AnnotateError(span, err)
		return err
	}

	lastBR := blocks[len(blocks)-1].Root()
	optimistic, err := s.cfg.ForkChoiceStore.IsOptimistic(lastBR)
	if err != nil {
		lastSlot := blocks[len(blocks)-1].Block().Slot()
		log.WithError(err).Errorf("Could not check if block is optimistic, Root: %#x, Slot: %d", lastBR, lastSlot)
		optimistic = true
	}

	for _, b := range blocks {
		blockCopy, err := b.Copy()
		if err != nil {
			return err
		}
		// Send notification of the processed block to the state feed.
		s.cfg.StateNotifier.StateFeed().Send(&feed.Event{
			Type: statefeed.BlockProcessed,
			Data: &statefeed.BlockProcessedData{
				Slot:        blockCopy.Block().Slot(),
				BlockRoot:   b.Root(),
				SignedBlock: blockCopy,
				Verified:    true,
				Optimistic:  optimistic,
			},
		})

		// Reports on blockCopy and fork choice metrics.
		cp := s.cfg.ForkChoiceStore.FinalizedCheckpoint()
		finalized := &ethpb.Checkpoint{Epoch: cp.Epoch, Root: bytesutil.SafeCopyBytes(cp.Root[:])}
		reportSlotMetrics(blockCopy.Block().Slot(), s.HeadSlot(), s.CurrentSlot(), finalized)
	}

	if err := s.cfg.BeaconDB.SaveBlocks(ctx, s.getInitSyncBlocks()); err != nil {
		return err
	}
	finalized := s.cfg.ForkChoiceStore.FinalizedCheckpoint()
	if finalized == nil {
		return errNilFinalizedInStore
	}
	if err := s.wsVerifier.VerifyWeakSubjectivity(s.ctx, finalized.Epoch); err != nil {
		// log.Fatalf will prevent defer from being called
		span.End()
		// Exit run time if the node failed to verify weak subjectivity checkpoint.
		log.WithError(err).Fatal("Could not verify weak subjectivity checkpoint")
	}

	return nil
}

// HasBlock returns true if the block of the input root exists in initial sync blocks cache or DB.
func (s *Service) HasBlock(ctx context.Context, root [32]byte) bool {
	if s.BlockBeingSynced(root) {
		return false
	}
	return s.hasBlockInInitSyncOrDB(ctx, root)
}

// ReceiveAttesterSlashing receives an attester slashing and inserts it to forkchoice
func (s *Service) ReceiveAttesterSlashing(ctx context.Context, slashing ethpb.AttSlashing) {
	s.cfg.ForkChoiceStore.Lock()
	defer s.cfg.ForkChoiceStore.Unlock()
	s.InsertSlashingsToForkChoiceStore(ctx, []ethpb.AttSlashing{slashing})
}

// prunePostBlockOperationPools only runs on new head otherwise should return a nil.
func (s *Service) prunePostBlockOperationPools(ctx context.Context, blk interfaces.ReadOnlySignedBeaconBlock, root [32]byte) error {
	headRoot, err := s.HeadRoot(ctx)
	if err != nil {
		return err
	}
	// By comparing the current headroot, that has already gone through forkchoice,
	// we can assume that if equal the current block root is canonical.
	if !bytes.Equal(headRoot, root[:]) {
		return nil
	}

	// Mark block exits as seen so we don't include same ones in future blocks.
	for _, e := range blk.Block().Body().VoluntaryExits() {
		s.cfg.ExitPool.MarkIncluded(e)
	}

	// Mark block BLS changes as seen so we don't include same ones in future blocks.
	if err := s.markIncludedBlockBLSToExecChanges(blk.Block()); err != nil {
		return errors.Wrap(err, "could not process BLSToExecutionChanges")
	}

	// Mark slashings as seen so we don't include same ones in future blocks.
	for _, as := range blk.Block().Body().AttesterSlashings() {
		s.cfg.SlashingPool.MarkIncludedAttesterSlashing(as)
	}
	for _, ps := range blk.Block().Body().ProposerSlashings() {
		s.cfg.SlashingPool.MarkIncludedProposerSlashing(ps)
	}

	return nil
}

func (s *Service) markIncludedBlockBLSToExecChanges(headBlock interfaces.ReadOnlyBeaconBlock) error {
	if headBlock.Version() < version.Capella {
		return nil
	}
	changes, err := headBlock.Body().BLSToExecutionChanges()
	if err != nil {
		return errors.Wrap(err, "could not get BLSToExecutionChanges")
	}
	for _, change := range changes {
		s.cfg.BLSToExecPool.MarkIncluded(change)
	}
	return nil
}

// This checks whether it's time to start saving hot state to DB.
// It's time when there's `epochsSinceFinalitySaveHotStateDB` epochs of non-finality.
// Requires a read lock on forkchoice
func (s *Service) checkSaveHotStateDB(ctx context.Context) error {
	currentEpoch := slots.ToEpoch(s.CurrentSlot())
	// Prevent `sinceFinality` going underflow.
	var sinceFinality primitives.Epoch
	finalized := s.cfg.ForkChoiceStore.FinalizedCheckpoint()
	if finalized == nil {
		return errNilFinalizedInStore
	}
	if currentEpoch > finalized.Epoch {
		sinceFinality = currentEpoch - finalized.Epoch
	}

	if sinceFinality >= epochsSinceFinalitySaveHotStateDB {
		s.cfg.StateGen.EnableSaveHotStateToDB(ctx)
		return nil
	}

	return s.cfg.StateGen.DisableSaveHotStateToDB(ctx)
}

func (s *Service) handleCaches() error {
	currentEpoch := slots.ToEpoch(s.CurrentSlot())
	// Prevent `sinceFinality` going underflow.
	var sinceFinality primitives.Epoch
	finalized := s.cfg.ForkChoiceStore.FinalizedCheckpoint()
	if finalized == nil {
		return errNilFinalizedInStore
	}
	if currentEpoch > finalized.Epoch {
		sinceFinality = currentEpoch - finalized.Epoch
	}

	if sinceFinality >= epochsSinceFinalityExpandCache {
		helpers.ExpandCommitteeCache()
		return nil
	}

	helpers.CompressCommitteeCache()
	return nil
}

// This performs the state transition function and returns the poststate or an
// error if the block fails to verify the consensus rules
func (s *Service) validateStateTransition(ctx context.Context, preState state.BeaconState, signed interfaces.ReadOnlySignedBeaconBlock) (state.BeaconState, error) {
	b := signed.Block()
	// Verify that the parent block is in forkchoice
	parentRoot := b.ParentRoot()
	if !s.InForkchoice(parentRoot) {
		return nil, ErrNotDescendantOfFinalized
	}
	stateTransitionStartTime := time.Now()
	postState, err := transition.ExecuteStateTransition(ctx, preState, signed)
	if err != nil {
		if ctx.Err() != nil || electra.IsExecutionRequestError(err) {
			return nil, err
		}
		return nil, invalidBlock{error: err}
	}
	stateTransitionProcessingTime.Observe(float64(time.Since(stateTransitionStartTime).Milliseconds()))
	return postState, nil
}

// updateJustificationOnBlock updates the justified checkpoint on DB if the
// incoming block has updated it on forkchoice.
func (s *Service) updateJustificationOnBlock(ctx context.Context, preState, postState state.BeaconState, preJustifiedEpoch primitives.Epoch) error {
	justified := s.cfg.ForkChoiceStore.JustifiedCheckpoint()
	preStateJustifiedEpoch := preState.CurrentJustifiedCheckpoint().Epoch
	postStateJustifiedEpoch := postState.CurrentJustifiedCheckpoint().Epoch
	if justified.Epoch > preJustifiedEpoch || (justified.Epoch == postStateJustifiedEpoch && justified.Epoch > preStateJustifiedEpoch) {
		if err := s.cfg.BeaconDB.SaveJustifiedCheckpoint(ctx, &ethpb.Checkpoint{
			Epoch: justified.Epoch, Root: justified.Root[:],
		}); err != nil {
			return err
		}
	}
	return nil
}

// updateFinalizationOnBlock performs some duties when the incoming block
// changes the finalized checkpoint. It returns true when this has happened.
func (s *Service) updateFinalizationOnBlock(ctx context.Context, preState, postState state.BeaconState, preFinalizedEpoch primitives.Epoch) (bool, error) {
	preStateFinalizedEpoch := preState.FinalizedCheckpoint().Epoch
	postStateFinalizedEpoch := postState.FinalizedCheckpoint().Epoch
	finalized := s.cfg.ForkChoiceStore.FinalizedCheckpoint()
	if finalized.Epoch > preFinalizedEpoch || (finalized.Epoch == postStateFinalizedEpoch && finalized.Epoch > preStateFinalizedEpoch) {
		if err := s.updateFinalized(ctx, &ethpb.Checkpoint{Epoch: finalized.Epoch, Root: finalized.Root[:]}); err != nil {
			return true, err
		}
		return true, nil
	}
	return false, nil
}

// sendNewFinalizedEvent sends a new finalization checkpoint event over the
// event feed. It needs to be called on the background
func (s *Service) sendNewFinalizedEvent(ctx context.Context, postState state.BeaconState) {
	isValidPayload := false
	s.headLock.RLock()
	if s.head != nil {
		isValidPayload = s.head.optimistic
	}
	s.headLock.RUnlock()

	blk, err := s.cfg.BeaconDB.Block(ctx, bytesutil.ToBytes32(postState.FinalizedCheckpoint().Root))
	if err != nil {
		log.WithError(err).Error("Could not retrieve block for finalized checkpoint root. Finalized event will not be emitted")
		return
	}
	if blk == nil || blk.IsNil() || blk.Block() == nil || blk.Block().IsNil() {
		log.WithError(err).Error("Block retrieved for finalized checkpoint root is nil. Finalized event will not be emitted")
		return
	}
	stateRoot := blk.Block().StateRoot()
	// Send an event regarding the new finalized checkpoint over a common event feed.
	s.cfg.StateNotifier.StateFeed().Send(&feed.Event{
		Type: statefeed.FinalizedCheckpoint,
		Data: &ethpbv1.EventFinalizedCheckpoint{
			Epoch:               postState.FinalizedCheckpoint().Epoch,
			Block:               postState.FinalizedCheckpoint().Root,
			State:               stateRoot[:],
			ExecutionOptimistic: isValidPayload,
		},
	})
}

// sendBlockAttestationsToSlasher sends the incoming block's attestation to the slasher
func (s *Service) sendBlockAttestationsToSlasher(signed interfaces.ReadOnlySignedBeaconBlock, preState state.BeaconState) {
	// Feed the indexed attestation to slasher if enabled. This action
	// is done in the background to avoid adding more load to this critical code path.
	ctx := s.ctx
	for _, att := range signed.Block().Body().Attestations() {
		committees, err := helpers.AttestationCommitteesFromState(ctx, preState, att)
		if err != nil {
			log.WithError(err).Error("Could not get attestation committees")
			continue
		}
		indexedAtt, err := attestation.ConvertToIndexed(ctx, att, committees...)
		if err != nil {
			log.WithError(err).Error("Could not convert to indexed attestation")
			continue
		}
		s.cfg.SlasherAttestationsFeed.Send(&types.WrappedIndexedAtt{IndexedAtt: indexedAtt})
	}
}

// validateExecutionOnBlock notifies the engine of the incoming block execution payload and returns true if the payload is valid
func (s *Service) validateExecutionOnBlock(ctx context.Context, ver int, header interfaces.ExecutionData, block blocks.ROBlock) (bool, error) {
	isValidPayload, err := s.notifyNewPayload(ctx, ver, header, block)
	if err != nil {
		s.cfg.ForkChoiceStore.Lock()
		err = s.handleInvalidExecutionError(ctx, err, block.Root(), block.Block().ParentRoot())
		s.cfg.ForkChoiceStore.Unlock()
		return false, err
	}
	if block.Block().Version() < version.Capella && isValidPayload {
		if err := s.validateMergeTransitionBlock(ctx, ver, header, block); err != nil {
			return isValidPayload, err
		}
	}
	return isValidPayload, nil
}
