core/state, core/types, trie: implement EIP-8032

This commit is contained in:
Guillaume Ballet 2025-10-08 21:01:35 +02:00
parent ee309827c6
commit e09e1aa791
18 changed files with 269 additions and 45 deletions

View file

@ -103,7 +103,8 @@ type Trie interface {
// any existing value is deleted from the trie. The value bytes must not be modified
// by the caller while they are stored in the trie. If a node was not found in the
// database, a trie.MissingNodeError is returned.
UpdateStorage(addr common.Address, key, value []byte) error
// Returns the depth at which the value was inserted in the trie.
UpdateStorage(addr common.Address, key, value []byte) (int, error)
// DeleteAccount abstracts an account deletion from the trie.
DeleteAccount(address common.Address) error

168
core/state/maxdepth_test.go Normal file
View file

@ -0,0 +1,168 @@
// Copyright 2025 The go-ethereum Authors
// This file is part of the go-ethereum library.
//
// The go-ethereum library is free software: you can redistribute it and/or modify
// it under the terms of the GNU Lesser General Public License as published by
// the Free Software Foundation, either version 3 of the License, or
// (at your option) any later version.
//
// The go-ethereum library is distributed in the hope that it will be useful,
// but WITHOUT ANY WARRANTY; without even the implied warranty of
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
// GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
package state
import (
"math/big"
"testing"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/rawdb"
"github.com/ethereum/go-ethereum/core/tracing"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/triedb"
"github.com/holiman/uint256"
)
// TestMaxDepthInitialization tests that MaxDepth starts at 0 for new accounts
func TestMaxDepthInitialization(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, nil)
sdb := NewDatabase(tdb, nil)
state, _ := New(types.EmptyRootHash, sdb)
addr := common.HexToAddress("0x1234")
state.AddBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
if maxDepth := state.GetMaxDepth(addr); maxDepth != 0 {
t.Errorf("Initial MaxDepth should be 0, got %d", maxDepth)
}
}
// TestMaxDepthMultipleStorage tests MaxDepth tracking with multiple storage writes
func TestMaxDepthMultipleStorage(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, nil)
sdb := NewDatabase(tdb, nil)
state, _ := New(types.EmptyRootHash, sdb)
addr := common.HexToAddress("0x1234")
for i := 0; i < 10; i++ {
key := common.BigToHash(big.NewInt(int64(i)))
val := common.BigToHash(big.NewInt(int64(i * 100)))
state.SetState(addr, key, val)
}
root, err := state.Commit(0, false, false)
if err != nil {
t.Fatalf("Failed to commit state: %v", err)
}
state, _ = New(root, sdb)
maxDepth := state.GetMaxDepth(addr)
if maxDepth == 0 {
t.Error("MaxDepth should be > 0 after storage writes")
}
t.Logf("MaxDepth after 10 storage writes: %d", maxDepth)
}
// TestMaxDepthEmptyAccount tests that non-existent accounts return 0 for MaxDepth
func TestMaxDepthEmptyAccount(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, nil)
sdb := NewDatabase(tdb, nil)
state, _ := New(types.EmptyRootHash, sdb)
addr := common.HexToAddress("0x9999")
// Check MaxDepth for non-existent account
maxDepth := state.GetMaxDepth(addr)
if maxDepth != 0 {
t.Errorf("Non-existent account should have MaxDepth 0, got %d", maxDepth)
}
}
// TestMaxDepthIncrementalIncrease tests that MaxDepth increases progressively
// as more keys are added to the storage trie. With carefully chosen keys that
// create hash collisions at specific depths (2-3), we can observe incremental increases.
func TestMaxDepthIncrementalIncrease(t *testing.T) {
db := rawdb.NewMemoryDatabase()
tdb := triedb.NewDatabase(db, nil)
sdb := NewDatabase(tdb, nil)
addr := common.HexToAddress("0x1234")
// Insert keys progressively and track MaxDepth changes.
// With enough keys, we'll naturally see depth increases.
// We insert in multiple "blocks" to simulate incremental growth.
var previousMaxDepth uint64 = 0
var depthIncreases []int // Track which blocks caused depth increases
// We'll insert keys in batches, starting from a fresh state each time
// This simulates blocks where storage grows incrementally
// Use larger, more spread-out values to increase hash collision chances
keyBatches := []int{1, 200, 200, 200, 200, 200} // Number of keys to insert in each "block"
for blockNum, totalKeys := range keyBatches {
state, _ := New(types.EmptyRootHash, sdb)
state.AddBalance(addr, uint256.NewInt(1), tracing.BalanceChangeUnspecified)
// Insert keys using larger, more spread-out values to increase
// the chance of hash collisions that create deeper trie structures
for i := range totalKeys {
// Use a larger multiplier to spread keys across hash space
key := common.BigToHash(big.NewInt(int64(i*1000 + 1)))
val := common.BigToHash(big.NewInt(int64(i*100 + 1)))
state.SetState(addr, key, val)
}
root, err := state.Commit(0, false, false)
if err != nil {
t.Fatalf("Block %d: Failed to commit state: %v", blockNum, err)
}
if err := tdb.Commit(root, false); err != nil {
t.Fatalf("Block %d: Failed to commit trie: %v", blockNum, err)
}
// Reload state and get MaxDepth
state, _ = New(root, sdb)
currentMaxDepth := state.GetMaxDepth(addr)
t.Logf("Block %d: Inserted %d keys, MaxDepth = %d (previous = %d)",
blockNum, totalKeys, currentMaxDepth, previousMaxDepth)
// Track if depth increased
if currentMaxDepth > previousMaxDepth {
depthIncreases = append(depthIncreases, blockNum)
}
// MaxDepth should never decrease
if currentMaxDepth < previousMaxDepth {
t.Errorf("Block %d: MaxDepth decreased from %d to %d", blockNum, previousMaxDepth, currentMaxDepth)
}
previousMaxDepth = currentMaxDepth
}
// Verify we saw at least some depth increases
if len(depthIncreases) == 0 {
t.Error("Expected to see at least one MaxDepth increase across blocks")
} else {
t.Logf("MaxDepth increased in %d blocks: %v", len(depthIncreases), depthIncreases)
}
// Final verification: with 31 keys, we should have decent depth
if previousMaxDepth < 2 {
t.Logf("Note: Final MaxDepth is %d with 31 keys - hash distribution may need more keys for deeper collisions", previousMaxDepth)
}
t.Logf("Final MaxDepth: %d", previousMaxDepth)
}

View file

@ -325,6 +325,7 @@ func (s *stateObject) updateTrie() (Trie, error) {
deletions []common.Hash
used = make([]common.Hash, 0, len(s.uncommittedStorage))
)
var maxDepth int
for key, origin := range s.uncommittedStorage {
// Skip noop changes, persist actual changes
value, exist := s.pendingStorage[key]
@ -337,10 +338,14 @@ func (s *stateObject) updateTrie() (Trie, error) {
continue
}
if (value != common.Hash{}) {
if err := tr.UpdateStorage(s.address, key[:], common.TrimLeftZeroes(value[:])); err != nil {
depth, err := tr.UpdateStorage(s.address, key[:], common.TrimLeftZeroes(value[:]))
if err != nil {
s.db.setError(err)
return nil, err
}
if depth > maxDepth {
maxDepth = depth
}
s.db.StorageUpdated.Add(1)
} else {
deletions = append(deletions, key)
@ -358,6 +363,9 @@ func (s *stateObject) updateTrie() (Trie, error) {
if s.db.prefetcher != nil {
s.db.prefetcher.used(s.addrHash, s.data.Root, nil, used)
}
if uint64(maxDepth) > s.data.MaxDepth {
s.data.MaxDepth = s.data.MaxDepth + 1
}
s.uncommittedStorage = make(Storage) // empties the commit markers
return tr, nil
}
@ -585,3 +593,7 @@ func (s *stateObject) Nonce() uint64 {
func (s *stateObject) Root() common.Hash {
return s.data.Root
}
func (s *stateObject) MaxDepth() uint64 {
return s.data.MaxDepth
}

View file

@ -327,6 +327,15 @@ func (s *StateDB) GetNonce(addr common.Address) uint64 {
return 0
}
// GetMaxDepth retrieves the max depth from the given address or 0 if object not found
func (s *StateDB) GetMaxDepth(addr common.Address) uint64 {
stateObject := s.getStateObject(addr)
if stateObject != nil {
return stateObject.MaxDepth()
}
return 0
}
// GetStorageRoot retrieves the storage root from the given address or empty
// if object not found.
func (s *StateDB) GetStorageRoot(addr common.Address) common.Hash {

View file

@ -61,6 +61,10 @@ func (s *hookedStateDB) GetNonce(addr common.Address) uint64 {
return s.inner.GetNonce(addr)
}
func (s *hookedStateDB) GetMaxDepth(addr common.Address) uint64 {
return s.inner.GetMaxDepth(addr)
}
func (s *hookedStateDB) GetCodeHash(addr common.Address) common.Hash {
return s.inner.GetCodeHash(addr)
}

View file

@ -16,6 +16,10 @@ func (obj *StateAccount) EncodeRLP(_w io.Writer) error {
}
w.WriteBytes(obj.Root[:])
w.WriteBytes(obj.CodeHash)
_tmp1 := obj.MaxDepth != 0
if _tmp1 {
w.WriteUint64(obj.MaxDepth)
}
w.ListEnd(_tmp0)
return w.Flush()
}

View file

@ -33,6 +33,7 @@ type StateAccount struct {
Balance *uint256.Int
Root common.Hash // merkle root of the storage trie
CodeHash []byte
MaxDepth uint64 `rlp:"optional"`
}
// NewEmptyStateAccount constructs an empty state account.
@ -55,6 +56,7 @@ func (acct *StateAccount) Copy() *StateAccount {
Balance: balance,
Root: acct.Root,
CodeHash: common.CopyBytes(acct.CodeHash),
MaxDepth: acct.MaxDepth,
}
}
@ -66,6 +68,7 @@ type SlimAccount struct {
Balance *uint256.Int
Root []byte // Nil if root equals to types.EmptyRootHash
CodeHash []byte // Nil if hash equals to types.EmptyCodeHash
MaxDepth uint64 `rlp:"optional"`
}
// SlimAccountRLP encodes the state account in 'slim RLP' format.
@ -80,6 +83,7 @@ func SlimAccountRLP(account StateAccount) []byte {
if !bytes.Equal(account.CodeHash, EmptyCodeHash[:]) {
slim.CodeHash = account.CodeHash
}
slim.MaxDepth = account.MaxDepth
data, err := rlp.EncodeToBytes(slim)
if err != nil {
panic(err)
@ -108,6 +112,7 @@ func FullAccount(data []byte) (*StateAccount, error) {
} else {
account.CodeHash = slim.CodeHash
}
account.MaxDepth = slim.MaxDepth
return &account, nil
}

View file

@ -39,6 +39,8 @@ type StateDB interface {
GetNonce(common.Address) uint64
SetNonce(common.Address, uint64, tracing.NonceChangeReason)
GetMaxDepth(common.Address) uint64
GetCodeHash(common.Address) common.Hash
GetCode(common.Address) []byte

View file

@ -42,6 +42,11 @@ func makeGasSStoreFunc(clearingRefund uint64) gasFunc {
// Check slot presence in the access list
if _, slotPresent := evm.StateDB.SlotInAccessList(contract.Address(), slot); !slotPresent {
cost = params.ColdSloadCostEIP2929
// Add MaxDepth cost for cold writes if depth exceeds threshold
maxDepth := evm.StateDB.GetMaxDepth(contract.Address())
if maxDepth >= params.MaxDepthGasThreshold {
cost += params.MaxDepthGasFactor * maxDepth
}
// If the caller cannot afford the cost, this change will be rolled back
evm.StateDB.AddSlotToAccessList(contract.Address(), slot)
}

View file

@ -69,6 +69,10 @@ const (
ColdSloadCostEIP2929 = uint64(2100) // COLD_SLOAD_COST
WarmStorageReadCostEIP2929 = uint64(100) // WARM_STORAGE_READ_COST
// MaxDepthGasFactor is the gas cost per unit of MaxDepth for cold SSTORE operations
MaxDepthGasFactor = uint64(200) // Gas cost multiplier for MaxDepth in cold SSTORE
MaxDepthGasThreshold = uint64(9) // Threshold depth - no extra cost charged until depth reaches this value
// In EIP-2200: SstoreResetGas was 5000.
// In EIP-2929: SstoreResetGas was changed to '5000 - COLD_SLOAD_COST'.
// In EIP-3529: SSTORE_CLEARS_SCHEDULE is defined as SSTORE_RESET_GAS + ACCESS_LIST_STORAGE_KEY_COST

View file

@ -47,7 +47,8 @@ func (h *ListHasher) Reset() {
// Update inserts a key-value pair into the trie.
func (h *ListHasher) Update(key []byte, value []byte) error {
key, value = bytes.Clone(key), bytes.Clone(value)
return h.tr.Update(key, value)
_, err := h.tr.Update(key, value)
return err
}
// Hash computes the root hash of all inserted key-value pairs.

View file

@ -197,17 +197,18 @@ func (t *StateTrie) MustUpdate(key, value []byte) {
// stored in the trie.
//
// If a node is not found in the database, a MissingNodeError is returned.
func (t *StateTrie) UpdateStorage(_ common.Address, key, value []byte) error {
// Returns the depth at which the value was inserted in the trie.
func (t *StateTrie) UpdateStorage(_ common.Address, key, value []byte) (int, error) {
hk := crypto.Keccak256(key)
v, _ := rlp.EncodeToBytes(value)
err := t.trie.Update(hk, v)
depth, err := t.trie.Update(hk, v)
if err != nil {
return err
return 0, err
}
if t.preimages != nil {
t.secKeyCache[common.Hash(hk)] = common.CopyBytes(key)
}
return nil
return depth, nil
}
// UpdateAccount will abstract the write of an account to the secure trie.
@ -217,7 +218,7 @@ func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccoun
if err != nil {
return err
}
if err := t.trie.Update(hk, data); err != nil {
if _, err := t.trie.Update(hk, data); err != nil {
return err
}
if t.preimages != nil {

View file

@ -119,7 +119,8 @@ func (t *TransitionTrie) PrefetchAccount(addresses []common.Address) error {
// UpdateStorage associates key with value in the trie. If value has length zero, any
// existing value is deleted from the trie. The value bytes must not be modified
// by the caller while they are stored in the trie.
func (t *TransitionTrie) UpdateStorage(address common.Address, key []byte, value []byte) error {
// Returns the depth at which the value was inserted in the trie.
func (t *TransitionTrie) UpdateStorage(address common.Address, key []byte, value []byte) (int, error) {
var v []byte
if len(value) >= 32 {
v = value[:32]

View file

@ -360,7 +360,7 @@ func (t *Trie) getNode(origNode node, path []byte, pos int) (item []byte, newnod
// MustUpdate is a wrapper of Update and will omit any encountered error but
// just print out an error message.
func (t *Trie) MustUpdate(key, value []byte) {
if err := t.Update(key, value); err != nil {
if _, err := t.Update(key, value); err != nil {
log.Error("Unhandled trie error in Trie.Update", "err", err)
}
}
@ -374,40 +374,43 @@ func (t *Trie) MustUpdate(key, value []byte) {
//
// If the requested node is not present in trie, no error will be returned.
// If the trie is corrupted, a MissingNodeError is returned.
func (t *Trie) Update(key, value []byte) error {
// Returns the depth at which the value was inserted.
func (t *Trie) Update(key, value []byte) (int, error) {
// Short circuit if the trie is already committed and not usable.
if t.committed {
return ErrCommitted
return 0, ErrCommitted
}
return t.update(key, value)
}
func (t *Trie) update(key, value []byte) error {
func (t *Trie) update(key, value []byte) (int, error) {
t.unhashed++
t.uncommitted++
k := keybytesToHex(key)
var depth int
if len(value) != 0 {
_, n, err := t.insert(t.root, nil, k, valueNode(value))
_, n, d, err := t.insert(t.root, nil, k, valueNode(value), 0)
if err != nil {
return err
return 0, err
}
t.root = n
depth = d
} else {
_, n, err := t.delete(t.root, nil, k)
if err != nil {
return err
return 0, err
}
t.root = n
}
return nil
return depth, nil
}
func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error) {
func (t *Trie) insert(n node, prefix, key []byte, value node, depth int) (bool, node, int, error) {
if len(key) == 0 {
if v, ok := n.(valueNode); ok {
return !bytes.Equal(v, value.(valueNode)), value, nil
return !bytes.Equal(v, value.(valueNode)), value, depth, nil
}
return true, value, nil
return true, value, depth, nil
}
switch n := n.(type) {
case *shortNode:
@ -415,26 +418,26 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error
// If the whole key matches, keep this short node as is
// and only update the value.
if matchlen == len(n.Key) {
dirty, nn, err := t.insert(n.Val, append(prefix, key[:matchlen]...), key[matchlen:], value)
dirty, nn, d, err := t.insert(n.Val, append(prefix, key[:matchlen]...), key[matchlen:], value, depth+1)
if !dirty || err != nil {
return false, n, err
return false, n, depth, err
}
return true, &shortNode{n.Key, nn, t.newFlag()}, nil
return true, &shortNode{n.Key, nn, t.newFlag()}, d, nil
}
// Otherwise branch out at the index where they differ.
branch := &fullNode{flags: t.newFlag()}
var err error
_, branch.Children[n.Key[matchlen]], err = t.insert(nil, append(prefix, n.Key[:matchlen+1]...), n.Key[matchlen+1:], n.Val)
_, branch.Children[n.Key[matchlen]], _, err = t.insert(nil, append(prefix, n.Key[:matchlen+1]...), n.Key[matchlen+1:], n.Val, depth+1)
if err != nil {
return false, nil, err
return false, nil, depth, err
}
_, branch.Children[key[matchlen]], err = t.insert(nil, append(prefix, key[:matchlen+1]...), key[matchlen+1:], value)
_, branch.Children[key[matchlen]], _, err = t.insert(nil, append(prefix, key[:matchlen+1]...), key[matchlen+1:], value, depth+1)
if err != nil {
return false, nil, err
return false, nil, depth, err
}
// Replace this shortNode with the branch if it occurs at index 0.
if matchlen == 0 {
return true, branch, nil
return true, branch, depth+1, nil
}
// New branch node is created as a child of the original short node.
// Track the newly inserted node in the tracer. The node identifier
@ -442,16 +445,16 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error
t.opTracer.onInsert(append(prefix, key[:matchlen]...))
// Replace it with a short node leading up to the branch.
return true, &shortNode{key[:matchlen], branch, t.newFlag()}, nil
return true, &shortNode{key[:matchlen], branch, t.newFlag()}, depth+1, nil
case *fullNode:
dirty, nn, err := t.insert(n.Children[key[0]], append(prefix, key[0]), key[1:], value)
dirty, nn, d, err := t.insert(n.Children[key[0]], append(prefix, key[0]), key[1:], value, depth+1)
if !dirty || err != nil {
return false, n, err
return false, n, depth, err
}
n.flags = t.newFlag()
n.Children[key[0]] = nn
return true, n, nil
return true, n, d, nil
case nil:
// New short node is created and track it in the tracer. The node identifier
@ -459,7 +462,7 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error
// since it's always embedded in its parent.
t.opTracer.onInsert(prefix)
return true, &shortNode{key, value, t.newFlag()}, nil
return true, &shortNode{key, value, t.newFlag()}, depth, nil
case hashNode:
// We've hit a part of the trie that isn't loaded yet. Load
@ -467,13 +470,13 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error
// the path to the value in the trie.
rn, err := t.resolveAndTrack(n, prefix)
if err != nil {
return false, nil, err
return false, nil, depth, err
}
dirty, nn, err := t.insert(rn, prefix, key, value)
dirty, nn, d, err := t.insert(rn, prefix, key, value, depth)
if !dirty || err != nil {
return false, rn, err
return false, rn, depth, err
}
return true, nn, nil
return true, nn, d, nil
default:
panic(fmt.Sprintf("%T: invalid node: %v", n, n))

View file

@ -120,7 +120,7 @@ func testMissingNode(t *testing.T, memonly bool, scheme string) {
t.Errorf("Unexpected error: %v", err)
}
trie, _ = New(TrieID(root), triedb)
err = trie.Update([]byte("120099"), []byte("zxcvzxcvzxcvzxcvzxcvzxcvzxcvzxcv"))
_, err = trie.Update([]byte("120099"), []byte("zxcvzxcvzxcvzxcvzxcvzxcvzxcvzxcv"))
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
@ -159,7 +159,7 @@ func testMissingNode(t *testing.T, memonly bool, scheme string) {
if err != nil {
t.Errorf("Unexpected error: %v", err)
}
err = trie.Update([]byte("120099"), []byte("zxcv"))
_, err = trie.Update([]byte("120099"), []byte("zxcv"))
if _, ok := err.(*MissingNodeError); !ok {
t.Errorf("Wrong error: %v", err)
}

View file

@ -179,7 +179,8 @@ func (t *VerkleTrie) UpdateAccount(addr common.Address, acc *types.StateAccount,
// UpdateStorage implements state.Trie, writing the provided storage slot into
// the tree. If the tree is corrupted, an error will be returned.
func (t *VerkleTrie) UpdateStorage(address common.Address, key, value []byte) error {
// Returns the depth at which the value was inserted in the trie (always 0 for verkle trees).
func (t *VerkleTrie) UpdateStorage(address common.Address, key, value []byte) (int, error) {
// Left padding the slot value to 32 bytes.
var v [32]byte
if len(value) >= 32 {
@ -188,7 +189,9 @@ func (t *VerkleTrie) UpdateStorage(address common.Address, key, value []byte) er
copy(v[32-len(value):], value[:])
}
k := utils.StorageSlotKeyWithEvaluatedAddress(t.cache.Get(address.Bytes()), key)
return t.root.Insert(k, v[:], t.nodeResolver)
err := t.root.Insert(k, v[:], t.nodeResolver)
// TODO: when eip is schedule, also implement this for verkle
return 0, err
}
// DeleteAccount leaves the account untouched, as no account deletion can happen

View file

@ -65,7 +65,7 @@ func TestVerkleTreeReadWrite(t *testing.T) {
t.Fatalf("Failed to update account, %v", err)
}
for key, val := range storages[addr] {
if err := tr.UpdateStorage(addr, key.Bytes(), val); err != nil {
if _, err := tr.UpdateStorage(addr, key.Bytes(), val); err != nil {
t.Fatalf("Failed to update storage, %v", err)
}
}
@ -106,7 +106,7 @@ func TestVerkleRollBack(t *testing.T) {
t.Fatalf("Failed to update account, %v", err)
}
for key, val := range storages[addr] {
if err := tr.UpdateStorage(addr, key.Bytes(), val); err != nil {
if _, err := tr.UpdateStorage(addr, key.Bytes(), val); err != nil {
t.Fatalf("Failed to update storage, %v", err)
}
}

View file

@ -123,7 +123,7 @@ func updateAccount(ctx *context, db database.NodeDatabase, addr common.Address)
if len(val) == 0 {
deletes = append(deletes, tkey)
} else {
err := st.Update(tkey.Bytes(), val)
_, err := st.Update(tkey.Bytes(), val)
if err != nil {
return err
}
@ -151,7 +151,8 @@ func updateAccount(ctx *context, db database.NodeDatabase, addr common.Address)
if err != nil {
return err
}
return ctx.accountTrie.Update(addrHash.Bytes(), full)
_, err = ctx.accountTrie.Update(addrHash.Bytes(), full)
return err
}
// deleteAccount the account was not present in prev-state, and is expected