diff --git a/trie/pure_committer.go b/trie/pure_committer.go new file mode 100644 index 0000000000..fdf0760cdb --- /dev/null +++ b/trie/pure_committer.go @@ -0,0 +1,374 @@ +// Copyright 2019 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 . + +package trie + +import ( + "errors" + "fmt" + "sync" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/rlp" + "golang.org/x/crypto/sha3" +) + +// Leaf represents a trie leaf value +type Leaf struct { + size int // size of the rlp data (estimate) + hash common.Hash // hash of rlp data + node node // the node to commit + vnodes bool // set to true if the node (possibly) contains a valueNode +} + +type committer struct { + tmp sliceBuffer + sha keccakState + + onleaf LeafCallback + leafCh chan *Leaf +} + +// committers live in a global db. +var committerPool = sync.Pool{ + New: func() interface{} { + return &committer{ + tmp: make(sliceBuffer, 0, 550), // cap is as large as a full fullNode. + sha: sha3.NewLegacyKeccak256().(keccakState), + } + }, +} + +func newCommitter(onleaf LeafCallback) *committer { + h := committerPool.Get().(*committer) + h.onleaf = onleaf + if onleaf != nil { + h.leafCh = make(chan *Leaf, 200) // arbitrary number + } + return h +} + +func returnCommitterToPool(h *committer) { + h.onleaf = nil + h.leafCh = nil + committerPool.Put(h) +} + +// hash collapses a node down into a hash node, also returning a copy of the +// original node initialized with the computed hash to replace the original one. +func (h *committer) commit(n node, db *Database, force bool) (node, error) { + // If we're not storing the node, just hashing, use available cached data + hash, dirty := n.cache() + if hash != nil && !dirty { + return hash, nil + } + if db == nil { + return nil, errors.New("no db provided") + } + // Commit children. then parent + // Remove the dirty flag. + switch cn := n.(type) { + case *shortNode: + // Commit child + collapsed := cn.copy() + if _, ok := cn.Val.(valueNode); !ok { + if childV, err := h.commit(cn.Val, db, false); err != nil { + return nil, err + } else { + collapsed.Val = childV + } + } + // The key needs to be copied, since we're delivering it to database + collapsed.Key = hexToCompact(cn.Key) + hashedNode := h.store(collapsed, db, force, true) + if hn, ok := hashedNode.(hashNode); ok { + cn.flags.dirty = false + return hn, nil + } else { + return collapsed, nil + } + case *fullNode: + hashedKids, hasVnodes, err := h.commitChildren(cn, db, force) + if err != nil { + return nil, err + } + collapsed := cn.copy() + collapsed.Children = hashedKids + + hashedNode := h.store(collapsed, db, force, hasVnodes) + if hn, ok := hashedNode.(hashNode); ok { + cn.flags.dirty = false + return hn, nil + } else { + return collapsed, nil + } + case valueNode: + return h.store(cn, db, force, false), nil + // hashnodes aren't stored + case hashNode: + return cn, nil + } + return hash, nil +} + +// commitChildren commits the children of the given fullnode +func (h *committer) commitChildren(n *fullNode, db *Database, force bool) ([17]node, bool, error) { + var children [17]node + var hasValueNodeChildren = false + for i, child := range n.Children { + if child == nil { + continue + } + hnode, err := h.commit(child, db, false) + if err != nil { + return children, false, err + } + children[i] = hnode + if _, ok := hnode.(valueNode); ok { + hasValueNodeChildren = true + } + } + return children, hasValueNodeChildren, nil +} + +// store hashes the node n and if we have a storage layer specified, it writes +// the key/value pair to it and tracks any node->child references as well as any +// node->external trie references. +func (h *committer) store(n node, db *Database, force bool, hasVnodeChildren bool) node { + // Larger nodes are replaced by their hash and stored in the database. + var ( + hash, _ = n.cache() + size = 0 + ) + if hash == nil { + if vn, ok := n.(valueNode); ok { + h.tmp.Reset() + if err := rlp.Encode(&h.tmp, vn); err != nil { + panic("encode error: " + err.Error()) + } + size = len(h.tmp) + if size < 32 && !force { + return n // Nodes smaller than 32 bytes are stored inside their parent + } + hash = h.makeHashNode(h.tmp) + } else { + // This was not generated - must be a small node stored in the parent + // No need to do anything here + return n + } + } else { + // 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 + size = estimateSize(n) + } + // If we're using channel-based leaf-reporting, send to channel. + // The leaf channel will be active only when there an active leaf-callback + if h.leafCh != nil { + h.leafCh <- &Leaf{ + size: size, + hash: common.BytesToHash(hash), + node: n, + vnodes: hasVnodeChildren, + } + } else if db != nil { + // No leaf-callback used, but there's still a database. Do serial + // insertion + db.lock.Lock() + db.insert(common.BytesToHash(hash), size, n) + db.lock.Unlock() + } + return hash +} + +// commitLoop does the actual insert + leaf callback for nodes +func (h *committer) commitLoop(db *Database, wg *sync.WaitGroup) { + defer wg.Done() + for item := range h.leafCh { + var ( + hash = item.hash + size = item.size + n = item.node + hasVnodes = item.vnodes + ) + // We are pooling the trie nodes into an intermediate memory cache + db.lock.Lock() + db.insert(hash, size, n) + db.lock.Unlock() + if h.onleaf != nil && hasVnodes { + switch n := n.(type) { + case *shortNode: + if child, ok := n.Val.(valueNode); ok { + h.onleaf(child, hash) + } + case *fullNode: + for i := 0; i < 16; i++ { + if child, ok := n.Children[i].(valueNode); ok { + h.onleaf(child, hash) + } + } + } + } + } +} + +func (h *committer) makeHashNode(data []byte) hashNode { + //fmt.Printf("hashing: %x\n", data) + n := make(hashNode, h.sha.Size()) + h.sha.Reset() + h.sha.Write(data) + h.sha.Read(n) + return n +} + +// 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 leafs, 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 += 1 + } + } + return s + case valueNode: + return 1 + len(n) + case hashNode: + return 1 + len(n) + default: + panic(fmt.Sprintf("node type %T", n)) + + } + return 0 +} + +/** +Todo, we could improve the situation for small trie commits (storage tries), +if we use one dedicated database-inserter, instead of having each one spin up a +separate instance. + +The gain is not only that we save some goroutine start/stop, it's also that +we can process trie M while we're still committing trie N -- since we don't +have to do the waitgroup-wait between each trie commit. + +The code below is a rough sketch, it needs to be integrated nicely without causing +dependency cycles between state, core and trie. + + + +type DbInserter struct { + inputCh chan *Leaf // This is where input to database is sent + reportCh chan int // At certain points, callers wants to know that we're done + db *Database + wg sync.WaitGroup +} + +// commitLoop does the actual insert + leaf callback for nodes +func (dbi *DbInserter) run() { + defer dbi.wg.Done() + for item := range dbi.inputCh { + var ( + hash = item.hash + size = item.size + n = item.node + hasVnodes = item.vnodes + onleaf = item.onLeaf + ) + if size < 0 { + // This is an end-marker object. + dbi.reportCh <- size + continue + } + // We are pooling the trie nodes into an intermediate memory cache + dbi.db.lock.Lock() + dbi.db.insert(hash, size, n) + dbi.db.lock.Unlock() + if onleaf != nil && hasVnodes { + switch n := n.(type) { + case *shortNode: + if child, ok := n.Val.(valueNode); ok { + onleaf(child, hash) + } + case *fullNode: + for i := 0; i < 16; i++ { + if child, ok := n.Children[i].(valueNode); ok { + onleaf(child, hash) + } + } + } + } + } +} + +func (dbi *DbInserter) Close() { + close(dbi.inputCh) + dbi.wg.Wait() +} + +func (dbi *DbInserter) Insert(leaf *Leaf) { + dbi.inputCh <- leaf +} + +// WaitForEmpty returns to the caller when all the data currently in the +// channel has been handled +func (dbi *DbInserter) WaitForEmpty() { + // Send an arbitrary id there + checksum := rand.Uint32() + dbi.inputCh <- &trie.Leaf{ + size: -checksum, + } + // And wait for it to come back + for { + select { + case retval <- dbi.reportCh: + if retval == checksum { + return + } + + } + } +} + +func (dbi *DbInserter) InsertBlob(blob []byte, blobHash common.Hash) { + dbi.inputCh <- &trie.Leaf{ + size: len(blob), + hash: blobHash, + node: rawNode(blob), + vnodes: false, + } +} + +func StartDBInserter(db *Database) *DbInserter { + + dbi := &DbInserter{ + inputCh: make(chan *Leaf, 200), + reportCh: make(chan int), + db: db, + } + go dbi.run() +} + + +*/ diff --git a/trie/pure_hasher.go b/trie/pure_hasher.go new file mode 100644 index 0000000000..f13d2607ba --- /dev/null +++ b/trie/pure_hasher.go @@ -0,0 +1,159 @@ +// Copyright 2019 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 . + +package trie + +import ( + "sync" + + "github.com/ethereum/go-ethereum/rlp" + "golang.org/x/crypto/sha3" +) + +type pureHasher struct { + sha keccakState + + tmp sliceBuffer + tmpKey []byte +} + +// hashers live in a global db. +var pureHasherPool = sync.Pool{ + New: func() interface{} { + return &pureHasher{ + tmp: make(sliceBuffer, 0, 550), // cap is as large as a full fullNode. + tmpKey: make([]byte, 64), // space for an packed key + sha: sha3.NewLegacyKeccak256().(keccakState), + } + }, +} + +func newPureHasher() *pureHasher { + h := pureHasherPool.Get().(*pureHasher) + return h +} + +func returnPureHasherToPool(h *pureHasher) { + pureHasherPool.Put(h) +} + +// hash collapses a node down into a hash node, also returning a copy of the +// original node initialized with the computed hash to replace the original one. +func (h *pureHasher) hash(n node, force bool) (hashed node, cached node) { + // We're not storing the node, just hashing, use available cached data + if hash, _ := n.cache(); hash != nil { + return hash, n + } + // Trie not processed yet or needs storage, walk the children + switch n := n.(type) { + case *shortNode: + collapsed, cached := h.hashShortNodeChildren(n) + hashed := h.shortnodeToHash(collapsed, force) + // We need to retain the possibly _not_ hashed node, in case it was too + // small to be hashed + if hn, ok := hashed.(hashNode); ok { + cached.flags.hash = hn + } else { + cached.flags.hash = nil + } + return hashed, cached + case *fullNode: + collapsed, cached := h.hashFullNodeChildren(n) + hashed = h.fullnodeToHash(collapsed, force) + if hn, ok := hashed.(hashNode); ok { + cached.flags.hash = hn + } else { + cached.flags.hash = nil + } + return hashed, cached + default: + // Value and hash nodes don't have children so they're left as were + return n, n + } +} + +// hashShortNodeChildren collapses the short node. The returned collapsed node +// holds a live reference to the Key, and must not be modified. +// The cached +func (h *pureHasher) hashShortNodeChildren(n *shortNode) (collapsed, cached *shortNode) { + // Hash the short node's child, caching the newly hashed subtree + collapsed, cached = n.copy(), n.copy() + // Previously, we did copy this one. We don't seem to need to actually + // do that, since we don't overwrite/reuse keys + //cached.Key = common.CopyBytes(n.Key) + collapsed.Key = hexToCompact(n.Key) + // Unless the child is a valuenode or hashnode, hash it + switch n.Val.(type) { + case *fullNode, *shortNode: + collapsed.Val, cached.Val = h.hash(n.Val, false) + } + return collapsed, cached +} + +func (h *pureHasher) hashFullNodeChildren(n *fullNode) (collapsed *fullNode, cached *fullNode) { + // Hash the full node's children, caching the newly hashed subtrees + cached = n.copy() + collapsed = n.copy() + for i := 0; i < 16; i++ { + if child := n.Children[i]; child != nil { + collapsed.Children[i], cached.Children[i] = h.hash(child, false) + } else { + collapsed.Children[i] = nilValueNode + } + } + cached.Children[16] = n.Children[16] + return collapsed, cached +} + +// shortnodeToHash creates a hashNode from a shortNode. The supplied shortnode +// should have hex-type Key, which will be converted (without modification) +// into compact form for RLP encoding. +// If the rlp data is smaller than 32 bytes, `nil` is returned. +func (h *pureHasher) shortnodeToHash(n *shortNode, force bool) node { + h.tmp.Reset() + if err := rlp.Encode(&h.tmp, n); err != nil { + panic("encode error: " + err.Error()) + } + + if len(h.tmp) < 32 && !force { + return n // Nodes smaller than 32 bytes are stored inside their parent + } + return h.hashData(h.tmp) +} + +// shortnodeToHash is used to creates a hashNode from a set of hashNodes, (which +// may contain nil values) +func (h *pureHasher) fullnodeToHash(n *fullNode, force bool) node { + h.tmp.Reset() + // Generate the RLP encoding of the node + if err := rlp.Encode(&h.tmp, n); err != nil { + panic("encode error: " + err.Error()) + } + + if len(h.tmp) < 32 && !force { + return n // Nodes smaller than 32 bytes are stored inside their parent + } + return h.hashData(h.tmp) +} + +// hashData hashes the provided data +func (h *pureHasher) hashData(data []byte) hashNode { + n := make(hashNode, 32) + h.sha.Reset() + h.sha.Write(data) + h.sha.Read(n) + return n +}