Skip to content

Commit 1e6577b

Browse files
Optimize search for hard fork boundary
1 parent 9d3ff46 commit 1e6577b

File tree

1 file changed

+136
-18
lines changed

1 file changed

+136
-18
lines changed

module/fork_spec.go

Lines changed: 136 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,15 @@ package module
33
import (
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

1317
type 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

Comments
 (0)