diff --git a/CHANGELOG.md b/CHANGELOG.md index 57bfc0e17d..1e7b4f3b39 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,7 @@ - Print an overview of configuration and system information at startup [#4451](https://github.com/hyperledger/besu/pull/4451) - Do not send new payloads to backward sync if initial sync is in progress [#4720](https://github.com/hyperledger/besu/issues/4720) - Improve the way transaction fee cap validation is done on London fee market to not depend on transient network conditions [#4598](https://github.com/hyperledger/besu/pull/4598) +- Preload and cache account and storage data from RocksDB to improve performance [#4737](https://github.com/hyperledger/besu/issues/4737) ### Bug Fixes - Restore updating chain head and finalized block during backward sync [#4718](https://github.com/hyperledger/besu/pull/4718) diff --git a/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java b/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java index e288f69b2c..2482591f9d 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java @@ -32,6 +32,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.methods.JsonRpcMethods; import org.hyperledger.besu.ethereum.blockcreation.MiningCoordinator; import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateArchive; import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.bonsai.CachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.chain.BlockchainStorage; import org.hyperledger.besu.ethereum.chain.DefaultBlockchain; @@ -300,8 +301,10 @@ public abstract class BesuControllerBuilder implements MiningParameterOverrides reorgLoggingThreshold, dataDirectory.toString()); + final CachedMerkleTrieLoader cachedMerkleTrieLoader = new CachedMerkleTrieLoader(metricsSystem); + final WorldStateArchive worldStateArchive = - createWorldStateArchive(worldStateStorage, blockchain); + createWorldStateArchive(worldStateStorage, blockchain, cachedMerkleTrieLoader); if (blockchain.getChainHeadBlockNumber() < 1) { genesisState.writeStateTo(worldStateArchive.getMutable()); @@ -628,14 +631,17 @@ public abstract class BesuControllerBuilder implements MiningParameterOverrides } private WorldStateArchive createWorldStateArchive( - final WorldStateStorage worldStateStorage, final Blockchain blockchain) { + final WorldStateStorage worldStateStorage, + final Blockchain blockchain, + final CachedMerkleTrieLoader cachedMerkleTrieLoader) { switch (dataStorageConfiguration.getDataStorageFormat()) { case BONSAI: return new BonsaiWorldStateArchive( (BonsaiWorldStateKeyValueStorage) worldStateStorage, blockchain, Optional.of(dataStorageConfiguration.getBonsaiMaxLayersToLoad()), - dataStorageConfiguration.useBonsaiSnapshots()); + dataStorageConfiguration.useBonsaiSnapshots(), + cachedMerkleTrieLoader); case FOREST: default: diff --git a/ethereum/core/build.gradle b/ethereum/core/build.gradle index cc3521c10f..cab2d0769f 100644 --- a/ethereum/core/build.gradle +++ b/ethereum/core/build.gradle @@ -43,6 +43,7 @@ dependencies { implementation project(':plugin-api') implementation project(':services:kvstore') + implementation 'com.fasterxml.jackson.core:jackson-databind' implementation 'com.google.guava:guava' implementation 'io.opentelemetry:opentelemetry-api' @@ -56,6 +57,8 @@ dependencies { implementation 'org.hyperledger.besu:bls12-381' implementation 'org.immutables:value-annotations' + implementation 'io.prometheus:simpleclient_guava' + implementation 'org.xerial.snappy:snappy-java' annotationProcessor 'org.immutables:value' diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiInMemoryWorldState.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiInMemoryWorldState.java index 27b495e497..7eb09a0bba 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiInMemoryWorldState.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiInMemoryWorldState.java @@ -61,12 +61,13 @@ public class BonsaiInMemoryWorldState extends BonsaiPersistedWorldState { updateAccountStorage(worldStateUpdater, addressMapEntry); }); - // for manicured tries and composting, trim and compost here - // next walk the account trie final StoredMerklePatriciaTrie accountTrie = new StoredMerklePatriciaTrie<>( - this::getAccountStateTrieNode, + (location, hash) -> + archive + .getCachedMerkleTrieLoader() + .getAccountStateTrieNode(worldStateStorage, location, hash), worldStateRootHash, Function.identity(), Function.identity()); @@ -96,7 +97,8 @@ public class BonsaiInMemoryWorldState extends BonsaiPersistedWorldState { private void updateAccountStorage( final BonsaiWorldStateUpdater worldStateUpdater, - final Map.Entry>> storageAccountUpdate) { + final Map.Entry>> + storageAccountUpdate) { final Address updatedAddress = storageAccountUpdate.getKey(); final Hash updatedAddressHash = Hash.hash(updatedAddress); if (worldStateUpdater.getAccountsToUpdate().containsKey(updatedAddress)) { @@ -105,9 +107,14 @@ public class BonsaiInMemoryWorldState extends BonsaiPersistedWorldState { final BonsaiAccount accountOriginal = accountValue.getPrior(); final Hash storageRoot = (accountOriginal == null) ? Hash.EMPTY_TRIE_HASH : accountOriginal.getStorageRoot(); + final StoredMerklePatriciaTrie storageTrie = new StoredMerklePatriciaTrie<>( - (location, key) -> getStorageTrieNode(updatedAddressHash, location, key), + (location, key) -> + archive + .getCachedMerkleTrieLoader() + .getAccountStorageTrieNode( + worldStateStorage, updatedAddressHash, location, key), storageRoot, Function.identity(), Function.identity()); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiPersistedWorldState.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiPersistedWorldState.java index d2383c8b46..9ffcd01831 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiPersistedWorldState.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiPersistedWorldState.java @@ -66,7 +66,17 @@ public class BonsaiPersistedWorldState implements MutableWorldState, BonsaiWorld Bytes32.wrap(worldStateStorage.getWorldStateRootHash().orElse(Hash.EMPTY_TRIE_HASH))); worldStateBlockHash = Hash.wrap(Bytes32.wrap(worldStateStorage.getWorldStateBlockHash().orElse(Hash.ZERO))); - updater = new BonsaiWorldStateUpdater(this); + updater = + new BonsaiWorldStateUpdater( + this, + (addr, value) -> + archive + .getCachedMerkleTrieLoader() + .preLoadAccount(worldStateStorage, worldStateRootHash, addr), + (addr, value) -> + archive + .getCachedMerkleTrieLoader() + .preLoadStorageSlot(worldStateStorage, addr, value)); } public BonsaiWorldStateArchive getArchive() { @@ -116,7 +126,10 @@ public class BonsaiPersistedWorldState implements MutableWorldState, BonsaiWorld // next walk the account trie final StoredMerklePatriciaTrie accountTrie = new StoredMerklePatriciaTrie<>( - this::getAccountStateTrieNode, + (location, hash) -> + archive + .getCachedMerkleTrieLoader() + .getAccountStateTrieNode(worldStateStorage, location, hash), worldStateRootHash, Function.identity(), Function.identity()); @@ -174,8 +187,8 @@ public class BonsaiPersistedWorldState implements MutableWorldState, BonsaiWorld private void updateAccountStorageState( final BonsaiWorldStateKeyValueStorage.BonsaiUpdater stateUpdater, final BonsaiWorldStateUpdater worldStateUpdater) { - for (final Map.Entry>> storageAccountUpdate : - worldStateUpdater.getStorageToUpdate().entrySet()) { + for (final Map.Entry>> + storageAccountUpdate : worldStateUpdater.getStorageToUpdate().entrySet()) { final Address updatedAddress = storageAccountUpdate.getKey(); final Hash updatedAddressHash = Hash.hash(updatedAddress); if (worldStateUpdater.getAccountsToUpdate().containsKey(updatedAddress)) { @@ -186,7 +199,11 @@ public class BonsaiPersistedWorldState implements MutableWorldState, BonsaiWorld (accountOriginal == null) ? Hash.EMPTY_TRIE_HASH : accountOriginal.getStorageRoot(); final StoredMerklePatriciaTrie storageTrie = new StoredMerklePatriciaTrie<>( - (location, key) -> getStorageTrieNode(updatedAddressHash, location, key), + (location, key) -> + archive + .getCachedMerkleTrieLoader() + .getAccountStorageTrieNode( + worldStateStorage, updatedAddressHash, location, key), storageRoot, Function.identity(), Function.identity()); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchive.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchive.java index 7bff5301a8..50bac540bb 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchive.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchive.java @@ -54,34 +54,44 @@ public class BonsaiWorldStateArchive implements WorldStateArchive { private final TrieLogManager trieLogManager; private final BonsaiPersistedWorldState persistedState; private final BonsaiWorldStateKeyValueStorage worldStateStorage; + + private final CachedMerkleTrieLoader cachedMerkleTrieLoader; + private final boolean useSnapshots; - public BonsaiWorldStateArchive(final StorageProvider provider, final Blockchain blockchain) { + public BonsaiWorldStateArchive( + final StorageProvider provider, + final Blockchain blockchain, + final CachedMerkleTrieLoader cachedMerkleTrieLoader) { this( (BonsaiWorldStateKeyValueStorage) provider.createWorldStateStorage(DataStorageFormat.BONSAI), blockchain, Optional.empty(), - provider.isWorldStateSnappable()); + provider.isWorldStateSnappable(), + cachedMerkleTrieLoader); } public BonsaiWorldStateArchive( final BonsaiWorldStateKeyValueStorage worldStateStorage, final Blockchain blockchain, - final Optional maxLayersToLoad) { + final Optional maxLayersToLoad, + final CachedMerkleTrieLoader cachedMerkleTrieLoader) { // overload while snapshots are an experimental option: this( worldStateStorage, blockchain, maxLayersToLoad, - DataStorageConfiguration.DEFAULT_BONSAI_USE_SNAPSHOTS); + DataStorageConfiguration.DEFAULT_BONSAI_USE_SNAPSHOTS, + cachedMerkleTrieLoader); } public BonsaiWorldStateArchive( final BonsaiWorldStateKeyValueStorage worldStateStorage, final Blockchain blockchain, final Optional maxLayersToLoad, - final boolean useSnapshots) { + final boolean useSnapshots, + final CachedMerkleTrieLoader cachedMerkleTrieLoader) { this( useSnapshots ? new SnapshotTrieLogManager( @@ -90,7 +100,8 @@ public class BonsaiWorldStateArchive implements WorldStateArchive { blockchain, worldStateStorage, maxLayersToLoad.orElse(RETAINED_LAYERS)), worldStateStorage, blockchain, - useSnapshots); + useSnapshots, + cachedMerkleTrieLoader); } @VisibleForTesting @@ -98,12 +109,14 @@ public class BonsaiWorldStateArchive implements WorldStateArchive { final TrieLogManager trieLogManager, final BonsaiWorldStateKeyValueStorage worldStateStorage, final Blockchain blockchain, - final boolean useSnapshots) { + final boolean useSnapshots, + final CachedMerkleTrieLoader cachedMerkleTrieLoader) { this.trieLogManager = trieLogManager; this.blockchain = blockchain; this.worldStateStorage = worldStateStorage; this.persistedState = new BonsaiPersistedWorldState(this, worldStateStorage); this.useSnapshots = useSnapshots; + this.cachedMerkleTrieLoader = cachedMerkleTrieLoader; blockchain.observeBlockAdded(this::blockAddedHandler); } @@ -278,6 +291,10 @@ public class BonsaiWorldStateArchive implements WorldStateArchive { return (BonsaiWorldStateUpdater) mutableState.updater(); } + public CachedMerkleTrieLoader getCachedMerkleTrieLoader() { + return cachedMerkleTrieLoader; + } + @Override public MutableWorldState getMutable() { return persistedState; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateKeyValueStorage.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateKeyValueStorage.java index b9ca341028..83caa3c010 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateKeyValueStorage.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateKeyValueStorage.java @@ -183,7 +183,6 @@ public class BonsaiWorldStateKeyValueStorage implements WorldStateStorage, AutoC .get(Bytes.concatenate(accountHash, slotHash).toArrayUnsafe()) .map(Bytes::wrap); if (response.isEmpty()) { - // after a snapsync/fastsync we only have the trie branches. final Optional account = getAccount(accountHash); final Optional worldStateRootHash = getWorldStateRootHash(); if (account.isPresent() && worldStateRootHash.isPresent()) { diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateUpdater.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateUpdater.java index a1a58623d4..5dcdf44dae 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateUpdater.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateUpdater.java @@ -29,7 +29,6 @@ import org.hyperledger.besu.evm.worldstate.WrappedEvmAccount; import java.util.Collection; import java.util.Collections; -import java.util.HashMap; import java.util.HashSet; import java.util.Iterator; import java.util.Map; @@ -39,30 +38,44 @@ import java.util.Set; import java.util.TreeSet; import java.util.concurrent.ConcurrentHashMap; +import com.google.common.collect.ForwardingMap; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt256; +import org.jetbrains.annotations.NotNull; public class BonsaiWorldStateUpdater extends AbstractWorldUpdater implements BonsaiWorldView { - private final Map> accountsToUpdate = - new ConcurrentHashMap<>(); + private final AccountConsumingMap> accountsToUpdate; + private final Consumer> accountPreloader; + private final Consumer storagePreloader; private final Map> codeToUpdate = new ConcurrentHashMap<>(); private final Set
storageToClear = Collections.synchronizedSet(new HashSet<>()); // storage sub mapped by _hashed_ key. This is because in self_destruct calls we need to // enumerate the old storage and delete it. Those are trie stored by hashed key by spec and the // alternative was to keep a giant pre-image cache of the entire trie. - private final Map>> storageToUpdate = + private final Map>> storageToUpdate = new ConcurrentHashMap<>(); BonsaiWorldStateUpdater(final BonsaiWorldView world) { + this(world, (__, ___) -> {}, (__, ___) -> {}); + } + + BonsaiWorldStateUpdater( + final BonsaiWorldView world, + final Consumer> accountPreloader, + final Consumer storagePreloader) { super(world); + this.accountsToUpdate = new AccountConsumingMap<>(new ConcurrentHashMap<>(), accountPreloader); + this.accountPreloader = accountPreloader; + this.storagePreloader = storagePreloader; } public BonsaiWorldStateUpdater copy() { - final BonsaiWorldStateUpdater copy = new BonsaiWorldStateUpdater(wrappedWorldView()); + final BonsaiWorldStateUpdater copy = + new BonsaiWorldStateUpdater(wrappedWorldView(), accountPreloader, storagePreloader); copy.cloneFromUpdater(this); return copy; } @@ -127,7 +140,7 @@ public class BonsaiWorldStateUpdater extends AbstractWorldUpdater>> getStorageToUpdate() { + Map>> getStorageToUpdate() { return storageToUpdate; } @@ -184,7 +197,11 @@ public class BonsaiWorldStateUpdater extends AbstractWorldUpdater> deletedStorageUpdates = - storageToUpdate.computeIfAbsent(deletedAddress, k -> new ConcurrentHashMap<>()); + storageToUpdate.computeIfAbsent( + deletedAddress, + k -> + new StorageConsumingMap<>( + deletedAddress, new ConcurrentHashMap<>(), storagePreloader)); final Iterator>> iter = deletedStorageUpdates.entrySet().iterator(); while (iter.hasNext()) { @@ -256,8 +273,12 @@ public class BonsaiWorldStateUpdater extends AbstractWorldUpdater> pendingStorageUpdates = - storageToUpdate.computeIfAbsent(updatedAddress, __ -> new ConcurrentHashMap<>()); + final StorageConsumingMap> pendingStorageUpdates = + storageToUpdate.computeIfAbsent( + updatedAddress, + __ -> + new StorageConsumingMap<>( + updatedAddress, new ConcurrentHashMap<>(), storagePreloader)); if (tracked.getStorageWasCleared()) { storageToClear.add(updatedAddress); pendingStorageUpdates.clear(); @@ -333,7 +354,11 @@ public class BonsaiWorldStateUpdater extends AbstractWorldUpdater storageToUpdate - .computeIfAbsent(address, key -> new HashMap<>()) + .computeIfAbsent( + address, + key -> + new StorageConsumingMap<>( + address, new ConcurrentHashMap<>(), storagePreloader)) .put(slotHash, new BonsaiValue<>(v, v))); return valueUInt; } @@ -412,7 +437,7 @@ public class BonsaiWorldStateUpdater extends AbstractWorldUpdater>> updatesStorage : + for (final Map.Entry>> updatesStorage : storageToUpdate.entrySet()) { final Address address = updatesStorage.getKey(); for (final Map.Entry> slotUpdate : @@ -588,7 +613,8 @@ public class BonsaiWorldStateUpdater extends AbstractWorldUpdater> maybeCreateStorageMap( final Map> storageMap, final Address address) { if (storageMap == null) { - final Map> newMap = new HashMap<>(); + final StorageConsumingMap> newMap = + new StorageConsumingMap<>(address, new ConcurrentHashMap<>(), storagePreloader); storageToUpdate.put(address, newMap); return newMap; } else { @@ -617,7 +643,10 @@ public class BonsaiWorldStateUpdater extends AbstractWorldUpdater(storageValue.get(), storageValue.get()); storageToUpdate - .computeIfAbsent(address, k -> new ConcurrentHashMap<>()) + .computeIfAbsent( + address, + k -> + new StorageConsumingMap<>(address, new ConcurrentHashMap<>(), storagePreloader)) .put(slotHash, slotValue); } } @@ -687,4 +716,67 @@ public class BonsaiWorldStateUpdater extends AbstractWorldUpdater extends ForwardingMap { + + private final ConcurrentHashMap accounts; + private final Consumer consumer; + + public AccountConsumingMap( + final ConcurrentHashMap accounts, final Consumer consumer) { + this.accounts = accounts; + this.consumer = consumer; + } + + @Override + public T put(@NotNull final Address address, @NotNull final T value) { + consumer.process(address, value); + return accounts.put(address, value); + } + + public Consumer getConsumer() { + return consumer; + } + + @Override + protected Map delegate() { + return accounts; + } + } + + public static class StorageConsumingMap extends ForwardingMap { + + private final Address address; + + private final ConcurrentHashMap storages; + private final Consumer consumer; + + public StorageConsumingMap( + final Address address, + final ConcurrentHashMap storages, + final Consumer consumer) { + this.address = address; + this.storages = storages; + this.consumer = consumer; + } + + @Override + public T put(@NotNull final Hash slotHash, @NotNull final T value) { + consumer.process(address, slotHash); + return storages.put(slotHash, value); + } + + public Consumer getConsumer() { + return consumer; + } + + @Override + protected Map delegate() { + return storages; + } + } + + public interface Consumer { + void process(final Address address, T value); + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/CachedMerkleTrieLoader.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/CachedMerkleTrieLoader.java new file mode 100644 index 0000000000..cf905f2635 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/CachedMerkleTrieLoader.java @@ -0,0 +1,151 @@ +/* + * Copyright Hyperledger Besu contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.ethereum.bonsai; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.trie.MerklePatriciaTrie; +import org.hyperledger.besu.ethereum.trie.MerkleTrieException; +import org.hyperledger.besu.ethereum.trie.StoredMerklePatriciaTrie; +import org.hyperledger.besu.metrics.BesuMetricCategory; +import org.hyperledger.besu.metrics.ObservableMetricsSystem; +import org.hyperledger.besu.metrics.prometheus.PrometheusMetricsSystem; + +import java.util.Optional; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; + +import com.google.common.annotations.VisibleForTesting; +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import io.prometheus.client.guava.cache.CacheMetricsCollector; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +public class CachedMerkleTrieLoader { + + private static final int ACCOUNT_CACHE_SIZE = 100_000; + private static final int STORAGE_CACHE_SIZE = 200_000; + private final Cache accountNodes = + CacheBuilder.newBuilder().recordStats().maximumSize(ACCOUNT_CACHE_SIZE).build(); + private final Cache storageNodes = + CacheBuilder.newBuilder().recordStats().maximumSize(STORAGE_CACHE_SIZE).build(); + + public CachedMerkleTrieLoader(final ObservableMetricsSystem metricsSystem) { + + CacheMetricsCollector cacheMetrics = new CacheMetricsCollector(); + cacheMetrics.addCache("accountsNodes", accountNodes); + cacheMetrics.addCache("storageNodes", storageNodes); + if (metricsSystem instanceof PrometheusMetricsSystem) + ((PrometheusMetricsSystem) metricsSystem) + .addCollector(BesuMetricCategory.BLOCKCHAIN, () -> cacheMetrics); + } + + public void preLoadAccount( + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final Hash worldStateRootHash, + final Address account) { + CompletableFuture.runAsync( + () -> cacheAccountNodes(worldStateStorage, worldStateRootHash, account)); + } + + @VisibleForTesting + public void cacheAccountNodes( + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final Hash worldStateRootHash, + final Address account) { + try { + final StoredMerklePatriciaTrie accountTrie = + new StoredMerklePatriciaTrie<>( + (location, hash) -> { + Optional node = worldStateStorage.getAccountStateTrieNode(location, hash); + node.ifPresent(bytes -> accountNodes.put(Hash.hash(bytes), bytes)); + return node; + }, + worldStateRootHash, + Function.identity(), + Function.identity()); + accountTrie.get(Hash.hash(account)); + } catch (MerkleTrieException e) { + // ignore exception for the cache + } + } + + public void preLoadStorageSlot( + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final Address account, + final Hash slotHash) { + CompletableFuture.runAsync(() -> cacheStorageNodes(worldStateStorage, account, slotHash)); + } + + @VisibleForTesting + public void cacheStorageNodes( + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final Address account, + final Hash slotHash) { + final Hash accountHash = Hash.hash(account); + worldStateStorage + .getStateTrieNode(Bytes.concatenate(accountHash, Bytes.EMPTY)) + .ifPresent( + storageRoot -> { + try { + final StoredMerklePatriciaTrie storageTrie = + new StoredMerklePatriciaTrie<>( + (location, hash) -> { + Optional node = + worldStateStorage.getAccountStorageTrieNode( + accountHash, location, hash); + node.ifPresent(bytes -> storageNodes.put(Hash.hash(bytes), bytes)); + return node; + }, + Hash.hash(storageRoot), + Function.identity(), + Function.identity()); + storageTrie.get(slotHash); + } catch (MerkleTrieException e) { + // ignore exception for the cache + } + }); + } + + public Optional getAccountStateTrieNode( + final BonsaiWorldStateKeyValueStorage worldStateKeyValueStorage, + final Bytes location, + final Bytes32 nodeHash) { + if (nodeHash.equals(MerklePatriciaTrie.EMPTY_TRIE_NODE_HASH)) { + return Optional.of(MerklePatriciaTrie.EMPTY_TRIE_NODE); + } else { + return Optional.ofNullable(accountNodes.getIfPresent(nodeHash)) + .or(() -> worldStateKeyValueStorage.getAccountStateTrieNode(location, nodeHash)); + } + } + + public Optional getAccountStorageTrieNode( + final BonsaiWorldStateKeyValueStorage worldStateKeyValueStorage, + final Hash accountHash, + final Bytes location, + final Bytes32 nodeHash) { + if (nodeHash.equals(MerklePatriciaTrie.EMPTY_TRIE_NODE_HASH)) { + return Optional.of(MerklePatriciaTrie.EMPTY_TRIE_NODE); + } else { + return Optional.ofNullable(storageNodes.getIfPresent(nodeHash)) + .or( + () -> + worldStateKeyValueStorage.getAccountStorageTrieNode( + accountHash, location, nodeHash)); + } + } +} diff --git a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/InMemoryKeyValueStorageProvider.java b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/InMemoryKeyValueStorageProvider.java index bccc21da8a..35621b71c4 100644 --- a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/InMemoryKeyValueStorageProvider.java +++ b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/InMemoryKeyValueStorageProvider.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.core; import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateArchive; +import org.hyperledger.besu.ethereum.bonsai.CachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.chain.DefaultBlockchain; import org.hyperledger.besu.ethereum.chain.MutableBlockchain; @@ -66,7 +67,10 @@ public class InMemoryKeyValueStorageProvider extends KeyValueStorageProvider { final Blockchain blockchain) { final InMemoryKeyValueStorageProvider inMemoryKeyValueStorageProvider = new InMemoryKeyValueStorageProvider(); - return new BonsaiWorldStateArchive(inMemoryKeyValueStorageProvider, blockchain); + final CachedMerkleTrieLoader cachedMerkleTrieLoader = + new CachedMerkleTrieLoader(new NoOpMetricsSystem()); + return new BonsaiWorldStateArchive( + inMemoryKeyValueStorageProvider, blockchain, cachedMerkleTrieLoader); } public static MutableWorldState createInMemoryWorldState() { diff --git a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TrieGenerator.java b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TrieGenerator.java index 63ef3d6925..28f47a992f 100644 --- a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TrieGenerator.java +++ b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/TrieGenerator.java @@ -16,14 +16,16 @@ package org.hyperledger.besu.ethereum.core; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateKeyValueStorage; import org.hyperledger.besu.ethereum.rlp.RLP; import org.hyperledger.besu.ethereum.trie.MerklePatriciaTrie; import org.hyperledger.besu.ethereum.trie.StoredMerklePatriciaTrie; import org.hyperledger.besu.ethereum.worldstate.StateTrieAccountValue; import org.hyperledger.besu.ethereum.worldstate.WorldStateStorage; -import java.util.ArrayList; import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.IntStream; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; @@ -31,31 +33,43 @@ import org.apache.tuweni.units.bigints.UInt256; public class TrieGenerator { - public static MerklePatriciaTrie generateTrie( + public static MerklePatriciaTrie generateTrie( final WorldStateStorage worldStateStorage, final int nbAccounts) { - final List accountHash = new ArrayList<>(); - final MerklePatriciaTrie accountStateTrie = + return generateTrie( + worldStateStorage, + IntStream.range(0, nbAccounts) + .mapToObj(operand -> Hash.wrap(Bytes32.leftPad(Bytes.of(operand + 1)))) + .collect(Collectors.toList())); + } + + public static MerklePatriciaTrie generateTrie( + final WorldStateStorage worldStateStorage, final List accounts) { + final MerklePatriciaTrie accountStateTrie = emptyAccountStateTrie(worldStateStorage); // Add some storage values - for (int i = 0; i < nbAccounts; i++) { + for (int i = 0; i < accounts.size(); i++) { final WorldStateStorage.Updater updater = worldStateStorage.updater(); - - accountHash.add(Hash.wrap(Bytes32.leftPad(Bytes.of(i + 1)))); - final MerklePatriciaTrie storageTrie = - emptyStorageTrie(worldStateStorage, accountHash.get(i)); - writeStorageValue(storageTrie, UInt256.ONE, UInt256.valueOf(2L)); - writeStorageValue(storageTrie, UInt256.valueOf(2L), UInt256.valueOf(4L)); - writeStorageValue(storageTrie, UInt256.valueOf(3L), UInt256.valueOf(6L)); + final MerklePatriciaTrie storageTrie = + emptyStorageTrie(worldStateStorage, accounts.get(i)); + writeStorageValue(updater, storageTrie, accounts.get(i), UInt256.ONE, UInt256.valueOf(2L)); + writeStorageValue( + updater, storageTrie, accounts.get(i), UInt256.valueOf(2L), UInt256.valueOf(4L)); + writeStorageValue( + updater, storageTrie, accounts.get(i), UInt256.valueOf(3L), UInt256.valueOf(6L)); int accountIndex = i; storageTrie.commit( (location, hash, value) -> - updater.putAccountStorageTrieNode( - accountHash.get(accountIndex), location, hash, value)); + updater.putAccountStorageTrieNode(accounts.get(accountIndex), location, hash, value)); final Bytes code = Bytes32.leftPad(Bytes.of(i + 10)); final Hash codeHash = Hash.hash(code); final StateTrieAccountValue accountValue = new StateTrieAccountValue(1L, Wei.of(2L), Hash.wrap(storageTrie.getRootHash()), codeHash); - accountStateTrie.put(accountHash.get(i), RLP.encode(accountValue::writeTo)); + accountStateTrie.put(accounts.get(i), RLP.encode(accountValue::writeTo)); + if (worldStateStorage instanceof BonsaiWorldStateKeyValueStorage) { + ((BonsaiWorldStateKeyValueStorage.Updater) updater) + .putAccountInfoState(accounts.get(i), RLP.encode(accountValue::writeTo)); + updater.putCode(accounts.get(i), code); + } accountStateTrie.commit(updater::putAccountStateTrieNode); updater.putCode(codeHash, code); // Persist updates @@ -65,13 +79,21 @@ public class TrieGenerator { } private static void writeStorageValue( - final MerklePatriciaTrie storageTrie, + final WorldStateStorage.Updater updater, + final MerklePatriciaTrie storageTrie, + final Hash hash, final UInt256 key, final UInt256 value) { - storageTrie.put(storageKeyHash(key), encodeStorageValue(value)); + final Hash keyHash = storageKeyHash(key); + final Bytes encodedValue = encodeStorageValue(value); + storageTrie.put(keyHash, encodeStorageValue(value)); + if (updater instanceof BonsaiWorldStateKeyValueStorage.Updater) { + ((BonsaiWorldStateKeyValueStorage.Updater) updater) + .putStorageValueBySlotHash(hash, keyHash, encodedValue); + } } - private static Bytes32 storageKeyHash(final UInt256 storageKey) { + private static Hash storageKeyHash(final UInt256 storageKey) { return Hash.hash(storageKey); } @@ -79,7 +101,7 @@ public class TrieGenerator { return RLP.encode(out -> out.writeBytes(storageValue.toMinimalBytes())); } - public static MerklePatriciaTrie emptyStorageTrie( + public static MerklePatriciaTrie emptyStorageTrie( final WorldStateStorage worldStateStorage, final Hash accountHash) { return new StoredMerklePatriciaTrie<>( (location, hash) -> @@ -88,7 +110,7 @@ public class TrieGenerator { b -> b); } - public static MerklePatriciaTrie emptyAccountStateTrie( + public static MerklePatriciaTrie emptyAccountStateTrie( final WorldStateStorage worldStateStorage) { return new StoredMerklePatriciaTrie<>( worldStateStorage::getAccountStateTrieNode, b -> b, b -> b); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/BlockImportExceptionHandlingTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/BlockImportExceptionHandlingTest.java index 50ab8b6f7c..6e7967b086 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/BlockImportExceptionHandlingTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/BlockImportExceptionHandlingTest.java @@ -26,6 +26,7 @@ import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.bonsai.BonsaiPersistedWorldState; import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateArchive; import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateKeyValueStorage; +import org.hyperledger.besu.ethereum.bonsai.CachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.chain.BadBlockManager; import org.hyperledger.besu.ethereum.chain.MutableBlockchain; import org.hyperledger.besu.ethereum.core.Block; @@ -44,6 +45,7 @@ import org.hyperledger.besu.ethereum.mainnet.MainnetTransactionProcessor; import org.hyperledger.besu.ethereum.storage.StorageProvider; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; import org.hyperledger.besu.ethereum.worldstate.WorldStateStorage; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.exception.StorageException; import java.util.Optional; @@ -75,10 +77,12 @@ public class BlockImportExceptionHandlingTest { private final WorldStateStorage worldStateStorage = new BonsaiWorldStateKeyValueStorage(storageProvider); + private CachedMerkleTrieLoader cachedMerkleTrieLoader; + private final WorldStateArchive worldStateArchive = // contains a BonsaiPersistedWorldState which we need to spy on. // do we need to also test with a DefaultWorldStateArchive? - spy(new BonsaiWorldStateArchive(storageProvider, blockchain)); + spy(new BonsaiWorldStateArchive(storageProvider, blockchain, cachedMerkleTrieLoader)); private final BonsaiPersistedWorldState persisted = spy( @@ -98,6 +102,7 @@ public class BlockImportExceptionHandlingTest { mainnetBlockValidator = new MainnetBlockValidator( blockHeaderValidator, blockBodyValidator, blockProcessor, badBlockManager); + cachedMerkleTrieLoader = new CachedMerkleTrieLoader(new NoOpMetricsSystem()); } @Test diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/AbstractIsolationTests.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/AbstractIsolationTests.java index 32d77cd48a..27e52e1866 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/AbstractIsolationTests.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/AbstractIsolationTests.java @@ -114,7 +114,8 @@ public abstract class AbstractIsolationTests { createKeyValueStorageProvider().createWorldStateStorage(DataStorageFormat.BONSAI), blockchain, Optional.of(16L), - shouldUseSnapshots()); + shouldUseSnapshots(), + new CachedMerkleTrieLoader(new NoOpMetricsSystem())); var ws = archive.getMutable(); genesisState.writeStateTo(ws); ws.persist(blockchain.getChainHeadHeader()); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldStateArchiveTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldStateArchiveTest.java index e9c96731ff..9802cc99ca 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldStateArchiveTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldStateArchiveTest.java @@ -34,6 +34,7 @@ import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.storage.StorageProvider; import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.storage.KeyValueStorageTransaction; import org.hyperledger.besu.plugin.services.storage.SnappableKeyValueStorage; @@ -62,10 +63,13 @@ public class BonsaiSnapshotWorldStateArchiveTest { BonsaiWorldStateArchive bonsaiWorldStateArchive; + CachedMerkleTrieLoader cachedMerkleTrieLoader; + @Before public void setUp() { when(storageProvider.getStorageBySegmentIdentifier(any(KeyValueSegmentIdentifier.class))) .thenReturn(keyValueStorage); + cachedMerkleTrieLoader = new CachedMerkleTrieLoader(new NoOpMetricsSystem()); } @Test @@ -85,7 +89,8 @@ public class BonsaiSnapshotWorldStateArchiveTest { new BonsaiWorldStateKeyValueStorage(storageProvider), blockchain, Optional.of(1L), - true); + true, + cachedMerkleTrieLoader); assertThat(bonsaiWorldStateArchive.getMutable(null, chainHead.getHash(), true)) .containsInstanceOf(BonsaiPersistedWorldState.class); @@ -95,7 +100,10 @@ public class BonsaiSnapshotWorldStateArchiveTest { public void testGetMutableReturnEmptyWhenLoadMoreThanLimitLayersBack() { bonsaiWorldStateArchive = new BonsaiWorldStateArchive( - new BonsaiWorldStateKeyValueStorage(storageProvider), blockchain, Optional.of(512L)); + new BonsaiWorldStateKeyValueStorage(storageProvider), + blockchain, + Optional.of(512L), + cachedMerkleTrieLoader); final BlockHeader blockHeader = blockBuilder.number(0).buildHeader(); final BlockHeader chainHead = blockBuilder.number(512).buildHeader(); when(blockchain.getBlockHeader(eq(blockHeader.getHash()))).thenReturn(Optional.of(blockHeader)); @@ -136,7 +144,8 @@ public class BonsaiSnapshotWorldStateArchiveTest { new SnapshotTrieLogManager(blockchain, worldStateStorage, 12L, worldStatesByHash), worldStateStorage, blockchain, - true)); + true, + cachedMerkleTrieLoader)); var worldState = (BonsaiPersistedWorldState) bonsaiWorldStateArchive.getMutable(); var updater = spy(bonsaiWorldStateArchive.getUpdaterFromPersistedState(worldState)); when(bonsaiWorldStateArchive.getUpdaterFromPersistedState(worldState)).thenReturn(updater); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchiveTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchiveTest.java index 16eec0d5c1..56676ec214 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchiveTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateArchiveTest.java @@ -35,6 +35,7 @@ import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.storage.StorageProvider; import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.storage.KeyValueStorageTransaction; import org.hyperledger.besu.plugin.services.storage.SnappableKeyValueStorage; @@ -86,7 +87,8 @@ public class BonsaiWorldStateArchiveTest { new BonsaiWorldStateKeyValueStorage(storageProvider), blockchain, Optional.of(1L), - false); + false, + new CachedMerkleTrieLoader(new NoOpMetricsSystem())); assertThat(bonsaiWorldStateArchive.getMutable(null, chainHead.getHash(), true)) .containsInstanceOf(BonsaiPersistedWorldState.class); @@ -99,7 +101,8 @@ public class BonsaiWorldStateArchiveTest { new BonsaiWorldStateKeyValueStorage(storageProvider), blockchain, Optional.of(512L), - false); + false, + new CachedMerkleTrieLoader(new NoOpMetricsSystem())); final BlockHeader blockHeader = blockBuilder.number(0).buildHeader(); final BlockHeader chainHead = blockBuilder.number(512).buildHeader(); when(blockchain.getBlockHeader(eq(blockHeader.getHash()))).thenReturn(Optional.of(blockHeader)); @@ -114,7 +117,8 @@ public class BonsaiWorldStateArchiveTest { new BonsaiWorldStateKeyValueStorage(storageProvider), blockchain, Optional.of(512L), - false); + false, + new CachedMerkleTrieLoader(new NoOpMetricsSystem())); final BlockHeader blockHeader = blockBuilder.number(0).buildHeader(); final BlockHeader chainHead = blockBuilder.number(511).buildHeader(); @@ -146,7 +150,8 @@ public class BonsaiWorldStateArchiveTest { blockchain, worldStateStorage, 12L, layeredWorldStatesByHash), worldStateStorage, blockchain, - false)); + false, + new CachedMerkleTrieLoader(new NoOpMetricsSystem()))); final BlockHeader blockHeader = blockBuilder.number(0).buildHeader(); when(blockchain.getBlockHeader(eq(blockHeader.getHash()))).thenReturn(Optional.of(blockHeader)); @@ -173,7 +178,8 @@ public class BonsaiWorldStateArchiveTest { blockchain, worldStateStorage, 12L, layeredWorldStatesByHash), worldStateStorage, blockchain, - false)); + false, + new CachedMerkleTrieLoader(new NoOpMetricsSystem()))); var worldState = (BonsaiPersistedWorldState) bonsaiWorldStateArchive.getMutable(); var updater = spy(bonsaiWorldStateArchive.getUpdaterFromPersistedState(worldState)); @@ -220,7 +226,8 @@ public class BonsaiWorldStateArchiveTest { blockchain, worldStateStorage, 12L, layeredWorldStatesByHash), worldStateStorage, blockchain, - false)); + false, + new CachedMerkleTrieLoader(new NoOpMetricsSystem()))); var worldState = (BonsaiPersistedWorldState) bonsaiWorldStateArchive.getMutable(); var updater = spy(bonsaiWorldStateArchive.getUpdaterFromPersistedState(worldState)); when(bonsaiWorldStateArchive.getUpdaterFromPersistedState(worldState)).thenReturn(updater); @@ -271,7 +278,8 @@ public class BonsaiWorldStateArchiveTest { blockchain, worldStateStorage, 12L, layeredWorldStatesByHash), worldStateStorage, blockchain, - false)); + false, + new CachedMerkleTrieLoader(new NoOpMetricsSystem()))); var worldState = (BonsaiPersistedWorldState) bonsaiWorldStateArchive.getMutable(); var updater = spy(bonsaiWorldStateArchive.getUpdaterFromPersistedState(worldState)); when(bonsaiWorldStateArchive.getUpdaterFromPersistedState(worldState)).thenReturn(updater); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateKeyValueStorageTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateKeyValueStorageTest.java index bf1f6af9df..145cbee9c9 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateKeyValueStorageTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiWorldStateKeyValueStorageTest.java @@ -168,7 +168,7 @@ public class BonsaiWorldStateKeyValueStorageTest { @Test public void getAccount_loadFromTrieWhenEmpty() { final BonsaiWorldStateKeyValueStorage storage = spy(emptyStorage()); - MerklePatriciaTrie trie = TrieGenerator.generateTrie(storage, 1); + MerklePatriciaTrie trie = TrieGenerator.generateTrie(storage, 1); final TreeMap accounts = (TreeMap) trie.entriesFrom(root -> StorageEntriesCollector.collectEntries(root, Hash.ZERO, 1)); @@ -192,7 +192,7 @@ public class BonsaiWorldStateKeyValueStorageTest { @Test public void getStorage_loadFromTrieWhenEmpty() { final BonsaiWorldStateKeyValueStorage storage = spy(emptyStorage()); - final MerklePatriciaTrie trie = TrieGenerator.generateTrie(storage, 1); + final MerklePatriciaTrie trie = TrieGenerator.generateTrie(storage, 1); final TreeMap accounts = (TreeMap) trie.entriesFrom(root -> StorageEntriesCollector.collectEntries(root, Hash.ZERO, 1)); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/CachedMerkleTrieLoaderTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/CachedMerkleTrieLoaderTest.java new file mode 100644 index 0000000000..3c92e949ae --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/CachedMerkleTrieLoaderTest.java @@ -0,0 +1,171 @@ +/* + * Copyright Hyperledger Besu contributors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + * + */ +package org.hyperledger.besu.ethereum.bonsai; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; +import org.hyperledger.besu.ethereum.core.TrieGenerator; +import org.hyperledger.besu.ethereum.rlp.RLP; +import org.hyperledger.besu.ethereum.storage.StorageProvider; +import org.hyperledger.besu.ethereum.trie.MerklePatriciaTrie; +import org.hyperledger.besu.ethereum.trie.StoredMerklePatriciaTrie; +import org.hyperledger.besu.ethereum.trie.TrieIterator; +import org.hyperledger.besu.ethereum.worldstate.StateTrieAccountValue; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.apache.tuweni.bytes.Bytes; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mockito; + +public class CachedMerkleTrieLoaderTest { + + private CachedMerkleTrieLoader merkleTrieLoader; + private final StorageProvider storageProvider = new InMemoryKeyValueStorageProvider(); + private final BonsaiWorldStateKeyValueStorage inMemoryWorldState = + Mockito.spy(new BonsaiWorldStateKeyValueStorage(storageProvider)); + + final List
accounts = + List.of(Address.fromHexString("0xdeadbeef"), Address.fromHexString("0xdeadbeee")); + + private MerklePatriciaTrie trie; + + @Before + public void setup() { + trie = + TrieGenerator.generateTrie( + inMemoryWorldState, accounts.stream().map(Hash::hash).collect(Collectors.toList())); + merkleTrieLoader = new CachedMerkleTrieLoader(new NoOpMetricsSystem()); + } + + @Test + public void shouldAddAccountNodesInCacheDuringPreload() { + merkleTrieLoader.cacheAccountNodes( + inMemoryWorldState, Hash.wrap(trie.getRootHash()), accounts.get(0)); + + final BonsaiWorldStateKeyValueStorage emptyStorage = + new BonsaiWorldStateKeyValueStorage(new InMemoryKeyValueStorageProvider()); + StoredMerklePatriciaTrie cachedTrie = + new StoredMerklePatriciaTrie<>( + (location, hash) -> + merkleTrieLoader.getAccountStateTrieNode(emptyStorage, location, hash), + trie.getRootHash(), + Function.identity(), + Function.identity()); + + final Hash hashAccountZero = Hash.hash(accounts.get(0)); + assertThat(cachedTrie.get(hashAccountZero)).isEqualTo(trie.get(hashAccountZero)); + } + + @Test + public void shouldAddStorageNodesInCacheDuringPreload() { + final Hash hashAccountZero = Hash.hash(accounts.get(0)); + final StateTrieAccountValue stateTrieAccountValue = + StateTrieAccountValue.readFrom(RLP.input(trie.get(hashAccountZero).orElseThrow())); + final StoredMerklePatriciaTrie storageTrie = + new StoredMerklePatriciaTrie<>( + (location, hash) -> + inMemoryWorldState.getAccountStorageTrieNode(hashAccountZero, location, hash), + stateTrieAccountValue.getStorageRoot(), + Function.identity(), + Function.identity()); + final List originalSlots = new ArrayList<>(); + storageTrie.visitLeafs( + (keyHash, node) -> { + merkleTrieLoader.cacheStorageNodes( + inMemoryWorldState, accounts.get(0), Hash.wrap(keyHash)); + originalSlots.add(node.getRlp()); + return TrieIterator.State.CONTINUE; + }); + + final List cachedSlots = new ArrayList<>(); + final BonsaiWorldStateKeyValueStorage emptyStorage = + new BonsaiWorldStateKeyValueStorage(new InMemoryKeyValueStorageProvider()); + final StoredMerklePatriciaTrie cachedTrie = + new StoredMerklePatriciaTrie<>( + (location, hash) -> + merkleTrieLoader.getAccountStorageTrieNode( + emptyStorage, hashAccountZero, location, hash), + stateTrieAccountValue.getStorageRoot(), + Function.identity(), + Function.identity()); + cachedTrie.visitLeafs( + (keyHash, node) -> { + cachedSlots.add(node.getRlp()); + return TrieIterator.State.CONTINUE; + }); + assertThat(originalSlots).isNotEmpty(); + assertThat(originalSlots).isEqualTo(cachedSlots); + } + + @Test + public void shouldFallbackWhenAccountNodesIsNotInCache() { + final StoredMerklePatriciaTrie cachedTrie = + new StoredMerklePatriciaTrie<>( + (location, hash) -> + merkleTrieLoader.getAccountStateTrieNode(inMemoryWorldState, location, hash), + trie.getRootHash(), + Function.identity(), + Function.identity()); + final Hash hashAccountZero = Hash.hash(accounts.get(0)); + assertThat(cachedTrie.get(hashAccountZero)).isEqualTo(trie.get(hashAccountZero)); + } + + @Test + public void shouldFallbackWhenStorageNodesIsNotInCache() { + final Hash hashAccountZero = Hash.hash(accounts.get(0)); + final StateTrieAccountValue stateTrieAccountValue = + StateTrieAccountValue.readFrom(RLP.input(trie.get(hashAccountZero).orElseThrow())); + final StoredMerklePatriciaTrie storageTrie = + new StoredMerklePatriciaTrie<>( + (location, hash) -> + inMemoryWorldState.getAccountStorageTrieNode(hashAccountZero, location, hash), + stateTrieAccountValue.getStorageRoot(), + Function.identity(), + Function.identity()); + final List originalSlots = new ArrayList<>(); + storageTrie.visitLeafs( + (keyHash, node) -> { + originalSlots.add(node.getRlp()); + return TrieIterator.State.CONTINUE; + }); + + final List cachedSlots = new ArrayList<>(); + final StoredMerklePatriciaTrie cachedTrie = + new StoredMerklePatriciaTrie<>( + (location, hash) -> + merkleTrieLoader.getAccountStorageTrieNode( + inMemoryWorldState, hashAccountZero, location, hash), + stateTrieAccountValue.getStorageRoot(), + Function.identity(), + Function.identity()); + cachedTrie.visitLeafs( + (keyHash, node) -> { + cachedSlots.add(node.getRlp()); + return TrieIterator.State.CONTINUE; + }); + assertThat(originalSlots).isNotEmpty(); + assertThat(originalSlots).isEqualTo(cachedSlots); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/LogRollingTests.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/LogRollingTests.java index bee1131855..7c4e1b4101 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/LogRollingTests.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/LogRollingTests.java @@ -32,6 +32,7 @@ import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier; import org.hyperledger.besu.evm.account.MutableAccount; import org.hyperledger.besu.evm.log.LogsBloomFilter; import org.hyperledger.besu.evm.worldstate.WorldUpdater; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.plugin.services.storage.KeyValueStorage; import org.hyperledger.besu.plugin.services.storage.KeyValueStorageTransaction; import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage; @@ -109,7 +110,9 @@ public class LogRollingTests { @Before public void createStorage() { final InMemoryKeyValueStorageProvider provider = new InMemoryKeyValueStorageProvider(); - archive = new BonsaiWorldStateArchive(provider, blockchain); + final CachedMerkleTrieLoader cachedMerkleTrieLoader = + new CachedMerkleTrieLoader(new NoOpMetricsSystem()); + archive = new BonsaiWorldStateArchive(provider, blockchain, cachedMerkleTrieLoader); accountStorage = (InMemoryKeyValueStorage) provider.getStorageBySegmentIdentifier(KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE); @@ -128,7 +131,10 @@ public class LogRollingTests { provider.getStorageBySegmentIdentifier(KeyValueSegmentIdentifier.TRIE_LOG_STORAGE); final InMemoryKeyValueStorageProvider secondProvider = new InMemoryKeyValueStorageProvider(); - secondArchive = new BonsaiWorldStateArchive(secondProvider, blockchain); + final CachedMerkleTrieLoader secondOptimizedMerkleTrieLoader = + new CachedMerkleTrieLoader(new NoOpMetricsSystem()); + secondArchive = + new BonsaiWorldStateArchive(secondProvider, blockchain, secondOptimizedMerkleTrieLoader); secondAccountStorage = (InMemoryKeyValueStorage) secondProvider.getStorageBySegmentIdentifier( diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/RollingImport.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/RollingImport.java index a1cf2948d6..5f37ea292e 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/RollingImport.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/RollingImport.java @@ -21,6 +21,7 @@ import static com.google.common.base.Preconditions.checkArgument; import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.services.kvstore.InMemoryKeyValueStorage; import org.hyperledger.besu.util.io.RollingFileReader; @@ -38,7 +39,10 @@ public class RollingImport { new RollingFileReader((i, c) -> Path.of(String.format(arg[0] + "-%04d.rdat", i)), false); final InMemoryKeyValueStorageProvider provider = new InMemoryKeyValueStorageProvider(); - final BonsaiWorldStateArchive archive = new BonsaiWorldStateArchive(provider, null); + final CachedMerkleTrieLoader cachedMerkleTrieLoader = + new CachedMerkleTrieLoader(new NoOpMetricsSystem()); + final BonsaiWorldStateArchive archive = + new BonsaiWorldStateArchive(provider, null, cachedMerkleTrieLoader); final InMemoryKeyValueStorage accountStorage = (InMemoryKeyValueStorage) provider.getStorageBySegmentIdentifier(KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/proof/WorldStateRangeProofProviderTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/proof/WorldStateRangeProofProviderTest.java index 373628591e..dc795f2636 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/proof/WorldStateRangeProofProviderTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/proof/WorldStateRangeProofProviderTest.java @@ -53,7 +53,7 @@ public class WorldStateRangeProofProviderTest { @Test public void rangeProofValidationNominalCase() { - final MerklePatriciaTrie accountStateTrie = + final MerklePatriciaTrie accountStateTrie = TrieGenerator.generateTrie(worldStateStorage, 15); // collect accounts in range final RangeStorageEntriesCollector collector = @@ -82,7 +82,7 @@ public class WorldStateRangeProofProviderTest { @Test public void rangeProofValidationMissingAccount() { - MerklePatriciaTrie accountStateTrie = + MerklePatriciaTrie accountStateTrie = TrieGenerator.generateTrie(worldStateStorage, 15); // collect accounts in range final RangeStorageEntriesCollector collector = @@ -120,7 +120,7 @@ public class WorldStateRangeProofProviderTest { @Test public void rangeProofValidationNoMonotonicIncreasing() { - MerklePatriciaTrie accountStateTrie = + MerklePatriciaTrie accountStateTrie = TrieGenerator.generateTrie(worldStateStorage, 15); // generate the invalid proof @@ -157,7 +157,7 @@ public class WorldStateRangeProofProviderTest { @Test public void rangeProofValidationEmptyProof() { - MerklePatriciaTrie accountStateTrie = + MerklePatriciaTrie accountStateTrie = TrieGenerator.generateTrie(worldStateStorage, 15); // generate the invalid proof @@ -185,7 +185,7 @@ public class WorldStateRangeProofProviderTest { @Test public void rangeProofValidationInvalidEmptyProof() { - MerklePatriciaTrie accountStateTrie = + MerklePatriciaTrie accountStateTrie = TrieGenerator.generateTrie(worldStateStorage, 15); // generate the invalid proof diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/RangeManagerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/RangeManagerTest.java index 37c8e6d77e..028dd1edf4 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/RangeManagerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/RangeManagerTest.java @@ -101,7 +101,7 @@ public final class RangeManagerTest { final WorldStateStorage worldStateStorage = new WorldStateKeyValueStorage(new InMemoryKeyValueStorage()); - final MerklePatriciaTrie accountStateTrie = + final MerklePatriciaTrie accountStateTrie = TrieGenerator.generateTrie(worldStateStorage, 15); final RangeStorageEntriesCollector collector = @@ -140,7 +140,7 @@ public final class RangeManagerTest { final WorldStateStorage worldStateStorage = new WorldStateKeyValueStorage(new InMemoryKeyValueStorage()); - final MerklePatriciaTrie accountStateTrie = + final MerklePatriciaTrie accountStateTrie = TrieGenerator.generateTrie(worldStateStorage, 15); final RangeStorageEntriesCollector collector = diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/StackTrieTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/StackTrieTest.java index dc6f9fffc0..992ebd6068 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/StackTrieTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/StackTrieTest.java @@ -47,7 +47,7 @@ public class StackTrieTest { final WorldStateStorage recreatedWorldStateStorage = new WorldStateKeyValueStorage(new InMemoryKeyValueStorage()); - final MerklePatriciaTrie accountStateTrie = + final MerklePatriciaTrie accountStateTrie = TrieGenerator.generateTrie(worldStateStorage, nbAccounts); final StackTrie stackTrie = @@ -99,7 +99,7 @@ public class StackTrieTest { final WorldStateStorage recreatedWorldStateStorage = new WorldStateKeyValueStorage(new InMemoryKeyValueStorage()); - final MerklePatriciaTrie accountStateTrie = + final MerklePatriciaTrie accountStateTrie = TrieGenerator.generateTrie(worldStateStorage, nbAccounts); final StackTrie stackTrie = diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/TaskGenerator.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/TaskGenerator.java index d4aeab3d1f..6bc933c95a 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/TaskGenerator.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/TaskGenerator.java @@ -49,8 +49,7 @@ public class TaskGenerator { final WorldStateProofProvider worldStateProofProvider = new WorldStateProofProvider(worldStateStorage); - final MerklePatriciaTrie trie = - TrieGenerator.generateTrie(worldStateStorage, 1); + final MerklePatriciaTrie trie = TrieGenerator.generateTrie(worldStateStorage, 1); final RangeStorageEntriesCollector collector = RangeStorageEntriesCollector.createCollector( Bytes32.ZERO, RangeManager.MAX_RANGE, 1, Integer.MAX_VALUE); diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 5be9b2e471..7ecaed10db 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -2322,6 +2322,11 @@ + + + + + @@ -2343,6 +2348,14 @@ + + + + + + + + diff --git a/gradle/versions.gradle b/gradle/versions.gradle index 7a8057a7d0..1c03023792 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -100,6 +100,7 @@ dependencyManagement { dependency 'io.prometheus:simpleclient_common:0.9.0' dependency 'io.prometheus:simpleclient_hotspot:0.9.0' dependency 'io.prometheus:simpleclient_pushgateway:0.9.0' + dependency 'io.prometheus:simpleclient_guava:0.16.0' dependency 'io.reactivex.rxjava2:rxjava:2.2.21'