Repository: ethereum/eth-tester (language: python)

Issue:
Implement `eth_getStorageAt`
* Version: 0.9.0b1
* Python: 3.10
* OS: macos


### What was wrong?

I get a value error for `RPC Endpoint has not been implemented: eth_getStorageAt`.

### How can it be fixed?

here is some Hardhat code but otherwise not sure:

```javascript
    const key = setLengthLeft(bigIntToBuffer(positionIndex), 32);

    const data = await this._runInBlockContext(blockNumberOrPending, () =>
      this._stateManager.getContractStorage(address, key)
    );

    const EXPECTED_DATA_SIZE = 32;
    if (data.length < EXPECTED_DATA_SIZE) {
      return Buffer.concat(
        [Buffer.alloc(EXPECTED_DATA_SIZE - data.length, 0), data],
        EXPECTED_DATA_SIZE
      );
    }

    return data;
```



Discussion / hints:
Hey @antazoey. This is via `web3.py` + `EthereumTesterProvider` right? It's not implemented in `eth-tester` but that message is coming from the web3.py library through that provider. I will work on getting this change in for both libraries 👍🏼 
Yes Im interacting through using web3.py

Fix patch:
diff --git a/newsfragments/264.feature.rst b/newsfragments/264.feature.rst
new file mode 100644
index 00000000..f0a829a7
--- /dev/null
+++ b/newsfragments/264.feature.rst
@@ -0,0 +1,1 @@
+Add support for ``eth_getStorageAt`` for ``PyEVMBackend`` via ``get_storage_at()`` method.


Test patch:
diff --git a/eth_tester/backends/pyevm/main.py b/eth_tester/backends/pyevm/main.py
index de3a12b6..e32f32b7 100644
--- a/eth_tester/backends/pyevm/main.py
+++ b/eth_tester/backends/pyevm/main.py
@@ -8,6 +8,7 @@
 from eth_abi.exceptions import DecodingError
 
 from eth_account.hdaccount import HDPath, seed_from_mnemonic
+from eth_typing import Address
 
 from eth_utils import (
     encode_hex,
@@ -538,6 +539,10 @@ def get_code(self, account, block_number="latest"):
         vm = _get_vm_for_block_number(self.chain, block_number)
         return vm.state.get_code(account)
 
+    def get_storage(self, account: Address, slot: int, block_number="latest") -> bytes:
+        vm = _get_vm_for_block_number(self.chain, block_number)
+        return vm.state.get_storage(account, slot)
+
     def get_base_fee(self, block_number="latest"):
         vm = _get_vm_for_block_number(self.chain, block_number)
         return vm.state.base_fee
diff --git a/eth_tester/main.py b/eth_tester/main.py
index 202a7b3e..cb9f131b 100644
--- a/eth_tester/main.py
+++ b/eth_tester/main.py
@@ -6,6 +6,10 @@
 import time
 import functools
 
+from eth_typing import (
+    HexAddress,
+    HexStr,
+)
 from eth_utils import (
     is_integer,
     is_same_address,
@@ -254,6 +258,24 @@ def get_code(self, account, block_number="latest"):
         code = self.normalizer.normalize_outbound_code(raw_code)
         return code
 
+    def get_storage_at(
+        self,
+        account: HexAddress,
+        slot: HexStr,
+        # properly type hint once eth-typing brings in updated `BlockIdentifier`
+        block_number="latest",
+    ) -> int:
+        self.validator.validate_inbound_account(account)
+        self.validator.validate_inbound_storage_slot(slot)
+        self.validator.validate_inbound_block_number(block_number)
+        raw_account = self.normalizer.normalize_inbound_account(account)
+        raw_slot = self.normalizer.normalize_inbound_storage_slot(slot)
+        raw_block_number = self.normalizer.normalize_inbound_block_number(block_number)
+        raw_storage = self.backend.get_storage(raw_account, raw_slot, raw_block_number)
+        self.validator.validate_outbound_storage(raw_storage)
+        storage = self.normalizer.normalize_outbound_storage(raw_storage)
+        return storage
+
     def get_nonce(self, account, block_number="latest"):
         self.validator.validate_inbound_account(account)
         self.validator.validate_inbound_block_number(block_number)
diff --git a/eth_tester/normalization/base.py b/eth_tester/normalization/base.py
index f858b513..5e47b32a 100644
--- a/eth_tester/normalization/base.py
+++ b/eth_tester/normalization/base.py
@@ -26,6 +26,9 @@ def normalize_inbound_private_key(self, private_key):
     def normalize_inbound_raw_transaction(self, raw_transaction_hex):
         raise NotImplementedError("must be implemented by subclasses")
 
+    def normalize_inbound_storage_slot(self, storage_slot):
+        raise NotImplementedError("must be implemented by subclasses")
+
     def normalize_inbound_timestamp(self, timestamp):
         raise NotImplementedError("must be implemented by subclasses")
 
diff --git a/eth_tester/normalization/common.py b/eth_tester/normalization/common.py
index c62f1fb8..7327d6d1 100644
--- a/eth_tester/normalization/common.py
+++ b/eth_tester/normalization/common.py
@@ -1,4 +1,9 @@
+from eth_tester.utils.encoding import (
+    int_to_32byte_big_endian,
+)
 from eth_utils import (
+    encode_hex,
+    is_hex,
     to_dict,
     to_tuple,
 )
@@ -33,3 +38,13 @@ def normalize_if(value, conditional_fn, normalizer):
         return normalizer(value)
     else:
         return value
+
+
+def to_integer_if_hex(value):
+    if is_hex(value):
+        return int(value, 16)
+    return value
+
+
+def int_to_32byte_hex(value):
+    return encode_hex(int_to_32byte_big_endian(value))
diff --git a/eth_tester/normalization/default.py b/eth_tester/normalization/default.py
index 0fbe6eac..141f245b 100644
--- a/eth_tester/normalization/default.py
+++ b/eth_tester/normalization/default.py
@@ -11,6 +11,10 @@
 from .base import (
     BaseNormalizer,
 )
+from .common import (
+    int_to_32byte_hex,
+    to_integer_if_hex,
+)
 from .inbound import (
     normalize_filter_params as normalize_inbound_filter_params,
     normalize_log_entry as normalize_inbound_log_entry,
@@ -40,6 +44,7 @@ class DefaultNormalizer(BaseNormalizer):
     normalize_inbound_log_entry = staticmethod(normalize_inbound_log_entry)
     normalize_inbound_private_key = staticmethod(normalize_inbound_private_key)
     normalize_inbound_raw_transaction = staticmethod(normalize_inbound_raw_transaction)
+    normalize_inbound_storage_slot = staticmethod(to_integer_if_hex)
     normalize_inbound_timestamp = staticmethod(identity)
     normalize_inbound_transaction = staticmethod(normalize_inbound_transaction)
     normalize_inbound_transaction_hash = staticmethod(decode_hex)
@@ -57,5 +62,6 @@ class DefaultNormalizer(BaseNormalizer):
     normalize_outbound_nonce = staticmethod(identity)
     normalize_outbound_receipt = staticmethod(normalize_outbound_receipt)
     normalize_outbound_return_data = staticmethod(encode_hex)
+    normalize_outbound_storage = staticmethod(int_to_32byte_hex)
     normalize_outbound_transaction = staticmethod(normalize_outbound_transaction)
     normalize_outbound_transaction_hash = staticmethod(encode_hex)
diff --git a/eth_tester/validation/base.py b/eth_tester/validation/base.py
index e41f95f3..64193e75 100644
--- a/eth_tester/validation/base.py
+++ b/eth_tester/validation/base.py
@@ -8,6 +8,9 @@ class BaseValidator:
     def validate_inbound_account(self, account):
         raise NotImplementedError("must be implemented by subclasses")
 
+    def validate_inbound_storage_slot(self, account):
+        raise NotImplementedError("must be implemented by subclasses")
+
     def validate_inbound_block_hash(self, block_hash):
         raise NotImplementedError("must be implemented by subclasses")
 
diff --git a/eth_tester/validation/default.py b/eth_tester/validation/default.py
index 077f4e62..be3b9ff1 100644
--- a/eth_tester/validation/default.py
+++ b/eth_tester/validation/default.py
@@ -14,6 +14,7 @@
     validate_filter_params as validate_inbound_filter_params,
     validate_private_key as validate_inbound_private_key,
     validate_raw_transaction as validate_inbound_raw_transaction,
+    validate_inbound_storage_slot as validate_inbound_storage_slot,
     validate_timestamp as validate_inbound_timestamp,
     validate_transaction as validate_inbound_transaction,
     validate_transaction_hash as validate_inbound_transaction_hash,
@@ -41,6 +42,7 @@ class DefaultValidator(BaseValidator):
     validate_inbound_filter_params = staticmethod(validate_inbound_filter_params)
     validate_inbound_private_key = staticmethod(validate_inbound_private_key)
     validate_inbound_raw_transaction = staticmethod(validate_inbound_raw_transaction)
+    validate_inbound_storage_slot = staticmethod(validate_inbound_storage_slot)
     validate_inbound_timestamp = staticmethod(validate_inbound_timestamp)
     validate_inbound_transaction = staticmethod(validate_inbound_transaction)
     validate_inbound_transaction_hash = staticmethod(validate_inbound_transaction_hash)
@@ -58,5 +60,6 @@ class DefaultValidator(BaseValidator):
     validate_outbound_log_entry = staticmethod(validate_outbound_log_entry)
     validate_outbound_receipt = staticmethod(validate_outbound_receipt)
     validate_outbound_return_data = staticmethod(validate_outbound_bytes)
+    validate_outbound_storage = staticmethod(validate_uint256)
     validate_outbound_transaction = staticmethod(validate_outbound_transaction)
     validate_outbound_transaction_hash = staticmethod(validate_32_byte_string)
diff --git a/eth_tester/validation/inbound.py b/eth_tester/validation/inbound.py
index fee724f4..80269169 100644
--- a/eth_tester/validation/inbound.py
+++ b/eth_tester/validation/inbound.py
@@ -15,6 +15,7 @@
     is_dict,
     is_hex,
     is_hex_address,
+    is_hexstr,
     is_integer,
     is_list_like,
     is_string,
@@ -107,6 +108,22 @@ def validate_account(value):
         raise ValidationError("Address does not validate EIP55 checksum")
 
 
+def validate_inbound_storage_slot(value):
+    error_msg = (
+        "Storage slot must be a hex string representation of a positive integer - "
+        f"slot: {value}"
+    )
+    if not (is_hexstr(value) and value.startswith("0x")):
+        raise ValidationError(error_msg)
+
+    try:
+        int_val = int(value, 16)
+    except ValueError:
+        raise ValidationError(error_msg)
+
+    validate_uint256(int_val)
+
+
 def is_valid_topic_array(value):
     if not is_list_like(value):
         return False
diff --git a/tests/backends/test_pyevm.py b/tests/backends/test_pyevm.py
index 5d7ff3d3..0140740a 100644
--- a/tests/backends/test_pyevm.py
+++ b/tests/backends/test_pyevm.py
@@ -16,6 +16,7 @@
     ShanghaiVM,
 )
 from eth_tester.normalization.outbound import normalize_withdrawal
+from eth_typing import HexStr
 from eth_utils import encode_hex, is_hexstr, to_wei
 
 from eth_tester import EthereumTester, PyEVMBackend
@@ -364,3 +365,26 @@ def test_pyevm_backend_with_custom_vm_configuration_post_merge(self):
         assert genesis_block.header.difficulty == POST_MERGE_DIFFICULTY
         assert genesis_block.header.nonce == POST_MERGE_NONCE
         assert genesis_block.header.mix_hash == POST_MERGE_MIX_HASH
+
+    def test_eth_get_storage_at(self):
+        # add storage to accounts in the genesis block
+        state_overrides = {
+            "storage": {
+                1: 1,
+                2: 2,
+            }
+        }
+
+        genesis_state = PyEVMBackend.generate_genesis_state(
+            overrides=state_overrides, num_accounts=3
+        )
+        pyevm_backend = PyEVMBackend(genesis_state=genesis_state)
+        tester = EthereumTester(backend=pyevm_backend)
+
+        accounts = tester.get_accounts()
+        assert len(accounts) == 3
+
+        for acct in accounts:
+            assert tester.get_storage_at(acct, HexStr("0x0")) == f"0x{'00'*32}"
+            assert tester.get_storage_at(acct, HexStr("0x1")) == f"0x{'00'*31}01"
+            assert tester.get_storage_at(acct, HexStr("0x2")) == f"0x{'00'*31}02"
diff --git a/tests/core/normalization/test_outbound_normalization.py b/tests/core/normalization/test_default_outbound_normalization.py
similarity index 59%
rename from tests/core/normalization/test_outbound_normalization.py
rename to tests/core/normalization/test_default_outbound_normalization.py
index ddc486bb..1e846e7e 100644
--- a/tests/core/normalization/test_outbound_normalization.py
+++ b/tests/core/normalization/test_default_outbound_normalization.py
@@ -1,7 +1,7 @@
 import pytest
 
-from eth_tester.normalization.outbound import (
-    normalize_receipt,
+from eth_tester.normalization import (
+    DefaultNormalizer,
 )
 
 from tests.utils import (
@@ -26,7 +26,19 @@ def test_outbound_receipt_contract_address_status_based_normalization(
     assert receipt["status"] == status
     assert receipt["contract_address"] == contract_address
 
-    normalized_receipt = normalize_receipt(receipt)
+    normalized_receipt = DefaultNormalizer.normalize_outbound_receipt(receipt)
 
     assert normalized_receipt["status"] == status
     assert normalized_receipt["contract_address"] == expected
+
+
+@pytest.mark.parametrize(
+    "value,expected",
+    (
+        (0, f"0x{'00'*32}"),
+        (1, f"0x{'00'*31}01"),
+        (2, f"0x{'00'*31}02"),
+    ),
+)
+def test_outbound_storage_normalization(value, expected):
+    assert DefaultNormalizer.normalize_outbound_storage(value) == expected
diff --git a/tests/core/validation/test_inbound_validation.py b/tests/core/validation/test_inbound_validation.py
index 2128889d..fdf3e1c3 100644
--- a/tests/core/validation/test_inbound_validation.py
+++ b/tests/core/validation/test_inbound_validation.py
@@ -2,7 +2,9 @@
 
 import pytest
 
-from eth_tester.validation.inbound import validate_inbound_withdrawals
+from eth_tester.validation.inbound import (
+    validate_inbound_withdrawals,
+)
 
 
 try:
@@ -623,3 +625,51 @@ def test_apply_withdrawals_inbound_dict_validation(withdrawals, is_valid):
 
     else:
         validate_inbound_withdrawals(withdrawals)
+
+
+@pytest.mark.parametrize(
+    "value,is_valid",
+    (
+        ("0x0", True),
+        ("0x1", True),
+        ("0x22", True),
+        ("0x4d2", True),
+        (0, False),
+        (1, False),
+        (-1, False),
+        ("1", False),
+        ("-0x1", False),
+        ("test", False),
+        (b"test", False),
+    ),
+)
+def test_validate_inbound_storage_slot(value, is_valid):
+    if not is_valid:
+        with pytest.raises(
+            ValidationError,
+            match=(
+                "Storage slot must be a hex string representation of a positive "
+                f"integer - slot: {value}"
+            ),
+        ):
+            DefaultValidator.validate_inbound_storage_slot(value)
+    else:
+        DefaultValidator.validate_inbound_storage_slot(value)
+
+
+@pytest.mark.parametrize(
+    "value,is_valid",
+    (
+        (hex(2**256 - 1), True),
+        (hex(2**256), False),
+    ),
+)
+def test_validate_inbound_storage_slot_integer_value_at_limit(value, is_valid):
+    if not is_valid:
+        with pytest.raises(
+            ValidationError,
+            match="Value exceeds maximum 256 bit integer size",
+        ):
+            DefaultValidator.validate_inbound_storage_slot(value)
+    else:
+        DefaultValidator.validate_inbound_storage_slot(value)