Skip to content

Commit 04ad236

Browse files
authored
feat: add FindHeaderForHeight to retrieve block header by height (#687)
1 parent 075d0ed commit 04ad236

File tree

5 files changed

+117
-19
lines changed

5 files changed

+117
-19
lines changed

pkg/services/chaintracks/bulk_manager.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/internal/logging"
1010
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/services/chaintracks/ingest"
1111
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/services/chaintracks/models"
12+
"github.com/bsv-blockchain/go-wallet-toolbox/pkg/wdk"
1213
"github.com/go-softwarelab/common/pkg/must"
1314
)
1415

@@ -84,6 +85,26 @@ func (bm *bulkManager) GetHeightRange() models.HeightRange {
8485
return models.NewHeightRange(minHeight, maxHeight)
8586
}
8687

88+
func (bm *bulkManager) FindHeaderForHeight(height uint) (*wdk.ChainBlockHeader, error) {
89+
bm.locker.RLock()
90+
defer bm.locker.RUnlock()
91+
92+
for _, bulkFile := range bm.bulkFiles {
93+
fileRange := bulkFile.Info.ToHeightRange()
94+
if fileRange.ContainsHeight(height) {
95+
index := height - bulkFile.Info.FirstHeight
96+
header, err := bulkFile.GetHeaderAtIndex(index)
97+
if err != nil {
98+
return nil, fmt.Errorf("failed to get header for height %d from bulk file %v: %w", height, bulkFile.Info, err)
99+
}
100+
101+
return header, nil
102+
}
103+
}
104+
105+
return nil, nil
106+
}
107+
87108
func (bm *bulkManager) processBulkChunks(ctx context.Context, bulkChunks []ingest.BulkHeaderFileInfo, downloader ingest.BulkFileDownloader) error {
88109
chunksToLoad := bm.getChunksToLoad(bulkChunks)
89110
loadedChunks := make([]ingest.BulkFileData, 0, len(chunksToLoad))

pkg/services/chaintracks/chaintracks_service.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -227,6 +227,30 @@ func (s *Service) FindChainTipHash(ctx context.Context) (string, error) {
227227
}
228228
}
229229

230+
// FindHeaderForHeight returns the chain block header for the given height from live storage or bulk storage if available.
231+
// Returns an error if the header is not found or if storage access fails.
232+
// In case of "not-found" the error wraps wdk.ErrNotFoundError for easier identification.
233+
func (s *Service) FindHeaderForHeight(ctx context.Context, height uint) (*wdk.ChainBlockHeader, error) {
234+
liveHeader, err := s.storage.Query(ctx).GetLiveHeaderByHeight(height)
235+
if err != nil {
236+
return nil, fmt.Errorf("failed to get live header by height: %w", err)
237+
}
238+
if liveHeader != nil {
239+
return &liveHeader.ChainBlockHeader, nil
240+
}
241+
242+
header, err := s.bulkMgr.FindHeaderForHeight(height)
243+
if err != nil {
244+
return nil, fmt.Errorf("failed to find header for height in bulk storage: %w", err)
245+
}
246+
247+
if header == nil {
248+
return nil, fmt.Errorf("header not found for height %d: %w", height, wdk.ErrNotFoundError)
249+
}
250+
251+
return header, nil
252+
}
253+
230254
func (s *Service) getMissingBlockHeader(ctx context.Context, hash string) *wdk.ChainBlockHeader {
231255
for _, liveIngestor := range s.liveIngestors {
232256
header, err := liveIngestor.Ingestor.GetHeaderByHash(ctx, hash)

pkg/services/chaintracks/chaintracks_service_test.go

Lines changed: 56 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -29,32 +29,70 @@ func TestService_Lifecycle(t *testing.T) {
2929
require.NoError(t, err, "make available should not return error")
3030
require.True(t, service.Available(), "service should be available after make available")
3131

32-
// when
33-
info, err := service.GetInfo(t.Context())
34-
35-
// then:
36-
require.NoError(t, err, "get info should not return error")
37-
assert.Equal(t, defs.NetworkMainnet, info.Chain)
38-
assert.Equal(t, "gorm-sqlite-inmemory", info.Storage)
39-
assert.Equal(t, []string{"woc_poll"}, info.LiveIngestors)
40-
assert.Equal(t, []string{"chaintracks_cdn (source_url=https://cdn.projectbabbage.com/blockheaders)", "whats_on_chain_cdn"}, info.BulkIngestors)
41-
t.Logf("Bulk height: %d", info.HeightBulk) // TODO: When we use live data (not mocked), this value is not constant; will be changed to assert.Equal later
42-
t.Logf("Live height: %d", info.HeightLive)
43-
4432
// when:
4533
height, err := service.GetPresentHeight(t.Context())
4634

4735
// then:
4836
require.NoError(t, err, "get present height should not return error")
4937
require.Greater(t, height, uint(900000), "present height should be greater than 900000")
5038

51-
// when:
52-
tipHeader, err := service.FindChainTipHeader(t.Context())
39+
t.Run("GetInfo", func(t *testing.T) {
40+
// when:
41+
info, err := service.GetInfo(t.Context())
42+
43+
// then:
44+
require.NoError(t, err, "get info should not return error")
45+
assert.Equal(t, defs.NetworkMainnet, info.Chain)
46+
assert.Equal(t, "gorm-sqlite-inmemory", info.Storage)
47+
assert.Equal(t, []string{"woc_poll"}, info.LiveIngestors)
48+
assert.Equal(t, []string{"chaintracks_cdn (source_url=https://cdn.projectbabbage.com/blockheaders)", "whats_on_chain_cdn"}, info.BulkIngestors)
49+
t.Logf("Bulk height: %d", info.HeightBulk) // TODO: When we use live data (not mocked), this value is not constant; will be changed to assert.Equal later
50+
t.Logf("Live height: %d", info.HeightLive)
51+
})
5352

54-
// then:
55-
require.NoError(t, err, "find chain tip header should not return error")
56-
require.NotNil(t, tipHeader, "tip header should not be nil")
57-
require.Equal(t, height, tipHeader.Height, "tip header height should be equal to present height")
53+
t.Run("FindChainTipHeader", func(t *testing.T) {
54+
// when:
55+
tipHeader, err := service.FindChainTipHeader(t.Context())
56+
57+
// then:
58+
require.NoError(t, err, "find chain tip header should not return error")
59+
require.NotNil(t, tipHeader, "tip header should not be nil")
60+
require.Equal(t, height, tipHeader.Height, "tip header height should be equal to present height")
61+
})
62+
63+
t.Run("FindHeaderForHeight_recent", func(t *testing.T) {
64+
// when:
65+
header, err := service.FindHeaderForHeight(t.Context(), height-10)
66+
67+
// then:
68+
require.NoError(t, err, "find header for height should not return error")
69+
require.NotNil(t, header, "header should not be nil")
70+
require.Equal(t, height-10, header.Height, "header height should be equal to requested height")
71+
t.Logf("Header at height %d: %#+v", header.Height, header)
72+
})
73+
74+
t.Run("FindHeaderForHeight_genesis", func(t *testing.T) {
75+
// when:
76+
header, err := service.FindHeaderForHeight(t.Context(), 0)
77+
78+
// then:
79+
require.NoError(t, err, "find header for height 0 should not return error")
80+
require.NotNil(t, header, "header at height 0 should not be nil")
81+
require.Equal(t, uint(0), header.Height, "header height should be 0")
82+
require.Equal(t, "000000000019d6689c085ae165831e934ff763ae46a2a6c172b3f1b60a8ce26f", header.Hash, "header hash at height 0 should be genesis hash")
83+
t.Logf("Genesis header: %#+v", header)
84+
})
85+
86+
t.Run("FindHeaderForHeight_old", func(t *testing.T) {
87+
// when:
88+
header, err := service.FindHeaderForHeight(t.Context(), height-5000)
89+
90+
// then:
91+
require.NoError(t, err, "find header for height should not return error")
92+
require.NotNil(t, header, "header should not be nil")
93+
require.Equal(t, height-5000, header.Height, "header height should be equal to requested height")
94+
t.Logf("Header at height %d: %#+v", header.Height, header)
95+
})
5896

5997
// when:
6098
service.Destroy()

pkg/services/chaintracks/gormstorage/storage_queries.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -164,3 +164,18 @@ func (i *storageQueries) FindLiveHeightRange() (models.HeightRange, error) {
164164

165165
return models.NewHeightRange(*res.MinHeight, *res.MaxHeight), nil
166166
}
167+
168+
func (i *storageQueries) GetLiveHeaderByHeight(height uint) (*models.LiveBlockHeader, error) {
169+
table := i.getQuery().ChaintracksLiveHeader
170+
model, err := table.
171+
Where(table.Height.Eq(height)).
172+
Where(table.IsActive.Is(true)).
173+
First()
174+
if err != nil {
175+
if errors.Is(err, gorm.ErrRecordNotFound) {
176+
return nil, nil
177+
}
178+
return nil, fmt.Errorf("failed to get live header by height: %w", err)
179+
}
180+
return mapLiveHeader(model), nil
181+
}

pkg/services/chaintracks/models/storage_queries.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,6 @@ type StorageQueries interface {
1414
SetChainTipByID(id uint, isChainTip bool) error
1515
InsertNewLiveHeader(header *LiveBlockHeader) error
1616
CountLiveHeaders() (int64, error)
17-
17+
GetLiveHeaderByHeight(height uint) (*LiveBlockHeader, error)
1818
FindLiveHeightRange() (HeightRange, error)
1919
}

0 commit comments

Comments
 (0)