@@ -5,113 +5,96 @@ 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 )
57
- }
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
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
66
52
}
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 ) {
109
- return nil
110
- }
111
- if err != nil {
112
- return err
113
- }
114
-
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 {
115
98
switch {
116
99
case from .Height () < to .Height ():
117
100
log .Infof ("move tail up from %d to %d, pruning the diff..." , from .Height (), to .Height ())
@@ -138,27 +121,91 @@ func (s *Syncer[H]) moveTail(ctx context.Context, to H) error {
138
121
return nil
139
122
}
140
123
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
124
+ // tailHash returns the expected tail hash.
125
+ // Does not return if the hash hasn't changed from the current tail hash.
126
+ func (s * Syncer [H ]) tailHash (oldTail H ) header.Hash {
127
+ hash := s .Params .SyncFromHash
128
+ if hash == nil {
129
+ return nil
130
+ }
131
+
132
+ updated := oldTail .IsZero () || ! bytes .Equal (hash , oldTail .Hash ())
133
+ if ! updated {
134
+ return nil
135
+ }
136
+
137
+ log .Debugw ("tail hash updated" , "hash" , hash )
138
+ return hash
139
+ }
140
+
141
+ // tailHeight figures the actual tail height based on the Syncer parameters.
142
+ func (s * Syncer [H ]) tailHeight (ctx context.Context , oldTail , head H ) (uint64 , error ) {
143
+ height := s .Params .SyncFromHeight
144
+ if height > 0 {
145
+ return height , nil
146
+ }
147
+
148
+ if oldTail .IsZero () {
149
+ return s .estimateTailHeader (head ), nil
150
+ }
151
+
152
+ height , err := s .findTailHeight (ctx , oldTail , head )
153
+ if err != nil {
154
+ return 0 , fmt .Errorf ("estimating oldTail height: %w" , err )
155
+ }
146
156
157
+ return height , nil
158
+ }
159
+
160
+ // estimateTailHeader estimates the tail header based on the current head.
161
+ // It respects the trusting period ensuring Syncer never initializes off an expired header.
162
+ func (s * Syncer [H ]) estimateTailHeader (head H ) uint64 {
163
+ headersToRetain := uint64 (s .Params .TrustingPeriod / s .Params .blockTime ) //nolint:gosec
147
164
if headersToRetain >= head .Height () {
165
+ // means chain is very young so we can keep all headers starting from genesis
148
166
return 1
149
167
}
150
- tail := head .Height () - headersToRetain
151
- return tail
152
- }
153
168
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
169
+ return head .Height () - headersToRetain
158
170
}
159
171
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
172
+ // findTailHeight find the tail height based on the current head and tail.
173
+ // It respects the pruning window ensuring Syncer maintains the tail within the window.
174
+ func (s * Syncer [H ]) findTailHeight (ctx context.Context , oldTail , head H ) (uint64 , error ) {
175
+ expectedTailTime := head .Time ().UTC ().Add (- s .Params .PruningWindow )
176
+ currentTailTime := oldTail .Time ().UTC ()
177
+
178
+ timeDiff := expectedTailTime .Sub (currentTailTime )
179
+ if timeDiff <= 0 {
180
+ // current tail is relevant as is
181
+ return oldTail .Height (), nil
182
+ }
183
+ log .Debugw (
184
+ "current tail is beyond pruning window" ,
185
+ "tail_height" , oldTail .Height (),
186
+ "time_diff" , timeDiff .String (),
187
+ "window" , s .Params .PruningWindow .String (),
188
+ )
189
+
190
+ heightDiff := uint64 (timeDiff / s .Params .blockTime ) //nolint:gosec
191
+ newTailHeight := oldTail .Height () + heightDiff
192
+ for {
193
+ newTail , err := s .store .GetByHeight (ctx , newTailHeight )
194
+ if err != nil {
195
+ return 0 , fmt .Errorf (
196
+ "getting estimated new tail(%d) from store: %w" ,
197
+ newTailHeight ,
198
+ err ,
199
+ )
200
+ }
201
+ if newTail .Time ().UTC ().Compare (expectedTailTime ) <= 0 {
202
+ // oldTail before or equal to expectedTailTime
203
+ break
204
+ }
205
+
206
+ newTailHeight ++
207
+ }
208
+
209
+ log .Debugw ("estimated new tail" , "new_height" , oldTail .Height ())
210
+ return newTailHeight , nil
164
211
}
0 commit comments