Skip to content

Commit

Permalink
feat(internal/ethapi)!: reject eth_getProof queries for historical …
Browse files Browse the repository at this point in the history
…blocks (#719)

- default behavior for pruning mode to reject blocks before the 32 blocks preceding the last accepted block
- default behavior for archive mode to reject blocks before ~24h worth of blocks preceding the last accepted block
- archive mode new option `historical-proof-query-window` to customize the blocks window, or set it to 0 to accept any block number
  • Loading branch information
qdm12 authored Jan 15, 2025
1 parent b6b4dfb commit 5013851
Show file tree
Hide file tree
Showing 15 changed files with 991 additions and 11 deletions.
4 changes: 4 additions & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Release Notes

## [v0.14.1](https://github.com/ava-labs/coreth/releases/tag/v0.14.1)

- IMPORTANT: `eth_getProof` calls for historical state will be rejected by default.
- On archive nodes (`"pruning-enabled": false`): queries for historical proofs for state older than approximately 24 hours preceding the last accepted block will be rejected by default. This can be adjusted with the new option `historical-proof-query-window` which defines the number of blocks before the last accepted block which should be accepted for state proof queries, or set to `0` to accept any block number state query (previous behavior).
- On `pruning` nodes: queries for proofs past the tip buffer (32 blocks) will be rejected. This is in support of moving to a path based storage scheme, which does not support historical state proofs.
- Remove API eth_getAssetBalance that was used to query ANT balances (deprecated since v0.10.0)
- Remove legacy gossip handler and metrics (deprecated since v0.10.0)
- Refactored trie_prefetcher.go to be structurally similar to [upstream](https://github.com/ethereum/go-ethereum/tree/v1.13.14).
Expand Down
6 changes: 3 additions & 3 deletions core/state_manager.go
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,12 @@ func init() {
}

const (
// tipBufferSize is the number of recent accepted tries to keep in the TrieDB
// TipBufferSize is the number of recent accepted tries to keep in the TrieDB
// dirties cache at tip (only applicable in [pruning] mode).
//
// Keeping extra tries around at tip enables clients to query data from
// recent trie roots.
tipBufferSize = 32
TipBufferSize = 32

// flushWindow is the distance to the [commitInterval] when we start
// optimistically flushing trie nodes to disk (only applicable in [pruning]
Expand Down Expand Up @@ -79,7 +79,7 @@ func NewTrieWriter(db TrieDB, config *CacheConfig) TrieWriter {
targetCommitSize: common.StorageSize(config.TrieDirtyCommitTarget) * 1024 * 1024,
imageCap: 4 * 1024 * 1024,
commitInterval: config.CommitInterval,
tipBuffer: NewBoundedBuffer(tipBufferSize, db.Dereference),
tipBuffer: NewBoundedBuffer(TipBufferSize, db.Dereference),
}
cm.flushStepSize = (cm.memoryCap - cm.targetCommitSize) / common.StorageSize(flushWindow)
return cm
Expand Down
6 changes: 3 additions & 3 deletions core/state_manager_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,10 +53,10 @@ func TestCappedMemoryTrieWriter(t *testing.T) {
assert.Equal(common.Hash{}, m.LastCommit, "should not have committed block on insert")

w.AcceptTrie(block)
if i <= tipBufferSize {
if i <= TipBufferSize {
assert.Equal(common.Hash{}, m.LastDereference, "should not have dereferenced block on accept")
} else {
assert.Equal(common.BigToHash(big.NewInt(int64(i-tipBufferSize))), m.LastDereference, "should have dereferenced old block on last accept")
assert.Equal(common.BigToHash(big.NewInt(int64(i-TipBufferSize))), m.LastDereference, "should have dereferenced old block on last accept")
m.LastDereference = common.Hash{}
}
if i < int(cacheConfig.CommitInterval) {
Expand All @@ -77,7 +77,7 @@ func TestNoPruningTrieWriter(t *testing.T) {
m := &MockTrieDB{}
w := NewTrieWriter(m, &CacheConfig{})
assert := assert.New(t)
for i := 0; i < tipBufferSize+1; i++ {
for i := 0; i < TipBufferSize+1; i++ {
bigI := big.NewInt(int64(i))
block := types.NewBlock(
&types.Header{
Expand Down
15 changes: 15 additions & 0 deletions eth/api_backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -60,13 +60,28 @@ type EthAPIBackend struct {
allowUnfinalizedQueries bool
eth *Ethereum
gpo *gasprice.Oracle

// historicalProofQueryWindow is the number of blocks before the last accepted block to be accepted for
// state queries when running archive mode.
historicalProofQueryWindow uint64
}

// ChainConfig returns the active chain configuration.
func (b *EthAPIBackend) ChainConfig() *params.ChainConfig {
return b.eth.blockchain.Config()
}

// IsArchive returns true if the node is running in archive mode, false otherwise.
func (b *EthAPIBackend) IsArchive() bool {
return !b.eth.config.Pruning
}

// HistoricalProofQueryWindow returns the number of blocks before the last accepted block to be accepted for state queries.
// It returns 0 to indicate to accept any block number for state queries.
func (b *EthAPIBackend) HistoricalProofQueryWindow() uint64 {
return b.historicalProofQueryWindow
}

func (b *EthAPIBackend) IsAllowUnfinalizedQueries() bool {
return b.allowUnfinalizedQueries
}
Expand Down
11 changes: 6 additions & 5 deletions eth/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -263,11 +263,12 @@ func New(
}

eth.APIBackend = &EthAPIBackend{
extRPCEnabled: stack.Config().ExtRPCEnabled(),
allowUnprotectedTxs: config.AllowUnprotectedTxs,
allowUnprotectedTxHashes: allowUnprotectedTxHashes,
allowUnfinalizedQueries: config.AllowUnfinalizedQueries,
eth: eth,
extRPCEnabled: stack.Config().ExtRPCEnabled(),
allowUnprotectedTxs: config.AllowUnprotectedTxs,
allowUnprotectedTxHashes: allowUnprotectedTxHashes,
allowUnfinalizedQueries: config.AllowUnfinalizedQueries,
historicalProofQueryWindow: config.HistoricalProofQueryWindow,
eth: eth,
}
if config.AllowUnprotectedTxs {
log.Info("Unprotected transactions allowed")
Expand Down
5 changes: 5 additions & 0 deletions eth/ethconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,11 @@ type Config struct {
// AllowUnfinalizedQueries allow unfinalized queries
AllowUnfinalizedQueries bool

// HistoricalProofQueryWindow is the number of blocks before the last accepted block to be accepted for state queries.
// For archive nodes, it defaults to 43200 and can be set to 0 to indicate to accept any block query.
// For non-archive nodes, it is forcibly set to the value of [core.TipBufferSize].
HistoricalProofQueryWindow uint64

// AllowUnprotectedTxs allow unprotected transactions to be locally issued.
// Unprotected transactions are transactions that are signed without EIP-155
// replay protection.
Expand Down
7 changes: 7 additions & 0 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -676,7 +676,14 @@ func (n *proofList) Delete(key []byte) error {
}

// GetProof returns the Merkle-proof for a given account and optionally some storage keys.
// If the requested block is part of historical blocks and the node does not accept
// getting proofs for historical blocks, an error is returned.
func (s *BlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (*AccountResult, error) {
err := s.stateQueryBlockNumberAllowed(blockNrOrHash)
if err != nil {
return nil, fmt.Errorf("historical proof query not allowed: %s", err)
}

var (
keys = make([]common.Hash, len(storageKeys))
keyLengths = make([]int, len(storageKeys))
Expand Down
39 changes: 39 additions & 0 deletions internal/ethapi/api_extra.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,3 +92,42 @@ func (s *BlockChainAPI) GetBadBlocks(ctx context.Context) ([]*BadBlockArgs, erro
}
return results, nil
}

// stateQueryBlockNumberAllowed returns a nil error if:
// - the node is configured to accept any state query (the query window is zero)
// - the block given has its number within the query window before the last accepted block.
// This query window is set to [core.TipBufferSize] when running in a non-archive mode.
//
// Otherwise, it returns a non-nil error containing block number information.
func (s *BlockChainAPI) stateQueryBlockNumberAllowed(blockNumOrHash rpc.BlockNumberOrHash) (err error) {
queryWindow := uint64(core.TipBufferSize)
if s.b.IsArchive() {
queryWindow = s.b.HistoricalProofQueryWindow()
if queryWindow == 0 {
return nil
}
}

lastAcceptedNumber := s.b.LastAcceptedBlock().NumberU64()

var number uint64
if blockNumOrHash.BlockNumber != nil {
number = uint64(blockNumOrHash.BlockNumber.Int64())
} else {
block, err := s.b.BlockByNumberOrHash(context.Background(), blockNumOrHash)
if err != nil {
return fmt.Errorf("failed to get block from hash: %s", err)
}
number = block.NumberU64()
}

var oldestAllowed uint64
if lastAcceptedNumber > queryWindow {
oldestAllowed = lastAcceptedNumber - queryWindow
}
if number >= oldestAllowed {
return nil
}
return fmt.Errorf("block number %d is before the oldest allowed block number %d (window of %d blocks)",
number, oldestAllowed, queryWindow)
}
132 changes: 132 additions & 0 deletions internal/ethapi/api_extra_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
// (c) 2019-2024, Ava Labs, Inc. All rights reserved.
// See the file LICENSE for licensing terms.

package ethapi

import (
"fmt"
"math/big"
"testing"

"github.com/ava-labs/coreth/core/types"
"github.com/ava-labs/coreth/rpc"
"github.com/ethereum/go-ethereum/common"
"github.com/stretchr/testify/assert"
"go.uber.org/mock/gomock"
)

func TestBlockChainAPI_stateQueryBlockNumberAllowed(t *testing.T) {
t.Parallel()

const queryWindow uint64 = 1024

makeBlockWithNumber := func(number uint64) *types.Block {
header := &types.Header{
Number: big.NewInt(int64(number)),
}
return types.NewBlock(header, nil, nil, nil, nil)
}

testCases := map[string]struct {
blockNumOrHash rpc.BlockNumberOrHash
makeBackend func(ctrl *gomock.Controller) *MockBackend
wantErrMessage string
}{
"zero_query_window": {
blockNumOrHash: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(1000)),
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
backend := NewMockBackend(ctrl)
backend.EXPECT().IsArchive().Return(true)
backend.EXPECT().HistoricalProofQueryWindow().Return(uint64(0))
return backend
},
},
"block_number_allowed_below_window": {
blockNumOrHash: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(1000)),
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
backend := NewMockBackend(ctrl)
backend.EXPECT().IsArchive().Return(true)
backend.EXPECT().HistoricalProofQueryWindow().Return(queryWindow)
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(1020))
return backend
},
},
"block_number_allowed": {
blockNumOrHash: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(2000)),
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
backend := NewMockBackend(ctrl)
backend.EXPECT().IsArchive().Return(true)
backend.EXPECT().HistoricalProofQueryWindow().Return(queryWindow)
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(2200))
return backend
},
},
"block_number_allowed_by_hash": {
blockNumOrHash: rpc.BlockNumberOrHashWithHash(common.Hash{99}, false),
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
backend := NewMockBackend(ctrl)
backend.EXPECT().IsArchive().Return(true)
backend.EXPECT().HistoricalProofQueryWindow().Return(queryWindow)
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(2200))
backend.EXPECT().
BlockByNumberOrHash(gomock.Any(), gomock.Any()).
Return(makeBlockWithNumber(2000), nil)
return backend
},
},
"block_number_allowed_by_hash_error": {
blockNumOrHash: rpc.BlockNumberOrHashWithHash(common.Hash{99}, false),
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
backend := NewMockBackend(ctrl)
backend.EXPECT().IsArchive().Return(true)
backend.EXPECT().HistoricalProofQueryWindow().Return(queryWindow)
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(2200))
backend.EXPECT().
BlockByNumberOrHash(gomock.Any(), gomock.Any()).
Return(nil, fmt.Errorf("test error"))
return backend
},
wantErrMessage: "failed to get block from hash: test error",
},
"block_number_out_of_window": {
blockNumOrHash: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(1000)),
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
backend := NewMockBackend(ctrl)
backend.EXPECT().IsArchive().Return(true)
backend.EXPECT().HistoricalProofQueryWindow().Return(queryWindow)
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(2200))
return backend
},
wantErrMessage: "block number 1000 is before the oldest allowed block number 1176 (window of 1024 blocks)",
},
"block_number_out_of_window_non_archive": {
blockNumOrHash: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(1000)),
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
backend := NewMockBackend(ctrl)
backend.EXPECT().IsArchive().Return(false)
// query window is 32 as set to core.TipBufferSize
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(1033))
return backend
},
wantErrMessage: "block number 1000 is before the oldest allowed block number 1001 (window of 32 blocks)",
},
}

for name, testCase := range testCases {
t.Run(name, func(t *testing.T) {
t.Parallel()
ctrl := gomock.NewController(t)

api := &BlockChainAPI{
b: testCase.makeBackend(ctrl),
}

err := api.stateQueryBlockNumberAllowed(testCase.blockNumOrHash)
if testCase.wantErrMessage == "" {
assert.NoError(t, err)
} else {
assert.EqualError(t, err, testCase.wantErrMessage)
}
})
}
}
6 changes: 6 additions & 0 deletions internal/ethapi/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -625,6 +625,12 @@ func (b testBackend) LastAcceptedBlock() *types.Block { panic("implement me") }
func (b testBackend) SuggestPrice(ctx context.Context) (*big.Int, error) {
panic("implement me")
}
func (b testBackend) IsArchive() bool {
panic("implement me")
}
func (b testBackend) HistoricalProofQueryWindow() (queryWindow uint64) {
panic("implement me")
}

func TestEstimateGas(t *testing.T) {
t.Parallel()
Expand Down
2 changes: 2 additions & 0 deletions internal/ethapi/backend.go
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ type Backend interface {
SubscribeChainHeadEvent(ch chan<- core.ChainHeadEvent) event.Subscription
SubscribeChainSideEvent(ch chan<- core.ChainSideEvent) event.Subscription
BadBlocks() ([]*types.Block, []*core.BadBlockReason)
IsArchive() bool
HistoricalProofQueryWindow() uint64

// Transaction pool API
SendTx(ctx context.Context, signedTx *types.Transaction) error
Expand Down
3 changes: 3 additions & 0 deletions internal/ethapi/mocks_generate_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package ethapi

//go:generate go run go.uber.org/mock/mockgen -package=$GOPACKAGE -destination=mocks_test.go . Backend
Loading

0 comments on commit 5013851

Please sign in to comment.