package core

import (
	"bytes"
	"encoding/binary"
	"errors"
	"fmt"
	"math"
	"math/big"
	"slices"
	"sync"
	"sync/atomic"
	"time"

	lru "github.com/hashicorp/golang-lru/v2"
	json "git.marketally.com/tutus-one/tutus-ordered-json"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config"
	"git.marketally.com/tutus-one/tutus-chain/pkg/config/limits"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/block"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/dao"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/interop/contract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/mempool"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/mpt"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativehashes"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativeids"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/nativenames"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/native/noderoles"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/state"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/stateroot"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/statesync"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/storage"
	"git.marketally.com/tutus-one/tutus-chain/pkg/core/transaction"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/hash"
	"git.marketally.com/tutus-one/tutus-chain/pkg/crypto/keys"
	"git.marketally.com/tutus-one/tutus-chain/pkg/encoding/fixedn"
	"git.marketally.com/tutus-one/tutus-chain/pkg/io"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/callflag"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/manifest"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/scparser"
	"git.marketally.com/tutus-one/tutus-chain/pkg/smartcontract/trigger"
	"git.marketally.com/tutus-one/tutus-chain/pkg/util"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/stackitem"
	"git.marketally.com/tutus-one/tutus-chain/pkg/vm/vmstate"
	"go.uber.org/zap"
)

// Tuning parameters.
const (
	version = "0.2.12"

	// DefaultInitialGAS is the default amount of GAS emitted to the standby validators
	// multisignature account during native GAS contract initialization.
	DefaultInitialGAS                      = 52000000_00000000
	defaultGCPeriod                        = 10000
	defaultMemPoolSize                     = 50000
	defaultP2PNotaryRequestPayloadPoolSize = 1000
	defaultMaxBlockSize                    = 262144
	defaultMaxBlockSystemFee               = 900000000000
	defaultMaxTraceableBlocks              = 2102400 // 1 year of 15s blocks
	defaultMaxTransactionsPerBlock         = 512
	defaultTimePerBlock                    = 15 * time.Second
	// HeaderVerificationGasLimit is the maximum amount of GAS for block header verification.
	HeaderVerificationGasLimit = 3_00000000 // 3 GAS

	// defaultBlockTimesCache should be sufficient for tryRunGC() to get in
	// sync with storeBlock(). Most of the time they differ by some thousands of
	// blocks and GC interval is more like 10K, so this is sufficient for 80K
	// deviation and should be sufficient. If it's not, it's not a big issue
	// either, the next cycle will still do the job (only transfers need this,
	// MPT won't notice at all).
	defaultBlockTimesCache = 8

	// persistSamples is the number of persist velocity samples to use for
	// storeBlock limit.
	persistSamples = 10
	// persistMinForSampling is the minimal number of keys to take persist
	// time into account wrt persist velocity.
	persistMinForSampling = 100
)

// stateChangeStage denotes the stage of state modification process.
type stateChangeStage byte

// A set of stages used to split state jump / state reset into atomic operations.
const (
	// none means that no state jump or state reset process was initiated yet.
	none stateChangeStage = 1 << iota
	// stateJumpStarted means that state jump was just initiated, but outdated storage items
	// were not yet removed.
	stateJumpStarted
	// newStorageItemsAdded means that contract storage items are up-to-date with the current
	// state.
	newStorageItemsAdded
	// staleBlocksRemoved means that state corresponding to the stale blocks (genesis block
	// in case of state jump) was removed from the storage.
	staleBlocksRemoved
	// headersReset denotes stale SYS-prefixed and IX-prefixed information was removed from
	// the storage (applicable to state reset only).
	headersReset
	// transfersReset denotes NEP transfers were successfully updated (applicable to state reset only).
	transfersReset
	// stateResetBit represents a bit identifier for state reset process. If this bit is not set, then
	// it's an unfinished state jump.
	stateResetBit byte = 1 << 7
)

var (
	// ErrAlreadyExists is returned when trying to add some transaction
	// that already exists on chain.
	ErrAlreadyExists = errors.New("already exists in blockchain")
	// ErrAlreadyInPool is returned when trying to add some already existing
	// transaction into the mempool.
	ErrAlreadyInPool = errors.New("already exists in mempool")
	// ErrOOM is returned when adding transaction to the memory pool because
	// it reached its full capacity.
	ErrOOM = errors.New("no space left in the memory pool")
	// ErrPolicy is returned on attempt to add transaction that doesn't
	// comply with node's configured policy into the mempool.
	ErrPolicy = errors.New("not allowed by policy")
	// ErrInvalidBlockIndex is returned when trying to add block with index
	// other than expected height of the blockchain.
	ErrInvalidBlockIndex = errors.New("invalid block index")
	// ErrHasConflicts is returned when trying to add some transaction which
	// conflicts with other transaction in the chain or pool according to
	// Conflicts attribute.
	ErrHasConflicts = errors.New("has conflicts")
)
var (
	persistInterval = 1 * time.Second
)

// Blockchain represents the blockchain. It maintans internal state representing
// the state of the ledger that can be accessed in various ways and changed by
// adding new blocks or headers.
type Blockchain struct {
	HeaderHashes

	config config.Blockchain

	// The only way chain state changes is by adding blocks, so we can't
	// allow concurrent block additions. It differs from the next lock in
	// that it's only for AddBlock method itself, the chain state is
	// protected by the lock below, but holding it during all of AddBlock
	// is too expensive (because the state only changes when persisting
	// change cache).
	addLock sync.Mutex

	// This lock ensures blockchain immutability for operations that need
	// that while performing their tasks. It's mostly used as a read lock
	// with the only writer being the block addition logic.
	lock sync.RWMutex

	// Data access object for CRUD operations around storage. It's write-cached.
	dao *dao.Simple

	// persistent is the same DB as dao, but we never write to it, so all reads
	// are directly from underlying persistent store.
	persistent *dao.Simple

	// Underlying persistent store.
	store storage.Store

	// Current index/height of the highest block.
	// Read access should always be called by BlockHeight().
	// Write access should only happen in storeBlock().
	blockHeight uint32

	// Current top Block wrapped in an atomic.Value for safe access.
	topBlock atomic.Value

	// Current persisted block count.
	persistedHeight uint32

	// keysPerPersist is the average number of persisted keys per persist
	// time limit.
	keysPerPersist uint32

	// persistCond is signaled each time persist cycle ends, this wakes
	// storeBlock if needed (when it has too many queued blocks)
	persistCond *sync.Cond

	// Index->Timestamp cache for garbage collector. Headers can be gone
	// by the time it runs, so we use a tiny little cache to sync block
	// removal (performed in storeBlock()) with transfer/MPT GC (tryRunGC())
	gcBlockTimes *lru.Cache[uint32, uint64]

	// gcLastUntraceableBlockHeight is the height of the latest untraceable block
	// that was removed by GC. Storing this value is cheaper than performing Seek
	// through all blocks every time we need to define the next header to be
	// removed.
	gcLastUntraceableBlockHeight uint32

	// Stop synchronization mechanisms.
	stopCh      chan struct{}
	runToExitCh chan struct{}
	// isRunning denotes whether blockchain routines are currently running.
	isRunning atomic.Value

	memPool *mempool.Pool

	// postBlock is a set of callback methods which should be run under the Blockchain lock after new block is persisted.
	// Block's transactions are passed via mempool.
	postBlock []func(func(*transaction.Transaction, *mempool.Pool, bool) bool, *mempool.Pool, *block.Block)

	log *zap.Logger

	lastBatch *storage.MemBatch

	contracts  *native.Contracts
	management native.IManagement
	policy     native.IPolicy
	tutus      native.ITutus
	lub        native.ILub
	designate  native.IDesignate
	oracle     native.IOracle
	notary     native.INotary
	vita       native.IVita
	federation native.IFederation
	treasury   native.ITreasury
	lex        native.ILex
	eligere    native.IEligere
	scire      native.IScire
	salus      native.ISalus
	sese       native.ISese
	tribute    native.ITribute
	opus       native.IOpus

	extensible atomic.Value

	// knownValidatorsCount is the latest known validators count used
	// for defaultBlockWitness.
	knownValidatorsCount atomic.Value
	// defaultBlockWitness stores transaction.Witness with m out of n multisig,
	// where n = knownValidatorsCount.
	defaultBlockWitness atomic.Value

	stateRoot *stateroot.Module

	// Notification subsystem.
	events  chan bcEvent
	subCh   chan any
	unsubCh chan any
}

// StateRoot represents local state root module.
type StateRoot interface {
	CurrentLocalHeight() uint32
	CurrentLocalStateRoot() util.Uint256
	CurrentValidatedHeight() uint32
	FindStates(root util.Uint256, prefix, start []byte, maxNum int) ([]storage.KeyValue, error)
	SeekStates(root util.Uint256, prefix []byte, f func(k, v []byte) bool)
	GetState(root util.Uint256, key []byte) ([]byte, error)
	GetStateProof(root util.Uint256, key []byte) ([][]byte, error)
	GetStateRoot(height uint32) (*state.MPTRoot, error)
	GetLatestStateHeight(root util.Uint256) (uint32, error)
}

// bcEvent is an internal event generated by the Blockchain and then
// broadcasted to other parties. It joins the new block and associated
// invocation logs, all the other events visible from outside can be produced
// from this combination.
type bcEvent struct {
	block          *block.Block
	appExecResults []*state.AppExecResult
}

// transferData is used for transfer caching during storeBlock.
type transferData struct {
	Info  state.TokenTransferInfo
	Log11 state.TokenTransferLog
	Log17 state.TokenTransferLog
}

// NewBlockchain returns a new blockchain object the will use the
// given Store as its underlying storage. For it to work correctly you need
// to spawn a goroutine for its Run method after this initialization. As an
// optional argument it accepts constructor of native contracts that, if set,
// will be used to allocate and override the list of native contracts with a
// custom implementation. It's required to keep the same IDs, names and hashes
// for those contracts that override existing natives; new contract IDs should
// follow next to the nativeids.Notary ID.
func NewBlockchain(s storage.Store, cfg config.Blockchain, log *zap.Logger, newNatives ...func(cfg config.ProtocolConfiguration) []interop.Contract) (*Blockchain, error) {
	if log == nil {
		return nil, errors.New("empty logger")
	}

	// Protocol configuration fixups/checks.
	if cfg.InitialLubSupply <= 0 {
		cfg.InitialLubSupply = fixedn.Fixed8(DefaultInitialGAS)
		log.Info("initial Lub supply is not set or wrong, setting default value", zap.Stringer("InitialLubSupply", cfg.InitialLubSupply))
	}
	if cfg.MemPoolSize <= 0 {
		cfg.MemPoolSize = defaultMemPoolSize
		log.Info("mempool size is not set or wrong, setting default value", zap.Int("MemPoolSize", cfg.MemPoolSize))
	}
	if cfg.P2PSigExtensions && cfg.P2PNotaryRequestPayloadPoolSize <= 0 {
		cfg.P2PNotaryRequestPayloadPoolSize = defaultP2PNotaryRequestPayloadPoolSize
		log.Info("P2PNotaryRequestPayloadPool size is not set or wrong, setting default value", zap.Int("P2PNotaryRequestPayloadPoolSize", cfg.P2PNotaryRequestPayloadPoolSize))
	}
	if cfg.MaxBlockSize == 0 {
		cfg.MaxBlockSize = defaultMaxBlockSize
		log.Info("MaxBlockSize is not set or wrong, setting default value", zap.Uint32("MaxBlockSize", cfg.MaxBlockSize))
	}
	if cfg.MaxBlockSystemFee <= 0 {
		cfg.MaxBlockSystemFee = defaultMaxBlockSystemFee
		log.Info("MaxBlockSystemFee is not set or wrong, setting default value", zap.Int64("MaxBlockSystemFee", cfg.MaxBlockSystemFee))
	}
	if cfg.MaxTraceableBlocks == 0 {
		cfg.MaxTraceableBlocks = defaultMaxTraceableBlocks
		log.Info("MaxTraceableBlocks is not set or wrong, using default value", zap.Uint32("MaxTraceableBlocks", cfg.MaxTraceableBlocks))
	}
	if cfg.Genesis.MaxTraceableBlocks == 0 {
		cfg.Genesis.MaxTraceableBlocks = cfg.MaxTraceableBlocks
		log.Info("Genesis MaxTraceableBlocks is not set or wrong, using default value",
			zap.Uint32("Genesis MaxTraceableBlocks", cfg.Genesis.MaxTraceableBlocks))
	}
	if cfg.MaxTransactionsPerBlock == 0 {
		cfg.MaxTransactionsPerBlock = defaultMaxTransactionsPerBlock
		log.Info("MaxTransactionsPerBlock is not set or wrong, using default value",
			zap.Uint16("MaxTransactionsPerBlock", cfg.MaxTransactionsPerBlock))
	}
	if cfg.TimePerBlock <= 0 {
		cfg.TimePerBlock = defaultTimePerBlock
		log.Info("TimePerBlock is not set or wrong, using default value",
			zap.Duration("TimePerBlock", cfg.TimePerBlock))
	} else if cfg.TimePerBlock%time.Millisecond != 0 {
		return nil, errors.New("TimePerBlock must be an integer number of milliseconds")
	}
	if cfg.Genesis.TimePerBlock <= 0 {
		cfg.Genesis.TimePerBlock = cfg.TimePerBlock
		log.Info("Genesis TimePerBlock is not set or wrong, using default value",
			zap.Duration("Genesis TimePerBlock", cfg.Genesis.TimePerBlock))
	} else if cfg.Genesis.TimePerBlock%time.Millisecond != 0 {
		return nil, errors.New("Genesis.TimePerBlock must be an integer number of milliseconds")
	}
	if cfg.MaxValidUntilBlockIncrement == 0 {
		const timePerDay = 24 * time.Hour

		cfg.MaxValidUntilBlockIncrement = uint32(timePerDay / cfg.TimePerBlock)
		log.Info("MaxValidUntilBlockIncrement is not set or wrong, using default value",
			zap.Uint32("MaxValidUntilBlockIncrement", cfg.MaxValidUntilBlockIncrement))
	}
	if cfg.Genesis.MaxValidUntilBlockIncrement == 0 {
		cfg.Genesis.MaxValidUntilBlockIncrement = cfg.MaxValidUntilBlockIncrement
		log.Info("Genesis MaxValidUntilBlockIncrement is not set or wrong, using default value",
			zap.Uint32("Genesis MaxValidUntilBlockIncrement", cfg.Genesis.MaxValidUntilBlockIncrement))
	}
	if cfg.P2PStateExchangeExtensions && cfg.NeoFSStateSyncExtensions {
		return nil, errors.New("P2PStateExchangeExtensions and NeoFSStateSyncExtensions cannot be enabled simultaneously")
	}
	if cfg.TrustedHeader.Index > 0 && !cfg.P2PStateExchangeExtensions && !cfg.NeoFSStateSyncExtensions {
		return nil, errors.New("TrustedHeader can not be used without P2PStateExchangeExtensions or NeoFSStateSyncExtensions")
	}
	if cfg.TrustedHeader.Index == 0 && (cfg.P2PStateExchangeExtensions || cfg.NeoFSStateSyncExtensions) {
		log.Info("TrustedHeader is not set, headers synchronisation will start from latest stored header")
	}
	if cfg.P2PStateExchangeExtensions {
		if !cfg.StateRootInHeader {
			return nil, errors.New("P2PStatesExchangeExtensions are enabled, but StateRootInHeader is off")
		}
		if cfg.KeepOnlyLatestState && !cfg.RemoveUntraceableBlocks {
			return nil, errors.New("P2PStateExchangeExtensions can be enabled either on MPT-complete node (KeepOnlyLatestState=false) or on light GC-enabled node (RemoveUntraceableBlocks=true)")
		}
		if cfg.StateSyncInterval <= 0 {
			cfg.StateSyncInterval = config.DefaultStateSyncInterval
			log.Info("StateSyncInterval is not set or wrong, using default value",
				zap.Int("StateSyncInterval", cfg.StateSyncInterval))
		}
	}
	if cfg.NeoFSStateSyncExtensions {
		if !cfg.NeoFSBlockFetcher.Enabled || !cfg.NeoFSStateFetcher.Enabled {
			return nil, errors.New("NeoFSStateSyncExtensions are enabled, but NeoFSBlockFetcher or NeoFSStateFetcher are off")
		}
		if !cfg.RemoveUntraceableBlocks {
			return nil, errors.New("NeoFSStateFetcher cannot be used on archival nodes; set RemoveUntraceableBlocks=true")
		}
		if cfg.StateSyncInterval <= 0 {
			cfg.StateSyncInterval = config.DefaultStateSyncInterval
			log.Info("StateSyncInterval is not set or wrong, using default value",
				zap.Int("StateSyncInterval", cfg.StateSyncInterval))
		}
	}
	if cfg.Hardforks == nil {
		cfg.Hardforks = map[string]uint32{}
		for _, hf := range config.StableHardforks {
			cfg.Hardforks[hf.String()] = 0
		}
		log.Info("Hardforks are not set, enabling all stable ones", zap.Stringer("latest", config.HFLatestStable))
	} else if len(cfg.Hardforks) != 0 {
		// Explicitly set the height of all old omitted hardforks to 0 for proper
		// IsHardforkEnabled behaviour.
		for _, hf := range config.Hardforks {
			if _, ok := cfg.Hardforks[hf.String()]; !ok {
				cfg.Hardforks[hf.String()] = 0
				continue
			}
			break
		}
	}

	// Local config consistency checks.
	if cfg.RemoveUntraceableBlocks && cfg.GarbageCollectionPeriod == 0 {
		cfg.GarbageCollectionPeriod = defaultGCPeriod
		log.Info("GarbageCollectionPeriod is not set or wrong, using default value", zap.Uint32("GarbageCollectionPeriod", cfg.GarbageCollectionPeriod))
	}
	var natives []interop.Contract
	if len(newNatives) != 0 && newNatives[0] != nil {
		natives = newNatives[0](cfg.ProtocolConfiguration)
	} else {
		natives = native.NewDefaultContracts(cfg.ProtocolConfiguration)
	}

	bc := &Blockchain{
		config:      cfg,
		dao:         dao.NewSimple(s, cfg.StateRootInHeader),
		persistent:  dao.NewSimple(s, cfg.StateRootInHeader),
		store:       s,
		stopCh:      make(chan struct{}),
		runToExitCh: make(chan struct{}),
		memPool:     mempool.New(cfg.MemPoolSize, 0, cfg.MempoolSubscriptionsEnabled, updateMempoolMetrics),
		log:         log,
		events:      make(chan bcEvent),
		subCh:       make(chan any),
		unsubCh:     make(chan any),
		contracts:   native.NewContracts(natives),
	}
	bc.management = bc.contracts.Management()
	if bc.management == native.IManagement(nil) {
		return nil, errors.New("native ContractManagement implementation is required")
	}
	if err := validateNative(bc.management, nativeids.ContractManagement, nativenames.Management, nativehashes.ContractManagement); err != nil {
		return nil, err
	}
	bc.tutus = bc.contracts.Tutus()
	if bc.tutus == native.ITutus(nil) {
		return nil, errors.New("native TutusToken implementation is required")
	}
	if err := validateNative(bc.tutus, nativeids.Tutus, nativenames.Tutus, nativehashes.Tutus); err != nil {
		return nil, err
	}
	bc.lub = bc.contracts.Lub()
	if bc.lub == native.ILub(nil) {
		return nil, errors.New("native LubToken implementation is required")
	}
	if err := validateNative(bc.lub, nativeids.Lub, nativenames.Lub, nativehashes.Lub); err != nil {
		return nil, err
	}
	bc.policy = bc.contracts.Policy()
	if bc.policy == native.IPolicy(nil) {
		return nil, errors.New("native Policy implementation is required")
	}
	if err := validateNative(bc.policy, nativeids.PolicyContract, nativenames.Policy, nativehashes.PolicyContract); err != nil {
		return nil, err
	}
	bc.designate = bc.contracts.Designate()
	if bc.designate == native.IDesignate(nil) {
		return nil, errors.New("native RoleManagement implementation is required")
	}
	if err := validateNative(bc.designate, nativeids.RoleManagement, nativenames.Designation, nativehashes.RoleManagement); err != nil {
		return nil, err
	}
	bc.oracle = bc.contracts.Oracle()
	if bc.oracle != native.IOracle(nil) {
		if err := validateNative(bc.oracle, nativeids.OracleContract, nativenames.Oracle, nativehashes.OracleContract); err != nil {
			return nil, err
		}
	}
	bc.notary = bc.contracts.Notary()
	if bc.notary != native.INotary(nil) {
		if err := validateNative(bc.notary, nativeids.Notary, nativenames.Notary, nativehashes.Notary); err != nil {
			return nil, err
		}
	}
	bc.vita = bc.contracts.Vita()
	if err := validateNative(bc.vita, nativeids.Vita, nativenames.Vita, nativehashes.Vita); err != nil {
		return nil, err
	}
	bc.federation = bc.contracts.Federation()
	if err := validateNative(bc.federation, nativeids.Federation, nativenames.Federation, nativehashes.Federation); err != nil {
		return nil, err
	}
	bc.treasury = bc.contracts.Treasury()
	if err := validateNative(bc.treasury, nativeids.Treasury, nativenames.Treasury, nativehashes.Treasury); err != nil {
		return nil, err
	}
	bc.lex = bc.contracts.Lex()
	if err := validateNative(bc.lex, nativeids.Lex, nativenames.Lex, nativehashes.Lex); err != nil {
		return nil, err
	}
	bc.eligere = bc.contracts.Eligere()
	if err := validateNative(bc.eligere, nativeids.Eligere, nativenames.Eligere, nativehashes.Eligere); err != nil {
		return nil, err
	}
	bc.scire = bc.contracts.Scire()
	if err := validateNative(bc.scire, nativeids.Scire, nativenames.Scire, nativehashes.Scire); err != nil {
		return nil, err
	}
	bc.salus = bc.contracts.Salus()
	if err := validateNative(bc.salus, nativeids.Salus, nativenames.Salus, nativehashes.Salus); err != nil {
		return nil, err
	}
	bc.sese = bc.contracts.Sese()
	if err := validateNative(bc.sese, nativeids.Sese, nativenames.Sese, nativehashes.Sese); err != nil {
		return nil, err
	}
	bc.tribute = bc.contracts.Tribute()
	if err := validateNative(bc.tribute, nativeids.Tribute, nativenames.Tribute, nativehashes.Tribute); err != nil {
		return nil, err
	}
	bc.opus = bc.contracts.Opus()
	if err := validateNative(bc.opus, nativeids.Opus, nativenames.Opus, nativehashes.Opus); err != nil {
		return nil, err
	}

	bc.persistCond = sync.NewCond(&bc.lock)
	bc.gcBlockTimes, _ = lru.New[uint32, uint64](defaultBlockTimesCache) // Never errors for positive size
	bc.stateRoot = stateroot.NewModule(cfg, bc.VerifyWitness, bc.log, bc.dao.Store)
	bc.designate.SetStateRootService(bc.stateRoot)

	if err := bc.init(); err != nil {
		return nil, err
	}

	bc.isRunning.Store(false)
	return bc, nil
}

// validateNative ensures that native contract is constructed according to the
// Blockchain expectations.
func validateNative(n interop.Contract, expectedID int32, expectedName string, expectedHash util.Uint160) error {
	md := n.Metadata()
	if md.ID != expectedID {
		return fmt.Errorf("native %s: expected ID %d, got %d", md.Name, expectedID, md.ID)
	}
	if md.Name != expectedName {
		return fmt.Errorf("native %s: expected name %s, got %s", md.Name, expectedName, md.Name)
	}
	if md.Hash != expectedHash {
		return fmt.Errorf("native %s: expected hash %s, got %s", md.Name, expectedHash.StringLE(), md.Hash.StringLE())
	}
	return nil
}

// GetDesignatedByRole returns a set of designated public keys for the given role
// relevant for the next block.
func (bc *Blockchain) GetDesignatedByRole(r noderoles.Role) (keys.PublicKeys, uint32, error) {
	// Retrieve designated nodes starting from the next block, because the current
	// block is already stored, thus, dependent services can't use PostPersist callback
	// to fetch relevant information at their start.
	res, h, err := bc.designate.GetDesignatedByRole(bc.dao, r, bc.BlockHeight()+1)
	return res, h, err
}

// getCurrentHF returns the latest currently enabled hardfork. In case if no hardforks are enabled, the
// default config.Hardfork(0) value is returned.
func (bc *Blockchain) getCurrentHF() config.Hardfork {
	var (
		height  = bc.BlockHeight()
		current config.Hardfork
	)
	// Rely on the fact that hardforks list is continuous.
	for _, hf := range config.Hardforks {
		enableHeight, ok := bc.config.Hardforks[hf.String()]
		if !ok || height < enableHeight {
			break
		}
		current = hf
	}
	return current
}

// SetOracle sets oracle module. It can safely be called on the running blockchain.
// To unregister Oracle service use SetOracle(nil).
func (bc *Blockchain) SetOracle(mod native.OracleService) {
	orc := bc.oracle
	if orc == native.IOracle(nil) {
		panic("native Oracle contract implementation is not initialized")
	}
	currentHF := bc.getCurrentHF()
	if mod != nil {
		orcMd := orc.Metadata().HFSpecificContractMD(&currentHF)
		md, ok := orcMd.GetMethod(manifest.MethodVerify, -1)
		if !ok {
			panic(fmt.Errorf("%s method not found", manifest.MethodVerify))
		}
		mod.UpdateNativeContract(orcMd.NEF.Script, orc.GetOracleResponseScript(),
			orc.Metadata().Hash, md.MD.Offset)
		keys, _, err := bc.GetDesignatedByRole(noderoles.Oracle)
		if err != nil {
			bc.log.Error("failed to get oracle key list")
			return
		}
		mod.UpdateOracleNodes(keys)
		reqs, err := orc.GetRequests(bc.dao)
		if err != nil {
			bc.log.Error("failed to get current oracle request list")
			return
		}
		mod.AddRequests(reqs)
	}
	orc.SetService(mod)
	bc.designate.SetOracleService(mod)
}

// SetNotary sets notary module. It may safely be called on the running blockchain.
// To unregister Notary service use SetNotary(nil).
func (bc *Blockchain) SetNotary(mod native.NotaryService) {
	if mod != nil {
		keys, _, err := bc.GetDesignatedByRole(noderoles.P2PNotary)
		if err != nil {
			bc.log.Error("failed to get notary key list")
			return
		}
		mod.UpdateNotaryNodes(keys)
	}
	bc.designate.SetNotaryService(mod)
}

func (bc *Blockchain) init() error {
	// If we could not find the version in the Store, we know that there is nothing stored.
	ver, err := bc.dao.GetVersion()
	if err != nil {
		bc.log.Info("no storage version found! creating genesis block")
		ver = dao.Version{
			StoragePrefix:              storage.STStorage,
			StateRootInHeader:          bc.config.StateRootInHeader,
			P2PSigExtensions:           bc.config.P2PSigExtensions,
			P2PStateExchangeExtensions: bc.config.P2PStateExchangeExtensions,
			KeepOnlyLatestState:        bc.config.KeepOnlyLatestState,
			Magic:                      uint32(bc.config.Magic),
			Value:                      version,
			SaveInvocations:            bc.config.SaveInvocations,
		}
		bc.dao.PutVersion(ver)
		bc.dao.Version = ver
		bc.persistent.Version = ver
		genesisBlock, err := CreateGenesisBlock(bc.config.ProtocolConfiguration)
		if err != nil {
			return err
		}
		var trusted = config.HashIndex{
			Hash:  genesisBlock.Hash(),
			Index: 0,
		}
		if bc.config.TrustedHeader.Index > 0 {
			minTrustedHeight := max(uint32(2*bc.config.StateSyncInterval), bc.GetMaxTraceableBlocks())
			if (bc.config.P2PStateExchangeExtensions || bc.config.NeoFSStateSyncExtensions) &&
				bc.config.TrustedHeader.Index <= minTrustedHeight {
				return fmt.Errorf("trusted header is too low to start state synchronization: need at least %d, got %d", minTrustedHeight, bc.config.TrustedHeader.Index)
			}
			trusted = bc.config.TrustedHeader
		}
		bc.initMinTrustedHeader(bc.dao, trusted)
		if err := bc.stateRoot.Init(0); err != nil {
			return fmt.Errorf("can't init MPT: %w", err)
		}
		return bc.storeBlock(genesisBlock, nil)
	}
	if ver.Value != version {
		return fmt.Errorf("storage version mismatch (expected=%s, actual=%s)", version, ver.Value)
	}
	if ver.StateRootInHeader != bc.config.StateRootInHeader {
		return fmt.Errorf("StateRootInHeader setting mismatch (config=%t, db=%t)",
			bc.config.StateRootInHeader, ver.StateRootInHeader)
	}
	if ver.P2PSigExtensions != bc.config.P2PSigExtensions {
		return fmt.Errorf("P2PSigExtensions setting mismatch (old=%t, new=%t)",
			ver.P2PSigExtensions, bc.config.P2PSigExtensions)
	}
	if ver.P2PStateExchangeExtensions != bc.config.P2PStateExchangeExtensions {
		return fmt.Errorf("P2PStateExchangeExtensions setting mismatch (old=%t, new=%t)",
			ver.P2PStateExchangeExtensions, bc.config.P2PStateExchangeExtensions)
	}
	if ver.KeepOnlyLatestState != bc.config.KeepOnlyLatestState {
		return fmt.Errorf("KeepOnlyLatestState setting mismatch (old=%v, new=%v)",
			ver.KeepOnlyLatestState, bc.config.KeepOnlyLatestState)
	}
	if ver.Magic != uint32(bc.config.Magic) {
		return fmt.Errorf("protocol configuration Magic mismatch (old=%v, new=%v)",
			ver.Magic, bc.config.Magic)
	}
	if ver.SaveInvocations != bc.config.SaveInvocations {
		return fmt.Errorf("SaveInvocations setting mismatch (old=%v, new=%v)",
			ver.SaveInvocations, bc.config.SaveInvocations)
	}
	bc.dao.Version = ver
	bc.persistent.Version = ver

	// At this point there was no version found in the storage which
	// implies a creating fresh storage with the version specified
	// and the genesis block as first block.
	bc.log.Info("restoring blockchain", zap.String("version", version))

	err = bc.HeaderHashes.init(bc.dao, bc.config.TrustedHeader)
	if err != nil {
		return err
	}

	// Check whether StateChangeState stage is in the storage and continue interrupted state jump / state reset if so.
	stateChStage, err := bc.dao.Store.Get([]byte{byte(storage.SYSStateChangeStage)})
	if err == nil {
		if len(stateChStage) != 1 {
			return fmt.Errorf("invalid state jump stage format")
		}
		// State jump / state reset wasn't finished yet, thus continue it.
		stateSyncPoint, err := bc.dao.GetStateSyncPoint()
		if err != nil {
			return fmt.Errorf("failed to get state sync point from the storage")
		}
		if (stateChStage[0] & stateResetBit) != 0 {
			return bc.resetStateInternal(stateSyncPoint, stateChangeStage(stateChStage[0]&(^stateResetBit)))
		}
		if (!bc.config.P2PStateExchangeExtensions || !bc.config.RemoveUntraceableBlocks) && !bc.config.NeoFSStateSyncExtensions {
			return errors.New("state jump was not completed, P2PStateExchangeExtensions (with RemoveUntraceableBlocks) " +
				"or NeoFSStateSyncExtensions must be enabled. " +
				"To start an archival node drop the database manually and restart the node")
		}
		return bc.jumpToStateInternal(stateSyncPoint, stateChangeStage(stateChStage[0]))
	}

	bHeight, err := bc.dao.GetCurrentBlockHeight()
	if err != nil {
		return fmt.Errorf("failed to retrieve current block height: %w", err)
	}
	bc.blockHeight = bHeight
	bc.persistedHeight = bHeight

	bc.dao.Store.Seek(storage.SeekRange{
		Prefix: []byte{byte(storage.IXHeaderHashList)},
	}, func(k, _ []byte) bool {
		bc.gcLastUntraceableBlockHeight = binary.BigEndian.Uint32(k[1:])
		return false
	})

	bc.log.Debug("initializing caches", zap.Uint32("blockHeight", bHeight))
	if err = bc.stateRoot.Init(bHeight); err != nil {
		return fmt.Errorf("can't init MPT at height %d: %w", bHeight, err)
	}

	err = bc.initializeNativeCache(bc.blockHeight, bc.dao)
	if err != nil {
		return fmt.Errorf("can't init natives cache: %w", err)
	}

	// Check autogenerated native contracts' manifests and NEFs against the stored ones.
	// Need to be done after native Management cache initialization to be able to get
	// contract state from DAO via high-level bc API.
	var current = bc.getCurrentHF()
	for _, c := range bc.contracts.List {
		md := c.Metadata()
		storedCS := bc.GetContractState(md.Hash)
		// Check that contract was deployed.
		if !bc.IsHardforkEnabled(c.ActiveIn(), bHeight) {
			if storedCS != nil {
				return fmt.Errorf("native contract %s is already stored, but marked as inactive for height %d in config", md.Name, bHeight)
			}
			continue
		}
		if storedCS == nil {
			return fmt.Errorf("native contract %s is not stored, but should be active at height %d according to config", md.Name, bHeight)
		}
		storedCSBytes, err := stackitem.SerializeConvertible(storedCS)
		if err != nil {
			return fmt.Errorf("failed to check native %s state against autogenerated one: %w", md.Name, err)
		}
		hfMD := md.HFSpecificContractMD(&current)
		autogenCS := &state.Contract{
			ContractBase:  hfMD.ContractBase,
			UpdateCounter: storedCS.UpdateCounter, // it can be restored only from the DB, so use the stored value.
		}
		autogenCSBytes, err := stackitem.SerializeConvertible(autogenCS)
		if err != nil {
			return fmt.Errorf("failed to check native %s state against autogenerated one: %w", md.Name, err)
		}
		if !bytes.Equal(storedCSBytes, autogenCSBytes) {
			storedJ, _ := json.Marshal(storedCS)
			autogenJ, _ := json.Marshal(autogenCS)
			return fmt.Errorf("native %s: version mismatch for the latest hardfork %s (stored contract state differs from autogenerated one), "+
				"try to resynchronize the node from the genesis: %s vs %s", md.Name, current, string(storedJ), string(autogenJ))
		}
	}

	updateBlockHeightMetric(bHeight)
	updatePersistedHeightMetric(bHeight)
	updateHeaderHeightMetric(bc.HeaderHeight())

	return bc.updateExtensibleWhitelist(bHeight)
}

// jumpToState is an atomic operation that changes Blockchain state to the one
// specified by the state sync point p. All the data needed for the jump must be
// collected by the state sync module.
func (bc *Blockchain) jumpToState(p uint32) error {
	bc.addLock.Lock()
	bc.lock.Lock()
	defer bc.lock.Unlock()
	defer bc.addLock.Unlock()

	return bc.jumpToStateInternal(p, none)
}

// jumpToStateInternal is an internal representation of jumpToState callback that
// changes Blockchain state to the one specified by state sync point p and state
// jump stage. All the data needed for the jump must be in the DB, otherwise an
// error is returned. It is not protected by mutex.
func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateChangeStage) error {
	if p >= bc.HeaderHeight() {
		return fmt.Errorf("invalid state sync point %d: headerHeignt is %d", p, bc.HeaderHeight())
	}

	bc.log.Info("jumping to state sync point", zap.Uint32("state sync point", p))
	start := time.Now()

	jumpStageKey := []byte{byte(storage.SYSStateChangeStage)}
	switch stage {
	case none:
		bc.dao.Store.Put(jumpStageKey, []byte{byte(stateJumpStarted)})
		_, err := bc.dao.Store.Persist()
		if err != nil {
			return fmt.Errorf("failed to persist %d stage of state jump: %w", none, err)
		}
		fallthrough
	case stateJumpStarted:
		newPrefix := statesync.TemporaryPrefix(bc.dao.Version.StoragePrefix)
		v, err := bc.dao.GetVersion()
		if err != nil {
			return fmt.Errorf("failed to get dao.Version: %w", err)
		}
		v.StoragePrefix = newPrefix
		bc.dao.PutVersion(v)
		bc.persistent.Version = v

		bc.dao.Store.Put(jumpStageKey, []byte{byte(newStorageItemsAdded)})
		_, err = bc.dao.Store.Persist()
		if err != nil {
			return fmt.Errorf("failed to persist %d stage of state jump: %w", stateJumpStarted, err)
		}

		fallthrough
	case newStorageItemsAdded:
		cache := bc.dao.GetPrivate()
		prefix := statesync.TemporaryPrefix(bc.dao.Version.StoragePrefix)
		bc.dao.Store.Seek(storage.SeekRange{Prefix: []byte{byte(prefix)}}, func(k, _ []byte) bool {
			// #1468, but don't need to copy here, because it is done by Store.
			cache.Store.Delete(k)
			return true
		})

		var (
			mtb = int64(bc.config.MaxTraceableBlocks)
			hf  = config.HFEchidna
			err error
		)
		if bc.IsHardforkEnabled(&hf, p) {
			// Native cache is not yet initialized, retrieve MaxTraceableBlocks from DAO directly using
			// new storage prefix since we already have up-to-date storage.
			mtb, err = bc.dao.GetInt(bc.policy.Metadata().ID, native.MaxTraceableBlocksKey)
			if err != nil {
				return fmt.Errorf("failed to get MaxTraceableBlocks from DAO: %w", err)
			}
		}
		// After current state is updated, we need to remove outdated state-related data if so.
		// The only outdated data we might have is genesis-related data, so check it.
		if int(p)-int(mtb) > 0 {
			// bc.HeaderHashes does not contain genesis hash since old hashes are removed with RUB.
			genesisBlock, err := CreateGenesisBlock(bc.config.ProtocolConfiguration)
			if err != nil {
				return fmt.Errorf("failed to retrieve genesis block hash: %w", err)
			}
			_, err = cache.DeleteBlock(genesisBlock.Hash())
			if err != nil {
				return fmt.Errorf("failed to remove outdated state data for the genesis block: %w", err)
			}
			prefixes := []byte{byte(storage.STNEP11Transfers), byte(storage.STNEP17Transfers), byte(storage.STTokenTransferInfo)}
			for i := range prefixes {
				cache.Store.Seek(storage.SeekRange{Prefix: prefixes[i : i+1]}, func(k, v []byte) bool {
					cache.Store.Delete(k)
					return true
				})
			}
		}
		// Update SYS-prefixed info.
		block, err := bc.dao.GetBlock(bc.GetHeaderHash(p))
		if err != nil {
			return fmt.Errorf("failed to get current block: %w", err)
		}
		cache.StoreAsCurrentBlock(block)
		cache.Store.Put(jumpStageKey, []byte{byte(staleBlocksRemoved)})
		_, err = cache.Persist()
		if err != nil {
			return fmt.Errorf("failed to persist old items removal: %w", err)
		}
		_, err = bc.dao.Store.Persist()
		if err != nil {
			return fmt.Errorf("failed to persist %d stage of state jump: %w", newStorageItemsAdded, err)
		}
	case staleBlocksRemoved:
		// there's nothing to do after that, so just continue with common operations
		// and remove state jump stage in the end.
	default:
		return fmt.Errorf("unknown state jump stage: %d", stage)
	}
	var root util.Uint256
	if bc.config.NeoFSStateSyncExtensions {
		ckpt, err := bc.dao.GetStateSyncCheckpoint()
		if err != nil {
			return fmt.Errorf("failed to get checkpoint metadata: %w", err)
		}
		root = ckpt.MPTRoot
	} else {
		blk, err := bc.dao.GetBlock(bc.GetHeaderHash(p + 1))
		if err != nil {
			return fmt.Errorf("failed to get block to init MPT: %w", err)
		}
		root = blk.PrevStateRoot
	}
	bc.stateRoot.JumpToState(&state.MPTRoot{
		Index: p,
		Root:  root,
	})

	bc.dao.Store.Delete(jumpStageKey)
	bc.dao.Store.Delete([]byte{byte(storage.SYSStateSyncCheckpoint)})
	_, err := bc.dao.Store.Persist()
	if err != nil {
		return fmt.Errorf("failed to persist %d stage of state jump: %w", staleBlocksRemoved, err)
	}

	err = bc.resetRAMState(p, false)
	if err != nil {
		return fmt.Errorf("failed to update in-memory blockchain data: %w", err)
	}

	bc.log.Info("state jump is completed",
		zap.Uint32("state sync point", p),
		zap.Duration("took", time.Since(start)),
	)
	return nil
}

// resetRAMState resets in-memory cached info.
func (bc *Blockchain) resetRAMState(height uint32, resetHeaders bool) error {
	if resetHeaders {
		err := bc.HeaderHashes.init(bc.dao, config.HashIndex{})
		if err != nil {
			return err
		}
	}
	block, err := bc.dao.GetBlock(bc.GetHeaderHash(height))
	if err != nil {
		return fmt.Errorf("failed to get current block: %w", err)
	}
	bc.topBlock.Store(block)
	atomic.StoreUint32(&bc.blockHeight, height)
	atomic.StoreUint32(&bc.persistedHeight, height)

	err = bc.initializeNativeCache(block.Index, bc.dao)
	if err != nil {
		return fmt.Errorf("failed to initialize natives cache: %w", err)
	}

	if err := bc.updateExtensibleWhitelist(height); err != nil {
		return fmt.Errorf("failed to update extensible whitelist: %w", err)
	}

	updateBlockHeightMetric(height)
	updatePersistedHeightMetric(height)
	updateHeaderHeightMetric(bc.HeaderHeight())
	return nil
}

// Reset resets chain state to the specified height if possible. This method
// performs direct DB changes and can be called on non-running Blockchain only.
func (bc *Blockchain) Reset(height uint32) error {
	if bc.isRunning.Load().(bool) {
		return errors.New("can't reset state of the running blockchain")
	}
	bc.dao.PutStateSyncPoint(height)
	return bc.resetStateInternal(height, none)
}

func (bc *Blockchain) resetStateInternal(height uint32, stage stateChangeStage) error {
	// Cache isn't yet initialized, so retrieve block height right from DAO.
	currHeight, err := bc.dao.GetCurrentBlockHeight()
	if err != nil {
		return fmt.Errorf("failed to retrieve current block height: %w", err)
	}
	// Headers are already initialized by this moment, thus may use chain's API.
	hHeight := bc.HeaderHeight()
	// State reset may already be started by this moment, so perform these checks only if it wasn't.
	if stage == none {
		if height > currHeight {
			return fmt.Errorf("current block height is %d, can't reset state to height %d", currHeight, height)
		}
		if height == currHeight && hHeight == currHeight {
			bc.log.Info("chain is at the proper state", zap.Uint32("height", height))
			return nil
		}
		if bc.config.KeepOnlyLatestState {
			return fmt.Errorf("KeepOnlyLatestState is enabled, state for height %d is outdated and removed from the storage", height)
		}
		var (
			mtb = int64(bc.config.MaxTraceableBlocks)
			hf  = config.HFEchidna
		)
		if bc.IsHardforkEnabled(&hf, currHeight) {
			// Cache isn't yet initialized, so retrieve MaxTraceableBlocks directly from DAO.
			mtb, err = bc.dao.GetInt(bc.policy.Metadata().ID, native.MaxTraceableBlocksKey)
			if err != nil {
				return fmt.Errorf("failed to get MaxTraceableBlocks from DAO: %w", err)
			}
		}
		if bc.config.RemoveUntraceableBlocks && currHeight >= uint32(mtb) {
			return fmt.Errorf("RemoveUntraceableBlocks is enabled, a necessary batch of traceable blocks has already been removed")
		}
	}

	// Retrieve necessary state before the DB modification.
	b, err := bc.GetBlock(bc.GetHeaderHash(height))
	if err != nil {
		return fmt.Errorf("failed to retrieve block %d: %w", height, err)
	}
	sr, err := bc.stateRoot.GetStateRoot(height)
	if err != nil {
		return fmt.Errorf("failed to retrieve stateroot for height %d: %w", height, err)
	}
	v := bc.dao.Version
	// dao is MemCachedStore over DB, we use dao directly to persist cached changes
	// right to the underlying DB.
	cache := bc.dao
	// upperCache is a private MemCachedStore over cache. During each of the state
	// sync stages we put the data inside the upperCache; in the end of each stage
	// we persist changes from upperCache to cache. Changes from cache are persisted
	// directly to the underlying persistent storage (boltDB, levelDB, etc.).
	// upperCache/cache segregation is needed to keep the DB state clean and to
	// persist data from different stages separately.
	upperCache := cache.GetPrivate()

	bc.log.Info("initializing state reset", zap.Uint32("target height", height))
	start := time.Now()
	p := start

	// Start batch persisting routine, it will be used for blocks/txs/AERs/storage items batches persist.
	type postPersist func(persistedKeys int, err error) error
	var (
		persistCh       = make(chan postPersist)
		persistToExitCh = make(chan struct{})
	)
	go func() {
		for {
			f, ok := <-persistCh
			if !ok {
				break
			}
			persistErr := f(cache.Persist())
			if persistErr != nil {
				bc.log.Fatal("persist failed", zap.Error(persistErr))
				panic(persistErr)
			}
		}
		close(persistToExitCh)
	}()
	defer func() {
		close(persistCh)
		<-persistToExitCh
		bc.log.Info("reset finished successfully", zap.Duration("took", time.Since(start)))
	}()

	resetStageKey := []byte{byte(storage.SYSStateChangeStage)}
	switch stage {
	case none:
		upperCache.Store.Put(resetStageKey, []byte{stateResetBit | byte(stateJumpStarted)})
		// Technically, there's no difference between Persist() and PersistSync() for the private
		// MemCached storage, but we'd better use the sync version in case of some further code changes.
		_, uerr := upperCache.PersistSync()
		if uerr != nil {
			panic(uerr)
		}
		upperCache = cache.GetPrivate()
		persistCh <- func(persistedKeys int, err error) error {
			if err != nil {
				return fmt.Errorf("failed to persist state reset start marker to the DB: %w", err)
			}
			return nil
		}
		fallthrough
	case stateJumpStarted:
		bc.log.Debug("trying to reset blocks, transactions and AERs")
		// Remove blocks/transactions/aers from currHeight down to height (not including height itself).
		// Keep headers for now, they'll be removed later. It's hard to handle the whole set of changes in
		// one stage, so persist periodically.
		const persistBatchSize = 100 * headerBatchCount // count blocks only, should be enough to avoid OOM killer even for large blocks
		var (
			pBlocksStart        = p
			blocksCnt, batchCnt int
			keysCnt             = new(int)
		)
		for i := height + 1; i <= currHeight; i++ {
			_, err := upperCache.DeleteBlock(bc.GetHeaderHash(i))
			if err != nil {
				return fmt.Errorf("error while removing block %d: %w", i, err)
			}
			blocksCnt++
			if blocksCnt == persistBatchSize {
				blocksCnt = 0
				batchCnt++
				bc.log.Info("intermediate batch of removed blocks, transactions and AERs is collected",
					zap.Int("batch", batchCnt),
					zap.Duration("took", time.Since(p)))

				persistStart := time.Now()
				persistBatch := batchCnt
				_, uerr := upperCache.PersistSync()
				if uerr != nil {
					panic(uerr)
				}
				upperCache = cache.GetPrivate()
				persistCh <- func(persistedKeys int, err error) error {
					if err != nil {
						return fmt.Errorf("failed to persist intermediate batch of removed blocks, transactions and AERs: %w", err)
					}
					*keysCnt += persistedKeys
					bc.log.Debug("intermediate batch of removed blocks, transactions and AERs is persisted",
						zap.Int("batch", persistBatch),
						zap.Duration("took", time.Since(persistStart)),
						zap.Int("keys", persistedKeys))
					return nil
				}
				p = time.Now()
			}
		}
		upperCache.Store.Put(resetStageKey, []byte{stateResetBit | byte(staleBlocksRemoved)})
		batchCnt++
		bc.log.Info("last batch of removed blocks, transactions and AERs is collected",
			zap.Int("batch", batchCnt),
			zap.Duration("took", time.Since(p)))
		bc.log.Info("blocks, transactions ans AERs are reset", zap.Duration("took", time.Since(pBlocksStart)))

		persistStart := time.Now()
		persistBatch := batchCnt
		_, uerr := upperCache.PersistSync()
		if uerr != nil {
			panic(uerr)
		}
		upperCache = cache.GetPrivate()
		persistCh <- func(persistedKeys int, err error) error {
			if err != nil {
				return fmt.Errorf("failed to persist last batch of removed blocks, transactions ans AERs: %w", err)
			}
			*keysCnt += persistedKeys
			bc.log.Debug("last batch of removed blocks, transactions and AERs is persisted",
				zap.Int("batch", persistBatch),
				zap.Duration("took", time.Since(persistStart)),
				zap.Int("keys", persistedKeys))
			return nil
		}
		p = time.Now()
		fallthrough
	case staleBlocksRemoved:
		// Completely remove contract IDs to update them later.
		bc.log.Debug("trying to reset contract storage items")
		pStorageStart := p

		p = time.Now()
		var mode = mpt.ModeAll
		if bc.config.RemoveUntraceableBlocks {
			mode |= mpt.ModeGCFlag
		}
		trieStore := mpt.NewTrieStore(sr.Root, mode, upperCache.Store)
		oldStoragePrefix := v.StoragePrefix
		newStoragePrefix := statesync.TemporaryPrefix(oldStoragePrefix)

		const persistBatchSize = 200000
		var cnt, storageItmsCnt, batchCnt int
		trieStore.Seek(storage.SeekRange{Prefix: []byte{byte(oldStoragePrefix)}}, func(k, v []byte) bool {
			if cnt >= persistBatchSize {
				cnt = 0
				batchCnt++
				bc.log.Info("intermediate batch of contract storage items and IDs is collected",
					zap.Int("batch", batchCnt),
					zap.Duration("took", time.Since(p)))

				persistStart := time.Now()
				persistBatch := batchCnt
				_, uerr := upperCache.PersistSync()
				if uerr != nil {
					panic(uerr)
				}
				upperCache = cache.GetPrivate()
				persistCh <- func(persistedKeys int, err error) error {
					if err != nil {
						return fmt.Errorf("failed to persist intermediate batch of contract storage items: %w", err)
					}
					bc.log.Debug("intermediate batch of contract storage items is persisted",
						zap.Int("batch", persistBatch),
						zap.Duration("took", time.Since(persistStart)),
						zap.Int("keys", persistedKeys))
					return nil
				}
				p = time.Now()
			}
			// May safely omit KV copying.
			k[0] = byte(newStoragePrefix)
			upperCache.Store.Put(k, v)
			cnt++
			storageItmsCnt++

			return true
		})
		trieStore.Close()

		upperCache.Store.Put(resetStageKey, []byte{stateResetBit | byte(newStorageItemsAdded)})
		batchCnt++
		persistBatch := batchCnt
		bc.log.Info("last batch of contract storage items is collected", zap.Int("batch", batchCnt), zap.Duration("took", time.Since(p)))
		bc.log.Info("contract storage items are reset", zap.Duration("took", time.Since(pStorageStart)),
			zap.Int("keys", storageItmsCnt))

		lastStart := time.Now()
		_, uerr := upperCache.PersistSync()
		if uerr != nil {
			panic(uerr)
		}
		upperCache = cache.GetPrivate()
		persistCh <- func(persistedKeys int, err error) error {
			if err != nil {
				return fmt.Errorf("failed to persist contract storage items and IDs changes to the DB: %w", err)
			}
			bc.log.Debug("last batch of contract storage items and IDs is persisted", zap.Int("batch", persistBatch), zap.Duration("took", time.Since(lastStart)), zap.Int("keys", persistedKeys))
			return nil
		}
		p = time.Now()
		fallthrough
	case newStorageItemsAdded:
		// Reset SYS-prefixed and IX-prefixed information.
		bc.log.Debug("trying to reset headers information")
		for i := height + 1; i <= hHeight; i++ {
			upperCache.PurgeHeader(bc.GetHeaderHash(i))
		}
		upperCache.DeleteHeaderHashesHead(height+1, headerBatchCount)
		upperCache.StoreAsCurrentBlock(b)
		upperCache.PutCurrentHeader(b.Hash(), height)
		v.StoragePrefix = statesync.TemporaryPrefix(v.StoragePrefix)
		upperCache.PutVersion(v)
		// It's important to manually change the cache's Version at this stage, so that native cache
		// can be properly initialized (with the correct contract storage data prefix) at the final
		// stage of the state reset. At the same time, DB's SYSVersion-prefixed data will be persisted
		// from upperCache to cache in a standard way (several lines below).
		cache.Version = v
		bc.persistent.Version = v

		upperCache.Store.Put(resetStageKey, []byte{stateResetBit | byte(headersReset)})
		bc.log.Info("headers information is reset", zap.Duration("took", time.Since(p)))

		persistStart := time.Now()
		_, uerr := upperCache.PersistSync()
		if uerr != nil {
			panic(uerr)
		}
		upperCache = cache.GetPrivate()
		persistCh <- func(persistedKeys int, err error) error {
			if err != nil {
				return fmt.Errorf("failed to persist headers changes to the DB: %w", err)
			}
			bc.log.Debug("headers information is persisted", zap.Duration("took", time.Since(persistStart)), zap.Int("keys", persistedKeys))
			return nil
		}
		p = time.Now()
		fallthrough
	case headersReset:
		// Reset MPT.
		bc.log.Debug("trying to reset state root information and NEP transfers")
		err = bc.stateRoot.ResetState(height, upperCache.Store)
		if err != nil {
			return fmt.Errorf("failed to rollback MPT state: %w", err)
		}

		// Reset transfers.
		err = bc.resetTransfers(upperCache, height)
		if err != nil {
			return fmt.Errorf("failed to strip transfer log / transfer info: %w", err)
		}

		upperCache.Store.Put(resetStageKey, []byte{stateResetBit | byte(transfersReset)})
		bc.log.Info("state root information and NEP transfers are reset", zap.Duration("took", time.Since(p)))

		persistStart := time.Now()
		_, uerr := upperCache.PersistSync()
		if uerr != nil {
			panic(uerr)
		}
		upperCache = cache.GetPrivate()
		persistCh <- func(persistedKeys int, err error) error {
			if err != nil {
				return fmt.Errorf("failed to persist contract storage items changes to the DB: %w", err)
			}

			bc.log.Debug("state root information and NEP transfers are persisted", zap.Duration("took", time.Since(persistStart)), zap.Int("keys", persistedKeys))
			return nil
		}
		p = time.Now()
		fallthrough
	case transfersReset:
		// there's nothing to do after that, so just continue with common operations
		// and remove state reset stage in the end.
	default:
		return fmt.Errorf("unknown state reset stage: %d", stage)
	}

	// Direct (cache-less) DB operation:  remove stale storage items.
	bc.log.Debug("trying to remove stale storage items")
	keys := 0
	err = bc.store.SeekGC(storage.SeekRange{
		Prefix: []byte{byte(statesync.TemporaryPrefix(v.StoragePrefix))},
	}, func(_, _ []byte) (bool, bool) {
		keys++
		return false, true
	})
	if err != nil {
		return fmt.Errorf("faield to remove stale storage items from DB: %w", err)
	}
	bc.log.Info("stale storage items are reset", zap.Duration("took", time.Since(p)), zap.Int("keys", keys))
	p = time.Now()

	bc.log.Debug("trying to remove state reset point")
	upperCache.Store.Delete(resetStageKey)
	// Unlike the state jump, state sync point must be removed as we have complete state for this height.
	upperCache.Store.Delete([]byte{byte(storage.SYSStateSyncPoint)})
	bc.log.Info("state reset point is removed", zap.Duration("took", time.Since(p)))

	persistStart := time.Now()
	_, uerr := upperCache.PersistSync()
	if uerr != nil {
		panic(uerr)
	}
	persistCh <- func(persistedKeys int, err error) error {
		if err != nil {
			return fmt.Errorf("failed to persist state reset stage to DAO: %w", err)
		}
		bc.log.Info("state reset point information is persisted", zap.Duration("took", time.Since(persistStart)), zap.Int("keys", persistedKeys))
		return nil
	}
	p = time.Now()

	err = bc.resetRAMState(height, true)
	if err != nil {
		return fmt.Errorf("failed to update in-memory blockchain data: %w", err)
	}
	return nil
}

func (bc *Blockchain) initializeNativeCache(blockHeight uint32, d *dao.Simple) error {
	for _, c := range bc.contracts.List {
		// Check that contract was deployed.
		if !bc.IsHardforkEnabled(c.ActiveIn(), blockHeight) {
			continue
		}
		err := c.InitializeCache(bc.IsHardforkEnabled, blockHeight, d)
		if err != nil {
			return fmt.Errorf("failed to initialize cache for %s: %w", c.Metadata().Name, err)
		}
	}
	return nil
}

// IsHardforkEnabled returns true if the specified hardfork is enabled at the
// given height. nil hardfork is treated as always enabled. This method relies on fact that
// heights of all omitted hardforks are sanitized by Blockchain constructor.
func (bc *Blockchain) IsHardforkEnabled(hf *config.Hardfork, blockHeight uint32) bool {
	hfs := bc.config.Hardforks
	if hf != nil {
		start, ok := hfs[hf.String()]
		if !ok || start > blockHeight {
			return false
		}
	}
	return true
}

// Run runs chain loop, it needs to be run as goroutine and executing it is
// critical for correct Blockchain operation.
func (bc *Blockchain) Run() {
	bc.isRunning.Store(true)
	persistTimer := time.NewTimer(persistInterval)
	defer func() {
		if _, err := bc.persist(); err != nil {
			bc.log.Warn("failed to persist", zap.Error(err))
		}
		if err := bc.dao.Store.Close(); err != nil {
			bc.log.Warn("failed to close db", zap.Error(err))
		}
		bc.isRunning.Store(false)
		close(bc.runToExitCh)
	}()
	go bc.notificationDispatcher()
	if bc.config.MempoolSubscriptionsEnabled {
		bc.memPool.RunSubscriptions()
	}
	for {
		select {
		case <-bc.stopCh:
			return
		case <-persistTimer.C:
			var oldPersisted uint32

			if bc.config.RemoveUntraceableBlocks {
				oldPersisted = atomic.LoadUint32(&bc.persistedHeight)
			}
			dur, err := bc.persist()
			if err != nil {
				bc.log.Error("failed to persist blockchain", zap.Error(err))
			}
			if bc.config.RemoveUntraceableBlocks {
				dur += bc.tryRunGC(oldPersisted)
			}
			interval := persistInterval - dur
			interval = max(interval, time.Microsecond) // Reset doesn't work with zero or negative value.
			persistTimer.Reset(interval)
		}
	}
}

func (bc *Blockchain) tryRunGC(oldHeight uint32) time.Duration {
	var dur time.Duration

	newHeight := atomic.LoadUint32(&bc.persistedHeight)
	// Technically there may be a race between GC routine and storeBlock (MaxTraceableBlocks
	// value update). Practically, it's not that important because MTB can only be decreased,
	// so the "worst" situation we may end up here is that `MTBOld-MTBNew` number of "extra"
	// blocks are kept in the storage until the next GC run with updated MTB value.
	mtb := bc.GetMaxTraceableBlocks()
	var tgtBlock = int64(newHeight)

	tgtBlock -= int64(mtb)
	if bc.config.P2PStateExchangeExtensions {
		// Old blocks should be removed up to P2-MaxTraceableBlocks which is required for
		// proper P2P state synchronization, hence align removal of transfers, MPT entries,
		// blocks and header hashes with this value.
		syncP := newHeight / uint32(bc.config.StateSyncInterval)
		syncP--
		syncP *= uint32(bc.config.StateSyncInterval)
		tgtBlock = min(tgtBlock, int64(syncP-mtb))
	}
	// Always round to the GCP.
	tgtBlock /= int64(bc.config.GarbageCollectionPeriod)
	tgtBlock *= int64(bc.config.GarbageCollectionPeriod)
	// Count periods.
	oldHeight /= bc.config.GarbageCollectionPeriod
	newHeight /= bc.config.GarbageCollectionPeriod
	if tgtBlock > int64(bc.config.GarbageCollectionPeriod) && newHeight != oldHeight {
		dur = bc.removeOldTransfers(uint32(tgtBlock))
		dur += bc.stateRoot.GC(uint32(tgtBlock), bc.store)
		dur += bc.removeUntraceableBlocks(newHeight, uint32(tgtBlock))
		dur += bc.removeOldHeaderHashes(uint32(tgtBlock))
	}
	return dur
}

// resetTransfers is a helper function that strips the top newest NEP17 and NEP11 transfer logs
// down to the given height (not including the height itself) and updates corresponding token
// transfer info.
func (bc *Blockchain) resetTransfers(cache *dao.Simple, height uint32) error {
	// Completely remove transfer info, updating it takes too much effort. We'll gather new
	// transfer info on-the-fly later.
	cache.Store.Seek(storage.SeekRange{
		Prefix: []byte{byte(storage.STTokenTransferInfo)},
	}, func(k, v []byte) bool {
		cache.Store.Delete(k)
		return true
	})

	// Look inside each transfer batch and iterate over the batch transfers, picking those that
	// not newer than the given height. Also, for each suitable transfer update transfer info
	// flushing changes after complete account's transfers processing.
	prefixes := []byte{byte(storage.STNEP11Transfers), byte(storage.STNEP17Transfers)}
	for i := range prefixes {
		var (
			acc             util.Uint160
			trInfo          *state.TokenTransferInfo
			removeFollowing bool
			seekErr         error
		)

		cache.Store.Seek(storage.SeekRange{
			Prefix:    prefixes[i : i+1],
			Backwards: false, // From oldest to newest batch.
		}, func(k, v []byte) bool {
			var batchAcc util.Uint160
			copy(batchAcc[:], k[1:])

			if batchAcc != acc { // Some new account we're iterating over.
				if trInfo != nil {
					seekErr = cache.PutTokenTransferInfo(acc, trInfo)
					if seekErr != nil {
						return false
					}
				}
				acc = batchAcc
				trInfo = nil
				removeFollowing = false
			} else if removeFollowing {
				cache.Store.Delete(bytes.Clone(k))
				return seekErr == nil
			}

			r := io.NewBinReaderFromBuf(v[1:])
			l := len(v)
			bytesRead := 1 // 1 is for batch size byte which is read by default.
			var (
				oldBatchSize = v[0]
				newBatchSize byte
			)
			for range v[0] { // From oldest to newest transfer of the batch.
				var t *state.NEP17Transfer
				if k[0] == byte(storage.STNEP11Transfers) {
					tr := new(state.NEP11Transfer)
					tr.DecodeBinary(r)
					t = &tr.NEP17Transfer
				} else {
					t = new(state.NEP17Transfer)
					t.DecodeBinary(r)
				}
				if r.Err != nil {
					seekErr = fmt.Errorf("failed to decode subsequent transfer: %w", r.Err)
					break
				}

				if t.Block > height {
					break
				}
				bytesRead = l - r.Len() // Including batch size byte.
				newBatchSize++
				if trInfo == nil {
					var err error
					trInfo, err = cache.GetTokenTransferInfo(batchAcc)
					if err != nil {
						seekErr = fmt.Errorf("failed to retrieve token transfer info for %s: %w", batchAcc.StringLE(), r.Err)
						return false
					}
				}
				appendTokenTransferInfo(trInfo, t.Asset, t.Block, t.Timestamp, k[0] == byte(storage.STNEP11Transfers), newBatchSize >= state.TokenTransferBatchSize)
			}
			if newBatchSize == oldBatchSize {
				// The batch is already in storage and doesn't need to be changed.
				return seekErr == nil
			}
			if newBatchSize > 0 {
				v[0] = newBatchSize
				cache.Store.Put(k, v[:bytesRead])
			} else {
				cache.Store.Delete(k)
				removeFollowing = true
			}
			return seekErr == nil
		})
		if seekErr != nil {
			return seekErr
		}
		if trInfo != nil {
			// Flush the last batch of transfer info changes.
			err := cache.PutTokenTransferInfo(acc, trInfo)
			if err != nil {
				return err
			}
		}
	}
	return nil
}

// appendTokenTransferInfo is a helper for resetTransfers that updates token transfer info
// wrt the given transfer that was added to the subsequent transfer batch.
func appendTokenTransferInfo(transferData *state.TokenTransferInfo,
	token int32, bIndex uint32, bTimestamp uint64, isNEP11 bool, lastTransferInBatch bool) {
	var (
		newBatch      *bool
		nextBatch     *uint32
		currTimestamp *uint64
	)
	if !isNEP11 {
		newBatch = &transferData.NewNEP17Batch
		nextBatch = &transferData.NextNEP17Batch
		currTimestamp = &transferData.NextNEP17NewestTimestamp
	} else {
		newBatch = &transferData.NewNEP11Batch
		nextBatch = &transferData.NextNEP11Batch
		currTimestamp = &transferData.NextNEP11NewestTimestamp
	}
	transferData.LastUpdated[token] = bIndex
	*newBatch = lastTransferInBatch
	if *newBatch {
		*nextBatch++
		*currTimestamp = bTimestamp
	}
}

func (bc *Blockchain) removeOldTransfers(index uint32) time.Duration {
	bc.log.Info("starting transfer data garbage collection", zap.Uint32("index", index))
	var (
		err     error
		kept    int64
		removed int64
		start   = time.Now()
		ts, ok  = bc.gcBlockTimes.Get(index)
	)

	if !ok {
		dur := time.Since(start)
		bc.log.Error("failed to get block timestamp transfer GC", zap.Duration("time", dur), zap.Uint32("index", index))
		return dur
	}

	prefixes := []byte{byte(storage.STNEP11Transfers), byte(storage.STNEP17Transfers)}

	for i := range prefixes {
		var acc util.Uint160
		var canDrop bool

		err = bc.store.SeekGC(storage.SeekRange{
			Prefix:    prefixes[i : i+1],
			Backwards: true, // From new to old.
		}, func(k, v []byte) (bool, bool) {
			// We don't look inside of the batches, it requires too much effort, instead
			// we drop batches that are confirmed to contain outdated entries.
			var batchAcc util.Uint160
			var batchTs = binary.BigEndian.Uint64(k[1+util.Uint160Size:])
			copy(batchAcc[:], k[1:])

			if batchAcc != acc { // Some new account we're iterating over.
				acc = batchAcc
			} else if canDrop { // We've seen this account and all entries in this batch are guaranteed to be outdated.
				removed++
				return false, true
			}
			// We don't know what's inside, so keep the current
			// batch anyway, but allow to drop older ones.
			canDrop = batchTs <= ts
			kept++
			return true, true
		})
		if err != nil {
			break
		}
	}
	dur := time.Since(start)
	if err != nil {
		bc.log.Error("failed to flush transfer data GC changeset", zap.Duration("time", dur), zap.Error(err))
	} else {
		bc.log.Info("finished transfer data garbage collection",
			zap.Int64("removed", removed),
			zap.Int64("kept", kept),
			zap.Duration("time", dur))
	}
	return dur
}

// removeOldHeaderHashes removes batches of header hashes starting from genesis up
// to the batch with header with the specified index (excluding this batch if some
// headers from this batch should not be removed).
func (bc *Blockchain) removeOldHeaderHashes(index uint32) time.Duration {
	bc.log.Info("starting header hashes garbage collection", zap.Uint32("index", index))
	var (
		err     error
		removed int64
		start   = time.Now()
		till    = ((int32(index)+1)/headerBatchCount - 1) * headerBatchCount
	)
	if till > 0 {
		err = bc.store.SeekGC(storage.SeekRange{
			Prefix: []byte{byte(storage.IXHeaderHashList)},
		}, func(k, _ []byte) (bool, bool) {
			first := binary.BigEndian.Uint32(k[1:])
			if first <= uint32(till) {
				removed += headerBatchCount
				return false, first != uint32(till)
			}
			return true, false
		})
	}

	dur := time.Since(start)
	if err != nil {
		bc.log.Error("failed to flush header hashes GC changeset", zap.Duration("time", dur), zap.Error(err))
	} else {
		bc.log.Info("finished header hashes garbage collection",
			zap.Int64("removed", removed),
			zap.Duration("time", dur))
	}
	return dur
}

func (bc *Blockchain) removeUntraceableBlocks(newPeriod, tgtHeight uint32) time.Duration {
	if newPeriod*bc.config.GarbageCollectionPeriod/headerBatchCount == tgtHeight/headerBatchCount { // current batch of header hashes is not stored yet.
		tgtHeight = uint32(max(0, (int(tgtHeight)/headerBatchCount-1)*headerBatchCount))
	}
	if tgtHeight == 0 {
		return 0
	}
	oldTgtHeight := bc.gcLastUntraceableBlockHeight

	bc.log.Info("starting untraceable blocks garbage collection", zap.Uint32("from", oldTgtHeight), zap.Uint32("to", tgtHeight))
	var (
		start   = time.Now()
		kvcache = bc.dao.GetPrivate()
		removed int
	)
	for index := oldTgtHeight; index < tgtHeight; index++ {
		hh := bc.GetHeaderHash(index)
		if hh.Equals(util.Uint256{}) {
			// Empty hashes are allowed if recovering from trusted height.
			continue
		}
		_, err := kvcache.DeleteBlock(hh)
		if err != nil {
			bc.log.Warn("error while removing old block",
				zap.Uint32("index", index),
				zap.Error(err))
		} else {
			removed++
		}
	}
	bc.gcLastUntraceableBlockHeight = tgtHeight

	dur := time.Since(start)
	_, err := kvcache.Persist()
	if err != nil {
		bc.log.Error("failed to flush untraceable blocks GC changeset", zap.Duration("time", dur), zap.Error(err))
	} else {
		bc.log.Info("finished untraceable blocks garbage collection",
			zap.Int("removed", removed),
			zap.Duration("time", dur))
	}

	return dur
}

// notificationDispatcher manages subscription to events and broadcasts new events.
func (bc *Blockchain) notificationDispatcher() {
	var (
		// These are just sets of subscribers, though modelled as maps
		// for ease of management (not a lot of subscriptions is really
		// expected, but maps are convenient for adding/deleting elements).
		blockFeed        = make(map[chan *block.Block]bool)
		headerFeed       = make(map[chan *block.Header]bool)
		txFeed           = make(map[chan *transaction.Transaction]bool)
		notificationFeed = make(map[chan *state.ContainedNotificationEvent]bool)
		executionFeed    = make(map[chan *state.AppExecResult]bool)
	)
	for {
		select {
		case <-bc.stopCh:
			return
		case sub := <-bc.subCh:
			switch ch := sub.(type) {
			case chan *block.Header:
				headerFeed[ch] = true
			case chan *block.Block:
				blockFeed[ch] = true
			case chan *transaction.Transaction:
				txFeed[ch] = true
			case chan *state.ContainedNotificationEvent:
				notificationFeed[ch] = true
			case chan *state.AppExecResult:
				executionFeed[ch] = true
			default:
				panic(fmt.Sprintf("bad subscription: %T", sub))
			}
		case unsub := <-bc.unsubCh:
			switch ch := unsub.(type) {
			case chan *block.Header:
				delete(headerFeed, ch)
			case chan *block.Block:
				delete(blockFeed, ch)
			case chan *transaction.Transaction:
				delete(txFeed, ch)
			case chan *state.ContainedNotificationEvent:
				delete(notificationFeed, ch)
			case chan *state.AppExecResult:
				delete(executionFeed, ch)
			default:
				panic(fmt.Sprintf("bad unsubscription: %T", unsub))
			}
		case event := <-bc.events:
			// We don't want to waste time looping through transactions when there are no
			// subscribers.
			if len(txFeed) != 0 || len(notificationFeed) != 0 || len(executionFeed) != 0 {
				aer := event.appExecResults[0]
				if !aer.Container.Equals(event.block.Hash()) {
					panic("inconsistent application execution results")
				}
				for ch := range executionFeed {
					ch <- aer
				}
				for i := range aer.Events {
					for ch := range notificationFeed {
						ch <- &state.ContainedNotificationEvent{
							Container:         aer.Container,
							NotificationEvent: aer.Events[i],
						}
					}
				}

				aerIdx := 1
				for _, tx := range event.block.Transactions {
					aer := event.appExecResults[aerIdx]
					if !aer.Container.Equals(tx.Hash()) {
						panic("inconsistent application execution results")
					}
					aerIdx++
					for ch := range executionFeed {
						ch <- aer
					}
					if aer.VMState == vmstate.Halt {
						for i := range aer.Events {
							for ch := range notificationFeed {
								ch <- &state.ContainedNotificationEvent{
									Container:         aer.Container,
									NotificationEvent: aer.Events[i],
								}
							}
						}
					}
					for ch := range txFeed {
						ch <- tx
					}
				}

				aer = event.appExecResults[aerIdx]
				if !aer.Container.Equals(event.block.Hash()) {
					panic("inconsistent application execution results")
				}
				for ch := range executionFeed {
					ch <- aer
				}
				for i := range aer.Events {
					for ch := range notificationFeed {
						ch <- &state.ContainedNotificationEvent{
							Container:         aer.Container,
							NotificationEvent: aer.Events[i],
						}
					}
				}
			}
			for ch := range headerFeed {
				ch <- &event.block.Header
			}
			for ch := range blockFeed {
				ch <- event.block
			}
		}
	}
}

// Close stops Blockchain's internal loop, syncs changes to persistent storage
// and closes it. The Blockchain is no longer functional after the call to Close.
func (bc *Blockchain) Close() {
	// If there is a block addition in progress, wait for it to finish and
	// don't allow new ones.
	bc.addLock.Lock()
	close(bc.stopCh)
	<-bc.runToExitCh
	bc.addLock.Unlock()
	if bc.config.MempoolSubscriptionsEnabled {
		bc.memPool.StopSubscriptions()
	}
	_ = bc.log.Sync()
}

// AddBlock accepts successive block for the Blockchain, verifies it and
// stores internally. Eventually it will be persisted to the backing storage.
func (bc *Blockchain) AddBlock(block *block.Block) error {
	bc.addLock.Lock()
	defer bc.addLock.Unlock()

	var mp *mempool.Pool
	expectedHeight := bc.BlockHeight() + 1
	if expectedHeight != block.Index {
		if block.Index > expectedHeight {
			return fmt.Errorf("expected %d, got %d: %w", expectedHeight, block.Index, ErrInvalidBlockIndex)
		}
		return fmt.Errorf("%w: block with index %d is already on chain", ErrAlreadyExists, block.Index)
	}
	if bc.config.StateRootInHeader != block.StateRootEnabled {
		return fmt.Errorf("%w: %v != %v",
			ErrHdrStateRootSetting, bc.config.StateRootInHeader, block.StateRootEnabled)
	}

	if block.Index == bc.HeaderHeight()+1 {
		err := bc.addHeaders(!bc.config.SkipBlockVerification, &block.Header)
		if err != nil {
			return err
		}
	} else {
		expectedH := bc.GetHeaderHash(block.Index)
		if expectedH != block.Hash() {
			return fmt.Errorf("invalid block: hash mismatch: expected %s, got %s", expectedH.StringLE(), block.Hash().StringLE())
		}
	}
	if !bc.config.SkipBlockVerification {
		merkle := block.ComputeMerkleRoot()
		if !block.MerkleRoot.Equals(merkle) {
			return errors.New("invalid block: MerkleRoot mismatch")
		}
		mp = mempool.New(len(block.Transactions), 0, false, nil)
		for _, tx := range block.Transactions {
			var err error
			// Transactions are verified before adding them
			// into the pool, so there is no point in doing
			// it again even if we're verifying in-block transactions.
			if bc.memPool.ContainsKey(tx.Hash()) {
				err = mp.Add(tx, bc)
				if err == nil {
					continue
				}
			} else {
				err = bc.verifyAndPoolTx(tx, mp, bc)
			}
			if err != nil {
				if bc.config.VerifyTransactions {
					return fmt.Errorf("transaction %s failed to verify: %w", tx.Hash().StringLE(), err)
				}
				bc.log.Warn(fmt.Sprintf("transaction %s failed to verify: %s", tx.Hash().StringLE(), err))
			}
		}
	}
	return bc.storeBlock(block, mp)
}

// AddHeaders processes the given headers and add them to the
// HeaderHashList. It expects headers to be sorted by index.
func (bc *Blockchain) AddHeaders(headers ...*block.Header) error {
	return bc.addHeaders(!bc.config.SkipBlockVerification, headers...)
}

// addHeaders is an internal implementation of AddHeaders (`verify` parameter
// tells it to verify or not verify given headers).
func (bc *Blockchain) addHeaders(verify bool, headers ...*block.Header) error {
	var (
		start = time.Now()
		err   error
	)

	if len(headers) > 0 {
		var i int
		curHeight := bc.HeaderHeight()
		for ; i < len(headers); i++ {
			if headers[i].Index > curHeight {
				break
			}
		}
		headers = headers[i:]
	}

	if len(headers) == 0 {
		return nil
	}
	// If it's a trusted header, verify its hash against configuration.
	if headers[0].Index == bc.config.TrustedHeader.Index && !headers[0].Hash().Equals(bc.config.TrustedHeader.Hash) {
		return fmt.Errorf("trusted header hash mismatch at %d: expected %s, got %s", headers[0].Index, bc.config.TrustedHeader.Hash.StringLE(), headers[0].Hash().StringLE())
	}
	if verify {
		// Verify that the chain of the headers is consistent.
		var (
			lastHeader *block.Header
			verifyFrom uint32
		)
		if headers[0].Index == bc.config.TrustedHeader.Index {
			verifyFrom++
			lastHeader = headers[0]
		}
		if len(headers) > int(verifyFrom) {
			if lastHeader == nil {
				if lastHeader, err = bc.GetHeader(headers[verifyFrom].PrevHash); err != nil {
					return fmt.Errorf("previous header %d (%s) was not found: %w", headers[verifyFrom].Index, headers[verifyFrom].PrevHash.StringLE(), err)
				}
			}
			for _, h := range headers[verifyFrom:] {
				if err = bc.verifyHeader(h, lastHeader); err != nil {
					return err
				}
				lastHeader = h
			}
		}
	}
	res := bc.HeaderHashes.addHeaders(headers...)
	if res == nil {
		bc.log.Debug("done processing headers",
			zap.Uint32("headerIndex", bc.HeaderHeight()),
			zap.Uint32("blockHeight", bc.BlockHeight()),
			zap.Duration("took", time.Since(start)))
	}
	return res
}

// GetStateRoot returns state root for the given height.
func (bc *Blockchain) GetStateRoot(height uint32) (*state.MPTRoot, error) {
	return bc.stateRoot.GetStateRoot(height)
}

// GetStateModule returns state root service instance.
func (bc *Blockchain) GetStateModule() StateRoot {
	return bc.stateRoot
}

// GetStateSyncModule returns new state sync service instance.
func (bc *Blockchain) GetStateSyncModule() *statesync.Module {
	return statesync.NewModule(bc, bc.stateRoot, bc.log, bc.dao, bc.jumpToState)
}

// storeBlock performs chain update using the block given, it executes all
// transactions with all appropriate side-effects and updates Blockchain state.
// This is the only way to change Blockchain state.
func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error {
	var (
		cache          = bc.dao.GetPrivate()
		aerCache       = bc.dao.GetPrivate()
		appExecResults = make([]*state.AppExecResult, 0, 2+len(block.Transactions))
		aerchan        = make(chan *state.AppExecResult, len(block.Transactions)/8) // Tested 8 and 4 with no practical difference, but feel free to test more and tune.
		aerdone        = make(chan error)
	)
	go func() {
		var (
			kvcache      = aerCache
			err          error
			txCnt        int
			baer1, baer2 *state.AppExecResult
			transCache   = make(map[util.Uint160]transferData)
		)
		kvcache.StoreAsCurrentBlock(block)
		if bc.config.RemoveUntraceableBlocks && block.Index%bc.config.GarbageCollectionPeriod == 0 {
			_ = bc.gcBlockTimes.Add(block.Index, block.Timestamp)
		}
		for aer := range aerchan {
			if aer.Container == block.Hash() {
				if baer1 == nil {
					baer1 = aer
				} else {
					baer2 = aer
				}
			} else {
				err = kvcache.StoreAsTransaction(block.Transactions[txCnt], block.Index, aer)
				txCnt++
			}
			if err != nil {
				err = fmt.Errorf("failed to store exec result: %w", err)
				break
			}
			if aer.VMState == vmstate.Halt {
				for j := range aer.Events {
					bc.handleNotification(&aer.Events[j], kvcache, transCache, block, aer.Container)
				}
			}
		}
		if err != nil {
			aerdone <- err
			return
		}
		if err := kvcache.StoreAsBlock(block, baer1, baer2); err != nil {
			aerdone <- err
			return
		}
		for acc, trData := range transCache {
			err = kvcache.PutTokenTransferInfo(acc, &trData.Info)
			if err != nil {
				aerdone <- err
				return
			}
			if !trData.Info.NewNEP11Batch {
				kvcache.PutTokenTransferLog(acc, trData.Info.NextNEP11NewestTimestamp, trData.Info.NextNEP11Batch, true, &trData.Log11)
			}
			if !trData.Info.NewNEP17Batch {
				kvcache.PutTokenTransferLog(acc, trData.Info.NextNEP17NewestTimestamp, trData.Info.NextNEP17Batch, false, &trData.Log17)
			}
		}
		close(aerdone)
	}()
	_ = cache.GetItemCtx() // Prime serialization context cache (it'll be reused by upper layer DAOs).
	aer, v, err := bc.runPersist(bc.contracts.GetPersistScript(), block, cache, trigger.OnPersist, nil)
	if err != nil {
		// Release goroutines, don't care about errors, we already have one.
		close(aerchan)
		<-aerdone
		return fmt.Errorf("onPersist failed: %w", err)
	}
	appExecResults = append(appExecResults, aer)
	aerchan <- aer

	for _, tx := range block.Transactions {
		systemInterop := bc.newInteropContext(trigger.Application, cache, block, tx)
		systemInterop.ReuseVM(v)
		v.LoadScriptWithFlags(tx.Script, callflag.All)
		v.SetGasLimit(tx.SystemFee)

		err := systemInterop.Exec()
		var faultException string
		if !v.HasFailed() {
			_, err := systemInterop.DAO.Persist()
			if err != nil {
				// Release goroutines, don't care about errors, we already have one.
				close(aerchan)
				<-aerdone
				return fmt.Errorf("failed to persist invocation results: %w", err)
			}
		} else {
			bc.log.Warn("contract invocation failed",
				zap.String("tx", tx.Hash().StringLE()),
				zap.Uint32("block", block.Index),
				zap.Error(err))
			faultException = err.Error()
		}
		aer := &state.AppExecResult{
			Container: tx.Hash(),
			Execution: state.Execution{
				Trigger:        trigger.Application,
				VMState:        v.State(),
				GasConsumed:    v.GasConsumed(),
				Stack:          v.Estack().ToArray(),
				Events:         systemInterop.Notifications,
				FaultException: faultException,
				Invocations:    systemInterop.InvocationCalls,
			},
		}
		appExecResults = append(appExecResults, aer)
		aerchan <- aer
	}

	aer, _, err = bc.runPersist(bc.contracts.GetPostPersistScript(), block, cache, trigger.PostPersist, v)
	if err != nil {
		// Release goroutines, don't care about errors, we already have one.
		close(aerchan)
		<-aerdone
		return fmt.Errorf("postPersist failed: %w", err)
	}
	appExecResults = append(appExecResults, aer)
	aerchan <- aer
	close(aerchan)
	b := mpt.MapToMPTBatch(cache.Store.GetStorageChanges())
	mpt, sr, err := bc.stateRoot.AddMPTBatch(block.Index, b, cache.Store)
	if err != nil {
		// Release goroutines, don't care about errors, we already have one.
		<-aerdone
		// Here MPT can be left in a half-applied state.
		// However if this error occurs, this is a bug somewhere in code
		// because changes applied are the ones from HALTed transactions.
		return fmt.Errorf("error while trying to apply MPT changes: %w", err)
	}
	if bc.config.StateRootInHeader && bc.HeaderHeight() > sr.Index && sr.Index > bc.config.TrustedHeader.Index {
		h, err := bc.GetHeader(bc.GetHeaderHash(sr.Index + 1))
		if err != nil {
			err = fmt.Errorf("failed to get next header %d to verify stateroot: %w", sr.Index+1, err)
		} else if h.PrevStateRoot != sr.Root {
			err = fmt.Errorf("local stateroot and next header's PrevStateRoot mismatch: %s vs %s", sr.Root.StringBE(), h.PrevStateRoot.StringBE())
		}
		if err != nil {
			// Release goroutines, don't care about errors, we already have one.
			<-aerdone
			return err
		}
	}

	if bc.config.SaveStorageBatch {
		bc.lastBatch = cache.GetBatch()
	}
	// Every persist cycle we also compact our in-memory MPT. It's flushed
	// already in AddMPTBatch, so collapsing it is safe.
	persistedHeight := atomic.LoadUint32(&bc.persistedHeight)
	if persistedHeight == block.Index-1 {
		// 10 is good and roughly estimated to fit remaining trie into 1M of memory.
		mpt.Collapse(10)
	}

	aererr := <-aerdone
	if aererr != nil {
		return aererr
	}

	bc.lock.Lock()
	// Wait for a while if we're lagging behind the persistence routine,
	// it's too easy to OOM otherwise. Keep in mind that this check can't
	// be perfect, so some tolerance (accepting more) is OK to have.
	var persistVelocity = atomic.LoadUint32(&bc.keysPerPersist)
	for persistVelocity != 0 && uint32(bc.dao.Store.Len()) > persistVelocity*4 {
		bc.persistCond.Wait()
	}

	_ = bc.dao.PersistPrivate(aerCache, cache)

	mpt.Store = bc.dao.Store
	bc.stateRoot.UpdateCurrentLocal(mpt, sr)
	bc.topBlock.Store(block)
	atomic.StoreUint32(&bc.blockHeight, block.Index)
	bc.memPool.RemoveStale(func(tx *transaction.Transaction) bool { return bc.IsTxStillRelevant(tx, txpool, false) }, bc)
	for _, f := range bc.postBlock {
		f(bc.IsTxStillRelevant, txpool, block)
	}
	if err := bc.updateExtensibleWhitelist(block.Index); err != nil {
		bc.lock.Unlock()
		return err
	}
	bc.lock.Unlock()

	updateBlockHeightMetric(block.Index)
	// Genesis block is stored when Blockchain is not yet running, so there
	// is no one to read this event. And it doesn't make much sense as event
	// anyway.
	if block.Index != 0 {
		bc.events <- bcEvent{block, appExecResults}
	}
	return nil
}

func (bc *Blockchain) updateExtensibleWhitelist(height uint32) error {
	updateCommittee := bc.config.ShouldUpdateCommitteeAt(height)
	stateVals, sh, err := bc.designate.GetDesignatedByRole(bc.dao, noderoles.StateValidator, height)
	if err != nil {
		return err
	}

	if bc.extensible.Load() != nil && !updateCommittee && sh != height {
		return nil
	}

	newList := []util.Uint160{bc.tutus.GetCommitteeAddress(bc.dao)}
	nextVals := bc.tutus.GetNextBlockValidatorsInternal(bc.dao)
	script, err := smartcontract.CreateDefaultMultiSigRedeemScript(nextVals)
	if err != nil {
		return err
	}
	newList = append(newList, hash.Hash160(script))
	bc.updateExtensibleList(&newList, nextVals)

	if len(stateVals) > 0 {
		h, err := bc.designate.GetLastDesignatedHash(bc.dao, noderoles.StateValidator)
		if err != nil {
			return err
		}
		newList = append(newList, h)
		bc.updateExtensibleList(&newList, stateVals)
	}

	slices.SortFunc(newList, util.Uint160.Compare)
	bc.extensible.Store(newList)
	return nil
}

func (bc *Blockchain) updateExtensibleList(s *[]util.Uint160, pubs keys.PublicKeys) {
	for _, pub := range pubs {
		*s = append(*s, pub.GetScriptHash())
	}
}

// IsExtensibleAllowed determines if script hash is allowed to send extensible payloads.
func (bc *Blockchain) IsExtensibleAllowed(u util.Uint160) bool {
	us := bc.extensible.Load().([]util.Uint160)
	_, ok := slices.BinarySearchFunc(us, u, util.Uint160.Compare)
	return ok
}

func (bc *Blockchain) runPersist(script []byte, block *block.Block, cache *dao.Simple, trig trigger.Type, v *vm.VM) (*state.AppExecResult, *vm.VM, error) {
	systemInterop := bc.newInteropContext(trig, cache, block, nil)
	if v == nil {
		v = systemInterop.SpawnVM()
	} else {
		systemInterop.ReuseVM(v)
	}
	v.LoadScriptWithFlags(script, callflag.All)
	if err := systemInterop.Exec(); err != nil {
		return nil, v, fmt.Errorf("VM has failed: %w", err)
	} else if _, err := systemInterop.DAO.Persist(); err != nil {
		return nil, v, fmt.Errorf("can't save changes: %w", err)
	}
	return &state.AppExecResult{
		Container: block.Hash(), // application logs can be retrieved by block hash
		Execution: state.Execution{
			Trigger:     trig,
			VMState:     v.State(),
			GasConsumed: v.GasConsumed(),
			Stack:       v.Estack().ToArray(),
			Events:      systemInterop.Notifications,
		},
	}, v, nil
}

func (bc *Blockchain) handleNotification(note *state.NotificationEvent, d *dao.Simple,
	transCache map[util.Uint160]transferData, b *block.Block, h util.Uint256) {
	if note.Name != "Transfer" {
		return
	}
	arr, ok := note.Item.Value().([]stackitem.Item)
	if !ok || (len(arr) != 3 && len(arr) != 4) {
		return
	}
	from, err := parseUint160(arr[0])
	if err != nil {
		return
	}
	to, err := parseUint160(arr[1])
	if err != nil {
		return
	}
	amount, err := arr[2].TryInteger()
	if err != nil {
		return
	}
	var id []byte
	if len(arr) == 4 {
		id, err = arr[3].TryBytes()
		if err != nil || len(id) > limits.MaxStorageKeyLen {
			return
		}
	}
	bc.processTokenTransfer(d, transCache, h, b, note.ScriptHash, from, to, amount, id)
}

func parseUint160(itm stackitem.Item) (util.Uint160, error) {
	_, ok := itm.(stackitem.Null) // Minting or burning.
	if ok {
		return util.Uint160{}, nil
	}
	bytes, err := itm.TryBytes()
	if err != nil {
		return util.Uint160{}, err
	}
	return util.Uint160DecodeBytesBE(bytes)
}

func (bc *Blockchain) processTokenTransfer(cache *dao.Simple, transCache map[util.Uint160]transferData,
	h util.Uint256, b *block.Block, sc util.Uint160, from util.Uint160, to util.Uint160,
	amount *big.Int, tokenID []byte) {
	var id int32
	nativeContract := bc.contracts.ByHash(sc)
	if nativeContract != nil {
		id = nativeContract.Metadata().ID
	} else {
		assetContract, err := native.GetContract(cache, bc.NativeManagementID(), sc)
		if err != nil {
			return
		}
		id = assetContract.ID
	}
	var transfer io.Serializable
	var nep17xfer *state.NEP17Transfer
	var isNEP11 = (tokenID != nil)
	if !isNEP11 {
		nep17xfer = &state.NEP17Transfer{
			Asset:        id,
			Amount:       amount,
			Block:        b.Index,
			Counterparty: to,
			Timestamp:    b.Timestamp,
			Tx:           h,
		}
		transfer = nep17xfer
	} else {
		nep11xfer := &state.NEP11Transfer{
			NEP17Transfer: state.NEP17Transfer{
				Asset:        id,
				Amount:       amount,
				Block:        b.Index,
				Counterparty: to,
				Timestamp:    b.Timestamp,
				Tx:           h,
			},
			ID: tokenID,
		}
		transfer = nep11xfer
		nep17xfer = &nep11xfer.NEP17Transfer
	}
	if !from.Equals(util.Uint160{}) {
		_ = nep17xfer.Amount.Neg(nep17xfer.Amount)
		err := appendTokenTransfer(cache, transCache, from, transfer, id, b.Index, b.Timestamp, isNEP11)
		_ = nep17xfer.Amount.Neg(nep17xfer.Amount)
		if err != nil {
			return
		}
	}
	if !to.Equals(util.Uint160{}) {
		nep17xfer.Counterparty = from
		_ = appendTokenTransfer(cache, transCache, to, transfer, id, b.Index, b.Timestamp, isNEP11) // Nothing useful we can do.
	}
}

func appendTokenTransfer(cache *dao.Simple, transCache map[util.Uint160]transferData, addr util.Uint160, transfer io.Serializable,
	token int32, bIndex uint32, bTimestamp uint64, isNEP11 bool) error {
	transferData, ok := transCache[addr]
	if !ok {
		balances, err := cache.GetTokenTransferInfo(addr)
		if err != nil {
			return err
		}
		if !balances.NewNEP11Batch {
			trLog, err := cache.GetTokenTransferLog(addr, balances.NextNEP11NewestTimestamp, balances.NextNEP11Batch, true)
			if err != nil {
				return err
			}
			transferData.Log11 = *trLog
		}
		if !balances.NewNEP17Batch {
			trLog, err := cache.GetTokenTransferLog(addr, balances.NextNEP17NewestTimestamp, balances.NextNEP17Batch, false)
			if err != nil {
				return err
			}
			transferData.Log17 = *trLog
		}
		transferData.Info = *balances
	}
	var (
		log           *state.TokenTransferLog
		nextBatch     uint32
		currTimestamp uint64
	)
	if !isNEP11 {
		log = &transferData.Log17
		nextBatch = transferData.Info.NextNEP17Batch
		currTimestamp = transferData.Info.NextNEP17NewestTimestamp
	} else {
		log = &transferData.Log11
		nextBatch = transferData.Info.NextNEP11Batch
		currTimestamp = transferData.Info.NextNEP11NewestTimestamp
	}
	err := log.Append(transfer)
	if err != nil {
		return err
	}
	newBatch := log.Size() >= state.TokenTransferBatchSize
	if newBatch {
		cache.PutTokenTransferLog(addr, currTimestamp, nextBatch, isNEP11, log)
		// Put makes a copy of it anyway.
		log.Reset()
	}
	appendTokenTransferInfo(&transferData.Info, token, bIndex, bTimestamp, isNEP11, newBatch)
	transCache[addr] = transferData
	return nil
}

// ForEachNEP17Transfer executes f for each NEP-17 transfer in log starting from
// the transfer with the newest timestamp up to the oldest transfer. It continues
// iteration until false is returned from f. The last non-nil error is returned.
func (bc *Blockchain) ForEachNEP17Transfer(acc util.Uint160, newestTimestamp uint64, f func(*state.NEP17Transfer) (bool, error)) error {
	return bc.dao.SeekNEP17TransferLog(acc, newestTimestamp, f)
}

// ForEachNEP11Transfer executes f for each NEP-11 transfer in log starting from
// the transfer with the newest timestamp up to the oldest transfer. It continues
// iteration until false is returned from f. The last non-nil error is returned.
func (bc *Blockchain) ForEachNEP11Transfer(acc util.Uint160, newestTimestamp uint64, f func(*state.NEP11Transfer) (bool, error)) error {
	return bc.dao.SeekNEP11TransferLog(acc, newestTimestamp, f)
}

// GetNEP17Contracts returns the list of deployed NEP-17 contracts.
func (bc *Blockchain) GetNEP17Contracts() []util.Uint160 {
	return bc.management.GetNEP17Contracts(bc.dao)
}

// GetNEP11Contracts returns the list of deployed NEP-11 contracts.
func (bc *Blockchain) GetNEP11Contracts() []util.Uint160 {
	return bc.management.GetNEP11Contracts(bc.dao)
}

// GetTokenLastUpdated returns a set of contract ids with the corresponding last updated
// block indexes. In case of an empty account, latest stored state synchronisation point
// is returned under Math.MinInt32 key.
func (bc *Blockchain) GetTokenLastUpdated(acc util.Uint160) (map[int32]uint32, error) {
	info, err := bc.dao.GetTokenTransferInfo(acc)
	if err != nil {
		return nil, err
	}
	if bc.config.P2PStateExchangeExtensions && bc.config.RemoveUntraceableBlocks {
		if _, ok := info.LastUpdated[bc.tutus.Metadata().ID]; !ok {
			nBalance, lub := bc.tutus.BalanceOf(bc.dao, acc)
			if nBalance.Sign() != 0 {
				info.LastUpdated[bc.tutus.Metadata().ID] = lub
			}
		}
	}
	stateSyncPoint, err := bc.dao.GetStateSyncPoint()
	if err == nil {
		info.LastUpdated[math.MinInt32] = stateSyncPoint
	}
	return info.LastUpdated, nil
}

// GetUtilityTokenBalance returns utility token (GAS) balance for the acc.
func (bc *Blockchain) GetUtilityTokenBalance(acc util.Uint160) *big.Int {
	bs := bc.lub.BalanceOf(bc.dao, acc)
	if bs == nil {
		return big.NewInt(0)
	}
	return bs
}

// IsVitaFeeExempt returns true if the account has an active Vita token
// (local, naturalized, visiting, or asylum) and is exempt from paying transaction fees.
// This implements the Feer interface.
func (bc *Blockchain) IsVitaFeeExempt(acc util.Uint160) bool {
	// Check local Vita first
	if bc.vita != nil {
		token, err := bc.vita.GetTokenByOwner(bc.dao, acc)
		if err == nil && token != nil && token.Status == state.TokenStatusActive {
			return true
		}
	}

	// Check federation-based exemptions
	if bc.federation != nil {
		// Naturalized citizens are exempt (treated same as local)
		if bc.federation.IsNaturalizedCitizen(bc.dao, acc) {
			return true
		}
		// Visitors are exempt
		if bc.federation.IsVisitor(bc.dao, acc) {
			return true
		}
		// Asylum seekers are exempt (humanitarian override)
		if bc.federation.HasAsylum(bc.dao, acc) {
			return true
		}
	}
	return false
}

// GetGoverningTokenBalance returns governing token (NEO) balance and the height
// of the last balance change for the account.
func (bc *Blockchain) GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32) {
	return bc.tutus.BalanceOf(bc.dao, acc)
}

// GetNotaryBalance returns Notary deposit amount for the specified account.
// Default value is returned if Notary contract is not yet active.
func (bc *Blockchain) GetNotaryBalance(acc util.Uint160) *big.Int {
	if bc.notary == native.INotary(nil) {
		return nil
	}
	if !bc.IsHardforkEnabled(bc.notary.ActiveIn(), bc.BlockHeight()) {
		return nil
	}
	return bc.notary.BalanceOf(bc.dao, acc)
}

// GetNotaryServiceFeePerKey returns a NotaryAssisted transaction attribute fee
// per key which is a reward per notary request key for designated notary nodes.
// Default value is returned if Notary contract is not yet active.
func (bc *Blockchain) GetNotaryServiceFeePerKey() int64 {
	if !bc.IsHardforkEnabled(&transaction.NotaryAssistedActivation, bc.BlockHeight()) {
		return 0
	}
	return bc.policy.GetAttributeFeeInternal(bc.dao, transaction.NotaryAssistedT)
}

// GetNotaryDepositExpiration returns Notary deposit expiration height for the specified account.
// Default value is returned if Notary contract is not yet active.
func (bc *Blockchain) GetNotaryDepositExpiration(acc util.Uint160) uint32 {
	if bc.notary == native.INotary(nil) {
		return 0
	}
	if !bc.IsHardforkEnabled(bc.notary.ActiveIn(), bc.BlockHeight()) {
		return 0
	}
	return bc.notary.ExpirationOf(bc.dao, acc)
}

// NativePolicyID returns ID of native PolicyContract contract.
func (bc *Blockchain) NativePolicyID() int32 {
	return bc.policy.Metadata().ID
}

// NativeManagementID returns ID of native ContractManagement contract.
func (bc *Blockchain) NativeManagementID() int32 {
	return bc.management.Metadata().ID
}

// LastBatch returns last persisted storage batch.
func (bc *Blockchain) LastBatch() *storage.MemBatch {
	return bc.lastBatch
}

// persist flushes current in-memory Store contents to the persistent storage.
func (bc *Blockchain) persist() (time.Duration, error) {
	var (
		start     = time.Now()
		duration  time.Duration
		persisted int
		err       error
	)

	persisted, err = bc.dao.Persist()
	if err != nil {
		return 0, err
	}
	if persisted > 0 {
		bHeight, err := bc.persistent.GetCurrentBlockHeight()
		if err != nil {
			return 0, err
		}
		oldHeight := atomic.SwapUint32(&bc.persistedHeight, bHeight)
		diff := bHeight - oldHeight

		storedHeaderHeight, _, err := bc.persistent.GetCurrentHeaderHeight()
		if err != nil {
			return 0, err
		}
		duration = time.Since(start)
		// Low number of keys is not representative and duration _can_
		// be zero in tests on strange platforms like Windows.
		if duration > 0 && persisted > persistMinForSampling {
			var (
				currentVelocity = uint32(int64(persisted) * int64(persistInterval) / int64(duration))
				persistVelocity = atomic.LoadUint32(&bc.keysPerPersist)
			)
			if persistVelocity != 0 {
				currentVelocity = min(currentVelocity, 2*persistVelocity) // Normalize sudden spikes.
				currentVelocity = (persistVelocity*(persistSamples-1) + currentVelocity) / persistSamples
			} // Otherwise it's the first sample and we take it as is.
			atomic.StoreUint32(&bc.keysPerPersist, currentVelocity)
			updateEstimatedPersistVelocityMetric(currentVelocity)
		}
		bc.log.Info("persisted to disk",
			zap.Uint32("blocks", diff),
			zap.Int("keys", persisted),
			zap.Uint32("headerHeight", storedHeaderHeight),
			zap.Uint32("blockHeight", bHeight),
			zap.Duration("took", duration))

		// update monitoring metrics.
		updatePersistedHeightMetric(bHeight)
	}
	bc.persistCond.Signal()

	return duration, nil
}

// GetTransaction returns a TX and its height by the given hash. The height is MaxUint32 if tx is in the mempool.
func (bc *Blockchain) GetTransaction(hash util.Uint256) (*transaction.Transaction, uint32, error) {
	if tx, ok := bc.memPool.TryGetValue(hash); ok {
		return tx, math.MaxUint32, nil // the height is not actually defined for memPool transaction.
	}
	return bc.dao.GetTransaction(hash)
}

// GetAppExecResults returns application execution results with the specified trigger by the given
// tx hash or block hash.
func (bc *Blockchain) GetAppExecResults(hash util.Uint256, trig trigger.Type) ([]state.AppExecResult, error) {
	return bc.dao.GetAppExecResults(hash, trig)
}

// GetStorageItem returns an item from storage.
func (bc *Blockchain) GetStorageItem(id int32, key []byte) state.StorageItem {
	return bc.dao.GetStorageItem(id, key)
}

// SeekStorage performs seek operation over contract storage. Prefix is trimmed in the resulting pair's key.
func (bc *Blockchain) SeekStorage(id int32, prefix []byte, cont func(k, v []byte) bool) {
	bc.dao.Seek(id, storage.SeekRange{Prefix: prefix}, cont)
}

// GetBlock returns a Block by the given hash.
func (bc *Blockchain) GetBlock(hash util.Uint256) (*block.Block, error) {
	topBlock := bc.topBlock.Load()
	if topBlock != nil {
		tb := topBlock.(*block.Block)
		if tb.Hash().Equals(hash) {
			return tb, nil
		}
	}

	block, err := bc.dao.GetBlock(hash)
	if err != nil {
		return nil, err
	}
	if !block.MerkleRoot.Equals(util.Uint256{}) && len(block.Transactions) == 0 {
		return nil, errors.New("only header is found")
	}
	for _, tx := range block.Transactions {
		stx, _, err := bc.dao.GetTransaction(tx.Hash())
		if err != nil {
			return nil, err
		}
		*tx = *stx
	}
	return block, nil
}

// GetHeader returns data block header identified with the given hash value.
func (bc *Blockchain) GetHeader(hash util.Uint256) (*block.Header, error) {
	topBlock := bc.topBlock.Load()
	if topBlock != nil {
		tb := topBlock.(*block.Block)
		if tb.Hash().Equals(hash) {
			return &tb.Header, nil
		}
	}
	block, err := bc.dao.GetBlock(hash)
	if err != nil {
		return nil, err
	}
	return &block.Header, nil
}

// HasBlock returns true if the blockchain contains the given
// block hash.
func (bc *Blockchain) HasBlock(hash util.Uint256) bool {
	if bc.haveRecentHash(hash, bc.BlockHeight()) {
		return true
	}

	if header, err := bc.GetHeader(hash); err == nil {
		return header.Index <= bc.BlockHeight()
	}
	return false
}

// CurrentBlockHash returns the highest processed block hash.
func (bc *Blockchain) CurrentBlockHash() util.Uint256 {
	topBlock := bc.topBlock.Load()
	if topBlock != nil {
		tb := topBlock.(*block.Block)
		return tb.Hash()
	}
	return bc.GetHeaderHash(bc.BlockHeight())
}

// BlockHeight returns the height/index of the highest block.
func (bc *Blockchain) BlockHeight() uint32 {
	return atomic.LoadUint32(&bc.blockHeight)
}

// GetContractState returns contract by its script hash.
func (bc *Blockchain) GetContractState(hash util.Uint160) *state.Contract {
	contract, err := native.GetContract(bc.dao, bc.NativeManagementID(), hash)
	if contract == nil && !errors.Is(err, storage.ErrKeyNotFound) {
		bc.log.Warn("failed to get contract state", zap.Error(err))
	}
	return contract
}

// GetContractScriptHash returns contract script hash by its ID.
func (bc *Blockchain) GetContractScriptHash(id int32) (util.Uint160, error) {
	return native.GetContractScriptHash(bc.dao, bc.NativeManagementID(), id)
}

// GetNativeContractScriptHash returns native contract script hash by its name.
func (bc *Blockchain) GetNativeContractScriptHash(name string) (util.Uint160, error) {
	c := bc.contracts.ByName(name)
	if c != nil {
		return c.Metadata().Hash, nil
	}
	return util.Uint160{}, errors.New("unknown native contract")
}

// GetNatives returns list of native contracts.
func (bc *Blockchain) GetNatives() []state.Contract {
	css := bc.contracts.List
	res := make([]state.Contract, 0, len(css))
	current := bc.getCurrentHF()
	for _, c := range css {
		activeIn := c.ActiveIn()
		if activeIn != nil && activeIn.Cmp(current) > 0 {
			continue
		}

		st := bc.GetContractState(c.Metadata().Hash)
		if st != nil { // Should never happen, but better safe than sorry.
			res = append(res, *st)
		}
	}
	return res
}

// GetConfig returns the config stored in the blockchain.
func (bc *Blockchain) GetConfig() config.Blockchain {
	return bc.config
}

// SubscribeForBlocks adds given channel to new block event broadcasting, so when
// there is a new block added to the chain you'll receive it via this channel.
// Make sure it's read from regularly as not reading these events might affect
// other Blockchain functions. Make sure you're not changing the received blocks,
// as it may affect the functionality of Blockchain and other subscribers.
func (bc *Blockchain) SubscribeForBlocks(ch chan *block.Block) {
	bc.subCh <- ch
}

// SubscribeForHeadersOfAddedBlocks adds given channel to new header event broadcasting, so
// when there is a new block added to the chain you'll receive its header via this
// channel. Make sure it's read from regularly as not reading these events might
// affect other Blockchain functions. Make sure you're not changing the received
// headers, as it may affect the functionality of Blockchain and other
// subscribers.
func (bc *Blockchain) SubscribeForHeadersOfAddedBlocks(ch chan *block.Header) {
	bc.subCh <- ch
}

// SubscribeForTransactions adds given channel to new transaction event
// broadcasting, so when there is a new transaction added to the chain (in a
// block) you'll receive it via this channel. Make sure it's read from regularly
// as not reading these events might affect other Blockchain functions. Make sure
// you're not changing the received transactions, as it may affect the
// functionality of Blockchain and other subscribers.
func (bc *Blockchain) SubscribeForTransactions(ch chan *transaction.Transaction) {
	bc.subCh <- ch
}

// SubscribeForNotifications adds given channel to new notifications event
// broadcasting, so when an in-block transaction execution generates a
// notification you'll receive it via this channel. Only notifications from
// successful transactions are broadcasted, if you're interested in failed
// transactions use SubscribeForExecutions instead. Make sure this channel is
// read from regularly as not reading these events might affect other Blockchain
// functions. Make sure you're not changing the received notification events, as
// it may affect the functionality of Blockchain and other subscribers.
func (bc *Blockchain) SubscribeForNotifications(ch chan *state.ContainedNotificationEvent) {
	bc.subCh <- ch
}

// SubscribeForExecutions adds given channel to new transaction execution event
// broadcasting, so when an in-block transaction execution happens you'll receive
// the result of it via this channel. Make sure it's read from regularly as not
// reading these events might affect other Blockchain functions. Make sure you're
// not changing the received execution results, as it may affect the
// functionality of Blockchain and other subscribers.
func (bc *Blockchain) SubscribeForExecutions(ch chan *state.AppExecResult) {
	bc.subCh <- ch
}

// UnsubscribeFromBlocks unsubscribes given channel from new block notifications,
// you can close it afterwards. Passing non-subscribed channel is a no-op, but
// the method can read from this channel (discarding any read data).
func (bc *Blockchain) UnsubscribeFromBlocks(ch chan *block.Block) {
unsubloop:
	for {
		select {
		case <-ch:
		case bc.unsubCh <- ch:
			break unsubloop
		}
	}
}

// UnsubscribeFromHeadersOfAddedBlocks unsubscribes given channel from new
// block's header notifications, you can close it afterwards. Passing
// non-subscribed channel is a no-op, but the method can read from this
// channel (discarding any read data).
func (bc *Blockchain) UnsubscribeFromHeadersOfAddedBlocks(ch chan *block.Header) {
unsubloop:
	for {
		select {
		case <-ch:
		case bc.unsubCh <- ch:
			break unsubloop
		}
	}
}

// UnsubscribeFromTransactions unsubscribes given channel from new transaction
// notifications, you can close it afterwards. Passing non-subscribed channel is
// a no-op, but the method can read from this channel (discarding any read data).
func (bc *Blockchain) UnsubscribeFromTransactions(ch chan *transaction.Transaction) {
unsubloop:
	for {
		select {
		case <-ch:
		case bc.unsubCh <- ch:
			break unsubloop
		}
	}
}

// UnsubscribeFromNotifications unsubscribes given channel from new
// execution-generated notifications, you can close it afterwards. Passing
// non-subscribed channel is a no-op, but the method can read from this channel
// (discarding any read data).
func (bc *Blockchain) UnsubscribeFromNotifications(ch chan *state.ContainedNotificationEvent) {
unsubloop:
	for {
		select {
		case <-ch:
		case bc.unsubCh <- ch:
			break unsubloop
		}
	}
}

// UnsubscribeFromExecutions unsubscribes given channel from new execution
// notifications, you can close it afterwards. Passing non-subscribed channel is
// a no-op, but the method can read from this channel (discarding any read data).
func (bc *Blockchain) UnsubscribeFromExecutions(ch chan *state.AppExecResult) {
unsubloop:
	for {
		select {
		case <-ch:
		case bc.unsubCh <- ch:
			break unsubloop
		}
	}
}

// CalculateClaimable calculates the amount of GAS generated by owning specified
// amount of NEO between specified blocks.
func (bc *Blockchain) CalculateClaimable(acc util.Uint160, endHeight uint32) (*big.Int, error) {
	nextBlock, err := bc.GetFakeNextBlock(bc.BlockHeight() + 1)
	if err != nil {
		return nil, err
	}
	ic := bc.newInteropContext(trigger.Application, bc.dao, nextBlock, nil)
	return bc.tutus.CalculateBonus(ic, acc, endHeight)
}

// FeePerByte returns transaction network fee per byte.
func (bc *Blockchain) FeePerByte() int64 {
	return bc.policy.GetFeePerByteInternal(bc.dao)
}

// GetMemPool returns the memory pool of the blockchain.
func (bc *Blockchain) GetMemPool() *mempool.Pool {
	return bc.memPool
}

// ApplyPolicyToTxSet applies configured policies to given transaction set. It
// expects slice to be ordered by fee and returns a subslice of it.
func (bc *Blockchain) ApplyPolicyToTxSet(txes []*transaction.Transaction) []*transaction.Transaction {
	maxTx := bc.config.MaxTransactionsPerBlock
	if maxTx != 0 && len(txes) > int(maxTx) {
		txes = txes[:maxTx]
	}
	maxBlockSize := bc.config.MaxBlockSize
	maxBlockSysFee := bc.config.MaxBlockSystemFee
	oldVC := bc.knownValidatorsCount.Load()
	defaultWitness := bc.defaultBlockWitness.Load()
	curVC := bc.config.GetNumOfCNs(bc.BlockHeight() + 1)
	if oldVC == nil || oldVC != curVC {
		m := smartcontract.GetDefaultHonestNodeCount(curVC)
		verification, _ := smartcontract.CreateDefaultMultiSigRedeemScript(bc.tutus.GetNextBlockValidatorsInternal(bc.dao))
		defaultWitness = transaction.Witness{
			InvocationScript:   make([]byte, 66*m),
			VerificationScript: verification,
		}
		bc.knownValidatorsCount.Store(curVC)
		bc.defaultBlockWitness.Store(defaultWitness)
	}
	var (
		b           = &block.Block{Header: block.Header{Script: defaultWitness.(transaction.Witness)}}
		blockSize   = uint32(b.GetExpectedBlockSizeWithoutTransactions(len(txes)))
		blockSysFee int64
	)
	for i, tx := range txes {
		blockSize += uint32(tx.Size())
		blockSysFee += tx.SystemFee
		if blockSize > maxBlockSize || blockSysFee > maxBlockSysFee {
			txes = txes[:i]
			break
		}
	}
	return txes
}

// Various errors that could be returns upon header verification.
var (
	ErrHdrHashMismatch     = errors.New("previous header hash doesn't match")
	ErrHdrIndexMismatch    = errors.New("previous header index doesn't match")
	ErrHdrInvalidTimestamp = errors.New("block is not newer than the previous one")
	ErrHdrStateRootSetting = errors.New("state root setting mismatch")
	ErrHdrInvalidStateRoot = errors.New("state root for previous block is invalid")
)

func (bc *Blockchain) verifyHeader(currHeader, prevHeader *block.Header) error {
	if bc.config.StateRootInHeader {
		if bc.stateRoot.CurrentLocalHeight() == prevHeader.Index {
			if sr := bc.stateRoot.CurrentLocalStateRoot(); currHeader.PrevStateRoot != sr {
				return fmt.Errorf("%w: %s != %s",
					ErrHdrInvalidStateRoot, currHeader.PrevStateRoot.StringLE(), sr.StringLE())
			}
		}
	}
	if prevHeader.Hash() != currHeader.PrevHash {
		return ErrHdrHashMismatch
	}
	if prevHeader.Index+1 != currHeader.Index {
		return ErrHdrIndexMismatch
	}
	if prevHeader.Timestamp >= currHeader.Timestamp {
		return ErrHdrInvalidTimestamp
	}
	return bc.verifyHeaderWitnesses(currHeader, prevHeader)
}

// Various errors that could be returned upon verification.
var (
	ErrTxExpired         = errors.New("transaction has expired")
	ErrInsufficientFunds = errors.New("insufficient funds")
	ErrTxSmallNetworkFee = errors.New("too small network fee")
	ErrTxTooBig          = errors.New("too big transaction")
	ErrMemPoolConflict   = errors.New("invalid transaction due to conflicts with the memory pool")
	ErrInvalidScript     = errors.New("invalid script")
	ErrInvalidAttribute  = errors.New("invalid attribute")
)

// verifyAndPoolTx verifies whether a transaction is bonafide or not and tries
// to add it to the mempool given.
func (bc *Blockchain) verifyAndPoolTx(t *transaction.Transaction, pool *mempool.Pool, feer mempool.Feer, data ...any) error {
	// This code can technically be moved out of here, because it doesn't
	// really require a chain lock.
	err := scparser.IsScriptCorrect(t.Script, nil)
	if err != nil {
		return fmt.Errorf("%w: %w", ErrInvalidScript, err)
	}

	height := bc.BlockHeight()
	isPartialTx := data != nil
	if t.ValidUntilBlock <= height || !isPartialTx && t.ValidUntilBlock > height+bc.GetMaxValidUntilBlockIncrement() {
		return fmt.Errorf("%w: ValidUntilBlock = %d, current height = %d", ErrTxExpired, t.ValidUntilBlock, height)
	}
	// Policying.
	if err := bc.policy.CheckPolicy(bc.dao, t); err != nil {
		// Only one %w can be used.
		return fmt.Errorf("%w: %w", ErrPolicy, err)
	}
	if t.SystemFee > bc.config.MaxBlockSystemFee {
		return fmt.Errorf("%w: too big system fee (%d > MaxBlockSystemFee %d)", ErrPolicy, t.SystemFee, bc.config.MaxBlockSystemFee)
	}
	size := t.Size()
	if size > transaction.MaxTransactionSize {
		return fmt.Errorf("%w: (%d > MaxTransactionSize %d)", ErrTxTooBig, size, transaction.MaxTransactionSize)
	}
	needNetworkFee := int64(size)*bc.FeePerByte() + bc.CalculateAttributesFee(t)
	netFee := t.NetworkFee - needNetworkFee
	// Skip fee check for Vita holders (local or visiting) - their fees are paid by Treasury
	if netFee < 0 && !bc.IsVitaFeeExempt(t.Sender()) {
		return fmt.Errorf("%w: net fee is %v, need %v", ErrTxSmallNetworkFee, t.NetworkFee, needNetworkFee)
	}
	// check that current tx wasn't included in the conflicts attributes of some other transaction which is already in the chain
	if err := bc.dao.HasTransaction(t.Hash(), t.Signers, height, bc.GetMaxTraceableBlocks()); err != nil {
		switch {
		case errors.Is(err, dao.ErrAlreadyExists):
			return ErrAlreadyExists
		case errors.Is(err, dao.ErrHasConflicts):
			return fmt.Errorf("blockchain: %w", ErrHasConflicts)
		default:
			return err
		}
	}
	err = bc.verifyTxWitnesses(t, nil, isPartialTx, netFee)
	if err != nil {
		return err
	}
	if err := bc.verifyTxAttributes(bc.dao, t, isPartialTx); err != nil {
		return err
	}
	err = pool.Add(t, feer, data...)
	if err != nil {
		switch {
		case errors.Is(err, mempool.ErrConflict):
			return ErrMemPoolConflict
		case errors.Is(err, mempool.ErrDup):
			return ErrAlreadyInPool
		case errors.Is(err, mempool.ErrInsufficientFunds):
			return ErrInsufficientFunds
		case errors.Is(err, mempool.ErrOOM):
			return ErrOOM
		case errors.Is(err, mempool.ErrConflictsAttribute):
			return fmt.Errorf("mempool: %w: %w", ErrHasConflicts, err)
		default:
			return err
		}
	}

	return nil
}

// CalculateAttributesFee returns network fee for all transaction attributes that should be
// paid according to native Policy.
func (bc *Blockchain) CalculateAttributesFee(tx *transaction.Transaction) int64 {
	var feeSum int64
	for _, attr := range tx.Attributes {
		base := bc.policy.GetAttributeFeeInternal(bc.dao, attr.Type)
		switch attr.Type {
		case transaction.ConflictsT:
			feeSum += base * int64(len(tx.Signers))
		case transaction.NotaryAssistedT:
			if bc.P2PSigExtensionsEnabled() {
				na := attr.Value.(*transaction.NotaryAssisted)
				feeSum += base * (int64(na.NKeys) + 1)
			}
		default:
			feeSum += base
		}
	}
	return feeSum
}

func (bc *Blockchain) verifyTxAttributes(d *dao.Simple, tx *transaction.Transaction, isPartialTx bool) error {
	for i := range tx.Attributes {
		switch attrType := tx.Attributes[i].Type; attrType {
		case transaction.HighPriority:
			h := bc.tutus.GetCommitteeAddress(d)
			if !tx.HasSigner(h) {
				return fmt.Errorf("%w: high priority tx is not signed by committee", ErrInvalidAttribute)
			}
		case transaction.OracleResponseT:
			if bc.oracle == native.IOracle(nil) {
				return fmt.Errorf("%w: native Oracle contract implementation is not initialized", ErrInvalidAttribute)
			}
			h, err := bc.oracle.GetScriptHash(bc.dao)
			if err != nil || h.Equals(util.Uint160{}) {
				return fmt.Errorf("%w: %w", ErrInvalidAttribute, err)
			}
			hasOracle := false
			for i := range tx.Signers {
				if tx.Signers[i].Scopes != transaction.None {
					return fmt.Errorf("%w: oracle tx has invalid signer scope", ErrInvalidAttribute)
				}
				if tx.Signers[i].Account.Equals(h) {
					hasOracle = true
				}
			}
			if !hasOracle {
				return fmt.Errorf("%w: oracle tx is not signed by oracle nodes", ErrInvalidAttribute)
			}
			if !bytes.Equal(tx.Script, bc.oracle.GetOracleResponseScript()) {
				return fmt.Errorf("%w: oracle tx has invalid script", ErrInvalidAttribute)
			}
			resp := tx.Attributes[i].Value.(*transaction.OracleResponse)
			req, err := bc.oracle.GetRequestInternal(bc.dao, resp.ID)
			if err != nil {
				return fmt.Errorf("%w: oracle tx points to invalid request: %w", ErrInvalidAttribute, err)
			}
			if uint64(tx.NetworkFee+tx.SystemFee) < req.GasForResponse {
				return fmt.Errorf("%w: oracle tx has insufficient gas", ErrInvalidAttribute)
			}
		case transaction.NotValidBeforeT:
			nvb := tx.Attributes[i].Value.(*transaction.NotValidBefore).Height
			curHeight := bc.BlockHeight()
			if isPartialTx {
				maxNVBDelta, err := bc.GetMaxNotValidBeforeDelta()
				if err != nil {
					return fmt.Errorf("%w: failed to retrieve MaxNotValidBeforeDelta value from native Notary contract: %w", ErrInvalidAttribute, err)
				}
				if curHeight+maxNVBDelta < nvb {
					return fmt.Errorf("%w: NotValidBefore (%d) bigger than MaxNVBDelta (%d) allows at height %d", ErrInvalidAttribute, nvb, maxNVBDelta, curHeight)
				}
				if nvb+maxNVBDelta < tx.ValidUntilBlock {
					return fmt.Errorf("%w: NotValidBefore (%d) set more than MaxNVBDelta (%d) away from VUB (%d)", ErrInvalidAttribute, nvb, maxNVBDelta, tx.ValidUntilBlock)
				}
			} else {
				if curHeight < nvb {
					return fmt.Errorf("%w: transaction is not yet valid: NotValidBefore = %d, current height = %d", ErrInvalidAttribute, nvb, curHeight)
				}
			}
		case transaction.ConflictsT:
			conflicts := tx.Attributes[i].Value.(*transaction.Conflicts)
			// Only fully-qualified dao.ErrAlreadyExists error bothers us here, thus, we
			// can safely omit the signers, current index and MTB arguments to HasTransaction call to improve performance a bit.
			if err := bc.dao.HasTransaction(conflicts.Hash, nil, 0, 0); errors.Is(err, dao.ErrAlreadyExists) {
				return fmt.Errorf("%w: conflicting transaction %s is already on chain", ErrInvalidAttribute, conflicts.Hash.StringLE())
			}
		case transaction.NotaryAssistedT:
			if !bc.IsHardforkEnabled(&transaction.NotaryAssistedActivation, bc.BlockHeight()) {
				return fmt.Errorf("%w: NotaryAssisted attribute was found, but %s is not active yet", ErrInvalidAttribute, transaction.NotaryAssistedActivation)
			}
			if !tx.HasSigner(nativehashes.Notary) {
				return fmt.Errorf("%w: NotaryAssisted attribute was found, but transaction is not signed by the Notary native contract", ErrInvalidAttribute)
			}
			if tx.Sender().Equals(nativehashes.Notary) && len(tx.Signers) != 2 {
				return fmt.Errorf("%w: NotaryAssisted attribute was found, Notary native contract is the sender, transaction should have 2 signers (got %d)", ErrInvalidAttribute, len(tx.Signers))
			}
		default:
			if !bc.config.ReservedAttributes && attrType >= transaction.ReservedLowerBound && attrType <= transaction.ReservedUpperBound {
				return fmt.Errorf("%w: attribute of reserved type was found, but ReservedAttributes are disabled", ErrInvalidAttribute)
			}
		}
	}
	return nil
}

// IsTxStillRelevant is a callback for mempool transaction filtering after the
// new block addition. It returns false for transactions added by the new block
// (passed via txpool) and does witness reverification for non-standard
// contracts. It operates under the assumption that full transaction verification
// was already done so we don't need to check basic things like size, input/output
// correctness, presence in blocks before the new one, etc.
func (bc *Blockchain) IsTxStillRelevant(t *transaction.Transaction, txpool *mempool.Pool, isPartialTx bool) bool {
	var (
		recheckWitness bool
		curheight      = bc.BlockHeight()
	)

	if t.ValidUntilBlock <= curheight {
		return false
	}
	if txpool == nil {
		if bc.dao.HasTransaction(t.Hash(), t.Signers, curheight, bc.GetMaxTraceableBlocks()) != nil {
			return false
		}
	} else if txpool.HasConflicts(t, bc) {
		return false
	}
	if err := bc.verifyTxAttributes(bc.dao, t, isPartialTx); err != nil {
		return false
	}
	for i := range t.Scripts {
		if !scparser.IsStandardContract(t.Scripts[i].VerificationScript) {
			recheckWitness = true
			break
		}
	}
	if recheckWitness {
		return bc.verifyTxWitnesses(t, nil, isPartialTx) == nil
	}
	return true
}

// VerifyTx verifies whether transaction is bonafide or not relative to the
// current blockchain state. Note that this verification is completely isolated
// from the main node's mempool.
func (bc *Blockchain) VerifyTx(t *transaction.Transaction) error {
	var mp = mempool.New(1, 0, false, nil)
	bc.lock.RLock()
	defer bc.lock.RUnlock()
	return bc.verifyAndPoolTx(t, mp, bc)
}

// PoolTx verifies and tries to add given transaction into the mempool. If not
// given, the default mempool is used. Passing multiple pools is not supported.
func (bc *Blockchain) PoolTx(t *transaction.Transaction, pools ...*mempool.Pool) error {
	var pool = bc.memPool

	bc.lock.RLock()
	defer bc.lock.RUnlock()
	// Programmer error.
	if len(pools) > 1 {
		panic("too many pools given")
	}
	if len(pools) == 1 {
		pool = pools[0]
	}
	return bc.verifyAndPoolTx(t, pool, bc)
}

// PoolTxWithData verifies and tries to add given transaction with additional data into the mempool.
func (bc *Blockchain) PoolTxWithData(t *transaction.Transaction, data any, mp *mempool.Pool, feer mempool.Feer, verificationFunction func(tx *transaction.Transaction, data any) error) error {
	bc.lock.RLock()
	defer bc.lock.RUnlock()

	if verificationFunction != nil {
		err := verificationFunction(t, data)
		if err != nil {
			return fmt.Errorf("data verification failed: %w", err)
		}
	}
	return bc.verifyAndPoolTx(t, mp, feer, data)
}

// GetCommittee returns the sorted list of public keys of nodes in committee.
func (bc *Blockchain) GetCommittee() (keys.PublicKeys, error) {
	pubs := bc.tutus.GetCommitteeMembers(bc.dao)
	slices.SortFunc(pubs, (*keys.PublicKey).Cmp)
	return pubs, nil
}

// ComputeNextBlockValidators returns current validators. Validators list
// returned from this method is updated once per CommitteeSize number of blocks.
// For the last block in the dBFT epoch this method returns the list of validators
// recalculated from the latest relevant information about NEO votes; in this case
// list of validators may differ from the one returned by GetNextBlockValidators.
// For the not-last block of dBFT epoch this method returns the same list as
// GetNextBlockValidators.
func (bc *Blockchain) ComputeNextBlockValidators() []*keys.PublicKey {
	return bc.tutus.ComputeNextBlockValidators(bc.dao)
}

// GetNextBlockValidators returns next block validators. Validators list returned
// from this method is the sorted top NumOfCNs number of public keys from the
// committee of the current dBFT round (that was calculated once for the
// CommitteeSize number of blocks), thus, validators list returned from this
// method is being updated once per (committee size) number of blocks, but not
// every block.
func (bc *Blockchain) GetNextBlockValidators() ([]*keys.PublicKey, error) {
	return bc.tutus.GetNextBlockValidatorsInternal(bc.dao), nil
}

// GetEnrollments returns all registered validators.
func (bc *Blockchain) GetEnrollments() ([]state.Validator, error) {
	return bc.tutus.GetCandidates(bc.dao)
}

// GetTestVM returns an interop context with VM set up for a test run.
func (bc *Blockchain) GetTestVM(t trigger.Type, tx *transaction.Transaction, b *block.Block) (*interop.Context, error) {
	if b == nil {
		var err error
		h := bc.BlockHeight() + 1
		b, err = bc.GetFakeNextBlock(h)
		if err != nil {
			return nil, fmt.Errorf("failed to create fake block for height %d: %w", h, err)
		}
	}
	systemInterop := bc.newInteropContext(t, bc.dao, b, tx)
	_ = systemInterop.SpawnVM() // All the other code suppose that the VM is ready.
	return systemInterop, nil
}

// GetTestHistoricVM returns an interop context with VM set up for a test run.
func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transaction, nextBlockHeight uint32) (*interop.Context, error) {
	if bc.config.KeepOnlyLatestState {
		return nil, errors.New("only latest state is supported")
	}
	b, err := bc.GetFakeNextBlock(nextBlockHeight)
	if err != nil {
		return nil, fmt.Errorf("failed to create fake block for height %d: %w", nextBlockHeight, err)
	}
	var mode = mpt.ModeAll
	if bc.config.RemoveUntraceableBlocks {
		if b.Index < bc.BlockHeight()-bc.GetMaxTraceableBlocks() {
			return nil, fmt.Errorf("state for height %d is outdated and removed from the storage", b.Index)
		}
		mode |= mpt.ModeGCFlag
	}
	if b.Index < 1 || b.Index > bc.BlockHeight()+1 {
		return nil, fmt.Errorf("unsupported historic chain's height: requested state for %d, chain height %d", b.Index, bc.blockHeight)
	}
	// Assuming that block N-th is processing during historic call, the historic invocation should be based on the storage state of height N-1.
	sr, err := bc.stateRoot.GetStateRoot(b.Index - 1)
	if err != nil {
		return nil, fmt.Errorf("failed to retrieve stateroot for height %d: %w", b.Index, err)
	}
	s := mpt.NewTrieStore(sr.Root, mode, storage.NewPrivateMemCachedStore(bc.dao.Store))
	dTrie := dao.NewSimple(s, bc.config.StateRootInHeader)
	dTrie.Version = bc.dao.Version
	// Initialize native cache before passing DAO to interop context constructor, because
	// the constructor will call BaseExecFee/StoragePrice policy methods on the passed DAO.
	err = bc.initializeNativeCache(b.Index, dTrie)
	if err != nil {
		return nil, fmt.Errorf("failed to initialize native cache backed by historic DAO: %w", err)
	}
	systemInterop := bc.newInteropContext(t, dTrie, b, tx)
	_ = systemInterop.SpawnVM() // All the other code suppose that the VM is ready.
	return systemInterop, nil
}

// GetFakeNextBlock returns fake block with the specified index and pre-filled Timestamp field.
func (bc *Blockchain) GetFakeNextBlock(nextBlockHeight uint32) (*block.Block, error) {
	b := block.New(bc.config.StateRootInHeader)
	b.Index = nextBlockHeight
	hdr, err := bc.GetHeader(bc.GetHeaderHash(nextBlockHeight - 1))
	if err != nil {
		return nil, err
	}
	b.Timestamp = hdr.Timestamp + uint64(bc.GetMillisecondsPerBlock())
	return b, nil
}

// GetMaxValidUntilBlockIncrement returns the maximum value for upper increment size
// of blockchain height (in blocks) exceeding that a transaction should fail
// validation. This method performs access to native Policy storage hence Policy is
// expected to be initialized by this moment.
func (bc *Blockchain) GetMaxValidUntilBlockIncrement() uint32 {
	var hf = config.HFEchidna
	if bc.IsHardforkEnabled(&hf, bc.BlockHeight()) {
		return bc.policy.GetMaxValidUntilBlockIncrementFromCache(bc.dao)
	}
	return bc.GetConfig().MaxValidUntilBlockIncrement
}

// GetMillisecondsPerBlock returns the time interval between blocks that consensus
// nodes work with (in milliseconds). This method performs access to native Policy
// cache hence Policy is expected to be initialized by this moment.
func (bc *Blockchain) GetMillisecondsPerBlock() uint32 {
	var hf = config.HFEchidna
	if bc.IsHardforkEnabled(&hf, bc.BlockHeight()) {
		return bc.policy.GetMillisecondsPerBlockInternal(bc.dao)
	}
	return uint32(bc.GetConfig().TimePerBlock.Milliseconds())
}

// GetMaxTraceableBlocks returns the length of the chain tail accessible to smart contracts.
// This method performs access to native Policy storage hence Policy is expected to be
// initialized by this moment.
func (bc *Blockchain) GetMaxTraceableBlocks() uint32 {
	var (
		hf = config.HFEchidna
		h  = bc.BlockHeight()
	)
	if bc.IsHardforkEnabled(&hf, h) {
		// A special case for Genesis block since Policy storage might not yet be initialized.
		if h == 0 {
			return bc.GetConfig().Genesis.MaxTraceableBlocks
		}
		return bc.policy.GetMaxTraceableBlocksInternal(bc.dao)
	}
	return bc.GetConfig().MaxTraceableBlocks
}

// Various witness verification errors.
var (
	ErrWitnessHashMismatch         = errors.New("witness hash mismatch")
	ErrNativeContractWitness       = errors.New("native contract witness must have empty verification script")
	ErrVerificationFailed          = errors.New("signature check failed")
	ErrInvalidInvocationScript     = errors.New("invalid invocation script")
	ErrInvalidSignature            = fmt.Errorf("%w: invalid signature", ErrVerificationFailed)
	ErrInvalidVerificationScript   = errors.New("invalid verification script")
	ErrUnknownVerificationContract = errors.New("unknown verification contract")
	ErrInvalidVerificationContract = errors.New("verification contract is missing `verify` method or `verify` method has unexpected return value")
)

// InitVerificationContext initializes context for witness check.
func (bc *Blockchain) InitVerificationContext(ic *interop.Context, hash util.Uint160, witness *transaction.Witness) error {
	if len(witness.VerificationScript) != 0 {
		if witness.ScriptHash() != hash {
			return fmt.Errorf("%w: expected %s, got %s", ErrWitnessHashMismatch, hash.StringLE(), witness.ScriptHash().StringLE())
		}
		if bc.contracts.ByHash(hash) != nil {
			return ErrNativeContractWitness
		}
		err := scparser.IsScriptCorrect(witness.VerificationScript, nil)
		if err != nil {
			return fmt.Errorf("%w: %w", ErrInvalidVerificationScript, err)
		}
		ic.VM.LoadScriptWithHash(witness.VerificationScript, hash, callflag.ReadOnly)
	} else {
		cs, err := ic.GetContract(hash)
		if err != nil {
			return ErrUnknownVerificationContract
		}
		md := cs.Manifest.ABI.GetMethod(manifest.MethodVerify, -1)
		if md == nil || md.ReturnType != smartcontract.BoolType {
			return ErrInvalidVerificationContract
		}
		verifyOffset := md.Offset
		initOffset := -1
		md = cs.Manifest.ABI.GetMethod(manifest.MethodInit, 0)
		if md != nil {
			initOffset = md.Offset
		}
		ic.Invocations[cs.Hash]++
		ic.VM.LoadNEFMethod(&cs.NEF, &cs.Manifest, util.Uint160{}, hash, callflag.ReadOnly,
			true, verifyOffset, initOffset, nil, false)
	}
	if len(witness.InvocationScript) != 0 {
		err := scparser.IsScriptCorrect(witness.InvocationScript, nil)
		if err != nil {
			return fmt.Errorf("%w: %w", ErrInvalidInvocationScript, err)
		}
		ic.VM.LoadScript(witness.InvocationScript)
	}
	return nil
}

// VerifyWitness checks that w is a correct witness for c signed by h. It returns
// the amount of GAS consumed during verification and an error.
func (bc *Blockchain) VerifyWitness(h util.Uint160, c hash.Hashable, w *transaction.Witness, gas int64) (int64, error) {
	ic := bc.newInteropContext(trigger.Verification, bc.dao, nil, nil)
	ic.Container = c
	if tx, ok := c.(*transaction.Transaction); ok {
		ic.Tx = tx
	}
	return bc.verifyHashAgainstScript(h, w, ic, gas)
}

// verifyHashAgainstScript verifies given hash against the given witness and returns the amount of GAS consumed.
func (bc *Blockchain) verifyHashAgainstScript(hash util.Uint160, witness *transaction.Witness, interopCtx *interop.Context, gas int64) (int64, error) {
	gas = min(gas, bc.policy.GetMaxVerificationGas(interopCtx.DAO))

	vm := interopCtx.SpawnVM()
	vm.SetGasLimit(gas)
	if err := bc.InitVerificationContext(interopCtx, hash, witness); err != nil {
		return 0, err
	}
	err := interopCtx.Exec()
	if vm.HasFailed() {
		return 0, fmt.Errorf("%w: vm execution has failed: %w", ErrVerificationFailed, err)
	}
	estack := vm.Estack()
	if estack.Len() > 0 {
		resEl := estack.Pop()
		res, err := resEl.Item().TryBool()
		if err != nil {
			return 0, fmt.Errorf("%w: invalid return value", ErrVerificationFailed)
		}
		if vm.Estack().Len() != 0 {
			return 0, fmt.Errorf("%w: expected exactly one returned value", ErrVerificationFailed)
		}
		if !res {
			return vm.GasConsumed(), ErrInvalidSignature
		}
	} else {
		return 0, fmt.Errorf("%w: no result returned from the script", ErrVerificationFailed)
	}
	return vm.GasConsumed(), nil
}

// verifyTxWitnesses verifies the scripts (witnesses) that come with a given
// transaction. It can reorder them by ScriptHash, because that's required to
// match a slice of script hashes from the Blockchain. Block parameter
// is used for easy interop access and can be omitted for transactions that are
// not yet added into any block. verificationFee argument can be provided to
// restrict the maximum amount of GAS allowed to spend on transaction
// verification.
// Golang implementation of VerifyWitnesses method in C# (https://github.com/neo-project/neo/blob/master/neo/SmartContract/Helper.cs#L87).
func (bc *Blockchain) verifyTxWitnesses(t *transaction.Transaction, block *block.Block, isPartialTx bool, verificationFee ...int64) error {
	interopCtx := bc.newInteropContext(trigger.Verification, bc.dao, block, t)
	var gasLimit int64
	if len(verificationFee) == 0 {
		gasLimit = t.NetworkFee - int64(t.Size())*bc.FeePerByte() - bc.CalculateAttributesFee(t)
	} else {
		gasLimit = verificationFee[0]
	}
	for i := range t.Signers {
		gasConsumed, err := bc.verifyHashAgainstScript(t.Signers[i].Account, &t.Scripts[i], interopCtx, gasLimit)
		if err != nil &&
			(i != 0 || !isPartialTx || !errors.Is(err, ErrInvalidSignature)) { // it's OK for partially-filled transaction with dummy first witness.
			return fmt.Errorf("witness #%d: %w", i, err)
		}
		gasLimit -= gasConsumed
	}

	return nil
}

// verifyHeaderWitnesses is a block-specific implementation of VerifyWitnesses logic.
func (bc *Blockchain) verifyHeaderWitnesses(currHeader, prevHeader *block.Header) error {
	hash := prevHeader.NextConsensus
	_, err := bc.VerifyWitness(hash, currHeader, &currHeader.Script, HeaderVerificationGasLimit)
	return err
}

// GoverningTokenHash returns the governing token (Tutus) native contract hash.
func (bc *Blockchain) GoverningTokenHash() util.Uint160 {
	return nativehashes.Tutus
}

// UtilityTokenHash returns the utility token (Lub) native contract hash.
func (bc *Blockchain) UtilityTokenHash() util.Uint160 {
	return nativehashes.Lub
}

// ManagementContractHash returns management contract's hash.
func (bc *Blockchain) ManagementContractHash() util.Uint160 {
	return nativehashes.ContractManagement
}

func (bc *Blockchain) newInteropContext(trig trigger.Type, d *dao.Simple, block *block.Block, tx *transaction.Transaction) *interop.Context {
	baseExecFee := int64(interop.DefaultBaseExecFee) * vm.ExecFeeFactorMultiplier
	if block == nil || block.Index != 0 {
		// Use provided dao instead of Blockchain's one to fetch possible ExecFeeFactor
		// changes that were not yet persisted to Blockchain's dao.
		baseExecFee = bc.policy.GetExecFeeFactorInternal(d)
	}
	baseStorageFee := int64(native.DefaultStoragePrice) * vm.ExecFeeFactorMultiplier
	if block == nil || block.Index != 0 {
		// Use provided dao instead of Blockchain's one to fetch possible StoragePrice
		// changes that were not yet persisted to Blockchain's dao.
		baseStorageFee = bc.policy.GetStoragePriceInternal(d)
	}
	ic := interop.NewContext(trig, bc, d, baseExecFee, baseStorageFee, func(d *dao.Simple, h util.Uint160) (*state.Contract, error) {
		return native.GetContract(d, bc.NativeManagementID(), h)
	}, bc.contracts.List, contract.LoadToken, block, tx, bc.log)
	ic.Functions = systemInterops
	switch {
	case tx != nil:
		ic.Container = tx
	case block != nil:
		ic.Container = block
	}
	ic.InitNonceData()
	return ic
}

// P2PSigExtensionsEnabled defines whether P2P signature extensions are enabled.
func (bc *Blockchain) P2PSigExtensionsEnabled() bool {
	return bc.config.P2PSigExtensions
}

// RegisterPostBlock appends provided function to the list of functions which should be run after new block
// is stored.
func (bc *Blockchain) RegisterPostBlock(f func(func(*transaction.Transaction, *mempool.Pool, bool) bool, *mempool.Pool, *block.Block)) {
	bc.postBlock = append(bc.postBlock, f)
}

// GetBaseExecFee return execution price for `NOP` in picoGAS units.
func (bc *Blockchain) GetBaseExecFee() int64 {
	if bc.BlockHeight() == 0 {
		return interop.DefaultBaseExecFee * vm.ExecFeeFactorMultiplier
	}
	return bc.policy.GetExecFeeFactorInternal(bc.dao)
}

// GetMaxVerificationGAS returns maximum verification GAS Policy limit.
func (bc *Blockchain) GetMaxVerificationGAS() int64 {
	return bc.policy.GetMaxVerificationGas(bc.dao)
}

// GetMaxNotValidBeforeDelta returns maximum NotValidBeforeDelta Notary limit.
func (bc *Blockchain) GetMaxNotValidBeforeDelta() (uint32, error) {
	if bc.notary == native.INotary(nil) {
		return 0, errors.New("native Notary contract implementation is not initialized")
	}
	if !bc.IsHardforkEnabled(bc.notary.ActiveIn(), bc.BlockHeight()) {
		return 0, fmt.Errorf("native Notary is active starting from %s", bc.notary.ActiveIn().String())
	}
	return bc.notary.GetMaxNotValidBeforeDelta(bc.dao), nil
}

// GetStoragePrice returns the current storage price in the units of picoGAS.
func (bc *Blockchain) GetStoragePrice() int64 {
	if bc.BlockHeight() == 0 {
		return native.DefaultStoragePrice * vm.ExecFeeFactorMultiplier
	}
	return bc.policy.GetStoragePriceInternal(bc.dao)
}

// GetTrimmedBlock returns a block with only the header and transaction hashes.
func (bc *Blockchain) GetTrimmedBlock(hash util.Uint256) (*block.Block, error) {
	return bc.dao.GetBlock(hash)
}
