diff --git a/execution/state/versionedio.go b/execution/state/versionedio.go index 52a1ae1fd1f..601316340cb 100644 --- a/execution/state/versionedio.go +++ b/execution/state/versionedio.go @@ -1336,7 +1336,8 @@ func (vr *versionedStateReader) TracePrefix() string { } func (vr *versionedStateReader) ReadAccountData(address accounts.Address) (*accounts.Account, error) { - if r, ok := vr.reads.GetAddress(address); ok && r.Val != nil && !r.Val.IsNil() { + r, recorded := vr.reads.GetAddress(address) + if recorded && r.Val != nil && !r.Val.IsNil() { account := r.Val.Account() updated := vr.applyVersionedUpdates(address, *account) return &updated, nil @@ -1387,7 +1388,9 @@ func (vr *versionedStateReader) ReadAccountData(address accounts.Address) (*acco } } - if vr.stateReader != nil { + // A recorded AddressPath read with no account is the tx's own conclusion that + // the address holds nothing; the domain cannot say otherwise. + if vr.stateReader != nil && !recorded { account, err := vr.stateReader.ReadAccountData(address) if err != nil { diff --git a/execution/state/versionedio_test.go b/execution/state/versionedio_test.go index dbe299c56f8..42dc7bdca93 100644 --- a/execution/state/versionedio_test.go +++ b/execution/state/versionedio_test.go @@ -17,6 +17,7 @@ package state import ( + "errors" "fmt" "sort" "testing" @@ -1596,6 +1597,58 @@ func TestVersionedUpdates_EstimateCellConsumed(t *testing.T) { require.Equal(t, newStorage, storageGot, "Estimate-cell storage must be consumed, not stale") } +// countingStateReader records how many account reads reached the domain. +type countingStateReader struct { + minimalStateReader + acc *accounts.Account + accountReads int + failOnAccounts bool +} + +func (r *countingStateReader) ReadAccountData(addr accounts.Address) (*accounts.Account, error) { + r.accountReads++ + if r.failOnAccounts { + return nil, errors.New("domain must not be consulted") + } + return r.acc, nil +} + +// A tx that read an address and found no account records the AddressPath entry +// header-only; that entry is the answer, not a gap to fill from the domain. +func TestVersionedStateReader_RecordedAbsentSkipsDomain(t *testing.T) { + t.Parallel() + + addr := accounts.InternAddress(common.HexToAddress("0xab5e17")) + reads := ReadSet{} + reads.SetAddress(addr, VersionedRead[AccountView]{ + ReadHeader: ReadHeader{Source: StorageRead, Version: UnknownVersion}, + }) + + reader := &countingStateReader{failOnAccounts: true} + vr := NewVersionedStateReader(3, reads, NewVersionMap(nil), reader) + + got, err := vr.ReadAccountData(addr) + require.NoError(t, err) + require.Nil(t, got) + require.Zero(t, reader.accountReads, "recorded-absent read must not reach the domain") +} + +func TestVersionedStateReader_UnrecordedAddressReadsDomain(t *testing.T) { + t.Parallel() + + addr := accounts.InternAddress(common.HexToAddress("0xc01d")) + domainAcc := accounts.NewAccount() + domainAcc.Nonce = 9 + reader := &countingStateReader{acc: &domainAcc} + vr := NewVersionedStateReader(3, ReadSet{}, NewVersionMap(nil), reader) + + got, err := vr.ReadAccountData(addr) + require.NoError(t, err) + require.NotNil(t, got) + require.Equal(t, uint64(9), got.Nonce) + require.Equal(t, 1, reader.accountReads) +} + // writeSetFixture builds a WriteSet covering every path, multiple addresses and // storage keys — the input for the iteration closed-loop tests. func writeSetFixture() (*WriteSet, []string) {