Skip to content

Commit 83e3a34

Browse files
committed
fix(sync): check if network head is updated
1 parent ac08c4e commit 83e3a34

File tree

2 files changed

+42
-20
lines changed

2 files changed

+42
-20
lines changed

sync/syncer_head.go

Lines changed: 22 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -13,14 +13,19 @@ import (
1313
// the exchange to request the head of the chain from the network.
1414
var NetworkHeadRequestTimeout = time.Second * 2
1515

16-
// Head returns the network head or an error. It will try to get the most recent network head or return the current
16+
// Head returns the most recent network head header.
17+
//
18+
// It will try to get the most recent network head or return the current
1719
// non-expired subjective head as a fallback.
1820
// If the head has changed, it will update the tail with the new head.
1921
func (s *Syncer[H]) Head(ctx context.Context, _ ...header.HeadOption[H]) (H, error) {
20-
netHead, err := s.networkHead(ctx)
22+
netHead, updated, err := s.networkHead(ctx)
2123
if err != nil {
2224
return netHead, err
2325
}
26+
if !updated {
27+
return netHead, nil
28+
}
2429

2530
if _, err = s.subjectiveTail(ctx, netHead); err != nil {
2631
return netHead, fmt.Errorf(
@@ -37,17 +42,20 @@ func (s *Syncer[H]) Head(ctx context.Context, _ ...header.HeadOption[H]) (H, err
3742
return s.localHead(ctx)
3843
}
3944

40-
// networkHead returns subjective head ensuring its recency.
41-
// If the subjective head is not recent, it attempts to request the most recent network head from trusted peers
42-
// assuming that trusted peers are always fully synced.
45+
// networkHead returns subjective head lazily ensuring its recency.
46+
//
47+
// If the subjective head is not recent, it requests a more recent network head. If the request is successful
48+
// and the new head is more recent than the current subjective head, it sets the new head as the local subjective head
49+
// and reports true.
50+
//
4351
// The request is limited with [NetworkHeadRequestTimeout], otherwise the unrecent subjective header is returned.
44-
func (s *Syncer[H]) networkHead(ctx context.Context) (H, error) {
52+
func (s *Syncer[H]) networkHead(ctx context.Context) (H, bool, error) {
4553
sbjHead, initialized, err := s.subjectiveHead(ctx)
4654
if err != nil {
47-
return sbjHead, err
55+
return sbjHead, false, err
4856
}
4957
if isRecent(sbjHead, s.Params.blockTime, s.Params.recencyThreshold) || initialized {
50-
return sbjHead, nil
58+
return sbjHead, initialized, nil
5159
}
5260

5361
s.metrics.outdatedHead(ctx)
@@ -75,7 +83,7 @@ func (s *Syncer[H]) networkHead(ctx context.Context) (H, error) {
7583
sbjHead.Height(),
7684
)
7785

78-
return sbjHead, nil
86+
return sbjHead, false, nil
7987
}
8088
// still check if even the newly requested head is outdated
8189
if !isRecent(newHead, s.Params.blockTime, s.Params.recencyThreshold) {
@@ -86,7 +94,7 @@ func (s *Syncer[H]) networkHead(ctx context.Context) (H, error) {
8694

8795
if newHead.Height() <= sbjHead.Height() {
8896
// nothing new, just return what we have already
89-
return sbjHead, nil
97+
return sbjHead, false, nil
9098
}
9199
// set the new head as subjective, skipping expensive verification
92100
// as it was already verified by the Exchange.
@@ -97,12 +105,13 @@ func (s *Syncer[H]) networkHead(ctx context.Context) (H, error) {
97105
"height",
98106
newHead.Height(),
99107
)
100-
return newHead, nil
108+
return newHead, true, nil
101109
}
102110

103111
// subjectiveHead returns the highest known non-expired subjective Head.
112+
//
104113
// If the current subjective head is expired or does not exist,
105-
// it performs automatic subjective (re) initialization by requesting the most recent head from trusted peers.
114+
// it lazily performs automatic subjective (re) initialization by requesting the most recent head from trusted peers.
106115
// Reports true if initialization was performed, false otherwise.
107116
func (s *Syncer[H]) subjectiveHead(ctx context.Context) (H, bool, error) {
108117
sbjHead, err := s.localHead(ctx)
@@ -136,7 +145,7 @@ func (s *Syncer[H]) subjectiveHead(ctx context.Context) (H, bool, error) {
136145
}
137146

138147
log.Infow("subjective initialization finished", "height", newHead.Height())
139-
return newHead, false, nil
148+
return newHead, true, nil
140149
}
141150

142151
// localHead reports the current highest locally known head.

sync/syncer_tail_test.go

Lines changed: 20 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,8 @@ func TestSyncer_TailEstimation(t *testing.T) {
6969
ctx, cancel := context.WithTimeout(context.Background(), time.Second*50)
7070
t.Cleanup(cancel)
7171

72-
remoteStore := headertest.NewStore[*headertest.DummyHeader](t, headertest.NewTestSuite(t), 100)
72+
suite := headertest.NewTestSuite(t)
73+
remoteStore := headertest.NewStore[*headertest.DummyHeader](t, suite, 100)
7374

7475
ds := dssync.MutexWrap(datastore.NewMapDatastore())
7576
localStore, err := store.NewStore[*headertest.DummyHeader](
@@ -100,21 +101,26 @@ func TestSyncer_TailEstimation(t *testing.T) {
100101
require.NoError(t, err)
101102
require.EqualValues(t, tail.Height(), 1)
102103

103-
// simulate new header arrival by triggering recency check
104+
// simulate new head
105+
err = remoteStore.Append(ctx, suite.NextHeader())
106+
require.NoError(t, err)
107+
108+
// trigger recency check
104109
head, err := syncer.Head(ctx)
105110
require.NoError(t, err)
106111
require.Equal(t, head.Height(), remoteStore.Height())
107112

108113
tail, err = localStore.Tail(ctx)
109114
require.NoError(t, err)
110-
require.EqualValues(t, 50, tail.Height())
115+
require.EqualValues(t, 51, tail.Height())
111116
}
112117

113118
func TestSyncer_TailReconfiguration(t *testing.T) {
114119
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
115120
t.Cleanup(cancel)
116121

117-
remoteStore := headertest.NewStore[*headertest.DummyHeader](t, headertest.NewTestSuite(t), 100)
122+
suite := headertest.NewTestSuite(t)
123+
remoteStore := headertest.NewStore[*headertest.DummyHeader](t, suite, 100)
118124

119125
ds := dssync.MutexWrap(datastore.NewMapDatastore())
120126
localStore, err := store.NewStore[*headertest.DummyHeader](
@@ -145,6 +151,10 @@ func TestSyncer_TailReconfiguration(t *testing.T) {
145151

146152
syncer.Params.SyncFromHeight = 69
147153

154+
// simulate new head
155+
err = remoteStore.Append(ctx, suite.NextHeader())
156+
require.NoError(t, err)
157+
148158
err = syncer.Start(ctx)
149159
require.NoError(t, err)
150160

@@ -157,7 +167,8 @@ func TestSyncer_TailInitialization(t *testing.T) {
157167
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
158168
t.Cleanup(cancel)
159169

160-
remoteStore := headertest.NewStore[*headertest.DummyHeader](t, headertest.NewTestSuite(t), 100)
170+
suite := headertest.NewTestSuite(t)
171+
remoteStore := headertest.NewStore[*headertest.DummyHeader](t, suite, 100)
161172

162173
expectedTail, err := remoteStore.GetByHeight(ctx, 69)
163174
require.NoError(t, err)
@@ -225,8 +236,6 @@ func TestSyncer_TailInitialization(t *testing.T) {
225236
headertest.NewDummySubscriber(),
226237
// make sure the blocktime is set for proper tail estimation
227238
WithBlockTime(headertest.HeaderTime),
228-
// make sure that recency check is not triggered
229-
WithRecencyThreshold(time.Minute),
230239
test.option,
231240
)
232241
require.NoError(t, err)
@@ -250,6 +259,10 @@ func TestSyncer_TailInitialization(t *testing.T) {
250259
expectedTail = test.expectedAfterRestart()
251260
syncer.Params.SyncFromHeight = expectedTail.Height()
252261
syncer.Params.SyncFromHash = expectedTail.Hash()
262+
263+
// simulate new head
264+
err = remoteStore.Append(ctx, suite.NextHeader())
265+
require.NoError(t, err)
253266
err = syncer.Start(ctx)
254267
require.NoError(t, err)
255268

0 commit comments

Comments
 (0)