// Copyright 2025 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 integrity

import (
	"bytes"
	"cmp"
	"context"
	"encoding/binary"
	"encoding/hex"
	"errors"
	"fmt"
	"os"
	"path/filepath"
	"runtime"
	"slices"
	"strings"
	"sync"
	"sync/atomic"
	"time"

	"github.com/c2h5oh/datasize"
	"golang.org/x/sync/errgroup"

	"github.com/erigontech/erigon/common"
	"github.com/erigontech/erigon/common/dbg"
	"github.com/erigontech/erigon/common/estimate"
	"github.com/erigontech/erigon/common/length"
	"github.com/erigontech/erigon/common/log/v3"
	"github.com/erigontech/erigon/db/dbservices"
	"github.com/erigontech/erigon/db/etl"
	"github.com/erigontech/erigon/db/kv"
	"github.com/erigontech/erigon/db/kv/order"
	"github.com/erigontech/erigon/db/seg"
	"github.com/erigontech/erigon/db/state"
	"github.com/erigontech/erigon/db/state/execctx"
	"github.com/erigontech/erigon/db/state/statecfg"
	"github.com/erigontech/erigon/db/version"
	"github.com/erigontech/erigon/execution/commitment"
	"github.com/erigontech/erigon/execution/commitment/commitmentdb"
)

func CheckCommitmentRoot(ctx context.Context, db kv.TemporalRoDB, br dbservices.FullBlockReader, failFast bool, logger log.Logger) error {
	tx, err := db.BeginTemporalRo(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	aggTx := state.AggTx(tx)
	defer aggTx.Close()
	allFiles := aggTx.Files(kv.CommitmentDomain)
	// atm our older files are missing the root due to purification, so this flag can be used to only check the last file
	onlyCheckLastFile := dbg.EnvBool("CHECK_COMMITMENT_ROOT_ONLY_LAST_FILE", false)
	// may want to check all files for root key presence, but only recompute for the last file (due to purification)
	onlyRecomputeLastFile := dbg.EnvBool("CHECK_COMMITMENT_ROOT_ONLY_LAST_FILE_RECOMPUTE", false)
	files := make([]state.VisibleFile, 0, len(allFiles))
	for _, f := range allFiles {
		if strings.HasSuffix(f.Fullpath(), ".kv") {
			files = append(files, f)
		}
	}
	logger.Info("[integrity] CommitmentRoot files discovered", "total", len(allFiles), "kvFiles", len(files), "onlyCheckLastFile", onlyCheckLastFile, "onlyRecomputeLastFile", onlyRecomputeLastFile)
	if len(files) == 0 {
		logger.Warn("[integrity] CommitmentRoot: no commitment .kv files found, nothing to check")
		return nil
	}
	if onlyCheckLastFile {
		files = files[len(files)-1:]
	}
	var integrityErr error
	for i, file := range files {
		recompute := !onlyRecomputeLastFile || i == len(files)-1
		err = checkCommitmentRootInFile(ctx, db, br, file, recompute, logger)
		if err != nil {
			if !errors.Is(err, ErrIntegrity) {
				return err
			}
			if failFast {
				return err
			}
			logger.Warn(err.Error())
			integrityErr = err
			continue
		}
	}
	return integrityErr
}

func checkCommitmentRootInFile(ctx context.Context, db kv.TemporalRoDB, br dbservices.FullBlockReader, f state.VisibleFile, recompute bool, logger log.Logger) error {
	tx, err := db.BeginTemporalRo(ctx) // we need separate RoTx per file if we re-compute commitment for more than 1 file
	if err != nil {
		return err
	}
	defer tx.Rollback()
	fileName := filepath.Base(f.Fullpath())
	startTxNum := f.StartRootNum()
	endTxNum := f.EndRootNum()
	logger.Info("[integrity] CommitmentRoot", "kv", fileName, "startTxNum", startTxNum, "endTxNum", endTxNum)
	info, err := checkCommitmentRootViaFileData(ctx, tx, br, f, logger)
	if err != nil {
		return fmt.Errorf("%w: in %s with startTxNum=%d, endTxNum=%d", err, fileName, startTxNum, endTxNum)
	}
	sd, err := checkCommitmentRootViaSd(ctx, tx, f, info, logger)
	if err != nil {
		return fmt.Errorf("%w: in %s with startTxNum=%d, endTxNum=%d", err, fileName, startTxNum, endTxNum)
	}
	defer sd.Close()
	if !recompute {
		logger.Info("[integrity] CommitmentRoot skipping recompute", "kv", fileName, "startTxNum", startTxNum, "endTxNum", endTxNum)
		return nil
	}
	err = checkCommitmentRootViaRecompute(ctx, tx, sd, info, f, logger)
	if err != nil {
		return fmt.Errorf("%w: in %s with startTxNum=%d, endTxNum=%d", err, fileName, startTxNum, endTxNum)
	}
	return nil
}

type commitmentRootInfo struct {
	rootHash      common.Hash
	blockNum      uint64
	txNum         uint64
	blockMinTxNum uint64
	blockMaxTxNum uint64
}

func (info commitmentRootInfo) PartialBlock() bool {
	return info.txNum < info.blockMaxTxNum
}

func checkCommitmentRootViaFileData(ctx context.Context, tx kv.TemporalTx, br dbservices.FullBlockReader, f state.VisibleFile, logger log.Logger) (commitmentRootInfo, error) {
	var info commitmentRootInfo
	startTxNum := f.StartRootNum()
	endTxNum := f.EndRootNum()
	maxTxNum := endTxNum - 1
	v, ok, start, end, err := tx.Debug().GetLatestFromFiles(kv.CommitmentDomain, commitmentdb.KeyCommitmentState, maxTxNum)
	if err != nil {
		return info, err
	}
	if !ok {
		return info, fmt.Errorf("%w: commitment root not found", ErrIntegrity)
	}
	if start != startTxNum {
		return info, fmt.Errorf("%w: commitment root not found with same startTxNum: %d != %d", ErrIntegrity, start, startTxNum)
	}
	if end != endTxNum {
		return info, fmt.Errorf("%w: commitment root not found with same endTxNum: %d != %d", ErrIntegrity, end, endTxNum)
	}
	rootHashBytes, blockNum, txNum, err := commitment.HexTrieExtractStateRoot(v)
	if err != nil {
		return info, fmt.Errorf("%w: commitment root could not be extracted: %w", ErrIntegrity, err)
	}
	if txNum >= endTxNum {
		return info, fmt.Errorf("%w: commitment root txNum is gte endTxNum: %d >= %d", ErrIntegrity, txNum, endTxNum)
	}
	if txNum < startTxNum {
		return info, fmt.Errorf("%w: commitment root txNum is lt startTxNum: %d < %d", ErrIntegrity, txNum, startTxNum)
	}
	txNumReader := br.TxnumReader()
	blockMinTxNum, err := txNumReader.Min(ctx, tx, blockNum)
	if err != nil {
		return info, err
	}
	blockMaxTxNum, err := txNumReader.Max(ctx, tx, blockNum)
	if err != nil {
		return info, err
	}
	if txNum > blockMaxTxNum {
		return info, fmt.Errorf("%w: commitment root txNum gt blockMaxTxNum for block %d: %d > %d", ErrIntegrity, blockNum, txNum, blockMaxTxNum)
	}
	if txNum < blockMinTxNum {
		return info, fmt.Errorf("%w: commitment root txNum is lt blockMinTxNum for block %d: %d < %d", ErrIntegrity, blockNum, txNum, blockMinTxNum)
	}
	if txNum == 0 {
		return info, fmt.Errorf("%w: commitment root txNum should not be zero", ErrIntegrity)
	}
	rootHash := common.Hash(rootHashBytes)
	if rootHash == (common.Hash{}) {
		return info, fmt.Errorf("%w: commitment root is empty", ErrIntegrity)
	}
	info.rootHash, info.blockNum, info.txNum, info.blockMinTxNum, info.blockMaxTxNum = rootHash, blockNum, txNum, blockMinTxNum, blockMaxTxNum
	if info.PartialBlock() {
		logger.Info("[integrity] CommitmentRoot skipping partial block", "file", filepath.Base(f.Fullpath()), "blockNum", blockNum, "txNum", txNum, "blockMinTxNum", blockMinTxNum, "blockMaxTxNum", blockMaxTxNum)
		return info, nil
	}
	h, err := br.HeaderByNumber(ctx, tx, blockNum)
	if err != nil {
		return info, err
	}
	if h == nil {
		return info, fmt.Errorf("%w: missing canonical header for block %d", ErrIntegrity, blockNum)
	}
	if h.Root != rootHash {
		return info, fmt.Errorf("%w: commitment root does not match header root for block %d: %s != %s", ErrIntegrity, blockNum, h.Root, rootHash)
	}
	return info, nil
}

func checkCommitmentRootViaSd(ctx context.Context, tx kv.TemporalTx, f state.VisibleFile, info commitmentRootInfo, logger log.Logger) (*execctx.SharedDomains, error) {
	maxTxNum := f.EndRootNum() - 1
	sd, err := execctx.NewSharedDomains(ctx, tx, logger, execctx.WithSequentialCommitment())
	if err != nil {
		return nil, err
	}
	if logger.Enabled(ctx, log.LvlTrace) {
		sd.GetCommitmentCtx().SetTraceWriter(os.Stderr)
	} else {
		sd.GetCommitmentCtx().SetTraceWriter(nil)
	}
	sd.GetCommitmentCtx().SetStateReader(commitmentdb.NewFilesOnlyStateReader(tx, maxTxNum))
	latestTxNum, _, err := sd.SeekCommitment(ctx, tx) // seek commitment again to use the new state reader instead
	if err != nil {
		return nil, err
	}
	if latestTxNum > maxTxNum {
		return nil, fmt.Errorf("%w: commitment root sd txNum should is gt maxTxNum: %d > %d", ErrIntegrity, latestTxNum, maxTxNum)
	}
	if latestTxNum > info.blockMaxTxNum {
		return nil, fmt.Errorf("%w: commitment root sd txNum should is gt blockMaxTxNum: %d > %d", ErrIntegrity, latestTxNum, info.blockMaxTxNum)
	}
	if latestTxNum < info.blockMinTxNum {
		return nil, fmt.Errorf("%w: commitment root sd txNum should is lt blockMinTxNum: %d < %d", ErrIntegrity, latestTxNum, info.blockMinTxNum)
	}
	if latestTxNum == 0 {
		return nil, fmt.Errorf("%w: commitment root sd txNum should not be zero", ErrIntegrity)
	}
	if info.PartialBlock() {
		logger.Info("[integrity] CommitmentRoot skipping sd partial block", "file", filepath.Base(f.Fullpath()), "blockNum", info.blockNum, "txNum", info.txNum, "blockMinTxNum", info.blockMinTxNum, "blockMaxTxNum", info.blockMaxTxNum)
		return sd, nil
	}
	rootHashBytes, err := sd.GetCommitmentCtx().Trie().RootHash()
	if err != nil {
		return nil, err
	}
	rootHash := common.Hash(rootHashBytes)
	if rootHash != info.rootHash {
		return nil, fmt.Errorf("%w: commitment root does not match verified root for block %d: %s != %s", ErrIntegrity, info.blockNum, rootHash, info.rootHash)
	}
	return sd, nil
}

func checkCommitmentRootViaRecompute(ctx context.Context, tx kv.TemporalTx, sd *execctx.SharedDomains, info commitmentRootInfo, f state.VisibleFile, logger log.Logger) error {
	accountTouches, storageTouches, err := sd.TouchChangedKeysFromHistory(tx, info.blockMinTxNum, info.txNum+1)
	if err != nil {
		return err
	}
	logger.Info("[integrity] CommitmentRoot recomputing",
		"accountTouches", accountTouches, "storageTouches", storageTouches,
		"file", filepath.Base(f.Fullpath()))
	recomputedBytes, err := sd.ComputeCommitment(ctx, tx, false /* saveStateAfter */, info.blockNum, info.txNum, "", nil /* commitProgress */)
	if err != nil {
		return err
	}
	recomputed := common.Hash(recomputedBytes)
	if recomputed != info.rootHash {
		return fmt.Errorf("%w: recomputed root does not match verified root: %s != %s", ErrIntegrity, recomputed, info.rootHash)
	}
	logger.Info("[integrity] CommitmentRoot recomputed matches",
		"root", recomputed,
		"accountTouches", accountTouches, "storageTouches", storageTouches,
		"file", filepath.Base(f.Fullpath()))
	return nil
}

func CheckCommitmentKvi(ctx context.Context, sc SamplerCfg, db kv.TemporalRoDB, cache *IntegrityCache, failFast bool, logger log.Logger) error {
	tx, err := db.BeginTemporalRo(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	return CheckKvis(ctx, tx, kv.CommitmentDomain, CommitmentKvi, sc, cache, failFast, logger)
}

func CheckCommitmentKvDeref(ctx context.Context, db kv.TemporalRoDB, cache *IntegrityCache, failFast bool, logger log.Logger) error {
	start := time.Now()
	tx, err := db.BeginTemporalRo(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	aggTx := state.AggTx(tx)
	files := aggTx.Files(kv.CommitmentDomain)
	stepSize := aggTx.StepSize()
	var eg *errgroup.Group
	if failFast {
		// if 1 goroutine fails, fail others
		eg, ctx = errgroup.WithContext(ctx)
	} else {
		eg = &errgroup.Group{}
	}
	if dbg.EnvBool("CHECK_COMMITMENT_KVS_DEREF_SEQUENTIAL", false) {
		eg.SetLimit(1)
	}

	type workItem struct {
		file state.VisibleFile
		fps  []fileFingerprint // nil when cache is disabled
	}
	var works []workItem
	for _, file := range files {
		if !strings.HasSuffix(file.Fullpath(), ".kv") {
			continue
		}
		var fps []fileFingerprint
		if cache != nil {
			kvPath := file.Fullpath()
			accPath, err := derivePathForOtherDomain(kvPath, kv.CommitmentDomain, kv.AccountsDomain)
			if err != nil {
				return err
			}
			stoPath, err := derivePathForOtherDomain(kvPath, kv.CommitmentDomain, kv.StorageDomain)
			if err != nil {
				return err
			}
			fps, err = fingerprintsOf(kvPath, accPath, stoPath)
			if err != nil {
				return err
			}
			if cache.has(string(CommitmentKvDeref), fps) {
				logger.Info("skipping (cache hit)", "kv", filepath.Base(kvPath))
				continue
			}
		}
		works = append(works, workItem{file, fps})
	}

	var successMu sync.Mutex
	var successFps [][]fileFingerprint
	var branchKeys, referencedAccounts, plainAccounts, referencedStorages, plainStorages atomic.Uint64
	for _, w := range works {
		eg.Go(func() error {
			counts, err := checkCommitmentKvDeref(ctx, w.file, stepSize, failFast, logger)
			if err == nil {
				branchKeys.Add(counts.branchKeys)
				referencedAccounts.Add(counts.referencedAccounts)
				plainAccounts.Add(counts.plainAccounts)
				referencedStorages.Add(counts.referencedStorages)
				plainStorages.Add(counts.plainStorages)
				if w.fps != nil {
					successMu.Lock()
					successFps = append(successFps, w.fps)
					successMu.Unlock()
				}
				return nil
			}
			if !failFast {
				logger.Warn(err.Error())
			}
			return err
		})
	}
	err = eg.Wait()
	if err != nil {
		return err
	}
	for _, fps := range successFps {
		cache.add(string(CommitmentKvDeref), fps)
	}
	logger.Info(
		"[integrity] CommitmentKvDeref",
		"dur", time.Since(start),
		"files", len(files),
		"branchKeys", branchKeys.Load(),
		"referencedAccounts", referencedAccounts.Load(),
		"plainAccounts", plainAccounts.Load(),
		"referencedStorages", referencedStorages.Load(),
		"plainStorages", plainStorages.Load(),
	)
	return nil
}

type derefCounts struct {
	branchKeys         uint64
	referencedAccounts uint64
	plainAccounts      uint64
	referencedStorages uint64
	plainStorages      uint64
}

func (dc *derefCounts) add(other derefCounts) {
	dc.branchKeys += other.branchKeys
	dc.referencedAccounts += other.referencedAccounts
	dc.plainAccounts += other.plainAccounts
	dc.referencedStorages += other.referencedStorages
	dc.plainStorages += other.plainStorages
}

// checkDerefBranch resolves all reference keys in a single commitment branch to plain
// keys via accReader/storageReader, then validates the resulting branch data.
// All issues are non-fatal: logged as warnings and accumulated into retErr (ErrIntegrity-wrapped).
func checkDerefBranch(
	branchKey, branchValue []byte,
	newBranchValueBuf, plainKeyBuf []byte,
	accReader, storageReader *seg.Reader,
	fileName string,
	trace bool,
	logger log.Logger,
) (dc derefCounts, newBranchData commitment.BranchData, retErr error) {
	branchData := commitment.BranchData(branchValue)
	var integrityErr error

	newBranchData, _ = branchData.ReplacePlainKeys(newBranchValueBuf[:0], func(key []byte, isStorage bool) ([]byte, error) {
		if trace {
			logger.Trace(
				"[integrity] CommitmentKvDeref",
				"branchKey", hex.EncodeToString(branchKey),
				"key", hex.EncodeToString(key),
				"isStorage", isStorage,
				"kv", fileName,
			)
		}
		if isStorage {
			if len(key) == length.Addr+length.Hash {
				if trace {
					logger.Trace(
						"[integrity] CommitmentKvDeref skipping plain storage",
						"branchKey", hex.EncodeToString(branchKey),
						"addr", common.BytesToAddress(key[:length.Addr]),
						"hash", common.BytesToHash(key[length.Addr:]),
						"kv", fileName,
					)
				}
				dc.plainStorages++
				return key, nil // not a referenced key, nothing to check
			}
			dc.referencedStorages++
			offset := state.DecodeReferenceKey(key)
			if offset >= uint64(storageReader.Size()) {
				err := fmt.Errorf("storage reference key %x out of bounds for branch %x in %s: %d vs %d", key, branchKey, fileName, offset, storageReader.Size())
				logger.Warn(err.Error())
				return key, nil
			}
			storageReader.Reset(offset)
			plainKey, _ := storageReader.Next(plainKeyBuf[:0])
			if len(plainKey) != length.Addr+length.Hash {
				err := fmt.Errorf("storage reference key %x has invalid plainKey for branch %x in %s", key, branchKey, fileName)
				logger.Warn(err.Error())
				integrityErr = fmt.Errorf("%w: %w", ErrIntegrity, err)
				return key, nil
			}
			if trace {
				logger.Trace(
					"[integrity] CommitmentKvDeref dereferenced storage",
					"branchKey", hex.EncodeToString(branchKey),
					"key", hex.EncodeToString(key),
					"offset", offset,
					"addr", common.BytesToAddress(plainKey[:length.Addr]),
					"hash", common.BytesToHash(plainKey[length.Addr:]),
					"kv", fileName,
				)
			}
			return plainKey, nil
		}
		if len(key) == length.Addr {
			if trace {
				logger.Trace(
					"[integrity] CommitmentKvDeref skipping plain account",
					"branchKey", hex.EncodeToString(branchKey),
					"addr", common.BytesToAddress(key[:length.Addr]),
					"kv", fileName,
				)
			}
			dc.plainAccounts++
			return key, nil // not a referenced key, nothing to check
		}
		dc.referencedAccounts++
		offset := state.DecodeReferenceKey(key)
		if offset >= uint64(accReader.Size()) {
			err := fmt.Errorf("account reference key %x out of bounds for branch %x in %s: %d vs %d", key, branchKey, fileName, offset, accReader.Size())
			logger.Warn(err.Error())
			return key, nil
		}
		accReader.Reset(offset)
		plainKey, _ := accReader.Next(plainKeyBuf[:0])
		if len(plainKey) != length.Addr {
			err := fmt.Errorf("account reference key %x has invalid plainKey for branch %x in %s", key, branchKey, fileName)
			logger.Warn(err.Error())
			integrityErr = fmt.Errorf("%w: %w", ErrIntegrity, err)
			return key, nil
		}
		if trace {
			logger.Trace(
				"[integrity] CommitmentKvDeref dereferenced account",
				"branchKey", hex.EncodeToString(branchKey),
				"key", hex.EncodeToString(key),
				"offset", offset,
				"addr", common.BytesToAddress(plainKey),
				"kv", fileName,
			)
		}
		return plainKey, nil
	})
	if err := newBranchData.Validate(branchKey); err != nil {
		err = fmt.Errorf("branch data validation failure for branch key %x in %s: %w", branchKey, fileName, err)
		logger.Warn(err.Error())
		integrityErr = fmt.Errorf("%w: %w", ErrIntegrity, err)
	}
	return dc, newBranchData, integrityErr
}

// commitmentReferencingMemo caches commitmentFileReferencing per file path. Integrity checks run
// over an immutable file set within a process and several phases query the same file, so the full
// content scan is done once per file.
var commitmentReferencingMemo sync.Map // string -> bool

// commitmentFileReferencing reports whether a commitment file carries shortened key references.
// The integrity tool full-scans the content (not the file's version stamp) to catch a mis-stamped
// file; any read error returns true rather than under-report as plain. Memoized per path.
func commitmentFileReferencing(file state.VisibleFile) bool {
	if v, ok := commitmentReferencingMemo.Load(file.Fullpath()); ok {
		return v.(bool)
	}
	r := computeCommitmentFileReferencing(file)
	commitmentReferencingMemo.Store(file.Fullpath(), r)
	return r
}

func computeCommitmentFileReferencing(file state.VisibleFile) bool {
	decomp, err := seg.NewDecompressor(file.Fullpath())
	if err != nil {
		log.Root().Warn("[integrity] commitmentFileReferencing: open failed, treating as referenced", "file", file.Fullpath(), "err", err)
		return true
	}
	defer decomp.Close()

	g := seg.NewReader(decomp.MakeGetter(), statecfg.Schema.GetDomainCfg(kv.CommitmentDomain).Compression)
	var k, v []byte
	for g.HasNext() {
		k, _ = g.Next(k[:0])
		if !g.HasNext() {
			break
		}
		v, _ = g.Next(v[:0])
		if bytes.Equal(k, commitmentdb.KeyCommitmentState) {
			continue
		}
		if commitment.BranchData(v).HasShortenedKeys() {
			return true
		}
	}
	return false
}

func checkCommitmentKvDeref(ctx context.Context, file state.VisibleFile, stepSize uint64, failFast bool, logger log.Logger) (derefCounts, error) {
	start := time.Now()
	fileName := filepath.Base(file.Fullpath())
	startTxNum := file.StartRootNum()
	endTxNum := file.EndRootNum()
	if !commitmentFileReferencing(file) {
		logger.Info(
			"[integrity] CommitmentKvDeref skipped, no shortened keys found (full scan)",
			"file", fileName,
			"startTxNum", startTxNum,
			"endTxNum", endTxNum,
			"steps", (endTxNum-startTxNum)/stepSize,
		)
		return derefCounts{}, nil
	}
	trace := logger.Enabled(ctx, log.LvlTrace)
	workers := max(dbg.EnvInt("CHECK_COMMITMENT_KVS_DEREF_WORKERS", 4), 1)
	logger.Info("[integrity] CommitmentKvDeref", "kv", fileName, "startTxNum", startTxNum, "endTxNum", endTxNum, "workers", workers)

	// Open shared decompressors — each worker creates independent readers via MakeGetter()
	commDecomp, err := seg.NewDecompressor(file.Fullpath())
	if err != nil {
		return derefCounts{}, err
	}
	defer commDecomp.Close()
	commCompression := statecfg.Schema.GetDomainCfg(kv.CommitmentDomain).Compression

	accDecomp, accCompression, err := deriveDecompForOtherDomain(file.Fullpath(), kv.CommitmentDomain, kv.AccountsDomain)
	if err != nil {
		return derefCounts{}, err
	}
	defer accDecomp.Close()

	storageDecomp, storageCompression, err := deriveDecompForOtherDomain(file.Fullpath(), kv.CommitmentDomain, kv.StorageDomain)
	if err != nil {
		return derefCounts{}, err
	}
	defer storageDecomp.Close()

	totalKeys := uint64(commDecomp.Count()) / 2

	type derefWorkItem struct {
		branchKey   []byte
		branchValue []byte
	}
	ch := make(chan derefWorkItem, workers*16)

	// Workers — set up errgroup before producer so producer can use its context
	var countsMu sync.Mutex
	var counts derefCounts
	var integrityErr error

	var eg *errgroup.Group
	if failFast {
		eg, ctx = errgroup.WithContext(ctx)
	} else {
		eg = &errgroup.Group{}
	}

	// producerCtx lets us unblock the producer after eg.Wait() returns,
	// even in non-failFast mode where the errgroup has no context.
	producerCtx, cancelProducer := context.WithCancel(ctx)

	// Producer: single goroutine iterating commitment keys sequentially.
	// producerErr is written before return, which triggers defer close(ch).
	var producerErr error
	var producerWg sync.WaitGroup
	producerWg.Go(func() {
		defer close(ch)
		commReader := seg.NewReader(commDecomp.MakeGetter(), commCompression)
		branchKeyBuf := make([]byte, 0, 128)
		branchValueBuf := make([]byte, 0, datasize.MB.Bytes())
		for commReader.HasNext() {
			branchKey, _ := commReader.Next(branchKeyBuf[:0])
			if !commReader.HasNext() {
				pErr := fmt.Errorf("%w: %w", ErrIntegrity, errors.New("invalid key/value pair during decompression"))
				if failFast {
					producerErr = pErr
					return
				}
				producerErr = pErr
				logger.Warn(pErr.Error())
				return
			}
			branchValue, _ := commReader.Next(branchValueBuf[:0])
			if bytes.Equal(branchKey, commitmentdb.KeyCommitmentState) {
				logger.Info("[integrity] CommitmentKvDeref skipping state key", "valueLen", len(branchValue), "file", fileName)
				continue
			}
			// Copy key and value before sending — reader buffers are reused
			item := derefWorkItem{
				branchKey:   append([]byte{}, branchKey...),
				branchValue: append([]byte{}, branchValue...),
			}
			select {
			case ch <- item:
			case <-producerCtx.Done():
				return
			}
		}
	})

	// Progress reporter
	var processed atomic.Uint64
	progressCtx, cancelProgress := context.WithCancel(ctx)
	defer cancelProgress()
	go func() {
		logTicker := time.NewTicker(30 * time.Second)
		defer logTicker.Stop()
		for {
			select {
			case <-progressCtx.Done():
				return
			case <-logTicker.C:
				p := processed.Load()
				elapsed := time.Since(start).Seconds()
				if elapsed > 0 && totalKeys > 0 && p > 0 && p < totalKeys {
					rate := float64(p) / elapsed
					eta := time.Duration(float64(totalKeys-p)/rate) * time.Second
					logger.Info(
						"[integrity] CommitmentKvDeref",
						"at", fmt.Sprintf("%d/%d", p, totalKeys),
						"p", fmt.Sprintf("%.1f%%", float64(p)/float64(totalKeys)*100),
						"k/s", rate,
						"eta", eta,
						"kv", fileName,
					)
				}
			}
		}
	}()

	for range workers {
		eg.Go(func() error {
			workerAccReader := seg.NewReader(accDecomp.MakeGetter(), accCompression)
			workerStorageReader := seg.NewReader(storageDecomp.MakeGetter(), storageCompression)
			newBranchValueBuf := make([]byte, 0, datasize.MB.Bytes())
			plainKeyBuf := make([]byte, 0, length.Addr+length.Hash)
			var localCounts derefCounts
			var localIntegrityErr error
			for item := range ch {
				localCounts.branchKeys++
				dc, _, err := checkDerefBranch(item.branchKey, item.branchValue, newBranchValueBuf, plainKeyBuf, workerAccReader, workerStorageReader, fileName, trace, logger)
				localCounts.referencedAccounts += dc.referencedAccounts
				localCounts.plainAccounts += dc.plainAccounts
				localCounts.referencedStorages += dc.referencedStorages
				localCounts.plainStorages += dc.plainStorages
				if err != nil {
					if errors.Is(err, ErrIntegrity) {
						localIntegrityErr = err
					} else {
						return err // non-integrity errors are always fatal
					}
				}
				processed.Add(1)
			}
			countsMu.Lock()
			counts.add(localCounts)
			if localIntegrityErr != nil {
				integrityErr = localIntegrityErr
			}
			countsMu.Unlock()
			return nil
		})
	}

	err = eg.Wait()
	cancelProgress()
	// Unblock the producer if it's stuck on channel send (e.g. all workers
	// returned early with non-integrity errors in non-failFast mode).
	cancelProducer()
	// Wait for producer to exit before closing decompressors it may reference.
	producerWg.Wait()

	// Check worker errors first — they are more informative than producer errors.
	if err != nil {
		return derefCounts{}, err
	}
	if producerErr != nil {
		// In non-failFast mode, producerErr is an integrity error — merge it
		if !failFast && errors.Is(producerErr, ErrIntegrity) {
			integrityErr = producerErr
		} else {
			return derefCounts{}, producerErr
		}
	}

	logger.Info(
		"[integrity] CommitmentKvDeref done",
		"dur", time.Since(start),
		"workers", workers,
		"branchKeys", counts.branchKeys,
		"referencedAccounts", counts.referencedAccounts,
		"plainAccounts", counts.plainAccounts,
		"referencedStorages", counts.referencedStorages,
		"plainStorages", counts.plainStorages,
		"kv", fileName,
	)
	return counts, integrityErr
}

func derivePathForOtherDomain(baseFile string, oldDomain, newDomain kv.Domain) (string, error) {
	fileVersionMask, err := version.ReplaceVersionWithMask(baseFile)
	if err != nil {
		return "", err
	}
	fileVersionMask = strings.Replace(fileVersionMask, oldDomain.String(), newDomain.String(), 1)
	newFile, _, ok, err := version.FindFilesWithVersionsByPattern(fileVersionMask)
	if err != nil {
		return "", err
	}
	if !ok {
		return "", fmt.Errorf("could not derive reader for other domain due to file not found: %s,%s->%s", baseFile, oldDomain, newDomain)
	}
	return newFile, nil
}

func deriveReaderForOtherDomain(baseFile string, oldDomain, newDomain kv.Domain) (*seg.Reader, func(), error) {
	newFile, err := derivePathForOtherDomain(baseFile, oldDomain, newDomain)
	if err != nil {
		return nil, nil, err
	}
	decomp, err := seg.NewDecompressor(newFile)
	if err != nil {
		return nil, nil, err
	}
	compression := statecfg.Schema.GetDomainCfg(newDomain).Compression
	return seg.NewReader(decomp.MakeGetter(), compression), decomp.Close, nil
}

func deriveDecompForOtherDomain(baseFile string, oldDomain, newDomain kv.Domain) (*seg.Decompressor, seg.FileCompression, error) {
	newFile, err := derivePathForOtherDomain(baseFile, oldDomain, newDomain)
	if err != nil {
		return nil, 0, err
	}
	decomp, err := seg.NewDecompressor(newFile)
	if err != nil {
		return nil, 0, err
	}
	compression := statecfg.Schema.GetDomainCfg(newDomain).Compression
	return decomp, compression, nil
}

func CheckCommitmentHistVal(ctx context.Context, sc SamplerCfg, db kv.TemporalRoDB, br dbservices.FullBlockReader, failFast bool, logger log.Logger) error {
	start := time.Now()
	tx, err := db.BeginTemporalRo(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	files := tx.Debug().DomainFiles(kv.CommitmentDomain)
	var eg *errgroup.Group
	if failFast {
		// if 1 goroutine fails, fail others
		eg, ctx = errgroup.WithContext(ctx)
	} else {
		eg = &errgroup.Group{}
	}
	if dbg.EnvBool("CHECK_COMMITMENT_HIST_VAL_SEQUENTIAL", false) {
		eg.SetLimit(1)
	} else {
		eg.SetLimit(dbg.EnvInt("CHECK_COMMITMENT_HIST_VAL_WORKERS", 8))
	}
	// numBuckets controls granularity; sampleRatio controls how many buckets per file to check.
	// e.g. sampleRatio=0.05 → ~50 out of 1000 buckets → ~5% of each file, as sequential scans.
	const numBuckets = 10000
	var completedBuckets atomic.Uint64
	var totalVals atomic.Uint64
	var filesChecked int
	var totalBuckets uint64
	for i, file := range files {
		if !strings.HasSuffix(file.Fullpath(), ".v") {
			continue
		}
		filesChecked++
		sampler := sc.NewWindowSampler(uint64(i))
		for bucket := range sampler.Buckets(0, numBuckets) {
			totalBuckets++
			eg.Go(func() error {
				tx, err := db.BeginTemporalRo(ctx)
				if err != nil {
					return err
				}
				defer tx.Rollback()
				n, err := checkCommitmentHistValBucket(ctx, tx, br, file, bucket, failFast, log.LvlDebug, logger)
				totalVals.Add(n)
				completedBuckets.Add(1)
				if err != nil && !failFast {
					logger.Warn(err.Error())
				}
				return err
			})
		}
	}
	progressCtx, cancelProgress := context.WithCancel(ctx)
	defer cancelProgress()
	if totalBuckets > 0 {
		logTicker := time.NewTicker(20 * time.Second)
		defer logTicker.Stop()
		go func() {
			for {
				select {
				case <-progressCtx.Done():
					return
				case <-logTicker.C:
					vals := totalVals.Load()
					elapsed := time.Since(start).Seconds()
					rate := 0.0
					if elapsed > 0 {
						rate = float64(vals) / elapsed
					}
					logger.Info("[integrity] CommitmentHistVal progress",
						"buckets", fmt.Sprintf("%d/%d", completedBuckets.Load(), totalBuckets),
						"vals", vals,
						"vals/s", rate,
						"seed", sc.Seed,
						"sampleRatio", sc.SampleRatio,
					)
				}
			}
		}()
	}
	err = eg.Wait()
	cancelProgress()
	if err != nil {
		return err
	}
	dur := time.Since(start)
	total := totalVals.Load()
	rate := 0.0
	if dur.Seconds() > 0 {
		rate = float64(total) / dur.Seconds()
	}
	logger.Info("[integrity] CommitmentHistVal", "dur", dur, "files", filesChecked, "buckets", totalBuckets, "vals", total, "vals/s", rate, "seed", sc.Seed, "sampleRatio", sc.SampleRatio)
	return nil
}

func checkCommitmentHistValBucket(ctx context.Context, tx kv.TemporalTx, br dbservices.FullBlockReader, file state.VisibleFile, bucket int, failFast bool, lvl log.Lvl, logger log.Logger) (uint64, error) {
	const numBuckets = 10000
	start := time.Now()
	fileName := filepath.Base(file.Fullpath())
	startTxNum := file.StartRootNum()
	endTxNum := file.EndRootNum()
	txCount := endTxNum - startTxNum
	if numBuckets > txCount {
		return 0, fmt.Errorf("numBuckets %d is greater than total tx count %d in file %s", numBuckets, txCount, fileName)
	}
	bucketSize := txCount / numBuckets
	bucketStart := startTxNum + uint64(bucket)*bucketSize
	bucketEnd := min(bucketStart+bucketSize, endTxNum)
	logger.Log(
		lvl,
		"[integrity] CommitmentHistVal",
		"v", fileName,
		"startTxNum", startTxNum,
		"endTxNum", endTxNum,
		"numBuckets", numBuckets,
		"bucket", bucket,
		"bucketSize", bucketSize,
		"bucketStart", bucketStart,
		"bucketEnd", bucketEnd,
	)
	txNumReader := br.TxnumReader()
	it, err := tx.HistoryRange(kv.CommitmentDomain, int(bucketStart), int(bucketEnd), order.Asc, -1)
	if err != nil {
		return 0, err
	}
	defer it.Close()
	logTicker := time.NewTicker(30 * time.Second)
	defer logTicker.Stop()
	var total uint64
	var integrityErr error
	for it.HasNext() {
		select {
		case <-ctx.Done():
			return 0, ctx.Err()
		case <-logTicker.C:
			rate := float64(total) / time.Since(start).Seconds()
			logger.Log(lvl, "[integrity] CommitmentHistVal progress", "at", total, "vals/s", rate, "v", fileName)
		default:
			// no-op
		}
		k, v, err := it.Next()
		if err != nil {
			return 0, err
		}
		if bytes.Equal(k, commitmentdb.KeyCommitmentState) {
			rootHashBytes, blockNum, txNum, err := commitment.HexTrieExtractStateRoot(v)
			if err != nil {
				return 0, fmt.Errorf("issue extracting state root value in %s for [%d,%d) tx nums: %w", fileName, bucketStart, bucketEnd, err)
			}
			maxTxNum, err := txNumReader.Max(ctx, tx, blockNum)
			if err != nil {
				return 0, err
			}
			if txNum < maxTxNum {
				logger.Log(lvl, "[integrity] CommitmentHistVal skipping partial block", "blockNum", blockNum, "txNum", txNum, "maxTxNum", maxTxNum, "v", fileName)
				continue
			}
			if txNum != maxTxNum {
				err = fmt.Errorf("commitment state txNum mismatch for block %d in %s: %d != %d", blockNum, fileName, txNum, maxTxNum)
				if failFast {
					return 0, err
				}
				logger.Warn(err.Error())
				integrityErr = fmt.Errorf("%w: %w", ErrIntegrity, err)
			}
			rootHash := common.Hash(rootHashBytes)
			header, err := br.HeaderByNumber(ctx, tx, blockNum)
			if err != nil {
				return 0, err
			}
			if rootHash != header.Root {
				err = fmt.Errorf("commitment state root mismatch for block %d, tx %d in %s: %s != %s", blockNum, txNum, fileName, rootHash, header.Root)
				if failFast {
					return 0, err
				}
				logger.Warn(err.Error())
				integrityErr = fmt.Errorf("%w: %w", ErrIntegrity, err)
			}
		} else {
			branchData := commitment.BranchData(v)
			err = branchData.Validate(k)
			if err != nil {
				err = fmt.Errorf("branch data validation failure for branch key %x in %s: %w", k, fileName, err)
				if failFast {
					return 0, err
				}
				logger.Warn(err.Error())
				integrityErr = fmt.Errorf("%w: %w", ErrIntegrity, err)
			}
		}
		total++
	}
	dur := time.Since(start)
	rate := float64(total) / dur.Seconds()
	logger.Log(lvl, "[integrity] CommitmentHistVal done", "dur", dur, "vals", total, "vals/s", rate, "v", fileName)
	return total, integrityErr
}

// checkCommitmentHistAtBlkWithIdx checks commitment for blockNum using the pre-built
// per-domain key index from ChangedKeysPerBlockIdx.
func checkCommitmentHistAtBlkWithIdx(ctx context.Context, tx kv.TemporalTx, sd *execctx.SharedDomains, br dbservices.FullBlockReader, blockNum uint64, idx *ChangedKeysPerBlockIdx, lvl log.Lvl, logger log.Logger) error {
	logger.Log(lvl, "checking commitment hist at block", "blockNum", blockNum)
	header, err := br.HeaderByNumber(ctx, tx, blockNum)
	if err != nil {
		return err
	}
	txNumsReader := br.TxnumReader()
	minTxNum, err := txNumsReader.Min(ctx, tx, blockNum)
	if err != nil {
		return err
	}
	maxTxNum, err := txNumsReader.Max(ctx, tx, blockNum)
	if err != nil {
		return err
	}
	// Use EndTxNumNoCommitment: this check reconstructs commitment from history
	// files only (not commitment domain), so commitment domain coverage must not
	// limit which blocks can be verified.
	aggTx := state.AggTx(tx)
	if aggMax := aggTx.EndTxNumNoCommitment(); maxTxNum+1 > aggMax {
		blockNumOfState, ok, err := txNumsReader.FindBlockNum(ctx, tx, aggMax)
		if err != nil {
			return fmt.Errorf("block %d is beyond state coverage and FindBlockNum failed: %w", blockNum, err)
		}
		if !ok {
			return fmt.Errorf("block %d is beyond state coverage (aggMax txNum=%d)", blockNum, aggMax)
		}
		return fmt.Errorf("block %d is beyond latest block with state %d", blockNum, blockNumOfState)
	}
	toTxNum := maxTxNum + 1
	// For blockNum==0 there is no prior commitment state (GetAsOf at txNum=0
	// falls back to latest for the commitment domain). Use commitmentAsOf=toTxNum
	// so the trie is restored from the committed state at the end of block 0.
	commitmentAsOf := minTxNum
	if blockNum == 0 {
		commitmentAsOf = toTxNum
	}
	// commitment branch data view: as of beginning of the block (or end for block 0)
	// plain state data view: as of end of the block
	splitStateReader := commitmentdb.NewSplitHistoryReader(tx, commitmentAsOf, toTxNum, true /* withHistory */)
	sd.GetCommitmentCtx().SetStateReader(splitStateReader)
	if logger.Enabled(ctx, log.LvlTrace) {
		sd.GetCommitmentCtx().SetTraceWriter(os.Stderr)
	} else {
		sd.GetCommitmentCtx().SetTraceWriter(nil)
	}
	latestTxNum, latestBlockNum, err := sd.SeekCommitment(ctx, tx) // seek commitment again with new history state reader
	if err != nil {
		return err
	}
	if latestBlockNum > blockNum {
		return fmt.Errorf("commitment state blockNum is ahead of blockNum: %d > %d", latestBlockNum, blockNum)
	}
	if latestBlockNum < blockNum {
		// Gap: commitment state is from an earlier block (empty blocks in between).
		// Verify the gap is truly empty by comparing state roots from block headers —
		// any state change would produce a different root, so this is sufficient.
		refHeader, err := br.HeaderByNumber(ctx, tx, latestBlockNum)
		if err != nil {
			return err
		}
		for gapBlock := latestBlockNum + 1; gapBlock < blockNum; gapBlock++ {
			gapHeader, err := br.HeaderByNumber(ctx, tx, gapBlock)
			if err != nil {
				return err
			}
			if gapHeader.Root != refHeader.Root {
				return fmt.Errorf("commitment state blockNum doesn't match blockNum: %d != %d (block %d has different state root: ref=%x header=%x)", latestBlockNum, blockNum, gapBlock, refHeader.Root, gapHeader.Root)
			}
		}
		logger.Log(lvl, "commitment state is from earlier block (empty blocks in between)", "commitmentBlockNum", latestBlockNum, "blockNum", blockNum)
	}
	if latestTxNum != maxTxNum && latestBlockNum == blockNum {
		return fmt.Errorf("commitment state txNum doesn't match maxTxNum: %d != %d", latestTxNum, maxTxNum)
	}
	logger.Log(lvl, "commitment recalc info", "blockNum", blockNum, "minTxNum", minTxNum, "maxTxNum", maxTxNum, "toTxNum", toTxNum)
	trace := logger.Enabled(ctx, log.LvlTrace)
	touchLoggingVisitor := func(k []byte) {
		if trace {
			args := []any{"key", common.Address(k[:length.Addr])}
			if len(k) > length.Addr {
				args = append(args, "slot", common.Hash(k[length.Addr:]))
			}
			logger.Trace("commitment touched key", args...)
		}
	}
	if _, err := touchHistoricalKeys(sd, tx, kv.AccountsDomain, minTxNum, toTxNum, blockNum, idx, touchLoggingVisitor); err != nil {
		return err
	}
	if _, err := touchHistoricalKeys(sd, tx, kv.StorageDomain, minTxNum, toTxNum, blockNum, idx, touchLoggingVisitor); err != nil {
		return err
	}
	if _, err := touchHistoricalKeys(sd, tx, kv.CodeDomain, minTxNum, toTxNum, blockNum, idx, touchLoggingVisitor); err != nil {
		return err
	}
	root, err := sd.ComputeCommitment(ctx, tx, false /* saveStateAfter */, blockNum, maxTxNum, "", nil /* commitProgress */)
	if err != nil {
		return err
	}
	rootHash := common.Hash(root)
	if header.Root != rootHash {
		return fmt.Errorf("commitment root mismatch: %s != %s (blockNum=%d,txNum=%d)", header.Root, rootHash, blockNum, maxTxNum)
	}
	return nil
}

func CheckCommitmentHistAtBlk(ctx context.Context, db kv.TemporalRoDB, br dbservices.FullBlockReader, blockNum uint64, lvl log.Lvl, logger log.Logger) error {
	tx, err := db.BeginTemporalRo(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	sd, err := execctx.NewSharedDomains(ctx, tx, logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment())
	if err != nil {
		return err
	}
	defer sd.Close()
	idx, err := NewChangedKeysPerBlockIdx(ctx, tx, br, blockNum, blockNum+1, logger)
	if err != nil {
		return err
	}
	return checkCommitmentHistAtBlkWithIdx(ctx, tx, sd, br, blockNum, idx, lvl, logger)
}

// checkCommitmentHistWindowSize is the number of blocks covered by a single
// pre-built ChangedKeysPerBlockIdx.  Large enough to amortize the index build cost
// across many sampled blocks; small enough to keep memory bounded (~few hundred MB).
const checkCommitmentHistWindowSize = 10_000

func CheckCommitmentHistAtBlkRange(ctx context.Context, sc SamplerCfg, db kv.TemporalRoDB, br dbservices.FullBlockReader, from, to uint64, logger log.Logger) error {
	if from >= to {
		return fmt.Errorf("invalid blk range: %d >= %d", from, to)
	}
	start := time.Now()
	var checked atomic.Uint64
	var windowsDone atomic.Uint64
	totalWindows := (to - from + checkCommitmentHistWindowSize - 1) / checkCommitmentHistWindowSize
	expectedBlks := sc.NewSampler().ExpectedN(to - from)

	const logInterval = 20 * time.Second
	logTicker := time.NewTicker(logInterval)
	defer logTicker.Stop()
	progressCtx, cancelProgress := context.WithCancel(ctx)
	defer cancelProgress()
	go func() {
		for {
			select {
			case <-progressCtx.Done():
				return
			case <-logTicker.C:
				done := checked.Load()
				wDone := windowsDone.Load()
				blkRate := float64(done) / time.Since(start).Seconds()
				logger.Info("[integrity] "+string(StateRootVerifyByHistory),
					"blks/s", fmt.Sprintf("%.1f", blkRate),
					"checked", fmt.Sprintf("%s/%s", common.PrettyCounter(done), common.PrettyCounter(expectedBlks)),
					"windows", fmt.Sprintf("%d/%d", wDone, totalWindows),
					"blkRange", fmt.Sprintf("%s-%s", common.PrettyCounter(from), common.PrettyCounter(to)),
				)
			}
		}
	}()

	// Each worker owns a window: builds its own ChangedKeysPerBlockIdx then checks
	// its own sampled blocks — index building and block checking both run in parallel.
	workerLimit := max(1, runtime.GOMAXPROCS(-1))
	g, wCtx := errgroup.WithContext(ctx)
	g.SetLimit(workerLimit)
	for windowStart := from; windowStart < to; windowStart += checkCommitmentHistWindowSize {
		windowStart := windowStart
		windowEnd := min(windowStart+checkCommitmentHistWindowSize, to)
		g.Go(func() error {
			if wCtx.Err() != nil {
				return wCtx.Err()
			}
			tx, err := db.BeginTemporalRo(wCtx)
			if err != nil {
				return err
			}
			defer tx.Rollback()
			sd, err := execctx.NewSharedDomains(wCtx, tx, logger, execctx.WithoutDeferredBranchUpdates(), execctx.WithSequentialCommitment())
			if err != nil {
				return err
			}
			defer sd.Close()
			idx, err := NewChangedKeysPerBlockIdx(wCtx, tx, br, windowStart, windowEnd, logger)
			if err != nil {
				return fmt.Errorf("CheckCommitmentHistAtBlkRange: build index window=[%d,%d): %w", windowStart, windowEnd, err)
			}
			// Each goroutine needs its own Sampler — the RNG is not goroutine-safe.
			sampler := sc.NewWindowSampler(windowStart)
			for blockNum := range sampler.BlockNums(windowStart, windowEnd) {
				// Fresh SharedDomains per block: an SD is committed-or-closed,
				// never reset in place.
				sd, err := execctx.NewSharedDomains(wCtx, tx, logger, execctx.WithoutDeferredBranchUpdates())
				if err != nil {
					return err
				}
				err = checkCommitmentHistAtBlkWithIdx(wCtx, tx, sd, br, blockNum, idx, log.LvlTrace, logger)
				sd.Close()
				if err != nil {
					return fmt.Errorf("checkCommitmentHistAtBlk: %d, %w", blockNum, err)
				}
				checked.Add(1)
			}
			windowsDone.Add(1)
			return nil
		})
	}
	if err := g.Wait(); err != nil {
		return err
	}

	dur := time.Since(start)
	n := checked.Load()
	rate := float64(n) / dur.Seconds()
	logger.Info("checked commitment hist at blk range", "dur", dur, "blks", n, "blks/s", fmt.Sprintf("%.1f", rate), "from", from, "to", to, "seed", sc.Seed, "sampleRatio", sc.SampleRatio)
	return nil
}

func CheckStateVerify(ctx context.Context, db kv.TemporalRoDB, failFast bool, fromStep uint64, logger log.Logger) error {
	start := time.Now()
	tx, err := db.BeginTemporalRo(ctx)
	if err != nil {
		return err
	}
	defer tx.Rollback()
	aggTx := state.AggTx(tx)
	files := aggTx.Files(kv.CommitmentDomain)
	stepSize := aggTx.StepSize()
	var integrityErr error
	var totalFiles int
	for _, file := range files {
		if !strings.HasSuffix(file.Fullpath(), ".kv") {
			continue
		}
		startTxNum := file.StartRootNum()
		fileStep := startTxNum / stepSize
		if fileStep < fromStep {
			continue
		}
		totalFiles++

		var checkErr error
		if startTxNum == 0 {
			// Base file: forward check (commitment refs count <= domain entries count)
			checkErr = checkStateCorrespondenceBase(ctx, file, stepSize, failFast, logger)
		} else {
			// Non-base file: reverse check (every domain key is in commitment refs)
			// Include the next commitment file's refs to handle step boundary effects:
			// accounts written near the end of a step may have their commitment branch
			// data in the next step's file.
			// Collect all previous files for no-op write detection.
			var nextFile state.VisibleFile
			var prevFiles []state.VisibleFile
			for j := range files {
				if files[j].StartRootNum() == file.EndRootNum() && strings.HasSuffix(files[j].Fullpath(), ".kv") {
					nextFile = files[j]
				}
				if files[j].EndRootNum() <= file.StartRootNum() && strings.HasSuffix(files[j].Fullpath(), ".kv") {
					prevFiles = append(prevFiles, files[j])
				}
			}
			checkErr = checkStateCorrespondenceReverse(ctx, file, nextFile, prevFiles, stepSize, failFast, logger)
		}
		if checkErr != nil {
			if !errors.Is(checkErr, ErrIntegrity) {
				return checkErr
			}
			if failFast {
				return checkErr
			}
			logger.Warn(checkErr.Error())
			integrityErr = checkErr
			continue
		}
	}
	logger.Info("[integrity] StateVerify done", "dur", time.Since(start), "files", totalFiles)
	return integrityErr
}

// checkStateCorrespondenceBase verifies base files (startTxNum==0) where commitment
// branches reference ALL keys in the trie, and the accounts/storage files contain
// all those keys. Forward check: commitment ref count <= domain entry count.
func checkStateCorrespondenceBase(ctx context.Context, file state.VisibleFile, stepSize uint64, failFast bool, logger log.Logger) error {
	start := time.Now()
	fileName := filepath.Base(file.Fullpath())
	startTxNum := file.StartRootNum()
	endTxNum := file.EndRootNum()

	logger.Info("[integrity] StateVerify checking base file", "kv", fileName, "startTxNum", startTxNum, "endTxNum", endTxNum)

	// Open commitment decompressor + reader
	commDecomp, err := seg.NewDecompressor(file.Fullpath())
	if err != nil {
		return err
	}
	defer commDecomp.Close()
	commCompression := statecfg.Schema.GetDomainCfg(kv.CommitmentDomain).Compression
	commReader := seg.NewReader(commDecomp.MakeGetter(), commCompression)

	// Open accounts decompressor + reader
	accDecomp, accReader, accClose, err := deriveDecompAndReaderForOtherDomain(file.Fullpath(), kv.CommitmentDomain, kv.AccountsDomain)
	if err != nil {
		return err
	}
	defer accClose()

	// Open storage decompressor + reader
	stoDecomp, storageReader, storageClose, err := deriveDecompAndReaderForOtherDomain(file.Fullpath(), kv.CommitmentDomain, kv.StorageDomain)
	if err != nil {
		return err
	}
	defer storageClose()

	// Count domain entries (key/value pairs, so divide by 2)
	expectedAccounts := uint64(accDecomp.Count()) / 2
	expectedStorages := uint64(stoDecomp.Count()) / 2

	isReferencing := commitmentFileReferencing(file)

	// Track unique keys found in commitment branches via ETL collectors (disk-spilling dedup).
	accCollector := etl.NewCollector("[integrity] StateVerify acc", "", etl.NewOldestEntryBuffer(etl.BufferOptimalSize), logger)
	defer accCollector.Close()
	stoCollector := etl.NewCollector("[integrity] StateVerify sto", "", etl.NewOldestEntryBuffer(etl.BufferOptimalSize), logger)
	defer stoCollector.Close()

	totalKeys := uint64(commDecomp.Count()) / 2
	logTicker := time.NewTicker(30 * time.Second)
	defer logTicker.Stop()
	branchKeyBuf := make([]byte, 0, 128)
	branchValueBuf := make([]byte, 0, datasize.MB.Bytes())
	var branchKeys uint64
	var integrityErr error

	for i := 0; commReader.HasNext(); i++ {
		if i%1024 == 0 {
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-logTicker.C:
				at := fmt.Sprintf("%d/%d", branchKeys, totalKeys)
				percent := fmt.Sprintf("%.1f%%", float64(branchKeys)/float64(totalKeys)*100)
				logger.Info("[integrity] StateVerify progress", "at", at, "p", percent, "kv", fileName)
			default:
			}
		}

		branchKey, _ := commReader.Next(branchKeyBuf[:0])
		if !commReader.HasNext() {
			err = errors.New("invalid key/value pair during decompression")
			if failFast {
				return fmt.Errorf("%w: %s in %s", ErrIntegrity, err, fileName)
			}
			integrityErr = fmt.Errorf("%w: %s in %s", ErrIntegrity, err, fileName)
			logger.Warn(err.Error())
			continue
		}
		branchValue, _ := commReader.Next(branchValueBuf[:0])

		if bytes.Equal(branchKey, commitmentdb.KeyCommitmentState) {
			continue
		}
		branchKeys++

		branchData := commitment.BranchData(branchValue)

		if branchData.IsTombstone() {
			continue // tombstones kept across merges, not corruption
		}

		// Check completeness
		if !branchData.IsComplete() {
			touchMap := uint16(0)
			afterMap := uint16(0)
			if len(branchData) >= 4 {
				touchMap = binary.BigEndian.Uint16(branchData[0:])
				afterMap = binary.BigEndian.Uint16(branchData[2:])
			}
			err := fmt.Errorf("%w: incomplete branch at key=%x (touchMap=0x%04x afterMap=0x%04x) in %s", ErrIntegrity, branchKey, touchMap, afterMap, fileName)
			if failFast {
				return err
			}
			logger.Warn(err.Error())
			integrityErr = err
			continue
		}

		// Walk the branch to extract all referenced keys.
		// checkKey handles both account and storage: validates reference key offsets/lengths
		// and collects keys into the ETL collector for deduplication and counting.
		checkKey := func(key []byte, collector *etl.Collector, reader *seg.Reader, expectedLen int, kind string) error {
			if len(key) == expectedLen {
				// Plain key: collect for counting only when not referencing.
				// In referencing files, plain-length keys are pass-through and not counted
				// (only offset-encoded refs contribute to the forward count).
				if !isReferencing {
					return collector.Collect(key, nil)
				}
				return nil
			}
			if !isReferencing {
				return fmt.Errorf("%w: unexpected %s key len=%d for branch %x in %s", ErrIntegrity, kind, len(key), branchKey, fileName)
			}
			offset := state.DecodeReferenceKey(key)
			if offset >= uint64(reader.Size()) {
				return fmt.Errorf("%w: %s reference key %x out of bounds for branch %x in %s: %d vs %d", ErrIntegrity, kind, key, branchKey, fileName, offset, reader.Size())
			}
			reader.Reset(offset)
			// Skip (single Huffman pass) — we only need the key length, not the key itself
			if _, keyLen := reader.Skip(); keyLen != expectedLen {
				return fmt.Errorf("%w: %s reference key %x has invalid plainKey len=%d for branch %x in %s", ErrIntegrity, kind, key, keyLen, branchKey, fileName)
			}
			var offsetKey [8]byte
			binary.BigEndian.PutUint64(offsetKey[:], offset)
			return collector.Collect(offsetKey[:], nil)
		}
		// The callback returns nil (keep original key in output) because the result of ReplacePlainKeys
		// is discarded (_): all side-effects happen before the return.
		_, err := branchData.ReplacePlainKeys(nil, func(key []byte, isStorage bool) ([]byte, error) {
			var checkErr error
			if isStorage {
				checkErr = checkKey(key, stoCollector, storageReader, length.Addr+length.Hash, "storage")
			} else {
				checkErr = checkKey(key, accCollector, accReader, length.Addr, "account")
			}
			if checkErr != nil {
				if failFast {
					return nil, checkErr
				}
				logger.Warn(checkErr.Error())
				integrityErr = checkErr
			}
			return nil, nil // safe: result of ReplacePlainKeys is discarded (_)
		})
		if err != nil {
			if failFast {
				return err
			}
			logger.Warn(err.Error())
			integrityErr = err
		}
	}

	// Count unique keys via ETL Load (handles dedup via OldestEntryBuffer).
	var foundAccounts, foundStorages uint64
	if err := accCollector.Load(nil, "", func(k, v []byte, _ etl.CurrentTableReader, _ etl.LoadNextFunc) error {
		foundAccounts++
		return nil
	}, etl.TransformArgs{Quit: ctx.Done()}); err != nil {
		return err
	}
	if err := stoCollector.Load(nil, "", func(k, v []byte, _ etl.CurrentTableReader, _ etl.LoadNextFunc) error {
		foundStorages++
		return nil
	}, etl.TransformArgs{Quit: ctx.Done()}); err != nil {
		return err
	}

	// Forward check: commitment must not reference MORE keys than exist in domain files.
	// Commitment may reference FEWER keys because branch data for accounts written near
	// step boundaries may land in the next step's commitment file.
	if foundAccounts > expectedAccounts {
		err := fmt.Errorf("%w: FAIL %s accounts_in_commitment=%d > accounts_in_file=%d (extra=%d)", ErrIntegrity, fileName, foundAccounts, expectedAccounts, foundAccounts-expectedAccounts)
		if failFast {
			return err
		}
		logger.Warn(err.Error())
		integrityErr = err
	}
	if foundStorages > expectedStorages {
		err := fmt.Errorf("%w: FAIL %s storage_in_commitment=%d > storage_in_file=%d (extra=%d)", ErrIntegrity, fileName, foundStorages, expectedStorages, foundStorages-expectedStorages)
		if failFast {
			return err
		}
		logger.Warn(err.Error())
		integrityErr = err
	}

	dur := time.Since(start)
	if integrityErr == nil {
		logger.Info("[integrity] StateVerify key correspondence PASS (base)", "kv", fileName,
			"accounts", fmt.Sprintf("%d/%d", foundAccounts, expectedAccounts),
			"storage", fmt.Sprintf("%d/%d", foundStorages, expectedStorages),
			"dur", dur)

		// Phase 2: Hash verification — only runs if key correspondence passes.
		numWorkers := dbg.EnvInt("CHECK_VERIFY_STATE_WORKERS", estimate.AlmostAllCPUs())
		hashErr := checkHashVerification(ctx, file, stepSize, failFast, numWorkers, logger)
		if hashErr != nil {
			integrityErr = hashErr
		}
	}
	return integrityErr
}

// checkStateCorrespondenceReverse verifies non-base files (startTxNum > 0) by checking
// that every key in accounts.kv and storage.kv is referenced by some commitment branch.
//
// nextFile (optional) is the next commitment .kv file. Refs from it are also extracted
// to handle boundary effects: accounts written near the end of a step may have their
// commitment branch data in the next step's file.
//
// Approach: walk commitment branches → write all extracted plain keys to temp files →
// sort+dedup → merge-join with domain .kv files (which are also sorted by key).
func checkStateCorrespondenceReverse(ctx context.Context, file state.VisibleFile, nextFile state.VisibleFile, prevFiles []state.VisibleFile, stepSize uint64, failFast bool, logger log.Logger) error {
	start := time.Now()
	fileName := filepath.Base(file.Fullpath())
	startTxNum := file.StartRootNum()
	endTxNum := file.EndRootNum()

	logger.Info("[integrity] StateVerify checking non-base file", "kv", fileName, "startTxNum", startTxNum, "endTxNum", endTxNum)

	// Open commitment decompressor + reader
	commDecomp, err := seg.NewDecompressor(file.Fullpath())
	if err != nil {
		return err
	}
	defer commDecomp.Close()
	commCompression := statecfg.Schema.GetDomainCfg(kv.CommitmentDomain).Compression
	commReader := seg.NewReader(commDecomp.MakeGetter(), commCompression)

	// Open accounts decompressor + reader (for dereferencing and reverse check)
	accDecomp, accReader, accClose, err := deriveDecompAndReaderForOtherDomain(file.Fullpath(), kv.CommitmentDomain, kv.AccountsDomain)
	if err != nil {
		return err
	}
	defer accClose()

	// Open storage decompressor + reader
	stoDecomp, storageReader, storageClose, err := deriveDecompAndReaderForOtherDomain(file.Fullpath(), kv.CommitmentDomain, kv.StorageDomain)
	if err != nil {
		return err
	}
	defer storageClose()

	expectedAccounts := uint64(accDecomp.Count()) / 2
	expectedStorages := uint64(stoDecomp.Count()) / 2

	// ETL collectors for commitment-extracted keys (disk-spilling dedup + sort).
	accCollector := etl.NewCollector("[integrity] StateVerify acc-refs", "", etl.NewOldestEntryBuffer(etl.BufferOptimalSize), logger)
	defer accCollector.Close()
	stoCollector := etl.NewCollector("[integrity] StateVerify sto-refs", "", etl.NewOldestEntryBuffer(etl.BufferOptimalSize), logger)
	defer stoCollector.Close()

	totalKeys := uint64(commDecomp.Count()) / 2
	logTicker := time.NewTicker(30 * time.Second)
	defer logTicker.Stop()
	branchKeyBuf := make([]byte, 0, 128)
	branchValueBuf := make([]byte, 0, datasize.MB.Bytes())
	plainKeyBuf := make([]byte, 0, length.Addr+length.Hash)
	var branchKeys uint64
	var integrityErr error
	var extractedAccKeys, extractedStoKeys, skippedAccKeys, skippedStoKeys uint64

	// Phase 1: Walk commitment branches, collect extracted plain keys into ETL collectors.
	for i := 0; commReader.HasNext(); i++ {
		if i%1024 == 0 {
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-logTicker.C:
				logger.Info("[integrity] StateVerify extracting refs", "at", fmt.Sprintf("%d/%d", branchKeys, totalKeys),
					"p", fmt.Sprintf("%.1f%%", float64(branchKeys)/float64(totalKeys)*100), "kv", fileName)
			default:
			}
		}

		branchKey, _ := commReader.Next(branchKeyBuf[:0])
		if !commReader.HasNext() {
			break
		}
		branchValue, _ := commReader.Next(branchValueBuf[:0])

		if bytes.Equal(branchKey, commitmentdb.KeyCommitmentState) {
			continue
		}
		branchKeys++

		branchData := commitment.BranchData(branchValue)
		if branchData.IsTombstone() {
			continue
		}

		if !branchData.IsComplete() {
			touchMap := uint16(0)
			afterMap := uint16(0)
			if len(branchData) >= 4 {
				touchMap = binary.BigEndian.Uint16(branchData[0:])
				afterMap = binary.BigEndian.Uint16(branchData[2:])
			}
			err := fmt.Errorf("%w: incomplete branch at key=%x (touchMap=0x%04x afterMap=0x%04x) in %s", ErrIntegrity, branchKey, touchMap, afterMap, fileName)
			if failFast {
				return err
			}
			logger.Warn(err.Error())
			integrityErr = err
			continue
		}

		_, err := branchData.ReplacePlainKeys(nil, func(key []byte, isStorage bool) ([]byte, error) {
			if isStorage {
				plainKey := key
				if len(key) != length.Addr+length.Hash {
					// Try to decode as file offset reference
					offset := state.DecodeReferenceKey(key)
					if offset < uint64(storageReader.Size()) {
						storageReader.Reset(offset)
						plainKey, _ = storageReader.Next(plainKeyBuf[:0])
					} else {
						skippedStoKeys++
						return key, nil
					}
				}
				if len(plainKey) == length.Addr+length.Hash {
					extractedStoKeys++
					if err := stoCollector.Collect(plainKey, nil); err != nil {
						return nil, err
					}
				}
				return plainKey, nil
			}
			// Account key
			plainKey := key
			if len(key) != length.Addr {
				// Try to decode as file offset reference
				offset := state.DecodeReferenceKey(key)
				if offset < uint64(accReader.Size()) {
					accReader.Reset(offset)
					plainKey, _ = accReader.Next(plainKeyBuf[:0])
				} else {
					skippedAccKeys++
					return key, nil
				}
			}
			if len(plainKey) == length.Addr {
				extractedAccKeys++
				if err := accCollector.Collect(plainKey, nil); err != nil {
					return nil, err
				}
			}
			return plainKey, nil
		})
		if err != nil {
			if failFast {
				return err
			}
			integrityErr = err
		}
	}

	// Also extract refs from the next commitment file (handles step boundary effects)
	if nextFile != nil {
		if err := extractCommitmentRefsToCollectors(ctx, nextFile, accCollector, stoCollector, logger); err != nil {
			logger.Warn("[integrity] StateVerify failed to extract refs from next file", "err", err)
			// Non-fatal: proceed with what we have
		}
	}

	logger.Info("[integrity] StateVerify extracted refs, sorting via ETL", "kv", fileName,
		"extractedAcc", extractedAccKeys, "extractedSto", extractedStoKeys,
		"skippedAcc", skippedAccKeys, "skippedSto", skippedStoKeys,
		"branches", branchKeys, "dur", time.Since(start))

	// Phase 2: Reverse check — merge-join domain .kv with ETL-sorted commitment refs.
	// Collect previous file paths for no-op write detection.
	var prevCommitmentPaths []string
	for _, pf := range prevFiles {
		prevCommitmentPaths = append(prevCommitmentPaths, pf.Fullpath())
	}
	// Sort newest-first so we check the most recent previous file first.
	slices.SortFunc(prevCommitmentPaths, func(a, b string) int {
		return cmp.Compare(b, a)
	})
	accMissing, err := reverseCheckDomainKeys(ctx, accDecomp, kv.AccountsDomain, accCollector, prevCommitmentPaths, fileName, failFast, logger)
	if err != nil && !errors.Is(err, ErrIntegrity) {
		return err
	}
	if err != nil {
		integrityErr = err
	}
	stoMissing, err := reverseCheckDomainKeys(ctx, stoDecomp, kv.StorageDomain, stoCollector, prevCommitmentPaths, fileName, failFast, logger)
	if err != nil && !errors.Is(err, ErrIntegrity) {
		return err
	}
	if err != nil {
		integrityErr = err
	}

	dur := time.Since(start)
	if integrityErr == nil {
		logger.Info("[integrity] StateVerify key correspondence PASS", "kv", fileName,
			"accounts", fmt.Sprintf("%d/%d", expectedAccounts-accMissing, expectedAccounts),
			"storage", fmt.Sprintf("%d/%d", expectedStorages-stoMissing, expectedStorages),
			"dur", dur)

		// Phase 2: Hash verification — only runs if key correspondence passes.
		numWorkers := dbg.EnvInt("CHECK_VERIFY_STATE_WORKERS", estimate.AlmostAllCPUs())
		hashErr := checkHashVerification(ctx, file, stepSize, failFast, numWorkers, logger)
		if hashErr != nil {
			integrityErr = hashErr
		}
	}
	return integrityErr
}

// missingEntry holds a domain key+value that wasn't found in commitment refs.
type missingEntry struct {
	key []byte
	val []byte
}

// reverseCheckDomainKeys checks that every key in the domain decompressor appears
// in the ETL-sorted commitment refs. Both sides are in ascending order, enabling a merge-join.
//
// prevCommitmentPaths are commitment file paths for previous steps (newest-first).
// When a key is missing from refs, its value is compared with previous domain files
// to detect no-op writes (same value recorded redundantly).
func reverseCheckDomainKeys(ctx context.Context, decomp *seg.Decompressor, domain kv.Domain, sortedKeys *etl.Collector, prevCommitmentPaths []string, commitFileName string, failFast bool, logger log.Logger) (missing uint64, retErr error) {
	compression := statecfg.Schema.GetDomainCfg(domain).Compression
	reader := seg.NewReader(decomp.MakeGetter(), compression)
	reader.Reset(0) // start from beginning

	// Stream sorted commitment refs via goroutine+channel for O(1) memory merge-join.
	refCh := make(chan []byte, 256)
	errCh := make(chan error, 1)
	loadCtx, cancelLoad := context.WithCancel(ctx)
	go func() {
		defer close(refCh)
		errCh <- sortedKeys.Load(nil, "", func(k, v []byte, _ etl.CurrentTableReader, _ etl.LoadNextFunc) error { //nolint:gocritic
			select {
			case refCh <- bytes.Clone(k):
				return nil
			case <-loadCtx.Done():
				return loadCtx.Err()
			}
		}, etl.TransformArgs{Quit: loadCtx.Done()})
	}()
	defer func() {
		cancelLoad()
		if loadErr := <-errCh; loadErr != nil && !errors.Is(loadErr, context.Canceled) && retErr == nil {
			retErr = loadErr
		}
	}()

	// Advance to first ref key.
	refKey, hasRef := <-refCh

	keyBuf := make([]byte, 0, length.Addr+length.Hash)
	valBuf := make([]byte, 0, 128)
	var checked, skippedEmpty uint64
	var missingEntries []missingEntry
	for reader.HasNext() {
		key, _ := reader.Next(keyBuf[:0])
		if !reader.HasNext() {
			break // malformed: key without value
		}
		val, _ := reader.Next(valBuf[:0])

		// Entries with empty values represent deletions — the key was removed
		// from the state trie. Commitment branches only reference live entries,
		// so we skip these.
		if len(val) == 0 {
			skippedEmpty++
			continue
		}

		checked++

		// Advance sorted refs until >= domain key.
		for hasRef && bytes.Compare(refKey, key) < 0 {
			refKey, hasRef = <-refCh
		}

		if !hasRef || !bytes.Equal(refKey, key) {
			// Collect for no-op verification against previous files.
			missingEntries = append(missingEntries, missingEntry{
				key: bytes.Clone(key),
				val: bytes.Clone(val),
			})
		}
	}

	if skippedEmpty > 0 {
		logger.Info("[integrity] StateVerify skipped empty-value entries (deletions)",
			"domain", domain, "skipped", skippedEmpty, "kv", commitFileName)
	}

	if len(missingEntries) == 0 {
		return 0, nil
	}

	// Check missing entries against previous domain files to detect no-op writes.
	genuineMissing := verifyMissingAgainstPrevFiles(missingEntries, domain, prevCommitmentPaths, commitFileName, logger)
	missing = uint64(genuineMissing)

	if missing > 0 {
		retErr = fmt.Errorf("%w: %s %d/%d keys not referenced by commitment in %s",
			ErrIntegrity, domain, missing, checked, commitFileName)
		logger.Warn(retErr.Error())
	}
	return missing, retErr
}

// verifyMissingAgainstPrevFiles checks each missing entry against previous domain files.
// For each missing key, it scans previous files (newest-first) to find the same key.
// If the value matches, it's a no-op write (not an error). If the value differs or
// the key isn't found in any previous file, it's a genuine missing entry.
//
// Uses merge-join within each file: missing entries are sorted by key, and the file
// is scanned sequentially, advancing both cursors in lockstep.
func verifyMissingAgainstPrevFiles(entries []missingEntry, domain kv.Domain, prevCommitmentPaths []string, commitFileName string, logger log.Logger) int {
	if len(prevCommitmentPaths) == 0 {
		// No previous files to check — all are genuine.
		for i, e := range entries {
			if i < 10 {
				logger.Warn("[integrity] StateVerify domain key not in commitment refs (no previous files)",
					"domain", domain, "key", hex.EncodeToString(e.key), "kv", commitFileName)
			}
		}
		return len(entries)
	}

	// Sort missing entries by key for merge-join.
	slices.SortFunc(entries, func(a, b missingEntry) int {
		return bytes.Compare(a.key, b.key)
	})

	// Track which entries are confirmed as no-ops.
	confirmed := make([]bool, len(entries))
	remaining := len(entries)

	compression := statecfg.Schema.GetDomainCfg(domain).Compression

	for _, prevCommitPath := range prevCommitmentPaths {
		if remaining == 0 {
			break
		}

		// Derive the domain file path from the commitment file path.
		prevDomainReader, prevClose, err := deriveReaderForOtherDomain(prevCommitPath, kv.CommitmentDomain, domain)
		if err != nil {
			logger.Warn("[integrity] StateVerify could not open previous domain file",
				"domain", domain, "err", err)
			continue
		}

		// Merge-join: walk the previous file and check all unconfirmed entries.
		_ = compression // reader already has compression configured
		keyBuf := make([]byte, 0, length.Addr+length.Hash)
		valBuf := make([]byte, 0, 128)
		ei := 0 // index into sorted entries

		for prevDomainReader.HasNext() && ei < len(entries) {
			// Skip already confirmed entries.
			for ei < len(entries) && confirmed[ei] {
				ei++
			}
			if ei >= len(entries) {
				break
			}

			prevKey, _ := prevDomainReader.Next(keyBuf[:0])
			if !prevDomainReader.HasNext() {
				break
			}
			prevVal, _ := prevDomainReader.Next(valBuf[:0])

			// Advance entries index past keys that are < prevKey.
			for ei < len(entries) && bytes.Compare(entries[ei].key, prevKey) < 0 {
				ei++
			}
			if ei >= len(entries) {
				break
			}

			if bytes.Equal(entries[ei].key, prevKey) {
				if bytes.Equal(entries[ei].val, prevVal) {
					// No-op write confirmed.
					confirmed[ei] = true
					remaining--
					logger.Info("[integrity] StateVerify no-op write confirmed (same value in previous file)",
						"domain", domain, "key", hex.EncodeToString(entries[ei].key),
						"kv", commitFileName, "prevKv", filepath.Base(prevCommitPath))
				}
				ei++
			}
		}

		prevClose()
	}

	// Report genuinely missing entries.
	genuine := 0
	for i, e := range entries {
		if !confirmed[i] {
			genuine++
			if genuine <= 10 {
				logger.Warn("[integrity] StateVerify domain key not in commitment refs",
					"domain", domain, "key", hex.EncodeToString(e.key), "kv", commitFileName)
			}
		}
	}
	return genuine
}

// extractCommitmentRefsToCollectors walks a commitment .kv file and appends all
// extracted plain keys to the provided ETL collectors. This is used to
// include refs from the "next" commitment file for boundary coverage.
// Opens its own domain readers for dereferencing (the next file's offsets point
// into its own domain files, not the current file's).
func extractCommitmentRefsToCollectors(ctx context.Context, file state.VisibleFile, accCollector, stoCollector *etl.Collector, logger log.Logger) error {
	nextFileName := filepath.Base(file.Fullpath())
	logger.Info("[integrity] StateVerify also extracting refs from next file", "kv", nextFileName)

	commDecomp, err := seg.NewDecompressor(file.Fullpath())
	if err != nil {
		return err
	}
	defer commDecomp.Close()
	commCompression := statecfg.Schema.GetDomainCfg(kv.CommitmentDomain).Compression
	commReader := seg.NewReader(commDecomp.MakeGetter(), commCompression)

	// Always open domain readers: this walk handles both referenced (offset) and plain
	// keys, so it works regardless of the file's version regime.
	_, nextAccReader, nextAccClose, err := deriveDecompAndReaderForOtherDomain(file.Fullpath(), kv.CommitmentDomain, kv.AccountsDomain)
	if err != nil {
		return err
	}
	defer nextAccClose()
	_, nextStoReader, nextStoClose, err := deriveDecompAndReaderForOtherDomain(file.Fullpath(), kv.CommitmentDomain, kv.StorageDomain)
	if err != nil {
		return err
	}
	defer nextStoClose()

	branchKeyBuf := make([]byte, 0, 128)
	branchValueBuf := make([]byte, 0, datasize.MB.Bytes())
	plainKeyBuf := make([]byte, 0, length.Addr+length.Hash)

	for commReader.HasNext() {
		select {
		case <-ctx.Done():
			return ctx.Err()
		default:
		}

		branchKey, _ := commReader.Next(branchKeyBuf[:0])
		if !commReader.HasNext() {
			break
		}
		branchValue, _ := commReader.Next(branchValueBuf[:0])

		if bytes.Equal(branchKey, commitmentdb.KeyCommitmentState) {
			continue
		}

		branchData := commitment.BranchData(branchValue)
		if !branchData.IsComplete() {
			continue
		}

		_, err := branchData.ReplacePlainKeys(nil, func(key []byte, isStorage bool) ([]byte, error) { //nolint:gocritic
			if isStorage {
				plainKey := key
				if len(key) != length.Addr+length.Hash {
					offset := state.DecodeReferenceKey(key)
					if offset >= uint64(nextStoReader.Size()) {
						return key, nil
					}
					nextStoReader.Reset(offset)
					plainKey, _ = nextStoReader.Next(plainKeyBuf[:0])
				}
				if len(plainKey) == length.Addr+length.Hash {
					if err := stoCollector.Collect(plainKey, nil); err != nil {
						return nil, err
					}
				}
				return plainKey, nil
			}
			plainKey := key
			if len(key) != length.Addr {
				offset := state.DecodeReferenceKey(key)
				if offset >= uint64(nextAccReader.Size()) {
					return key, nil
				}
				nextAccReader.Reset(offset)
				plainKey, _ = nextAccReader.Next(plainKeyBuf[:0])
			}
			if len(plainKey) == length.Addr {
				if err := accCollector.Collect(plainKey, nil); err != nil {
					return nil, err
				}
			}
			return plainKey, nil
		})
		if err != nil {
			return err
		}
	}
	return nil
}

// hashWorkItem holds a single commitment branch entry for hash verification.
type hashWorkItem struct {
	branchKey   []byte
	branchValue []byte
}

var valMapPool = sync.Pool{New: func() any { return make(map[string][]byte, 8) }}

// checkHashVerification verifies that stateHash stored in each commitment branch cell
// matches the hash recomputed from the actual domain values. Uses a producer-consumer
// pattern: 1 producer reads the commitment file sequentially, N workers each open their
// own domain readers and verify hashes in parallel.
func checkHashVerification(ctx context.Context, file state.VisibleFile, stepSize uint64, failFast bool, numWorkers int, logger log.Logger) error {
	start := time.Now()
	fileName := filepath.Base(file.Fullpath())

	isReferencing := commitmentFileReferencing(file)

	logger.Info("[integrity] StateVerify hash verification starting",
		"kv", fileName, "workers", numWorkers, "referencing", isReferencing)

	if numWorkers < 1 {
		numWorkers = 1
	}

	// For non-referencing files, preload domain key→value maps so workers
	// can look up values for plain keys. These files are small enough to fit in memory.
	var preloadedAccValues, preloadedStoValues map[string][]byte
	if !isReferencing {
		var err error
		preloadedAccValues, err = preloadDomainValues(file.Fullpath(), kv.CommitmentDomain, kv.AccountsDomain, length.Addr)
		if err != nil {
			return fmt.Errorf("preload accounts: %w", err)
		}
		preloadedStoValues, err = preloadDomainValues(file.Fullpath(), kv.CommitmentDomain, kv.StorageDomain, length.Addr+length.Hash)
		if err != nil {
			return fmt.Errorf("preload storage: %w", err)
		}
		logger.Info("[integrity] StateVerify preloaded domain values",
			"accounts", len(preloadedAccValues), "storage", len(preloadedStoValues), "kv", fileName)
	}

	// Channel for sending work items from producer to workers.
	workCh := make(chan hashWorkItem, numWorkers*4)
	var hashMismatches atomic.Uint64
	var hashChecked atomic.Uint64

	// Set up errgroup with context for cancellation on failure.
	var eg *errgroup.Group
	if failFast {
		eg, ctx = errgroup.WithContext(ctx)
	} else {
		eg = &errgroup.Group{}
	}

	// Launch N worker goroutines.
	for w := 0; w < numWorkers; w++ {
		eg.Go(func() error {
			// Each worker opens its own domain readers for referencing files.
			var accReader, stoReader *seg.Reader
			var accClose, stoClose func()
			if isReferencing {
				var err error
				accReader, accClose, err = deriveReaderForOtherDomain(file.Fullpath(), kv.CommitmentDomain, kv.AccountsDomain)
				if err != nil {
					return fmt.Errorf("worker: open accounts reader: %w", err)
				}
				defer accClose()
				stoReader, stoClose, err = deriveReaderForOtherDomain(file.Fullpath(), kv.CommitmentDomain, kv.StorageDomain)
				if err != nil {
					return fmt.Errorf("worker: open storage reader: %w", err)
				}
				defer stoClose()
			}

			plainKeyBuf := make([]byte, 0, length.Addr+length.Hash)
			valBuf := make([]byte, 0, 128)

			for {
				var (
					item hashWorkItem
					ok   bool
				)
				select {
				case <-ctx.Done():
					return ctx.Err()
				case item, ok = <-workCh:
					if !ok {
						return nil
					}
				}
				if err := verifyHashItem(item, failFast, fileName, isReferencing,
					preloadedAccValues, preloadedStoValues,
					accReader, stoReader,
					plainKeyBuf, valBuf,
					&hashMismatches, &hashChecked,
					logger); err != nil {
					return err
				}
			}
		})
	}

	// Producer: read commitment file and send work items.
	eg.Go(func() error {
		defer close(workCh)

		commDecomp, err := seg.NewDecompressor(file.Fullpath())
		if err != nil {
			return err
		}
		defer commDecomp.Close()
		commCompression := statecfg.Schema.GetDomainCfg(kv.CommitmentDomain).Compression
		commReader := seg.NewReader(commDecomp.MakeGetter(), commCompression)

		branchKeyBuf := make([]byte, 0, 128)
		branchValueBuf := make([]byte, 0, datasize.MB.Bytes())
		logTicker := time.NewTicker(30 * time.Second)
		defer logTicker.Stop()
		var produced uint64

		for commReader.HasNext() {
			select {
			case <-ctx.Done():
				return ctx.Err()
			case <-logTicker.C:
				logger.Info("[integrity] StateVerify hash verification progress",
					"produced", produced,
					"checked", hashChecked.Load(),
					"mismatches", hashMismatches.Load(),
					"kv", fileName)
			default:
			}

			branchKey, _ := commReader.Next(branchKeyBuf[:0])
			if !commReader.HasNext() {
				break
			}
			branchValue, _ := commReader.Next(branchValueBuf[:0])

			if bytes.Equal(branchKey, commitmentdb.KeyCommitmentState) {
				continue
			}

			branchData := commitment.BranchData(branchValue)
			if !branchData.IsComplete() {
				continue
			}

			produced++

			// Copy data since buffers are reused.
			item := hashWorkItem{
				branchKey:   bytes.Clone(branchKey),
				branchValue: bytes.Clone(branchValue),
			}

			select {
			case workCh <- item:
			case <-ctx.Done():
				return ctx.Err()
			}
		}
		return nil
	})

	err := eg.Wait()
	dur := time.Since(start)

	checked := hashChecked.Load()
	mismatches := hashMismatches.Load()

	if err != nil {
		logger.Warn("[integrity] StateVerify hash verification FAIL",
			"checked", checked, "mismatches", mismatches,
			"dur", dur, "kv", fileName, "err", err)
		return err
	}

	logger.Info("[integrity] StateVerify hash verification PASS",
		"checked", checked, "mismatches", mismatches,
		"dur", dur, "kv", fileName)
	return nil
}

func verifyHashItem(
	item hashWorkItem,
	failFast bool,
	fileName string,
	isReferencing bool,
	preloadedAccValues, preloadedStoValues map[string][]byte,
	accReader, stoReader *seg.Reader,
	plainKeyBuf, valBuf []byte,
	hashMismatches, hashChecked *atomic.Uint64,
	logger log.Logger,
) error {
	accountValues := valMapPool.Get().(map[string][]byte)
	storageValues := valMapPool.Get().(map[string][]byte)
	defer func() {
		clear(accountValues)
		clear(storageValues)
		valMapPool.Put(accountValues)
		valMapPool.Put(storageValues)
	}()

	branchData := commitment.BranchData(item.branchValue)
	resolvedBranchData, err := branchData.ReplacePlainKeys(nil, func(key []byte, isStorage bool) ([]byte, error) {
		if isStorage {
			plainKey := key
			isRef := len(key) != length.Addr+length.Hash
			if isRef {
				if !isReferencing {
					return key, nil
				}
				offset := state.DecodeReferenceKey(key)
				if offset >= uint64(stoReader.Size()) {
					return key, nil
				}
				stoReader.Reset(offset)
				plainKey, _ = stoReader.Next(plainKeyBuf[:0])
				if len(plainKey) != length.Addr+length.Hash {
					return key, nil
				}
				val, _ := stoReader.Next(valBuf[:0])
				storageValues[string(plainKey)] = bytes.Clone(val)
			} else if preloadedStoValues != nil {
				strKey := string(plainKey)
				if val, ok := preloadedStoValues[strKey]; ok {
					storageValues[strKey] = val
				}
			}
			return plainKey, nil
		}

		plainKey := key
		isRef := len(key) != length.Addr
		if isRef {
			if !isReferencing {
				return key, nil
			}
			offset := state.DecodeReferenceKey(key)
			if offset >= uint64(accReader.Size()) {
				return key, nil
			}
			accReader.Reset(offset)
			plainKey, _ = accReader.Next(plainKeyBuf[:0])
			if len(plainKey) != length.Addr {
				return key, nil
			}
			val, _ := accReader.Next(valBuf[:0])
			accountValues[string(plainKey)] = bytes.Clone(val)
		} else if preloadedAccValues != nil {
			strKey := string(plainKey)
			if val, ok := preloadedAccValues[strKey]; ok {
				accountValues[strKey] = val
			}
		}
		return plainKey, nil
	})
	if err != nil {
		if failFast {
			return err
		}
		logger.Warn("[integrity] StateVerify hash: ReplacePlainKeys error", "err", err, "kv", fileName)
		return nil
	}

	if len(accountValues) == 0 && len(storageValues) == 0 {
		return nil
	}

	err = commitment.VerifyBranchHashes(item.branchKey, resolvedBranchData, accountValues, storageValues)
	if err != nil {
		hashMismatches.Add(1)
		if failFast {
			return fmt.Errorf("%w: %s in %s", ErrIntegrity, err.Error(), fileName)
		}
		logger.Warn("[integrity] StateVerify hash mismatch", "err", err, "kv", fileName)
		return nil
	}
	hashChecked.Add(1)
	return nil
}

// preloadDomainValues reads all key-value pairs from a domain .kv file into a map.
// Used for non-referencing files where keys are plain (not file offsets).
func preloadDomainValues(commitmentFile string, oldDomain, newDomain kv.Domain, expectedKeyLen int) (map[string][]byte, error) {
	reader, closeFn, err := deriveReaderForOtherDomain(commitmentFile, oldDomain, newDomain)
	if err != nil {
		return nil, err
	}
	defer closeFn()

	values := make(map[string][]byte)
	keyBuf := make([]byte, 0, expectedKeyLen)
	valBuf := make([]byte, 0, 128)
	for reader.HasNext() {
		key, _ := reader.Next(keyBuf[:0])
		if !reader.HasNext() {
			break
		}
		val, _ := reader.Next(valBuf[:0])
		if len(key) == expectedKeyLen {
			values[string(key)] = bytes.Clone(val)
		}
	}
	return values, nil
}

func deriveDecompAndReaderForOtherDomain(baseFile string, oldDomain, newDomain kv.Domain) (*seg.Decompressor, *seg.Reader, func(), error) {
	fileVersionMask, err := version.ReplaceVersionWithMask(baseFile)
	if err != nil {
		return nil, nil, nil, err
	}
	fileVersionMask = strings.Replace(fileVersionMask, oldDomain.String(), newDomain.String(), 1)
	newFile, _, ok, err := version.FindFilesWithVersionsByPattern(fileVersionMask)
	if err != nil {
		return nil, nil, nil, err
	}
	if !ok {
		return nil, nil, nil, fmt.Errorf("could not derive reader for other domain due to file not found: %s,%s->%s", baseFile, oldDomain, newDomain)
	}
	decomp, err := seg.NewDecompressor(newFile)
	if err != nil {
		return nil, nil, nil, err
	}
	compression := statecfg.Schema.GetDomainCfg(newDomain).Compression
	return decomp, seg.NewReader(decomp.MakeGetter(), compression), decomp.Close, nil
}

func touchHistoricalKeys(sd *execctx.SharedDomains, tx kv.TemporalTx, d kv.Domain, fromTxNum uint64, toTxNum uint64, blockNum uint64, idx *ChangedKeysPerBlockIdx, visitor func(k []byte)) (uint64, error) {
	if idx != nil {
		domainIdx := idx[d]
		offsets := domainIdx.Offsets(blockNum)
		for _, off := range offsets {
			k := domainIdx.Key(off)
			if visitor != nil {
				visitor([]byte(k))
			}
			sd.GetCommitmentCtx().TouchKey(d, k, nil)
		}
		return uint64(len(offsets)), nil
	}
	it, err := tx.Debug().HistoryKeyTxNumRange(d, int(fromTxNum), int(toTxNum), order.Asc, -1)
	if err != nil {
		return 0, err
	}
	defer it.Close()
	var touches uint64
	for it.HasNext() {
		k, _, err := it.Next()
		if err != nil {
			return 0, err
		}
		if visitor != nil {
			visitor(k)
		}
		sd.GetCommitmentCtx().TouchKey(d, string(k), nil)
		touches++
	}
	return touches, nil
}
