Skip to content

Commit 95c9515

Browse files
authored
Merge branch 'main' into perf-pure-go-cairo0-class-hash
2 parents 3933b97 + 7c2d309 commit 95c9515

8 files changed

Lines changed: 301 additions & 55 deletions

File tree

db/memory/batch.go

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -132,12 +132,18 @@ func (b *batch) DeleteRange(start, end []byte) error {
132132
return errBatchClosed
133133
}
134134

135-
it, err := b.NewIterator(start, false)
135+
// Range-based, matching pebble's DeleteRange semantics: delete every
136+
// key in [start, end). We iterate with a nil prefix (all keys), Seek
137+
// to start, and stop at end. Prefix-bounded iteration would miss keys
138+
// whose first bytes only partially share `start` — e.g. a chunk
139+
// spanning multiple per-block entries under one address prefix.
140+
it, err := b.NewIterator(nil, false)
136141
if err != nil {
137142
return err
138143
}
144+
defer it.Close()
139145

140-
for it.Next() {
146+
for ok := it.Seek(start); ok; ok = it.Next() {
141147
if bytes.Compare(it.Key(), end) >= 0 {
142148
break
143149
}

db/testutil.go

Lines changed: 98 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -571,64 +571,89 @@ func TestKeyValueStoreSuite(t *testing.T, newDB func() KeyValueStore) {
571571
database := newDB()
572572
defer database.Close()
573573

574-
// Helper to add a range of keys
575-
addRange := func(start, stop int) {
576-
for i := start; i <= stop; i++ {
577-
key := strconv.Itoa(i)
578-
value := "value-" + key
579-
err := database.Put([]byte(key), []byte(value))
580-
require.NoError(t, err, "Put operation failed for key %s", key)
581-
}
582-
}
583-
584-
// Helper to check if a range of keys exists
585-
checkRange := func(start, stop int, expected bool) {
586-
for i := start; i <= stop; i++ {
587-
key := []byte(strconv.Itoa(i))
588-
has, err := database.Has(key)
589-
require.NoError(t, err, "Has operation failed for key %s", key)
590-
if expected {
591-
require.True(t, has, "key %s should exist", key)
592-
} else {
593-
require.False(t, has, "key %s should not exist", key)
594-
}
595-
}
596-
}
597-
598-
addRange(1, 9)
574+
addRange(t, database, 9)
599575
require.NoError(t, database.DeleteRange([]byte("9"), []byte("1"))) // no-op
600-
checkRange(1, 9, true)
576+
checkRange(t, database, 1, 9, true)
601577
require.NoError(t, database.DeleteRange([]byte("5"), []byte("5"))) // no-op, exclusive end
602-
checkRange(1, 9, true)
578+
checkRange(t, database, 1, 9, true)
603579
require.NoError(t, database.DeleteRange([]byte("5"), []byte("50"))) // delete only key 5
604-
checkRange(1, 4, true)
605-
checkRange(5, 5, false)
606-
checkRange(6, 9, true)
580+
checkRange(t, database, 1, 4, true)
581+
checkRange(t, database, 5, 5, false)
582+
checkRange(t, database, 6, 9, true)
607583
require.NoError(t, database.DeleteRange([]byte(""), []byte("a"))) // delete all
608-
checkRange(1, 9, false)
584+
checkRange(t, database, 1, 9, false)
609585

610-
addRange(1, 999)
586+
addRange(t, database, 999)
611587
require.NoError(t, database.DeleteRange([]byte("12345"), []byte("54321")))
612-
checkRange(1, 1, true)
613-
checkRange(2, 5, false)
614-
checkRange(6, 12, true)
615-
checkRange(13, 54, false)
616-
checkRange(55, 123, true)
617-
checkRange(124, 543, false)
618-
checkRange(544, 999, true)
619-
620-
addRange(1, 999)
588+
checkRange(t, database, 1, 1, true)
589+
checkRange(t, database, 2, 5, false)
590+
checkRange(t, database, 6, 12, true)
591+
checkRange(t, database, 13, 54, false)
592+
checkRange(t, database, 55, 123, true)
593+
checkRange(t, database, 124, 543, false)
594+
checkRange(t, database, 544, 999, true)
595+
596+
addRange(t, database, 999)
621597
require.NoError(t, database.DeleteRange([]byte("3"), []byte("7")))
622-
checkRange(1, 2, true)
623-
checkRange(3, 6, false)
624-
checkRange(7, 29, true)
625-
checkRange(30, 69, false)
626-
checkRange(70, 299, true)
627-
checkRange(300, 699, false)
628-
checkRange(700, 999, true)
598+
checkRange(t, database, 1, 2, true)
599+
checkRange(t, database, 3, 6, false)
600+
checkRange(t, database, 7, 29, true)
601+
checkRange(t, database, 30, 69, false)
602+
checkRange(t, database, 70, 299, true)
603+
checkRange(t, database, 300, 699, false)
604+
checkRange(t, database, 700, 999, true)
629605

630606
require.NoError(t, database.DeleteRange([]byte(""), []byte("a")))
631-
checkRange(1, 999, false)
607+
checkRange(t, database, 1, 999, false)
608+
})
609+
610+
// Mirrors the DeleteRange subtest above but routes every DeleteRange
611+
// through a Batch + Write, pinning batch and direct paths to the same
612+
// behavior across backends.
613+
t.Run("BatchDeleteRange", func(t *testing.T) {
614+
database := newDB()
615+
defer database.Close()
616+
617+
deleteRangeViaBatch := func(start, end []byte) {
618+
batch := database.NewBatch()
619+
require.NoError(t, batch.DeleteRange(start, end))
620+
require.NoError(t, batch.Write())
621+
}
622+
623+
addRange(t, database, 9)
624+
deleteRangeViaBatch([]byte("9"), []byte("1"))
625+
checkRange(t, database, 1, 9, true)
626+
deleteRangeViaBatch([]byte("5"), []byte("5"))
627+
checkRange(t, database, 1, 9, true)
628+
deleteRangeViaBatch([]byte("5"), []byte("50"))
629+
checkRange(t, database, 1, 4, true)
630+
checkRange(t, database, 5, 5, false)
631+
checkRange(t, database, 6, 9, true)
632+
deleteRangeViaBatch([]byte(""), []byte("a"))
633+
checkRange(t, database, 1, 9, false)
634+
635+
addRange(t, database, 999)
636+
deleteRangeViaBatch([]byte("12345"), []byte("54321"))
637+
checkRange(t, database, 1, 1, true)
638+
checkRange(t, database, 2, 5, false)
639+
checkRange(t, database, 6, 12, true)
640+
checkRange(t, database, 13, 54, false)
641+
checkRange(t, database, 55, 123, true)
642+
checkRange(t, database, 124, 543, false)
643+
checkRange(t, database, 544, 999, true)
644+
645+
addRange(t, database, 999)
646+
deleteRangeViaBatch([]byte("3"), []byte("7"))
647+
checkRange(t, database, 1, 2, true)
648+
checkRange(t, database, 3, 6, false)
649+
checkRange(t, database, 7, 29, true)
650+
checkRange(t, database, 30, 69, false)
651+
checkRange(t, database, 70, 299, true)
652+
checkRange(t, database, 300, 699, false)
653+
checkRange(t, database, 700, 999, true)
654+
655+
deleteRangeViaBatch([]byte(""), []byte("a"))
656+
checkRange(t, database, 1, 999, false)
632657
})
633658

634659
t.Run("Snapshot", func(t *testing.T) {
@@ -797,6 +822,30 @@ func TestKeyValueStoreSuite(t *testing.T, newDB func() KeyValueStore) {
797822
})
798823
}
799824

825+
// Helper to add keys 1..stop.
826+
func addRange(t *testing.T, database KeyValueStore, stop int) {
827+
for i := 1; i <= stop; i++ {
828+
key := strconv.Itoa(i)
829+
value := "value-" + key
830+
err := database.Put([]byte(key), []byte(value))
831+
require.NoError(t, err, "Put operation failed for key %s", key)
832+
}
833+
}
834+
835+
// Helper to check if a range of keys exists
836+
func checkRange(t *testing.T, database KeyValueStore, start, stop int, expected bool) {
837+
for i := start; i <= stop; i++ {
838+
key := []byte(strconv.Itoa(i))
839+
has, err := database.Has(key)
840+
require.NoError(t, err, "Has operation failed for key %s", key)
841+
if expected {
842+
require.True(t, has, "key %s should exist", key)
843+
} else {
844+
require.False(t, has, "key %s should not exist", key)
845+
}
846+
}
847+
}
848+
800849
func assertIteratorErrorsAfterClose(t *testing.T, it Iterator) {
801850
t.Helper()
802851
require.NoError(t, it.Close(), "iterator close should succeed")

l1/l1.go

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,17 @@ func (c *Client) Run(ctx context.Context) error {
167167
return c.watchL1StateUpdates(ctx)
168168
}
169169

170+
// CatchUpL1Head verifies the chain ID then writes the L1 head to the
171+
// database, without entering the live subscription loop. Closes the
172+
// underlying Subscriber on return; the Client must not be reused.
173+
func (c *Client) CatchUpL1Head(ctx context.Context) error {
174+
defer c.l1.Close()
175+
if err := c.checkChainID(ctx); err != nil {
176+
return err
177+
}
178+
return c.catchUpL1HeadUpdates(ctx)
179+
}
180+
170181
func (c *Client) watchL1StateUpdates(ctx context.Context) error {
171182
buffer := 128
172183

l1/l1_test.go

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -281,6 +281,74 @@ func TestEventListenerCatchUp(t *testing.T) {
281281
require.Equal(t, *want, persisted)
282282
}
283283

284+
// TestCatchUpL1Head is the regression for the history-pruning migration
285+
// bootstrap (node/migration.go). The migration crashes with a bare "key
286+
// not found" if it runs before any L1 head is on disk; the fix calls
287+
// Client.CatchUpL1Head in node/migration.go to write one up front. This
288+
// test pins the contract that CatchUpL1Head used by that path actually
289+
// persists the head — if a future change drops the setL1Head call inside
290+
// catchUpL1HeadUpdates, or short-circuits Client.CatchUpL1Head before it
291+
// reaches catch-up, chain.L1Head() below fails and the bug returns.
292+
func TestCatchUpL1Head(t *testing.T) {
293+
t.Parallel()
294+
295+
ctrl := gomock.NewController(t)
296+
nopLog := log.NewNopZapLogger()
297+
network := networks.Mainnet
298+
chain := blockchain.New(
299+
memory.New(),
300+
&network,
301+
blockchain.WithNewState(statetestutils.UseNewState()),
302+
)
303+
304+
subscriber := mocks.NewMockSubscriber(ctrl)
305+
subscriber.EXPECT().ChainID(gomock.Any()).Return(network.L1ChainID, nil).AnyTimes()
306+
subscriber.EXPECT().LatestHeight(gomock.Any()).Return(uint64(10), nil).AnyTimes()
307+
subscriber.EXPECT().FinalisedHeight(gomock.Any()).Return(uint64(5), nil).AnyTimes()
308+
subscriber.
309+
EXPECT().
310+
FilterLogStateUpdate(gomock.Any(), uint64(0), uint64(10)).
311+
Return([]*contract.StarknetLogStateUpdate{{
312+
BlockNumber: new(big.Int).SetUint64(7),
313+
BlockHash: new(big.Int).SetUint64(7),
314+
GlobalRoot: new(big.Int).SetUint64(7),
315+
Raw: types.Log{BlockNumber: 3},
316+
}}, nil).
317+
AnyTimes()
318+
subscriber.EXPECT().Close().AnyTimes()
319+
320+
client := l1.NewClient(subscriber, chain, nopLog)
321+
require.NoError(t, client.CatchUpL1Head(t.Context()))
322+
323+
persisted, err := chain.L1Head()
324+
require.NoError(t, err)
325+
require.Equal(t, core.L1Head{
326+
BlockNumber: 7,
327+
BlockHash: new(felt.Felt).SetUint64(7),
328+
StateRoot: new(felt.Felt).SetUint64(7),
329+
}, persisted)
330+
}
331+
332+
func TestCatchUpL1Head_ChainIDMismatch(t *testing.T) {
333+
t.Parallel()
334+
335+
ctrl := gomock.NewController(t)
336+
nopLog := log.NewNopZapLogger()
337+
network := networks.Mainnet
338+
chain := blockchain.New(
339+
memory.New(),
340+
&network,
341+
blockchain.WithNewState(statetestutils.UseNewState()),
342+
)
343+
344+
subscriber := mocks.NewMockSubscriber(ctrl)
345+
subscriber.EXPECT().ChainID(gomock.Any()).Return(big.NewInt(999), nil)
346+
subscriber.EXPECT().Close()
347+
348+
err := l1.NewClient(subscriber, chain, nopLog).CatchUpL1Head(t.Context())
349+
require.ErrorContains(t, err, "mismatched L1 and L2 networks")
350+
}
351+
284352
func newTestL1Client(service service) *rpc.Server {
285353
server := rpc.NewServer()
286354
if err := server.RegisterName("eth", service); err != nil {

node/migration.go

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ package node
22

33
import (
44
"context"
5+
"errors"
56
"fmt"
67

8+
"github.com/NethermindEth/juno/blockchain"
9+
"github.com/NethermindEth/juno/core"
710
"github.com/NethermindEth/juno/db"
811
"github.com/NethermindEth/juno/migration"
912
"github.com/NethermindEth/juno/migration/blocktransactions"
@@ -32,26 +35,34 @@ func registerMigrations(cfg *Config) *migration.Registry {
3235
// migration progress via health check endpoints.
3336
func migrateIfNeeded(
3437
ctx context.Context,
35-
db db.KeyValueStore,
38+
database db.KeyValueStore,
3639
config *Config,
40+
chain *blockchain.Blockchain,
3741
logger log.Logger,
3842
) error {
3943
migrateFn := func() error {
4044
// Run deprecated migrations first
4145
if err := deprecated.MigrateIfNeeded(
4246
ctx,
43-
db,
47+
database,
4448
&config.Network,
4549
logger,
4650
); err != nil {
4751
return fmt.Errorf("deprecated migration failed: %w", err)
4852
}
4953

54+
// Make sure there is an available L1 head before starting pruning migration
55+
if config.Prune {
56+
if err := fetchL1HeadIfMissing(ctx, database, config, chain, logger); err != nil {
57+
return fmt.Errorf("fetch L1 head for pruning: %w", err)
58+
}
59+
}
60+
5061
// Run new migrations
5162
registry := registerMigrations(config)
5263
runner, err := migration.NewRunner(
5364
registry,
54-
db,
65+
database,
5566
&config.Network,
5667
logger,
5768
)
@@ -73,3 +84,38 @@ func migrateIfNeeded(
7384

7485
return migrateFn()
7586
}
87+
88+
// fetchL1HeadIfMissing writes an L1 head to disk before the history pruning
89+
// migration reads it. No-op when one is already stored.
90+
func fetchL1HeadIfMissing(
91+
ctx context.Context,
92+
database db.KeyValueStore,
93+
config *Config,
94+
chain *blockchain.Blockchain,
95+
logger log.StructuredLogger,
96+
) error {
97+
_, err := core.GetL1Head(database)
98+
if err == nil {
99+
return nil
100+
}
101+
if !errors.Is(err, db.ErrKeyNotFound) {
102+
return err
103+
}
104+
105+
logger.Info("Fetching the L1 head before running the prune migration")
106+
client, err := newL1Client(config.EthNode, config.Metrics, chain, logger)
107+
if err != nil {
108+
return fmt.Errorf("creating a new L1 client: %w", err)
109+
}
110+
if err := client.CatchUpL1Head(ctx); err != nil {
111+
return err
112+
}
113+
114+
if _, err := core.GetL1Head(database); err != nil {
115+
if errors.Is(err, db.ErrKeyNotFound) {
116+
return errors.New("couldn't find a finalized Starknet state update on L1")
117+
}
118+
return err
119+
}
120+
return nil
121+
}

0 commit comments

Comments
 (0)