Skip to content

Commit e5095a5

Browse files
authored
feat(rpcv10): new INCLUDE_LAST_UPDATE_BLOCK flag for the starknet_getStorageAt method method (#3470)
* feat: add StorageAt to the v10 handler (direct copy from v9 for now) * test: copy TestStorageAt and some test utilities from rpcv9 * chore: update v10 handler to use v10.StorageAt method * feat: AI template for the new feature + tests * feat: implement JSON marshal/unmarshal logic to the StorageAtResult type + tests * docs: add comments for historical bucket constants in buckets.go to clarify purpose and usage * feat: add HistoryBlockNumber method to Reader interface and implement it in Blockchain * test: add unit tests for HistoryLastUpdateBlock function in schema_test.go * refactor: remove useless resolveBlockNumber func and add last update block retrieval logic with HistoryBlockNumber * test: update StorageAt tests with new test cases * test: fix TestStorageAt test case * ci: fix linters * refactor: update StorageAt method to return pointers for result * chore: nitpicks from PR comments * test: remove dedicated JSON marshal/unmarshal tests, create a new validation logic * ci: fix linter conflict * docs: update Iterator interface comments to clarify return values for First, Prev, and Seek methods (based on pebble original docs) * chore: simplify iterator checks in HistoryLastUpdateBlock function * chore: add comment in the linter file explaining the new config * test: replace custom block ID helper functions with direct calls to rpc package methods * refactor: rename StorageAtResult to StorageAtResponse and update related tests * chore: update error messages for unknown storage response flags in JSON unmarshalling * test: add test case for StorageAt with pre-confirmed block ID handling * ci: fix linter in tests
1 parent a0ea97c commit e5095a5

File tree

12 files changed

+816
-25
lines changed

12 files changed

+816
-25
lines changed

.golangci.yaml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,8 @@ linters:
9898
- "24"
9999
- "32"
100100
nolintlint:
101-
allow-unused: false
101+
# permissive here and strict in .golangci_diff.yaml to avoid conflicts
102+
allow-unused: true
102103
require-explanation: false # todo(rdr): this should be switched to true for non test files
103104
require-specific: true
104105
prealloc:

blockchain/blockchain.go

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ type Reader interface {
3535
BlockNumberAndIndexByTxHash(
3636
hash *felt.TransactionHash,
3737
) (blockNumber uint64, index uint64, err error)
38+
HistoryBlockNumber(historyKeyPrefix []byte, upToBlock uint64) (uint64, bool, error)
3839

3940
TransactionByHash(hash *felt.Felt) (transaction core.Transaction, err error)
4041
TransactionByBlockNumberAndIndex(
@@ -636,6 +637,15 @@ func (b *Blockchain) StateAtBlockHash(
636637
), noopStateCloser, nil
637638
}
638639

640+
// HistoryBlockNumber finds the most recent block number (up to upToBlock) recorded
641+
// in a history bucket for the given key prefix.
642+
func (b *Blockchain) HistoryBlockNumber(
643+
historyKeyPrefix []byte, upToBlock uint64,
644+
) (uint64, bool, error) {
645+
b.listener.OnRead("HistoryBlockNumber")
646+
return db.HistoryLastUpdateBlock(b.database, historyKeyPrefix, upToBlock)
647+
}
648+
639649
// EventFilter returns an EventFilter object that is tied to a snapshot of the blockchain
640650
func (b *Blockchain) EventFilter(
641651
addresses []felt.Address,

db/buckets.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,10 +23,15 @@ const (
2323
ReceiptsByBlockNumberAndIndex // maps block number and index to transaction receipt
2424
StateUpdatesByBlockNumber
2525
ClassesTrie
26+
// ContractStorageHistory + Contract address + storage location + block number -> old value.
27+
// For these three history buckets, the block number is when the current value was set, and
28+
// the old value is the value before that.
2629
ContractStorageHistory
30+
// ContractNonceHistory + Contract address + block number -> old nonce.
2731
ContractNonceHistory
32+
// ContractClassHashHistory + Contract address + block number -> old class hash.
2833
ContractClassHashHistory
29-
ContractDeploymentHeight
34+
ContractDeploymentHeight // maps contract addresses to their deployment block number
3035
L1Height
3136
DeprecatedSchemaVersion
3237
Unused // Previously used for storing Pending Block

db/iterator.go

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@ type Iterator interface {
1111
// Valid returns true if the iterator is positioned at a valid key/value pair.
1212
Valid() bool
1313

14-
// First moves the iterator to the first key/value pair.
14+
// First moves the iterator to the first key/value pair. Returns true
15+
// if the iterator is pointing at a valid entry and false otherwise.
1516
First() bool
1617

17-
// Prev moves the iterator to the previous key/value pair
18+
// Prev moves the iterator to the previous key/value pair. Returns true
19+
// if the iterator is pointing at a valid entry and false otherwise.
1820
Prev() bool
1921

2022
// Next moves the iterator to the next key/value pair. It returns whether the
@@ -38,6 +40,7 @@ type Iterator interface {
3840
UncopiedValue() ([]byte, error)
3941

4042
// Seek would seek to the provided key if present. If absent, it would seek to the next
41-
// key in lexicographical order
43+
// key in lexicographical order. Returns true if the iterator is pointing at a valid entry
44+
// and false otherwise.
4245
Seek(key []byte) bool
4346
}

db/schema.go

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -161,3 +161,40 @@ func uint64ToBytes(num uint64) [8]byte {
161161
func StateHashToTrieRootsKey(stateCommitment *felt.StateRootHash) []byte {
162162
return StateHashToTrieRoots.Key(stateCommitment.Marshal())
163163
}
164+
165+
// HistoryLastUpdateBlock finds the most recent block number (up to upToBlock) recorded in
166+
// a history bucket for the given key prefix. All history buckets ([ContractStorageHistory],
167+
// [ContractNonceHistory], and [ContractClassHashHistory]) share the same key layout:
168+
// prefix + uint64 block number in big-endian.
169+
//
170+
// Returns (blockNumber, true, nil) if found, or (0, false, nil) if no history entry
171+
// exists for the prefix.
172+
func HistoryLastUpdateBlock(
173+
reader KeyValueReader,
174+
historyKeyPrefix []byte,
175+
upToBlock uint64,
176+
) (uint64, bool, error) {
177+
it, err := reader.NewIterator(historyKeyPrefix, true)
178+
if err != nil {
179+
return 0, false, err
180+
}
181+
defer it.Close()
182+
183+
seekKey := binary.BigEndian.AppendUint64(historyKeyPrefix, upToBlock)
184+
185+
if it.Seek(seekKey) {
186+
seekedKey := it.Key()
187+
seekedBlock := binary.BigEndian.Uint64(seekedKey[len(historyKeyPrefix):])
188+
if seekedBlock == upToBlock {
189+
return upToBlock, true, nil
190+
}
191+
}
192+
193+
if !it.Prev() {
194+
return 0, false, nil
195+
}
196+
197+
foundKey := it.Key()
198+
blockNum := binary.BigEndian.Uint64(foundKey[len(historyKeyPrefix):])
199+
return blockNum, true, nil
200+
}

db/schema_test.go

Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
package db_test
2+
3+
import (
4+
"testing"
5+
6+
"github.com/NethermindEth/juno/core/felt"
7+
"github.com/NethermindEth/juno/db"
8+
"github.com/NethermindEth/juno/db/memory"
9+
"github.com/stretchr/testify/assert"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestHistoryLastUpdateBlock(t *testing.T) {
14+
addr := felt.NewFromUint64[felt.Felt](1)
15+
loc := felt.NewFromUint64[felt.Felt](2)
16+
prefix := db.ContractStorageHistoryKey(addr, loc)
17+
18+
putAt := func(t *testing.T, database db.KeyValueStore, blockNum uint64) {
19+
t.Helper()
20+
key := db.ContractStorageHistoryAtBlockKey(addr, loc, blockNum)
21+
require.NoError(t, database.Put(key, []byte("value")))
22+
}
23+
24+
t.Run("empty db returns not found", func(t *testing.T) {
25+
database := memory.New()
26+
defer database.Close()
27+
28+
blockNum, found, err := db.HistoryLastUpdateBlock(database, prefix, 10)
29+
require.NoError(t, err)
30+
assert.False(t, found)
31+
assert.Equal(t, uint64(0), blockNum)
32+
})
33+
34+
t.Run("exact match returns upToBlock", func(t *testing.T) {
35+
database := memory.New()
36+
defer database.Close()
37+
38+
putAt(t, database, 5)
39+
40+
blockNum, found, err := db.HistoryLastUpdateBlock(database, prefix, 5)
41+
require.NoError(t, err)
42+
assert.True(t, found)
43+
assert.Equal(t, uint64(5), blockNum)
44+
})
45+
46+
t.Run("returns latest block before upToBlock", func(t *testing.T) {
47+
database := memory.New()
48+
defer database.Close()
49+
50+
putAt(t, database, 3)
51+
putAt(t, database, 7)
52+
putAt(t, database, 10)
53+
54+
blockNum, found, err := db.HistoryLastUpdateBlock(database, prefix, 8)
55+
require.NoError(t, err)
56+
assert.True(t, found)
57+
assert.Equal(t, uint64(7), blockNum)
58+
})
59+
60+
t.Run("all entries are after upToBlock returns not found", func(t *testing.T) {
61+
database := memory.New()
62+
defer database.Close()
63+
64+
putAt(t, database, 5)
65+
putAt(t, database, 10)
66+
67+
blockNum, found, err := db.HistoryLastUpdateBlock(database, prefix, 3)
68+
require.NoError(t, err)
69+
assert.False(t, found)
70+
assert.Equal(t, uint64(0), blockNum)
71+
})
72+
73+
t.Run("multiple entries with exact match at upToBlock", func(t *testing.T) {
74+
database := memory.New()
75+
defer database.Close()
76+
77+
putAt(t, database, 1)
78+
putAt(t, database, 5)
79+
putAt(t, database, 9)
80+
81+
blockNum, found, err := db.HistoryLastUpdateBlock(database, prefix, 9)
82+
require.NoError(t, err)
83+
assert.True(t, found)
84+
assert.Equal(t, uint64(9), blockNum)
85+
})
86+
87+
t.Run("different contract prefix does not affect result", func(t *testing.T) {
88+
database := memory.New()
89+
defer database.Close()
90+
91+
otherAddr := new(felt.Felt).SetUint64(99)
92+
otherPrefix := db.ContractStorageHistoryKey(otherAddr, loc)
93+
94+
putAt(t, database, 5)
95+
96+
// Write entries under a different prefix
97+
otherKey := db.ContractStorageHistoryAtBlockKey(otherAddr, loc, 3)
98+
require.NoError(t, database.Put(otherKey, []byte("other")))
99+
100+
blockNum, found, err := db.HistoryLastUpdateBlock(database, prefix, 10)
101+
require.NoError(t, err)
102+
assert.True(t, found)
103+
assert.Equal(t, uint64(5), blockNum)
104+
105+
// Query the other prefix
106+
blockNum, found, err = db.HistoryLastUpdateBlock(database, otherPrefix, 10)
107+
require.NoError(t, err)
108+
assert.True(t, found)
109+
assert.Equal(t, uint64(3), blockNum)
110+
})
111+
}

mocks/mock_blockchain.go

Lines changed: 30 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

rpc/handlers.go

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -218,9 +218,12 @@ func (h *Handler) MethodsV0_10() ([]jsonrpc.Method, string) {
218218
{
219219
Name: "starknet_getStorageAt",
220220
Params: []jsonrpc.Parameter{
221-
{Name: "contract_address"}, {Name: "key"}, {Name: "block_id"},
221+
{Name: "contract_address"},
222+
{Name: "key"},
223+
{Name: "block_id"},
224+
{Name: "response_flags", Optional: true},
222225
},
223-
Handler: h.rpcv9Handler.StorageAt,
226+
Handler: h.rpcv10Handler.StorageAt,
224227
},
225228
{
226229
Name: "starknet_getClassHashAt",

0 commit comments

Comments
 (0)