diff --git a/api/api.go b/api/api.go index d2657d86..a9a603be 100644 --- a/api/api.go +++ b/api/api.go @@ -3,9 +3,11 @@ package api import ( "context" _ "embed" + "encoding/hex" "errors" "fmt" "math/big" + "strings" evmTypes "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/go-ethereum/common" @@ -690,28 +692,6 @@ func (b *BlockChainAPI) GetCode( return code, nil } -// handleError takes in an error and in case the error is of type ErrNotFound -// it returns nil instead of an error since that is according to the API spec, -// if the error is not of type ErrNotFound it will return the error and the generic -// empty type. -func handleError[T any](log zerolog.Logger, err error) (T, error) { - var zero T - switch { - // as per specification returning nil and nil for not found resources - case errors.Is(err, storageErrs.ErrNotFound): - return zero, nil - case errors.Is(err, storageErrs.ErrInvalidRange): - return zero, err - case errors.Is(err, requester.ErrOutOfRange): - return zero, fmt.Errorf("requested height is out of supported range") - case errors.Is(err, errs.ErrInvalid): - return zero, err - default: - log.Error().Err(err).Msg("api error") - return zero, errs.ErrInternal - } -} - func (b *BlockChainAPI) fetchBlockTransactions( ctx context.Context, block *evmTypes.Block, @@ -896,6 +876,78 @@ func (b *BlockChainAPI) FeeHistory( }, nil } +// GetStorageAt returns the storage from the state at the given address, key and +// block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta block +// numbers are also allowed. +func (b *BlockChainAPI) GetStorageAt( + ctx context.Context, + address common.Address, + storageSlot string, + blockNumberOrHash *rpc.BlockNumberOrHash, +) (hexutil.Bytes, error) { + if err := rateLimit(ctx, b.limiter, b.logger); err != nil { + return nil, err + } + + key, _, err := decodeHash(storageSlot) + if err != nil { + return handleError[hexutil.Bytes](b.logger, errors.Join(errs.ErrInvalid, err)) + } + + evmHeight, err := b.getBlockNumber(blockNumberOrHash) + if err != nil { + return handleError[hexutil.Bytes](b.logger, err) + } + + result, err := b.evm.GetStorageAt(ctx, address, key, evmHeight) + if err != nil { + return handleError[hexutil.Bytes](b.logger, err) + } + + return result[:], nil +} + +// handleError takes in an error and in case the error is of type ErrNotFound +// it returns nil instead of an error since that is according to the API spec, +// if the error is not of type ErrNotFound it will return the error and the generic +// empty type. +func handleError[T any](log zerolog.Logger, err error) (T, error) { + var zero T + switch { + // as per specification returning nil and nil for not found resources + case errors.Is(err, storageErrs.ErrNotFound): + return zero, nil + case errors.Is(err, storageErrs.ErrInvalidRange): + return zero, err + case errors.Is(err, requester.ErrOutOfRange): + return zero, fmt.Errorf("requested height is out of supported range") + case errors.Is(err, errs.ErrInvalid): + return zero, err + default: + log.Error().Err(err).Msg("api error") + return zero, errs.ErrInternal + } +} + +// decodeHash parses a hex-encoded 32-byte hash. The input may optionally +// be prefixed by 0x and can have a byte length up to 32. +func decodeHash(s string) (h common.Hash, inputLength int, err error) { + if strings.HasPrefix(s, "0x") || strings.HasPrefix(s, "0X") { + s = s[2:] + } + if (len(s) & 1) > 0 { + s = "0" + s + } + b, err := hex.DecodeString(s) + if err != nil { + return common.Hash{}, 0, errors.New("hex string invalid") + } + if len(b) > 32 { + return common.Hash{}, len(b), errors.New("hex string too long, want at most 32 bytes") + } + return common.BytesToHash(b), len(b), nil +} + /* Static responses section @@ -978,18 +1030,6 @@ func (b *BlockChainAPI) GetProof( return nil, errs.ErrNotSupported } -// GetStorageAt returns the storage from the state at the given address, key and -// block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta block -// numbers are also allowed. -func (b *BlockChainAPI) GetStorageAt( - ctx context.Context, - address common.Address, - storageSlot string, - blockNumberOrHash *rpc.BlockNumberOrHash, -) (hexutil.Bytes, error) { - return nil, errs.ErrNotSupported -} - // CreateAccessList creates an EIP-2930 type AccessList for the given transaction. // Reexec and blockNumberOrHash can be specified to create the accessList on top of a certain state. func (b *BlockChainAPI) CreateAccessList( diff --git a/emulator/remote_state.go b/services/requester/remote_state.go similarity index 94% rename from emulator/remote_state.go rename to services/requester/remote_state.go index 47633c5f..0878ecc3 100644 --- a/emulator/remote_state.go +++ b/services/requester/remote_state.go @@ -1,4 +1,4 @@ -package emulator +package requester import ( "context" @@ -15,7 +15,7 @@ import ( "google.golang.org/grpc/status" ) -var previewnetStorage = flow.HexToAddress("0x4f6fd534ddd3fc5f") +var previewnetStorageAddress = flow.HexToAddress("0x4f6fd534ddd3fc5f") var _ atree.Ledger = &remoteLedger{} @@ -64,6 +64,7 @@ func (l *remoteLedger) GetValue(owner, key []byte) ([]byte, error) { } if response != nil && len(response.Values) > 0 { + // we only request one register so 0 index return response.Values[0], nil } diff --git a/emulator/remote_state_test.go b/services/requester/remote_state_test.go similarity index 94% rename from emulator/remote_state_test.go rename to services/requester/remote_state_test.go index b1bd0015..68c6e3e6 100644 --- a/emulator/remote_state_test.go +++ b/services/requester/remote_state_test.go @@ -1,4 +1,4 @@ -package emulator +package requester import ( "context" @@ -24,7 +24,7 @@ func Test_E2E_Previewnet_RemoteLedger(t *testing.T) { require.NoError(t, err) testAddress := types.NewAddressFromBytes(addrBytes).ToCommon() - stateDB, err := state.NewStateDB(ledger, previewnetStorage) + stateDB, err := state.NewStateDB(ledger, previewnetStorageAddress) require.NoError(t, err) assert.NotEmpty(t, stateDB.GetCode(testAddress)) @@ -51,7 +51,7 @@ func Benchmark_RemoteLedger_GetBalance(b *testing.B) { ledger, err := newRemoteLedger(previewnetHost, cadenceHeight) require.NoError(b, err) - stateDB, err := state.NewStateDB(ledger, previewnetStorage) + stateDB, err := state.NewStateDB(ledger, previewnetStorageAddress) require.NoError(b, err) addrBytes, err := hex.DecodeString("BC9985a24c0846cbEdd6249868020A84Df83Ea85") diff --git a/services/requester/requester.go b/services/requester/requester.go index 6b5493b2..39632d1c 100644 --- a/services/requester/requester.go +++ b/services/requester/requester.go @@ -15,6 +15,7 @@ import ( "github.com/onflow/flow-go-sdk" "github.com/onflow/flow-go-sdk/crypto" "github.com/onflow/flow-go/fvm/evm/emulator" + "github.com/onflow/flow-go/fvm/evm/emulator/state" "github.com/onflow/flow-go/fvm/evm/stdlib" evmTypes "github.com/onflow/flow-go/fvm/evm/types" "github.com/onflow/flow-go/fvm/systemcontracts" @@ -86,6 +87,9 @@ type Requester interface { // GetLatestEVMHeight returns the latest EVM height of the network. GetLatestEVMHeight(ctx context.Context) (uint64, error) + + // GetStorageAt returns the storage from the state at the given address, key and block number. + GetStorageAt(ctx context.Context, address common.Address, hash common.Hash, evmHeight int64) (common.Hash, error) } var _ Requester = &EVM{} @@ -370,6 +374,43 @@ func (e *EVM) GetNonce( return nonce, nil } +func (e *EVM) stateAt(evmHeight int64) (*state.StateDB, error) { + cadenceHeight, err := e.evmToCadenceHeight(evmHeight) + if err != nil { + return nil, err + } + + if cadenceHeight == LatestBlockHeight { + h, err := e.client.GetLatestBlockHeader(context.Background(), true) + if err != nil { + return nil, err + } + cadenceHeight = h.Height + } + + ledger, err := newRemoteLedger(e.config.AccessNodeHost, cadenceHeight) + if err != nil { + return nil, fmt.Errorf("could not create a remote ledger: %w", err) + } + + return state.NewStateDB(ledger, previewnetStorageAddress) +} + +func (e *EVM) GetStorageAt( + ctx context.Context, + address common.Address, + hash common.Hash, + evmHeight int64, +) (common.Hash, error) { + stateDB, err := e.stateAt(evmHeight) + if err != nil { + return common.Hash{}, err + } + + result := stateDB.GetState(address, hash) + return result, stateDB.Error() +} + func (e *EVM) Call( ctx context.Context, data []byte,