Repository: lightninglabs/neutrino

Issue:
neutrino: add support for side-header loading on initial sync
One way to speed up the initial sync for `neutrino` is to actually package a set of headers (both regular and filter headers) along side the application that packages `neutrino`. This would allow one to package a set of (possibly compressed) headers that will be written to disk on start up before we start to fetch headers from the network.

Steps To Completion 
- [ ] Modify the initial constructor to add a new set of functional options for side loaded headers. 

- [ ] On start up, before syncing, if this is IBD, we should read these headers and write them directly to disk. In the suggested model, we skip verification all together, as it's assumed that these headers are being fetched from a trusted source. 

- [ ] As a bonus, we can also compress the set of headers, and decompress them within `neutrino`. This may be useful for contexts such as mobile applications, where reducing the size of the apk is desirable. An example of a header specific compression scheme we may want to look at is: https://github.com/petertodd/rust-bitcoin-headers#how-it-works. There likely some additional optimizations on top of this that we can explore and later implement. 

Discussion:
@rawtxapp: This could be useful for more than just the initial sync.

For example, on iOS, while Apple doesn't allow arbitrary code execution, it does allow background downloads. So what we could do is, once or twice a day, download the latest filters in the background and sync with them when the app is opened.

I think the filter fetching logic might need to be reimplemented in swift, but it's feasible. Or if the user has a trusted neutrino node with some sort of http endpoint serving filters, it could potentially just download those.@Chinwendu20: Hello, how about this for a header compression scheme:
https://github.com/willcl-ark/compressed-block-headers/blob/v1.0/compressed-block-headers.adoc@guggero: That sounds like an interesting optimization! Though I'm not sure if anyone has ever actually implemented or used that proposed format. Also I'm not sure if removing the previous hash is a good idea in the context of Neutrino as that would make it impossible to verify the headers in parallel (as you would strictly need to calculate them one-by-one in order). But perhaps that's a small tradeoff for the win in compression that we get from it.

BTW, I started with a small side project (https://github.com/guggero/cfilter-cdn) for creating block header, filter header and filter files that could be distributed over HTTP (with the idea to support fetching them from a Neutrino client over a CDN).
Maybe we could implement the optimization there to see how much of a difference it really makes.

The current file sizes for mainnet (~780k blocks) are:
 - block headers: 54MB
 - filter headers: 23MB
 - filters: 8.5GB

So I think it would make sense to side-load the block headers and filter headers but then actually fetch the filters that we need (deciding from the wallet's birthday).@Chinwendu20: Please I would like to get everyone's perspective on this:

If we want to preload cfheaders in neutrino's store, we would need a source that serves cfheaders.

But the thing is that if we have a source of cfheaders, we really cannot verify it as far as I know ( please if you think otherwise, point me to a piece of code or doc).

What would be more useful is having a source of filters, so we doublehash it according to bip157 and verify with checkpoints.

What do you all think, we sideload cfheaders with a source of filters which we would use to create cfheaders that we would store in neutrino?@guggero: Yes, as far as I know we cannot validate the cfheaders by themselves. But what we can do is after preloading connect to a certain number of peers and compare our latest cfheader with theirs. If they match up, that's a good sign. And then whenever we fetch an actual filter from the network, we can validate its cfheader as well (previous cfheader hash plus the filter hash should equal current cfheader hash, or something like this, haven't looked it up in a while).

I don't think pre-loading filters makes sense, since they are the large part (you don't want your app to be 9 GB in size) and you often don't need all of them anyway (you'd only ever download those for the block since your wallet birthday block).

The whole point of pre-loading is to trade speed vs. trust. So you do have to trust the source of the pre-loaded files somewhat. But since everyone could run their own [`block-dn`](https://github.com/guggero/block-dn) and create their own pre-load files (or fetch them over HTTPS instead of packing the files into an app), the trust assumptions should be somewhat mitigated.@Chinwendu20: Thanks for  this
> Yes, as far as I know we cannot validate the cfheaders by themselves. But what we can do is after preloading connect to a certain number of peers and compare our latest cfheader with theirs. 

I do not know if this would work as I thinkthe latest cfheader checking out as valid does not necessarily mean the rest of the  cfheaders preloaded would be. 


> I don't think pre-loading filters makes sense, since they are the large part (you don't want your app to be 9 GB in size) and you often don't need all of them anyway (you'd only ever download those for the block since your wallet birthday block).

Even with p2p sync though, a [call to fetch cfheaders returns filter hashses](C:/Users/amaka/go/pkg/mod/github.com/btcsuite/btcd@v0.23.5-0.20230711222809-7faa9b266231/wire/msgcfheaders.go:29) and not cfheaders so we would just be mimicking the same behavior for non-p2p. Also I we would just use the filters to create cfheaders then verify with checkpoints and then *not store the actual filters* just as it is done with p2p sync.

> 
> The whole point of pre-loading is to trade speed vs. trust. So you do have to trust the source of the pre-loaded files somewhat. But since everyone could run their own [`block-dn`](https://github.com/guggero/block-dn) and create their own pre-load files (or fetch them over HTTPS instead of packing the files into an app), the trust assumptions should be somewhat mitigated.

So we skip verifying entirely?



Solution patch:
diff --git a/blockmanager.go b/blockmanager.go
index ec69a6a..42e9456 100644
--- a/blockmanager.go
+++ b/blockmanager.go
@@ -1371,7 +1371,7 @@ func (b *blockManager) resolveConflict(
 	for peer, cp := range checkpoints {
 		for i, header := range cp {
 			height := uint32((i + 1) * wire.CFCheckptInterval)
-			err := chainsync.ControlCFHeader(
+			err := chainsync.ValidateCFHeader(
 				b.cfg.ChainParams, fType, height, header,
 			)
 			if err == chainsync.ErrCheckpointMismatch {
diff --git a/chainimport/block_headers_validator.go b/chainimport/block_headers_validator.go
new file mode 100644
index 0000000..fd8aa95
--- /dev/null
+++ b/chainimport/block_headers_validator.go
@@ -0,0 +1,317 @@
+package chainimport
+
+import (
+	"context"
+	"fmt"
+	"math"
+
+	"github.com/btcsuite/btcd/blockchain"
+	"github.com/btcsuite/btcd/chaincfg"
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/lightninglabs/neutrino/headerfs"
+)
+
+// blockHeadersImportSourceValidator implements headersValidator for block
+// headers.
+type blockHeadersImportSourceValidator struct {
+	// targetChainParams contains the blockchain network parameters for
+	// validation against the target chain.
+	targetChainParams chaincfg.Params
+
+	// targetBlockHeaderStore is the destination store where validated
+	// headers will be written later in the import process.
+	targetBlockHeaderStore headerfs.BlockHeaderStore
+
+	// flags defines the behavior flags used during header validation.
+	flags blockchain.BehaviorFlags
+
+	// blockHeadersImportSource provides access to the source headers
+	// for validation and lookup operations.
+	blockHeadersImportSource HeaderImportSource
+}
+
+// Compile-time assertion to ensure blockHeadersImportSourceValidator implements
+// headersValidator interface.
+var _ HeadersValidator = (*blockHeadersImportSourceValidator)(nil)
+
+// newBlockHeadersImportSourceValidator creates a new validator for block
+// headers import source.
+func newBlockHeadersImportSourceValidator(targetChainParams chaincfg.Params,
+	targetBlockHeaderStore headerfs.BlockHeaderStore,
+	flags blockchain.BehaviorFlags,
+	blockHeadersImportSource HeaderImportSource) HeadersValidator {
+
+	return &blockHeadersImportSourceValidator{
+		targetChainParams:        targetChainParams,
+		targetBlockHeaderStore:   targetBlockHeaderStore,
+		flags:                    flags,
+		blockHeadersImportSource: blockHeadersImportSource,
+	}
+}
+
+// Validate performs thorough validation of a batch of block headers.
+func (v *blockHeadersImportSourceValidator) Validate(ctx context.Context,
+	it HeaderIterator) error {
+
+	var (
+		start      = it.GetStartIndex()
+		end        = it.GetEndIndex()
+		batchSize  = it.GetBatchSize()
+		count      = 0
+		lastHeader Header
+	)
+
+	for batch, err := range it.BatchIterator(start, end, batchSize) {
+		if err != nil {
+			return fmt.Errorf("failed to get next batch for "+
+				"validation: %w", err)
+		}
+
+		if err := ctxCancelled(ctx); err != nil {
+			return nil
+		}
+
+		if err = v.ValidateBatch(batch); err != nil {
+			return fmt.Errorf("batch validation failed at "+
+				"position %d: %w", count, err)
+		}
+
+		// If this is not the first batch, validate header connection
+		// points between batches.
+		if lastHeader != nil && len(batch) > 0 {
+			err := v.ValidatePair(lastHeader, batch[0])
+			if err != nil {
+				return fmt.Errorf("cross-batch validation "+
+					"failed at position %d: %w", count, err)
+			}
+		}
+
+		count += len(batch)
+		if len(batch) > 0 {
+			lastHeader = batch[len(batch)-1]
+		}
+	}
+
+	log.Debugf("Successfully validated %d block headers", count)
+	return nil
+}
+
+// ValidateSingle validates a single block header for basic sanity.
+func (v *blockHeadersImportSourceValidator) ValidateSingle(h Header) error {
+	header, err := assertBlockHeader(h)
+	if err != nil {
+		return err
+	}
+
+	return blockchain.CheckBlockHeaderSanity(
+		header.BlockHeader.BlockHeader, v.targetChainParams.PowLimit,
+		blockchain.NewMedianTime(), v.flags,
+	)
+}
+
+// ValidatePair verifies that two consecutive block headers form a valid chain
+// link.
+func (v *blockHeadersImportSourceValidator) ValidatePair(prev,
+	current Header) error {
+
+	prevBlk, err := assertBlockHeader(prev)
+	if err != nil {
+		return err
+	}
+	currentBlk, err := assertBlockHeader(current)
+	if err != nil {
+		return err
+	}
+
+	prevBlockHeader := prevBlk.BlockHeader
+	currBlockHeader := currentBlk.BlockHeader
+	prevHeight, currHeight := prevBlk.Height, currentBlk.Height
+
+	if currHeight != prevHeight+1 {
+		return fmt.Errorf("height mismatch: previous height=%d, "+
+			"current height=%d", prevHeight, currHeight)
+	}
+
+	prevHash := prevBlockHeader.BlockHash()
+	if !currBlockHeader.PrevBlock.IsEqual(&prevHash) {
+		return fmt.Errorf("header chain broken: current header's "+
+			"PrevBlock (%v) doesn't match previous header's hash "+
+			"(%v)", currBlockHeader.PrevBlock, prevHash)
+	}
+
+	parentCtx := &lightHeaderCtx{
+		height:    int32(prevHeight),
+		bits:      prevBlockHeader.Bits,
+		timestamp: prevBlockHeader.Timestamp.Unix(),
+		validator: v,
+	}
+
+	tCP := v.targetChainParams
+
+	chainCtx := &lightChainCtx{
+		params: &tCP,
+		blocksPerRetarget: int32(tCP.TargetTimespan.Seconds() /
+			tCP.TargetTimePerBlock.Seconds()),
+		minRetargetTimespan: int64(tCP.TargetTimespan.Seconds() /
+			float64(tCP.RetargetAdjustmentFactor)),
+		maxRetargetTimespan: int64(tCP.TargetTimespan.Seconds() *
+			float64(tCP.RetargetAdjustmentFactor)),
+	}
+
+	if err := blockchain.CheckBlockHeaderContext(
+		currBlockHeader.BlockHeader, parentCtx, v.flags, chainCtx, true,
+	); err != nil {
+		return fmt.Errorf("block header contextual validation "+
+			"failed: %w", err)
+	}
+
+	if err := v.ValidateSingle(current); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ValidateBatch performs validation on a batch of block headers.
+func (v *blockHeadersImportSourceValidator) ValidateBatch(
+	headers []Header) error {
+
+	if len(headers) == 1 {
+		return v.ValidateSingle(headers[0])
+	}
+
+	for i := 1; i < len(headers); i++ {
+		if err := v.ValidatePair(headers[i-1], headers[i]); err != nil {
+			return fmt.Errorf("validation failed at batch "+
+				"position %d: %w", i, err)
+		}
+	}
+
+	return nil
+}
+
+// lightHeaderCtx implements the blockchain.HeaderCtx interface.
+type lightHeaderCtx struct {
+	height    int32
+	bits      uint32
+	timestamp int64
+	validator *blockHeadersImportSourceValidator
+}
+
+// Compile-time assertion to ensure lightHeaderCtx implements
+// blockchain.HeaderCtx interface.
+var _ blockchain.HeaderCtx = (*lightHeaderCtx)(nil)
+
+// Height returns the height for the underlying header this context was created
+// from.
+func (l *lightHeaderCtx) Height() int32 {
+	return l.height
+}
+
+// Bits returns the difficulty bits for the underlying header this context was
+// created from.
+func (l *lightHeaderCtx) Bits() uint32 {
+	return l.bits
+}
+
+// Timestamp returns the timestamp for the underlying header this context was
+// created from.
+func (l *lightHeaderCtx) Timestamp() int64 {
+	return l.timestamp
+}
+
+// RelativeAncestorCtx returns the ancestor header context that is distance
+// blocks before the current header.
+func (l *lightHeaderCtx) RelativeAncestorCtx(
+	distance int32) blockchain.HeaderCtx {
+
+	ancestorHeight := uint32(math.Max(0, float64(l.height-distance)))
+
+	ancestorIndex := targetHeightToImportSourceIndex(
+		ancestorHeight, 0,
+	)
+
+	// Lookup the ancestor in the target store.
+	targetStore := l.validator.targetBlockHeaderStore
+	ancestor, err := targetStore.FetchHeaderByHeight(ancestorHeight)
+	if err == nil {
+		return &lightHeaderCtx{
+			height:    int32(ancestorHeight),
+			bits:      ancestor.Bits,
+			timestamp: ancestor.Timestamp.Unix(),
+		}
+	}
+
+	// Fallback to import source if ancestor not found in target store.
+	importAncestor, err := l.validator.blockHeadersImportSource.GetHeader(
+		ancestorIndex,
+	)
+	if err != nil {
+		return nil
+	}
+
+	importBlockAncestor, err := assertBlockHeader(importAncestor)
+	if err != nil {
+		return nil
+	}
+
+	return &lightHeaderCtx{
+		height:    int32(ancestorHeight),
+		bits:      importBlockAncestor.BlockHeader.Bits,
+		timestamp: importBlockAncestor.BlockHeader.Timestamp.Unix(),
+	}
+}
+
+// Parent returns the parent header context.
+func (l *lightHeaderCtx) Parent() blockchain.HeaderCtx {
+	return nil
+}
+
+// lightChainCtx implements the blockchain.ChainCtx interface.
+type lightChainCtx struct {
+	params              *chaincfg.Params
+	blocksPerRetarget   int32
+	minRetargetTimespan int64
+	maxRetargetTimespan int64
+}
+
+// Compile-time assertion to ensure lightChainCtx implements
+// blockchain.ChainCtx interface.
+var _ blockchain.ChainCtx = (*lightChainCtx)(nil)
+
+// ChainParams returns the chain parameters for the underlying chain this
+// context was created from.
+func (l *lightChainCtx) ChainParams() *chaincfg.Params {
+	return l.params
+}
+
+// BlocksPerRetarget returns the number of blocks before retargeting occurs.
+func (l *lightChainCtx) BlocksPerRetarget() int32 {
+	return l.blocksPerRetarget
+}
+
+// MinRetargetTimespan returns the minimum amount of time to use in the
+// difficulty calculation.
+func (l *lightChainCtx) MinRetargetTimespan() int64 {
+	return l.minRetargetTimespan
+}
+
+// MaxRetargetTimespan returns the maximum amount of time to use in the
+// difficulty calculation.
+func (l *lightChainCtx) MaxRetargetTimespan() int64 {
+	return l.maxRetargetTimespan
+}
+
+// VerifyCheckpoint returns whether the passed height and hash match the
+// checkpoint data.
+func (l *lightChainCtx) VerifyCheckpoint(height int32,
+	hash *chainhash.Hash) bool {
+
+	return false
+}
+
+// FindPreviousCheckpoint returns the most recent checkpoint that we have
+// validated.
+func (l *lightChainCtx) FindPreviousCheckpoint() (blockchain.HeaderCtx, error) {
+	return nil, nil
+}
diff --git a/chainimport/file_source.go b/chainimport/file_source.go
new file mode 100644
index 0000000..5040d22
--- /dev/null
+++ b/chainimport/file_source.go
@@ -0,0 +1,219 @@
+package chainimport
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+
+	"golang.org/x/exp/mmap"
+)
+
+// fileHeaderImportSource implements headerImportSource for header files.
+//
+// Expected file format:
+//   - ImportMetadata (10 bytes): Network magic (4), version (1),
+//     header type (1), start height (4)
+//   - Header data: Consecutive raw headers starting from the specified height
+//
+// The file contains a fixed-size metadata header followed by a sequence of
+// blockchain headers. Each header's size depends on its type – 80 bytes for
+// block headers and 32 bytes for filter headers. Headers must be stored
+// consecutively without gaps or padding.
+type fileHeaderImportSource struct {
+	// uri is the file path or location identifier for the header source.
+	uri string
+
+	// file provides the underlying file interface for reading header data.
+	file ImportHeadersFile
+
+	// fileSize stores the total size of the header file in bytes.
+	fileSize int
+
+	// metadata contains parsed header metadata.
+	metadata *headerMetadata
+
+	// headerFactory creates new Header instances for deserialization.
+	headerFactory func() Header
+
+	// headerBuffer is a pre-allocated buffer for reading header data.
+	headerBuffer []byte
+}
+
+// Compile-time assertion to ensure fileHeaderImportSource implements
+// headerImportSource interface.
+var _ HeaderImportSource = (*fileHeaderImportSource)(nil)
+
+// newFileHeaderImportSource creates a new file header import source with the
+// given URI and header factory.
+func newFileHeaderImportSource(uri string,
+	headerFactory func() Header) *fileHeaderImportSource {
+
+	return &fileHeaderImportSource{
+		uri:           uri,
+		headerFactory: headerFactory,
+	}
+}
+
+// Open opens the file and initializes the reader.
+func (f *fileHeaderImportSource) Open() error {
+	r, err := mmap.Open(f.GetURI())
+	if err != nil {
+		return fmt.Errorf("failed to mmap file: %w", err)
+	}
+
+	f.file = newMmapFile(r)
+	f.fileSize = f.file.Len()
+
+	mData, err := f.GetHeaderMetadata()
+	if err != nil {
+		return fmt.Errorf("failed to get header metadata: %w", err)
+	}
+	f.metadata = mData
+	f.metadata.endHeight = mData.startHeight + mData.headersCount - 1
+
+	f.headerBuffer = make([]byte, mData.headerSize)
+	return nil
+}
+
+// Close closes the file and releases the mmap reader.
+func (f *fileHeaderImportSource) Close() error {
+	return f.file.Close()
+}
+
+// GetHeaderMetadata reads the metadata from the file. The metadata is memoized
+// after the first call, with subsequent calls returning the cached result
+// without re-reading the file.
+func (f *fileHeaderImportSource) GetHeaderMetadata() (*headerMetadata, error) {
+	if f.metadata != nil {
+		return f.metadata, nil
+	}
+
+	if f.file == nil {
+		return nil, errors.New("file reader not initialized")
+	}
+
+	importMetadata := &importMetadata{}
+	if err := importMetadata.decode(f.file); err != nil {
+		return nil, err
+	}
+
+	headerMetadata := &headerMetadata{
+		importMetadata: importMetadata,
+	}
+
+	headerSize, err := importMetadata.headerType.Size()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get header size: %v", err)
+	}
+	headerMetadata.headerSize = headerSize
+
+	usableFileSize := f.fileSize - importMetadata.size()
+
+	if usableFileSize == 0 {
+		return nil, errors.New("no headers available in import source")
+	}
+
+	if usableFileSize%headerSize != 0 {
+		return nil, fmt.Errorf("file size (%d) is not a multiple of "+
+			"header size (%d); possible data corruption",
+			usableFileSize, headerSize)
+	}
+
+	headerMetadata.headersCount = uint32(usableFileSize / headerSize)
+
+	return headerMetadata, err
+}
+
+// GetHeader retrieves a single header at the specified index.
+func (f *fileHeaderImportSource) GetHeader(index uint32) (Header, error) {
+	var empty Header
+
+	if f.file == nil {
+		return empty, errors.New("file reader not initialized")
+	}
+	if f.metadata == nil {
+		return empty, errors.New("header metadata not initialized")
+	}
+
+	offset := ImportMetadataSize + (index * uint32(f.metadata.headerSize))
+
+	_, err := f.file.ReadAt(f.headerBuffer, int64(offset))
+	if err != nil {
+		return empty, fmt.Errorf("failed to read header at "+
+			"index %d: %w", index, err)
+	}
+	reader := bytes.NewReader(f.headerBuffer)
+
+	height := index + f.metadata.startHeight
+
+	header := f.headerFactory()
+	if err := header.Deserialize(reader, height); err != nil {
+		return empty, err
+	}
+
+	return header, nil
+}
+
+// Iterator returns an efficient iterator for sequential header access.
+func (f *fileHeaderImportSource) Iterator(start, end uint32,
+	batchSize uint32) HeaderIterator {
+
+	return &importSourceHeaderIterator{
+		source:     f,
+		startIndex: start,
+		endIndex:   end,
+		batchSize:  batchSize,
+	}
+}
+
+// SetURI sets the file path for this import source. This method is primarily
+// used by HTTP import sources to dynamically update the file path after
+// downloading headers to a temporary file.
+func (f *fileHeaderImportSource) SetURI(uri string) {
+	f.uri = uri
+}
+
+// GetURI returns the file path for this import source.
+func (f *fileHeaderImportSource) GetURI() string {
+	return f.uri
+}
+
+// mmapFile wraps mmap.ReaderAt to provide ImportHeadersFile interface.
+type mmapFile struct {
+	readerAt *mmap.ReaderAt
+	offset   int64
+}
+
+// Compile-time assertion to ensure mmapFile implements ImportHeadersFile
+// interface.
+var _ ImportHeadersFile = (*mmapFile)(nil)
+
+// newMmapFile creates a new memory-mapped file adapter for mmap.ReaderAt.
+func newMmapFile(readerAt *mmap.ReaderAt) *mmapFile {
+	return &mmapFile{
+		readerAt: readerAt,
+		offset:   0,
+	}
+}
+
+// Read implements io.Reader interface.
+func (m *mmapFile) Read(p []byte) (int, error) {
+	n, err := m.readerAt.ReadAt(p, m.offset)
+	m.offset += int64(n)
+	return n, err
+}
+
+// ReadAt implements io.ReaderAt interface.
+func (m *mmapFile) ReadAt(p []byte, off int64) (int, error) {
+	return m.readerAt.ReadAt(p, off)
+}
+
+// Close implements io.Closer interface.
+func (m *mmapFile) Close() error {
+	return m.readerAt.Close()
+}
+
+// Len returns the length of the underlying reader.
+func (m *mmapFile) Len() int {
+	return m.readerAt.Len()
+}
diff --git a/chainimport/file_source_mock.go b/chainimport/file_source_mock.go
new file mode 100644
index 0000000..3e68152
--- /dev/null
+++ b/chainimport/file_source_mock.go
@@ -0,0 +1,62 @@
+package chainimport
+
+import (
+	"github.com/stretchr/testify/mock"
+)
+
+// mockHeaderImportSource mocks a header import source for testing import source
+// interactions.
+type mockHeaderImportSource struct {
+	mock.Mock
+	uri string
+}
+
+// Open opens the mock header import source.
+func (m *mockHeaderImportSource) Open() error {
+	args := m.Called()
+	return args.Error(0)
+}
+
+// Close closes the mock header import source.
+func (m *mockHeaderImportSource) Close() error {
+	args := m.Called()
+	return args.Error(0)
+}
+
+// GetHeaderMetadata gets header metadata from the mock header import source.
+func (m *mockHeaderImportSource) GetHeaderMetadata() (*headerMetadata, error) {
+	args := m.Called()
+	if args.Get(0) == nil {
+		return nil, args.Error(1)
+	}
+	return args.Get(0).(*headerMetadata), args.Error(1)
+}
+
+// GetHeader gets a header by index from the mock header import source.
+func (m *mockHeaderImportSource) GetHeader(index uint32) (Header, error) {
+	args := m.Called(index)
+	if args.Get(0) == nil {
+		return nil, args.Error(1)
+	}
+	return args.Get(0).(Header), args.Error(1)
+}
+
+// Iterator returns a header iterator from the mock header import source.
+func (m *mockHeaderImportSource) Iterator(start, end uint32,
+	batchSize uint32) HeaderIterator {
+
+	args := m.Called(start, end, batchSize)
+	return args.Get(0).(HeaderIterator)
+}
+
+// GetURI gets the URI from the mock header import source.
+func (m *mockHeaderImportSource) GetURI() string {
+	args := m.Called()
+	return args.String(0)
+}
+
+// SetURI sets the URI for the mock header import source.
+func (m *mockHeaderImportSource) SetURI(uri string) {
+	m.Called(uri)
+	m.uri = uri
+}
diff --git a/chainimport/filter_headers_validator.go b/chainimport/filter_headers_validator.go
new file mode 100644
index 0000000..1b55a00
--- /dev/null
+++ b/chainimport/filter_headers_validator.go
@@ -0,0 +1,130 @@
+package chainimport
+
+import (
+	"context"
+	"fmt"
+
+	"github.com/btcsuite/btcd/chaincfg"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightninglabs/neutrino/chainsync"
+)
+
+// FilterHeadersImportSourceValidator implements HeaderValidator for filter
+// headers.
+type filterHeadersImportSourceValidator struct {
+	// targetChainParams contains the blockchain network parameters for
+	// validation against the target chain.
+	targetChainParams chaincfg.Params
+}
+
+// Compile-time assertion to ensure filterHeadersImportSourceValidator
+// implements headersValidator interface.
+var _ HeadersValidator = (*filterHeadersImportSourceValidator)(nil)
+
+// newFilterHeadersImportSourceValidator creates a new validator for filter
+// headers import source.
+func newFilterHeadersImportSourceValidator(
+	targetChainParams chaincfg.Params) HeadersValidator {
+
+	return &filterHeadersImportSourceValidator{
+		targetChainParams: targetChainParams,
+	}
+}
+
+// Validate performs validation on a batch of filter headers using hardcoded
+// checkpoints.
+//
+// The validation utilizes the existing checkpointing mechanism based on the
+// hardcoded filter headers at different checkpoints. Individual headers are
+// validated using ValidateSingle which returns ErrCheckpointMismatch on any
+// mismatch with known checkpoints.
+//
+// Note: Full cryptographic validation is not possible during import since we
+// don't have access to the compact filters. Filter headers are normally
+// calculated using:
+//
+// FilterHeader_N = double-SHA256(FilterHeader_N-1 || double-SHA256(Filter_N)).
+//
+// where FilterHeader_N-1 is the previous filter header in little-endian byte
+// order, Filter_N is the compact filter for the block, and || represents
+// concatenation.
+func (v *filterHeadersImportSourceValidator) Validate(ctx context.Context,
+	it HeaderIterator) error {
+
+	var (
+		start     = it.GetStartIndex()
+		end       = it.GetEndIndex()
+		batchSize = it.GetBatchSize()
+		count     = 0
+	)
+
+	for batch, err := range it.BatchIterator(start, end, batchSize) {
+		if err != nil {
+			return fmt.Errorf("failed to get next batch for "+
+				"validation: %w", err)
+		}
+
+		if err := ctxCancelled(ctx); err != nil {
+			return nil
+		}
+
+		if err = v.ValidateBatch(batch); err != nil {
+			return fmt.Errorf("batch validation failed at "+
+				"position %d: %w", count, err)
+		}
+
+		count += len(batch)
+	}
+
+	log.Debugf("Successfully validated %d filter headers", count)
+	return nil
+}
+
+// ValidateBatch performs validation on a batch of filter headers.
+func (v *filterHeadersImportSourceValidator) ValidateBatch(
+	headers []Header) error {
+
+	for _, header := range headers {
+		if err := v.ValidateSingle(header); err != nil {
+			return err
+		}
+	}
+
+	return nil
+}
+
+// ValidateSingle validates a single filter header. It utilizes the existing
+// checkpointing mechanism based on the hardcoded filter headers at different
+// checkpoints. On any mismatch it returns ErrCheckpointMismatch. If the given
+// header doesn't exist in the hardcoded checkpoints, it returns no error.
+func (v *filterHeadersImportSourceValidator) ValidateSingle(h Header) error {
+	filterHeader, err := assertFilterHeader(h)
+	if err != nil {
+		return err
+	}
+
+	if err := chainsync.ValidateCFHeader(
+		v.targetChainParams, wire.GCSFilterRegular, filterHeader.Height,
+		&filterHeader.FilterHash,
+	); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// ValidatePair checks if two consecutive filter headers maintain the correct
+// cryptographic relationship.
+//
+// In a full validation, we would verify that each filter header is correctly
+// derived by hashing the previous filter header with the current block's
+// compact filter.
+//
+// However, during import, we don't have access to the compact filters, so we
+// can't validate if two consecutive filter headers maintain the correct
+// cryptographic relationship.
+func (v *filterHeadersImportSourceValidator) ValidatePair(prev,
+	current Header) error {
+
+	return nil
+}
diff --git a/chainimport/headers.go b/chainimport/headers.go
new file mode 100644
index 0000000..474ded1
--- /dev/null
+++ b/chainimport/headers.go
@@ -0,0 +1,196 @@
+package chainimport
+
+import (
+	"encoding/binary"
+	"fmt"
+	"io"
+
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightninglabs/neutrino/headerfs"
+)
+
+const (
+	// networkMagicSize is the Network Magic size: 4 bytes.
+	networkMagicSize = 4
+
+	// versionSize is the Version size: 1 byte.
+	versionSize = 1
+
+	// headerTypeSize is the Header Type size: 1 byte.
+	headerTypeSize = 1
+
+	// startHeaderHeightSize is the Start Header Height size: 4 bytes.
+	startHeaderHeightSize = 4
+
+	// importMetadataSize is the ImportMetadata size in bytes
+	// (sum of all above): 10 bytes.
+	ImportMetadataSize = networkMagicSize + versionSize + headerTypeSize +
+		startHeaderHeightSize
+)
+
+// blockHeader represents a block header that can be imported into the chain
+// store. It wraps a headerfs.BlockHeader with additional functionality needed
+// for the import process.
+type blockHeader struct {
+	headerfs.BlockHeader
+}
+
+// Compile-time assertion to ensure blockHeader implements HeaderBase interface.
+var _ Header = (*blockHeader)(nil)
+
+// newBlockHeader creates and returns a new block header instance.
+func newBlockHeader() Header {
+	return &blockHeader{
+		BlockHeader: headerfs.BlockHeader{
+			BlockHeader: &wire.BlockHeader{},
+		},
+	}
+}
+
+// Deserialize reconstructs a block header from binary data at the specified
+// height.
+func (b *blockHeader) Deserialize(r io.Reader, height uint32) error {
+	// Deserialize the wire.BlockHeader portion.
+	if err := b.BlockHeader.BlockHeader.Deserialize(r); err != nil {
+		return fmt.Errorf("failed to deserialize wire.BlockHeader: "+
+			"%w", err)
+	}
+
+	// Set block header height.
+	b.BlockHeader.Height = height
+
+	return nil
+}
+
+// filterHeader represents a filter header that can be imported into the chain
+// store. It wraps a headerfs.FilterHeader with additional functionality needed
+// for the import process.
+type filterHeader struct {
+	headerfs.FilterHeader
+}
+
+// Compile-time assertion to ensure filterHeader implements HeaderBase
+// interface.
+var _ Header = (*filterHeader)(nil)
+
+// newFilterHeader creates and returns a new filter header instance.
+func newFilterHeader() Header {
+	return &filterHeader{
+		FilterHeader: headerfs.FilterHeader{},
+	}
+}
+
+// Deserialize reconstructs a filter header from binary data at the specified
+// height.
+func (f *filterHeader) Deserialize(r io.Reader, height uint32) error {
+	// Read the filter hash (32 bytes).
+	if _, err := io.ReadFull(r, f.FilterHash[:]); err != nil {
+		return fmt.Errorf("failed to read filter hash: %w", err)
+	}
+
+	// Set filter header height.
+	f.FilterHeader.Height = height
+
+	return nil
+}
+
+// headerMetadata contains metadata for a headers import operation, including
+// the range and size information for the headers being imported.
+type headerMetadata struct {
+	// importMetadata contains the basic import metadata
+	// (network, version, etc.).
+	*importMetadata
+
+	// endHeight is the block height where the header sequence ends.
+	endHeight uint32
+
+	// headerSize is the size in bytes of each individual header.
+	headerSize int
+
+	// headersCount is the total number of headers in the sequence.
+	headersCount uint32
+}
+
+// importMetadata contains the basic metadata required for importing headers,
+// including network magic, version, header type, and starting height.
+type importMetadata struct {
+	// networkMagic identifies the Bitcoin network (mainnet, testnet, etc.).
+	networkMagic wire.BitcoinNet
+
+	// version indicates the data format version, currently 0 for
+	// uncompressed headers. Future versions may support compressed header
+	// formats.
+	version uint8
+
+	// headerType specifies whether these are block headers or filter
+	// headers.
+	headerType headerfs.HeaderType
+
+	// startHeight is the block height where the header sequence begins.
+	startHeight uint32
+}
+
+// encode writes the import metadata to the provided writer in binary format.
+//
+// NOTE: The writer's position is supposed to be at the beginning of the file.
+func (m *importMetadata) encode(w io.Writer) error {
+	err := binary.Write(w, binary.LittleEndian, m.networkMagic)
+	if err != nil {
+		return fmt.Errorf("failed to write chain type: %w", err)
+	}
+
+	if err = binary.Write(w, binary.LittleEndian, m.version); err != nil {
+		return fmt.Errorf("failed to write version: %w", err)
+	}
+
+	err = binary.Write(w, binary.LittleEndian, byte(m.headerType))
+	if err != nil {
+		return fmt.Errorf("failed to write header type: %w", err)
+	}
+
+	err = binary.Write(w, binary.LittleEndian, m.startHeight)
+	if err != nil {
+		return fmt.Errorf("failed to write start height: %w", err)
+	}
+
+	return nil
+}
+
+// decode reads the import metadata from the provided reader.
+//
+// NOTE: The reader's position is supposed to be at the beginning of the file.
+func (m *importMetadata) decode(r io.Reader) error {
+	err := binary.Read(r, binary.LittleEndian, &m.networkMagic)
+	if err != nil {
+		return fmt.Errorf("failed to read network magic: %w", err)
+	}
+
+	if err = binary.Read(r, binary.LittleEndian, &m.version); err != nil {
+		return fmt.Errorf("failed to read version: %w", err)
+	}
+
+	if m.version != 0 {
+		return fmt.Errorf("unsupported header format version %d, "+
+			"only version 0 (uncompressed) is currently supported",
+			m.version)
+	}
+
+	var headerTypeByte byte
+	err = binary.Read(r, binary.LittleEndian, &headerTypeByte)
+	if err != nil {
+		return fmt.Errorf("failed to read header type: %w", err)
+	}
+	m.headerType = headerfs.HeaderType(headerTypeByte)
+
+	err = binary.Read(r, binary.LittleEndian, &m.startHeight)
+	if err != nil {
+		return fmt.Errorf("failed to read start height: %w", err)
+	}
+
+	return nil
+}
+
+// size returns the binary size of the import metadata in bytes.
+func (m *importMetadata) size() int {
+	return ImportMetadataSize
+}
diff --git a/chainimport/headers_import.go b/chainimport/headers_import.go
new file mode 100644
index 0000000..acaa33d
--- /dev/null
+++ b/chainimport/headers_import.go
@@ -0,0 +1,1023 @@
+package chainimport
+
+import (
+	"context"
+	"errors"
+	"fmt"
+	"io"
+	"strings"
+	"time"
+
+	"github.com/btcsuite/btcd/blockchain"
+	"github.com/btcsuite/btcd/chaincfg"
+	"github.com/lightninglabs/neutrino/headerfs"
+)
+
+const (
+	// defaultWriteBatchSizePerRegion defines the default number of headers
+	// to process in a single batch when no specific batch size is provided.
+	defaultWriteBatchSizePerRegion = 16384
+)
+
+// processingRegions currently contains regions to process. Those regions
+// overlap, divergence, and new headers region are all detected but only new
+// headers region is processed.
+type processingRegions struct {
+	// importStartHeight defines the starting block height for the import
+	// process.
+	importStartHeight uint32
+
+	// importEndHeight defines the ending block height for the import
+	// process.
+	importEndHeight uint32
+
+	// effectiveTip represents the current chain tip height that is
+	// effective for processing.
+	effectiveTip uint32
+
+	// overlap contains the region of headers that overlap with the target
+	// chain.
+	overlap headerRegion
+
+	// divergence contains the region of headers that diverge from the
+	// target chain.
+	divergence headerRegion
+
+	// newHeaders contains the region of new headers that need to be
+	// processed.
+	newHeaders headerRegion
+}
+
+// headerRegion represents a contiguous range of headers.
+type headerRegion struct {
+	// start is the beginning height of this header region.
+	start uint32
+
+	// end is the ending height of this header region.
+	end uint32
+
+	// exists indicates whether this region has headers to process.
+	exists bool
+}
+
+// headersImport orchestrates the import of blockchain headers from external
+// sources into local header stores. It handles validation, processing, and
+// atomic writes of both block headers and filter headers while maintaining
+// chain integrity and consistency between stores.
+type headersImport struct {
+	// blockHeadersImportSource provides access to block headers from import
+	// source.
+	blockHeadersImportSource HeaderImportSource
+
+	// blockHeadersImportSource provides access to filter headers from
+	// import source.
+	filterHeadersImportSource HeaderImportSource
+
+	// blockHeadersValidator validates the imported block headers.
+	blockHeadersValidator HeadersValidator
+
+	// filterHeadersValidator validates the imported filter headers.
+	filterHeadersValidator HeadersValidator
+
+	// options contains configuration parameters for the import process.
+	options *ImportOptions
+}
+
+// NewHeadersImport creates a new headersImport instance with the given options.
+func NewHeadersImport(options *ImportOptions) (*headersImport, error) {
+	if err := options.validate(); err != nil {
+		return nil, err
+	}
+
+	if options.WriteBatchSizePerRegion <= 0 {
+		options.WriteBatchSizePerRegion = defaultWriteBatchSizePerRegion
+		log.Infof("Using default write batch size of %d "+
+			"headers per region", options.WriteBatchSizePerRegion)
+	}
+
+	blockHeadersSource := options.createBlockHeaderImportSrc()
+	filterHeadersSource := options.createFilterHeaderImportSrc()
+
+	blockheadersValidator := options.createBlockHeaderValidator(
+		blockHeadersSource,
+	)
+	filterheadersValidator := options.createFilterHeaderValidator()
+
+	importer := &headersImport{
+		blockHeadersImportSource:  blockHeadersSource,
+		filterHeadersImportSource: filterHeadersSource,
+		blockHeadersValidator:     blockheadersValidator,
+		filterHeadersValidator:    filterheadersValidator,
+		options:                   options,
+	}
+
+	return importer, nil
+}
+
+// Import is a multi-pass algorithm that loads, validates, and processes headers
+// from the configured import sources into the target header stores. The Import
+// process is currently performed only if the target stores are completely empty
+// except for gensis block/filter header otherwise it is entirely skipped. On
+// first development iteration, it is designed to serve new users who don't yet
+// have headers data, or existing users who are willing to reset their headers
+// data.
+func (h *headersImport) Import(ctx context.Context) (*ImportResult, error) {
+	isFresh, err := h.isTargetFresh(
+		h.options.TargetBlockHeaderStore,
+		h.options.TargetFilterHeaderStore,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("failed to detect if target stores "+
+			"are fresh import failed: %w", err)
+	}
+	if !isFresh {
+		log.Info("Skipping headers import: target header stores are " +
+			"not empty")
+		return &ImportResult{}, nil
+	}
+
+	result := &ImportResult{
+		StartTime: time.Now(),
+	}
+
+	if err := h.openSources(); err != nil {
+		return nil, fmt.Errorf("failed to open sources: %w", err)
+	}
+	defer h.closeSources()
+
+	if err := h.validateSourcesCompatibility(); err != nil {
+		return nil, fmt.Errorf("failed to validate compatibility of "+
+			"header import sources with each other: %w", err)
+	}
+
+	if err := h.validateChainContinuity(); err != nil {
+		return nil, fmt.Errorf("failed to validate continuity of "+
+			"import headers chain with target chain: %v", err)
+	}
+
+	// Get header metadata from import souces. We can safely use this header
+	// metadata for both block headers and filter headers since we've
+	// already validated that those header metadata are compatible with each
+	// other.
+	metadata, err := h.blockHeadersImportSource.GetHeaderMetadata()
+	if err != nil {
+		return nil, err
+	}
+
+	log.Debugf("Validating %d block headers", metadata.headersCount)
+	blockHeadersIterator := h.blockHeadersImportSource.Iterator(
+		0, metadata.headersCount-1,
+		uint32(h.options.WriteBatchSizePerRegion),
+	)
+
+	err = h.blockHeadersValidator.Validate(ctx, blockHeadersIterator)
+	if err != nil {
+		return nil, fmt.Errorf("failed to validate block "+
+			"headers: %w", err)
+	}
+
+	log.Debugf("Validating %d filter headers", metadata.headersCount)
+	filterHeadersIterator := h.filterHeadersImportSource.Iterator(
+		0, metadata.headersCount-1,
+		uint32(h.options.WriteBatchSizePerRegion),
+	)
+
+	err = h.filterHeadersValidator.Validate(ctx, filterHeadersIterator)
+	if err != nil {
+		return nil, fmt.Errorf("failed to validate filter "+
+			"headers: %w", err)
+	}
+
+	// Determine processing regions that partition the import task into
+	// disjoint height ranges.
+	regions, err := h.determineProcessingRegions()
+	if err != nil {
+		return nil, fmt.Errorf("failed to determine processing "+
+			"regions: %w", err)
+	}
+	result.StartHeight = regions.importStartHeight
+	result.EndHeight = regions.importEndHeight
+
+	// TODO(mohamedawnallah): Process the divergence region. This includes
+	// strategy/strategies for handling divergence region that may exist in
+	// the target header stores while importing.
+
+	// TODO(mohamedawnallah): process the overlap region. This mainly
+	// includes a validation strategy for the overlap region between headers
+	// from import and target sources.
+
+	// Process new headers region.
+	// Add headers from the import source to the target stores, extending
+	// from their highest existing header up to the import source's end
+	// height. This assumes the target stores are consistent and valid
+	// (i.e., no divergence), allowing for safe extension with new data.
+	err = h.processNewHeadersRegion(ctx, regions.newHeaders, result)
+	if err != nil {
+		return nil, fmt.Errorf("headers import failed: new headers "+
+			"processing failed: %w", err)
+	}
+
+	result.EndTime = time.Now()
+	result.Duration = result.EndTime.Sub(result.StartTime)
+
+	log.Infof("Headers import completed: processed %d headers "+
+		"(block and filter) (added: %d, skipped: %d) from height "+
+		"%d to %d in %s (%.2f headers/sec, %.2f%% new)",
+		result.ProcessedCount, result.AddedCount, result.SkippedCount,
+		result.StartHeight, result.EndHeight, result.Duration,
+		result.HeadersPerSecond(), result.NewHeadersPercentage())
+
+	return result, nil
+}
+
+// validateChainContinuity ensures that headers from import sources can be
+// properly connected to the existing headers in the target stores.
+func (h *headersImport) validateChainContinuity() error {
+	// Get metadata from block header source. We can safely use this count
+	// for both block headers and filter headers since we've already
+	// validated that the counts match across all import sources.
+	sourceMetadata, err := h.blockHeadersImportSource.GetHeaderMetadata()
+	if err != nil {
+		return fmt.Errorf("failed to get block header "+
+			"metadata: %w", err)
+	}
+
+	_, blockTipHeight, err := h.options.TargetBlockHeaderStore.ChainTip()
+	if err != nil {
+		return fmt.Errorf("failed to get target block header chain "+
+			"tip: %w", err)
+	}
+
+	_, filterTipHeight, err := h.options.TargetFilterHeaderStore.ChainTip()
+	if err != nil {
+		return fmt.Errorf("failed to get target filter header chain "+
+			"tip: %w", err)
+	}
+
+	// Ensure that both target header stores have the same tip height.
+	// A mismatch indicates a divergence region that has not yet been
+	// processed. Once a resolution strategy is implemented, this check
+	// will no longer return an error, and the effective tip height will be
+	// defined as the minimum of the two.
+	if blockTipHeight != filterTipHeight {
+		return fmt.Errorf("divergence detected between target header "+
+			"store tip heights (block=%d, filter=%d)",
+			blockTipHeight, filterTipHeight)
+	}
+
+	importStartHeight := sourceMetadata.startHeight
+	importEndHeight := sourceMetadata.endHeight
+
+	// If import wants to start after height 1, we'd have a gap.
+	if importStartHeight > 1 {
+		return fmt.Errorf("target stores contain only genesis block "+
+			"(height 0) but import data starts at height %d, "+
+			"creating a gap", importStartHeight)
+	}
+
+	// If import includes genesis block (starts at 0), verify it matches.
+	if importStartHeight == 0 {
+		err := h.verifyHeadersAtTargetHeight(importStartHeight)
+		if err != nil {
+			return fmt.Errorf("genesis header mismatch: %v", err)
+		}
+		log.Infof("Genesis headers verified, import data will extend " +
+			"chain from genesis")
+	} else {
+		// Import starts at height 1, which connects to genesis.
+		// Validate that the block header at height 1 from the import
+		// source connects with the previous header in the target block
+		// store.
+		if err := h.validateHeaderConnection(
+			importStartHeight, blockTipHeight, sourceMetadata,
+		); err != nil {
+			return fmt.Errorf("failed to validate header "+
+				"connection: %v", err)
+		}
+
+		log.Info("Target stores contain only genesis block, import " +
+			"data will extend chain from height 1")
+	}
+
+	log.Infof("Chain continuity validation successful: import data "+
+		"(%d-%d) connects properly with target chain",
+		importStartHeight, importEndHeight)
+
+	return nil
+}
+
+// verifyHeadersAtTargetHeight ensures headers at the specified height match
+// exactly between import and target sources by performing a byte-level
+// comparison. It retrieves the header from both sources at the given height and
+// verifies they are identical, returning an error if any discrepancy is found.
+func (h *headersImport) verifyHeadersAtTargetHeight(height uint32) error {
+	// Get header metadata from import souces. We can safely use this header
+	// metadata for both block headers and filter headers since we've
+	// already validated that those header metadata are compatible with each
+	// other.
+	headerMetadata, err := h.blockHeadersImportSource.GetHeaderMetadata()
+	if err != nil {
+		return fmt.Errorf("failed to get block header "+
+			"metadata: %w", err)
+	}
+
+	importSourceIndex := targetHeightToImportSourceIndex(
+		height, headerMetadata.startHeight,
+	)
+
+	importSourceHeader, err := h.blockHeadersImportSource.GetHeader(
+		importSourceIndex,
+	)
+	if err != nil {
+		return fmt.Errorf("failed to get block header from import "+
+			"source at height %d: %w", height, err)
+	}
+
+	importSourceBlkHeader, err := assertBlockHeader(importSourceHeader)
+	if err != nil {
+		return err
+	}
+
+	targetBlkHeaderStore := h.options.TargetBlockHeaderStore
+	targetBlkHeader, err := targetBlkHeaderStore.FetchHeaderByHeight(
+		height,
+	)
+	if err != nil {
+		return fmt.Errorf("failed to get block header from target at "+
+			"height %d: %w", height, err)
+	}
+
+	sourceBlkHeaderHash := importSourceBlkHeader.BlockHash()
+	targetBlkHeaderHash := targetBlkHeader.BlockHash()
+	if !sourceBlkHeaderHash.IsEqual(&targetBlkHeaderHash) {
+		return fmt.Errorf("block header mismatch at height %d: source "+
+			"has %v but target has %v", height,
+			sourceBlkHeaderHash, targetBlkHeaderHash)
+	}
+
+	importSourceHeader, err = h.filterHeadersImportSource.GetHeader(
+		importSourceIndex,
+	)
+	if err != nil {
+		return fmt.Errorf("failed to get filter header from import "+
+			"source at height %d: %w", height, err)
+	}
+
+	importSourceFilterHeader, err := assertFilterHeader(importSourceHeader)
+	if err != nil {
+		return err
+	}
+
+	filterHeaderStore := h.options.TargetFilterHeaderStore
+	targetFilterHeader, err := filterHeaderStore.FetchHeaderByHeight(
+		height,
+	)
+	if err != nil {
+		return fmt.Errorf("failed to get filter header from target at "+
+			"height %d: %w", height, err)
+	}
+
+	sourceFilterHeaderHash := importSourceFilterHeader.FilterHash
+	targetFilterHeaderHash := *targetFilterHeader
+	if !sourceFilterHeaderHash.IsEqual(&targetFilterHeaderHash) {
+		return fmt.Errorf("filter header mismatch at height %d: "+
+			"source has %v but target has %v", height,
+			sourceFilterHeaderHash, targetFilterHeaderHash)
+	}
+
+	log.Debugf("Headers from %s (block) and %s (filter) verified at "+
+		"height %d", h.blockHeadersImportSource.GetURI(),
+		h.filterHeadersImportSource.GetURI(), height)
+
+	return nil
+}
+
+// determineProcessingRegions partitions the header height space into regions
+// satisfying the MECE property (Mutually Exclusive, Collectively Exhaustive).
+// This strict partitioning is critical for ensuring the idempotence of the
+// overall import operation - repeated imports with the same parameters will
+// produce identical results without side effects. By cleanly separating heights
+// into non-overlapping regions with distinct processing logic, we can ensure
+// consistent application of import policies regardless of how many times the
+// operation is performed. The regions are:
+//  1. Overlap: Heights common to both source and targets
+//  2. Divergence: Heights where target stores differ within import range
+//  3. NewHeaders: Heights in source not yet in targets
+//
+//nolint:lll
+func (h *headersImport) determineProcessingRegions() (*processingRegions, error) {
+	metadata, err := h.blockHeadersImportSource.GetHeaderMetadata()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get header metadata: %v", err)
+	}
+	importStartHeight := metadata.startHeight
+	importEndHeight := metadata.endHeight
+
+	_, bTipHeight, err := h.options.TargetBlockHeaderStore.ChainTip()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get target block header "+
+			"store chain tip: %v", err)
+	}
+	_, fTipHeight, err := h.options.TargetFilterHeaderStore.ChainTip()
+	if err != nil {
+		return nil, fmt.Errorf("failed to get target filter header "+
+			"store chain tip: %v", err)
+	}
+	effectiveTipHeight := min(bTipHeight, fTipHeight)
+
+	regions := &processingRegions{
+		importStartHeight: importStartHeight,
+		importEndHeight:   importEndHeight,
+		effectiveTip:      effectiveTipHeight,
+	}
+
+	// 1. Overlap region.
+	// This region contains headers that exist in both the import source and
+	// target stores, from the start of the import range up to the effective
+	// tip height.
+	overlapStart := importStartHeight
+	overlapEnd := min(effectiveTipHeight, importEndHeight)
+	regions.overlap = headerRegion{
+		start:  overlapStart,
+		end:    overlapEnd,
+		exists: overlapStart <= overlapEnd,
+	}
+
+	// 2. Divergence region.
+	// This region contains headers where one store extends beyond the
+	// effective tip but still within the import range. It represents
+	// heights where targetblock and filter headers are out of sync and need
+	// reconciliation.
+	divergeStart := effectiveTipHeight + 1
+	divergeEnd := min(max(bTipHeight, fTipHeight), importEndHeight)
+	regions.divergence = headerRegion{
+		start:  divergeStart,
+		end:    divergeEnd,
+		exists: bTipHeight != fTipHeight && divergeStart <= divergeEnd,
+	}
+
+	// 3. New Headers region.
+	// This region contains headers that are in the import source but not
+	// yet in either target store. They start one height beyond the highest
+	// tip of either store (ensuring no overlap with divergence region) and
+	// extend to the end of the import data. These headers need to be added
+	// to both stores. It only exists if there are headers beyond both tips.
+	//
+	// Note: This region is supposed to be processed after handling the
+	// overlap, and divergence regions, ensuring that any potential
+	// inconsistencies in existing data are resolved before adding new
+	// headers. This sequential processing guarantees that new headers
+	// are only added on top of a verified and consistent chain state.
+	newStart := max(bTipHeight, fTipHeight) + 1
+	newEnd := importEndHeight
+	regions.newHeaders = headerRegion{
+		start:  newStart,
+		end:    newEnd,
+		exists: newStart <= newEnd,
+	}
+
+	return regions, nil
+}
+
+// processNewHeadersRegion imports headers from the specified region into the
+// target stores. This method handles the case where headers exist in the import
+// source but not in the target stores.
+func (h *headersImport) processNewHeadersRegion(ctx context.Context,
+	region headerRegion, result *ImportResult) error {
+
+	if !region.exists {
+		return nil
+	}
+
+	log.Infof("Adding %d new headers (block and filter) from heights "+
+		"%d to %d", region.end-region.start+1, region.start, region.end)
+
+	err := h.appendNewHeaders(ctx, region.start, region.end)
+	if err != nil {
+		return fmt.Errorf("failed to append new headers: %w", err)
+	}
+
+	result.ProcessedCount += int(region.end - region.start + 1)
+	result.AddedCount += int(region.end - region.start + 1)
+
+	return nil
+}
+
+// appendNewHeaders adds new headers from import source.
+func (h *headersImport) appendNewHeaders(ctx context.Context, startHeight,
+	endHeight uint32) error {
+
+	metadata, err := h.blockHeadersImportSource.GetHeaderMetadata()
+	if err != nil {
+		return fmt.Errorf("failed to get block header "+
+			"metadata: %w", err)
+	}
+
+	totalHeaders := endHeight - startHeight + 1
+	log.Infof("Appending %d new headers in batches of %d", totalHeaders,
+		h.options.WriteBatchSizePerRegion)
+
+	sourceStartIdx := targetHeightToImportSourceIndex(
+		startHeight, metadata.startHeight,
+	)
+	sourceEndIdx := targetHeightToImportSourceIndex(
+		endHeight, metadata.startHeight,
+	)
+
+	blockIter := h.blockHeadersImportSource.Iterator(
+		sourceStartIdx, sourceEndIdx,
+		uint32(h.options.WriteBatchSizePerRegion),
+	)
+
+	filterIter := h.filterHeadersImportSource.Iterator(
+		sourceStartIdx, sourceEndIdx,
+		uint32(h.options.WriteBatchSizePerRegion),
+	)
+
+	batchStart := startHeight
+	for {
+		if err := ctxCancelled(ctx); err != nil {
+			return err
+		}
+
+		batchEnd, err := h.processBatch(
+			blockIter, filterIter, batchStart,
+		)
+		if err == io.EOF {
+			break
+		}
+		if err != nil {
+			return err
+		}
+
+		// Move to next batch.
+		batchStart = batchEnd + 1
+	}
+
+	log.Infof("Successfully added %d new headers from heights %d to %d",
+		totalHeaders, startHeight, endHeight)
+
+	return nil
+}
+
+// processBatch processes a single batch of headers from the iterators. It
+// returns the batch end height on success, or an error including io.EOF when no
+// more batches.
+func (h *headersImport) processBatch(blockIter, filterIter HeaderIterator,
+	batchStart uint32) (uint32, error) {
+
+	blockBatch, blockErr := blockIter.ReadBatch(
+		batchStart, blockIter.GetEndIndex(), blockIter.GetBatchSize(),
+	)
+	if blockErr == io.EOF {
+		return 0, io.EOF
+	}
+	if blockErr != nil {
+		return 0, fmt.Errorf("failed to read block headers "+
+			"batch at height %d: %w", batchStart, blockErr)
+	}
+
+	// Get corresponding filter headers batch.
+	filterBatch, filterErr := filterIter.ReadBatch(
+		batchStart, blockIter.GetEndIndex(), blockIter.GetBatchSize(),
+	)
+	if filterErr == io.EOF {
+		return 0, errors.New("filter headers ended before block " +
+			"headers")
+	}
+	if filterErr != nil {
+		return 0, fmt.Errorf("failed to read filter headers "+
+			"batch at height %d: %w", batchStart, filterErr)
+	}
+
+	// Convert block header batches to target store format.
+	blockHeaders := make([]headerfs.BlockHeader, 0, len(blockBatch))
+	for _, header := range blockBatch {
+		blkHeader, err := assertBlockHeader(header)
+		if err != nil {
+			return 0, err
+		}
+		blockHeaders = append(
+			blockHeaders, blkHeader.BlockHeader,
+		)
+	}
+
+	// Convert filter header batches to target store format.
+	var filterHeaders []headerfs.FilterHeader
+	filterHeaders = make(
+		[]headerfs.FilterHeader, 0, len(filterBatch),
+	)
+	for _, header := range filterBatch {
+		fHeader, err := assertFilterHeader(header)
+		if err != nil {
+			return 0, err
+		}
+		filterHeaders = append(
+			filterHeaders, fHeader.FilterHeader,
+		)
+	}
+
+	// The length check conditions should never be triggered during normal
+	// import operations as validation occurs earlier. They serve as sanity
+	// checks to catch unexpected inconsistencies.
+	if len(blockHeaders) != len(filterHeaders) {
+		return 0, fmt.Errorf("mismatch between block headers "+
+			"(%d) and filter headers (%d)", len(blockHeaders),
+			len(filterHeaders))
+	}
+
+	if len(blockHeaders) == 0 {
+		return 0, errors.New("no headers read")
+	}
+
+	setLastFilterHeaderHash(filterHeaders, blockHeaders)
+
+	batchEnd := batchStart + uint32(len(blockBatch)) - 1
+
+	err := h.writeHeadersToTargetStores(
+		blockHeaders, filterHeaders, batchStart, batchEnd,
+	)
+	if err != nil {
+		return 0, fmt.Errorf("failed to write headers to target "+
+			"stores: %v", err)
+	}
+
+	log.Debugf("Wrote headers batch from height %d to %d "+
+		"(%d headers)", batchStart, batchEnd,
+		batchEnd-batchStart+1)
+
+	return batchEnd, nil
+}
+
+// Write block and filter headers to the target stores in a specific order to
+// ensure proper rollback operations if needed. It also rollbacks any headers
+// written if any to target stores incase of any failures.
+func (h *headersImport) writeHeadersToTargetStores(
+	blockHeaders []headerfs.BlockHeader,
+	filterHeaders []headerfs.FilterHeader,
+	batchStart, batchEnd uint32) error {
+
+	if err := h.options.TargetBlockHeaderStore.WriteHeaders(
+		blockHeaders...,
+	); err != nil {
+		return fmt.Errorf("failed to write block headers "+
+			"batch %d-%d: %w", batchStart, batchEnd, err)
+	}
+
+	if err := h.options.TargetFilterHeaderStore.WriteHeaders(
+		filterHeaders...,
+	); err != nil {
+		// If we've reached here, the headers failed to be written to
+		// target filter store because of I/O errors regarding binary
+		// file or filter db store, and it is automatically rolled back
+		// upstream.
+		//
+		// The whole import operation needs to satisfy the conjunction
+		// property for both block and filter header stores - it's all
+		// or nothing, so they need to be at the same length. We must
+		// rollback the block headers to maintain this consistency.
+		blkStore := h.options.TargetBlockHeaderStore
+		blockHeadersToTruncate := uint32(len(blockHeaders))
+		_, rollbackErr := blkStore.RollbackBlockHeaders(
+			blockHeadersToTruncate,
+		)
+		if rollbackErr != nil {
+			return fmt.Errorf("failed to rollback %d headers from "+
+				"target block header store after filter "+
+				"headers write failure. Block error: %w, "+
+				"filter error: %v", blockHeadersToTruncate,
+				rollbackErr, err)
+		}
+
+		return fmt.Errorf("failed to write filter headers "+
+			"batch %d-%d: %w", batchStart, batchEnd, err)
+	}
+
+	return nil
+}
+
+// validateHeaderConnection verifies that a block header from the import source
+// properly connects with the previous header in the target block store.
+func (h *headersImport) validateHeaderConnection(targetStartHeight,
+	prevTargetBlockHeight uint32, headerMetadata *headerMetadata) error {
+
+	// Get the previous block header from target store.
+	prevBlkHdr, err := h.options.TargetBlockHeaderStore.FetchHeaderByHeight(
+		prevTargetBlockHeight,
+	)
+	if err != nil {
+		return fmt.Errorf("failed to get block header from "+
+			"target at height %d: %w", prevTargetBlockHeight, err)
+	}
+
+	// Convert target height to the equivalent index for import sources.
+	importSourceIndex := targetHeightToImportSourceIndex(
+		targetStartHeight, headerMetadata.startHeight,
+	)
+
+	// Get block header at that index from the import source.
+	currHeader, err := h.blockHeadersImportSource.GetHeader(
+		importSourceIndex,
+	)
+	if err != nil {
+		return fmt.Errorf("failed to get block header from "+
+			"import source at height %d: %w",
+			targetStartHeight, err)
+	}
+
+	currBlkHeader, err := assertBlockHeader(currHeader)
+	if err != nil {
+		return err
+	}
+
+	// Ensure the current header's previous block hash matches the
+	// hash of the previously fetched block header to maintain chain
+	// integrity.
+	prevHash := prevBlkHdr.BlockHash()
+	if !currBlkHeader.PrevBlock.IsEqual(&prevHash) {
+		return fmt.Errorf("header chain broken: current "+
+			"header's PrevBlock (%v) doesn't match "+
+			"previous header's hash (%v)",
+			currBlkHeader.PrevBlock, prevHash)
+	}
+
+	log.Debugf("Validated block header connection: import height %d "+
+		"properly connects to target chain at height %d "+
+		"(prev hash: %v)", targetStartHeight, prevTargetBlockHeight,
+		prevHash)
+
+	return nil
+}
+
+// isTargetFresh checks if the target header stores are in their initial state,
+// meaning they contain only the genesis header (height 0).
+func (h *headersImport) isTargetFresh(
+	targetBlockHeaderStore headerfs.BlockHeaderStore,
+	targetFilterHeaderStore headerfs.FilterHeaderStore) (bool, error) {
+
+	// Get the chain tip from both target stores.
+	_, blockTipHeight, err := targetBlockHeaderStore.ChainTip()
+	if err != nil {
+		return false, fmt.Errorf("failed to get target block header "+
+			"chain tip: %w", err)
+	}
+
+	_, filterTipHeight, err := targetFilterHeaderStore.ChainTip()
+	if err != nil {
+		return false, fmt.Errorf("failed to get target filter header "+
+			"chain tip: %w", err)
+	}
+
+	if blockTipHeight == 0 && filterTipHeight == 0 {
+		return true, nil
+	}
+
+	return false, nil
+}
+
+// openSources initializes and opens all required header import sources. It
+// verifies that all necessary import sources and validators are properly
+// configured, then opens each source to prepare for data reading. Returns an
+// error if any source is missing or fails to open.
+func (h *headersImport) openSources() error {
+	if h.blockHeadersImportSource == nil ||
+		h.filterHeadersImportSource == nil {
+
+		return fmt.Errorf("missing required header sources - block "+
+			"headers source: %v, filter headers source: %v",
+			h.blockHeadersImportSource != nil,
+			h.filterHeadersImportSource != nil)
+	}
+
+	if h.blockHeadersValidator == nil || h.filterHeadersValidator == nil {
+		return fmt.Errorf("missing required header validators - block "+
+			"headers validator: %v, filter headers "+
+			"validator: %v", h.blockHeadersValidator != nil,
+			h.filterHeadersValidator != nil)
+	}
+
+	if err := h.blockHeadersImportSource.Open(); err != nil {
+		return err
+	}
+
+	if err := h.filterHeadersImportSource.Open(); err != nil {
+		return err
+	}
+
+	return nil
+}
+
+// closeSources safely closes all open header sources and logs any warnings
+// encountered during cleanup.
+func (h *headersImport) closeSources() {
+	if err := h.blockHeadersImportSource.Close(); err != nil {
+		log.Warnf("Failed to close block headers source: %v", err)
+	}
+
+	if err := h.filterHeadersImportSource.Close(); err != nil {
+		log.Warnf("Failed to close filter headers source: %v", err)
+	}
+}
+
+// validateSourcesCompatibility ensures that block and filter header sources
+// are compatible with each other and with the target chain.
+func (h *headersImport) validateSourcesCompatibility() error {
+	blockMetadata, err := h.blockHeadersImportSource.GetHeaderMetadata()
+	if err != nil {
+		return err
+	}
+	filterMetadata, err := h.filterHeadersImportSource.GetHeaderMetadata()
+	if err != nil {
+		return err
+	}
+
+	if blockMetadata.headerType != headerfs.Block {
+		return fmt.Errorf("incorrect block header type: expected %s, "+
+			"got %s", headerfs.Block, blockMetadata.headerType)
+	}
+
+	if filterMetadata.headerType != headerfs.RegularFilter {
+		return fmt.Errorf("incorrect filter header type: expected %v, "+
+			"got %v", headerfs.RegularFilter,
+			filterMetadata.headerType)
+	}
+
+	if blockMetadata.networkMagic != filterMetadata.networkMagic {
+		return fmt.Errorf("network type mismatch: block headers "+
+			"from %s (%v), filter headers from "+
+			"%s (%v)", h.blockHeadersImportSource.GetURI(),
+			blockMetadata.networkMagic,
+			h.filterHeadersImportSource.GetURI(),
+			filterMetadata.networkMagic)
+	}
+
+	if blockMetadata.networkMagic != h.options.TargetChainParams.Net {
+		return fmt.Errorf("network mismatch: headers from import "+
+			"sources are for %v, but target is %v",
+			blockMetadata.networkMagic,
+			h.options.TargetChainParams.Net)
+	}
+
+	if blockMetadata.startHeight != filterMetadata.startHeight {
+		return fmt.Errorf("start height mismatch: block headers start "+
+			"at %d, filter headers start at %d",
+			blockMetadata.startHeight, filterMetadata.startHeight)
+	}
+
+	if filterMetadata.headersCount != blockMetadata.headersCount {
+		return fmt.Errorf("headers count mismatch: block headers "+
+			"import source %s (%d), filter headers import source "+
+			"%s (%d)", h.blockHeadersImportSource.GetURI(),
+			blockMetadata.headersCount,
+			h.filterHeadersImportSource.GetURI(),
+			filterMetadata.headersCount)
+	}
+
+	return nil
+}
+
+// ImportOptions defines parameters for the import process.
+type ImportOptions struct {
+	// TargetChainParams specifies the blockchain network parameters for the
+	// chain into which headers will be imported.
+	TargetChainParams chaincfg.Params
+
+	// TargetBlockHeaderStore is the storage backend where block headers
+	// will be written during the import.
+	TargetBlockHeaderStore headerfs.BlockHeaderStore
+
+	// TargetFilterHeaderStore is the storage backend where filter headers
+	// will be written during the import.
+	TargetFilterHeaderStore headerfs.FilterHeaderStore
+
+	// BlockHeadersSource is the file path or source location for block
+	// headers to be imported.
+	BlockHeadersSource string
+
+	// FilterHeadersSource is the file path or source location for filter
+	// headers to be imported.
+	FilterHeadersSource string
+
+	// WriteBatchSizePerRegion specifies the number of headers to write in
+	// each batch per region. This controls performance characteristics of
+	// the import.
+	WriteBatchSizePerRegion int
+
+	// ValidationFlags specifies the behavior flags used during header
+	// validation. It defaults to BFNone.
+	ValidationFlags blockchain.BehaviorFlags
+}
+
+// validate checks that all required fields in import options are properly set
+// and returns an error if any validation fails.
+func (options *ImportOptions) validate() error {
+	if options.BlockHeadersSource == "" {
+		return errors.New("missing block headers source path")
+	}
+
+	if options.FilterHeadersSource == "" {
+		return errors.New("missing filter headers source path")
+	}
+
+	return nil
+}
+
+// createBlockHeaderImportSrc creates the appropriate import source for block
+// headers.
+func (options *ImportOptions) createBlockHeaderImportSrc() HeaderImportSource {
+	// Check if the block headers source is a HTTP(s) URI.
+	if strings.HasPrefix(options.BlockHeadersSource, "http") {
+		// The empty string ("") URI passed will be replaced by the
+		// temporary file name once the file has been downloaded from
+		// the HTTP source.
+		return newHTTPHeaderImportSource(
+			options.BlockHeadersSource, newHTTPClient(),
+			newFileHeaderImportSource("", newBlockHeader),
+		)
+	}
+
+	// Otherwise, fallback to file headers import source.
+	return newFileHeaderImportSource(
+		options.BlockHeadersSource, newBlockHeader,
+	)
+}
+
+// createFilterHeaderImportSrc creates the appropriate import source for
+// filter headers.
+func (options *ImportOptions) createFilterHeaderImportSrc() HeaderImportSource {
+	// Check if the filter headers source is a HTTP(s) URI.
+	if strings.HasPrefix(options.FilterHeadersSource, "http") {
+		return newHTTPHeaderImportSource(
+			options.FilterHeadersSource, newHTTPClient(),
+			newFileHeaderImportSource("", newFilterHeader),
+		)
+	}
+
+	// Otherwise, fallback to file headers import source.
+	return newFileHeaderImportSource(
+		options.FilterHeadersSource, newFilterHeader,
+	)
+}
+
+// createBlockHeaderValidator creates the appropriate validator for block
+// headers.
+func (options *ImportOptions) createBlockHeaderValidator(
+	blockHeadersImportSource HeaderImportSource) HeadersValidator {
+
+	return newBlockHeadersImportSourceValidator(
+		options.TargetChainParams, options.TargetBlockHeaderStore,
+		options.ValidationFlags, blockHeadersImportSource,
+	)
+}
+
+// createFilterHeaderValidator creates the appropriate validator for filter
+// headers.
+func (options *ImportOptions) createFilterHeaderValidator() HeadersValidator {
+	return newFilterHeadersImportSourceValidator(options.TargetChainParams)
+}
+
+// ImportResult contains statistics about a header import operation.
+type ImportResult struct {
+	// ProcessedCount is the total number of headers examined.
+	ProcessedCount int
+
+	// AddedCount is the number of headers newly added to destination.
+	AddedCount int
+
+	// SkippedCount is the number of headers already in destination.
+	SkippedCount int
+
+	// StartHeight is the first height processed.
+	StartHeight uint32
+
+	// EndHeight is the last height processed.
+	EndHeight uint32
+
+	// StartTime is the time when import operation started.
+	StartTime time.Time
+
+	// EndTime is the time when import operation completed.
+	EndTime time.Time
+
+	// Duration is the total time taken for the import operation.
+	Duration time.Duration
+}
+
+// HeadersPerSecond calculates the processing rate in headers per second as a
+// performance metric. Returns 0 if Duration is zero to avoid division by zero.
+func (r *ImportResult) HeadersPerSecond() float64 {
+	if r.Duration.Seconds() > 0 {
+		return float64(r.ProcessedCount) / r.Duration.Seconds()
+	}
+	return 0
+}
+
+// NewHeadersPercentage calculates the percentage of processed headers that were
+// newly added (not already present in the target). Returns 0 if no headers were
+// processed to avoid division by zero.
+func (r *ImportResult) NewHeadersPercentage() float64 {
+	if r.ProcessedCount > 0 {
+		return float64(r.AddedCount) / float64(r.ProcessedCount) * 100
+	}
+	return 0
+}
diff --git a/chainimport/http_source.go b/chainimport/http_source.go
new file mode 100644
index 0000000..2c9b9c3
--- /dev/null
+++ b/chainimport/http_source.go
@@ -0,0 +1,117 @@
+package chainimport
+
+import (
+	"fmt"
+	"io"
+	"net/http"
+	"os"
+)
+
+// httpHeaderImportSource implements headerImportSource for serving header files
+// over HTTP(s).
+type httpHeaderImportSource struct {
+	uri        string
+	httpClient HttpClient
+	file       HeaderImportSource
+}
+
+// Compile-time assertion to ensure httpHeaderImportSource implements
+// headerImportSource interface.
+var _ HeaderImportSource = (*httpHeaderImportSource)(nil)
+
+// newHTTPHeaderImportSource creates a new HTTP header import source.
+func newHTTPHeaderImportSource(uri string, httpClient HttpClient,
+	importSource HeaderImportSource) *httpHeaderImportSource {
+
+	return &httpHeaderImportSource{
+		uri:        uri,
+		httpClient: httpClient,
+		file:       importSource,
+	}
+}
+
+// Open opens the HTTP header import source based on file header import source.
+func (h *httpHeaderImportSource) Open() error {
+	resp, err := h.httpClient.Get(h.uri)
+	if err != nil {
+		return err
+	}
+	defer resp.Body.Close()
+
+	if resp.StatusCode != http.StatusOK {
+		return fmt.Errorf("failed to download file: status code %d",
+			resp.StatusCode)
+	}
+
+	tempFile, err := os.CreateTemp("", "neutrino-headers-http-import-*.tmp")
+	if err != nil {
+		return err
+	}
+	cleanup := func() {
+		tempFile.Close()
+		os.Remove(tempFile.Name())
+	}
+
+	_, err = io.Copy(tempFile, resp.Body)
+	if err != nil {
+		cleanup()
+		return err
+	}
+
+	if err = tempFile.Sync(); err != nil {
+		cleanup()
+		return fmt.Errorf("failed to sync temporary file: %w", err)
+	}
+
+	tempFile.Close()
+
+	h.file.SetURI(tempFile.Name())
+	return h.file.Open()
+}
+
+// Close closes the HTTP header import source resources.
+func (h *httpHeaderImportSource) Close() error {
+	if err := h.file.Close(); err != nil {
+		return fmt.Errorf("failed to close file import source: %w", err)
+	}
+
+	if err := os.Remove(h.file.GetURI()); err != nil {
+		return fmt.Errorf("failed to remove temporary file: %w", err)
+	}
+
+	return nil
+}
+
+// GetHeaderMetadata reads the metadata from the file. The metadata is memoized
+// after the first call, with subsequent calls returning the cached result
+// without re-reading the file.
+func (h *httpHeaderImportSource) GetHeaderMetadata() (*headerMetadata, error) {
+	return h.file.GetHeaderMetadata()
+}
+
+// Iterator returns an efficient iterator for sequential header access.
+func (h *httpHeaderImportSource) Iterator(start, end uint32,
+	batchSize uint32) HeaderIterator {
+
+	return h.file.Iterator(start, end, batchSize)
+}
+
+// GetHeader retrieves a single header at the specified index.
+func (h *httpHeaderImportSource) GetHeader(index uint32) (Header, error) {
+	return h.file.GetHeader(index)
+}
+
+// GetURI returns the HTTP URL for this import source.
+func (h *httpHeaderImportSource) GetURI() string {
+	return h.uri
+}
+
+// SetURI sets the HTTP URL for this import source.
+func (h *httpHeaderImportSource) SetURI(uri string) {
+	h.uri = uri
+}
+
+// newHTTPClient creates a new HTTP client.
+func newHTTPClient() HttpClient {
+	return &http.Client{}
+}
diff --git a/chainimport/http_source_mock.go b/chainimport/http_source_mock.go
new file mode 100644
index 0000000..259f951
--- /dev/null
+++ b/chainimport/http_source_mock.go
@@ -0,0 +1,22 @@
+package chainimport
+
+import (
+	"net/http"
+
+	"github.com/stretchr/testify/mock"
+)
+
+// mockHTTPClient mocks an HTTP client for testing HTTP header import source
+// interactions.
+type mockHTTPClient struct {
+	mock.Mock
+}
+
+// Get returns a response from the mock HTTP client.
+func (m *mockHTTPClient) Get(url string) (*http.Response, error) {
+	args := m.Called(url)
+	if args.Get(0) == nil {
+		return nil, args.Error(1)
+	}
+	return args.Get(0).(*http.Response), args.Error(1)
+}
diff --git a/chainimport/interfaces.go b/chainimport/interfaces.go
new file mode 100644
index 0000000..b450c11
--- /dev/null
+++ b/chainimport/interfaces.go
@@ -0,0 +1,115 @@
+package chainimport
+
+import (
+	"context"
+	"io"
+	"iter"
+	"net/http"
+)
+
+// ImportHeadersFile defines the interface for file-like objects used in header
+// import operations. It combines standard I/O interfaces with length info,
+// typically implemented by memory-mapped files for efficient random access to
+// header data.
+type ImportHeadersFile interface {
+	io.Reader
+	io.ReaderAt
+	io.Closer
+	Len() int
+}
+
+// Header is the base interface for header operations.
+type Header interface {
+	// Deserialize reconstructs the header from binary data at the specified
+	// height.
+	Deserialize(io.Reader, uint32) error
+}
+
+// HeaderIterator provides stateless iteration capabilities over headers. Each
+// call to Iterator/BatchIterator creates a fresh, independent sequence for the
+// specified range.
+type HeaderIterator interface {
+	// Iterator returns an iter.Seq2[header, error] for the specified range.
+	// Each call creates a fresh iterator that independently traverses the
+	// range from start to end index.
+	Iterator(start, end uint32) iter.Seq2[Header, error]
+
+	// BatchIterator returns an iterator that yields batches of headers for
+	// the specified range, where each batch respects the configured batch
+	// size limit. Each call creates a fresh iterator that independently
+	// traverses the range from start to end index.
+	BatchIterator(start, end, batchSize uint32) iter.Seq2[[]Header, error]
+
+	// ReadBatch collects all headers from the specified range into a slice,
+	// respecting the configured batch size limit.
+	ReadBatch(start, end, batchSize uint32) ([]Header, error)
+
+	// GetStartIndex returns the configured start index for this iterator.
+	// This is useful for accessing the intended range, validation, and
+	// as a fallback when you want to use the configured range.
+	GetStartIndex() uint32
+
+	// GetEndIndex returns the configured end index for this iterator.
+	// This is useful for accessing the intended range, validation, and
+	// as a fallback when you want to use the configured range.
+	GetEndIndex() uint32
+
+	// GetBatchSize returns the configured batch size for this iterator.
+	GetBatchSize() uint32
+}
+
+// HeaderImportSource is an interface for loading headers from various sources
+// (files, HTTP endpoints) with support for both sequential iteration and
+// efficient random access using memory-mapped files.
+type HeaderImportSource interface {
+	// Open initializes the header source and prepares it for reading.
+	Open() error
+
+	// Close releases any resources associated with the header source.
+	Close() error
+
+	// GetHeaderMetadata retrieves metadata information about the headers
+	// collection.
+	GetHeaderMetadata() (*headerMetadata, error)
+
+	// Iterator returns a headerIterator that can traverse headers between
+	// start and end indices.
+	Iterator(start, end uint32, batchSize uint32) HeaderIterator
+
+	// GetHeader retrieves a single header at the specified index using
+	// efficient random access (memory-mapped files) without loading the
+	// entire file into memory.
+	GetHeader(index uint32) (Header, error)
+
+	// GetURI returns the location identifier for this header source
+	// (file path for file sources, URL for HTTP sources).
+	GetURI() string
+
+	// SetURI sets the location identifier for this header source
+	// (file path for file sources, URL for HTTP sources).
+	SetURI(uri string)
+}
+
+// HeadersValidator defines methods for validating blockchain headers.
+type HeadersValidator interface {
+	// Validate performs comprehensive validation on a sequence of headers
+	// provided by the iterator. It checks that the entire sequence forms
+	// a valid chain.
+	Validate(context.Context, HeaderIterator) error
+
+	// ValidateBatch performs validation on a batch of headers, checking
+	// that they form a valid chain segment.
+	ValidateBatch([]Header) error
+
+	// ValidatePair verifies that two consecutive headers (prev and current)
+	// form a valid chain link.
+	ValidatePair(prev, current Header) error
+
+	// ValidateSingle validates a single header for basic sanity checks.
+	ValidateSingle(header Header) error
+}
+
+// HttpClient defines the interface for making HTTP GET requests.
+type HttpClient interface {
+	Get(url string) (*http.Response, error)
+}
diff --git a/chainimport/iter.go b/chainimport/iter.go
new file mode 100644
index 0000000..3f484e7
--- /dev/null
+++ b/chainimport/iter.go
@@ -0,0 +1,120 @@
+package chainimport
+
+import (
+	"io"
+	"iter"
+)
+
+// importSourceHeaderIterator provides efficient iteration over headers from
+// a source.
+type importSourceHeaderIterator struct {
+	// source is the HeaderImportSource from which headers are retrieved
+	// during iteration.
+	source HeaderImportSource
+
+	// startIndex is the default inclusive start index exposed by
+	// GetStartIndex.
+	startIndex uint32
+
+	// endIndex is the default inclusive end index exposed by GetEndIndex.
+	endIndex uint32
+
+	// batchSize determines how many headers are fetched in a single
+	// operation to optimize performance during iteration.
+	batchSize uint32
+}
+
+// Compile-time assertion to ensure importSourceHeaderIterator implements
+// headerIterator interface.
+var _ HeaderIterator = (*importSourceHeaderIterator)(nil)
+
+// Iterator returns a stateless iter.Seq2[header, error] for the specified
+// headers range. Each call creates a fresh iterator that independently
+// traverses the range from start to end index.
+func (it *importSourceHeaderIterator) Iterator(startIdx,
+	endIdx uint32) iter.Seq2[Header, error] {
+
+	return func(yield func(Header, error) bool) {
+		if startIdx > endIdx {
+			yield(nil, io.EOF)
+			return
+		}
+
+		for idx := startIdx; idx <= endIdx; idx++ {
+			header, err := it.source.GetHeader(idx)
+			if err != nil && !yield(nil, err) {
+				return
+			}
+			if !yield(header, nil) {
+				return
+			}
+		}
+	}
+}
+
+// BatchIterator returns a stateless iterator that yields batches of headers for
+// the specified range, where each batch respects the configured batch size
+// limit. Each call creates a fresh iterator that independently traverses the
+// range.
+func (it *importSourceHeaderIterator) BatchIterator(startIdx,
+	endIdx, batchSize uint32) iter.Seq2[[]Header, error] {
+
+	return func(yield func([]Header, error) bool) {
+		if startIdx > endIdx {
+			yield(nil, io.EOF)
+			return
+		}
+
+		currentIdx := startIdx
+		for currentIdx <= endIdx {
+			batch, err := it.ReadBatch(
+				currentIdx, endIdx, batchSize,
+			)
+			if err != nil && !yield(nil, err) {
+				return
+			}
+			if !yield(batch, nil) {
+				return
+			}
+			currentIdx += uint32(len(batch))
+		}
+	}
+}
+
+// ReadBatch is a stateless method that collects headers from the given range
+// into a slice, respecting the configured batch size limit. It returns io.EOF
+// if no headers are found.
+func (it *importSourceHeaderIterator) ReadBatch(
+	startIdx, endIdx, batchSize uint32) ([]Header, error) {
+
+	var headers []Header
+
+	actualEndIdx := min(endIdx, startIdx+batchSize-1)
+
+	for header, err := range it.Iterator(startIdx, actualEndIdx) {
+		if err != nil {
+			return nil, err
+		}
+
+		if header != nil {
+			headers = append(headers, header)
+		}
+	}
+
+	return headers, nil
+}
+
+// GetStartIndex returns the configured start index for this iterator.
+func (it *importSourceHeaderIterator) GetStartIndex() uint32 {
+	return it.startIndex
+}
+
+// GetEndIndex returns the configured end index for this iterator.
+func (it *importSourceHeaderIterator) GetEndIndex() uint32 {
+	return it.endIndex
+}
+
+// GetBatchSize returns the configured batch size for this iterator.
+func (it *importSourceHeaderIterator) GetBatchSize() uint32 {
+	return it.batchSize
+}
diff --git a/chainimport/iter_mock.go b/chainimport/iter_mock.go
new file mode 100644
index 0000000..44df497
--- /dev/null
+++ b/chainimport/iter_mock.go
@@ -0,0 +1,58 @@
+package chainimport
+
+import (
+	"iter"
+
+	"github.com/stretchr/testify/mock"
+)
+
+// mockHeaderIterator mocks a header iterator for testing header iteration
+// logic.
+type mockHeaderIterator struct {
+	mock.Mock
+}
+
+// Iterator returns an iter.Seq2[Header, error] for the specified range.
+func (m *mockHeaderIterator) Iterator(start,
+	end uint32) iter.Seq2[Header, error] {
+
+	args := m.Called(start, end)
+	return args.Get(0).(iter.Seq2[Header, error])
+}
+
+// BatchIterator returns an iterator that yields batches of headers.
+func (m *mockHeaderIterator) BatchIterator(start, end,
+	batchSize uint32) iter.Seq2[[]Header, error] {
+
+	args := m.Called(start, end, batchSize)
+	return args.Get(0).(iter.Seq2[[]Header, error])
+}
+
+// ReadBatch collects all headers from the given range into a slice.
+func (m *mockHeaderIterator) ReadBatch(start, end,
+	batchSize uint32) ([]Header, error) {
+
+	args := m.Called(start, end, batchSize)
+	if args.Get(0) == nil {
+		return nil, args.Error(1)
+	}
+	return args.Get(0).([]Header), args.Error(1)
+}
+
+// GetStartIndex returns the configured start index for this iterator.
+func (m *mockHeaderIterator) GetStartIndex() uint32 {
+	args := m.Called()
+	return args.Get(0).(uint32)
+}
+
+// GetEndIndex returns the configured end index for this iterator.
+func (m *mockHeaderIterator) GetEndIndex() uint32 {
+	args := m.Called()
+	return args.Get(0).(uint32)
+}
+
+// GetBatchSize returns the configured batch size for this iterator.
+func (m *mockHeaderIterator) GetBatchSize() uint32 {
+	args := m.Called()
+	return args.Get(0).(uint32)
+}
diff --git a/chainimport/log.go b/chainimport/log.go
new file mode 100644
index 0000000..7bb18ab
--- /dev/null
+++ b/chainimport/log.go
@@ -0,0 +1,25 @@
+package chainimport
+
+import "github.com/btcsuite/btclog"
+
+// log is a logger that is initialized with no output filters.  This
+// means the package will not perform any logging by default until the caller
+// requests it.
+var log btclog.Logger
+
+// log is a logger that is initialized with no output filters. This means the
+// package will not perform any logging by default until the caller requests it.
+func init() {
+	DisableLog()
+}
+
+// DisableLog disables all library log output. Logging output is disabled by
+// default until either UseLogger or SetLogWriter are called.
+func DisableLog() {
+	log = btclog.Disabled
+}
+
+// UseLogger uses a specified Logger to output package logging info.
+func UseLogger(logger btclog.Logger) {
+	log = logger
+}
diff --git a/chainimport/utils.go b/chainimport/utils.go
new file mode 100644
index 0000000..e42825a
--- /dev/null
+++ b/chainimport/utils.go
@@ -0,0 +1,121 @@
+package chainimport
+
+import (
+	"context"
+	"fmt"
+	"io"
+	"os"
+	"path/filepath"
+
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightninglabs/neutrino/headerfs"
+)
+
+// AddHeadersImportMetadata prepares a header file for import by adding the
+// necessary metadata to the file. This function takes an existing header file
+// and prepends metadata required by Neutrino's header import feature.
+func AddHeadersImportMetadata(sourceFilePath string,
+	networkMagic wire.BitcoinNet, version uint8,
+	headerType headerfs.HeaderType, startHeight uint32) error {
+
+	sourceFile, err := os.Open(sourceFilePath)
+	if err != nil {
+		return fmt.Errorf("failed to open source file: %w", err)
+	}
+	defer sourceFile.Close()
+
+	tmpPattern := fmt.Sprintf("%s_with_metadata_*.tmp",
+		filepath.Base(sourceFilePath))
+
+	tempFile, err := os.CreateTemp(filepath.Dir(sourceFilePath), tmpPattern)
+	if err != nil {
+		return fmt.Errorf("failed to create temporary file: %w", err)
+	}
+	tempFilePath := tempFile.Name()
+
+	defer func() {
+		tempFile.Close()
+		if _, statErr := os.Stat(tempFilePath); statErr == nil {
+			os.Remove(tempFilePath)
+		}
+	}()
+
+	metadata := &importMetadata{
+		networkMagic: networkMagic,
+		version:      version,
+		headerType:   headerType,
+		startHeight:  startHeight,
+	}
+
+	if err = metadata.encode(tempFile); err != nil {
+		return fmt.Errorf("failed to encode and write "+
+			"metadata: %w", err)
+	}
+
+	if _, err = io.Copy(tempFile, sourceFile); err != nil {
+		return fmt.Errorf("failed to copy original file content: %w",
+			err)
+	}
+
+	if err = tempFile.Sync(); err != nil {
+		return fmt.Errorf("failed to sync temporary file: %w", err)
+	}
+
+	if err = os.Rename(tempFilePath, sourceFilePath); err != nil {
+		return fmt.Errorf("failed to replace original file: %w", err)
+	}
+
+	return nil
+}
+
+// setLastFilterHeaderHash updates the HeaderHash of the last filter header to
+// match the block hash of the corresponding block header. This maintains chain
+// tip consistency for the regular tip.
+func setLastFilterHeaderHash(filterHeaders []headerfs.FilterHeader,
+	blockHeaders []headerfs.BlockHeader) {
+
+	// We only need to set the block header hash of the last filter header
+	// to maintain chain tip consistency for regular tip.
+	lastIdx := len(filterHeaders) - 1
+	chainTipHash := blockHeaders[lastIdx].BlockHeader.BlockHash()
+	filterHeaders[lastIdx].HeaderHash = chainTipHash
+}
+
+// targetHeightToImportSourceIndex converts the absolute blockchain target
+// height to the equivalent import source height based on the start height
+// input.
+func targetHeightToImportSourceIndex(targetH, importStartH uint32) uint32 {
+	return targetH - importStartH
+}
+
+// ctxCancelled checks if context is cancelled and returns the error if so. It
+// is used during long-running headers import operations to provide responsive
+// cancellation.
+func ctxCancelled(ctx context.Context) error {
+	select {
+	case <-ctx.Done():
+		return ctx.Err()
+	default:
+		return nil
+	}
+}
+
+// assertBlockHeader type asserts header to *blockHeader or returns error.
+func assertBlockHeader(header Header) (*blockHeader, error) {
+	blkHeader, ok := header.(*blockHeader)
+	if !ok {
+		return nil, fmt.Errorf("expected blockHeader type, got %T",
+			header)
+	}
+	return blkHeader, nil
+}
+
+// assertFilterHeader type asserts header to *filterHeader or returns error.
+func assertFilterHeader(header Header) (*filterHeader, error) {
+	fHeader, ok := header.(*filterHeader)
+	if !ok {
+		return nil, fmt.Errorf("expected filterHeader type, got %T",
+			header)
+	}
+	return fHeader, nil
+}
diff --git a/chainsync/filtercontrol.go b/chainsync/filtercontrol.go
index 6c4483d..2abfe5b 100644
--- a/chainsync/filtercontrol.go
+++ b/chainsync/filtercontrol.go
@@ -48,10 +48,10 @@ var filterHeaderCheckpoints = map[wire.BitcoinNet]map[uint32]*chainhash.Hash{
 	},
 }
 
-// ControlCFHeader controls the given filter header against our list of
+// ValidateCFHeader validates the given filter header against our list of
 // checkpoints. It returns ErrCheckpointMismatch if we have a checkpoint at the
 // given height, and it doesn't match.
-func ControlCFHeader(params chaincfg.Params, fType wire.FilterType,
+func ValidateCFHeader(params chaincfg.Params, fType wire.FilterType,
 	height uint32, filterHeader *chainhash.Hash) error {
 
 	if fType != wire.GCSFilterRegular {
@@ -75,6 +75,18 @@ func ControlCFHeader(params chaincfg.Params, fType wire.FilterType,
 	return nil
 }
 
+// ControlCFHeader controls the given filter header against our list of
+// checkpoints. It returns ErrCheckpointMismatch if we have a checkpoint at the
+// given height, and it doesn't match.
+//
+// Deprecated: This function will be removed in a future version. Use
+// ValidateCFHeader as an equivalent replacement.
+func ControlCFHeader(params chaincfg.Params, fType wire.FilterType,
+	height uint32, filterHeader *chainhash.Hash) error {
+
+	return ValidateCFHeader(params, fType, height, filterHeader)
+}
+
 // hashFromStr makes a chainhash.Hash from a valid hex string. If the string is
 // invalid, a nil pointer will be returned.
 func hashFromStr(hexStr string) *chainhash.Hash {
diff --git a/go.mod b/go.mod
index 1e4bf4a..2c548d6 100644
--- a/go.mod
+++ b/go.mod
@@ -13,6 +13,7 @@ require (
 	github.com/lightninglabs/neutrino/cache v1.1.2
 	github.com/lightningnetwork/lnd/queue v1.0.1
 	github.com/stretchr/testify v1.9.0
+	golang.org/x/exp v0.0.0-20250811191247-51f88131bc50
 )
 
 require (
diff --git a/go.sum b/go.sum
index 96a18b8..2d7b146 100644
--- a/go.sum
+++ b/go.sum
@@ -129,6 +129,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.22.0 h1:g1v0xeRhjcugydODzvb3mEM9SQ0HGp9s/nh3COQ/C30=
 golang.org/x/crypto v0.22.0/go.mod h1:vr6Su+7cTlO45qkww3VDJlzDn0ctJvRgYbC2NvXHt+M=
+golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 h1:3yiSh9fhy5/RhCSntf4Sy0Tnx50DmMpQ4MQdKKk4yg4=
+golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg=
 golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20190206173232-65e2d4e15006/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
diff --git a/log.go b/log.go
index 5e57fbb..34619f6 100644
--- a/log.go
+++ b/log.go
@@ -8,6 +8,7 @@ import (
 	"github.com/btcsuite/btcd/txscript"
 	"github.com/btcsuite/btclog"
 	"github.com/lightninglabs/neutrino/blockntfns"
+	"github.com/lightninglabs/neutrino/chainimport"
 	"github.com/lightninglabs/neutrino/chanutils"
 	"github.com/lightninglabs/neutrino/filterdb"
 	"github.com/lightninglabs/neutrino/pushtx"
@@ -45,4 +46,5 @@ func UseLogger(logger btclog.Logger) {
 	query.UseLogger(logger)
 	filterdb.UseLogger(logger)
 	chanutils.UseLogger(logger)
+	chainimport.UseLogger(logger)
 }
diff --git a/neutrino.go b/neutrino.go
index 6b5e319..ffa7d7c 100644
--- a/neutrino.go
+++ b/neutrino.go
@@ -4,6 +4,7 @@
 package neutrino
 
 import (
+	"context"
 	"errors"
 	"fmt"
 	"net"
@@ -25,6 +26,7 @@ import (
 	"github.com/lightninglabs/neutrino/banman"
 	"github.com/lightninglabs/neutrino/blockntfns"
 	"github.com/lightninglabs/neutrino/cache/lru"
+	"github.com/lightninglabs/neutrino/chainimport"
 	"github.com/lightninglabs/neutrino/chanutils"
 	"github.com/lightninglabs/neutrino/filterdb"
 	"github.com/lightninglabs/neutrino/headerfs"
@@ -602,6 +604,12 @@ type Config struct {
 	// they're doing something like a key import.
 	PersistToDisk bool
 
+	// HeadersImport contains configuration options for importing headers
+	// from external sources. When these options are set, neutrino will
+	// attempt to import headers from file before falling back to P2P
+	// synchronization.
+	HeadersImport *HeadersImportConfig
+
 	// AssertFilterHeader is an optional field that allows the creator of
 	// the ChainService to ensure that if any chain data exists, it's
 	// compliant with the expected filter header state. If neutrino starts
@@ -620,6 +628,41 @@ type Config struct {
 	BroadcastTimeout time.Duration
 }
 
+// HeadersImportConfig contains configuration options for importing headers
+// from external sources.
+type HeadersImportConfig struct {
+	// BlockHeadersSource specifies where to obtain block headers from.
+	// This could be a file path, URL, or other source identifier.
+	BlockHeadersSource string
+
+	// FilterHeadersSource specifies where to obtain filter headers from.
+	// This could be a file path, URL, or other source identifier.
+	FilterHeadersSource string
+
+	// WriteBatchSizePerRegion defines the number of headers to write in a
+	// single batch per processing region during import. The import process
+	// divides header ranges into distinct regions
+	// (overlap, divergence, and new headers), each with different
+	// processing requirements. Each region is processed in batches of this
+	// size to optimize database performance while maintaining data
+	// integrity boundaries. Larger values improve speed but increase memory
+	// usage.
+	//
+	// Default value: 16,384 (2^14) entries.
+	// This results in 16,384 block headers and 16,384 filter headers per
+	// batch. The total upper bound size per batch is:
+	//   - Block headers: (16,384 * 80 bytes) / (2^10) = 1.28 MB
+	//   - Filter headers: (16,384 * 32 bytes) / (2^10) = 0.5 MB
+	// Peak memory usage observed during benchmarking was ≈ 66 MB.
+	// Actual memory usage can vary depending on factors such as database
+	// state, Go garbage collector semantics and activity.
+	WriteBatchSizePerRegion int
+
+	// ValidationFlags specifies the behavior flags used during header
+	// validation. It defaults to BFNone.
+	ValidationFlags blockchain.BehaviorFlags
+}
+
 // peerSubscription holds a peer subscription which we'll notify about any
 // connected peers.
 type peerSubscription struct {
@@ -641,6 +684,8 @@ type ChainService struct { // nolint:maligned
 	RegFilterHeaders headerfs.FilterHeaderStore
 	persistToDisk    bool
 
+	headersImport *HeadersImportConfig
+
 	FilterCache *lru.Cache[FilterCacheKey, *CacheableFilter]
 	BlockCache  *lru.Cache[wire.InvVect, *CacheableBlock]
 
@@ -737,6 +782,7 @@ func NewChainService(cfg Config) (*ChainService, error) {
 		dialer:            dialer,
 		persistToDisk:     cfg.PersistToDisk,
 		broadcastTimeout:  cfg.BroadcastTimeout,
+		headersImport:     cfg.HeadersImport,
 	}
 	s.workManager = query.NewWorkManager(&query.Config{
 		ConnectedPeers: s.ConnectedPeers,
@@ -1609,12 +1655,33 @@ func (s *ChainService) ChainParams() chaincfg.Params {
 }
 
 // Start begins connecting to peers and syncing the blockchain.
-func (s *ChainService) Start() error {
+func (s *ChainService) Start(ctx context.Context) error {
 	// Already started?
 	if atomic.AddInt32(&s.started, 1) != 1 {
 		return nil
 	}
 
+	// Import headers if configured.
+	//nolint:lll
+	if s.headersImport != nil {
+		options := chainimport.ImportOptions{
+			BlockHeadersSource:      s.headersImport.BlockHeadersSource,
+			FilterHeadersSource:     s.headersImport.FilterHeadersSource,
+			TargetChainParams:       s.chainParams,
+			TargetBlockHeaderStore:  s.BlockHeaders,
+			TargetFilterHeaderStore: s.RegFilterHeaders,
+			ValidationFlags:         s.headersImport.ValidationFlags,
+			WriteBatchSizePerRegion: s.headersImport.WriteBatchSizePerRegion,
+		}
+		importer, err := chainimport.NewHeadersImport(&options)
+		if err != nil {
+			return err
+		}
+		if _, err := importer.Import(ctx); err != nil {
+			return err
+		}
+	}
+
 	// Start the address manager and block manager, both of which are
 	// needed by peers.
 	s.addrManager.Start()



Test patch:
diff --git a/chainimport/headers_import_test.go b/chainimport/headers_import_test.go
new file mode 100644
index 0000000..483e235
--- /dev/null
+++ b/chainimport/headers_import_test.go
@@ -0,0 +1,7268 @@
+package chainimport
+
+import (
+	"bytes"
+	"context"
+	"encoding/binary"
+	"encoding/hex"
+	"errors"
+	"fmt"
+	"io"
+	"math/big"
+	"net/http"
+	"os"
+	"path/filepath"
+	"regexp"
+	"testing"
+	"time"
+
+	"github.com/btcsuite/btcd/blockchain"
+	"github.com/btcsuite/btcd/chaincfg"
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/btcsuite/btcd/wire"
+	"github.com/btcsuite/btcwallet/walletdb"
+	_ "github.com/btcsuite/btcwallet/walletdb/bdb"
+	"github.com/lightninglabs/neutrino/chainsync"
+	"github.com/lightninglabs/neutrino/headerfs"
+	"github.com/stretchr/testify/mock"
+	"github.com/stretchr/testify/require"
+	"golang.org/x/exp/mmap"
+)
+
+// Block headers for testing captured from simnet network.
+var blockHdrs = []string{
+	"010000000000000000000000000000000000000000000000000000000000" +
+		"0000000000003ba3edfd7a7b12b27ac72c3e67768f617fc81bc3" +
+		"888a51323a9fb8aa4b1e5e4a45068653ffff7f2002000000",
+	"00000020f67ad7695d9b662a72ff3d8edbbb2de0bfa67b13974bb9910d11" +
+		"6d5cbd863e68c552826d121f12fcb288895d9488d189891ce0a6" +
+		"5a56193ea2ff3d4b99eabb875fac5a68ffff7f2003000000",
+	"000000200582f786cda8187a3bb13c044a70f11a5f299cbdb55dd43744a2" +
+		"de24cef76a72964688cc27da9f45261b8c35b00edea462f26469" +
+		"67fcb6052063d0140a1275de60ac5a68ffff7f2001000000",
+	"00000020f83e8ae2309315ff0a36646e2d43e7aa777b7aaa1eadb4876073" +
+		"e7a8dac11c1dc3a5e71065b6ab83ed8972d277de2670ceed1fc4" +
+		"3fd03f066cc84047d95eeaa360ac5a68ffff7f2002000000",
+	"000000203513820c27ba7b218bb6732e851ef404986f299f44b4275334d5" +
+		"eab0db09710835f6fc14632ebb23e141f680ae6aec6bdf76557b" +
+		"46daf1b4c0160631d89e1ac461ac5a68ffff7f2000000000",
+}
+
+// Filter headers for testing captured from simnet network.
+var filterHdrs = []string{
+	"b2ef0f5c5d790832d79fc9c9a7b3cef02dd94f143c63feba9d836248cad6" +
+		"24cf",
+	"b14a448b043b12401327695318318bbb53ec955e1e7963e3fd569a450448" +
+		"9177",
+	"75ae9eebc6e956fcb4fa00853aec5f252cf0046ed03587feece580386a6c" +
+		"d113",
+	"f99cbb96ca78c36c741b3765d78b22f0c1039add8afa6d2f6284b5cd6ab9" +
+		"d8d6",
+	"33e95706f9580a84e2cb167faf2239079805113cc7d3aaefff194b1ce6e6" +
+		"a26c",
+}
+
+// TestHeadersConjunctionProperty tests the headers conjunction property for
+// the import sources. Both block and filter headers are required to be present
+// for the import to start.
+func TestHeadersConjunctionProperty(t *testing.T) {
+	t.Parallel()
+	testCases := []struct {
+		name         string
+		options      *ImportOptions
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "MissingFilterHeaderImportSource",
+			options: &ImportOptions{
+				BlockHeadersSource:  "/path/to/blocks",
+				FilterHeadersSource: "",
+			},
+			expectErr: true,
+			expectErrMsg: "missing filter headers source " +
+				"path",
+		},
+		{
+			name: "MissingBlockHeaderImportSource",
+			options: &ImportOptions{
+				BlockHeadersSource:  "",
+				FilterHeadersSource: "/path/to/filters",
+			},
+			expectErr: true,
+			expectErrMsg: "missing block headers source " +
+				"path",
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			_, err := NewHeadersImport(tc.options)
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+// TestImportSkipOperation tests the import skip operation. It checks that the
+// import is skipped if the target header store is populated with headers.
+func TestImportSkipOperation(t *testing.T) {
+	t.Parallel()
+	ctx := context.Background()
+	type prep struct {
+		options *ImportOptions
+	}
+	type verify struct {
+		tc            *testing.T
+		importOptions *ImportOptions
+		importResult  *ImportResult
+	}
+	testCases := []struct {
+		name         string
+		prep         func() prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "TargetFilterStorePopulatedWithHeaders",
+			prep: func() prep {
+				tBS := &headerfs.MockBlockHeaderStore{}
+				tBS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+
+				tFS := &headerfs.MockFilterHeaderStore{}
+				tFS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(3), nil,
+				)
+
+				bIS := "/path/to/blocks"
+				fIS := "/path/to/filters"
+
+				ops := &ImportOptions{
+					BlockHeadersSource:      bIS,
+					FilterHeadersSource:     fIS,
+					TargetBlockHeaderStore:  tBS,
+					TargetFilterHeaderStore: tFS,
+				}
+
+				return prep{
+					options: ops,
+				}
+			},
+			verify: func(v verify) {
+				// Verify the default batch size is set.
+				ops := v.importOptions
+				require.Equal(
+					v.tc, defaultWriteBatchSizePerRegion,
+					ops.WriteBatchSizePerRegion,
+				)
+
+				// Verify no headers added/processed.
+				require.Equal(
+					v.tc, 0, v.importResult.AddedCount,
+				)
+				require.Equal(
+					v.tc, 0, v.importResult.ProcessedCount,
+				)
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			hI, err := NewHeadersImport(prep.options)
+			require.NoError(t, err)
+			importResult, err := hI.Import(ctx)
+			verify := verify{
+				tc:            t,
+				importOptions: prep.options,
+				importResult:  importResult,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// TestImportOperationOnFileHeaderSource tests the import operation on a file
+// header source. It checks that the import is successful and that the headers
+// are written to the target header stores.
+func TestImportOperationOnFileHeaderSource(t *testing.T) {
+	t.Parallel()
+	ctx := context.Background()
+	type prep struct {
+		options *ImportOptions
+		cleanup func()
+		err     error
+	}
+	type verify struct {
+		tc            *testing.T
+		importOptions *ImportOptions
+		importResult  *ImportResult
+	}
+	testCases := []struct {
+		name         string
+		prep         func() prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ImportWithoutErrors",
+			prep: func() prep {
+				tempDir := t.TempDir()
+				c1 := func() {
+					os.RemoveAll(tempDir)
+				}
+
+				dbPath := filepath.Join(tempDir, "test.db")
+				db, err := walletdb.Create(
+					"bdb", dbPath, true, time.Second*10,
+				)
+				c2 := func() {
+					db.Close()
+					c1()
+				}
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				b, err := headerfs.NewBlockHeaderStore(
+					tempDir, db, &chaincfg.SimNetParams,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				f, err := headerfs.NewFilterHeaderStore(
+					tempDir, db, headerfs.RegularFilter,
+					&chaincfg.SimNetParams, nil,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				bFile, c3, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				c4 := func() {
+					c3()
+					c2()
+				}
+				if err != nil {
+					return prep{
+						cleanup: c4,
+						err:     err,
+					}
+				}
+				bPath := bFile.Name()
+
+				// Close block headers import source to be
+				// opened during import.
+				bFile.Close()
+
+				fFile, c5, err := setupFileWithHdrs(
+					headerfs.RegularFilter, true,
+				)
+				cleanup := func() {
+					c5()
+					c4()
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+				fPath := fFile.Name()
+
+				// Close filter headers import source to be
+				// opened during import.
+				fFile.Close()
+
+				tCP := chaincfg.SimNetParams
+				flags := blockchain.BFFastAdd
+
+				ops := &ImportOptions{
+					BlockHeadersSource:      bPath,
+					FilterHeadersSource:     fPath,
+					TargetBlockHeaderStore:  b,
+					TargetFilterHeaderStore: f,
+					TargetChainParams:       tCP,
+					WriteBatchSizePerRegion: 128,
+					ValidationFlags:         flags,
+				}
+
+				return prep{
+					options: ops,
+					cleanup: cleanup,
+				}
+			},
+			verify: func(v verify) {
+				// Verify the user write batch size is used
+				// instead of the default one.
+				ops := v.importOptions
+				require.Equal(
+					v.tc, 128, ops.WriteBatchSizePerRegion,
+				)
+
+				// Verify headers added/processed excluding the
+				// genesis header.
+				require.Equal(
+					v.tc, len(blockHdrs)-1,
+					v.importResult.AddedCount,
+				)
+				require.Equal(
+					v.tc, len(blockHdrs)-1,
+					v.importResult.ProcessedCount,
+				)
+
+				// Verify no headers skipped.
+				require.Equal(
+					v.tc, 0, v.importResult.SkippedCount,
+				)
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+
+			hI, err := NewHeadersImport(prep.options)
+			require.NoError(t, err)
+			importResult, err := hI.Import(ctx)
+			verify := verify{
+				tc:            t,
+				importOptions: prep.options,
+				importResult:  importResult,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// TestImportOperationOnHTTPHeaderSource tests the import operation on a HTTP
+// header source. It checks that the import is successful and that the headers
+// are written to the target header stores.
+func TestImportOperationOnHTTPHeaderSource(t *testing.T) {
+	t.Parallel()
+	ctx := context.Background()
+	type prep struct {
+		hImport *headersImport
+		cleanup func()
+		err     error
+	}
+	type verify struct {
+		tc           *testing.T
+		importResult *ImportResult
+	}
+	testCases := []struct {
+		name         string
+		prep         func() prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ImportWithoutErrors",
+			prep: func() prep {
+				tempDir := t.TempDir()
+				c1 := func() {
+					os.RemoveAll(tempDir)
+				}
+
+				dbPath := filepath.Join(tempDir, "test.db")
+				db, err := walletdb.Create(
+					"bdb", dbPath, true, time.Second*10,
+				)
+				c2 := func() {
+					db.Close()
+					c1()
+				}
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				b, err := headerfs.NewBlockHeaderStore(
+					tempDir, db, &chaincfg.SimNetParams,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				f, err := headerfs.NewFilterHeaderStore(
+					tempDir, db, headerfs.RegularFilter,
+					&chaincfg.SimNetParams, nil,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				bFile, c3, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				c4 := func() {
+					c3()
+					c2()
+				}
+				if err != nil {
+					return prep{
+						cleanup: c4,
+						err:     err,
+					}
+				}
+
+				fFile, c5, err := setupFileWithHdrs(
+					headerfs.RegularFilter, true,
+				)
+				cleanup := func() {
+					c5()
+					c4()
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				mockHTTPClient := &mockHTTPClient{}
+				origin := "http://localhost:8311"
+
+				bRS := origin + "/headers/0"
+
+				blockStatus := http.StatusOK
+				blockBody := io.NopCloser(bFile)
+				blockRes := &http.Response{
+					StatusCode: blockStatus,
+					Body:       blockBody,
+				}
+				mockHTTPClient.On("Get", bRS).Return(
+					blockRes, nil,
+				)
+
+				fRS := origin + "/filter-headers/0"
+
+				filterStatus := http.StatusOK
+				filterBody := io.NopCloser(fFile)
+				filterRes := &http.Response{
+					StatusCode: filterStatus,
+					Body:       filterBody,
+				}
+				mockHTTPClient.On("Get", fRS).Return(
+					filterRes, nil,
+				)
+
+				bIS := newFileHeaderImportSource(
+					"", newBlockHeader,
+				)
+
+				fIS := newFileHeaderImportSource(
+					"", newFilterHeader,
+				)
+
+				bS := newHTTPHeaderImportSource(
+					bRS, mockHTTPClient, bIS,
+				)
+
+				fS := newHTTPHeaderImportSource(
+					fRS, mockHTTPClient, fIS,
+				)
+
+				tCP := chaincfg.SimNetParams
+				flags := blockchain.BFFastAdd
+
+				ops := &ImportOptions{
+					TargetChainParams:       tCP,
+					TargetBlockHeaderStore:  b,
+					TargetFilterHeaderStore: f,
+					BlockHeadersSource:      bRS,
+					FilterHeadersSource:     fRS,
+					WriteBatchSizePerRegion: 101,
+					ValidationFlags:         flags,
+				}
+
+				bV := ops.createBlockHeaderValidator(bS)
+				fV := ops.createFilterHeaderValidator()
+
+				headersImport := &headersImport{
+					options:                   ops,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			verify: func(v verify) {
+				// Verify headers added/processed excluding the
+				// genesis header.
+				require.Equal(
+					v.tc, len(blockHdrs)-1,
+					v.importResult.AddedCount,
+				)
+				require.Equal(
+					v.tc, len(blockHdrs)-1,
+					v.importResult.ProcessedCount,
+				)
+
+				// Verify no headers skipped.
+				require.Equal(
+					v.tc, 0, v.importResult.SkippedCount,
+				)
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			importResult, err := prep.hImport.Import(ctx)
+			verify := verify{
+				tc:           t,
+				importResult: importResult,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// TestTargetStoreFreshnessDetection tests the logic for detecting whether the
+// target header stores are fresh (only contain genesis headers). It covers
+// various combinations of block and filter header heights and error cases.
+func TestTargetStoreFreshnessDetection(t *testing.T) {
+	t.Parallel()
+	testCases := []struct {
+		name                 string
+		blockHeight          uint32
+		filterHeight         uint32
+		expectFresh          bool
+		expectErr            bool
+		expectFilterStoreErr error
+		expectBlockStoreErr  error
+	}{
+		{
+			name:         "OnlyGenesisHeadersExist",
+			blockHeight:  0,
+			filterHeight: 0,
+			expectFresh:  true,
+		},
+		{
+			name:         "BlockHeightGreaterThanZero",
+			blockHeight:  1,
+			filterHeight: 0,
+			expectFresh:  false,
+		},
+		{
+			name:         "FilterHeightGreaterThanZero",
+			blockHeight:  0,
+			filterHeight: 1,
+			expectFresh:  false,
+		},
+		{
+			name:         "BothHeightsGreaterThanZero",
+			blockHeight:  10,
+			filterHeight: 10,
+			expectFresh:  false,
+		},
+		{
+			name:      "ErrorOnBlockStore",
+			expectErr: true,
+			expectBlockStoreErr: errors.New(
+				"failed to get target block header",
+			),
+		},
+		{
+			name:      "ErrorOnFilterStore",
+			expectErr: true,
+			expectFilterStoreErr: errors.New(
+				"failed to get target filter header",
+			),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			mockBlockStore := &headerfs.MockBlockHeaderStore{}
+			mockBlockStore.On("ChainTip").Return(
+				&wire.BlockHeader{}, tc.blockHeight,
+				tc.expectBlockStoreErr,
+			)
+
+			mockFilterStore := &headerfs.MockFilterHeaderStore{}
+			mockFilterStore.On("ChainTip").Return(
+				&chainhash.Hash{}, tc.filterHeight,
+				tc.expectFilterStoreErr,
+			)
+
+			importer := &headersImport{}
+
+			isFresh, err := importer.isTargetFresh(
+				mockBlockStore, mockFilterStore,
+			)
+			if tc.expectErr {
+				if tc.expectBlockStoreErr != nil {
+					require.ErrorContains(
+						t, err,
+						tc.expectBlockStoreErr.Error(),
+					)
+				}
+				if tc.expectFilterStoreErr != nil {
+					require.ErrorContains(
+						t, err,
+						tc.expectFilterStoreErr.Error(),
+					)
+				}
+				return
+			}
+			require.NoError(t, err)
+			require.Equal(t, isFresh, tc.expectFresh)
+		})
+	}
+}
+
+// TestHeaderMetadataRetrieval tests the header metadata retrieval from the
+// import sources. It checks that the header metadata is retrieved correctly
+// from the import sources.
+func TestHeaderMetadataRetrieval(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		hImport *headersImport
+		cleanup func()
+		err     error
+	}
+	type verify struct {
+		tc        *testing.T
+		hMetadata *headerMetadata
+	}
+	testCases := []struct {
+		name         string
+		prep         func() prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ErrorOnReaderNotInitialized",
+			prep: func() prep {
+				bF, cleanup, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bF.Name())
+
+				headersImport := &headersImport{
+					options:                  opts,
+					blockHeadersImportSource: bS,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "file reader not initialized",
+		},
+		{
+			name: "ErrorOnHeaderRead",
+			prep: func() prep {
+				bFile, err := os.CreateTemp(
+					t.TempDir(), "invalid-block-header-*",
+				)
+				c1 := func() {
+					bFile.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				err = AddHeadersImportMetadata(
+					bFile.Name(), wire.SimNet, 0,
+					headerfs.Block, 0,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				// Reopen the file to get an updated file
+				// descriptor.
+				bFile.Close()
+				bFile, err = os.OpenFile(
+					bFile.Name(), os.O_RDWR, 0644,
+				)
+				c1 = func() {
+					bFile.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				// Remove the last byte of header
+				// metadata to simulate EOF error.
+				fileInfo, err := bFile.Stat()
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+				fileSize := fileInfo.Size()
+				if fileSize == 0 {
+					err := fmt.Errorf("empty file: %s",
+						bFile.Name())
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+				err = bFile.Truncate(fileSize - 1)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+				err = bFile.Sync()
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				bFS, ok := bS.(*fileHeaderImportSource)
+				require.True(t, ok)
+
+				reader, err := mmap.Open(bFile.Name())
+				cleanup := func() {
+					reader.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+				bFS.file = newMmapFile(reader)
+				bFS.fileSize = reader.Len()
+
+				bFS.metadata = nil
+
+				headersImport := &headersImport{
+					options:                  opts,
+					blockHeadersImportSource: bFS,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "failed to read start height",
+		},
+		{
+			name: "ErrorOnUnknownHeaderType",
+			prep: func() prep {
+				bFile, err := os.CreateTemp(
+					t.TempDir(),
+					"invalid-block-header-*",
+				)
+				c1 := func() {
+					bFile.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				err = AddHeadersImportMetadata(
+					bFile.Name(), wire.SimNet, 0,
+					headerfs.UnknownHeader, 0,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				// Reopen the file to get an updated file
+				// descriptor.
+				bFile.Close()
+				bFile, err = os.OpenFile(
+					bFile.Name(), os.O_RDWR, 0644,
+				)
+				c1 = func() {
+					bFile.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bs := opts.createBlockHeaderImportSrc()
+				bs.SetURI(bFile.Name())
+
+				bFS, ok := bs.(*fileHeaderImportSource)
+				require.True(t, ok)
+
+				reader, err := mmap.Open(bFile.Name())
+				cleanup := func() {
+					reader.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+				bFS.file = newMmapFile(reader)
+				bFS.fileSize = reader.Len()
+
+				bFS.metadata = nil
+
+				headersImport := &headersImport{
+					options:                  opts,
+					blockHeadersImportSource: bFS,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "failed to get header size: unknown " +
+				"header type: 255",
+		},
+		{
+			name: "ErrorOnNoHeadersData",
+			prep: func() prep {
+				bFile, err := os.CreateTemp(
+					t.TempDir(), "invalid-block-header-*",
+				)
+				c1 := func() {
+					bFile.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				err = AddHeadersImportMetadata(
+					bFile.Name(), wire.SimNet, 0,
+					headerfs.Block, 0,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				// Reopen the file to get an updated file
+				// descriptor.
+				bFile.Close()
+				bFile, err = os.OpenFile(
+					bFile.Name(), os.O_RDWR, 0644,
+				)
+				c1 = func() {
+					bFile.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bs := opts.createBlockHeaderImportSrc()
+				bs.SetURI(bFile.Name())
+
+				bFS, ok := bs.(*fileHeaderImportSource)
+				require.True(t, ok)
+
+				reader, err := mmap.Open(bFile.Name())
+				cleanup := func() {
+					reader.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+				bFS.file = newMmapFile(reader)
+				bFS.fileSize = reader.Len()
+
+				bFS.metadata = nil
+
+				headersImport := &headersImport{
+					options:                  opts,
+					blockHeadersImportSource: bFS,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "no headers available in import source",
+		},
+		{
+			name: "ErrorOnPartialHeadersData",
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				// Remove the last byte of the file to trigger
+				// data corruption.
+				fileInfo, err := bFile.Stat()
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+				fileSize := fileInfo.Size()
+				if fileSize == 0 {
+					err := fmt.Errorf("empty file: %s",
+						bFile.Name())
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+				err = bFile.Truncate(fileSize - 1)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+				err = bFile.Sync()
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bs := opts.createBlockHeaderImportSrc()
+				bs.SetURI(bFile.Name())
+
+				bFS, ok := bs.(*fileHeaderImportSource)
+				require.True(t, ok)
+
+				reader, err := mmap.Open(bFile.Name())
+				cleanup := func() {
+					reader.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+				bFS.file = newMmapFile(reader)
+				bFS.fileSize = reader.Len()
+
+				bFS.metadata = nil
+
+				headersImport := &headersImport{
+					options:                  opts,
+					blockHeadersImportSource: bFS,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "possible data corruption",
+		},
+		{
+			name: "ReturnsCachedMetadataWhenAvailable",
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				// Force a cache miss by opening the source file
+				// for the first time.
+				err = bS.Open()
+				c2 := func() {
+					bS.Close()
+					os.Remove(bS.GetURI())
+					c1()
+				}
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				// Remove the source file to ensure next call
+				// must use cached data.
+				err = bS.Close()
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				headersImport := &headersImport{
+					options:                  opts,
+					blockHeadersImportSource: bS,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: c1,
+				}
+			},
+			verify: func(v verify) {
+				// Next call should result in a cache hit since
+				// the file is gone.
+				bHdrType := headerfs.Block
+				expectBlockMetadata := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   bHdrType,
+						startHeight:  0,
+					},
+					endHeight:    4,
+					headerSize:   80,
+					headersCount: 5,
+				}
+				require.Equal(
+					v.tc, expectBlockMetadata,
+					v.hMetadata,
+				)
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+
+			bS := prep.hImport.blockHeadersImportSource
+			metadata, err := bS.GetHeaderMetadata()
+			verify := verify{
+				tc:        t,
+				hMetadata: metadata,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// TestHeaderMetadataStorage tests the header metadata storage to the file.
+// It checks that the header metadata is stored correctly to the file.
+func TestHeaderMetadataStorage(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		file    headerfs.File
+		data    []byte
+		cleanup func()
+		err     error
+	}
+	type verify struct {
+		tc   *testing.T
+		file headerfs.File
+		data []byte
+	}
+	testCases := []struct {
+		name         string
+		networkMagic wire.BitcoinNet
+		version      uint8
+		headerType   headerfs.HeaderType
+		startHeight  uint32
+		prep         func() prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ErrorOnSourceFileNotExist",
+			prep: func() prep {
+				return prep{
+					cleanup: func() {},
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "failed to open source file",
+		},
+		{
+			name:         "PreservesOriginalFileContents",
+			networkMagic: wire.SimNet,
+			version:      0,
+			headerType:   headerfs.Block,
+			startHeight:  0,
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, false,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				// Read the data before adding metadata for
+				// later assertion.
+				dataBefore, err := io.ReadAll(bFile)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				return prep{
+					file:    bFile,
+					data:    dataBefore,
+					cleanup: c1,
+				}
+			},
+			verify: func(v verify) {
+				// Reopen the file to get an updated file
+				// descriptor after adding header metadata
+				// atomically.
+				v.file.Close()
+				srcFile, err := os.OpenFile(
+					v.file.Name(), os.O_RDONLY, 0644,
+				)
+				cleanup := func() {
+					srcFile.Close()
+					os.Remove(srcFile.Name())
+				}
+				v.tc.Cleanup(cleanup)
+				require.NoError(v.tc, err)
+
+				data, err := io.ReadAll(srcFile)
+				require.NoError(v.tc, err)
+
+				// Ensure original file content's not modified.
+				after := data[ImportMetadataSize:]
+				areEqual := bytes.Equal(after, v.data)
+				require.True(v.tc, areEqual)
+			},
+		},
+		{
+			name:         "AddsBlockHeaderMetadataToFile",
+			networkMagic: wire.TestNet3,
+			version:      0,
+			headerType:   headerfs.Block,
+			startHeight:  1,
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, false,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				// Read the data before adding metadata for
+				// later assertion.
+				dataBefore, err := io.ReadAll(bFile)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				return prep{
+					file:    bFile,
+					data:    dataBefore,
+					cleanup: c1,
+				}
+			},
+			verify: func(v verify) {
+				// Reopen the file to get an updated file
+				// descriptor after adding header metadata
+				// atomically.
+				v.file.Close()
+				srcFile, err := os.OpenFile(
+					v.file.Name(), os.O_RDONLY, 0644,
+				)
+				cleanup := func() {
+					srcFile.Close()
+					os.Remove(srcFile.Name())
+				}
+				v.tc.Cleanup(cleanup)
+				require.NoError(v.tc, err)
+
+				data, err := io.ReadAll(srcFile)
+				require.NoError(v.tc, err)
+
+				networkMagicOffset := 0
+				versionOffset := networkMagicSize
+				hTOffset := versionOffset + versionSize
+				startHeightOffset := hTOffset + headerTypeSize
+
+				net := data[networkMagicOffset:versionOffset]
+				networkMagic := wire.BitcoinNet(
+					binary.LittleEndian.Uint32(net),
+				)
+				require.Equal(v.tc, wire.TestNet3, networkMagic)
+
+				version := data[versionOffset]
+				require.Equal(v.tc, uint8(0), version)
+
+				headerType := headerfs.HeaderType(
+					data[hTOffset],
+				)
+				require.Equal(v.tc, headerfs.Block, headerType)
+
+				hMS := ImportMetadataSize
+				sHeightD := data[startHeightOffset:hMS]
+				sHeight := binary.LittleEndian.Uint32(sHeightD)
+				require.Equal(v.tc, uint32(1), sHeight)
+
+				// Ensure original file content's not modified.
+				after := data[hMS:]
+				areEqual := bytes.Equal(after, v.data)
+				require.True(v.tc, areEqual)
+			},
+		},
+		{
+			name:         "AddsFilterHeaderMetadataToFile",
+			networkMagic: wire.TestNet4,
+			version:      0,
+			headerType:   headerfs.RegularFilter,
+			startHeight:  3,
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.RegularFilter, false,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				// Read the data before adding metadata
+				// for later assertion.
+				dataBefore, err := io.ReadAll(bFile)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				return prep{
+					file:    bFile,
+					data:    dataBefore,
+					cleanup: c1,
+				}
+			},
+			verify: func(v verify) {
+				// Reopen the file to get an updated file
+				// descriptor after adding header metadata
+				// atomically.
+				v.file.Close()
+				srcFile, err := os.OpenFile(
+					v.file.Name(), os.O_RDONLY, 0644,
+				)
+				cleanup := func() {
+					srcFile.Close()
+					os.Remove(srcFile.Name())
+				}
+				v.tc.Cleanup(cleanup)
+				require.NoError(v.tc, err)
+
+				data, err := io.ReadAll(srcFile)
+				require.NoError(v.tc, err)
+
+				networkMagicOffset := 0
+				versionOffset := networkMagicSize
+				hTOffset := versionOffset + versionSize
+				startHeightOffset := hTOffset + headerTypeSize
+
+				net := data[networkMagicOffset:versionOffset]
+				networkMagic := wire.BitcoinNet(
+					binary.LittleEndian.Uint32(net),
+				)
+				require.Equal(v.tc, wire.TestNet4, networkMagic)
+
+				version := data[versionOffset]
+				require.Equal(v.tc, uint8(0), version)
+
+				headerType := headerfs.HeaderType(
+					data[hTOffset],
+				)
+				require.Equal(
+					v.tc, headerfs.RegularFilter,
+					headerType,
+				)
+
+				hMS := ImportMetadataSize
+				sHeightD := data[startHeightOffset:hMS]
+				sHeight := binary.LittleEndian.Uint32(sHeightD)
+				require.Equal(v.tc, uint32(3), sHeight)
+
+				// Ensure original file content's not modified.
+				after := data[hMS:]
+				areEqual := bytes.Equal(after, v.data)
+				require.True(v.tc, areEqual)
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+
+			var srcFilePath string
+			if prep.file != nil {
+				srcFilePath = prep.file.Name()
+			}
+
+			err := AddHeadersImportMetadata(
+				srcFilePath, tc.networkMagic, tc.version,
+				tc.headerType, tc.startHeight,
+			)
+			verify := verify{
+				tc:   t,
+				file: prep.file,
+				data: prep.data,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// TestOpenFileHeaderImportSources tests the open operation on a file header
+// import source.
+func TestOpenFileHeaderImportSources(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		hImport *headersImport
+		cleanup func()
+		err     error
+	}
+	type verify struct {
+		tc      *testing.T
+		hImport *headersImport
+	}
+	testCases := []struct {
+		name         string
+		prep         func() prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "MissingBlockANDFilterHeaderImportSource",
+			prep: func() prep {
+				opts := &ImportOptions{}
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  nil,
+					filterHeadersImportSource: nil,
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+					err:     nil,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "missing required header sources",
+		},
+		{
+			name: "MissingBlockHeaderImportSource",
+			prep: func() prep {
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: nil,
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+					err:     nil,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "missing required header sources",
+		},
+		{
+			name: "MissingFilterHeaderImportSource",
+			prep: func() prep {
+				opts := &ImportOptions{}
+				fS := opts.createFilterHeaderImportSrc()
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  nil,
+					filterHeadersImportSource: fS,
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+					err:     nil,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "missing required header sources",
+		},
+		{
+			name: "MissingBlockANDFilterHeaderValidators",
+			prep: func() prep {
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				fS := opts.createFilterHeaderImportSrc()
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     nil,
+					filterHeadersValidator:    nil,
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+					err:     nil,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "missing required header validators",
+		},
+		{
+			name: "MissingBlockHeaderValidator",
+			prep: func() prep {
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				fS := opts.createFilterHeaderImportSrc()
+				fV := opts.createFilterHeaderValidator()
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     nil,
+					filterHeadersValidator:    fV,
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+					err:     nil,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "missing required header validators",
+		},
+		{
+			name: "MissingFilterHeaderValidator",
+			prep: func() prep {
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				fS := opts.createFilterHeaderImportSrc()
+				bV := opts.createBlockHeaderValidator(bS)
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    nil,
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+					err:     nil,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "missing required header validators",
+		},
+		{
+			name: "ErrorOnBlockFileNotExist",
+			prep: func() prep {
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				fS := opts.createFilterHeaderImportSrc()
+				bV := opts.createBlockHeaderValidator(bS)
+				fV := opts.createFilterHeaderValidator()
+				filePath := "/path/to/nonexistent/file"
+				bS.SetURI(filePath)
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+					err:     nil,
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "failed to mmap file: open " +
+				"/path/to/nonexistent/file",
+		},
+		{
+			name: "ErrorOnFilterFileNotExist",
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				fS := opts.createFilterHeaderImportSrc()
+				bV := opts.createBlockHeaderValidator(bS)
+				fV := opts.createFilterHeaderValidator()
+
+				bS.SetURI(bFile.Name())
+
+				filePath := "/path/to/nonexistent/file"
+				bS.SetURI(filePath)
+
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: c1,
+					err:     nil,
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "failed to mmap file: open " +
+				"/path/to/nonexistent/file",
+		},
+		{
+			name: "ErrorOnGetBlockHeaderMetadata",
+			prep: func() prep {
+				blockFile, err := os.CreateTemp(
+					t.TempDir(),
+					"empty-block-header-*",
+				)
+				cleanup := func() {
+					blockFile.Close()
+					os.Remove(blockFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				fS := opts.createFilterHeaderImportSrc()
+				bV := opts.createBlockHeaderValidator(bS)
+				fV := opts.createFilterHeaderValidator()
+
+				bS.SetURI(blockFile.Name())
+
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "failed to read network magic: EOF",
+		},
+		{
+			name: "ErrorOnGetFilterHeaderMetadata",
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				fFile, err := os.CreateTemp(
+					t.TempDir(), "empty-filter-header-*",
+				)
+				c2 := func() {
+					fFile.Close()
+					os.Remove(fFile.Name())
+				}
+				cleanup := func() {
+					c2()
+					c1()
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				fS := opts.createFilterHeaderImportSrc()
+				bV := opts.createBlockHeaderValidator(bS)
+				fV := opts.createFilterHeaderValidator()
+
+				bS.SetURI(bFile.Name())
+				fS.SetURI(fFile.Name())
+
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+				cleanup = func() {
+					headersImport.closeSources()
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "failed to read network magic: EOF",
+		},
+		{
+			name: "OpenSourcesCorrectly",
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				fFile, c2, err := setupFileWithHdrs(
+					headerfs.RegularFilter, true,
+				)
+				cleanup := func() {
+					c2()
+					c1()
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				fS := opts.createFilterHeaderImportSrc()
+				bV := opts.createBlockHeaderValidator(bS)
+				fV := opts.createFilterHeaderValidator()
+
+				bS.SetURI(bFile.Name())
+				fS.SetURI(fFile.Name())
+
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+				cleanup = func() {
+					headersImport.closeSources()
+				}
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			verify: func(v verify) {
+				bHdrType := headerfs.Block
+				expectBlockMetadata := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   bHdrType,
+						startHeight:  0,
+					},
+					endHeight:    4,
+					headerSize:   80,
+					headersCount: 5,
+				}
+
+				fHdrType := headerfs.RegularFilter
+				expectFilterMetadata := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   fHdrType,
+						startHeight:  0,
+					},
+					endHeight:    4,
+					headerSize:   32,
+					headersCount: 5,
+				}
+
+				bS := v.hImport.blockHeadersImportSource
+				metadata, err := bS.GetHeaderMetadata()
+				require.NoError(v.tc, err)
+				require.Equal(
+					v.tc, expectBlockMetadata, metadata,
+				)
+
+				f := v.hImport.filterHeadersImportSource
+				metadata, err = f.GetHeaderMetadata()
+				require.NoError(v.tc, err)
+				require.Equal(
+					v.tc, expectFilterMetadata, metadata,
+				)
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+
+			err := prep.hImport.openSources()
+			verify := verify{
+				tc:      t,
+				hImport: prep.hImport,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// TestOpenHTTPHeaderImportSources tests the open operation on a HTTP header
+// import source.
+func TestOpenHTTPHeaderImportSources(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		hImport *headersImport
+		cleanup func()
+		err     error
+	}
+	testCases := []struct {
+		name         string
+		prep         func() prep
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ErrorOnGetBlockHeadersOverHTTP",
+			prep: func() prep {
+				mockHTTPClient := &mockHTTPClient{}
+				origin := "http://localhost:8234"
+
+				bRS := origin + "/headers/0"
+				mockHTTPClient.On("Get", bRS).Return(
+					nil, errors.New("failed to "+
+						"download block "+
+						"headers"),
+				).Once()
+
+				fRS := origin + "/filter-headers/0"
+
+				status := http.StatusOK
+				body := io.NopCloser(bytes.NewBufferString(""))
+				res := &http.Response{
+					StatusCode: status,
+					Body:       body,
+				}
+				mockHTTPClient.On("Get", fRS).Return(res, nil)
+
+				opts := &ImportOptions{
+					BlockHeadersSource:  bRS,
+					FilterHeadersSource: fRS,
+				}
+				bS := newHTTPHeaderImportSource(
+					bRS, mockHTTPClient,
+					&mockHeaderImportSource{},
+				)
+				fS := newHTTPHeaderImportSource(
+					fRS, mockHTTPClient,
+					&mockHeaderImportSource{},
+				)
+				bV := opts.createBlockHeaderValidator(bS)
+				fV := opts.createFilterHeaderValidator()
+
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+				}
+			},
+			expectErr:    true,
+			expectErrMsg: "failed to download block headers",
+		},
+		{
+			name: "ErrorOnGetBlockHeadersOverHTTPNotFound",
+			prep: func() prep {
+				mockHTTPClient := &mockHTTPClient{}
+				origin := "http://localhost:7334"
+
+				bRS := origin + "/headers/0"
+				status := http.StatusNotFound
+				body := io.NopCloser(
+					bytes.NewBufferString(""),
+				)
+				res := &http.Response{
+					StatusCode: status,
+					Body:       body,
+				}
+				mockHTTPClient.On("Get", bRS).Return(
+					res, nil,
+				).Once()
+
+				fRS := origin + "/filter-headers/0"
+
+				filterStatus := http.StatusOK
+				filterBody := io.NopCloser(
+					bytes.NewBufferString(""),
+				)
+				filterRes := &http.Response{
+					StatusCode: filterStatus,
+					Body:       filterBody,
+				}
+				mockHTTPClient.On("Get", fRS).Return(
+					filterRes, nil,
+				)
+
+				opts := &ImportOptions{
+					BlockHeadersSource:  bRS,
+					FilterHeadersSource: fRS,
+				}
+				bS := newHTTPHeaderImportSource(
+					bRS, mockHTTPClient,
+					&mockHeaderImportSource{},
+				)
+				fS := newHTTPHeaderImportSource(
+					fRS, mockHTTPClient,
+					&mockHeaderImportSource{},
+				)
+				bV := opts.createBlockHeaderValidator(bS)
+				fV := opts.createFilterHeaderValidator()
+
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+				}
+			},
+			expectErr: true,
+			expectErrMsg: fmt.Sprintf("failed to download file: "+
+				"status code %d", http.StatusNotFound),
+		},
+		{
+			name: "ErrorOnGetFilterHeadersOverHTTP",
+			prep: func() prep {
+				mockHTTPClient := &mockHTTPClient{}
+				origin := "http://localhost:8344"
+
+				bRS := origin + "/headers/0"
+
+				blockStatus := http.StatusOK
+				blockBody := io.NopCloser(
+					bytes.NewBufferString(""),
+				)
+				blockRes := &http.Response{
+					StatusCode: blockStatus,
+					Body:       blockBody,
+				}
+				mockHTTPClient.On("Get", bRS).Return(
+					blockRes, nil,
+				)
+
+				fRS := origin + "/filter-headers/0"
+				mockHTTPClient.On("Get", fRS).Return(
+					nil, errors.New("failed to "+
+						"download filter "+
+						"headers"),
+				).Once()
+
+				bIS := &mockHeaderImportSource{}
+				bIS.On("Open").Return(nil)
+				bIS.On("SetURI", mock.Anything).Return()
+
+				opts := &ImportOptions{
+					BlockHeadersSource:  bRS,
+					FilterHeadersSource: fRS,
+				}
+				bS := newHTTPHeaderImportSource(
+					bRS, mockHTTPClient, bIS,
+				)
+				fS := newHTTPHeaderImportSource(
+					fRS, mockHTTPClient,
+					&mockHeaderImportSource{},
+				)
+				cleanup := func() {
+					os.Remove(bS.uri)
+				}
+
+				bV := opts.createBlockHeaderValidator(bS)
+				fV := opts.createFilterHeaderValidator()
+
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			expectErr:    true,
+			expectErrMsg: "failed to download filter headers",
+		},
+		{
+			name: "ErrorOnGetHTTPFilterHeadersNotFound",
+			prep: func() prep {
+				mockHTTPClient := &mockHTTPClient{}
+				origin := "http://localhost:8334"
+
+				bRS := origin + "/headers/0"
+
+				blockStatus := http.StatusOK
+				blockBody := io.NopCloser(
+					bytes.NewBufferString(""),
+				)
+				blockRes := &http.Response{
+					StatusCode: blockStatus,
+					Body:       blockBody,
+				}
+				mockHTTPClient.On("Get", bRS).Return(
+					blockRes, nil,
+				)
+
+				fRS := origin + "/filter-headers/0"
+				status := http.StatusNotFound
+				body := io.NopCloser(bytes.NewBufferString(""))
+				res := &http.Response{
+					StatusCode: status,
+					Body:       body,
+				}
+				mockHTTPClient.On("Get", fRS).Return(
+					res, nil,
+				).Once()
+
+				bIS := &mockHeaderImportSource{}
+				bIS.On("Open").Return(nil)
+				bIS.On("SetURI", mock.Anything).Return()
+
+				opts := &ImportOptions{
+					BlockHeadersSource:  bRS,
+					FilterHeadersSource: fRS,
+				}
+				bS := newHTTPHeaderImportSource(
+					bRS, mockHTTPClient, bIS,
+				)
+				fS := newHTTPHeaderImportSource(
+					fRS, mockHTTPClient,
+					&mockHeaderImportSource{},
+				)
+				cleanup := func() {
+					os.Remove(bS.uri)
+				}
+
+				bV := opts.createBlockHeaderValidator(bS)
+				fV := opts.createFilterHeaderValidator()
+
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: fmt.Sprintf("failed to download file: "+
+				"status code %d", http.StatusNotFound),
+		},
+		{
+			name: "OpenSourcesCorrectly",
+			prep: func() prep {
+				mockHTTPClient := &mockHTTPClient{}
+				origin := "http://localhost:8323"
+
+				bRS := origin + "/headers/0"
+
+				blockStatus := http.StatusOK
+				blockBody := io.NopCloser(
+					bytes.NewBufferString(""),
+				)
+				blockRes := &http.Response{
+					StatusCode: blockStatus,
+					Body:       blockBody,
+				}
+				mockHTTPClient.On("Get", bRS).Return(
+					blockRes, nil,
+				)
+
+				fRS := origin + "/filter-headers/0"
+
+				filterStatus := http.StatusOK
+				filterBody := io.NopCloser(
+					bytes.NewBufferString(""),
+				)
+				filterRes := &http.Response{
+					StatusCode: filterStatus,
+					Body:       filterBody,
+				}
+				mockHTTPClient.On("Get", fRS).Return(
+					filterRes, nil,
+				)
+
+				bIS := &mockHeaderImportSource{}
+				bIS.On("Open").Return(nil)
+				bIS.On("SetURI", mock.Anything).Return()
+
+				fIS := &mockHeaderImportSource{}
+				fIS.On("Open").Return(nil)
+				fIS.On("SetURI", mock.Anything).Return()
+
+				opts := &ImportOptions{
+					BlockHeadersSource:  bRS,
+					FilterHeadersSource: fRS,
+				}
+				bS := newHTTPHeaderImportSource(
+					bRS, mockHTTPClient,
+					bIS,
+				)
+				fS := newHTTPHeaderImportSource(
+					fRS, mockHTTPClient,
+					fIS,
+				)
+				cleanup := func() {
+					os.Remove(fS.uri)
+					os.Remove(bS.uri)
+				}
+
+				bV := opts.createBlockHeaderValidator(bS)
+				fV := opts.createFilterHeaderValidator()
+
+				headersImport := &headersImport{
+					options:                   opts,
+					blockHeadersImportSource:  bS,
+					filterHeadersImportSource: fS,
+					blockHeadersValidator:     bV,
+					filterHeadersValidator:    fV,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: cleanup,
+				}
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+
+			err := prep.hImport.openSources()
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+// TestImportAndTargetSourcesCompatibilityConstraint tests the import and
+// target sources compatibility constraint. It checks that the import and
+// target sources are compatible.
+func TestImportAndTargetSourcesCompatibilityConstraint(t *testing.T) {
+	t.Parallel()
+	testCases := []struct {
+		name         string
+		prep         func() *headersImport
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ErrorOnGetBlockHeaderMetadata",
+			prep: func() *headersImport {
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					nil, errors.New("I/O read error"),
+				)
+				hImport := &headersImport{
+					blockHeadersImportSource: bIS,
+				}
+				return hImport
+			},
+			expectErr:    true,
+			expectErrMsg: "I/O read error",
+		},
+		{
+			name: "ErrorOnGetFilterHeaderMetadata",
+			prep: func() *headersImport {
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{}, nil,
+				)
+
+				fIS := &mockHeaderImportSource{}
+				fIS.On("GetHeaderMetadata").Return(
+					nil, errors.New("I/O read error"),
+				)
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+				}
+				return hImport
+			},
+			expectErr:    true,
+			expectErrMsg: "I/O read error",
+		},
+		{
+			name: "ErrorOnIncorrectBlockHeaderType",
+			prep: func() *headersImport {
+				bIS := &mockHeaderImportSource{}
+				rFT := headerfs.RegularFilter
+				bM := &headerMetadata{
+					importMetadata: &importMetadata{
+						headerType: rFT,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(bM, nil)
+
+				fIS := &mockHeaderImportSource{}
+				fIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{}, nil,
+				)
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+				}
+				return hImport
+			},
+			expectErr: true,
+			expectErrMsg: "incorrect block header type: expected " +
+				"BlockHeader, got RegularFilterHeader",
+		},
+		{
+			name: "ErrorOnIncorrectFilterHeaderType",
+			prep: func() *headersImport {
+				bIS := &mockHeaderImportSource{}
+				bM := &headerMetadata{
+					importMetadata: &importMetadata{
+						headerType: headerfs.Block,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(bM, nil)
+
+				fIS := &mockHeaderImportSource{}
+				fM := &headerMetadata{
+					importMetadata: &importMetadata{
+						headerType: headerfs.Block,
+					},
+				}
+				fIS.On("GetHeaderMetadata").Return(fM, nil)
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+				}
+				return hImport
+			},
+			expectErr: true,
+			expectErrMsg: "incorrect filter header type: " +
+				"expected RegularFilterHeader, got BlockHeader",
+		},
+		{
+			name: "ErrorOnMismatchImportChainTypes",
+			prep: func() *headersImport {
+				bIS := &mockHeaderImportSource{}
+				bT := headerfs.Block
+				bM := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.MainNet,
+						version:      0,
+						headerType:   bT,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(bM, nil)
+				bIS.On("GetURI").Return("/path/to/blocks")
+
+				fIS := &mockHeaderImportSource{}
+				rFT := headerfs.RegularFilter
+				fM := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   rFT,
+					},
+				}
+				fIS.On("GetHeaderMetadata").Return(fM, nil)
+				fIS.On("GetURI").Return("/path/to/filters")
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+				}
+				return hImport
+			},
+			expectErr: true,
+			expectErrMsg: fmt.Sprintf("network type mismatch: "+
+				"block headers from /path/to/blocks (%s), "+
+				"filter headers from /path/to/filters (%s)",
+				wire.MainNet, wire.SimNet),
+		},
+		{
+			name: "ErrorOnMismatchImportTargetChainTypes",
+			prep: func() *headersImport {
+				bIS := &mockHeaderImportSource{}
+				bT := headerfs.Block
+				bM := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   bT,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(bM, nil)
+
+				fIS := &mockHeaderImportSource{}
+				rFT := headerfs.RegularFilter
+				fM := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   rFT,
+					},
+				}
+				fIS.On("GetHeaderMetadata").Return(fM, nil)
+
+				tCP := chaincfg.Params{
+					Net: wire.MainNet,
+				}
+
+				ops := &ImportOptions{
+					TargetChainParams: tCP,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+				return hImport
+			},
+			expectErr: true,
+			expectErrMsg: fmt.Sprintf("network mismatch: headers "+
+				"from import sources are for %s, but target "+
+				"is %s", wire.SimNet, wire.MainNet),
+		},
+		{
+			name: "ErrorOnMismatchImportStartHeights",
+			prep: func() *headersImport {
+				bIS := &mockHeaderImportSource{}
+				bT := headerfs.Block
+				bM := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   bT,
+						startHeight:  1,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(bM, nil)
+				bIS.On("GetURI").Return("/path/to/blocks")
+
+				fIS := &mockHeaderImportSource{}
+				rFT := headerfs.RegularFilter
+				fM := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   rFT,
+						startHeight:  3,
+					},
+				}
+				fIS.On("GetHeaderMetadata").Return(fM, nil)
+				fIS.On("GetURI").Return("/path/to/filters")
+
+				tCP := chaincfg.Params{
+					Net: wire.SimNet,
+				}
+
+				ops := &ImportOptions{
+					TargetChainParams: tCP,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+				return hImport
+			},
+			expectErr: true,
+			expectErrMsg: "start height mismatch: block headers " +
+				"start at 1, filter headers start at 3",
+		},
+		{
+			name: "ErrorOnMismatchImportHeadersCount",
+			prep: func() *headersImport {
+				bIS := &mockHeaderImportSource{}
+				bT := headerfs.Block
+				bM := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   bT,
+						startHeight:  1,
+					},
+					headersCount: 3,
+				}
+				bIS.On("GetHeaderMetadata").Return(bM, nil)
+				bIS.On("GetURI").Return("/path/to/blocks")
+
+				fIS := &mockHeaderImportSource{}
+				rFT := headerfs.RegularFilter
+				fM := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   rFT,
+						startHeight:  1,
+					},
+					headersCount: 5,
+				}
+				fIS.On("GetHeaderMetadata").Return(fM, nil)
+				fIS.On("GetURI").Return("/path/to/filters")
+
+				tCP := chaincfg.Params{
+					Net: wire.SimNet,
+				}
+
+				ops := &ImportOptions{
+					TargetChainParams: tCP,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+				return hImport
+			},
+			expectErr: true,
+			expectErrMsg: "headers count mismatch: block headers " +
+				"import source /path/to/blocks (3), filter " +
+				"headers import source /path/to/filters (5)",
+		},
+		{
+			name: "ValidateSourcesSuccessfully",
+			prep: func() *headersImport {
+				bIS := &mockHeaderImportSource{}
+				bT := headerfs.Block
+				bM := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+
+						headerType:  bT,
+						startHeight: 1,
+					},
+					headersCount: 3,
+				}
+				bIS.On("GetHeaderMetadata").Return(bM, nil)
+
+				fIS := &mockHeaderImportSource{}
+				rFT := headerfs.RegularFilter
+				fM := &headerMetadata{
+					importMetadata: &importMetadata{
+						networkMagic: wire.SimNet,
+						version:      0,
+						headerType:   rFT,
+						startHeight:  1,
+					},
+					headersCount: 3,
+				}
+				fIS.On("GetHeaderMetadata").Return(fM, nil)
+
+				tCP := chaincfg.Params{
+					Net: wire.SimNet,
+				}
+
+				ops := &ImportOptions{
+					TargetChainParams: tCP,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+				return hImport
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			hImport := tc.prep()
+			err := hImport.validateSourcesCompatibility()
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+// TestImportAndTargetSourcesChainContinuityConstraint tests the import and
+// target sources chain continuity constraint. It checks that the import and
+// target sources are continuous.
+func TestImportAndTargetSourcesChainContinuityConstraint(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		hImport *headersImport
+		err     error
+	}
+	testCases := []struct {
+		name         string
+		prep         func() prep
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ErrorOnGetHeaderMetadata",
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					nil, errors.New("I/O read error"),
+				)
+				hImport := &headersImport{
+					blockHeadersImportSource: bIS,
+				}
+				return prep{
+					hImport: hImport,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to get block header metadata: " +
+				"I/O read error",
+		},
+		{
+			name: "ErrorOnGetChainTipForTargetBlockStore",
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{}, nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0),
+					errors.New("I/O read error"),
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore: bHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to get target block " +
+				"header chain tip: I/O read error",
+		},
+		{
+			name: "ErrorOnGetChainTipForTargetFilterStore",
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{}, nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0),
+					errors.New("I/O read error"),
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to get target filter header " +
+				"chain tip: I/O read error",
+		},
+		{
+			name: "ErrorOnDivergenceInTargetHeaderStores",
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{}, nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(2), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "divergence detected between target " +
+				"header store tip heights (block=0, filter=2)",
+		},
+		{
+			name: "ErrorOnImportStartHeightCreatesGap",
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				metadata := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 2,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(
+					metadata, nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "target stores contain only genesis " +
+				"block (height 0) but import data starts at " +
+				"height 2, creating a gap",
+		},
+		{
+			name: "ErrorOnGenesisBlockHeadersMismatch",
+			prep: func() prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+				bIS.On("GetHeader", uint32(0)).Return(
+					newBlockHeader(), nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+				bHS.On("FetchHeaderByHeight", uint32(0)).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "genesis header mismatch: block header " +
+				"mismatch at height 0",
+		},
+		{
+			name: "ErrorOnGenesisFilterHeadersMismatch",
+			prep: func() prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				fH, err := constructFilterHdr(
+					filterHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+				bIS.On("GetHeader", uint32(0)).Return(bH, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+				bHS.On("FetchHeaderByHeight", uint32(0)).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0), nil,
+				)
+				fHS.On("FetchHeaderByHeight", uint32(0)).Return(
+					&fH.FilterHash, nil,
+				)
+
+				fIS := &mockHeaderImportSource{}
+				fIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{}, nil,
+				)
+				fIS.On("GetHeader", uint32(0)).Return(
+					newFilterHeader(), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "genesis header mismatch: filter " +
+				"header mismatch at height 0",
+		},
+		{
+			name: "ValidateOnGenesisHeightSuccessfully",
+			prep: func() prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				fH, err := constructFilterHdr(
+					filterHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+				bIS.On("GetHeader", uint32(0)).Return(bH, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+				bHS.On("FetchHeaderByHeight", uint32(0)).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+				bIS.On("GetURI").Return("/path/to/blocks")
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0), nil,
+				)
+				fHS.On("FetchHeaderByHeight", uint32(0)).Return(
+					&fH.FilterHash, nil,
+				)
+
+				fIS := &mockHeaderImportSource{}
+				fIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{}, nil,
+				)
+				fIS.On("GetHeader", uint32(0)).Return(
+					fH, nil,
+				)
+				fIS.On("GetURI").Return("/path/to/filters")
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+		},
+		{
+			name: "ErrorOnGetBlockHeaderFromTargetStore",
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				metadata := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 1,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(
+					metadata, nil,
+				)
+				bIS.On("GetHeader", uint32(0)).Return(
+					newBlockHeader(), nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+				bHS.On("FetchHeaderByHeight", uint32(0)).Return(
+					nil, errors.New("I/O read error"),
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to get block header from " +
+				"target at height 0: I/O read error",
+		},
+		{
+			name: "ErrorOnGetBlockHeaderFromImportSource",
+			prep: func() prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				metadata := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 1,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(
+					metadata, nil,
+				)
+				bIS.On("GetHeader", uint32(0)).Return(
+					nil, errors.New("I/O read error"),
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0),
+					nil,
+				)
+				bHS.On("FetchHeaderByHeight", uint32(0)).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to get block header from " +
+				"import source at height 1: I/O read error",
+		},
+		{
+			name: "ErrorOnTypeAssertingBlockHeader",
+			prep: func() prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				metadata := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 1,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(
+					metadata, nil,
+				)
+				bIS.On("GetHeader", uint32(0)).Return(
+					newFilterHeader(), nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+				bHS.On(
+					"FetchHeaderByHeight", uint32(0),
+				).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "expected blockHeader type, got " +
+				"*chainimport.filterHeader",
+		},
+		{
+			name: "ErrorOnFirstHeaderChainBroken",
+			prep: func() prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				metadata := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 1,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(
+					metadata, nil,
+				)
+				bIS.On("GetHeader", uint32(0)).Return(
+					newBlockHeader(), nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+				bHS.On("FetchHeaderByHeight", uint32(0)).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to validate header connection: " +
+				"header chain broken",
+		},
+		{
+			name: "ValidateOnHeightOneSuccessfully",
+			prep: func() prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bH2, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				metadata := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 1,
+					},
+				}
+				bIS.On("GetHeaderMetadata").Return(
+					metadata, nil,
+				)
+				bIS.On("GetHeader", uint32(0)).Return(
+					bH2, nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0), nil,
+				)
+				bHS.On("FetchHeaderByHeight", uint32(0)).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			require.NoError(t, prep.err)
+			err := prep.hImport.validateChainContinuity()
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+// TestImportAndTargetSourcesHeadersVerificationConstraint tests the import and
+// target sources headers verification constraint. It checks that the import
+// and target sources are verified correctly.
+func TestImportAndTargetSourcesHeadersVerificationConstraint(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		hI  *headersImport
+		err error
+	}
+	testCases := []struct {
+		name         string
+		height       uint32
+		prep         func(height uint32) prep
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ErrorOnGetHeaderMetadata",
+			prep: func(uint32) prep {
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					nil, errors.New("I/O read error"),
+				)
+				hImport := &headersImport{
+					blockHeadersImportSource: bIS,
+				}
+				return prep{
+					hI: hImport,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to get block header metadata: " +
+				"I/O read error",
+		},
+		{
+			name:   "ErrorOnGetBlockHeaderFromImportSrc",
+			height: 101,
+			prep: func(height uint32) prep {
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx := height - 1
+				bIS.On("GetHeader", importIndx).Return(
+					nil, errors.New("I/O read error"),
+				)
+				hImport := &headersImport{
+					blockHeadersImportSource: bIS,
+				}
+				return prep{
+					hI: hImport,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to get block header from " +
+				"import source at height 101: I/O read error",
+		},
+		{
+			name:   "ErrorOnTypeAssertingBlockHeader",
+			height: 101,
+			prep: func(height uint32) prep {
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx := height - 1
+				bIS.On("GetHeader", importIndx).Return(
+					newFilterHeader(), nil,
+				)
+				hImport := &headersImport{
+					blockHeadersImportSource: bIS,
+				}
+				return prep{
+					hI: hImport,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "expected blockHeader type, got " +
+				"*chainimport.filterHeader",
+		},
+		{
+			name:   "ErrorOnGetBlockHeaderFromTargetStore",
+			height: 101,
+			prep: func(height uint32) prep {
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx := height - 1
+				bIS.On("GetHeader", importIndx).Return(
+					newBlockHeader(), nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				h := height
+				bHS.On("FetchHeaderByHeight", h).Return(
+					nil, errors.New("I/O read error"),
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore: bHS,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+				return prep{
+					hI: hImport,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to get block header from " +
+				"target at height 101: I/O read error",
+		},
+		{
+			name:   "ErrorOnBlockHeadersMismatch",
+			height: 101,
+			prep: func(height uint32) prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx := height - 1
+				bIS.On("GetHeader", importIndx).Return(
+					newBlockHeader(), nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("FetchHeaderByHeight", height).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore: bHS,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource: bIS,
+					options:                  ops,
+				}
+				return prep{
+					hI: hImport,
+				}
+			},
+			expectErr:    true,
+			expectErrMsg: "block header mismatch at height 101",
+		},
+		{
+			name:   "ErrorOnGetFilterHeaderFromImportSrc",
+			height: 101,
+			prep: func(height uint32) prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx := height - 1
+				bIS.On("GetHeader", importIndx).Return(bH, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("FetchHeaderByHeight", height).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fIS := &mockHeaderImportSource{}
+				fIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx = height - 1
+				fIS.On("GetHeader", importIndx).Return(
+					nil, errors.New("I/O read error"),
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore: bHS,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hI: hImport,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to get filter header from " +
+				"import source at height 101: I/O read error",
+		},
+		{
+			name:   "ErrorOnTypeAssertingFilterHeader",
+			height: 101,
+			prep: func(height uint32) prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx := height - 1
+				bIS.On("GetHeader", importIndx).Return(
+					bH, nil,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("FetchHeaderByHeight", height).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fIS := &mockHeaderImportSource{}
+				fIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx = height - 1
+				fIS.On("GetHeader", importIndx).Return(bH, nil)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore: bHS,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hI: hImport,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "expected filterHeader type, got " +
+				"*chainimport.blockHeader",
+		},
+		{
+			name:   "ErrorOnGetFilterHeaderFromTargetStore",
+			height: 101,
+			prep: func(height uint32) prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx := height - 1
+				bIS.On("GetHeader", importIndx).Return(bH, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("FetchHeaderByHeight", height).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fIS := &mockHeaderImportSource{}
+				fIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx = height - 1
+				fIS.On("GetHeader", importIndx).Return(
+					newFilterHeader(), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("FetchHeaderByHeight", height).Return(
+					nil, errors.New("I/O read error"),
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hI: hImport,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "failed to get filter header from " +
+				"target at height 101: I/O read error",
+		},
+		{
+			name:   "ErrorOnFilterHeadersMismatch",
+			height: 101,
+			prep: func(height uint32) prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				fH, err := constructFilterHdr(
+					filterHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx := height - 1
+				bIS.On("GetHeader", importIndx).Return(bH, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("FetchHeaderByHeight", height).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fIS := &mockHeaderImportSource{}
+				fIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx = height - 1
+				fIS.On("GetHeader", importIndx).Return(
+					newFilterHeader(), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("FetchHeaderByHeight", height).Return(
+					&fH.FilterHash, nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hI: hImport,
+				}
+			},
+			expectErr:    true,
+			expectErrMsg: "filter header mismatch at height 101",
+		},
+		{
+			name:   "VerifyHeadersAtTargetHeightWithNoErrs",
+			height: 101,
+			prep: func(height uint32) prep {
+				bH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				fH, err := constructFilterHdr(
+					filterHdrs[1],
+					uint32(1),
+				)
+				if err != nil {
+					return prep{err: err}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx := height - 1
+				bIS.On("GetHeader", importIndx).Return(bH, nil)
+				bIS.On("GetURI").Return("/path/to/blocks")
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("FetchHeaderByHeight", height).Return(
+					bH.BlockHeader.BlockHeader, nil,
+				)
+
+				fIS := &mockHeaderImportSource{}
+				fIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: &importMetadata{
+							startHeight: uint32(1),
+						},
+					}, nil,
+				)
+				importIndx = height - 1
+				fIS.On("GetHeader", importIndx).Return(fH, nil)
+				fIS.On("GetURI").Return("/path/to/filters")
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("FetchHeaderByHeight", height).Return(
+					&fH.FilterHash, nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hI: hImport,
+				}
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep(tc.height)
+			require.NoError(t, prep.err)
+			err := prep.hI.verifyHeadersAtTargetHeight(tc.height)
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+// TestHeaderRetrievalOnSingleHeader tests the header retrieval on a single
+// header. It checks that the header is retrieved correctly from the import
+// source.
+func TestHeaderRetrievalOnSingleHeader(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		hISource HeaderImportSource
+		cleanup  func()
+		err      error
+	}
+	type verify struct {
+		tc     *testing.T
+		header Header
+		index  int
+	}
+	testCases := []struct {
+		name         string
+		index        int
+		hType        headerfs.HeaderType
+		prep         func(headerfs.HeaderType) prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name:  "ErrorOnReaderNotInitialized",
+			hType: headerfs.Block,
+			prep: func(hType headerfs.HeaderType) prep {
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				return prep{
+					hISource: bS,
+					cleanup:  func() {},
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "file reader not initialized",
+		},
+		{
+			name:  "ErrorOnHeaderMetadataNotInitialized",
+			hType: headerfs.Block,
+			prep: func(hType headerfs.HeaderType) prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				bFS, ok := bS.(*fileHeaderImportSource)
+				require.True(t, ok)
+
+				reader, err := mmap.Open(bFile.Name())
+				cleanup := func() {
+					reader.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+				bFS.file = newMmapFile(reader)
+
+				bFS.metadata = nil
+
+				return prep{
+					hISource: bFS,
+					cleanup:  cleanup,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "header metadata not initialized",
+		},
+		{
+			name:  "ErrorOnDeserializeUnknownHeaderType",
+			hType: headerfs.UnknownHeader,
+			prep: func(hType headerfs.HeaderType) prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				bFS, ok := bS.(*fileHeaderImportSource)
+				require.True(t, ok)
+
+				reader, err := mmap.Open(bFile.Name())
+				cleanup := func() {
+					reader.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+				bFS.file = newMmapFile(reader)
+
+				bFS.metadata = &headerMetadata{
+					importMetadata: &importMetadata{
+						headerType: hType,
+					},
+				}
+
+				return prep{
+					hISource: bFS,
+					cleanup:  cleanup,
+				}
+			},
+			verify:    func(v verify) {},
+			expectErr: true,
+			expectErrMsg: "failed to deserialize " +
+				"wire.BlockHeader: EOF",
+		},
+		{
+			name:  "ErrorOnHeaderIndexOutOfBounds",
+			hType: headerfs.Block,
+			index: len(blockHdrs),
+			prep: func(hType headerfs.HeaderType) prep {
+				bFile, c1, err := setupFileWithHdrs(hType, true)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				err = bS.Open()
+				cleanup := func() {
+					bS.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				return prep{
+					hISource: bS,
+					cleanup:  cleanup,
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: fmt.Sprintf("failed to read header at "+
+				"index %d", len(blockHdrs)),
+		},
+		{
+			name:  "GetBlockHeaderSuccessfully",
+			hType: headerfs.Block,
+			index: 3,
+			prep: func(hType headerfs.HeaderType) prep {
+				bFile, c1, err := setupFileWithHdrs(hType, true)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				err = bS.Open()
+				cleanup := func() {
+					bS.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				return prep{
+					hISource: bS,
+					cleanup:  cleanup,
+				}
+			},
+			verify: func(v verify) {
+				bH, ok := v.header.(*blockHeader)
+				require.True(v.tc, ok)
+
+				bHExpected, err := constructBlkHdr(
+					blockHdrs[v.index], uint32(v.index),
+				)
+				require.NoError(t, err)
+
+				// Assert that the known block header at this
+				// index matches the retrieved one.
+				require.Equal(v.tc, bHExpected, bH)
+			},
+		},
+		{
+			name:  "GetFilterHeaderSuccessfully",
+			hType: headerfs.RegularFilter,
+			index: 3,
+			prep: func(hType headerfs.HeaderType) prep {
+				fFile, c1, err := setupFileWithHdrs(hType, true)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				fS := opts.createFilterHeaderImportSrc()
+				fS.SetURI(fFile.Name())
+
+				err = fS.Open()
+				cleanup := func() {
+					fS.Close()
+					os.Remove(fFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				return prep{
+					hISource: fS,
+					cleanup:  cleanup,
+				}
+			},
+			verify: func(v verify) {
+				fH, ok := v.header.(*filterHeader)
+				require.True(v.tc, ok)
+
+				fHExpected, err := constructFilterHdr(
+					filterHdrs[v.index], uint32(v.index),
+				)
+				require.NoError(t, err)
+
+				// Assert that the known filter header at this
+				// index matches the retrieved one.
+				require.Equal(v.tc, fHExpected, fH)
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep(tc.hType)
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+
+			header, err := prep.hISource.GetHeader(uint32(tc.index))
+			verify := verify{
+				tc:     t,
+				header: header,
+				index:  tc.index,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// TestHeaderRetrievalOnSequentialHeaders tests the header retrieval on
+// sequential headers. It checks that the header is retrieved correctly from
+// the import source.
+func TestHeaderRetrievalOnSequentialHeaders(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		hIterator HeaderIterator
+		cleanup   func()
+		err       error
+	}
+	type verify struct {
+		tc        *testing.T
+		hIterator HeaderIterator
+		header    Header
+	}
+	testCases := []struct {
+		name         string
+		index        int
+		hType        headerfs.HeaderType
+		prep         func(hT headerfs.HeaderType, indx int) prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name:  "ErrorOnHeaderOutOfBounds",
+			index: len(blockHdrs),
+			hType: headerfs.Block,
+			prep: func(hT headerfs.HeaderType, i int) prep {
+				bFile, c1, err := setupFileWithHdrs(hT, true)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				err = bS.Open()
+				c2 := func() {
+					bS.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				current := uint32(i)
+				end := uint32(i)
+				iter := &importSourceHeaderIterator{
+					source:     bS,
+					startIndex: current,
+					endIndex:   end,
+					batchSize:  128,
+				}
+
+				cleanup := func() {
+					c2()
+				}
+
+				return prep{
+					hIterator: iter,
+					cleanup:   cleanup,
+				}
+			},
+			verify: func(v verify) {
+				require.Nil(v.tc, v.header)
+			},
+			expectErr:    true,
+			expectErrMsg: io.EOF.Error(),
+		},
+		{
+			name:  "NoMoreHeadersToIterateOver",
+			index: len(blockHdrs) - 1,
+			hType: headerfs.Block,
+			prep: func(hT headerfs.HeaderType, i int) prep {
+				bFile, c1, err := setupFileWithHdrs(hT, true)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				err = bS.Open()
+				c2 := func() {
+					bS.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				current := uint32(i + 1)
+				end := uint32(i)
+				iter := &importSourceHeaderIterator{
+					source:     bS,
+					startIndex: current,
+					endIndex:   end,
+					batchSize:  128,
+				}
+
+				cleanup := func() {
+					c2()
+				}
+
+				return prep{
+					hIterator: iter,
+					cleanup:   cleanup,
+				}
+			},
+			verify: func(v verify) {
+				require.Nil(v.tc, v.header)
+			},
+			expectErr:    true,
+			expectErrMsg: io.EOF.Error(),
+		},
+		{
+			name:  "IterateOverBlockHeadersSuccessfully",
+			index: 0,
+			hType: headerfs.Block,
+			prep: func(hT headerfs.HeaderType, i int) prep {
+				bFile, c1, err := setupFileWithHdrs(hT, true)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				err = bS.Open()
+				c2 := func() {
+					bS.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				current := uint32(i)
+				end := uint32(len(blockHdrs) - 1)
+				iter := &importSourceHeaderIterator{
+					source:     bS,
+					startIndex: current,
+					endIndex:   end,
+					batchSize:  128,
+				}
+
+				cleanup := func() {
+					c2()
+				}
+
+				return prep{
+					hIterator: iter,
+					cleanup:   cleanup,
+				}
+			},
+			verify: func(v verify) {
+				iter := v.hIterator
+				nBHs := len(blockHdrs)
+				headerSeq := iter.Iterator(1, uint32(nBHs-1))
+				i := 1
+				for h, err := range headerSeq {
+					require.NoError(v.tc, err)
+					hE, err := constructBlkHdr(
+						blockHdrs[i], uint32(i),
+					)
+					require.NoError(t, err)
+					require.Equal(v.tc, hE, h)
+					i++
+				}
+			},
+		},
+		{
+			name:  "IterateOverFilterHeadersSuccessfully",
+			index: 0,
+			hType: headerfs.RegularFilter,
+			prep: func(hT headerfs.HeaderType, i int) prep {
+				fFile, c1, err := setupFileWithHdrs(
+					hT, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{}
+				fS := opts.createFilterHeaderImportSrc()
+				fS.SetURI(fFile.Name())
+
+				err = fS.Open()
+				c2 := func() {
+					fS.Close()
+					os.Remove(fFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c2,
+						err:     err,
+					}
+				}
+
+				current := uint32(i)
+				end := uint32(len(filterHdrs) - 1)
+				iter := &importSourceHeaderIterator{
+					source:     fS,
+					startIndex: current,
+					endIndex:   end,
+					batchSize:  128,
+				}
+
+				cleanup := func() {
+					c2()
+				}
+
+				return prep{
+					hIterator: iter,
+					cleanup:   cleanup,
+				}
+			},
+			verify: func(v verify) {
+				iter := v.hIterator
+				nFHs := len(filterHdrs)
+				headerSeq := iter.Iterator(1, uint32(nFHs-1))
+				i := 1
+				for h, err := range headerSeq {
+					require.NoError(v.tc, err)
+					hE, err := constructFilterHdr(
+						filterHdrs[i], uint32(i),
+					)
+					require.NoError(t, err)
+					require.Equal(v.tc, hE, h)
+					i++
+				}
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep(tc.hType, tc.index)
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+
+			headerSeq := prep.hIterator.Iterator(
+				prep.hIterator.GetStartIndex(),
+				prep.hIterator.GetEndIndex(),
+			)
+
+			// Attempt to get the first header from the sequence.
+			var (
+				header Header
+				err    error
+			)
+			for header, err = range headerSeq {
+				break
+			}
+
+			verify := verify{
+				tc:        t,
+				hIterator: prep.hIterator,
+				header:    header,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// TestHeaderValidationOnBlockHeadersPair tests the header validation on a block
+// header pair. It checks that the header is validated correctly.
+func TestHeaderValidationOnBlockHeadersPair(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		hValidator HeadersValidator
+		prev       Header
+		current    Header
+		err        error
+	}
+	testCases := []struct {
+		name         string
+		tCP          chaincfg.Params
+		prep         func(tCP chaincfg.Params) prep
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ErrorOnMismatchPreviousHeaderType",
+			tCP:  chaincfg.Params{},
+			prep: func(chaincfg.Params) prep {
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bHV := opts.createBlockHeaderValidator(bS)
+				return prep{
+					hValidator: bHV,
+					prev:       newFilterHeader(),
+					current:    newBlockHeader(),
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "expected blockHeader type, got " +
+				"\\*chainimport.filterHeader",
+		},
+		{
+			name: "ErrorOnMismatchCurrentHeaderType",
+			tCP:  chaincfg.Params{},
+			prep: func(chaincfg.Params) prep {
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bHV := opts.createBlockHeaderValidator(bS)
+				return prep{
+					hValidator: bHV,
+					prev:       newBlockHeader(),
+					current:    newFilterHeader(),
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "expected blockHeader type, got " +
+				"\\*chainimport.filterHeader",
+		},
+		{
+			name: "ErrorOnNonConsecutiveHeaderChain",
+			tCP:  chaincfg.Params{},
+			prep: func(chaincfg.Params) prep {
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bHV := opts.createBlockHeaderValidator(bS)
+
+				prevH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				nBH := len(blockHdrs)
+				currentH, err := constructBlkHdr(
+					blockHdrs[nBH-1], uint32(nBH-1),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				return prep{
+					hValidator: bHV,
+					prev:       prevH,
+					current:    currentH,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "height mismatch: previous height=0, " +
+				"current height=4",
+		},
+		{
+			name: "ErrorOnInvalidHeaderHashChain",
+			tCP:  chaincfg.Params{},
+			prep: func(chaincfg.Params) prep {
+				opts := &ImportOptions{}
+				bS := opts.createBlockHeaderImportSrc()
+				bHV := opts.createBlockHeaderValidator(bS)
+
+				originH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				prevH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				currentH, err := constructBlkHdr(
+					blockHdrs[2], uint32(2),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				currentH.PrevBlock = originH.BlockHash()
+
+				return prep{
+					hValidator: bHV,
+					prev:       prevH,
+					current:    currentH,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "header chain broken: current header's " +
+				"PrevBlock (.*) doesn't match previous " +
+				"header's hash (.*)",
+		},
+		{
+			name: "ErrorOnDifficultyDropExceedsLimit",
+			tCP:  chaincfg.SimNetParams,
+			prep: func(tCP chaincfg.Params) prep {
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+				}
+				bS := opts.createBlockHeaderImportSrc()
+				bHV := opts.createBlockHeaderValidator(bS)
+
+				prevH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				currentH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				// Set a difficulty that would cause a drop
+				// exceeding simnet's retarget adjustment factor
+				// limit. Simnet allows difficulty to drop by up
+				// to 4x (RetargetAdjustmentFactor = 4).
+				prevDifficulty := blockchain.CompactToBig(
+					prevH.Bits,
+				)
+
+				// Create a difficulty that's 5x lower
+				// (exceeds the 4x limit).
+				excessiveDrop := new(big.Int).Div(
+					prevDifficulty, big.NewInt(5),
+				)
+				currentH.Bits = blockchain.BigToCompact(
+					excessiveDrop,
+				)
+
+				return prep{
+					hValidator: bHV,
+					prev:       prevH,
+					current:    currentH,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "block header contextual validation " +
+				"failed: block difficulty of .* is not the " +
+				"expected value of .*",
+		},
+		{
+			name: "ErrorOnZeroDifficultyTarget",
+			tCP:  chaincfg.SimNetParams,
+			prep: func(tCP chaincfg.Params) prep {
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+					ValidationFlags:   blockchain.BFFastAdd,
+				}
+				bS := opts.createBlockHeaderImportSrc()
+				bHV := opts.createBlockHeaderValidator(bS)
+
+				prevH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				currentH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				currentH.Bits = 0
+
+				return prep{
+					hValidator: bHV,
+					prev:       prevH,
+					current:    currentH,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "block target difficulty of .* is too " +
+				"low",
+		},
+		{
+			name: "ErrorOnDifficultyExceedsMaximumAllowed",
+			tCP:  chaincfg.SimNetParams,
+			prep: func(tCP chaincfg.Params) prep {
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+					ValidationFlags:   blockchain.BFFastAdd,
+				}
+				bS := opts.createBlockHeaderImportSrc()
+				bHV := opts.createBlockHeaderValidator(bS)
+
+				prevH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				currentH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				invT := new(big.Int).Add(
+					tCP.PowLimit, big.NewInt(1),
+				)
+				newT := blockchain.BigToCompact(invT)
+				currentH.Bits = newT
+
+				return prep{
+					hValidator: bHV,
+					prev:       prevH,
+					current:    currentH,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "block target difficulty of .* is " +
+				"higher than max of .*",
+		},
+		{
+			name: "ErrorOnHashHigherThanTarget",
+			tCP:  chaincfg.SimNetParams,
+			prep: func(tCP chaincfg.Params) prep {
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+					ValidationFlags:   blockchain.BFFastAdd,
+				}
+				bS := opts.createBlockHeaderImportSrc()
+				bHV := opts.createBlockHeaderValidator(bS)
+
+				prevH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				currentH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				// Set an extremely high difficulty
+				// (very small target).
+				currentH.Bits = 0x1f000001
+
+				// We need to ensure the block hash will be
+				// higher than target.
+				currentH.Nonce = 0xffffffff
+				currentH.MerkleRoot = chainhash.Hash{
+					0xff, 0xff, 0xff,
+				}
+
+				return prep{
+					hValidator: bHV,
+					prev:       prevH,
+					current:    currentH,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "block hash of .* is higher than " +
+				"expected max of .*",
+		},
+		{
+			name: "ErrorOnSubSecondTimestampPrecision",
+			tCP:  chaincfg.SimNetParams,
+			prep: func(tCP chaincfg.Params) prep {
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+					ValidationFlags:   blockchain.BFFastAdd,
+				}
+				bS := opts.createBlockHeaderImportSrc()
+				bHV := opts.createBlockHeaderValidator(bS)
+
+				prevH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				currentH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				currentH.Timestamp = time.Unix(0, 50)
+				return prep{
+					hValidator: bHV,
+					prev:       prevH,
+					current:    currentH,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "block timestamp of .* has a higher " +
+				"precision than one second",
+		},
+		{
+			name: "ValidateBlockHeadersPairSuccessfully",
+			tCP:  chaincfg.SimNetParams,
+			prep: func(tCP chaincfg.Params) prep {
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+					ValidationFlags:   blockchain.BFFastAdd,
+				}
+				bS := opts.createBlockHeaderImportSrc()
+				bHV := opts.createBlockHeaderValidator(bS)
+
+				prevH, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				currentH, err := constructBlkHdr(
+					blockHdrs[1], uint32(1),
+				)
+				if err != nil {
+					return prep{
+						err: err,
+					}
+				}
+
+				return prep{
+					hValidator: bHV,
+					prev:       prevH,
+					current:    currentH,
+				}
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep(tc.tCP)
+			require.NoError(t, prep.err)
+
+			err := prep.hValidator.ValidatePair(
+				prep.prev, prep.current,
+			)
+			if tc.expectErr {
+				matched, matchErr := regexp.MatchString(
+					tc.expectErrMsg, err.Error(),
+				)
+				require.NoError(t, matchErr)
+				require.True(
+					t, matched, "failed with: "+err.Error(),
+				)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+// TestHeaderValidationOnSequentialBlockHeaders tests the header validation on
+// sequential block headers. It checks that the header is validated correctly.
+func TestHeaderValidationOnSequentialBlockHeaders(t *testing.T) {
+	t.Parallel()
+	ctx := context.Background()
+	type prep struct {
+		iterator  HeaderIterator
+		validator HeadersValidator
+		cleanup   func()
+		err       error
+	}
+	testCases := []struct {
+		name         string
+		index        int
+		prep         func() prep
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name:  "ErrorOnHeaderOutOfBounds",
+			index: len(blockHdrs),
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				tCP := chaincfg.SimNetParams
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+				}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				err = bS.Open()
+				cleanup := func() {
+					bS.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				index := len(blockHdrs)
+				start := uint32(index - 1)
+				end := uint32(index)
+				bIterator := bS.Iterator(
+					start, end,
+					defaultWriteBatchSizePerRegion,
+				)
+
+				bV := opts.createBlockHeaderValidator(bS)
+
+				return prep{
+					iterator:  bIterator,
+					validator: bV,
+					cleanup:   cleanup,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: fmt.Sprintf("failed to read "+
+				"header at index %d", len(blockHdrs)),
+		},
+		{
+			name: "NoMoreHeadersToValidate",
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				tCP := chaincfg.SimNetParams
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+					ValidationFlags:   blockchain.BFFastAdd,
+				}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+
+				err = bS.Open()
+				cleanup := func() {
+					bS.Close()
+					os.Remove(bFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				index := len(blockHdrs) - 1
+				start := uint32(index + 1)
+				end := uint32(index)
+				bIterator := bS.Iterator(
+					start, end,
+					defaultWriteBatchSizePerRegion,
+				)
+
+				bV := opts.createBlockHeaderValidator(bS)
+
+				return prep{
+					iterator:  bIterator,
+					validator: bV,
+					cleanup:   cleanup,
+				}
+			},
+			expectErr:    true,
+			expectErrMsg: io.EOF.Error(),
+		},
+		{
+			name: "ValidSequentialHeaders",
+			prep: func() prep {
+				bFile, c1, err := setupFileWithHdrs(
+					headerfs.Block, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				tCP := chaincfg.SimNetParams
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+					ValidationFlags:   blockchain.BFFastAdd,
+				}
+				bS := opts.createBlockHeaderImportSrc()
+				bS.SetURI(bFile.Name())
+				err = bS.Open()
+				cleanup := func() {
+					bS.Close()
+					os.Remove(bS.GetURI())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				meta, err := bS.GetHeaderMetadata()
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				bIt := bS.Iterator(
+					0, meta.headersCount-1,
+					defaultWriteBatchSizePerRegion,
+				)
+
+				bV := opts.createBlockHeaderValidator(bS)
+
+				return prep{
+					iterator:  bIt,
+					validator: bV,
+					cleanup:   cleanup,
+				}
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+			err := prep.validator.Validate(ctx, prep.iterator)
+			if tc.expectErr {
+				require.ErrorContains(
+					t, err, tc.expectErrMsg,
+				)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+// TestHeaderValidationOnSequentialFilterHeaders tests the header validation
+// on sequential filter headers. It checks that the header is validated
+// correctly.
+func TestHeaderValidationOnSequentialFilterHeaders(t *testing.T) {
+	t.Parallel()
+	ctx := context.Background()
+	type prep struct {
+		iterator  HeaderIterator
+		validator HeadersValidator
+		cleanup   func()
+		err       error
+	}
+	testCases := []struct {
+		name         string
+		index        int
+		tCP          *chaincfg.Params
+		prep         func(chaincfg.Params) prep
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ErrorOnInvalidSequentialHeaders",
+			tCP:  &chaincfg.MainNetParams,
+			prep: func(tCP chaincfg.Params) prep {
+				fFile, c1, err := setupFileWithHdrs(
+					headerfs.RegularFilter, false,
+				)
+
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				fFile.Close()
+
+				// Add header metadata to the file. Deliberately
+				// make the start height one before the
+				// hardcoded checkpointed filter header
+				// (height=100000) to evaluate the filter header
+				// validation behavior
+				err = AddHeadersImportMetadata(
+					fFile.Name(), wire.MainNet, 0,
+					headerfs.RegularFilter, 99999,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				fFile, err = os.OpenFile(
+					fFile.Name(), os.O_RDWR, 0644,
+				)
+				c1 = func() {
+					fFile.Close()
+					os.Remove(fFile.Name())
+				}
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+				}
+				fS := opts.createFilterHeaderImportSrc()
+				fS.SetURI(fFile.Name())
+				err = fS.Open()
+				cleanup := func() {
+					fS.Close()
+					os.Remove(fS.GetURI())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				meta, err := fS.GetHeaderMetadata()
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				fIterator := fS.Iterator(
+					0, meta.headersCount-1,
+					uint32(opts.WriteBatchSizePerRegion),
+				)
+
+				fV := opts.createFilterHeaderValidator()
+
+				return prep{
+					iterator:  fIterator,
+					validator: fV,
+					cleanup:   cleanup,
+				}
+			},
+			expectErr: true,
+			expectErrMsg: "batch validation failed at position " +
+				"0: " + chainsync.ErrCheckpointMismatch.Error(),
+		},
+		{
+			name: "ValidSequentialHeaders",
+			tCP:  &chaincfg.SimNetParams,
+			prep: func(tCP chaincfg.Params) prep {
+				fFile, c1, err := setupFileWithHdrs(
+					headerfs.RegularFilter, true,
+				)
+				if err != nil {
+					return prep{
+						cleanup: c1,
+						err:     err,
+					}
+				}
+
+				opts := &ImportOptions{
+					TargetChainParams: tCP,
+				}
+				fS := opts.createFilterHeaderImportSrc()
+				fS.SetURI(fFile.Name())
+				err = fS.Open()
+				cleanup := func() {
+					fS.Close()
+					os.Remove(fS.GetURI())
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				meta, err := fS.GetHeaderMetadata()
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				fIterator := fS.Iterator(
+					0, meta.headersCount-1,
+					uint32(opts.WriteBatchSizePerRegion),
+				)
+
+				fV := opts.createFilterHeaderValidator()
+
+				return prep{
+					iterator:  fIterator,
+					validator: fV,
+					cleanup:   cleanup,
+				}
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep(*tc.tCP)
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+			err := prep.validator.Validate(ctx, prep.iterator)
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				return
+			}
+			require.NoError(t, err)
+		})
+	}
+}
+
+// TestHeaderProcessing tests the header processing. It checks that the header
+// processing is done correctly.
+//
+// The goal of these test cases is to determine the processing regions. For
+// convenience and shorter test case names, we'll use the following notation:
+// - A refers to data from import sources. No divergence.
+// - B refers to data from target sources when both target stores have the same
+// length. In case of divergence, B specifically refers to the target block
+// headers store, while F refers to the target filter headers store.
+func TestHeaderProcessing(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		hImport *headersImport
+		err     error
+	}
+	type verify struct {
+		tc                *testing.T
+		processingRegions *processingRegions
+	}
+	testCases := []struct {
+		name         string
+		prep         func() prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		// ❌ Error validation test cases.
+		{
+			name: "ErrorOnGetHeaderMetadataFailure",
+			prep: func() prep {
+				expectErr := errors.New("failed to get " +
+					"header metadata")
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(
+					nil, expectErr,
+				)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(60), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(60), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "failed to get header metadata",
+		},
+		{
+			name: "ErrorOnTargetBlockStoreChainTipFailure",
+			prep: func() prep {
+				expectErr := errors.New("failed to get block " +
+					"header store chain tip")
+
+				hM := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 50,
+					},
+					endHeight: 90,
+				}
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(hM, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(0),
+					expectErr,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(60), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "failed to get block header store " +
+				"chain tip",
+		},
+		{
+			name: "ErrorOnGetChainTipForTargetFilterStore",
+			prep: func() prep {
+				expectErr := errors.New("failed to get " +
+					"filter header store chain tip")
+
+				hM := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 50,
+					},
+					endHeight: 90,
+				}
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(hM, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(60), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(0), expectErr,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "failed to get filter header store " +
+				"chain tip",
+		},
+
+		// ✅ Non-divergent cases (B = F).
+
+		/*
+			A:     [=========]
+			B: [=======]
+			F: [=======]
+		*/
+		{
+			name: "AAndBOverlap_AEndsAfterB",
+			prep: func() prep {
+				hM := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 50,
+					},
+					endHeight: 90,
+				}
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(hM, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(60), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(60), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify: func(v verify) {
+				dR := v.processingRegions.divergence
+				require.False(v.tc, dR.exists)
+
+				oR := v.processingRegions.overlap
+				oRE := headerRegion{
+					start:  50,
+					end:    60,
+					exists: true,
+				}
+				require.Equal(v.tc, oRE, oR)
+
+				nHR := v.processingRegions.newHeaders
+				nHRE := headerRegion{
+					start:  61,
+					end:    90,
+					exists: true,
+				}
+				require.Equal(v.tc, nHRE, nHR)
+			},
+		},
+
+		/*
+			A:     [=====]
+			B: [===========]
+			F: [===========]
+		*/
+		{
+			name: "BCompletelyOverlapsA",
+			prep: func() prep {
+				hM := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 50,
+					},
+					endHeight: 70,
+				}
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(hM, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(90), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(90), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify: func(v verify) {
+				dR := v.processingRegions.divergence
+				require.False(v.tc, dR.exists)
+
+				nHR := v.processingRegions.newHeaders
+				require.False(v.tc, nHR.exists)
+
+				oR := v.processingRegions.overlap
+				oRE := headerRegion{
+					start:  50,
+					end:    70,
+					exists: true,
+				}
+				require.Equal(v.tc, oRE, oR)
+			},
+		},
+
+		/*
+			A:         [======]
+			B: [======]
+			F: [======]
+		*/
+		{
+			name: "AAndBDoNotOverlap_AComesAfterB",
+			prep: func() prep {
+				hM := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 51,
+					},
+					endHeight: 90,
+				}
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(hM, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(50), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(50), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify: func(v verify) {
+				dR := v.processingRegions.divergence
+				require.False(v.tc, dR.exists)
+
+				oR := v.processingRegions.overlap
+				require.False(v.tc, oR.exists)
+
+				nHR := v.processingRegions.newHeaders
+				nHRE := headerRegion{
+					start:  51,
+					end:    90,
+					exists: true,
+				}
+				require.Equal(v.tc, nHRE, nHR)
+			},
+		},
+
+		// ⚠️ Divergent cases (B ≠ F), B & F start at same pos.
+
+		/*
+			A: [===========]
+			B: [=====]
+			F: [===============]
+		*/
+		{
+			name: "Divergence_AFFullyOverlapsB",
+			prep: func() prep {
+				hM := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 0,
+					},
+					endHeight: 70,
+				}
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(hM, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(40), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(90), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify: func(v verify) {
+				nHR := v.processingRegions.newHeaders
+				require.False(v.tc, nHR.exists)
+
+				oR := v.processingRegions.overlap
+				oRE := headerRegion{
+					start:  0,
+					end:    40,
+					exists: true,
+				}
+				require.Equal(v.tc, oRE, oR)
+
+				dR := v.processingRegions.divergence
+				dRE := headerRegion{
+					start:  41,
+					end:    70,
+					exists: true,
+				}
+				require.Equal(v.tc, dRE, dR)
+			},
+		},
+
+		/*
+			A: [===============]
+			B: [=====]
+			F: [===========]
+		*/
+		{
+			name: "Divergence_AFullyOverlapsBF",
+			prep: func() prep {
+				hM := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 0,
+					},
+					endHeight: 90,
+				}
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(hM, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(40), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(70), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify: func(v verify) {
+				oR := v.processingRegions.overlap
+				oRE := headerRegion{
+					start:  0,
+					end:    40,
+					exists: true,
+				}
+				require.Equal(v.tc, oRE, oR)
+
+				dR := v.processingRegions.divergence
+				dRE := headerRegion{
+					start:  41,
+					end:    70,
+					exists: true,
+				}
+				require.Equal(v.tc, dRE, dR)
+
+				nHR := v.processingRegions.newHeaders
+				nHRE := headerRegion{
+					start:  71,
+					end:    90,
+					exists: true,
+				}
+				require.Equal(v.tc, nHRE, nHR)
+			},
+		},
+
+		/*
+			A: [===========]
+			B: [===============]
+			F: [=====]
+		*/
+		{
+			name: "Divergence_ABFullyOverlapsF",
+			prep: func() prep {
+				hM := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 0,
+					},
+					endHeight: 70,
+				}
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(hM, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(90), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(40), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify: func(v verify) {
+				nHR := v.processingRegions.newHeaders
+				require.False(v.tc, nHR.exists)
+
+				oR := v.processingRegions.overlap
+				oRE := headerRegion{
+					start:  0,
+					end:    40,
+					exists: true,
+				}
+				require.Equal(v.tc, oRE, oR)
+
+				dR := v.processingRegions.divergence
+				dRE := headerRegion{
+					start:  41,
+					end:    70,
+					exists: true,
+				}
+				require.Equal(v.tc, dRE, dR)
+			},
+		},
+
+		/*
+			A:     [=======]
+			B: [===========]
+			F: [=========]
+		*/
+		{
+			name: "Divergence_BAndFCompletelyContainA",
+			prep: func() prep {
+				hM := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 40,
+					},
+					endHeight: 90,
+				}
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(hM, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(90), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(70), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify: func(v verify) {
+				nHR := v.processingRegions.newHeaders
+				require.False(v.tc, nHR.exists)
+
+				oR := v.processingRegions.overlap
+				oRE := headerRegion{
+					start:  40,
+					end:    70,
+					exists: true,
+				}
+				require.Equal(v.tc, oRE, oR)
+
+				dR := v.processingRegions.divergence
+				dRE := headerRegion{
+					start:  71,
+					end:    90,
+					exists: true,
+				}
+				require.Equal(v.tc, dRE, dR)
+			},
+		},
+
+		/*
+			A:     [===========]
+			B: [=====]
+			F: [======]
+		*/
+		{
+			name: "Divergence_AEndsAfterBothBAndF",
+			prep: func() prep {
+				hM := &headerMetadata{
+					importMetadata: &importMetadata{
+						startHeight: 30,
+					},
+					endHeight: 90,
+				}
+
+				hIS := &mockHeaderImportSource{}
+				hIS.On("GetHeaderMetadata").Return(hM, nil)
+
+				bHS := &headerfs.MockBlockHeaderStore{}
+				bHS.On("ChainTip").Return(
+					&wire.BlockHeader{}, uint32(40), nil,
+				)
+
+				fHS := &headerfs.MockFilterHeaderStore{}
+				fHS.On("ChainTip").Return(
+					&chainhash.Hash{}, uint32(50), nil,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  bHS,
+					TargetFilterHeaderStore: fHS,
+				}
+
+				h := &headersImport{
+					blockHeadersImportSource: hIS,
+					options:                  ops,
+				}
+
+				return prep{
+					hImport: h,
+				}
+			},
+			verify: func(v verify) {
+				oR := v.processingRegions.overlap
+				oRE := headerRegion{
+					start:  30,
+					end:    40,
+					exists: true,
+				}
+				require.Equal(v.tc, oRE, oR)
+
+				dR := v.processingRegions.divergence
+				dRE := headerRegion{
+					start:  41,
+					end:    50,
+					exists: true,
+				}
+				require.Equal(v.tc, dRE, dR)
+
+				nHR := v.processingRegions.newHeaders
+				nHRE := headerRegion{
+					start:  51,
+					end:    90,
+					exists: true,
+				}
+				require.Equal(v.tc, nHRE, nHR)
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			require.NoError(t, prep.err)
+			imprt := prep.hImport
+			regs, err := imprt.determineProcessingRegions()
+			verify := verify{
+				tc:                t,
+				processingRegions: regs,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// TestHeaderStorage tests the header storage to the target header stores. It
+// checks that the headers are written correctly to the target header stores.
+func TestHeaderStorage(t *testing.T) {
+	t.Parallel()
+	type prep struct {
+		hImport       *headersImport
+		blockHeaders  []headerfs.BlockHeader
+		filterHeaders []headerfs.FilterHeader
+		cleanup       func()
+		err           error
+	}
+	type verify struct {
+		tc      *testing.T
+		hImport *headersImport
+	}
+	testCases := []struct {
+		name         string
+		prep         func() prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "ErrorOnBlockHeadersWrite",
+			prep: func() prep {
+				b := &headerfs.MockBlockHeaderStore{}
+				args := mock.Anything
+				b.On("WriteHeaders", args).Return(
+					errors.New("I/O write error"),
+				)
+
+				f := &headerfs.MockFilterHeaderStore{}
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  b,
+					TargetFilterHeaderStore: f,
+				}
+
+				headersImport := &headersImport{
+					options: ops,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+				}
+			},
+			verify:    func(v verify) {},
+			expectErr: true,
+			expectErrMsg: fmt.Sprintf("failed to write block "+
+				"headers batch 0-%d: I/O write error",
+				len(blockHdrs)-1),
+		},
+		{
+			name: "RollbackBlockHdrsAfterFilterHdrsFailure",
+			prep: func() prep {
+				tempDir := t.TempDir()
+				c1 := func() {
+					os.RemoveAll(tempDir)
+				}
+
+				dbPath := filepath.Join(tempDir, "test.db")
+				db, err := walletdb.Create(
+					"bdb", dbPath, true, time.Second*10,
+				)
+				cleanup := func() {
+					db.Close()
+					c1()
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				b, err := headerfs.NewBlockHeaderStore(
+					tempDir, db, &chaincfg.SimNetParams,
+				)
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				f := &headerfs.MockFilterHeaderStore{}
+				args := mock.Anything
+				f.On("WriteHeaders", args).Return(
+					errors.New("I/O write error"),
+				)
+
+				// Prep block headers to write to the target
+				// headers store. Ignore the genesis block
+				// header since it was already written when
+				// creating the block header store.
+				nBHs := len(blockHdrs)
+				blkHdrsToWrite := make(
+					[]headerfs.BlockHeader, nBHs-1,
+				)
+				for i := 1; i < nBHs; i++ {
+					blockHdr := blockHdrs[i]
+					h, err := constructBlkHdr(
+						blockHdr, uint32(i),
+					)
+					res := prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+					if err != nil {
+						return res
+					}
+					bHValue := h.BlockHeader
+					blkHdrsToWrite[i-1] = bHValue
+				}
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  b,
+					TargetFilterHeaderStore: f,
+				}
+
+				headersImport := &headersImport{
+					options: ops,
+				}
+
+				return prep{
+					hImport:       headersImport,
+					blockHeaders:  blkHdrsToWrite,
+					filterHeaders: nil,
+					cleanup:       cleanup,
+				}
+			},
+			verify: func(v verify) {
+				options := v.hImport.options
+				tBS := options.TargetBlockHeaderStore
+				chainTipB, height, err := tBS.ChainTip()
+				require.NoError(v.tc, err)
+
+				// Since we have wrote 4 headers and those
+				// rolledback on filter headers write failure,
+				// we can expect the chain tip height to be 0.
+				require.Equal(v.tc, uint32(0), height)
+
+				// Assert that the known block header at this
+				// index matches the retrieved one.
+				chainTipBEx, err := constructBlkHdr(
+					blockHdrs[0], uint32(0),
+				)
+				require.NoError(v.tc, err)
+				b := chainTipBEx.BlockHeader.BlockHeader
+				require.Equal(v.tc, b, chainTipB)
+			},
+			expectErr: true,
+			expectErrMsg: fmt.Sprintf("failed to write filter "+
+				"headers batch 0-%d: I/O write error",
+				len(blockHdrs)-1),
+		},
+		{
+			name: "ErrorOnBlockHeadersRollback",
+			prep: func() prep {
+				b := &headerfs.MockBlockHeaderStore{}
+				a := mock.Anything
+				b.On("WriteHeaders", a).Return(nil)
+				b.On("RollbackBlockHeaders", a).Return(
+					nil,
+					errors.New("I/O blocks rollback error"),
+				)
+
+				f := &headerfs.MockFilterHeaderStore{}
+				f.On("WriteHeaders", a).Return(
+					errors.New("I/O write err"),
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  b,
+					TargetFilterHeaderStore: f,
+				}
+
+				headersImport := &headersImport{
+					options: ops,
+				}
+
+				return prep{
+					hImport: headersImport,
+					cleanup: func() {},
+				}
+			},
+			verify:    func(v verify) {},
+			expectErr: true,
+			expectErrMsg: "failed to rollback 0 headers from " +
+				"target block header store after filter " +
+				"headers write failure. Block error: I/O " +
+				"blocks rollback error",
+		},
+		{
+			name: "NoErrorOnEmptyBlockAndFilterHeaders",
+			prep: func() prep {
+				b := &headerfs.MockBlockHeaderStore{}
+				args := mock.Anything
+				b.On("WriteHeaders", args).Return(nil)
+
+				f := &headerfs.MockFilterHeaderStore{}
+				f.On("WriteHeaders", args).Return(nil)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  b,
+					TargetFilterHeaderStore: f,
+				}
+
+				headersImport := &headersImport{
+					options: ops,
+				}
+
+				return prep{
+					hImport:       headersImport,
+					blockHeaders:  nil,
+					filterHeaders: nil,
+					cleanup:       func() {},
+				}
+			},
+			verify: func(v verify) {},
+		},
+		{
+			name: "StoreBothBlockAndFilterHeaders",
+			prep: func() prep {
+				tempDir := t.TempDir()
+				c1 := func() {
+					os.RemoveAll(tempDir)
+				}
+
+				dbPath := filepath.Join(tempDir, "test.db")
+				db, err := walletdb.Create(
+					"bdb", dbPath, true, time.Second*10,
+				)
+				cleanup := func() {
+					db.Close()
+					c1()
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				b, err := headerfs.NewBlockHeaderStore(
+					tempDir, db, &chaincfg.SimNetParams,
+				)
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				f, err := headerfs.NewFilterHeaderStore(
+					tempDir, db, headerfs.RegularFilter,
+					&chaincfg.SimNetParams, nil,
+				)
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				// Prep block headers to write to the target
+				// headers store. Ignore the genesis block
+				// header since it was already written when
+				// creating the block header store.
+				nBHs := len(blockHdrs)
+				blkHdrsToWrite := make(
+					[]headerfs.BlockHeader,
+					nBHs-1,
+				)
+				for i := 1; i < nBHs; i++ {
+					blockHdr := blockHdrs[i]
+					h, err := constructBlkHdr(
+						blockHdr, uint32(i),
+					)
+					res := prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+					if err != nil {
+						return res
+					}
+					bHValue := h.BlockHeader
+					blkHdrsToWrite[i-1] = bHValue
+				}
+
+				// Prep filter headers to write to the target
+				// headers store. Ignore the genesis filter
+				// header since it was already written when
+				// creating the filter header store.
+				nFHs := len(filterHdrs)
+				filtHdrsToWrite := make(
+					[]headerfs.FilterHeader, nFHs-1,
+				)
+				for i := 1; i < nFHs; i++ {
+					filterHdr := filterHdrs[i]
+					h, err := constructFilterHdr(
+						filterHdr, uint32(i),
+					)
+					res := prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+					if err != nil {
+						return res
+					}
+					fH := h.FilterHeader
+					filtHdrsToWrite[i-1] = fH
+				}
+
+				setLastFilterHeaderHash(
+					filtHdrsToWrite, blkHdrsToWrite,
+				)
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  b,
+					TargetFilterHeaderStore: f,
+				}
+
+				headersImport := &headersImport{
+					options: ops,
+				}
+
+				return prep{
+					hImport:       headersImport,
+					blockHeaders:  blkHdrsToWrite,
+					filterHeaders: filtHdrsToWrite,
+					cleanup:       cleanup,
+				}
+			},
+			verify: func(v verify) {
+				options := v.hImport.options
+				tBS := options.TargetBlockHeaderStore
+				chainTipB, height, err := tBS.ChainTip()
+				require.NoError(v.tc, err)
+
+				nBHs := len(blockHdrs)
+				require.Equal(v.tc, uint32(nBHs-1), height)
+
+				// Assert that the known block header at this
+				// index matches the retrieved one.
+				chainTipBEx, err := constructBlkHdr(
+					blockHdrs[nBHs-1], uint32(nBHs-1),
+				)
+				require.NoError(v.tc, err)
+				b := chainTipBEx.BlockHeader.BlockHeader
+				require.Equal(v.tc, b, chainTipB)
+
+				tFS := options.TargetFilterHeaderStore
+				chainTipF, height, err := tFS.ChainTip()
+				require.NoError(v.tc, err)
+
+				nFHs := len(filterHdrs)
+				require.Equal(v.tc, uint32(nFHs-1), height)
+
+				// Assert that the known filter header at this
+				// index matches the retrieved one.
+				chainTipFEx, err := constructFilterHdr(
+					filterHdrs[nFHs-1], uint32(nFHs-1),
+				)
+				require.NoError(v.tc, err)
+				f := &chainTipFEx.FilterHash
+				require.Equal(v.tc, f, chainTipF)
+			},
+		},
+		{
+			name: "StoreBlockHeadersOnly",
+			prep: func() prep {
+				tempDir := t.TempDir()
+				c1 := func() {
+					os.RemoveAll(tempDir)
+				}
+
+				dbPath := filepath.Join(tempDir, "test.db")
+				db, err := walletdb.Create(
+					"bdb", dbPath, true, time.Second*10,
+				)
+				cleanup := func() {
+					db.Close()
+					c1()
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				b, err := headerfs.NewBlockHeaderStore(
+					tempDir, db, &chaincfg.SimNetParams,
+				)
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				f := &headerfs.MockFilterHeaderStore{}
+				args := mock.Anything
+				f.On("WriteHeaders", args).Return(nil)
+
+				// Prep block headers to write to the target
+				// headers store. Ignore the genesis block
+				// header since it was already written when
+				// creating the block header store.
+				nBHs := len(blockHdrs)
+				blkHdrsToWrite := make(
+					[]headerfs.BlockHeader, nBHs-1,
+				)
+				for i := 1; i < nBHs; i++ {
+					blockHdr := blockHdrs[i]
+					h, err := constructBlkHdr(
+						blockHdr, uint32(i),
+					)
+					res := prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+					if err != nil {
+						return res
+					}
+					bHValue := h.BlockHeader
+					blkHdrsToWrite[i-1] = bHValue
+				}
+
+				ops := &ImportOptions{
+					TargetBlockHeaderStore:  b,
+					TargetFilterHeaderStore: f,
+				}
+
+				headersImport := &headersImport{
+					options: ops,
+				}
+
+				return prep{
+					hImport:       headersImport,
+					blockHeaders:  blkHdrsToWrite,
+					filterHeaders: nil,
+					cleanup:       cleanup,
+				}
+			},
+			verify: func(v verify) {
+				options := v.hImport.options
+				tBS := options.TargetBlockHeaderStore
+				chainTipB, height, err := tBS.ChainTip()
+				require.NoError(v.tc, err)
+
+				nBHs := len(blockHdrs)
+				require.Equal(v.tc, uint32(nBHs-1), height)
+
+				// Assert that the known block header at this
+				// index matches the retrieved one.
+				chainTipBEx, err := constructBlkHdr(
+					blockHdrs[nBHs-1], uint32(nBHs-1),
+				)
+				require.NoError(v.tc, err)
+				b := chainTipBEx.BlockHeader.BlockHeader
+				require.Equal(v.tc, b, chainTipB)
+			},
+		},
+		{
+			name: "StoreFilterHeadersOnly",
+			prep: func() prep {
+				tempDir := t.TempDir()
+				c1 := func() {
+					os.RemoveAll(tempDir)
+				}
+
+				dbPath := filepath.Join(tempDir, "test.db")
+				db, err := walletdb.Create(
+					"bdb", dbPath, true, time.Second*10,
+				)
+				cleanup := func() {
+					db.Close()
+					c1()
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				f, err := headerfs.NewFilterHeaderStore(
+					tempDir, db, headerfs.RegularFilter,
+					&chaincfg.SimNetParams, nil,
+				)
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				b := &headerfs.MockBlockHeaderStore{}
+				args := mock.Anything
+				b.On("WriteHeaders", args).Return(nil)
+
+				// Prep filter headers to write to the target
+				// headers store. Ignore the genesis filter
+				// header since it was already written when
+				// creating the filter header store.
+				nFHs := len(filterHdrs)
+				filtHdrsToWrite := make(
+					[]headerfs.FilterHeader, nFHs-1,
+				)
+				for i := 1; i < nFHs; i++ {
+					filterHdr := filterHdrs[i]
+					h, err := constructFilterHdr(
+						filterHdr, uint32(i),
+					)
+					res := prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+					if err != nil {
+						return res
+					}
+					fH := h.FilterHeader
+					filtHdrsToWrite[i-1] = fH
+				}
+
+				lbH, err := constructBlkHdr(
+					blockHdrs[len(blockHdrs)-1],
+					uint32(len(blockHdrs)-1),
+				)
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+				bH := lbH.BlockHash()
+				filtHdrsToWrite[nFHs-2].HeaderHash = bH
+
+				ops := &ImportOptions{
+					TargetFilterHeaderStore: f,
+					TargetBlockHeaderStore:  b,
+				}
+
+				headersImport := &headersImport{
+					options: ops,
+				}
+
+				return prep{
+					hImport:       headersImport,
+					blockHeaders:  nil,
+					filterHeaders: filtHdrsToWrite,
+					cleanup:       cleanup,
+				}
+			},
+			verify: func(v verify) {
+				options := v.hImport.options
+				tFS := options.TargetFilterHeaderStore
+				_, _, err := tFS.ChainTip()
+				expectErrMsg := "target height not found in " +
+					"index"
+				require.ErrorContains(v.tc, err, expectErrMsg)
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+			err := prep.hImport.writeHeadersToTargetStores(
+				prep.blockHeaders, prep.filterHeaders,
+				0, uint32(len(blockHdrs)-1),
+			)
+			verify := verify{
+				tc:      t,
+				hImport: prep.hImport,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// TestHeaderStorageOnNewHeadersRegion tests the header storage on the new
+// headers region. It checks that the headers are written correctly to the
+// target header stores.
+func TestHeaderStorageOnNewHeadersRegion(t *testing.T) {
+	t.Parallel()
+	ctx := context.Background()
+	type prep struct {
+		hImport *headersImport
+		cleanup func()
+		err     error
+	}
+	type verify struct {
+		tc            *testing.T
+		importOptions *ImportOptions
+		importResult  *ImportResult
+	}
+	testCases := []struct {
+		name         string
+		region       headerRegion
+		importResult *ImportResult
+		prep         func() prep
+		verify       func(verify)
+		expectErr    bool
+		expectErrMsg string
+	}{
+		{
+			name: "NoErrorOnNonExistentRegion",
+			region: headerRegion{
+				start:  1000,
+				end:    2000,
+				exists: false,
+			},
+			importResult: &ImportResult{},
+			prep: func() prep {
+				return prep{
+					hImport: &headersImport{},
+					cleanup: func() {},
+				}
+			},
+			verify: func(verify) {},
+		},
+		{
+			name: "ErrorOnGetHeaderMetadata",
+			region: headerRegion{
+				start:  1,
+				end:    100,
+				exists: true,
+			},
+			importResult: &ImportResult{},
+			prep: func() prep {
+				// Mock GetHeaderMetadata.
+				bIS := &mockHeaderImportSource{}
+				bIS.On("GetHeaderMetadata").Return(
+					nil, errors.New("I/O read error"),
+				)
+				hImport := &headersImport{
+					blockHeadersImportSource: bIS,
+				}
+				return prep{
+					hImport: hImport,
+					cleanup: func() {},
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "I/O read error",
+		},
+		{
+			name: "ErrorOnGetBlockHeader",
+			region: headerRegion{
+				start:  1,
+				end:    100,
+				exists: true,
+			},
+			importResult: &ImportResult{},
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+
+				bIt := &mockHeaderIterator{}
+
+				in := mock.Anything
+				bIt.On("ReadBatch", in, in, in).Return(
+					nil, errors.New("I/O read error"),
+				)
+				bIt.On("GetEndIndex").Return(uint32(100))
+				bIt.On("GetBatchSize").Return(uint32(100))
+				bIS.On("Iterator", in, in, in).Return(bIt)
+
+				fIS := &mockHeaderImportSource{}
+
+				fIt := &mockHeaderIterator{}
+				fIt.On("ReadBatch", in, in, in).Return(nil, nil)
+				fIt.On("GetEndIndex").Return(uint32(100))
+				fIt.On("GetBatchSize").Return(uint32(100))
+				fIS.On("Iterator", in, in, in).Return(fIt)
+
+				ops := &ImportOptions{
+					WriteBatchSizePerRegion: 100,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hImport: hImport,
+					cleanup: func() {},
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "failed to read block headers batch at " +
+				"height 1: I/O read error",
+		},
+		{
+			name: "ErrorOnTypeAssertingBlockHeader",
+			region: headerRegion{
+				start:  1,
+				end:    100,
+				exists: true,
+			},
+			importResult: &ImportResult{},
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+
+				bIt := &mockHeaderIterator{}
+
+				in := mock.Anything
+				bIt.On("ReadBatch", in, in, in).Return(
+					[]Header{
+						newFilterHeader(),
+					}, nil,
+				)
+				bIt.On("GetEndIndex").Return(uint32(100))
+				bIt.On("GetBatchSize").Return(uint32(100))
+				bIS.On("Iterator", in, in, in).Return(bIt)
+
+				fIS := &mockHeaderImportSource{}
+
+				fIt := &mockHeaderIterator{}
+				fIt.On("ReadBatch", in, in, in).Return(nil, nil)
+				fIt.On("GetEndIndex").Return(uint32(100))
+				fIt.On("GetBatchSize").Return(uint32(100))
+				fIS.On("Iterator", in, in, in).Return(fIt)
+
+				ops := &ImportOptions{
+					WriteBatchSizePerRegion: 100,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hImport: hImport,
+					cleanup: func() {},
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "expected blockHeader type, got " +
+				"*chainimport.filterHeader",
+		},
+		{
+			name: "ErrorOnGetFilterHeader",
+			region: headerRegion{
+				start:  1,
+				end:    100,
+				exists: true,
+			},
+			importResult: &ImportResult{},
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+
+				bIt := &mockHeaderIterator{}
+
+				in := mock.Anything
+				bIt.On("ReadBatch", in, in, in).Return(
+					[]Header{}, nil,
+				)
+				bIt.On("GetEndIndex").Return(uint32(100))
+				bIt.On("GetBatchSize").Return(uint32(100))
+				bIS.On("Iterator", in, in, in).Return(bIt)
+
+				fIS := &mockHeaderImportSource{}
+
+				fIt := &mockHeaderIterator{}
+
+				fIt.On("ReadBatch", in, in, in).Return(
+					nil, errors.New("I/O read error"),
+				)
+				fIt.On("GetEndIndex").Return(uint32(100))
+				fIt.On("GetBatchSize").Return(uint32(100))
+				fIS.On("Iterator", in, in, in).Return(fIt)
+
+				ops := &ImportOptions{
+					WriteBatchSizePerRegion: 100,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hImport: hImport,
+					cleanup: func() {},
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "failed to read filter headers batch " +
+				"at height 1: I/O read error",
+		},
+		{
+			name: "ErrorOnTypeAssertingFilterHeader",
+			region: headerRegion{
+				start:  1,
+				end:    100,
+				exists: true,
+			},
+			importResult: &ImportResult{},
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+
+				bIt := &mockHeaderIterator{}
+
+				in := mock.Anything
+				bIt.On("ReadBatch", in, in, in).Return(
+					[]Header{}, nil,
+				)
+				bIt.On("GetEndIndex").Return(uint32(100))
+				bIt.On("GetBatchSize").Return(uint32(100))
+				bIS.On("Iterator", in, in, in).Return(bIt)
+
+				fIS := &mockHeaderImportSource{}
+
+				fIt := &mockHeaderIterator{}
+				fIt.On("ReadBatch", in, in, in).Return(
+					[]Header{
+						newBlockHeader(),
+					}, nil,
+				)
+				fIt.On("GetEndIndex").Return(uint32(100))
+				fIt.On("GetBatchSize").Return(uint32(100))
+				fIS.On("Iterator", in, in, in).Return(fIt)
+
+				ops := &ImportOptions{
+					WriteBatchSizePerRegion: 100,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hImport: hImport,
+					cleanup: func() {},
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "expected filterHeader type, got " +
+				"*chainimport.blockHeader",
+		},
+		{
+			name: "ErrorOnHeadersLengthMismatch",
+			region: headerRegion{
+				start:  1,
+				end:    100,
+				exists: true,
+			},
+			importResult: &ImportResult{},
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+
+				bIt := &mockHeaderIterator{}
+
+				in := mock.Anything
+				bIt.On("ReadBatch", in, in, in).Return(
+					[]Header{}, nil,
+				)
+				bIt.On("GetEndIndex").Return(uint32(100))
+				bIt.On("GetBatchSize").Return(uint32(100))
+				bIS.On("Iterator", in, in, in).Return(bIt)
+
+				fIS := &mockHeaderImportSource{}
+
+				fIt := &mockHeaderIterator{}
+				fIt.On("ReadBatch", in, in, in).Return(
+					[]Header{
+						newFilterHeader(),
+					}, nil,
+				)
+				fIt.On("GetEndIndex").Return(uint32(100))
+				fIt.On("GetBatchSize").Return(uint32(100))
+				fIS.On("Iterator", in, in, in).Return(fIt)
+
+				ops := &ImportOptions{
+					WriteBatchSizePerRegion: 100,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hImport: hImport,
+					cleanup: func() {},
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "mismatch between block headers (0) " +
+				"and filter headers (1)",
+		},
+		{
+			name: "ErrorOnNoHeadersRead",
+			region: headerRegion{
+				start:  1,
+				end:    100,
+				exists: true,
+			},
+			importResult: &ImportResult{},
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+
+				bIt := &mockHeaderIterator{}
+				in := mock.Anything
+				bIt.On("ReadBatch", in, in, in).Return(
+					[]Header{}, nil,
+				)
+				bIt.On("GetEndIndex").Return(uint32(100))
+				bIt.On("GetBatchSize").Return(uint32(100))
+				bIS.On("Iterator", in, in, in).Return(bIt)
+
+				fIS := &mockHeaderImportSource{}
+
+				fIt := &mockHeaderIterator{}
+				fIt.On("ReadBatch", in, in, in).Return(
+					[]Header{}, nil,
+				)
+				fIt.On("GetEndIndex").Return(uint32(100))
+				fIt.On("GetBatchSize").Return(uint32(100))
+				fIS.On("Iterator", in, in, in).Return(fIt)
+
+				ops := &ImportOptions{
+					WriteBatchSizePerRegion: 100,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hImport: hImport,
+					cleanup: func() {},
+				}
+			},
+			verify:       func(verify) {},
+			expectErr:    true,
+			expectErrMsg: "no headers read",
+		},
+		{
+			name: "ErrorOnWriteHeadersToTargetStores",
+			region: headerRegion{
+				start:  1,
+				end:    100,
+				exists: true,
+			},
+			importResult: &ImportResult{},
+			prep: func() prep {
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+
+				bIt := &mockHeaderIterator{}
+
+				in := mock.Anything
+				bIt.On("ReadBatch", in, in, in).Return(
+					[]Header{
+						newBlockHeader(),
+					}, nil,
+				)
+				bIt.On("GetEndIndex").Return(uint32(100))
+				bIt.On("GetBatchSize").Return(uint32(100))
+				bIS.On("Iterator", in, in, in).Return(bIt)
+
+				fIS := &mockHeaderImportSource{}
+
+				fIt := &mockHeaderIterator{}
+
+				fIt.On("ReadBatch", in, in, in).Return(
+					[]Header{
+						newFilterHeader(),
+					}, nil,
+				)
+				fIt.On("GetEndIndex").Return(uint32(100))
+				fIt.On("GetBatchSize").Return(uint32(100))
+				fIS.On("Iterator", in, in, in).Return(fIt)
+
+				b := &headerfs.MockBlockHeaderStore{}
+				in = mock.Anything
+				b.On("WriteHeaders", in).Return(
+					errors.New("I/O write error"),
+				)
+
+				ops := &ImportOptions{
+					WriteBatchSizePerRegion: 100,
+					TargetBlockHeaderStore:  b,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hImport: hImport,
+					cleanup: func() {},
+				}
+			},
+			verify:    func(verify) {},
+			expectErr: true,
+			expectErrMsg: "failed to write headers to target " +
+				"stores",
+		},
+		{
+			name: "ProcessNewHeadersRegionSuccessfully",
+			region: headerRegion{
+				start:  1,
+				end:    4,
+				exists: true,
+			},
+			importResult: &ImportResult{},
+			prep: func() prep {
+				tempDir := t.TempDir()
+				c1 := func() {
+					os.RemoveAll(tempDir)
+				}
+
+				dbPath := filepath.Join(tempDir, "test.db")
+				db, err := walletdb.Create(
+					"bdb", dbPath, true, time.Second*10,
+				)
+				cleanup := func() {
+					db.Close()
+					c1()
+				}
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				b, err := headerfs.NewBlockHeaderStore(
+					tempDir, db, &chaincfg.SimNetParams,
+				)
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				f, err := headerfs.NewFilterHeaderStore(
+					tempDir, db, headerfs.RegularFilter,
+					&chaincfg.SimNetParams, nil,
+				)
+				if err != nil {
+					return prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+				}
+
+				bIS := &mockHeaderImportSource{}
+				importMetadata := &importMetadata{}
+				bIS.On("GetHeaderMetadata").Return(
+					&headerMetadata{
+						importMetadata: importMetadata,
+					}, nil,
+				)
+
+				bIt := &mockHeaderIterator{}
+
+				// Prep block headers to write to the target
+				// headers store. Ignore the genesis block
+				// header since it was already written when
+				// creating the block header store.
+				nBHs := len(blockHdrs)
+				blkHdrsToWrite := make([]Header, nBHs-1)
+				for i := 1; i < nBHs; i++ {
+					blockHdr := blockHdrs[i]
+					h, err := constructBlkHdr(
+						blockHdr, uint32(i),
+					)
+					res := prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+					if err != nil {
+						return res
+					}
+					blkHdrsToWrite[i-1] = h
+				}
+
+				in := mock.Anything
+				bIt.On("ReadBatch", in, in, in).Return(
+					blkHdrsToWrite, nil,
+				).Once()
+				bIt.On("ReadBatch", in, in, in).Return(
+					nil, io.EOF,
+				)
+				bIt.On("GetEndIndex").Return(uint32(4))
+				bIt.On("GetBatchSize").Return(uint32(128))
+				bIS.On("Iterator", in, in, in).Return(bIt)
+
+				fIS := &mockHeaderImportSource{}
+
+				fIt := &mockHeaderIterator{}
+
+				// Prep filter headers to write to the target
+				// headers store. Ignore the genesis filter
+				// header since it was already written when
+				// creating the filter header store.
+				nFHs := len(filterHdrs)
+				filtHdrsToWrite := make([]Header, nFHs-1)
+				for i := 1; i < nFHs; i++ {
+					filterHdr := filterHdrs[i]
+					h, err := constructFilterHdr(
+						filterHdr, uint32(i),
+					)
+					res := prep{
+						cleanup: cleanup,
+						err:     err,
+					}
+					if err != nil {
+						return res
+					}
+					filtHdrsToWrite[i-1] = h
+				}
+
+				fIt.On("ReadBatch", in, in, in).Return(
+					filtHdrsToWrite, nil,
+				).Once()
+				fIt.On("ReadBatch", in, in, in).Return(
+					nil, io.EOF,
+				)
+				fIt.On("GetEndIndex").Return(uint32(4))
+				fIt.On("GetBatchSize").Return(uint32(128))
+				fIS.On("Iterator", in, in, in).Return(fIt)
+
+				ops := &ImportOptions{
+					WriteBatchSizePerRegion: 128,
+					TargetBlockHeaderStore:  b,
+					TargetFilterHeaderStore: f,
+				}
+
+				hImport := &headersImport{
+					blockHeadersImportSource:  bIS,
+					filterHeadersImportSource: fIS,
+					options:                   ops,
+				}
+
+				return prep{
+					hImport: hImport,
+					cleanup: cleanup,
+				}
+			},
+			verify: func(v verify) {
+				// Assert that added/processed headers count
+				// equal the prep test data ignoring the genesis
+				// header.
+				require.Equal(
+					v.tc, len(blockHdrs)-1,
+					v.importResult.AddedCount,
+				)
+				require.Equal(
+					v.tc, len(blockHdrs)-1,
+					v.importResult.ProcessedCount,
+				)
+
+				// Ensure no headers are skipped in the new
+				// headers region.
+				require.Equal(
+					v.tc, 0, v.importResult.SkippedCount,
+				)
+
+				// Verify those added align with the data
+				// inserted in the target stores.
+				ops := v.importOptions
+				tBS := ops.TargetBlockHeaderStore
+				chainTipB, height, err := tBS.ChainTip()
+				require.NoError(v.tc, err)
+				require.Equal(
+					v.tc, uint32(len(blockHdrs)-1), height,
+				)
+
+				// Assert that the known block header at this
+				// index matches the retrieved one.
+				chainTipBEx, err := constructBlkHdr(
+					blockHdrs[len(blockHdrs)-1],
+					uint32(len(blockHdrs)-1),
+				)
+				require.NoError(v.tc, err)
+				b := chainTipBEx.BlockHeader.BlockHeader
+				require.Equal(v.tc, b, chainTipB)
+			},
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			prep := tc.prep()
+			t.Cleanup(prep.cleanup)
+			require.NoError(t, prep.err)
+			err := prep.hImport.processNewHeadersRegion(
+				ctx, tc.region, tc.importResult,
+			)
+			verify := verify{
+				tc:            t,
+				importOptions: prep.hImport.options,
+				importResult:  tc.importResult,
+			}
+			if tc.expectErr {
+				require.ErrorContains(t, err, tc.expectErrMsg)
+				tc.verify(verify)
+				return
+			}
+			require.NoError(t, err)
+			tc.verify(verify)
+		})
+	}
+}
+
+// setupFileWithHdrs creates a temporary file with headers and returns the file,
+// a cleanup function, and an error if any.
+func setupFileWithHdrs(hT headerfs.HeaderType,
+	includeMetadata bool) (headerfs.File, func(), error) {
+
+	fileName := fmt.Sprintf("test-%s-*", hT)
+	tempFile, err := os.CreateTemp("", fileName)
+	cleanup := func() {
+		tempFile.Close()
+		os.Remove(tempFile.Name())
+	}
+	if err != nil {
+		return nil, cleanup, err
+	}
+
+	var hdrsData []string
+
+	switch hT {
+	case headerfs.Block:
+		hdrsData = blockHdrs
+	case headerfs.RegularFilter:
+		hdrsData = filterHdrs
+	default:
+		return nil, cleanup, fmt.Errorf("%s", hT)
+	}
+
+	if includeMetadata {
+		err = AddHeadersImportMetadata(
+			tempFile.Name(), wire.SimNet, 0, hT, 0,
+		)
+		if err != nil {
+			return nil, cleanup, err
+		}
+
+		// We need to reopen it again to update the file descriptor
+		// since previous operation closes the file atomically.
+		tempFile, err = os.OpenFile(
+			tempFile.Name(), os.O_RDWR|os.O_APPEND, 0644,
+		)
+		if err != nil {
+			return nil, cleanup, fmt.Errorf("failed to open file "+
+				"for writing headers: %w", err)
+		}
+	}
+
+	for _, hdrHex := range hdrsData {
+		hdrBytes, err := hex.DecodeString(hdrHex)
+		if err != nil {
+			return nil, cleanup, fmt.Errorf("failed to decode "+
+				"header hex: %w", err)
+		}
+
+		_, err = tempFile.Write(hdrBytes)
+		if err != nil {
+			return nil, cleanup, fmt.Errorf("failed to write "+
+				"header data: %w", err)
+		}
+	}
+
+	if err := tempFile.Sync(); err != nil {
+		return nil, cleanup, fmt.Errorf("failed to sync file: %w", err)
+	}
+
+	if _, err := tempFile.Seek(0, 0); err != nil {
+		return nil, cleanup, fmt.Errorf("failed to reset file "+
+			"position: %w", err)
+	}
+
+	return tempFile, cleanup, nil
+}
+
+// constructBlkHdr constructs a block header from a hex string and height.
+func constructBlkHdr(blockHeaderHex string,
+	height uint32) (*blockHeader, error) {
+
+	buff, err := hex.DecodeString(blockHeaderHex)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decode block header hex: %v",
+			err)
+	}
+	reader := bytes.NewReader(buff)
+	bHExpected := newBlockHeader()
+	bHExpected.Deserialize(reader, height)
+
+	bH, err := assertBlockHeader(bHExpected)
+	if err != nil {
+		return nil, err
+	}
+
+	return bH, nil
+}
+
+// constructFilterHdr constructs a filter header from a hex string and height.
+func constructFilterHdr(filterHeaderHex string,
+	height uint32) (*filterHeader, error) {
+
+	buff, err := hex.DecodeString(filterHeaderHex)
+	if err != nil {
+		return nil, fmt.Errorf("failed to decode filter header hex: %v",
+			err)
+	}
+	reader := bytes.NewReader(buff)
+	fHExpected := newFilterHeader()
+	fHExpected.Deserialize(reader, height)
+
+	fH, err := assertFilterHeader(fHExpected)
+	if err != nil {
+		return nil, err
+	}
+
+	return fH, nil
+}
diff --git a/chainsync/filtercontrol_test.go b/chainsync/filtercontrol_test.go
index 55f7efb..f10fee3 100644
--- a/chainsync/filtercontrol_test.go
+++ b/chainsync/filtercontrol_test.go
@@ -6,15 +6,20 @@ import (
 	"github.com/btcsuite/btcd/chaincfg"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/wire"
+	"github.com/stretchr/testify/require"
 )
 
-func TestControlCFHeader(t *testing.T) {
+// TestValidateCFHeader tests filter header validation against checkpoints,
+// including successful validation, mismatch detection, and unknown height
+// handling.
+func TestValidateCFHeader(t *testing.T) {
 	t.Parallel()
 
 	// We'll modify our backing list of checkpoints for this test.
 	height := uint32(999)
 	header := hashFromStr(
-		"4a242283a406a7c089f671bb8df7671e5d5e9ba577cea1047d30a7f4919df193",
+		"4a242283a406a7c089f671bb8df7671e5d5e9ba577cea1047d30a7f4919d" +
+			"f193",
 	)
 	filterHeaderCheckpoints = map[wire.BitcoinNet]map[uint32]*chainhash.Hash{
 		chaincfg.MainNetParams.Net: {
@@ -23,30 +28,25 @@ func TestControlCFHeader(t *testing.T) {
 	}
 
 	// Expect the control at height to succeed.
-	err := ControlCFHeader(
+	err := ValidateCFHeader(
 		chaincfg.MainNetParams, wire.GCSFilterRegular, height, header,
 	)
-	if err != nil {
-		t.Fatalf("error checking height: %v", err)
-	}
+	require.NoError(t, err)
 
 	// Pass an invalid header, this should return an error.
 	header = hashFromStr(
-		"000000000006a7c089f671bb8df7671e5d5e9ba577cea1047d30a7f4919df193",
+		"000000000006a7c089f671bb8df7671e5d5e9ba577cea1047d30a7f4919d" +
+			"f193",
 	)
-	err = ControlCFHeader(
+	err = ValidateCFHeader(
 		chaincfg.MainNetParams, wire.GCSFilterRegular, height, header,
 	)
-	if err != ErrCheckpointMismatch {
-		t.Fatalf("expected ErrCheckpointMismatch, got %v", err)
-	}
+	require.ErrorIs(t, err, ErrCheckpointMismatch)
 
 	// Finally, control an unknown height. This should also pass since we
 	// don't have the checkpoint stored.
-	err = ControlCFHeader(
+	err = ValidateCFHeader(
 		chaincfg.MainNetParams, wire.GCSFilterRegular, 99, header,
 	)
-	if err != nil {
-		t.Fatalf("error checking height: %v", err)
-	}
+	require.NoError(t, err)
 }
diff --git a/sync_test.go b/sync_test.go
index 2b06e2e..1efa137 100644
--- a/sync_test.go
+++ b/sync_test.go
@@ -2,18 +2,22 @@ package neutrino_test
 
 import (
 	"bytes"
+	"compress/bzip2"
+	"context"
 	"encoding/hex"
 	"fmt"
 	"io"
-	"io/ioutil"
 	"os"
+	"path/filepath"
 	"reflect"
 	"runtime"
 	"strings"
 	"sync"
+	"syscall"
 	"testing"
 	"time"
 
+	"github.com/btcsuite/btcd/blockchain"
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcjson"
 	"github.com/btcsuite/btcd/btcutil"
@@ -30,7 +34,9 @@ import (
 	_ "github.com/btcsuite/btcwallet/walletdb/bdb"
 	"github.com/lightninglabs/neutrino"
 	"github.com/lightninglabs/neutrino/banman"
+	"github.com/lightninglabs/neutrino/chainimport"
 	"github.com/lightninglabs/neutrino/headerfs"
+	"github.com/stretchr/testify/require"
 )
 
 var (
@@ -189,6 +195,18 @@ var (
 	ourKnownTxsByFilteredBlock = make(map[chainhash.Hash][]*btcutil.Tx)
 )
 
+// init is called when the package is initialized, before any tests run.
+func init() {
+	// Set up logging once for all tests.
+	logger := btclog.NewBackend(os.Stdout)
+	chainLogger := logger.Logger("CHAIN")
+	chainLogger.SetLevel(logLevel)
+	neutrino.UseLogger(chainLogger)
+	rpcLogger := logger.Logger("RPCC")
+	rpcLogger.SetLevel(logLevel)
+	rpcclient.UseLogger(rpcLogger)
+}
+
 // secSource is an implementation of btcwallet/txauthor/SecretsSource that
 // stores WitnessPubKeyHash addresses.
 type secSource struct {
@@ -263,7 +281,7 @@ type syncTestCase struct {
 	test func(harness *neutrinoHarness, t *testing.T)
 }
 
-var testCases = []*syncTestCase{
+var testCasesWithoutImport = []*syncTestCase{
 	{
 		name: "initial sync",
 		test: testInitialSync,
@@ -1034,78 +1052,257 @@ func testRandomBlocks(harness *neutrinoHarness, t *testing.T) {
 	}
 }
 
-func TestNeutrinoSync(t *testing.T) {
-	// Set up logging.
-	logger := btclog.NewBackend(os.Stdout)
-	chainLogger := logger.Logger("CHAIN")
-	chainLogger.SetLevel(logLevel)
-	neutrino.UseLogger(chainLogger)
-	rpcLogger := logger.Logger("RPCC")
-	rpcLogger.SetLevel(logLevel)
-	rpcclient.UseLogger(rpcLogger)
+// TestNeutrinoSyncWithHeadersImport tests the functionality of Neutrino's
+// headers import feature. The test creates a btcd SimNet node, generates
+// blocks, and syncs a temporary Neutrino instance with this node to obtain
+// headers. It then prepares header files for import by copying them and adding
+// the necessary metadata.
+//
+// The first part of the test creates a new Neutrino instance that imports
+// these headers from files without connecting to any peers. It verifies
+// that headers are correctly imported and the chain is properly synchronized
+// without network assistance.
+//
+// The second part generates additional blocks and creates another Neutrino
+// instance with the same import configuration but with network connectivity.
+// This tests that Neutrino correctly handles the case where header stores are
+// already populated, skipping the import process and continuing synchronization
+// from its current state.
+//
+// This test validates that the headers import mechanism works correctly both as
+// a way to bootstrap new nodes and when restarting existing nodes,
+// significantly improving sync performance in the former scenario.
+func TestNeutrinoSyncWithHeadersImport(t *testing.T) {
+	rootCtx := context.Background()
+
+	// Create a btcd SimNet node and generate 800 blocks.
+	h1, err := rpctest.New(
+		&chaincfg.SimNetParams, nil, []string{"--txindex"}, "",
+	)
+	require.NoError(t, err)
+	defer h1.TearDown()
+
+	err = h1.SetUp(false, 0)
+	require.NoError(t, err)
+
+	_, err = h1.Client.Generate(800)
+	require.NoError(t, err)
+
+	tempDir := t.TempDir()
+	defer os.RemoveAll(tempDir)
+
+	db, err := walletdb.Create(
+		"bdb", tempDir+"/filters.db", true, dbOpenTimeout,
+	)
+	require.NoError(t, err)
+	defer db.Close()
+
+	config := neutrino.Config{
+		DataDir:     tempDir,
+		Database:    db,
+		ChainParams: chaincfg.SimNetParams,
+		AddPeers: []string{
+			h1.P2PAddress(),
+		},
+		HeadersImport: nil,
+	}
+
+	neutrino.MaxPeers = 1
+	neutrino.BanDuration = 5 * time.Second
+	neutrino.QueryPeerConnectTimeout = 10 * time.Second
+
+	// Set up export service to connect to h1 and download headers.
+	exportSvc, err := neutrino.NewChainService(config)
+	require.NoError(t, err)
+	exportSvc.Start(rootCtx)
+
+	testHarness := &neutrinoHarness{
+		h1:  h1,
+		h2:  nil,
+		h3:  nil,
+		svc: exportSvc,
+	}
+
+	t.Logf("Waiting for export service to sync with h1 node...")
+	testInitialSync(testHarness, t)
+	t.Logf("Export service synced with h1 node")
+
+	importDir := t.TempDir()
+	defer os.RemoveAll(importDir)
+
+	blockHeadersFileName := "block_headers.bin"
+	filterHeadersFileName := "reg_filter_headers.bin"
+
+	blockHeadersFilePath := filepath.Join(
+		tempDir, blockHeadersFileName,
+	)
+	filterHeadersFilePath := filepath.Join(
+		tempDir, filterHeadersFileName,
+	)
+
+	blockHeadersImportPath := filepath.Join(importDir, blockHeadersFileName)
+	filterHeadersImportPath := filepath.Join(
+		importDir, filterHeadersFileName,
+	)
+
+	err = copyFile(blockHeadersFilePath, blockHeadersImportPath)
+	require.NoError(t, err)
+
+	err = copyFile(filterHeadersFilePath, filterHeadersImportPath)
+	require.NoError(t, err)
+
+	err = chainimport.AddHeadersImportMetadata(
+		blockHeadersImportPath, chaincfg.SimNetParams.Net, 0,
+		headerfs.Block, 0,
+	)
+	require.NoError(t, err)
+
+	err = chainimport.AddHeadersImportMetadata(
+		filterHeadersImportPath, chaincfg.SimNetParams.Net, 0,
+		headerfs.RegularFilter, 0,
+	)
+	require.NoError(t, err)
+
+	exportSvc.Stop()
+	require.NoError(t, db.Close())
+	require.NoError(t, os.RemoveAll(tempDir))
+
+	tempDir = t.TempDir()
+	defer os.RemoveAll(tempDir)
+
+	db, err = walletdb.Create(
+		"bdb", tempDir+"/filters.db", true, dbOpenTimeout,
+	)
+	require.NoError(t, err)
+	defer db.Close()
+
+	// Create service with headers import but with no peers. This will test
+	// that the headers are imported correctly without network sync. No
+	// peers specified –  we want to test the import unctionality by itself.
+	importConfig := neutrino.Config{
+		DataDir:     tempDir,
+		Database:    db,
+		ChainParams: chaincfg.SimNetParams,
+		HeadersImport: &neutrino.HeadersImportConfig{
+			BlockHeadersSource:      blockHeadersImportPath,
+			FilterHeadersSource:     filterHeadersImportPath,
+			ValidationFlags:         blockchain.BFFastAdd,
+			WriteBatchSizePerRegion: 64,
+		},
+		AddPeers: nil,
+	}
+
+	importSvc, err := neutrino.NewChainService(importConfig)
+	require.NoError(t, err)
+
+	importSvc.Start(rootCtx)
+	defer importSvc.Stop()
+
+	// Ensure that neutrino initial synced using the imported headers.
+	testHarness = &neutrinoHarness{
+		h1:  h1,
+		h2:  nil,
+		h3:  nil,
+		svc: importSvc,
+	}
+	testInitialSync(testHarness, t)
+
+	// Generate an additional 300 blocks on h1. This ensures that when we
+	// sync again, the client will need to fetch these new blocks from the
+	// network. Since the header stores are no longer empty after the first
+	// import, the import process should be skipped and Neutrino should
+	// continue syncing from its current state.
+	_, err = h1.Client.Generate(300)
+	require.NoError(t, err)
+	t.Log("Syncing again after generating more 300 blocks on h1")
+
+	// Create a new service configuration that includes both the headers
+	// import and a connection to h1. Since the stores already contain data
+	// from the previous import, the import operation should be skipped and
+	// synchronization should continue from the current state.
+	importConfig = neutrino.Config{
+		DataDir:     tempDir,
+		Database:    db,
+		ChainParams: chaincfg.SimNetParams,
+		HeadersImport: &neutrino.HeadersImportConfig{
+			BlockHeadersSource:      blockHeadersImportPath,
+			FilterHeadersSource:     filterHeadersImportPath,
+			ValidationFlags:         blockchain.BFFastAdd,
+			WriteBatchSizePerRegion: 64,
+		},
+		AddPeers: []string{h1.P2PAddress()},
+	}
+
+	importSvcToBeSkipped, err := neutrino.NewChainService(importConfig)
+	require.NoError(t, err)
+
+	// Start the service with p2p network connectivity.
+	importSvcToBeSkipped.Start(rootCtx)
+	defer importSvcToBeSkipped.Stop()
+
+	// This test doesn't explicitly verify that the import is skipped, but
+	// demonstrates that the service can successfully sync to the chain tip
+	// after the database has already been populated with headers.
+	testHarness = &neutrinoHarness{
+		h1:  h1,
+		h2:  nil,
+		h3:  nil,
+		svc: importSvcToBeSkipped,
+	}
+	testInitialSync(testHarness, t)
+}
+
+// TestNeutrinoSyncWithoutHeadersImport tests the standard synchronization
+// behavior of Neutrino without using the headers import feature.
+func TestNeutrinoSyncWithoutHeadersImport(t *testing.T) {
+	rootCtx := context.Background()
 
 	// Create a btcd SimNet node and generate 800 blocks
 	h1, err := rpctest.New(
 		&chaincfg.SimNetParams, nil, []string{"--txindex"}, "",
 	)
-	if err != nil {
-		t.Fatalf("Couldn't create harness: %s", err)
-	}
+	require.NoError(t, err)
 	defer h1.TearDown()
+
 	err = h1.SetUp(false, 0)
-	if err != nil {
-		t.Fatalf("Couldn't set up harness: %s", err)
-	}
+	require.NoError(t, err)
+
 	_, err = h1.Client.Generate(800)
-	if err != nil {
-		t.Fatalf("Couldn't generate blocks: %s", err)
-	}
+	require.NoError(t, err)
 
 	// Create a second btcd SimNet node
 	h2, err := rpctest.New(
 		&chaincfg.SimNetParams, nil, []string{"--txindex"}, "",
 	)
-	if err != nil {
-		t.Fatalf("Couldn't create harness: %s", err)
-	}
+	require.NoError(t, err)
 	defer h2.TearDown()
+
 	err = h2.SetUp(false, 0)
-	if err != nil {
-		t.Fatalf("Couldn't set up harness: %s", err)
-	}
+	require.NoError(t, err)
 
 	// Create a third btcd SimNet node and generate 1200 blocks
 	h3, err := rpctest.New(
 		&chaincfg.SimNetParams, nil, []string{"--txindex"}, "",
 	)
-	if err != nil {
-		t.Fatalf("Couldn't create harness: %s", err)
-	}
+	require.NoError(t, err)
 	defer h3.TearDown()
+
 	err = h3.SetUp(false, 0)
-	if err != nil {
-		t.Fatalf("Couldn't set up harness: %s", err)
-	}
+	require.NoError(t, err)
+
 	_, err = h3.Client.Generate(1200)
-	if err != nil {
-		t.Fatalf("Couldn't generate blocks: %s", err)
-	}
+	require.NoError(t, err)
 
 	// Connect, sync, and disconnect h1 and h2
 	err = csd([]*rpctest.Harness{h1, h2})
-	if err != nil {
-		t.Fatalf("Couldn't connect/sync/disconnect h1 and h2: %s", err)
-	}
+	require.NoError(t, err)
 
 	// Generate 300 blocks on the first node and 350 on the second
 	_, err = h1.Client.Generate(300)
-	if err != nil {
-		t.Fatalf("Couldn't generate blocks: %s", err)
-	}
+	require.NoError(t, err)
+
 	_, err = h2.Client.Generate(350)
-	if err != nil {
-		t.Fatalf("Couldn't generate blocks: %s", err)
-	}
+	require.NoError(t, err)
 
 	// Now we have a node with 1100 blocks (h1), 1150 blocks (h2), and
 	// 1200 blocks (h3). The chains of nodes h1 and h2 match up to block
@@ -1122,10 +1319,7 @@ func TestNeutrinoSync(t *testing.T) {
 	modParams := chaincfg.SimNetParams
 	for _, height := range []int64{111, 333, 555, 777, 999} {
 		hash, err := h1.Client.GetBlockHash(height)
-		if err != nil {
-			t.Fatalf("Couldn't get block hash for height %d: %s",
-				height, err)
-		}
+		require.NoError(t, err)
 		modParams.Checkpoints = append(modParams.Checkpoints,
 			chaincfg.Checkpoint{
 				Hash:   hash,
@@ -1135,18 +1329,15 @@ func TestNeutrinoSync(t *testing.T) {
 
 	// Create a temporary directory, initialize an empty walletdb with an
 	// SPV chain namespace, and create a configuration for the ChainService.
-	tempDir, err := ioutil.TempDir("", "neutrino")
-	if err != nil {
-		t.Fatalf("Failed to create temporary directory: %s", err)
-	}
+	tempDir := t.TempDir()
 	defer os.RemoveAll(tempDir)
+
 	db, err := walletdb.Create(
 		"bdb", tempDir+"/weks.db", true, dbOpenTimeout,
 	)
-	if err != nil {
-		t.Fatalf("Error opening DB: %s\n", err)
-	}
+	require.NoError(t, err)
 	defer db.Close()
+
 	config := neutrino.Config{
 		DataDir:     tempDir,
 		Database:    db,
@@ -1156,22 +1347,22 @@ func TestNeutrinoSync(t *testing.T) {
 			h2.P2PAddress(),
 			h1.P2PAddress(),
 		},
+		HeadersImport: nil,
 	}
 
 	neutrino.MaxPeers = 3
 	neutrino.BanDuration = 5 * time.Second
 	neutrino.QueryPeerConnectTimeout = 10 * time.Second
 	svc, err := neutrino.NewChainService(config)
-	if err != nil {
-		t.Fatalf("Error creating ChainService: %s", err)
-	}
-	svc.Start()
+	require.NoError(t, err)
+
+	svc.Start(rootCtx)
 	defer svc.Stop()
 
 	// Create a test harness with the three nodes and the neutrino instance.
 	testHarness := &neutrinoHarness{h1, h2, h3, svc}
 
-	for _, testCase := range testCases {
+	for _, testCase := range testCasesWithoutImport {
 		if ok := t.Run(testCase.name, func(t *testing.T) {
 			testCase.test(testHarness, t)
 		}); !ok {
@@ -1180,6 +1371,195 @@ func TestNeutrinoSync(t *testing.T) {
 	}
 }
 
+// BenchmarkHeadersImport benchmarks the headers import functionality. It
+// decompresses the block and filter headers from the testdata directory and
+// imports them into a temporary database. It then benchmarks the import time
+// for different batch sizes.
+func BenchmarkHeadersImport(b *testing.B) {
+	blockHeadersPath, cleanupBlock, err := decompressBZ2ToTempFile(
+		"testdata/block-headers-mainnet-1-100_000.bz2",
+	)
+	require.NoError(b, err)
+	defer cleanupBlock()
+
+	filterHeadersPath, cleanupFilter, err := decompressBZ2ToTempFile(
+		"testdata/filter-headers-mainnet-1-100_000.bz2",
+	)
+	require.NoError(b, err)
+	defer cleanupFilter()
+
+	expectedHeaders := 100_000
+
+	validateFileSize(
+		b, blockHeadersPath, expectedHeaders, headerfs.Block,
+	)
+
+	validateFileSize(
+		b, filterHeadersPath, expectedHeaders, headerfs.RegularFilter,
+	)
+
+	for exp := 10; exp <= 19; exp++ {
+		// Calculate batch size which is 2^exp.
+		batch := 1 << exp
+		b.Run(fmt.Sprintf("batchSize=%d", batch), func(b *testing.B) {
+			var memStats runtime.MemStats
+			var peakMemory uint64
+
+			// Take initial memory snapshot before starting the
+			// benchmark. This establishes a baseline for memory
+			// usage measurements.
+			runtime.GC()
+			runtime.ReadMemStats(&memStats)
+			initialHeapInUse := memStats.HeapInuse
+
+			tempDir := b.TempDir()
+			dbPath := filepath.Join(tempDir, "test.db")
+			db, err := walletdb.Create(
+				"bdb", dbPath, true, time.Second*10,
+			)
+			require.NoError(b, err)
+			defer db.Close()
+
+			blockStore, err := headerfs.NewBlockHeaderStore(
+				tempDir, db, &chaincfg.MainNetParams,
+			)
+			require.NoError(b, err)
+
+			filterStore, err := headerfs.NewFilterHeaderStore(
+				tempDir, db, headerfs.RegularFilter,
+				&chaincfg.MainNetParams, nil,
+			)
+			require.NoError(b, err)
+
+			opts := &chainimport.ImportOptions{
+				TargetChainParams:       chaincfg.MainNetParams,
+				TargetBlockHeaderStore:  blockStore,
+				TargetFilterHeaderStore: filterStore,
+				BlockHeadersSource:      blockHeadersPath,
+				FilterHeadersSource:     filterHeadersPath,
+				WriteBatchSizePerRegion: batch,
+			}
+
+			// Get initial CPU time for measuring CPU usage during
+			// import.
+			var rusageStart, rusageEnd syscall.Rusage
+			syscall.Getrusage(syscall.RUSAGE_SELF, &rusageStart)
+
+			// Start memory monitoring goroutine to track peak
+			// memory usage. It samples memory stats every 10ms to
+			// capture the highest usage.
+			stopMemMonitor := make(chan struct{})
+			memMonitorDone := make(chan struct{})
+			go func() {
+				ticker := time.NewTicker(10 * time.Millisecond)
+				defer ticker.Stop()
+				defer close(memMonitorDone)
+
+				for {
+					select {
+					case <-ticker.C:
+						runtime.ReadMemStats(&memStats)
+						heapInUse := memStats.HeapInuse
+						if heapInUse > peakMemory {
+							peakMemory = heapInUse
+						}
+					case <-stopMemMonitor:
+						return
+					}
+				}
+			}()
+
+			b.ResetTimer()
+			startTime := time.Now()
+			ctx := context.Background()
+
+			importer, err := chainimport.NewHeadersImport(opts)
+			require.NoError(b, err)
+
+			_, err = importer.Import(ctx)
+			require.NoError(b, err)
+
+			elapsed := time.Since(startTime)
+			b.StopTimer()
+
+			close(stopMemMonitor)
+			<-memMonitorDone
+
+			// Get final CPU usage after import completion.
+			syscall.Getrusage(syscall.RUSAGE_SELF, &rusageEnd)
+
+			// Calculate total CPU time (user + system) in seconds.
+			rEndUtime := rusageEnd.Utime
+			rStartUtime := rusageStart.Utime
+			userTimeSec := float64(rEndUtime.Sec-rStartUtime.Sec) +
+				float64(rEndUtime.Usec-rStartUtime.Usec)/1e6
+
+			rEndStime := rusageEnd.Stime
+			rStartStime := rusageStart.Stime
+			sysTimeSec := float64(rEndStime.Sec-rStartStime.Sec) +
+				float64(rEndStime.Usec-rStartStime.Usec)/1e6
+
+			cpuTimeSec := userTimeSec + sysTimeSec
+
+			// Calculate import throughput in headers per second.
+			hPS := float64(expectedHeaders) / elapsed.Seconds()
+
+			peakMemoryB := float64(peakMemory - initialHeapInUse)
+			peakMemoryMB := peakMemoryB / (1024 * 1024)
+			b.ReportMetric(peakMemoryMB, "MB_peak")
+
+			b.ReportMetric(hPS, "headers/s")
+			b.ReportMetric(cpuTimeSec, "cpu_s")
+		})
+	}
+}
+
+// decompressBZ2ToTempFile decompresses a bz2 file to a temporary file.
+func decompressBZ2ToTempFile(srcPath string) (string, func(), error) {
+	f, err := os.Open(srcPath)
+	if err != nil {
+		return "", nil, err
+	}
+	defer f.Close()
+
+	bz2r := bzip2.NewReader(f)
+	tempFile, err := os.CreateTemp(
+		"", filepath.Base(srcPath)+"-decompressed-*",
+	)
+	if err != nil {
+		return "", nil, err
+	}
+
+	if _, err := io.Copy(tempFile, bz2r); err != nil {
+		tempFile.Close()
+		os.Remove(tempFile.Name())
+		return "", nil, err
+	}
+	tempFile.Close()
+
+	cleanup := func() { os.Remove(tempFile.Name()) }
+	return tempFile.Name(), cleanup, nil
+}
+
+// validateFileSize is a helper function that validates a file contains
+// exactly the expected number of headers with the given size per header
+// plus the constant size of header metadata at front.
+func validateFileSize(b *testing.B, filePath string, nHeaders int,
+	hType headerfs.HeaderType) {
+
+	hSize, err := hType.Size()
+	require.NoError(b, err)
+
+	fileInfo, err := os.Stat(filePath)
+	require.NoError(b, err)
+	expectedSize := int64(nHeaders*hSize + chainimport.ImportMetadataSize)
+
+	require.Equal(b, expectedSize, fileInfo.Size(),
+		"%s file should contain exactly %d headers (%d bytes) + %d "+
+			"bytes metadata", hType, nHeaders, nHeaders*hSize,
+		chainimport.ImportMetadataSize)
+}
+
 // csd does a connect-sync-disconnect between nodes in order to support
 // reorg testing. It brings up and tears down a temporary node, otherwise the
 // nodes try to reconnect to each other which results in unintended reorgs.
@@ -1552,3 +1932,28 @@ func goroutineDump() string {
 	runtime.Stack(buf, true)
 	return string(buf)
 }
+
+// copyFile is a helper function to copy a file from source to destination.
+func copyFile(src, dst string) error {
+	sourceFile, err := os.Open(src)
+	if err != nil {
+		return fmt.Errorf("failed to open source file: %w", err)
+	}
+	defer sourceFile.Close()
+
+	destFile, err := os.Create(dst)
+	if err != nil {
+		return fmt.Errorf("failed to create destination file: %w", err)
+	}
+	defer destFile.Close()
+
+	if _, err = io.Copy(destFile, sourceFile); err != nil {
+		return fmt.Errorf("failed to copy file contents: %w", err)
+	}
+
+	if err = destFile.Sync(); err != nil {
+		return fmt.Errorf("failed to sync file: %w", err)
+	}
+
+	return nil
+}
diff --git a/testdata/block-headers-mainnet-1-100_000.bz2 b/testdata/block-headers-mainnet-1-100_000.bz2
new file mode 100644
index 0000000..179e070
Binary files /dev/null and b/testdata/block-headers-mainnet-1-100_000.bz2 differ
diff --git a/testdata/filter-headers-mainnet-1-100_000.bz2 b/testdata/filter-headers-mainnet-1-100_000.bz2
new file mode 100644
index 0000000..4fbabc7
Binary files /dev/null and b/testdata/filter-headers-mainnet-1-100_000.bz2 differ