@@ -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 "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
@@ -234,22 +238,11 @@ func GetBoundaryHeight(ctx context.Context, headerFn getHeaderFn, currentHeight
234238 boundaryHeight = v
235239 } else {
236240 logger .DebugContext (ctx , "seek fork height" , "currentHeight" , currentHeight , "ts" , ts )
237- for i := int64 (currentHeight ); i >= 0 ; i -- {
238- h , err := headerFn (ctx , uint64 (i ))
239- if err != nil {
240- return nil , err
241- }
242- if MilliTimestamp (h ) == ts {
243- boundaryHeight = h .Number .Uint64 ()
244- logger .DebugContext (ctx , "seek fork height found" , "currentHeight" , currentHeight , "ts" , ts , "boundaryHeight" , boundaryHeight )
245- boundaryHeightCache [ts ] = boundaryHeight
246- break
247- } else if MilliTimestamp (h ) < ts {
248- boundaryHeight = h .Number .Uint64 () + 1
249- logger .DebugContext (ctx , "seek fork height found" , "currentHeight" , currentHeight , "ts" , ts , "boundaryHeight" , boundaryHeight )
250- boundaryHeightCache [ts ] = boundaryHeight
251- break
252- }
241+ var err error
242+ boundaryHeight , err = searchBoundaryHeight (ctx , currentHeight , ts , headerFn )
243+ boundaryHeightCache [ts ] = boundaryHeight
244+ if err != nil {
245+ return nil , err
253246 }
254247 }
255248 }
@@ -258,3 +251,128 @@ func GetBoundaryHeight(ctx context.Context, headerFn getHeaderFn, currentHeight
258251 CurrentForkSpec : currentForkSpec ,
259252 }, nil
260253}
254+
255+ func searchBoundaryHeight (ctx context.Context , currentHeight uint64 , targetTs uint64 , headerFn getHeaderFn ) (uint64 , error ) {
256+ // There are potentially many blocks between the boundary and the current
257+ // blocks. Also, finding the timestamp for a particular block is expensive
258+ // as it requires an RPC call to a node.
259+ //
260+ // Thus, this implementation aims to prune a large number of blocks from the
261+ // search space by estimating the distance between the boundary and the
262+ // current block (based on the average rate of block production) and jumping
263+ // directly to a candidate block at that distance. In case of a miss, all
264+ // blocks on one side of the candidate can be discarded, and a new attempt
265+ // can be made by re-estimating the new distance and jumping to a candidate
266+ // on the other side.
267+ //
268+ // Theoretical worst-case performance is O(N), but since the rate of block
269+ // production can be predicted with high accuracy, this implementation is
270+ // expected to be faster than binary search in practice.
271+ var (
272+ position uint64 = currentHeight // candidate block number currently under consideration
273+ low uint64 = 0 // inclusive lower bound of the current search range
274+ high uint64 = currentHeight + 1 // exclusive upper bound of the current seach range
275+ previousHeader * types.Header // header of the block seen in the previous iteration
276+ )
277+
278+ // Loop invariant:
279+ //
280+ // 0 <= low <= position < high <= currentHeight + 1
281+ // &&
282+ // low <= result < high
283+ //
284+ // Bound function (decreases in each iteration, and is always >= 0):
285+ //
286+ // high - low
287+ for low < high {
288+ currentHeader , err := headerFn (ctx , uint64 (position ))
289+ if err != nil {
290+ return 0 , err
291+ }
292+
293+ currentTs := MilliTimestamp (currentHeader )
294+ if currentTs == targetTs {
295+ return currentHeader .Number .Uint64 (), nil
296+ }
297+
298+ distance := estimateDistance (previousHeader , currentHeader , targetTs )
299+
300+ if currentTs > targetTs {
301+ // Jump to a lower block.
302+ high = position
303+
304+ // Since these are unsigned, position-distance might underflow.
305+ if low + distance > position {
306+ position = low
307+ } else {
308+ position = position - distance
309+ }
310+ } else {
311+ // Jump to a higher block.
312+ low = position + 1
313+
314+ position = position + distance
315+
316+ if position >= high {
317+ position = high - 1
318+ }
319+ }
320+
321+ previousHeader = currentHeader
322+ }
323+
324+ // If no block with an exact timestamp match was found, then we want the
325+ // earliest block that's _after_ the target timestamp, and thus outside of
326+ // our search range.
327+ return high , 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+ // Blocks are being produced so slowly that the current block is still expected
372+ // to be the latest block at any future timestamp. Return 1 to avoid getting stuck
373+ // in the current block.
374+ return 1
375+ }
376+
377+ return uint64 (math .Ceil (avgBlocksPerMs * float64 (timeDiffTargetCur )))
378+ }
0 commit comments