mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-24 13:46:43 +00:00
trie: swap out the pools in StackTrie with an arena allocator
Arena allocators are a perfect match for something like StackTrie where allocations happens in stack-like "frames" and can then be deallocation at the end of the "allocation frame" We can construct the "allocation frames" every time we create a new extension/branch node and decontstruct the frame by deallocating everything allocated on the arena during that frame. This saves us from dealing with individual allocations/deallocations
This commit is contained in:
parent
0dacfef8ac
commit
773b0b290a
4 changed files with 231 additions and 103 deletions
61
common/arena.go
Normal file
61
common/arena.go
Normal file
|
|
@ -0,0 +1,61 @@
|
||||||
|
// Copyright 2025 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 common
|
||||||
|
|
||||||
|
// Arena is an allocation primitive that allows individual allocations
|
||||||
|
// out of a page of items and bulk de-allocations of last N allocations.
|
||||||
|
// The most common way of using an Arena is to pair it up with a sync.Pool
|
||||||
|
// of pages.
|
||||||
|
type Arena[T any] struct {
|
||||||
|
used uint32
|
||||||
|
pages [][]T
|
||||||
|
|
||||||
|
PageSize uint32
|
||||||
|
NewPage func() any
|
||||||
|
ReleasePage func(any)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Alloc returns the next free item on the arena
|
||||||
|
// Allocates a new page if needed
|
||||||
|
func (a *Arena[T]) Alloc() *T {
|
||||||
|
pageIndex := a.used / a.PageSize
|
||||||
|
pageOffset := a.used % a.PageSize
|
||||||
|
if pageOffset == 0 {
|
||||||
|
a.pages = append(a.pages, a.NewPage().([]T))
|
||||||
|
}
|
||||||
|
a.used++
|
||||||
|
return &a.pages[pageIndex][pageOffset]
|
||||||
|
}
|
||||||
|
|
||||||
|
// Used returns the number of items that live on this arena
|
||||||
|
func (a *Arena[T]) Used() uint32 {
|
||||||
|
return a.used
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset rollsback the active set of live elements to the given number
|
||||||
|
func (a *Arena[T]) Reset(to uint32) {
|
||||||
|
a.used = to
|
||||||
|
}
|
||||||
|
|
||||||
|
// Release releases all the pages that the arena currently owns
|
||||||
|
func (a *Arena[T]) Release() {
|
||||||
|
for _, page := range a.pages {
|
||||||
|
a.ReleasePage(page)
|
||||||
|
}
|
||||||
|
a.pages = nil
|
||||||
|
a.used = 0
|
||||||
|
}
|
||||||
|
|
@ -1,64 +0,0 @@
|
||||||
// Copyright 2024 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
|
|
||||||
|
|
||||||
// bytesPool is a pool for byte slices. It is safe for concurrent use.
|
|
||||||
type bytesPool struct {
|
|
||||||
c chan []byte
|
|
||||||
w int
|
|
||||||
}
|
|
||||||
|
|
||||||
// newBytesPool creates a new bytesPool. The sliceCap sets the capacity of
|
|
||||||
// newly allocated slices, and the nitems determines how many items the pool
|
|
||||||
// will hold, at maximum.
|
|
||||||
func newBytesPool(sliceCap, nitems int) *bytesPool {
|
|
||||||
return &bytesPool{
|
|
||||||
c: make(chan []byte, nitems),
|
|
||||||
w: sliceCap,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get returns a slice. Safe for concurrent use.
|
|
||||||
func (bp *bytesPool) Get() []byte {
|
|
||||||
select {
|
|
||||||
case b := <-bp.c:
|
|
||||||
return b
|
|
||||||
default:
|
|
||||||
return make([]byte, 0, bp.w)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetWithSize returns a slice with specified byte slice size.
|
|
||||||
func (bp *bytesPool) GetWithSize(s int) []byte {
|
|
||||||
b := bp.Get()
|
|
||||||
if cap(b) < s {
|
|
||||||
return make([]byte, s)
|
|
||||||
}
|
|
||||||
return b[:s]
|
|
||||||
}
|
|
||||||
|
|
||||||
// Put returns a slice to the pool. Safe for concurrent use. This method
|
|
||||||
// will ignore slices that are too small or too large (>3x the cap)
|
|
||||||
func (bp *bytesPool) Put(b []byte) {
|
|
||||||
if c := cap(b); c < bp.w || c > 3*bp.w {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
select {
|
|
||||||
case bp.c <- b:
|
|
||||||
default:
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -20,14 +20,30 @@ import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"errors"
|
"errors"
|
||||||
"sync"
|
"sync"
|
||||||
|
"unsafe"
|
||||||
|
|
||||||
"github.com/ethereum/go-ethereum/common"
|
"github.com/ethereum/go-ethereum/common"
|
||||||
"github.com/ethereum/go-ethereum/core/types"
|
"github.com/ethereum/go-ethereum/core/types"
|
||||||
|
"github.com/ethereum/go-ethereum/metrics"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var stNodeAllocationMeter = metrics.NewRegisteredMeter("stacktrie/allocation/node", nil)
|
||||||
|
var byteAllocationMeter = metrics.NewRegisteredMeter("stacktrie/allocation/byte", nil)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
stPool = sync.Pool{New: func() any { return new(stNode) }}
|
stPageSize = 1024
|
||||||
bPool = newBytesPool(32, 100)
|
stPagePool = sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
stNodeAllocationMeter.Mark(int64(unsafe.Sizeof(stNode{})) * int64(stPageSize))
|
||||||
|
return make([]stNode, stPageSize)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
bytePagePool = sync.Pool{
|
||||||
|
New: func() any {
|
||||||
|
byteAllocationMeter.Mark(int64(unsafe.Sizeof([32]byte{})) * int64(stPageSize))
|
||||||
|
return make([][32]byte, stPageSize)
|
||||||
|
},
|
||||||
|
}
|
||||||
_ = types.TrieHasher((*StackTrie)(nil))
|
_ = types.TrieHasher((*StackTrie)(nil))
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -40,6 +56,14 @@ var (
|
||||||
// it after the call ends.
|
// it after the call ends.
|
||||||
type OnTrieNode func(path []byte, hash common.Hash, blob []byte)
|
type OnTrieNode func(path []byte, hash common.Hash, blob []byte)
|
||||||
|
|
||||||
|
// allocationFrame keeps track of the position of the allocators in StackTrie
|
||||||
|
// at the time of a new branch/ext node being created. At the point where this
|
||||||
|
// node is hashed, allocators can be reset to the positions stored in the allocation
|
||||||
|
// frame
|
||||||
|
type allocationFrame struct {
|
||||||
|
node, bytes uint32
|
||||||
|
}
|
||||||
|
|
||||||
// StackTrie is a trie implementation that expects keys to be inserted
|
// StackTrie is a trie implementation that expects keys to be inserted
|
||||||
// in order. Once it determines that a subtree will no longer be inserted
|
// in order. Once it determines that a subtree will no longer be inserted
|
||||||
// into, it will hash it and free up the memory it uses.
|
// into, it will hash it and free up the memory it uses.
|
||||||
|
|
@ -48,20 +72,47 @@ type StackTrie struct {
|
||||||
h *hasher
|
h *hasher
|
||||||
last []byte
|
last []byte
|
||||||
onTrieNode OnTrieNode
|
onTrieNode OnTrieNode
|
||||||
|
|
||||||
|
nodeAllocator common.Arena[stNode]
|
||||||
|
byteAllocator common.Arena[[32]byte]
|
||||||
|
allocationStackFrames []allocationFrame
|
||||||
|
|
||||||
kBuf []byte // buf space used for hex-key during insertions
|
kBuf []byte // buf space used for hex-key during insertions
|
||||||
pBuf []byte // buf space used for path during insertions
|
pBuf []byte // buf space used for path during insertions
|
||||||
|
tmpNode *stNode // used as a temporary ext node when needed
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStackTrie allocates and initializes an empty trie. The committed nodes
|
// NewStackTrie allocates and initializes an empty trie. The committed nodes
|
||||||
// will be discarded immediately if no callback is configured.
|
// will be discarded immediately if no callback is configured.
|
||||||
func NewStackTrie(onTrieNode OnTrieNode) *StackTrie {
|
func NewStackTrie(onTrieNode OnTrieNode) *StackTrie {
|
||||||
return &StackTrie{
|
t := StackTrie{
|
||||||
root: stPool.Get().(*stNode),
|
|
||||||
h: newHasher(false),
|
h: newHasher(false),
|
||||||
onTrieNode: onTrieNode,
|
onTrieNode: onTrieNode,
|
||||||
kBuf: make([]byte, 64),
|
kBuf: make([]byte, 64),
|
||||||
pBuf: make([]byte, 64),
|
pBuf: make([]byte, 64),
|
||||||
|
nodeAllocator: common.Arena[stNode]{NewPage: stPagePool.Get, PageSize: uint32(stPageSize), ReleasePage: stPagePool.Put},
|
||||||
|
byteAllocator: common.Arena[[32]byte]{NewPage: bytePagePool.Get, PageSize: uint32(stPageSize), ReleasePage: bytePagePool.Put},
|
||||||
}
|
}
|
||||||
|
t.root = t.nodeAllocator.Alloc().reset()
|
||||||
|
t.tmpNode = t.nodeAllocator.Alloc().reset()
|
||||||
|
return &t
|
||||||
|
}
|
||||||
|
|
||||||
|
// creates a new allocation frame by saving positions of internal allocators
|
||||||
|
func (t *StackTrie) pushAllocationFrame() {
|
||||||
|
t.allocationStackFrames = append(t.allocationStackFrames, allocationFrame{
|
||||||
|
node: t.nodeAllocator.Used(),
|
||||||
|
bytes: t.byteAllocator.Used(),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// pops a saved allocation frame and rollsback allocators to the state they were
|
||||||
|
// at the beginning of the frame
|
||||||
|
func (t *StackTrie) popAllocationFrame() {
|
||||||
|
allocationFrame := t.allocationStackFrames[len(t.allocationStackFrames)-1]
|
||||||
|
t.nodeAllocator.Reset(allocationFrame.node)
|
||||||
|
t.byteAllocator.Reset(allocationFrame.bytes)
|
||||||
|
t.allocationStackFrames = t.allocationStackFrames[:len(t.allocationStackFrames)-1]
|
||||||
}
|
}
|
||||||
|
|
||||||
func (t *StackTrie) grow(key []byte) {
|
func (t *StackTrie) grow(key []byte) {
|
||||||
|
|
@ -94,7 +145,10 @@ func (t *StackTrie) Update(key, value []byte) error {
|
||||||
|
|
||||||
// Reset resets the stack trie object to empty state.
|
// Reset resets the stack trie object to empty state.
|
||||||
func (t *StackTrie) Reset() {
|
func (t *StackTrie) Reset() {
|
||||||
t.root = stPool.Get().(*stNode)
|
t.nodeAllocator.Reset(0)
|
||||||
|
t.byteAllocator.Reset(0)
|
||||||
|
t.root = t.nodeAllocator.Alloc().reset()
|
||||||
|
t.tmpNode = t.nodeAllocator.Alloc().reset()
|
||||||
t.last = nil
|
t.last = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -116,18 +170,17 @@ type stNode struct {
|
||||||
// newLeaf constructs a leaf node with provided node key and value. The key
|
// newLeaf constructs a leaf node with provided node key and value. The key
|
||||||
// will be deep-copied in the function and safe to modify afterwards, but
|
// will be deep-copied in the function and safe to modify afterwards, but
|
||||||
// value is not.
|
// value is not.
|
||||||
func newLeaf(key, val []byte) *stNode {
|
func (t *StackTrie) newLeaf(key, val []byte) *stNode {
|
||||||
st := stPool.Get().(*stNode)
|
st := t.nodeAllocator.Alloc().reset()
|
||||||
st.typ = leafNode
|
st.typ = leafNode
|
||||||
st.key = append(st.key, key...)
|
st.key = append(st.key, key...)
|
||||||
st.val = val
|
st.val = val
|
||||||
return st
|
return st
|
||||||
}
|
}
|
||||||
|
|
||||||
// newExt constructs an extension node with provided node key and child. The
|
// makeExt constructs an extension node with provided node key and child. The
|
||||||
// key will be deep-copied in the function and safe to modify afterwards.
|
// key will be deep-copied in the function and safe to modify afterwards.
|
||||||
func newExt(key []byte, child *stNode) *stNode {
|
func makeExt(st *stNode, key []byte, child *stNode) *stNode {
|
||||||
st := stPool.Get().(*stNode)
|
|
||||||
st.typ = extNode
|
st.typ = extNode
|
||||||
st.key = append(st.key, key...)
|
st.key = append(st.key, key...)
|
||||||
st.children[0] = child
|
st.children[0] = child
|
||||||
|
|
@ -144,12 +197,6 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
func (n *stNode) reset() *stNode {
|
func (n *stNode) reset() *stNode {
|
||||||
if n.typ == hashedNode {
|
|
||||||
// On hashnodes, we 'own' the val: it is guaranteed to be not held
|
|
||||||
// by external caller. Hence, when we arrive here, we can put it back
|
|
||||||
// into the pool
|
|
||||||
bPool.Put(n.val)
|
|
||||||
}
|
|
||||||
n.key = n.key[:0]
|
n.key = n.key[:0]
|
||||||
n.val = nil
|
n.val = nil
|
||||||
for i := range n.children {
|
for i := range n.children {
|
||||||
|
|
@ -194,7 +241,7 @@ func (t *StackTrie) insert(st *stNode, key, value []byte, path []byte) {
|
||||||
|
|
||||||
// Add new child
|
// Add new child
|
||||||
if st.children[idx] == nil {
|
if st.children[idx] == nil {
|
||||||
st.children[idx] = newLeaf(key[1:], value)
|
st.children[idx] = t.newLeaf(key[1:], value)
|
||||||
} else {
|
} else {
|
||||||
t.insert(st.children[idx], key[1:], value, append(path, key[0]))
|
t.insert(st.children[idx], key[1:], value, append(path, key[0]))
|
||||||
}
|
}
|
||||||
|
|
@ -223,8 +270,14 @@ func (t *StackTrie) insert(st *stNode, key, value []byte, path []byte) {
|
||||||
// Break on the non-last byte, insert an intermediate
|
// Break on the non-last byte, insert an intermediate
|
||||||
// extension. The path prefix of the newly-inserted
|
// extension. The path prefix of the newly-inserted
|
||||||
// extension should also contain the different byte.
|
// extension should also contain the different byte.
|
||||||
n = newExt(st.key[diffidx+1:], st.children[0])
|
e := makeExt(t.tmpNode.reset(), st.key[diffidx+1:], st.children[0]) // build a temporary extension node to hash
|
||||||
t.hash(n, append(path, st.key[:diffidx+1]...))
|
t.hash(e, append(path, st.key[:diffidx+1]...)) // frame belonging to st gets popped here
|
||||||
|
|
||||||
|
// allocate a new node to hold the hashed e
|
||||||
|
n = t.nodeAllocator.Alloc().reset()
|
||||||
|
n.typ = hashedNode
|
||||||
|
n.val = e.val
|
||||||
|
t.pushAllocationFrame() // for the "new" st that might end up being a branch or a ext
|
||||||
} else {
|
} else {
|
||||||
// Break on the last byte, no need to insert
|
// Break on the last byte, no need to insert
|
||||||
// an extension node: reuse the current node.
|
// an extension node: reuse the current node.
|
||||||
|
|
@ -245,12 +298,14 @@ func (t *StackTrie) insert(st *stNode, key, value []byte, path []byte) {
|
||||||
// the common prefix is at least one byte
|
// the common prefix is at least one byte
|
||||||
// long, insert a new intermediate branch
|
// long, insert a new intermediate branch
|
||||||
// node.
|
// node.
|
||||||
st.children[0] = stPool.Get().(*stNode)
|
st.children[0] = t.nodeAllocator.Alloc().reset()
|
||||||
st.children[0].typ = branchNode
|
st.children[0].typ = branchNode
|
||||||
|
t.pushAllocationFrame() // for the new branch child
|
||||||
p = st.children[0]
|
p = st.children[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create a leaf for the inserted part
|
// Create a leaf for the inserted part
|
||||||
o := newLeaf(key[diffidx+1:], value)
|
o := t.newLeaf(key[diffidx+1:], value)
|
||||||
|
|
||||||
// Insert both child leaves where they belong:
|
// Insert both child leaves where they belong:
|
||||||
origIdx := st.key[diffidx]
|
origIdx := st.key[diffidx]
|
||||||
|
|
@ -282,11 +337,14 @@ func (t *StackTrie) insert(st *stNode, key, value []byte, path []byte) {
|
||||||
st.typ = branchNode
|
st.typ = branchNode
|
||||||
p = st
|
p = st
|
||||||
st.children[0] = nil
|
st.children[0] = nil
|
||||||
|
t.pushAllocationFrame() // leafnode turning into a branch node
|
||||||
} else {
|
} else {
|
||||||
// Convert current node into an ext,
|
// Convert current node into an ext,
|
||||||
// and insert a child branch node.
|
// and insert a child branch node.
|
||||||
st.typ = extNode
|
st.typ = extNode
|
||||||
st.children[0] = stPool.Get().(*stNode)
|
t.pushAllocationFrame() // leafnode turning into a ext node
|
||||||
|
st.children[0] = t.nodeAllocator.Alloc().reset()
|
||||||
|
t.pushAllocationFrame() // new branch node
|
||||||
st.children[0].typ = branchNode
|
st.children[0].typ = branchNode
|
||||||
p = st.children[0]
|
p = st.children[0]
|
||||||
}
|
}
|
||||||
|
|
@ -295,11 +353,11 @@ func (t *StackTrie) insert(st *stNode, key, value []byte, path []byte) {
|
||||||
// value and another containing the new value. The child leaf
|
// value and another containing the new value. The child leaf
|
||||||
// is hashed directly in order to free up some memory.
|
// is hashed directly in order to free up some memory.
|
||||||
origIdx := st.key[diffidx]
|
origIdx := st.key[diffidx]
|
||||||
p.children[origIdx] = newLeaf(st.key[diffidx+1:], st.val)
|
p.children[origIdx] = t.newLeaf(st.key[diffidx+1:], st.val)
|
||||||
t.hash(p.children[origIdx], append(path, st.key[:diffidx+1]...))
|
t.hash(p.children[origIdx], append(path, st.key[:diffidx+1]...))
|
||||||
|
|
||||||
newIdx := key[diffidx]
|
newIdx := key[diffidx]
|
||||||
p.children[newIdx] = newLeaf(key[diffidx+1:], value)
|
p.children[newIdx] = t.newLeaf(key[diffidx+1:], value)
|
||||||
|
|
||||||
// Finally, cut off the key part that has been passed
|
// Finally, cut off the key part that has been passed
|
||||||
// over to the children.
|
// over to the children.
|
||||||
|
|
@ -359,9 +417,8 @@ func (t *StackTrie) hash(st *stNode, path []byte) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
st.children[i] = nil
|
st.children[i] = nil
|
||||||
stPool.Put(child.reset()) // Release child back to pool.
|
|
||||||
}
|
}
|
||||||
|
t.popAllocationFrame()
|
||||||
case extNode:
|
case extNode:
|
||||||
// recursively hash and commit child as the first step
|
// recursively hash and commit child as the first step
|
||||||
t.hash(st.children[0], append(path, st.key...))
|
t.hash(st.children[0], append(path, st.key...))
|
||||||
|
|
@ -373,10 +430,8 @@ func (t *StackTrie) hash(st *stNode, path []byte) {
|
||||||
}
|
}
|
||||||
n.encode(t.h.encbuf)
|
n.encode(t.h.encbuf)
|
||||||
blob = t.h.encodedBytes()
|
blob = t.h.encodedBytes()
|
||||||
|
|
||||||
stPool.Put(st.children[0].reset()) // Release child back to pool.
|
|
||||||
st.children[0] = nil
|
st.children[0] = nil
|
||||||
|
t.popAllocationFrame()
|
||||||
case leafNode:
|
case leafNode:
|
||||||
st.key = append(st.key, byte(16))
|
st.key = append(st.key, byte(16))
|
||||||
n := leafNodeEncoder{
|
n := leafNodeEncoder{
|
||||||
|
|
@ -398,13 +453,13 @@ func (t *StackTrie) hash(st *stNode, path []byte) {
|
||||||
// Skip committing the non-root node if the size is smaller than 32 bytes
|
// Skip committing the non-root node if the size is smaller than 32 bytes
|
||||||
// as tiny nodes are always embedded in their parent except root node.
|
// as tiny nodes are always embedded in their parent except root node.
|
||||||
if len(blob) < 32 && len(path) > 0 {
|
if len(blob) < 32 && len(path) > 0 {
|
||||||
st.val = bPool.GetWithSize(len(blob))
|
st.val = t.byteAllocator.Alloc()[:len(blob)]
|
||||||
copy(st.val, blob)
|
copy(st.val, blob)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// Write the hash to the 'val'. We allocate a new val here to not mutate
|
// Write the hash to the 'val'. We allocate a new val here to not mutate
|
||||||
// input values.
|
// input values.
|
||||||
st.val = bPool.GetWithSize(32)
|
st.val = t.byteAllocator.Alloc()[:32]
|
||||||
t.h.hashDataTo(st.val, blob)
|
t.h.hashDataTo(st.val, blob)
|
||||||
|
|
||||||
// Invoke the callback it's provided. Notably, the path and blob slices are
|
// Invoke the callback it's provided. Notably, the path and blob slices are
|
||||||
|
|
@ -422,5 +477,8 @@ func (t *StackTrie) hash(st *stNode, path []byte) {
|
||||||
func (t *StackTrie) Hash() common.Hash {
|
func (t *StackTrie) Hash() common.Hash {
|
||||||
n := t.root
|
n := t.root
|
||||||
t.hash(n, nil)
|
t.hash(n, nil)
|
||||||
return common.BytesToHash(n.val)
|
hash := common.BytesToHash(n.val)
|
||||||
|
t.byteAllocator.Release()
|
||||||
|
t.nodeAllocator.Release()
|
||||||
|
return hash
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,6 +26,7 @@ import (
|
||||||
"github.com/ethereum/go-ethereum/core/rawdb"
|
"github.com/ethereum/go-ethereum/core/rawdb"
|
||||||
"github.com/ethereum/go-ethereum/crypto"
|
"github.com/ethereum/go-ethereum/crypto"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestStackTrieInsertAndHash(t *testing.T) {
|
func TestStackTrieInsertAndHash(t *testing.T) {
|
||||||
|
|
@ -444,3 +445,75 @@ func TestInsert100K(t *testing.T) {
|
||||||
t.Fatalf("hash wrong, have %x want %x", have, want)
|
t.Fatalf("hash wrong, have %x want %x", have, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestAllocationFrame(t *testing.T) {
|
||||||
|
val := []byte{0}
|
||||||
|
|
||||||
|
t.Run("leaf turning into a branch", func(t *testing.T) {
|
||||||
|
s := NewStackTrie(nil)
|
||||||
|
require.NoError(t, s.Update([]byte{0x01, 0x00}, val))
|
||||||
|
assert.Equal(t, uint8(leafNode), s.root.typ)
|
||||||
|
assert.Equal(t, []byte{0, 0x1, 0, 0}, s.root.key)
|
||||||
|
require.NoError(t, s.Update([]byte{0x11, 0x00}, val))
|
||||||
|
assert.Equal(t, uint8(branchNode), s.root.typ)
|
||||||
|
assert.Len(t, s.allocationStackFrames, 1)
|
||||||
|
s.Hash()
|
||||||
|
assert.Len(t, s.allocationStackFrames, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("leaf turning into an extension node", func(t *testing.T) {
|
||||||
|
s := NewStackTrie(nil)
|
||||||
|
require.NoError(t, s.Update([]byte{0x01, 0x00}, val))
|
||||||
|
assert.Equal(t, uint8(leafNode), s.root.typ)
|
||||||
|
assert.Equal(t, []byte{0, 0x1, 0, 0}, s.root.key)
|
||||||
|
require.NoError(t, s.Update([]byte{0x01, 0x10}, val))
|
||||||
|
assert.Equal(t, uint8(extNode), s.root.typ)
|
||||||
|
assert.Len(t, s.allocationStackFrames, 2)
|
||||||
|
s.Hash()
|
||||||
|
assert.Len(t, s.allocationStackFrames, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("extension node creates an intermediate branch", func(t *testing.T) {
|
||||||
|
s := NewStackTrie(nil)
|
||||||
|
require.NoError(t, s.Update([]byte{0x01, 0x00}, val))
|
||||||
|
assert.Equal(t, uint8(leafNode), s.root.typ)
|
||||||
|
assert.Equal(t, []byte{0, 0x1, 0, 0}, s.root.key)
|
||||||
|
require.NoError(t, s.Update([]byte{0x01, 0x10}, val))
|
||||||
|
assert.Equal(t, uint8(extNode), s.root.typ)
|
||||||
|
assert.Len(t, s.allocationStackFrames, 2)
|
||||||
|
require.NoError(t, s.Update([]byte{0x02, 0x10}, val))
|
||||||
|
assert.Equal(t, uint8(extNode), s.root.typ)
|
||||||
|
assert.Len(t, s.allocationStackFrames, 2)
|
||||||
|
s.Hash()
|
||||||
|
assert.Len(t, s.allocationStackFrames, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("extension node creates an intermediate extension node", func(t *testing.T) {
|
||||||
|
s := NewStackTrie(nil)
|
||||||
|
require.NoError(t, s.Update([]byte{0x01, 0x00}, val))
|
||||||
|
assert.Equal(t, uint8(leafNode), s.root.typ)
|
||||||
|
assert.Equal(t, []byte{0, 0x1, 0, 0}, s.root.key)
|
||||||
|
require.NoError(t, s.Update([]byte{0x01, 0x10}, val))
|
||||||
|
assert.Equal(t, uint8(extNode), s.root.typ)
|
||||||
|
assert.Len(t, s.allocationStackFrames, 2)
|
||||||
|
require.NoError(t, s.Update([]byte{0x11, 0x10}, val))
|
||||||
|
assert.Equal(t, uint8(branchNode), s.root.typ)
|
||||||
|
assert.Len(t, s.allocationStackFrames, 1)
|
||||||
|
s.Hash()
|
||||||
|
assert.Len(t, s.allocationStackFrames, 0)
|
||||||
|
})
|
||||||
|
t.Run("extension node creates an intermediate extension node (2)", func(t *testing.T) {
|
||||||
|
s := NewStackTrie(nil)
|
||||||
|
require.NoError(t, s.Update([]byte{0x00, 0x10}, val))
|
||||||
|
assert.Equal(t, uint8(leafNode), s.root.typ)
|
||||||
|
assert.Equal(t, []byte{0, 0, 1, 0}, s.root.key)
|
||||||
|
require.NoError(t, s.Update([]byte{0x00, 0x11}, val))
|
||||||
|
assert.Equal(t, uint8(extNode), s.root.typ)
|
||||||
|
assert.Len(t, s.allocationStackFrames, 2)
|
||||||
|
require.NoError(t, s.Update([]byte{0x01, 0x11}, val))
|
||||||
|
assert.Equal(t, uint8(extNode), s.root.typ)
|
||||||
|
assert.Len(t, s.allocationStackFrames, 2)
|
||||||
|
s.Hash()
|
||||||
|
assert.Len(t, s.allocationStackFrames, 0)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue