diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiTrieFactory.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiTrieFactory.java new file mode 100644 index 00000000000..029a2bc1f9c --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiTrieFactory.java @@ -0,0 +1,81 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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.trie.pathbased.bonsai.worldview; + +import org.hyperledger.besu.ethereum.trie.MerkleTrie; +import org.hyperledger.besu.ethereum.trie.NoOpMerkleTrie; +import org.hyperledger.besu.ethereum.trie.NodeLoader; +import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig; +import org.hyperledger.besu.ethereum.trie.patricia.ParallelStoredMerklePatriciaTrie; +import org.hyperledger.besu.ethereum.trie.patricia.StoredMerklePatriciaTrie; + +import java.util.function.Function; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; + +/** + * Creates Merkle trie instances based on the execution context. This centralizes the decision of + * which trie implementation to use, keeping it out of the callers. + * + *

The abstraction is intentionally minimal and package-private. It is introduced to eliminate + * the overhead of {@link ParallelStoredMerklePatriciaTrie} in latency-sensitive paths (like + * per-transaction {@code frontierRootHash()} calls) while preserving it for throughput-oriented + * batch computation. The structure is designed so the policy can be elevated to a broader scope in + * the future without changing callers. + */ +public class BonsaiTrieFactory { + + /** + * Describes the execution profile under which a trie is being created. This is not a choice of + * implementation — it is a declaration of latency/throughput intent that the factory maps to the + * appropriate trie type. + */ + enum TrieMode { + /** May use parallel trie if the global config allows it. */ + PARALLELIZE_ALLOWED, + + /** Always uses sequential trie regardless of config. */ + ALWAYS_SEQUENTIAL + } + + private final WorldStateConfig worldStateConfig; + + BonsaiTrieFactory(final WorldStateConfig worldStateConfig) { + this.worldStateConfig = worldStateConfig; + } + + /** + * Creates a Merkle trie appropriate for the given trieMode context. + * + * @param nodeLoader loader for trie nodes from storage + * @param rootHash root hash to start from + * @param trieMode the execution context declaring latency/throughput intent + * @return a trie instance; never null + */ + MerkleTrie create( + final NodeLoader nodeLoader, final Bytes32 rootHash, final TrieMode trieMode) { + if (worldStateConfig.isTrieDisabled()) { + return new NoOpMerkleTrie<>(); + } + if (trieMode == TrieMode.PARALLELIZE_ALLOWED + && worldStateConfig.isParallelStateRootComputationEnabled()) { + return new ParallelStoredMerklePatriciaTrie<>( + nodeLoader, rootHash, Function.identity(), Function.identity()); + } + return new StoredMerklePatriciaTrie<>( + nodeLoader, rootHash, Function.identity(), Function.identity()); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldState.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldState.java index eb55f9355d2..7e1dc9c4f1a 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldState.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldState.java @@ -20,9 +20,9 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.StorageSlotKey; import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.mainnet.staterootcommitter.StateRootCommitter; import org.hyperledger.besu.ethereum.trie.MerkleTrie; import org.hyperledger.besu.ethereum.trie.MerkleTrieException; -import org.hyperledger.besu.ethereum.trie.NoOpMerkleTrie; import org.hyperledger.besu.ethereum.trie.NodeLoader; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.BonsaiAccount; import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.BonsaiWorldStateProvider; @@ -39,16 +39,14 @@ import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig; import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.accumulator.PathBasedWorldStateUpdateAccumulator; import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.accumulator.preload.StorageConsumingMap; -import org.hyperledger.besu.ethereum.trie.patricia.ParallelStoredMerklePatriciaTrie; -import org.hyperledger.besu.ethereum.trie.patricia.StoredMerklePatriciaTrie; import org.hyperledger.besu.evm.account.Account; import org.hyperledger.besu.evm.internal.EvmConfiguration; +import org.hyperledger.besu.plugin.data.BlockHeader; import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.ConcurrentHashMap; -import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Stream; @@ -64,6 +62,8 @@ public class BonsaiWorldState extends PathBasedWorldState { protected BonsaiCachedMerkleTrieLoader bonsaiCachedMerkleTrieLoader; private final CodeCache codeCache; + private final BonsaiTrieFactory trieFactory; + private final FrontierRootHashTracker frontierRootHashTracker; public BonsaiWorldState( final BonsaiWorldStateProvider archive, @@ -92,7 +92,7 @@ public BonsaiWorldState( super(worldStateKeyValueStorage, cachedWorldStorageManager, trieLogManager, worldStateConfig); this.bonsaiCachedMerkleTrieLoader = bonsaiCachedMerkleTrieLoader; this.worldStateKeyValueStorage = worldStateKeyValueStorage; - this.setAccumulator( + final BonsaiWorldStateUpdateAccumulator acc = new BonsaiWorldStateUpdateAccumulator( this, (addr, value) -> @@ -102,7 +102,20 @@ public BonsaiWorldState( this.bonsaiCachedMerkleTrieLoader.preLoadStorageSlot( getWorldStateStorage(), addr, value), evmConfiguration, - codeCache)); + codeCache); + this.setAccumulator(acc); + this.trieFactory = new BonsaiTrieFactory(worldStateConfig); + this.frontierRootHashTracker = + new FrontierRootHashTracker( + acc, + rootHash -> + trieFactory.create( + (location, hash) -> + bonsaiCachedMerkleTrieLoader.getAccountStateTrieNode( + getWorldStateStorage(), location, hash), + rootHash, + BonsaiTrieFactory.TrieMode.ALWAYS_SEQUENTIAL), + (address, storageUpdates) -> updateFrontierStorageState(acc, address, storageUpdates)); this.codeCache = codeCache; } @@ -248,6 +261,19 @@ private void updateAccountStorageState( final BonsaiWorldStateUpdateAccumulator worldStateUpdater, final Map.Entry>> storageAccountUpdate) { + updateAccountStorageState( + maybeStateUpdater, + worldStateUpdater, + storageAccountUpdate, + BonsaiTrieFactory.TrieMode.PARALLELIZE_ALLOWED); + } + + private void updateAccountStorageState( + final Optional maybeStateUpdater, + final BonsaiWorldStateUpdateAccumulator worldStateUpdater, + final Map.Entry>> + storageAccountUpdate, + final BonsaiTrieFactory.TrieMode trieMode) { final Address updatedAddress = storageAccountUpdate.getKey(); final Hash updatedAddressHash = updatedAddress.addressHash(); if (worldStateUpdater.getAccountsToUpdate().containsKey(updatedAddress)) { @@ -260,11 +286,12 @@ private void updateAccountStorageState( ? Hash.EMPTY_TRIE_HASH : accountOriginal.getStorageRoot(); final MerkleTrie storageTrie = - createTrie( + trieFactory.create( (location, key) -> bonsaiCachedMerkleTrieLoader.getAccountStorageTrieNode( getWorldStateStorage(), updatedAddressHash, location, key), - Bytes32.wrap(storageRoot.getBytes())); + Bytes32.wrap(storageRoot.getBytes()), + trieMode); // for manicured tries and composting, collect branches here (not implemented) for (final Map.Entry> storageUpdate : @@ -394,16 +421,15 @@ static Optional incrementBytes32(final Bytes32 value) { return incremented.isZero() ? Optional.empty() : Optional.of(incremented); } + @Override + public void persist(final BlockHeader blockHeader, final StateRootCommitter committer) { + frontierRootHashTracker.reset(); + super.persist(blockHeader, committer); + } + @Override public Hash frontierRootHash() { - return calculateRootHash( - Optional.of( - new BonsaiWorldStateKeyValueStorage.Updater( - noOpSegmentedTx, - noOpTx, - worldStateKeyValueStorage.getFlatDbStrategy(), - worldStateKeyValueStorage.getComposedWorldStateStorage())), - accumulator.copy()); + return frontierRootHashTracker.frontierRootHash(worldStateRootHash); } @Override @@ -423,6 +449,17 @@ protected Optional getStorageTrieNode( return getWorldStateStorage().getAccountStorageTrieNode(accountHash, location, nodeHash); } + private void updateFrontierStorageState( + final BonsaiWorldStateUpdateAccumulator accumulator, + final Address address, + final StorageConsumingMap> storageUpdates) { + updateAccountStorageState( + Optional.empty(), + accumulator, + Map.entry(address, storageUpdates), + BonsaiTrieFactory.TrieMode.ALWAYS_SEQUENTIAL); + } + private void writeStorageTrieNode( final BonsaiWorldStateKeyValueStorage.Updater stateUpdater, final Hash accountHash, @@ -481,15 +518,7 @@ public void disableCacheMerkleTrieLoader() { } private MerkleTrie createTrie(final NodeLoader nodeLoader, final Bytes32 rootHash) { - if (worldStateConfig.isTrieDisabled()) { - return new NoOpMerkleTrie<>(); - } - if (worldStateConfig.isParallelStateRootComputationEnabled()) { - return new ParallelStoredMerklePatriciaTrie<>( - nodeLoader, rootHash, Function.identity(), Function.identity()); - } - return new StoredMerklePatriciaTrie<>( - nodeLoader, rootHash, Function.identity(), Function.identity()); + return trieFactory.create(nodeLoader, rootHash, BonsaiTrieFactory.TrieMode.PARALLELIZE_ALLOWED); } protected Hash hashAndSavePreImage(final Bytes value) { diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldStateUpdateAccumulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldStateUpdateAccumulator.java index 7a6552454fe..9ed4311ae6c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldStateUpdateAccumulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiWorldStateUpdateAccumulator.java @@ -28,9 +28,13 @@ import org.hyperledger.besu.evm.internal.EvmConfiguration; import org.hyperledger.besu.evm.worldstate.UpdateTrackingAccount; +import java.util.HashSet; +import java.util.Set; + public class BonsaiWorldStateUpdateAccumulator extends PathBasedWorldStateUpdateAccumulator { private final CodeCache codeCache; + private final Set

frontierDirtyAddresses = new HashSet<>(); public BonsaiWorldStateUpdateAccumulator( final PathBasedWorldView world, @@ -43,17 +47,16 @@ public BonsaiWorldStateUpdateAccumulator( this.codeCache = codeCache; } + /** Copy constructor. */ + protected BonsaiWorldStateUpdateAccumulator(final BonsaiWorldStateUpdateAccumulator source) { + super(source); + this.codeCache = source.codeCache; + this.frontierDirtyAddresses.addAll(source.frontierDirtyAddresses); + } + @Override - public PathBasedWorldStateUpdateAccumulator copy() { - final BonsaiWorldStateUpdateAccumulator copy = - new BonsaiWorldStateUpdateAccumulator( - wrappedWorldView(), - getAccountPreloader(), - getStoragePreloader(), - getEvmConfiguration(), - codeCache); - copy.cloneFromUpdater(this); - return copy; + public BonsaiWorldStateUpdateAccumulator copy() { + return new BonsaiWorldStateUpdateAccumulator(this); } @Override @@ -102,6 +105,27 @@ protected void assertCloseEnoughForDiffing( BonsaiAccount.assertCloseEnoughForDiffing(source, account, context); } + @Override + public void commit() { + super.commit(); + getDeletedAccountAddresses().forEach(frontierDirtyAddresses::add); + getUpdatedAccounts().forEach(account -> frontierDirtyAddresses.add(account.getAddress())); + } + + public Set
getFrontierDirtyAddresses() { + return new HashSet<>(frontierDirtyAddresses); + } + + public void clearFrontierDirtyAddresses(final Set
processed) { + frontierDirtyAddresses.removeAll(processed); + } + + @Override + public void reset() { + super.reset(); + frontierDirtyAddresses.clear(); + } + @Override public CodeCache codeCache() { return codeCache; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/FrontierRootHashTracker.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/FrontierRootHashTracker.java new file mode 100644 index 00000000000..cd0dd09c4e9 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/FrontierRootHashTracker.java @@ -0,0 +1,163 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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.trie.pathbased.bonsai.worldview; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.StorageSlotKey; +import org.hyperledger.besu.ethereum.trie.MerkleTrie; +import org.hyperledger.besu.ethereum.trie.MerkleTrieException; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.BonsaiAccount; +import org.hyperledger.besu.ethereum.trie.pathbased.common.PathBasedValue; +import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.accumulator.preload.StorageConsumingMap; + +import java.util.Optional; +import java.util.Set; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; + +public class FrontierRootHashTracker { + + /** Creates a Merkle trie for the frontier account state from a given root hash. */ + @FunctionalInterface + public interface AccountTrieFactory { + MerkleTrie create(Bytes32 rootHash); + } + + /** Updates the storage root for an account that has pending storage changes. */ + @FunctionalInterface + public interface StorageRootUpdater { + void update( + Address address, + StorageConsumingMap> storageUpdates); + } + + private final BonsaiWorldStateUpdateAccumulator accumulator; + private final AccountTrieFactory accountTrieFactory; + private final StorageRootUpdater storageRootUpdater; + + private MerkleTrie frontierTrie; + private Hash frontierRootHashCache; + + public FrontierRootHashTracker( + final BonsaiWorldStateUpdateAccumulator accumulator, + final AccountTrieFactory accountTrieFactory, + final StorageRootUpdater storageRootUpdater) { + this.accumulator = accumulator; + this.accountTrieFactory = accountTrieFactory; + this.storageRootUpdater = storageRootUpdater; + } + + /** + * Computes the intermediate state root reflecting all transactions committed so far in the + * current block. Called once per transaction on pre-Byzantium blocks to populate the receipt's + * state root field. + * + *

The trie is created lazily on the first call and cached across subsequent calls within the + * same block. Only accounts dirtied since the last call are applied, keeping per-call cost + * proportional to the transaction's footprint rather than the entire block's accumulated state. + * + *

Must be called after {@code commit()} and {@code markTransactionBoundary()} so that + * dirty addresses have been captured. Must be followed by {@link #reset()} (via {@code + * persist()}) at the block boundary to discard the cached trie before the next block. + * + * @param baseRootHash the persisted state root from the end of the previous block + * @return the state root hash incorporating all committed transactions + */ + public Hash frontierRootHash(final Hash baseRootHash) { + final Set

dirty = accumulator.getFrontierDirtyAddresses(); + + if (dirty.isEmpty()) { + if (frontierRootHashCache == null) { + frontierRootHashCache = baseRootHash; + } + return frontierRootHashCache; + } + + if (frontierTrie == null) { + frontierTrie = accountTrieFactory.create(Bytes32.wrap(baseRootHash.getBytes())); + } + + try { + for (final Address address : dirty) { + final StorageConsumingMap> storageUpdates = + accumulator.getStorageToUpdate().get(address); + if (storageUpdates != null) { + storageRootUpdater.update(address, storageUpdates); + } else if (accumulator.getStorageToClear().contains(address)) { + final PathBasedValue accountValue = + accumulator.getAccountsToUpdate().get(address); + if (accountValue != null && accountValue.getUpdated() != null) { + accountValue.getUpdated().setStorageRoot(Hash.EMPTY_TRIE_HASH); + } + } + + // Every dirty address was added during commit(), which runs super.commit() first. + // super.commit() always populates accountsToUpdate before we capture the address, + // so a missing entry here means a bug in the dirty-tracking logic. + final PathBasedValue accountValue = + accumulator.getAccountsToUpdate().get(address); + if (accountValue == null) { + throw new IllegalStateException( + "Dirty address " + address.toHexString() + " not found in accountsToUpdate"); + } + final BonsaiAccount updated = accountValue.getUpdated(); + if (updated == null) { + removeAccountFromTrie(frontierTrie, address); + } else { + updateAccountInTrie(frontierTrie, address, updated); + } + } + + frontierRootHashCache = Hash.wrap(frontierTrie.getRootHash()); + accumulator.clearFrontierDirtyAddresses(dirty); + return frontierRootHashCache; + } catch (final MerkleTrieException e) { + // Discard the potentially-corrupted cached trie so the next call rebuilds from scratch. + // Dirty addresses are intentionally NOT cleared — they will be reprocessed on retry. + reset(); + throw e; + } + } + + public void reset() { + frontierTrie = null; + frontierRootHashCache = null; + } + + private static void removeAccountFromTrie( + final MerkleTrie trie, final Address address) { + try { + trie.remove(address.addressHash().getBytes()); + } catch (final MerkleTrieException e) { + throw new MerkleTrieException( + e.getMessage(), Optional.of(address), e.getHash(), e.getLocation()); + } + } + + private static void updateAccountInTrie( + final MerkleTrie trie, + final Address address, + final BonsaiAccount updatedAccount) { + try { + trie.put(updatedAccount.getAddressHash().getBytes(), updatedAccount.serializeAccount()); + } catch (final MerkleTrieException e) { + throw new MerkleTrieException( + e.getMessage(), Optional.of(address), e.getHash(), e.getLocation()); + } + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/accumulator/PathBasedWorldStateUpdateAccumulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/accumulator/PathBasedWorldStateUpdateAccumulator.java index 6ff962e6f01..3cd4213c036 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/accumulator/PathBasedWorldStateUpdateAccumulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/pathbased/common/worldview/accumulator/PathBasedWorldStateUpdateAccumulator.java @@ -90,7 +90,14 @@ public PathBasedWorldStateUpdateAccumulator( this.evmConfiguration = evmConfiguration; } - public void cloneFromUpdater(final PathBasedWorldStateUpdateAccumulator source) { + /** Copy constructor. Subclasses should chain via {@code super(source)}. */ + protected PathBasedWorldStateUpdateAccumulator( + final PathBasedWorldStateUpdateAccumulator source) { + this( + source.wrappedWorldView(), + source.accountPreloader, + source.storagePreloader, + source.evmConfiguration); accountsToUpdate.putAll(source.getAccountsToUpdate()); codeToUpdate.putAll(source.codeToUpdate); storageToClear.addAll(source.storageToClear); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiFrontierRootHashTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiFrontierRootHashTest.java new file mode 100644 index 00000000000..62518ea603f --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiFrontierRootHashTest.java @@ -0,0 +1,243 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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.trie.pathbased.bonsai.worldview; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.BonsaiWorldStateProvider; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.util.Optional; + +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.Test; + +class BonsaiFrontierRootHashTest { + + private static final Address CONTRACT = + Address.fromHexString("0xdeadbeefdeadbeefdeadbeefdeadbeefdeadbeef"); + private static final Address ACCOUNT_B = + Address.fromHexString("0x1000000000000000000000000000000000000002"); + private static final Address ACCOUNT_C = + Address.fromHexString("0x1000000000000000000000000000000000000003"); + + @Test + void frontierRootHashMatchesFullRecalculationWhenSameAccountChangesTwice() { + final BonsaiWorldState worldState = createWorldStateWithContractStorage(); + + final WorldUpdater firstUpdater = worldState.updater(); + final MutableAccount firstAccount = firstUpdater.getAccount(CONTRACT); + firstAccount.setBalance(Wei.of(2)); + firstAccount.setStorageValue(UInt256.ONE, UInt256.valueOf(2)); + firstUpdater.commit(); + firstUpdater.markTransactionBoundary(); + + // frontierRootHash reflects the state after TX 1 (unlike rootHash which stays at the + // persisted block root until persist() is called) + final Hash firstFrontierRoot = worldState.frontierRootHash(); + assertThat(firstFrontierRoot).isEqualTo(fullRecalculatedRoot(worldState)); + + final WorldUpdater secondUpdater = worldState.updater(); + final MutableAccount secondAccount = secondUpdater.getAccount(CONTRACT); + secondAccount.setBalance(Wei.of(3)); + secondAccount.setStorageValue(UInt256.ONE, UInt256.valueOf(3)); + secondUpdater.commit(); + secondUpdater.markTransactionBoundary(); + + // frontierRootHash advances with each transaction within the block + final Hash secondFrontierRoot = worldState.frontierRootHash(); + assertThat(secondFrontierRoot).isEqualTo(fullRecalculatedRoot(worldState)); + assertThat(secondFrontierRoot).isNotEqualTo(firstFrontierRoot); + } + + @Test + void frontierRootHashMatchesFullRecalculationWhenStorageIsClearedWithoutNewSlots() { + final BonsaiWorldState worldState = createWorldStateWithContractStorage(); + + final WorldUpdater updater = worldState.updater(); + final MutableAccount account = updater.getAccount(CONTRACT); + account.clearStorage(); + updater.commit(); + updater.markTransactionBoundary(); + + final Hash afterClear = worldState.frontierRootHash(); + assertThat(afterClear).isEqualTo(fullRecalculatedRoot(worldState)); + assertThat(afterClear) + .as("Clearing storage should change the root hash") + .isNotEqualTo(worldState.rootHash()); + } + + @Test + void frontierRootHashHandlesDeleteThenRecreateInSameBlock() { + final BonsaiWorldState worldState = createWorldStateWithContractStorage(); + + final WorldUpdater deleter = worldState.updater(); + deleter.deleteAccount(CONTRACT); + deleter.commit(); + deleter.markTransactionBoundary(); + + final Hash afterDelete = worldState.frontierRootHash(); + assertThat(afterDelete).isEqualTo(fullRecalculatedRoot(worldState)); + assertThat(afterDelete) + .as("Deleting an account should change the root hash") + .isNotEqualTo(worldState.rootHash()); + + final WorldUpdater creator = worldState.updater(); + final MutableAccount recreated = creator.createAccount(CONTRACT); + recreated.setBalance(Wei.of(999)); + recreated.setStorageValue(UInt256.valueOf(7), UInt256.valueOf(7)); + creator.commit(); + creator.markTransactionBoundary(); + + final Hash afterRecreate = worldState.frontierRootHash(); + assertThat(afterRecreate).isEqualTo(fullRecalculatedRoot(worldState)); + assertThat(afterRecreate).isNotEqualTo(afterDelete); + } + + @Test + void frontierRootHashResetsCorrectlyAcrossBlockBoundary() { + final BonsaiWorldState worldState = createWorldStateWithContractStorage(); + + final WorldUpdater blockNUpdater = worldState.updater(); + blockNUpdater.getAccount(CONTRACT).setBalance(Wei.of(50)); + blockNUpdater.commit(); + blockNUpdater.markTransactionBoundary(); + final Hash blockNRoot = worldState.frontierRootHash(); + assertThat(blockNRoot).isEqualTo(fullRecalculatedRoot(worldState)); + + worldState.persist(null); + + final WorldUpdater blockN1Updater = worldState.updater(); + blockN1Updater.getAccount(CONTRACT).setBalance(Wei.of(75)); + blockN1Updater.commit(); + blockN1Updater.markTransactionBoundary(); + final Hash blockN1Root = worldState.frontierRootHash(); + assertThat(blockN1Root).isEqualTo(fullRecalculatedRoot(worldState)); + assertThat(blockN1Root).isNotEqualTo(blockNRoot); + } + + @Test + void frontierRootHashRespectsTrieDisabledConfig() { + final Blockchain blockchain = mock(Blockchain.class); + final BonsaiWorldStateProvider archive = + InMemoryKeyValueStorageProvider.createBonsaiInMemoryWorldStateArchive(blockchain); + final BonsaiWorldState worldState = (BonsaiWorldState) archive.getWorldState(); + + final WorldUpdater setup = worldState.updater(); + setup.createAccount(CONTRACT).setBalance(Wei.of(1)); + setup.commit(); + setup.markTransactionBoundary(); + worldState.persist(null); + + worldState.disableTrie(); + + final WorldUpdater updater = worldState.updater(); + updater.getAccount(CONTRACT).setBalance(Wei.of(42)); + updater.commit(); + updater.markTransactionBoundary(); + + assertThat(worldState.frontierRootHash()).isNotNull(); + } + + @Test + void frontierRootHashMatchesWhenDifferentAccountsDirtyInDifferentTransactions() { + final BonsaiWorldState worldState = createWorldStateWithContractStorage(); + + // TX1 only touches CONTRACT + final WorldUpdater tx1 = worldState.updater(); + tx1.getAccount(CONTRACT).setBalance(Wei.of(10)); + tx1.commit(); + tx1.markTransactionBoundary(); + + final Hash afterTx1 = worldState.frontierRootHash(); + assertThat(afterTx1).isEqualTo(fullRecalculatedRoot(worldState)); + + // TX2 only touches ACCOUNT_B (a new account, not persisted) + final WorldUpdater tx2 = worldState.updater(); + tx2.createAccount(ACCOUNT_B).setBalance(Wei.of(77)); + tx2.commit(); + tx2.markTransactionBoundary(); + + // The tracker should process only ACCOUNT_B for this call, but the result + // must still reflect both TX1 and TX2 + final Hash afterTx2 = worldState.frontierRootHash(); + assertThat(afterTx2).isEqualTo(fullRecalculatedRoot(worldState)); + assertThat(afterTx2).isNotEqualTo(afterTx1); + + // TX3 touches a third account + final WorldUpdater tx3 = worldState.updater(); + tx3.createAccount(ACCOUNT_C).setBalance(Wei.of(33)); + tx3.commit(); + tx3.markTransactionBoundary(); + + final Hash afterTx3 = worldState.frontierRootHash(); + assertThat(afterTx3).isEqualTo(fullRecalculatedRoot(worldState)); + assertThat(afterTx3).isNotEqualTo(afterTx2); + } + + @Test + void frontierRootHashMatchesWhenNewAccountCreatedMidBlock() { + final BonsaiWorldState worldState = createWorldStateWithContractStorage(); + + // Create a brand-new account (not in persisted state) with storage + final WorldUpdater updater = worldState.updater(); + final MutableAccount newAccount = updater.createAccount(ACCOUNT_B); + newAccount.setBalance(Wei.of(500)); + newAccount.setStorageValue(UInt256.valueOf(42), UInt256.valueOf(42)); + updater.commit(); + updater.markTransactionBoundary(); + + final Hash afterCreate = worldState.frontierRootHash(); + assertThat(afterCreate).isEqualTo(fullRecalculatedRoot(worldState)); + assertThat(afterCreate) + .as("Creating a new account should change the root hash") + .isNotEqualTo(worldState.rootHash()); + } + + private static BonsaiWorldState createWorldStateWithContractStorage() { + final Blockchain blockchain = mock(Blockchain.class); + final BonsaiWorldStateProvider archive = + InMemoryKeyValueStorageProvider.createBonsaiInMemoryWorldStateArchive(blockchain); + final BonsaiWorldState worldState = (BonsaiWorldState) archive.getWorldState(); + + final WorldUpdater setup = worldState.updater(); + final MutableAccount account = setup.createAccount(CONTRACT); + account.setBalance(Wei.of(1)); + account.setStorageValue(UInt256.ONE, UInt256.ONE); + setup.commit(); + setup.markTransactionBoundary(); + worldState.persist(null); + + return worldState; + } + + /** + * Canonical oracle: copies the entire accumulator and rebuilds the trie from scratch. This is the + * O(N²) path that the incremental tracker replaces, used here as a reference to verify + * correctness — it cannot produce a wrong result. + */ + private static Hash fullRecalculatedRoot(final BonsaiWorldState worldState) { + final BonsaiWorldStateUpdateAccumulator accumulatorCopy = + (BonsaiWorldStateUpdateAccumulator) worldState.getAccumulator().copy(); + return worldState.calculateRootHash(Optional.empty(), accumulatorCopy); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiTrieFactoryTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiTrieFactoryTest.java new file mode 100644 index 00000000000..348bf9b4cfd --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/BonsaiTrieFactoryTest.java @@ -0,0 +1,93 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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.trie.pathbased.bonsai.worldview; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.ethereum.trie.MerkleTrie; +import org.hyperledger.besu.ethereum.trie.NoOpMerkleTrie; +import org.hyperledger.besu.ethereum.trie.pathbased.common.worldview.WorldStateConfig; +import org.hyperledger.besu.ethereum.trie.patricia.ParallelStoredMerklePatriciaTrie; +import org.hyperledger.besu.ethereum.trie.patricia.StoredMerklePatriciaTrie; + +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.Test; + +class BonsaiTrieFactoryTest { + + private static final Bytes32 EMPTY_ROOT = Bytes32.ZERO; + + @Test + void parallelizeAllowedWithParallelEnabledReturnsParallelTrie() { + final BonsaiTrieFactory factory = createFactory(false, true); + final MerkleTrie trie = + factory.create( + emptyNodeLoader(), EMPTY_ROOT, BonsaiTrieFactory.TrieMode.PARALLELIZE_ALLOWED); + assertThat(trie).isInstanceOf(ParallelStoredMerklePatriciaTrie.class); + } + + @Test + void alwaysSequentialWithParallelEnabledReturnsSequentialTrie() { + final BonsaiTrieFactory factory = createFactory(false, true); + final MerkleTrie trie = + factory.create(emptyNodeLoader(), EMPTY_ROOT, BonsaiTrieFactory.TrieMode.ALWAYS_SEQUENTIAL); + assertThat(trie).isInstanceOf(StoredMerklePatriciaTrie.class); + assertThat(trie).isNotInstanceOf(ParallelStoredMerklePatriciaTrie.class); + } + + @Test + void parallelizeAllowedWithParallelDisabledReturnsSequentialTrie() { + final BonsaiTrieFactory factory = createFactory(false, false); + final MerkleTrie trie = + factory.create( + emptyNodeLoader(), EMPTY_ROOT, BonsaiTrieFactory.TrieMode.PARALLELIZE_ALLOWED); + assertThat(trie).isInstanceOf(StoredMerklePatriciaTrie.class); + assertThat(trie).isNotInstanceOf(ParallelStoredMerklePatriciaTrie.class); + } + + @Test + void trieDisabledReturnsNoOpForParallelizeAllowed() { + final BonsaiTrieFactory factory = createFactory(true, true); + final MerkleTrie trie = + factory.create( + emptyNodeLoader(), EMPTY_ROOT, BonsaiTrieFactory.TrieMode.PARALLELIZE_ALLOWED); + assertThat(trie).isInstanceOf(NoOpMerkleTrie.class); + } + + @Test + void trieDisabledReturnsNoOpForAlwaysSequential() { + final BonsaiTrieFactory factory = createFactory(true, true); + final MerkleTrie trie = + factory.create(emptyNodeLoader(), EMPTY_ROOT, BonsaiTrieFactory.TrieMode.ALWAYS_SEQUENTIAL); + assertThat(trie).isInstanceOf(NoOpMerkleTrie.class); + } + + private static BonsaiTrieFactory createFactory( + final boolean trieDisabled, final boolean parallelEnabled) { + final WorldStateConfig config = + WorldStateConfig.newBuilder() + .trieDisabled(trieDisabled) + .parallelStateRootComputationEnabled(parallelEnabled) + .build(); + return new BonsaiTrieFactory(config); + } + + private static org.hyperledger.besu.ethereum.trie.NodeLoader emptyNodeLoader() { + return (location, hash) -> Optional.empty(); + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/FrontierRootHashTrackerTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/FrontierRootHashTrackerTest.java new file mode 100644 index 00000000000..d76e0e1bd74 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/trie/pathbased/bonsai/worldview/FrontierRootHashTrackerTest.java @@ -0,0 +1,181 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * 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.trie.pathbased.bonsai.worldview; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.datatypes.Address; +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.datatypes.Wei; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider; +import org.hyperledger.besu.ethereum.trie.MerkleTrie; +import org.hyperledger.besu.ethereum.trie.MerkleTrieException; +import org.hyperledger.besu.ethereum.trie.pathbased.bonsai.BonsaiWorldStateProvider; +import org.hyperledger.besu.ethereum.trie.patricia.StoredMerklePatriciaTrie; +import org.hyperledger.besu.evm.account.MutableAccount; +import org.hyperledger.besu.evm.worldstate.WorldUpdater; + +import java.util.Optional; +import java.util.function.Function; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt256; +import org.junit.jupiter.api.Test; + +/** + * Tests for {@link FrontierRootHashTracker} verifying correctness of incremental frontier root hash + * computation through the public BonsaiWorldState API. + */ +class FrontierRootHashTrackerTest { + + private static final Address ACCOUNT_A = + Address.fromHexString("0x1000000000000000000000000000000000000001"); + private static final Address ACCOUNT_B = + Address.fromHexString("0x1000000000000000000000000000000000000002"); + + @Test + void incrementalResultMatchesFullRecalculationForSingleTransaction() { + final BonsaiWorldState worldState = createWorldStateWithAccounts(); + + final WorldUpdater updater = worldState.updater(); + updater.getAccount(ACCOUNT_A).setBalance(Wei.of(100)); + updater.commit(); + updater.markTransactionBoundary(); + + assertThat(worldState.frontierRootHash()).isEqualTo(fullRecalculatedRoot(worldState)); + } + + @Test + void incrementalResultMatchesFullRecalculationAcrossMultipleTransactions() { + final BonsaiWorldState worldState = createWorldStateWithAccounts(); + + for (int i = 0; i < 10; i++) { + final WorldUpdater updater = worldState.updater(); + updater.getAccount(ACCOUNT_A).setBalance(Wei.of(100 + i)); + updater.getAccount(ACCOUNT_B).setStorageValue(UInt256.valueOf(i), UInt256.valueOf(i)); + updater.commit(); + updater.markTransactionBoundary(); + + assertThat(worldState.frontierRootHash()) + .as("Mismatch at tx %d", i) + .isEqualTo(fullRecalculatedRoot(worldState)); + } + } + + @Test + void resetClearsTrieCacheAndNextCallRebuildsFromScratch() { + final BonsaiWorldState worldState = createWorldStateWithAccounts(); + + final WorldUpdater updater = worldState.updater(); + updater.getAccount(ACCOUNT_A).setBalance(Wei.of(42)); + updater.commit(); + updater.markTransactionBoundary(); + + final Hash beforeReset = worldState.frontierRootHash(); + + // Persist resets the tracker; new block starts fresh + worldState.persist(null); + + final WorldUpdater updater2 = worldState.updater(); + updater2.getAccount(ACCOUNT_B).setBalance(Wei.of(99)); + updater2.commit(); + updater2.markTransactionBoundary(); + + final Hash afterReset = worldState.frontierRootHash(); + assertThat(afterReset).isEqualTo(fullRecalculatedRoot(worldState)); + assertThat(afterReset).isNotEqualTo(beforeReset); + } + + @Test + void returnsBaseRootHashWhenNothingIsDirty() { + final BonsaiWorldState worldState = createWorldStateWithAccounts(); + assertThat(worldState.frontierRootHash()).isEqualTo(worldState.rootHash()); + } + + private static BonsaiWorldState createWorldStateWithAccounts() { + final Blockchain blockchain = mock(Blockchain.class); + final BonsaiWorldStateProvider archive = + InMemoryKeyValueStorageProvider.createBonsaiInMemoryWorldStateArchive(blockchain); + final BonsaiWorldState worldState = (BonsaiWorldState) archive.getWorldState(); + + final WorldUpdater setup = worldState.updater(); + final MutableAccount a = setup.createAccount(ACCOUNT_A); + a.setBalance(Wei.of(1)); + a.setStorageValue(UInt256.ONE, UInt256.ONE); + final MutableAccount b = setup.createAccount(ACCOUNT_B); + b.setBalance(Wei.of(2)); + b.setStorageValue(UInt256.ONE, UInt256.valueOf(2)); + setup.commit(); + setup.markTransactionBoundary(); + worldState.persist(null); + + return worldState; + } + + @Test + @SuppressWarnings("unchecked") + void recoversAfterMerkleTrieExceptionByRebuildingFromScratch() { + final BonsaiWorldState worldState = createWorldStateWithAccounts(); + final BonsaiWorldStateUpdateAccumulator acc = + (BonsaiWorldStateUpdateAccumulator) worldState.getAccumulator(); + + final WorldUpdater updater = worldState.updater(); + updater.getAccount(ACCOUNT_A).setBalance(Wei.of(100)); + updater.commit(); + updater.markTransactionBoundary(); + + final Hash expectedRoot = fullRecalculatedRoot(worldState); + + final MerkleTrie brokenTrie = mock(MerkleTrie.class); + doThrow(new MerkleTrieException("simulated node missing")) + .when(brokenTrie) + .put(any(Bytes.class), any(Bytes.class)); + + final FrontierRootHashTracker.AccountTrieFactory factory = + mock(FrontierRootHashTracker.AccountTrieFactory.class); + when(factory.create(any())) + .thenReturn(brokenTrie) + .thenAnswer( + inv -> + new StoredMerklePatriciaTrie<>( + (location, hash) -> + worldState.getWorldStateStorage().getAccountStateTrieNode(location, hash), + inv.getArgument(0), + Function.identity(), + Function.identity())); + + final FrontierRootHashTracker tracker = + new FrontierRootHashTracker(acc, factory, (address, storageUpdates) -> {}); + + assertThatThrownBy(() -> tracker.frontierRootHash(worldState.rootHash())) + .isInstanceOf(MerkleTrieException.class); + + final Hash result = tracker.frontierRootHash(worldState.rootHash()); + assertThat(result).isEqualTo(expectedRoot); + } + + private static Hash fullRecalculatedRoot(final BonsaiWorldState worldState) { + final BonsaiWorldStateUpdateAccumulator accumulatorCopy = + (BonsaiWorldStateUpdateAccumulator) worldState.getAccumulator().copy(); + return worldState.calculateRootHash(Optional.empty(), accumulatorCopy); + } +}