mirror of
https://github.com/ethereum/go-ethereum.git
synced 2026-07-24 21:56:43 +00:00
Polish binary tree implementations (#551)
* trie: export PrevalueTracer * trie, core/types: polish binary trie * trie/bintrie: update tests * trie/bintrie: fix incorrect node size
This commit is contained in:
parent
931b8fb13e
commit
123723b9da
19 changed files with 219 additions and 137 deletions
|
|
@ -45,4 +45,7 @@ var (
|
|||
|
||||
// EmptyVerkleHash is the known hash of an empty verkle trie.
|
||||
EmptyVerkleHash = common.Hash{}
|
||||
|
||||
// EmptyBinaryHash is the known hash of an empty binary trie.
|
||||
EmptyBinaryHash = common.Hash{}
|
||||
)
|
||||
|
|
|
|||
|
|
@ -43,8 +43,7 @@ const (
|
|||
// BinaryNode is an interface for a binary trie node.
|
||||
type BinaryNode interface {
|
||||
Get([]byte, NodeResolverFn) ([]byte, error)
|
||||
Insert([]byte, []byte, NodeResolverFn) (BinaryNode, error)
|
||||
// Commit() common.Hash
|
||||
Insert([]byte, []byte, NodeResolverFn, int) (BinaryNode, error)
|
||||
Copy() BinaryNode
|
||||
Hash() common.Hash
|
||||
GetValuesAtStem([]byte, NodeResolverFn) ([][]byte, error)
|
||||
|
|
@ -65,7 +64,7 @@ func SerializeNode(node BinaryNode) []byte {
|
|||
copy(serialized[33:65], n.right.Hash().Bytes())
|
||||
return serialized[:]
|
||||
case *StemNode:
|
||||
var serialized [32 + 256*32]byte
|
||||
var serialized [32 + 32 + 256*32]byte
|
||||
serialized[0] = nodeTypeStem
|
||||
copy(serialized[1:32], node.(*StemNode).Stem)
|
||||
bitmap := serialized[32:64]
|
||||
|
|
|
|||
|
|
@ -28,12 +28,13 @@ func (e Empty) Get(_ []byte, _ NodeResolverFn) ([]byte, error) {
|
|||
return nil, nil
|
||||
}
|
||||
|
||||
func (e Empty) Insert(key []byte, value []byte, _ NodeResolverFn) (BinaryNode, error) {
|
||||
func (e Empty) Insert(key []byte, value []byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
|
||||
var values [256][]byte
|
||||
values[key[31]] = value
|
||||
return &StemNode{
|
||||
Stem: slices.Clone(key[:31]),
|
||||
Values: values[:],
|
||||
depth: depth,
|
||||
}, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -47,7 +47,7 @@ func TestEmptyInsert(t *testing.T) {
|
|||
key[31] = 0x34
|
||||
value := common.HexToHash("0xabcd").Bytes()
|
||||
|
||||
newNode, err := node.Insert(key, value, nil)
|
||||
newNode, err := node.Insert(key, value, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ func (h HashedNode) Get(_ []byte, _ NodeResolverFn) ([]byte, error) {
|
|||
panic("not implemented") // TODO: Implement
|
||||
}
|
||||
|
||||
func (h HashedNode) Insert(key []byte, value []byte, resolver NodeResolverFn) (BinaryNode, error) {
|
||||
func (h HashedNode) Insert(key []byte, value []byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
|
||||
return nil, errors.New("insert not implemented for hashed node")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -62,7 +62,7 @@ func TestHashedNodeInsert(t *testing.T) {
|
|||
key := make([]byte, 32)
|
||||
value := make([]byte, 32)
|
||||
|
||||
_, err := node.Insert(key, value, nil)
|
||||
_, err := node.Insert(key, value, nil, 0)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for Insert on HashedNode")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -25,17 +25,14 @@ import (
|
|||
)
|
||||
|
||||
func keyToPath(depth int, key []byte) ([]byte, error) {
|
||||
path := make([]byte, 0, depth+1)
|
||||
|
||||
if depth > 31*8 {
|
||||
return nil, errors.New("node too deep")
|
||||
}
|
||||
|
||||
path := make([]byte, 0, depth+1)
|
||||
for i := range depth + 1 {
|
||||
bit := key[i/8] >> (7 - (i % 8)) & 1
|
||||
path = append(path, bit)
|
||||
}
|
||||
|
||||
return path, nil
|
||||
}
|
||||
|
||||
|
|
@ -81,16 +78,16 @@ func (bt *InternalNode) GetValuesAtStem(stem []byte, resolver NodeResolverFn) ([
|
|||
func (bt *InternalNode) Get(key []byte, resolver NodeResolverFn) ([]byte, error) {
|
||||
values, err := bt.GetValuesAtStem(key[:31], resolver)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("Get error: %w", err)
|
||||
return nil, fmt.Errorf("get error: %w", err)
|
||||
}
|
||||
return values[key[31]], nil
|
||||
}
|
||||
|
||||
// Insert inserts a new key-value pair into the trie.
|
||||
func (bt *InternalNode) Insert(key []byte, value []byte, resolver NodeResolverFn) (BinaryNode, error) {
|
||||
func (bt *InternalNode) Insert(key []byte, value []byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
|
||||
var values [256][]byte
|
||||
values[key[31]] = value
|
||||
return bt.InsertValuesAtStem(key[:31], values[:], resolver, 0)
|
||||
return bt.InsertValuesAtStem(key[:31], values[:], resolver, depth)
|
||||
}
|
||||
|
||||
// Copy creates a deep copy of the node.
|
||||
|
|
@ -121,11 +118,11 @@ func (bt *InternalNode) Hash() common.Hash {
|
|||
// InsertValuesAtStem inserts a full value group at the given stem in the internal node.
|
||||
// Already-existing values will be overwritten.
|
||||
func (bt *InternalNode) InsertValuesAtStem(stem []byte, values [][]byte, resolver NodeResolverFn, depth int) (BinaryNode, error) {
|
||||
bit := stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
|
||||
var (
|
||||
child *BinaryNode
|
||||
err error
|
||||
)
|
||||
bit := stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
|
||||
if bit == 0 {
|
||||
child = &bt.left
|
||||
} else {
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ func TestInternalNodeInsert(t *testing.T) {
|
|||
leftKey[31] = 10
|
||||
leftValue := common.HexToHash("0x0101").Bytes()
|
||||
|
||||
newNode, err := node.Insert(leftKey, leftValue, nil)
|
||||
newNode, err := node.Insert(leftKey, leftValue, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert: %v", err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -114,7 +114,7 @@ func (it *binaryNodeIterator) Next(descend bool) bool {
|
|||
return it.Next(descend)
|
||||
case HashedNode:
|
||||
// resolve the node
|
||||
data, err := it.trie.FlatdbNodeResolver(it.Path(), common.Hash(node))
|
||||
data, err := it.trie.nodeResolver(it.Path(), common.Hash(node))
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
|
|
|||
79
trie/bintrie/key_encoding.go
Normal file
79
trie/bintrie/key_encoding.go
Normal file
|
|
@ -0,0 +1,79 @@
|
|||
// Copyright 2025 go-ethereum Authors
|
||||
// This file is part of the go-ethereum library.
|
||||
//
|
||||
// The go-ethereum library is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU Lesser General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// The go-ethereum library is distributed in the hope that it will be useful,
|
||||
// but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
// GNU Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License
|
||||
// along with the go-ethereum library. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
package bintrie
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
|
||||
"github.com/ethereum/go-ethereum/common"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
||||
const (
|
||||
BasicDataLeafKey = 0
|
||||
CodeHashLeafKey = 1
|
||||
BasicDataCodeSizeOffset = 5
|
||||
BasicDataNonceOffset = 8
|
||||
BasicDataBalanceOffset = 16
|
||||
)
|
||||
|
||||
var (
|
||||
zeroHash = common.Hash{}
|
||||
codeOffset = uint256.NewInt(128)
|
||||
)
|
||||
|
||||
func GetBinaryTreeKey(addr common.Address, key []byte) []byte {
|
||||
hasher := sha256.New()
|
||||
hasher.Write(zeroHash[:12])
|
||||
hasher.Write(addr[:])
|
||||
hasher.Write(key[:31])
|
||||
k := hasher.Sum(nil)
|
||||
k[31] = key[31]
|
||||
return k
|
||||
}
|
||||
|
||||
func GetBinaryTreeKeyCodeHash(addr common.Address) []byte {
|
||||
var k [32]byte
|
||||
k[31] = CodeHashLeafKey
|
||||
return GetBinaryTreeKey(addr, k[:])
|
||||
}
|
||||
|
||||
func GetBinaryTreeKeyStorageSlot(address common.Address, key []byte) []byte {
|
||||
var k [32]byte
|
||||
|
||||
// Case when the key belongs to the account header
|
||||
if bytes.Equal(key[:31], zeroHash[:31]) && key[31] < 64 {
|
||||
k[31] = 64 + key[31]
|
||||
return GetBinaryTreeKey(address, k[:])
|
||||
}
|
||||
|
||||
// Set the main storage offset
|
||||
// note that the first 64 bytes of the main offset storage
|
||||
// are unreachable, which is consistent with the spec and
|
||||
// what verkle does.
|
||||
k[0] = 1 // 1 << 248
|
||||
copy(k[1:], key[:31])
|
||||
k[31] = key[31]
|
||||
|
||||
return GetBinaryTreeKey(address, k[:])
|
||||
}
|
||||
|
||||
func GetBinaryTreeKeyCodeChunk(address common.Address, chunknr *uint256.Int) []byte {
|
||||
chunkOffset := new(uint256.Int).Add(codeOffset, chunknr).Bytes()
|
||||
return GetBinaryTreeKey(address, chunkOffset)
|
||||
}
|
||||
|
|
@ -39,7 +39,7 @@ func (bt *StemNode) Get(key []byte, _ NodeResolverFn) ([]byte, error) {
|
|||
}
|
||||
|
||||
// Insert inserts a new key-value pair into the node.
|
||||
func (bt *StemNode) Insert(key []byte, value []byte, _ NodeResolverFn) (BinaryNode, error) {
|
||||
func (bt *StemNode) Insert(key []byte, value []byte, _ NodeResolverFn, depth int) (BinaryNode, error) {
|
||||
if !bytes.Equal(bt.Stem, key[:31]) {
|
||||
bitStem := bt.Stem[bt.depth/8] >> (7 - (bt.depth % 8)) & 1
|
||||
|
||||
|
|
@ -59,7 +59,7 @@ func (bt *StemNode) Insert(key []byte, value []byte, _ NodeResolverFn) (BinaryNo
|
|||
bitKey := key[new.depth/8] >> (7 - (new.depth % 8)) & 1
|
||||
if bitKey == bitStem {
|
||||
var err error
|
||||
*child, err = (*child).Insert(key, value, nil)
|
||||
*child, err = (*child).Insert(key, value, nil, depth+1)
|
||||
if err != nil {
|
||||
return new, fmt.Errorf("insert error: %w", err)
|
||||
}
|
||||
|
|
@ -70,16 +70,14 @@ func (bt *StemNode) Insert(key []byte, value []byte, _ NodeResolverFn) (BinaryNo
|
|||
*other = &StemNode{
|
||||
Stem: slices.Clone(key[:31]),
|
||||
Values: values[:],
|
||||
depth: new.depth + 1,
|
||||
depth: depth + 1,
|
||||
}
|
||||
}
|
||||
|
||||
return new, nil
|
||||
}
|
||||
if len(value) != 32 {
|
||||
return bt, errors.New("invalid insertion: value length")
|
||||
}
|
||||
|
||||
bt.Values[key[31]] = value
|
||||
return bt, nil
|
||||
}
|
||||
|
|
@ -181,7 +179,6 @@ func (bt *StemNode) InsertValuesAtStem(key []byte, values [][]byte, _ NodeResolv
|
|||
depth: new.depth + 1,
|
||||
}
|
||||
}
|
||||
|
||||
return new, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ func TestStemNodeInsertSameStem(t *testing.T) {
|
|||
key[31] = 10
|
||||
value := common.HexToHash("0x0202").Bytes()
|
||||
|
||||
newNode, err := node.Insert(key, value, nil)
|
||||
newNode, err := node.Insert(key, value, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert: %v", err)
|
||||
}
|
||||
|
|
@ -86,7 +86,7 @@ func TestStemNodeInsertDifferentStem(t *testing.T) {
|
|||
key[0] = 0x80 // First bit is 1 instead of 0
|
||||
value := common.HexToHash("0x0202").Bytes()
|
||||
|
||||
newNode, err := node.Insert(key, value, nil)
|
||||
newNode, err := node.Insert(key, value, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("Failed to insert: %v", err)
|
||||
}
|
||||
|
|
@ -137,7 +137,7 @@ func TestStemNodeInsertInvalidValueLength(t *testing.T) {
|
|||
copy(key[:31], stem)
|
||||
invalidValue := []byte{1, 2, 3} // Not 32 bytes
|
||||
|
||||
_, err := node.Insert(key, invalidValue, nil)
|
||||
_, err := node.Insert(key, invalidValue, nil, 0)
|
||||
if err == nil {
|
||||
t.Fatal("Expected error for invalid value length")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -27,7 +27,6 @@ import (
|
|||
"github.com/ethereum/go-ethereum/ethdb"
|
||||
"github.com/ethereum/go-ethereum/trie"
|
||||
"github.com/ethereum/go-ethereum/trie/trienode"
|
||||
"github.com/ethereum/go-ethereum/trie/utils"
|
||||
"github.com/ethereum/go-ethereum/triedb/database"
|
||||
"github.com/holiman/uint256"
|
||||
)
|
||||
|
|
@ -39,11 +38,10 @@ func NewBinaryNode() BinaryNode {
|
|||
return Empty{}
|
||||
}
|
||||
|
||||
// BinaryTrie is a wrapper around VerkleNode that implements the trie.Trie
|
||||
// interface so that Verkle trees can be reused verbatim.
|
||||
// BinaryTrie is the implementation of https://eips.ethereum.org/EIPS/eip-7864.
|
||||
type BinaryTrie struct {
|
||||
root BinaryNode
|
||||
reader *trie.TrieReader
|
||||
reader *trie.Reader
|
||||
tracer *trie.PrevalueTracer
|
||||
}
|
||||
|
||||
|
|
@ -55,36 +53,43 @@ func (t *BinaryTrie) ToDot() string {
|
|||
|
||||
// NewBinaryTrie creates a new binary trie.
|
||||
func NewBinaryTrie(root common.Hash, db database.NodeDatabase) (*BinaryTrie, error) {
|
||||
reader, err := trie.NewTrieReader(root, common.Hash{}, db)
|
||||
reader, err := trie.NewReader(root, common.Hash{}, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Parse the root verkle node if it's not empty.
|
||||
node := NewBinaryNode()
|
||||
if root != types.EmptyVerkleHash && root != types.EmptyRootHash {
|
||||
blob, err := reader.Node(nil, common.Hash{})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node, err = DeserializeNode(blob, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
return &BinaryTrie{
|
||||
root: node,
|
||||
t := &BinaryTrie{
|
||||
root: NewBinaryNode(),
|
||||
reader: reader,
|
||||
}, nil
|
||||
tracer: trie.NewPrevalueTracer(),
|
||||
}
|
||||
// Parse the root node if it's not empty
|
||||
if root != types.EmptyBinaryHash && root != types.EmptyRootHash {
|
||||
blob, err := t.nodeResolver(nil, root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
node, err := DeserializeNode(blob, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.root = node
|
||||
}
|
||||
return t, nil
|
||||
}
|
||||
|
||||
// FlatdbNodeResolver is a node resolver that reads nodes from the flatdb.
|
||||
func (t *BinaryTrie) FlatdbNodeResolver(path []byte, hash common.Hash) ([]byte, error) {
|
||||
// nodeResolver is a node resolver that reads nodes from the flatdb.
|
||||
func (t *BinaryTrie) nodeResolver(path []byte, hash common.Hash) ([]byte, error) {
|
||||
// empty nodes will be serialized as common.Hash{}, so capture
|
||||
// this special use case.
|
||||
if hash == (common.Hash{}) {
|
||||
return nil, nil // empty node
|
||||
}
|
||||
return t.reader.Node(path, hash)
|
||||
blob, err := t.reader.Node(path, hash)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.tracer.Put(path, blob)
|
||||
return blob, nil
|
||||
}
|
||||
|
||||
// GetKey returns the sha3 preimage of a hashed key that was previously used
|
||||
|
|
@ -93,30 +98,23 @@ func (t *BinaryTrie) GetKey(key []byte) []byte {
|
|||
return key
|
||||
}
|
||||
|
||||
// Get returns the value for key stored in the trie. The value bytes must
|
||||
// not be modified by the caller. If a node was not found in the database, a
|
||||
// trie.MissingNodeError is returned.
|
||||
func (t *BinaryTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) {
|
||||
return t.root.Get(utils.GetBinaryTreeKey(addr, key), t.FlatdbNodeResolver)
|
||||
}
|
||||
|
||||
// GetWithHashedKey returns the value, assuming that the key has already
|
||||
// been hashed.
|
||||
func (t *BinaryTrie) GetWithHashedKey(key []byte) ([]byte, error) {
|
||||
return t.root.Get(key, t.FlatdbNodeResolver)
|
||||
return t.root.Get(key, t.nodeResolver)
|
||||
}
|
||||
|
||||
// GetAccount returns the account information for the given address.
|
||||
func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error) {
|
||||
acc := &types.StateAccount{}
|
||||
versionkey := utils.GetBinaryTreeKey(addr, zero[:])
|
||||
var (
|
||||
values [][]byte
|
||||
err error
|
||||
acc = &types.StateAccount{}
|
||||
key = GetBinaryTreeKey(addr, zero[:])
|
||||
)
|
||||
switch r := t.root.(type) {
|
||||
case *InternalNode:
|
||||
values, err = r.GetValuesAtStem(versionkey[:31], t.FlatdbNodeResolver)
|
||||
values, err = r.GetValuesAtStem(key[:31], t.nodeResolver)
|
||||
case *StemNode:
|
||||
values = r.Values
|
||||
case Empty:
|
||||
|
|
@ -130,47 +128,53 @@ func (t *BinaryTrie) GetAccount(addr common.Address) (*types.StateAccount, error
|
|||
return nil, fmt.Errorf("GetAccount (%x) error: %v", addr, err)
|
||||
}
|
||||
|
||||
// The following code is required for the MPT->VKT conversion.
|
||||
// An account can be partially migrated, where storage slots were moved to the VKT
|
||||
// The following code is required for the MPT->Binary conversion.
|
||||
// An account can be partially migrated, where storage slots were moved to the binary
|
||||
// but not yet the account. This means some account information as (header) storage slots
|
||||
// are in the VKT but basic account information must be read in the base tree (MPT).
|
||||
// are in the binary trie but basic account information must be read in the base tree (MPT).
|
||||
// TODO: we can simplify this logic depending if the conversion is in progress or finished.
|
||||
emptyAccount := true
|
||||
|
||||
for i := 0; values != nil && i <= utils.CodeHashLeafKey && emptyAccount; i++ {
|
||||
for i := 0; values != nil && i <= CodeHashLeafKey && emptyAccount; i++ {
|
||||
emptyAccount = emptyAccount && values[i] == nil
|
||||
}
|
||||
if emptyAccount {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// if the account has been deleted, then values[10] will be 0 and not nil. If it has
|
||||
// If the account has been deleted, then values[10] will be 0 and not nil. If it has
|
||||
// been recreated after that, then its code keccak will NOT be 0. So return `nil` if
|
||||
// the nonce, and values[10], and code keccak is 0.
|
||||
if bytes.Equal(values[utils.BasicDataLeafKey], zero[:]) && len(values) > 10 && len(values[10]) > 0 && bytes.Equal(values[utils.CodeHashLeafKey], zero[:]) {
|
||||
if bytes.Equal(values[BasicDataLeafKey], zero[:]) && len(values) > 10 && len(values[10]) > 0 && bytes.Equal(values[CodeHashLeafKey], zero[:]) {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
acc.Nonce = binary.BigEndian.Uint64(values[utils.BasicDataLeafKey][utils.BasicDataNonceOffset:])
|
||||
acc.Nonce = binary.BigEndian.Uint64(values[BasicDataLeafKey][BasicDataNonceOffset:])
|
||||
var balance [16]byte
|
||||
copy(balance[:], values[utils.BasicDataLeafKey][utils.BasicDataBalanceOffset:])
|
||||
copy(balance[:], values[BasicDataLeafKey][BasicDataBalanceOffset:])
|
||||
acc.Balance = new(uint256.Int).SetBytes(balance[:])
|
||||
acc.CodeHash = values[utils.CodeHashLeafKey]
|
||||
acc.CodeHash = values[CodeHashLeafKey]
|
||||
|
||||
return acc, nil
|
||||
}
|
||||
|
||||
// GetStorage returns the value for key stored in the trie. The value bytes must
|
||||
// not be modified by the caller. If a node was not found in the database, a
|
||||
// trie.MissingNodeError is returned.
|
||||
func (t *BinaryTrie) GetStorage(addr common.Address, key []byte) ([]byte, error) {
|
||||
return t.root.Get(GetBinaryTreeKey(addr, key), t.nodeResolver)
|
||||
}
|
||||
|
||||
// UpdateAccount updates the account information for the given address.
|
||||
func (t *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount, codeLen int) error {
|
||||
var (
|
||||
err error
|
||||
basicData [32]byte
|
||||
values = make([][]byte, NodeWidth)
|
||||
stem = utils.GetBinaryTreeKey(addr, zero[:])
|
||||
stem = GetBinaryTreeKey(addr, zero[:])
|
||||
)
|
||||
binary.BigEndian.PutUint32(basicData[BasicDataCodeSizeOffset-1:], uint32(codeLen))
|
||||
binary.BigEndian.PutUint64(basicData[BasicDataNonceOffset:], acc.Nonce)
|
||||
|
||||
binary.BigEndian.PutUint32(basicData[utils.BasicDataCodeSizeOffset-1:], uint32(codeLen))
|
||||
binary.BigEndian.PutUint64(basicData[utils.BasicDataNonceOffset:], acc.Nonce)
|
||||
// Because the balance is a max of 16 bytes, truncate
|
||||
// the extra values. This happens in devmode, where
|
||||
// 0xff**32 is allocated to the developer account.
|
||||
|
|
@ -181,33 +185,33 @@ func (t *BinaryTrie) UpdateAccount(addr common.Address, acc *types.StateAccount,
|
|||
balanceBytes = balanceBytes[16:]
|
||||
}
|
||||
copy(basicData[32-len(balanceBytes):], balanceBytes[:])
|
||||
values[utils.BasicDataLeafKey] = basicData[:]
|
||||
values[utils.CodeHashLeafKey] = acc.CodeHash[:]
|
||||
values[BasicDataLeafKey] = basicData[:]
|
||||
values[CodeHashLeafKey] = acc.CodeHash[:]
|
||||
|
||||
t.root, err = t.root.InsertValuesAtStem(stem, values, t.FlatdbNodeResolver, 0)
|
||||
t.root, err = t.root.InsertValuesAtStem(stem, values, t.nodeResolver, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
// UpdateStem updates the values for the given stem key.
|
||||
func (t *BinaryTrie) UpdateStem(key []byte, values [][]byte) error {
|
||||
var err error
|
||||
t.root, err = t.root.InsertValuesAtStem(key, values, t.FlatdbNodeResolver, 0)
|
||||
t.root, err = t.root.InsertValuesAtStem(key, values, t.nodeResolver, 0)
|
||||
return err
|
||||
}
|
||||
|
||||
// Update associates key with value in the trie. If value has length zero, any
|
||||
// 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
|
||||
// by the caller while they are stored in the trie. If a node was not found in the
|
||||
// database, a trie.MissingNodeError is returned.
|
||||
func (t *BinaryTrie) UpdateStorage(address common.Address, key, value []byte) error {
|
||||
k := utils.GetBinaryTreeKeyStorageSlot(address, key)
|
||||
k := GetBinaryTreeKeyStorageSlot(address, key)
|
||||
var v [32]byte
|
||||
if len(value) >= 32 {
|
||||
copy(v[:], value[:32])
|
||||
} else {
|
||||
copy(v[32-len(value):], value[:])
|
||||
}
|
||||
root, err := t.root.Insert(k, v[:], t.FlatdbNodeResolver)
|
||||
root, err := t.root.Insert(k, v[:], t.nodeResolver, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("UpdateStorage (%x) error: %v", address, err)
|
||||
}
|
||||
|
|
@ -220,12 +224,12 @@ func (t *BinaryTrie) DeleteAccount(addr common.Address) error {
|
|||
return nil
|
||||
}
|
||||
|
||||
// Delete removes any existing value for key from the trie. If a node was not
|
||||
// DeleteStorage removes any existing value for key from the trie. If a node was not
|
||||
// found in the database, a trie.MissingNodeError is returned.
|
||||
func (t *BinaryTrie) DeleteStorage(addr common.Address, key []byte) error {
|
||||
k := utils.GetBinaryTreeKey(addr, key)
|
||||
k := GetBinaryTreeKey(addr, key)
|
||||
var zero [32]byte
|
||||
root, err := t.root.Insert(k, zero[:], t.FlatdbNodeResolver)
|
||||
root, err := t.root.Insert(k, zero[:], t.nodeResolver, 0)
|
||||
if err != nil {
|
||||
return fmt.Errorf("DeleteStorage (%x) error: %v", addr, err)
|
||||
}
|
||||
|
|
@ -290,6 +294,8 @@ func (t *BinaryTrie) IsVerkle() bool {
|
|||
return true
|
||||
}
|
||||
|
||||
// UpdateContractCode updates the contract code into the trie.
|
||||
//
|
||||
// Note: the basic data leaf needs to have been previously created for this to work
|
||||
func (t *BinaryTrie) UpdateContractCode(addr common.Address, codeHash common.Hash, code []byte) error {
|
||||
var (
|
||||
|
|
@ -304,7 +310,7 @@ func (t *BinaryTrie) UpdateContractCode(addr common.Address, codeHash common.Has
|
|||
values = make([][]byte, NodeWidth)
|
||||
var offset [32]byte
|
||||
binary.LittleEndian.PutUint64(offset[24:], chunknr+128)
|
||||
key = utils.GetBinaryTreeKey(addr, offset[:])
|
||||
key = GetBinaryTreeKey(addr, offset[:])
|
||||
}
|
||||
values[groupOffset] = chunks[i : i+32]
|
||||
|
||||
|
|
|
|||
|
|
@ -35,7 +35,7 @@ var (
|
|||
|
||||
func TestSingleEntry(t *testing.T) {
|
||||
tree := NewBinaryNode()
|
||||
tree, err := tree.Insert(zeroKey[:], oneKey[:], nil)
|
||||
tree, err := tree.Insert(zeroKey[:], oneKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -52,11 +52,11 @@ func TestSingleEntry(t *testing.T) {
|
|||
func TestTwoEntriesDiffFirstBit(t *testing.T) {
|
||||
var err error
|
||||
tree := NewBinaryNode()
|
||||
tree, err = tree.Insert(zeroKey[:], oneKey[:], nil)
|
||||
tree, err = tree.Insert(zeroKey[:], oneKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000").Bytes(), twoKey[:], nil)
|
||||
tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000000").Bytes(), twoKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -71,19 +71,19 @@ func TestTwoEntriesDiffFirstBit(t *testing.T) {
|
|||
func TestOneStemColocatedValues(t *testing.T) {
|
||||
var err error
|
||||
tree := NewBinaryNode()
|
||||
tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil)
|
||||
tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil)
|
||||
tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000009").Bytes(), threeKey[:], nil)
|
||||
tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000009").Bytes(), threeKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tree, err = tree.Insert(common.HexToHash("00000000000000000000000000000000000000000000000000000000000000FF").Bytes(), fourKey[:], nil)
|
||||
tree, err = tree.Insert(common.HexToHash("00000000000000000000000000000000000000000000000000000000000000FF").Bytes(), fourKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -96,20 +96,20 @@ func TestTwoStemColocatedValues(t *testing.T) {
|
|||
var err error
|
||||
tree := NewBinaryNode()
|
||||
// stem: 0...0
|
||||
tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil)
|
||||
tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil)
|
||||
tree, err = tree.Insert(common.HexToHash("0000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// stem: 10...0
|
||||
tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil)
|
||||
tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000003").Bytes(), oneKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil)
|
||||
tree, err = tree.Insert(common.HexToHash("8000000000000000000000000000000000000000000000000000000000000004").Bytes(), twoKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -124,11 +124,11 @@ func TestTwoKeysMatchFirst42Bits(t *testing.T) {
|
|||
// key1 and key 2 have the same prefix of 42 bits (b0*42+b1+b1) and differ after.
|
||||
key1 := common.HexToHash("0000000000C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0C0").Bytes()
|
||||
key2 := common.HexToHash("0000000000E00000000000000000000000000000000000000000000000000000").Bytes()
|
||||
tree, err = tree.Insert(key1, oneKey[:], nil)
|
||||
tree, err = tree.Insert(key1, oneKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tree, err = tree.Insert(key2, twoKey[:], nil)
|
||||
tree, err = tree.Insert(key2, twoKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -139,11 +139,11 @@ func TestTwoKeysMatchFirst42Bits(t *testing.T) {
|
|||
func TestInsertDuplicateKey(t *testing.T) {
|
||||
var err error
|
||||
tree := NewBinaryNode()
|
||||
tree, err = tree.Insert(oneKey[:], oneKey[:], nil)
|
||||
tree, err = tree.Insert(oneKey[:], oneKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
tree, err = tree.Insert(oneKey[:], twoKey[:], nil)
|
||||
tree, err = tree.Insert(oneKey[:], twoKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -161,7 +161,7 @@ func TestLargeNumberOfEntries(t *testing.T) {
|
|||
for i := range 256 {
|
||||
var key [32]byte
|
||||
key[0] = byte(i)
|
||||
tree, err = tree.Insert(key[:], ffKey[:], nil)
|
||||
tree, err = tree.Insert(key[:], ffKey[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
@ -184,7 +184,7 @@ func TestMerkleizeMultipleEntries(t *testing.T) {
|
|||
for i, key := range keys {
|
||||
var v [32]byte
|
||||
binary.LittleEndian.PutUint64(v[:8], uint64(i))
|
||||
tree, err = tree.Insert(key, v[:], nil)
|
||||
tree, err = tree.Insert(key, v[:], nil, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -571,7 +571,7 @@ func VerifyRangeProof(rootHash common.Hash, firstKey []byte, keys [][]byte, valu
|
|||
root: root,
|
||||
reader: newEmptyReader(),
|
||||
opTracer: newOpTracer(),
|
||||
prevalueTracer: newPrevalueTracer(),
|
||||
prevalueTracer: NewPrevalueTracer(),
|
||||
}
|
||||
if empty {
|
||||
tr.root = nil
|
||||
|
|
|
|||
|
|
@ -104,17 +104,17 @@ type PrevalueTracer struct {
|
|||
lock sync.RWMutex
|
||||
}
|
||||
|
||||
// newPrevalueTracer initializes the tracer for capturing resolved trie nodes.
|
||||
func newPrevalueTracer() *PrevalueTracer {
|
||||
// NewPrevalueTracer initializes the tracer for capturing resolved trie nodes.
|
||||
func NewPrevalueTracer() *PrevalueTracer {
|
||||
return &PrevalueTracer{
|
||||
data: make(map[string][]byte),
|
||||
}
|
||||
}
|
||||
|
||||
// put tracks the newly loaded trie node and caches its RLP-encoded
|
||||
// Put tracks the newly loaded trie node and caches its RLP-encoded
|
||||
// blob internally. Do not modify the value outside this function,
|
||||
// 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()
|
||||
|
||||
|
|
@ -130,9 +130,9 @@ func (t *PrevalueTracer) Get(path []byte) []byte {
|
|||
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.
|
||||
func (t *PrevalueTracer) hasList(list [][]byte) []bool {
|
||||
func (t *PrevalueTracer) HasList(list [][]byte) []bool {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
|
|
@ -144,24 +144,24 @@ func (t *PrevalueTracer) hasList(list [][]byte) []bool {
|
|||
return exists
|
||||
}
|
||||
|
||||
// values returns a list of values of the cached trie nodes.
|
||||
func (t *PrevalueTracer) values() map[string][]byte {
|
||||
// Values returns a list of values of the cached trie nodes.
|
||||
func (t *PrevalueTracer) Values() map[string][]byte {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
return maps.Clone(t.data)
|
||||
}
|
||||
|
||||
// reset resets the cached content in the prevalueTracer.
|
||||
func (t *PrevalueTracer) reset() {
|
||||
// Reset resets the cached content in the prevalueTracer.
|
||||
func (t *PrevalueTracer) Reset() {
|
||||
t.lock.Lock()
|
||||
defer t.lock.Unlock()
|
||||
|
||||
clear(t.data)
|
||||
}
|
||||
|
||||
// copy returns a copied prevalueTracer instance.
|
||||
func (t *PrevalueTracer) copy() *PrevalueTracer {
|
||||
// Copy returns a copied prevalueTracer instance.
|
||||
func (t *PrevalueTracer) Copy() *PrevalueTracer {
|
||||
t.lock.RLock()
|
||||
defer t.lock.RUnlock()
|
||||
|
||||
|
|
|
|||
16
trie/trie.go
16
trie/trie.go
|
|
@ -55,7 +55,7 @@ type Trie struct {
|
|||
uncommitted int
|
||||
|
||||
// reader is the handler trie can retrieve nodes from.
|
||||
reader *TrieReader
|
||||
reader *Reader
|
||||
|
||||
// Various tracers for capturing the modifications to trie
|
||||
opTracer *opTracer
|
||||
|
|
@ -77,7 +77,7 @@ func (t *Trie) Copy() *Trie {
|
|||
uncommitted: t.uncommitted,
|
||||
reader: t.reader,
|
||||
opTracer: t.opTracer.copy(),
|
||||
prevalueTracer: t.prevalueTracer.copy(),
|
||||
prevalueTracer: t.prevalueTracer.Copy(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -88,7 +88,7 @@ func (t *Trie) Copy() *Trie {
|
|||
// empty, otherwise, the root node must be present in database or returns
|
||||
// a MissingNodeError if not.
|
||||
func New(id *ID, db database.NodeDatabase) (*Trie, error) {
|
||||
reader, err := NewTrieReader(id.StateRoot, id.Owner, db)
|
||||
reader, err := NewReader(id.StateRoot, id.Owner, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -96,7 +96,7 @@ func New(id *ID, db database.NodeDatabase) (*Trie, error) {
|
|||
owner: id.Owner,
|
||||
reader: reader,
|
||||
opTracer: newOpTracer(),
|
||||
prevalueTracer: newPrevalueTracer(),
|
||||
prevalueTracer: NewPrevalueTracer(),
|
||||
}
|
||||
if id.Root != (common.Hash{}) && id.Root != types.EmptyRootHash {
|
||||
rootnode, err := trie.resolveAndTrack(id.Root[:], nil)
|
||||
|
|
@ -659,7 +659,7 @@ func (t *Trie) resolveAndTrack(n hashNode, prefix []byte) (node, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.prevalueTracer.put(prefix, blob)
|
||||
t.prevalueTracer.Put(prefix, blob)
|
||||
|
||||
// The returned node blob won't be changed afterward. No need to
|
||||
// deep-copy the slice.
|
||||
|
|
@ -673,7 +673,7 @@ func (t *Trie) deletedNodes() [][]byte {
|
|||
var (
|
||||
pos int
|
||||
list = t.opTracer.deletedList()
|
||||
flags = t.prevalueTracer.hasList(list)
|
||||
flags = t.prevalueTracer.HasList(list)
|
||||
)
|
||||
for i := 0; i < len(list); i++ {
|
||||
if flags[i] {
|
||||
|
|
@ -753,7 +753,7 @@ func (t *Trie) hashRoot() []byte {
|
|||
|
||||
// Witness returns a set containing all trie nodes that have been accessed.
|
||||
func (t *Trie) Witness() map[string][]byte {
|
||||
return t.prevalueTracer.values()
|
||||
return t.prevalueTracer.Values()
|
||||
}
|
||||
|
||||
// Reset drops the referenced root node and cleans all internal state.
|
||||
|
|
@ -763,6 +763,6 @@ func (t *Trie) Reset() {
|
|||
t.unhashed = 0
|
||||
t.uncommitted = 0
|
||||
t.opTracer.reset()
|
||||
t.prevalueTracer.reset()
|
||||
t.prevalueTracer.Reset()
|
||||
t.committed = false
|
||||
}
|
||||
|
|
|
|||
|
|
@ -22,39 +22,39 @@ import (
|
|||
"github.com/ethereum/go-ethereum/triedb/database"
|
||||
)
|
||||
|
||||
// TrieReader is a wrapper of the underlying node reader. It's not safe
|
||||
// Reader is a wrapper of the underlying database reader. It's not safe
|
||||
// for concurrent usage.
|
||||
type TrieReader struct {
|
||||
type Reader struct {
|
||||
owner common.Hash
|
||||
reader database.NodeReader
|
||||
banned map[string]struct{} // Marker to prevent node from being accessed, for tests
|
||||
}
|
||||
|
||||
// NewTrieReader initializes the trie reader with the given node reader.
|
||||
func NewTrieReader(stateRoot, owner common.Hash, db database.NodeDatabase) (*TrieReader, error) {
|
||||
// NewReader initializes the trie reader with the given database reader.
|
||||
func NewReader(stateRoot, owner common.Hash, db database.NodeDatabase) (*Reader, error) {
|
||||
if stateRoot == (common.Hash{}) || stateRoot == types.EmptyRootHash {
|
||||
return &TrieReader{owner: owner}, nil
|
||||
return &Reader{owner: owner}, nil
|
||||
}
|
||||
reader, err := db.NodeReader(stateRoot)
|
||||
if err != nil {
|
||||
return nil, &MissingNodeError{Owner: owner, NodeHash: stateRoot, err: err}
|
||||
}
|
||||
return &TrieReader{owner: owner, reader: reader}, nil
|
||||
return &Reader{owner: owner, reader: reader}, nil
|
||||
}
|
||||
|
||||
// newEmptyReader initializes the pure in-memory reader. All read operations
|
||||
// should be forbidden and returns the MissingNodeError.
|
||||
func newEmptyReader() *TrieReader {
|
||||
return &TrieReader{}
|
||||
func newEmptyReader() *Reader {
|
||||
return &Reader{}
|
||||
}
|
||||
|
||||
// node retrieves the rlp-encoded trie node with the provided trie node
|
||||
// Node retrieves the rlp-encoded trie node with the provided trie node
|
||||
// information. An MissingNodeError will be returned in case the node is
|
||||
// not found or any error is encountered.
|
||||
//
|
||||
// Don't modify the returned byte slice since it's not deep-copied and
|
||||
// still be referenced by database.
|
||||
func (r *TrieReader) Node(path []byte, hash common.Hash) ([]byte, error) {
|
||||
func (r *Reader) Node(path []byte, hash common.Hash) ([]byte, error) {
|
||||
// Perform the logics in tests for preventing trie node access.
|
||||
if r.banned != nil {
|
||||
if _, ok := r.banned[string(path)]; ok {
|
||||
|
|
|
|||
|
|
@ -41,13 +41,13 @@ var (
|
|||
type VerkleTrie struct {
|
||||
root verkle.VerkleNode
|
||||
cache *utils.PointCache
|
||||
reader *TrieReader
|
||||
reader *Reader
|
||||
tracer *PrevalueTracer
|
||||
}
|
||||
|
||||
// NewVerkleTrie constructs a verkle tree based on the specified root hash.
|
||||
func NewVerkleTrie(root common.Hash, db database.NodeDatabase, cache *utils.PointCache) (*VerkleTrie, error) {
|
||||
reader, err := NewTrieReader(root, common.Hash{}, db)
|
||||
reader, err := NewReader(root, common.Hash{}, db)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
|
@ -55,7 +55,7 @@ func NewVerkleTrie(root common.Hash, db database.NodeDatabase, cache *utils.Poin
|
|||
root: verkle.New(),
|
||||
cache: cache,
|
||||
reader: reader,
|
||||
tracer: newPrevalueTracer(),
|
||||
tracer: NewPrevalueTracer(),
|
||||
}
|
||||
// Parse the root verkle node if it's not empty.
|
||||
if root != types.EmptyVerkleHash && root != types.EmptyRootHash {
|
||||
|
|
@ -326,7 +326,7 @@ func (t *VerkleTrie) Copy() *VerkleTrie {
|
|||
root: t.root.Copy(),
|
||||
cache: t.cache,
|
||||
reader: t.reader,
|
||||
tracer: t.tracer.copy(),
|
||||
tracer: t.tracer.Copy(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -451,7 +451,7 @@ func (t *VerkleTrie) nodeResolver(path []byte) ([]byte, error) {
|
|||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.tracer.put(path, blob)
|
||||
t.tracer.Put(path, blob)
|
||||
return blob, nil
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue