trie: introduce UpdateBatch

This commit is contained in:
Gary Rong 2025-08-15 11:28:57 +08:00
parent 2dbb580f51
commit cd191ed55a
6 changed files with 232 additions and 6 deletions

View file

@ -91,12 +91,18 @@ type Trie interface {
// in the trie with provided address. // in the trie with provided address.
UpdateAccount(address common.Address, account *types.StateAccount, codeLen int) error UpdateAccount(address common.Address, account *types.StateAccount, codeLen int) error
// UpdateAccountBatch attempts to update a list accounts in the batch manner.
UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, _ []int) error
// UpdateStorage associates key with value in the trie. If value has length zero, // UpdateStorage associates key with value in the trie. If value has length zero,
// any existing value is deleted from the trie. The value bytes must not be modified // any existing value is deleted from the trie. The value bytes must not be modified
// by the caller while they are stored in the trie. If a node was not found in the // by the caller while they are stored in the trie. If a node was not found in the
// database, a trie.MissingNodeError is returned. // database, a trie.MissingNodeError is returned.
UpdateStorage(addr common.Address, key, value []byte) error UpdateStorage(addr common.Address, key, value []byte) error
// UpdateStorageBatch attempts to update a list storages in the batch manner.
UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error
// DeleteAccount abstracts an account deletion from the trie. // DeleteAccount abstracts an account deletion from the trie.
DeleteAccount(address common.Address) error DeleteAccount(address common.Address) error

View file

@ -190,6 +190,29 @@ func (t *StateTrie) UpdateStorage(_ common.Address, key, value []byte) error {
return nil return nil
} }
// UpdateStorageBatch attempts to update a list storages in the batch manner.
func (t *StateTrie) UpdateStorageBatch(_ common.Address, keys [][]byte, values [][]byte) error {
var (
hkeys = make([][]byte, 0, len(keys))
evals = make([][]byte, 0, len(values))
)
for _, key := range keys {
hk := crypto.Keccak256(key)
if t.preimages != nil {
t.secKeyCache[common.Hash(hk)] = key
}
hkeys = append(hkeys, hk)
}
for _, val := range values {
data, err := rlp.EncodeToBytes(val)
if err != nil {
return err
}
evals = append(evals, data)
}
return t.trie.UpdateBatch(hkeys, evals)
}
// UpdateAccount will abstract the write of an account to the secure trie. // UpdateAccount will abstract the write of an account to the secure trie.
func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccount, _ int) error { func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccount, _ int) error {
hk := crypto.Keccak256(address.Bytes()) hk := crypto.Keccak256(address.Bytes())
@ -206,6 +229,29 @@ func (t *StateTrie) UpdateAccount(address common.Address, acc *types.StateAccoun
return nil return nil
} }
// UpdateAccountBatch attempts to update a list accounts in the batch manner.
func (t *StateTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, _ []int) error {
var (
hkeys = make([][]byte, 0, len(addresses))
values = make([][]byte, 0, len(accounts))
)
for _, addr := range addresses {
hk := crypto.Keccak256(addr.Bytes())
if t.preimages != nil {
t.secKeyCache[common.Hash(hk)] = addr.Bytes()
}
hkeys = append(hkeys, hk)
}
for _, acc := range accounts {
data, err := rlp.EncodeToBytes(acc)
if err != nil {
return err
}
values = append(values, data)
}
return t.trie.UpdateBatch(hkeys, values)
}
func (t *StateTrie) UpdateContractCode(_ common.Address, _ common.Hash, _ []byte) error { func (t *StateTrie) UpdateContractCode(_ common.Address, _ common.Hash, _ []byte) error {
return nil return nil
} }

View file

@ -19,6 +19,7 @@ package trie
import ( import (
"maps" "maps"
"slices" "slices"
"sync"
) )
// opTracer tracks the changes of trie nodes. During the trie operations, // opTracer tracks the changes of trie nodes. During the trie operations,
@ -33,12 +34,10 @@ import (
// while the latter is inserted/deleted in order to follow the rule of trie. // while the latter is inserted/deleted in order to follow the rule of trie.
// This tool can track all of them no matter the node is embedded in its // This tool can track all of them no matter the node is embedded in its
// parent or not, but valueNode is never tracked. // parent or not, but valueNode is never tracked.
//
// Note opTracer is not thread-safe, callers should be responsible for handling
// the concurrency issues by themselves.
type opTracer struct { type opTracer struct {
inserts map[string]struct{} inserts map[string]struct{}
deletes map[string]struct{} deletes map[string]struct{}
lock sync.RWMutex
} }
// newOpTracer initializes the tracer for capturing trie changes. // newOpTracer initializes the tracer for capturing trie changes.
@ -53,6 +52,9 @@ func newOpTracer() *opTracer {
// in the deletion set (resurrected node), then just wipe it from // in the deletion set (resurrected node), then just wipe it from
// the deletion set as it's "untouched". // the deletion set as it's "untouched".
func (t *opTracer) onInsert(path []byte) { func (t *opTracer) onInsert(path []byte) {
t.lock.Lock()
defer t.lock.Unlock()
if _, present := t.deletes[string(path)]; present { if _, present := t.deletes[string(path)]; present {
delete(t.deletes, string(path)) delete(t.deletes, string(path))
return return
@ -64,6 +66,9 @@ func (t *opTracer) onInsert(path []byte) {
// in the addition set, then just wipe it from the addition set // in the addition set, then just wipe it from the addition set
// as it's untouched. // as it's untouched.
func (t *opTracer) onDelete(path []byte) { func (t *opTracer) onDelete(path []byte) {
t.lock.Lock()
defer t.lock.Unlock()
if _, present := t.inserts[string(path)]; present { if _, present := t.inserts[string(path)]; present {
delete(t.inserts, string(path)) delete(t.inserts, string(path))
return return
@ -73,12 +78,18 @@ func (t *opTracer) onDelete(path []byte) {
// reset clears the content tracked by tracer. // reset clears the content tracked by tracer.
func (t *opTracer) reset() { func (t *opTracer) reset() {
t.lock.Lock()
defer t.lock.Unlock()
clear(t.inserts) clear(t.inserts)
clear(t.deletes) clear(t.deletes)
} }
// copy returns a deep copied tracer instance. // copy returns a deep copied tracer instance.
func (t *opTracer) copy() *opTracer { func (t *opTracer) copy() *opTracer {
t.lock.RLock()
defer t.lock.RUnlock()
return &opTracer{ return &opTracer{
inserts: maps.Clone(t.inserts), inserts: maps.Clone(t.inserts),
deletes: maps.Clone(t.deletes), deletes: maps.Clone(t.deletes),
@ -87,6 +98,9 @@ func (t *opTracer) copy() *opTracer {
// deletedList returns a list of node paths which are deleted from the trie. // deletedList returns a list of node paths which are deleted from the trie.
func (t *opTracer) deletedList() [][]byte { func (t *opTracer) deletedList() [][]byte {
t.lock.RLock()
defer t.lock.RUnlock()
paths := make([][]byte, 0, len(t.deletes)) paths := make([][]byte, 0, len(t.deletes))
for path := range t.deletes { for path := range t.deletes {
paths = append(paths, []byte(path)) paths = append(paths, []byte(path))
@ -97,11 +111,9 @@ func (t *opTracer) deletedList() [][]byte {
// prevalueTracer tracks the original values of resolved trie nodes. Cached trie // prevalueTracer tracks the original values of resolved trie nodes. Cached trie
// node values are expected to be immutable. A zero-size node value is treated as // node values are expected to be immutable. A zero-size node value is treated as
// non-existent and should not occur in practice. // non-existent and should not occur in practice.
//
// Note prevalueTracer is not thread-safe, callers should be responsible for
// handling the concurrency issues by themselves.
type prevalueTracer struct { type prevalueTracer struct {
data map[string][]byte data map[string][]byte
lock sync.RWMutex
} }
// newPrevalueTracer initializes the tracer for capturing resolved trie nodes. // newPrevalueTracer initializes the tracer for capturing resolved trie nodes.
@ -115,18 +127,27 @@ func newPrevalueTracer() *prevalueTracer {
// blob internally. Do not modify the value outside this function, // blob internally. Do not modify the value outside this function,
// as it is not deep-copied. // as it is not deep-copied.
func (t *prevalueTracer) put(path []byte, val []byte) { func (t *prevalueTracer) put(path []byte, val []byte) {
t.lock.Lock()
defer t.lock.Unlock()
t.data[string(path)] = val t.data[string(path)] = val
} }
// get returns the cached trie node value. If the node is not found, nil will // get returns the cached trie node value. If the node is not found, nil will
// be returned. // be returned.
func (t *prevalueTracer) get(path []byte) []byte { func (t *prevalueTracer) get(path []byte) []byte {
t.lock.RLock()
defer t.lock.RUnlock()
return t.data[string(path)] return t.data[string(path)]
} }
// hasList returns a list of flags indicating whether the corresponding trie nodes // hasList returns a list of flags indicating whether the corresponding trie nodes
// specified by the path exist in the trie. // specified by the path exist in the trie.
func (t *prevalueTracer) hasList(list [][]byte) []bool { func (t *prevalueTracer) hasList(list [][]byte) []bool {
t.lock.RLock()
defer t.lock.RUnlock()
exists := make([]bool, 0, len(list)) exists := make([]bool, 0, len(list))
for _, path := range list { for _, path := range list {
_, ok := t.data[string(path)] _, ok := t.data[string(path)]
@ -137,16 +158,25 @@ func (t *prevalueTracer) hasList(list [][]byte) []bool {
// values returns a list of values of the cached trie nodes. // values returns a list of values of the cached trie nodes.
func (t *prevalueTracer) values() [][]byte { func (t *prevalueTracer) values() [][]byte {
t.lock.RLock()
defer t.lock.RUnlock()
return slices.Collect(maps.Values(t.data)) return slices.Collect(maps.Values(t.data))
} }
// reset resets the cached content in the prevalueTracer. // reset resets the cached content in the prevalueTracer.
func (t *prevalueTracer) reset() { func (t *prevalueTracer) reset() {
t.lock.Lock()
defer t.lock.Unlock()
clear(t.data) clear(t.data)
} }
// copy returns a copied prevalueTracer instance. // copy returns a copied prevalueTracer instance.
func (t *prevalueTracer) copy() *prevalueTracer { func (t *prevalueTracer) copy() *prevalueTracer {
t.lock.RLock()
defer t.lock.RUnlock()
// Shadow clone is used, as the cached trie node values are immutable // Shadow clone is used, as the cached trie node values are immutable
return &prevalueTracer{ return &prevalueTracer{
data: maps.Clone(t.data), data: maps.Clone(t.data),

View file

@ -28,6 +28,7 @@ import (
"github.com/ethereum/go-ethereum/log" "github.com/ethereum/go-ethereum/log"
"github.com/ethereum/go-ethereum/trie/trienode" "github.com/ethereum/go-ethereum/trie/trienode"
"github.com/ethereum/go-ethereum/triedb/database" "github.com/ethereum/go-ethereum/triedb/database"
"golang.org/x/sync/errgroup"
) )
// Trie represents a Merkle Patricia Trie. Use New to create a trie that operates // Trie represents a Merkle Patricia Trie. Use New to create a trie that operates
@ -406,6 +407,66 @@ func (t *Trie) insert(n node, prefix, key []byte, value node) (bool, node, error
} }
} }
// UpdateBatch updates a batch of entries concurrently.
func (t *Trie) UpdateBatch(keys [][]byte, values [][]byte) error {
// Short circuit if the trie is already committed and unusable.
if t.committed {
return ErrCommitted
}
if len(keys) != len(values) {
return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values))
}
// Insert the entries sequentially if there are not too many
// trie nodes in the trie.
fn, ok := t.root.(*fullNode)
if !ok || len(keys) < 4 { // TODO(rjl493456442) the parallelism threshold should be twisted
for i, key := range keys {
err := t.Update(key, values[i])
if err != nil {
return err
}
}
return nil
}
var (
ikeys = make(map[byte][][]byte)
ivals = make(map[byte][][]byte)
eg errgroup.Group
)
for i, key := range keys {
hkey := keybytesToHex(key)
ikeys[hkey[0]] = append(ikeys[hkey[0]], hkey)
ivals[hkey[0]] = append(ivals[hkey[0]], values[i])
}
for pos, ks := range ikeys {
eg.Go(func() error {
vs := ivals[pos]
for i, k := range ks {
if len(vs[i]) != 0 {
_, n, err := t.insert(fn.Children[pos], []byte{pos}, k[1:], valueNode(vs[i]))
if err != nil {
return err
}
fn.Children[pos] = n
} else {
_, n, err := t.delete(fn.Children[pos], []byte{pos}, k[1:])
if err != nil {
return err
}
fn.Children[pos] = n
}
}
return nil
})
}
if err := eg.Wait(); err != nil {
return err
}
t.unhashed += len(keys)
t.uncommitted += len(keys)
return nil
}
// MustDelete is a wrapper of Delete and will omit any encountered error but // MustDelete is a wrapper of Delete and will omit any encountered error but
// just print out an error message. // just print out an error message.
func (t *Trie) MustDelete(key []byte) { func (t *Trie) MustDelete(key []byte) {

View file

@ -1499,3 +1499,57 @@ func testTrieCopyNewTrie(t *testing.T, entries []kv) {
t.Errorf("Hash mismatch: old %v, new %v", hash, tr.Hash()) t.Errorf("Hash mismatch: old %v, new %v", hash, tr.Hash())
} }
} }
func TestUpdateBatch(t *testing.T) {
testUpdateBatch(t, []kv{
{k: []byte("do"), v: []byte("verb")},
{k: []byte("ether"), v: []byte("wookiedoo")},
{k: []byte("horse"), v: []byte("stallion")},
{k: []byte("shaman"), v: []byte("horse")},
{k: []byte("doge"), v: []byte("coin")},
{k: []byte("dog"), v: []byte("puppy")},
})
var entries []kv
for i := 0; i < 256; i++ {
entries = append(entries, kv{k: testrand.Bytes(32), v: testrand.Bytes(32)})
}
testUpdateBatch(t, entries)
}
func testUpdateBatch(t *testing.T, entries []kv) {
var (
base = NewEmpty(nil)
keys [][]byte
vals [][]byte
)
for _, entry := range entries {
base.Update(entry.k, entry.v)
keys = append(keys, entry.k)
vals = append(vals, entry.v)
}
for i := 0; i < 10; i++ {
k, v := testrand.Bytes(32), testrand.Bytes(32)
base.Update(k, v)
keys = append(keys, k)
vals = append(vals, v)
}
cmp := NewEmpty(nil)
if err := cmp.UpdateBatch(keys, vals); err != nil {
t.Fatalf("Failed to update batch, %v", err)
}
// Traverse the original tree, the changes made on the copy one shouldn't
// affect the old one
for _, key := range keys {
v1, _ := base.Get(key)
v2, _ := cmp.Get(key)
if !bytes.Equal(v1, v2) {
t.Errorf("Unexpected data, key: %v, want: %v, got: %v", key, v1, v2)
}
}
if base.Hash() != cmp.Hash() {
t.Errorf("Hash mismatch: want %x, got %x", base.Hash(), cmp.Hash())
}
}

View file

@ -155,6 +155,22 @@ func (t *VerkleTrie) UpdateAccount(addr common.Address, acc *types.StateAccount,
return nil return nil
} }
// UpdateAccountBatch attempts to update a list accounts in the batch manner.
func (t *VerkleTrie) UpdateAccountBatch(addresses []common.Address, accounts []*types.StateAccount, codeLens []int) error {
if len(addresses) != len(accounts) {
return fmt.Errorf("address and accounts length mismatch: %d != %d", len(addresses), len(accounts))
}
if len(addresses) != len(codeLens) {
return fmt.Errorf("address and code length mismatch: %d != %d", len(addresses), len(codeLens))
}
for i, addr := range addresses {
if err := t.UpdateAccount(addr, accounts[i], codeLens[i]); err != nil {
return err
}
}
return nil
}
// UpdateStorage implements state.Trie, writing the provided storage slot into // UpdateStorage implements state.Trie, writing the provided storage slot into
// the tree. If the tree is corrupted, an error will be returned. // the tree. If the tree is corrupted, an error will be returned.
func (t *VerkleTrie) UpdateStorage(address common.Address, key, value []byte) error { func (t *VerkleTrie) UpdateStorage(address common.Address, key, value []byte) error {
@ -169,6 +185,19 @@ func (t *VerkleTrie) UpdateStorage(address common.Address, key, value []byte) er
return t.root.Insert(k, v[:], t.nodeResolver) return t.root.Insert(k, v[:], t.nodeResolver)
} }
// UpdateStorageBatch attempts to update a list storages in the batch manner.
func (t *VerkleTrie) UpdateStorageBatch(address common.Address, keys [][]byte, values [][]byte) error {
if len(keys) != len(values) {
return fmt.Errorf("keys and values length mismatch: %d != %d", len(keys), len(values))
}
for i, key := range keys {
if err := t.UpdateStorage(address, key, values[i]); err != nil {
return err
}
}
return nil
}
// DeleteAccount leaves the account untouched, as no account deletion can happen // DeleteAccount leaves the account untouched, as no account deletion can happen
// in verkle. // in verkle.
// There is a special corner case, in which an account that is prefunded, CREATE2-d // There is a special corner case, in which an account that is prefunded, CREATE2-d