diff --git a/core/state.go b/core/state.go index 71198afbe6..5e5e89f0a3 100644 --- a/core/state.go +++ b/core/state.go @@ -129,6 +129,18 @@ func (s *State) storage() (*trie.Trie, func() error, error) { return s.globalTrie(db.StateTrie, trie.NewTriePedersen) } +func (s *State) StorageTrie() (*trie.Trie, func() error, error) { + return s.storage() +} + +func (s *State) ClassTrie() (*trie.Trie, func() error, error) { + return s.classesTrie() +} + +func (s *State) StorageTrieForAddr(addr *felt.Felt) (*trie.Trie, error) { + return storage(addr, s.txn) +} + func (s *State) classesTrie() (*trie.Trie, func() error, error) { return s.globalTrie(db.ClassesTrie, trie.NewTriePoseidon) } diff --git a/rpc/contract.go b/rpc/contract.go index e0eb3b7c2a..ba7de93029 100644 --- a/rpc/contract.go +++ b/rpc/contract.go @@ -27,22 +27,3 @@ func (h *Handler) Nonce(id BlockID, address felt.Felt) (*felt.Felt, *jsonrpc.Err return nonce, nil } - -// StorageAt gets the value of the storage at the given address and key. -// -// It follows the specification defined here: -// https://github.com/starkware-libs/starknet-specs/blob/a789ccc3432c57777beceaa53a34a7ae2f25fda0/api/starknet_api_openrpc.json#L110 -func (h *Handler) StorageAt(address, key felt.Felt, id BlockID) (*felt.Felt, *jsonrpc.Error) { - stateReader, stateCloser, rpcErr := h.stateByBlockID(&id) - if rpcErr != nil { - return nil, rpcErr - } - defer h.callAndLogErr(stateCloser, "Error closing state reader in getStorageAt") - - value, err := stateReader.ContractStorage(&address, &key) - if err != nil { - return nil, ErrContractNotFound - } - - return value, nil -} diff --git a/rpc/contract_test.go b/rpc/contract_test.go index 8f9e100aa3..522ab0bb62 100644 --- a/rpc/contract_test.go +++ b/rpc/contract_test.go @@ -86,85 +86,3 @@ func TestNonce(t *testing.T) { assert.Equal(t, expectedNonce, nonce) }) } - -func TestStorageAt(t *testing.T) { - mockCtrl := gomock.NewController(t) - t.Cleanup(mockCtrl.Finish) - - mockReader := mocks.NewMockReader(mockCtrl) - log := utils.NewNopZapLogger() - handler := rpc.New(mockReader, nil, nil, "", log) - - t.Run("empty blockchain", func(t *testing.T) { - mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) - - storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Latest: true}) - require.Nil(t, storage) - assert.Equal(t, rpc.ErrBlockNotFound, rpcErr) - }) - - t.Run("non-existent block hash", func(t *testing.T) { - mockReader.EXPECT().StateAtBlockHash(&felt.Zero).Return(nil, nil, db.ErrKeyNotFound) - - storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Hash: &felt.Zero}) - require.Nil(t, storage) - assert.Equal(t, rpc.ErrBlockNotFound, rpcErr) - }) - - t.Run("non-existent block number", func(t *testing.T) { - mockReader.EXPECT().StateAtBlockNumber(uint64(0)).Return(nil, nil, db.ErrKeyNotFound) - - storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Number: 0}) - require.Nil(t, storage) - assert.Equal(t, rpc.ErrBlockNotFound, rpcErr) - }) - - mockState := mocks.NewMockStateHistoryReader(mockCtrl) - - t.Run("non-existent contract", func(t *testing.T) { - mockReader.EXPECT().HeadState().Return(mockState, nopCloser, nil) - mockState.EXPECT().ContractStorage(gomock.Any(), gomock.Any()).Return(nil, errors.New("non-existent contract")) - - storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Latest: true}) - require.Nil(t, storage) - assert.Equal(t, rpc.ErrContractNotFound, rpcErr) - }) - - t.Run("non-existent key", func(t *testing.T) { - mockReader.EXPECT().HeadState().Return(mockState, nopCloser, nil) - mockState.EXPECT().ContractStorage(gomock.Any(), gomock.Any()).Return(&felt.Zero, errors.New("non-existent key")) - - storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Latest: true}) - require.Nil(t, storage) - assert.Equal(t, rpc.ErrContractNotFound, rpcErr) - }) - - expectedStorage := new(felt.Felt).SetUint64(1) - - t.Run("blockID - latest", func(t *testing.T) { - mockReader.EXPECT().HeadState().Return(mockState, nopCloser, nil) - mockState.EXPECT().ContractStorage(gomock.Any(), gomock.Any()).Return(expectedStorage, nil) - - storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Latest: true}) - require.Nil(t, rpcErr) - assert.Equal(t, expectedStorage, storage) - }) - - t.Run("blockID - hash", func(t *testing.T) { - mockReader.EXPECT().StateAtBlockHash(&felt.Zero).Return(mockState, nopCloser, nil) - mockState.EXPECT().ContractStorage(gomock.Any(), gomock.Any()).Return(expectedStorage, nil) - - storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Hash: &felt.Zero}) - require.Nil(t, rpcErr) - assert.Equal(t, expectedStorage, storage) - }) - - t.Run("blockID - number", func(t *testing.T) { - mockReader.EXPECT().StateAtBlockNumber(uint64(0)).Return(mockState, nopCloser, nil) - mockState.EXPECT().ContractStorage(gomock.Any(), gomock.Any()).Return(expectedStorage, nil) - - storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Number: 0}) - require.Nil(t, rpcErr) - assert.Equal(t, expectedStorage, storage) - }) -} diff --git a/rpc/handlers.go b/rpc/handlers.go index cfaf1f37a1..8704abdb2e 100644 --- a/rpc/handlers.go +++ b/rpc/handlers.go @@ -392,6 +392,11 @@ func (h *Handler) MethodsV0_6() ([]jsonrpc.Method, string) { //nolint: funlen Params: []jsonrpc.Parameter{{Name: "contract_address"}, {Name: "key"}, {Name: "block_id"}}, Handler: h.StorageAt, }, + { + Name: "starknet_getStorageProof", + Params: []jsonrpc.Parameter{}, + Handler: h.StorageProof, + }, { Name: "starknet_getClassHashAt", Params: []jsonrpc.Parameter{{Name: "block_id"}, {Name: "contract_address"}}, diff --git a/rpc/storage.go b/rpc/storage.go new file mode 100644 index 0000000000..facaf47539 --- /dev/null +++ b/rpc/storage.go @@ -0,0 +1,64 @@ +package rpc + +import ( + "github.com/NethermindEth/juno/core" + "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/juno/core/trie" + "github.com/NethermindEth/juno/jsonrpc" +) + +/**************************************************** + Storage Handlers +*****************************************************/ + +// StorageAt gets the value of the storage at the given address and key. +// +// It follows the specification defined here: +// https://github.com/starkware-libs/starknet-specs/blob/a789ccc3432c57777beceaa53a34a7ae2f25fda0/api/starknet_api_openrpc.json#L110 +func (h *Handler) StorageAt(address, key felt.Felt, id BlockID) (*felt.Felt, *jsonrpc.Error) { + stateReader, stateCloser, rpcErr := h.stateByBlockID(&id) + if rpcErr != nil { + return nil, rpcErr + } + defer h.callAndLogErr(stateCloser, "Error closing state reader in getStorageAt") + + value, err := stateReader.ContractStorage(&address, &key) + if err != nil { + return nil, ErrContractNotFound + } + + return value, nil +} + +// StorageProof returns the merkle paths in one of the state tries: global state, classes, individual contract +// +// It follows the specification defined here: +// https://github.com/starkware-libs/starknet-specs/blob/647caa00c0223e1daab1b2f3acc4e613ba2138aa/api/starknet_api_openrpc.json#L910 +func (h *Handler) StorageProof(classes, contracts []felt.Felt, storageKeys []StorageKeys) (*felt.Felt, *jsonrpc.Error) { + stateReader, stateCloser, err := h.bcReader.HeadState() + if err != nil { + return nil, ErrInternal.CloneWithData(err) + } + defer h.callAndLogErr(stateCloser, "Error closing state reader in getStorageProof") + + // TODO: Extend state reader interface? + s := stateReader.(*core.State) + clt, _, err := s.ClassTrie() + if err != nil { + return nil, ErrInternal.CloneWithData(err) + } + for _, elt := range classes { + feltBytes := elt.Bytes() + key := trie.NewKey(core.ContractStorageTrieHeight, feltBytes[:]) + nodes, err := trie.GetProof(&key, clt) + // adapt proofs to the expected format + } + + return nil, ErrUnexpectedError +} + +// https://github.com/starkware-libs/starknet-specs/blob/647caa00c0223e1daab1b2f3acc4e613ba2138aa/api/starknet_api_openrpc.json#L938 +type StorageKeys struct { + Contract felt.Felt `json:"contract_address"` + Keys []felt.Felt `json:"storage_keys"` +} diff --git a/rpc/storage_test.go b/rpc/storage_test.go new file mode 100644 index 0000000000..33cf25f0cc --- /dev/null +++ b/rpc/storage_test.go @@ -0,0 +1,178 @@ +package rpc_test + +import ( + "errors" + "github.com/NethermindEth/juno/core/felt" + "github.com/NethermindEth/juno/db" + "github.com/NethermindEth/juno/mocks" + "github.com/NethermindEth/juno/rpc" + "github.com/NethermindEth/juno/utils" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.uber.org/mock/gomock" + "testing" +) + +func TestStorageAt(t *testing.T) { + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + mockReader := mocks.NewMockReader(mockCtrl) + log := utils.NewNopZapLogger() + handler := rpc.New(mockReader, nil, nil, "", log) + + t.Run("empty blockchain", func(t *testing.T) { + mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) + + storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Latest: true}) + require.Nil(t, storage) + assert.Equal(t, rpc.ErrBlockNotFound, rpcErr) + }) + + t.Run("non-existent block hash", func(t *testing.T) { + mockReader.EXPECT().StateAtBlockHash(&felt.Zero).Return(nil, nil, db.ErrKeyNotFound) + + storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Hash: &felt.Zero}) + require.Nil(t, storage) + assert.Equal(t, rpc.ErrBlockNotFound, rpcErr) + }) + + t.Run("non-existent block number", func(t *testing.T) { + mockReader.EXPECT().StateAtBlockNumber(uint64(0)).Return(nil, nil, db.ErrKeyNotFound) + + storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Number: 0}) + require.Nil(t, storage) + assert.Equal(t, rpc.ErrBlockNotFound, rpcErr) + }) + + mockState := mocks.NewMockStateHistoryReader(mockCtrl) + + t.Run("non-existent contract", func(t *testing.T) { + mockReader.EXPECT().HeadState().Return(mockState, nopCloser, nil) + mockState.EXPECT().ContractStorage(gomock.Any(), gomock.Any()).Return(nil, errors.New("non-existent contract")) + + storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Latest: true}) + require.Nil(t, storage) + assert.Equal(t, rpc.ErrContractNotFound, rpcErr) + }) + + t.Run("non-existent key", func(t *testing.T) { + mockReader.EXPECT().HeadState().Return(mockState, nopCloser, nil) + mockState.EXPECT().ContractStorage(gomock.Any(), gomock.Any()).Return(&felt.Zero, errors.New("non-existent key")) + + storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Latest: true}) + require.Nil(t, storage) + assert.Equal(t, rpc.ErrContractNotFound, rpcErr) + }) + + expectedStorage := new(felt.Felt).SetUint64(1) + + t.Run("blockID - latest", func(t *testing.T) { + mockReader.EXPECT().HeadState().Return(mockState, nopCloser, nil) + mockState.EXPECT().ContractStorage(gomock.Any(), gomock.Any()).Return(expectedStorage, nil) + + storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Latest: true}) + require.Nil(t, rpcErr) + assert.Equal(t, expectedStorage, storage) + }) + + t.Run("blockID - hash", func(t *testing.T) { + mockReader.EXPECT().StateAtBlockHash(&felt.Zero).Return(mockState, nopCloser, nil) + mockState.EXPECT().ContractStorage(gomock.Any(), gomock.Any()).Return(expectedStorage, nil) + + storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Hash: &felt.Zero}) + require.Nil(t, rpcErr) + assert.Equal(t, expectedStorage, storage) + }) + + t.Run("blockID - number", func(t *testing.T) { + mockReader.EXPECT().StateAtBlockNumber(uint64(0)).Return(mockState, nopCloser, nil) + mockState.EXPECT().ContractStorage(gomock.Any(), gomock.Any()).Return(expectedStorage, nil) + + storage, rpcErr := handler.StorageAt(felt.Zero, felt.Zero, rpc.BlockID{Number: 0}) + require.Nil(t, rpcErr) + assert.Equal(t, expectedStorage, storage) + }) +} + +func TestStorageProof(t *testing.T) { + mockCtrl := gomock.NewController(t) + t.Cleanup(mockCtrl.Finish) + + mockReader := mocks.NewMockReader(mockCtrl) + log := utils.NewNopZapLogger() + handler := rpc.New(mockReader, nil, nil, "", log) + + t.Run("empty blockchain", func(t *testing.T) { + //mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) + + proof, rpcErr := handler.StorageProof(nil, nil, nil) + require.Nil(t, proof) + + assert.Equal(t, rpc.ErrUnexpectedError, rpcErr) + }) + t.Run("class trie hash does not exist in a trie", func(t *testing.T) { + //mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) + + proof, rpcErr := handler.StorageProof(nil, nil, nil) + require.Nil(t, proof) + + assert.Equal(t, rpc.ErrUnexpectedError, rpcErr) + }) + t.Run("class trie hash exists in a trie", func(t *testing.T) { + //mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) + + proof, rpcErr := handler.StorageProof(nil, nil, nil) + require.Nil(t, proof) + + assert.Equal(t, rpc.ErrUnexpectedError, rpcErr) + }) + t.Run("storage trie address does not exist in a trie", func(t *testing.T) { + //mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) + + proof, rpcErr := handler.StorageProof(nil, nil, nil) + require.Nil(t, proof) + + assert.Equal(t, rpc.ErrUnexpectedError, rpcErr) + }) + t.Run("storage trie address exists in a trie", func(t *testing.T) { + //mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) + + proof, rpcErr := handler.StorageProof(nil, nil, nil) + require.Nil(t, proof) + + assert.Equal(t, rpc.ErrUnexpectedError, rpcErr) + }) + t.Run("contract storage trie address does not exist in a trie", func(t *testing.T) { + //mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) + + proof, rpcErr := handler.StorageProof(nil, nil, nil) + require.Nil(t, proof) + + assert.Equal(t, rpc.ErrUnexpectedError, rpcErr) + }) + t.Run("contract storage trie key slot does not exist in a trie", func(t *testing.T) { + //mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) + + proof, rpcErr := handler.StorageProof(nil, nil, nil) + require.Nil(t, proof) + + assert.Equal(t, rpc.ErrUnexpectedError, rpcErr) + }) + t.Run("contract storage trie address/key exists in a trie", func(t *testing.T) { + //mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) + + proof, rpcErr := handler.StorageProof(nil, nil, nil) + require.Nil(t, proof) + + assert.Equal(t, rpc.ErrUnexpectedError, rpcErr) + }) + t.Run("class & storage tries proofs requested", func(t *testing.T) { + //mockReader.EXPECT().HeadState().Return(nil, nil, db.ErrKeyNotFound) + + proof, rpcErr := handler.StorageProof(nil, nil, nil) + require.Nil(t, proof) + + assert.Equal(t, rpc.ErrUnexpectedError, rpcErr) + }) +}