Skip to content

Commit abcc6a6

Browse files
cyberhorseydavidtaikochaYoGhurt111
authored
feat(taiko-client): lookahead sliding window (#19322)
Co-authored-by: David <[email protected]> Co-authored-by: Gavin Yu <[email protected]>
1 parent 6c257ef commit abcc6a6

File tree

10 files changed

+324
-99
lines changed

10 files changed

+324
-99
lines changed

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -309,7 +309,7 @@ require (
309309
rsc.io/tmplfunc v0.0.3 // indirect
310310
)
311311

312-
replace github.com/ethereum/go-ethereum v1.15.5 => github.com/taikoxyz/taiko-geth v1.12.1-0.20250311074847-364acd00d1f2
312+
replace github.com/ethereum/go-ethereum v1.15.5 => github.com/taikoxyz/taiko-geth v0.0.0-20250422211912-3a4f3775cc30
313313

314314
replace github.com/ethereum-optimism/optimism v1.7.4 => github.com/taikoxyz/optimism v0.0.0-20250407113505-a4338a4857e6
315315

go.sum

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -878,8 +878,8 @@ github.com/taikoxyz/hive v0.0.0-20240827015317-405b241dd082 h1:ymZR+Y88LOnA8i3Ke
878878
github.com/taikoxyz/hive v0.0.0-20240827015317-405b241dd082/go.mod h1:RHnIu3EFehrWX3JhFAMQSXD5uz7l0xaNroTzXrap7EQ=
879879
github.com/taikoxyz/optimism v0.0.0-20250407113505-a4338a4857e6 h1:TZqB0xhP6eqTbuPSnMYB6md6EtGoYoYCMAK/v9HV1YM=
880880
github.com/taikoxyz/optimism v0.0.0-20250407113505-a4338a4857e6/go.mod h1:V0VCkKtCzuaJH6qcL75SRcbdlakM9LhurMEJUhO6VXA=
881-
github.com/taikoxyz/taiko-geth v1.12.1-0.20250311074847-364acd00d1f2 h1:MnjxihB0mYW3MMUXkDiwWPJVyfHKtYY+4S+Dmaz2hew=
882-
github.com/taikoxyz/taiko-geth v1.12.1-0.20250311074847-364acd00d1f2/go.mod h1:1LG2LnMOx2yPRHR/S+xuipXH29vPr6BIH6GElD8N/fo=
881+
github.com/taikoxyz/taiko-geth v0.0.0-20250422211912-3a4f3775cc30 h1:zduJWfvVFUMoSiBQ2SjlmhckQY3X+XkdtHitooaHpKg=
882+
github.com/taikoxyz/taiko-geth v0.0.0-20250422211912-3a4f3775cc30/go.mod h1:1LG2LnMOx2yPRHR/S+xuipXH29vPr6BIH6GElD8N/fo=
883883
github.com/tarm/serial v0.0.0-20180830185346-98f6abe2eb07/go.mod h1:kDXzergiv9cbyO7IOYJZWg1U88JhDg3PB6klq9Hg2pA=
884884
github.com/testcontainers/testcontainers-go v0.35.0 h1:uADsZpTKFAtp8SLK+hMwSaa+X+JiERHtd4sQAFmXeMo=
885885
github.com/testcontainers/testcontainers-go v0.35.0/go.mod h1:oEVBj5zrfJTrgjwONs1SsRbnBtH9OKl+IGl3UMcr2B4=

packages/taiko-client/driver/config.go

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,12 @@ import (
77
"net/url"
88
"time"
99

10+
p2pFlags "github.com/ethereum-optimism/optimism/op-node/flags"
1011
"github.com/ethereum-optimism/optimism/op-node/p2p"
1112
p2pCli "github.com/ethereum-optimism/optimism/op-node/p2p/cli"
1213
"github.com/ethereum-optimism/optimism/op-node/rollup"
1314
"github.com/ethereum/go-ethereum/common"
15+
"github.com/ethereum/go-ethereum/crypto"
1416
"github.com/ethereum/go-ethereum/log"
1517
"github.com/urfave/cli/v2"
1618

@@ -32,6 +34,7 @@ type Config struct {
3234
P2PConfigs *p2p.Config
3335
P2PSignerConfigs p2p.SignerSetup
3436
PreconfHandoverSkipSlots uint64
37+
PreconfOperatorAddress common.Address
3538
}
3639

3740
// NewConfigFromCliContext creates a new config instance from
@@ -124,6 +127,16 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) {
124127
)
125128
}
126129

130+
var preconfOperatorAddress common.Address
131+
if c.IsSet(p2pFlags.SequencerP2PKeyName) {
132+
sequencerP2PKey, err := crypto.ToECDSA(common.FromHex(c.String(p2pFlags.SequencerP2PKeyName)))
133+
if err != nil {
134+
return nil, err
135+
}
136+
137+
preconfOperatorAddress = crypto.PubkeyToAddress(sequencerP2PKey.PublicKey)
138+
}
139+
127140
return &Config{
128141
ClientConfig: clientConfig,
129142
RetryInterval: c.Duration(flags.BackOffRetryInterval.Name),
@@ -136,5 +149,6 @@ func NewConfigFromCliContext(c *cli.Context) (*Config, error) {
136149
P2PConfigs: p2pConfigs,
137150
P2PSignerConfigs: signerConfigs,
138151
PreconfHandoverSkipSlots: preconfHandoverSkipSlots,
152+
PreconfOperatorAddress: preconfOperatorAddress,
139153
}, nil
140154
}

packages/taiko-client/driver/driver.go

Lines changed: 40 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -379,23 +379,30 @@ func (d *Driver) cacheLookaheadLoop() {
379379
var (
380380
seenBlockNumber uint64 = 0
381381
lastSlot uint64 = 0
382+
opWin = preconfBlocks.NewOpWindow(
383+
d.PreconfHandoverSkipSlots,
384+
d.rpc.L1Beacon.SlotsPerEpoch,
385+
)
382386
)
383387

384388
for {
385389
select {
386390
case <-d.ctx.Done():
387391
return
388392
case <-ticker.C:
389-
currentSlot := d.rpc.L1Beacon.CurrentSlot()
393+
var (
394+
currentEpoch = d.rpc.L1Beacon.CurrentEpoch()
395+
currentSlot = d.rpc.L1Beacon.CurrentSlot()
396+
slotInEpoch = d.rpc.L1Beacon.SlotInEpoch()
397+
slotsLeftInEpoch = d.rpc.L1Beacon.SlotsPerEpoch - d.rpc.L1Beacon.SlotInEpoch()
398+
)
390399

391400
latestSeenBlockNumber, err := d.rpc.L1.BlockNumber(d.ctx)
392401
if err != nil {
393402
log.Error("Failed to fetch the latest L1 head for lookahead", "error", err)
394403
continue
395404
}
396405

397-
// Avoid fetching on missed slots, otherwise the previous "current" can get tagged as
398-
// the new "current" for this epoch.
399406
if latestSeenBlockNumber == seenBlockNumber {
400407
// Leave some grace period for the block to arrive.
401408
if lastSlot != currentSlot &&
@@ -412,39 +419,50 @@ func (d *Driver) cacheLookaheadLoop() {
412419
continue
413420
}
414421

415-
seenBlockNumber = latestSeenBlockNumber
416422
lastSlot = currentSlot
423+
seenBlockNumber = latestSeenBlockNumber
417424

418-
var (
419-
currentEpoch = d.rpc.L1Beacon.CurrentEpoch()
420-
remainingSlots = d.rpc.L1Beacon.SlotsPerEpoch - d.rpc.L1Beacon.SlotInEpoch()
421-
)
422-
423-
currentOperatorAddress, err := d.rpc.GetPreconfWhiteListOperator(nil)
425+
currOp, err := d.rpc.GetPreconfWhiteListOperator(nil)
424426
if err != nil {
425-
log.Warn("Failed to get current preconf whitelist operator address", "error", err)
427+
log.Warn("Could not fetch current operator", "err", err)
426428
continue
427429
}
428430

429-
nextOperatorAddress, err := d.rpc.GetNextPreconfWhiteListOperator(nil)
431+
nextOp, err := d.rpc.GetNextPreconfWhiteListOperator(nil)
430432
if err != nil {
431-
log.Warn("Failed to get next preconf whitelist operator address", "error", err)
432-
nextOperatorAddress = rpc.ZeroAddress
433+
log.Warn("Could not fetch next operator", "err", err)
434+
continue
433435
}
434436

435-
// Update the lookahead information for the preconfirmation
436-
// block server.
437+
// push into our 3‑epoch ring
438+
opWin.Push(currentEpoch, currOp, nextOp)
439+
440+
// Push next epoch (nextOp becomes currOp at next epoch)
441+
opWin.Push(currentEpoch+1, nextOp, common.Address{}) // we don't know next-next-op, safe to leave zero
442+
443+
var (
444+
currRanges = opWin.SequencingWindowSplit(d.PreconfOperatorAddress, true)
445+
nextRanges = opWin.SequencingWindowSplit(d.PreconfOperatorAddress, false)
446+
)
447+
437448
d.preconfBlockServer.UpdateLookahead(&preconfBlocks.Lookahead{
438-
CurrOperator: currentOperatorAddress,
439-
NextOperator: nextOperatorAddress,
449+
CurrOperator: currOp,
450+
NextOperator: nextOp,
451+
CurrRanges: currRanges,
452+
NextRanges: nextRanges,
440453
UpdatedAt: time.Now().UTC(),
441454
})
442455

443-
log.Debug("Lookahead information",
444-
"remainingSlots", remainingSlots,
456+
log.Info(
457+
"Lookahead information refreshed",
458+
"currentSlot", currentSlot,
445459
"currentEpoch", currentEpoch,
446-
"currentOperator", currentOperatorAddress.Hex(),
447-
"nextOperator", nextOperatorAddress.Hex(),
460+
"slotsLeftInEpoch", slotsLeftInEpoch,
461+
"slotInEpoch", slotInEpoch,
462+
"currOp", currOp.Hex(),
463+
"nextOp", nextOp.Hex(),
464+
"currRanges", currRanges,
465+
"nextRanges", nextRanges,
448466
)
449467
}
450468
}

packages/taiko-client/driver/preconf_blocks/api.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,7 @@ func (s *PreconfBlockAPIServer) BuildPreconfBlock(c echo.Context) error {
8282

8383
// Check if the fee recipient the current operator or the next operator if its in handover window.
8484
if s.rpc.L1Beacon != nil {
85-
if err := s.checkLookaheadHandover(reqBody.ExecutableData.FeeRecipient, s.rpc.L1Beacon.SlotInEpoch()); err != nil {
85+
if err := s.checkLookaheadHandover(reqBody.ExecutableData.FeeRecipient, s.rpc.L1Beacon.CurrentSlot()); err != nil {
8686
return s.returnError(c, http.StatusBadRequest, err)
8787
}
8888
}
Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,110 @@
11
package preconfblocks
22

33
import (
4+
"sort"
45
"time"
56

67
"github.com/ethereum/go-ethereum/common"
78
)
89

9-
// Lookahead represents the lookahead information in the current beacon consensus.
10+
// SlotRange represents a half‑open [Start,End) range of L1 slots.
11+
type SlotRange struct {
12+
Start uint64
13+
End uint64
14+
}
15+
16+
// mergeRanges coalesces overlapping or adjacent SlotRanges.
17+
func mergeRanges(r []SlotRange) []SlotRange {
18+
if len(r) == 0 {
19+
return r
20+
}
21+
22+
sort.Slice(r, func(i, j int) bool {
23+
return r[i].Start < r[j].Start
24+
})
25+
26+
out := []SlotRange{r[0]}
27+
28+
for _, rng := range r[1:] {
29+
last := &out[len(out)-1]
30+
31+
if rng.Start <= last.End {
32+
if rng.End > last.End {
33+
last.End = rng.End
34+
}
35+
} else {
36+
out = append(out, rng)
37+
}
38+
}
39+
return out
40+
}
41+
42+
// opWindow holds the last three epochs’ operator addresses.
43+
type opWindow struct {
44+
epochs [3]uint64
45+
currOps [3]common.Address
46+
nextOps [3]common.Address
47+
valid [3]bool
48+
handoverSkipSlots uint64
49+
slotsPerEpoch uint64
50+
}
51+
52+
// NewOpWindow creates a new opWindow instance.
53+
func NewOpWindow(handoverSkipSlots, slotsPerEpoch uint64) *opWindow {
54+
return &opWindow{handoverSkipSlots: handoverSkipSlots, slotsPerEpoch: slotsPerEpoch}
55+
}
56+
57+
// Push records the operator pair for one epoch into the ring.
58+
func (w *opWindow) Push(epoch uint64, curr, next common.Address) {
59+
idx := int(epoch % 3)
60+
61+
w.epochs[idx] = epoch
62+
63+
w.currOps[idx] = curr
64+
65+
w.nextOps[idx] = next
66+
67+
w.valid[idx] = true
68+
}
69+
70+
// SequencingWindowSplit creates a slot range for either the current or the next operator
71+
func (w *opWindow) SequencingWindowSplit(operator common.Address, curr bool) []SlotRange {
72+
var ranges []SlotRange
73+
threshold := w.slotsPerEpoch - w.handoverSkipSlots
74+
75+
for i := 0; i < 3; i++ {
76+
if !w.valid[i] {
77+
continue
78+
}
79+
80+
epoch := w.epochs[i]
81+
startEpoch := epoch * w.slotsPerEpoch
82+
83+
if curr {
84+
if w.currOps[i] == operator {
85+
ranges = append(ranges, SlotRange{
86+
Start: startEpoch,
87+
End: startEpoch + threshold,
88+
})
89+
}
90+
} else {
91+
if w.nextOps[i] == operator {
92+
ranges = append(ranges, SlotRange{
93+
Start: startEpoch + threshold,
94+
End: (epoch + 1) * w.slotsPerEpoch,
95+
})
96+
}
97+
}
98+
}
99+
100+
return mergeRanges(ranges)
101+
}
102+
103+
// Lookahead holds the up‑to‑date sequencing window and operator addrs.
10104
type Lookahead struct {
11105
CurrOperator common.Address
12106
NextOperator common.Address
107+
CurrRanges []SlotRange // slots allowed for CurrOperator (0..threshold-1)
108+
NextRanges []SlotRange // slots allowed for NextOperator (threshold..slotsPerEpoch-1)
13109
UpdatedAt time.Time
14110
}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
package preconfblocks
2+
3+
import (
4+
"reflect"
5+
6+
"github.com/ethereum/go-ethereum/common"
7+
)
8+
9+
func (s *PreconfBlockAPIServerTestSuite) TestLookheadMergeRangesEmpty() {
10+
s.Len(mergeRanges([]SlotRange{}), 0, "expected empty slice")
11+
}
12+
13+
func (s *PreconfBlockAPIServerTestSuite) TestLookheadMergeRangesNonOverlapping() {
14+
input := []SlotRange{{Start: 0, End: 5}, {Start: 10, End: 15}}
15+
got := mergeRanges(input)
16+
s.True(reflect.DeepEqual(got, input), "expected %v, got %v", input, got)
17+
}
18+
19+
func (s *PreconfBlockAPIServerTestSuite) TestLookheadMergeRangesOverlapping() {
20+
input := []SlotRange{{Start: 0, End: 5}, {Start: 3, End: 10}, {Start: 8, End: 12}}
21+
want := []SlotRange{{Start: 0, End: 12}}
22+
got := mergeRanges(input)
23+
s.True(reflect.DeepEqual(got, want), "expected %v, got %v", want, got)
24+
}
25+
26+
func (s *PreconfBlockAPIServerTestSuite) TestLookheadMergeRangesAdjacent() {
27+
input := []SlotRange{{Start: 0, End: 5}, {Start: 5, End: 8}}
28+
want := []SlotRange{{Start: 0, End: 8}}
29+
got := mergeRanges(input)
30+
s.True(reflect.DeepEqual(got, want), "expected %v, got %v", want, got)
31+
}
32+
33+
func (s *PreconfBlockAPIServerTestSuite) TestLookheadSequencingWindowSplit() {
34+
handoverSlots := uint64(4)
35+
slotsPerEpoch := uint64(32)
36+
37+
w := NewOpWindow(handoverSlots, slotsPerEpoch)
38+
addr := common.HexToAddress("0xabc")
39+
other := common.HexToAddress("0xdef")
40+
w.Push(0, addr, other) // addr is curr at epoch 0
41+
w.Push(1, other, addr) // addr is next at epoch 1
42+
43+
currRanges := w.SequencingWindowSplit(addr, true)
44+
nextRanges := w.SequencingWindowSplit(addr, false)
45+
46+
s.True(reflect.DeepEqual(currRanges, []SlotRange{{Start: 0, End: 28}}), "currRanges = %v", currRanges)
47+
s.True(reflect.DeepEqual(nextRanges, []SlotRange{{Start: 60, End: 64}}), "nextRanges = %v", nextRanges)
48+
}
49+
50+
func (s *PreconfBlockAPIServerTestSuite) TestLookheadSequencingWindowSplitWithDualEpochPush() {
51+
handoverSlots := uint64(4)
52+
slotsPerEpoch := uint64(32)
53+
54+
w := NewOpWindow(handoverSlots, slotsPerEpoch)
55+
addr := common.HexToAddress("0xabc")
56+
other := common.HexToAddress("0xdef")
57+
w.Push(0, addr, other) // addr is curr at epoch 0
58+
w.Push(1, other, addr) // addr is next at epoch 1
59+
w.Push(2, addr, other) // addr is curr again at epoch 2
60+
61+
currRanges := w.SequencingWindowSplit(addr, true)
62+
nextRanges := w.SequencingWindowSplit(addr, false)
63+
64+
s.True(reflect.DeepEqual(currRanges, []SlotRange{
65+
{Start: 0, End: 28},
66+
{Start: 64, End: 92},
67+
}), "currRanges = %v", currRanges)
68+
69+
s.True(reflect.DeepEqual(nextRanges, []SlotRange{
70+
{Start: 60, End: 64},
71+
}), "nextRanges = %v", nextRanges)
72+
}
73+
74+
func (s *PreconfBlockAPIServerTestSuite) TestLookheadSequencingWindowSplitCurrRange() {
75+
handoverSlots := uint64(4)
76+
slotsPerEpoch := uint64(32)
77+
78+
w := NewOpWindow(handoverSlots, slotsPerEpoch)
79+
addr := common.HexToAddress("0xabc")
80+
w.Push(0, addr, addr)
81+
w.Push(1, addr, common.Address{})
82+
83+
currRanges := w.SequencingWindowSplit(addr, true)
84+
nextRanges := w.SequencingWindowSplit(addr, false)
85+
86+
s.True(reflect.DeepEqual(currRanges, []SlotRange{
87+
{Start: 0, End: 28},
88+
{Start: 32, End: 60},
89+
}), "currRanges = %v", currRanges)
90+
91+
s.True(reflect.DeepEqual(nextRanges, []SlotRange{
92+
{Start: 28, End: 32},
93+
}), "nextRanges = %v", nextRanges)
94+
}

0 commit comments

Comments
 (0)