@@ -5,112 +5,100 @@ import (
5
5
"context"
6
6
"errors"
7
7
"fmt"
8
- "time"
9
8
10
9
"github.com/celestiaorg/go-header"
11
10
)
12
11
13
- // TODO:
14
- // * Refactor tests
15
-
16
- // subjectiveTail returns the current Tail header.
12
+ // subjectiveTail returns the current actual Tail header.
17
13
// Lazily fetching it if it doesn't exist locally or moving it to a different height.
18
14
// Moving is done if either parameters are changed or tail moved outside a pruning window.
19
15
func (s * Syncer [H ]) subjectiveTail (ctx context.Context , head H ) (H , error ) {
20
- tail , err := s .store .Tail (ctx )
16
+ oldTail , err := s .store .Tail (ctx )
21
17
if err != nil && ! errors .Is (err , header .ErrEmptyStore ) {
22
- return tail , err
18
+ return oldTail , err
19
+ }
20
+
21
+ newTail , err := s .updateTail (ctx , oldTail , head )
22
+ if err != nil {
23
+ return oldTail , fmt .Errorf ("updating tail: %w" , err )
23
24
}
24
25
25
- var fetched bool
26
- if tailHash , outdated := s .isTailHashOutdated (tail ); outdated {
27
- log .Debugw ("tail hash updated" , "hash" , tailHash )
28
- tail , err = s .store .Get (ctx , tailHash )
26
+ if err := s .moveTail (ctx , oldTail , newTail ); err != nil {
27
+ return oldTail , fmt .Errorf (
28
+ "moving tail from %d to %d: %w" ,
29
+ oldTail .Height (),
30
+ newTail .Height (),
31
+ err ,
32
+ )
33
+ }
34
+
35
+ return newTail , nil
36
+ }
37
+
38
+ // updateTail updates the tail header based on the Syncer parameters.
39
+ func (s * Syncer [H ]) updateTail (ctx context.Context , oldTail , head H ) (newTail H , err error ) {
40
+ switch tailHash := s .tailHash (oldTail ); tailHash {
41
+ case nil :
42
+ tailHeight , err := s .tailHeight (ctx , oldTail , head )
29
43
if err != nil {
30
- log .Debugw ("tail hash not available locally, fetching..." , "hash" , tailHash )
31
- tail , err = s .getter .Get (ctx , tailHash )
32
- if err != nil {
33
- return tail , fmt .Errorf ("getting SyncFromHash tail(%x): %w" , tailHash , err )
34
- }
35
- fetched = true
44
+ return oldTail , err
36
45
}
37
- } else if tailHeight , outdated := s .isTailHeightOutdated (tail ); outdated {
38
- log .Debugw ("tail height updated" , "height" , tailHeight )
46
+
39
47
if tailHeight <= s .store .Height () {
40
- tail , err = s .store .GetByHeight (ctx , tailHeight )
41
- }
42
- if err != nil || tailHeight != tail .Height () {
43
- log .Debugw ("tail height not available locally, fetching..." , "height" , tailHeight )
44
- tail , err = s .getter .GetByHeight (ctx , tailHeight )
45
- if err != nil {
46
- return tail , fmt .Errorf ("getting SyncFromHeight tail(%d): %w" , tailHeight , err )
47
- }
48
- fetched = true
49
- }
50
- } else if tailHash == nil && tailHeight == 0 {
51
- if tail .IsZero () {
52
- // no previously known Tail available - estimate solely on Head
53
- estimatedHeight := estimateTail (head , s .Params .blockTime , s .Params .TrustingPeriod )
54
- tail , err = s .getter .GetByHeight (ctx , estimatedHeight )
55
- if err != nil {
56
- return tail , fmt .Errorf ("getting estimated tail(%d): %w" , tailHeight , err )
48
+ // check if the new tail is below the current head to avoid heightSub blocking
49
+ newTail , err = s .store .GetByHeight (ctx , tailHeight )
50
+ if err == nil {
51
+ return newTail , nil
57
52
}
58
- fetched = true
59
- } else {
60
- // have a known Tail - estimate basing on it.
61
- cutoffTime := head .Time ().UTC ().Add (- s .Params .PruningWindow )
62
- diff := cutoffTime .Sub (tail .Time ().UTC ())
63
- if diff <= 0 {
64
- // current tail is relevant as is
65
- return tail , nil
66
- }
67
- log .Debugw ("current tail is beyond pruning window" , "current_height" , tail .Height (), "diff" , diff .String ())
68
-
69
- toDeleteEstimate := uint64 (diff / s .Params .blockTime ) //nolint:gosec
70
- estimatedNewTail := tail .Height () + toDeleteEstimate
71
-
72
- for {
73
- tail , err = s .store .GetByHeight (ctx , estimatedNewTail )
74
- if err != nil {
75
- log .Errorw ("getting estimated tail from store" , "height" , estimatedNewTail , "error" , err )
76
- return tail , err
77
- }
78
- if tail .Time ().UTC ().Compare (cutoffTime ) <= 0 {
79
- // tail before or equal to cutoffTime
80
- break
81
- }
82
-
83
- estimatedNewTail ++
53
+ if ! errors .Is (err , header .ErrNotFound ) {
54
+ return newTail , fmt .Errorf (
55
+ "loading SyncFromHeight tail from store(%d): %w" ,
56
+ tailHeight ,
57
+ err ,
58
+ )
84
59
}
60
+ }
85
61
86
- log .Debugw ("estimated new tail" , "new_height" , tail .Height ())
62
+ log .Debugw ("tail height not available locally, fetching..." , "height" , tailHeight )
63
+ newTail , err = s .getter .GetByHeight (ctx , tailHeight )
64
+ if err != nil {
65
+ return newTail , fmt .Errorf ("fetching SyncFromHeight tail(%d): %w" , tailHeight , err )
66
+ }
67
+ default :
68
+ newTail , err = s .store .Get (ctx , tailHash )
69
+ if err == nil {
70
+ return newTail , nil
71
+ }
72
+ if ! errors .Is (err , header .ErrNotFound ) {
73
+ return newTail , fmt .Errorf (
74
+ "loading SyncFromHash tail from store(%x): %w" ,
75
+ tailHash ,
76
+ err ,
77
+ )
87
78
}
88
- }
89
79
90
- if fetched {
91
- if err := s .store .Append (ctx , tail ); err != nil {
92
- return tail , fmt .Errorf ("appending tail header: %w" , err )
80
+ log .Debugw ("tail hash not available locally, fetching..." , "hash" , tailHash )
81
+ newTail , err = s .getter .Get (ctx , tailHash )
82
+ if err != nil {
83
+ return newTail , fmt .Errorf ("fetching SyncFromHash tail(%x): %w" , tailHash , err )
93
84
}
94
85
}
95
86
96
- if err := s .moveTail (ctx , tail ); err != nil {
97
- return tail , fmt .Errorf ("moving tail: %w" , err )
87
+ if err := s .store . Append (ctx , newTail ); err != nil {
88
+ return newTail , fmt .Errorf ("appending tail header : %w" , err )
98
89
}
99
90
100
- return tail , nil
91
+ return newTail , nil
101
92
}
102
93
103
94
// moveTail moves the Tail to be the given header.
104
95
// It will prune the store if the new Tail is higher than the old one or
105
- // sync up if the new Tail is lower than the old one.
106
- func (s * Syncer [H ]) moveTail (ctx context.Context , to H ) error {
107
- from , err := s . store . Tail ( ctx )
108
- if errors . Is ( err , header . ErrEmptyStore ) {
96
+ // sync up the difference if the new Tail is lower than the old one.
97
+ func (s * Syncer [H ]) moveTail (ctx context.Context , from , to H ) error {
98
+ if from . IsZero () {
99
+ // no need to move the tail if it was not set previously
109
100
return nil
110
101
}
111
- if err != nil {
112
- return err
113
- }
114
102
115
103
switch {
116
104
case from .Height () < to .Height ():
@@ -138,27 +126,91 @@ func (s *Syncer[H]) moveTail(ctx context.Context, to H) error {
138
126
return nil
139
127
}
140
128
141
- func estimateTail [H header.Header [H ]](
142
- head H ,
143
- blockTime , trustingPeriod time.Duration ,
144
- ) (height uint64 ) {
145
- headersToRetain := uint64 (trustingPeriod / blockTime ) //nolint:gosec
129
+ // tailHash returns the expected tail hash.
130
+ // Does not return if the hash hasn't changed from the current tail hash.
131
+ func (s * Syncer [H ]) tailHash (oldTail H ) header.Hash {
132
+ hash := s .Params .SyncFromHash
133
+ if hash == nil {
134
+ return nil
135
+ }
146
136
137
+ updated := oldTail .IsZero () || ! bytes .Equal (hash , oldTail .Hash ())
138
+ if ! updated {
139
+ return nil
140
+ }
141
+
142
+ log .Debugw ("tail hash updated" , "hash" , hash )
143
+ return hash
144
+ }
145
+
146
+ // tailHeight figures the actual tail height based on the Syncer parameters.
147
+ func (s * Syncer [H ]) tailHeight (ctx context.Context , oldTail , head H ) (uint64 , error ) {
148
+ height := s .Params .SyncFromHeight
149
+ if height > 0 {
150
+ return height , nil
151
+ }
152
+
153
+ if oldTail .IsZero () {
154
+ return s .estimateTailHeader (head ), nil
155
+ }
156
+
157
+ height , err := s .findTailHeight (ctx , oldTail , head )
158
+ if err != nil {
159
+ return 0 , fmt .Errorf ("estimating oldTail height: %w" , err )
160
+ }
161
+
162
+ return height , nil
163
+ }
164
+
165
+ // estimateTailHeader estimates the tail header based on the current head.
166
+ // It respects the trusting period, ensuring Syncer never initializes off an expired header.
167
+ func (s * Syncer [H ]) estimateTailHeader (head H ) uint64 {
168
+ headersToRetain := uint64 (s .Params .TrustingPeriod / s .Params .blockTime ) //nolint:gosec
147
169
if headersToRetain >= head .Height () {
170
+ // means chain is very young so we can keep all headers starting from genesis
148
171
return 1
149
172
}
150
- tail := head .Height () - headersToRetain
151
- return tail
152
- }
153
173
154
- func (s * Syncer [H ]) isTailHashOutdated (h H ) (header.Hash , bool ) {
155
- wantHash := s .Params .SyncFromHash
156
- outdated := wantHash != nil && (h .IsZero () || ! bytes .Equal (wantHash , h .Hash ()))
157
- return wantHash , outdated
174
+ return head .Height () - headersToRetain
158
175
}
159
176
160
- func (s * Syncer [H ]) isTailHeightOutdated (h H ) (uint64 , bool ) {
161
- wantHeight := s .Params .SyncFromHeight
162
- outdated := wantHeight > 0 && (h .IsZero () || h .Height () != wantHeight )
163
- return wantHeight , outdated
177
+ // findTailHeight find the tail height based on the current head and tail.
178
+ // It respects the pruning window, ensuring Syncer maintains the tail within the window.
179
+ func (s * Syncer [H ]) findTailHeight (ctx context.Context , oldTail , head H ) (uint64 , error ) {
180
+ expectedTailTime := head .Time ().UTC ().Add (- s .Params .PruningWindow )
181
+ currentTailTime := oldTail .Time ().UTC ()
182
+
183
+ timeDiff := expectedTailTime .Sub (currentTailTime )
184
+ if timeDiff <= 0 {
185
+ // current tail is relevant as is
186
+ return oldTail .Height (), nil
187
+ }
188
+ log .Debugw (
189
+ "current tail is beyond pruning window" ,
190
+ "tail_height" , oldTail .Height (),
191
+ "time_diff" , timeDiff .String (),
192
+ "window" , s .Params .PruningWindow .String (),
193
+ )
194
+
195
+ heightDiff := uint64 (timeDiff / s .Params .blockTime ) //nolint:gosec
196
+ newTailHeight := oldTail .Height () + heightDiff
197
+ for {
198
+ newTail , err := s .store .GetByHeight (ctx , newTailHeight )
199
+ if err != nil {
200
+ return 0 , fmt .Errorf (
201
+ "getting estimated new tail(%d) from store: %w" ,
202
+ newTailHeight ,
203
+ err ,
204
+ )
205
+ }
206
+ if newTail .Time ().UTC ().Compare (expectedTailTime ) <= 0 {
207
+ // oldTail before or equal to expectedTailTime
208
+ break
209
+ }
210
+
211
+ newTailHeight ++
212
+ }
213
+
214
+ log .Debugw ("estimated new tail" , "new_height" , oldTail .Height ())
215
+ return newTailHeight , nil
164
216
}
0 commit comments