trie: optimize node encoding using in-place RLP encoder.

This commit is contained in:
qianbin 2024-01-01 23:29:42 +08:00
parent c053eb71b6
commit 17d0291609
10 changed files with 59 additions and 76 deletions

1
go.mod
View file

@ -53,6 +53,7 @@ require (
github.com/olekukonko/tablewriter v0.0.5 github.com/olekukonko/tablewriter v0.0.5
github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7 github.com/peterh/liner v1.1.1-0.20190123174540-a2c9a5303de7
github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7 github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7
github.com/qianbin/drlp v0.0.0-20231230065804-db474f66bf91
github.com/rs/cors v1.7.0 github.com/rs/cors v1.7.0
github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible github.com/shirou/gopsutil v3.21.4-0.20210419000835-c7a38de76ee5+incompatible
github.com/status-im/keycard-go v0.2.0 github.com/status-im/keycard-go v0.2.0

2
go.sum
View file

@ -522,6 +522,8 @@ github.com/prometheus/procfs v0.7.3 h1:4jVXhlkAyzOScmCkXBTOLRLTz8EeU+eyjrwB/EPq0
github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA=
github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7 h1:cZC+usqsYgHtlBaGulVnZ1hfKAi8iWtujBnRLQE698c= github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7 h1:cZC+usqsYgHtlBaGulVnZ1hfKAi8iWtujBnRLQE698c=
github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7/go.mod h1:IToEjHuttnUzwZI5KBSM/LOOW3qLbbrHOEfp3SbECGY= github.com/protolambda/bls12-381-util v0.0.0-20220416220906-d8552aa452c7/go.mod h1:IToEjHuttnUzwZI5KBSM/LOOW3qLbbrHOEfp3SbECGY=
github.com/qianbin/drlp v0.0.0-20231230065804-db474f66bf91 h1:N1pcRULtPB7CndtexGDr94ai8wsJLx55AKYfo7F6qEs=
github.com/qianbin/drlp v0.0.0-20231230065804-db474f66bf91/go.mod h1:OnClEjurpFUtR3RUCauP9HxNNl8xjfGAOv0kWYTznOc=
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4=

View file

@ -139,7 +139,7 @@ func (c *committer) store(path []byte, n node) node {
} }
// Collect the dirty node to nodeset for return. // Collect the dirty node to nodeset for return.
nhash := common.BytesToHash(hash) nhash := common.BytesToHash(hash)
c.nodes.AddNode(path, trienode.New(nhash, nodeToBytes(n))) c.nodes.AddNode(path, trienode.New(nhash, n.encode(nil)))
// Collect the corresponding leaf node if it's required. We don't check // Collect the corresponding leaf node if it's required. We don't check
// full node since it's impossible to store value in fullNode. The key // full node since it's impossible to store value in fullNode. The key

View file

@ -20,7 +20,6 @@ import (
"sync" "sync"
"github.com/ethereum/go-ethereum/crypto" "github.com/ethereum/go-ethereum/crypto"
"github.com/ethereum/go-ethereum/rlp"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
) )
@ -29,7 +28,6 @@ import (
type hasher struct { type hasher struct {
sha crypto.KeccakState sha crypto.KeccakState
tmp []byte tmp []byte
encbuf rlp.EncoderBuffer
parallel bool // Whether to use parallel threads when hashing parallel bool // Whether to use parallel threads when hashing
} }
@ -39,7 +37,6 @@ var hasherPool = sync.Pool{
return &hasher{ return &hasher{
tmp: make([]byte, 0, 550), // cap is as large as a full fullNode. tmp: make([]byte, 0, 550), // cap is as large as a full fullNode.
sha: sha3.NewLegacyKeccak256().(crypto.KeccakState), sha: sha3.NewLegacyKeccak256().(crypto.KeccakState),
encbuf: rlp.NewEncoderBuffer(nil),
} }
}, },
} }
@ -143,41 +140,23 @@ func (h *hasher) hashFullNodeChildren(n *fullNode) (collapsed *fullNode, cached
// into compact form for RLP encoding. // into compact form for RLP encoding.
// If the rlp data is smaller than 32 bytes, `nil` is returned. // If the rlp data is smaller than 32 bytes, `nil` is returned.
func (h *hasher) shortnodeToHash(n *shortNode, force bool) node { func (h *hasher) shortnodeToHash(n *shortNode, force bool) node {
n.encode(h.encbuf) h.tmp = n.encode(h.tmp[:0])
enc := h.encodedBytes()
if len(enc) < 32 && !force { if len(h.tmp) < 32 && !force {
return n // Nodes smaller than 32 bytes are stored inside their parent return n // Nodes smaller than 32 bytes are stored inside their parent
} }
return h.hashData(enc) return h.hashData(h.tmp)
} }
// fullnodeToHash is used to create a hashNode from a fullNode, (which // fullnodeToHash is used to create a hashNode from a fullNode, (which
// may contain nil values) // may contain nil values)
func (h *hasher) fullnodeToHash(n *fullNode, force bool) node { func (h *hasher) fullnodeToHash(n *fullNode, force bool) node {
n.encode(h.encbuf) h.tmp = n.encode(h.tmp[:0])
enc := h.encodedBytes()
if len(enc) < 32 && !force { if len(h.tmp) < 32 && !force {
return n // Nodes smaller than 32 bytes are stored inside their parent return n // Nodes smaller than 32 bytes are stored inside their parent
} }
return h.hashData(enc) return h.hashData(h.tmp)
}
// encodedBytes returns the result of the last encoding operation on h.encbuf.
// This also resets the encoder buffer.
//
// All node encoding must be done like this:
//
// node.encode(h.encbuf)
// enc := h.encodedBytes()
//
// This convention exists because node.encode can only be inlined/escape-analyzed when
// called on a concrete receiver type.
func (h *hasher) encodedBytes() []byte {
h.tmp = h.encbuf.AppendToBytes(h.tmp[:0])
h.encbuf.Reset(nil)
return h.tmp
} }
// hashData hashes the provided data // hashData hashes the provided data

View file

@ -242,7 +242,7 @@ func (it *nodeIterator) LeafProof() [][]byte {
// Gather nodes that end up as hash nodes (or the root) // Gather nodes that end up as hash nodes (or the root)
node, hashed := hasher.proofHash(item.node) node, hashed := hasher.proofHash(item.node)
if _, ok := hashed.(hashNode); ok || i == 0 { if _, ok := hashed.(hashNode); ok || i == 0 {
proofs = append(proofs, nodeToBytes(node)) proofs = append(proofs, node.encode(nil))
} }
} }
return proofs return proofs

View file

@ -29,7 +29,8 @@ var indices = []string{"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b
type node interface { type node interface {
cache() (hashNode, bool) cache() (hashNode, bool)
encode(w rlp.EncoderBuffer) // encode appends encoded bytes to buf and returns then extended buffer.
encode(buf []byte) []byte
fstring(string) string fstring(string) string
} }
@ -53,9 +54,11 @@ var nilValueNode = valueNode(nil)
// EncodeRLP encodes a full node into the consensus RLP format. // EncodeRLP encodes a full node into the consensus RLP format.
func (n *fullNode) EncodeRLP(w io.Writer) error { func (n *fullNode) EncodeRLP(w io.Writer) error {
eb := rlp.NewEncoderBuffer(w) h := newHasher(false)
n.encode(eb) defer returnHasherToPool(h)
return eb.Flush() h.tmp = n.encode(h.tmp[:0])
_, err := w.Write(h.tmp)
return err
} }
func (n *fullNode) copy() *fullNode { copy := *n; return &copy } func (n *fullNode) copy() *fullNode { copy := *n; return &copy }

View file

@ -17,48 +17,46 @@
package trie package trie
import ( import (
"github.com/ethereum/go-ethereum/rlp" "github.com/qianbin/drlp"
) )
func nodeToBytes(n node) []byte { func (n *fullNode) encode(buf []byte) []byte {
w := rlp.NewEncoderBuffer(nil) if buf == nil {
n.encode(w) buf = make([]byte, 0, 550)
result := w.ToBytes() }
w.Flush() offset := len(buf)
return result
}
func (n *fullNode) encode(w rlp.EncoderBuffer) {
offset := w.List()
for _, c := range n.Children { for _, c := range n.Children {
if c != nil { if c != nil {
c.encode(w) buf = c.encode(buf)
} else { } else {
w.Write(rlp.EmptyString) buf = drlp.AppendUint(buf, 0)
} }
} }
w.ListEnd(offset) return drlp.EndList(buf, offset)
} }
func (n *shortNode) encode(w rlp.EncoderBuffer) { func (n *shortNode) encode(buf []byte) []byte {
offset := w.List() if buf == nil {
w.WriteBytes(n.Key) buf = make([]byte, 0, len(n.Key)+40)
}
offset := len(buf)
buf = drlp.AppendString(buf, n.Key)
if n.Val != nil { if n.Val != nil {
n.Val.encode(w) buf = n.Val.encode(buf)
} else { } else {
w.Write(rlp.EmptyString) buf = drlp.AppendUint(buf, 0)
} }
w.ListEnd(offset) return drlp.EndList(buf, offset)
} }
func (n hashNode) encode(w rlp.EncoderBuffer) { func (n hashNode) encode(buf []byte) []byte {
w.WriteBytes(n) return drlp.AppendString(buf, n)
} }
func (n valueNode) encode(w rlp.EncoderBuffer) { func (n valueNode) encode(buf []byte) []byte {
w.WriteBytes(n) return drlp.AppendString(buf, n)
} }
func (n rawNode) encode(w rlp.EncoderBuffer) { func (n rawNode) encode(buf []byte) []byte {
w.Write(n) return append(buf, n...)
} }

View file

@ -108,7 +108,7 @@ func BenchmarkEncodeShortNode(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
nodeToBytes(node) node.encode(nil)
} }
} }
@ -126,7 +126,7 @@ func BenchmarkEncodeFullNode(b *testing.B) {
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
nodeToBytes(node) node.encode(nil)
} }
} }
@ -140,7 +140,7 @@ func BenchmarkDecodeShortNode(b *testing.B) {
Key: []byte{0x1, 0x2}, Key: []byte{0x1, 0x2},
Val: hashNode(randBytes(32)), Val: hashNode(randBytes(32)),
} }
blob := nodeToBytes(node) blob := node.encode(nil)
hash := crypto.Keccak256(blob) hash := crypto.Keccak256(blob)
b.ResetTimer() b.ResetTimer()
@ -161,7 +161,7 @@ func BenchmarkDecodeShortNodeUnsafe(b *testing.B) {
Key: []byte{0x1, 0x2}, Key: []byte{0x1, 0x2},
Val: hashNode(randBytes(32)), Val: hashNode(randBytes(32)),
} }
blob := nodeToBytes(node) blob := node.encode(nil)
hash := crypto.Keccak256(blob) hash := crypto.Keccak256(blob)
b.ResetTimer() b.ResetTimer()
@ -182,7 +182,7 @@ func BenchmarkDecodeFullNode(b *testing.B) {
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
node.Children[i] = hashNode(randBytes(32)) node.Children[i] = hashNode(randBytes(32))
} }
blob := nodeToBytes(node) blob := node.encode(nil)
hash := crypto.Keccak256(blob) hash := crypto.Keccak256(blob)
b.ResetTimer() b.ResetTimer()
@ -203,7 +203,7 @@ func BenchmarkDecodeFullNodeUnsafe(b *testing.B) {
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
node.Children[i] = hashNode(randBytes(32)) node.Children[i] = hashNode(randBytes(32))
} }
blob := nodeToBytes(node) blob := node.encode(nil)
hash := crypto.Keccak256(blob) hash := crypto.Keccak256(blob)
b.ResetTimer() b.ResetTimer()

View file

@ -90,7 +90,7 @@ func (t *Trie) Prove(key []byte, proofDb ethdb.KeyValueWriter) error {
if hash, ok := hn.(hashNode); ok || i == 0 { if hash, ok := hn.(hashNode); ok || i == 0 {
// If the node's database encoding is a hash (or is the // If the node's database encoding is a hash (or is the
// root node), it becomes a proof element. // root node), it becomes a proof element.
enc := nodeToBytes(n) enc := n.encode(nil)
if !ok { if !ok {
hash = hasher.hashData(enc) hash = hasher.hashData(enc)
} }

View file

@ -377,8 +377,8 @@ func (t *StackTrie) hash(st *stNode, path []byte) {
st.children[i] = nil st.children[i] = nil
stPool.Put(child.reset()) // Release child back to pool. stPool.Put(child.reset()) // Release child back to pool.
} }
nodes.encode(t.h.encbuf) t.h.tmp = nodes.encode(t.h.tmp[:0])
blob = t.h.encodedBytes() blob = t.h.tmp
case extNode: case extNode:
// recursively hash and commit child as the first step // recursively hash and commit child as the first step
@ -400,8 +400,8 @@ func (t *StackTrie) hash(st *stNode, path []byte) {
} else { } else {
n.Val = hashNode(st.children[0].val) n.Val = hashNode(st.children[0].val)
} }
n.encode(t.h.encbuf) t.h.tmp = n.encode(t.h.tmp[:0])
blob = t.h.encodedBytes() blob = t.h.tmp
stPool.Put(st.children[0].reset()) // Release child back to pool. stPool.Put(st.children[0].reset()) // Release child back to pool.
st.children[0] = nil st.children[0] = nil
@ -410,8 +410,8 @@ func (t *StackTrie) hash(st *stNode, path []byte) {
st.key = append(st.key, byte(16)) st.key = append(st.key, byte(16))
n := shortNode{Key: hexToCompactInPlace(st.key), Val: valueNode(st.val)} n := shortNode{Key: hexToCompactInPlace(st.key), Val: valueNode(st.val)}
n.encode(t.h.encbuf) t.h.tmp = n.encode(t.h.tmp[:0])
blob = t.h.encodedBytes() blob = t.h.tmp
default: default:
panic("invalid node type") panic("invalid node type")