// Copyright 2024 The Erigon Authors
// This file is part of Erigon.
//
// Erigon is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// Erigon is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with Erigon. If not, see <http://www.gnu.org/licenses/>.

package stageloop

import (
	"context"
	"errors"
	"fmt"
	"runtime"
	"time"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/datadir"
	"github.com/erigontech/erigon/db/downloader"
	"github.com/erigontech/erigon/db/kv"
	"github.com/erigontech/erigon/db/kv/membatchwithdb"
	"github.com/erigontech/erigon/db/kv/rawdbv3"
	"github.com/erigontech/erigon/db/rawdb"
	"github.com/erigontech/erigon/db/rawdb/blockio"
	"github.com/erigontech/erigon/db/services"
	"github.com/erigontech/erigon/db/state/execctx"
	"github.com/erigontech/erigon/execution/chain"
	"github.com/erigontech/erigon/execution/engineapi/engine_helpers"
	"github.com/erigontech/erigon/execution/metrics"
	execp2p "github.com/erigontech/erigon/execution/p2p"
	"github.com/erigontech/erigon/execution/protocol/misc"
	"github.com/erigontech/erigon/execution/protocol/rules"
	"github.com/erigontech/erigon/execution/stagedsync"
	"github.com/erigontech/erigon/execution/stagedsync/headerdownload"
	"github.com/erigontech/erigon/execution/stagedsync/stages"
	"github.com/erigontech/erigon/execution/tracing"
	"github.com/erigontech/erigon/execution/tracing/tracers"
	"github.com/erigontech/erigon/execution/types"
	"github.com/erigontech/erigon/execution/vm"
	"github.com/erigontech/erigon/node/ethconfig"
	"github.com/erigontech/erigon/node/gointerfaces"
	"github.com/erigontech/erigon/node/shards"
	"github.com/erigontech/erigon/node/silkworm"
	"github.com/erigontech/erigon/p2p"
	"github.com/erigontech/erigon/p2p/protocols/eth"
	"github.com/erigontech/erigon/p2p/sentry/sentry_multi_client"
)

var inMemHistoryReads = dbg.EnvBool("ERIGON_IN_MEM_HISTORY", true)

// StageLoop runs the continuous loop of staged sync
func StageLoop(
	ctx context.Context,
	db kv.TemporalRwDB,
	sync *stagedsync.Sync,
	hd *headerdownload.HeaderDownload,
	waitForDone chan struct{},
	loopMinTime time.Duration,
	logger log.Logger,
	blockReader services.FullBlockReader,
	hook *Hook,
) {
	defer close(waitForDone)

	if err := ProcessFrozenBlocks(ctx, db, blockReader, sync, hook, false /* onlySnapDownload */, logger); err != nil {
		if errors.Is(err, common.ErrStopped) || errors.Is(err, context.Canceled) {
			return
		}

		logger.Error("Staged Sync", "err", err)
		if recoveryErr := hd.RecoverFromDb(db); recoveryErr != nil {
			logger.Error("Failed to recover header sentriesClient", "err", recoveryErr)
		}
	}

	logger.Debug("[stageloop] Starting iteration")
	initialCycle := true
	for {
		start := time.Now()
		select {
		case <-hd.ShutdownCh:
			return
		default:
			// continue
		}
		hook.LastNewBlockSeen(hd.Progress())
		t := time.Now()
		err := StageLoopIteration(ctx, db, sync, initialCycle, false, logger, blockReader, hook)
		if err != nil {
			if errors.Is(err, common.ErrStopped) || errors.Is(err, context.Canceled) {
				return
			}
			logger.Error("Staged Sync", "err", err)
			if recoveryErr := hd.RecoverFromDb(db); recoveryErr != nil {
				logger.Error("Failed to recover header sentriesClient", "err", recoveryErr)
			}
			time.Sleep(500 * time.Millisecond) // just to avoid too many similar error logs
			continue
		}
		if time.Since(t) < 5*time.Minute {
			initialCycle = false
		}
		if !initialCycle {
			hd.AfterInitialCycle()
		}
		if loopMinTime != 0 {
			waitTime := loopMinTime - time.Since(start)
			logger.Info("Wait time until next loop", "for", waitTime)
			c := time.After(waitTime)
			select {
			case <-ctx.Done():
				return
			case <-c:
			}
		}
	}
}

func ProcessFrozenBlocks(ctx context.Context, db kv.TemporalRwDB, blockReader services.FullBlockReader, sync *stagedsync.Sync, hook *Hook, onlySnapDownload bool, logger log.Logger) error {
	sawZeroBlocksTimes := 0
	tx, err := db.BeginTemporalRw(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	// run stages first time - it will download blocks
	err = sync.RunSnapshots(nil, tx)
	if err != nil {
		return err
	}
	if onlySnapDownload {
		return nil
	}

	// after StageSnapshots (files downloading): if domains are ahead of block files, then nothing to execute.
	if execctx.IsDomainAheadOfBlocks(ctx, tx, logger) {
		return tx.Commit()
	}

	doms, err := execctx.NewSharedDomains(ctx, tx, logger)
	if err != nil {
		return err
	}
	defer doms.Close()
	doms.SetInMemHistoryReads(inMemHistoryReads)

	var finishStageBeforeSync uint64
	if hook != nil {
		finishStageBeforeSync, err = stages.GetStageProgress(tx, stages.Finish)
		if err != nil {
			return err
		}
		if err = hook.BeforeRun(tx, false); err != nil {
			return err
		}
	}
	initialCycle, firstCycle := true, false
	for more := true; more; {
		more, err = sync.Run(doms, tx, initialCycle, firstCycle)
		if err != nil {
			return fmt.Errorf("ProcessFrozenBlocks: %w", err)
		}

		if err := sync.RunPrune(ctx, tx, initialCycle, 0); err != nil {
			return fmt.Errorf("ProcessFrozenBlocks: %w", err)
		}

		var finStageProgress uint64
		if blockReader.FrozenBlocks() > 0 {
			finStageProgress, err = stages.GetStageProgress(tx, stages.Finish)
			if err != nil {
				return err
			}
			if finStageProgress >= blockReader.FrozenBlocks() {
				break
			}
		} else {
			// having 0 frozen blocks - also may mean we didn't download them. so stages. 1 time is enough.
			// during testing we may have 0 frozen blocks and firstCycle expected to be false
			sawZeroBlocksTimes++
			if sawZeroBlocksTimes > 2 {
				break
			}
		}

		if err := doms.Flush(ctx, tx); err != nil {
			return fmt.Errorf("ProcessFrozenBlocks: %w", err)
		}
		doms.ClearRam(true)
		if err := tx.Commit(); err != nil {
			return err
		}
		if tx, err = db.BeginTemporalRw(ctx); err != nil {
			return err
		}
	}

	doms.ClearRam(true)

	if err := tx.Commit(); err != nil {
		return err
	}

	if hook != nil {
		if err := db.View(ctx, func(tx kv.Tx) error {
			headersProgress, _, _, err := stagesHeadersAndFinish(tx)
			if err != nil {
				return err
			}
			err = hook.AfterRun(tx, finishStageBeforeSync, false)
			if err != nil {
				return err
			}
			hook.LastNewBlockSeen(headersProgress)
			return nil
		}); err != nil {
			return err
		}
	}
	return nil
}

func StageLoopIteration(ctx context.Context, db kv.TemporalRwDB, sync *stagedsync.Sync, initialCycle, firstCycle bool, logger log.Logger, blockReader services.FullBlockReader, hook *Hook) (err error) {
	// avoid crash because Erigon's core does many things
	defer dbg.RecoverPanicIntoError(logger, &err)
	hasMore := true
	for hasMore {
		err = db.UpdateTemporal(ctx, func(tx kv.TemporalRwTx) error {
			sd, err := execctx.NewSharedDomains(ctx, tx, logger)
			if err != nil {
				return err
			}
			defer sd.Close()
			sd.SetInMemHistoryReads(inMemHistoryReads)
			hasMore, err = stageLoopIteration(ctx, sd, tx, sync, initialCycle, firstCycle, logger, blockReader, hook)
			if err != nil {
				return err
			}
			return sd.Flush(ctx, tx)
		})
		if err != nil {
			return err
		}
	}
	return nil
}

func stageLoopIteration(ctx context.Context, sd *execctx.SharedDomains, tx kv.TemporalRwTx, sync *stagedsync.Sync, initialCycle, firstCycle bool, logger log.Logger, blockReader services.FullBlockReader, hook *Hook) (hasMore bool, err error) {
	finishProgressBefore, headersProgressBefore, gasUsed, err := stagesHeadersAndFinish(tx)
	if err != nil {
		return false, err
	}
	// Sync from scratch must be able Commit partial progress
	// In all other cases - process blocks batch in 1 RwTx
	// 2 corner-cases: when sync with --snapshots=false and when executed only blocks from snapshots (in this case all stages progress is equal and > 0, but node is not synced)
	isSynced := finishProgressBefore > 0 && finishProgressBefore > blockReader.FrozenBlocks() && finishProgressBefore == headersProgressBefore
	// Main steps:
	// - process new blocks
	// - commit(no_sync). NoSync - making data available for readers as-soon-as-possible. Can
	//       send notifications Now and do write to disks Later.
	// - Send Notifications: about new blocks, new receipts, state changes, etc...
	// - Prune(limited time)+Commit(sync). Write to disk happening here.
	if err = hook.BeforeRun(tx, isSynced); err != nil {
		return false, err
	}
	hasMore, err = sync.Run(sd, tx, initialCycle, firstCycle)
	if err != nil {
		return false, err
	}
	logCtx := sync.PrintTimings()
	// -- send notifications START
	if err = hook.AfterRun(tx, finishProgressBefore, isSynced); err != nil {
		return false, err
	}
	var m runtime.MemStats
	dbg.ReadMemStats(&m)
	if gasUsed > 0 {
		var mgasPerSec float64
		// this is a bit hacky
		for i, v := range logCtx {
			if stringVal, ok := v.(string); ok && stringVal == "Execution" {
				execTime := logCtx[i+1].(string)
				// convert from ..ms to duration
				execTimeDuration, err := time.ParseDuration(execTime)
				if err != nil {
					logger.Error("Failed to parse execution time", "err", err)
				} else {
					gasUsedMgas := float64(gasUsed) / 1e6
					mgasPerSec = gasUsedMgas / execTimeDuration.Seconds()
				}
			}
		}
		if mgasPerSec > 0 {
			metrics.ChainTipMgasPerSec.Add(mgasPerSec)
			logCtx = append(logCtx, "mgas/s", mgasPerSec)
		}
	}
	logCtx = append(logCtx, "alloc", common.ByteCount(m.Alloc), "sys", common.ByteCount(m.Sys))
	logger.Info("Timings", logCtx...)
	// -- send notifications END
	// -- Prune+commit(sync)
	err = sync.RunPrune(ctx, tx, initialCycle, 0)
	if err != nil {
		return false, err
	}
	return hasMore, nil
}

func stagesHeadersAndFinish(tx kv.Tx) (head, fin uint64, gasUsed uint64, err error) {
	if fin, err = stages.GetStageProgress(tx, stages.Finish); err != nil {
		return head, fin, gasUsed, err
	}
	if head, err = stages.GetStageProgress(tx, stages.Headers); err != nil {
		return head, fin, gasUsed, err
	}
	h := rawdb.ReadHeaderByNumber(tx, head)
	if h != nil {
		gasUsed = h.GasUsed
	}
	return head, fin, gasUsed, nil
}

type Hook struct {
	ctx                                 context.Context
	notifications                       *shards.Notifications
	sync                                *stagedsync.Sync
	chainConfig                         *chain.Config
	logger                              log.Logger
	updateHead                          func(ctx context.Context)
	statusDataGetter                    sentry_multi_client.StatusGetter
	blockRangePublisher                 *execp2p.Publisher
	lastAnnouncedBlockRangeLatestNumber uint64
	lastAnnouncedBlockRangeTime         time.Time
}

func NewHook(
	ctx context.Context,
	notifications *shards.Notifications,
	sync *stagedsync.Sync,
	chainConfig *chain.Config,
	logger log.Logger,
	updateHead func(ctx context.Context),
	statusDataGetter sentry_multi_client.StatusGetter,
	blockRangePublisher *execp2p.Publisher,
) *Hook {
	return &Hook{
		ctx:                 ctx,
		notifications:       notifications,
		sync:                sync,
		chainConfig:         chainConfig,
		logger:              logger,
		updateHead:          updateHead,
		statusDataGetter:    statusDataGetter,
		blockRangePublisher: blockRangePublisher,
	}
}

func (h *Hook) beforeRun(tx kv.Tx, inSync bool) error {
	notifications := h.notifications
	if notifications != nil && notifications.Accumulator != nil && inSync {
		stateVersion, err := rawdb.GetStateVersion(tx)
		if err != nil {
			h.logger.Error("problem reading plain state version", "err", err)
		}
		notifications.Accumulator.Reset(stateVersion)
	}
	return nil
}

func (h *Hook) LastNewBlockSeen(n uint64) {
	if h == nil || h.notifications == nil {
		return
	}
	h.notifications.NewLastBlockSeen(n)
}

func (h *Hook) BeforeRun(tx kv.Tx, inSync bool) error {
	if h == nil {
		return nil
	}
	return h.beforeRun(tx, inSync)
}

func (h *Hook) AfterRun(tx kv.Tx, finishProgressBefore uint64, isSynced bool) error {
	if h == nil {
		return nil
	}
	return h.afterRun(tx, finishProgressBefore, isSynced)
}

func (h *Hook) afterRun(tx kv.Tx, finishProgressBefore uint64, isSynced bool) error {
	// Update sentry status for peers to see our sync status
	if h.updateHead != nil {
		h.updateHead(h.ctx)
	}
	finishStageAfterSync, err := stages.GetStageProgress(tx, stages.Finish)
	if err != nil {
		return err
	}

	h.maybeAnnounceBlockRange(finishProgressBefore, finishStageAfterSync, isSynced)
	return h.sendNotifications(tx, finishProgressBefore, finishStageAfterSync)
}

func (h *Hook) sendNotifications(tx kv.Tx, finishStageBeforeSync, finishStageAfterSync uint64) error {
	if h.notifications == nil {
		return nil
	}

	var err error

	// update the accumulator with a new plain state version so the cache can be notified that
	// state has moved on
	if h.notifications.Accumulator != nil {
		plainStateVersion, err := rawdb.GetStateVersion(tx)
		if err != nil {
			return err
		}

		h.notifications.Accumulator.SetStateID(plainStateVersion)
	}

	if h.notifications.Events != nil {
		unwindTo := h.sync.PrevUnwindPoint()

		var notifyFrom uint64
		var isUnwind bool
		if unwindTo != nil && *unwindTo != 0 && (*unwindTo) < finishStageBeforeSync {
			notifyFrom = *unwindTo
			isUnwind = true
		} else {
			heightSpan := min(finishStageAfterSync-finishStageBeforeSync, 1024)
			notifyFrom = finishStageAfterSync - heightSpan
		}
		notifyFrom++
		notifyTo := finishStageAfterSync + 1 //[from, to)

		if err = stagedsync.NotifyNewHeaders(h.ctx, notifyFrom, notifyTo, h.notifications.Events, tx, h.logger); err != nil {
			return nil
		}
		h.notifications.RecentReceipts.NotifyReceipts(h.notifications.Events, notifyFrom, notifyTo, isUnwind)
		h.notifications.RecentReceipts.NotifyLogs(h.notifications.Events, notifyFrom, notifyTo, isUnwind)
	}

	currentHeader := rawdb.ReadCurrentHeader(tx)
	if h.notifications.Accumulator != nil && currentHeader != nil {
		if changes := h.notifications.Accumulator.Changes(); len(changes) == 0 || changes[len(changes)-1].BlockHeight < currentHeader.Number.Uint64() {
			h.notifications.Accumulator.StartChange(currentHeader, nil, false)
		}

		pendingBaseFee := misc.CalcBaseFee(h.chainConfig, currentHeader)
		pendingBlobFee := h.chainConfig.GetMinBlobGasPrice()
		if currentHeader.ExcessBlobGas != nil {
			nextBlockTime := currentHeader.Time + h.chainConfig.SecondsPerSlot()
			excessBlobGas := misc.CalcExcessBlobGas(h.chainConfig, currentHeader, nextBlockTime)
			f, err := misc.GetBlobGasPrice(h.chainConfig, excessBlobGas, nextBlockTime)
			if err != nil {
				return err
			}
			pendingBlobFee = f.Uint64()
		}

		var finalizedBlock uint64
		if fb := rawdb.ReadHeaderNumber(tx, rawdb.ReadForkchoiceFinalized(tx)); fb != nil {
			finalizedBlock = *fb
		}

		//h.logger.Debug("[hook] Sending state changes", "currentBlock", currentHeader.Number.Uint64(), "finalizedBlock", finalizedBlock)
		h.notifications.Accumulator.SendAndReset(h.ctx, h.notifications.StateChangesConsumer, pendingBaseFee.Uint64(), pendingBlobFee, currentHeader.GasLimit, finalizedBlock)
	}
	return nil
}

func (h *Hook) maybeAnnounceBlockRange(finishStageBeforeSync, finishStageAfterSync uint64, isSynced bool) {
	if h.blockRangePublisher == nil || h.statusDataGetter == nil {
		return
	}

	// we want to announce BlockRangeUpdate
	// - after initialization
	// - during sync: every 1 minute. A time limit is used to prevent excessive announcements in
	//									case of fast execution due to empty blocks or small SyncLoopBlockLimit
	// - after sync: every 32 blocks or on unwind of any depth
	hadUnwind := h.sync != nil && h.sync.PrevUnwindPoint() != nil && *h.sync.PrevUnwindPoint() < finishStageBeforeSync
	isInitialAnnouncement := h.lastAnnouncedBlockRangeLatestNumber == 0
	progressed := finishStageAfterSync >= h.lastAnnouncedBlockRangeLatestNumber+32
	timeElapsed := time.Since(h.lastAnnouncedBlockRangeTime)
	shouldPublishByTime := !isSynced && timeElapsed >= 1*time.Minute

	if !hadUnwind && !isInitialAnnouncement && !progressed && !shouldPublishByTime {
		return
	}

	status, err := h.statusDataGetter.GetStatusData(h.ctx)
	if err != nil {
		h.logger.Warn("[hook] block range update skipped; status data unavailable", "err", err)
		return
	}

	packet := eth.BlockRangeUpdatePacket{
		Earliest:   status.MinimumBlockHeight,
		Latest:     status.MaxBlockHeight,
		LatestHash: gointerfaces.ConvertH256ToHash(status.BestHash),
	}

	if err := packet.Validate(); err != nil {
		h.logger.Warn("[hook] block range update skipped", "err", err)
		return
	}

	h.logger.Debug("[hook] publishing block range update", "earliest", packet.Earliest, "latest", packet.Latest, "hadUnwind", hadUnwind, "isSynced", isSynced, "timeElapsed", timeElapsed)
	h.blockRangePublisher.PublishBlockRangeUpdate(packet)
	h.lastAnnouncedBlockRangeLatestNumber = packet.Latest
	h.lastAnnouncedBlockRangeTime = time.Now()
}

func MiningStep(ctx context.Context, db kv.TemporalRwDB, mining *stagedsync.Sync, tmpDir string, logger log.Logger) (err error) {
	defer func() {
		if rec := recover(); rec != nil {
			err = fmt.Errorf("%+v, trace: %s", rec, dbg.Stack())
		}
	}() // avoid crash because Erigon's core does many things
	tx, err := db.BeginTemporalRw(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()

	mb := membatchwithdb.NewMemoryBatch(tx, tmpDir, logger)
	defer mb.Close()

	sd, err := execctx.NewSharedDomains(ctx, mb, logger)
	if err != nil {
		return err
	}
	defer sd.Close()

	hasMore, err := mining.Run(sd, mb, false /* firstCycle */, false)
	if err != nil {
		return err
	}
	if hasMore {
		return errors.New("unexpected mining step has more work")
	}
	return nil
}

func addAndVerifyBlockStep(batch kv.RwTx, engine rules.Engine, chainReader rules.ChainReader, currentHeader *types.Header, currentBody *types.RawBody) error {
	currentHeight := currentHeader.Number.Uint64()
	currentHash := currentHeader.Hash()
	if chainReader != nil {
		if err := engine.VerifyHeader(chainReader, currentHeader, true); err != nil {
			log.Warn("Header Verification Failed", "number", currentHeight, "hash", currentHash, "reason", err)
			return fmt.Errorf("%w: %v", rules.ErrInvalidBlock, err)
		}
		if err := engine.VerifyUncles(chainReader, currentHeader, currentBody.Uncles); err != nil {
			log.Warn("Unlcles Verification Failed", "number", currentHeight, "hash", currentHash, "reason", err)
			return fmt.Errorf("%w: %v", rules.ErrInvalidBlock, err)
		}
	}
	// Prepare memory state for block execution
	if err := rawdb.WriteHeader(batch, currentHeader); err != nil {
		return err
	}
	prevHash, err := rawdb.ReadCanonicalHash(batch, currentHeight)
	if err != nil {
		return err
	}
	if err := rawdb.WriteCanonicalHash(batch, currentHash, currentHeight); err != nil {
		return err
	}
	if err := rawdb.WriteHeadHeaderHash(batch, currentHash); err != nil {
		return err
	}
	if _, err := rawdb.WriteRawBodyIfNotExists(batch, currentHash, currentHeight, currentBody); err != nil {
		return err
	}
	if prevHash != currentHash {
		if err := rawdbv3.TxNums.Truncate(batch, currentHeight); err != nil {
			return err
		}
		if err := rawdb.AppendCanonicalTxNums(batch, currentHeight); err != nil {
			return err
		}
	}

	if err := stages.SaveStageProgress(batch, stages.Headers, currentHeight); err != nil {
		return err
	}
	if err := stages.SaveStageProgress(batch, stages.Bodies, currentHeight); err != nil {
		return err
	}
	return nil
}

func cleanupProgressIfNeeded(batch kv.RwTx, header *types.Header) error {
	// If we fail state root then we have wrong execution stage progress set (+1), we need to decrease by one!
	progress, err := stages.GetStageProgress(batch, stages.Execution)
	if err != nil {
		return err
	}
	if progress == header.Number.Uint64() && progress > 0 {
		progress--
		if err := stages.SaveStageProgress(batch, stages.Execution, progress); err != nil {
			return err
		}
	}
	return nil
}

func StateStep(ctx context.Context, chainReader rules.ChainReader, engine rules.Engine, sd *execctx.SharedDomains, tx kv.TemporalRwTx, stateSync *stagedsync.Sync, unwindPoint uint64, headersChain []*types.Header, bodiesChain []*types.RawBody) (err error) {
	defer func() {
		if rec := recover(); rec != nil {
			err = fmt.Errorf("%+v, trace: %s", rec, dbg.Stack())
		}
	}() // avoid crash because Erigon's core does many things
	shouldUnwind := unwindPoint > 0
	// Construct side fork if we have one
	if shouldUnwind {
		// Run it through the unwind
		if err := stateSync.UnwindTo(unwindPoint, stagedsync.StagedUnwind, nil); err != nil {
			return err
		}
		if err = stateSync.RunUnwind(sd, tx); err != nil {
			return err
		}
	}
	lastNum := headersChain[len(headersChain)-1].Number.Uint64()
	if err := rawdb.TruncateCanonicalChain(ctx, tx, lastNum+1); err != nil {
		return err
	}
	// Once we unwound we can start constructing the chain (assumption: len(headersChain) == len(bodiesChain))
	for i := range headersChain {
		currentHeader := headersChain[i]
		currentBody := bodiesChain[i]
		if err := addAndVerifyBlockStep(tx, engine, chainReader, currentHeader, currentBody); err != nil {
			return err
		}
		// Run state sync
		hasMore, err := stateSync.RunNoInterrupt(sd, tx)
		if err != nil {
			// Don't lose the original error, like cancellation or common.ErrStopped.
			return errors.Join(err, cleanupProgressIfNeeded(tx, currentHeader))
		}
		if hasMore {
			// should not ever happen since we exec blocks 1 by 1
			if err := cleanupProgressIfNeeded(tx, currentHeader); err != nil {
				return err
			}
			return errors.New("unexpected state step has more work")
		}
	}
	return nil
}

func SilkwormForExecutionStage(silkworm *silkworm.Silkworm, cfg *ethconfig.Config) *silkworm.Silkworm {
	if cfg.SilkwormExecution {
		return silkworm
	}
	return nil
}

func NewDefaultStages(ctx context.Context,
	db kv.TemporalRwDB,
	p2pCfg p2p.Config,
	cfg *ethconfig.Config,
	controlServer *sentry_multi_client.MultiClient,
	notifications *shards.Notifications,
	snapDownloader downloader.Client,
	blockReader services.FullBlockReader,
	blockRetire services.BlockRetire,
	silkworm *silkworm.Silkworm,
	forkValidator *engine_helpers.ForkValidator,
	tracer *tracers.Tracer,
) []*stagedsync.Stage {
	var tracingHooks *tracing.Hooks
	if tracer != nil {
		tracingHooks = tracer.Hooks
	}
	dirs := cfg.Dirs
	blockWriter := blockio.NewBlockWriter()
	return stagedsync.DefaultStages(
		ctx,
		stagedsync.StageSnapshotsCfg(db, controlServer.ChainConfig, cfg.Sync, dirs, blockRetire, snapDownloader, blockReader, notifications, cfg.InternalCL && cfg.CaplinConfig.ArchiveBlocks, cfg.CaplinConfig.ArchiveBlobs, cfg.CaplinConfig.ArchiveStates, silkworm, cfg.Prune),
		stagedsync.StageHeadersCfg(controlServer.Hd, controlServer.ChainConfig, cfg.Sync, controlServer.SendHeaderRequest, controlServer.PropagateNewBlockHashes, controlServer.Penalize, p2pCfg.NoDiscovery, blockReader),
		stagedsync.StageBlockHashesCfg(dirs.Tmp, blockWriter),
		stagedsync.StageBodiesCfg(controlServer.Bd, controlServer.SendBodyRequest, controlServer.Penalize, controlServer.BroadcastNewBlock, cfg.Sync.BodyDownloadTimeoutSeconds, controlServer.ChainConfig, blockReader, blockWriter),
		stagedsync.StageSendersCfg(controlServer.ChainConfig, cfg.Sync, false /* badBlockHalt */, dirs.Tmp, cfg.Prune, blockReader, controlServer.Hd),
		stagedsync.StageExecuteBlocksCfg(db, cfg.Prune, cfg.BatchSize, controlServer.ChainConfig, controlServer.Engine, &vm.Config{Tracer: tracingHooks}, notifications, cfg.StateStream, false /* badBlockHalt */, dirs, blockReader, controlServer.Hd, cfg.Genesis, cfg.Sync, SilkwormForExecutionStage(silkworm, cfg), cfg.ExperimentalBAL),
		stagedsync.StageTxLookupCfg(cfg.Prune, dirs.Tmp, blockReader),
		stagedsync.StageFinishCfg(forkValidator),
	)
}

func NewPipelineStages(ctx context.Context,
	db kv.TemporalRwDB,
	cfg *ethconfig.Config,
	controlServer *sentry_multi_client.MultiClient,
	notifications *shards.Notifications,
	snapDownloader downloader.Client,
	blockReader services.FullBlockReader,
	blockRetire services.BlockRetire,
	silkworm *silkworm.Silkworm,
	forkValidator *engine_helpers.ForkValidator,
	tracer *tracers.Tracer,
) []*stagedsync.Stage {
	var tracingHooks *tracing.Hooks
	if tracer != nil {
		tracingHooks = tracer.Hooks
	}
	dirs := cfg.Dirs
	blockWriter := blockio.NewBlockWriter()

	var depositContract common.Address
	if cfg.Genesis != nil {
		depositContract = cfg.Genesis.Config.DepositContract
	}
	_ = depositContract

	return stagedsync.PipelineStages(ctx,
		stagedsync.StageSnapshotsCfg(db, controlServer.ChainConfig, cfg.Sync, dirs, blockRetire, snapDownloader, blockReader, notifications, cfg.InternalCL && cfg.CaplinConfig.ArchiveBlocks, cfg.CaplinConfig.ArchiveBlobs, cfg.CaplinConfig.ArchiveStates, silkworm, cfg.Prune),
		stagedsync.StageBlockHashesCfg(dirs.Tmp, blockWriter),
		stagedsync.StageSendersCfg(controlServer.ChainConfig, cfg.Sync, false /* badBlockHalt */, dirs.Tmp, cfg.Prune, blockReader, controlServer.Hd),
		stagedsync.StageExecuteBlocksCfg(db, cfg.Prune, cfg.BatchSize, controlServer.ChainConfig, controlServer.Engine, &vm.Config{Tracer: tracingHooks}, notifications, cfg.StateStream, false, dirs, blockReader, controlServer.Hd, cfg.Genesis, cfg.Sync, SilkwormForExecutionStage(silkworm, cfg), cfg.ExperimentalBAL),
		stagedsync.StageTxLookupCfg(cfg.Prune, dirs.Tmp, blockReader),
		stagedsync.StageFinishCfg(forkValidator),
		stagedsync.StageWitnessProcessingCfg(controlServer.ChainConfig, controlServer.WitnessBuffer),
	)
}

func NewInMemoryExecution(
	ctx context.Context,
	db kv.TemporalRwDB,
	cfg *ethconfig.Config,
	controlServer *sentry_multi_client.MultiClient,
	dirs datadir.Dirs,
	notifications *shards.Notifications,
	blockReader services.FullBlockReader,
	blockWriter *blockio.BlockWriter,
	silkworm *silkworm.Silkworm,
	logger log.Logger,
) *stagedsync.Sync {
	return stagedsync.New(
		cfg.Sync,
		stagedsync.StateStages(ctx,
			stagedsync.StageHeadersCfg(controlServer.Hd, controlServer.ChainConfig, cfg.Sync, controlServer.SendHeaderRequest, controlServer.PropagateNewBlockHashes, controlServer.Penalize, false /* noP2PDiscovery */, blockReader),
			stagedsync.StageBodiesCfg(controlServer.Bd, controlServer.SendBodyRequest, controlServer.Penalize, controlServer.BroadcastNewBlock, cfg.Sync.BodyDownloadTimeoutSeconds, controlServer.ChainConfig, blockReader, blockWriter),
			stagedsync.StageBlockHashesCfg(dirs.Tmp, blockWriter),
			stagedsync.StageSendersCfg(controlServer.ChainConfig, cfg.Sync, true /* badBlockHalt */, dirs.Tmp, cfg.Prune, blockReader, controlServer.Hd),
			stagedsync.StageExecuteBlocksCfg(db, cfg.Prune, cfg.BatchSize, controlServer.ChainConfig, controlServer.Engine, &vm.Config{}, notifications, cfg.StateStream, true, cfg.Dirs, blockReader, controlServer.Hd, cfg.Genesis, cfg.Sync, SilkwormForExecutionStage(silkworm, cfg), cfg.ExperimentalBAL),
		),
		stagedsync.StateUnwindOrder,
		nil, /* pruneOrder */
		logger,
		stages.ModeForkValidation,
	)
}
