@@ -3,11 +3,15 @@ package module
33import (
44 "context"
55 "fmt"
6- "github.com/cockroachdb/errors "
7- "github.com/hyperledger-labs/yui-relayer/log "
6+ "math "
7+ "math/big "
88 "os"
99 "slices"
1010 "strconv"
11+
12+ "github.com/cockroachdb/errors"
13+ "github.com/ethereum/go-ethereum/core/types"
14+ "github.com/hyperledger-labs/yui-relayer/log"
1115)
1216
1317type Network string
@@ -225,6 +229,7 @@ func FindTargetForkSpec(forkSpecs []*ForkSpec, height uint64, timestamp uint64)
225229var boundaryHeightCache = make (map [uint64 ]uint64 )
226230
227231func GetBoundaryHeight (ctx context.Context , headerFn getHeaderFn , currentHeight uint64 , currentForkSpec ForkSpec ) (* BoundaryHeight , error ) {
232+ var err error
228233 logger := log .GetLogger ()
229234 boundaryHeight := uint64 (0 )
230235 if condition , ok := currentForkSpec .GetHeightOrTimestamp ().(* ForkSpec_Height ); ok {
@@ -235,27 +240,139 @@ func GetBoundaryHeight(ctx context.Context, headerFn getHeaderFn, currentHeight
235240 boundaryHeight = v
236241 } else {
237242 logger .DebugContext (ctx , "seek fork height" , "currentHeight" , currentHeight , "ts" , ts )
238- for i := int64 (currentHeight ); i >= 0 ; i -- {
239- h , err := headerFn (ctx , uint64 (i ))
240- if err != nil {
241- return nil , err
242- }
243- if MilliTimestamp (h ) == ts {
244- boundaryHeight = h .Number .Uint64 ()
245- logger .DebugContext (ctx , "seek fork height found" , "currentHeight" , currentHeight , "ts" , ts , "boundaryHeight" , boundaryHeight )
246- boundaryHeightCache [ts ] = boundaryHeight
247- break
248- } else if MilliTimestamp (h ) < ts {
249- boundaryHeight = h .Number .Uint64 () + 1
250- logger .DebugContext (ctx , "seek fork height found" , "currentHeight" , currentHeight , "ts" , ts , "boundaryHeight" , boundaryHeight )
251- boundaryHeightCache [ts ] = boundaryHeight
252- break
253- }
243+ boundaryHeight , err = searchBoundaryHeight (ctx , currentHeight , ts , headerFn )
244+ if err != nil {
245+ return nil , err
254246 }
247+ boundaryHeightCache [ts ] = boundaryHeight
255248 }
256249 }
257250 return & BoundaryHeight {
258251 Height : boundaryHeight ,
259252 CurrentForkSpec : currentForkSpec ,
260253 }, nil
261254}
255+
256+ func searchBoundaryHeight (ctx context.Context , currentHeight uint64 , targetTs uint64 , headerFn getHeaderFn ) (uint64 , error ) {
257+ // There are potentially many blocks between the boundary and the current
258+ // blocks. Also, finding the timestamp for a particular block is expensive
259+ // as it requires an RPC call to a node.
260+ //
261+ // Thus, this implementation aims to prune a large number of blocks from the
262+ // search space by estimating the distance between the boundary and the
263+ // current block (based on the average rate of block production) and jumping
264+ // directly to a candidate block at that distance. In case of a miss, all
265+ // blocks on one side of the candidate can be discarded, and a new attempt
266+ // can be made by re-estimating the new distance and jumping to a candidate
267+ // on the other side.
268+ //
269+ // Theoretical worst-case performance is O(N), but since the rate of block
270+ // production can be predicted with high accuracy, this implementation is
271+ // expected to be faster than binary search in practice.
272+ var (
273+ position uint64 = currentHeight // candidate block number currently under consideration
274+ low uint64 = 0 // inclusive lower bound of the current search range
275+ high uint64 = currentHeight + 1 // exclusive upper bound of the current search range
276+ previousHeader * types.Header // header of the block seen in the previous iteration
277+ )
278+
279+ // Loop invariant:
280+ //
281+ // 0 <= low <= position < high <= currentHeight + 1
282+ // &&
283+ // low <= result < high
284+ //
285+ // Bound function (decreases in each iteration, and is always >= 0):
286+ //
287+ // high - low
288+ for low < high {
289+ currentHeader , err := headerFn (ctx , uint64 (position ))
290+ if err != nil {
291+ return 0 , err
292+ }
293+
294+ currentTs := MilliTimestamp (currentHeader )
295+ if currentTs == targetTs {
296+ return currentHeader .Number .Uint64 (), nil
297+ }
298+
299+ distance := estimateDistance (previousHeader , currentHeader , targetTs )
300+
301+ if currentTs > targetTs {
302+ // Jump to a lower block.
303+ high = position
304+
305+ // Since these are unsigned, position-distance might underflow.
306+ if low + distance > position {
307+ position = low
308+ } else {
309+ position = position - distance
310+ }
311+ } else {
312+ // Jump to a higher block.
313+ low = position + 1
314+
315+ position = position + distance
316+
317+ if position >= high {
318+ position = high - 1
319+ }
320+ }
321+
322+ previousHeader = currentHeader
323+ }
324+
325+ // If no block with an exact timestamp match was found, then we want the
326+ // earliest block that's _after_ the target timestamp.
327+ return low , nil
328+ }
329+
330+ // estimateDistance returns the estimated number of blocks between the block indicated by currentHeader
331+ // and the boundary block nearest to targetTs. It assumes that previousHeader either is nil, or refers to
332+ // a different block than currentHeader.
333+ func estimateDistance (previousHeader , currentHeader * types.Header , targetTs uint64 ) uint64 {
334+ if previousHeader == nil {
335+ return 1
336+ }
337+
338+ var (
339+ timeDiffPrevCur uint64 // milliseconds between the previous and current blocks
340+ timeDiffTargetCur uint64 // milliseconds between the current block and target timestamp
341+ )
342+
343+ currentTs := MilliTimestamp (currentHeader )
344+ previousTs := MilliTimestamp (previousHeader )
345+
346+ blockCountPrevCurBig := new (big.Int ).Sub (previousHeader .Number , currentHeader .Number )
347+ blockCountPrevCurBig = blockCountPrevCurBig .Abs (blockCountPrevCurBig )
348+ blockCountPrevCur , _ := blockCountPrevCurBig .Float64 ()
349+
350+ if currentTs > previousTs {
351+ timeDiffPrevCur = currentTs - previousTs
352+ } else {
353+ timeDiffPrevCur = previousTs - currentTs
354+ }
355+
356+ if timeDiffPrevCur == 0 {
357+ // Found two different blocks with the same timestamp. The distance
358+ // should be at least 1 to avoid getting stuck in the current block.
359+ return 1
360+ }
361+
362+ if currentTs > targetTs {
363+ timeDiffTargetCur = currentTs - targetTs
364+ } else {
365+ timeDiffTargetCur = targetTs - currentTs
366+ }
367+
368+ avgBlocksPerMs := blockCountPrevCur / float64 (timeDiffPrevCur )
369+
370+ if avgBlocksPerMs > 0 {
371+ return uint64 (math .Ceil (avgBlocksPerMs * float64 (timeDiffTargetCur )))
372+ }
373+
374+ // Blocks are being produced so slowly that the current block is still expected
375+ // to be the latest block at any future timestamp. Return 1 to avoid getting stuck
376+ // in the current block.
377+ return 1
378+ }
0 commit comments