erigon-pulse/trie/trie_observers_test.go

460 lines
12 KiB
Go

package trie
import (
"fmt"
"math/rand"
"testing"
"github.com/ledgerwatch/turbo-geth/common"
"github.com/ledgerwatch/turbo-geth/common/dbutils"
"github.com/ledgerwatch/turbo-geth/core/types/accounts"
"github.com/ledgerwatch/turbo-geth/crypto"
"github.com/stretchr/testify/assert"
)
func genAccount() *accounts.Account {
acc := &accounts.Account{}
acc.Nonce = 123
return acc
}
func genNKeys(n int) [][]byte {
result := make([][]byte, n)
for i := range result {
result[i] = crypto.Keccak256([]byte{0x0, 0x0, 0x0, byte(i % 256), byte(i / 256)})
}
return result
}
func genByteArrayOfLen(n int) []byte {
result := make([]byte, n)
for i := range result {
result[i] = byte(rand.Intn(255))
}
return result
}
type partialObserver struct {
NoopObserver
callbackCalled bool
}
func (o *partialObserver) BranchNodeDeleted(_ []byte) {
o.callbackCalled = true
}
type mockObserver struct {
createdNodes map[string]uint
deletedNodes map[string]struct{}
touchedNodes map[string]int
unloadedNodes map[string]int
unloadedNodeHashes map[string][]byte
reloadedNodes map[string]int
}
func newMockObserver() *mockObserver {
return &mockObserver{
createdNodes: make(map[string]uint),
deletedNodes: make(map[string]struct{}),
touchedNodes: make(map[string]int),
unloadedNodes: make(map[string]int),
unloadedNodeHashes: make(map[string][]byte),
reloadedNodes: make(map[string]int),
}
}
func (m *mockObserver) BranchNodeCreated(hex []byte) {
m.createdNodes[common.Bytes2Hex(hex)] = 1
}
func (m *mockObserver) CodeNodeCreated(hex []byte, size uint) {
m.createdNodes[common.Bytes2Hex(hex)] = size
}
func (m *mockObserver) BranchNodeDeleted(hex []byte) {
m.deletedNodes[common.Bytes2Hex(hex)] = struct{}{}
}
func (m *mockObserver) CodeNodeDeleted(hex []byte) {
m.deletedNodes[common.Bytes2Hex(hex)] = struct{}{}
}
func (m *mockObserver) BranchNodeTouched(hex []byte) {
value := m.touchedNodes[common.Bytes2Hex(hex)]
value++
m.touchedNodes[common.Bytes2Hex(hex)] = value
}
func (m *mockObserver) CodeNodeTouched(hex []byte) {
value := m.touchedNodes[common.Bytes2Hex(hex)]
value++
m.touchedNodes[common.Bytes2Hex(hex)] = value
}
func (m *mockObserver) CodeNodeSizeChanged(hex []byte, newSize uint) {
m.createdNodes[common.Bytes2Hex(hex)] = newSize
}
func (m *mockObserver) WillUnloadBranchNode(hex []byte, hash common.Hash, incarnation uint64) {
}
func (m *mockObserver) WillUnloadNode(hex []byte, hash common.Hash) {
dictKey := common.Bytes2Hex(hex)
value := m.unloadedNodes[dictKey]
value++
m.unloadedNodes[dictKey] = value
m.unloadedNodeHashes[dictKey] = common.CopyBytes(hash[:])
}
func (m *mockObserver) BranchNodeLoaded(hex []byte, incarnation uint64) {
dictKey := common.Bytes2Hex(hex)
value := m.reloadedNodes[dictKey]
value++
m.reloadedNodes[dictKey] = value
}
func TestObserversBranchNodesCreateDelete(t *testing.T) {
trie := newEmpty()
observer := newMockObserver()
trie.AddObserver(observer)
keys := genNKeys(100)
for _, key := range keys {
acc := genAccount()
code := genByteArrayOfLen(100)
codeHash := crypto.Keccak256(code)
acc.CodeHash = common.BytesToHash(codeHash)
trie.UpdateAccount(key, acc)
trie.UpdateAccountCode(key, codeNode(code)) //nolint:errcheck
}
expectedNodes := calcSubtreeNodes(trie.root)
assert.True(t, expectedNodes >= 100, "should register all code nodes")
assert.Equal(t, expectedNodes, len(observer.createdNodes), "should register all")
for _, key := range keys {
trie.Delete(key)
}
assert.Equal(t, expectedNodes, len(observer.deletedNodes), "should register all")
}
func TestObserverCodeSizeChanged(t *testing.T) {
rand.Seed(9999)
trie := newEmpty()
observer := newMockObserver()
trie.AddObserver(observer)
keys := genNKeys(10)
for _, key := range keys {
acc := genAccount()
code := genByteArrayOfLen(100)
codeHash := crypto.Keccak256(code)
acc.CodeHash = common.BytesToHash(codeHash)
trie.UpdateAccount(key, acc)
trie.UpdateAccountCode(key, codeNode(code)) //nolint:errcheck
hex := keybytesToHex(key)
hex = hex[:len(hex)-1]
newSize, ok := observer.createdNodes[common.Bytes2Hex(hex)]
assert.True(t, ok, "account should be registed as created")
assert.Equal(t, 100, int(newSize), "account size should increase when the account code grows")
code2 := genByteArrayOfLen(50)
codeHash2 := crypto.Keccak256(code2)
acc.CodeHash = common.BytesToHash(codeHash2)
trie.UpdateAccount(key, acc)
trie.UpdateAccountCode(key, codeNode(code2)) //nolint:errcheck
newSize2, ok := observer.createdNodes[common.Bytes2Hex(hex)]
assert.True(t, ok, "account should be registed as created")
assert.Equal(t, -50, int(newSize2)-int(newSize), "account size should decrease when the account code shrinks")
}
}
func TestObserverUnloadStorageNodes(t *testing.T) {
rand.Seed(9999)
trie := newEmpty()
observer := newMockObserver()
trie.AddObserver(observer)
key := genNKeys(1)[0]
storageKeys := genNKeys(10)
// group all storage keys into a single fullNode
for i := range storageKeys {
storageKeys[i][0] = byte(0)
storageKeys[i][1] = byte(i)
}
fullKeys := make([][]byte, len(storageKeys))
for i, storageKey := range storageKeys {
fullKey := dbutils.GenerateCompositeTrieKey(common.BytesToHash(key), common.BytesToHash(storageKey))
fullKeys[i] = fullKey
}
acc := genAccount()
trie.UpdateAccount(key, acc)
for i, fullKey := range fullKeys {
trie.Update(fullKey, []byte(fmt.Sprintf("test-value-%d", i)))
}
rootHash := trie.Hash()
// adding nodes doesn't add anything
assert.Equal(t, 0, len(observer.reloadedNodes), "adding nodes doesn't add anything")
// unloading nodes adds to the list
hex := keybytesToHex(key)
hex = hex[:len(hex)-1]
hex = append(hex, 0x0, 0x0, 0x0)
trie.EvictNode(hex)
newRootHash := trie.Hash()
assert.Equal(t, rootHash, newRootHash, "root hash shouldn't change")
assert.Equal(t, 1, len(observer.unloadedNodes), "should unload one full node")
hex = keybytesToHex(key)
storageKey := fmt.Sprintf("%s000000", common.Bytes2Hex(hex[:len(hex)-1]))
assert.Equal(t, 1, observer.unloadedNodes[storageKey], "should unload structure nodes")
accNode, ok := trie.getAccount(trie.root, hex, 0)
assert.True(t, ok, "account should be found")
sn, ok := accNode.storage.(*shortNode)
assert.True(t, ok, "storage should be the shortnode contaning hash")
_, ok = sn.Val.(hashNode)
assert.True(t, ok, "storage should be the shortnode contaning hash")
}
func TestObserverLoadNodes(t *testing.T) {
rand.Seed(9999)
subtrie := newEmpty()
observer := newMockObserver()
// this test needs a specific trie structure
// ( full )
// (full) (duo) (duo)
// (short)(short)(short) (short)(short) (short)(short)
// (acc1) (acc2) (acc3) (acc4) (acc5) (acc6) (acc7)
//
// to ensure this structure we override prefixes of
// random account keys with the follwing paths
prefixes := [][]byte{
{0x00, 0x00}, //acc1
{0x00, 0x02}, //acc2
{0x00, 0x05}, //acc3
{0x02, 0x02}, //acc4
{0x02, 0x05}, //acc5
{0x0A, 0x00}, //acc6
{0x0A, 0x03}, //acc7
}
keys := genNKeys(7)
for i := range keys {
copy(keys[i][:2], prefixes[i])
}
storageKeys := genNKeys(10)
// group all storage keys into a single fullNode
for i := range storageKeys {
storageKeys[i][0] = byte(0)
storageKeys[i][1] = byte(i)
}
for _, key := range keys {
acc := genAccount()
subtrie.UpdateAccount(key, acc)
for i, storageKey := range storageKeys {
fullKey := dbutils.GenerateCompositeTrieKey(common.BytesToHash(key), common.BytesToHash(storageKey))
subtrie.Update(fullKey, []byte(fmt.Sprintf("test-value-%d", i)))
}
}
trie := newEmpty()
trie.AddObserver(observer)
hash := subtrie.Hash()
//nolint:errcheck
trie.hook([]byte{}, subtrie.root, hash[:])
// fullNode
assert.Equal(t, 1, observer.reloadedNodes["000000"], "should reload structure nodes")
// duoNode
assert.Equal(t, 1, observer.reloadedNodes["000200"], "should reload structure nodes")
// duoNode
assert.Equal(t, 1, observer.reloadedNodes["000a00"], "should reload structure nodes")
// root
assert.Equal(t, 1, observer.reloadedNodes["00"], "should reload structure nodes")
// check storages (should have a single fullNode per account)
for _, key := range keys {
hex := keybytesToHex(key)
storageKey := fmt.Sprintf("%s000000", common.Bytes2Hex(hex[:len(hex)-1]))
assert.Equal(t, 1, observer.reloadedNodes[storageKey], "should reload structure nodes")
}
}
func TestObserverTouches(t *testing.T) {
rand.Seed(9999)
trie := newEmpty()
observer := newMockObserver()
trie.AddObserver(observer)
keys := genNKeys(3)
for i := range keys {
keys[i][0] = 0x00 // 3 belong to the same branch
}
var acc *accounts.Account
for _, key := range keys {
// creation touches the account
acc = genAccount()
trie.UpdateAccount(key, acc)
}
key := keys[0]
branchNodeHex := "0000"
assert.Equal(t, uint(1), observer.createdNodes[branchNodeHex], "node is created")
assert.Equal(t, 1, observer.touchedNodes[branchNodeHex])
// updating touches the account
code := genByteArrayOfLen(100)
codeHash := crypto.Keccak256(code)
acc.CodeHash = common.BytesToHash(codeHash)
trie.UpdateAccount(key, acc)
assert.Equal(t, 2, observer.touchedNodes[branchNodeHex])
// updating code touches the account
trie.UpdateAccountCode(key, codeNode(code)) //nolint:errcheck
// 2 touches -- retrieve + updae
assert.Equal(t, 4, observer.touchedNodes[branchNodeHex])
// changing storage touches the account
storageKey := genNKeys(1)[0]
fullKey := dbutils.GenerateCompositeTrieKey(common.BytesToHash(key), common.BytesToHash(storageKey))
trie.Update(fullKey, []byte("value-1"))
assert.Equal(t, 5, observer.touchedNodes[branchNodeHex])
trie.Update(fullKey, []byte("value-2"))
assert.Equal(t, 6, observer.touchedNodes[branchNodeHex])
// getting storage touches the account
_, ok := trie.Get(fullKey)
assert.True(t, ok, "should be able to receive storage")
assert.Equal(t, 7, observer.touchedNodes[branchNodeHex])
// deleting storage touches the account
trie.Delete(fullKey)
assert.Equal(t, 8, observer.touchedNodes[branchNodeHex])
// getting code touches the account
_, ok = trie.GetAccountCode(key)
assert.True(t, ok, "should be able to receive code")
assert.Equal(t, 9, observer.touchedNodes[branchNodeHex])
// getting account touches the account
_, ok = trie.GetAccount(key)
assert.True(t, ok, "should be able to receive account")
assert.Equal(t, 10, observer.touchedNodes[branchNodeHex])
}
func TestObserverMux(t *testing.T) {
trie := newEmpty()
observer1 := newMockObserver()
observer2 := newMockObserver()
mux := NewTrieObserverMux()
mux.AddChild(observer1)
mux.AddChild(observer2)
trie.AddObserver(mux)
keys := genNKeys(100)
for _, key := range keys {
acc := genAccount()
trie.UpdateAccount(key, acc)
code := genByteArrayOfLen(100)
codeHash := crypto.Keccak256(code)
acc.CodeHash = common.BytesToHash(codeHash)
trie.UpdateAccount(key, acc)
trie.UpdateAccountCode(key, codeNode(code)) //nolint:errcheck
_, ok := trie.GetAccount(key)
assert.True(t, ok, "acount should be found")
}
trie.Hash()
for i, key := range keys {
if i < 80 {
trie.Delete(key)
} else {
hex := keybytesToHex(key)
hex = hex[:len(hex)-1]
trie.EvictNode(CodeKeyFromAddrHash(hex))
}
}
assert.Equal(t, observer1.createdNodes, observer2.createdNodes, "should propagate created events")
assert.Equal(t, observer1.deletedNodes, observer2.deletedNodes, "should propagate deleted events")
assert.Equal(t, observer1.touchedNodes, observer2.touchedNodes, "should propagate touched events")
assert.Equal(t, observer1.unloadedNodes, observer2.unloadedNodes, "should propagage unloads")
assert.Equal(t, observer1.unloadedNodeHashes, observer2.unloadedNodeHashes, "should propagage unloads")
assert.Equal(t, observer1.reloadedNodes, observer2.reloadedNodes, "should propagage reloads")
}
func TestObserverPartial(t *testing.T) {
trie := newEmpty()
observer := &partialObserver{callbackCalled: false} // only implements `BranchNodeDeleted`
trie.AddObserver(observer)
keys := genNKeys(2)
for _, key := range keys {
acc := genAccount()
trie.UpdateAccount(key, acc)
code := genByteArrayOfLen(100)
codeHash := crypto.Keccak256(code)
acc.CodeHash = common.BytesToHash(codeHash)
trie.UpdateAccount(key, acc)
trie.UpdateAccountCode(key, codeNode(code)) //nolint:errcheck
}
for _, key := range keys {
trie.Delete(key)
}
assert.True(t, observer.callbackCalled, "should be called")
}