diff --git a/cli/query/query_test.go b/cli/query/query_test.go
index c85d282e0..0f7bbd4e6 100644
--- a/cli/query/query_test.go
+++ b/cli/query/query_test.go
@@ -63,7 +63,7 @@ func TestQueryTx(t *testing.T) {
 
 	_, height, err := e.Chain.GetTransaction(txHash)
 	require.NoError(t, err)
-	e.CheckNextLine(t, `BlockHash:\s+`+e.Chain.GetHeaderHash(int(height)).StringLE())
+	e.CheckNextLine(t, `BlockHash:\s+`+e.Chain.GetHeaderHash(height).StringLE())
 	e.CheckNextLine(t, `Success:\s+true`)
 	e.CheckEOF(t)
 
@@ -117,7 +117,7 @@ func compareQueryTxVerbose(t *testing.T, e *testcli.Executor, tx *transaction.Tr
 	e.CheckNextLine(t, `OnChain:\s+true`)
 	_, height, err := e.Chain.GetTransaction(tx.Hash())
 	require.NoError(t, err)
-	e.CheckNextLine(t, `BlockHash:\s+`+e.Chain.GetHeaderHash(int(height)).StringLE())
+	e.CheckNextLine(t, `BlockHash:\s+`+e.Chain.GetHeaderHash(height).StringLE())
 
 	res, _ := e.Chain.GetAppExecResults(tx.Hash(), trigger.Application)
 	e.CheckNextLine(t, fmt.Sprintf(`Success:\s+%t`, res[0].Execution.VMState == vmstate.Halt))
diff --git a/internal/fakechain/fakechain.go b/internal/fakechain/fakechain.go
index 34c1f9b6c..4ba989fdc 100644
--- a/internal/fakechain/fakechain.go
+++ b/internal/fakechain/fakechain.go
@@ -2,7 +2,6 @@ package fakechain
 
 import (
 	"errors"
-	"math"
 	"math/big"
 	"sync/atomic"
 
@@ -236,11 +235,8 @@ func (chain *FakeChain) GetNativeContractScriptHash(name string) (util.Uint160,
 }
 
 // GetHeaderHash implements the Blockchainer interface.
-func (chain *FakeChain) GetHeaderHash(n int) util.Uint256 {
-	if n < 0 || n > math.MaxUint32 {
-		return util.Uint256{}
-	}
-	return chain.hdrHashes[uint32(n)]
+func (chain *FakeChain) GetHeaderHash(n uint32) util.Uint256 {
+	return chain.hdrHashes[n]
 }
 
 // GetHeader implements the Blockchainer interface.
diff --git a/internal/testchain/address.go b/internal/testchain/address.go
index d116214fc..3db905638 100644
--- a/internal/testchain/address.go
+++ b/internal/testchain/address.go
@@ -158,7 +158,7 @@ func SignCommittee(h hash.Hashable) []byte {
 func NewBlock(t *testing.T, bc Ledger, offset uint32, primary uint32, txs ...*transaction.Transaction) *block.Block {
 	witness := transaction.Witness{VerificationScript: MultisigVerificationScript()}
 	height := bc.BlockHeight()
-	h := bc.GetHeaderHash(int(height))
+	h := bc.GetHeaderHash(height)
 	hdr, err := bc.GetHeader(h)
 	require.NoError(t, err)
 	b := &block.Block{
diff --git a/internal/testchain/transaction.go b/internal/testchain/transaction.go
index 2e05a804c..630902ac8 100644
--- a/internal/testchain/transaction.go
+++ b/internal/testchain/transaction.go
@@ -27,7 +27,7 @@ type Ledger interface {
 	FeePerByte() int64
 	GetBaseExecFee() int64
 	GetHeader(hash util.Uint256) (*block.Header, error)
-	GetHeaderHash(int) util.Uint256
+	GetHeaderHash(uint32) util.Uint256
 	HeaderHeight() uint32
 	ManagementContractHash() util.Uint160
 }
diff --git a/pkg/consensus/consensus_test.go b/pkg/consensus/consensus_test.go
index fcca5b2f5..c3f3c6098 100644
--- a/pkg/consensus/consensus_test.go
+++ b/pkg/consensus/consensus_test.go
@@ -119,7 +119,7 @@ func TestService_NextConsensus(t *testing.T) {
 	require.NoError(t, err)
 
 	checkNextConsensus := func(t *testing.T, bc *core.Blockchain, height uint32, h util.Uint160) {
-		hdrHash := bc.GetHeaderHash(int(height))
+		hdrHash := bc.GetHeaderHash(height)
 		hdr, err := bc.GetHeader(hdrHash)
 		require.NoError(t, err)
 		require.Equal(t, h, hdr.NextConsensus)
diff --git a/pkg/core/bench_test.go b/pkg/core/bench_test.go
index 2885487b7..f7d25e67d 100644
--- a/pkg/core/bench_test.go
+++ b/pkg/core/bench_test.go
@@ -109,10 +109,10 @@ func benchmarkForEachNEP17Transfer(t *testing.B, ps storage.Store, startFromBloc
 		e.CheckHalt(t, tx.Hash())
 	}
 
-	newestB, err := bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight()) - startFromBlock + 1))
+	newestB, err := bc.GetBlock(bc.GetHeaderHash(bc.BlockHeight() - uint32(startFromBlock) + 1))
 	require.NoError(t, err)
 	newestTimestamp := newestB.Timestamp
-	oldestB, err := bc.GetBlock(bc.GetHeaderHash(int(newestB.Index) - nBlocksToTake))
+	oldestB, err := bc.GetBlock(bc.GetHeaderHash(newestB.Index - uint32(nBlocksToTake)))
 	require.NoError(t, err)
 	oldestTimestamp := oldestB.Timestamp
 
diff --git a/pkg/core/blockchain.go b/pkg/core/blockchain.go
index 59b0404ef..490e6c699 100644
--- a/pkg/core/blockchain.go
+++ b/pkg/core/blockchain.go
@@ -45,8 +45,7 @@ import (
 
 // Tuning parameters.
 const (
-	headerBatchCount = 2000
-	version          = "0.2.6"
+	version = "0.2.6"
 
 	defaultInitialGAS                      = 52000000_00000000
 	defaultGCPeriod                        = 10000
@@ -115,6 +114,8 @@ var (
 // 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.ProtocolConfiguration
 
 	// The only way chain state changes is by adding blocks, so we can't
@@ -151,13 +152,6 @@ type Blockchain struct {
 	// Current persisted block count.
 	persistedHeight uint32
 
-	// Number of headers stored in the chain file.
-	storedHeaderCount uint32
-
-	// Header hashes list with associated lock.
-	headerHashesLock sync.RWMutex
-	headerHashes     []util.Uint256
-
 	// Stop synchronization mechanisms.
 	stopCh      chan struct{}
 	runToExitCh chan struct{}
@@ -380,8 +374,7 @@ func (bc *Blockchain) init() error {
 		if err != nil {
 			return err
 		}
-		bc.headerHashes = []util.Uint256{genesisBlock.Hash()}
-		bc.dao.PutCurrentHeader(genesisBlock.Hash(), genesisBlock.Index)
+		bc.HeaderHashes.initGenesis(bc.dao, genesisBlock.Hash())
 		if err := bc.stateRoot.Init(0); err != nil {
 			return fmt.Errorf("can't init MPT: %w", err)
 		}
@@ -414,53 +407,11 @@ func (bc *Blockchain) init() error {
 	// and the genesis block as first block.
 	bc.log.Info("restoring blockchain", zap.String("version", version))
 
-	bc.headerHashes, err = bc.dao.GetHeaderHashes()
+	err = bc.HeaderHashes.init(bc.dao)
 	if err != nil {
 		return err
 	}
 
-	bc.storedHeaderCount = uint32(len(bc.headerHashes))
-
-	currHeaderHeight, currHeaderHash, err := bc.dao.GetCurrentHeaderHeight()
-	if err != nil {
-		return fmt.Errorf("failed to retrieve current header info: %w", err)
-	}
-	if bc.storedHeaderCount == 0 && currHeaderHeight == 0 {
-		bc.headerHashes = append(bc.headerHashes, currHeaderHash)
-	}
-
-	// There is a high chance that the Node is stopped before the next
-	// batch of 2000 headers was stored. Via the currentHeaders stored we can sync
-	// that with stored blocks.
-	if currHeaderHeight >= bc.storedHeaderCount {
-		hash := currHeaderHash
-		var targetHash util.Uint256
-		if len(bc.headerHashes) > 0 {
-			targetHash = bc.headerHashes[len(bc.headerHashes)-1]
-		} else {
-			genesisBlock, err := CreateGenesisBlock(bc.config)
-			if err != nil {
-				return err
-			}
-			targetHash = genesisBlock.Hash()
-			bc.headerHashes = append(bc.headerHashes, targetHash)
-		}
-		headers := make([]*block.Header, 0)
-
-		for hash != targetHash {
-			header, err := bc.GetHeader(hash)
-			if err != nil {
-				return fmt.Errorf("could not get header %s: %w", hash, err)
-			}
-			headers = append(headers, header)
-			hash = header.PrevHash
-		}
-		headerSliceReverse(headers)
-		for _, h := range headers {
-			bc.headerHashes = append(bc.headerHashes, h.Hash())
-		}
-	}
-
 	// 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 {
@@ -551,8 +502,8 @@ func (bc *Blockchain) jumpToState(p uint32) error {
 // 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+1 >= uint32(len(bc.headerHashes)) {
-		return fmt.Errorf("invalid state sync point %d: headerHeignt is %d", p, len(bc.headerHashes))
+	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))
@@ -587,7 +538,7 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateChangeStage) erro
 		// 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 p-bc.config.MaxTraceableBlocks > 0 {
-			err := cache.DeleteBlock(bc.headerHashes[0])
+			err := cache.DeleteBlock(bc.GetHeaderHash(0))
 			if err != nil {
 				return fmt.Errorf("failed to remove outdated state data for the genesis block: %w", err)
 			}
@@ -600,7 +551,7 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateChangeStage) erro
 			}
 		}
 		// Update SYS-prefixed info.
-		block, err := bc.dao.GetBlock(bc.headerHashes[p])
+		block, err := bc.dao.GetBlock(bc.GetHeaderHash(p))
 		if err != nil {
 			return fmt.Errorf("failed to get current block: %w", err)
 		}
@@ -616,7 +567,7 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateChangeStage) erro
 	default:
 		return fmt.Errorf("unknown state jump stage: %d", stage)
 	}
-	block, err := bc.dao.GetBlock(bc.headerHashes[p+1])
+	block, err := bc.dao.GetBlock(bc.GetHeaderHash(p + 1))
 	if err != nil {
 		return fmt.Errorf("failed to get block to init MPT: %w", err)
 	}
@@ -637,10 +588,12 @@ func (bc *Blockchain) jumpToStateInternal(p uint32, stage stateChangeStage) erro
 // resetRAMState resets in-memory cached info.
 func (bc *Blockchain) resetRAMState(height uint32, resetHeaders bool) error {
 	if resetHeaders {
-		bc.headerHashes = bc.headerHashes[:height+1]
-		bc.storedHeaderCount = height + 1
+		err := bc.HeaderHashes.init(bc.dao)
+		if err != nil {
+			return err
+		}
 	}
-	block, err := bc.dao.GetBlock(bc.headerHashes[height])
+	block, err := bc.dao.GetBlock(bc.GetHeaderHash(height))
 	if err != nil {
 		return fmt.Errorf("failed to get current block: %w", err)
 	}
@@ -697,7 +650,7 @@ func (bc *Blockchain) resetStateInternal(height uint32, stage stateChangeStage)
 	}
 
 	// Retrieve necessary state before the DB modification.
-	b, err := bc.GetBlock(bc.headerHashes[height])
+	b, err := bc.GetBlock(bc.GetHeaderHash(height))
 	if err != nil {
 		return fmt.Errorf("failed to retrieve block %d: %w", height, err)
 	}
@@ -733,7 +686,7 @@ func (bc *Blockchain) resetStateInternal(height uint32, stage stateChangeStage)
 			blocksCnt, batchCnt, keysCnt int
 		)
 		for i := height + 1; i <= currHeight; i++ {
-			err := cache.DeleteBlock(bc.GetHeaderHash(int(i)))
+			err := cache.DeleteBlock(bc.GetHeaderHash(i))
 			if err != nil {
 				return fmt.Errorf("error while removing block %d: %w", i, err)
 			}
@@ -861,7 +814,7 @@ func (bc *Blockchain) resetStateInternal(height uint32, stage stateChangeStage)
 		// Reset SYS-prefixed and IX-prefixed information.
 		bc.log.Info("trying to reset headers information")
 		for i := height + 1; i <= hHeight; i++ {
-			cache.PurgeHeader(bc.GetHeaderHash(int(i)))
+			cache.PurgeHeader(bc.GetHeaderHash(i))
 		}
 		cache.DeleteHeaderHashes(height+1, headerBatchCount)
 		cache.StoreAsCurrentBlock(b)
@@ -1186,7 +1139,7 @@ func appendTokenTransferInfo(transferData *state.TokenTransferInfo,
 func (bc *Blockchain) removeOldTransfers(index uint32) time.Duration {
 	bc.log.Info("starting transfer data garbage collection", zap.Uint32("index", index))
 	start := time.Now()
-	h, err := bc.GetHeader(bc.GetHeaderHash(int(index)))
+	h, err := bc.GetHeader(bc.GetHeaderHash(index))
 	if err != nil {
 		dur := time.Since(start)
 		bc.log.Error("failed to find block header for transfer GC", zap.Duration("time", dur), zap.Error(err))
@@ -1418,7 +1371,6 @@ func (bc *Blockchain) AddHeaders(headers ...*block.Header) error {
 func (bc *Blockchain) addHeaders(verify bool, headers ...*block.Header) error {
 	var (
 		start = time.Now()
-		batch = bc.dao.GetPrivate()
 		err   error
 	)
 
@@ -1448,44 +1400,14 @@ func (bc *Blockchain) addHeaders(verify bool, headers ...*block.Header) error {
 			lastHeader = h
 		}
 	}
-
-	bc.headerHashesLock.Lock()
-	defer bc.headerHashesLock.Unlock()
-	oldlen := len(bc.headerHashes)
-	var lastHeader *block.Header
-	for _, h := range headers {
-		if int(h.Index) != len(bc.headerHashes) {
-			continue
-		}
-		err = batch.StoreHeader(h)
-		if err != nil {
-			return err
-		}
-		bc.headerHashes = append(bc.headerHashes, h.Hash())
-		lastHeader = h
-	}
-
-	if oldlen != len(bc.headerHashes) {
-		for int(lastHeader.Index)-headerBatchCount >= int(bc.storedHeaderCount) {
-			err = batch.StoreHeaderHashes(bc.headerHashes[bc.storedHeaderCount:bc.storedHeaderCount+headerBatchCount],
-				bc.storedHeaderCount)
-			if err != nil {
-				return err
-			}
-			bc.storedHeaderCount += headerBatchCount
-		}
-
-		batch.PutCurrentHeader(lastHeader.Hash(), lastHeader.Index)
-		updateHeaderHeightMetric(len(bc.headerHashes) - 1)
-		if _, err = batch.Persist(); err != nil {
-			return err
-		}
+	res := bc.HeaderHashes.addHeaders(headers...)
+	if res == nil {
 		bc.log.Debug("done processing headers",
-			zap.Int("headerIndex", len(bc.headerHashes)-1),
+			zap.Uint32("headerIndex", bc.HeaderHeight()),
 			zap.Uint32("blockHeight", bc.BlockHeight()),
 			zap.Duration("took", time.Since(start)))
 	}
-	return nil
+	return res
 }
 
 // GetStateRoot returns state root for the given height.
@@ -1540,7 +1462,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
 				stop = start + 1
 			}
 			for index := start; index < stop; index++ {
-				err := kvcache.DeleteBlock(bc.headerHashes[index])
+				err := kvcache.DeleteBlock(bc.GetHeaderHash(index))
 				if err != nil {
 					bc.log.Warn("error while removing old block",
 						zap.Uint32("index", index),
@@ -1662,7 +1584,7 @@ func (bc *Blockchain) storeBlock(block *block.Block, txpool *mempool.Pool) error
 		return fmt.Errorf("error while trying to apply MPT changes: %w", err)
 	}
 	if bc.config.StateRootInHeader && bc.HeaderHeight() > sr.Index {
-		h, err := bc.GetHeader(bc.GetHeaderHash(int(sr.Index) + 1))
+		h, err := bc.GetHeader(bc.GetHeaderHash(sr.Index + 1))
 		if err != nil {
 			err = fmt.Errorf("failed to get next header: %w", err)
 		} else if h.PrevStateRoot != sr.Root {
@@ -2163,15 +2085,9 @@ func (bc *Blockchain) HasTransaction(hash util.Uint256) bool {
 // HasBlock returns true if the blockchain contains the given
 // block hash.
 func (bc *Blockchain) HasBlock(hash util.Uint256) bool {
-	var height = bc.BlockHeight()
-	bc.headerHashesLock.RLock()
-	for i := int(height); i >= int(height)-4 && i >= 0; i-- {
-		if hash.Equals(bc.headerHashes[i]) {
-			bc.headerHashesLock.RUnlock()
-			return true
-		}
+	if bc.HeaderHashes.haveRecentHash(hash, bc.BlockHeight()) {
+		return true
 	}
-	bc.headerHashesLock.RUnlock()
 
 	if header, err := bc.GetHeader(hash); err == nil {
 		return header.Index <= bc.BlockHeight()
@@ -2186,28 +2102,7 @@ func (bc *Blockchain) CurrentBlockHash() util.Uint256 {
 		tb := topBlock.(*block.Block)
 		return tb.Hash()
 	}
-	return bc.GetHeaderHash(int(bc.BlockHeight()))
-}
-
-// CurrentHeaderHash returns the hash of the latest known header.
-func (bc *Blockchain) CurrentHeaderHash() util.Uint256 {
-	bc.headerHashesLock.RLock()
-	hash := bc.headerHashes[len(bc.headerHashes)-1]
-	bc.headerHashesLock.RUnlock()
-	return hash
-}
-
-// GetHeaderHash returns hash of the header/block with specified index, if
-// Blockchain doesn't have a hash for this height, zero Uint256 value is returned.
-func (bc *Blockchain) GetHeaderHash(i int) util.Uint256 {
-	bc.headerHashesLock.RLock()
-	defer bc.headerHashesLock.RUnlock()
-
-	hashesLen := len(bc.headerHashes)
-	if hashesLen <= i {
-		return util.Uint256{}
-	}
-	return bc.headerHashes[i]
+	return bc.GetHeaderHash(bc.BlockHeight())
 }
 
 // BlockHeight returns the height/index of the highest block.
@@ -2215,14 +2110,6 @@ func (bc *Blockchain) BlockHeight() uint32 {
 	return atomic.LoadUint32(&bc.blockHeight)
 }
 
-// HeaderHeight returns the index/height of the highest header.
-func (bc *Blockchain) HeaderHeight() uint32 {
-	bc.headerHashesLock.RLock()
-	n := len(bc.headerHashes)
-	bc.headerHashesLock.RUnlock()
-	return uint32(n - 1)
-}
-
 // GetContractState returns contract by its script hash.
 func (bc *Blockchain) GetContractState(hash util.Uint160) *state.Contract {
 	contract, err := bc.contracts.Management.GetContract(bc.dao, hash)
@@ -2759,7 +2646,7 @@ func (bc *Blockchain) GetTestHistoricVM(t trigger.Type, tx *transaction.Transact
 func (bc *Blockchain) getFakeNextBlock(nextBlockHeight uint32) (*block.Block, error) {
 	b := block.New(bc.config.StateRootInHeader)
 	b.Index = nextBlockHeight
-	hdr, err := bc.GetHeader(bc.GetHeaderHash(int(nextBlockHeight - 1)))
+	hdr, err := bc.GetHeader(bc.GetHeaderHash(nextBlockHeight - 1))
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/core/blockchain_core_test.go b/pkg/core/blockchain_core_test.go
index 500b526f8..529d12f49 100644
--- a/pkg/core/blockchain_core_test.go
+++ b/pkg/core/blockchain_core_test.go
@@ -225,7 +225,7 @@ func TestBlockchain_InitWithIncompleteStateJump(t *testing.T) {
 	t.Run("invalid state sync point", func(t *testing.T) {
 		bcSpout.dao.Store.Put(bPrefix, []byte{byte(stateJumpStarted)})
 		point := make([]byte, 4)
-		binary.LittleEndian.PutUint32(point, uint32(len(bcSpout.headerHashes)))
+		binary.LittleEndian.PutUint32(point, bcSpout.lastHeaderIndex()+1)
 		bcSpout.dao.Store.Put([]byte{byte(storage.SYSStateSyncPoint)}, point)
 		checkNewBlockchainErr(t, boltCfg, bcSpout.dao.Store, "invalid state sync point")
 	})
@@ -304,7 +304,7 @@ func TestChainWithVolatileNumOfValidators(t *testing.T) {
 			},
 		}
 		curWit = nextWit
-		b.PrevHash = bc.GetHeaderHash(i - 1)
+		b.PrevHash = bc.GetHeaderHash(uint32(i) - 1)
 		b.Timestamp = uint64(time.Now().UTC().Unix())*1000 + uint64(i)
 		b.Index = uint32(i)
 		b.RebuildMerkleRoot()
diff --git a/pkg/core/blockchain_neotest_test.go b/pkg/core/blockchain_neotest_test.go
index 3c325dc4a..d7b8cbfcf 100644
--- a/pkg/core/blockchain_neotest_test.go
+++ b/pkg/core/blockchain_neotest_test.go
@@ -146,14 +146,15 @@ func TestBlockchain_StartFromExistingDB(t *testing.T) {
 
 		// Corrupt headers hashes batch.
 		cache := storage.NewMemCachedStore(ps) // Extra wrapper to avoid good DB corruption.
-		key := make([]byte, 5)
-		key[0] = byte(storage.IXHeaderHashList)
-		binary.BigEndian.PutUint32(key[1:], 1)
-		cache.Put(key, []byte{1, 2, 3})
+		// Make the chain think we're at 2000+ which will trigger page 0 read.
+		buf := io.NewBufBinWriter()
+		buf.WriteBytes(util.Uint256{}.BytesLE())
+		buf.WriteU32LE(2000)
+		cache.Put([]byte{byte(storage.SYSCurrentHeader)}, buf.Bytes())
 
 		_, _, _, err := chain.NewMultiWithCustomConfigAndStoreNoCheck(t, customConfig, cache)
 		require.Error(t, err)
-		require.True(t, strings.Contains(err.Error(), "failed to read batch of 2000"), err)
+		require.True(t, strings.Contains(err.Error(), "failed to retrieve header hash page"), err)
 	})
 	t.Run("corrupted current header height", func(t *testing.T) {
 		ps = newPS(t)
@@ -1970,12 +1971,12 @@ func TestBlockchain_ResetState(t *testing.T) {
 	neoH := e.NativeHash(t, nativenames.Neo)
 	gasID := e.NativeID(t, nativenames.Gas)
 	neoID := e.NativeID(t, nativenames.Neo)
-	resetBlockHash := bc.GetHeaderHash(int(resetBlockIndex))
+	resetBlockHash := bc.GetHeaderHash(resetBlockIndex)
 	resetBlockHeader, err := bc.GetHeader(resetBlockHash)
 	require.NoError(t, err)
 	topBlockHeight := bc.BlockHeight()
-	topBH := bc.GetHeaderHash(int(bc.BlockHeight()))
-	staleBH := bc.GetHeaderHash(int(resetBlockIndex + 1))
+	topBH := bc.GetHeaderHash(bc.BlockHeight())
+	staleBH := bc.GetHeaderHash(resetBlockIndex + 1)
 	staleB, err := bc.GetBlock(staleBH)
 	require.NoError(t, err)
 	staleTx := staleB.Transactions[0]
@@ -2043,7 +2044,7 @@ func TestBlockchain_ResetState(t *testing.T) {
 	require.Equal(t, uint32(0), bc.GetStateModule().CurrentValidatedHeight())
 
 	// Try to get the latest block\header.
-	bh := bc.GetHeaderHash(int(resetBlockIndex))
+	bh := bc.GetHeaderHash(resetBlockIndex)
 	require.Equal(t, resetBlockHash, bh)
 	h, err := bc.GetHeader(bh)
 	require.NoError(t, err)
@@ -2054,7 +2055,7 @@ func TestBlockchain_ResetState(t *testing.T) {
 
 	// Check that stale blocks/headers/txs/aers/sr are not reachable.
 	for i := resetBlockIndex + 1; i <= topBlockHeight; i++ {
-		hHash := bc.GetHeaderHash(int(i))
+		hHash := bc.GetHeaderHash(i)
 		require.Equal(t, util.Uint256{}, hHash)
 		_, err = bc.GetStateRoot(i)
 		require.Error(t, err)
diff --git a/pkg/core/chaindump/dump.go b/pkg/core/chaindump/dump.go
index d2cf45469..802395591 100644
--- a/pkg/core/chaindump/dump.go
+++ b/pkg/core/chaindump/dump.go
@@ -14,14 +14,14 @@ type DumperRestorer interface {
 	AddBlock(block *block.Block) error
 	GetBlock(hash util.Uint256) (*block.Block, error)
 	GetConfig() config.ProtocolConfiguration
-	GetHeaderHash(int) util.Uint256
+	GetHeaderHash(uint32) util.Uint256
 }
 
 // Dump writes count blocks from start to the provided writer.
 // Note: header needs to be written separately by a client.
 func Dump(bc DumperRestorer, w *io.BinWriter, start, count uint32) error {
 	for i := start; i < start+count; i++ {
-		bh := bc.GetHeaderHash(int(i))
+		bh := bc.GetHeaderHash(i)
 		b, err := bc.GetBlock(bh)
 		if err != nil {
 			return err
diff --git a/pkg/core/dao/dao.go b/pkg/core/dao/dao.go
index f802dab52..8fb33efaa 100644
--- a/pkg/core/dao/dao.go
+++ b/pkg/core/dao/dao.go
@@ -1,7 +1,6 @@
 package dao
 
 import (
-	"bytes"
 	"context"
 	"encoding/binary"
 	"errors"
@@ -582,25 +581,23 @@ func (dao *Simple) GetStateSyncCurrentBlockHeight() (uint32, error) {
 	return binary.LittleEndian.Uint32(b), nil
 }
 
-// GetHeaderHashes returns a sorted list of header hashes retrieved from
+// GetHeaderHashes returns a page of header hashes retrieved from
 // the given underlying store.
-func (dao *Simple) GetHeaderHashes() ([]util.Uint256, error) {
-	var hashes = make([]util.Uint256, 0)
+func (dao *Simple) GetHeaderHashes(height uint32) ([]util.Uint256, error) {
+	var hashes []util.Uint256
 
-	var seekErr error
-	dao.Store.Seek(storage.SeekRange{
-		Prefix: dao.mkKeyPrefix(storage.IXHeaderHashList),
-	}, func(k, v []byte) bool {
-		newHashes, err := read2000Uint256Hashes(v)
-		if err != nil {
-			seekErr = fmt.Errorf("failed to read batch of 2000 header hashes: %w", err)
-			return false
-		}
-		hashes = append(hashes, newHashes...)
-		return true
-	})
+	key := dao.mkHeaderHashKey(height)
+	b, err := dao.Store.Get(key)
+	if err != nil {
+		return nil, err
+	}
 
-	return hashes, seekErr
+	br := io.NewBinReaderFromBuf(b)
+	br.ReadArray(&hashes)
+	if br.Err != nil {
+		return nil, br.Err
+	}
+	return hashes, nil
 }
 
 // DeleteHeaderHashes removes batches of header hashes starting from the one that
@@ -683,19 +680,6 @@ func (dao *Simple) PutStateSyncCurrentBlockHeight(h uint32) {
 	dao.Store.Put(dao.mkKeyPrefix(storage.SYSStateSyncCurrentBlockHeight), buf.Bytes())
 }
 
-// read2000Uint256Hashes attempts to read 2000 Uint256 hashes from
-// the given byte array.
-func read2000Uint256Hashes(b []byte) ([]util.Uint256, error) {
-	r := bytes.NewReader(b)
-	br := io.NewBinReaderFromIO(r)
-	hashes := make([]util.Uint256, 0)
-	br.ReadArray(&hashes)
-	if br.Err != nil {
-		return nil, br.Err
-	}
-	return hashes, nil
-}
-
 func (dao *Simple) mkHeaderHashKey(h uint32) []byte {
 	b := dao.getKeyBuf(1 + 4)
 	b[0] = byte(storage.IXHeaderHashList)
diff --git a/pkg/core/headerhashes.go b/pkg/core/headerhashes.go
new file mode 100644
index 000000000..4d9ce042a
--- /dev/null
+++ b/pkg/core/headerhashes.go
@@ -0,0 +1,211 @@
+package core
+
+import (
+	"fmt"
+	"sync"
+
+	lru "github.com/hashicorp/golang-lru"
+	"github.com/nspcc-dev/neo-go/pkg/core/block"
+	"github.com/nspcc-dev/neo-go/pkg/core/dao"
+	"github.com/nspcc-dev/neo-go/pkg/util"
+)
+
+const (
+	headerBatchCount = 2000
+	pagesCache       = 8
+)
+
+// HeaderHashes is a header hash manager part of the Blockchain. It can't be used
+// without Blockchain.
+type HeaderHashes struct {
+	// Backing storage.
+	dao *dao.Simple
+
+	// Lock for all internal state fields.
+	lock sync.RWMutex
+
+	// The latest header hashes (storedHeaderCount+).
+	latest []util.Uint256
+
+	// Previously completed page of header hashes (pre-storedHeaderCount).
+	previous []util.Uint256
+
+	// Number of headers stored in the chain file.
+	storedHeaderCount uint32
+
+	// Cache for accessed pages of header hashes.
+	cache *lru.Cache
+}
+
+func (h *HeaderHashes) initGenesis(dao *dao.Simple, hash util.Uint256) {
+	h.dao = dao
+	h.cache, _ = lru.New(pagesCache) // Never errors for positive size.
+	h.previous = make([]util.Uint256, headerBatchCount)
+	h.latest = make([]util.Uint256, 0, headerBatchCount)
+	h.latest = append(h.latest, hash)
+	dao.PutCurrentHeader(hash, 0)
+}
+
+func (h *HeaderHashes) init(dao *dao.Simple) error {
+	h.dao = dao
+	h.cache, _ = lru.New(pagesCache) // Never errors for positive size.
+
+	currHeaderHeight, currHeaderHash, err := h.dao.GetCurrentHeaderHeight()
+	if err != nil {
+		return fmt.Errorf("failed to retrieve current header info: %w", err)
+	}
+	h.storedHeaderCount = ((currHeaderHeight + 1) / headerBatchCount) * headerBatchCount
+
+	if h.storedHeaderCount >= headerBatchCount {
+		h.previous, err = h.dao.GetHeaderHashes(h.storedHeaderCount - headerBatchCount)
+		if err != nil {
+			return fmt.Errorf("failed to retrieve header hash page %d: %w", h.storedHeaderCount-headerBatchCount, err)
+		}
+	} else {
+		h.previous = make([]util.Uint256, headerBatchCount)
+	}
+	h.latest = make([]util.Uint256, 0, headerBatchCount)
+
+	// There is a high chance that the Node is stopped before the next
+	// batch of 2000 headers was stored. Via the currentHeaders stored we can sync
+	// that with stored blocks.
+	if currHeaderHeight >= h.storedHeaderCount {
+		hash := currHeaderHash
+		var targetHash util.Uint256
+		if h.storedHeaderCount >= headerBatchCount {
+			targetHash = h.previous[len(h.previous)-1]
+		}
+		headers := make([]util.Uint256, 0, headerBatchCount)
+
+		for hash != targetHash {
+			blk, err := h.dao.GetBlock(hash)
+			if err != nil {
+				return fmt.Errorf("could not get header %s: %w", hash, err)
+			}
+			headers = append(headers, blk.Hash())
+			hash = blk.PrevHash
+		}
+		hashSliceReverse(headers)
+		h.latest = append(h.latest, headers...)
+	}
+	return nil
+}
+
+func (h *HeaderHashes) lastHeaderIndex() uint32 {
+	return h.storedHeaderCount + uint32(len(h.latest)) - 1
+}
+
+// HeaderHeight returns the index/height of the highest header.
+func (h *HeaderHashes) HeaderHeight() uint32 {
+	h.lock.RLock()
+	n := h.lastHeaderIndex()
+	h.lock.RUnlock()
+	return n
+}
+
+func (h *HeaderHashes) addHeaders(headers ...*block.Header) error {
+	var (
+		batch      = h.dao.GetPrivate()
+		lastHeader *block.Header
+		err        error
+	)
+
+	h.lock.Lock()
+	defer h.lock.Unlock()
+
+	for _, head := range headers {
+		if head.Index != h.lastHeaderIndex()+1 {
+			continue
+		}
+		err = batch.StoreHeader(head)
+		if err != nil {
+			return err
+		}
+		lastHeader = head
+		h.latest = append(h.latest, head.Hash())
+		if len(h.latest) == headerBatchCount {
+			err = batch.StoreHeaderHashes(h.latest, h.storedHeaderCount)
+			if err != nil {
+				return err
+			}
+			copy(h.previous, h.latest)
+			h.latest = h.latest[:0]
+			h.storedHeaderCount += headerBatchCount
+		}
+	}
+	if lastHeader != nil {
+		batch.PutCurrentHeader(lastHeader.Hash(), lastHeader.Index)
+		updateHeaderHeightMetric(lastHeader.Index)
+		if _, err = batch.Persist(); err != nil {
+			return err
+		}
+	}
+	return nil
+}
+
+// CurrentHeaderHash returns the hash of the latest known header.
+func (h *HeaderHashes) CurrentHeaderHash() util.Uint256 {
+	var hash util.Uint256
+
+	h.lock.RLock()
+	if len(h.latest) > 0 {
+		hash = h.latest[len(h.latest)-1]
+	} else {
+		hash = h.previous[len(h.previous)-1]
+	}
+	h.lock.RUnlock()
+	return hash
+}
+
+// GetHeaderHash returns hash of the header/block with specified index, if
+// HeaderHashes doesn't have a hash for this height, zero Uint256 value is returned.
+func (h *HeaderHashes) GetHeaderHash(i uint32) util.Uint256 {
+	h.lock.RLock()
+	res, ok := h.getLocalHeaderHash(i)
+	h.lock.RUnlock()
+	if ok {
+		return res
+	}
+	// If it's not in the latest/previous, then it's in the cache or DB, those
+	// need no additional locks.
+	page := (i / headerBatchCount) * headerBatchCount
+	cache, ok := h.cache.Get(page)
+	if ok {
+		hashes := cache.([]util.Uint256)
+		return hashes[i-page]
+	}
+	hashes, err := h.dao.GetHeaderHashes(page)
+	if err != nil {
+		return util.Uint256{}
+	}
+	_ = h.cache.Add(page, hashes)
+	return hashes[i-page]
+}
+
+// getLocalHeaderHash looks for the index in the latest and previous caches.
+// Locking is left to the user.
+func (h *HeaderHashes) getLocalHeaderHash(i uint32) (util.Uint256, bool) {
+	if i > h.lastHeaderIndex() {
+		return util.Uint256{}, false
+	}
+	if i >= h.storedHeaderCount {
+		return h.latest[i-h.storedHeaderCount], true
+	}
+	previousStored := h.storedHeaderCount - headerBatchCount
+	if i >= previousStored {
+		return h.previous[i-previousStored], true
+	}
+	return util.Uint256{}, false
+}
+
+func (h *HeaderHashes) haveRecentHash(hash util.Uint256, i uint32) bool {
+	h.lock.RLock()
+	defer h.lock.RUnlock()
+	for ; i > 0; i-- {
+		lh, ok := h.getLocalHeaderHash(i)
+		if ok && hash.Equals(lh) {
+			return true
+		}
+	}
+	return false
+}
diff --git a/pkg/core/helper_test.go b/pkg/core/helper_test.go
index 63134688a..fc74f3849 100644
--- a/pkg/core/helper_test.go
+++ b/pkg/core/helper_test.go
@@ -59,7 +59,7 @@ func (bc *Blockchain) newBlock(txs ...*transaction.Transaction) *block.Block {
 	lastBlock, ok := bc.topBlock.Load().(*block.Block)
 	if !ok {
 		var err error
-		lastBlock, err = bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight())))
+		lastBlock, err = bc.GetBlock(bc.GetHeaderHash(bc.BlockHeight()))
 		if err != nil {
 			panic(err)
 		}
diff --git a/pkg/core/interop/context.go b/pkg/core/interop/context.go
index 85daf01ae..616ccfffd 100644
--- a/pkg/core/interop/context.go
+++ b/pkg/core/interop/context.go
@@ -40,7 +40,7 @@ type Ledger interface {
 	CurrentBlockHash() util.Uint256
 	GetBlock(hash util.Uint256) (*block.Block, error)
 	GetConfig() config.ProtocolConfiguration
-	GetHeaderHash(int) util.Uint256
+	GetHeaderHash(uint32) util.Uint256
 }
 
 // Context represents context in which interops are executed.
@@ -377,7 +377,7 @@ func (ic *Context) BlockHeight() uint32 {
 // CurrentBlockHash returns current block hash got from Context's block if it's set.
 func (ic *Context) CurrentBlockHash() util.Uint256 {
 	if ic.Block != nil {
-		return ic.Chain.GetHeaderHash(int(ic.Block.Index - 1)) // Persisting block is not yet stored.
+		return ic.Chain.GetHeaderHash(ic.Block.Index - 1) // Persisting block is not yet stored.
 	}
 	return ic.Chain.CurrentBlockHash()
 }
diff --git a/pkg/core/native/ledger.go b/pkg/core/native/ledger.go
index 708bc7e6d..51734c226 100644
--- a/pkg/core/native/ledger.go
+++ b/pkg/core/native/ledger.go
@@ -196,7 +196,7 @@ func getBlockHashFromItem(ic *interop.Context, item stackitem.Item) util.Uint256
 		if uint32(index) > ic.BlockHeight() {
 			panic(fmt.Errorf("no block with index %d", index))
 		}
-		return ic.Chain.GetHeaderHash(int(index))
+		return ic.Chain.GetHeaderHash(uint32(index))
 	}
 	hash, err := getUint256FromItem(item)
 	if err != nil {
diff --git a/pkg/core/native/native_test/ledger_test.go b/pkg/core/native/native_test/ledger_test.go
index 83455f4c5..1208aa298 100644
--- a/pkg/core/native/native_test/ledger_test.go
+++ b/pkg/core/native/native_test/ledger_test.go
@@ -111,7 +111,7 @@ func TestLedger_GetTransactionFromBlock(t *testing.T) {
 	ledgerInvoker := c.WithSigners(c.Committee)
 
 	ledgerInvoker.Invoke(t, e.Chain.BlockHeight(), "currentIndex") // Adds a block.
-	b := e.GetBlockByIndex(t, int(e.Chain.BlockHeight()))
+	b := e.GetBlockByIndex(t, e.Chain.BlockHeight())
 
 	check := func(t testing.TB, stack []stackitem.Item) {
 		require.Equal(t, 1, len(stack))
@@ -148,8 +148,8 @@ func TestLedger_GetBlock(t *testing.T) {
 	e := c.Executor
 	ledgerInvoker := c.WithSigners(c.Committee)
 
-	ledgerInvoker.Invoke(t, e.Chain.GetHeaderHash(int(e.Chain.BlockHeight())).BytesBE(), "currentHash") // Adds a block.
-	b := e.GetBlockByIndex(t, int(e.Chain.BlockHeight()))
+	ledgerInvoker.Invoke(t, e.Chain.GetHeaderHash(e.Chain.BlockHeight()).BytesBE(), "currentHash") // Adds a block.
+	b := e.GetBlockByIndex(t, e.Chain.BlockHeight())
 
 	expected := []stackitem.Item{
 		stackitem.NewByteArray(b.Hash().BytesBE()),
diff --git a/pkg/core/prometheus.go b/pkg/core/prometheus.go
index b81fb847d..f47b60e20 100644
--- a/pkg/core/prometheus.go
+++ b/pkg/core/prometheus.go
@@ -44,7 +44,7 @@ func updatePersistedHeightMetric(pHeight uint32) {
 	persistedHeight.Set(float64(pHeight))
 }
 
-func updateHeaderHeightMetric(hHeight int) {
+func updateHeaderHeightMetric(hHeight uint32) {
 	headerHeight.Set(float64(hHeight))
 }
 
diff --git a/pkg/core/statesync/module.go b/pkg/core/statesync/module.go
index 38d603e1d..58ce4c0cb 100644
--- a/pkg/core/statesync/module.go
+++ b/pkg/core/statesync/module.go
@@ -66,7 +66,7 @@ type Ledger interface {
 	BlockHeight() uint32
 	GetConfig() config.ProtocolConfiguration
 	GetHeader(hash util.Uint256) (*block.Header, error)
-	GetHeaderHash(int) util.Uint256
+	GetHeaderHash(uint32) util.Uint256
 	HeaderHeight() uint32
 }
 
@@ -214,7 +214,7 @@ func (s *Module) defineSyncStage() error {
 		s.log.Info("MPT is in sync",
 			zap.Uint32("stateroot height", s.stateMod.CurrentLocalHeight()))
 	} else if s.syncStage&headersSynced != 0 {
-		header, err := s.bc.GetHeader(s.bc.GetHeaderHash(int(s.syncPoint + 1)))
+		header, err := s.bc.GetHeader(s.bc.GetHeaderHash(s.syncPoint + 1))
 		if err != nil {
 			return fmt.Errorf("failed to get header to initialize MPT billet: %w", err)
 		}
diff --git a/pkg/core/statesync/neotest_test.go b/pkg/core/statesync/neotest_test.go
index d799d9afa..31a7a3589 100644
--- a/pkg/core/statesync/neotest_test.go
+++ b/pkg/core/statesync/neotest_test.go
@@ -16,9 +16,9 @@ import (
 )
 
 func TestStateSyncModule_Init(t *testing.T) {
-	var (
-		stateSyncInterval        = 2
-		maxTraceable      uint32 = 3
+	const (
+		stateSyncInterval = 2
+		maxTraceable      = 3
 	)
 	spoutCfg := func(c *config.ProtocolConfiguration) {
 		c.StateRootInHeader = true
@@ -55,7 +55,7 @@ func TestStateSyncModule_Init(t *testing.T) {
 
 	t.Run("inactive: bolt chain height is close enough to spout chain height", func(t *testing.T) {
 		bcBolt, _, _ := chain.NewMultiWithCustomConfig(t, boltCfg)
-		for i := 1; i < int(bcSpout.BlockHeight())-stateSyncInterval; i++ {
+		for i := uint32(1); i < bcSpout.BlockHeight()-stateSyncInterval; i++ {
 			b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i))
 			require.NoError(t, err)
 			require.NoError(t, bcBolt.AddBlock(b))
@@ -114,9 +114,9 @@ func TestStateSyncModule_Init(t *testing.T) {
 		require.NoError(t, module.Init(bcSpout.BlockHeight()))
 
 		// firstly, fetch all headers to create proper DB state (where headers are in sync)
-		stateSyncPoint := (int(bcSpout.BlockHeight()) / stateSyncInterval) * stateSyncInterval
+		stateSyncPoint := (bcSpout.BlockHeight() / stateSyncInterval) * stateSyncInterval
 		var expectedHeader *block.Header
-		for i := 1; i <= int(bcSpout.HeaderHeight()); i++ {
+		for i := uint32(1); i <= bcSpout.HeaderHeight(); i++ {
 			header, err := bcSpout.GetHeader(bcSpout.GetHeaderHash(i))
 			require.NoError(t, err)
 			require.NoError(t, module.AddHeaders(header))
@@ -142,7 +142,7 @@ func TestStateSyncModule_Init(t *testing.T) {
 		require.Equal(t, expectedHeader.PrevStateRoot, unknownNodes[0])
 
 		// add several blocks to create DB state where blocks are not in sync yet, but it's not a genesis.
-		for i := stateSyncPoint - int(maxTraceable) + 1; i <= stateSyncPoint-stateSyncInterval-1; i++ {
+		for i := stateSyncPoint - maxTraceable + 1; i <= stateSyncPoint-stateSyncInterval-1; i++ {
 			block, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i))
 			require.NoError(t, err)
 			require.NoError(t, module.AddBlock(block))
@@ -283,10 +283,10 @@ func TestStateSyncModule_Init(t *testing.T) {
 
 func TestStateSyncModule_RestoreBasicChain(t *testing.T) {
 	check := func(t *testing.T, spoutEnableGC bool) {
-		var (
-			stateSyncInterval        = 4
-			maxTraceable      uint32 = 6
-			stateSyncPoint           = 24
+		const (
+			stateSyncInterval = 4
+			maxTraceable      = 6
+			stateSyncPoint    = 24
 		)
 		spoutCfg := func(c *config.ProtocolConfiguration) {
 			c.KeepOnlyLatestState = spoutEnableGC
@@ -325,7 +325,7 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) {
 			require.Error(t, module.AddHeaders(h))
 		})
 		t.Run("no error: add blocks before initialisation", func(t *testing.T) {
-			b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(int(bcSpout.BlockHeight())))
+			b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(bcSpout.BlockHeight()))
 			require.NoError(t, err)
 			require.NoError(t, module.AddBlock(b))
 		})
@@ -342,7 +342,7 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) {
 		// add headers to module
 		headers := make([]*block.Header, 0, bcSpout.HeaderHeight())
 		for i := uint32(1); i <= bcSpout.HeaderHeight(); i++ {
-			h, err := bcSpout.GetHeader(bcSpout.GetHeaderHash(int(i)))
+			h, err := bcSpout.GetHeader(bcSpout.GetHeaderHash(i))
 			require.NoError(t, err)
 			headers = append(headers, h)
 		}
@@ -355,7 +355,7 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) {
 
 		// add blocks
 		t.Run("error: unexpected block index", func(t *testing.T) {
-			b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(stateSyncPoint - int(maxTraceable)))
+			b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(stateSyncPoint - maxTraceable))
 			require.NoError(t, err)
 			require.Error(t, module.AddBlock(b))
 		})
@@ -379,7 +379,7 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) {
 			require.Error(t, module.AddBlock(b))
 		})
 
-		for i := stateSyncPoint - int(maxTraceable) + 1; i <= stateSyncPoint; i++ {
+		for i := uint32(stateSyncPoint - maxTraceable + 1); i <= stateSyncPoint; i++ {
 			b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i))
 			require.NoError(t, err)
 			require.NoError(t, module.AddBlock(b))
@@ -432,7 +432,7 @@ func TestStateSyncModule_RestoreBasicChain(t *testing.T) {
 		require.Equal(t, uint32(stateSyncPoint), bcBolt.BlockHeight())
 
 		// add missing blocks to bcBolt: should be ok, because state is synced
-		for i := stateSyncPoint + 1; i <= int(bcSpout.BlockHeight()); i++ {
+		for i := uint32(stateSyncPoint + 1); i <= bcSpout.BlockHeight(); i++ {
 			b, err := bcSpout.GetBlock(bcSpout.GetHeaderHash(i))
 			require.NoError(t, err)
 			require.NoError(t, bcBolt.AddBlock(b))
diff --git a/pkg/core/util.go b/pkg/core/util.go
index 8ba894c52..2694e8885 100644
--- a/pkg/core/util.go
+++ b/pkg/core/util.go
@@ -64,8 +64,8 @@ func getNextConsensusAddress(validators []*keys.PublicKey) (val util.Uint160, er
 	return hash.Hash160(raw), nil
 }
 
-// headerSliceReverse reverses the given slice of *Header.
-func headerSliceReverse(dest []*block.Header) {
+// hashSliceReverse reverses the given slice of util.Uint256.
+func hashSliceReverse(dest []util.Uint256) {
 	for i, j := 0, len(dest)-1; i < j; i, j = i+1, j-1 {
 		dest[i], dest[j] = dest[j], dest[i]
 	}
diff --git a/pkg/neotest/basic.go b/pkg/neotest/basic.go
index b0653c940..8c1c5b307 100644
--- a/pkg/neotest/basic.go
+++ b/pkg/neotest/basic.go
@@ -52,7 +52,7 @@ func NewExecutor(t testing.TB, bc *core.Blockchain, validator, committee Signer)
 
 // TopBlock returns the block with the highest index.
 func (e *Executor) TopBlock(t testing.TB) *block.Block {
-	b, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(int(e.Chain.BlockHeight())))
+	b, err := e.Chain.GetBlock(e.Chain.GetHeaderHash(e.Chain.BlockHeight()))
 	require.NoError(t, err)
 	return b
 }
@@ -361,7 +361,7 @@ func (e *Executor) AddBlockCheckHalt(t testing.TB, txs ...*transaction.Transacti
 
 // TestInvoke creates a test VM with a dummy block and executes a transaction in it.
 func TestInvoke(bc *core.Blockchain, tx *transaction.Transaction) (*vm.VM, error) {
-	lastBlock, err := bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight())))
+	lastBlock, err := bc.GetBlock(bc.GetHeaderHash(bc.BlockHeight()))
 	if err != nil {
 		return nil, err
 	}
@@ -392,7 +392,7 @@ func (e *Executor) GetTransaction(t testing.TB, h util.Uint256) (*transaction.Tr
 }
 
 // GetBlockByIndex returns a block by the specified index.
-func (e *Executor) GetBlockByIndex(t testing.TB, idx int) *block.Block {
+func (e *Executor) GetBlockByIndex(t testing.TB, idx uint32) *block.Block {
 	h := e.Chain.GetHeaderHash(idx)
 	require.NotEmpty(t, h)
 	b, err := e.Chain.GetBlock(h)
diff --git a/pkg/network/server.go b/pkg/network/server.go
index 4a8512e11..11737d61b 100644
--- a/pkg/network/server.go
+++ b/pkg/network/server.go
@@ -61,7 +61,7 @@ type (
 		GetBlock(hash util.Uint256) (*block.Block, error)
 		GetConfig() config.ProtocolConfiguration
 		GetHeader(hash util.Uint256) (*block.Header, error)
-		GetHeaderHash(int) util.Uint256
+		GetHeaderHash(uint32) util.Uint256
 		GetMaxVerificationGAS() int64
 		GetMemPool() *mempool.Pool
 		GetNotaryBalance(acc util.Uint160) *big.Int
@@ -972,7 +972,7 @@ func (s *Server) handleGetBlocksCmd(p Peer, gb *payload.GetBlocks) error {
 	}
 	blockHashes := make([]util.Uint256, 0)
 	for i := start.Index + 1; i <= start.Index+uint32(count); i++ {
-		hash := s.chain.GetHeaderHash(int(i))
+		hash := s.chain.GetHeaderHash(i)
 		if hash.Equals(util.Uint256{}) {
 			break
 		}
@@ -995,7 +995,7 @@ func (s *Server) handleGetBlockByIndexCmd(p Peer, gbd *payload.GetBlockByIndex)
 		count = payload.MaxHashesCount
 	}
 	for i := gbd.IndexStart; i < gbd.IndexStart+uint32(count); i++ {
-		hash := s.chain.GetHeaderHash(int(i))
+		hash := s.chain.GetHeaderHash(i)
 		if hash.Equals(util.Uint256{}) {
 			break
 		}
@@ -1026,7 +1026,7 @@ func (s *Server) handleGetHeadersCmd(p Peer, gh *payload.GetBlockByIndex) error
 	resp := payload.Headers{}
 	resp.Hdrs = make([]*block.Header, 0, count)
 	for i := gh.IndexStart; i < gh.IndexStart+uint32(count); i++ {
-		hash := s.chain.GetHeaderHash(int(i))
+		hash := s.chain.GetHeaderHash(i)
 		if hash.Equals(util.Uint256{}) {
 			break
 		}
diff --git a/pkg/services/rpcsrv/client_test.go b/pkg/services/rpcsrv/client_test.go
index b93ac2e78..f9749d261 100644
--- a/pkg/services/rpcsrv/client_test.go
+++ b/pkg/services/rpcsrv/client_test.go
@@ -1272,7 +1272,7 @@ func TestInvokeVerify(t *testing.T) {
 	})
 
 	t.Run("positive, historic, by block, with signer", func(t *testing.T) {
-		res, err := c.InvokeContractVerifyWithState(chain.GetHeaderHash(int(chain.BlockHeight())-1), contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}})
+		res, err := c.InvokeContractVerifyWithState(chain.GetHeaderHash(chain.BlockHeight()-1), contract, []smartcontract.Parameter{}, []transaction.Signer{{Account: testchain.PrivateKeyByID(0).PublicKey().GetScriptHash()}})
 		require.NoError(t, err)
 		require.Equal(t, "HALT", res.State)
 		require.Equal(t, 1, len(res.Stack))
diff --git a/pkg/services/rpcsrv/server.go b/pkg/services/rpcsrv/server.go
index 349c2676e..b0dd254b4 100644
--- a/pkg/services/rpcsrv/server.go
+++ b/pkg/services/rpcsrv/server.go
@@ -78,7 +78,7 @@ type (
 		GetEnrollments() ([]state.Validator, error)
 		GetGoverningTokenBalance(acc util.Uint160) (*big.Int, uint32)
 		GetHeader(hash util.Uint256) (*block.Header, error)
-		GetHeaderHash(int) util.Uint256
+		GetHeaderHash(uint32) util.Uint256
 		GetMaxVerificationGAS() int64
 		GetMemPool() *mempool.Pool
 		GetNEP11Contracts() []util.Uint160
@@ -652,7 +652,7 @@ func (s *Server) fillBlockMetadata(obj io.Serializable, h *block.Header) result.
 		Confirmations: s.chain.BlockHeight() - h.Index + 1,
 	}
 
-	hash := s.chain.GetHeaderHash(int(h.Index) + 1)
+	hash := s.chain.GetHeaderHash(h.Index + 1)
 	if !hash.Equals(util.Uint256{}) {
 		res.NextBlockHash = &hash
 	}
@@ -1646,7 +1646,7 @@ func (s *Server) getrawtransaction(reqParams params.Params) (interface{}, *neorp
 		if height == math.MaxUint32 { // Mempooled transaction.
 			return res, nil
 		}
-		_header := s.chain.GetHeaderHash(int(height))
+		_header := s.chain.GetHeaderHash(height)
 		header, err := s.chain.GetHeader(_header)
 		if err != nil {
 			return nil, neorpc.NewRPCError("Failed to get header for the transaction", err.Error())
@@ -2037,15 +2037,12 @@ func (s *Server) getHistoricParams(reqParams params.Params) (uint32, *neorpc.Err
 			if err != nil {
 				return 0, neorpc.NewInvalidParamsError(fmt.Sprintf("unknown block or stateroot: %s", err))
 			}
-			height = int(stateH)
+			height = stateH
 		} else {
-			height = int(b.Index)
+			height = b.Index
 		}
 	}
-	if height > math.MaxUint32 {
-		return 0, neorpc.NewInvalidParamsError("historic height exceeds max uint32 value")
-	}
-	return uint32(height) + 1, nil
+	return height + 1, nil
 }
 
 func (s *Server) prepareInvocationContext(t trigger.Type, script []byte, contractScriptHash util.Uint160, tx *transaction.Transaction, nextH *uint32, verbose bool) (*interop.Context, *neorpc.Error) {
@@ -2683,16 +2680,16 @@ drainloop:
 	close(s.notaryRequestCh)
 }
 
-func (s *Server) blockHeightFromParam(param *params.Param) (int, *neorpc.Error) {
+func (s *Server) blockHeightFromParam(param *params.Param) (uint32, *neorpc.Error) {
 	num, err := param.GetInt()
 	if err != nil {
 		return 0, neorpc.ErrInvalidParams
 	}
 
-	if num < 0 || num > int(s.chain.BlockHeight()) {
+	if num < 0 || int64(num) > int64(s.chain.BlockHeight()) {
 		return 0, invalidBlockHeightError(0, num)
 	}
-	return num, nil
+	return uint32(num), nil
 }
 
 func (s *Server) packResponse(r *params.In, result interface{}, respErr *neorpc.Error) abstract {
diff --git a/pkg/services/rpcsrv/server_test.go b/pkg/services/rpcsrv/server_test.go
index ec6270e86..134dc4dd8 100644
--- a/pkg/services/rpcsrv/server_test.go
+++ b/pkg/services/rpcsrv/server_test.go
@@ -2271,7 +2271,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
 		})
 
 		t.Run("verbose != 0", func(t *testing.T) {
-			nextHash := chain.GetHeaderHash(int(hdr.Index) + 1)
+			nextHash := chain.GetHeaderHash(hdr.Index + 1)
 			expected := &result.Header{
 				Header: *hdr,
 				BlockMetadata: result.BlockMetadata{
@@ -2315,7 +2315,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
 		testNEP17T := func(t *testing.T, start, stop, limit, page int, sent, rcvd []int) {
 			ps := []string{`"` + testchain.PrivateKeyByID(0).Address() + `"`}
 			if start != 0 {
-				h, err := e.chain.GetHeader(e.chain.GetHeaderHash(start))
+				h, err := e.chain.GetHeader(e.chain.GetHeaderHash(uint32(start)))
 				var ts uint64
 				if err == nil {
 					ts = h.Timestamp
@@ -2325,7 +2325,7 @@ func testRPCProtocol(t *testing.T, doRPCCall func(string, string, *testing.T) []
 				ps = append(ps, strconv.FormatUint(ts, 10))
 			}
 			if stop != 0 {
-				h, err := e.chain.GetHeader(e.chain.GetHeaderHash(stop))
+				h, err := e.chain.GetHeader(e.chain.GetHeaderHash(uint32(stop)))
 				var ts uint64
 				if err == nil {
 					ts = h.Timestamp
@@ -2846,7 +2846,7 @@ func checkNep17TransfersAux(t *testing.T, e *executor, acc interface{}, sent, rc
 	rublesHash, err := util.Uint160DecodeStringLE(testContractHash)
 	require.NoError(t, err)
 
-	blockWithFAULTedTx, err := e.chain.GetBlock(e.chain.GetHeaderHash(int(faultedTxBlock))) // Transaction with ABORT inside.
+	blockWithFAULTedTx, err := e.chain.GetBlock(e.chain.GetHeaderHash(faultedTxBlock)) // Transaction with ABORT inside.
 	require.NoError(t, err)
 	require.Equal(t, 1, len(blockWithFAULTedTx.Transactions))
 	txFAULTed := blockWithFAULTedTx.Transactions[0]
diff --git a/scripts/gendump/main.go b/scripts/gendump/main.go
index 6f79ff917..4e7a0d309 100644
--- a/scripts/gendump/main.go
+++ b/scripts/gendump/main.go
@@ -66,7 +66,7 @@ func main() {
 	handleError("can't get next block validators", err)
 	valScript, err := smartcontract.CreateDefaultMultiSigRedeemScript(nbVals)
 	handleError("can't create verification script", err)
-	lastBlock, err := bc.GetBlock(bc.GetHeaderHash(int(bc.BlockHeight())))
+	lastBlock, err := bc.GetBlock(bc.GetHeaderHash(bc.BlockHeight()))
 	handleError("can't fetch last block", err)
 
 	txMoveNeo, err := testchain.NewTransferFromOwner(bc, bc.GoverningTokenHash(), h, native.NEOTotalSupply, 0, 2)