diff --git a/CHANGELOG.md b/CHANGELOG.md index 24e1f4a7dc2..b12eb009b30 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,7 @@ - Updated jackson-databind library to version 2.13.4.2 addressing [CVE-2022-42003](https://nvd.nist.gov/vuln/detail/CVE-2022-42003) - Gradle task allows custom docker image configs e.g. `./gradlew distDocker -PdockerImageName=my/besu -PdockerVariants=openjdk-17,openjdk-19` - Update snapsync feature to avoid restarting the download of the world state from scratch when restarting Besu [#4381](https://github.com/hyperledger/besu/pull/4381) +- Added worldstate snapshot isolation to improve the stability of bonsai (`--Xbonsai-use-snapshots=true`) [#4351](https://github.com/hyperledger/besu/pull/4531) ### Bug Fixes - Fixed default fromBlock value and improved parameter interpretation in eth_getLogs RPC handler [#4513](https://github.com/hyperledger/besu/pull/4513) diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/DataStorageOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/DataStorageOptions.java index 38f68190e1e..35c88ac92a5 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/DataStorageOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/DataStorageOptions.java @@ -17,6 +17,7 @@ package org.hyperledger.besu.cli.options.stable; import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD; +import static org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration.DEFAULT_BONSAI_USE_SNAPSHOTS; import org.hyperledger.besu.cli.options.CLIOptions; import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; @@ -34,6 +35,8 @@ public class DataStorageOptions implements CLIOptions private static final String BONSAI_STORAGE_FORMAT_MAX_LAYERS_TO_LOAD = "--bonsai-maximum-back-layers-to-load"; + private static final String BONSAI_STORAGE_FORMAT_USE_SNAPSHOTS = "--Xbonsai-use-snapshots"; + // Use Bonsai DB @Option( names = {DATA_STORAGE_FORMAT}, @@ -50,6 +53,15 @@ public class DataStorageOptions implements CLIOptions arity = "1") private final Long bonsaiMaxLayersToLoad = DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD; + @Option( + names = {BONSAI_STORAGE_FORMAT_USE_SNAPSHOTS}, + paramLabel = "", + hidden = true, + description = + "Use database snapshots for mutable worldstates with BONSAI (default: ${DEFAULT-VALUE}).", + arity = "1") + private final Boolean bonsaiUseSnapshots = DEFAULT_BONSAI_USE_SNAPSHOTS; + public static DataStorageOptions create() { return new DataStorageOptions(); } @@ -59,6 +71,7 @@ public DataStorageConfiguration toDomainObject() { return ImmutableDataStorageConfiguration.builder() .dataStorageFormat(dataStorageFormat) .bonsaiMaxLayersToLoad(bonsaiMaxLayersToLoad) + .useBonsaiSnapshots(bonsaiUseSnapshots) .build(); } @@ -68,6 +81,8 @@ public List getCLIOptions() { DATA_STORAGE_FORMAT, dataStorageFormat.toString(), BONSAI_STORAGE_FORMAT_MAX_LAYERS_TO_LOAD, - bonsaiMaxLayersToLoad.toString()); + bonsaiMaxLayersToLoad.toString(), + BONSAI_STORAGE_FORMAT_USE_SNAPSHOTS, + bonsaiUseSnapshots.toString()); } } 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 7befb787f24..c8017637d25 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java @@ -32,7 +32,6 @@ 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.TrieLogManager; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.chain.BlockchainStorage; import org.hyperledger.besu.ethereum.chain.DefaultBlockchain; @@ -632,12 +631,11 @@ private WorldStateArchive createWorldStateArchive( switch (dataStorageConfiguration.getDataStorageFormat()) { case BONSAI: return new BonsaiWorldStateArchive( - new TrieLogManager( - blockchain, - (BonsaiWorldStateKeyValueStorage) worldStateStorage, - dataStorageConfiguration.getBonsaiMaxLayersToLoad()), - storageProvider, - blockchain); + (BonsaiWorldStateKeyValueStorage) worldStateStorage, + blockchain, + Optional.of(dataStorageConfiguration.getBonsaiMaxLayersToLoad()), + dataStorageConfiguration.useBonsaiSnapshots()); + case FOREST: default: final WorldStatePreimageStorage preimageStorage = diff --git a/besu/src/test/java/org/hyperledger/besu/controller/BesuControllerBuilderTest.java b/besu/src/test/java/org/hyperledger/besu/controller/BesuControllerBuilderTest.java index 331256df210..69c8814bfee 100644 --- a/besu/src/test/java/org/hyperledger/besu/controller/BesuControllerBuilderTest.java +++ b/besu/src/test/java/org/hyperledger/besu/controller/BesuControllerBuilderTest.java @@ -131,10 +131,12 @@ public void setup() { when(worldStatePreimageStorage.updater()) .thenReturn(mock(WorldStatePreimageStorage.Updater.class)); when(worldStateStorage.updater()).thenReturn(mock(WorldStateStorage.Updater.class)); - BonsaiWorldStateKeyValueStorage.Updater bonsaiUpdater = - mock(BonsaiWorldStateKeyValueStorage.Updater.class); + BonsaiWorldStateKeyValueStorage.BonsaiUpdater bonsaiUpdater = + mock(BonsaiWorldStateKeyValueStorage.BonsaiUpdater.class); when(bonsaiUpdater.getTrieLogStorageTransaction()) .thenReturn(mock(KeyValueStorageTransaction.class)); + when(bonsaiUpdater.getTrieBranchStorageTransaction()) + .thenReturn(mock(KeyValueStorageTransaction.class)); when(bonsaiWorldStateStorage.updater()).thenReturn(bonsaiUpdater); besuControllerBuilder = visitWithMockConfigs(new MainnetBesuControllerBuilder()); } @@ -167,6 +169,7 @@ public void shouldDisablePruningIfBonsaiIsEnabled() { ImmutableDataStorageConfiguration.builder() .dataStorageFormat(DataStorageFormat.BONSAI) .bonsaiMaxLayersToLoad(DataStorageConfiguration.DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD) + .useBonsaiSnapshots(DataStorageConfiguration.DEFAULT_BONSAI_USE_SNAPSHOTS) .build()); besuControllerBuilder.build(); @@ -183,6 +186,7 @@ public void shouldUsePruningIfForestIsEnabled() { ImmutableDataStorageConfiguration.builder() .dataStorageFormat(DataStorageFormat.FOREST) .bonsaiMaxLayersToLoad(DataStorageConfiguration.DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD) + .useBonsaiSnapshots(DataStorageConfiguration.DEFAULT_BONSAI_USE_SNAPSHOTS) .build()); besuControllerBuilder.build(); diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml index 629e11bf119..6128c5f8e6b 100644 --- a/besu/src/test/resources/everything_config.toml +++ b/besu/src/test/resources/everything_config.toml @@ -192,6 +192,7 @@ ethstats-contact="contact@mail.n" # Data storage data-storage-format="BONSAI" bonsai-maximum-back-layers-to-load=512 +Xbonsai-use-snapshots=true # feature flags Xsecp256k1-native-enabled=false diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLDataFetchers.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLDataFetchers.java index 96c03910983..ad96b788ebf 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLDataFetchers.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/GraphQLDataFetchers.java @@ -46,7 +46,6 @@ import org.hyperledger.besu.ethereum.transaction.TransactionInvalidReason; import org.hyperledger.besu.evm.account.Account; import org.hyperledger.besu.evm.log.LogTopic; -import org.hyperledger.besu.evm.worldstate.WorldState; import org.hyperledger.besu.plugin.data.SyncStatus; import java.math.BigInteger; @@ -219,31 +218,37 @@ DataFetcher> getAccountDataFetcher() { final Address addr = dataFetchingEnvironment.getArgument("address"); final Long bn = dataFetchingEnvironment.getArgument("blockNumber"); if (bn != null) { - final Optional ws = blockchainQuery.getWorldState(bn); - if (ws.isPresent()) { - final Account account = ws.get().get(addr); - if (account == null) { - return Optional.of(new EmptyAccountAdapter(addr)); - } - return Optional.of(new AccountAdapter(account)); - } else if (bn > blockchainQuery.getBlockchain().getChainHeadBlockNumber()) { - // block is past chainhead - throw new GraphQLException(GraphQLError.INVALID_PARAMS); - } else { - // we don't have that block - throw new GraphQLException(GraphQLError.CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE); - } + return blockchainQuery + .mapWorldState( + bn, + ws -> { + final Account account = ws.get(addr); + if (account == null) { + return new EmptyAccountAdapter(addr); + } + return new AccountAdapter(account); + }) + .or( + () -> { + if (bn > blockchainQuery.getBlockchain().getChainHeadBlockNumber()) { + // block is past chainhead + throw new GraphQLException(GraphQLError.INVALID_PARAMS); + } else { + // we don't have that block + throw new GraphQLException(GraphQLError.CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE); + } + }); } else { // return account on latest block final long latestBn = blockchainQuery.latestBlock().get().getHeader().getNumber(); - final Optional ows = blockchainQuery.getWorldState(latestBn); - return ows.flatMap( + return blockchainQuery.mapWorldState( + latestBn, ws -> { final Account account = ws.get(addr); if (account == null) { - return Optional.of(new EmptyAccountAdapter(addr)); + return new EmptyAccountAdapter(addr); } - return Optional.of(new AccountAdapter(account)); + return new AccountAdapter(account); }); } }; diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/BlockAdapterBase.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/BlockAdapterBase.java index 002e446b5f5..50a91478658 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/BlockAdapterBase.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/BlockAdapterBase.java @@ -96,8 +96,7 @@ public Optional getMiner(final DataFetchingEnvironment environment) } return query - .getWorldState(blockNumber) - .map(ws -> ws.get(header.getCoinbase())) + .mapWorldState(blockNumber, ws -> ws.get(header.getCoinbase())) .map(account -> (AdapterBase) new AccountAdapter(account)) .or(() -> Optional.of(new EmptyAccountAdapter(header.getCoinbase()))); } @@ -147,13 +146,12 @@ public Optional getAccount(final DataFetchingEnvironment environ final BlockchainQueries query = getBlockchainQueries(environment); final long bn = header.getNumber(); - return query - .getWorldState(bn) - .map( - ws -> { - final Address address = environment.getArgument("address"); - return new AccountAdapter(ws.get(address)); - }); + return query.mapWorldState( + bn, + ws -> { + final Address address = environment.getArgument("address"); + return new AccountAdapter(ws.get(address)); + }); } public List getLogs(final DataFetchingEnvironment environment) { diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/LogAdapter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/LogAdapter.java index a638044ea49..dcc8c175994 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/LogAdapter.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/LogAdapter.java @@ -63,8 +63,7 @@ public Optional getAccount(final DataFetchingEnvironment environ blockNumber = bn; } - return query - .getWorldState(blockNumber) - .map(ws -> new AccountAdapter(ws.get(logWithMetadata.getLogger()))); + return query.mapWorldState( + blockNumber, ws -> new AccountAdapter(ws.get(logWithMetadata.getLogger()))); } } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/PendingStateAdapter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/PendingStateAdapter.java index 96f67fa62f3..04bedb0dbfb 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/PendingStateAdapter.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/PendingStateAdapter.java @@ -24,7 +24,6 @@ import org.hyperledger.besu.ethereum.transaction.CallParameter; import org.hyperledger.besu.ethereum.transaction.TransactionSimulator; import org.hyperledger.besu.ethereum.transaction.TransactionSimulatorResult; -import org.hyperledger.besu.evm.worldstate.WorldState; import java.util.List; import java.util.Map; @@ -65,10 +64,8 @@ public Optional getAccount( final Address addr = dataFetchingEnvironment.getArgument("address"); final Long blockNumber = dataFetchingEnvironment.getArgument("blockNumber"); final long latestBlockNumber = blockchainQuery.latestBlock().get().getHeader().getNumber(); - final Optional optionalWorldState = - blockchainQuery.getWorldState(latestBlockNumber); - return optionalWorldState - .flatMap(worldState -> Optional.ofNullable(worldState.get(addr))) + return blockchainQuery + .mapWorldState(latestBlockNumber, ws -> ws.get(addr)) .map(AccountAdapter::new); } diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java index e0f7b30756d..d7badd8d745 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/graphql/internal/pojoadapter/TransactionAdapter.java @@ -25,7 +25,6 @@ import org.hyperledger.besu.ethereum.core.LogWithMetadata; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; -import org.hyperledger.besu.evm.worldstate.WorldState; import java.util.ArrayList; import java.util.List; @@ -83,12 +82,11 @@ public Optional getFrom(final DataFetchingEnvironment environmen if (blockNumber == null) { blockNumber = transactionWithMetadata.getBlockNumber().orElseGet(query::headBlockNumber); } - return query - .getWorldState(blockNumber) - .map( - mutableWorldState -> - new AccountAdapter( - mutableWorldState.get(transactionWithMetadata.getTransaction().getSender()))); + return query.mapWorldState( + blockNumber, + mutableWorldState -> + new AccountAdapter( + mutableWorldState.get(transactionWithMetadata.getTransaction().getSender()))); } public Optional getTo(final DataFetchingEnvironment environment) { @@ -98,15 +96,15 @@ public Optional getTo(final DataFetchingEnvironment environment) blockNumber = transactionWithMetadata.getBlockNumber().orElseGet(query::headBlockNumber); } - return query - .getWorldState(blockNumber) - .flatMap( - ws -> { - return transactionWithMetadata - .getTransaction() - .getTo() - .map(address -> new AccountAdapter(address, ws.get(address))); - }); + return query.mapWorldState( + blockNumber, + ws -> + transactionWithMetadata + .getTransaction() + .getTo() + .map(address -> new AccountAdapter(address, ws.get(address))) + // safe because mapWorldState returns Optional.ofNullable + .orElse(null)); } public Optional getValue() { @@ -176,11 +174,7 @@ public Optional getCreatedContract(final DataFetchingEnvironment return Optional.empty(); } final long blockNumber = bn.orElseGet(txBlockNumber::get); - - final Optional ws = query.getWorldState(blockNumber); - if (ws.isPresent()) { - return Optional.of(new AccountAdapter(ws.get().get(addr.get()))); - } + return query.mapWorldState(blockNumber, ws -> new AccountAdapter(ws.get(addr.get()))); } } return Optional.empty(); diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountRange.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountRange.java index 28208459cee..00ef66dd8a5 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountRange.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugAccountRange.java @@ -24,7 +24,6 @@ import org.hyperledger.besu.ethereum.api.query.BlockWithMetadata; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.evm.worldstate.WorldState; import org.hyperledger.besu.evm.worldstate.WorldState.StreamableAccount; import java.util.Collections; @@ -74,34 +73,32 @@ public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { } // TODO deal with mid-block locations - - final Optional state = - blockchainQueries.get().getWorldState(blockHeaderOptional.get().getNumber()); - - if (state.isEmpty()) { - return emptyResponse(requestContext); - } else { - final List accounts = - state - .get() - .streamAccounts(Bytes32.fromHexStringLenient(addressHash), maxResults + 1) - .collect(Collectors.toList()); - Bytes32 nextKey = Bytes32.ZERO; - if (accounts.size() == maxResults + 1) { - nextKey = accounts.get(maxResults).getAddressHash(); - accounts.remove(maxResults); - } - - return new JsonRpcSuccessResponse( - requestContext.getRequest().getId(), - new DebugAccountRangeAtResult( - accounts.stream() - .collect( - Collectors.toMap( - account -> account.getAddressHash().toString(), - account -> account.getAddress().orElse(Address.ZERO).toString())), - nextKey.toString())); - } + return blockchainQueries + .get() + .mapWorldState( + blockHeaderOptional.get().getNumber(), + state -> { + final List accounts = + state + .streamAccounts(Bytes32.fromHexStringLenient(addressHash), maxResults + 1) + .collect(Collectors.toList()); + Bytes32 nextKey = Bytes32.ZERO; + if (accounts.size() == maxResults + 1) { + nextKey = accounts.get(maxResults).getAddressHash(); + accounts.remove(maxResults); + } + + return new JsonRpcSuccessResponse( + requestContext.getRequest().getId(), + new DebugAccountRangeAtResult( + accounts.stream() + .collect( + Collectors.toMap( + account -> account.getAddressHash().toString(), + account -> account.getAddress().orElse(Address.ZERO).toString())), + nextKey.toString())); + }) + .orElse(emptyResponse(requestContext)); } private Optional hashFromParameter(final BlockParameterOrBlockHash blockParameter) { diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugStorageRangeAt.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugStorageRangeAt.java index 76e62ae78bc..8a954fb9968 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugStorageRangeAt.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/DebugStorageRangeAt.java @@ -104,8 +104,8 @@ public JsonRpcResponse response(final JsonRpcRequestContext requestContext) { () -> blockchainQueries .get() - .getWorldState(blockHeaderOptional.get().getNumber()) - .map( + .mapWorldState( + blockHeaderOptional.get().getNumber(), worldState -> extractStorageAt( requestContext, accountAddress, startKey, limit, worldState)) diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProof.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProof.java index d7e037c6620..2a479ac5bf4 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProof.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProof.java @@ -26,7 +26,6 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.proof.GetProofResult; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; import org.hyperledger.besu.ethereum.proof.WorldStateProof; -import org.hyperledger.besu.evm.worldstate.WorldState; import java.util.Arrays; import java.util.List; @@ -58,27 +57,28 @@ protected Object resultByBlockHash( final Address address = requestContext.getRequiredParameter(0, Address.class); final List storageKeys = getStorageKeys(requestContext); - final Optional worldState = getBlockchainQueries().getWorldState(blockHash); - - if (worldState.isPresent()) { - Optional proofOptional = - getBlockchainQueries() - .getWorldStateArchive() - .getAccountProof(worldState.get().rootHash(), address, storageKeys); - return proofOptional - .map( - proof -> - (JsonRpcResponse) - new JsonRpcSuccessResponse( - requestContext.getRequest().getId(), - GetProofResult.buildGetProofResult(address, proof))) - .orElse( - new JsonRpcErrorResponse( - requestContext.getRequest().getId(), JsonRpcError.NO_ACCOUNT_FOUND)); - } - - return new JsonRpcErrorResponse( - requestContext.getRequest().getId(), JsonRpcError.WORLD_STATE_UNAVAILABLE); + return getBlockchainQueries() + .mapWorldState( + blockHash, + worldState -> { + Optional proofOptional = + getBlockchainQueries() + .getWorldStateArchive() + .getAccountProof(worldState.rootHash(), address, storageKeys); + return proofOptional + .map( + proof -> + (JsonRpcResponse) + new JsonRpcSuccessResponse( + requestContext.getRequest().getId(), + GetProofResult.buildGetProofResult(address, proof))) + .orElse( + new JsonRpcErrorResponse( + requestContext.getRequest().getId(), JsonRpcError.NO_ACCOUNT_FOUND)); + }) + .orElse( + new JsonRpcErrorResponse( + requestContext.getRequest().getId(), JsonRpcError.WORLD_STATE_UNAVAILABLE)); } @Override diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceCallMany.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceCallMany.java index 4b9e47b17c5..eecb648c5e7 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceCallMany.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/TraceCallMany.java @@ -109,33 +109,43 @@ protected Object resultByBlockNumber( final BlockHeader blockHeader = maybeBlockHeader.get(); final List traceCallResults = new ArrayList<>(); - final WorldUpdater updater = transactionSimulator.getWorldUpdater(blockHeader); - try { - Arrays.stream(transactionsAndTraceTypeParameters) - .forEachOrdered( - param -> { - final WorldUpdater localUpdater = updater.updater(); - traceCallResults.add( - getSingleCallResult( - param.getTuple().getJsonCallParameter(), - param.getTuple().getTraceTypeParameter(), - blockHeader, - localUpdater)); - localUpdater.commit(); - }); - } catch (final TransactionInvalidException e) { - LOG.error("Invalid transaction simulator result"); - return new JsonRpcErrorResponse(requestContext.getRequest().getId(), INTERNAL_ERROR); - } catch (final EmptySimulatorResultException e) { - LOG.error( - "Empty simulator result, call params: {}, blockHeader: {} ", - JsonCallParameterUtil.validateAndGetCallParams(requestContext), - blockHeader); - return new JsonRpcErrorResponse(requestContext.getRequest().getId(), INTERNAL_ERROR); - } catch (final Exception e) { - return new JsonRpcErrorResponse(requestContext.getRequest().getId(), INTERNAL_ERROR); - } - return traceCallResults; + + return getBlockchainQueries() + .mapWorldState( + blockHeader.getBlockHash(), + ws -> { + final WorldUpdater updater = + transactionSimulator.getEffectiveWorldStateUpdater(blockHeader, ws); + try { + Arrays.stream(transactionsAndTraceTypeParameters) + .forEachOrdered( + param -> { + final WorldUpdater localUpdater = updater.updater(); + traceCallResults.add( + getSingleCallResult( + param.getTuple().getJsonCallParameter(), + param.getTuple().getTraceTypeParameter(), + blockHeader, + localUpdater)); + localUpdater.commit(); + }); + } catch (final TransactionInvalidException e) { + LOG.error("Invalid transaction simulator result"); + return new JsonRpcErrorResponse( + requestContext.getRequest().getId(), INTERNAL_ERROR); + } catch (final EmptySimulatorResultException e) { + LOG.error( + "Empty simulator result, call params: {}, blockHeader: {} ", + JsonCallParameterUtil.validateAndGetCallParams(requestContext), + blockHeader); + return new JsonRpcErrorResponse( + requestContext.getRequest().getId(), INTERNAL_ERROR); + } catch (final Exception e) { + return new JsonRpcErrorResponse( + requestContext.getRequest().getId(), INTERNAL_ERROR); + } + return traceCallResults; + }); } private JsonNode getSingleCallResult( diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/processor/BlockReplay.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/processor/BlockReplay.java index aad1158fd3b..6f47b451a5e 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/processor/BlockReplay.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/processor/BlockReplay.java @@ -145,14 +145,18 @@ private Optional performActionWithBlock( if (previous == null) { return Optional.empty(); } - final MutableWorldState mutableWorldState = + try (final MutableWorldState mutableWorldState = worldStateArchive .getMutable(previous.getStateRoot(), previous.getHash(), false) - .orElse(null); - if (mutableWorldState == null) { + .orElseThrow( + () -> + new IllegalArgumentException( + "Missing worldstate for stateroot " + + previous.getStateRoot().toShortHexString()))) { + return action.perform(body, header, blockchain, mutableWorldState, transactionProcessor); + } catch (Exception ex) { return Optional.empty(); } - return action.perform(body, header, blockchain, mutableWorldState, transactionProcessor); } private Optional getBlock(final Hash blockHash) { diff --git a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/query/BlockchainQueries.java b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/query/BlockchainQueries.java index 01100f18458..ae096150cd1 100644 --- a/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/query/BlockchainQueries.java +++ b/ethereum/api/src/main/java/org/hyperledger/besu/ethereum/api/query/BlockchainQueries.java @@ -29,13 +29,13 @@ import org.hyperledger.besu.ethereum.core.BlockBody; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.LogWithMetadata; +import org.hyperledger.besu.ethereum.core.MutableWorldState; import org.hyperledger.besu.ethereum.core.Transaction; import org.hyperledger.besu.ethereum.core.TransactionReceipt; import org.hyperledger.besu.ethereum.eth.manager.EthScheduler; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; import org.hyperledger.besu.evm.account.Account; import org.hyperledger.besu.evm.log.LogsBloomFilter; -import org.hyperledger.besu.evm.worldstate.WorldState; import java.io.EOFException; import java.io.IOException; @@ -294,8 +294,7 @@ public long getTransactionCount(final Address address, final long blockNumber) { * @return The number of transactions sent from the given address. */ public long getTransactionCount(final Address address, final Hash blockHash) { - return getWorldState(blockHash) - .map(worldState -> worldState.get(address)) + return mapWorldState(blockHash, worldState -> worldState.get(address)) .map(Account::getNonce) .orElse(0L); } @@ -835,29 +834,46 @@ public List matchingLogs( } /** - * Returns the world state for the corresponding block number + * Wraps an operation on MutableWorldState with try-with-resources the corresponding block hash * - * @param blockNumber the block number + * @param return type of the operation on the MutableWorldState + * @param blockHash the block hash + * @param mapper Function which performs an operation on a MutableWorldState * @return the world state at the block number */ - public Optional getWorldState(final long blockNumber) { - final Hash blockHash = - getBlockHeaderByNumber(blockNumber).map(BlockHeader::getHash).orElse(Hash.EMPTY); - - return getWorldState(blockHash); + public Optional mapWorldState( + final Hash blockHash, final Function mapper) { + return blockchain + .getBlockHeader(blockHash) + .flatMap( + blockHeader -> { + try (var ws = + worldStateArchive + .getMutable(blockHeader.getStateRoot(), blockHeader.getHash(), false) + .orElse(null)) { + if (ws != null) { + return Optional.ofNullable(mapper.apply(ws)); + } + } catch (Exception ex) { + LOG.error("failed worldstate query for " + blockHash.toShortHexString(), ex); + } + return Optional.empty(); + }); } /** - * Returns the world state for the corresponding block hash + * Wraps an operation on MutableWorldState with try-with-resources the corresponding block number * - * @param blockHash the block hash - * @return the world state at the block hash + * @param return type of the operation on the MutableWorldState + * @param blockNumber the block number + * @param mapper Function which performs an operation on a MutableWorldState returning type U + * @return the world state at the block number */ - public Optional getWorldState(final Hash blockHash) { - final Optional header = blockchain.getBlockHeader(blockHash); - return header.flatMap( - blockHeader -> - worldStateArchive.getMutable(blockHeader.getStateRoot(), blockHeader.getHash(), false)); + public Optional mapWorldState( + final long blockNumber, final Function mapper) { + final Hash blockHash = + getBlockHeaderByNumber(blockNumber).map(BlockHeader::getHash).orElse(Hash.EMPTY); + return mapWorldState(blockHash, mapper); } public Optional gasPrice() { @@ -917,17 +933,12 @@ public Optional gasPriorityFee() { (int) ((gasCollection.length) * apiConfig.getGasPriceFraction()))])); } - private Optional fromWorldState( - final Hash blockHash, final Function getter) { - return getWorldState(blockHash).map(getter); - } - private Optional fromAccount( final Address address, final Hash blockHash, final Function getter, final T noAccountValue) { - return fromWorldState( + return mapWorldState( blockHash, worldState -> Optional.ofNullable(worldState.get(address)).map(getter).orElse(noAccountValue)); diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/BlockDataFetcherTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/BlockDataFetcherTest.java index da5693cdd06..fbce733017d 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/BlockDataFetcherTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/graphql/BlockDataFetcherTest.java @@ -16,7 +16,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Address; @@ -73,7 +72,6 @@ public void ibftMiner() throws Exception { when(query.blockByNumber(ArgumentMatchers.anyLong())) .thenReturn(Optional.of(new BlockWithMetadata<>(header, null, null, null, 0))); when(header.getCoinbase()).thenReturn(testAddress); - when(query.getWorldState(anyLong())).thenReturn(Optional.of(mutableWorldState)); final Optional maybeBlock = fetcher.get(environment); assertThat(maybeBlock).isPresent(); diff --git a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProofTest.java b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProofTest.java index b8ab222f875..a9e0bddf2ab 100644 --- a/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProofTest.java +++ b/ethereum/api/src/test/java/org/hyperledger/besu/ethereum/api/jsonrpc/internal/methods/EthGetProofTest.java @@ -19,6 +19,7 @@ import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -30,6 +31,7 @@ import org.hyperledger.besu.ethereum.api.jsonrpc.internal.exception.InvalidJsonRpcParameters; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcError; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcErrorResponse; +import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.proof.GetProofResult; import org.hyperledger.besu.ethereum.api.query.BlockchainQueries; @@ -42,6 +44,7 @@ import java.util.Collections; import java.util.Optional; +import java.util.function.Function; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt256; @@ -139,7 +142,7 @@ void errorWhenAccountNotFound() { void errorWhenWorldStateUnavailable() { when(blockchainQueries.headBlockNumber()).thenReturn(14L); - when(blockchainQueries.getWorldState(any())).thenReturn(Optional.empty()); + when(blockchainQueries.mapWorldState(any(), any())).thenReturn(Optional.empty()); final JsonRpcErrorResponse expectedResponse = new JsonRpcErrorResponse(null, JsonRpcError.WORLD_STATE_UNAVAILABLE); @@ -221,7 +224,14 @@ private GetProofResult generateWorldState() { final MutableWorldState mutableWorldState = mock(MutableWorldState.class); when(mutableWorldState.rootHash()).thenReturn(rootHash); - when(blockchainQueries.getWorldState(any())).thenReturn(Optional.of(mutableWorldState)); + doAnswer( + invocation -> + Optional.of( + invocation + .>getArgument(1) + .apply(mutableWorldState))) + .when(blockchainQueries) + .mapWorldState(any(), any()); return GetProofResult.buildGetProofResult(address, worldStateProof); } diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java index 9f3c3e53a20..0c17537e706 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/AbstractBlockCreator.java @@ -148,7 +148,8 @@ protected BlockCreationResult createBlock( final Optional maybePrevRandao, final long timestamp, boolean rewardCoinbase) { - try { + + try (final MutableWorldState disposableWorldState = duplicateWorldStateAtParent()) { final ProcessableBlockHeader processableBlockHeader = createPendingBlockHeader(timestamp, maybePrevRandao); final Address miningBeneficiary = @@ -156,10 +157,6 @@ protected BlockCreationResult createBlock( throwIfStopped(); - final MutableWorldState disposableWorldState = duplicateWorldStateAtParent(); - - throwIfStopped(); - final List ommers = maybeOmmers.orElse(selectOmmers()); throwIfStopped(); @@ -212,7 +209,6 @@ protected BlockCreationResult createBlock( } catch (final StorageException ex) { throw ex; } catch (final Exception ex) { - // TODO(tmm): How are we going to know this has exploded, and thus restart it? throw new IllegalStateException( "Block creation failed unexpectedly. Will restart on next block added to chain.", ex); } @@ -252,21 +248,19 @@ private BlockTransactionSelector.TransactionSelectionResults selectTransactions( private MutableWorldState duplicateWorldStateAtParent() { final Hash parentStateRoot = parentHeader.getStateRoot(); - final MutableWorldState worldState = - protocolContext - .getWorldStateArchive() - .getMutable(parentStateRoot, parentHeader.getHash(), false) - .orElseThrow( - () -> { - LOG.info("Unable to create block because world state is not available"); - return new CancellationException( - "World state not available for block " - + parentHeader.getNumber() - + " with state root " - + parentStateRoot); - }); - - return worldState.copy(); + return protocolContext + .getWorldStateArchive() + .getMutable(parentStateRoot, parentHeader.getHash(), false) + .orElseThrow( + () -> { + LOG.info("Unable to create block because world state is not available"); + return new CancellationException( + "World state not available for block " + + parentHeader.getNumber() + + " with state root " + + parentStateRoot); + }) + .copy(); } private List selectOmmers() { diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/AbstractTrieLogManager.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/AbstractTrieLogManager.java new file mode 100644 index 00000000000..e5e220b8737 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/AbstractTrieLogManager.java @@ -0,0 +1,152 @@ +/* + * 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.hyperledger.besu.util.Slf4jLambdaHelper.debugLambda; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.bonsai.TrieLogManager.CachedLayer; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.MutableWorldState; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.util.Map; +import java.util.Optional; +import java.util.stream.Collectors; + +import org.apache.tuweni.bytes.Bytes32; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public abstract class AbstractTrieLogManager implements TrieLogManager { + private static final Logger LOG = LoggerFactory.getLogger(AbstractTrieLogManager.class); + public static final long RETAINED_LAYERS = 512; // at least 256 + typical rollbacks + + protected final Blockchain blockchain; + protected final BonsaiWorldStateKeyValueStorage worldStateStorage; + + protected final Map cachedWorldStatesByHash; + protected final long maxLayersToLoad; + + public AbstractTrieLogManager( + final Blockchain blockchain, + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final long maxLayersToLoad, + final Map cachedWorldStatesByHash) { + this.blockchain = blockchain; + this.worldStateStorage = worldStateStorage; + this.cachedWorldStatesByHash = cachedWorldStatesByHash; + this.maxLayersToLoad = maxLayersToLoad; + } + + @Override + public synchronized void saveTrieLog( + final BonsaiWorldStateArchive worldStateArchive, + final BonsaiWorldStateUpdater localUpdater, + final Hash worldStateRootHash, + final BlockHeader blockHeader) { + // do not overwrite a trielog layer that already exists in the database. + // if it's only in memory we need to save it + // for example, in case of reorg we don't replace a trielog layer + if (worldStateStorage.getTrieLog(blockHeader.getHash()).isEmpty()) { + final BonsaiWorldStateKeyValueStorage.BonsaiUpdater stateUpdater = + worldStateStorage.updater(); + boolean success = false; + try { + final TrieLogLayer trieLog = + prepareTrieLog(blockHeader, worldStateRootHash, localUpdater, worldStateArchive); + persistTrieLog(blockHeader, worldStateRootHash, trieLog, stateUpdater); + success = true; + } finally { + if (success) { + stateUpdater.commit(); + } else { + stateUpdater.rollback(); + } + } + } + } + + private TrieLogLayer prepareTrieLog( + final BlockHeader blockHeader, + final Hash currentWorldStateRootHash, + final BonsaiWorldStateUpdater localUpdater, + final BonsaiWorldStateArchive worldStateArchive) { + debugLambda(LOG, "Adding layered world state for {}", blockHeader::toLogString); + final TrieLogLayer trieLog = localUpdater.generateTrieLog(blockHeader.getBlockHash()); + trieLog.freeze(); + addCachedLayer(blockHeader, currentWorldStateRootHash, trieLog, worldStateArchive); + scrubCachedLayers(blockHeader.getNumber()); + return trieLog; + } + + synchronized void scrubCachedLayers(final long newMaxHeight) { + final long waterline = newMaxHeight - RETAINED_LAYERS; + cachedWorldStatesByHash.values().stream() + .filter(layer -> layer.getHeight() < waterline) + .collect(Collectors.toList()) + .stream() + .forEach( + layer -> { + cachedWorldStatesByHash.remove(layer.getTrieLog().getBlockHash()); + try { + layer.getMutableWorldState().close(); + } catch (Exception e) { + LOG.warn("Error closing bonsai worldstate layer", e); + } + }); + } + + private void persistTrieLog( + final BlockHeader blockHeader, + final Hash worldStateRootHash, + final TrieLogLayer trieLog, + final BonsaiWorldStateKeyValueStorage.BonsaiUpdater stateUpdater) { + debugLambda( + LOG, + "Persisting trie log for block hash {} and world state root {}", + blockHeader::toLogString, + worldStateRootHash::toHexString); + final BytesValueRLPOutput rlpLog = new BytesValueRLPOutput(); + trieLog.writeTo(rlpLog); + stateUpdater + .getTrieLogStorageTransaction() + .put(blockHeader.getHash().toArrayUnsafe(), rlpLog.encoded().toArrayUnsafe()); + } + + @Override + public Optional getBonsaiCachedWorldState(final Hash blockHash) { + if (cachedWorldStatesByHash.containsKey(blockHash)) { + return Optional.of(cachedWorldStatesByHash.get(blockHash).getMutableWorldState()); + } + return Optional.empty(); + } + + @Override + public long getMaxLayersToLoad() { + return maxLayersToLoad; + } + + @Override + public Optional getTrieLogLayer(final Hash blockHash) { + if (cachedWorldStatesByHash.containsKey(blockHash)) { + return Optional.of(cachedWorldStatesByHash.get(blockHash).getTrieLog()); + } else { + return worldStateStorage.getTrieLog(blockHash).map(TrieLogLayer::fromBytes); + } + } +} 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 aed904c32d5..7c7e70257ce 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 @@ -75,7 +75,6 @@ public BonsaiWorldStateArchive getArchive() { @Override public MutableWorldState copy() { - // TODO: consider returning a snapshot rather than a copy here. BonsaiInMemoryWorldStateKeyValueStorage bonsaiInMemoryWorldStateKeyValueStorage = new BonsaiInMemoryWorldStateKeyValueStorage( worldStateStorage.accountStorage, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldState.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldState.java index f4f2e883297..f91a922ddd8 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldState.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldState.java @@ -15,19 +15,18 @@ */ package org.hyperledger.besu.ethereum.bonsai; -import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.MutableWorldState; import org.hyperledger.besu.ethereum.core.SnapshotMutableWorldState; import org.hyperledger.besu.plugin.services.storage.SnappableKeyValueStorage; import org.hyperledger.besu.plugin.services.storage.SnappedKeyValueStorage; /** - * This class takes a snapshot of the worldstate as the basis of a mutable worldstate. It is able to - * commit/perist as a trielog layer only. This is useful for async blockchain opperations like block - * creation and/or point-in-time queries since the snapshot worldstate is fully isolated from the - * main BonsaiPersistedWorldState. + * This class extends BonsaiPersistedWorldstate directly such that it commits/perists directly to + * the transaction state. A SnapshotMutableWorldState is used to accumulate changes to a + * non-persisting mutable world state rather than writing worldstate changes directly. */ -public class BonsaiSnapshotWorldState extends BonsaiInMemoryWorldState +public class BonsaiSnapshotWorldState extends BonsaiPersistedWorldState implements SnapshotMutableWorldState { private final SnappedKeyValueStorage accountSnap; @@ -59,14 +58,11 @@ public static BonsaiSnapshotWorldState create( } @Override - public void persist(final BlockHeader blockHeader) { - super.persist(blockHeader); - // persist roothash to snapshot tx - trieBranchSnap - .getSnapshotTransaction() - .put( - BonsaiWorldStateKeyValueStorage.WORLD_ROOT_HASH_KEY, - worldStateRootHash.toArrayUnsafe()); + public Hash rootHash() { + if (updater.isDirty()) { + this.worldStateRootHash = calculateRootHash(worldStateStorage.updater(), updater); + } + return this.worldStateRootHash; } @Override diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldStateKeyValueStorage.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldStateKeyValueStorage.java index 89fc33d6746..ed6e4ccd0c4 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldStateKeyValueStorage.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldStateKeyValueStorage.java @@ -16,6 +16,8 @@ package org.hyperledger.besu.ethereum.bonsai; import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.storage.StorageProvider; +import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier; import org.hyperledger.besu.ethereum.trie.MerklePatriciaTrie; import org.hyperledger.besu.ethereum.worldstate.WorldStateStorage; import org.hyperledger.besu.plugin.services.storage.KeyValueStorage; @@ -27,6 +29,25 @@ public class BonsaiSnapshotWorldStateKeyValueStorage extends BonsaiWorldStateKeyValueStorage { + public BonsaiSnapshotWorldStateKeyValueStorage(final StorageProvider snappableStorageProvider) { + this( + snappableStorageProvider + .getSnappableStorageBySegmentIdentifier(KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE) + .takeSnapshot(), + snappableStorageProvider + .getSnappableStorageBySegmentIdentifier(KeyValueSegmentIdentifier.CODE_STORAGE) + .takeSnapshot(), + snappableStorageProvider + .getSnappableStorageBySegmentIdentifier( + KeyValueSegmentIdentifier.ACCOUNT_STORAGE_STORAGE) + .takeSnapshot(), + snappableStorageProvider + .getSnappableStorageBySegmentIdentifier(KeyValueSegmentIdentifier.TRIE_BRANCH_STORAGE) + .takeSnapshot(), + snappableStorageProvider.getStorageBySegmentIdentifier( + KeyValueSegmentIdentifier.TRIE_LOG_STORAGE)); + } + public BonsaiSnapshotWorldStateKeyValueStorage( final SnappedKeyValueStorage accountStorage, final SnappedKeyValueStorage codeStorage, @@ -47,8 +68,6 @@ public BonsaiUpdater updater() { } public static class SnapshotUpdater implements BonsaiWorldStateKeyValueStorage.BonsaiUpdater { - // private static final Logger LOG = - // LoggerFactory.getLogger(BonsaiSnapshotWorldStateKeyValueStorage.class); private final SnappedKeyValueStorage accountStorage; private final SnappedKeyValueStorage codeStorage; 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 d99a0a9b563..937fc6795b5 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 @@ -18,6 +18,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static org.hyperledger.besu.datatypes.Hash.fromPlugin; +import static org.hyperledger.besu.ethereum.bonsai.LayeredTrieLogManager.RETAINED_LAYERS; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.Hash; @@ -28,6 +29,8 @@ import org.hyperledger.besu.ethereum.core.SnapshotMutableWorldState; import org.hyperledger.besu.ethereum.proof.WorldStateProof; import org.hyperledger.besu.ethereum.storage.StorageProvider; +import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; +import org.hyperledger.besu.ethereum.worldstate.DataStorageFormat; import org.hyperledger.besu.ethereum.worldstate.PeerTrieNodeFinder; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; import org.hyperledger.besu.evm.worldstate.WorldState; @@ -36,6 +39,7 @@ import java.util.List; import java.util.Optional; +import com.google.common.annotations.VisibleForTesting; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt256; import org.slf4j.Logger; @@ -50,23 +54,56 @@ public class BonsaiWorldStateArchive implements WorldStateArchive { private final TrieLogManager trieLogManager; private final BonsaiPersistedWorldState persistedState; private final BonsaiWorldStateKeyValueStorage worldStateStorage; + private final boolean useSnapshots; public BonsaiWorldStateArchive(final StorageProvider provider, final Blockchain blockchain) { - this.blockchain = blockchain; - this.worldStateStorage = new BonsaiWorldStateKeyValueStorage(provider); - this.persistedState = new BonsaiPersistedWorldState(this, worldStateStorage); - this.trieLogManager = new TrieLogManager(blockchain, worldStateStorage); - blockchain.observeBlockAdded(this::blockAddedHandler); + this( + (BonsaiWorldStateKeyValueStorage) + provider.createWorldStateStorage(DataStorageFormat.BONSAI), + blockchain, + Optional.empty(), + provider.isWorldStateSnappable()); + } + + public BonsaiWorldStateArchive( + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final Blockchain blockchain, + final Optional maxLayersToLoad) { + // overload while snapshots are an experimental option: + this( + worldStateStorage, + blockchain, + maxLayersToLoad, + DataStorageConfiguration.DEFAULT_BONSAI_USE_SNAPSHOTS); } public BonsaiWorldStateArchive( + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final Blockchain blockchain, + final Optional maxLayersToLoad, + final boolean useSnapshots) { + this( + useSnapshots + ? new SnapshotTrieLogManager( + blockchain, worldStateStorage, maxLayersToLoad.orElse(RETAINED_LAYERS)) + : new LayeredTrieLogManager( + blockchain, worldStateStorage, maxLayersToLoad.orElse(RETAINED_LAYERS)), + worldStateStorage, + blockchain, + useSnapshots); + } + + @VisibleForTesting + BonsaiWorldStateArchive( final TrieLogManager trieLogManager, - final StorageProvider provider, - final Blockchain blockchain) { + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final Blockchain blockchain, + final boolean useSnapshots) { this.trieLogManager = trieLogManager; this.blockchain = blockchain; - this.worldStateStorage = new BonsaiWorldStateKeyValueStorage(provider); + this.worldStateStorage = worldStateStorage; this.persistedState = new BonsaiPersistedWorldState(this, worldStateStorage); + this.useSnapshots = useSnapshots; blockchain.observeBlockAdded(this::blockAddedHandler); } @@ -74,7 +111,7 @@ private void blockAddedHandler(final BlockAddedEvent event) { LOG.debug("New block add event {}", event); if (event.isNewCanonicalHead()) { final BlockHeader eventBlockHeader = event.getBlock().getHeader(); - trieLogManager.updateLayeredWorldState( + trieLogManager.updateCachedLayers( eventBlockHeader.getParentHash(), eventBlockHeader.getHash()); } } @@ -82,7 +119,7 @@ private void blockAddedHandler(final BlockAddedEvent event) { @Override public Optional get(final Hash rootHash, final Hash blockHash) { final Optional layeredWorldState = - trieLogManager.getBonsaiLayeredWorldState(blockHash); + trieLogManager.getBonsaiCachedWorldState(blockHash); if (layeredWorldState.isPresent()) { return Optional.of(layeredWorldState.get()); } else if (rootHash.equals(persistedState.blockHash())) { @@ -94,12 +131,12 @@ public Optional get(final Hash rootHash, final Hash blockHash) { @Override public boolean isWorldStateAvailable(final Hash rootHash, final Hash blockHash) { - return trieLogManager.getBonsaiLayeredWorldState(blockHash).isPresent() + return trieLogManager.getBonsaiCachedWorldState(blockHash).isPresent() || persistedState.blockHash().equals(blockHash) || worldStateStorage.isWorldStateAvailable(rootHash, blockHash); } - public Optional getMutableSnapshot(final Hash blockHash) { + public Optional getMutableSnapshot(final Hash blockHash) { return rollMutableStateToBlockHash( BonsaiSnapshotWorldState.create(this, worldStateStorage), blockHash) .map(SnapshotMutableWorldState.class::cast); @@ -107,58 +144,58 @@ public Optional getMutableSnapshot(final Hash blockHa @Override public Optional getMutable( - final long blockNumber, final boolean isPersistingState) { - final Optional blockHashByNumber = blockchain.getBlockHashByNumber(blockNumber); - if (blockHashByNumber.isPresent()) { - return getMutable(null, blockHashByNumber.get(), isPersistingState); + final Hash rootHash, final Hash blockHash, final boolean shouldPersistState) { + if (shouldPersistState) { + return getMutable(rootHash, blockHash); + } else { + return trieLogManager + .getBonsaiCachedWorldState(blockHash) + .or( + () -> + blockchain + .getBlockHeader(blockHash) + .filter( + header -> { + if (blockchain.getChainHeadHeader().getNumber() - header.getNumber() + >= trieLogManager.getMaxLayersToLoad()) { + LOG.warn( + "Exceeded the limit of back layers that can be loaded ({})", + trieLogManager.getMaxLayersToLoad()); + return false; + } + return true; + }) + .flatMap(header -> snapshotOrLayeredWorldState(blockHash, header))); } - return Optional.empty(); } - @Override - public Optional getMutable( - final Hash rootHash, final Hash blockHash, final boolean isPersistingState) { - if (!isPersistingState) { - final Optional layeredWorldState = - trieLogManager.getBonsaiLayeredWorldState(blockHash); - if (layeredWorldState.isPresent()) { - return layeredWorldState; - } else { - final BlockHeader header = blockchain.getBlockHeader(blockHash).get(); - final BlockHeader currentHeader = blockchain.getChainHeadHeader(); - if ((currentHeader.getNumber() - header.getNumber()) - >= trieLogManager.getMaxLayersToLoad()) { - LOG.warn( - "Exceeded the limit of back layers that can be loaded ({})", - trieLogManager.getMaxLayersToLoad()); - return Optional.empty(); - } - final Optional trieLogLayer = trieLogManager.getTrieLogLayer(blockHash); - if (trieLogLayer.isPresent()) { - return Optional.of( + private Optional snapshotOrLayeredWorldState( + final Hash blockHash, final BlockHeader blockHeader) { + if (useSnapshots) { + // use snapshots: + return getMutableSnapshot(blockHash); + } else { + // otherwise use layered worldstate: + final Optional trieLogLayer = trieLogManager.getTrieLogLayer(blockHash); + return trieLogLayer.map( + layer -> new BonsaiLayeredWorldState( blockchain, this, Optional.empty(), - header.getNumber(), - fromPlugin(header.getStateRoot()), - trieLogLayer.get())); - } - } - } else { - return getMutable(rootHash, blockHash); + blockHeader.getNumber(), + fromPlugin(blockHeader.getStateRoot()), + layer)); } - return Optional.empty(); } @Override public Optional getMutable(final Hash rootHash, final Hash blockHash) { - return rollMutableStateToBlockHash(persistedState, blockHash) - .map(MutableWorldState.class::cast); + return rollMutableStateToBlockHash(persistedState, blockHash); } - private Optional rollMutableStateToBlockHash( - final T mutableState, final Hash blockHash) { + private Optional rollMutableStateToBlockHash( + final BonsaiPersistedWorldState mutableState, final Hash blockHash) { if (blockHash.equals(mutableState.blockHash())) { return Optional.of(mutableState); } else { @@ -226,10 +263,11 @@ private Optional rollMutableStateToBloc } catch (final Exception e) { // if we fail we must clean up the updater bonsaiUpdater.reset(); - throw new RuntimeException(e); + LOG.debug("Archive rolling failed for block hash " + blockHash, e); + return Optional.empty(); } } catch (final RuntimeException re) { - re.printStackTrace(System.out); + LOG.debug("Archive rolling failed for block hash " + blockHash, re); return Optional.empty(); } } 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 e2e8e07e62e..cafe6c93bf9 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 @@ -47,14 +47,15 @@ public class BonsaiWorldStateUpdater extends AbstractWorldUpdater implements BonsaiWorldView { - private Map> accountsToUpdate = new HashMap<>(); - private Map> codeToUpdate = new HashMap<>(); - private Set
storageToClear = new HashSet<>(); + private final Map> accountsToUpdate = new HashMap<>(); + private final Map> codeToUpdate = new HashMap<>(); + private final Set
storageToClear = 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 Map>> storageToUpdate = new ConcurrentHashMap<>(); + private final Map>> storageToUpdate = + new ConcurrentHashMap<>(); BonsaiWorldStateUpdater(final BonsaiWorldView world) { super(world); @@ -62,15 +63,19 @@ public class BonsaiWorldStateUpdater extends AbstractWorldUpdater(accountsToUpdate); - copy.codeToUpdate = new HashMap<>(codeToUpdate); - copy.storageToClear = new HashSet<>(storageToClear); - copy.storageToUpdate = new ConcurrentHashMap<>(storageToUpdate); - copy.updatedAccounts = new HashMap<>(updatedAccounts); - copy.deletedAccounts = new HashSet<>(deletedAccounts); + copy.cloneFromUpdater(this); return copy; } + void cloneFromUpdater(final BonsaiWorldStateUpdater source) { + accountsToUpdate.putAll(source.getAccountsToUpdate()); + codeToUpdate.putAll(source.codeToUpdate); + storageToClear.addAll(source.storageToClear); + storageToUpdate.putAll(source.storageToUpdate); + updatedAccounts.putAll(source.updatedAccounts); + deletedAccounts.addAll(source.deletedAccounts); + } + @Override public Account get(final Address address) { return super.get(address); @@ -664,4 +669,13 @@ public void reset() { accountsToUpdate.clear(); super.reset(); } + + public boolean isDirty() { + return !(accountsToUpdate.isEmpty() + && updatedAccounts.isEmpty() + && deletedAccounts.isEmpty() + && storageToUpdate.isEmpty() + && storageToClear.isEmpty() + && codeToUpdate.isEmpty()); + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/LayeredTrieLogManager.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/LayeredTrieLogManager.java new file mode 100644 index 00000000000..2d69af64f13 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/LayeredTrieLogManager.java @@ -0,0 +1,117 @@ +/* + * 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.hyperledger.besu.util.Slf4jLambdaHelper.debugLambda; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes32; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class LayeredTrieLogManager + extends AbstractTrieLogManager { + private static final Logger LOG = LoggerFactory.getLogger(LayeredTrieLogManager.class); + + public LayeredTrieLogManager( + final Blockchain blockchain, + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final long maxLayersToLoad, + final Map cachedWorldStatesByHash) { + super(blockchain, worldStateStorage, maxLayersToLoad, cachedWorldStatesByHash); + } + + public LayeredTrieLogManager( + final Blockchain blockchain, + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final long maxLayersToLoad) { + this(blockchain, worldStateStorage, maxLayersToLoad, new HashMap<>()); + } + + public LayeredTrieLogManager( + final Blockchain blockchain, final BonsaiWorldStateKeyValueStorage worldStateStorage) { + this(blockchain, worldStateStorage, RETAINED_LAYERS, new HashMap<>()); + } + + @Override + public synchronized void addCachedLayer( + final BlockHeader blockHeader, + final Hash worldStateRootHash, + final TrieLogLayer trieLog, + final BonsaiWorldStateArchive worldStateArchive) { + + final BonsaiLayeredWorldState bonsaiLayeredWorldState = + new BonsaiLayeredWorldState( + blockchain, + worldStateArchive, + Optional.of((BonsaiPersistedWorldState) worldStateArchive.getMutable()), + blockHeader.getNumber(), + worldStateRootHash, + trieLog); + debugLambda( + LOG, + "adding layered world state for block {}, state root hash {}", + blockHeader::toLogString, + worldStateRootHash::toHexString); + cachedWorldStatesByHash.put( + blockHeader.getHash(), new LayeredWorldStateCache(bonsaiLayeredWorldState)); + } + + @Override + public synchronized void updateCachedLayers(final Hash blockParentHash, final Hash blockHash) { + cachedWorldStatesByHash.computeIfPresent( + blockParentHash, + (parentHash, bonsaiLayeredWorldState) -> { + if (cachedWorldStatesByHash.containsKey(blockHash)) { + bonsaiLayeredWorldState + .getMutableWorldState() + .setNextWorldView( + Optional.of(cachedWorldStatesByHash.get(blockHash).getMutableWorldState())); + } + return bonsaiLayeredWorldState; + }); + } + + public static class LayeredWorldStateCache implements CachedLayer { + + final BonsaiLayeredWorldState layeredWorldState; + + public LayeredWorldStateCache(final BonsaiLayeredWorldState layeredWorldState) { + this.layeredWorldState = layeredWorldState; + } + + @Override + public long getHeight() { + return layeredWorldState.getHeight(); + } + + @Override + public TrieLogLayer getTrieLog() { + return layeredWorldState.getTrieLog(); + } + + @Override + public BonsaiLayeredWorldState getMutableWorldState() { + return layeredWorldState; + } + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/SnapshotTrieLogManager.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/SnapshotTrieLogManager.java new file mode 100644 index 00000000000..6b70c4e3d6b --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/SnapshotTrieLogManager.java @@ -0,0 +1,115 @@ +/* + * 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.hyperledger.besu.util.Slf4jLambdaHelper.debugLambda; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.MutableWorldState; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import org.apache.tuweni.bytes.Bytes32; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class SnapshotTrieLogManager + extends AbstractTrieLogManager { + private static final Logger LOG = LoggerFactory.getLogger(SnapshotTrieLogManager.class); + + public SnapshotTrieLogManager( + final Blockchain blockchain, + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final long maxLayersToLoad) { + this(blockchain, worldStateStorage, maxLayersToLoad, new HashMap<>()); + } + + public SnapshotTrieLogManager( + final Blockchain blockchain, + final BonsaiWorldStateKeyValueStorage worldStateStorage, + final long maxLayersToLoad, + final Map cachedWorldStatesByHash) { + super(blockchain, worldStateStorage, maxLayersToLoad, cachedWorldStatesByHash); + } + + @Override + public void addCachedLayer( + final BlockHeader blockHeader, + final Hash worldStateRootHash, + final TrieLogLayer trieLog, + final BonsaiWorldStateArchive worldStateArchive) { + + debugLambda( + LOG, + "adding snapshot world state for block {}, state root hash {}", + blockHeader::toLogString, + worldStateRootHash::toHexString); + worldStateArchive + .getMutableSnapshot(worldStateRootHash) + .map(BonsaiSnapshotWorldState.class::cast) + .ifPresent( + snapshot -> + cachedWorldStatesByHash.put( + blockHeader.getHash(), + new CachedSnapshotWorldState(snapshot, trieLog, blockHeader.getNumber()))); + } + + @Override + public Optional getBonsaiCachedWorldState(final Hash blockHash) { + if (cachedWorldStatesByHash.containsKey(blockHash)) { + return Optional.of(cachedWorldStatesByHash.get(blockHash).getMutableWorldState().copy()); + } + return Optional.empty(); + } + + @Override + public void updateCachedLayers(final Hash blockParentHash, final Hash blockHash) { + // no-op. Snapshots are independent and do not need to update 'next' worldstates + } + + public static class CachedSnapshotWorldState implements CachedLayer { + + final BonsaiSnapshotWorldState snapshot; + final TrieLogLayer trieLog; + final long height; + + public CachedSnapshotWorldState( + final BonsaiSnapshotWorldState snapshot, final TrieLogLayer trieLog, final long height) { + this.snapshot = snapshot; + this.trieLog = trieLog; + this.height = height; + } + + @Override + public long getHeight() { + return height; + } + + @Override + public TrieLogLayer getTrieLog() { + return trieLog; + } + + @Override + public MutableWorldState getMutableWorldState() { + return snapshot; + } + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/TrieLogManager.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/TrieLogManager.java index e14e710ae99..4b9550ec808 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/TrieLogManager.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/bonsai/TrieLogManager.java @@ -11,173 +11,43 @@ * specific language governing permissions and limitations under the License. * * SPDX-License-Identifier: Apache-2.0 + * */ package org.hyperledger.besu.ethereum.bonsai; -import static org.hyperledger.besu.util.Slf4jLambdaHelper.debugLambda; - import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.MutableWorldState; -import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; -import java.util.HashMap; -import java.util.Map; import java.util.Optional; -import org.apache.tuweni.bytes.Bytes32; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -public class TrieLogManager { - - private static final long RETAINED_LAYERS = 512; // at least 256 + typical rollbacks - - private static final Logger LOG = LoggerFactory.getLogger(TrieLogManager.class); - - private final Blockchain blockchain; - private final BonsaiWorldStateKeyValueStorage worldStateStorage; - - private final Map layeredWorldStatesByHash; - private final long maxLayersToLoad; - - public TrieLogManager( - final Blockchain blockchain, - final BonsaiWorldStateKeyValueStorage worldStateStorage, - final long maxLayersToLoad, - final Map layeredWorldStatesByHash) { - this.blockchain = blockchain; - this.worldStateStorage = worldStateStorage; - this.layeredWorldStatesByHash = layeredWorldStatesByHash; - this.maxLayersToLoad = maxLayersToLoad; - } - - public TrieLogManager( - final Blockchain blockchain, - final BonsaiWorldStateKeyValueStorage worldStateStorage, - final long maxLayersToLoad) { - this(blockchain, worldStateStorage, maxLayersToLoad, new HashMap<>()); - } - - public TrieLogManager( - final Blockchain blockchain, final BonsaiWorldStateKeyValueStorage worldStateStorage) { - this(blockchain, worldStateStorage, RETAINED_LAYERS, new HashMap<>()); - } +public interface TrieLogManager { - public synchronized void saveTrieLog( + void saveTrieLog( final BonsaiWorldStateArchive worldStateArchive, final BonsaiWorldStateUpdater localUpdater, final Hash worldStateRootHash, - final BlockHeader blockHeader) { - // do not overwrite a trielog layer that already exists in the database. - // if it's only in memory we need to save it - // for example, like that in case of reorg we don't replace a trielog layer - if (worldStateStorage.getTrieLog(blockHeader.getHash()).isEmpty()) { - final BonsaiWorldStateKeyValueStorage.BonsaiUpdater stateUpdater = - worldStateStorage.updater(); - boolean success = false; - try { - final TrieLogLayer trieLog = - prepareTrieLog(blockHeader, worldStateRootHash, localUpdater, worldStateArchive); - persistTrieLog(blockHeader, worldStateRootHash, trieLog, stateUpdater); - success = true; - } finally { - if (success) { - stateUpdater.commit(); - } else { - stateUpdater.rollback(); - } - } - } - } + final BlockHeader blockHeader); + + Optional getBonsaiCachedWorldState(final Hash blockHash); + + long getMaxLayersToLoad(); - public synchronized void addLayeredWorldState( + void addCachedLayer( final BlockHeader blockHeader, final Hash worldStateRootHash, final TrieLogLayer trieLog, - final BonsaiWorldStateArchive worldStateArchive) { + final BonsaiWorldStateArchive worldStateArchive); - final BonsaiLayeredWorldState bonsaiLayeredWorldState = - new BonsaiLayeredWorldState( - blockchain, - worldStateArchive, - Optional.of((BonsaiPersistedWorldState) worldStateArchive.getMutable()), - blockHeader.getNumber(), - worldStateRootHash, - trieLog); - debugLambda( - LOG, - "adding layered world state for block {}, state root hash {}", - blockHeader::toLogString, - worldStateRootHash::toHexString); - layeredWorldStatesByHash.put(blockHeader.getHash(), bonsaiLayeredWorldState); - scrubLayeredCache(blockHeader.getNumber()); - } - - public synchronized void updateLayeredWorldState( - final Hash blockParentHash, final Hash blockHash) { - layeredWorldStatesByHash.computeIfPresent( - blockParentHash, - (parentHash, bonsaiLayeredWorldState) -> { - if (layeredWorldStatesByHash.containsKey(blockHash)) { - bonsaiLayeredWorldState.setNextWorldView( - Optional.of(layeredWorldStatesByHash.get(blockHash))); - } - return bonsaiLayeredWorldState; - }); - } - - public synchronized void scrubLayeredCache(final long newMaxHeight) { - final long waterline = newMaxHeight - RETAINED_LAYERS; - layeredWorldStatesByHash.entrySet().removeIf(entry -> entry.getValue().getHeight() < waterline); - } + void updateCachedLayers(final Hash blockParentHash, final Hash blockHash); - public long getMaxLayersToLoad() { - return maxLayersToLoad; - } + Optional getTrieLogLayer(final Hash blockHash); - public Optional getTrieLogLayer(final Hash blockHash) { - if (layeredWorldStatesByHash.containsKey(blockHash)) { - return Optional.of(layeredWorldStatesByHash.get(blockHash).getTrieLog()); - } else { - return worldStateStorage.getTrieLog(blockHash).map(TrieLogLayer::fromBytes); - } - } + interface CachedLayer { + long getHeight(); - public Optional getBonsaiLayeredWorldState(final Hash blockHash) { - if (layeredWorldStatesByHash.containsKey(blockHash)) { - return Optional.of(layeredWorldStatesByHash.get(blockHash)); - } - return Optional.empty(); - } + TrieLogLayer getTrieLog(); - private TrieLogLayer prepareTrieLog( - final BlockHeader blockHeader, - final Hash currentWorldStateRootHash, - final BonsaiWorldStateUpdater localUpdater, - final BonsaiWorldStateArchive worldStateArchive) { - debugLambda(LOG, "Adding layered world state for {}", blockHeader::toLogString); - final TrieLogLayer trieLog = localUpdater.generateTrieLog(blockHeader.getBlockHash()); - trieLog.freeze(); - addLayeredWorldState(blockHeader, currentWorldStateRootHash, trieLog, worldStateArchive); - return trieLog; - } - - private void persistTrieLog( - final BlockHeader blockHeader, - final Hash worldStateRootHash, - final TrieLogLayer trieLog, - final BonsaiWorldStateKeyValueStorage.BonsaiUpdater stateUpdater) { - debugLambda( - LOG, - "Persisting trie log for block hash {} and world state root {}", - blockHeader::toLogString, - worldStateRootHash::toHexString); - final BytesValueRLPOutput rlpLog = new BytesValueRLPOutput(); - trieLog.writeTo(rlpLog); - stateUpdater - .getTrieLogStorageTransaction() - .put(blockHeader.getHash().toArrayUnsafe(), rlpLog.encoded().toArrayUnsafe()); + MutableWorldState getMutableWorldState(); } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/StorageProvider.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/StorageProvider.java index d09609adb4c..ed644163e40 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/StorageProvider.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/StorageProvider.java @@ -45,4 +45,6 @@ public interface StorageProvider extends Closeable { GoQuorumPrivateStorage createGoQuorumPrivateStorage(); boolean isWorldStateIterable(); + + boolean isWorldStateSnappable(); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/GoQuorumKeyValueStorageProvider.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/GoQuorumKeyValueStorageProvider.java index 35c5d54b700..0f8012f9c7f 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/GoQuorumKeyValueStorageProvider.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/GoQuorumKeyValueStorageProvider.java @@ -40,7 +40,8 @@ public GoQuorumKeyValueStorageProvider( storageCreator, worldStatePreimageStorage, privateWorldStatePreimageStorage, - segmentIsolationSupported); + segmentIsolationSupported, + false); } @Override diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStorageProvider.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStorageProvider.java index ec30446af85..94a7e0b3251 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStorageProvider.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStorageProvider.java @@ -35,11 +35,12 @@ public class KeyValueStorageProvider implements StorageProvider { - private final Function storageCreator; + protected final Function storageCreator; private final KeyValueStorage worldStatePreimageStorage; private final KeyValueStorage privateWorldStatePreimageStorage; private final boolean isWorldStateIterable; - private final Map storageInstances = new HashMap<>(); + private final boolean isWorldStateSnappable; + protected final Map storageInstances = new HashMap<>(); public KeyValueStorageProvider( final Function storageCreator, @@ -49,17 +50,20 @@ public KeyValueStorageProvider( this.worldStatePreimageStorage = worldStatePreimageStorage; this.privateWorldStatePreimageStorage = null; this.isWorldStateIterable = segmentIsolationSupported; + this.isWorldStateSnappable = false; } public KeyValueStorageProvider( final Function storageCreator, final KeyValueStorage worldStatePreimageStorage, final KeyValueStorage privateWorldStatePreimageStorage, - final boolean segmentIsolationSupported) { + final boolean segmentIsolationSupported, + final boolean storageSnapshotIsolationSupported) { this.storageCreator = storageCreator; this.worldStatePreimageStorage = worldStatePreimageStorage; this.privateWorldStatePreimageStorage = privateWorldStatePreimageStorage; this.isWorldStateIterable = segmentIsolationSupported; + this.isWorldStateSnappable = storageSnapshotIsolationSupported; } @Override @@ -92,7 +96,7 @@ public KeyValueStorage getStorageBySegmentIdentifier(final SegmentIdentifier seg @Override public SnappableKeyValueStorage getSnappableStorageBySegmentIdentifier( final SegmentIdentifier segment) { - return (SnappableKeyValueStorage) storageInstances.computeIfAbsent(segment, storageCreator); + return (SnappableKeyValueStorage) getStorageBySegmentIdentifier(segment); } @Override @@ -117,6 +121,11 @@ public boolean isWorldStateIterable() { return isWorldStateIterable; } + @Override + public boolean isWorldStateSnappable() { + return isWorldStateSnappable; + } + @Override public void close() throws IOException { for (final KeyValueStorage kvs : storageInstances.values()) { diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStorageProviderBuilder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStorageProviderBuilder.java index 0be2831e17e..fdae34c2849 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStorageProviderBuilder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/storage/keyvalue/KeyValueStorageProviderBuilder.java @@ -80,7 +80,8 @@ public KeyValueStorageProvider build() { segment -> storageFactory.create(segment, commonConfiguration, metricsSystem), worldStatePreImageStorage, privateWorldStatePreImageStorage, - storageFactory.isSegmentIsolationSupported()); + storageFactory.isSegmentIsolationSupported(), + storageFactory.isSnapshotIsolationSupported()); } } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java index b74437f30a5..34fee5950d1 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/TransactionSimulator.java @@ -144,21 +144,31 @@ public Optional process( return Optional.empty(); } - WorldUpdater updater; - try { - updater = getWorldUpdater(header); - } catch (final IllegalArgumentException e) { - return Optional.empty(); - } + try (var ws = getWorldState(header)) { + + WorldUpdater updater = getEffectiveWorldStateUpdater(header, ws); + + // in order to trace the state diff we need to make sure that + // the world updater always has a parent + if (operationTracer instanceof DebugOperationTracer) { + updater = updater.parentUpdater().isPresent() ? updater : updater.updater(); + } + + return processWithWorldUpdater( + callParams, transactionValidationParams, operationTracer, header, updater); - // in order to trace the state diff we need to make sure that - // the world updater always has a parent - if (operationTracer instanceof DebugOperationTracer) { - updater = updater.parentUpdater().isPresent() ? updater : updater.updater(); + } catch (final Exception e) { + return Optional.empty(); } + } - return processWithWorldUpdater( - callParams, transactionValidationParams, operationTracer, header, updater); + private MutableWorldState getWorldState(final BlockHeader header) { + return worldStateArchive + .getMutable(header.getStateRoot(), header.getHash(), false) + .orElseThrow( + () -> + new IllegalArgumentException( + "Public world state not available for block " + header.getNumber())); } @Nonnull @@ -303,20 +313,8 @@ private Optional buildTransaction( return Optional.ofNullable(transaction); } - public WorldUpdater getWorldUpdater(final BlockHeader header) { - final MutableWorldState publicWorldState = - worldStateArchive.getMutable(header.getStateRoot(), header.getHash(), false).orElse(null); - - if (publicWorldState == null) { - throw new IllegalArgumentException( - "Public world state not available for block " + header.getNumber()); - } - - return getEffectiveWorldStateUpdater(header, publicWorldState); - } - // return combined private/public world state updater if GoQuorum mode, otherwise the public state - private WorldUpdater getEffectiveWorldStateUpdater( + public WorldUpdater getEffectiveWorldStateUpdater( final BlockHeader header, final MutableWorldState publicWorldState) { if (maybePrivacyParameters.isPresent() @@ -334,10 +332,14 @@ private WorldUpdater getEffectiveWorldStateUpdater( public Optional doesAddressExistAtHead(final Address address) { final BlockHeader header = blockchain.getChainHeadHeader(); - final MutableWorldState worldState = - worldStateArchive.getMutable(header.getStateRoot(), header.getHash(), false).orElse(null); - - return doesAddressExist(worldState, address, header); + try (final MutableWorldState worldState = + worldStateArchive + .getMutable(header.getStateRoot(), header.getHash(), false) + .orElseThrow()) { + return doesAddressExist(worldState, address, header); + } catch (Exception ex) { + return Optional.empty(); + } } public Optional doesAddressExist( diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DataStorageConfiguration.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DataStorageConfiguration.java index 93b22807ee9..82338257fae 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DataStorageConfiguration.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DataStorageConfiguration.java @@ -22,14 +22,18 @@ public interface DataStorageConfiguration { long DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD = 512; + boolean DEFAULT_BONSAI_USE_SNAPSHOTS = false; DataStorageConfiguration DEFAULT_CONFIG = ImmutableDataStorageConfiguration.builder() .dataStorageFormat(DataStorageFormat.FOREST) .bonsaiMaxLayersToLoad(DEFAULT_BONSAI_MAX_LAYERS_TO_LOAD) + .useBonsaiSnapshots(DEFAULT_BONSAI_USE_SNAPSHOTS) .build(); DataStorageFormat getDataStorageFormat(); Long getBonsaiMaxLayersToLoad(); + + Boolean useBonsaiSnapshots(); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DefaultWorldStateArchive.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DefaultWorldStateArchive.java index ae93b31ba3d..cf4173c638f 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DefaultWorldStateArchive.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/DefaultWorldStateArchive.java @@ -54,13 +54,6 @@ public boolean isWorldStateAvailable(final Hash rootHash, final Hash blockHash) return worldStateStorage.isWorldStateAvailable(rootHash, blockHash); } - @Override - public Optional getMutable( - final long blockNumber, final boolean isPersistingState) { - throw new UnsupportedOperationException( - "Get mutable by block number is not available with the forest mode"); - } - @Override public Optional getMutable( final Hash rootHash, final Hash blockHash, final boolean isPersistingState) { diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/WorldStateArchive.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/WorldStateArchive.java index ab2de6bbb59..677560a4e1c 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/WorldStateArchive.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/worldstate/WorldStateArchive.java @@ -35,9 +35,6 @@ public interface WorldStateArchive { boolean isWorldStateAvailable(Hash rootHash, Hash blockHash); - @Deprecated - Optional getMutable(long blockNumber, boolean isPersistingState); - Optional getMutable(Hash rootHash, Hash blockHash, boolean isPersistingState); Optional getMutable(Hash rootHash, Hash blockHash); 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 18de012ed30..7976249b7fb 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,8 +15,6 @@ package org.hyperledger.besu.ethereum.core; import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateArchive; -import org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateKeyValueStorage; -import org.hyperledger.besu.ethereum.bonsai.TrieLogManager; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.chain.DefaultBlockchain; import org.hyperledger.besu.ethereum.chain.MutableBlockchain; @@ -40,7 +38,8 @@ public InMemoryKeyValueStorageProvider() { segmentIdentifier -> new InMemoryKeyValueStorage(), new InMemoryKeyValueStorage(), new InMemoryKeyValueStorage(), - true); + true, + false); } public static MutableBlockchain createInMemoryBlockchain(final Block genesisBlock) { @@ -67,11 +66,7 @@ public static BonsaiWorldStateArchive createBonsaiInMemoryWorldStateArchive( final Blockchain blockchain) { final InMemoryKeyValueStorageProvider inMemoryKeyValueStorageProvider = new InMemoryKeyValueStorageProvider(); - return new BonsaiWorldStateArchive( - new TrieLogManager( - blockchain, new BonsaiWorldStateKeyValueStorage(inMemoryKeyValueStorageProvider)), - inMemoryKeyValueStorageProvider, - blockchain); + return new BonsaiWorldStateArchive(inMemoryKeyValueStorageProvider, blockchain); } public static MutableWorldState createInMemoryWorldState() { 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 4851701d072..a3e4b8f272b 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,7 +26,6 @@ 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.TrieLogManager; import org.hyperledger.besu.ethereum.chain.BadBlockManager; import org.hyperledger.besu.ethereum.chain.MutableBlockchain; import org.hyperledger.besu.ethereum.core.Block; @@ -79,12 +78,7 @@ public class BlockImportExceptionHandlingTest { 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( - new TrieLogManager( - blockchain, (BonsaiWorldStateKeyValueStorage) worldStateStorage, 1), - storageProvider, - blockchain)); + spy(new BonsaiWorldStateArchive(storageProvider, blockchain)); private final BonsaiPersistedWorldState persisted = spy( diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/MainnetBlockValidatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/MainnetBlockValidatorTest.java index c6aafaed466..0c066f322b0 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/MainnetBlockValidatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/MainnetBlockValidatorTest.java @@ -16,6 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.mock; @@ -112,7 +113,7 @@ public void shouldDetectAndCacheInvalidBlocksWhenParentWorldStateNotAvailable() eq(protocolContext), eq(HeaderValidationMode.DETACHED_ONLY))) .thenReturn(true); - when(worldStateArchive.getMutable(any(Hash.class), any(Hash.class))) + when(worldStateArchive.getMutable(any(Hash.class), any(Hash.class), anyBoolean())) .thenReturn(Optional.empty()); assertThat(badBlockManager.getBadBlocks().size()).isEqualTo(0); @@ -134,7 +135,7 @@ public void shouldDetectAndCacheInvalidBlocksWhenProcessBlockFailed() { eq(protocolContext), eq(HeaderValidationMode.DETACHED_ONLY))) .thenReturn(true); - when(worldStateArchive.getMutable(any(Hash.class), any(Hash.class))) + when(worldStateArchive.getMutable(any(Hash.class), any(Hash.class), anyBoolean())) .thenReturn(Optional.of(mock(MutableWorldState.class))); when(blockProcessor.processBlock(eq(blockchain), any(MutableWorldState.class), eq(badBlock))) .thenReturn(new BlockProcessingResult(Optional.empty())); @@ -157,7 +158,7 @@ public void shouldDetectAndCacheInvalidBlocksWhenBodyInvalid() { eq(protocolContext), eq(HeaderValidationMode.DETACHED_ONLY))) .thenReturn(true); - when(worldStateArchive.getMutable(any(Hash.class), any(Hash.class))) + when(worldStateArchive.getMutable(any(Hash.class), any(Hash.class), anyBoolean())) .thenReturn(Optional.of(mock(MutableWorldState.class))); when(blockProcessor.processBlock(eq(blockchain), any(MutableWorldState.class), eq(badBlock))) .thenReturn(new BlockProcessingResult(Optional.empty())); @@ -180,6 +181,8 @@ public void shouldNotCacheWhenValidBlocks() { eq(protocolContext), eq(HeaderValidationMode.DETACHED_ONLY))) .thenReturn(true); + when(worldStateArchive.getMutable(any(Hash.class), any(Hash.class), anyBoolean())) + .thenReturn(Optional.of(mock(MutableWorldState.class))); when(worldStateArchive.getMutable(any(Hash.class), any(Hash.class))) .thenReturn(Optional.of(mock(MutableWorldState.class))); when(blockProcessor.processBlock(eq(blockchain), any(MutableWorldState.class), eq(badBlock))) diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotIsolationTests.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotIsolationTests.java index 316e95625de..d074fbcfeb3 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotIsolationTests.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotIsolationTests.java @@ -63,6 +63,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Supplier; import java.util.stream.Collectors; @@ -85,9 +86,6 @@ public class BonsaiSnapshotIsolationTests { key -> SignatureAlgorithmFactory.getInstance() .createKeyPair(SECPPrivateKey.create(Bytes32.fromHexString(key), "ECDSA")); - final Function extractAddress = - ga -> Address.fromHexString(ga.getAddress()); - private final ProtocolSchedule protocolSchedule = MainnetProtocolSchedule.fromConfig(GenesisConfigFile.development().getConfigOptions()); private final GenesisState genesisState = @@ -172,7 +170,7 @@ public void testIsolatedSnapshotMutation() { assertThat(archive.getMutable().get(testAddress)).isNull(); // roll the persisted world state to the new trie log from the persisted snapshot - var ws = archive.getMutable(firstBlock.getHeader().getNumber(), true); + var ws = archive.getMutable(null, firstBlock.getHash()); assertThat(ws).isPresent(); assertThat(ws.get().get(testAddress)).isNotNull(); assertThat(ws.get().get(testAddress).getBalance()) @@ -246,8 +244,38 @@ public void testSnapshotCloneIsolation() { @Test public void assertSnapshotDoesNotClose() { - // TODO: add unit test to assert snapshot does not close on clone if parent tx is closed + Address testAddress = Address.fromHexString("0xdeadbeef"); + + // create a snapshot worldstate, and then clone it: + var isolated = archive.getMutableSnapshot(genesisState.getBlock().getHash()).get(); + + // execute a block with a single transaction on the first snapshot: + var firstBlock = forTransactions(List.of(burnTransaction(sender1, 0L, testAddress))); + var res = executeBlock(isolated, firstBlock); + + assertThat(res.isSuccessful()).isTrue(); + Consumer checkIsolatedState = + (ws) -> { + assertThat(ws.rootHash()).isEqualTo(firstBlock.getHeader().getStateRoot()); + assertThat(ws.get(testAddress)).isNotNull(); + assertThat(ws.get(testAddress).getBalance()) + .isEqualTo(Wei.of(1_000_000_000_000_000_000L)); + }; + checkIsolatedState.accept(isolated); + + var isolatedClone = isolated.copy(); + checkIsolatedState.accept(isolatedClone); + + try { + // close the first snapshot worldstate. The second worldstate should still be able to read + // through its snapshot + isolated.close(); + } catch (Exception ex) { + // meh + } + // copy of closed isolated worldstate should still pass check + checkIsolatedState.accept(isolatedClone); } @Test @@ -270,7 +298,7 @@ public void testSnapshotRollToTrieLogBlockHash() { // roll chain and worldstate to block 2 blockchain.rewindToBlock(2L); - var block1State = archive.getMutable(2L, true); + var block1State = archive.getMutable(null, block2.getHash()); // BonsaiPersistedWorldState should be at block 2 assertThat(block1State.get().get(testAddress)).isNotNull(); 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 new file mode 100644 index 00000000000..ebf29718a01 --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/BonsaiSnapshotWorldStateArchiveTest.java @@ -0,0 +1,93 @@ +/* + * Copyright ConsenSys AG. + * + * 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 static org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateKeyValueStorage.WORLD_BLOCK_HASH_KEY; +import static org.hyperledger.besu.ethereum.bonsai.BonsaiWorldStateKeyValueStorage.WORLD_ROOT_HASH_KEY; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.chain.Blockchain; +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.ethereum.storage.StorageProvider; +import org.hyperledger.besu.ethereum.storage.keyvalue.KeyValueSegmentIdentifier; +import org.hyperledger.besu.plugin.services.storage.SnappableKeyValueStorage; + +import java.util.Optional; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.junit.MockitoJUnitRunner; + +@RunWith(MockitoJUnitRunner.class) +public class BonsaiSnapshotWorldStateArchiveTest { + + final BlockHeaderTestFixture blockBuilder = new BlockHeaderTestFixture(); + + @Mock Blockchain blockchain; + + @Mock StorageProvider storageProvider; + + @Mock SnappableKeyValueStorage keyValueStorage; + + BonsaiWorldStateArchive bonsaiWorldStateArchive; + + @Before + public void setUp() { + when(storageProvider.getStorageBySegmentIdentifier(any(KeyValueSegmentIdentifier.class))) + .thenReturn(keyValueStorage); + } + + @Test + public void testGetMutableReturnPersistedStateWhenNeeded() { + final BlockHeader chainHead = blockBuilder.number(0).buildHeader(); + + when(keyValueStorage.get(WORLD_ROOT_HASH_KEY)) + .thenReturn(Optional.of(chainHead.getStateRoot().toArrayUnsafe())); + when(keyValueStorage.get(WORLD_BLOCK_HASH_KEY)) + .thenReturn(Optional.of(chainHead.getHash().toArrayUnsafe())); + when(keyValueStorage.get(WORLD_ROOT_HASH_KEY)) + .thenReturn(Optional.of(chainHead.getStateRoot().toArrayUnsafe())); + when(keyValueStorage.get(WORLD_BLOCK_HASH_KEY)) + .thenReturn(Optional.of(chainHead.getHash().toArrayUnsafe())); + bonsaiWorldStateArchive = + new BonsaiWorldStateArchive( + new BonsaiWorldStateKeyValueStorage(storageProvider), + blockchain, + Optional.of(1L), + true); + + assertThat(bonsaiWorldStateArchive.getMutable(null, chainHead.getHash(), true)) + .containsInstanceOf(BonsaiPersistedWorldState.class); + } + + @Test + public void testGetMutableReturnEmptyWhenLoadMoreThanLimitLayersBack() { + bonsaiWorldStateArchive = + new BonsaiWorldStateArchive( + new BonsaiWorldStateKeyValueStorage(storageProvider), blockchain, Optional.of(512L)); + final BlockHeader blockHeader = blockBuilder.number(0).buildHeader(); + final BlockHeader chainHead = blockBuilder.number(512).buildHeader(); + when(blockchain.getBlockHeader(eq(blockHeader.getHash()))).thenReturn(Optional.of(blockHeader)); + when(blockchain.getChainHeadHeader()).thenReturn(chainHead); + assertThat(bonsaiWorldStateArchive.getMutable(null, blockHeader.getHash(), false)).isEmpty(); + } +} 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 1f3002094d3..6a4bad76d44 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 @@ -28,14 +28,15 @@ import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.bonsai.LayeredTrieLogManager.LayeredWorldStateCache; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockHeader; 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.plugin.services.storage.KeyValueStorage; import org.hyperledger.besu.plugin.services.storage.KeyValueStorageTransaction; +import org.hyperledger.besu.plugin.services.storage.SnappableKeyValueStorage; import java.util.HashMap; import java.util.Map; @@ -58,7 +59,7 @@ public class BonsaiWorldStateArchiveTest { @Mock StorageProvider storageProvider; - @Mock KeyValueStorage keyValueStorage; + @Mock SnappableKeyValueStorage keyValueStorage; BonsaiWorldStateArchive bonsaiWorldStateArchive; @@ -82,9 +83,7 @@ public void testGetMutableReturnPersistedStateWhenNeeded() { .thenReturn(Optional.of(chainHead.getHash().toArrayUnsafe())); bonsaiWorldStateArchive = new BonsaiWorldStateArchive( - new TrieLogManager(blockchain, new BonsaiWorldStateKeyValueStorage(storageProvider), 1), - storageProvider, - blockchain); + new BonsaiWorldStateKeyValueStorage(storageProvider), blockchain, Optional.of(1L)); assertThat(bonsaiWorldStateArchive.getMutable(null, chainHead.getHash(), true)) .containsInstanceOf(BonsaiPersistedWorldState.class); @@ -94,10 +93,7 @@ public void testGetMutableReturnPersistedStateWhenNeeded() { public void testGetMutableReturnEmptyWhenLoadMoreThanLimitLayersBack() { bonsaiWorldStateArchive = new BonsaiWorldStateArchive( - new TrieLogManager( - blockchain, new BonsaiWorldStateKeyValueStorage(storageProvider), 512), - storageProvider, - blockchain); + new BonsaiWorldStateKeyValueStorage(storageProvider), blockchain, Optional.of(512L)); final BlockHeader blockHeader = blockBuilder.number(0).buildHeader(); final BlockHeader chainHead = blockBuilder.number(512).buildHeader(); when(blockchain.getBlockHeader(eq(blockHeader.getHash()))).thenReturn(Optional.of(blockHeader)); @@ -109,10 +105,7 @@ blockchain, new BonsaiWorldStateKeyValueStorage(storageProvider), 512), public void testGetMutableWhenLoadLessThanLimitLayersBack() { bonsaiWorldStateArchive = new BonsaiWorldStateArchive( - new TrieLogManager( - blockchain, new BonsaiWorldStateKeyValueStorage(storageProvider), 512), - storageProvider, - blockchain); + new BonsaiWorldStateKeyValueStorage(storageProvider), blockchain, Optional.of(512L)); final BlockHeader blockHeader = blockBuilder.number(0).buildHeader(); final BlockHeader chainHead = blockBuilder.number(511).buildHeader(); @@ -136,15 +129,15 @@ public void testGetMutableWithStorageInconsistencyRollbackTheState() { when(keyValueStorage.startTransaction()).thenReturn(mock(KeyValueStorageTransaction.class)); final Map layeredWorldStatesByHash = mock(HashMap.class); + var worldStateStorage = new BonsaiWorldStateKeyValueStorage(storageProvider); bonsaiWorldStateArchive = - new BonsaiWorldStateArchive( - new TrieLogManager( + spy( + new BonsaiWorldStateArchive( + new LayeredTrieLogManager( + blockchain, worldStateStorage, 12L, layeredWorldStatesByHash), + worldStateStorage, blockchain, - new BonsaiWorldStateKeyValueStorage(storageProvider), - 12, - layeredWorldStatesByHash), - storageProvider, - blockchain); + false)); final BlockHeader blockHeader = blockBuilder.number(0).buildHeader(); when(blockchain.getBlockHeader(eq(blockHeader.getHash()))).thenReturn(Optional.of(blockHeader)); @@ -163,16 +156,15 @@ public void testGetMutableWithStorageConsistencyNotRollbackTheState() { when(keyValueStorage.startTransaction()).thenReturn(mock(KeyValueStorageTransaction.class)); final Map layeredWorldStatesByHash = mock(HashMap.class); + var worldStateStorage = new BonsaiWorldStateKeyValueStorage(storageProvider); bonsaiWorldStateArchive = spy( new BonsaiWorldStateArchive( - new TrieLogManager( - blockchain, - new BonsaiWorldStateKeyValueStorage(storageProvider), - 12, - layeredWorldStatesByHash), - storageProvider, - blockchain)); + new LayeredTrieLogManager( + blockchain, worldStateStorage, 12L, layeredWorldStatesByHash), + worldStateStorage, + blockchain, + false)); var worldState = (BonsaiPersistedWorldState) bonsaiWorldStateArchive.getMutable(); var updater = spy(bonsaiWorldStateArchive.getUpdaterFromPersistedState(worldState)); @@ -187,7 +179,6 @@ public void testGetMutableWithStorageConsistencyNotRollbackTheState() { .containsInstanceOf(BonsaiPersistedWorldState.class); // verify is not trying to get the trie log layer to rollback when block is present - verify(layeredWorldStatesByHash).entrySet(); verify(updater, times(0)).rollBack(any()); verify(updater, times(0)).rollForward(any()); } @@ -202,23 +193,25 @@ public void testGetMutableWithStorageConsistencyToRollbackAndRollForwardTheState final BlockHeader blockHeaderChainB = blockBuilder.number(1).timestamp(2).parentHash(genesis.getHash()).buildHeader(); - final Map layeredWorldStatesByHash = mock(HashMap.class); + final Map layeredWorldStatesByHash = mock(HashMap.class); when(layeredWorldStatesByHash.containsKey(any(Bytes32.class))).thenReturn(true); when(layeredWorldStatesByHash.get(eq(blockHeaderChainA.getHash()))) - .thenReturn(mock(BonsaiLayeredWorldState.class, Answers.RETURNS_MOCKS)); + .thenReturn( + new LayeredWorldStateCache(mock(BonsaiLayeredWorldState.class, Answers.RETURNS_MOCKS))); when(layeredWorldStatesByHash.get(eq(blockHeaderChainB.getHash()))) - .thenReturn(mock(BonsaiLayeredWorldState.class, Answers.RETURNS_MOCKS)); + .thenReturn( + new LayeredWorldStateCache(mock(BonsaiLayeredWorldState.class, Answers.RETURNS_MOCKS))); + + var worldStateStorage = new BonsaiWorldStateKeyValueStorage(storageProvider); bonsaiWorldStateArchive = spy( new BonsaiWorldStateArchive( - new TrieLogManager( - blockchain, - new BonsaiWorldStateKeyValueStorage(storageProvider), - 12, - layeredWorldStatesByHash), - storageProvider, - blockchain)); + new LayeredTrieLogManager( + blockchain, worldStateStorage, 12L, layeredWorldStatesByHash), + worldStateStorage, + blockchain, + false)); var worldState = (BonsaiPersistedWorldState) bonsaiWorldStateArchive.getMutable(); var updater = spy(bonsaiWorldStateArchive.getUpdaterFromPersistedState(worldState)); when(bonsaiWorldStateArchive.getUpdaterFromPersistedState(worldState)).thenReturn(updater); @@ -237,7 +230,6 @@ public void testGetMutableWithStorageConsistencyToRollbackAndRollForwardTheState verify(layeredWorldStatesByHash).get(eq(blockHeaderChainA.getHash())); verify(layeredWorldStatesByHash).containsKey(eq(blockHeaderChainB.getHash())); verify(layeredWorldStatesByHash).get(eq(blockHeaderChainB.getHash())); - verify(layeredWorldStatesByHash).entrySet(); verify(updater, times(1)).rollBack(any()); verify(updater, times(1)).rollForward(any()); } @@ -254,23 +246,23 @@ public void testGetMutableWithRollbackNotOverrideTrieLogLayer() { final BlockHeader blockHeaderChainB = blockBuilder.number(1).timestamp(2).parentHash(genesis.getHash()).buildHeader(); - final Map layeredWorldStatesByHash = mock(HashMap.class); + final Map layeredWorldStatesByHash = mock(HashMap.class); when(layeredWorldStatesByHash.containsKey(any(Bytes32.class))).thenReturn(true); when(layeredWorldStatesByHash.get(eq(blockHeaderChainA.getHash()))) - .thenReturn(mock(BonsaiLayeredWorldState.class, Answers.RETURNS_MOCKS)); + .thenReturn( + new LayeredWorldStateCache(mock(BonsaiLayeredWorldState.class, Answers.RETURNS_MOCKS))); when(layeredWorldStatesByHash.get(eq(blockHeaderChainB.getHash()))) - .thenReturn(mock(BonsaiLayeredWorldState.class, Answers.RETURNS_MOCKS)); - + .thenReturn( + new LayeredWorldStateCache(mock(BonsaiLayeredWorldState.class, Answers.RETURNS_MOCKS))); + var worldStateStorage = new BonsaiWorldStateKeyValueStorage(storageProvider); bonsaiWorldStateArchive = spy( new BonsaiWorldStateArchive( - new TrieLogManager( - blockchain, - new BonsaiWorldStateKeyValueStorage(storageProvider), - 12, - layeredWorldStatesByHash), - storageProvider, - blockchain)); + new LayeredTrieLogManager( + blockchain, worldStateStorage, 12L, layeredWorldStatesByHash), + worldStateStorage, + blockchain, + false)); 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/LogRollingTests.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/bonsai/LogRollingTests.java index f6f7ea5decc..bee11318552 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 @@ -109,11 +109,7 @@ public class LogRollingTests { @Before public void createStorage() { final InMemoryKeyValueStorageProvider provider = new InMemoryKeyValueStorageProvider(); - archive = - new BonsaiWorldStateArchive( - new TrieLogManager(blockchain, new BonsaiWorldStateKeyValueStorage(provider)), - provider, - blockchain); + archive = new BonsaiWorldStateArchive(provider, blockchain); accountStorage = (InMemoryKeyValueStorage) provider.getStorageBySegmentIdentifier(KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE); @@ -132,11 +128,7 @@ public void createStorage() { provider.getStorageBySegmentIdentifier(KeyValueSegmentIdentifier.TRIE_LOG_STORAGE); final InMemoryKeyValueStorageProvider secondProvider = new InMemoryKeyValueStorageProvider(); - secondArchive = - new BonsaiWorldStateArchive( - new TrieLogManager(blockchain, new BonsaiWorldStateKeyValueStorage(secondProvider)), - secondProvider, - blockchain); + secondArchive = new BonsaiWorldStateArchive(secondProvider, blockchain); 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 3c45cc16e7f..a1cf2948d6a 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 @@ -38,11 +38,7 @@ public static void main(final String[] arg) throws IOException { new RollingFileReader((i, c) -> Path.of(String.format(arg[0] + "-%04d.rdat", i)), false); final InMemoryKeyValueStorageProvider provider = new InMemoryKeyValueStorageProvider(); - final BonsaiWorldStateArchive archive = - new BonsaiWorldStateArchive( - new TrieLogManager(null, new BonsaiWorldStateKeyValueStorage(provider)), - provider, - null); + final BonsaiWorldStateArchive archive = new BonsaiWorldStateArchive(provider, null); final InMemoryKeyValueStorage accountStorage = (InMemoryKeyValueStorage) provider.getStorageBySegmentIdentifier(KeyValueSegmentIdentifier.ACCOUNT_INFO_STATE); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java index 0a19a9d131f..68f6baaa05b 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPool.java @@ -317,28 +317,25 @@ && strictReplayProtectionShouldBeEnforceLocally(chainHeadBlockHeader) "EIP-1559 transaction are not allowed yet"); } - return protocolContext - .getWorldStateArchive() - .getMutable(chainHeadBlockHeader.getStateRoot(), chainHeadBlockHeader.getHash(), false) - .map( - worldState -> { - try { - final Account senderAccount = worldState.get(transaction.getSender()); - return new ValidationResultAndAccount( - senderAccount, - getTransactionValidator() - .validateForSender( - transaction, - senderAccount, - TransactionValidationParams.transactionPool())); - } catch (MerkleTrieException ex) { - LOG.debug( - "MerkleTrieException while validating transaction for sender {}", - transaction.getSender()); - return ValidationResultAndAccount.invalid(CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE); - } - }) - .orElseGet(() -> ValidationResultAndAccount.invalid(CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE)); + try (var worldState = + protocolContext + .getWorldStateArchive() + .getMutable(chainHeadBlockHeader.getStateRoot(), chainHeadBlockHeader.getHash(), false) + .orElseThrow()) { + final Account senderAccount = worldState.get(transaction.getSender()); + return new ValidationResultAndAccount( + senderAccount, + getTransactionValidator() + .validateForSender( + transaction, senderAccount, TransactionValidationParams.transactionPool())); + } catch (MerkleTrieException ex) { + LOG.debug( + "MerkleTrieException while validating transaction for sender {}", + transaction.getSender()); + return ValidationResultAndAccount.invalid(CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE); + } catch (Exception ex) { + return ValidationResultAndAccount.invalid(CHAIN_HEAD_WORLD_STATE_NOT_AVAILABLE); + } } private boolean strictReplayProtectionShouldBeEnforceLocally( diff --git a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/WorldState.java b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/WorldState.java index 10667e69c04..7c68c7b5d58 100644 --- a/evm/src/main/java/org/hyperledger/besu/evm/worldstate/WorldState.java +++ b/evm/src/main/java/org/hyperledger/besu/evm/worldstate/WorldState.java @@ -36,7 +36,7 @@ * not mutable. In other words, objects implementing this interface are not guaranteed to be * thread-safe, though some particular implementations may provide such guarantees. */ -public interface WorldState extends WorldView { +public interface WorldState extends WorldView, AutoCloseable { /** * The root hash of the world state this represents. @@ -116,4 +116,9 @@ public NavigableMap storageEntriesFrom( return accountState.storageEntriesFrom(startKeyHash, limit); } } + + @Override + default void close() throws Exception { + // default no-op + } } diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index 6376c119121..648fd64349d 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -66,7 +66,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'QC/7QGfjlWA5tfyfQdf/esATYzLfZbeJ9AnLKkaCy3Q=' + knownHash = 'nBDCEeFH318uhGZEBmuTGOfYLI1+9tLDyjn/RDe5saI=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/storage/KeyValueStorageFactory.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/storage/KeyValueStorageFactory.java index 85a30e6c2a1..53f6c0688aa 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/storage/KeyValueStorageFactory.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/services/storage/KeyValueStorageFactory.java @@ -64,4 +64,14 @@ KeyValueStorage create( * false when keys of different segments can collide with each other. */ boolean isSegmentIsolationSupported(); + + /** + * Whether storage supports repeatable reads AKA snapshots. + * + * @return true when the created storage supports snapshots false when + * it does not. + */ + default boolean isSnapshotIsolationSupported() { + return false; + } } diff --git a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactory.java b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactory.java index 4671b2f2e67..03d95257331 100644 --- a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactory.java +++ b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/RocksDBKeyValueStorageFactory.java @@ -204,4 +204,9 @@ public boolean isSegmentIsolationSupported() { isSegmentIsolationSupported, "Whether segment isolation is supported will be determined during creation. Call a creation method first"); } + + @Override + public boolean isSnapshotIsolationSupported() { + return true; + } } diff --git a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBSnapshot.java b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBSnapshot.java new file mode 100644 index 00000000000..82d2b0182c2 --- /dev/null +++ b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBSnapshot.java @@ -0,0 +1,49 @@ +/* + * 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.plugin.services.storage.rocksdb.segmented; + +import java.util.concurrent.atomic.AtomicInteger; + +import org.rocksdb.OptimisticTransactionDB; +import org.rocksdb.Snapshot; + +/** + * Wraps and reference counts a Snapshot object from an OptimisticTransactionDB such that it can be + * used as the basis of multiple RocksDBSnapshotTransaction's, and released once it is no longer in + * use. + */ +class RocksDBSnapshot { + private final OptimisticTransactionDB db; + private final Snapshot dbSnapshot; + private final AtomicInteger usages = new AtomicInteger(0); + + RocksDBSnapshot(final OptimisticTransactionDB db) { + this.db = db; + this.dbSnapshot = db.getSnapshot(); + } + + Snapshot markAndUseSnapshot() { + usages.incrementAndGet(); + return dbSnapshot; + } + + void unMarkSnapshot() { + if (usages.decrementAndGet() < 1) { + dbSnapshot.close(); + db.releaseSnapshot(dbSnapshot); + } + } +} diff --git a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBSnapshotTransaction.java b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBSnapshotTransaction.java index 659d4c5ce18..659b690ad28 100644 --- a/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBSnapshotTransaction.java +++ b/plugins/rocksdb/src/main/java/org/hyperledger/besu/plugin/services/storage/rocksdb/segmented/RocksDBSnapshotTransaction.java @@ -30,7 +30,6 @@ import org.rocksdb.ReadOptions; import org.rocksdb.RocksDBException; import org.rocksdb.RocksIterator; -import org.rocksdb.Snapshot; import org.rocksdb.Transaction; import org.rocksdb.WriteOptions; import org.slf4j.Logger; @@ -44,7 +43,7 @@ public class RocksDBSnapshotTransaction implements KeyValueStorageTransaction, A private final OptimisticTransactionDB db; private final ColumnFamilyHandle columnFamilyHandle; private final Transaction snapTx; - private final Snapshot snapshot; + private final RocksDBSnapshot snapshot; private final WriteOptions writeOptions; private final ReadOptions readOptions; @@ -55,24 +54,26 @@ public class RocksDBSnapshotTransaction implements KeyValueStorageTransaction, A this.metrics = metrics; this.db = db; this.columnFamilyHandle = columnFamilyHandle; - this.snapshot = db.getSnapshot(); + this.snapshot = new RocksDBSnapshot(db); this.writeOptions = new WriteOptions(); this.snapTx = db.beginTransaction(writeOptions); - this.readOptions = new ReadOptions().setSnapshot(snapshot); + this.readOptions = new ReadOptions().setSnapshot(snapshot.markAndUseSnapshot()); } private RocksDBSnapshotTransaction( final OptimisticTransactionDB db, final ColumnFamilyHandle columnFamilyHandle, final RocksDBMetrics metrics, - final Snapshot snapshot) { + final RocksDBSnapshot snapshot, + final Transaction snapTx, + final ReadOptions readOptions) { this.metrics = metrics; this.db = db; this.columnFamilyHandle = columnFamilyHandle; this.snapshot = snapshot; this.writeOptions = new WriteOptions(); - this.snapTx = db.beginTransaction(writeOptions); - this.readOptions = new ReadOptions().setSnapshot(snapshot); + this.readOptions = readOptions; + this.snapTx = snapTx; } public Optional get(final byte[] key) { @@ -144,19 +145,24 @@ public void rollback() { } public RocksDBSnapshotTransaction copy() { - // TODO: if we use snapshot as the basis of a cloned state, we need to ensure close() of this - // transaction does not release and close the snapshot in use by the cloned state. - return new RocksDBSnapshotTransaction(db, columnFamilyHandle, metrics, snapshot); + try { + var copyReadOptions = new ReadOptions().setSnapshot(snapshot.markAndUseSnapshot()); + var copySnapTx = db.beginTransaction(writeOptions); + copySnapTx.rebuildFromWriteBatch(snapTx.getWriteBatch().getWriteBatch()); + return new RocksDBSnapshotTransaction( + db, columnFamilyHandle, metrics, snapshot, copySnapTx, copyReadOptions); + } catch (Exception ex) { + LOG.error("Failed to copy snapshot transaction", ex); + snapshot.unMarkSnapshot(); + throw new StorageException(ex); + } } @Override public void close() { - // TODO: this is unsafe since another transaction might be using this snapshot - db.releaseSnapshot(snapshot); - - snapshot.close(); snapTx.close(); writeOptions.close(); readOptions.close(); + snapshot.unMarkSnapshot(); } }