Skip to content

Commit 37d8c80

Browse files
chainimport: add more unit tests
1 parent 3b03fd9 commit 37d8c80

File tree

2 files changed

+1230
-522
lines changed

2 files changed

+1230
-522
lines changed

chainimport/headers_import.go

Lines changed: 103 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -110,10 +110,16 @@ const (
110110
type AppendMode uint8
111111

112112
const (
113+
// AppendBlockAndFilter indicates both block and filter headers should
114+
// be appended during synchronization.
113115
AppendBlockAndFilter AppendMode = iota
114116

117+
// AppendBlockOnly indicates only block headers should be appended
118+
// during synchronization.
115119
AppendBlockOnly
116120

121+
// AppendFilterOnly indicates only filter headers should be appended
122+
// during synchronization.
117123
AppendFilterOnly
118124
)
119125

@@ -1043,7 +1049,9 @@ func (s *HeadersImport) validateSourcesCompatibility() error {
10431049
}
10441050

10451051
// validateChainContinuity ensures that headers from import sources can be
1046-
// properly connected to the existing headers in the target stores.
1052+
// properly connected to the existing headers in the target stores. It uses
1053+
// sampling method to validate headers data in overlapping ranges (if any) to
1054+
// minimize processing time.
10471055
func (s *HeadersImport) validateChainContinuity() error {
10481056
// Get metadata from block header source. We can safely use this count
10491057
// for both block headers and filter headers since we've already
@@ -1067,51 +1075,80 @@ func (s *HeadersImport) validateChainContinuity() error {
10671075
"tip: %w", err)
10681076
}
10691077

1070-
// Ensure that both target header stores have the same tip height.
1071-
// A mismatch indicates a divergence region that has not yet been
1072-
// processed. Once a resolution strategy is implemented, this check
1073-
// will no longer return an error, and the effective tip height
1074-
// will be defined as the minimum of the two.
1078+
// Take the minimum of the two heights as the effective chain tip height
1079+
// to handle the case where one store might be ahead in case of existent
1080+
// divergence region.
1081+
effectiveTipHeight := min(blockTipHeight, filterTipHeight)
10751082
if blockTipHeight != filterTipHeight {
1076-
return fmt.Errorf("divergence detected between target header "+
1077-
"store tip heights (block=%d, filter=%d)",
1078-
blockTipHeight, filterTipHeight)
1083+
log.Infof("Target header stores at different heights "+
1084+
"(block=%d, filter=%d), using effective tip height %d",
1085+
blockTipHeight, filterTipHeight, effectiveTipHeight)
10791086
}
10801087

10811088
// Extract import height range.
10821089
importStartHeight := sourceMetadata.StartHeight
10831090
importEndHeight := sourceMetadata.EndHeight
10841091

1085-
// If import wants to start after height 1, we'd have a gap.
1086-
if importStartHeight > 1 {
1087-
return fmt.Errorf("target stores contain only genesis block "+
1088-
"(height 0) but import data starts at height %d, "+
1089-
"creating a gap", importStartHeight)
1090-
}
1091-
1092-
// If import includes genesis block (starts at 0), verify it matches.
1093-
if importStartHeight == 0 {
1094-
if err := s.verifyHeadersAtTargetHeight(
1095-
importStartHeight, VerifyBlockAndFilter,
1096-
); err != nil {
1097-
return fmt.Errorf("genesis header mismatch: %v", err)
1098-
}
1099-
log.Infof("Genesis headers verified, import data will extend " +
1100-
"chain from genesis")
1101-
} else {
1102-
// Import starts at height 1, which connects to genesis.
1103-
// Validate that the block header at height 1 from the import
1104-
// source connects with the previous header in the target block
1105-
// store.
1092+
switch {
1093+
case importStartHeight > effectiveTipHeight+1:
1094+
// Import data doesn't start at the next height after the target
1095+
// tip height, there would be a gap in the chain.
1096+
return fmt.Errorf("import data starts at height %d but target "+
1097+
"tip is at %d, creating a gap",
1098+
importStartHeight, effectiveTipHeight)
1099+
1100+
case importStartHeight > effectiveTipHeight:
1101+
// Import data starts immediately after the target tip height.
1102+
// This is a forward extension.
11061103
if err := s.validateHeaderConnection(
1107-
importStartHeight, blockTipHeight, sourceMetadata,
1104+
importStartHeight, blockTipHeight,
1105+
sourceMetadata,
11081106
); err != nil {
11091107
return fmt.Errorf("failed to validate header "+
11101108
"connection: %v", err)
11111109
}
1110+
log.Infof("Import headers data will extend chain from height "+
1111+
"%d", importStartHeight)
1112+
1113+
case importStartHeight <= effectiveTipHeight:
1114+
// Import data starts before or at the target tip height. This
1115+
// means there is an overlap, so we need to verify compatibility
1116+
// using sampling approach for minimal processing time.
1117+
1118+
// First we need to determine the overlap range.
1119+
overlapStart := importStartHeight
1120+
overlapEnd := min(effectiveTipHeight, importEndHeight)
1121+
1122+
// Now we can verify headers at the start of the overlap range.
1123+
if err = s.verifyHeadersAtTargetHeight(
1124+
overlapStart, VerifyBlockAndFilter,
1125+
); err != nil {
1126+
return err
1127+
}
1128+
1129+
// If overlap range is more than 1 header, we can also verify at
1130+
// the end.
1131+
if overlapEnd > overlapStart {
1132+
if err = s.verifyHeadersAtTargetHeight(
1133+
overlapEnd, VerifyBlockAndFilter,
1134+
); err != nil {
1135+
return err
1136+
}
1137+
}
1138+
1139+
// If the overlap range is more than 2 headers, also verify at
1140+
// the middle point.
1141+
if overlapEnd-overlapStart > 2 {
1142+
middleHeight := (overlapStart + overlapEnd) / 2
1143+
if err = s.verifyHeadersAtTargetHeight(
1144+
middleHeight, VerifyBlockAndFilter,
1145+
); err != nil {
1146+
return err
1147+
}
1148+
}
11121149

1113-
log.Info("Target stores contain only genesis block, import " +
1114-
"data will extend chain from height 1")
1150+
log.Infof("Sampling validation successful for overlap headers "+
1151+
"range (%d-%d)", overlapStart, overlapEnd)
11151152
}
11161153

11171154
log.Infof("Chain continuity validation successful: import data "+
@@ -1568,8 +1605,8 @@ func (s *HeadersImport) processDivergenceHeadersRegion(ctx context.Context,
15681605
}
15691606

15701607
// validateLeadAndSyncLag resolves divergence between target stores by
1571-
// validating the leading store against import source and syncing the lagging
1572-
// store with headers from the import source.
1608+
// validating the leading target store against import source and syncing the
1609+
// lagging target store with headers from the import source.
15731610
func (s *HeadersImport) validateLeadAndSyncLag(ctx context.Context,
15741611
region HeaderRegion, options *ImportOptions) error {
15751612

@@ -1616,21 +1653,25 @@ func (s *HeadersImport) validateLeadAndSyncLag(ctx context.Context,
16161653

16171654
// determineSyncModes determines the verification and append modes based on the
16181655
// relative heights of block and filter header stores.
1656+
//
1657+
// NOTE: This is supposed to be called after divergence region has been
1658+
// detected.
16191659
func (s *HeadersImport) determineSyncModes(blockTipHeight,
1620-
filterTipHeight uint32) (SyncModes, error) {
1660+
filterTipHeight uint32) (*SyncModes, error) {
16211661

1622-
if blockTipHeight > filterTipHeight {
1623-
return SyncModes{
1662+
switch {
1663+
case blockTipHeight > filterTipHeight:
1664+
return &SyncModes{
16241665
Verify: VerifyBlockOnly,
16251666
Append: AppendFilterOnly,
16261667
}, nil
1627-
} else if blockTipHeight < filterTipHeight {
1628-
return SyncModes{
1668+
case blockTipHeight < filterTipHeight:
1669+
return &SyncModes{
16291670
Verify: VerifyFilterOnly,
16301671
Append: AppendBlockOnly,
16311672
}, nil
1632-
} else {
1633-
return SyncModes{}, fmt.Errorf("both header stores have "+
1673+
default:
1674+
return nil, fmt.Errorf("both header stores have "+
16341675
"equal height (%d) - this indicates a logic error in "+
16351676
"divergence detection", blockTipHeight)
16361677
}
@@ -1762,11 +1803,11 @@ func (s *HeadersImport) appendNewHeaders(startHeight, endHeight uint32,
17621803

17631804
for {
17641805
header, hasMore, err := filterIter.Next()
1765-
height := batchStart + uint32(len(filterHeaders))
1806+
h := batchStart + uint32(len(filterHeaders))
17661807
if err != nil {
17671808
return fmt.Errorf("failed to read "+
17681809
"filter header at height %d: "+
1769-
"%w", height, err)
1810+
"%w", h, err)
17701811
}
17711812

17721813
if header != nil {
@@ -1793,31 +1834,25 @@ func (s *HeadersImport) appendNewHeaders(startHeight, endHeight uint32,
17931834
"batch %d-%d", batchStart, batchEnd)
17941835
}
17951836

1796-
// Update the block header hash of the last filter
1797-
// header in the very last batch.
1798-
1799-
// Get the chain tip from both target stores.
1800-
tBHS := s.options.TargetBlockHeaderStore
1801-
lH, height, err := tBHS.ChainTip()
1802-
if err != nil {
1803-
return fmt.Errorf("failed to get target block "+
1804-
"header chain tip: %w", err)
1805-
}
1806-
18071837
// Check if we are in append filter only mode and
18081838
// this is the very last batch before updating the last
18091839
// header hash.
1810-
if appendMode == AppendFilterOnly &&
1811-
batchEnd+1 > endHeight {
1840+
isLastBatch := batchEnd+1 > endHeight
1841+
if appendMode == AppendFilterOnly && isLastBatch {
1842+
// Get the chain tip from both target stores.
1843+
tBHS := s.options.TargetBlockHeaderStore
1844+
lastH, height, err := tBHS.ChainTip()
1845+
if err != nil {
1846+
return fmt.Errorf("failed to get "+
1847+
"target block header chain "+
1848+
"tip: %w", err)
1849+
}
18121850

1813-
lBH := headerfs.BlockHeader{
1814-
BlockHeader: lH,
1851+
lastBH := headerfs.BlockHeader{
1852+
BlockHeader: lastH,
18151853
Height: height,
18161854
}
1817-
blockHeaders := []headerfs.BlockHeader{lBH}
1818-
setLastFilterHeaderHash(
1819-
filterHeaders, blockHeaders,
1820-
)
1855+
setLastFilterHeaderHash(filterHeaders, lastBH)
18211856
}
18221857
}
18231858

@@ -1835,7 +1870,8 @@ func (s *HeadersImport) appendNewHeaders(startHeight, endHeight uint32,
18351870
}
18361871

18371872
// Set the filter header hash.
1838-
setLastFilterHeaderHash(filterHeaders, blockHeaders)
1873+
lastBH := blockHeaders[len(blockHeaders)-1]
1874+
setLastFilterHeaderHash(filterHeaders, lastBH)
18391875
}
18401876

18411877
// Write the headers to the target stores.
@@ -2172,13 +2208,12 @@ func AddHeadersImportMetadata(sourceFilePath string, chainType wire.BitcoinNet,
21722208
// to match the block hash of the corresponding block header. This maintains
21732209
// chain tip consistency for the regular tip.
21742210
func setLastFilterHeaderHash(filterHeaders []headerfs.FilterHeader,
2175-
blockHeaders []headerfs.BlockHeader) {
2211+
chainTipBlockHeader headerfs.BlockHeader) {
21762212

21772213
// We only need to set the block header hash of the last filter
21782214
// header to maintain chain tip consistency for regular tip.
2179-
lastIdx := len(filterHeaders) - 1
2180-
chainTipHash := blockHeaders[lastIdx].BlockHeader.BlockHash()
2181-
filterHeaders[lastIdx].HeaderHash = chainTipHash
2215+
chainTipHash := chainTipBlockHeader.BlockHeader.BlockHash()
2216+
filterHeaders[len(filterHeaders)-1].HeaderHash = chainTipHash
21822217
}
21832218

21842219
// targetHeightToImportSourceIndex converts the absolute blockchain target

0 commit comments

Comments
 (0)