@@ -3,6 +3,7 @@ package sync
3
3
import (
4
4
"context"
5
5
"errors"
6
+ "fmt"
6
7
"time"
7
8
8
9
"github.com/celestiaorg/go-header"
@@ -164,6 +165,7 @@ func (s *Syncer[H]) incomingNetworkHead(ctx context.Context, head H) error {
164
165
}
165
166
166
167
// verify verifies given network head candidate.
168
+ // bool reports whether the returned error is a soft error.
167
169
func (s * Syncer [H ]) verify (ctx context.Context , newHead H ) (bool , error ) {
168
170
sbjHead , err := s .subjectiveHead (ctx )
169
171
if err != nil {
@@ -178,7 +180,12 @@ func (s *Syncer[H]) verify(ctx context.Context, newHead H) (bool, error) {
178
180
}
179
181
180
182
var verErr * header.VerifyError
181
- if errors .As (err , & verErr ) && ! verErr .SoftFailure {
183
+ if errors .As (err , & verErr ) {
184
+ if verErr .SoftFailure {
185
+ err := s .verifyBifurcating (ctx , sbjHead , newHead )
186
+ return err != nil , err
187
+ }
188
+
182
189
logF := log .Warnw
183
190
if errors .Is (err , header .ErrKnownHeader ) {
184
191
logF = log .Debugw
@@ -194,6 +201,69 @@ func (s *Syncer[H]) verify(ctx context.Context, newHead H) (bool, error) {
194
201
return verErr .SoftFailure , err
195
202
}
196
203
204
+ // verifyBifurcating verifies networkHead against subjHead via the interim headers when direct
205
+ // verification is impossible.
206
+ // It tries to find a header (or several headers if necessary) between the networkHead and
207
+ // the subjectiveHead such that non-adjacent (or in the worst case adjacent) verification
208
+ // passes and the networkHead can be verified as a valid sync target against the syncer's
209
+ // subjectiveHead.
210
+ // A non-nil error is returned when networkHead can't be verified.
211
+ func (s * Syncer [H ]) verifyBifurcating (ctx context.Context , subjHead , networkHead H ) error {
212
+ log .Warnw ("header bifurcation started" ,
213
+ "height" , networkHead .Height (),
214
+ "hash" , networkHead .Hash ().String (),
215
+ )
216
+
217
+ subjHeight := subjHead .Height ()
218
+
219
+ diff := networkHead .Height () - subjHeight
220
+
221
+ for diff > 1 {
222
+ candidateHeight := subjHeight + diff / 2
223
+
224
+ candidateHeader , err := s .getter .GetByHeight (ctx , candidateHeight )
225
+ if err != nil {
226
+ return err
227
+ }
228
+
229
+ if err := header .Verify (subjHead , candidateHeader ); err != nil {
230
+ var verErr * header.VerifyError
231
+ if errors .As (err , & verErr ) && ! verErr .SoftFailure {
232
+ return err
233
+ }
234
+
235
+ // candidate failed, go deeper in 1st half.
236
+ diff /= 2
237
+ continue
238
+ }
239
+
240
+ // candidate was validated properly, update subjHead.
241
+ subjHead = candidateHeader
242
+
243
+ if err := header .Verify (subjHead , networkHead ); err == nil {
244
+ // network head validate properly, return success.
245
+ return nil
246
+ }
247
+
248
+ // new subjHead failed, go deeper in 2nd half.
249
+ subjHeight = subjHead .Height ()
250
+ diff = networkHead .Height () - subjHeight
251
+ }
252
+
253
+ s .metrics .failedBifurcation (ctx , networkHead .Height (), networkHead .Hash ().String ())
254
+ log .Errorw ("header bifurcation failed" ,
255
+ "height" , networkHead .Height (),
256
+ "hash" , networkHead .Hash ().String (),
257
+ )
258
+
259
+ return & header.VerifyError {
260
+ Reason : fmt .Errorf ("sync: header validation against subjHead height:%d hash:%s" ,
261
+ networkHead .Height (), networkHead .Hash ().String (),
262
+ ),
263
+ SoftFailure : false ,
264
+ }
265
+ }
266
+
197
267
// isExpired checks if header is expired against trusting period.
198
268
func isExpired [H header.Header [H ]](header H , period time.Duration ) bool {
199
269
expirationTime := header .Time ().Add (period )
0 commit comments