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);
+ }
+}