ethapi: add partial state awareness to RPC layer (Phase 4)

Add partial state mode support to the RPC API. In partial state mode:
- Account queries (balance, nonce, account proofs) work for ALL accounts
- Storage/code queries only work for tracked contracts
- Clear error codes help clients understand limitations

Changes:
- New error types: StorageNotTrackedError (-32001), CodeNotTrackedError (-32002)
- Backend interface: PartialStateEnabled(), IsContractTracked()
- Modified RPCs: GetStorageAt, GetCode, GetProof check tracked status
- 7 new tests verify correct behavior for tracked/untracked contracts
This commit is contained in:
CPerezz 2026-02-03 12:02:54 +01:00
parent 9f52b96b6c
commit df2a91fb0a
No known key found for this signature in database
GPG key ID: 62045F34B97177DD
5 changed files with 346 additions and 0 deletions

View file

@ -519,3 +519,21 @@ func (b *EthAPIBackend) BlockAccessListByNumberOrHash(number rpc.BlockNumberOrHa
}
return block.AccessList().StringableRepresentation(), nil
}
// PartialStateEnabled returns true if partial state mode is active.
func (b *EthAPIBackend) PartialStateEnabled() bool {
return b.eth.config.PartialState.Enabled
}
// IsContractTracked returns true if the contract's storage is tracked.
// For full nodes (partial state disabled), this always returns true.
func (b *EthAPIBackend) IsContractTracked(addr common.Address) bool {
if !b.eth.config.PartialState.Enabled {
return true // Full node tracks everything
}
ps := b.eth.blockchain.PartialState()
if ps == nil {
return true // Shouldn't happen if config says enabled, but be safe
}
return ps.Filter().IsTracked(addr)
}

View file

@ -374,6 +374,11 @@ func (api *BlockChainAPI) GetProof(ctx context.Context, address common.Address,
keyLengths = make([]int, len(storageKeys))
storageProof = make([]StorageResult, len(storageKeys))
)
// In partial state mode, storage proofs are only available for tracked contracts.
// Account proofs work for ALL accounts since we have the full account trie.
if len(storageKeys) > 0 && api.b.PartialStateEnabled() && !api.b.IsContractTracked(address) {
return nil, &StorageNotTrackedError{Address: address}
}
// Deserialize all keys. This prevents state access on invalid input.
for i, hexKey := range storageKeys {
var err error
@ -579,6 +584,12 @@ 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.
func (api *BlockChainAPI) GetCode(ctx context.Context, address common.Address, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) {
// Check if code is available for this contract in partial state mode
// Note: Account code hash is available for all accounts, but actual bytecode
// is only stored for tracked contracts in partial state mode.
if api.b.PartialStateEnabled() && !api.b.IsContractTracked(address) {
return nil, &CodeNotTrackedError{Address: address}
}
state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
return nil, err
@ -591,6 +602,10 @@ func (api *BlockChainAPI) GetCode(ctx context.Context, address common.Address, b
// block number. The rpc.LatestBlockNumber and rpc.PendingBlockNumber meta block
// numbers are also allowed.
func (api *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Address, hexKey string, blockNrOrHash rpc.BlockNumberOrHash) (hexutil.Bytes, error) {
// Check if storage is available for this contract in partial state mode
if api.b.PartialStateEnabled() && !api.b.IsContractTracked(address) {
return nil, &StorageNotTrackedError{Address: address}
}
state, _, err := api.b.StateAndHeaderByNumberOrHash(ctx, blockNrOrHash)
if state == nil || err != nil {
return nil, err

View file

@ -3979,6 +3979,12 @@ func (b *testBackend) RPCTxSyncMaxTimeout() time.Duration {
func (b *backendMock) RPCTxSyncDefaultTimeout() time.Duration { return 2 * time.Second }
func (b *backendMock) RPCTxSyncMaxTimeout() time.Duration { return 5 * time.Minute }
// Partial state awareness methods - test backends behave as full nodes
func (b *testBackend) PartialStateEnabled() bool { return false }
func (b *testBackend) IsContractTracked(addr common.Address) bool { return true }
func (b *backendMock) PartialStateEnabled() bool { return false }
func (b *backendMock) IsContractTracked(addr common.Address) bool { return true }
func makeSignedRaw(t *testing.T, api *TransactionAPI, from, to common.Address, value *big.Int) (hexutil.Bytes, *types.Transaction) {
t.Helper()
@ -4157,3 +4163,276 @@ func TestGetStorageValues(t *testing.T) {
t.Fatal("expected error for exceeding slot limit")
}
}
// ============================================================================
// Partial State Mode Tests
// ============================================================================
// partialStateTestBackend wraps a testBackend to simulate partial state mode.
// It tracks a specific set of contracts and returns errors for untracked ones.
type partialStateTestBackend struct {
*testBackend
trackedContracts map[common.Address]struct{}
}
func newPartialStateTestBackend(tb *testBackend, tracked []common.Address) *partialStateTestBackend {
m := make(map[common.Address]struct{}, len(tracked))
for _, addr := range tracked {
m[addr] = struct{}{}
}
return &partialStateTestBackend{
testBackend: tb,
trackedContracts: m,
}
}
func (b *partialStateTestBackend) PartialStateEnabled() bool {
return true
}
func (b *partialStateTestBackend) IsContractTracked(addr common.Address) bool {
_, ok := b.trackedContracts[addr]
return ok
}
func TestPartialState_GetStorageAt_UntrackedContract(t *testing.T) {
t.Parallel()
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
common.HexToAddress("0x1111111111111111111111111111111111111111"): {
Balance: big.NewInt(1000000000),
Storage: map[common.Hash]common.Hash{
common.HexToHash("0x0"): common.HexToHash("0x42"),
},
},
},
}
tb := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
// Create partial state backend with no tracked contracts
b := newPartialStateTestBackend(tb, nil)
api := NewBlockChainAPI(b)
// Query storage for untracked contract should fail
untrackedAddr := common.HexToAddress("0x1111111111111111111111111111111111111111")
_, err := api.GetStorageAt(context.Background(), untrackedAddr, "0x0", rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
if err == nil {
t.Fatal("expected error for untracked contract storage")
}
var storageErr *StorageNotTrackedError
if !errors.As(err, &storageErr) {
t.Fatalf("expected StorageNotTrackedError, got %T: %v", err, err)
}
if storageErr.Address != untrackedAddr {
t.Errorf("expected address %s, got %s", untrackedAddr.Hex(), storageErr.Address.Hex())
}
if storageErr.ErrorCode() != errCodeStorageNotTracked {
t.Errorf("expected error code %d, got %d", errCodeStorageNotTracked, storageErr.ErrorCode())
}
}
func TestPartialState_GetStorageAt_TrackedContract(t *testing.T) {
t.Parallel()
trackedAddr := common.HexToAddress("0x1111111111111111111111111111111111111111")
expectedValue := common.HexToHash("0x42")
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
trackedAddr: {
Balance: big.NewInt(1000000000),
Storage: map[common.Hash]common.Hash{
common.HexToHash("0x0"): expectedValue,
},
},
},
}
tb := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
// Create partial state backend with the contract tracked
b := newPartialStateTestBackend(tb, []common.Address{trackedAddr})
api := NewBlockChainAPI(b)
// Query storage for tracked contract should succeed
result, err := api.GetStorageAt(context.Background(), trackedAddr, "0x0", rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if common.BytesToHash(result) != expectedValue {
t.Errorf("expected value %s, got %s", expectedValue.Hex(), common.BytesToHash(result).Hex())
}
}
func TestPartialState_GetCode_UntrackedContract(t *testing.T) {
t.Parallel()
contractAddr := common.HexToAddress("0x1111111111111111111111111111111111111111")
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
contractAddr: {
Balance: big.NewInt(1000000000),
Code: []byte{0x60, 0x00}, // PUSH1 0x00
},
},
}
tb := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
// Create partial state backend with no tracked contracts
b := newPartialStateTestBackend(tb, nil)
api := NewBlockChainAPI(b)
// Query code for untracked contract should fail
_, err := api.GetCode(context.Background(), contractAddr, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
if err == nil {
t.Fatal("expected error for untracked contract code")
}
var codeErr *CodeNotTrackedError
if !errors.As(err, &codeErr) {
t.Fatalf("expected CodeNotTrackedError, got %T: %v", err, err)
}
if codeErr.ErrorCode() != errCodeCodeNotTracked {
t.Errorf("expected error code %d, got %d", errCodeCodeNotTracked, codeErr.ErrorCode())
}
}
func TestPartialState_GetCode_TrackedContract(t *testing.T) {
t.Parallel()
contractAddr := common.HexToAddress("0x1111111111111111111111111111111111111111")
expectedCode := []byte{0x60, 0x00} // PUSH1 0x00
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
contractAddr: {
Balance: big.NewInt(1000000000),
Code: expectedCode,
},
},
}
tb := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
// Create partial state backend with the contract tracked
b := newPartialStateTestBackend(tb, []common.Address{contractAddr})
api := NewBlockChainAPI(b)
// Query code for tracked contract should succeed
result, err := api.GetCode(context.Background(), contractAddr, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if !bytes.Equal(result, expectedCode) {
t.Errorf("expected code %x, got %x", expectedCode, result)
}
}
func TestPartialState_GetProof_AccountOnly(t *testing.T) {
t.Parallel()
// Any account should work for account-only proofs (no storage keys)
accountAddr := common.HexToAddress("0x1111111111111111111111111111111111111111")
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
accountAddr: {
Balance: big.NewInt(1000000000),
},
},
}
tb := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
// Create partial state backend with no tracked contracts
b := newPartialStateTestBackend(tb, nil)
api := NewBlockChainAPI(b)
// Account-only proof should succeed even for untracked addresses
result, err := api.GetProof(context.Background(), accountAddr, nil, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if result.Address != accountAddr {
t.Errorf("expected address %s, got %s", accountAddr.Hex(), result.Address.Hex())
}
if result.Balance.ToInt().Cmp(big.NewInt(1000000000)) != 0 {
t.Errorf("expected balance 1000000000, got %s", result.Balance.String())
}
}
func TestPartialState_GetProof_StorageKeysUntracked(t *testing.T) {
t.Parallel()
accountAddr := common.HexToAddress("0x1111111111111111111111111111111111111111")
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
accountAddr: {
Balance: big.NewInt(1000000000),
Storage: map[common.Hash]common.Hash{
common.HexToHash("0x0"): common.HexToHash("0x42"),
},
},
},
}
tb := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
// Create partial state backend with no tracked contracts
b := newPartialStateTestBackend(tb, nil)
api := NewBlockChainAPI(b)
// Proof with storage keys should fail for untracked contracts
_, err := api.GetProof(context.Background(), accountAddr, []string{"0x0"}, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
if err == nil {
t.Fatal("expected error for storage proof on untracked contract")
}
var storageErr *StorageNotTrackedError
if !errors.As(err, &storageErr) {
t.Fatalf("expected StorageNotTrackedError, got %T: %v", err, err)
}
}
func TestPartialState_GetProof_StorageKeysTracked(t *testing.T) {
t.Parallel()
trackedAddr := common.HexToAddress("0x1111111111111111111111111111111111111111")
genesis := &core.Genesis{
Config: params.TestChainConfig,
Alloc: types.GenesisAlloc{
trackedAddr: {
Balance: big.NewInt(1000000000),
Storage: map[common.Hash]common.Hash{
common.HexToHash("0x0"): common.HexToHash("0x42"),
},
},
},
}
tb := newTestBackend(t, 0, genesis, ethash.NewFaker(), nil)
// Create partial state backend with the contract tracked
b := newPartialStateTestBackend(tb, []common.Address{trackedAddr})
api := NewBlockChainAPI(b)
// Proof with storage keys should succeed for tracked contracts
result, err := api.GetProof(context.Background(), trackedAddr, []string{"0x0"}, rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber))
if err != nil {
t.Fatalf("unexpected error: %v", err)
}
if len(result.StorageProof) != 1 {
t.Fatalf("expected 1 storage proof, got %d", len(result.StorageProof))
}
if result.StorageProof[0].Value.ToInt().Cmp(big.NewInt(0x42)) != 0 {
t.Errorf("expected storage value 0x42, got %s", result.StorageProof[0].Value.String())
}
}

View file

@ -92,6 +92,10 @@ type Backend interface {
Engine() consensus.Engine
HistoryPruningCutoff() uint64
// Partial state awareness
PartialStateEnabled() bool // returns true if partial state mode is active
IsContractTracked(addr common.Address) bool // returns true if contract storage is tracked
// This is copied from filters.Backend
// eth/filters needs to be initialized from this backend type, so methods needed by
// it must also be included here.

View file

@ -172,6 +172,36 @@ func (e *invalidBlockTimestampError) ErrorCode() int { return errCodeBlockTimest
type blockGasLimitReachedError struct{ message string }
// Partial state error codes per EIP-7928 / partial statefulness spec
const (
errCodeStorageNotTracked = -32001
errCodeCodeNotTracked = -32002
)
// StorageNotTrackedError is returned when querying storage for a contract
// that is not tracked in partial statefulness mode.
type StorageNotTrackedError struct {
Address common.Address
}
func (e *StorageNotTrackedError) Error() string {
return fmt.Sprintf("storage not tracked for contract %s", e.Address.Hex())
}
func (e *StorageNotTrackedError) ErrorCode() int { return errCodeStorageNotTracked }
// CodeNotTrackedError is returned when querying bytecode for a contract
// that is not tracked in partial statefulness mode.
type CodeNotTrackedError struct {
Address common.Address
}
func (e *CodeNotTrackedError) Error() string {
return fmt.Sprintf("code not tracked for contract %s", e.Address.Hex())
}
func (e *CodeNotTrackedError) ErrorCode() int { return errCodeCodeNotTracked }
func (e *blockGasLimitReachedError) Error() string { return e.message }
func (e *blockGasLimitReachedError) ErrorCode() int { return errCodeBlockGasLimitReached }