Skip to content

feat(l1): default omitted block to latest on eth state methods#6780

Open
MysticRyuujin wants to merge 3 commits into
lambdaclass:mainfrom
MysticRyuujin:fix/default-block-latest-state-methods
Open

feat(l1): default omitted block to latest on eth state methods#6780
MysticRyuujin wants to merge 3 commits into
lambdaclass:mainfrom
MysticRyuujin:fix/default-block-latest-state-methods

Conversation

@MysticRyuujin
Copy link
Copy Markdown

Summary

Two related JSON-RPC changes aligning ethrex with ethereum/execution-apis#812 by @manusw7 (Block parameter optional, default latest):

  1. Default an omitted block parameter to latest on the state methods that previously required it. eth_getBalance, eth_getCode, eth_getStorageAt, eth_getTransactionCount, and eth_getProof did a strict params.len() != N check and returned -32000 "Expected N params" when the block was omitted. They now accept the block as optional and default it to latest via a new impl Default for BlockIdentifierOrHash (latest).
  2. Implement eth_getStorageValues (previously -32601 method not found): batches storage-slot reads for multiple accounts at a block, mirroring eth_getStorageAt, with the same optional-block default and a 1024-slot cap. Registered in the eth dispatch.

Motivation

Aligns ethrex with execution-apis#812 (@manusw7) and matches the other clients implementing the same change. Full credit to @manusw7 for the proposal.

Testing

  • cargo test -p ethrex-rpc --lib — 64 passed, 0 failed. New parse-layer tests assert each state method resolves an omitted block to latest (test_state_methods_default_block_to_latest_when_omitted) and that eth_getStorageValues parses (and rejects an empty request).
  • Confirmed via the hive rpc-compat simulator that the omitted-block request now produces the same response as an explicit "latest" request for every method (previously the omitted form errored).

Note on hive rpc-compat value fixtures (out of scope)

ethrex does not currently pass the rpc-compat value fixtures for these state methods in the hive default chain — but this is pre-existing and unrelated to this change: a stock ethrex build returns the same results for an explicit "latest" request (e.g. eth_getBalance(addr, "latest") returns 0x0 where other clients return the funded balance), and *-blockhash requests error with "Could not resolve block number". That points to an ethrex/hive default-chain state-serving issue, which is out of scope here. This PR makes the omitted-block path behave identically to the explicit-latest path (the spec change), proven by the unit tests above.

Related

…orageValues

The state methods (eth_getBalance, eth_getCode, eth_getStorageAt,
eth_getTransactionCount, eth_getProof) required an exact param count, so
omitting the block returned -32000 'Expected N params'. Make the block
parameter optional and default to latest (per execution-apis), via a new
Default for BlockIdentifierOrHash (latest).

Also implement eth_getStorageValues (previously -32601 method not found):
batch storage-slot reads for multiple accounts at a block, mirroring
eth_getStorageAt, with the same optional-block default and a 1024-slot cap.
…s parse

Parse-layer tests proving eth_getBalance/getCode/getStorageAt/
getTransactionCount/getProof resolve an omitted block to latest, and that
eth_getStorageValues parses a request (and rejects an empty one).
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

This PR aligns several state-related RPC methods with execution-apis by making the block parameter optional (defaulting to latest) and adds a new eth_getStorageValues RPC for batch storage reads.

Changes:

  • Add Default for BlockIdentifierOrHash to represent the omitted-block default (latest).
  • Update state RPC request parsing to accept an omitted block parameter and default it to latest.
  • Introduce eth_getStorageValues request/handler with slot-count limiting and tests.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 2 comments.

File Description
crates/networking/rpc/types/block_identifier.rs Adds Default for BlockIdentifierOrHash to support omitted block params defaulting to latest.
crates/networking/rpc/rpc.rs Wires new eth_getStorageValues method into the request router and imports.
crates/networking/rpc/eth/account.rs Makes block param optional for state methods; adds GetStorageValuesRequest implementation and tests.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +300 to +313
let raw: HashMap<String, Vec<U256>> = serde_json::from_value(params[0].clone())?;
let mut requests = HashMap::with_capacity(raw.len());
let mut total_slots = 0usize;
for (address, slots) in raw {
total_slots += slots.len();
if total_slots > MAX_STORAGE_VALUES_SLOTS {
return Err(RpcErr::BadParams(format!(
"too many slots (max {MAX_STORAGE_VALUES_SLOTS})"
)));
}
let address: Address = serde_json::from_value(Value::String(address))?;
let slots = slots.iter().map(H256::from_uint).collect();
requests.insert(address, slots);
}
Comment on lines +333 to +337
let Some(block_number) = self.block.resolve_block_number(&context.storage).await? else {
return Err(RpcErr::Internal(
"Could not resolve block number".to_owned(),
));
};
@greptile-apps
Copy link
Copy Markdown

greptile-apps Bot commented Jun 2, 2026

Greptile Summary

This PR aligns ethrex with execution-apis#812 by making the block parameter optional (defaulting to latest) on eth_getBalance, eth_getCode, eth_getStorageAt, eth_getTransactionCount, and eth_getProof, and implements the new eth_getStorageValues batch-slot-read method with a 1024-slot cap.

  • The optional-block logic is clean: a new Default for BlockIdentifierOrHash chains through BlockIdentifier::default()BlockTag::Latest, and each handler uses .get(1).map(...).transpose()?.unwrap_or_default() to drop to that default when the parameter is absent.
  • eth_getStorageValues parses a {address → [slots]} map, resolves the block, and returns a {address → [values]} map mirroring slot order; a 1024-slot cap rejects oversized requests, and an empty request is also rejected.

Confidence Score: 4/5

The core optional-block-default logic is correct and well-tested; the new eth_getStorageValues handler works end-to-end but has two surface-area rough edges around slot format parity and response address key casing.

The Default chain and all five amended parse methods are straightforward and consistent. The two open questions — decimal slot values and checksummed address key matching — are usability concerns that don't break existing callers but may trip up new consumers of eth_getStorageValues.

crates/networking/rpc/eth/account.rs — specifically the GetStorageValuesRequest parse and handle implementations

Important Files Changed

Filename Overview
crates/networking/rpc/eth/account.rs Makes block parameter optional (defaulting to latest) for 5 state methods and adds new eth_getStorageValues handler; slot parsing and response address key format may diverge from eth_getStorageAt conventions
crates/networking/rpc/types/block_identifier.rs Adds Default impl for BlockIdentifierOrHash (delegates to BlockIdentifier::default() → BlockTag::Latest); correct and safe
crates/networking/rpc/rpc.rs Registers eth_getStorageValues in the eth dispatch table; minimal one-line change, straightforward
Prompt To Fix All With AI
Fix the following 2 code review issues. Work through them one at a time, proposing concise fixes.

---

### Issue 1 of 2
crates/networking/rpc/eth/account.rs:300
**Slot deserialization may reject decimal values accepted by `eth_getStorageAt`**

`eth_getStorageAt` uses `serde_utils::u256::deser_hex_or_dec_str` for the storage slot, which accepts both `"0x1"` (hex) and `"1"` (decimal). Here `slots` are deserialized via `serde_json::from_value::<Vec<U256>>`, which relies on `U256`'s built-in serde. If a client sends decimal-formatted slot indices (e.g. `{"0xaddr": ["1", "2"]}`), this call will fail while the equivalent `eth_getStorageAt(addr, "1", ...)` would succeed — a silent behavioural asymmetry between the two methods.

### Issue 2 of 2
crates/networking/rpc/eth/account.rs:348
**Response address keys use lowercase hex — callers sending checksummed or mixed-case addresses can't do a direct key lookup**

`format!("{address:#x}")` renders the address as lowercase `0x`-prefixed hex. A client that sends `{"0xDeAdBeEfDeAdBeEfDeAdBeEfDeAdBeEfDeAdBeEf": ["0x0"]}` receives a response keyed on `"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"`. Any code that naively looks up its original request-address string against the response map will miss the entry. Returning the address in EIP-55 checksummed form (or normalising to lowercase at parse time and documenting it) would make the correlation unambiguous.

Reviews (1): Last reviewed commit: "test: state methods default omitted bloc..." | Re-trigger Greptile

return Err(RpcErr::BadParams("Expected 1 or 2 params".to_owned()));
};
// params[0] is an object mapping address -> array of storage slots.
let raw: HashMap<String, Vec<U256>> = serde_json::from_value(params[0].clone())?;
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Slot deserialization may reject decimal values accepted by eth_getStorageAt

eth_getStorageAt uses serde_utils::u256::deser_hex_or_dec_str for the storage slot, which accepts both "0x1" (hex) and "1" (decimal). Here slots are deserialized via serde_json::from_value::<Vec<U256>>, which relies on U256's built-in serde. If a client sends decimal-formatted slot indices (e.g. {"0xaddr": ["1", "2"]}), this call will fail while the equivalent eth_getStorageAt(addr, "1", ...) would succeed — a silent behavioural asymmetry between the two methods.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/rpc/eth/account.rs
Line: 300

Comment:
**Slot deserialization may reject decimal values accepted by `eth_getStorageAt`**

`eth_getStorageAt` uses `serde_utils::u256::deser_hex_or_dec_str` for the storage slot, which accepts both `"0x1"` (hex) and `"1"` (decimal). Here `slots` are deserialized via `serde_json::from_value::<Vec<U256>>`, which relies on `U256`'s built-in serde. If a client sends decimal-formatted slot indices (e.g. `{"0xaddr": ["1", "2"]}`), this call will fail while the equivalent `eth_getStorageAt(addr, "1", ...)` would succeed — a silent behavioural asymmetry between the two methods.

How can I resolve this? If you propose a fix, please make it concise.

.unwrap_or_default();
values.push(format!("{:#x}", H256::from_uint(&value)));
}
result.insert(format!("{address:#x}"), values);
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Response address keys use lowercase hex — callers sending checksummed or mixed-case addresses can't do a direct key lookup

format!("{address:#x}") renders the address as lowercase 0x-prefixed hex. A client that sends {"0xDeAdBeEfDeAdBeEfDeAdBeEfDeAdBeEfDeAdBeEf": ["0x0"]} receives a response keyed on "0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef". Any code that naively looks up its original request-address string against the response map will miss the entry. Returning the address in EIP-55 checksummed form (or normalising to lowercase at parse time and documenting it) would make the correlation unambiguous.

Prompt To Fix With AI
This is a comment left during a code review.
Path: crates/networking/rpc/eth/account.rs
Line: 348

Comment:
**Response address keys use lowercase hex — callers sending checksummed or mixed-case addresses can't do a direct key lookup**

`format!("{address:#x}")` renders the address as lowercase `0x`-prefixed hex. A client that sends `{"0xDeAdBeEfDeAdBeEfDeAdBeEfDeAdBeEfDeAdBeEf": ["0x0"]}` receives a response keyed on `"0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"`. Any code that naively looks up its original request-address string against the response map will miss the entry. Returning the address in EIP-55 checksummed form (or normalising to lowercase at parse time and documenting it) would make the correlation unambiguous.

How can I resolve this? If you propose a fix, please make it concise.

Note: If this suggestion doesn't match your team's coding style, reply to this and let me know. I'll remember it for next time!

@MysticRyuujin
Copy link
Copy Markdown
Author

Thanks Copilot — reviewed both:

  • Slot limit enforced after deserialization / usize overflow — not changing. The request body is already size-capped by the RPC server, which bounds the deserialized map; and total_slots overflowing usize would require ~2^64 slots (≈10^20 bytes of request body), which the body cap makes impossible. This also matches the existing state methods (getProof/getStorageAt deserialize params before their own checks), so I'd rather keep it consistent than special-case this one.
  • RpcErr::Internal for an unresolvable block → should be BadParams — reasonable in the abstract, but this mirrors the existing sibling methods (GetBalance/GetCode/GetStorageAt all return Internal("Could not resolve block number")). Reclassifying just getStorageValues would make it inconsistent; reclassifying all of them is a separate, codebase-wide change beyond this PR's scope.

@MysticRyuujin
Copy link
Copy Markdown
Author

Thanks Greptile — both are accurate observations; my take on each:

  • Response address keys are lowercase — keeping lowercase. This matches the reference client (go-ethereum returns lowercase 0x… keys for eth_getStorageValues) and the execution-apis conformance fixture, which is keyed lowercase. Returning EIP-55 checksummed keys would actually diverge from the reference and fail the rpc-compat fixture, so lowercase is the correct choice here.
  • Decimal slot values vs eth_getStorageAt — correct that there's an asymmetry, but I'm leaving eth_getStorageValues hex-only. The execution-apis storage-key schema is bytesMax32 (hex); eth_getStorageAt's acceptance of bare decimal strings is a non-standard leniency, and I'd rather a brand-new method adhere to the spec than propagate it. If the team prefers the two methods to accept identical slot formats, that's a quick follow-up (deserialize slots as strings and run each through serde_utils::u256::deser_hex_or_dec_str).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants