d3411b9f67
This PR fixes an error in trie commit. If the trie.root is nil, it can be two possible scenarios: - The trie was empty, and no change happens - The trie was non-empty and all nodes are dropped For the latter one, we should collect the deletions and apply them into database(e.g. in PBSS).
226 lines
7.0 KiB
Go
226 lines
7.0 KiB
Go
// Copyright 2020 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 trie
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/ethereum/go-ethereum/common"
|
|
)
|
|
|
|
// leaf represents a trie leaf node
|
|
type leaf struct {
|
|
blob []byte // raw blob of leaf
|
|
parent common.Hash // the hash of parent node
|
|
}
|
|
|
|
// committer is the tool used for the trie Commit operation. The committer will
|
|
// capture all dirty nodes during the commit process and keep them cached in
|
|
// insertion order.
|
|
type committer struct {
|
|
nodes *NodeSet
|
|
tracer *tracer
|
|
collectLeaf bool
|
|
}
|
|
|
|
// newCommitter creates a new committer or picks one from the pool.
|
|
func newCommitter(owner common.Hash, tracer *tracer, collectLeaf bool) *committer {
|
|
return &committer{
|
|
nodes: NewNodeSet(owner),
|
|
tracer: tracer,
|
|
collectLeaf: collectLeaf,
|
|
}
|
|
}
|
|
|
|
// Commit collapses a node down into a hash node and returns it along with
|
|
// the modified nodeset.
|
|
func (c *committer) Commit(n node) (hashNode, *NodeSet, error) {
|
|
h, err := c.commit(nil, n)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
// Some nodes can be deleted from trie which can't be captured
|
|
// by committer itself. Iterate all deleted nodes tracked by
|
|
// tracer and marked them as deleted only if they are present
|
|
// in database previously.
|
|
c.tracer.markDeletions(c.nodes)
|
|
return h.(hashNode), c.nodes, nil
|
|
}
|
|
|
|
// commit collapses a node down into a hash node and returns it.
|
|
func (c *committer) commit(path []byte, n node) (node, error) {
|
|
// if this path is clean, use available cached data
|
|
hash, dirty := n.cache()
|
|
if hash != nil && !dirty {
|
|
return hash, nil
|
|
}
|
|
// Commit children, then parent, and remove the dirty flag.
|
|
switch cn := n.(type) {
|
|
case *shortNode:
|
|
// Commit child
|
|
collapsed := cn.copy()
|
|
|
|
// If the child is fullNode, recursively commit,
|
|
// otherwise it can only be hashNode or valueNode.
|
|
if _, ok := cn.Val.(*fullNode); ok {
|
|
childV, err := c.commit(append(path, cn.Key...), cn.Val)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
collapsed.Val = childV
|
|
}
|
|
// The key needs to be copied, since we're adding it to the
|
|
// modified nodeset.
|
|
collapsed.Key = hexToCompact(cn.Key)
|
|
hashedNode := c.store(path, collapsed)
|
|
if hn, ok := hashedNode.(hashNode); ok {
|
|
return hn, nil
|
|
}
|
|
// The short node now is embedded in its parent. Mark the node as
|
|
// deleted if it's present in database previously. It's equivalent
|
|
// as deletion from database's perspective.
|
|
if prev := c.tracer.getPrev(path); len(prev) != 0 {
|
|
c.nodes.markDeleted(path, prev)
|
|
}
|
|
return collapsed, nil
|
|
case *fullNode:
|
|
hashedKids, err := c.commitChildren(path, cn)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
collapsed := cn.copy()
|
|
collapsed.Children = hashedKids
|
|
|
|
hashedNode := c.store(path, collapsed)
|
|
if hn, ok := hashedNode.(hashNode); ok {
|
|
return hn, nil
|
|
}
|
|
// The full node now is embedded in its parent. Mark the node as
|
|
// deleted if it's present in database previously. It's equivalent
|
|
// as deletion from database's perspective.
|
|
if prev := c.tracer.getPrev(path); len(prev) != 0 {
|
|
c.nodes.markDeleted(path, prev)
|
|
}
|
|
return collapsed, nil
|
|
case hashNode:
|
|
return cn, nil
|
|
default:
|
|
// nil, valuenode shouldn't be committed
|
|
panic(fmt.Sprintf("%T: invalid node: %v", n, n))
|
|
}
|
|
}
|
|
|
|
// commitChildren commits the children of the given fullnode
|
|
func (c *committer) commitChildren(path []byte, n *fullNode) ([17]node, error) {
|
|
var children [17]node
|
|
for i := 0; i < 16; i++ {
|
|
child := n.Children[i]
|
|
if child == nil {
|
|
continue
|
|
}
|
|
// If it's the hashed child, save the hash value directly.
|
|
// Note: it's impossible that the child in range [0, 15]
|
|
// is a valueNode.
|
|
if hn, ok := child.(hashNode); ok {
|
|
children[i] = hn
|
|
continue
|
|
}
|
|
// Commit the child recursively and store the "hashed" value.
|
|
// Note the returned node can be some embedded nodes, so it's
|
|
// possible the type is not hashNode.
|
|
hashed, err := c.commit(append(path, byte(i)), child)
|
|
if err != nil {
|
|
return children, err
|
|
}
|
|
children[i] = hashed
|
|
}
|
|
// For the 17th child, it's possible the type is valuenode.
|
|
if n.Children[16] != nil {
|
|
children[16] = n.Children[16]
|
|
}
|
|
return children, nil
|
|
}
|
|
|
|
// store hashes the node n and adds it to the modified nodeset. If leaf collection
|
|
// is enabled, leaf nodes will be tracked in the modified nodeset as well.
|
|
func (c *committer) store(path []byte, n node) node {
|
|
// Larger nodes are replaced by their hash and stored in the database.
|
|
var hash, _ = n.cache()
|
|
|
|
// This was not generated - must be a small node stored in the parent.
|
|
// In theory, we should check if the node is leaf here (embedded node
|
|
// usually is leaf node). But small value (less than 32bytes) is not
|
|
// our target (leaves in account trie only).
|
|
if hash == nil {
|
|
return n
|
|
}
|
|
// We have the hash already, estimate the RLP encoding-size of the node.
|
|
// The size is used for mem tracking, does not need to be exact
|
|
var (
|
|
size = estimateSize(n)
|
|
nhash = common.BytesToHash(hash)
|
|
mnode = &memoryNode{
|
|
hash: nhash,
|
|
node: simplifyNode(n),
|
|
size: uint16(size),
|
|
}
|
|
)
|
|
// Collect the dirty node to nodeset for return.
|
|
c.nodes.markUpdated(path, mnode, c.tracer.getPrev(path))
|
|
|
|
// Collect the corresponding leaf node if it's required. We don't check
|
|
// full node since it's impossible to store value in fullNode. The key
|
|
// length of leaves should be exactly same.
|
|
if c.collectLeaf {
|
|
if sn, ok := n.(*shortNode); ok {
|
|
if val, ok := sn.Val.(valueNode); ok {
|
|
c.nodes.addLeaf(&leaf{blob: val, parent: nhash})
|
|
}
|
|
}
|
|
}
|
|
return hash
|
|
}
|
|
|
|
// estimateSize estimates the size of an rlp-encoded node, without actually
|
|
// rlp-encoding it (zero allocs). This method has been experimentally tried, and with a trie
|
|
// with 1000 leaves, the only errors above 1% are on small shortnodes, where this
|
|
// method overestimates by 2 or 3 bytes (e.g. 37 instead of 35)
|
|
func estimateSize(n node) int {
|
|
switch n := n.(type) {
|
|
case *shortNode:
|
|
// A short node contains a compacted key, and a value.
|
|
return 3 + len(n.Key) + estimateSize(n.Val)
|
|
case *fullNode:
|
|
// A full node contains up to 16 hashes (some nils), and a key
|
|
s := 3
|
|
for i := 0; i < 16; i++ {
|
|
if child := n.Children[i]; child != nil {
|
|
s += estimateSize(child)
|
|
} else {
|
|
s++
|
|
}
|
|
}
|
|
return s
|
|
case valueNode:
|
|
return 1 + len(n)
|
|
case hashNode:
|
|
return 1 + len(n)
|
|
default:
|
|
panic(fmt.Sprintf("node type %T", n))
|
|
}
|
|
}
|