erigon-pulse/turbo/trie/trie_root_test.go
a 436493350e
Sentinel refactor (#8296)
1. changes sentinel to use an http-like interface

2. moves hexutil, crypto/blake2b, metrics packages to erigon-lib
2023-10-22 01:17:18 +02:00

651 lines
25 KiB
Go

package trie_test
import (
"bytes"
"context"
"encoding/binary"
"github.com/ledgerwatch/erigon-lib/common/hexutil"
"github.com/ledgerwatch/erigon-lib/kv/dbutils"
"math/big"
"testing"
"time"
"github.com/holiman/uint256"
libcommon "github.com/ledgerwatch/erigon-lib/common"
"github.com/ledgerwatch/erigon-lib/common/hexutility"
"github.com/ledgerwatch/erigon-lib/kv"
"github.com/ledgerwatch/erigon-lib/kv/memdb"
"github.com/ledgerwatch/erigon/core/types/accounts"
"github.com/ledgerwatch/erigon/crypto"
"github.com/ledgerwatch/erigon/eth/stagedsync"
"github.com/ledgerwatch/erigon/turbo/trie"
"github.com/ledgerwatch/log/v3"
"github.com/stretchr/testify/require"
)
// initialFlatDBTrieBuild leverages the stagedsync code to perform the initial
// trie computation while also collecting the assorted hashes and loading them
// into the TrieOfAccounts and TrieOfStorage tables
func initialFlatDBTrieBuild(t *testing.T, db kv.RwDB) libcommon.Hash {
t.Helper()
//startTime := time.Now()
tx, err := db.BeginRw(context.Background())
require.NoError(t, err)
defer tx.Rollback()
stageTrieCfg := stagedsync.StageTrieCfg(db, false, false, false, t.TempDir(), nil, nil, false, nil)
hash, err := stagedsync.RegenerateIntermediateHashes("test", tx, stageTrieCfg, libcommon.Hash{}, context.Background(), log.New())
require.NoError(t, err)
tx.Commit()
//t.Logf("Initial hash is %s and took %v", hash, time.Since(startTime))
return hash
}
// seedInitialAccounts uses the provided hashes to commit simpleAccountValBytes
// at each corresponding key in the HashedAccounts table.
func seedInitialAccounts(t *testing.T, db kv.RwDB, hashes []libcommon.Hash) {
tx, err := db.BeginRw(context.Background())
require.NoError(t, err)
defer tx.Rollback()
for _, hash := range hashes {
//t.Logf("Seeding initial account with hash %s", hash)
err = tx.Put(kv.HashedAccounts, hash[:], simpleAccountValBytes)
require.NoError(t, err)
}
err = tx.Commit()
require.NoError(t, err)
}
// seedModifiedAccounts uses the provided hashes to commit
// simpleModifiedAccountValBytes at each corresponding key in the HashedAccounts
// table. It returns a slice of keys to be used when constructing the retain
// list for the FlatDBTrie computations.
func seedModifiedAccounts(t *testing.T, db kv.RwDB, hashes []libcommon.Hash) [][]byte {
toRetain := make([][]byte, 0, len(hashes))
tx, err := db.BeginRw(context.Background())
require.NoError(t, err)
defer tx.Rollback()
for _, hash := range hashes {
//t.Logf("Seeding modified account with hash %s", hash)
err = tx.Put(kv.HashedAccounts, hash[:], simpleModifiedAccountValBytes)
require.NoError(t, err)
toRetain = append(toRetain, append([]byte{}, hash[:]...))
}
err = tx.Commit()
require.NoError(t, err)
return toRetain
}
// seedInitialStorage creates a single account with hash storageAccountHash in
// the HashedAccounts table, as well as a storage entry in HashedStorage for
// this account with value storageInitialValue for each provided hash. A slice
// of the constructed storage keys is returned.
func seedInitialStorage(t *testing.T, db kv.RwDB, hashes []libcommon.Hash) [][]byte {
// We seed a single account, and all storage keys belong to this account.
keys := make([][]byte, 0, len(hashes))
tx, err := db.BeginRw(context.Background())
require.NoError(t, err)
defer tx.Rollback()
err = tx.Put(kv.HashedAccounts, storageAccountHash[:], simpleContractAccountValBytes)
require.NoError(t, err)
var storageKey [72]byte
binary.BigEndian.PutUint64(storageKey[32:40], 1)
for _, hash := range hashes {
copy(storageKey[:32], storageAccountHash[:])
copy(storageKey[40:], hash[:])
//t.Logf("Seeding storage with key 0x%x", storageKey)
err = tx.Put(kv.HashedStorage, storageKey[:], storageInitialValue[:])
require.NoError(t, err)
keys = append(keys, append([]byte{}, storageKey[:]...))
}
err = tx.Commit()
require.NoError(t, err)
return keys
}
// seedModifedStorage writes a storage entry in HashedStorage for
// storageAccountHash with value storageModifiedValue for each provided hash. A
// slice containing the modified keys is returned to help the caller construct
// the retain list necessary for the FlatDBTrie computations.
func seedModifiedStorage(t *testing.T, db kv.RwDB, hashes []libcommon.Hash) [][]byte {
// Modify this same account's storage entries. For now, this test only
// validates modifying existing or adding new keys (no deletions).
toRetain := make([][]byte, 0, len(hashes))
var storageKey [72]byte
binary.BigEndian.PutUint64(storageKey[32:40], 1)
tx, err := db.BeginRw(context.Background())
require.NoError(t, err)
defer tx.Rollback()
for _, hash := range hashes {
copy(storageKey[:32], storageAccountHash[:])
copy(storageKey[40:], hash[:])
//t.Logf("Seeding storage with modified hash 0x%x", storageKey)
err = tx.Put(kv.HashedStorage, storageKey[:], storageModifiedValue[:])
require.NoError(t, err)
toRetain = append(toRetain, append([]byte{}, storageKey[:]...))
}
err = tx.Commit()
require.NoError(t, err)
return toRetain
}
// rebuildFlatDBTrieHash will re-compute the root hash using the given retain
// list. Even without a retain list, this is not a no-op in the accounts case,
// as the root of the trie is not actually stored and must be re-computed
// regardless. However, for storage entries at least one element must be in the
// retain list or the root is simply returned.
func rebuildFlatDBTrieHash(t *testing.T, rl *trie.RetainList, db kv.RoDB) libcommon.Hash {
t.Helper()
//startTime := time.Now()
loader := trie.NewFlatDBTrieLoader("test", rl, nil, nil, false)
tx, err := db.BeginRo(context.Background())
defer tx.Rollback()
require.NoError(t, err)
hash, err := loader.CalcTrieRoot(tx, nil)
tx.Rollback()
require.NoError(t, err)
//t.Logf("Rebuilt hash is %s and took %v", hash, time.Since(startTime))
return hash
}
// proveFlatDB will generate an account proof result utilizing the FlatDBTrie
// hashing mechanism. The proofKeys cannot span more than one account (ie, an
// account and its storage nodes is acceptable). It returns the root hash for
// the computation as well as the proof result.
func proveFlatDB(t *testing.T, db kv.RoDB, accountMissing bool, retainKeys, proofKeys [][]byte) (libcommon.Hash, *accounts.AccProofResult) {
t.Helper()
//startTime := time.Now()
rl := trie.NewRetainList(0)
for _, retainKey := range retainKeys {
rl.AddKeyWithMarker(retainKey, true)
}
loader := trie.NewFlatDBTrieLoader("test", rl, nil, nil, false)
acc := &accounts.Account{}
if !accountMissing {
acc.Incarnation = 1
acc.Initialised = true
}
pr := trie.NewManualProofRetainer(t, acc, rl, proofKeys)
loader.SetProofRetainer(pr)
tx, err := db.BeginRo(context.Background())
defer tx.Rollback()
require.NoError(t, err)
hash, err := loader.CalcTrieRoot(tx, nil)
tx.Rollback()
require.NoError(t, err)
//t.Logf("Proof root hash is %s and took %v", hash, time.Since(startTime))
res, err := pr.ProofResult()
require.NoError(t, err)
return hash, res
}
// logTrieTables simply writes the TrieOfAccounts and TrieOfStorage to the test
// output. It can be very helpful when debugging failed fuzzing cases.
func logTrieTables(t *testing.T, db kv.RoDB) { //nolint
t.Helper()
tx, err := db.BeginRo(context.Background())
require.NoError(t, err)
defer tx.Rollback()
trieAccC, err := tx.Cursor(kv.TrieOfAccounts)
require.NoError(t, err)
defer trieAccC.Close()
var k, v []byte
t.Logf("TrieOfAccounts iteration begin")
for k, v, err = trieAccC.First(); err == nil && k != nil; k, v, err = trieAccC.Next() {
t.Logf("\t%x=%x", k, v)
}
require.NoError(t, err)
t.Logf("TrieOfAccounts iteration end")
trieStorageC, err := tx.CursorDupSort(kv.TrieOfStorage)
require.NoError(t, err)
defer trieStorageC.Close()
t.Logf("TrieOfStorage iteration begin")
for k, v, err = trieStorageC.First(); err == nil && k != nil; k, v, err = trieStorageC.Next() {
t.Logf("\t%x=%x", k, v)
}
require.NoError(t, err)
t.Logf("TrieStorage iteration end")
}
var (
// simpleAccountValBytes is a simple marshaled EoA without storage or balance
simpleAccountValBytes = func() []byte {
accVal := accounts.Account{Nonce: 1, Initialised: true}
encodedAccVal := make([]byte, accVal.EncodingLengthForStorage())
accVal.EncodeForStorage(encodedAccVal)
return encodedAccVal
}()
simpleModifiedAccountValBytes = func() []byte {
accVal := accounts.Account{Nonce: 2, Initialised: true}
encodedAccVal := make([]byte, accVal.EncodingLengthForStorage())
accVal.EncodeForStorage(encodedAccVal)
return encodedAccVal
}()
// simpleContractAccountValBytes is a simple marshaled account with storage and code
simpleContractAccountValBytes = func() []byte {
accVal := accounts.Account{Nonce: 1, Initialised: true, Incarnation: 1, CodeHash: libcommon.Hash{9}}
encodedAccVal := make([]byte, accVal.EncodingLengthForStorage())
accVal.EncodeForStorage(encodedAccVal)
return encodedAccVal
}()
storageAccountHash = libcommon.Hash{1}
storageAccountCodeHash = libcommon.Hash{9}
storageInitialValue = libcommon.Hash{2}
storageModifiedValue = libcommon.Hash{3}
// missingHash is a constant value that is impossible for the fuzzers to
// generate (since the last 26 bytes of all fuzzing keys must be 0). The
// tests know that when this value is encountered the expected result should
// be 'missing', ie in the storage proof the value should be 0, and in the
// account proof the nonce/codeHash/storageHash/balance should be empty.
missingHash = libcommon.HexToHash("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa")
)
// naiveTriesAndHashFromDB constructs the naive trie implementation by simply adding
// all of the accounts and storage keys linearly. It also returns a map of
// storage tries
func naiveTriesAndHashFromDB(t *testing.T, db kv.RoDB) (*trie.Trie, *trie.Trie, libcommon.Hash) {
t.Helper()
startTime := time.Now()
testTrie := trie.NewTestRLPTrie(libcommon.Hash{})
tx, err := db.BeginRo(context.Background())
require.NoError(t, err)
defer tx.Rollback()
ac, err := tx.Cursor(kv.HashedAccounts)
require.NoError(t, err)
defer ac.Close()
var k, v []byte
var storageTrie *trie.Trie
for k, v, err = ac.First(); k != nil; k, v, err = ac.Next() {
accHash := k[:32]
var acc accounts.Account
err := acc.DecodeForStorage(v)
require.NoError(t, err)
sc, err := tx.Cursor(kv.HashedStorage)
require.NoError(t, err)
defer sc.Close()
startStorageKey := dbutils.GenerateCompositeStorageKey(libcommon.BytesToHash(k), acc.Incarnation, libcommon.Hash{})
endStorageKey := dbutils.GenerateCompositeStorageKey(libcommon.BytesToHash(k), acc.Incarnation+1, libcommon.Hash{})
storageTrie = trie.NewTestRLPTrie(libcommon.Hash{})
for k, v, err = sc.Seek(startStorageKey); k != nil; k, v, err = sc.Next() {
if bytes.Compare(k, endStorageKey) >= 0 {
break
}
sk := k[32+8:]
sv := append([]byte{}, v...)
esv, err := trie.EncodeAsValue(sv)
require.NoError(t, err)
storageTrie.Update(sk, esv)
}
require.NoError(t, err)
acc.Root = storageTrie.Hash()
accBytes := make([]byte, acc.EncodingLengthForHashing())
acc.EncodeForHashing(accBytes)
testTrie.Update(accHash, accBytes)
}
require.NoError(t, err)
naiveHash := testTrie.Hash()
t.Logf("Naive hash is %s and took %v", naiveHash, time.Since(startTime))
return testTrie, storageTrie, naiveHash
}
// addFuzzTrieSeeds adds a common set of trie hashes which are problematic for
// the flatDB trie hash construction. Because there is a code path for account
// tries and a slightly separate one for storage tries, we use this common set
// for both. These seeeds were constructed by executing the fuzzing until a
// failing case was detected, then refining to the minimal set of keys (and
// re-naming nibbles to be more readable if possible").
func addFuzzTrieSeeds(f *testing.F) {
f.Helper()
noModificationTests := [][]string{
{"0x0a00", "0x0bc0", "0x0bd0"},
{"0xff00", "0xfff0", "0xffff"},
{"0xa000", "0xaa00", "0xaaa0", "0xb000", "0xcc00", "0xccc0", "0xcdd0"},
{"0xa000", "0xaa00", "0xbb00", "0xbbb0", "0xbbbb"},
{"0xa00000", "0xaaa000", "0xaaaa00", "0xaaaaa0", "0xb00000", "0xccc000", "0xccc0c0", "0xcccc00"},
{"a000", "b000", "b0b0", "bbb0"},
{"a00000", "bbbb00", "bbbb0b", "bbbbb0", "c00000", "cc0000", "ccc000"},
}
for _, tt := range noModificationTests {
var buffer bytes.Buffer
fragLen := len(tt[0])
require.Equal(f, 0, fragLen%2, "Fragments must be even in length")
for _, fragment := range tt {
buffer.Write(hexutility.FromHex(fragment))
require.Len(f, fragment, fragLen, "All fragments must be the same length")
}
f.Add(len(tt), 0, buffer.Bytes())
}
modificationTests := []struct {
initial []string
modified []string
}{
{initial: []string{"0xaa"}, modified: []string{"0xbb"}},
{initial: []string{"0xa000", "0xa0aa", "0xaaaa"}, modified: []string{"0xa00a"}},
{initial: []string{"0xa000", "0xa0a0", "0xab00", "0xabb0"}, modified: []string{"0xaa00"}},
{initial: []string{"0xaaa0", "0xaaaa", "0xaaba", "0xaba0", "0xabb0"}, modified: []string{"0xaaa0", "0xaba0"}},
}
for _, tt := range modificationTests {
var buffer bytes.Buffer
fragLen := len(tt.initial[0])
require.Equal(f, 0, fragLen%2, "Fragments must be even in length")
for _, fragment := range tt.initial {
buffer.Write(hexutility.FromHex(fragment))
require.Len(f, fragment, fragLen, "All fragments must be the same length")
}
for _, fragment := range tt.modified {
buffer.Write(hexutility.FromHex(fragment))
require.Len(f, fragment, fragLen, "All fragments must be the same length")
}
f.Add(len(tt.initial), len(tt.modified), buffer.Bytes())
}
}
// validateFuzzInputs ensures that the raw fuzzing inputs can be mapped into the
// actual required fuzzing structure of a series of hashes. For the purpose of
// narrowing the search space, we require that all hashes are between 1 and 6
// bytes long.
func validateFuzzInputs(t *testing.T, initialCount, modifiedCount int, hashFragments []byte) ([]libcommon.Hash, []libcommon.Hash) {
t.Helper()
if initialCount <= 0 || modifiedCount < 0 || len(hashFragments) == 0 {
t.Skip("account count must be positive and hashFragments cannot be empty")
}
count := initialCount + modifiedCount
if count > 100 {
t.Skip("all failures thusfar can be reproduced within 10 keys, so 100 seems like a reasonable cutoff to balance speed of fuzzing with coverage")
}
if len(hashFragments)%count != 0 {
t.Skip("hash fragments must be divisible by accountCount")
}
fragmentSize := len(hashFragments) / count
if fragmentSize > 6 {
t.Skip("hash fragments cannot exceed 6 bytes")
}
uniqueInitialKeys := map[libcommon.Hash]struct{}{}
uniqueModifiedKeys := map[libcommon.Hash]struct{}{}
var inititalKeys, modifiedKeys []libcommon.Hash
for i := 0; i < count; i++ {
var hash libcommon.Hash
copy(hash[:], hashFragments[i*fragmentSize:(i+1)*fragmentSize])
if i < initialCount {
if _, ok := uniqueInitialKeys[hash]; ok {
t.Skip("hash fragments must be unique")
}
uniqueInitialKeys[hash] = struct{}{}
inititalKeys = append(inititalKeys, hash)
} else {
if _, ok := uniqueModifiedKeys[hash]; ok {
t.Skip("hash fragments must be unique")
}
uniqueModifiedKeys[hash] = struct{}{}
modifiedKeys = append(modifiedKeys, hash)
}
}
return inititalKeys, modifiedKeys
}
// FuzzTrieRootStorage will fuzz assorted storage hash tables. First, it will
// build an initial hash, adding the storage keys to a single account entry.
// Then, if the set of modifications was empty, it will verify that the same
// hash is re-computed. Then, it will compute the naive trie hash and compare it
// with the flat db hash. In the event that the modfiications are empty, this
// check additionally confirms that the initial trie hashing was correct.
// See the validateFuzzInputs function for details on how the paramaters are
// translated to initial and modified hashes.
func FuzzTrieRootStorage(f *testing.F) {
addFuzzTrieSeeds(f)
f.Fuzz(func(t *testing.T, initialCount, modifiedCount int, hashFragments []byte) {
initialKeys, modifiedKeys := validateFuzzInputs(t, initialCount, modifiedCount, hashFragments)
db := memdb.NewTestDB(t)
defer db.Close()
storageKeys := seedInitialStorage(t, db, initialKeys)
initialHash := initialFlatDBTrieBuild(t, db)
//logTrieTables(t, db)
retainKeys := seedModifiedStorage(t, db, modifiedKeys)
rl := trie.NewRetainList(0)
if len(retainKeys) == 0 {
// If there are no modifications, we add the first storage key to the
// retain list, because the inputs are random, this is not necessarily
// the actual lexicographically first key, but without some retained key
// then the root hash is simply returned.
rl.AddKey(storageKeys[0])
}
for _, retainKey := range retainKeys {
rl.AddKeyWithMarker(retainKey, true)
}
reHash := rebuildFlatDBTrieHash(t, rl, db)
if len(modifiedKeys) == 0 {
require.Equal(t, initialHash, reHash)
}
_, _, naiveHash := naiveTriesAndHashFromDB(t, db)
require.Equal(t, reHash, naiveHash)
})
}
// FuzzTrieRootAccounts will fuzz assorted account hash tables. First, it will
// build an initial hash, then modify the account hash table and re-compute the
// hash. If the set of modifications was empty, it will verify that the same
// hash is re-computed. Then, it will compute the naive trie hash and compare
// it with the flat db hash. In the event that the modfiications are empty,
// this check additionally confirms that the initial trie hashing was correct.
// See the validateFuzzInputs function for details on how the paramaters are
// translated to initial and modified hashes.
func FuzzTrieRootAccounts(f *testing.F) {
addFuzzTrieSeeds(f)
f.Fuzz(func(t *testing.T, initialCount, modifiedCount int, hashFragments []byte) {
initialKeys, modifiedKeys := validateFuzzInputs(t, initialCount, modifiedCount, hashFragments)
db := memdb.NewTestDB(t)
defer db.Close()
seedInitialAccounts(t, db, initialKeys)
initialHash := initialFlatDBTrieBuild(t, db)
//logTrieTables(t, db)
retainKeys := seedModifiedAccounts(t, db, modifiedKeys)
rl := trie.NewRetainList(0)
for _, retainKey := range retainKeys {
rl.AddKeyWithMarker(retainKey, true)
}
reHash := rebuildFlatDBTrieHash(t, rl, db)
if len(modifiedKeys) == 0 {
require.Equal(t, initialHash, reHash)
}
_, _, naiveHash := naiveTriesAndHashFromDB(t, db)
require.Equal(t, reHash, naiveHash)
})
}
// FuzzTrieRootAccountProofs seeds the database with an initial set of accounts and
// populates the trie tables. Then, it modifies the database of accounts but
// does not update the trie tables. Finally, for every seeded account (modified
// and not), it computes a proof for that key and verifies that the proof
// matches the one as computed by the naive trie implementation and that that
// proof is valid.
func FuzzTrieRootAccountProofs(f *testing.F) {
addFuzzTrieSeeds(f)
f.Fuzz(func(t *testing.T, initialCount, modifiedCount int, hashFragments []byte) {
initialKeys, modifiedKeys := validateFuzzInputs(t, initialCount, modifiedCount, hashFragments)
db := memdb.NewTestDB(t)
defer db.Close()
seedInitialAccounts(t, db, initialKeys)
initialFlatDBTrieBuild(t, db)
retainKeys := seedModifiedAccounts(t, db, modifiedKeys)
naiveTrie, _, naiveHash := naiveTriesAndHashFromDB(t, db)
allKeys := make([]libcommon.Hash, 1, initialCount+modifiedCount+1)
allKeys[0] = missingHash
wasModified := map[libcommon.Hash]struct{}{}
for _, hash := range modifiedKeys {
allKeys = append(allKeys, hash)
wasModified[hash] = struct{}{}
}
for _, hash := range initialKeys {
if _, ok := wasModified[hash]; ok {
continue
}
allKeys = append(allKeys, hash)
}
for _, hash := range allKeys {
t.Logf("Processing account key %x", hash)
// First compute the naive proof and verify that the proof is correct.
naiveProofBytes, err := naiveTrie.Prove(hash[:], 0, false)
require.NoError(t, err)
naiveAccountProof := make([]hexutility.Bytes, len(naiveProofBytes))
for i, part := range naiveProofBytes {
naiveAccountProof[i] = hexutility.Bytes(part)
}
var nonce uint64
var codeHash, storageHash libcommon.Hash
if hash != missingHash {
nonce = uint64(1)
if _, ok := wasModified[hash]; ok {
nonce++
}
storageHash = trie.EmptyRoot
codeHash = trie.EmptyCodeHash
}
naiveProof := &accounts.AccProofResult{
Nonce: hexutil.Uint64(nonce),
AccountProof: naiveAccountProof,
StorageHash: storageHash,
Balance: (*hexutil.Big)(new(uint256.Int).ToBig()),
StorageProof: []accounts.StorProofResult{},
CodeHash: codeHash,
}
err = trie.VerifyAccountProofByHash(naiveHash, hash, naiveProof)
require.NoError(t, err, "Invalid naive proof")
// Now do the same but with the 'real' flat implementation and verify
// that the result matches byte for byte.
flatHash, flatProof := proveFlatDB(t, db, hash == missingHash, retainKeys, [][]byte{hash[:]})
require.Equal(t, naiveHash, flatHash)
require.Equal(t, len(naiveProof.AccountProof), len(flatProof.AccountProof))
for i, proof := range naiveProof.AccountProof {
require.Equal(t, proof, flatProof.AccountProof[i], "mismatch at index %d", i)
}
flatProof.Nonce = hexutil.Uint64(nonce)
flatProof.CodeHash = codeHash
require.Equal(t, flatProof, naiveProof, "for key %s", hash)
}
})
}
// FuzzTrieRootStorageProofs seeds the database with an initial account and set
// of storage nodes for that account and populates the trie tables. Then, it
// modifies the database of storage but does not update the trie tables.
// Finally, for every seeded storage key (modified and not), it computes a proof for
// that key and verifies that the proof matches the one as computed by the naive
// trie implementation and that that proof is valid.
func FuzzTrieRootStorageProofs(f *testing.F) {
addFuzzTrieSeeds(f)
f.Fuzz(func(t *testing.T, initialCount, modifiedCount int, hashFragments []byte) {
initialKeys, modifiedKeys := validateFuzzInputs(t, initialCount, modifiedCount, hashFragments)
db := memdb.NewTestDB(t)
defer db.Close()
initialStorageKeys := seedInitialStorage(t, db, initialKeys)
initialFlatDBTrieBuild(t, db)
retainKeys := seedModifiedStorage(t, db, modifiedKeys)
naiveTrie, naiveStorageTrie, naiveHash := naiveTriesAndHashFromDB(t, db)
allKeys := make([][]byte, 1, initialCount+modifiedCount+1)
allKeys[0] = dbutils.GenerateCompositeStorageKey(storageAccountHash, 1, missingHash)
wasModified := map[string]struct{}{}
for _, key := range retainKeys {
allKeys = append(allKeys, key)
wasModified[string(key)] = struct{}{}
}
for _, key := range initialStorageKeys {
if _, ok := wasModified[string(key)]; ok {
continue
}
allKeys = append(allKeys, key)
}
// omniProof constructs a proof for the account and all of the keys to be
// tested. We later check that this composite proof matches the individual
// proofs on an element basis.
_, omniProof := proveFlatDB(t, db, false, retainKeys, allKeys)
for i, storageKey := range allKeys {
//t.Logf("Processing storage key %x", storageKey)
// First compute the naive proof and verify that the proof is correct.
naiveAccountProofBytes, err := naiveTrie.Prove(storageAccountHash[:], 0, false)
require.NoError(t, err)
naiveAccountProof := make([]hexutility.Bytes, len(naiveAccountProofBytes))
for i, part := range naiveAccountProofBytes {
naiveAccountProof[i] = hexutility.Bytes(part)
}
naiveStorageProofBytes, err := naiveStorageTrie.Prove(storageKey[40:], 0, true)
require.NoError(t, err)
naiveStorageProof := make([]hexutility.Bytes, len(naiveStorageProofBytes))
for i, part := range naiveStorageProofBytes {
naiveStorageProof[i] = hexutility.Bytes(part)
}
var storageKeyHash libcommon.Hash
copy(storageKeyHash[:], storageKey[40:])
var storageValue big.Int
if storageKeyHash != missingHash {
if _, ok := wasModified[string(storageKey)]; !ok {
storageValue.SetBytes(storageInitialValue[:])
} else {
storageValue.SetBytes(storageModifiedValue[:])
}
}
naiveProof := &accounts.AccProofResult{
Nonce: 1,
AccountProof: naiveAccountProof,
StorageHash: crypto.Keccak256Hash(naiveStorageProof[0]),
Balance: (*hexutil.Big)(new(uint256.Int).ToBig()),
StorageProof: []accounts.StorProofResult{
{
Key: trie.FakePreimage(storageKeyHash),
Value: (*hexutil.Big)(&storageValue),
Proof: naiveStorageProof,
},
},
CodeHash: storageAccountCodeHash,
}
err = trie.VerifyAccountProofByHash(naiveHash, storageAccountHash, naiveProof)
require.NoError(t, err, "Invalid naive account proof")
err = trie.VerifyStorageProofByHash(naiveProof.StorageHash, storageKeyHash, naiveProof.StorageProof[0])
require.NoError(t, err, "Invalid naive storage proof")
// Now do the same but with the 'real' flat implementation and verify
// that the result matches byte for byte.
flatHash, flatProof := proveFlatDB(t, db, false, retainKeys, [][]byte{storageAccountHash[:], storageKey})
require.Equal(t, naiveHash, flatHash)
for i, proof := range naiveProof.AccountProof {
require.Equal(t, proof, flatProof.AccountProof[i], "mismatch at index %d", i)
}
flatProof.Nonce = 1
flatProof.CodeHash = storageAccountCodeHash
require.Equal(t, flatProof, naiveProof, "failed for hash %s", storageKeyHash)
require.Equal(t, flatProof.StorageProof[0], omniProof.StorageProof[i])
}
})
}