diff --git a/erigon-lib/commitment/hex_patricia_hashed.go b/erigon-lib/commitment/hex_patricia_hashed.go index cd4359a1b6a..46dd0251fb5 100644 --- a/erigon-lib/commitment/hex_patricia_hashed.go +++ b/erigon-lib/commitment/hex_patricia_hashed.go @@ -1879,115 +1879,6 @@ func (hph *HexPatriciaHashed) RootHash() ([]byte, error) { return rootHash[1:], nil // first byte is 128+hash_len=160 } -func (hph *HexPatriciaHashed) GenerateProofTrie(ctx context.Context, updates *Updates, expectedRootHash []byte, logPrefix string) (proofTrie *trie.Trie, rootHash []byte, err error) { - var ( - m runtime.MemStats - ki uint64 - - updatesCount = updates.Size() - logEvery = time.NewTicker(20 * time.Second) - ) - defer logEvery.Stop() - var tries []*trie.Trie = make([]*trie.Trie, 0, len(updates.keys)) // slice of tries, i.e the witness for each key, these will be all merged into single trie - err = updates.HashSort(ctx, func(hashedKey, plainKey []byte, stateUpdate *Update) error { - select { - case <-logEvery.C: - dbg.ReadMemStats(&m) - log.Info(fmt.Sprintf("[%s][agg] computing trie", logPrefix), - "progress", fmt.Sprintf("%s/%s", common.PrettyCounter(ki), common.PrettyCounter(updatesCount)), - "alloc", common.ByteCount(m.Alloc), "sys", common.ByteCount(m.Sys)) - - default: - } - - var tr *trie.Trie - var computedRootHash []byte - - fmt.Printf("\n%d/%d) plainKey [%x] hashedKey [%x] currentKey [%x]\n", ki+1, updatesCount, plainKey, hashedKey, hph.currentKey[:hph.currentKeyLen]) - - if len(plainKey) == 20 { // account - account, err := hph.ctx.Account(plainKey) - if err != nil { - return fmt.Errorf("account with plainkey=%x not found: %w", plainKey, err) - } else { - addrHash := ecrypto.Keccak256(plainKey) - fmt.Printf("account with plainKey=%x, addrHash=%x FOUND = %v\n", plainKey, addrHash, account) - } - } else { - storage, err := hph.ctx.Storage(plainKey) - if err != nil { - return fmt.Errorf("storage with plainkey=%x not found: %w", plainKey, err) - } - fmt.Printf("storage found = %v\n", storage.Storage) - } - - // Keep folding until the currentKey is the prefix of the key we modify - for hph.needFolding(hashedKey) { - if err := hph.fold(); err != nil { - return fmt.Errorf("fold: %w", err) - } - } - // Now unfold until we step on an empty cell - for unfolding := hph.needUnfolding(hashedKey); unfolding > 0; unfolding = hph.needUnfolding(hashedKey) { - if err := hph.unfold(hashedKey, unfolding); err != nil { - return fmt.Errorf("unfold: %w", err) - } - } - hph.PrintGrid() - - // convert grid to trie.Trie - tr, err = hph.ToTrie(hashedKey, nil) // build witness trie for this key, based on the current state of the grid - if err != nil { - return err - } - computedRootHash = tr.Root() - fmt.Printf("computedRootHash = %x\n", computedRootHash) - - if !bytes.Equal(computedRootHash, expectedRootHash) { - return nil - } - - tries = append(tries, tr) - ki++ - return nil - }) - - if err != nil { - return nil, nil, fmt.Errorf("hash sort failed: %w", err) - } - - // Folding everything up to the root - for hph.activeRows > 0 { - if err := hph.fold(); err != nil { - return nil, nil, fmt.Errorf("final fold: %w", err) - } - } - - rootHash, err = hph.RootHash() - if err != nil { - return nil, nil, fmt.Errorf("root hash evaluation failed: %w", err) - } - if hph.trace { - fmt.Printf("root hash %x updates %d\n", rootHash, updatesCount) - } - - // merge all individual tries - proofTrie, err = trie.MergeTries(tries) - if err != nil { - return nil, nil, err - } - - proofTrieRootHash := proofTrie.Root() - - fmt.Printf("mergedTrieRootHash = %x\n", proofTrieRootHash) - - if !bytes.Equal(proofTrieRootHash, expectedRootHash) { - return nil, nil, fmt.Errorf("root hash mismatch witnessTrieRootHash(%x)!=expectedRootHash(%x)", proofTrieRootHash, expectedRootHash) - } - - return proofTrie, rootHash, nil -} - // Generate the block witness. This works by loading each key from the list of updates (they are not really updates since we won't modify the trie, // but currently need to be defined like that for the fold/unfold algorithm) into the grid and traversing the grid to convert it into `trie.Trie`. // All the individual tries are combined to create the final witness trie. diff --git a/erigon-lib/state/domain_shared.go b/erigon-lib/state/domain_shared.go index 77d4636e55f..3eacf3fbb54 100644 --- a/erigon-lib/state/domain_shared.go +++ b/erigon-lib/state/domain_shared.go @@ -31,6 +31,7 @@ import ( "unsafe" "github.com/erigontech/erigon-lib/seg" + "github.com/erigontech/erigon-lib/trie" "github.com/pkg/errors" "golang.org/x/crypto/sha3" @@ -161,6 +162,10 @@ func (sd *SharedDomains) SavePastChangesetAccumulator(blockHash common.Hash, blo sd.pastChangesAccumulator[toStringZeroCopy(key)] = acc } +func (sd *SharedDomains) GetCommitmentContext() *SharedDomainsCommitmentContext { + return sd.sdCtx +} + func (sd *SharedDomains) GetDiffset(tx kv.RwTx, blockHash common.Hash, blockNumber uint64) ([kv.DomainLen][]DomainEntryDiff, bool, error) { var key [40]byte binary.BigEndian.PutUint64(key[:8], blockNumber) @@ -1318,6 +1323,15 @@ func (sdc *SharedDomainsCommitmentContext) TouchKey(d kv.Domain, key string, val } } +func (sdc *SharedDomainsCommitmentContext) GenerateTouchedKeyTrie(ctx context.Context, expectedRoot []byte, logPrefix string) (proofTrie *trie.Trie, rootHash []byte, err error) { + hexPatriciaHashed, ok := sdc.Trie().(*commitment.HexPatriciaHashed) + if ok { + return hexPatriciaHashed.GenerateWitness(ctx, sdc.updates, nil, expectedRoot, logPrefix) + } + + return nil, nil, errors.New("shared domains commitment context doesn't have HexPatriciaHashed") +} + // Evaluates commitment for processed state. func (sdc *SharedDomainsCommitmentContext) ComputeCommitment(ctx context.Context, saveState bool, blockNum uint64, logPrefix string) (rootHash []byte, err error) { sdc.ResetBranchCache() diff --git a/erigon-lib/trie/proof.go b/erigon-lib/trie/proof.go index f4ca438ec83..7726c41a87b 100644 --- a/erigon-lib/trie/proof.go +++ b/erigon-lib/trie/proof.go @@ -355,15 +355,21 @@ func VerifyStorageProofByHash(storageRoot libcommon.Hash, keyHash libcommon.Hash if proof.Value.ToInt().Sign() != 0 { return errors.New("empty storage root cannot have non-zero values") } - // The spec here is a bit unclear. The yellow paper makes it clear that the - // EmptyRoot hash is a special case where the trie is empty. Since the trie - // is empty there are no proof elements to collect. But, EIP-1186 also - // clearly states that the proof must be "starting with the - // storageHash-Node", which could imply an RLP encoded `[]byte(nil)` (the - // pre-image of the EmptyRoot) should be included. This implementation - // chooses to require the proof be empty. - if len(proof.Proof) > 0 { - return errors.New("empty storage root should not have proof nodes") + // if storage root is zero (0000000) then we should have an empty proof + // if it corresponds to empty storage tree, having value EmptyRoot above + // then proof should be RLP encoding of empty proof (0x80) + if storageRoot == EmptyRoot { + for i, _ := range proof.Proof { + if len(proof.Proof[i]) != 1 || proof.Proof[i][0] != 0x80 { + return errors.New("empty storage root should have RLP encoding of empty proof") + } + } + } else { + for i, _ := range proof.Proof { + if len(proof.Proof[i]) != 0 { + return errors.New("zero storage root should have empty proof") + } + } } return nil } diff --git a/turbo/jsonrpc/eth_call.go b/turbo/jsonrpc/eth_call.go index f2df3f0549c..07f0c475701 100644 --- a/turbo/jsonrpc/eth_call.go +++ b/turbo/jsonrpc/eth_call.go @@ -387,33 +387,18 @@ func (api *APIImpl) getProof(ctx context.Context, address libcommon.Address, sto return nil, err } defer roTx2.Rollback() - txBatch2 := membatchwithdb.NewMemoryBatch(roTx2, "", logger) - defer txBatch2.Rollback() - domains, err := libstate.NewSharedDomains(txBatch2, log.New()) + domains, err := libstate.NewSharedDomains(roTx2, log.New()) if err != nil { return nil, err } - sdCtx := libstate.NewSharedDomainsCommitmentContext(domains, commitment.ModeUpdate, commitment.VariantHexPatriciaTrie) - patricieTrie := sdCtx.Trie() - hph, ok := patricieTrie.(*commitment.HexPatriciaHashed) - if !ok { - return nil, errors.New("casting to HexPatriciaTrieHashed failed") - } + sdCtx := domains.GetCommitmentContext() - // define these keys as "updates", but we are not really updating anything, we just want to load them into the grid, - // so this is just to satisfy the current hex patricia trie api. - updates := commitment.NewUpdates(commitment.ModeDirect, sdCtx.TempDir(), hph.HashAndNibblizeKey) // touch account - updates.TouchPlainKey(string(address.Bytes()), nil, updates.TouchAccount) - // touch storage keys - for _, storageKey := range storageKeys { - updates.TouchPlainKey(string(common.FromHex(address.Hex()[2:]+storageKey.String()[2:])), nil, updates.TouchStorage) - } - hph.SetTrace(false) + sdCtx.TouchKey(kv.AccountsDomain, string(address.Bytes()), nil) // generate the trie for proofs, this works by loading the merkle paths to the touched keys - proofTrie, proofRootHash, err := hph.GenerateProofTrie(ctx, updates, header.Root[:], "eth_getProof") + proofTrie, proofRootHash, err := sdCtx.GenerateTouchedKeyTrie(ctx, header.Root[:], "eth_getProof") if err != nil { return nil, err } @@ -448,19 +433,50 @@ func (api *APIImpl) getProof(ctx context.Context, address libcommon.Address, sto proof.StorageProof[i] = accounts.StorProofResult{ Key: k, Value: (*hexutil.Big)(a), - Proof: []hexutility.Bytes{}, + Proof: nil, } } return proof, nil - } else { - proof.Balance = (*hexutil.Big)(acc.Balance.ToBig()) - proof.Nonce = hexutil.Uint64(acc.Nonce) - proof.CodeHash = acc.CodeHash - proof.StorageHash = acc.Root + } + + proof.Balance = (*hexutil.Big)(acc.Balance.ToBig()) + proof.Nonce = hexutil.Uint64(acc.Nonce) + proof.CodeHash = acc.CodeHash + proof.StorageHash = acc.Root + + // if storage is not empty touch keys and build trie + fmt.Println("shota", proof.StorageHash.String()) + if proof.StorageHash.Cmp(libcommon.BytesToHash(commitment.EmptyRootHash)) != 0 && len(storageKeys) != 0 { + // touch storage keys + for _, storageKey := range storageKeys { + sdCtx.TouchKey(kv.StorageDomain, string(common.FromHex(address.Hex()[2:]+storageKey.String()[2:])), nil) + } + + // generate the trie for proofs, this works by loading the merkle paths to the touched keys + proofTrie, proofRootHash, err = sdCtx.GenerateTouchedKeyTrie(ctx, header.Root[:], "eth_getProof") + if err != nil { + return nil, err + } + + // verify hash + if !bytes.Equal(proofRootHash, header.Root[:]) { + return nil, fmt.Errorf("proof root hash mismatch actual(%x)!=expected(%x)", proofRootHash, header.Root[:]) + } } // get storage key proofs for i, keyHash := range storageKeys { + proof.StorageProof[i].Key = keyHash + + n := new(big.Int) + // if we have simple non contract account just set values directly without requesting any key proof + if proof.StorageHash.Cmp(libcommon.BytesToHash(commitment.EmptyRootHash)) == 0 { + n.SetInt64(0) + proof.StorageProof[i].Proof = nil + proof.StorageProof[i].Value = (*hexutil.Big)(unsafe.Pointer(n)) + continue + } + // prepare key path (keccak(address) | keccak(key)) var fullKey []byte fullKey = append(fullKey, crypto.Keccak256(address.Bytes())...) @@ -472,14 +488,10 @@ func (api *APIImpl) getProof(ctx context.Context, address libcommon.Address, sto return nil, errors.New("cannot verify store proof") } - // Decode the hexadecimal string into the big.Int - // The base is 16 for hexadecimal - n := new(big.Int) n.SetString(hex.EncodeToString(value), 16) - - // set key proof - proof.StorageProof[i].Key = keyHash proof.StorageProof[i].Value = (*hexutil.Big)(unsafe.Pointer(n)) + + // 0x80 represents RLP encoding of an empty proof slice proof.StorageProof[i].Proof = []hexutility.Bytes{[]byte{0x80}} if storageProof != nil && len(storageProof) != 0 { proof.StorageProof[i].Proof = *(*[]hexutility.Bytes)(unsafe.Pointer(&storageProof)) diff --git a/turbo/jsonrpc/eth_call_test.go b/turbo/jsonrpc/eth_call_test.go index ad216d0fa8e..4bd3d6ba205 100644 --- a/turbo/jsonrpc/eth_call_test.go +++ b/turbo/jsonrpc/eth_call_test.go @@ -162,26 +162,6 @@ func TestGetProof(t *testing.T) { blockNum: 3, stateVal: 0, }, - { - name: "currentBlockNoAccountMissingState", - addr: libcommon.HexToAddress("0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddead0"), - storageKeys: []libcommon.Hash{libcommon.HexToHash("0xdeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddeaddead")}, - blockNum: 3, - stateVal: 0, - }, - { - name: "olderBlockWithState", - addr: contractAddr, - blockNum: 2, - storageKeys: []libcommon.Hash{key(1), key(5), key(9), key(13)}, - stateVal: 1, - }, - { - name: "tooOldBlock", - addr: contractAddr, - blockNum: 1, - expectedErr: "requested block is too old, block must be within 1 blocks of the head block number (currently 3)", - }, } for _, tt := range tests {