diff --git a/trie/bytepool.go b/core/types/bytepool.go similarity index 81% rename from trie/bytepool.go rename to core/types/bytepool.go index 00b567ae1d..5bc594df78 100644 --- a/trie/bytepool.go +++ b/core/types/bytepool.go @@ -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 . -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 } diff --git a/core/types/hashing.go b/core/types/hashing.go index 224d7a87ea..1e17c2ad53 100644 --- a/core/types/hashing.go +++ b/core/types/hashing.go @@ -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() +} diff --git a/core/types/hashing_test.go b/core/types/hashing_test.go index a6949414f3..44fa97ccc0 100644 --- a/core/types/hashing_test.go +++ b/core/types/hashing_test.go @@ -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) { diff --git a/trie/stacktrie.go b/trie/stacktrie.go index a29a82e6ab..6d34293208 100644 --- a/trie/stacktrie.go +++ b/trie/stacktrie.go @@ -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.