erigon-pulse/turbo/trie/trie_observers_test.go

460 lines
12 KiB
Go
Raw Normal View History

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) {
Merge account and storage resolvers (#504) * add_incarnation_to_acc_root_in_ih * merge cached resolver into stateful resolver * - move account root set to "post iteration" of resolver - rename "cache" to IntermediateHash * remove blockNR and bucket params from walker * fix out of range panic * calc acc.Root on the fly * remove fieldSet field from resolver, make logic of root - lazy * remove 2 parameters * working version of forward-only walk over Acc and Storage * improve test * rebase master * save progress - more tests for PrepareResolveParams, add dedicated ResolveSet for storage. Problem: See duplicates in ResolveSet hexes. Next test failing: oracle_test.go * skip old incarnations * don't rebuild when 0 requests * fix tests * start from account key when need resolve storage * Error: stateless prototype faced hashNode when extracting witness * Statless works: copy touches * Remove getAccRoot function * Remove "isAccount" parameter from resolver signature * Fix: use correct storageResolveSet in finaliseStorageRoot * Fix: when startKey changed - reset storage buffers also * Fix: if account incarnation=0 - set EmptyRoot * Fix: remove account roots by default from IntermediateHash bucket * Fix: skip abandoned storage - which appeared just after startKey * Fix: did reset acc key incorrectly * Fix: clean previous key if receive IH * Fix: IH observer - subscribe only to branch nodes (was subscribed to value nodes also) * Add DISABLE_IH and STORE_ACCOUNT_ROOT env variables for tests * Remove accNode from IH cycle * Fix flags * Fix: reset succStorage also * Fix: skip IH if it has wrong incarnation * Fix: skip IH if it has wrong incarnation * Fix: skip IH if it has wrong incarnation * Fix: use rssStorage to HashOnly check * Fix: remove termination symbol from resolveRequest * cleanup * Fix: skip abandoned storage after IH * Debug This reverts commit 9c5eb69465f25607d546b03359b2cbcb1bd46689. * Fix linters * add_incarnation_to_acc_root_in_ih * merge cached resolver into stateful resolver * - move account root set to "post iteration" of resolver - rename "cache" to IntermediateHash * remove blockNR and bucket params from walker * fix out of range panic * calc acc.Root on the fly * remove fieldSet field from resolver, make logic of root - lazy * remove 2 parameters * working version of forward-only walk over Acc and Storage * improve test * rebase master * save progress - more tests for PrepareResolveParams, add dedicated ResolveSet for storage. Problem: See duplicates in ResolveSet hexes. Next test failing: oracle_test.go * skip old incarnations * don't rebuild when 0 requests * fix tests * start from account key when need resolve storage * Error: stateless prototype faced hashNode when extracting witness * Statless works: copy touches * Remove getAccRoot function * Remove "isAccount" parameter from resolver signature * Fix: use correct storageResolveSet in finaliseStorageRoot * Fix: when startKey changed - reset storage buffers also * Fix: if account incarnation=0 - set EmptyRoot * Fix: remove account roots by default from IntermediateHash bucket * Fix: skip abandoned storage - which appeared just after startKey * Fix: did reset acc key incorrectly * Fix: clean previous key if receive IH * Fix: IH observer - subscribe only to branch nodes (was subscribed to value nodes also) * Add DISABLE_IH and STORE_ACCOUNT_ROOT env variables for tests * Remove accNode from IH cycle * Fix flags * Fix: reset succStorage also * Fix: skip IH if it has wrong incarnation * Fix: skip IH if it has wrong incarnation * Fix: skip IH if it has wrong incarnation * Fix: use rssStorage to HashOnly check * Fix: remove termination symbol from resolveRequest * cleanup * Fix: skip abandoned storage after IH * remove inc * remove inc from rss * tr.succStorage.Reset() * remove inc from rss * Remove hard-coding * succ.Reset * Enable CalcTrieRoots * Proper dumping of the trie * Debug * Fix for CalcTrieRoot * Fix another inteference bug * Temp * Fix test * Cleanup * remove STORE_ACCOUNT_ROOT=true flag * Fix linter * Fix linter * Disable getnodedata by default * Fix test * Fix test Co-authored-by: Alexey Akhunov <akhounov@gmail.com>
2020-05-02 18:00:57 +00:00
}
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")
}