@@ -42,6 +42,7 @@ import (
4242 "github.com/ethereum/go-ethereum/crypto"
4343 "github.com/ethereum/go-ethereum/params"
4444 "github.com/ethereum/go-ethereum/rpc"
45+ "github.com/ethereum/go-ethereum/trie"
4546 "github.com/ethereum/go-ethereum/triedb"
4647 "github.com/holiman/uint256"
4748 "github.com/stretchr/testify/assert"
@@ -278,6 +279,73 @@ func TestStorageRangeAt(t *testing.T) {
278279 }
279280}
280281
282+ // TestStorageRangeAtMissingNode ensures that storageRangeAt returns an error,
283+ // rather than a silently truncated result, when a storage trie node cannot be
284+ // resolved mid-range. The iterator reports the end of a range and an I/O
285+ // failure the same way (Next returns false), so without checking the iterator
286+ // error a partial result would be returned with a nil NextKey, falsely claiming
287+ // that every key was included.
288+ func TestStorageRangeAtMissingNode (t * testing.T ) {
289+ t .Parallel ()
290+
291+ // Build a state where an account has enough storage entries that its storage
292+ // trie contains internal nodes (so there is a non-root node to remove).
293+ var (
294+ mdb = rawdb .NewMemoryDatabase ()
295+ tdb = triedb .NewDatabase (mdb , & triedb.Config {Preimages : true })
296+ db = state .NewDatabase (tdb , nil )
297+ sdb , _ = state .New (types .EmptyRootHash , db )
298+ addr = common.Address {0x01 }
299+ )
300+ for i := 0 ; i < 64 ; i ++ {
301+ sdb .SetState (addr , common .BytesToHash ([]byte {byte (i )}), common .BytesToHash ([]byte {byte (i + 1 )}))
302+ }
303+ root , _ := sdb .Commit (params.Rules {}, 0 )
304+
305+ // Flush the committed nodes to the underlying disk so a freshly opened trie
306+ // database is forced to resolve them from there.
307+ if err := tdb .Commit (root , false ); err != nil {
308+ t .Fatal (err )
309+ }
310+
311+ // Locate a non-root node of the account's storage trie.
312+ sdb , _ = state .New (root , db )
313+ storageRoot := sdb .GetStorageRoot (addr )
314+ id := trie .StorageTrieID (root , crypto .Keccak256Hash (addr .Bytes ()), storageRoot )
315+ str , err := trie .NewStateTrie (id , tdb )
316+ if err != nil {
317+ t .Fatal (err )
318+ }
319+ nodeIt , err := str .NodeIterator (nil )
320+ if err != nil {
321+ t .Fatal (err )
322+ }
323+ var victim common.Hash
324+ for nodeIt .Next (true ) {
325+ if h := nodeIt .Hash (); h != (common.Hash {}) && h != storageRoot {
326+ victim = h
327+ break
328+ }
329+ }
330+ if victim == (common.Hash {}) {
331+ t .Fatal ("could not find a non-root storage trie node to remove" )
332+ }
333+
334+ // Remove the node from disk, simulating a missing/corrupt trie node, then
335+ // re-open the state on a fresh trie database so the deletion is not masked by
336+ // an in-memory cache.
337+ rawdb .DeleteTrieNode (mdb , common.Hash {}, nil , victim , tdb .Scheme ())
338+
339+ tdb2 := triedb .NewDatabase (mdb , & triedb.Config {Preimages : true })
340+ sdb2 , err := state .New (root , state .NewDatabase (tdb2 , nil ))
341+ if err != nil {
342+ t .Fatal (err )
343+ }
344+ if _ , err := storageRangeAt (sdb2 , root , addr , nil , 1000 ); err == nil {
345+ t .Fatal ("expected an error when a storage trie node is missing, got nil" )
346+ }
347+ }
348+
281349func TestGetModifiedAccounts (t * testing.T ) {
282350 t .Parallel ()
283351
0 commit comments