diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index d50a8a9e90..ebdbbee202 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -53,6 +53,10 @@ import ( // allowed to produce in order to speed up calculations. const estimateGasErrorRatio = 0.015 +// maxGetStorageSlots is the maximum total number of storage slots that can +// be requested in a single eth_getStorageValues call. +const maxGetStorageSlots = 1024 + var errBlobTxNotSupported = errors.New("signing blob transactions not supported") var errSubClosed = errors.New("chain subscription closed") @@ -589,6 +593,41 @@ func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Addre return res[:], state.Error() } +// GetStorageValues returns multiple storage slot values for multiple accounts +// at the given 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. + var totalSlots int + for _, keys := range requests { + totalSlots += len(keys) + if totalSlots > maxGetStorageSlots { + return nil, &clientLimitExceededError{message: fmt.Sprintf("too many slots (max %d)", maxGetStorageSlots)} + } + } + if totalSlots == 0 { + return nil, &invalidParamsError{message: "empty request"} + } + + state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) + if state == nil || err != nil { + return nil, err + } + + result := make(map[common.Address][]hexutil.Bytes, len(requests)) + for addr, keys := range requests { + vals := make([]hexutil.Bytes, len(keys)) + for i, key := range keys { + v := state.GetState(addr, key) + vals[i] = v[:] + } + if err := state.Error(); err != nil { + return nil, err + } + result[addr] = vals + } + return result, nil +} + // GetBlockReceipts returns the block receipts for the given block hash or number or tag. func (api *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]map[string]interface{}, error) { var ( @@ -1925,36 +1964,7 @@ type DebugAPI struct { // NewDebugAPI creates a new instance of DebugAPI. func NewDebugAPI(b Backend) *DebugAPI { - return &DebugAPI{b: b} -} - -// BatchGetStorage returns multiple storage slots for multiple accounts at the given block. -// Params: {address: [hexKey, ...], ...}, blockNrOrHash -// Returns a map {address: [value,...]} preserving the order within each key list. -func (api *DebugAPI) BatchGetStorage(ctx context.Context, req map[common.Address][]string, blockNrOrHash rpc.BlockNumberOrHash) (map[common.Address][]hexutil.Bytes, error) { - state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash) - if state == nil || err != nil { - return nil, err - } - out := make(map[common.Address][]hexutil.Bytes, len(req)) - for addr, keys := range req { - vals := make([]hexutil.Bytes, len(keys)) - for i, k := range keys { - key, _, err := decodeHash(k) - if err != nil { - return nil, fmt.Errorf("unable to decode storage key for %s at index %d: %w", addr.Hex(), i, err) - } - v := state.GetState(addr, key) - vv := make([]byte, len(v)) - copy(vv, v[:]) - vals[i] = vv - } - out[addr] = vals - } - if err := state.Error(); err != nil { - return nil, err - } - return out, nil + return &DebugAPI{b: b} } // GetRawHeader retrieves the RLP encoding for a single header. diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 837df8b662..2f0c07694d 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -4065,3 +4065,91 @@ func TestSendRawTransactionSync_Timeout(t *testing.T) { t.Fatalf("expected ErrorData=%s, got %v", want, got) } } + +func TestGetStorageValues(t *testing.T) { + t.Parallel() + + var ( + addr1 = common.HexToAddress("0x1111") + addr2 = common.HexToAddress("0x2222") + slot0 = common.Hash{} + slot1 = common.BigToHash(big.NewInt(1)) + slot2 = common.BigToHash(big.NewInt(2)) + val0 = common.BigToHash(big.NewInt(42)) + val1 = common.BigToHash(big.NewInt(100)) + val2 = common.BigToHash(big.NewInt(200)) + + genesis = &core.Genesis{ + Config: params.MergedTestChainConfig, + Alloc: types.GenesisAlloc{ + addr1: { + Balance: big.NewInt(params.Ether), + Storage: map[common.Hash]common.Hash{ + slot0: val0, + slot1: val1, + }, + }, + addr2: { + Balance: big.NewInt(params.Ether), + Storage: map[common.Hash]common.Hash{ + slot2: val2, + }, + }, + }, + } + ) + api := NewBlockChainAPI(newTestBackend(t, 1, genesis, beacon.New(ethash.NewFaker()), func(i int, b *core.BlockGen) { + b.SetPoS() + })) + latest := rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber) + + // Happy path: multiple addresses, multiple slots. + result, err := api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ + addr1: {slot0, slot1}, + addr2: {slot2}, + }, latest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(result) != 2 { + t.Fatalf("expected 2 addresses in result, got %d", len(result)) + } + if got := common.BytesToHash(result[addr1][0]); got != val0 { + t.Errorf("addr1 slot0: want %x, got %x", val0, got) + } + if got := common.BytesToHash(result[addr1][1]); got != val1 { + t.Errorf("addr1 slot1: want %x, got %x", val1, got) + } + if got := common.BytesToHash(result[addr2][0]); got != val2 { + t.Errorf("addr2 slot2: want %x, got %x", val2, got) + } + + // Missing slot returns zero. + result, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ + addr1: {common.HexToHash("0xff")}, + }, latest) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if got := common.BytesToHash(result[addr1][0]); got != (common.Hash{}) { + t.Errorf("missing slot: want zero, got %x", got) + } + + // Empty request returns error. + _, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{}, latest) + if err == nil { + t.Fatal("expected error for empty request") + } + + // Exceeding slot limit returns error. + tooMany := make([]common.Hash, maxGetStorageSlots+1) + for i := range tooMany { + tooMany[i] = common.BigToHash(big.NewInt(int64(i))) + } + _, err = api.GetStorageValues(context.Background(), map[common.Address][]common.Hash{ + addr1: tooMany, + }, latest) + if err == nil { + t.Fatal("expected error for exceeding slot limit") + } +} diff --git a/internal/ethapi/batch_storage_test.go b/internal/ethapi/batch_storage_test.go deleted file mode 100644 index 2afeb802c1..0000000000 --- a/internal/ethapi/batch_storage_test.go +++ /dev/null @@ -1,105 +0,0 @@ -package ethapi - -import ( - "context" - "testing" - - "github.com/ethereum/go-ethereum/accounts" - "github.com/ethereum/go-ethereum/common" - "github.com/ethereum/go-ethereum/common/hexutil" - "github.com/ethereum/go-ethereum/core" - "github.com/ethereum/go-ethereum/core/state" - "github.com/ethereum/go-ethereum/core/types" - "github.com/ethereum/go-ethereum/core/vm" - "github.com/ethereum/go-ethereum/ethdb" - "github.com/ethereum/go-ethereum/event" - "github.com/ethereum/go-ethereum/params" - "github.com/ethereum/go-ethereum/rpc" -) - -// storageBackend composes backendMock and overrides StateAndHeaderByNumberOrHash -// to return a prepared in-memory StateDB for testing. -type storageBackend struct { - *backendMock - state *state.StateDB -} - -func (b *storageBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*state.StateDB, *types.Header, error) { - return b.state, nil, nil -} - -// Ensure storageBackend still satisfies the Backend interface by forwarding -// the remaining methods via the embedded backendMock (compile-time check). -var _ Backend = (*storageBackend)(nil) - -func TestDebugBatchGetStorage(t *testing.T) { - t.Parallel() - - // Prepare an in-memory state database and set some storage values. - sdb, _ := state.New(types.EmptyRootHash, state.NewDatabaseForTesting()) - - addr1 := common.HexToAddress("0x1000000000000000000000000000000000000001") - addr2 := common.HexToAddress("0x2000000000000000000000000000000000000002") - - keyA := common.HexToHash("0x01") - keyB := common.HexToHash("0x02") - keyC := common.HexToHash("0x03") - - valA := common.HexToHash("0xaaa") - valB := common.HexToHash("0xbbb") - valC := common.HexToHash("0xccc") - - sdb.SetState(addr1, keyA, valA) - sdb.SetState(addr1, keyB, valB) - sdb.SetState(addr2, keyC, valC) - - // Wire the API with a backend that returns our state. - base := newBackendMock() - b := &storageBackend{backendMock: base, state: sdb} - api := NewDebugAPI(b) - - // Build request: addr1 asks [keyB, keyA] (order test), addr2 asks [keyC, missing] - req := map[common.Address][]string{ - addr1: {"0x02", "0x01"}, - addr2: {"0x03", "0x04"}, - } - - got, err := api.BatchGetStorage(context.Background(), req, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber)) - if err != nil { - t.Fatalf("BatchGetStorage returned error: %v", err) - } - - // Validate addr1 order and values - if len(got[addr1]) != 2 { - t.Fatalf("addr1 length mismatch: got %d want 2", len(got[addr1])) - } - if hexutil.Bytes(got[addr1][0]).String() != hexutil.Bytes(valB[:]).String() { - t.Fatalf("addr1[0] mismatch: got %s want %s", hexutil.Bytes(got[addr1][0]).String(), hexutil.Bytes(valB[:]).String()) - } - if hexutil.Bytes(got[addr1][1]).String() != hexutil.Bytes(valA[:]).String() { - t.Fatalf("addr1[1] mismatch: got %s want %s", hexutil.Bytes(got[addr1][1]).String(), hexutil.Bytes(valA[:]).String()) - } - - // Validate addr2 values (existing and zero for missing) - if len(got[addr2]) != 2 { - t.Fatalf("addr2 length mismatch: got %d want 2", len(got[addr2])) - } - if hexutil.Bytes(got[addr2][0]).String() != hexutil.Bytes(valC[:]).String() { - t.Fatalf("addr2[0] mismatch: got %s want %s", hexutil.Bytes(got[addr2][0]).String(), hexutil.Bytes(valC[:]).String()) - } - if hexutil.Bytes(got[addr2][1]).String() != hexutil.Bytes(common.Hash{}[:]).String() { - t.Fatalf("addr2[1] mismatch: got %s want %s (zero)", hexutil.Bytes(got[addr2][1]).String(), hexutil.Bytes(common.Hash{}[:]).String()) - } -} - -// Ensure backendMock compiles in this file by referencing needed imports -// (no-ops; prevents unused import errors if interface evolves). -var ( - _ = accounts.Account{} - _ = core.ChainEvent{} - _ vm.Config - _ ethdb.Database - _ event.Subscription - _ params.ChainConfig -) - diff --git a/internal/web3ext/web3ext.go b/internal/web3ext/web3ext.go index f3132d7692..9ba8776360 100644 --- a/internal/web3ext/web3ext.go +++ b/internal/web3ext/web3ext.go @@ -415,11 +415,6 @@ web3._extend({ call: 'debug_storageRangeAt', params: 5, }), - new web3._extend.Method({ - name: 'batchGetStorage', - call: 'debug_batchGetStorage', - params: 2, - }), new web3._extend.Method({ name: 'getModifiedAccountsByNumber', call: 'debug_getModifiedAccountsByNumber', @@ -572,6 +567,12 @@ web3._extend({ params: 3, inputFormatter: [web3._extend.formatters.inputAddressFormatter, null, web3._extend.formatters.inputBlockNumberFormatter] }), + new web3._extend.Method({ + name: 'getStorageValues', + call: 'eth_getStorageValues', + params: 2, + inputFormatter: [null, web3._extend.formatters.inputBlockNumberFormatter] + }), new web3._extend.Method({ name: 'createAccessList', call: 'eth_createAccessList',