Skip to content

Commit eea6cdd

Browse files
authored
Merge pull request #2445 from CortexFoundation/dev
reduce the memory allocation in trie hashing
2 parents 12722fc + 66005fa commit eea6cdd

6 files changed

Lines changed: 126 additions & 129 deletions

File tree

trie/hasher.go

Lines changed: 99 additions & 99 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
1-
// Copyright 2019 The go-ethereum Authors
2-
// This file is part of The go-ethereum library.
1+
// Copyright 2016 The go-ethereum Authors
2+
// This file is part of the go-ethereum library.
33
//
44
// The go-ethereum library is free software: you can redistribute it and/or modify
55
// it under the terms of the GNU Lesser General Public License as published by
@@ -12,11 +12,13 @@
1212
// GNU Lesser General Public License for more details.
1313
//
1414
// You should have received a copy of the GNU Lesser General Public License
15-
// along with The go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
15+
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
1616

1717
package trie
1818

1919
import (
20+
"bytes"
21+
"fmt"
2022
"sync"
2123

2224
"github.com/CortexFoundation/CortexTheseus/crypto"
@@ -29,7 +31,7 @@ type hasher struct {
2931
sha crypto.KeccakState
3032
tmp []byte
3133
encbuf rlp.EncoderBuffer
32-
parallel bool // Whether to use paralallel threads when hashing
34+
parallel bool // Whether to use parallel threads when hashing
3335
}
3436

3537
// hasherPool holds pureHashers
@@ -53,119 +55,119 @@ func returnHasherToPool(h *hasher) {
5355
hasherPool.Put(h)
5456
}
5557

56-
// hash collapses a node down into a hash node, also returning a copy of the
57-
// original node initialized with the computed hash to replace the original one.
58-
func (h *hasher) hash(n node, force bool) node {
58+
// hash collapses a node down into a hash node.
59+
func (h *hasher) hash(n node, force bool) []byte {
5960
// Return the cached hash if it's available
6061
if hash, _ := n.cache(); hash != nil {
6162
return hash
6263
}
6364
// Trie not processed yet, walk the children
6465
switch n := n.(type) {
6566
case *shortNode:
66-
collapsed := h.hashShortNodeChildren(n)
67-
hashed := h.shortnodeToHash(collapsed, force)
68-
// We need to retain the possibly _not_ hashed node, in case it was too
69-
// small to be hashed
70-
if hn, ok := hashed.(hashNode); ok {
71-
n.flags.hash = hn
72-
} else {
73-
n.flags.hash = nil
67+
enc := h.encodeShortNode(n)
68+
if len(enc) < 32 && !force {
69+
// Nodes smaller than 32 bytes are embedded directly in their parent.
70+
// In such cases, return the raw encoded blob instead of the node hash.
71+
// It's essential to deep-copy the node blob, as the underlying buffer
72+
// of enc will be reused later.
73+
buf := make([]byte, len(enc))
74+
copy(buf, enc)
75+
return buf
7476
}
75-
return hashed
77+
hash := h.hashData(enc)
78+
n.flags.hash = hash
79+
return hash
80+
7681
case *fullNode:
77-
collapsed := h.hashFullNodeChildren(n)
78-
hashed := h.fullnodeToHash(collapsed, force)
79-
if hn, ok := hashed.(hashNode); ok {
80-
n.flags.hash = hn
81-
} else {
82-
n.flags.hash = nil
82+
enc := h.encodeFullNode(n)
83+
if len(enc) < 32 && !force {
84+
// Nodes smaller than 32 bytes are embedded directly in their parent.
85+
// In such cases, return the raw encoded blob instead of the node hash.
86+
// It's essential to deep-copy the node blob, as the underlying buffer
87+
// of enc will be reused later.
88+
buf := make([]byte, len(enc))
89+
copy(buf, enc)
90+
return buf
8391
}
84-
return hashed
85-
default:
86-
// Value and hash nodes don't have children, so they're left as were
92+
hash := h.hashData(enc)
93+
n.flags.hash = hash
94+
return hash
95+
96+
case hashNode:
97+
// hash nodes don't have children, so they're left as were
8798
return n
99+
100+
default:
101+
panic(fmt.Errorf("unexpected node type, %T", n))
88102
}
89103
}
90104

91-
// hashShortNodeChildren collapses the short node. The returned collapsed node
92-
// holds a live reference to the Key, and must not be modified.
93-
func (h *hasher) hashShortNodeChildren(n *shortNode) *shortNode {
94-
// Hash the short node's child, caching the newly hashed subtree
95-
//collapsed, cached = n.copy(), n.copy()
96-
var collapsed shortNode
97-
// Previously, we did copy this one. We don't seem to need to actually
98-
// do that, since we don't overwrite/reuse keys
99-
// cached.Key = common.CopyBytes(n.Key)
100-
collapsed.Key = hexToCompact(n.Key)
101-
// Unless the child is a valuenode or hashnode, hash it
102-
switch n.Val.(type) {
103-
case *fullNode, *shortNode:
104-
collapsed.Val = h.hash(n.Val, false)
105-
default:
106-
collapsed.Val = n.Val
105+
// encodeShortNode encodes the provided shortNode into the bytes. Notably, the
106+
// return slice must be deep-copied explicitly, otherwise the underlying slice
107+
// will be reused later.
108+
func (h *hasher) encodeShortNode(n *shortNode) []byte {
109+
// Encode leaf node
110+
if hasTerm(n.Key) {
111+
var ln leafNodeEncoder
112+
ln.Key = hexToCompact(n.Key)
113+
ln.Val = n.Val.(valueNode)
114+
ln.encode(h.encbuf)
115+
return h.encodedBytes()
107116
}
108-
return &collapsed
117+
// Encode extension node
118+
var en extNodeEncoder
119+
en.Key = hexToCompact(n.Key)
120+
en.Val = h.hash(n.Val, false)
121+
en.encode(h.encbuf)
122+
return h.encodedBytes()
123+
}
124+
125+
// fnEncoderPool is the pool for storing shared fullNode encoder to mitigate
126+
// the significant memory allocation overhead.
127+
var fnEncoderPool = sync.Pool{
128+
New: func() interface{} {
129+
var enc fullnodeEncoder
130+
return &enc
131+
},
109132
}
110133

111-
func (h *hasher) hashFullNodeChildren(n *fullNode) *fullNode {
112-
// Hash the full node's children, caching the newly hashed subtrees
113-
var children [17]node
134+
// encodeFullNode encodes the provided fullNode into the bytes. Notably, the
135+
// return slice must be deep-copied explicitly, otherwise the underlying slice
136+
// will be reused later.
137+
func (h *hasher) encodeFullNode(n *fullNode) []byte {
138+
fn := fnEncoderPool.Get().(*fullnodeEncoder)
139+
fn.reset()
140+
114141
if h.parallel {
115142
var wg sync.WaitGroup
116143
for i := 0; i < 16; i++ {
117-
if child := n.Children[i]; child != nil {
118-
wg.Add(1)
119-
go func(i int) {
120-
hasher := newHasher(false)
121-
children[i] = hasher.hash(child, false)
122-
returnHasherToPool(hasher)
123-
wg.Done()
124-
}(i)
125-
} else {
126-
children[i] = nilValueNode
144+
if n.Children[i] == nil {
145+
continue
127146
}
147+
wg.Add(1)
148+
go func(i int) {
149+
defer wg.Done()
150+
151+
h := newHasher(false)
152+
fn.Children[i] = h.hash(n.Children[i], false)
153+
returnHasherToPool(h)
154+
}(i)
128155
}
129156
wg.Wait()
130157
} else {
131158
for i := 0; i < 16; i++ {
132159
if child := n.Children[i]; child != nil {
133-
children[i] = h.hash(child, false)
134-
} else {
135-
children[i] = nilValueNode
160+
fn.Children[i] = h.hash(child, false)
136161
}
137162
}
138163
}
139164
if n.Children[16] != nil {
140-
children[16] = n.Children[16]
141-
}
142-
return &fullNode{flags: nodeFlag{}, Children: children}
143-
}
144-
145-
// shortnodeToHash creates a hashNode from a shortNode. The supplied shortnode
146-
// should have hex-type Key, which will be converted (without modification)
147-
// into compact form for RLP encoding.
148-
// If the rlp data is smaller than 32 bytes, `nil` is returned.
149-
func (h *hasher) shortnodeToHash(n *shortNode, force bool) node {
150-
n.encode(h.encbuf)
151-
enc := h.encodedBytes()
152-
153-
if len(enc) < 32 && !force {
154-
return n // Nodes smaller than 32 bytes are stored inside their parent
165+
fn.Children[16] = n.Children[16].(valueNode)
155166
}
156-
return h.hashData(enc)
157-
}
158-
159-
// fullnodeToHash is used to create a hashNode from a fullNode, (which
160-
// may contain nil values)
161-
func (h *hasher) fullnodeToHash(n *fullNode, force bool) node {
162-
n.encode(h.encbuf)
163-
enc := h.encodedBytes()
167+
fn.encode(h.encbuf)
168+
fnEncoderPool.Put(fn)
164169

165-
if len(enc) < 32 && !force {
166-
return n // Nodes smaller than 32 bytes are stored inside their parent
167-
}
168-
return h.hashData(enc)
170+
return h.encodedBytes()
169171
}
170172

171173
// encodedBytes returns the result of the last encoding operation on h.encbuf.
@@ -184,9 +186,10 @@ func (h *hasher) encodedBytes() []byte {
184186
return h.tmp
185187
}
186188

187-
// hashData hashes the provided data
188-
func (h *hasher) hashData(data []byte) hashNode {
189-
n := make(hashNode, 32)
189+
// hashData hashes the provided data. It is safe to modify the returned slice after
190+
// the function returns.
191+
func (h *hasher) hashData(data []byte) []byte {
192+
n := make([]byte, 32)
190193
h.sha.Reset()
191194
h.sha.Write(data)
192195
h.sha.Read(n)
@@ -201,20 +204,17 @@ func (h *hasher) hashDataTo(dst, data []byte) {
201204
h.sha.Read(dst)
202205
}
203206

204-
// proofHash is used to construct trie proofs, and returns the 'collapsed'
205-
// node (for later RLP encoding) aswell as the hashed node -- unless the
206-
// node is smaller than 32 bytes, in which case it will be returned as is.
207-
// This method does not do anything on value- or hash-nodes.
208-
func (h *hasher) proofHash(original node) (collapsed, hashed node) {
207+
// proofHash is used to construct trie proofs, returning the rlp-encoded node blobs.
208+
// Note, only resolved node (shortNode or fullNode) is expected for proofing.
209+
//
210+
// It is safe to modify the returned slice after the function returns.
211+
func (h *hasher) proofHash(original node) []byte {
209212
switch n := original.(type) {
210213
case *shortNode:
211-
sn := h.hashShortNodeChildren(n)
212-
return sn, h.shortnodeToHash(sn, false)
214+
return bytes.Clone(h.encodeShortNode(n))
213215
case *fullNode:
214-
fn := h.hashFullNodeChildren(n)
215-
return fn, h.fullnodeToHash(fn, false)
216+
return bytes.Clone(h.encodeFullNode(n))
216217
default:
217-
// Value and hash nodes don't have children, so they're left as were
218-
return n, n
218+
panic(fmt.Errorf("unexpected node type, %T", original))
219219
}
220220
}

trie/iterator.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -198,9 +198,9 @@ func (it *nodeIterator) LeafProof() [][]byte {
198198

199199
for i, item := range it.stack[:len(it.stack)-1] {
200200
// Gather nodes that end up as hash nodes (or the root)
201-
node, hashed := hasher.proofHash(item.node)
202-
if _, ok := hashed.(hashNode); ok || i == 0 {
203-
proofs = append(proofs, nodeToBytes(node))
201+
enc := hasher.proofHash(item.node)
202+
if len(enc) >= 32 || i == 0 {
203+
proofs = append(proofs, enc)
204204
}
205205
}
206206
return proofs

trie/node.go

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -68,10 +68,6 @@ type (
6868
}
6969
)
7070

71-
// nilValueNode is used when collapsing internal trie nodes for hashing, since
72-
// unset children need to serialize correctly.
73-
var nilValueNode = valueNode(nil)
74-
7571
// EncodeRLP encodes a full node into the consensus RLP format.
7672
func (n *fullNode) EncodeRLP(w io.Writer) error {
7773
eb := rlp.NewEncoderBuffer(w)

trie/node_enc.go

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -42,18 +42,29 @@ func (n *fullNode) encode(w rlp.EncoderBuffer) {
4242

4343
func (n *fullnodeEncoder) encode(w rlp.EncoderBuffer) {
4444
offset := w.List()
45-
for _, c := range n.Children {
46-
if c == nil {
45+
for i, c := range n.Children {
46+
if len(c) == 0 {
4747
w.Write(rlp.EmptyString)
48-
} else if len(c) < 32 {
49-
w.Write(c) // rawNode
5048
} else {
51-
w.WriteBytes(c) // hashNode
49+
// valueNode or hashNode
50+
if i == 16 || len(c) >= 32 {
51+
w.WriteBytes(c)
52+
} else {
53+
w.Write(c) // rawNode
54+
}
5255
}
5356
}
5457
w.ListEnd(offset)
5558
}
5659

60+
func (n *fullnodeEncoder) reset() {
61+
for i, c := range n.Children {
62+
if len(c) != 0 {
63+
n.Children[i] = n.Children[i][:0]
64+
}
65+
}
66+
}
67+
5768
func (n *shortNode) encode(w rlp.EncoderBuffer) {
5869
offset := w.List()
5970
w.WriteBytes(n.Key)
@@ -70,7 +81,7 @@ func (n *extNodeEncoder) encode(w rlp.EncoderBuffer) {
7081
w.WriteBytes(n.Key)
7182

7283
if n.Val == nil {
73-
w.Write(rlp.EmptyString)
84+
w.Write(rlp.EmptyString) // theoretically impossible to happen
7485
} else if len(n.Val) < 32 {
7586
w.Write(n.Val) // rawNode
7687
} else {

trie/proof.go

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ import (
2222
"fmt"
2323

2424
"github.com/CortexFoundation/CortexTheseus/common"
25+
"github.com/CortexFoundation/CortexTheseus/crypto"
2526
"github.com/CortexFoundation/CortexTheseus/ctxcdb"
2627
"github.com/CortexFoundation/CortexTheseus/ctxcdb/memorydb"
2728
"github.com/CortexFoundation/CortexTheseus/log"
@@ -69,20 +70,9 @@ func (t *Trie) Prove(key []byte, fromLevel uint, proofDb ctxcdb.KeyValueWriter)
6970
defer returnHasherToPool(hasher)
7071

7172
for i, n := range nodes {
72-
if fromLevel > 0 {
73-
fromLevel--
74-
continue
75-
}
76-
var hn node
77-
n, hn = hasher.proofHash(n)
78-
if hash, ok := hn.(hashNode); ok || i == 0 {
79-
// If the node's database encoding is a hash (or is the
80-
// root node), it becomes a proof element.
81-
enc := nodeToBytes(n)
82-
if !ok {
83-
hash = hasher.hashData(enc)
84-
}
85-
proofDb.Put(hash, enc)
73+
enc := hasher.proofHash(n)
74+
if len(enc) >= 32 || i == 0 {
75+
proofDb.Put(crypto.Keccak256(enc), enc)
8676
}
8777
}
8878
return nil

trie/trie.go

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,7 @@ func (t *Trie) resolveBlob(n hashNode, prefix []byte) ([]byte, error) {
615615
// Hash returns the root hash of the trie. It does not write to the
616616
// database and can be used even if the trie doesn't have one.
617617
func (t *Trie) Hash() common.Hash {
618-
return common.BytesToHash(t.hashRoot().(hashNode))
618+
return common.BytesToHash(t.hashRoot())
619619
}
620620

621621
// Commit writes all nodes to the trie's memory database, tracking the internal
@@ -674,9 +674,9 @@ func (t *Trie) Commit(onleaf LeafCallback) (root common.Hash, err error) {
674674
}
675675

676676
// hashRoot calculates the root hash of the given trie
677-
func (t *Trie) hashRoot() node {
677+
func (t *Trie) hashRoot() []byte {
678678
if t.root == nil {
679-
return hashNode(types.EmptyRootHash.Bytes())
679+
return types.EmptyRootHash.Bytes()
680680
}
681681
// If the number of changes is below 100, we let one thread handle it
682682
h := newHasher(t.unhashed >= 100)

0 commit comments

Comments
 (0)