trie: experimental parallel commit

trie: test which shows that the commit is erroneous
trie: make parallel committer correct
trie/trienode: better merge of set
fix error in bench
This commit is contained in:
Martin Holst Swende 2024-10-03 13:52:00 +02:00
parent 0c04b56228
commit 3609881707
No known key found for this signature in database
GPG key ID: 683B438C05A5DDF0
5 changed files with 139 additions and 69 deletions

View file

@ -44,31 +44,17 @@ func newCommitter(nodes *trienode.NodeSet, tracer *tracer, collectLeaf bool, par
} }
} }
type wrapNode struct {
node *trienode.Node
path string
leafHash common.Hash // optional, the parent hash of the related leaf
leafBlob []byte // optional, the blob of the related leaf
}
// Commit collapses a node down into a hash node. // Commit collapses a node down into a hash node.
func (c *committer) Commit(n node) hashNode { func (c *committer) Commit(n node) hashNode {
hn, wnodes := c.commit(nil, n, true) return c.commit(nil, n, true).(hashNode)
for _, wn := range wnodes {
c.nodes.AddNode(wn.path, wn.node)
if wn.leafHash != (common.Hash{}) {
c.nodes.AddLeaf(wn.leafHash, wn.leafBlob)
}
}
return hn.(hashNode)
} }
// commit collapses a node down into a hash node and returns it. // commit collapses a node down into a hash node and returns it.
func (c *committer) commit(path []byte, n node, topmost bool) (node, []*wrapNode) { func (c *committer) commit(path []byte, n node, topmost bool) node {
// if this path is clean, use available cached data // if this path is clean, use available cached data
hash, dirty := n.cache() hash, dirty := n.cache()
if hash != nil && !dirty { if hash != nil && !dirty {
return hash, nil return hash
} }
// Commit children, then parent, and remove the dirty flag. // Commit children, then parent, and remove the dirty flag.
switch cn := n.(type) { switch cn := n.(type) {
@ -78,36 +64,29 @@ func (c *committer) commit(path []byte, n node, topmost bool) (node, []*wrapNode
// If the child is fullNode, recursively commit, // If the child is fullNode, recursively commit,
// otherwise it can only be hashNode or valueNode. // otherwise it can only be hashNode or valueNode.
var nodes []*wrapNode
if _, ok := cn.Val.(*fullNode); ok { if _, ok := cn.Val.(*fullNode); ok {
collapsed.Val, nodes = c.commit(append(path, cn.Key...), cn.Val, false) collapsed.Val = c.commit(append(path, cn.Key...), cn.Val, false)
} }
// The key needs to be copied, since we're adding it to the // The key needs to be copied, since we're adding it to the
// modified nodeset. // modified nodeset.
collapsed.Key = hexToCompact(cn.Key) collapsed.Key = hexToCompact(cn.Key)
hashedNode, wNode := c.store(path, collapsed) hashedNode := c.store(path, collapsed)
if wNode != nil {
nodes = append(nodes, wNode)
}
if hn, ok := hashedNode.(hashNode); ok { if hn, ok := hashedNode.(hashNode); ok {
return hn, nodes return hn
} }
return collapsed, nodes return collapsed
case *fullNode: case *fullNode:
hashedKids, nodes := c.commitChildren(path, cn, topmost && c.parallel) hashedKids := c.commitChildren(path, cn, topmost && c.parallel)
collapsed := cn.copy() collapsed := cn.copy()
collapsed.Children = hashedKids collapsed.Children = hashedKids
hashedNode, wNode := c.store(path, collapsed) hashedNode := c.store(path, collapsed)
if wNode != nil {
nodes = append(nodes, wNode)
}
if hn, ok := hashedNode.(hashNode); ok { if hn, ok := hashedNode.(hashNode); ok {
return hn, nodes return hn
} }
return collapsed, nodes return collapsed
case hashNode: case hashNode:
return cn, nil return cn
default: default:
// nil, valuenode shouldn't be committed // nil, valuenode shouldn't be committed
panic(fmt.Sprintf("%T: invalid node: %v", n, n)) panic(fmt.Sprintf("%T: invalid node: %v", n, n))
@ -115,11 +94,10 @@ func (c *committer) commit(path []byte, n node, topmost bool) (node, []*wrapNode
} }
// commitChildren commits the children of the given fullnode // commitChildren commits the children of the given fullnode
func (c *committer) commitChildren(path []byte, n *fullNode, parallel bool) ([17]node, []*wrapNode) { func (c *committer) commitChildren(path []byte, n *fullNode, parallel bool) [17]node {
var ( var (
wg sync.WaitGroup wg sync.WaitGroup
children [17]node children [17]node
results [16][]*wrapNode
) )
for i := 0; i < 16; i++ { for i := 0; i < 16; i++ {
child := n.Children[i] child := n.Children[i]
@ -137,34 +115,34 @@ func (c *committer) commitChildren(path []byte, n *fullNode, parallel bool) ([17
// Note the returned node can be some embedded nodes, so it's // Note the returned node can be some embedded nodes, so it's
// possible the type is not hashNode. // possible the type is not hashNode.
if !parallel { if !parallel {
children[i], results[i] = c.commit(append(path, byte(i)), child, false) children[i] = c.commit(append(path, byte(i)), child, false)
} else { } else {
wg.Add(1) wg.Add(1)
go func(index int) { go func(index int) {
defer wg.Done() p := append(path, byte(i))
children[index], results[index] = c.commit(append(path, byte(index)), child, false) set := trienode.NewNodeSet(c.nodes.Owner)
childComitter := newCommitter(set, c.tracer, c.collectLeaf, false)
h := childComitter.commit(p, child, false)
children[index] = h
c.nodes.MergeSet(set)
wg.Done()
}(i) }(i)
} }
} }
if parallel { if parallel {
wg.Wait() wg.Wait()
} }
// For the 17th child, it's possible the type is valuenode. // For the 17th child, it's possible the type is valuenode.
if n.Children[16] != nil { if n.Children[16] != nil {
children[16] = n.Children[16] children[16] = n.Children[16]
} }
var wnodes []*wrapNode return children
for i := 0; i < 16; i++ {
if results[i] != nil {
wnodes = append(wnodes, results[i]...)
}
}
return children, wnodes
} }
// store hashes the node n and adds it to the modified nodeset. If leaf collection // store hashes the node n and adds it to the modified nodeset. If leaf collection
// is enabled, leaf nodes will be tracked in the modified nodeset as well. // is enabled, leaf nodes will be tracked in the modified nodeset as well.
func (c *committer) store(path []byte, n node) (node, *wrapNode) { func (c *committer) store(path []byte, n node) node {
// Larger nodes are replaced by their hash and stored in the database. // Larger nodes are replaced by their hash and stored in the database.
var hash, _ = n.cache() var hash, _ = n.cache()
@ -178,30 +156,24 @@ func (c *committer) store(path []byte, n node) (node, *wrapNode) {
// deleted only if the node was existent in database before. // deleted only if the node was existent in database before.
_, ok := c.tracer.accessList[string(path)] _, ok := c.tracer.accessList[string(path)]
if ok { if ok {
return n, &wrapNode{ c.nodes.AddNode(path, trienode.NewDeleted()) // TODO
path: string(path),
node: trienode.NewDeleted(),
} }
} return n
return n, nil
} }
nhash := common.BytesToHash(hash) nhash := common.BytesToHash(hash)
wNode := &wrapNode{ c.nodes.AddNode(path, trienode.New(nhash, nodeToBytes(n))) // TODO
path: string(path),
node: trienode.New(nhash, nodeToBytes(n)),
}
// 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
// length of leaves should be exactly same. // length of leaves should be exactly same.
if c.collectLeaf { if c.collectLeaf {
if sn, ok := n.(*shortNode); ok { if sn, ok := n.(*shortNode); ok {
if val, ok := sn.Val.(valueNode); ok { if val, ok := sn.Val.(valueNode); ok {
wNode.leafHash = nhash c.nodes.AddLeaf(nhash, val) // TODO
wNode.leafBlob = val
} }
} }
} }
return hash, wNode return hash
} }
// ForGatherChildren decodes the provided node and traverses the children inside. // ForGatherChildren decodes the provided node and traverses the children inside.

View file

@ -613,7 +613,7 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
} }
nodes := trienode.NewNodeSet(t.owner) nodes := trienode.NewNodeSet(t.owner)
for _, path := range paths { for _, path := range paths {
nodes.AddNode(path, trienode.NewDeleted()) nodes.AddNode([]byte(path), trienode.NewDeleted())
} }
return types.EmptyRootHash, nodes // case (b) return types.EmptyRootHash, nodes // case (b)
} }
@ -631,7 +631,7 @@ func (t *Trie) Commit(collectLeaf bool) (common.Hash, *trienode.NodeSet) {
} }
nodes := trienode.NewNodeSet(t.owner) nodes := trienode.NewNodeSet(t.owner)
for _, path := range t.tracer.deletedNodes() { for _, path := range t.tracer.deletedNodes() {
nodes.AddNode(path, trienode.NewDeleted()) nodes.AddNode([]byte(path), trienode.NewDeleted())
} }
// If the number of changes is below 100, we let one thread handle it // If the number of changes is below 100, we let one thread handle it
t.root = newCommitter(nodes, t.tracer, collectLeaf, t.mutate > 100).Commit(t.root) t.root = newCommitter(nodes, t.tracer, collectLeaf, t.mutate > 100).Commit(t.root)

View file

@ -40,6 +40,7 @@ import (
"github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/trienode"
"github.com/holiman/uint256" "github.com/holiman/uint256"
"golang.org/x/crypto/sha3" "golang.org/x/crypto/sha3"
"strings"
) )
func init() { func init() {
@ -1236,11 +1237,11 @@ func FuzzTrie(f *testing.F) {
// BenchmarkCommit/commit-5000nodes-parallel // BenchmarkCommit/commit-5000nodes-parallel
// BenchmarkCommit/commit-5000nodes-parallel-8 450 2725071 ns/op 6471357 B/op 74938 allocs/op // BenchmarkCommit/commit-5000nodes-parallel-8 450 2725071 ns/op 6471357 B/op 74938 allocs/op
func BenchmarkCommit(b *testing.B) { func BenchmarkCommit(b *testing.B) {
benchmarkCommit(b, 100) //benchmarkCommit(b, 100)
benchmarkCommit(b, 200) //benchmarkCommit(b, 200)
benchmarkCommit(b, 500) //benchmarkCommit(b, 500)
benchmarkCommit(b, 1000) //benchmarkCommit(b, 1000)
benchmarkCommit(b, 2000) //benchmarkCommit(b, 2000)
benchmarkCommit(b, 5000) benchmarkCommit(b, 5000)
} }
@ -1269,8 +1270,73 @@ func testCommit(b *testing.B, n int, parallel bool) {
} }
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < len(tries); i++ { for i := 0; i < len(tries); i++ {
tries[i].Commit(true) tries[i].Commit(true)
} }
} }
func TestCommitCorrect(t *testing.T) {
var paraTrie = NewEmpty(nil)
var refTrie = NewEmpty(nil)
for j := 0; j < 5000; j++ {
key := testrand.Bytes(32)
val := testrand.Bytes(32)
paraTrie.Update(key, val)
refTrie.Update(common.CopyBytes(key), common.CopyBytes(val))
}
paraTrie.Hash()
//paraTrie.mutate = 0
refTrie.Hash()
refTrie.mutate = 0
haveRoot, haveNodes := paraTrie.Commit(true)
wantRoot, wantNodes := refTrie.Commit(true)
if haveRoot != wantRoot {
t.Fatalf("have %x want %x", haveRoot, wantRoot)
}
have := printSet(haveNodes)
want := printSet(wantNodes)
if have != want {
i := 0
for i = 0; i < len(have); i++ {
if have[i] != want[i] {
break
}
}
if i > 100 {
i -= 100
}
t.Fatalf("have != want\nhave %q\nwant %q", have[i:], want[i:])
}
}
func printSet(set *trienode.NodeSet) string {
var out = new(strings.Builder)
fmt.Fprintf(out, "nodeset owner: %v\n", set.Owner)
var paths []string
for k, _ := range set.Nodes {
paths = append(paths, k)
}
sort.Strings(paths)
for _, path := range paths {
n := set.Nodes[path]
// Deletion
if n.IsDeleted() {
fmt.Fprintf(out, " [-]: %x\n", path)
continue
}
// Insertion or update
fmt.Fprintf(out, " [+/*]: %x -> %v \n", path, n.Hash)
}
sort.Slice(set.Leaves, func(i, j int) bool {
a := set.Leaves[i]
b := set.Leaves[j]
return bytes.Compare(a.Parent[:], b.Parent[:]) < 0
})
for _, n := range set.Leaves {
fmt.Fprintf(out, "[leaf]: %v\n", n)
}
return out.String()
}

View file

@ -22,6 +22,7 @@ import (
"strings" "strings"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
"sync"
) )
// Node is a wrapper which contains the encoded blob of the trie node and its // Node is a wrapper which contains the encoded blob of the trie node and its
@ -59,6 +60,8 @@ type leaf struct {
// NodeSet contains a set of nodes collected during the commit operation. // NodeSet contains a set of nodes collected during the commit operation.
// Each node is keyed by path. It's not thread-safe to use. // Each node is keyed by path. It's not thread-safe to use.
type NodeSet struct { type NodeSet struct {
mu sync.Mutex
Owner common.Hash Owner common.Hash
Leaves []*leaf Leaves []*leaf
Nodes map[string]*Node Nodes map[string]*Node
@ -90,13 +93,40 @@ func (set *NodeSet) ForEachWithOrder(callback func(path string, n *Node)) {
} }
// AddNode adds the provided node into set. // AddNode adds the provided node into set.
func (set *NodeSet) AddNode(path string, n *Node) { func (set *NodeSet) AddNode(path []byte, n *Node) {
if n.IsDeleted() { if n.IsDeleted() {
set.deletes += 1 set.deletes += 1
} else { } else {
set.updates += 1 set.updates += 1
} }
set.Nodes[path] = n set.Nodes[string(path)] = n
}
func (set *NodeSet) MergeSet(other *NodeSet) error {
if set.Owner != other.Owner {
return fmt.Errorf("nodesets belong to different owner are not mergeable %x-%x", set.Owner, other.Owner)
}
set.mu.Lock()
defer set.mu.Unlock()
for path, node := range other.Nodes {
prev, ok := set.Nodes[path]
if ok {
// overwrite happens, revoke the counter
if prev.IsDeleted() {
set.deletes -= 1
} else {
set.updates -= 1
}
}
if node.IsDeleted() {
set.deletes += 1
} else {
set.updates += 1
}
set.Nodes[path] = node
}
set.Leaves = append(set.Leaves, other.Leaves...)
return nil
} }
// Merge adds a set of nodes into the set. // Merge adds a set of nodes into the set.
@ -104,6 +134,8 @@ func (set *NodeSet) Merge(owner common.Hash, nodes map[string]*Node) error {
if set.Owner != owner { if set.Owner != owner {
return fmt.Errorf("nodesets belong to different owner are not mergeable %x-%x", set.Owner, owner) return fmt.Errorf("nodesets belong to different owner are not mergeable %x-%x", set.Owner, owner)
} }
set.mu.Lock()
defer set.mu.Unlock()
for path, node := range nodes { for path, node := range nodes {
prev, ok := set.Nodes[path] prev, ok := set.Nodes[path]
if ok { if ok {

View file

@ -258,7 +258,7 @@ func (t *VerkleTrie) Commit(_ bool) (common.Hash, *trienode.NodeSet) {
nodeset := trienode.NewNodeSet(common.Hash{}) nodeset := trienode.NewNodeSet(common.Hash{})
for _, node := range nodes { for _, node := range nodes {
// Hash parameter is not used in pathdb // Hash parameter is not used in pathdb
nodeset.AddNode(string(node.Path), trienode.New(common.Hash{}, node.SerializedBytes)) nodeset.AddNode(node.Path, trienode.New(common.Hash{}, node.SerializedBytes))
} }
// Serialize root commitment form // Serialize root commitment form
return t.Hash(), nodeset return t.Hash(), nodeset