internal/ethapi: default block parameter to latest on state methods (#35100)

Make the `Block` parameter optional on the six state-reading methods,
defaulting to `latest` when omitted:

- `eth_getBalance`
- `eth_getCode`
- `eth_getStorageAt`
- `eth_getTransactionCount`
- `eth_getProof`
- `eth_getStorageValues`

This implements the behavior proposed in https://github.com/ethereum/execution-apis/pull/812.

---------

Co-authored-by: Sina M <1591639+s1na@users.noreply.github.com>
This commit is contained in:
Chase Wright 2026-06-03 05:35:12 -05:00 committed by GitHub
parent 80d9ba5d97
commit 6b451a4245
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 116 additions and 23 deletions

View file

@ -328,11 +328,21 @@ func (api *BlockChainAPI) BlockNumber() hexutil.Uint64 {
return hexutil.Uint64(header.Number.Uint64()) return hexutil.Uint64(header.Number.Uint64())
} }
// blockNrOrHashOrLatest resolves an optional block selector, defaulting to the
// latest block when the parameter was omitted by the caller (nil).
func blockNrOrHashOrLatest(blockNrOrHash *rpc.BlockNumberOrHash) rpc.BlockNumberOrHash {
if blockNrOrHash != nil {
return *blockNrOrHash
}
return rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)
}
// GetBalance returns the amount of wei for the given address in the state of the // GetBalance returns the amount of wei for the given address in the state of the
// given block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta // given block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta
// block numbers are also allowed. // block numbers are also allowed. When the block parameter is omitted, it
func (api *BlockChainAPI) GetBalance(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Big, error) { // defaults to the latest block.
state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) func (api *BlockChainAPI) GetBalance(ctx context.Context, address common.Address, blockNrOrHash *rpc.BlockNumberOrHash) (*hexutil.Big, error) {
state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHashOrLatest(blockNrOrHash))
if state == nil || err != nil { if state == nil || err != nil {
return nil, err return nil, err
} }
@ -371,7 +381,8 @@ func (n *proofList) Delete(key []byte) error {
} }
// GetProof returns the Merkle-proof for a given account and optionally some storage keys. // GetProof returns the Merkle-proof for a given account and optionally some storage keys.
func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (*AccountResult, error) { // When the block parameter is omitted, it defaults to the latest block.
func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash *rpc.BlockNumberOrHash) (*AccountResult, error) {
if len(storageKeys) > maxGetProofKeys { if len(storageKeys) > maxGetProofKeys {
return nil, &invalidParamsError{fmt.Sprintf("too many storage keys requested (max %d, got %d)", maxGetProofKeys, len(storageKeys))} return nil, &invalidParamsError{fmt.Sprintf("too many storage keys requested (max %d, got %d)", maxGetProofKeys, len(storageKeys))}
} }
@ -388,7 +399,7 @@ func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address,
return nil, &invalidParamsError{fmt.Sprintf("%v: %q", err, hexKey)} return nil, &invalidParamsError{fmt.Sprintf("%v: %q", err, hexKey)}
} }
} }
statedb, header, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) statedb, header, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHashOrLatest(blockNrOrHash))
if statedb == nil || err != nil { if statedb == nil || err != nil {
return nil, err return nil, err
} }
@ -584,8 +595,9 @@ func (api *BlockChainAPI) GetUncleCountByBlockHash(ctx context.Context, blockHas
} }
// GetCode returns the code stored at the given address in the state for the given block number. // GetCode returns the code stored at the given address in the state for the given block number.
func (api *BlockChainAPI) GetCode(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { // When the block parameter is omitted, it defaults to the latest block.
state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) func (api *BlockChainAPI) GetCode(ctx context.Context, address common.Address, blockNrOrHash *rpc.BlockNumberOrHash) (hexutil.Bytes, error) {
state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHashOrLatest(blockNrOrHash))
if state == nil || err != nil { if state == nil || err != nil {
return nil, err return nil, err
} }
@ -595,9 +607,10 @@ func (api *BlockChainAPI) GetCode(ctx context.Context, address common.Address, b
// GetStorageAt returns the storage from the state at the given address, key and // GetStorageAt returns the storage from the state at the given address, key and
// block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta block // block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta block
// numbers are also allowed. // numbers are also allowed. When the block parameter is omitted, it defaults to
func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Address, hexKey string, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) { // the latest block.
state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Address, hexKey string, blockNrOrHash *rpc.BlockNumberOrHash) (hexutil.Bytes, error) {
state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHashOrLatest(blockNrOrHash))
if state == nil || err != nil { if state == nil || err != nil {
return nil, err return nil, err
} }
@ -610,8 +623,9 @@ func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Addre
} }
// GetStorageValues returns multiple storage slot values for multiple accounts // GetStorageValues returns multiple storage slot values for multiple accounts
// at the given block. // at the given block. When the block parameter is omitted, it defaults to the
func (api *BlockChainAPI) GetStorageValues(ctx context.Context, requests map[common.Address][]common.Hash, blockNrOrHash rpc.BlockNumberOrHash) (map[common.Address][]hexutil.Bytes, error) { // latest block.
func (api *BlockChainAPI) GetStorageValues(ctx context.Context, requests map[common.Address][]common.Hash, blockNrOrHash *rpc.BlockNumberOrHash) (map[common.Address][]hexutil.Bytes, error) {
// Count total slots requested. // Count total slots requested.
var totalSlots int var totalSlots int
for _, keys := range requests { for _, keys := range requests {
@ -624,7 +638,7 @@ func (api *BlockChainAPI) GetStorageValues(ctx context.Context, requests map[com
return nil, &invalidParamsError{message: "empty request"} return nil, &invalidParamsError{message: "empty request"}
} }
state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHashOrLatest(blockNrOrHash))
if state == nil || err != nil { if state == nil || err != nil {
return nil, err return nil, err
} }
@ -1483,10 +1497,12 @@ func (api *TransactionAPI) GetRawTransactionByBlockHashAndIndex(ctx context.Cont
return nil return nil
} }
// GetTransactionCount returns the number of transactions the given address has sent for the given block number // GetTransactionCount returns the number of transactions the given address has sent for the given block number.
func (api *TransactionAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (*hexutil.Uint64, error) { // When the block parameter is omitted, it defaults to the latest block.
func (api *TransactionAPI) GetTransactionCount(ctx context.Context, address common.Address, blockNrOrHash *rpc.BlockNumberOrHash) (*hexutil.Uint64, error) {
bnh := blockNrOrHashOrLatest(blockNrOrHash)
// Ask transaction pool for the nonce which includes pending transactions // Ask transaction pool for the nonce which includes pending transactions
if blockNr, ok := blockNrOrHash.Number(); ok && blockNr == rpc.PendingBlockNumber { if blockNr, ok := bnh.Number(); ok && blockNr == rpc.PendingBlockNumber {
nonce, err := api.b.GetPoolNonce(ctx, address) nonce, err := api.b.GetPoolNonce(ctx, address)
if err != nil { if err != nil {
return nil, err return nil, err
@ -1494,7 +1510,7 @@ func (api *TransactionAPI) GetTransactionCount(ctx context.Context, address comm
return (*hexutil.Uint64)(&nonce), nil return (*hexutil.Uint64)(&nonce), nil
} }
// Resolve block number and use its state to ask for the nonce // Resolve block number and use its state to ask for the nonce
state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, bnh)
if state == nil || err != nil { if state == nil || err != nil {
return nil, err return nil, err
} }

View file

@ -4216,7 +4216,7 @@ func TestGetStorageValues(t *testing.T) {
result, err := api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ result, err := api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{
addr1: {slot0, slot1}, addr1: {slot0, slot1},
addr2: {slot2}, addr2: {slot2},
}, latest) }, &latest)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
@ -4236,7 +4236,7 @@ func TestGetStorageValues(t *testing.T) {
// Missing slot returns zero. // Missing slot returns zero.
result, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ result, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{
addr1: {common.HexToHash("0xff")}, addr1: {common.HexToHash("0xff")},
}, latest) }, &latest)
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
@ -4245,7 +4245,7 @@ func TestGetStorageValues(t *testing.T) {
} }
// Empty request returns error. // Empty request returns error.
_, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{}, latest) _, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{}, &latest)
if err == nil { if err == nil {
t.Fatal("expected error for empty request") t.Fatal("expected error for empty request")
} }
@ -4257,8 +4257,85 @@ func TestGetStorageValues(t *testing.T) {
} }
_, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ _, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{
addr1: tooMany, addr1: tooMany,
}, latest) }, &latest)
if err == nil { if err == nil {
t.Fatal("expected error for exceeding slot limit") t.Fatal("expected error for exceeding slot limit")
} }
} }
// TestStateMethodsDefaultToLatest verifies that the state-reading methods
// default the optional block parameter to "latest".
func TestStateMethodsDefaultToLatest(t *testing.T) {
t.Parallel()
var (
accounts = newAccounts(2)
slot = common.HexToHash("0x01")
val = common.HexToHash("0x42")
code = []byte{0x60, 0x00, 0x60, 0x00}
genesis = &core.Genesis{
Config: params.MergedTestChainConfig,
Alloc: types.GenesisAlloc{
accounts[0].addr: {Balance: big.NewInt(params.Ether)},
accounts[1].addr: {
Balance: big.NewInt(2 * params.Ether),
Nonce: 7,
Code: code,
Storage: map[common.Hash]common.Hash{slot: val},
},
},
}
acc = accounts[1].addr
ctx = context.Background()
)
backend := newTestBackend(t, 1, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) {
b.SetPoS()
})
srv := rpc.NewServer()
if err := srv.RegisterName("eth", NewBlockChainAPI(backend)); err != nil {
t.Fatal(err)
}
if err := srv.RegisterName("eth", NewTransactionAPI(backend, new(AddrLocker))); err != nil {
t.Fatal(err)
}
client := rpc.DialInProc(srv)
defer client.Close()
// call invokes method twice: once omitting the block param and once passing
// "latest" explicitly. Both must succeed and return identical results.
call := func(name string, dst func() any, explicit []any, omitted []any) {
t.Helper()
gotOmitted := dst()
if err := client.CallContext(ctx, gotOmitted, name, omitted...); err != nil {
t.Fatalf("%s with omitted block: unexpected error: %v", name, err)
}
gotLatest := dst()
if err := client.CallContext(ctx, gotLatest, name, explicit...); err != nil {
t.Fatalf("%s with explicit latest: unexpected error: %v", name, err)
}
o, _ := json.Marshal(gotOmitted)
l, _ := json.Marshal(gotLatest)
if !bytes.Equal(o, l) {
t.Errorf("%s: omitted-block result %s != latest result %s", name, o, l)
}
}
call("eth_getBalance",
func() any { return new(hexutil.Big) },
[]any{acc, "latest"}, []any{acc})
call("eth_getCode",
func() any { return new(hexutil.Bytes) },
[]any{acc, "latest"}, []any{acc})
call("eth_getTransactionCount",
func() any { return new(hexutil.Uint64) },
[]any{acc, "latest"}, []any{acc})
call("eth_getStorageAt",
func() any { return new(hexutil.Bytes) },
[]any{acc, slot, "latest"}, []any{acc, slot})
call("eth_getProof",
func() any { return new(AccountResult) },
[]any{acc, []string{slot.Hex()}, "latest"}, []any{acc, []string{slot.Hex()}})
call("eth_getStorageValues",
func() any { return new(map[common.Address][]hexutil.Bytes) },
[]any{map[common.Address][]common.Hash{acc: {slot}}, "latest"},
[]any{map[common.Address][]common.Hash{acc: {slot}}})
}

View file

@ -566,13 +566,13 @@ web3._extend({
name: 'getProof', name: 'getProof',
call: 'eth_getProof', call: 'eth_getProof',
params: 3, params: 3,
inputFormatter: [web3._extend.formatters.inputAddressFormatter, null, web3._extend.formatters.inputBlockNumberFormatter] inputFormatter: [web3._extend.formatters.inputAddressFormatter, null, web3._extend.formatters.inputDefaultBlockNumberFormatter]
}), }),
new web3._extend.Method({ new web3._extend.Method({
name: 'getStorageValues', name: 'getStorageValues',
call: 'eth_getStorageValues', call: 'eth_getStorageValues',
params: 2, params: 2,
inputFormatter: [null, web3._extend.formatters.inputBlockNumberFormatter] inputFormatter: [null, web3._extend.formatters.inputDefaultBlockNumberFormatter]
}), }),
new web3._extend.Method({ new web3._extend.Method({
name: 'createAccessList', name: 'createAccessList',