mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-06-08 16:01:36 +00:00
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:
parent
9f52b96b6c
commit
df2a91fb0a
5 changed files with 346 additions and 0 deletions
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue