Signed-off-by: jsvisa <delweng@gmail.com>
This commit is contained in:
jsvisa 2025-07-07 11:27:36 +00:00
parent f5f0129f85
commit 0281e226bf
3 changed files with 47 additions and 145 deletions

View file

@ -339,6 +339,8 @@ func (tree *layerTree) lookupStorage(accountHash common.Hash, slotHash common.Ha
return l, nil return l, nil
} }
// lookupNode returns the layer that is guaranteed to contain the trie node
// data corresponding to the specified state root being queried.
func (tree *layerTree) lookupNode(accountHash common.Hash, path string, state common.Hash) (layer, error) { func (tree *layerTree) lookupNode(accountHash common.Hash, path string, state common.Hash) (layer, error) {
// Hold the read lock to prevent the unexpected layer changes // Hold the read lock to prevent the unexpected layer changes
tree.lock.RLock() tree.lock.RLock()

View file

@ -27,6 +27,7 @@ import (
"golang.org/x/sync/errgroup" "golang.org/x/sync/errgroup"
) )
// storageNodesShardCount is the number of shards used for storage nodes.
const storageNodesShardCount = 16 const storageNodesShardCount = 16
// storageKey returns a key for uniquely identifying the storage slot. // storageKey returns a key for uniquely identifying the storage slot.
@ -52,10 +53,17 @@ type lookup struct {
// where the slot was modified, with the order from oldest to newest. // where the slot was modified, with the order from oldest to newest.
storages map[[64]byte][]common.Hash storages map[[64]byte][]common.Hash
// accountNodes represents the mutation history for specific account
// trie nodes. The key is the trie path of the node, and the value is a slice
// of **diff layer** IDs indicating where the account was modified,
// with the order from oldest to newest.
accountNodes map[string][]common.Hash accountNodes map[string][]common.Hash
// Key is accountHash.Hex() + path, distributed across 16 shards // storageNodes represents the mutation history for specific storage
// This eliminates the need for two-level map lookups // slot trie nodes, distributed across 16 shards for efficiency.
// The key is the account address hash and the trie path of the node,
// the value is a slice of **diff layer** IDs indicating where the
// slot was modified, with the order from oldest to newest.
storageNodes [storageNodesShardCount]map[string][]common.Hash storageNodes [storageNodesShardCount]map[string][]common.Hash
// descendant is the callback indicating whether the layer with // descendant is the callback indicating whether the layer with
@ -186,6 +194,17 @@ func (l *lookup) storageTip(accountHash common.Hash, slotHash common.Hash, state
return common.Hash{} return common.Hash{}
} }
// nodeTip traverses the layer list associated with the given account and path
// in reverse order to locate the first entry that either matches
// the specified stateID or is a descendant of it.
//
// If found, the trie node data corresponding to the supplied stateID resides
// in that layer. Otherwise, two scenarios are possible:
//
// (a) the trie node remains unmodified from the current disk layer up to
// the state layer specified by the stateID: fallback to the disk layer for
// data retrieval, (b) or the layer specified by the stateID is stale: reject
// the data retrieval.
func (l *lookup) nodeTip(accountHash common.Hash, path string, stateID common.Hash, base common.Hash) common.Hash { func (l *lookup) nodeTip(accountHash common.Hash, path string, stateID common.Hash, base common.Hash) common.Hash {
var list []common.Hash var list []common.Hash
if accountHash == (common.Hash{}) { if accountHash == (common.Hash{}) {
@ -448,17 +467,16 @@ func (l *lookup) removeStorageNodes(state common.Hash, nodes map[common.Hash]map
for accountHash, slots := range nodes { for accountHash, slots := range nodes {
accountHex := accountHash.Hex() accountHex := accountHash.Hex()
for path := range slots { for path := range slots {
// Construct the combined key and find the correct shard shard := l.storageNodes[getStorageShardIndex(path)]
key := accountHex + path key := accountHex + path
shardIndex := getStorageShardIndex(path) found, list := removeFromList(shard[key], state)
found, list := removeFromList(l.storageNodes[shardIndex][key], state)
if !found { if !found {
return fmt.Errorf("storage lookup is not found, %x %x, state: %x", accountHash, path, state) return fmt.Errorf("storage lookup is not found, %x %x, state: %x", accountHash, path, state)
} }
if len(list) != 0 { if len(list) != 0 {
l.storageNodes[shardIndex][key] = list shard[key] = list
} else { } else {
delete(l.storageNodes[shardIndex], key) delete(shard, key)
} }
} }
} }

View file

@ -2,7 +2,6 @@ package pathdb
import ( import (
"crypto/rand" "crypto/rand"
"fmt"
"testing" "testing"
"github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/common"
@ -43,173 +42,56 @@ func generateRandomStorageNodes(accountCount, nodesPerAccount int) map[common.Ha
return storageNodes return storageNodes
} }
// addNodes is a helper method for testing that adds nodes to the lookup structure
func (l *lookup) addNodes(stateHash common.Hash, accountNodes map[string]*trienode.Node, storageNodes map[common.Hash]map[string]*trienode.Node) {
// Add account nodes
for path := range accountNodes {
list, exists := l.accountNodes[path]
if !exists {
list = make([]common.Hash, 0, 16)
}
list = append(list, stateHash)
l.accountNodes[path] = list
}
// Add storage nodes using single-level sharded structure
for accountHash, slots := range storageNodes {
accountHex := accountHash.Hex()
for path := range slots {
// Construct the combined key but use only path for shard calculation
key := accountHex + path
shardIndex := getStorageShardIndex(path) // Use only path for sharding
shardMap := l.storageNodes[shardIndex]
list, exists := shardMap[key]
if !exists {
list = make([]common.Hash, 0, 16)
}
list = append(list, stateHash)
shardMap[key] = list
}
}
}
func BenchmarkAddNodes(b *testing.B) { func BenchmarkAddNodes(b *testing.B) {
tests := []struct { tests := []struct {
name string name string
accountNodeCount int accountNodeCount int
storageAccountCount int
nodesPerAccount int nodesPerAccount int
}{ }{
{ {
name: "Small-100-accounts-10-nodes", name: "Small-100-accounts-10-nodes",
accountNodeCount: 100, accountNodeCount: 100,
storageAccountCount: 100,
nodesPerAccount: 10, nodesPerAccount: 10,
}, },
{ {
name: "Medium-500-accounts-20-nodes", name: "Medium-500-accounts-20-nodes",
accountNodeCount: 500, accountNodeCount: 500,
storageAccountCount: 500,
nodesPerAccount: 20, nodesPerAccount: 20,
}, },
{ {
name: "Large-2000-accounts-40-nodes", name: "Large-2000-accounts-40-nodes",
accountNodeCount: 2000, accountNodeCount: 2000,
storageAccountCount: 2000,
nodesPerAccount: 40, nodesPerAccount: 40,
}, },
} }
for _, tc := range tests { for _, tc := range tests {
b.Run(tc.name, func(b *testing.B) { b.Run(tc.name, func(b *testing.B) {
accountNodes := generateRandomAccountNodes(tc.accountNodeCount) storageNodes := generateRandomStorageNodes(tc.accountNodeCount, tc.nodesPerAccount)
storageNodes := generateRandomStorageNodes(tc.storageAccountCount, tc.nodesPerAccount)
lookup := &lookup{ lookup := &lookup{
accountNodes: make(map[string][]common.Hash), accountNodes: make(map[string][]common.Hash),
} }
// Initialize all 16 storage node shards // Initialize all 16 storage node shards
for i := 0; i < 16; i++ { for i := 0; i < storageNodesShardCount; i++ {
lookup.storageNodes[i] = make(map[string][]common.Hash) lookup.storageNodes[i] = make(map[string][]common.Hash)
} }
var stateHash common.Hash var state common.Hash
rand.Read(stateHash[:]) rand.Read(state[:])
b.ResetTimer() b.ResetTimer()
b.ReportAllocs() b.ReportAllocs()
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
// Clear the nodes maps for each iteration // Reset the lookup instance for each benchmark iteration
lookup.accountNodes = make(map[string][]common.Hash) for j := 0; j < storageNodesShardCount; j++ {
// Reinitialize all 16 storage node shards
for j := 0; j < 16; j++ {
lookup.storageNodes[j] = make(map[string][]common.Hash) lookup.storageNodes[j] = make(map[string][]common.Hash)
} }
lookup.addNodes(stateHash, accountNodes, storageNodes) lookup.addStorageNodes(state, storageNodes)
} }
}) })
} }
} }
func TestConcurrentStorageNodesUpdate(b *testing.T) {
// Create a lookup instance
lookup := &lookup{
accountNodes: make(map[string][]common.Hash),
}
// Initialize all storage node shards
for i := 0; i < storageNodesShardCount; i++ {
lookup.storageNodes[i] = make(map[string][]common.Hash)
}
// Create test data with known shard distribution
testData := map[common.Hash]map[string]*trienode.Node{}
// Create accounts that will distribute across different shards
for i := 0; i < 100; i++ {
var accountHash common.Hash
accountHash[0] = byte(i)
testData[accountHash] = make(map[string]*trienode.Node)
// Create paths that will hash to different shards
for j := 0; j < 10; j++ {
path := fmt.Sprintf("path_%d_%d", i, j)
var nodeHash common.Hash
nodeHash[0] = byte(j)
testData[accountHash][path] = &trienode.Node{Hash: nodeHash}
}
}
// Add nodes using the concurrent method
var stateHash common.Hash
stateHash[0] = 0x42
lookup.addNodes(stateHash, nil, testData)
// Verify that all nodes were added correctly
totalNodes := 0
for accountHash, slots := range testData {
accountHex := accountHash.Hex()
for path := range slots {
key := accountHex + path
shardIndex := getStorageShardIndex(path)
list, exists := lookup.storageNodes[shardIndex][key]
if !exists {
b.Errorf("Node not found: account=%x, path=%s, shard=%d", accountHash, path, shardIndex)
continue
}
if len(list) != 1 {
b.Errorf("Expected 1 state hash, got %d: account=%x, path=%s", len(list), accountHash, path)
continue
}
if list[0] != stateHash {
b.Errorf("Expected state hash %x, got %x: account=%x, path=%s", stateHash, list[0], accountHash, path)
continue
}
totalNodes++
}
}
expectedTotal := 100 * 10 // 100 accounts * 10 nodes each
if totalNodes != expectedTotal {
b.Errorf("Expected %d total nodes, got %d", expectedTotal, totalNodes)
}
// Verify shard distribution
for i := 0; i < storageNodesShardCount; i++ {
shardSize := len(lookup.storageNodes[i])
if shardSize == 0 {
b.Logf("Shard %d is empty", i)
} else {
b.Logf("Shard %d has %d entries", i, shardSize)
}
}
}