trie, core: rough idea to improve derivesha

This commit is contained in:
Martin Holst Swende 2024-11-12 09:32:54 +01:00
parent aacb8e6f06
commit 0455e7a9c6
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
4 changed files with 115 additions and 22 deletions

View file

@ -14,26 +14,26 @@
// 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
package types
// bytesPool is a pool for byteslices. It is safe for concurrent use.
type bytesPool struct {
// BytesPool is a pool for byteslices. 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
// 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{
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 {
func (bp *BytesPool) Get() []byte {
select {
case b := <-bp.c:
return b
@ -44,7 +44,7 @@ func (bp *bytesPool) Get() []byte {
// 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) {
func (bp *BytesPool) Put(b []byte) {
if c := cap(b); c < bp.w || c > 3*bp.w {
return
}

View file

@ -84,6 +84,11 @@ type TrieHasher interface {
Hash() common.Hash
}
type RecyclingTrieHasher interface {
TrieHasher
Reclaim(*BytesPool)
}
// DerivableList is the input to DeriveSha.
// It is implemented by the 'Transactions' and 'Receipts' types.
// This is internal, do not use these methods.
@ -95,10 +100,7 @@ type DerivableList interface {
func encodeForDerive(list DerivableList, i int, buf *bytes.Buffer) []byte {
buf.Reset()
list.EncodeIndex(i, buf)
// It's really unfortunate that we need to perform this copy.
// StackTrie holds onto the values until Hash is called, so the values
// written to it must not alias.
return common.CopyBytes(buf.Bytes())
return buf.Bytes()
}
// DeriveSha creates the tree hashes of transactions, receipts, and withdrawals in a block header.
@ -118,17 +120,84 @@ func DeriveSha(list DerivableList, hasher TrieHasher) common.Hash {
for i := 1; i < list.Len() && i <= 0x7f; i++ {
indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i))
value := encodeForDerive(list, i, valueBuf)
// It's really unfortunate that we need to perform this copy.
// StackTrie holds onto the values until Hash is called, so the values
// written to it must not alias.
value = common.CopyBytes(value)
hasher.Update(indexBuf, value)
}
if list.Len() > 0 {
indexBuf = rlp.AppendUint64(indexBuf[:0], 0)
value := encodeForDerive(list, 0, valueBuf)
value = common.CopyBytes(value)
hasher.Update(indexBuf, value)
}
for i := 0x80; i < list.Len(); i++ {
indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i))
value := encodeForDerive(list, i, valueBuf)
value = common.CopyBytes(value)
hasher.Update(indexBuf, value)
}
return hasher.Hash()
}
func DeriveShaNG(list DerivableList, hasher RecyclingTrieHasher) common.Hash {
hasher.Reset()
valueBuf := encodeBufferPool.Get().(*bytes.Buffer)
defer encodeBufferPool.Put(valueBuf)
cap := 300
vPool := NewBytesPool(cap, 40)
hasher.Reclaim(vPool)
// StackTrie requires values to be inserted in increasing hash order, which is not the
// order that `list` provides hashes in. This insertion sequence ensures that the
// order is correct.
//
// The error returned by hasher is omitted because hasher will produce an incorrect
// hash in case any error occurs.
var indexBuf []byte
for i := 1; i < list.Len() && i <= 0x7f; i++ {
indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i))
value := encodeForDerive(list, i, valueBuf)
// It's really unfortunate that we need to perform this copy.
// StackTrie holds onto the values until Hash is called, so the values
// written to it must not alias
vBuf := vPool.Get()
if cap < len(value) {
vBuf = common.CopyBytes(value)
} else {
vBuf = vBuf[:len(value)]
copy(vBuf, value)
}
hasher.Update(indexBuf, vBuf)
}
if list.Len() > 0 {
indexBuf = rlp.AppendUint64(indexBuf[:0], 0)
value := encodeForDerive(list, 0, valueBuf)
vBuf := vPool.Get()
if cap < len(value) {
vBuf = common.CopyBytes(value)
} else {
vBuf = vBuf[:len(value)]
copy(vBuf, value)
}
hasher.Update(indexBuf, vBuf)
}
for i := 0x80; i < list.Len(); i++ {
indexBuf = rlp.AppendUint64(indexBuf[:0], uint64(i))
value := encodeForDerive(list, i, valueBuf)
vBuf := vPool.Get()
if cap < len(value) {
vBuf = common.CopyBytes(value)
} else {
vBuf = vBuf[:len(value)]
copy(vBuf, value)
}
hasher.Update(indexBuf, vBuf)
}
return hasher.Hash()
}

View file

@ -81,13 +81,18 @@ func BenchmarkDeriveSha200(b *testing.B) {
if err != nil {
b.Fatal(err)
}
var exp common.Hash
var got common.Hash
var exp = types.DeriveSha(txs, trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)))
var have common.Hash
b.Run("std_trie", func(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
exp = types.DeriveSha(txs, trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)))
have = types.DeriveSha(txs, trie.NewEmpty(triedb.NewDatabase(rawdb.NewMemoryDatabase(), nil)))
}
if have != exp {
b.Errorf("got %x exp %x", have, exp)
}
})
@ -95,12 +100,22 @@ func BenchmarkDeriveSha200(b *testing.B) {
b.ResetTimer()
b.ReportAllocs()
for i := 0; i < b.N; i++ {
got = types.DeriveSha(txs, trie.NewStackTrie(nil))
have = types.DeriveShaNG(txs, trie.NewStackTrie(nil))
}
if have != exp {
b.Errorf("got %x exp %x", have, exp)
}
})
if got != exp {
b.Errorf("got %x exp %x", got, exp)
}
//b.Run("stack_trie_reuse", func(b *testing.B) {
// b.ResetTimer()
// b.ReportAllocs()
// for i := 0; i < b.N; i++ {
// have = types.DeriveShaNG(txs, trie.NewStackTrie(nil))
// }
// if have != exp {
// b.Errorf("got %x exp %x", have, exp)
// }
//})
}
func TestFuzzDeriveSha(t *testing.T) {

View file

@ -27,7 +27,7 @@ import (
var (
stPool = sync.Pool{New: func() any { return new(stNode) }}
bPool = newBytesPool(32, 100)
bPool = types.NewBytesPool(32, 100)
_ = types.TrieHasher((*StackTrie)(nil))
)
@ -50,6 +50,7 @@ type StackTrie struct {
onTrieNode OnTrieNode
kBuf []byte // buf space used for hex-key during insertions
pBuf []byte // buf space used for path during insertions
vPool *types.BytesPool
}
// NewStackTrie allocates and initializes an empty trie. The committed nodes
@ -64,13 +65,17 @@ func NewStackTrie(onTrieNode OnTrieNode) *StackTrie {
}
}
func (st *StackTrie) Reclaim(vPool *types.BytesPool) {
st.vPool = vPool
}
// Update inserts a (key, value) pair into the stack trie.
func (t *StackTrie) Update(key, value []byte) error {
if len(value) == 0 {
return errors.New("trying to insert empty (deletion)")
}
var k []byte
{ // Need to expand the 'key' into hex-form. We use the dedicated buf for that.
{ // Need to expand the 'key' into hex-form. We use the dedicated buf for that.
if cap(t.kBuf) < 2*len(key) { // realloc to ensure sufficient cap
t.kBuf = make([]byte, 2*len(key), 2*len(key))
}
@ -392,7 +397,11 @@ func (t *StackTrie) hash(st *stNode, path []byte) {
st.typ = hashedNode
st.key = st.key[:0]
st.val = nil // Release reference to potentially externally held slice.
// Release reference to (potentially externally held) value-slice.
if len(st.val) > 0 && t.vPool != nil {
t.vPool.Put(st.val)
}
st.val = nil
// 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.