Skip to content

Commit 5589510

Browse files
committed
feat: add a state version guard
1 parent 8309b93 commit 5589510

3 files changed

Lines changed: 139 additions & 0 deletions

File tree

blockchain/state_version.go

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package blockchain
2+
3+
import (
4+
"errors"
5+
"fmt"
6+
7+
"github.com/NethermindEth/juno/core"
8+
"github.com/NethermindEth/juno/db"
9+
)
10+
11+
// ValidateStateVersion checks that the state implementation selected by useNewState
12+
// is consistent with the data already stored in disk. It must be called before any
13+
// state reads or writes so that incompatible databases are rejected early.
14+
//
15+
// It returns an error if:
16+
// - the database was built with the deprecated state and --new-state is set, or
17+
// - the database was built with the new state and --new-state is not set.
18+
//
19+
// An empty database is always accepted.
20+
func ValidateStateVersion(disk db.KeyValueStore, useNewState bool) error {
21+
// A fresh database has no conflict.
22+
if _, err := core.GetChainHeight(disk); errors.Is(err, db.ErrKeyNotFound) {
23+
return nil
24+
}
25+
26+
hasNewState, err := bucketHasData(disk, db.ClassTrie)
27+
if err != nil {
28+
return fmt.Errorf("probe new-state bucket: %w", err)
29+
}
30+
hasOldState, err := bucketHasData(disk, db.ClassesTrie)
31+
if err != nil {
32+
return fmt.Errorf("probe state bucket: %w", err)
33+
}
34+
35+
switch {
36+
case useNewState && hasOldState:
37+
return errors.New(
38+
"database was built with the existing state implementation, " +
39+
"but --new-state is set; remove --new-state to use this database",
40+
)
41+
case !useNewState && hasNewState:
42+
return errors.New(
43+
"database was built with the new state implementation, " +
44+
"but --new-state is not set; add --new-state to use this database",
45+
)
46+
}
47+
return nil
48+
}
49+
50+
func bucketHasData(disk db.KeyValueStore, bucket db.Bucket) (bool, error) {
51+
it, err := disk.NewIterator(bucket.Key(), false)
52+
if err != nil {
53+
return false, err
54+
}
55+
defer it.Close()
56+
return it.Next(), nil
57+
}

blockchain/state_version_test.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package blockchain_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/NethermindEth/juno/blockchain"
7+
"github.com/NethermindEth/juno/core"
8+
"github.com/NethermindEth/juno/db"
9+
"github.com/NethermindEth/juno/db/memory"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestValidateStateVersion(t *testing.T) {
14+
t.Run("fresh database is always accepted", func(t *testing.T) {
15+
testDB := memory.New()
16+
require.NoError(t, blockchain.ValidateStateVersion(testDB, false))
17+
require.NoError(t, blockchain.ValidateStateVersion(testDB, true))
18+
})
19+
20+
t.Run("deprecated-state DB with new-state flag returns error", func(t *testing.T) {
21+
testDB := memory.New()
22+
seedChainHeight(t, testDB)
23+
seedBucket(t, testDB, db.ClassesTrie)
24+
25+
err := blockchain.ValidateStateVersion(testDB, true)
26+
require.ErrorContains(t, err, "deprecated state")
27+
require.ErrorContains(t, err, "--new-state")
28+
})
29+
30+
t.Run("new-state DB without new-state flag returns error", func(t *testing.T) {
31+
testDB := memory.New()
32+
seedChainHeight(t, testDB)
33+
seedBucket(t, testDB, db.ClassTrie)
34+
35+
err := blockchain.ValidateStateVersion(testDB, false)
36+
require.ErrorContains(t, err, "new state")
37+
require.ErrorContains(t, err, "--new-state")
38+
})
39+
40+
t.Run("deprecated-state DB without new-state flag is accepted", func(t *testing.T) {
41+
testDB := memory.New()
42+
seedChainHeight(t, testDB)
43+
seedBucket(t, testDB, db.ClassesTrie)
44+
45+
require.NoError(t, blockchain.ValidateStateVersion(testDB, false))
46+
})
47+
48+
t.Run("new-state DB with new-state flag is accepted", func(t *testing.T) {
49+
testDB := memory.New()
50+
seedChainHeight(t, testDB)
51+
seedBucket(t, testDB, db.ClassTrie)
52+
53+
require.NoError(t, blockchain.ValidateStateVersion(testDB, true))
54+
})
55+
56+
t.Run("DB with blocks but no trie data is accepted regardless of flag", func(t *testing.T) {
57+
testDB := memory.New()
58+
seedChainHeight(t, testDB)
59+
60+
require.NoError(t, blockchain.ValidateStateVersion(testDB, false))
61+
require.NoError(t, blockchain.ValidateStateVersion(testDB, true))
62+
})
63+
}
64+
65+
func seedChainHeight(t *testing.T, testDB db.KeyValueStore) {
66+
t.Helper()
67+
batch := testDB.NewBatch()
68+
require.NoError(t, core.WriteChainHeight(batch, 1))
69+
require.NoError(t, batch.Write())
70+
}
71+
72+
func seedBucket(t *testing.T, testDB db.KeyValueStore, bucket db.Bucket) {
73+
t.Helper()
74+
batch := testDB.NewBatch()
75+
key := append(bucket.Key(), []byte("sentinel")...)
76+
require.NoError(t, batch.Put(key, []byte("1")))
77+
require.NoError(t, batch.Write())
78+
}

node/node.go

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,10 @@ func New(cfg *Config, version string, logLevel *utils.LogLevel) (*Node, error) {
190190
if err != nil {
191191
return nil, fmt.Errorf("open DB: %w", err)
192192
}
193+
if err = blockchain.ValidateStateVersion(database, cfg.NewState); err != nil {
194+
return nil, err
195+
}
196+
193197
ua := fmt.Sprintf("Juno/%s Starknet Client", version)
194198

195199
services := make([]service.Service, 0)

0 commit comments

Comments
 (0)