Using eth_getStorageAt instead of debug_storageRangeAt on Reth

The debug_storageRangeAt method is not supported in Reth. See Reth Issue 3417.

In this article, we'll walk through a practical Python-based workaround script to replicate the essential functionality of debug_storageRangeAt using standard Ethereum RPC calls (eth_getStorageAt). This approach ensures you can still achieve your debugging goals without relying on methods unavailable in Reth.

We'll use BASE and Python as an example.

We'll replicate specifically the following call on the BASE mainnet:

curl --request POST \
     --url CHAINSTACK_NODE_URL \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "jsonrpc": "2.0",
  "method": "debug_storageRangeAt",
  "id": 1,
  "params": [
    "0xc40b7058b5b80e565dfb986fe852c047733291291c8de1be8888ae64b5457bbd",
    25,
    "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
    "0x00000000000000000000000000000000",
    2
  ]
}
'

Workaround overview

The workaround leverages the standard Ethereum RPC method eth_getStorageAt to mimic the storage exploration functionality:

  • Sequentially query storage slots.
  • Retrieve non-zero storage entries.
  • Implement pagination to handle large storage queries efficiently.

Full script in Python

#!/usr/bin/env python3
"""
Workaround for debug_storageRangeAt RPC method on Reth nodes.
This script provides similar functionality using standard eth_getStorageAt calls.
"""

# =============================================================================
# Configuration - Edit these values
# =============================================================================

# Your Chainstack node URL (required)
RPC_URL = "CHAINSTACK_NODE_URL"

# Block hash or number to query storage at
BLOCK_HASH = "YOUR_TARGET_BLOCK_HASH"

# Transaction index in the block
TX_INDEX = INDEX_NUMBER

# Contract address to inspect storage for
CONTRACT_ADDRESS = "0x833589fcd6eYOUR_CONTRACT_ADDRESSdb6e08f4c7c32d4f71b54bda02913"

# Starting storage key (32 bytes hex string)
START_KEY = "0x00000000000000000000000000000000"

# Maximum number of storage entries to return
MAX_RESULTS = 10

# ==============
# Implementation
# ==============

from web3 import Web3
from eth_utils import keccak, to_bytes, to_hex
import json
from typing import Optional, Dict, Any, Union
from dataclasses import dataclass

@dataclass
class StorageRangeResult:
    """Container for storage range query results."""
    storage: Dict[str, Dict[str, Optional[str]]]  # Storage slot -> {key, value} mapping
    next_key: Optional[str] = None                # Next key for pagination


class StorageRangeWorkaround:
    """Implements debug_storageRangeAt functionality using eth_getStorageAt."""

    def __init__(self, rpc_url: str):
        """Initialize with RPC URL for the Ethereum node."""
        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
        if not self.w3.is_connected():
            raise ConnectionError("Failed to connect to Ethereum node")

    def get_storage_range_at(
        self,
        block_hash: Union[str, bytes],
        tx_index: int,
        contract_address: str,
        start_key: str,
        max_results: int
    ) -> StorageRangeResult:
        """
        Emulate debug_storageRangeAt functionality using eth_getStorageAt.
        
        Args:
            block_hash: Block hash or number
            tx_index: Transaction index in the block (ignored in this implementation)
            contract_address: Address of the contract to inspect
            start_key: Starting storage key (32 bytes hex string)
            max_results: Maximum number of storage entries to return
            
        Returns:
            StorageRangeResult containing storage entries and next key if any
        """
        # Normalize parameters
        if isinstance(block_hash, str) and block_hash.startswith('0x'):
            block_hash = block_hash[2:]
        block_hash = bytes.fromhex(block_hash) if isinstance(block_hash, str) else block_hash
        
        contract_address = self.w3.to_checksum_address(contract_address)
        start_key = start_key if start_key.startswith('0x') else '0x' + start_key

        # Get block information
        try:
            block = self.w3.eth.get_block(block_hash)
        except Exception as e:
            raise Exception(f"Failed to get block: {e}")

        # Get storage entries
        storage_entries = {}
        current_key_int = int(start_key, 16)
        found_entries = 0
        
        # Keep trying keys until we find max_results non-zero entries or hit a reasonable limit
        max_attempts = max_results * 10  # Try up to 10x max_results to find non-zero values
        attempts = 0
        
        while found_entries < max_results and attempts < max_attempts:
            current_key_hex = hex(current_key_int)
            if not current_key_hex.startswith('0x'):
                current_key_hex = '0x' + current_key_hex[2:].zfill(64)
            
            try:
                value = self.w3.eth.get_storage_at(
                    contract_address,
                    current_key_hex,
                    block['number']
                )
                value_hex = '0x' + value.hex()
                
                # Only include non-zero values
                if value_hex != '0x' + '00' * 32:
                    storage_entries[current_key_hex] = value_hex
                    found_entries += 1
                
            except Exception as e:
                print(f"Warning: Failed to get storage at {current_key_hex}: {e}")
            
            current_key_int += 1
            attempts += 1

        # Determine next key for pagination
        next_key = None
        if found_entries >= max_results:
            next_key = hex(current_key_int)
            if not next_key.startswith('0x'):
                next_key = '0x' + next_key[2:].zfill(64)

        # Format result
        result = StorageRangeResult(
            storage={
                k: {"key": None, "value": v} for k, v in storage_entries.items()
            },
            next_key=next_key
        )
        
        return result


def compute_mapping_slot(mapping_slot: int, key: Union[str, int, bytes]) -> str:
    """
    Compute the storage slot for a mapping entry.
    
    Args:
        mapping_slot: Slot number where the mapping is declared
        key: The key in the mapping (address, uint, etc.)
        
    Returns:
        The 32-byte hex string of the storage slot
    """
    if isinstance(key, str) and key.startswith('0x'):
        key = bytes.fromhex(key[2:])
    elif isinstance(key, int):
        key = key.to_bytes(32, 'big')
    elif not isinstance(key, bytes):
        raise ValueError("Key must be hex string, integer, or bytes")
        
    key = key.rjust(32, b'\x00')  # pad to 32 bytes
    slot_bytes = mapping_slot.to_bytes(32, 'big')
    
    slot_hash = keccak(key + slot_bytes)
    return '0x' + slot_hash.hex()


def main():
    """Main entry point for the script."""
    try:
        storage_range = StorageRangeWorkaround(RPC_URL)
        
        result = storage_range.get_storage_range_at(
            block_hash=BLOCK_HASH,
            tx_index=TX_INDEX,
            contract_address=CONTRACT_ADDRESS,
            start_key=START_KEY,
            max_results=MAX_RESULTS
        )
        
        # Print results in a JSON-RPC style response
        response = {
            "jsonrpc": "2.0",
            "id": 1,
            "result": {
                "storage": result.storage,
                "nextKey": result.next_key
            }
        }
        print(json.dumps(response, indent=2))
        
    except Exception as e:
        # Format error as JSON-RPC error response
        error_response = {
            "jsonrpc": "2.0",
            "id": 1,
            "error": {
                "code": -32000,
                "message": str(e)
            }
        }
        print(json.dumps(error_response, indent=2))


if __name__ == '__main__':
    main() 

Configuration

Adjust these parameters according to your context:

# Your Chainstack node URL (replace with your own)
RPC_URL = "CHAINSTACK_NODE_URL"

# Target block hash for querying storage
BLOCK_HASH = "YOUR_TARGET_BLOCK_HASH"

# Transaction index in the block
TX_INDEX = INDEX_NUMBER

# Contract address you want to inspect
CONTRACT_ADDRESS = "YOUR_CONTRACT_ADDRESS"

# Starting storage key
START_KEY = "0x00000000000000000000000000000000"

# Maximum results per run
MAX_RESULTS = 10
  1. Interpret the Output:
    You'll see a JSON-formatted output similar to what debug_storageRangeAt provides, including storage entries and the next key for pagination.

Use this snippet to identify and query the exact slots needed.

Pagination explained

Due to the potentially massive size of contract storage, the script includes pagination. If more entries exist beyond the queried range, nextKey is returned. Use this as your new START_KEY in the subsequent query.

Limitations & Best Practices

  • Performance: This workaround may be slower than native implementations due to multiple RPC calls.
  • Accuracy: It returns hashed storage keys without preimage (original key). You'll need additional logic or external knowledge to map these hashes to original keys.

Full example comparison

Let's do the same parameters call debug_storageRangeAt on BASE testinprod and eth_getStorageAt on Reth.

debug_storageRangeAt on BASE testinprod

Call:

curl --request POST \
     --url https://base-mainnet.core.chainstack.com/2fc1de7f08c0465f6a28e3c355e0cb14/ \
     --header 'accept: application/json' \
     --header 'content-type: application/json' \
     --data '
{
  "jsonrpc": "2.0",
  "method": "debug_storageRangeAt",
  "id": 1,
  "params": [
    "0xc40b7058b5b80e565dfb986fe852c047733291291c8de1be8888ae64b5457bbd",
    25,
    "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
    "0x00000000000000000000000000000000",
    2
  ]
}
'

Output:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "storage": {
      "0x0175b7a638427703f0dbe7bb9bbf987a2551717b34e79f33b5b1008d1fa01db9": {
        "key": "0x000000000000000000000000000000000000000000000000000000000000000b",
        "value": "0x000000000000000000000000000000000000000000000000000224743a3e0b18"
      },
      "0x036b6384b5eca791c62761152d0c79bb0604c104a5fb6f4eb0703f3154bb3db0": {
        "key": "0x0000000000000000000000000000000000000000000000000000000000000005",
        "value": "0x5553444300000000000000000000000000000000000000000000000000000008"
      },
      "0x290decd9548b62a8d60345a988386fc84ba6bc95484008f6362f93160ef3e563": {
        "key": "0x0000000000000000000000000000000000000000000000000000000000000000",
        "value": "0x0000000000000000000000003abd6f64a422225e61e435bae41db12096106df7"
      },
      "0x405787fa12a823e0f2b7631cc41b3ba8828b3321ca811111fa75cd3aa3bb5ace": {
        "key": "0x0000000000000000000000000000000000000000000000000000000000000002",
        "value": "0x0000000000000000000000004d15e70518a20fc8828b5c3853f32e35238d0b77"
      },
      "0x8a35acfbc15ff81a39ae7d344fd709f28e8600b4aa8c65c6b64bfe7fe36bd19b": {
        "key": "0x0000000000000000000000000000000000000000000000000000000000000004",
        "value": "0x55534420436f696e000000000000000000000000000000000000000000000010"
      },
      "0x8d1108e10bcb7c27dddfc02ed9d693a074039d026cf4ea4240b40f7d581ac802": {
        "key": "0x000000000000000000000000000000000000000000000000000000000000000f",
        "value": "0x02fa7265e7c5d81118673727957699e4d68f74cd74b7db77da710fe8a2c7834f"
      },
      "0xa66cc928b5edb82af9bd49922954155ab7b0942694bea4ce44661d9a8736c688": {
        "key": "0x0000000000000000000000000000000000000000000000000000000000000007",
        "value": "0x5553440000000000000000000000000000000000000000000000000000000006"
      },
      "0xb10e2d527612073b26eecdfd717e6a320cf44b4afac2b0732d9fcbe2b7fa0cf6": {
        "key": "0x0000000000000000000000000000000000000000000000000000000000000001",
        "value": "0x000000000000000000000000d3571b3bc51cecff49194ad67afffc648d5e07b4"
      },
      "0xf3f7a9fe364faab93b216da50a3214154f22a0a2b415b23a84c8169e8b636ee3": {
        "key": "0x0000000000000000000000000000000000000000000000000000000000000008",
        "value": "0x0000000000000000000000012230393edad0299b7e7b59f20aa856cd1bed52e1"
      },
      "0xf652222313e28459528d920b65115c16c04f3efc82aaedc97be59f3f377c0d3f": {
        "key": "0x0000000000000000000000000000000000000000000000000000000000000006",
        "value": "0x0000000000000000000000000000000000000000000000000000000000000006"
      }
    },
    "nextKey": "0x0000000000000000000000000000000000000000000000000000000000000012"
  }
}

eth_getStorageAt

Call:

(Note the Chainstack Node URL is a public one for this Developer portal, so it's rate-limited and suitable for one-off example calls only).

#!/usr/bin/env python3
"""
Workaround for debug_storageRangeAt RPC method on Reth nodes.
This script provides similar functionality using standard eth_getStorageAt calls.
"""

# =============================================================================
# Configuration - Edit these values
# =============================================================================

# Your Chainstack node URL (required)
RPC_URL = "https://base-mainnet.core.chainstack.com/2fc1de7f08c0465f6a28e3c355e0cb14/"

# Block hash or number to query storage at
BLOCK_HASH = "0xc40b7058b5b80e565dfb986fe852c047733291291c8de1be8888ae64b5457bbd"

# Transaction index in the block
TX_INDEX = 25

# Contract address to inspect storage for
CONTRACT_ADDRESS = "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913"

# Starting storage key (32 bytes hex string)
START_KEY = "0x00000000000000000000000000000000"

# Maximum number of storage entries to return
MAX_RESULTS = 10

# =============================================================================
# Implementation - Do not modify below this line unless you know what you're doing
# =============================================================================

from web3 import Web3
from eth_utils import keccak, to_bytes, to_hex
import json
from typing import Optional, Dict, Any, Union
from dataclasses import dataclass


@dataclass
class StorageRangeResult:
    """Container for storage range query results."""
    storage: Dict[str, Dict[str, Optional[str]]]  # Storage slot -> {key, value} mapping
    next_key: Optional[str] = None                # Next key for pagination


class StorageRangeWorkaround:
    """Implements debug_storageRangeAt functionality using eth_getStorageAt."""

    def __init__(self, rpc_url: str):
        """Initialize with RPC URL for the Ethereum node."""
        self.w3 = Web3(Web3.HTTPProvider(rpc_url))
        if not self.w3.is_connected():
            raise ConnectionError("Failed to connect to Ethereum node")

    def get_storage_range_at(
        self,
        block_hash: Union[str, bytes],
        tx_index: int,
        contract_address: str,
        start_key: str,
        max_results: int
    ) -> StorageRangeResult:
        """
        Emulate debug_storageRangeAt functionality using eth_getStorageAt.
        
        Args:
            block_hash: Block hash or number
            tx_index: Transaction index in the block (ignored in this implementation)
            contract_address: Address of the contract to inspect
            start_key: Starting storage key (32 bytes hex string)
            max_results: Maximum number of storage entries to return
            
        Returns:
            StorageRangeResult containing storage entries and next key if any
        """
        # Normalize parameters
        if isinstance(block_hash, str) and block_hash.startswith('0x'):
            block_hash = block_hash[2:]
        block_hash = bytes.fromhex(block_hash) if isinstance(block_hash, str) else block_hash
        
        contract_address = self.w3.to_checksum_address(contract_address)
        start_key = start_key if start_key.startswith('0x') else '0x' + start_key

        # Get block information
        try:
            block = self.w3.eth.get_block(block_hash)
        except Exception as e:
            raise Exception(f"Failed to get block: {e}")

        # Get storage entries
        storage_entries = {}
        current_key_int = int(start_key, 16)
        found_entries = 0
        
        # Keep trying keys until we find max_results non-zero entries or hit a reasonable limit
        max_attempts = max_results * 10  # Try up to 10x max_results to find non-zero values
        attempts = 0
        
        while found_entries < max_results and attempts < max_attempts:
            current_key_hex = hex(current_key_int)
            if not current_key_hex.startswith('0x'):
                current_key_hex = '0x' + current_key_hex[2:].zfill(64)
            
            try:
                value = self.w3.eth.get_storage_at(
                    contract_address,
                    current_key_hex,
                    block['number']
                )
                value_hex = '0x' + value.hex()
                
                # Only include non-zero values
                if value_hex != '0x' + '00' * 32:
                    storage_entries[current_key_hex] = value_hex
                    found_entries += 1
                
            except Exception as e:
                print(f"Warning: Failed to get storage at {current_key_hex}: {e}")
            
            current_key_int += 1
            attempts += 1

        # Determine next key for pagination
        next_key = None
        if found_entries >= max_results:
            next_key = hex(current_key_int)
            if not next_key.startswith('0x'):
                next_key = '0x' + next_key[2:].zfill(64)

        # Format result
        result = StorageRangeResult(
            storage={
                k: {"key": None, "value": v} for k, v in storage_entries.items()
            },
            next_key=next_key
        )
        
        return result


def compute_mapping_slot(mapping_slot: int, key: Union[str, int, bytes]) -> str:
    """
    Compute the storage slot for a mapping entry.
    
    Args:
        mapping_slot: Slot number where the mapping is declared
        key: The key in the mapping (address, uint, etc.)
        
    Returns:
        The 32-byte hex string of the storage slot
    """
    if isinstance(key, str) and key.startswith('0x'):
        key = bytes.fromhex(key[2:])
    elif isinstance(key, int):
        key = key.to_bytes(32, 'big')
    elif not isinstance(key, bytes):
        raise ValueError("Key must be hex string, integer, or bytes")
        
    key = key.rjust(32, b'\x00')  # pad to 32 bytes
    slot_bytes = mapping_slot.to_bytes(32, 'big')
    
    slot_hash = keccak(key + slot_bytes)
    return '0x' + slot_hash.hex()


def main():
    """Main entry point for the script."""
    try:
        storage_range = StorageRangeWorkaround(RPC_URL)
        
        result = storage_range.get_storage_range_at(
            block_hash=BLOCK_HASH,
            tx_index=TX_INDEX,
            contract_address=CONTRACT_ADDRESS,
            start_key=START_KEY,
            max_results=MAX_RESULTS
        )
        
        # Print results in a JSON-RPC style response
        response = {
            "jsonrpc": "2.0",
            "id": 1,
            "result": {
                "storage": result.storage,
                "nextKey": result.next_key
            }
        }
        print(json.dumps(response, indent=2))
        
    except Exception as e:
        # Format error as JSON-RPC error response
        error_response = {
            "jsonrpc": "2.0",
            "id": 1,
            "error": {
                "code": -32000,
                "message": str(e)
            }
        }
        print(json.dumps(error_response, indent=2))


if __name__ == '__main__':
    main() 

Output:

{
  "jsonrpc": "2.0",
  "id": 1,
  "result": {
    "storage": {
      "0x0": {
        "key": null,
        "value": "0x0000000000000000000000003abd6f64a422225e61e435bae41db12096106df7"
      },
      "0x1": {
        "key": null,
        "value": "0x000000000000000000000000d3571b3bc51cecff49194ad67afffc648d5e07b4"
      },
      "0x2": {
        "key": null,
        "value": "0x0000000000000000000000004d15e70518a20fc8828b5c3853f32e35238d0b77"
      },
      "0x4": {
        "key": null,
        "value": "0x55534420436f696e000000000000000000000000000000000000000000000010"
      },
      "0x5": {
        "key": null,
        "value": "0x5553444300000000000000000000000000000000000000000000000000000008"
      },
      "0x6": {
        "key": null,
        "value": "0x0000000000000000000000000000000000000000000000000000000000000006"
      },
      "0x7": {
        "key": null,
        "value": "0x5553440000000000000000000000000000000000000000000000000000000006"
      },
      "0x8": {
        "key": null,
        "value": "0x0000000000000000000000012230393edad0299b7e7b59f20aa856cd1bed52e1"
      },
      "0xb": {
        "key": null,
        "value": "0x000000000000000000000000000000000000000000000000000224743a3e0b18"
      },
      "0xf": {
        "key": null,
        "value": "0x02fa7265e7c5d81118673727957699e4d68f74cd74b7db77da710fe8a2c7834f"
      }
    },
    "nextKey": "0x10"
  }
}

References & further reading:

Ake

🛠️ Developer Experience Director @ Chainstack
💸 Talk to me all things Web3

20 years in technology | 8+ years in Web3 full time years experience

Trusted advisor helping developers navigate the complexities of blockchain infrastructure