Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

core/state: implement fast storage deletion #27955

Merged
merged 7 commits into from
Aug 26, 2023

Conversation

rjl493456442
Copy link
Member

@rjl493456442 rjl493456442 commented Aug 21, 2023

Deletion of 328239 slots 22.47 MiB statesize in time 812.148619ms
800ms vs 3.6 s

@holiman
Copy link
Contributor

holiman commented Aug 21, 2023

Looking at the old code now.. Ok, so for hashdb, it first iterates the storage trie, collects it into the set var: aborted, slots, set, err := s.deleteStorage(addr, addrHash, prev.Root),

The set are merged into nodes

		if err := nodes.Merge(set); err != nil {
			return nil, err
		}

Other changes are merged into the same nodes, and eventually, we update the triedb:

		if err := s.db.TrieDB().Update(root, origin, block, nodes, triestate.New(s.accountsOrigin, s.storagesOrigin, incomplete)); err != nil {
			return common.Hash{}, err
		}

And, eventually, it reaches Update, and does

	for _, owner := range order {
		subset := nodes.Sets[owner]
		subset.ForEachWithOrder(func(path string, n *trienode.Node) {
			if n.IsDeleted() {
				return // ignore deletion
			}

So it just ignores all deletions in the end. So all the work we did there was just discarded in the end? Or was there some period where it was used in a memory-capacity, and we just discard the actual disk-write?

If indeed we just iterate/collect the storage nodes just to ignore them in hash-mode, it seems that we can optimize that particular pipeline earlier, and ignore the iteration too?

@holiman
Copy link
Contributor

holiman commented Aug 21, 2023

Some more thoughts. Wouldn't this be a non-changing refactoring?

The original code collects the keys first, but it doesn't actually do anything with it, like sort it, just iterates it again. It looks like the only reason for the first collection is to filter out the empty-element, but that can be easily done in the second loop. Am I missing something?

diff --git a/trie/triedb/hashdb/database.go b/trie/triedb/hashdb/database.go
index b3ae54dbe3..e00b082a65 100644
--- a/trie/triedb/hashdb/database.go
+++ b/trie/triedb/hashdb/database.go
@@ -587,18 +587,10 @@ func (db *Database) Update(root common.Hash, parent common.Hash, block uint64, n
 	//
 	// Note, the storage tries must be flushed before the account trie to
 	// retain the invariant that children go into the dirty cache first.
-	var order []common.Hash
-	for owner := range nodes.Sets {
+	for owner, subset := range nodes.Sets {
 		if owner == (common.Hash{}) {
 			continue
 		}
-		order = append(order, owner)
-	}
-	if _, ok := nodes.Sets[common.Hash{}]; ok {
-		order = append(order, common.Hash{})
-	}
-	for _, owner := range order {
-		subset := nodes.Sets[owner]
 		subset.ForEachWithOrder(func(path string, n *trienode.Node) {
 			if n.IsDeleted() {
 				return // ignore deletion
@@ -609,6 +601,12 @@ func (db *Database) Update(root common.Hash, parent common.Hash, block uint64, n
 	// Link up the account trie and storage trie if the node points
 	// to an account trie leaf.
 	if set, present := nodes.Sets[common.Hash{}]; present {
+		set.ForEachWithOrder(func(path string, n *trienode.Node) {
+			if n.IsDeleted() {
+				return // ignore deletion
+			}
+			db.insert(n.Hash, n.Blob)
+		})
 		for _, n := range set.Leaves {
 			var account types.StateAccount
 			if err := rlp.DecodeBytes(n.Blob, &account); err != nil {

@rjl493456442
Copy link
Member Author

So it just ignores all deletions in the end. So all the work we did there was just discarded in the end? Or was there some period where it was used in a memory-capacity, and we just discard the actual disk-write?

Yes, in hash mode, deletion is not supported, and it's totally useless in hash mode, just try to align with path mode.

and ignore the iteration too?

I added this logic in this PR.

@holiman
Copy link
Contributor

holiman commented Aug 21, 2023

I added this logic in this PR.

Right, gotcha! I'm a couple of steps behind here :)

@rjl493456442
Copy link
Member Author

It looks like the only reason for the first collection is to filter out the empty-element, but that can be easily done in the second loop. Am I missing something?

Yep, basically we need to flush storage trie(s) first, and then account trie. I think logically the refactor is OK, but i would prefer to not change it, because last time I changed the logic here and result in a big bug in this part :)

@rjl493456442
Copy link
Member Author

Running a full sync on sepolia to ensure nothing is broken.

@rjl493456442 rjl493456442 marked this pull request as ready for review August 22, 2023 07:03
Comment on lines +1063 to +1065
if stack.Hash() != root {
return false, 0, nil, nil, fmt.Errorf("snapshot is not matched, exp %x, got %x", root, stack.Hash())
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
if stack.Hash() != root {
return false, 0, nil, nil, fmt.Errorf("snapshot is not matched, exp %x, got %x", root, stack.Hash())
}
if it.Err() != nil {
return false, 0, nil, nil, it.Err()
}
if stack.Hash() != root {
return false, 0, nil, nil, fmt.Errorf("snapshot is not matched, exp %x, got %x", root, stack.Hash())
}

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In theory, both it.Next() and it.Hash() can internally error, so we should check it.Err() after each invocation. If we fail to do so, and ignore an error from it.Hash(), and subsequently call it.Next(), then we will be rewarded with panic(fmt.Sprintf("called Next of failed iterator: %v", it.fail))

But I guess it should be enough to have one check after the loop, like my comment, and another check right after slots[iter.Hash()] = slot ?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, we need to check the failure after each iteration, however, i guess it's unnecessary. The cached error won't be clean anyway.

And the reason I don't add the iterator error checking here is: if we encounter any failure in iterator, the stackTrie will produce a different root hash anyway.

But yeah, I will add one check right after the loop. It's cheap anyway and would be helpful to bubble up the "real issue".

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

And the reason I don't add the iterator error checking here is: if we encounter any failure in iterator, the stackTrie will produce a different root hash anyway.

Ah, no, as I pointed out, we won't get a different root, we will encounter a panic crash.

Copy link
Contributor

@holiman holiman left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a test aswell -- maybe it was superfluous, I couldn't really tell. Anyway, LGTM!

@holiman holiman added this to the 1.13.0 milestone Aug 23, 2023
@holiman holiman merged commit 3ff6b3c into ethereum:master Aug 26, 2023
1 of 2 checks passed
devopsbo3 pushed a commit to HorizenOfficial/go-ethereum that referenced this pull request Nov 10, 2023
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 <martin@swende.se>
devopsbo3 added a commit to HorizenOfficial/go-ethereum that referenced this pull request Nov 10, 2023
devopsbo3 added a commit to HorizenOfficial/go-ethereum that referenced this pull request Nov 10, 2023
maoueh pushed a commit to streamingfast/go-ethereum that referenced this pull request Nov 29, 2023
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 <martin@swende.se>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants