Skip to content

Commit 61babe4

Browse files
authored
fix(sync): discard headers failed bifurcation (#269)
#219 introduced bifurcation however we missed there an issue with setting a sync target as discovered in #268 (comment). Basically, a soft failed header failing bifurcation would still be used a sync target. This is wrong and defeats the purpose of bifurcation. This patch changes this so that bifurcation is used as source of truth for headers we are unsure about(soft failed). If such header appears, we bifurcate and get to the bottom whether its valid or not, s.t. the header never enters syncing pipeline in pending cache and doesn't have to be treated differently and thus simplify syncing flow. Besides, this change sets all the discovered which were headers along the bifurcation as subjective head, facilitating the syncing progress.
1 parent 510b17b commit 61babe4

File tree

2 files changed

+38
-46
lines changed

2 files changed

+38
-46
lines changed

sync/sync_head.go

Lines changed: 20 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -138,52 +138,46 @@ func (s *Syncer[H]) incomingNetworkHead(ctx context.Context, head H) error {
138138
s.incomingMu.Lock()
139139
defer s.incomingMu.Unlock()
140140

141-
softFailure, err := s.verify(ctx, head)
142-
if err != nil && !softFailure {
141+
err := s.verify(ctx, head)
142+
if err != nil {
143143
return err
144144
}
145145

146-
// TODO(@Wondertan):
147-
// Implement setSyncTarget and use it for soft failures
148146
s.setSubjectiveHead(ctx, head)
149147
return err
150148
}
151149

152150
// verify verifies given network head candidate.
153-
// bool reports whether the returned error is a soft error.
154-
func (s *Syncer[H]) verify(ctx context.Context, newHead H) (bool, error) {
151+
func (s *Syncer[H]) verify(ctx context.Context, newHead H) error {
155152
sbjHead, err := s.subjectiveHead(ctx)
156153
if err != nil {
157154
log.Errorw("getting subjective head during validation", "err", err)
158-
// local error, so soft
159-
return true, &header.VerifyError{Reason: err, SoftFailure: true}
155+
return err
160156
}
161157

162158
err = header.Verify(sbjHead, newHead)
163159
if err == nil {
164-
return false, nil
160+
return nil
165161
}
166162

167163
var verErr *header.VerifyError
168-
if errors.As(err, &verErr) {
169-
if verErr.SoftFailure {
170-
err := s.verifyBifurcating(ctx, sbjHead, newHead)
171-
return err != nil, err
172-
}
164+
if errors.As(err, &verErr) && verErr.SoftFailure {
165+
// bifurcate for soft failures only
166+
return s.verifyBifurcating(ctx, sbjHead, newHead)
167+
}
173168

174-
logF := log.Warnw
175-
if errors.Is(err, header.ErrKnownHeader) {
176-
logF = log.Debugw
177-
}
178-
logF("invalid network header",
179-
"height_of_invalid", newHead.Height(),
180-
"hash_of_invalid", newHead.Hash(),
181-
"height_of_subjective", sbjHead.Height(),
182-
"hash_of_subjective", sbjHead.Hash(),
183-
"reason", verErr.Reason)
169+
logF := log.Warnw
170+
if errors.Is(err, header.ErrKnownHeader) {
171+
logF = log.Debugw
184172
}
173+
logF("invalid network header",
174+
"height_of_invalid", newHead.Height(),
175+
"hash_of_invalid", newHead.Hash(),
176+
"height_of_subjective", sbjHead.Height(),
177+
"hash_of_subjective", sbjHead.Hash(),
178+
"reason", verErr.Reason)
185179

186-
return verErr.SoftFailure, err
180+
return err
187181
}
188182

189183
// verifyBifurcating verifies networkHead against subjHead via the interim headers when direct
@@ -224,6 +218,7 @@ func (s *Syncer[H]) verifyBifurcating(ctx context.Context, subjHead, networkHead
224218

225219
// candidate was validated properly, update subjHead.
226220
subjHead = candidateHeader
221+
s.setSubjectiveHead(ctx, subjHead)
227222

228223
if err := header.Verify(subjHead, networkHead); err == nil {
229224
// network head validate properly, return success.

sync/sync_test.go

Lines changed: 18 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -294,12 +294,11 @@ func TestSyncerIncomingDuplicate(t *testing.T) {
294294
require.NoError(t, err)
295295
}
296296

297-
// TestSync_InvalidSyncTarget tests the possible case that a sync target
298-
// passes non-adjacent verification but is actually invalid once it is processed
299-
// via VerifyAdjacent during sync. The expected behavior is that the syncer would
300-
// discard the invalid sync target and listen for a new sync target from headersub
301-
// and sync the valid chain.
302-
func TestSync_InvalidSyncTarget(t *testing.T) {
297+
// TestSync_SoftFailureBifurcate asserts that network head soft failure is handled correctly,
298+
// triggering a bifurcation.
299+
// The expected behavior is that the syncer discards the invalid sync target if bifurcation confirms
300+
// its invalidity. Yet it recovers if the new sync target is valid.
301+
func TestSync_SoftFailureBifurcate(t *testing.T) {
303302
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
304303
t.Cleanup(cancel)
305304

@@ -323,32 +322,30 @@ func TestSync_InvalidSyncTarget(t *testing.T) {
323322
// generate 300 more headers
324323
headers := suite.GenDummyHeaders(300)
325324
// malform the remote store's head so that it can serve
326-
// the syncer a "bad" sync target that passes initial validation,
327-
// but not verification.
325+
// the syncer a soft failed head
328326
maliciousHeader := headers[299]
329327
maliciousHeader.VerifyFailure = true
328+
maliciousHeader.SoftFailure = true
330329
err = remoteStore.Append(ctx, headers...)
331330
require.NoError(t, err)
332331

333332
// start syncer so that it immediately requests the bad
334333
// sync target from the remote peer via Head request
335334
err = syncer.Start(ctx)
336335
require.NoError(t, err)
337-
338-
// give syncer some time to register the sync job before
339-
// we wait for it to "finish syncing"
336+
// await for sync to finish
340337
time.Sleep(time.Millisecond * 100)
338+
err = syncer.SyncWait(ctx)
339+
require.NoError(t, err)
340+
341+
// ensure that syncer progressed even with invalid header
342+
// yet excluding the invalid header
343+
state := syncer.State()
344+
assert.Equal(t, maliciousHeader.Height()-1, state.Height)
345+
assert.True(t, state.Finished())
346+
assert.EqualValues(t, 2, state.FromHeight)
341347

342-
// expect syncer to not be able to finish the sync
343-
// job as the bad sync target cannot be applied
344-
shortCtx, cancel := context.WithTimeout(ctx, time.Millisecond*200)
345-
err = syncer.SyncWait(shortCtx)
346-
require.ErrorIs(t, err, context.DeadlineExceeded)
347-
cancel()
348-
// ensure that syncer still expects to sync to the bad sync target's
349-
// height
350-
require.Equal(t, maliciousHeader.Height(), syncer.State().ToHeight)
351-
// ensure syncer could only sync up to one header below the bad sync target
348+
// ensure syncer could only sync up to one header below the invalid header
352349
h, err := localStore.Head(ctx)
353350
require.NoError(t, err)
354351
require.Equal(t, maliciousHeader.Height()-1, h.Height())

0 commit comments

Comments
 (0)