Skip to content

Commit 3ff6b3c

Browse files
core/state: implement fast storage deletion (#27955)
This changes implements faster post-selfdestruct iteration of storage slots for deletion, by using snapshot-storage+stacktrie to recover the trienodes to be deleted. This mechanism is only implemented for path-based schema. For hash-based schema, the entire post-selfdestruct storage iteration is skipped, with this change, since hash-based does not actually perform deletion anyway. --------- Co-authored-by: Martin Holst Swende <[email protected]>
1 parent 5ca7fb8 commit 3ff6b3c

File tree

3 files changed

+182
-29
lines changed

3 files changed

+182
-29
lines changed

core/state/statedb.go

Lines changed: 110 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,12 @@ import (
3636
"github.com/ethereum/go-ethereum/trie/triestate"
3737
)
3838

39+
const (
40+
// storageDeleteLimit denotes the highest permissible memory allocation
41+
// employed for contract storage deletion.
42+
storageDeleteLimit = 512 * 1024 * 1024
43+
)
44+
3945
type revision struct {
4046
id int
4147
journalIndex int
@@ -983,59 +989,130 @@ func (s *StateDB) clearJournalAndRefund() {
983989
s.validRevisions = s.validRevisions[:0] // Snapshots can be created without journal entries
984990
}
985991

986-
// deleteStorage iterates the storage trie belongs to the account and mark all
987-
// slots inside as deleted.
988-
func (s *StateDB) deleteStorage(addr common.Address, addrHash common.Hash, root common.Hash) (bool, map[common.Hash][]byte, *trienode.NodeSet, error) {
989-
start := time.Now()
992+
// fastDeleteStorage is the function that efficiently deletes the storage trie
993+
// of a specific account. It leverages the associated state snapshot for fast
994+
// storage iteration and constructs trie node deletion markers by creating
995+
// stack trie with iterated slots.
996+
func (s *StateDB) fastDeleteStorage(addrHash common.Hash, root common.Hash) (bool, common.StorageSize, map[common.Hash][]byte, *trienode.NodeSet, error) {
997+
iter, err := s.snaps.StorageIterator(s.originalRoot, addrHash, common.Hash{})
998+
if err != nil {
999+
return false, 0, nil, nil, err
1000+
}
1001+
defer iter.Release()
1002+
1003+
var (
1004+
size common.StorageSize
1005+
nodes = trienode.NewNodeSet(addrHash)
1006+
slots = make(map[common.Hash][]byte)
1007+
)
1008+
stack := trie.NewStackTrie(func(owner common.Hash, path []byte, hash common.Hash, blob []byte) {
1009+
nodes.AddNode(path, trienode.NewDeleted())
1010+
size += common.StorageSize(len(path))
1011+
})
1012+
for iter.Next() {
1013+
if size > storageDeleteLimit {
1014+
return true, size, nil, nil, nil
1015+
}
1016+
slot := common.CopyBytes(iter.Slot())
1017+
if iter.Error() != nil { // error might occur after Slot function
1018+
return false, 0, nil, nil, err
1019+
}
1020+
size += common.StorageSize(common.HashLength + len(slot))
1021+
slots[iter.Hash()] = slot
1022+
1023+
if err := stack.Update(iter.Hash().Bytes(), slot); err != nil {
1024+
return false, 0, nil, nil, err
1025+
}
1026+
}
1027+
if iter.Error() != nil { // error might occur during iteration
1028+
return false, 0, nil, nil, err
1029+
}
1030+
if stack.Hash() != root {
1031+
return false, 0, nil, nil, fmt.Errorf("snapshot is not matched, exp %x, got %x", root, stack.Hash())
1032+
}
1033+
return false, size, slots, nodes, nil
1034+
}
1035+
1036+
// slowDeleteStorage serves as a less-efficient alternative to "fastDeleteStorage,"
1037+
// employed when the associated state snapshot is not available. It iterates the
1038+
// storage slots along with all internal trie nodes via trie directly.
1039+
func (s *StateDB) slowDeleteStorage(addr common.Address, addrHash common.Hash, root common.Hash) (bool, common.StorageSize, map[common.Hash][]byte, *trienode.NodeSet, error) {
9901040
tr, err := s.db.OpenStorageTrie(s.originalRoot, addr, root)
9911041
if err != nil {
992-
return false, nil, nil, fmt.Errorf("failed to open storage trie, err: %w", err)
1042+
return false, 0, nil, nil, fmt.Errorf("failed to open storage trie, err: %w", err)
9931043
}
9941044
it, err := tr.NodeIterator(nil)
9951045
if err != nil {
996-
return false, nil, nil, fmt.Errorf("failed to open storage iterator, err: %w", err)
1046+
return false, 0, nil, nil, fmt.Errorf("failed to open storage iterator, err: %w", err)
9971047
}
9981048
var (
999-
set = trienode.NewNodeSet(addrHash)
1000-
slots = make(map[common.Hash][]byte)
1001-
stateSize common.StorageSize
1002-
nodeSize common.StorageSize
1049+
size common.StorageSize
1050+
nodes = trienode.NewNodeSet(addrHash)
1051+
slots = make(map[common.Hash][]byte)
10031052
)
10041053
for it.Next(true) {
1005-
// arbitrary stateSize limit, make it configurable
1006-
if stateSize+nodeSize > 512*1024*1024 {
1007-
log.Info("Skip large storage deletion", "address", addr.Hex(), "states", stateSize, "nodes", nodeSize)
1008-
if metrics.EnabledExpensive {
1009-
slotDeletionSkip.Inc(1)
1010-
}
1011-
return true, nil, nil, nil
1054+
if size > storageDeleteLimit {
1055+
return true, size, nil, nil, nil
10121056
}
10131057
if it.Leaf() {
10141058
slots[common.BytesToHash(it.LeafKey())] = common.CopyBytes(it.LeafBlob())
1015-
stateSize += common.StorageSize(common.HashLength + len(it.LeafBlob()))
1059+
size += common.StorageSize(common.HashLength + len(it.LeafBlob()))
10161060
continue
10171061
}
10181062
if it.Hash() == (common.Hash{}) {
10191063
continue
10201064
}
1021-
nodeSize += common.StorageSize(len(it.Path()))
1022-
set.AddNode(it.Path(), trienode.NewDeleted())
1065+
size += common.StorageSize(len(it.Path()))
1066+
nodes.AddNode(it.Path(), trienode.NewDeleted())
10231067
}
10241068
if err := it.Error(); err != nil {
1069+
return false, 0, nil, nil, err
1070+
}
1071+
return false, size, slots, nodes, nil
1072+
}
1073+
1074+
// deleteStorage is designed to delete the storage trie of a designated account.
1075+
// It could potentially be terminated if the storage size is excessively large,
1076+
// potentially leading to an out-of-memory panic. The function will make an attempt
1077+
// to utilize an efficient strategy if the associated state snapshot is reachable;
1078+
// otherwise, it will resort to a less-efficient approach.
1079+
func (s *StateDB) deleteStorage(addr common.Address, addrHash common.Hash, root common.Hash) (bool, map[common.Hash][]byte, *trienode.NodeSet, error) {
1080+
var (
1081+
start = time.Now()
1082+
err error
1083+
aborted bool
1084+
size common.StorageSize
1085+
slots map[common.Hash][]byte
1086+
nodes *trienode.NodeSet
1087+
)
1088+
// The fast approach can be failed if the snapshot is not fully
1089+
// generated, or it's internally corrupted. Fallback to the slow
1090+
// one just in case.
1091+
if s.snap != nil {
1092+
aborted, size, slots, nodes, err = s.fastDeleteStorage(addrHash, root)
1093+
}
1094+
if s.snap == nil || err != nil {
1095+
aborted, size, slots, nodes, err = s.slowDeleteStorage(addr, addrHash, root)
1096+
}
1097+
if err != nil {
10251098
return false, nil, nil, err
10261099
}
10271100
if metrics.EnabledExpensive {
1028-
if int64(len(slots)) > slotDeletionMaxCount.Value() {
1029-
slotDeletionMaxCount.Update(int64(len(slots)))
1101+
if aborted {
1102+
slotDeletionSkip.Inc(1)
10301103
}
1031-
if int64(stateSize+nodeSize) > slotDeletionMaxSize.Value() {
1032-
slotDeletionMaxSize.Update(int64(stateSize + nodeSize))
1104+
n := int64(len(slots))
1105+
if n > slotDeletionMaxCount.Value() {
1106+
slotDeletionMaxCount.Update(n)
1107+
}
1108+
if int64(size) > slotDeletionMaxSize.Value() {
1109+
slotDeletionMaxSize.Update(int64(size))
10331110
}
10341111
slotDeletionTimer.UpdateSince(start)
1035-
slotDeletionCount.Mark(int64(len(slots)))
1036-
slotDeletionSize.Mark(int64(stateSize + nodeSize))
1112+
slotDeletionCount.Mark(n)
1113+
slotDeletionSize.Mark(int64(size))
10371114
}
1038-
return false, slots, set, nil
1115+
return aborted, slots, nodes, nil
10391116
}
10401117

10411118
// handleDestruction processes all destruction markers and deletes the account
@@ -1063,7 +1140,13 @@ func (s *StateDB) deleteStorage(addr common.Address, addrHash common.Hash, root
10631140
// In case (d), **original** account along with its storages should be deleted,
10641141
// with their values be tracked as original value.
10651142
func (s *StateDB) handleDestruction(nodes *trienode.MergedNodeSet) (map[common.Address]struct{}, error) {
1143+
// Short circuit if geth is running with hash mode. This procedure can consume
1144+
// considerable time and storage deletion isn't supported in hash mode, thus
1145+
// preemptively avoiding unnecessary expenses.
10661146
incomplete := make(map[common.Address]struct{})
1147+
if s.db.TrieDB().Scheme() == rawdb.HashScheme {
1148+
return incomplete, nil
1149+
}
10671150
for addr, prev := range s.stateObjectsDestruct {
10681151
// The original account was non-existing, and it's marked as destructed
10691152
// in the scope of block. It can be case (a) or (b).

core/state/statedb_fuzz_test.go

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -31,10 +31,12 @@ import (
3131

3232
"github.com/ethereum/go-ethereum/common"
3333
"github.com/ethereum/go-ethereum/core/rawdb"
34+
"github.com/ethereum/go-ethereum/core/state/snapshot"
3435
"github.com/ethereum/go-ethereum/core/types"
3536
"github.com/ethereum/go-ethereum/crypto"
3637
"github.com/ethereum/go-ethereum/rlp"
3738
"github.com/ethereum/go-ethereum/trie"
39+
"github.com/ethereum/go-ethereum/trie/triedb/pathdb"
3840
"github.com/ethereum/go-ethereum/trie/triestate"
3941
)
4042

@@ -179,16 +181,28 @@ func (test *stateTest) run() bool {
179181
storageList = append(storageList, copy2DSet(states.Storages))
180182
}
181183
disk = rawdb.NewMemoryDatabase()
182-
tdb = trie.NewDatabase(disk, &trie.Config{OnCommit: onCommit})
184+
tdb = trie.NewDatabase(disk, &trie.Config{OnCommit: onCommit, PathDB: pathdb.Defaults})
183185
sdb = NewDatabaseWithNodeDB(disk, tdb)
184186
byzantium = rand.Intn(2) == 0
185187
)
188+
defer disk.Close()
189+
defer tdb.Close()
190+
191+
var snaps *snapshot.Tree
192+
if rand.Intn(3) == 0 {
193+
snaps, _ = snapshot.New(snapshot.Config{
194+
CacheSize: 1,
195+
Recovery: false,
196+
NoBuild: false,
197+
AsyncBuild: false,
198+
}, disk, tdb, types.EmptyRootHash)
199+
}
186200
for i, actions := range test.actions {
187201
root := types.EmptyRootHash
188202
if i != 0 {
189203
root = roots[len(roots)-1]
190204
}
191-
state, err := New(root, sdb, nil)
205+
state, err := New(root, sdb, snaps)
192206
if err != nil {
193207
panic(err)
194208
}

core/state/statedb_test.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,8 @@ import (
3939
"github.com/ethereum/go-ethereum/trie"
4040
"github.com/ethereum/go-ethereum/trie/triedb/hashdb"
4141
"github.com/ethereum/go-ethereum/trie/triedb/pathdb"
42+
"github.com/ethereum/go-ethereum/trie/trienode"
43+
"github.com/holiman/uint256"
4244
)
4345

4446
// Tests that updating a state trie does not leak any database writes prior to
@@ -1135,3 +1137,57 @@ func TestResetObject(t *testing.T) {
11351137
t.Fatalf("Unexpected storage slot value %v", slot)
11361138
}
11371139
}
1140+
1141+
func TestDeleteStorage(t *testing.T) {
1142+
var (
1143+
disk = rawdb.NewMemoryDatabase()
1144+
tdb = trie.NewDatabase(disk, nil)
1145+
db = NewDatabaseWithNodeDB(disk, tdb)
1146+
snaps, _ = snapshot.New(snapshot.Config{CacheSize: 10}, disk, tdb, types.EmptyRootHash)
1147+
state, _ = New(types.EmptyRootHash, db, snaps)
1148+
addr = common.HexToAddress("0x1")
1149+
)
1150+
// Initialize account and populate storage
1151+
state.SetBalance(addr, big.NewInt(1))
1152+
state.CreateAccount(addr)
1153+
for i := 0; i < 1000; i++ {
1154+
slot := common.Hash(uint256.NewInt(uint64(i)).Bytes32())
1155+
value := common.Hash(uint256.NewInt(uint64(10 * i)).Bytes32())
1156+
state.SetState(addr, slot, value)
1157+
}
1158+
root, _ := state.Commit(0, true)
1159+
// Init phase done, create two states, one with snap and one without
1160+
fastState, _ := New(root, db, snaps)
1161+
slowState, _ := New(root, db, nil)
1162+
1163+
obj := fastState.GetOrNewStateObject(addr)
1164+
storageRoot := obj.data.Root
1165+
1166+
_, _, fastNodes, err := fastState.deleteStorage(addr, crypto.Keccak256Hash(addr[:]), storageRoot)
1167+
if err != nil {
1168+
t.Fatal(err)
1169+
}
1170+
1171+
_, _, slowNodes, err := slowState.deleteStorage(addr, crypto.Keccak256Hash(addr[:]), storageRoot)
1172+
if err != nil {
1173+
t.Fatal(err)
1174+
}
1175+
check := func(set *trienode.NodeSet) string {
1176+
var a []string
1177+
set.ForEachWithOrder(func(path string, n *trienode.Node) {
1178+
if n.Hash != (common.Hash{}) {
1179+
t.Fatal("delete should have empty hashes")
1180+
}
1181+
if len(n.Blob) != 0 {
1182+
t.Fatal("delete should have have empty blobs")
1183+
}
1184+
a = append(a, fmt.Sprintf("%x", path))
1185+
})
1186+
return strings.Join(a, ",")
1187+
}
1188+
slowRes := check(slowNodes)
1189+
fastRes := check(fastNodes)
1190+
if slowRes != fastRes {
1191+
t.Fatalf("difference found:\nfast: %v\nslow: %v\n", fastRes, slowRes)
1192+
}
1193+
}

0 commit comments

Comments
 (0)