Skip to content
This repository was archived by the owner on Nov 25, 2025. It is now read-only.

Commit 5013851

Browse files
authored
feat(internal/ethapi)!: reject eth_getProof queries for historical 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
1 parent b6b4dfb commit 5013851

File tree

15 files changed

+991
-11
lines changed

15 files changed

+991
-11
lines changed

RELEASES.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Release Notes
22

33
## [v0.14.1](https://github.com/ava-labs/coreth/releases/tag/v0.14.1)
4+
5+
- IMPORTANT: `eth_getProof` calls for historical state will be rejected by default.
6+
- 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).
7+
- 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.
48
- Remove API eth_getAssetBalance that was used to query ANT balances (deprecated since v0.10.0)
59
- Remove legacy gossip handler and metrics (deprecated since v0.10.0)
610
- Refactored trie_prefetcher.go to be structurally similar to [upstream](https://github.com/ethereum/go-ethereum/tree/v1.13.14).

core/state_manager.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -41,12 +41,12 @@ func init() {
4141
}
4242

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

5151
// flushWindow is the distance to the [commitInterval] when we start
5252
// optimistically flushing trie nodes to disk (only applicable in [pruning]
@@ -79,7 +79,7 @@ func NewTrieWriter(db TrieDB, config *CacheConfig) TrieWriter {
7979
targetCommitSize: common.StorageSize(config.TrieDirtyCommitTarget) * 1024 * 1024,
8080
imageCap: 4 * 1024 * 1024,
8181
commitInterval: config.CommitInterval,
82-
tipBuffer: NewBoundedBuffer(tipBufferSize, db.Dereference),
82+
tipBuffer: NewBoundedBuffer(TipBufferSize, db.Dereference),
8383
}
8484
cm.flushStepSize = (cm.memoryCap - cm.targetCommitSize) / common.StorageSize(flushWindow)
8585
return cm

core/state_manager_test.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,10 @@ func TestCappedMemoryTrieWriter(t *testing.T) {
5353
assert.Equal(common.Hash{}, m.LastCommit, "should not have committed block on insert")
5454

5555
w.AcceptTrie(block)
56-
if i <= tipBufferSize {
56+
if i <= TipBufferSize {
5757
assert.Equal(common.Hash{}, m.LastDereference, "should not have dereferenced block on accept")
5858
} else {
59-
assert.Equal(common.BigToHash(big.NewInt(int64(i-tipBufferSize))), m.LastDereference, "should have dereferenced old block on last accept")
59+
assert.Equal(common.BigToHash(big.NewInt(int64(i-TipBufferSize))), m.LastDereference, "should have dereferenced old block on last accept")
6060
m.LastDereference = common.Hash{}
6161
}
6262
if i < int(cacheConfig.CommitInterval) {
@@ -77,7 +77,7 @@ func TestNoPruningTrieWriter(t *testing.T) {
7777
m := &MockTrieDB{}
7878
w := NewTrieWriter(m, &CacheConfig{})
7979
assert := assert.New(t)
80-
for i := 0; i < tipBufferSize+1; i++ {
80+
for i := 0; i < TipBufferSize+1; i++ {
8181
bigI := big.NewInt(int64(i))
8282
block := types.NewBlock(
8383
&types.Header{

eth/api_backend.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,13 +60,28 @@ type EthAPIBackend struct {
6060
allowUnfinalizedQueries bool
6161
eth *Ethereum
6262
gpo *gasprice.Oracle
63+
64+
// historicalProofQueryWindow is the number of blocks before the last accepted block to be accepted for
65+
// state queries when running archive mode.
66+
historicalProofQueryWindow uint64
6367
}
6468

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

74+
// IsArchive returns true if the node is running in archive mode, false otherwise.
75+
func (b *EthAPIBackend) IsArchive() bool {
76+
return !b.eth.config.Pruning
77+
}
78+
79+
// HistoricalProofQueryWindow returns the number of blocks before the last accepted block to be accepted for state queries.
80+
// It returns 0 to indicate to accept any block number for state queries.
81+
func (b *EthAPIBackend) HistoricalProofQueryWindow() uint64 {
82+
return b.historicalProofQueryWindow
83+
}
84+
7085
func (b *EthAPIBackend) IsAllowUnfinalizedQueries() bool {
7186
return b.allowUnfinalizedQueries
7287
}

eth/backend.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -263,11 +263,12 @@ func New(
263263
}
264264

265265
eth.APIBackend = &EthAPIBackend{
266-
extRPCEnabled: stack.Config().ExtRPCEnabled(),
267-
allowUnprotectedTxs: config.AllowUnprotectedTxs,
268-
allowUnprotectedTxHashes: allowUnprotectedTxHashes,
269-
allowUnfinalizedQueries: config.AllowUnfinalizedQueries,
270-
eth: eth,
266+
extRPCEnabled: stack.Config().ExtRPCEnabled(),
267+
allowUnprotectedTxs: config.AllowUnprotectedTxs,
268+
allowUnprotectedTxHashes: allowUnprotectedTxHashes,
269+
allowUnfinalizedQueries: config.AllowUnfinalizedQueries,
270+
historicalProofQueryWindow: config.HistoricalProofQueryWindow,
271+
eth: eth,
271272
}
272273
if config.AllowUnprotectedTxs {
273274
log.Info("Unprotected transactions allowed")

eth/ethconfig/config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,11 @@ type Config struct {
137137
// AllowUnfinalizedQueries allow unfinalized queries
138138
AllowUnfinalizedQueries bool
139139

140+
// HistoricalProofQueryWindow is the number of blocks before the last accepted block to be accepted for state queries.
141+
// For archive nodes, it defaults to 43200 and can be set to 0 to indicate to accept any block query.
142+
// For non-archive nodes, it is forcibly set to the value of [core.TipBufferSize].
143+
HistoricalProofQueryWindow uint64
144+
140145
// AllowUnprotectedTxs allow unprotected transactions to be locally issued.
141146
// Unprotected transactions are transactions that are signed without EIP-155
142147
// replay protection.

internal/ethapi/api.go

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -676,7 +676,14 @@ func (n *proofList) Delete(key []byte) error {
676676
}
677677

678678
// GetProof returns the Merkle-proof for a given account and optionally some storage keys.
679+
// If the requested block is part of historical blocks and the node does not accept
680+
// getting proofs for historical blocks, an error is returned.
679681
func (s *BlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNrOrHash rpc.BlockNumberOrHash) (*AccountResult, error) {
682+
err := s.stateQueryBlockNumberAllowed(blockNrOrHash)
683+
if err != nil {
684+
return nil, fmt.Errorf("historical proof query not allowed: %s", err)
685+
}
686+
680687
var (
681688
keys = make([]common.Hash, len(storageKeys))
682689
keyLengths = make([]int, len(storageKeys))

internal/ethapi/api_extra.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -92,3 +92,42 @@ func (s *BlockChainAPI) GetBadBlocks(ctx context.Context) ([]*BadBlockArgs, erro
9292
}
9393
return results, nil
9494
}
95+
96+
// stateQueryBlockNumberAllowed returns a nil error if:
97+
// - the node is configured to accept any state query (the query window is zero)
98+
// - the block given has its number within the query window before the last accepted block.
99+
// This query window is set to [core.TipBufferSize] when running in a non-archive mode.
100+
//
101+
// Otherwise, it returns a non-nil error containing block number information.
102+
func (s *BlockChainAPI) stateQueryBlockNumberAllowed(blockNumOrHash rpc.BlockNumberOrHash) (err error) {
103+
queryWindow := uint64(core.TipBufferSize)
104+
if s.b.IsArchive() {
105+
queryWindow = s.b.HistoricalProofQueryWindow()
106+
if queryWindow == 0 {
107+
return nil
108+
}
109+
}
110+
111+
lastAcceptedNumber := s.b.LastAcceptedBlock().NumberU64()
112+
113+
var number uint64
114+
if blockNumOrHash.BlockNumber != nil {
115+
number = uint64(blockNumOrHash.BlockNumber.Int64())
116+
} else {
117+
block, err := s.b.BlockByNumberOrHash(context.Background(), blockNumOrHash)
118+
if err != nil {
119+
return fmt.Errorf("failed to get block from hash: %s", err)
120+
}
121+
number = block.NumberU64()
122+
}
123+
124+
var oldestAllowed uint64
125+
if lastAcceptedNumber > queryWindow {
126+
oldestAllowed = lastAcceptedNumber - queryWindow
127+
}
128+
if number >= oldestAllowed {
129+
return nil
130+
}
131+
return fmt.Errorf("block number %d is before the oldest allowed block number %d (window of %d blocks)",
132+
number, oldestAllowed, queryWindow)
133+
}

internal/ethapi/api_extra_test.go

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,132 @@
1+
// (c) 2019-2024, Ava Labs, Inc. All rights reserved.
2+
// See the file LICENSE for licensing terms.
3+
4+
package ethapi
5+
6+
import (
7+
"fmt"
8+
"math/big"
9+
"testing"
10+
11+
"github.com/ava-labs/coreth/core/types"
12+
"github.com/ava-labs/coreth/rpc"
13+
"github.com/ethereum/go-ethereum/common"
14+
"github.com/stretchr/testify/assert"
15+
"go.uber.org/mock/gomock"
16+
)
17+
18+
func TestBlockChainAPI_stateQueryBlockNumberAllowed(t *testing.T) {
19+
t.Parallel()
20+
21+
const queryWindow uint64 = 1024
22+
23+
makeBlockWithNumber := func(number uint64) *types.Block {
24+
header := &types.Header{
25+
Number: big.NewInt(int64(number)),
26+
}
27+
return types.NewBlock(header, nil, nil, nil, nil)
28+
}
29+
30+
testCases := map[string]struct {
31+
blockNumOrHash rpc.BlockNumberOrHash
32+
makeBackend func(ctrl *gomock.Controller) *MockBackend
33+
wantErrMessage string
34+
}{
35+
"zero_query_window": {
36+
blockNumOrHash: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(1000)),
37+
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
38+
backend := NewMockBackend(ctrl)
39+
backend.EXPECT().IsArchive().Return(true)
40+
backend.EXPECT().HistoricalProofQueryWindow().Return(uint64(0))
41+
return backend
42+
},
43+
},
44+
"block_number_allowed_below_window": {
45+
blockNumOrHash: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(1000)),
46+
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
47+
backend := NewMockBackend(ctrl)
48+
backend.EXPECT().IsArchive().Return(true)
49+
backend.EXPECT().HistoricalProofQueryWindow().Return(queryWindow)
50+
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(1020))
51+
return backend
52+
},
53+
},
54+
"block_number_allowed": {
55+
blockNumOrHash: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(2000)),
56+
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
57+
backend := NewMockBackend(ctrl)
58+
backend.EXPECT().IsArchive().Return(true)
59+
backend.EXPECT().HistoricalProofQueryWindow().Return(queryWindow)
60+
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(2200))
61+
return backend
62+
},
63+
},
64+
"block_number_allowed_by_hash": {
65+
blockNumOrHash: rpc.BlockNumberOrHashWithHash(common.Hash{99}, false),
66+
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
67+
backend := NewMockBackend(ctrl)
68+
backend.EXPECT().IsArchive().Return(true)
69+
backend.EXPECT().HistoricalProofQueryWindow().Return(queryWindow)
70+
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(2200))
71+
backend.EXPECT().
72+
BlockByNumberOrHash(gomock.Any(), gomock.Any()).
73+
Return(makeBlockWithNumber(2000), nil)
74+
return backend
75+
},
76+
},
77+
"block_number_allowed_by_hash_error": {
78+
blockNumOrHash: rpc.BlockNumberOrHashWithHash(common.Hash{99}, false),
79+
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
80+
backend := NewMockBackend(ctrl)
81+
backend.EXPECT().IsArchive().Return(true)
82+
backend.EXPECT().HistoricalProofQueryWindow().Return(queryWindow)
83+
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(2200))
84+
backend.EXPECT().
85+
BlockByNumberOrHash(gomock.Any(), gomock.Any()).
86+
Return(nil, fmt.Errorf("test error"))
87+
return backend
88+
},
89+
wantErrMessage: "failed to get block from hash: test error",
90+
},
91+
"block_number_out_of_window": {
92+
blockNumOrHash: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(1000)),
93+
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
94+
backend := NewMockBackend(ctrl)
95+
backend.EXPECT().IsArchive().Return(true)
96+
backend.EXPECT().HistoricalProofQueryWindow().Return(queryWindow)
97+
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(2200))
98+
return backend
99+
},
100+
wantErrMessage: "block number 1000 is before the oldest allowed block number 1176 (window of 1024 blocks)",
101+
},
102+
"block_number_out_of_window_non_archive": {
103+
blockNumOrHash: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(1000)),
104+
makeBackend: func(ctrl *gomock.Controller) *MockBackend {
105+
backend := NewMockBackend(ctrl)
106+
backend.EXPECT().IsArchive().Return(false)
107+
// query window is 32 as set to core.TipBufferSize
108+
backend.EXPECT().LastAcceptedBlock().Return(makeBlockWithNumber(1033))
109+
return backend
110+
},
111+
wantErrMessage: "block number 1000 is before the oldest allowed block number 1001 (window of 32 blocks)",
112+
},
113+
}
114+
115+
for name, testCase := range testCases {
116+
t.Run(name, func(t *testing.T) {
117+
t.Parallel()
118+
ctrl := gomock.NewController(t)
119+
120+
api := &BlockChainAPI{
121+
b: testCase.makeBackend(ctrl),
122+
}
123+
124+
err := api.stateQueryBlockNumberAllowed(testCase.blockNumOrHash)
125+
if testCase.wantErrMessage == "" {
126+
assert.NoError(t, err)
127+
} else {
128+
assert.EqualError(t, err, testCase.wantErrMessage)
129+
}
130+
})
131+
}
132+
}

internal/ethapi/api_test.go

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,12 @@ func (b testBackend) LastAcceptedBlock() *types.Block { panic("implement me") }
625625
func (b testBackend) SuggestPrice(ctx context.Context) (*big.Int, error) {
626626
panic("implement me")
627627
}
628+
func (b testBackend) IsArchive() bool {
629+
panic("implement me")
630+
}
631+
func (b testBackend) HistoricalProofQueryWindow() (queryWindow uint64) {
632+
panic("implement me")
633+
}
628634

629635
func TestEstimateGas(t *testing.T) {
630636
t.Parallel()

0 commit comments

Comments
 (0)