Skip to content

Commit d0f670d

Browse files
committed
feat: copy StorageProof method from v9 into v10 + tests
1 parent 0e5535d commit d0f670d

3 files changed

Lines changed: 1345 additions & 1 deletion

File tree

rpc/handlers.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -374,7 +374,7 @@ func (h *Handler) MethodsV0_10() ([]jsonrpc.Method, string) {
374374
{Name: "contract_addresses", Optional: true},
375375
{Name: "contracts_storage_keys", Optional: true},
376376
},
377-
Handler: h.rpcv9Handler.StorageProof,
377+
Handler: h.rpcv10Handler.StorageProof,
378378
},
379379
}, "/v0_10"
380380
}

rpc/v10/storage.go

Lines changed: 366 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,21 @@ import (
55
"errors"
66
"fmt"
77

8+
"github.com/NethermindEth/juno/core"
89
"github.com/NethermindEth/juno/core/felt"
10+
"github.com/NethermindEth/juno/core/trie"
911
"github.com/NethermindEth/juno/db"
1012
"github.com/NethermindEth/juno/jsonrpc"
1113
"github.com/NethermindEth/juno/rpc/rpccore"
14+
"github.com/NethermindEth/juno/utils"
1215
"go.uber.org/zap"
1316
)
1417

18+
const (
19+
MissingContractAddress = "missing field: contract_address"
20+
MissingStorageKeys = "missing field: storage_keys"
21+
)
22+
1523
// StorageAtResponseFlags represents the flags for the `starknet_getStorageAt` operation.
1624
type StorageAtResponseFlags struct {
1725
IncludeLastUpdateBlock bool
@@ -71,6 +79,10 @@ func (st *StorageAtResponse) UnmarshalJSON(data []byte) error {
7179
return st.Value.UnmarshalJSON(data)
7280
}
7381

82+
/****************************************************
83+
Contract Handlers
84+
*****************************************************/
85+
7486
// StorageAt gets the value of the storage at the given address and key.
7587
//
7688
// It follows the specification defined here:
@@ -125,3 +137,357 @@ func (h *Handler) StorageAt(
125137

126138
return &result, nil
127139
}
140+
141+
type StorageProofResult struct {
142+
ClassesProof []*HashToNode `json:"classes_proof"`
143+
ContractsProof *ContractProof `json:"contracts_proof"`
144+
ContractsStorageProofs [][]*HashToNode `json:"contracts_storage_proofs"`
145+
GlobalRoots *GlobalRoots `json:"global_roots"`
146+
}
147+
148+
// StorageAt get merkle paths in one of the state tries: global state, classes,
149+
// individual contract. A single request can query for any mix of the three types
150+
// of storage proofs (classes, contracts, and storage).
151+
//
152+
// It follows the specification defined here:
153+
// https://github.com/starkware-libs/starknet-specs/blob/release/v0.10.2/api/starknet_api_openrpc.json#L980
154+
func (h *Handler) StorageProof(
155+
id *BlockID, classes, contracts []felt.Felt, storageKeys []StorageKeys,
156+
) (*StorageProofResult, *jsonrpc.Error) {
157+
state, closer, err := h.bcReader.HeadState()
158+
if err != nil {
159+
return nil, rpccore.ErrInternal.CloneWithData(err)
160+
}
161+
162+
chainHeight, err := h.bcReader.Height()
163+
if err != nil {
164+
return nil, rpccore.ErrInternal.CloneWithData(err)
165+
}
166+
167+
// TODO(infrmtcs): This is still a half baked solution because we're using another
168+
// transaction to get the block number. We don't use the head query directly to avoid
169+
// race condition where there is a new incoming block. Currently it's still working
170+
// because we don't have revert yet. We should figure out a way to merge the two transactions.
171+
header, err := h.bcReader.BlockHeaderByNumber(chainHeight)
172+
if err != nil {
173+
return nil, rpccore.ErrInternal.CloneWithData(err)
174+
}
175+
176+
// We do not support historical storage proofs for now
177+
// Ensure that the block requested is the head block
178+
if rpcErr := h.isBlockSupported(id, chainHeight); rpcErr != nil {
179+
return nil, rpcErr
180+
}
181+
182+
defer h.callAndLogErr(closer, "Error closing state reader in getStorageProof")
183+
184+
classTrie, err := state.ClassTrie()
185+
if err != nil {
186+
return nil, rpccore.ErrInternal.CloneWithData(err)
187+
}
188+
189+
contractTrie, err := state.ContractTrie()
190+
if err != nil {
191+
return nil, rpccore.ErrInternal.CloneWithData(err)
192+
}
193+
194+
// Do a sanity check and remove duplicates from the inputs
195+
classes = utils.Set(classes)
196+
contracts = utils.Set(contracts)
197+
uniqueStorageKeys, rpcErr := processStorageKeys(storageKeys)
198+
if rpcErr != nil {
199+
return nil, rpcErr
200+
}
201+
202+
classProof, err := getClassProof(classTrie, classes)
203+
if err != nil {
204+
return nil, rpccore.ErrInternal.CloneWithData(err)
205+
}
206+
207+
contractProof, err := getContractProof(contractTrie, state, contracts)
208+
if err != nil {
209+
return nil, rpccore.ErrInternal.CloneWithData(err)
210+
}
211+
212+
contractStorageProof, err := getContractStorageProof(state, uniqueStorageKeys)
213+
if err != nil {
214+
return nil, rpccore.ErrInternal.CloneWithData(err)
215+
}
216+
217+
contractTreeRoot, err := contractTrie.Hash()
218+
if err != nil {
219+
return nil, rpccore.ErrInternal.CloneWithData(err)
220+
}
221+
222+
classTreeRoot, err := classTrie.Hash()
223+
if err != nil {
224+
return nil, rpccore.ErrInternal.CloneWithData(err)
225+
}
226+
227+
return &StorageProofResult{
228+
ClassesProof: classProof,
229+
ContractsProof: contractProof,
230+
ContractsStorageProofs: contractStorageProof,
231+
GlobalRoots: &GlobalRoots{
232+
ContractsTreeRoot: &contractTreeRoot,
233+
ClassesTreeRoot: &classTreeRoot,
234+
BlockHash: header.Hash,
235+
},
236+
}, nil
237+
}
238+
239+
// Ensures each contract is unique and each storage key in each contract is unique
240+
func processStorageKeys(storageKeys []StorageKeys) ([]StorageKeys, *jsonrpc.Error) {
241+
if len(storageKeys) == 0 {
242+
return nil, nil
243+
}
244+
245+
merged := make(map[felt.Felt][]felt.Felt, len(storageKeys))
246+
for _, sk := range storageKeys {
247+
// Ensure that both contract and keys are provided
248+
if sk.Contract == nil {
249+
return nil, jsonrpc.Err(jsonrpc.InvalidParams, MissingContractAddress)
250+
}
251+
if len(sk.Keys) == 0 {
252+
return nil, jsonrpc.Err(jsonrpc.InvalidParams, MissingStorageKeys)
253+
}
254+
255+
contract := *sk.Contract
256+
merged[contract] = append(merged[contract], sk.Keys...)
257+
}
258+
259+
uniqueStorageKeys := make([]StorageKeys, 0, len(merged))
260+
for contract, keys := range merged {
261+
uniqueStorageKeys = append(
262+
uniqueStorageKeys,
263+
StorageKeys{Contract: &contract, Keys: utils.Set(keys)},
264+
)
265+
}
266+
267+
return uniqueStorageKeys, nil
268+
}
269+
270+
// isBlockSupported checks if the block ID requested is supported for storage proofs
271+
// Currently returns true only if the block ID requested matches the head block
272+
func (h *Handler) isBlockSupported(blockID *BlockID, chainHeight uint64) *jsonrpc.Error {
273+
var blockNumber uint64
274+
switch {
275+
case blockID.IsLatest():
276+
return nil
277+
case blockID.IsPreConfirmed():
278+
return rpccore.ErrCallOnPreConfirmed
279+
case blockID.IsHash():
280+
header, err := h.bcReader.BlockHeaderByHash(blockID.Hash())
281+
if err != nil {
282+
if errors.Is(err, db.ErrKeyNotFound) {
283+
return rpccore.ErrBlockNotFound
284+
}
285+
return rpccore.ErrInternal.CloneWithData(err)
286+
}
287+
blockNumber = header.Number
288+
case blockID.IsNumber():
289+
blockNumber = blockID.Number()
290+
case blockID.IsL1Accepted():
291+
return rpccore.ErrStorageProofNotSupported
292+
default:
293+
panic(fmt.Sprintf("invalid block id type %d", blockID.Type()))
294+
}
295+
296+
switch {
297+
case blockNumber < chainHeight:
298+
return rpccore.ErrStorageProofNotSupported
299+
case blockNumber > chainHeight:
300+
return rpccore.ErrBlockNotFound
301+
}
302+
return nil
303+
}
304+
305+
func getClassProof(tr core.Trie, classes []felt.Felt) ([]*HashToNode, error) {
306+
// TODO(maksym): remove after trie2 integration. RPC packages shouldn't
307+
// care about which trie implementation is being used and the output format should be the same
308+
t, ok := tr.(*trie.Trie)
309+
if !ok {
310+
return nil, fmt.Errorf("unknown trie type: %T", tr)
311+
}
312+
313+
classProof := trie.NewProofNodeSet()
314+
for _, class := range classes {
315+
if err := t.Prove(&class, classProof); err != nil {
316+
return nil, err
317+
}
318+
}
319+
320+
return adaptProofNodes(classProof), nil
321+
}
322+
323+
func getContractProof(
324+
tr core.Trie,
325+
state core.StateReader,
326+
contracts []felt.Felt,
327+
) (*ContractProof, error) {
328+
// TODO(maksym): remove after trie2 integration. RPC packages shouldn't
329+
// care about which trie implementation is being used and the output format should be the same
330+
t, ok := tr.(*trie.Trie)
331+
if !ok {
332+
return nil, fmt.Errorf("unknown trie type: %T", tr)
333+
}
334+
335+
contractProof := trie.NewProofNodeSet()
336+
contractLeavesData := make([]*LeafData, len(contracts))
337+
for i, contract := range contracts {
338+
if err := t.Prove(&contract, contractProof); err != nil {
339+
return nil, err
340+
}
341+
342+
classHash, err := state.ContractClassHash(&contract)
343+
if err != nil {
344+
if errors.Is(err, db.ErrKeyNotFound) { // contract does not exist, skip getting leaf data
345+
continue
346+
}
347+
return nil, err
348+
}
349+
350+
nonce, err := state.ContractNonce(&contract)
351+
if err != nil {
352+
return nil, err
353+
}
354+
355+
contractStorageTrie, err := state.ContractStorageTrie(&contract)
356+
if err != nil {
357+
return nil, err
358+
}
359+
360+
storageRoot, err := contractStorageTrie.Hash()
361+
if err != nil {
362+
return nil, err
363+
}
364+
365+
contractLeavesData[i] = &LeafData{
366+
Nonce: &nonce,
367+
ClassHash: &classHash,
368+
StorageRoot: &storageRoot,
369+
}
370+
}
371+
372+
return &ContractProof{
373+
Nodes: adaptProofNodes(contractProof),
374+
LeavesData: contractLeavesData,
375+
}, nil
376+
}
377+
378+
func getContractStorageProof(
379+
state core.StateReader,
380+
storageKeys []StorageKeys,
381+
) ([][]*HashToNode, error) {
382+
contractStorageRes := make([][]*HashToNode, len(storageKeys))
383+
for i, storageKey := range storageKeys {
384+
contractStorageTrie, err := state.ContractStorageTrie(storageKey.Contract)
385+
if err != nil {
386+
return nil, err
387+
}
388+
389+
t, ok := contractStorageTrie.(*trie.Trie)
390+
if !ok {
391+
return nil, fmt.Errorf("unknown trie type: %T", t)
392+
}
393+
contractStorageProof := trie.NewProofNodeSet()
394+
for _, key := range storageKey.Keys {
395+
if err := t.Prove(&key, contractStorageProof); err != nil {
396+
return nil, err
397+
}
398+
}
399+
400+
contractStorageRes[i] = adaptProofNodes(contractStorageProof)
401+
}
402+
403+
return contractStorageRes, nil
404+
}
405+
406+
func adaptProofNodes(proof *trie.ProofNodeSet) []*HashToNode {
407+
nodes := make([]*HashToNode, proof.Size())
408+
nodeList := proof.List()
409+
for i, hash := range proof.Keys() {
410+
var node Node
411+
412+
switch n := nodeList[i].(type) {
413+
case *trie.Binary:
414+
node = &BinaryNode{
415+
Left: n.LeftHash,
416+
Right: n.RightHash,
417+
}
418+
case *trie.Edge:
419+
path := n.Path.Felt()
420+
node = &EdgeNode{
421+
Path: path.String(),
422+
Length: int(n.Path.Len()),
423+
Child: n.Child,
424+
}
425+
}
426+
427+
nodes[i] = &HashToNode{
428+
Hash: &hash,
429+
Node: node,
430+
}
431+
}
432+
433+
return nodes
434+
}
435+
436+
type StorageKeys struct {
437+
Contract *felt.Felt `json:"contract_address"`
438+
Keys []felt.Felt `json:"storage_keys"`
439+
}
440+
441+
type Node interface {
442+
AsProofNode() trie.ProofNode
443+
}
444+
445+
type BinaryNode struct {
446+
Left *felt.Felt `json:"left"`
447+
Right *felt.Felt `json:"right"`
448+
}
449+
450+
type EdgeNode struct {
451+
Path string `json:"path"`
452+
Length int `json:"length"`
453+
Child *felt.Felt `json:"child"`
454+
}
455+
456+
func (e *EdgeNode) AsProofNode() trie.ProofNode {
457+
f, _ := felt.NewFromString[felt.Felt](e.Path)
458+
pbs := f.Bytes()
459+
460+
return &trie.Edge{
461+
Path: new(trie.BitArray).SetBytes(uint8(e.Length), pbs[:]),
462+
Child: e.Child,
463+
}
464+
}
465+
466+
func (b *BinaryNode) AsProofNode() trie.ProofNode {
467+
return &trie.Binary{
468+
LeftHash: b.Left,
469+
RightHash: b.Right,
470+
}
471+
}
472+
473+
type HashToNode struct {
474+
Hash *felt.Felt `json:"node_hash"`
475+
Node Node `json:"node"`
476+
}
477+
478+
type LeafData struct {
479+
Nonce *felt.Felt `json:"nonce"`
480+
ClassHash *felt.Felt `json:"class_hash"`
481+
StorageRoot *felt.Felt `json:"storage_root"`
482+
}
483+
484+
type ContractProof struct {
485+
Nodes []*HashToNode `json:"nodes"`
486+
LeavesData []*LeafData `json:"contract_leaves_data"`
487+
}
488+
489+
type GlobalRoots struct {
490+
ContractsTreeRoot *felt.Felt `json:"contracts_tree_root"`
491+
ClassesTreeRoot *felt.Felt `json:"classes_tree_root"`
492+
BlockHash *felt.Felt `json:"block_hash"`
493+
}

0 commit comments

Comments
 (0)