From 33085dd2331d137bb846d2d16833341b3dec53eb Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Wed, 18 Sep 2024 05:35:06 +0200 Subject: [PATCH 01/37] Fix flakiness ofTxPoolOptionsTest::txpoolForcePriceBumpToZeroWhenZeroBaseFeeMarket (#7610) Signed-off-by: Fabio Di Fabio --- .../hyperledger/besu/cli/CommandTestAbstract.java | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index 3dd9641b487..7d11a4a8e99 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -24,7 +24,6 @@ import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.atLeast; -import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.lenient; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -43,7 +42,6 @@ import org.hyperledger.besu.cli.options.unstable.MetricsCLIOptions; import org.hyperledger.besu.cli.options.unstable.NetworkingOptions; import org.hyperledger.besu.cli.options.unstable.SynchronizerOptions; -import org.hyperledger.besu.components.BesuComponent; import org.hyperledger.besu.config.GenesisConfigOptions; import org.hyperledger.besu.controller.BesuController; import org.hyperledger.besu.controller.BesuControllerBuilder; @@ -256,10 +254,8 @@ public abstract class CommandTestAbstract { @BeforeEach public void initMocks() throws Exception { - // doReturn used because of generic BesuController - doReturn(mockControllerBuilder) - .when(mockControllerBuilderFactory) - .fromEthNetworkConfig(any(), any()); + when(mockControllerBuilderFactory.fromEthNetworkConfig(any(), any())) + .thenReturn(mockControllerBuilder); when(mockControllerBuilder.synchronizerConfiguration(any())).thenReturn(mockControllerBuilder); when(mockControllerBuilder.ethProtocolConfiguration(any())).thenReturn(mockControllerBuilder); when(mockControllerBuilder.transactionPoolConfiguration(any())) @@ -288,14 +284,12 @@ public void initMocks() throws Exception { when(mockControllerBuilder.maxPeers(anyInt())).thenReturn(mockControllerBuilder); when(mockControllerBuilder.maxRemotelyInitiatedPeers(anyInt())) .thenReturn(mockControllerBuilder); - when(mockControllerBuilder.besuComponent(any(BesuComponent.class))) - .thenReturn(mockControllerBuilder); + when(mockControllerBuilder.besuComponent(any())).thenReturn(mockControllerBuilder); when(mockControllerBuilder.cacheLastBlocks(any())).thenReturn(mockControllerBuilder); when(mockControllerBuilder.genesisStateHashCacheEnabled(any())) .thenReturn(mockControllerBuilder); - // doReturn used because of generic BesuController - doReturn(mockController).when(mockControllerBuilder).build(); + when(mockControllerBuilder.build()).thenReturn(mockController); lenient().when(mockController.getProtocolManager()).thenReturn(mockEthProtocolManager); lenient().when(mockController.getProtocolSchedule()).thenReturn(mockProtocolSchedule); lenient().when(mockController.getProtocolContext()).thenReturn(mockProtocolContext); From 578104e222619eedcf90740b8b1176b10c7ce165 Mon Sep 17 00:00:00 2001 From: Gabriel-Trintinalia Date: Wed, 18 Sep 2024 15:20:14 +1000 Subject: [PATCH 02/37] Fix Snap Server Account Range tests (#7552) Signed-off-by: Gabriel-Trintinalia --- .../ethereum/core/BlockchainSetupUtil.java | 21 +- .../ethereum/eth/manager/snap/SnapServer.java | 18 +- .../messages/snap/AccountRangeMessage.java | 25 +- .../messages/snap/GetAccountRangeMessage.java | 12 +- .../snap/SnapServerGetAccountRangeTest.java | 495 ++++++++++++++++++ .../eth/manager/snap/SnapServerTest.java | 2 +- .../snap/AccountRangeMessageTest.java | 83 ++- .../besu/testutil/BlockTestUtil.java | 20 + .../src/main/resources/snap/snapGenesis.json | 111 ++++ .../main/resources/snap/testBlockchain.blocks | Bin 0 -> 342468 bytes 10 files changed, 772 insertions(+), 15 deletions(-) create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerGetAccountRangeTest.java create mode 100644 testutil/src/main/resources/snap/snapGenesis.json create mode 100644 testutil/src/main/resources/snap/testBlockchain.blocks diff --git a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockchainSetupUtil.java b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockchainSetupUtil.java index 551f593e825..62d670c07ab 100644 --- a/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockchainSetupUtil.java +++ b/ethereum/core/src/test-support/java/org/hyperledger/besu/ethereum/core/BlockchainSetupUtil.java @@ -85,6 +85,13 @@ public Blockchain importAllBlocks() { return blockchain; } + public Blockchain importAllBlocks( + final HeaderValidationMode headerValidationMode, + final HeaderValidationMode ommerValidationMode) { + importBlocks(blocks, headerValidationMode, ommerValidationMode); + return blockchain; + } + public void importFirstBlocks(final int count) { importBlocks(blocks.subList(0, count)); } @@ -126,6 +133,10 @@ public static BlockchainSetupUtil forUpgradedFork() { return createForEthashChain(BlockTestUtil.getUpgradedForkResources(), DataStorageFormat.FOREST); } + public static BlockchainSetupUtil forSnapTesting(final DataStorageFormat storageFormat) { + return createForEthashChain(BlockTestUtil.getSnapTestChainResources(), storageFormat); + } + public static BlockchainSetupUtil createForEthashChain( final ChainResources chainResources, final DataStorageFormat storageFormat) { return create( @@ -241,6 +252,13 @@ public TransactionPool getTransactionPool() { } private void importBlocks(final List blocks) { + importBlocks(blocks, HeaderValidationMode.FULL, HeaderValidationMode.FULL); + } + + private void importBlocks( + final List blocks, + final HeaderValidationMode headerValidationMode, + final HeaderValidationMode ommerValidationMode) { for (final Block block : blocks) { if (block.getHeader().getNumber() == BlockHeader.GENESIS_BLOCK_NUMBER) { continue; @@ -248,7 +266,8 @@ private void importBlocks(final List blocks) { final ProtocolSpec protocolSpec = protocolSchedule.getByBlockHeader(block.getHeader()); final BlockImporter blockImporter = protocolSpec.getBlockImporter(); final BlockImportResult result = - blockImporter.importBlock(protocolContext, block, HeaderValidationMode.FULL); + blockImporter.importBlock( + protocolContext, block, headerValidationMode, ommerValidationMode); if (!result.isImported()) { throw new IllegalStateException("Unable to import block " + block.getHeader().getNumber()); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServer.java index c1b855f2d6f..7de933e1370 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServer.java @@ -31,6 +31,7 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.proof.WorldStateProofProvider; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; +import org.hyperledger.besu.ethereum.rlp.RLP; import org.hyperledger.besu.ethereum.trie.CompactEncoding; import org.hyperledger.besu.ethereum.trie.MerkleTrie; import org.hyperledger.besu.ethereum.trie.diffbased.bonsai.BonsaiWorldStateProvider; @@ -240,12 +241,9 @@ MessageData constructGetAccountRangeResponse(final MessageData message) { stopWatch, maxResponseBytes, (pair) -> { - var rlpOutput = new BytesValueRLPOutput(); - rlpOutput.startList(); - rlpOutput.writeBytes(pair.getFirst()); - rlpOutput.writeRLPBytes(pair.getSecond()); - rlpOutput.endList(); - return rlpOutput.encodedSize(); + Bytes bytes = + AccountRangeMessage.toSlimAccount(RLP.input(pair.getSecond())); + return Hash.SIZE + bytes.size(); }); final Bytes32 endKeyBytes = range.endKeyHash(); @@ -257,11 +255,15 @@ MessageData constructGetAccountRangeResponse(final MessageData message) { storage.streamFlatAccounts(range.startKeyHash(), shouldContinuePredicate); if (accounts.isEmpty() && shouldContinuePredicate.shouldContinue.get()) { + var fromNextHash = + range.endKeyHash().compareTo(range.startKeyHash()) >= 0 + ? range.endKeyHash() + : range.startKeyHash(); // fetch next account after range, if it exists LOGGER.debug( "found no accounts in range, taking first value starting from {}", - asLogHash(range.endKeyHash())); - accounts = storage.streamFlatAccounts(range.endKeyHash(), UInt256.MAX_VALUE, 1L); + asLogHash(fromNextHash)); + accounts = storage.streamFlatAccounts(fromNextHash, UInt256.MAX_VALUE, 1L); } final var worldStateProof = diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessage.java index 8e1b1a31a54..71d04295925 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessage.java @@ -14,6 +14,7 @@ */ package org.hyperledger.besu.ethereum.eth.messages.snap; +import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.AbstractSnapMessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; @@ -28,6 +29,7 @@ import java.util.Optional; import java.util.TreeMap; +import com.google.common.annotations.VisibleForTesting; import com.google.common.collect.Maps; import kotlin.collections.ArrayDeque; import org.apache.tuweni.bytes.Bytes; @@ -117,7 +119,8 @@ public AccountRangeData accountData(final boolean withRequestId) { return ImmutableAccountRangeData.builder().accounts(accounts).proofs(proofs).build(); } - private Bytes toFullAccount(final RLPInput rlpInput) { + @VisibleForTesting + public static Bytes toFullAccount(final RLPInput rlpInput) { final StateTrieAccountValue accountValue = StateTrieAccountValue.readFrom(rlpInput); final BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); @@ -131,6 +134,26 @@ private Bytes toFullAccount(final RLPInput rlpInput) { return rlpOutput.encoded(); } + public static Bytes toSlimAccount(final RLPInput rlpInput) { + StateTrieAccountValue accountValue = StateTrieAccountValue.readFrom(rlpInput); + var rlpOutput = new BytesValueRLPOutput(); + rlpOutput.startList(); + rlpOutput.writeLongScalar(accountValue.getNonce()); + rlpOutput.writeUInt256Scalar(accountValue.getBalance()); + if (accountValue.getStorageRoot().equals(Hash.EMPTY_TRIE_HASH)) { + rlpOutput.writeNull(); + } else { + rlpOutput.writeBytes(accountValue.getStorageRoot()); + } + if (accountValue.getCodeHash().equals(Hash.EMPTY)) { + rlpOutput.writeNull(); + } else { + rlpOutput.writeBytes(accountValue.getCodeHash()); + } + rlpOutput.endList(); + return rlpOutput.encoded(); + } + @Value.Immutable public interface AccountRangeData { diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/GetAccountRangeMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/GetAccountRangeMessage.java index 8f5fcaf9a73..6d8c8c128c6 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/GetAccountRangeMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/snap/GetAccountRangeMessage.java @@ -24,6 +24,7 @@ import java.math.BigInteger; import java.util.Optional; +import com.google.common.annotations.VisibleForTesting; import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; import org.immutables.value.Value; @@ -48,12 +49,21 @@ public static GetAccountRangeMessage readFrom(final MessageData message) { public static GetAccountRangeMessage create( final Hash worldStateRootHash, final Bytes32 startKeyHash, final Bytes32 endKeyHash) { + return create(worldStateRootHash, startKeyHash, endKeyHash, SIZE_REQUEST); + } + + @VisibleForTesting + public static GetAccountRangeMessage create( + final Hash worldStateRootHash, + final Bytes32 startKeyHash, + final Bytes32 endKeyHash, + final BigInteger sizeRequest) { final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); tmp.startList(); tmp.writeBytes(worldStateRootHash); tmp.writeBytes(startKeyHash); tmp.writeBytes(endKeyHash); - tmp.writeBigIntegerScalar(SIZE_REQUEST); + tmp.writeBigIntegerScalar(sizeRequest); tmp.endList(); return new GetAccountRangeMessage(tmp.encoded()); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerGetAccountRangeTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerGetAccountRangeTest.java new file mode 100644 index 00000000000..6d8180c8c0e --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerGetAccountRangeTest.java @@ -0,0 +1,495 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.eth.manager.snap; + +import static org.assertj.core.api.AssertionsForClassTypes.assertThat; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.ProtocolContext; +import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; +import org.hyperledger.besu.ethereum.eth.manager.EthMessages; +import org.hyperledger.besu.ethereum.eth.messages.snap.AccountRangeMessage; +import org.hyperledger.besu.ethereum.eth.messages.snap.GetAccountRangeMessage; +import org.hyperledger.besu.ethereum.eth.sync.snapsync.ImmutableSnapSyncConfiguration; +import org.hyperledger.besu.ethereum.eth.sync.snapsync.SnapSyncConfiguration; +import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; +import org.hyperledger.besu.ethereum.trie.diffbased.common.DiffBasedWorldStateProvider; +import org.hyperledger.besu.ethereum.worldstate.WorldStateStorageCoordinator; +import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; + +import java.math.BigInteger; +import java.util.NavigableMap; + +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class SnapServerGetAccountRangeTest { + private Hash rootHash; + public Bytes32 firstAccount; + public Bytes32 secondAccount; + private SnapServer snapServer; + private static ProtocolContext protocolContext; + + @BeforeAll + public static void setup() { + // Setup local blockchain for testing + BlockchainSetupUtil localBlockchainSetup = + BlockchainSetupUtil.forSnapTesting(DataStorageFormat.BONSAI); + localBlockchainSetup.importAllBlocks( + HeaderValidationMode.LIGHT_DETACHED_ONLY, HeaderValidationMode.LIGHT); + + protocolContext = localBlockchainSetup.getProtocolContext(); + } + + @BeforeEach + public void setupTest() { + WorldStateStorageCoordinator worldStateStorageCoordinator = + new WorldStateStorageCoordinator( + ((DiffBasedWorldStateProvider) protocolContext.getWorldStateArchive()) + .getWorldStateKeyValueStorage()); + + SnapSyncConfiguration snapSyncConfiguration = + ImmutableSnapSyncConfiguration.builder().isSnapServerEnabled(true).build(); + snapServer = + new SnapServer( + snapSyncConfiguration, + new EthMessages(), + worldStateStorageCoordinator, + protocolContext) + .start(); + initAccounts(); + } + + /** + * In this test, we request the entire state range, but limit the response to 4000 bytes. + * Expected: 86 accounts. + */ + @Test + public void test0_RequestEntireStateRangeWith4000BytesLimit() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .responseBytes(4000) + .expectedAccounts(86) + .expectedFirstAccount(firstAccount) + .expectedLastAccount( + Bytes32.fromHexString( + "0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099")) + .build()); + } + + /** + * In this test, we request the entire state range, but limit the response to 3000 bytes. + * Expected: 65 accounts. + */ + @Test + public void test1_RequestEntireStateRangeWith3000BytesLimit() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .responseBytes(3000) + .expectedAccounts(65) + .expectedFirstAccount(firstAccount) + .expectedLastAccount( + Bytes32.fromHexString( + "0x2e6fe1362b3e388184fd7bf08e99e74170b26361624ffd1c5f646da7067b58b6")) + .build()); + } + + /** + * In this test, we request the entire state range, but limit the response to 2000 bytes. + * Expected: 44 accounts. + */ + @Test + public void test2_RequestEntireStateRangeWith2000BytesLimit() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .responseBytes(2000) + .expectedAccounts(44) + .expectedFirstAccount(firstAccount) + .expectedLastAccount( + Bytes32.fromHexString( + "0x1c3f74249a4892081ba0634a819aec9ed25f34c7653f5719b9098487e65ab595")) + .build()); + } + + /** + * In this test, we request the entire state range, but limit the response to 1 byte. The server + * should return the first account of the state. Expected: 1 account. + */ + @Test + public void test3_RequestEntireStateRangeWith1ByteLimit() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .responseBytes(1) + .expectedAccounts(1) + .expectedFirstAccount(firstAccount) + .expectedLastAccount(firstAccount) + .build()); + } + + /** + * Here we request with a responseBytes limit of zero. The server should return one account. + * Expected: 1 account. + */ + @Test + public void test4_RequestEntireStateRangeWithZeroBytesLimit() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .responseBytes(0) + .expectedAccounts(1) + .expectedFirstAccount(firstAccount) + .expectedLastAccount(firstAccount) + .build()); + } + + /** + * In this test, we request a range where startingHash is before the first available account key, + * and limitHash is after. The server should return the first and second account of the state + * (because the second account is the 'next available'). Expected: 2 accounts. + */ + @Test + public void test5_RequestRangeBeforeFirstAccountKey() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .startHash(hashAdd(firstAccount, -500)) + .limitHash(hashAdd(firstAccount, 1)) + .expectedAccounts(2) + .expectedFirstAccount(firstAccount) + .expectedLastAccount(secondAccount) + .build()); + } + + /** + * Here we request range where both bounds are before the first available account key. This should + * return the first account (even though it's out of bounds). Expected: 1 account. + */ + @Test + public void test6_RequestRangeBothBoundsBeforeFirstAccountKey() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .startHash(hashAdd(firstAccount, -500)) + .limitHash(hashAdd(firstAccount, -400)) + .expectedAccounts(1) + .expectedFirstAccount(firstAccount) + .expectedLastAccount(firstAccount) + .build()); + } + + /** + * In this test, both startingHash and limitHash are zero. The server should return the first + * available account. Expected: 1 account. + */ + @Test + public void test7_RequestBothBoundsZero() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .startHash(Hash.ZERO) + .limitHash(Hash.ZERO) + .expectedAccounts(1) + .expectedFirstAccount(firstAccount) + .expectedLastAccount(firstAccount) + .build()); + } + + /** + * In this test, startingHash is exactly the first available account key. The server should return + * the first available account of the state as the first item. Expected: 86 accounts. + */ + @Test + public void test8_RequestStartingHashFirstAvailableAccountKey() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .startHash(firstAccount) + .responseBytes(4000) + .expectedAccounts(86) + .expectedFirstAccount(firstAccount) + .expectedLastAccount( + Bytes32.fromHexString( + "0x445cb5c1278fdce2f9cbdb681bdd76c52f8e50e41dbd9e220242a69ba99ac099")) + .build()); + } + + /** + * In this test, startingHash is after the first available key. The server should return the + * second account of the state as the first item. Expected: 86 accounts. + */ + @Test + public void test9_RequestStartingHashAfterFirstAvailableKey() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .startHash(secondAccount) + .responseBytes(4000) + .expectedAccounts(86) + .expectedFirstAccount(secondAccount) + .expectedLastAccount( + Bytes32.fromHexString( + "0x4615e5f5df5b25349a00ad313c6cd0436b6c08ee5826e33a018661997f85ebaa")) + .build()); + } + + /** This test requests a non-existent state root. Expected: 0 accounts. */ + @Test + public void test10_RequestNonExistentStateRoot() { + Hash rootHash = + Hash.fromHexString("1337000000000000000000000000000000000000000000000000000000000000"); + testAccountRangeRequest( + new AccountRangeRequestParams.Builder().rootHash(rootHash).expectedAccounts(0).build()); + } + + /** + * This test requests data at a state root that is 127 blocks old. We expect the server to have + * this state available. Expected: 84 accounts. + */ + @Test + public void test12_RequestStateRoot127BlocksOld() { + + Hash rootHash = + protocolContext + .getBlockchain() + .getBlockHeader((protocolContext.getBlockchain().getChainHeadBlockNumber() - 127)) + .orElseThrow() + .getStateRoot(); + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .expectedAccounts(84) + .responseBytes(4000) + .expectedFirstAccount(firstAccount) + .expectedLastAccount( + Bytes32.fromHexString( + "0x580aa878e2f92d113a12c0a3ce3c21972b03dbe80786858d49a72097e2c491a3")) + .build()); + } + + /** + * This test requests data at a state root that is actually the storage root of an existing + * account. The server is supposed to ignore this request. Expected: 0 accounts. + */ + @Test + public void test13_RequestStateRootIsStorageRoot() { + Hash rootHash = + Hash.fromHexString("df97f94bc47471870606f626fb7a0b42eed2d45fcc84dc1200ce62f7831da990"); + testAccountRangeRequest( + new AccountRangeRequestParams.Builder().rootHash(rootHash).expectedAccounts(0).build()); + } + + /** + * In this test, the startingHash is after limitHash (wrong order). The server should ignore this + * invalid request. Expected: 0 accounts. + */ + @Test + public void test14_RequestStartingHashAfterLimitHash() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .startHash(Hash.LAST) + .limitHash(Hash.ZERO) + .expectedAccounts(0) + .build()); + } + + /** + * In this test, the startingHash is the first available key, and limitHash is a key before + * startingHash (wrong order). The server should return the first available key. Expected: 1 + * account. + */ + @Test + public void test15_RequestStartingHashFirstAvailableKeyAndLimitHashBefore() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .startHash(firstAccount) + .limitHash(hashAdd(firstAccount, -1)) + .expectedAccounts(1) + .expectedFirstAccount(firstAccount) + .expectedLastAccount(firstAccount) + .build()); + } + + /** + * In this test, the startingHash is the first available key and limitHash is zero. (wrong order). + * The server should return the first available key. Expected: 1 account. + */ + @Test + public void test16_RequestStartingHashFirstAvailableKeyAndLimitHashZero() { + testAccountRangeRequest( + new AccountRangeRequestParams.Builder() + .rootHash(rootHash) + .startHash(firstAccount) + .limitHash(Hash.ZERO) + .expectedAccounts(1) + .expectedFirstAccount(firstAccount) + .expectedLastAccount(firstAccount) + .build()); + } + + private void testAccountRangeRequest(final AccountRangeRequestParams params) { + NavigableMap accounts = getAccountRange(params); + assertThat(accounts.size()).isEqualTo(params.getExpectedAccounts()); + + if (params.getExpectedAccounts() > 0) { + assertThat(accounts.firstKey()).isEqualTo(params.getExpectedFirstAccount()); + assertThat(accounts.lastKey()).isEqualTo(params.getExpectedLastAccount()); + } + } + + private NavigableMap getAccountRange(final AccountRangeRequestParams params) { + Hash rootHash = params.getRootHash(); + Bytes32 startHash = params.getStartHash(); + Bytes32 limitHash = params.getLimitHash(); + BigInteger sizeRequest = BigInteger.valueOf(params.getResponseBytes()); + + GetAccountRangeMessage requestMessage = + GetAccountRangeMessage.create(rootHash, startHash, limitHash, sizeRequest); + + AccountRangeMessage resultMessage = + AccountRangeMessage.readFrom( + snapServer.constructGetAccountRangeResponse( + requestMessage.wrapMessageData(BigInteger.ONE))); + NavigableMap accounts = resultMessage.accountData(false).accounts(); + return accounts; + } + + @SuppressWarnings("UnusedVariable") + private void initAccounts() { + rootHash = protocolContext.getWorldStateArchive().getMutable().rootHash(); + GetAccountRangeMessage requestMessage = + GetAccountRangeMessage.create(rootHash, Hash.ZERO, Hash.LAST, BigInteger.valueOf(4000)); + AccountRangeMessage resultMessage = + AccountRangeMessage.readFrom( + snapServer.constructGetAccountRangeResponse( + requestMessage.wrapMessageData(BigInteger.ONE))); + NavigableMap accounts = resultMessage.accountData(false).accounts(); + firstAccount = accounts.firstEntry().getKey(); + secondAccount = + accounts.entrySet().stream().skip(1).findFirst().orElse(accounts.firstEntry()).getKey(); + } + + private Bytes32 hashAdd(final Bytes32 hash, final int value) { + var result = Hash.wrap(hash).toBigInteger().add(BigInteger.valueOf(value)); + Bytes resultBytes = Bytes.wrap(result.toByteArray()); + return Bytes32.leftPad(resultBytes); + } + + public static class AccountRangeRequestParams { + private final Hash rootHash; + private final Bytes32 startHash; + private final Bytes32 limitHash; + private final int responseBytes; + private final int expectedAccounts; + private final Bytes32 expectedFirstAccount; + private final Bytes32 expectedLastAccount; + + private AccountRangeRequestParams(final Builder builder) { + this.rootHash = builder.rootHash; + this.startHash = builder.startHash; + this.limitHash = builder.limitHash; + this.responseBytes = builder.responseBytes; + this.expectedAccounts = builder.expectedAccounts; + this.expectedFirstAccount = builder.expectedFirstAccount; + this.expectedLastAccount = builder.expectedLastAccount; + } + + public static class Builder { + private Hash rootHash = null; + private Bytes32 startHash = Bytes32.ZERO; + private Bytes32 limitHash = Hash.LAST; + private int responseBytes = Integer.MAX_VALUE; + private int expectedAccounts = 0; + private Bytes32 expectedFirstAccount = null; + private Bytes32 expectedLastAccount = null; + + public Builder rootHash(final Hash rootHashHex) { + this.rootHash = rootHashHex; + return this; + } + + public Builder startHash(final Bytes32 startHashHex) { + this.startHash = startHashHex; + return this; + } + + public Builder limitHash(final Bytes32 limitHashHex) { + this.limitHash = limitHashHex; + return this; + } + + public Builder responseBytes(final int responseBytes) { + this.responseBytes = responseBytes; + return this; + } + + public Builder expectedAccounts(final int expectedAccounts) { + this.expectedAccounts = expectedAccounts; + return this; + } + + public Builder expectedFirstAccount(final Bytes32 expectedFirstAccount) { + this.expectedFirstAccount = expectedFirstAccount; + return this; + } + + public Builder expectedLastAccount(final Bytes32 expectedLastAccount) { + this.expectedLastAccount = expectedLastAccount; + return this; + } + + public AccountRangeRequestParams build() { + return new AccountRangeRequestParams(this); + } + } + + // Getters for each field + public Hash getRootHash() { + return rootHash; + } + + public Bytes32 getStartHash() { + return startHash; + } + + public Bytes32 getLimitHash() { + return limitHash; + } + + public int getResponseBytes() { + return responseBytes; + } + + public int getExpectedAccounts() { + return expectedAccounts; + } + + public Bytes32 getExpectedFirstAccount() { + return expectedFirstAccount; + } + + public Bytes32 getExpectedLastAccount() { + return expectedLastAccount; + } + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerTest.java index a41da1ef6b8..e168b7e2fe3 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/snap/SnapServerTest.java @@ -188,7 +188,7 @@ public void assertEmptyRangeLeftProofOfExclusionAndNextAccount() { public void assertAccountLimitRangeResponse() { // assert we limit the range response according to size final int acctCount = 2000; - final long acctRLPSize = 105; + final long acctRLPSize = 37; List randomLoad = IntStream.range(1, 4096).boxed().collect(Collectors.toList()); Collections.shuffle(randomLoad); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessageTest.java index bd715b64597..c7e5bf74c42 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessageTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/snap/AccountRangeMessageTest.java @@ -14,11 +14,15 @@ */ package org.hyperledger.besu.ethereum.eth.messages.snap; +import static org.assertj.core.api.Assertions.assertThat; + import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.datatypes.Wei; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.rlp.RLP; +import org.hyperledger.besu.ethereum.rlp.RLPInput; import org.hyperledger.besu.ethereum.worldstate.StateTrieAccountValue; import java.util.ArrayList; @@ -28,7 +32,6 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.bytes.Bytes32; -import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; public final class AccountRangeMessageTest { @@ -51,7 +54,81 @@ public void roundTripTest() { // check match originals. final AccountRangeMessage.AccountRangeData range = message.accountData(false); - Assertions.assertThat(range.accounts()).isEqualTo(keys); - Assertions.assertThat(range.proofs()).isEqualTo(proofs); + assertThat(range.accounts()).isEqualTo(keys); + assertThat(range.proofs()).isEqualTo(proofs); + } + + @Test + public void toSlimAccountTest() { + // Initialize nonce and balance + long nonce = 1L; + Wei balance = Wei.of(2L); + + // Create a StateTrieAccountValue with the given nonce and balance + final StateTrieAccountValue accountValue = + new StateTrieAccountValue(nonce, balance, Hash.EMPTY_TRIE_HASH, Hash.EMPTY); + + // Encode the account value to RLP + final BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); + accountValue.writeTo(rlpOut); + + // Convert the encoded account value to a slim account representation + Bytes slimAccount = AccountRangeMessage.toSlimAccount(RLP.input(rlpOut.encoded())); + + // Read the slim account RLP input + RLPInput in = RLP.input(slimAccount); + in.enterList(); + + // Verify the nonce and balance + final long expectedNonce = in.readLongScalar(); + final Wei expectedWei = Wei.of(in.readUInt256Scalar()); + assertThat(expectedNonce).isEqualTo(nonce); + assertThat(expectedWei).isEqualTo(balance); + + // Check that the storageRoot is empty + assertThat(in.nextIsNull()).isTrue(); + in.skipNext(); + + // Check that the codeHash is empty + assertThat(in.nextIsNull()).isTrue(); + in.skipNext(); + + // Exit the list + in.leaveList(); + } + + @Test + public void toFullAccountTest() { + // Initialize nonce and balance + long nonce = 1L; + Wei balance = Wei.of(2L); + + // Create a StateTrieAccountValue with the given nonce and balance + final StateTrieAccountValue accountValue = + new StateTrieAccountValue(nonce, balance, Hash.EMPTY_TRIE_HASH, Hash.EMPTY); + + // Encode the account value to RLP + final BytesValueRLPOutput rlpOut = new BytesValueRLPOutput(); + accountValue.writeTo(rlpOut); + + // Convert the encoded account value to a full account representation + Bytes fullAccount = AccountRangeMessage.toFullAccount(RLP.input(rlpOut.encoded())); + + // Read the full account RLP input + RLPInput in = RLP.input(fullAccount); + in.enterList(); + + // Verify the nonce and balance + final long expectedNonce = in.readLongScalar(); + final Wei expectedWei = Wei.of(in.readUInt256Scalar()); + assertThat(expectedNonce).isEqualTo(nonce); + assertThat(expectedWei).isEqualTo(balance); + + // Verify the storageRoot and codeHash + assertThat(in.readBytes32()).isEqualTo(Hash.EMPTY_TRIE_HASH); + assertThat(in.readBytes32()).isEqualTo(Hash.EMPTY); + + // Exit the list + in.leaveList(); } } diff --git a/testutil/src/main/java/org/hyperledger/besu/testutil/BlockTestUtil.java b/testutil/src/main/java/org/hyperledger/besu/testutil/BlockTestUtil.java index 9f31283bfcf..4c45d3cf740 100644 --- a/testutil/src/main/java/org/hyperledger/besu/testutil/BlockTestUtil.java +++ b/testutil/src/main/java/org/hyperledger/besu/testutil/BlockTestUtil.java @@ -52,6 +52,8 @@ private BlockTestUtil() { Suppliers.memoize(BlockTestUtil::supplyUpgradedForkResources); private static final Supplier testRpcCompactChainSupplier = Suppliers.memoize(BlockTestUtil::supplyTestRpcCompactResources); + private static final Supplier snapTestChainSupplier = + Suppliers.memoize(BlockTestUtil::supplySnapTestChainResources); /** * Gets test blockchain url. @@ -156,6 +158,15 @@ public static ChainResources getEthRefTestResources() { return testRpcCompactChainSupplier.get(); } + /** + * Gets test chain resources for Snap tests. + * + * @return the test chain resources + */ + public static ChainResources getSnapTestChainResources() { + return snapTestChainSupplier.get(); + } + private static ChainResources supplyTestChainResources() { final URL genesisURL = ensureFileUrl(BlockTestUtil.class.getClassLoader().getResource("testGenesis.json")); @@ -164,6 +175,15 @@ private static ChainResources supplyTestChainResources() { return new ChainResources(genesisURL, blocksURL); } + private static ChainResources supplySnapTestChainResources() { + final URL genesisURL = + ensureFileUrl(BlockTestUtil.class.getClassLoader().getResource("snap/snapGenesis.json")); + final URL blocksURL = + ensureFileUrl( + BlockTestUtil.class.getClassLoader().getResource("snap/testBlockchain.blocks")); + return new ChainResources(genesisURL, blocksURL); + } + private static ChainResources supplyHiveTestChainResources() { final URL genesisURL = ensureFileUrl(BlockTestUtil.class.getClassLoader().getResource("hive/testGenesis.json")); diff --git a/testutil/src/main/resources/snap/snapGenesis.json b/testutil/src/main/resources/snap/snapGenesis.json new file mode 100644 index 00000000000..20979e7f747 --- /dev/null +++ b/testutil/src/main/resources/snap/snapGenesis.json @@ -0,0 +1,111 @@ +{ + "config": { + "ethash": {}, + "chainID": 3503995874084926, + "homesteadBlock": 0, + "eip150Block": 6, + "eip155Block": 12, + "eip158Block": 12, + "byzantiumBlock": 18, + "constantinopleBlock": 24, + "constantinopleFixBlock": 30, + "istanbulBlock": 36, + "muirGlacierBlock": 42, + "berlinBlock": 48, + "londonBlock": 54, + "arrowGlacierBlock": 60, + "grayGlacierBlock": 66, + "mergeNetsplitBlock": 72, + "terminalTotalDifficulty": 9454784, + "shanghaiTime": 780, + "cancunTime": 840 + }, + "nonce": "0x0", + "timestamp": "0x0", + "extraData": "0x68697665636861696e", + "gasLimit": "0x23f3e20", + "difficulty": "0x20000", + "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "coinbase": "0x0000000000000000000000000000000000000000", + "alloc": { + "000f3df6d732807ef1319fb7b8bb8522d0beac02": { + "code": "0x3373fffffffffffffffffffffffffffffffffffffffe14604d57602036146024575f5ffd5b5f35801560495762001fff810690815414603c575f5ffd5b62001fff01545f5260205ff35b5f5ffd5b62001fff42064281555f359062001fff015500", + "balance": "0x2a" + }, + "0c2c51a0990aee1d73c1228de158688341557508": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "14e46043e63d0e3cdcf2530519f4cfaf35058cb2": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "16c57edf7fa9d9525378b0b81bf8a3ced0620c1c": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "1f4924b14f34e24159387c0a4cdbaa32f3ddb0cf": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "1f5bde34b4afc686f136c7a3cb6ec376f7357759": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "2d389075be5be9f2246ad654ce152cf05990b209": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "3ae75c08b4c907eb63a8960c45b86e1e9ab6123c": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "4340ee1b812acb40a1eb561c019c327b243b92df": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "4a0f1452281bcec5bd90c3dce6162a5995bfe9df": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "4dde844b71bcdf95512fb4dc94e84fb67b512ed8": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "5f552da00dfb4d3749d9e62dcee3c918855a86a0": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "654aa64f5fbefb84c270ec74211b81ca8c44a72e": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "717f8aa2b982bee0e29f573d31df288663e1ce16": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "7435ed30a8b4aeb0877cef0c6e8cffe834eb865f": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "83c7e323d189f18725ac510004fdc2941f8c4a78": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "84e75c28348fb86acea1a93a39426d7d60f4cc46": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "8bebc8ba651aee624937e7d897853ac30c95a067": { + "storage": { + "0x0000000000000000000000000000000000000000000000000000000000000001": "0x0000000000000000000000000000000000000000000000000000000000000001", + "0x0000000000000000000000000000000000000000000000000000000000000002": "0x0000000000000000000000000000000000000000000000000000000000000002", + "0x0000000000000000000000000000000000000000000000000000000000000003": "0x0000000000000000000000000000000000000000000000000000000000000003" + }, + "balance": "0x1", + "nonce": "0x1" + }, + "c7b99a164efd027a93f147376cc7da7c67c6bbe0": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "d803681e487e6ac18053afc5a6cd813c86ec3e4d": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "e7d13f7aa2a838d24c59b40186a0aca1e21cffcc": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + }, + "eda8645ba6948855e3b3cd596bbb07596d59c603": { + "balance": "0xc097ce7bc90715b34b9f1000000000" + } + }, + "number": "0x0", + "gasUsed": "0x0", + "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000", + "baseFeePerGas": null, + "excessBlobGas": null, + "blobGasUsed": null +} \ No newline at end of file diff --git a/testutil/src/main/resources/snap/testBlockchain.blocks b/testutil/src/main/resources/snap/testBlockchain.blocks new file mode 100644 index 0000000000000000000000000000000000000000..0df8d2f63ad54f13682181181b114e78f0e4229f GIT binary patch literal 342468 zcmeF4c|278`~S@#B5TMNW68c{7cr>p+4m*;maK`aV~s+#qEwEulx#^t_R79wNo2_u zBH1FO{LTzh-QCN4xF7dFzpwXw&v|dxIWx`ma-HjW-e+dkpwKnQHvkJm0S>pQO%2tX z`aBGKVprVKP1{9&k}mP``GfOma^K+H2?~jSbpYVddi>?Y{)nt=H`p!Dr4%&qa<~y2 zX2(@er*R97VSm#C^rjdO(Y5GiZibwkh_7CCHdY*_yWx~u4YA~ZGGfs% ze`+iQiiL#;g~~|75Qs>7hYL1V_7)D#0H*gK0&P%Ty#|d~gM0ywiwqY>wszvd40&&= z5y7H|E%?fdoF&L)H5mMLUmMHqQUs$j*6z);gD>LB7uA}K8>KHZ&S)VnN=_I^2YB!N(3PgXcqzEe|P`tmDNZDBm{zsk3f`2nG=a) zBg~1+u_Vl~mdvr_gS5qoNf9MQ=(ZGzsVR0M#r)M= z2q;BjYEX*ie~_Z6Ih4YjS>Mge-No6C*UG}l$=c$)g@=QWjX9Qgka^GSX?|q}K`8eA1hRJ~|hP}MWE771_FCyg{@}y70 zhODtwhi%qRt-NoI;##6`)3d4wegP(8tFboSUCK3m?A<&0cbgJ?!!$KRRi|wQv$Wr} z9Qy*fDkpqG@-qPqkY61UMnH>)M@>_a?p5~vCPl*3m(EOJE}RlV0wFUMadO=TdwM^3_Fpl->MH3l&iWc0+A<= zloggaOv@Z@Yz{Zaf|VptZF4VVYw9va4ARKSb09;JFmk2A+_&yyKKIKz5oJ%--{-E>oOMVs{∨M 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�H&F!^bdyob?nq3cZroaYEZ$T@>N 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

4STe(;*%O0HVBDiN@v}+fy;}fnSs&TAt2I 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

F zI=f(W4TL2%1rW7T$Mic?Cn{C6rc&OYGhozz#sY9p%GaKri5}GzoP^Q}%StJlH$}%A z%yZ%{3L~GCJQ>}Psje298Klx;kv6X6X)tlbcpDi|zJq25x~|{3Q=HNKx^6cpUeDT} zaU!GGHuqCO(GXx&q}8<;v6?4utD1*>&)bH1@KS2pn~qKa%YIKW^- z*01n*3-u_KKMku)>vRxlw;|h(3W62!;4Sb|N87nHgY*yQz_+YKLp8*I$x8Xdvr?u4 z^7KKpn<91fXF+Jf7VF-svq7+|RQQRwcc``cIHBRH9K^&X%1Htt_^#_7r4}!^6`gOo z2mp8RM7yg5A7@8_v~bT~A18XFALDTneg5=@4v^(UR}6bfD$J%-CH(A`Ep=29JSM}H zTsLViSuL2WqR9!V%w^JR7-VN3ky`fE?qVwQk;su)bpiheY`5-ybr#IQW1g8b9Uw`{ zr#pS9EFC&v|5L(G*YW>W1w!pSj9Q3IY5o?onKjcH?ytwpMYEanDX$vRXJLyzC(+b9 zZ%N!h$1%5z4za1bB%HO1-HNT^y^^`DHkdbqLu-~RHxR_aio4k?=N&bmHl>IfY+RoD z+=gNCZjE2yn@;4K=_~nLUHdjQM%mLuFlrq6C~#4`&5!uX>L#y}exje#R3Ao#yb40w z5!sT(L&_rIVLA1>I4tT!JgAx$tKmtb6Mv?N2Tinfo##*?8W@(?Q^HL;=B zAu=RiD=G>SY02Lm;l7-__Z`0{|IwyCc^JxC#D6XRU|~zVQirE(iy?n`B0JiYW*cp= zi#RVAP-!cdU+o=yK^{EJ8D0n`Sd+!OoWibv_GH}U>G`1%>I0kN)XhjY_6y^Sp~>%! zYttDQ&m|AkOe7vH$>~$iEatzt>^MI5@fQ?RzCK z8h8G>v?|lute{4M_EvhzYpP(R6vdMh@+d^s@F*ojFVZc_n=+7w<$1i5$v+5Nbk1vTH|4}{h?862{F*GZ7ylOaE1 zeJ%!=kYf^D`dBv8kSU`hilh3DUo)X$C(W~$TZCeJ9-|TVSAJMlDkO;&U6c=8GZ=Pf zD_g-K<+@4Q6j7j0JELBCnH$v%-w|n@l0GLBovbfSbyHf6CD_Nl`c0BzQ!$Cwmo2q3 zvE;XdftemYB#r7?%qBa3o4J-0#a#8Fkyajl&cg1SepP4HB#yIEzrguR1BBLPc)W$$ zl-HkzbyczFM&q|@91B8@cRAbxCLWf|OAZL(Uc|Hn>r92*F@|TQ0%2Jx!i@U}1dg7p zIZc8Txg0_qtFlEu-YfMutI}qIwGvrUTz@3N$wN4K>`UIemV4F)!>Hvv*)?5&!f=YO kW+?&l+=YeEH-oPpt`9ttw!?7i1`SlV774ugTwebF01?>9A^-pY literal 0 HcmV?d00001 From 0d9fa16a1f5ec11d22e4a03942a563940f7c35f4 Mon Sep 17 00:00:00 2001 From: Bhanu Pulluri <59369753+pullurib@users.noreply.github.com> Date: Wed, 18 Sep 2024 05:35:00 -0400 Subject: [PATCH 03/37] [#4292] Fix mounted data path directory permissions for besu user (#7575) * Fix mounted data path directory permissions for besu user Signed-off-by: Bhanu Pulluri * Add besu CLI option to output dirs needing permission update Signed-off-by: Bhanu Pulluri * run spotless apply to handle PR test failure Signed-off-by: Bhanu Pulluri * Remove newly added --print-paths-and-exit option from config file test This option doesn't have a corresponding config file entry as it's a standalone option to be used with docker containers Signed-off-by: Bhanu Pulluri * Add optional user argument to --print-paths-and-exit and fix directory permissions Signed-off-by: Bhanu Pulluri * Correct build.gradle changes, remove a duplicate line and extra whitespaces Signed-off-by: Bhanu Pulluri * Fix checking for user in path's group membership Signed-off-by: Bhanu Pulluri * Add platform check to restrict --print-paths-and-exit option usage to Linux and Mac Signed-off-by: Bhanu Pulluri * Apply suggestions from code review Co-authored-by: Fabio Di Fabio Signed-off-by: Bhanu Pulluri <59369753+pullurib@users.noreply.github.com> --------- Signed-off-by: Bhanu Pulluri Signed-off-by: Bhanu Pulluri <59369753+pullurib@users.noreply.github.com> Co-authored-by: Bhanu Pulluri Co-authored-by: Fabio Di Fabio --- CHANGELOG.md | 2 + .../org/hyperledger/besu/cli/BesuCommand.java | 137 ++++++++++++++++++ besu/src/main/scripts/besu-entry.sh | 49 +++++++ .../hyperledger/besu/cli/BesuCommandTest.java | 1 + build.gradle | 1 + docker/Dockerfile | 10 +- docker/test.sh | 8 + docker/tests/02/goss.yaml | 10 ++ docker/tests/dgoss | 2 +- 9 files changed, 217 insertions(+), 3 deletions(-) create mode 100755 besu/src/main/scripts/besu-entry.sh create mode 100644 docker/tests/02/goss.yaml diff --git a/CHANGELOG.md b/CHANGELOG.md index c2418386370..d30ad08c98e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,8 +11,10 @@ - Remove privacy test classes support [#7569](https://github.com/hyperledger/besu/pull/7569) ### Bug fixes +- Fix mounted data path directory permissions for besu user [#7575](https://github.com/hyperledger/besu/pull/7575) - Fix for `debug_traceCall` to handle transactions without specified gas price. [#7510](https://github.com/hyperledger/besu/pull/7510) + ## 24.9.1 ### Upcoming Breaking Changes diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index ecfc0eaadb2..01ee6172f60 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -16,6 +16,7 @@ import static com.google.common.base.Preconditions.checkNotNull; import static com.google.common.base.Preconditions.checkState; +import static java.nio.charset.StandardCharsets.UTF_8; import static java.util.Arrays.asList; import static java.util.Collections.singletonList; import static org.hyperledger.besu.cli.DefaultCommandValues.getDefaultBesuDataPath; @@ -203,16 +204,23 @@ import org.hyperledger.besu.util.number.Percentage; import org.hyperledger.besu.util.number.PositiveNumber; +import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.InputStream; +import java.io.InputStreamReader; import java.math.BigInteger; import java.net.InetAddress; import java.net.SocketException; import java.net.URI; import java.net.URL; import java.net.UnknownHostException; +import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.attribute.GroupPrincipal; +import java.nio.file.attribute.PosixFileAttributes; +import java.nio.file.attribute.PosixFilePermission; +import java.nio.file.attribute.UserPrincipal; import java.time.Clock; import java.util.ArrayList; import java.util.Arrays; @@ -232,6 +240,7 @@ import java.util.stream.Collectors; import com.google.common.annotations.VisibleForTesting; +import com.google.common.base.Splitter; import com.google.common.base.Strings; import com.google.common.base.Suppliers; import com.google.common.collect.ImmutableMap; @@ -243,6 +252,8 @@ import org.apache.tuweni.bytes.Bytes; import org.apache.tuweni.units.bigints.UInt256; import org.slf4j.Logger; +import oshi.PlatformEnum; +import oshi.SystemInfo; import picocli.AutoComplete; import picocli.CommandLine; import picocli.CommandLine.Command; @@ -382,6 +393,28 @@ public class BesuCommand implements DefaultCommandValues, Runnable { arity = "1") private final Optional identityString = Optional.empty(); + private Boolean printPathsAndExit = Boolean.FALSE; + private String besuUserName = "besu"; + + @Option( + names = "--print-paths-and-exit", + paramLabel = "", + description = "Print the configured paths and exit without starting the node.", + arity = "0..1") + void setUserName(final String userName) { + PlatformEnum currentPlatform = SystemInfo.getCurrentPlatform(); + // Only allow on Linux and macOS + if (currentPlatform == PlatformEnum.LINUX || currentPlatform == PlatformEnum.MACOS) { + if (userName != null) { + besuUserName = userName; + } + printPathsAndExit = Boolean.TRUE; + } else { + throw new UnsupportedOperationException( + "--print-paths-and-exit is only supported on Linux and macOS."); + } + } + // P2P Discovery Option Group @CommandLine.ArgGroup(validate = false, heading = "@|bold P2P Discovery Options|@%n") P2PDiscoveryOptionGroup p2PDiscoveryOptionGroup = new P2PDiscoveryOptionGroup(); @@ -1093,6 +1126,12 @@ public void run() { try { configureLogging(true); + if (printPathsAndExit) { + // Print configured paths requiring read/write permissions to be adjusted + checkPermissionsAndPrintPaths(besuUserName); + System.exit(0); // Exit before any services are started + } + // set merge config on the basis of genesis config setMergeConfigOptions(); @@ -1138,6 +1177,104 @@ public void run() { } } + private void checkPermissionsAndPrintPaths(final String userName) { + // Check permissions for the data path + checkPermissions(dataDir(), userName, false); + + // Check permissions for genesis file + try { + if (genesisFile != null) { + checkPermissions(genesisFile.toPath(), userName, true); + } + } catch (Exception e) { + commandLine + .getOut() + .println("Error: Failed checking genesis file: Reason: " + e.getMessage()); + } + } + + // Helper method to check permissions on a given path + private void checkPermissions(final Path path, final String besuUser, final boolean readOnly) { + try { + // Get the permissions of the file + // check if besu user is the owner - get owner permissions if yes + // else, check if besu user and owner are in the same group - if yes, check the group + // permission + // otherwise check permissions for others + + // Get the owner of the file or directory + UserPrincipal owner = Files.getOwner(path); + boolean hasReadPermission, hasWritePermission; + + // Get file permissions + Set permissions = Files.getPosixFilePermissions(path); + + // Check if besu is the owner + if (owner.getName().equals(besuUser)) { + // Owner permissions + hasReadPermission = permissions.contains(PosixFilePermission.OWNER_READ); + hasWritePermission = permissions.contains(PosixFilePermission.OWNER_WRITE); + } else { + // Get the group of the file + // Get POSIX file attributes and then group + PosixFileAttributes attrs = Files.readAttributes(path, PosixFileAttributes.class); + GroupPrincipal group = attrs.group(); + + // Check if besu user belongs to this group + boolean isMember = isGroupMember(besuUserName, group); + + if (isMember) { + // Group's permissions + hasReadPermission = permissions.contains(PosixFilePermission.GROUP_READ); + hasWritePermission = permissions.contains(PosixFilePermission.GROUP_WRITE); + } else { + // Others' permissions + hasReadPermission = permissions.contains(PosixFilePermission.OTHERS_READ); + hasWritePermission = permissions.contains(PosixFilePermission.OTHERS_WRITE); + } + } + + if (!hasReadPermission || (!readOnly && !hasWritePermission)) { + String accessType = readOnly ? "READ" : "READ_WRITE"; + commandLine.getOut().println("PERMISSION_CHECK_PATH:" + path + ":" + accessType); + } + } catch (Exception e) { + // Do nothing upon catching an error + commandLine + .getOut() + .println( + "Error: Failed to check permissions for path: '" + + path + + "'. Reason: " + + e.getMessage()); + } + } + + private static boolean isGroupMember(final String userName, final GroupPrincipal group) + throws IOException { + // Get the groups of the user by executing 'id -Gn username' + Process process = Runtime.getRuntime().exec(new String[] {"id", "-Gn", userName}); + BufferedReader reader = + new BufferedReader(new InputStreamReader(process.getInputStream(), UTF_8)); + + // Read the output of the command + String line = reader.readLine(); + boolean isMember = false; + if (line != null) { + // Split the groups + Iterable userGroups = Splitter.on(" ").split(line); + // Check if any of the user's groups match the file's group + + for (String grp : userGroups) { + if (grp.equals(group.getName())) { + isMember = true; + break; + } + } + } + return isMember; + } + @VisibleForTesting void setBesuConfiguration(final BesuConfigurationImpl pluginCommonConfiguration) { this.pluginCommonConfiguration = pluginCommonConfiguration; diff --git a/besu/src/main/scripts/besu-entry.sh b/besu/src/main/scripts/besu-entry.sh new file mode 100755 index 00000000000..ed3687b2291 --- /dev/null +++ b/besu/src/main/scripts/besu-entry.sh @@ -0,0 +1,49 @@ +#!/bin/bash +## +## Copyright contributors to Hyperledger Besu. +## +## Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with +## the License. You may obtain a copy of the License at +## +## http://www.apache.org/licenses/LICENSE-2.0 +## +## Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on +## an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the +## specific language governing permissions and limitations under the License. +## +## SPDX-License-Identifier: Apache-2.0 +## + +# Run Besu first to get paths needing permission adjustment +output=$(/opt/besu/bin/besu --print-paths-and-exit $BESU_USER_NAME "$@") + +# Parse the output to find the paths and their required access types +echo "$output" | while IFS=: read -r prefix path accessType; do + if [[ "$prefix" == "PERMISSION_CHECK_PATH" ]]; then + # Change ownership to besu user and group + chown -R $BESU_USER_NAME:$BESU_USER_NAME $path + + # Ensure read/write permissions for besu user + + echo "Setting permissions for: $path with access: $accessType" + + if [[ "$accessType" == "READ" ]]; then + # Set read-only permissions for besu user + # Add execute for directories to allow access + find $path -type d -exec chmod u+rx {} \; + find $path -type f -exec chmod u+r {} \; + elif [[ "$accessType" == "READ_WRITE" ]]; then + # Set read/write permissions for besu user + # Add execute for directories to allow access + find $path -type d -exec chmod u+rwx {} \; + find $path -type f -exec chmod u+rw {} \; + fi + fi +done + +# Finally, run Besu with the actual arguments passed to the container +# Construct the command as a single string +COMMAND="/opt/besu/bin/besu $@" + +# Switch to the besu user and execute the command +exec su -s /bin/bash $BESU_USER_NAME -c "$COMMAND" diff --git a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java index e6e0e859517..99b45ada1b8 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/BesuCommandTest.java @@ -390,6 +390,7 @@ public void tomlThatConfiguresEverythingExceptPermissioningToml() throws IOExcep options.remove(spec.optionsMap().get("--config-file")); options.remove(spec.optionsMap().get("--help")); options.remove(spec.optionsMap().get("--version")); + options.remove(spec.optionsMap().get("--print-paths-and-exit")); for (final String tomlKey : tomlResult.keySet()) { final CommandLine.Model.OptionSpec optionSpec = spec.optionsMap().get("--" + tomlKey); diff --git a/build.gradle b/build.gradle index 792355417f4..ee4392c9fc1 100644 --- a/build.gradle +++ b/build.gradle @@ -1097,6 +1097,7 @@ distributions { from("build/reports/license/license-dependency.html") { into "." } from("./docs/GettingStartedBinaries.md") { into "." } from("./docs/DocsArchive0.8.0.html") { into "." } + from("./besu/src/main/scripts/besu-entry.sh") { into "./bin/" } from(autocomplete) { into "." } } } diff --git a/docker/Dockerfile b/docker/Dockerfile index c16345a82bd..a45a3ac73d8 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -18,7 +18,8 @@ RUN apt-get update $NO_PROXY_CACHE && \ chown besu:besu /opt/besu && \ chmod 0755 /opt/besu -USER besu +ARG BESU_USER=besu +USER ${BESU_USER} WORKDIR /opt/besu COPY --chown=besu:besu besu /opt/besu/ @@ -43,7 +44,12 @@ ENV OTEL_RESOURCE_ATTRIBUTES="service.name=besu,service.version=$VERSION" ENV OLDPATH="${PATH}" ENV PATH="/opt/besu/bin:${OLDPATH}" -ENTRYPOINT ["besu"] +USER root +RUN chmod +x /opt/besu/bin/besu-entry.sh + +ENV BESU_USER_NAME=${BESU_USER} + +ENTRYPOINT ["besu-entry.sh"] HEALTHCHECK --start-period=5s --interval=5s --timeout=1s --retries=10 CMD bash -c "[ -f /tmp/pid ]" # Build-time metadata as defined at http://label-schema.org diff --git a/docker/test.sh b/docker/test.sh index 6e08db13b55..e230f79ad1c 100755 --- a/docker/test.sh +++ b/docker/test.sh @@ -41,4 +41,12 @@ bash $TEST_PATH/dgoss run --sysctl net.ipv6.conf.all.disable_ipv6=1 $DOCKER_IMAG --graphql-http-enabled \ > ./reports/01.xml || i=`expr $i + 1` +if [[ $i != 0 ]]; then exit $i; fi + +# Test for directory permissions +GOSS_FILES_PATH=$TEST_PATH/02 \ +bash $TEST_PATH/dgoss run --sysctl net.ipv6.conf.all.disable_ipv6=1 -v besu-data:/var/lib/besu $DOCKER_IMAGE --data-path=/var/lib/besu \ +--network=dev \ +> ./reports/02.xml || i=`expr $i + 1` + exit $i diff --git a/docker/tests/02/goss.yaml b/docker/tests/02/goss.yaml new file mode 100644 index 00000000000..d266cafa399 --- /dev/null +++ b/docker/tests/02/goss.yaml @@ -0,0 +1,10 @@ +--- +# runtime docker tests +file: + /var/lib/besu: + exists: true + owner: besu + mode: "0755" +process: + java: + running: true diff --git a/docker/tests/dgoss b/docker/tests/dgoss index 59bbc4683e1..170270eff35 100755 --- a/docker/tests/dgoss +++ b/docker/tests/dgoss @@ -76,7 +76,7 @@ GOSS_PATH="${GOSS_PATH:-$(which goss 2> /dev/null || true)}" [[ $GOSS_PATH ]] || { error "Couldn't find goss installation, please set GOSS_PATH to it"; } [[ ${GOSS_OPTS+x} ]] || GOSS_OPTS="--color --format documentation" [[ ${GOSS_WAIT_OPTS+x} ]] || GOSS_WAIT_OPTS="-r 30s -s 1s > /dev/null" -GOSS_SLEEP=${GOSS_SLEEP:-0.2} +GOSS_SLEEP=${GOSS_SLEEP:-1.0} case "$1" in run) From 6df4149f2a04555fb36702911b8f6142b843c5b3 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Wed, 18 Sep 2024 15:55:10 +0200 Subject: [PATCH 04/37] Fix logging the evaluation time when a tx is selected for block creation (#7636) Signed-off-by: Fabio Di Fabio --- .../blockcreation/txselection/BlockTransactionSelector.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java index 3d56c1fc07b..e07b43b9904 100644 --- a/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java +++ b/ethereum/blockcreation/src/main/java/org/hyperledger/besu/ethereum/blockcreation/txselection/BlockTransactionSelector.java @@ -401,7 +401,7 @@ private TransactionSelectionResult handleTransactionSelected( LOG.atTrace() .setMessage("Selected {} for block creation, evaluated in {}") .addArgument(transaction::toTraceLog) - .addArgument(evaluationContext.getPendingTransaction()) + .addArgument(evaluationContext.getEvaluationTimer()) .log(); return SELECTED; } From 7d3e376771f99598e3730a0569793c7c1bd58834 Mon Sep 17 00:00:00 2001 From: Justin Florentine Date: Wed, 18 Sep 2024 10:31:03 -0400 Subject: [PATCH 05/37] shift creation of plugin context to BesuCommand for now (#7625) * shift creation of plugin context to BesuCommand for now * mock component will provide a no-op metrics sys --------- Signed-off-by: Justin Florentine --- CHANGELOG.md | 1 + .../dsl/node/ProcessBesuNodeRunner.java | 125 +++++++++--------- .../dsl/node/ThreadBesuNodeRunner.java | 23 ++-- .../main/java/org/hyperledger/besu/Besu.java | 5 +- .../org/hyperledger/besu/cli/BesuCommand.java | 38 ++++-- .../besu/components/BesuCommandModule.java | 12 +- .../besu/components/BesuComponent.java | 4 +- .../components/BesuPluginContextModule.java | 16 --- .../controller/BesuControllerBuilder.java | 2 +- .../besu/cli/CommandTestAbstract.java | 7 + .../components/MockBesuCommandModule.java | 16 +++ .../BonsaiCachedMerkleTrieLoaderModule.java | 5 +- .../besu/metrics/MetricsSystemModule.java | 6 - .../opentelemetry/OpenTelemetrySystem.java | 2 + 14 files changed, 148 insertions(+), 114 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d30ad08c98e..ff4d7cfbf94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,7 @@ ### Bug fixes - Fix mounted data path directory permissions for besu user [#7575](https://github.com/hyperledger/besu/pull/7575) - Fix for `debug_traceCall` to handle transactions without specified gas price. [#7510](https://github.com/hyperledger/besu/pull/7510) +- Corrects a regression where custom plugin services are not initialized correctly. [#7625](https://github.com/hyperledger/besu/pull/7625) ## 24.9.1 diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java index c6696b15a22..6e00701ef2b 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ProcessBesuNodeRunner.java @@ -75,6 +75,70 @@ public void startNode(final BesuNode node) { final Path dataDir = node.homeDirectory(); + final List params = commandlineArgs(node, dataDir); + + LOG.info("Creating besu process with params {}", params); + final ProcessBuilder processBuilder = + new ProcessBuilder(params) + .directory(new File(System.getProperty("user.dir")).getParentFile().getParentFile()) + .redirectErrorStream(true) + .redirectInput(Redirect.INHERIT); + if (!node.getPlugins().isEmpty()) { + processBuilder + .environment() + .put( + "BESU_OPTS", + "-Dbesu.plugins.dir=" + dataDir.resolve("plugins").toAbsolutePath().toString()); + } + // Use non-blocking randomness for acceptance tests + processBuilder + .environment() + .put( + "JAVA_OPTS", + "-Djava.security.properties=" + + "acceptance-tests/tests/build/resources/test/acceptanceTesting.security"); + // add additional environment variables + processBuilder.environment().putAll(node.getEnvironment()); + + try { + int debugPort = Integer.parseInt(System.getenv("BESU_DEBUG_CHILD_PROCESS_PORT")); + LOG.warn("Waiting for debugger to attach to SUSPENDED child process"); + String debugOpts = + " -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:" + debugPort; + String prevJavaOpts = processBuilder.environment().get("JAVA_OPTS"); + if (prevJavaOpts == null) { + processBuilder.environment().put("JAVA_OPTS", debugOpts); + } else { + processBuilder.environment().put("JAVA_OPTS", prevJavaOpts + debugOpts); + } + + } catch (NumberFormatException e) { + LOG.debug( + "Child process may be attached to by exporting BESU_DEBUG_CHILD_PROCESS_PORT= to env"); + } + + try { + checkState( + isNotAliveOrphan(node.getName()), + "A live process with name: %s, already exists. Cannot create another with the same name as it would orphan the first", + node.getName()); + + final Process process = processBuilder.start(); + process.onExit().thenRun(() -> node.setExitCode(process.exitValue())); + outputProcessorExecutor.execute(() -> printOutput(node, process)); + besuProcesses.put(node.getName(), process); + } catch (final IOException e) { + LOG.error("Error starting BesuNode process", e); + } + + if (node.getRunCommand().isEmpty()) { + waitForFile(dataDir, "besu.ports"); + waitForFile(dataDir, "besu.networks"); + } + MDC.remove("node"); + } + + private List commandlineArgs(final BesuNode node, final Path dataDir) { final List params = new ArrayList<>(); params.add("build/install/besu/bin/besu"); @@ -388,66 +452,7 @@ public void startNode(final BesuNode node) { } params.addAll(node.getRunCommand()); - - LOG.info("Creating besu process with params {}", params); - final ProcessBuilder processBuilder = - new ProcessBuilder(params) - .directory(new File(System.getProperty("user.dir")).getParentFile().getParentFile()) - .redirectErrorStream(true) - .redirectInput(Redirect.INHERIT); - if (!node.getPlugins().isEmpty()) { - processBuilder - .environment() - .put( - "BESU_OPTS", - "-Dbesu.plugins.dir=" + dataDir.resolve("plugins").toAbsolutePath().toString()); - } - // Use non-blocking randomness for acceptance tests - processBuilder - .environment() - .put( - "JAVA_OPTS", - "-Djava.security.properties=" - + "acceptance-tests/tests/build/resources/test/acceptanceTesting.security"); - // add additional environment variables - processBuilder.environment().putAll(node.getEnvironment()); - - try { - int debugPort = Integer.parseInt(System.getenv("BESU_DEBUG_CHILD_PROCESS_PORT")); - LOG.warn("Waiting for debugger to attach to SUSPENDED child process"); - String debugOpts = - " -agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=*:" + debugPort; - String prevJavaOpts = processBuilder.environment().get("JAVA_OPTS"); - if (prevJavaOpts == null) { - processBuilder.environment().put("JAVA_OPTS", debugOpts); - } else { - processBuilder.environment().put("JAVA_OPTS", prevJavaOpts + debugOpts); - } - - } catch (NumberFormatException e) { - LOG.debug( - "Child process may be attached to by exporting BESU_DEBUG_CHILD_PROCESS_PORT= to env"); - } - - try { - checkState( - isNotAliveOrphan(node.getName()), - "A live process with name: %s, already exists. Cannot create another with the same name as it would orphan the first", - node.getName()); - - final Process process = processBuilder.start(); - process.onExit().thenRun(() -> node.setExitCode(process.exitValue())); - outputProcessorExecutor.execute(() -> printOutput(node, process)); - besuProcesses.put(node.getName(), process); - } catch (final IOException e) { - LOG.error("Error starting BesuNode process", e); - } - - if (node.getRunCommand().isEmpty()) { - waitForFile(dataDir, "besu.ports"); - waitForFile(dataDir, "besu.networks"); - } - MDC.remove("node"); + return params; } private boolean isNotAliveOrphan(final String name) { diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java index d4ec54045d3..5d78f1460c4 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java @@ -138,7 +138,8 @@ public void startNode(final BesuNode node) { final PermissioningServiceImpl permissioningService = new PermissioningServiceImpl(); GlobalOpenTelemetry.resetForTest(); - final ObservableMetricsSystem metricsSystem = component.getObservableMetricsSystem(); + final ObservableMetricsSystem metricsSystem = + (ObservableMetricsSystem) component.getMetricsSystem(); final List bootnodes = node.getConfiguration().getBootnodes().stream().map(EnodeURLImpl::fromURI).toList(); @@ -280,6 +281,16 @@ public BesuNodeProviderModule(final BesuNode toProvide) { this.toProvide = toProvide; } + @Provides + @Singleton + MetricsConfiguration provideMetricsConfiguration() { + if (toProvide.getMetricsConfiguration() != null) { + return toProvide.getMetricsConfiguration(); + } else { + return MetricsConfiguration.builder().build(); + } + } + @Provides public BesuNode provideBesuNodeRunner() { return toProvide; @@ -410,13 +421,13 @@ public BesuControllerBuilder provideBesuControllerBuilder( public BesuController provideBesuController( final SynchronizerConfiguration synchronizerConfiguration, final BesuControllerBuilder builder, - final ObservableMetricsSystem metricsSystem, + final MetricsSystem metricsSystem, final KeyValueStorageProvider storageProvider, final MiningParameters miningParameters) { builder .synchronizerConfiguration(synchronizerConfiguration) - .metricsSystem(metricsSystem) + .metricsSystem((ObservableMetricsSystem) metricsSystem) .dataStorageConfiguration(DataStorageConfiguration.DEFAULT_FOREST_CONFIG) .ethProtocolConfiguration(EthProtocolConfiguration.defaultConfig()) .clock(Clock.systemUTC()) @@ -562,12 +573,6 @@ BesuCommand provideBesuCommand(final BesuPluginContextImpl pluginContext) { return besuCommand; } - @Provides - @Singleton - MetricsConfiguration provideMetricsConfiguration() { - return MetricsConfiguration.builder().build(); - } - @Provides @Named("besuCommandLogger") @Singleton diff --git a/besu/src/main/java/org/hyperledger/besu/Besu.java b/besu/src/main/java/org/hyperledger/besu/Besu.java index 8d40a10eb49..3ba0af0a9b1 100644 --- a/besu/src/main/java/org/hyperledger/besu/Besu.java +++ b/besu/src/main/java/org/hyperledger/besu/Besu.java @@ -16,6 +16,7 @@ import org.hyperledger.besu.cli.BesuCommand; import org.hyperledger.besu.cli.logging.BesuLoggingConfigurationFactory; +import org.hyperledger.besu.components.BesuComponent; import org.hyperledger.besu.components.DaggerBesuComponent; import io.netty.util.internal.logging.InternalLoggerFactory; @@ -36,13 +37,15 @@ public Besu() {} */ public static void main(final String... args) { setupLogging(); - final BesuCommand besuCommand = DaggerBesuComponent.create().getBesuCommand(); + final BesuComponent besuComponent = DaggerBesuComponent.create(); + final BesuCommand besuCommand = besuComponent.getBesuCommand(); int exitCode = besuCommand.parse( new RunLast(), besuCommand.parameterExceptionHandler(), besuCommand.executionExceptionHandler(), System.in, + besuComponent, args); System.exit(exitCode); diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 01ee6172f60..79879538bd5 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -85,6 +85,7 @@ import org.hyperledger.besu.cli.util.CommandLineUtils; import org.hyperledger.besu.cli.util.ConfigDefaultValueProviderStrategy; import org.hyperledger.besu.cli.util.VersionProvider; +import org.hyperledger.besu.components.BesuComponent; import org.hyperledger.besu.config.CheckpointConfigOptions; import org.hyperledger.besu.config.GenesisConfigFile; import org.hyperledger.besu.config.GenesisConfigOptions; @@ -145,7 +146,6 @@ import org.hyperledger.besu.metrics.BesuMetricCategory; import org.hyperledger.besu.metrics.MetricCategoryRegistryImpl; import org.hyperledger.besu.metrics.MetricsProtocol; -import org.hyperledger.besu.metrics.MetricsSystemFactory; import org.hyperledger.besu.metrics.ObservableMetricsSystem; import org.hyperledger.besu.metrics.StandardMetricCategory; import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; @@ -423,6 +423,7 @@ void setUserName(final String userName) { private final TransactionPoolValidatorServiceImpl transactionValidatorServiceImpl; private final TransactionSimulationServiceImpl transactionSimulationServiceImpl; private final BlockchainServiceImpl blockchainServiceImpl; + private BesuComponent besuComponent; static class P2PDiscoveryOptionGroup { @@ -897,9 +898,6 @@ static class PrivacyOptionGroup { private BesuController besuController; private BesuConfigurationImpl pluginCommonConfiguration; - private final Supplier metricsSystem = - Suppliers.memoize(() -> MetricsSystemFactory.create(metricsConfiguration())); - private Vertx vertx; private EnodeDnsConfiguration enodeDnsConfiguration; private KeyValueStorageProvider keyValueStorageProvider; @@ -1029,6 +1027,7 @@ protected BesuCommand( * @param parameterExceptionHandler Handler for exceptions related to command line parameters. * @param executionExceptionHandler Handler for exceptions during command execution. * @param in The input stream for commands. + * @param besuComponent The Besu component. * @param args The command line arguments. * @return The execution result status code. */ @@ -1037,8 +1036,12 @@ public int parse( final BesuParameterExceptionHandler parameterExceptionHandler, final BesuExecutionExceptionHandler executionExceptionHandler, final InputStream in, + final BesuComponent besuComponent, final String... args) { - + if (besuComponent == null) { + throw new IllegalArgumentException("BesuComponent must be provided"); + } + this.besuComponent = besuComponent; initializeCommandLineSettings(in); // Create the execution strategy chain. @@ -1142,7 +1145,7 @@ public void run() { logger.info("Starting Besu"); // Need to create vertx after cmdline has been parsed, such that metricsSystem is configurable - vertx = createVertx(createVertxOptions(metricsSystem.get())); + vertx = createVertx(createVertxOptions(besuComponent.getMetricsSystem())); validateOptions(); @@ -1527,8 +1530,8 @@ private void validatePrivacyPluginOptions() { } private void setReleaseMetrics() { - metricsSystem - .get() + besuComponent + .getMetricsSystem() .createLabelledGauge( StandardMetricCategory.PROCESS, "release", "Release information", "version") .labels(() -> 1, BesuInfo.version()); @@ -1992,7 +1995,7 @@ public BesuControllerBuilder setupControllerBuilder() { .miningParameters(miningParametersSupplier.get()) .transactionPoolConfiguration(buildTransactionPoolConfiguration()) .nodeKey(new NodeKey(securityModule())) - .metricsSystem(metricsSystem.get()) + .metricsSystem((ObservableMetricsSystem) besuComponent.getMetricsSystem()) .messagePermissioningProviders(permissioningService.getMessagePermissioningProviders()) .privacyParameters(privacyParameters()) .clock(Clock.systemUTC()) @@ -2012,7 +2015,8 @@ public BesuControllerBuilder setupControllerBuilder() { .randomPeerPriority(p2PDiscoveryOptionGroup.randomPeerPriority) .chainPruningConfiguration(unstableChainPruningOptions.toDomainObject()) .cacheLastBlocks(numberOfblocksToCache) - .genesisStateHashCacheEnabled(genesisStateHashCacheEnabled); + .genesisStateHashCacheEnabled(genesisStateHashCacheEnabled) + .besuComponent(besuComponent); } private JsonRpcConfiguration createEngineJsonRpcConfiguration( @@ -2414,7 +2418,6 @@ private Runner synchronize( p2pTLSConfiguration.ifPresent(runnerBuilder::p2pTLSConfiguration); - final ObservableMetricsSystem metricsSystem = this.metricsSystem.get(); final Runner runner = runnerBuilder .vertx(vertx) @@ -2441,7 +2444,7 @@ private Runner synchronize( .pidPath(pidPath) .dataDir(dataDir()) .bannedNodeIds(p2PDiscoveryOptionGroup.bannedNodeIds) - .metricsSystem(metricsSystem) + .metricsSystem((ObservableMetricsSystem) besuComponent.getMetricsSystem()) .permissioningService(permissioningService) .metricsConfiguration(metricsConfiguration) .staticNodes(staticNodes) @@ -2608,7 +2611,7 @@ private File resolveNodePrivateKeyFile(final File nodePrivateKeyFile) { * @return Instance of MetricsSystem */ public MetricsSystem getMetricsSystem() { - return metricsSystem.get(); + return besuComponent.getMetricsSystem(); } private Set loadStaticNodes() throws IOException { @@ -2946,4 +2949,13 @@ && getDataStorageConfiguration().getBonsaiLimitTrieLogsEnabled()) { return builder.build(); } + + /** + * Returns the plugin context. + * + * @return the plugin context. + */ + public BesuPluginContextImpl getBesuPluginContext() { + return besuPluginContext; + } } diff --git a/besu/src/main/java/org/hyperledger/besu/components/BesuCommandModule.java b/besu/src/main/java/org/hyperledger/besu/components/BesuCommandModule.java index 86aa8c75949..59e2d60bf37 100644 --- a/besu/src/main/java/org/hyperledger/besu/components/BesuCommandModule.java +++ b/besu/src/main/java/org/hyperledger/besu/components/BesuCommandModule.java @@ -42,9 +42,7 @@ public BesuCommandModule() {} @Provides @Singleton - BesuCommand provideBesuCommand( - final BesuPluginContextImpl pluginContext, - final @Named("besuCommandLogger") Logger commandLogger) { + BesuCommand provideBesuCommand(final @Named("besuCommandLogger") Logger commandLogger) { final BesuCommand besuCommand = new BesuCommand( RlpBlockImporter::new, @@ -52,7 +50,7 @@ BesuCommand provideBesuCommand( RlpBlockExporter::new, new RunnerBuilder(), new BesuController.Builder(), - pluginContext, + new BesuPluginContextImpl(), System.getenv(), commandLogger); besuCommand.toCommandLine(); @@ -71,4 +69,10 @@ MetricsConfiguration provideMetricsConfiguration(final BesuCommand provideFrom) Logger provideBesuCommandLogger() { return Besu.getFirstLogger(); } + + @Provides + @Singleton + BesuPluginContextImpl provideBesuPluginContextImpl(final BesuCommand provideFrom) { + return provideFrom.getBesuPluginContext(); + } } diff --git a/besu/src/main/java/org/hyperledger/besu/components/BesuComponent.java b/besu/src/main/java/org/hyperledger/besu/components/BesuComponent.java index 9f810a6dc6e..b0d5c3da0f0 100644 --- a/besu/src/main/java/org/hyperledger/besu/components/BesuComponent.java +++ b/besu/src/main/java/org/hyperledger/besu/components/BesuComponent.java @@ -20,7 +20,7 @@ import org.hyperledger.besu.ethereum.trie.diffbased.bonsai.cache.BonsaiCachedMerkleTrieLoader; import org.hyperledger.besu.ethereum.trie.diffbased.bonsai.cache.BonsaiCachedMerkleTrieLoaderModule; import org.hyperledger.besu.metrics.MetricsSystemModule; -import org.hyperledger.besu.metrics.ObservableMetricsSystem; +import org.hyperledger.besu.plugin.services.MetricsSystem; import org.hyperledger.besu.services.BesuPluginContextImpl; import javax.inject.Named; @@ -60,7 +60,7 @@ public interface BesuComponent { * * @return ObservableMetricsSystem */ - ObservableMetricsSystem getObservableMetricsSystem(); + MetricsSystem getMetricsSystem(); /** * a Logger specifically configured to provide configuration feedback to users. diff --git a/besu/src/main/java/org/hyperledger/besu/components/BesuPluginContextModule.java b/besu/src/main/java/org/hyperledger/besu/components/BesuPluginContextModule.java index d62ab702244..702b63af190 100644 --- a/besu/src/main/java/org/hyperledger/besu/components/BesuPluginContextModule.java +++ b/besu/src/main/java/org/hyperledger/besu/components/BesuPluginContextModule.java @@ -14,9 +14,7 @@ */ package org.hyperledger.besu.components; -import org.hyperledger.besu.plugin.services.BesuConfiguration; import org.hyperledger.besu.services.BesuConfigurationImpl; -import org.hyperledger.besu.services.BesuPluginContextImpl; import javax.inject.Singleton; @@ -35,18 +33,4 @@ public BesuPluginContextModule() {} BesuConfigurationImpl provideBesuPluginConfig() { return new BesuConfigurationImpl(); } - - /** - * Creates a BesuPluginContextImpl, used for plugin service discovery. - * - * @param pluginConfig the BesuConfigurationImpl - * @return the BesuPluginContext - */ - @Provides - @Singleton - public BesuPluginContextImpl provideBesuPluginContext(final BesuConfigurationImpl pluginConfig) { - BesuPluginContextImpl retval = new BesuPluginContextImpl(); - retval.addService(BesuConfiguration.class, pluginConfig); - return retval; - } } 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 6bb3fb117c1..bded2a38ac7 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/BesuControllerBuilder.java @@ -552,7 +552,7 @@ public BesuController build() { checkNotNull(evmConfiguration, "Missing evm config"); checkNotNull(networkingConfiguration, "Missing network configuration"); checkNotNull(dataStorageConfiguration, "Missing data storage configuration"); - + checkNotNull(besuComponent, "Must supply a BesuComponent"); prepForBuild(); final ProtocolSchedule protocolSchedule = createProtocolSchedule(); diff --git a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java index 7d11a4a8e99..5b1274389d6 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/CommandTestAbstract.java @@ -42,6 +42,7 @@ import org.hyperledger.besu.cli.options.unstable.MetricsCLIOptions; import org.hyperledger.besu.cli.options.unstable.NetworkingOptions; import org.hyperledger.besu.cli.options.unstable.SynchronizerOptions; +import org.hyperledger.besu.components.BesuComponent; import org.hyperledger.besu.config.GenesisConfigOptions; import org.hyperledger.besu.controller.BesuController; import org.hyperledger.besu.controller.BesuControllerBuilder; @@ -69,6 +70,7 @@ import org.hyperledger.besu.ethereum.storage.StorageProvider; import org.hyperledger.besu.ethereum.worldstate.DataStorageConfiguration; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; +import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; import org.hyperledger.besu.plugin.services.PicoCLIOptions; import org.hyperledger.besu.plugin.services.StorageService; @@ -204,6 +206,9 @@ public abstract class CommandTestAbstract { @Mock(lenient = true) protected BesuController mockController; + @Mock(lenient = true) + protected BesuComponent mockBesuComponent; + @Mock protected RlpBlockExporter rlpBlockExporter; @Mock protected JsonBlockImporter jsonBlockImporter; @Mock protected RlpBlockImporter rlpBlockImporter; @@ -344,6 +349,7 @@ public void initMocks() throws Exception { when(mockRunnerBuilder.allowedSubnets(any())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.poaDiscoveryRetryBootnodes(anyBoolean())).thenReturn(mockRunnerBuilder); when(mockRunnerBuilder.build()).thenReturn(mockRunner); + when(mockBesuComponent.getMetricsSystem()).thenReturn(new NoOpMetricsSystem()); final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); @@ -451,6 +457,7 @@ protected TestBesuCommand parseCommand( besuCommand.parameterExceptionHandler(), besuCommand.executionExceptionHandler(), in, + mockBesuComponent, args); return besuCommand; } diff --git a/besu/src/test/java/org/hyperledger/besu/components/MockBesuCommandModule.java b/besu/src/test/java/org/hyperledger/besu/components/MockBesuCommandModule.java index 743b4ee8de9..3695fe54f78 100644 --- a/besu/src/test/java/org/hyperledger/besu/components/MockBesuCommandModule.java +++ b/besu/src/test/java/org/hyperledger/besu/components/MockBesuCommandModule.java @@ -18,6 +18,9 @@ import org.hyperledger.besu.cli.BesuCommand; import org.hyperledger.besu.metrics.prometheus.MetricsConfiguration; +import org.hyperledger.besu.plugin.services.BesuConfiguration; +import org.hyperledger.besu.services.BesuConfigurationImpl; +import org.hyperledger.besu.services.BesuPluginContextImpl; import javax.inject.Named; import javax.inject.Singleton; @@ -47,4 +50,17 @@ MetricsConfiguration provideMetricsConfiguration() { Logger provideBesuCommandLogger() { return LoggerFactory.getLogger(MockBesuCommandModule.class); } + + /** + * Creates a BesuPluginContextImpl, used for plugin service discovery. + * + * @return the BesuPluginContext + */ + @Provides + @Singleton + public BesuPluginContextImpl provideBesuPluginContext() { + BesuPluginContextImpl retval = new BesuPluginContextImpl(); + retval.addService(BesuConfiguration.class, new BesuConfigurationImpl()); + return retval; + } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/diffbased/bonsai/cache/BonsaiCachedMerkleTrieLoaderModule.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/diffbased/bonsai/cache/BonsaiCachedMerkleTrieLoaderModule.java index 8ed7daa35f1..b506d5a5ff1 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/diffbased/bonsai/cache/BonsaiCachedMerkleTrieLoaderModule.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/trie/diffbased/bonsai/cache/BonsaiCachedMerkleTrieLoaderModule.java @@ -15,6 +15,7 @@ package org.hyperledger.besu.ethereum.trie.diffbased.bonsai.cache; import org.hyperledger.besu.metrics.ObservableMetricsSystem; +import org.hyperledger.besu.plugin.services.MetricsSystem; import dagger.Module; import dagger.Provides; @@ -24,7 +25,7 @@ public class BonsaiCachedMerkleTrieLoaderModule { @Provides BonsaiCachedMerkleTrieLoader provideCachedMerkleTrieLoaderModule( - final ObservableMetricsSystem metricsSystem) { - return new BonsaiCachedMerkleTrieLoader(metricsSystem); + final MetricsSystem metricsSystem) { + return new BonsaiCachedMerkleTrieLoader((ObservableMetricsSystem) metricsSystem); } } diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsSystemModule.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsSystemModule.java index 044085ef426..1347d6faaaf 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsSystemModule.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/MetricsSystemModule.java @@ -36,10 +36,4 @@ public MetricsSystemModule() {} MetricsSystem provideMetricsSystem(final MetricsConfiguration metricsConfig) { return MetricsSystemFactory.create(metricsConfig); } - - @Provides - @Singleton - ObservableMetricsSystem provideObservableMetricsSystem(final MetricsConfiguration metricsConfig) { - return MetricsSystemFactory.create(metricsConfig); - } } diff --git a/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java b/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java index ca1dc5dd3a3..a399b283734 100644 --- a/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java +++ b/metrics/core/src/main/java/org/hyperledger/besu/metrics/opentelemetry/OpenTelemetrySystem.java @@ -41,6 +41,7 @@ import java.util.concurrent.TimeUnit; import java.util.function.DoubleSupplier; import java.util.stream.Stream; +import javax.inject.Singleton; import com.google.common.collect.ImmutableSet; import io.opentelemetry.api.common.AttributeKey; @@ -67,6 +68,7 @@ import org.slf4j.LoggerFactory; /** Metrics system relying on the native OpenTelemetry format. */ +@Singleton public class OpenTelemetrySystem implements ObservableMetricsSystem { private static final Logger LOG = LoggerFactory.getLogger(OpenTelemetrySystem.class); From 96e9873dd9624200f3a78cc6759a70b159c01d80 Mon Sep 17 00:00:00 2001 From: Bhanu Pulluri <59369753+pullurib@users.noreply.github.com> Date: Wed, 18 Sep 2024 13:53:11 -0400 Subject: [PATCH 06/37] Handle hadolint check failure with proper risk assessment (#7637) Signed-off-by: Bhanu Pulluri Co-authored-by: Bhanu Pulluri Co-authored-by: Fabio Di Fabio --- docker/Dockerfile | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docker/Dockerfile b/docker/Dockerfile index a45a3ac73d8..fe91c7026fd 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -44,6 +44,12 @@ ENV OTEL_RESOURCE_ATTRIBUTES="service.name=besu,service.version=$VERSION" ENV OLDPATH="${PATH}" ENV PATH="/opt/besu/bin:${OLDPATH}" + +# The entry script just sets permissions as needed based on besu config +# and is replaced by the besu process running as besu user. +# Suppressing this warning as there's no risk here because the root user +# only sets permissions and does not continue running the main process. +# hadolint ignore=DL3002 USER root RUN chmod +x /opt/besu/bin/besu-entry.sh From beaee59212f18ca8767498fbb018381af7531893 Mon Sep 17 00:00:00 2001 From: Suyash Nayan <89125422+7suyash7@users.noreply.github.com> Date: Thu, 19 Sep 2024 20:27:06 +0530 Subject: [PATCH 07/37] Add BlobMetrics (#7622) * Add BlobMetrics Signed-off-by: 7suyash7 * refactor Signed-off-by: 7suyash7 * remove unused blob_storage Signed-off-by: 7suyash7 * add .size() to BlobCache Signed-off-by: 7suyash7 * Add to Changelog Signed-off-by: 7suyash7 --------- Signed-off-by: 7suyash7 Co-authored-by: Fabio Di Fabio --- CHANGELOG.md | 1 + .../ethereum/eth/transactions/BlobCache.java | 4 ++++ .../eth/transactions/TransactionPool.java | 14 ++++++++++++++ .../transactions/TransactionPoolMetrics.java | 17 +++++++++++++++++ 4 files changed, 36 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index ff4d7cfbf94..c1ecca978ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ ### Additions and Improvements - Remove privacy test classes support [#7569](https://github.com/hyperledger/besu/pull/7569) +- Add Blob Transaction Metrics [#7622](https://github.com/hyperledger/besu/pull/7622) ### Bug fixes - Fix mounted data path directory permissions for besu user [#7575](https://github.com/hyperledger/besu/pull/7575) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/BlobCache.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/BlobCache.java index 3d3a435f1f8..1cca3f5ee25 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/BlobCache.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/BlobCache.java @@ -91,4 +91,8 @@ public Optional restoreBlob(final Transaction transaction) { public BlobsWithCommitments.BlobQuad get(final VersionedHash vh) { return cache.getIfPresent(vh); } + + public long size() { + return cache.estimatedSize(); + } } 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 315f82921bb..7bbf1abe3b7 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 @@ -129,6 +129,7 @@ public TransactionPool( this.blockAddedEventOrderedProcessor = ethContext.getScheduler().createOrderedProcessor(this::processBlockAddedEvent); this.cacheForBlobsOfTransactionsAddedToABlock = blobCache; + initializeBlobMetrics(); initLogForReplay(); subscribePendingTransactions(this::mapBlobsOnTransactionAdded); subscribeDroppedTransactions(this::unmapBlobsOnTransactionDropped); @@ -686,6 +687,19 @@ public boolean isEnabled() { return isPoolEnabled.get(); } + public int getBlobCacheSize() { + return (int) cacheForBlobsOfTransactionsAddedToABlock.size(); + } + + public int getBlobMapSize() { + return mapOfBlobsInTransactionPool.size(); + } + + private void initializeBlobMetrics() { + metrics.createBlobCacheSizeMetric(this::getBlobCacheSize); + metrics.createBlobMapSizeMetric(this::getBlobMapSize); + } + class PendingTransactionsListenersProxy { private final Subscribers onAddedListeners = Subscribers.create(); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolMetrics.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolMetrics.java index e08805551f9..fac9b3174d0 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolMetrics.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/transactions/TransactionPoolMetrics.java @@ -28,6 +28,7 @@ import java.util.HashMap; import java.util.Map; import java.util.function.DoubleSupplier; +import java.util.function.IntSupplier; import org.apache.commons.lang3.tuple.Pair; import org.slf4j.Logger; @@ -285,4 +286,20 @@ private String location(final boolean receivedFromLocalSource) { private String priority(final boolean hasPriority) { return hasPriority ? "yes" : "no"; } + + public void createBlobCacheSizeMetric(final IntSupplier sizeSupplier) { + metricsSystem.createIntegerGauge( + BesuMetricCategory.TRANSACTION_POOL, + "blob_cache_size", + "Current size of the blob cache", + sizeSupplier); + } + + public void createBlobMapSizeMetric(final IntSupplier sizeSupplier) { + metricsSystem.createIntegerGauge( + BesuMetricCategory.TRANSACTION_POOL, + "blob_map_size", + "Current size of the blob map", + sizeSupplier); + } } From 25186d322110313d89c7cf9e77301f8e2671b8c0 Mon Sep 17 00:00:00 2001 From: Danno Ferrin Date: Thu, 19 Sep 2024 17:30:49 -0600 Subject: [PATCH 08/37] Remove Danno Ferrin as maintainer (#7644) I am no longer aligned with the Governance policies and practices of LFDT and resign my position as maintainer. Signed-off-by: Danno Ferrin --- MAINTAINERS.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MAINTAINERS.md b/MAINTAINERS.md index 1bd4f851f57..f699321efbd 100644 --- a/MAINTAINERS.md +++ b/MAINTAINERS.md @@ -28,7 +28,6 @@ | Matthew Whitehead| matthew1001 | matthew.whitehead | | Meredith Baxter | mbaxter | mbaxter | | Stefan Pingel | pinges | pinges | -| Danno Ferrin | shemnon | shemnon | | Simon Dudley | siladu | siladu | | Usman Saleem | usmansaleem | usmansaleem | @@ -52,6 +51,7 @@ | Rai Sur | RatanRSur | ratanraisur | | Rob Dawson | rojotek | RobDawson | | Sajida Zouarhi | sajz | SajidaZ | +| Danno Ferrin | shemnon | shemnon | | Taccat Isid | taccatisid | taccatisid | | Tim Beiko | timbeiko | timbeiko | | Vijay Michalik | vmichalik | VijayMichalik | From cb1e36a992bdf455916f604edfc22039b64f071e Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Fri, 20 Sep 2024 03:56:25 +0200 Subject: [PATCH 09/37] Fix 7702 signature bound checks (#7641) * create separate signature class for code delegations as they have different bound checks Signed-off-by: Daniel Lehrner * test if increasing xmx let's failing acceptance test pass Signed-off-by: Daniel Lehrner * javadoc Signed-off-by: Sally MacFarlane --------- Signed-off-by: Daniel Lehrner Signed-off-by: Sally MacFarlane Co-authored-by: Sally MacFarlane --- .github/workflows/acceptance-tests.yml | 2 +- .../besu/crypto/AbstractSECP256.java | 6 ++ .../besu/crypto/CodeDelegationSignature.java | 59 ++++++++++++ .../besu/crypto/SignatureAlgorithm.java | 11 +++ .../crypto/CodeDelegationSignatureTest.java | 93 +++++++++++++++++++ .../CodeDelegationTransactionDecoder.java | 5 +- .../mainnet/MainnetTransactionValidator.java | 8 ++ .../encoding/CodeDelegationDecoderTest.java | 12 +++ 8 files changed, 193 insertions(+), 3 deletions(-) create mode 100644 crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/CodeDelegationSignature.java create mode 100644 crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/CodeDelegationSignatureTest.java diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 287f402bced..a8f6981d896 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -11,7 +11,7 @@ concurrency: cancel-in-progress: true env: - GRADLE_OPTS: "-Xmx6g" + GRADLE_OPTS: "-Xmx7g" total-runners: 12 jobs: diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java index b10e654626f..ce376512668 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java @@ -212,6 +212,12 @@ public SECPSignature createSignature(final BigInteger r, final BigInteger s, fin return SECPSignature.create(r, s, recId, curveOrder); } + @Override + public CodeDelegationSignature createCodeDelegationSignature( + final BigInteger r, final BigInteger s, final long yParity) { + return CodeDelegationSignature.create(r, s, yParity); + } + @Override public SECPSignature decodeSignature(final Bytes bytes) { return SECPSignature.decode(bytes, curveOrder); diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/CodeDelegationSignature.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/CodeDelegationSignature.java new file mode 100644 index 00000000000..4bb2e4653e2 --- /dev/null +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/CodeDelegationSignature.java @@ -0,0 +1,59 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.crypto; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.math.BigInteger; + +/** Secp signature with code delegation. */ +public class CodeDelegationSignature extends SECPSignature { + private static final BigInteger TWO_POW_256 = BigInteger.TWO.pow(256); + + /** + * Instantiates a new SECPSignature. + * + * @param r the r part of the signature + * @param s the s part of the signature + * @param yParity the parity of the y coordinate of the public key + */ + public CodeDelegationSignature(final BigInteger r, final BigInteger s, final byte yParity) { + super(r, s, yParity); + } + + /** + * Create a new CodeDelegationSignature. + * + * @param r the r part of the signature + * @param s the s part of the signature + * @param yParity the parity of the y coordinate of the public key + * @return the new CodeDelegationSignature + */ + public static CodeDelegationSignature create( + final BigInteger r, final BigInteger s, final long yParity) { + checkNotNull(r); + checkNotNull(s); + + if (r.compareTo(TWO_POW_256) >= 0) { + throw new IllegalArgumentException("Invalid 'r' value, should be < 2^256 but got " + r); + } + + if (s.compareTo(TWO_POW_256) >= 0) { + throw new IllegalArgumentException("Invalid 's' value, should be < 2^256 but got " + s); + } + + return new CodeDelegationSignature(r, s, (byte) yParity); + } +} diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java index db9565d18d0..8e19b608544 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java @@ -215,6 +215,17 @@ Optional recoverPublicKeyFromSignature( */ SECPSignature createSignature(final BigInteger r, final BigInteger s, final byte recId); + /** + * Create code delegation signature which have different bounds than transaction signatures. + * + * @param r the r part of the signature + * @param s the s part of the signature + * @param yParity the parity of the y coordinate of the public key + * @return the code delegation signature + */ + CodeDelegationSignature createCodeDelegationSignature( + final BigInteger r, final BigInteger s, final long yParity); + /** * Decode secp signature. * diff --git a/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/CodeDelegationSignatureTest.java b/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/CodeDelegationSignatureTest.java new file mode 100644 index 00000000000..1cc66966a78 --- /dev/null +++ b/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/CodeDelegationSignatureTest.java @@ -0,0 +1,93 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.crypto; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import java.math.BigInteger; + +import org.junit.jupiter.api.Test; + +class CodeDelegationSignatureTest { + + private static final BigInteger TWO_POW_256 = BigInteger.valueOf(2).pow(256); + + @Test + void testValidInputs() { + BigInteger r = BigInteger.ONE; + BigInteger s = BigInteger.TEN; + long yParity = 1L; + + CodeDelegationSignature result = CodeDelegationSignature.create(r, s, yParity); + + assertThat(r).isEqualTo(result.getR()); + assertThat(s).isEqualTo(result.getS()); + assertThat((byte) yParity).isEqualTo(result.getRecId()); + } + + @Test + void testNullRValue() { + BigInteger s = BigInteger.TEN; + long yParity = 0L; + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> CodeDelegationSignature.create(null, s, yParity)); + } + + @Test + void testNullSValue() { + BigInteger r = BigInteger.ONE; + long yParity = 0L; + + assertThatExceptionOfType(NullPointerException.class) + .isThrownBy(() -> CodeDelegationSignature.create(r, null, yParity)); + } + + @Test + void testRValueExceedsTwoPow256() { + BigInteger r = TWO_POW_256; + BigInteger s = BigInteger.TEN; + long yParity = 0L; + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> CodeDelegationSignature.create(r, s, yParity)) + .withMessageContainingAll("Invalid 'r' value, should be < 2^256"); + } + + @Test + void testSValueExceedsTwoPow256() { + BigInteger r = BigInteger.ONE; + BigInteger s = TWO_POW_256; + long yParity = 0L; + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> CodeDelegationSignature.create(r, s, yParity)) + .withMessageContainingAll("Invalid 's' value, should be < 2^256"); + } + + @Test + void testValidYParityZero() { + BigInteger r = BigInteger.ONE; + BigInteger s = BigInteger.TEN; + long yParity = 0L; + + CodeDelegationSignature result = CodeDelegationSignature.create(r, s, yParity); + + assertThat(r).isEqualTo(result.getR()); + assertThat(s).isEqualTo(result.getS()); + assertThat((byte) yParity).isEqualTo(result.getRecId()); + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java index 8961431c9cd..d3ef60bfc41 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java @@ -81,13 +81,14 @@ public static CodeDelegation decodeInnerPayload(final RLPInput input) { final Address address = Address.wrap(input.readBytes()); final long nonce = input.readLongScalar(); - final byte yParity = (byte) input.readUnsignedByteScalar(); + final long yParity = input.readUnsignedIntScalar(); final BigInteger r = input.readUInt256Scalar().toUnsignedBigInteger(); final BigInteger s = input.readUInt256Scalar().toUnsignedBigInteger(); input.leaveList(); - final SECPSignature signature = SIGNATURE_ALGORITHM.get().createSignature(r, s, yParity); + final SECPSignature signature = + SIGNATURE_ALGORITHM.get().createCodeDelegationSignature(r, s, yParity); return new org.hyperledger.besu.ethereum.core.CodeDelegation( chainId, address, nonce, signature); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java index e67ccbb4c62..0e86b6b8783 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetTransactionValidator.java @@ -52,6 +52,8 @@ */ public class MainnetTransactionValidator implements TransactionValidator { + public static final BigInteger TWO_POW_256 = BigInteger.TWO.pow(256); + private final GasCalculator gasCalculator; private final GasLimitCalculator gasLimitCalculator; private final FeeMarket feeMarket; @@ -163,6 +165,12 @@ private static ValidationResult validateCodeDelegation .map( codeDelegations -> { for (CodeDelegation codeDelegation : codeDelegations) { + if (codeDelegation.chainId().compareTo(TWO_POW_256) >= 0) { + throw new IllegalArgumentException( + "Invalid 'chainId' value, should be < 2^256 but got " + + codeDelegation.chainId()); + } + if (codeDelegation.signature().getS().compareTo(halfCurveOrder) > 0) { return ValidationResult.invalid( TransactionInvalidReason.INVALID_SIGNATURE, diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationDecoderTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationDecoderTest.java index d6ff585b4fc..a0c7689bc71 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationDecoderTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationDecoderTest.java @@ -99,4 +99,16 @@ void shouldDecodeInnerPayloadWithChainIdZero() { assertThat(signature.getS().toString(16)) .isEqualTo("3c8a25b2becd6e666f69803d1ae3322f2e137b7745c2c7f19da80f993ffde4df"); } + + @Test + void shouldDecodeInnerPayloadWhenSignatureIsZero() { + final BytesValueRLPInput input = + new BytesValueRLPInput( + Bytes.fromHexString( + "0xdf8501a1f0ff5a947a40026a3b9a41754a95eec8c92c6b99886f440c5b808080"), + true); + final CodeDelegation authorization = CodeDelegationTransactionDecoder.decodeInnerPayload(input); + + assertThat(authorization.chainId()).isEqualTo(new BigInteger("01a1f0ff5a", 16)); + } } From 637ebcb9c57107656b9a8e1cc261a2cd10a2e829 Mon Sep 17 00:00:00 2001 From: Gabriel Fukushima Date: Fri, 20 Sep 2024 12:25:16 +1000 Subject: [PATCH 10/37] add Teku EL-bootnode to holesky genesis (#7648) Signed-off-by: Gabriel Fukushima --- config/src/main/resources/holesky.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/src/main/resources/holesky.json b/config/src/main/resources/holesky.json index d91971b3515..ea83279953d 100644 --- a/config/src/main/resources/holesky.json +++ b/config/src/main/resources/holesky.json @@ -21,7 +21,8 @@ "bootnodes": [ "enode://ac906289e4b7f12df423d654c5a962b6ebe5b3a74cc9e06292a85221f9a64a6f1cfdd6b714ed6dacef51578f92b34c60ee91e9ede9c7f8fadc4d347326d95e2b@146.190.13.128:30303", "enode://a3435a0155a3e837c02f5e7f5662a2f1fbc25b48e4dc232016e1c51b544cb5b4510ef633ea3278c0e970fa8ad8141e2d4d0f9f95456c537ff05fdf9b31c15072@178.128.136.233:30303", - "enode://7fa09f1e8bb179ab5e73f45d3a7169a946e7b3de5ef5cea3a0d4546677e4435ee38baea4dd10b3ddfdc1f1c5e869052932af8b8aeb6f9738598ec4590d0b11a6@65.109.94.124:30303" + "enode://7fa09f1e8bb179ab5e73f45d3a7169a946e7b3de5ef5cea3a0d4546677e4435ee38baea4dd10b3ddfdc1f1c5e869052932af8b8aeb6f9738598ec4590d0b11a6@65.109.94.124:30303", + "enode://3524632a412f42dee4b9cc899b946912359bb20103d7596bddb9c8009e7683b7bff39ea20040b7ab64d23105d4eac932d86b930a605e632357504df800dba100@172.174.35.249:30303" ] } }, From e1f44897411eaa8a11a87c23c381090bb6ed18b1 Mon Sep 17 00:00:00 2001 From: Jason Frame Date: Fri, 20 Sep 2024 16:07:27 +1000 Subject: [PATCH 11/37] Disable body validation for POS networks during sync (#7646) Signed-off-by: Jason Frame Co-authored-by: Stefan Pingel <16143240+pinges@users.noreply.github.com> --- .../besu/ethereum/BlockValidator.java | 11 +- .../besu/ethereum/MainnetBlockValidator.java | 8 +- .../besu/ethereum/core/BlockImporter.java | 6 +- .../mainnet/BaseFeeBlockBodyValidator.java | 6 +- .../ethereum/mainnet/BlockBodyValidator.java | 3 +- .../ethereum/mainnet/BodyValidationMode.java | 26 ++++ .../mainnet/MainnetBlockBodyValidator.java | 34 +++-- .../mainnet/MainnetBlockImporter.java | 10 +- .../ethereum/MainnetBlockValidatorTest.java | 44 ++++--- .../MainnetBlockBodyValidatorTest.java | 123 +++++++++++++++++- .../mainnet/PragueRequestsValidatorTest.java | 3 +- .../FastSyncDownloadPipelineFactory.java | 8 +- .../eth/sync/fastsync/ImportBlocksStep.java | 14 +- .../sync/fastsync/ImportBlocksStepTest.java | 18 ++- 14 files changed, 255 insertions(+), 59 deletions(-) create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BodyValidationMode.java diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/BlockValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/BlockValidator.java index e8136062bbd..8cfafee730b 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/BlockValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/BlockValidator.java @@ -17,6 +17,7 @@ import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.Request; import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.mainnet.BodyValidationMode; import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import java.util.List; @@ -83,8 +84,8 @@ BlockProcessingResult validateAndProcessBlock( final boolean shouldRecordBadBlock); /** - * Performs fast block validation with the given context, block, transaction receipts, requests, - * header validation mode, and ommer validation mode. + * Performs fast block validation appropriate for use during syncing skipping transaction receipt + * roots and receipts roots as these are done during the download of the blocks. * * @param context the protocol context * @param block the block to validate @@ -92,13 +93,15 @@ BlockProcessingResult validateAndProcessBlock( * @param requests the requests * @param headerValidationMode the header validation mode * @param ommerValidationMode the ommer validation mode + * @param bodyValidationMode the body validation mode * @return true if the block is valid, false otherwise */ - boolean fastBlockValidation( + boolean validateBlockForSyncing( final ProtocolContext context, final Block block, final List receipts, final Optional> requests, final HeaderValidationMode headerValidationMode, - final HeaderValidationMode ommerValidationMode); + final HeaderValidationMode ommerValidationMode, + final BodyValidationMode bodyValidationMode); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/MainnetBlockValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/MainnetBlockValidator.java index 5d80a951505..0c56a419e35 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/MainnetBlockValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/MainnetBlockValidator.java @@ -25,6 +25,7 @@ import org.hyperledger.besu.ethereum.mainnet.BlockBodyValidator; import org.hyperledger.besu.ethereum.mainnet.BlockHeaderValidator; import org.hyperledger.besu.ethereum.mainnet.BlockProcessor; +import org.hyperledger.besu.ethereum.mainnet.BodyValidationMode; import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import org.hyperledger.besu.ethereum.trie.MerkleTrieException; import org.hyperledger.besu.plugin.services.exception.StorageException; @@ -247,13 +248,14 @@ protected BlockProcessingResult processBlock( } @Override - public boolean fastBlockValidation( + public boolean validateBlockForSyncing( final ProtocolContext context, final Block block, final List receipts, final Optional> requests, final HeaderValidationMode headerValidationMode, - final HeaderValidationMode ommerValidationMode) { + final HeaderValidationMode ommerValidationMode, + final BodyValidationMode bodyValidationMode) { final BlockHeader header = block.getHeader(); if (!blockHeaderValidator.validateHeader(header, context, headerValidationMode)) { String description = String.format("Failed header validation (%s)", headerValidationMode); @@ -262,7 +264,7 @@ public boolean fastBlockValidation( } if (!blockBodyValidator.validateBodyLight( - context, block, receipts, requests, ommerValidationMode)) { + context, block, receipts, requests, ommerValidationMode, bodyValidationMode)) { badBlockManager.addBadBlock( block, BadBlockCause.fromValidationFailure("Failed body validation (light)")); return false; diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/BlockImporter.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/BlockImporter.java index ce5c849e3a2..468485b9fde 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/BlockImporter.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/BlockImporter.java @@ -16,6 +16,7 @@ import org.hyperledger.besu.ethereum.ProtocolContext; import org.hyperledger.besu.ethereum.mainnet.BlockImportResult; +import org.hyperledger.besu.ethereum.mainnet.BodyValidationMode; import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import java.util.List; @@ -73,10 +74,11 @@ BlockImportResult importBlock( * @return {@code BlockImportResult} * @see BlockImportResult */ - BlockImportResult fastImportBlock( + BlockImportResult importBlockForSyncing( ProtocolContext context, Block block, List receipts, HeaderValidationMode headerValidationMode, - HeaderValidationMode ommerValidationMode); + HeaderValidationMode ommerValidationMode, + BodyValidationMode bodyValidationMode); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BaseFeeBlockBodyValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BaseFeeBlockBodyValidator.java index 0e288185cfc..aabc4ee5d09 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BaseFeeBlockBodyValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BaseFeeBlockBodyValidator.java @@ -43,9 +43,11 @@ public boolean validateBodyLight( final Block block, final List receipts, final Optional> requests, - final HeaderValidationMode ommerValidationMode) { + final HeaderValidationMode ommerValidationMode, + final BodyValidationMode bodyValidationMode) { - return super.validateBodyLight(context, block, receipts, requests, ommerValidationMode) + return super.validateBodyLight( + context, block, receipts, requests, ommerValidationMode, bodyValidationMode) && validateTransactionGasPrice(block); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockBodyValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockBodyValidator.java index 62b4985f5ca..c84be3771b9 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockBodyValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BlockBodyValidator.java @@ -59,5 +59,6 @@ boolean validateBodyLight( Block block, List receipts, final Optional> requests, - final HeaderValidationMode ommerValidationMode); + final HeaderValidationMode ommerValidationMode, + final BodyValidationMode bodyValidationMode); } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BodyValidationMode.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BodyValidationMode.java new file mode 100644 index 00000000000..344a950e365 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/BodyValidationMode.java @@ -0,0 +1,26 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.mainnet; + +public enum BodyValidationMode { + /** No Validation. data must be pre-validated */ + NONE, + + /** Skip receipts and transactions root validation */ + LIGHT, + + /** Fully validate the body */ + FULL; +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockBodyValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockBodyValidator.java index 3202440dd89..68951f9f2a3 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockBodyValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockBodyValidator.java @@ -29,6 +29,7 @@ import java.util.Optional; import java.util.Set; +import com.google.common.annotations.VisibleForTesting; import org.apache.tuweni.bytes.Bytes32; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -55,7 +56,8 @@ public boolean validateBody( final Hash worldStateRootHash, final HeaderValidationMode ommerValidationMode) { - if (!validateBodyLight(context, block, receipts, requests, ommerValidationMode)) { + if (!validateBodyLight( + context, block, receipts, requests, ommerValidationMode, BodyValidationMode.FULL)) { return false; } @@ -77,18 +79,26 @@ public boolean validateBodyLight( final Block block, final List receipts, final Optional> requests, - final HeaderValidationMode ommerValidationMode) { + final HeaderValidationMode ommerValidationMode, + final BodyValidationMode bodyValidationMode) { + if (bodyValidationMode == BodyValidationMode.NONE) { + return true; + } + final BlockHeader header = block.getHeader(); final BlockBody body = block.getBody(); - final Bytes32 transactionsRoot = BodyValidation.transactionsRoot(body.getTransactions()); - if (!validateTransactionsRoot(header, header.getTransactionsRoot(), transactionsRoot)) { - return false; - } + // these checks are only needed for full validation and can be skipped for light validation + if (bodyValidationMode == BodyValidationMode.FULL) { + final Bytes32 transactionsRoot = BodyValidation.transactionsRoot(body.getTransactions()); + if (!validateTransactionsRoot(header, header.getTransactionsRoot(), transactionsRoot)) { + return false; + } - final Bytes32 receiptsRoot = BodyValidation.receiptsRoot(receipts); - if (!validateReceiptsRoot(header, header.getReceiptsRoot(), receiptsRoot)) { - return false; + final Bytes32 receiptsRoot = BodyValidation.receiptsRoot(receipts); + if (!validateReceiptsRoot(header, header.getReceiptsRoot(), receiptsRoot)) { + return false; + } } final long gasUsed = @@ -115,7 +125,8 @@ public boolean validateBodyLight( return true; } - private static boolean validateTransactionsRoot( + @VisibleForTesting + protected boolean validateTransactionsRoot( final BlockHeader header, final Bytes32 expected, final Bytes32 actual) { if (!expected.equals(actual)) { LOG.info( @@ -157,7 +168,8 @@ private static boolean validateGasUsed( return true; } - private static boolean validateReceiptsRoot( + @VisibleForTesting + protected boolean validateReceiptsRoot( final BlockHeader header, final Bytes32 expected, final Bytes32 actual) { if (!expected.equals(actual)) { LOG.warn( diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockImporter.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockImporter.java index 62b708d33e6..8646467a282 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockImporter.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockImporter.java @@ -57,20 +57,22 @@ public synchronized BlockImportResult importBlock( } @Override - public BlockImportResult fastImportBlock( + public BlockImportResult importBlockForSyncing( final ProtocolContext context, final Block block, final List receipts, final HeaderValidationMode headerValidationMode, - final HeaderValidationMode ommerValidationMode) { + final HeaderValidationMode ommerValidationMode, + final BodyValidationMode bodyValidationMode) { - if (blockValidator.fastBlockValidation( + if (blockValidator.validateBlockForSyncing( context, block, receipts, block.getBody().getRequests(), headerValidationMode, - ommerValidationMode)) { + ommerValidationMode, + bodyValidationMode)) { context.getBlockchain().appendBlock(block, receipts); return new BlockImportResult(true); } 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 361353ee2b2..40573ee8d6e 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 @@ -34,6 +34,7 @@ import org.hyperledger.besu.ethereum.mainnet.BlockBodyValidator; import org.hyperledger.besu.ethereum.mainnet.BlockHeaderValidator; import org.hyperledger.besu.ethereum.mainnet.BlockProcessor; +import org.hyperledger.besu.ethereum.mainnet.BodyValidationMode; import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import org.hyperledger.besu.ethereum.trie.MerkleTrieException; import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; @@ -98,7 +99,8 @@ public void setup() { when(blockHeaderValidator.validateHeader(any(), any(), any(), any())).thenReturn(true); when(blockBodyValidator.validateBody(any(), any(), any(), any(), any(), any())) .thenReturn(true); - when(blockBodyValidator.validateBodyLight(any(), any(), any(), any(), any())).thenReturn(true); + when(blockBodyValidator.validateBodyLight(any(), any(), any(), any(), any(), any())) + .thenReturn(true); when(blockProcessor.processBlock(any(), any(), any())).thenReturn(successfulProcessingResult); when(blockProcessor.processBlock(any(), any(), any(), any())) .thenReturn(successfulProcessingResult); @@ -343,55 +345,65 @@ public void validateAndProcessBlock_withShouldRecordBadBlockNotSet() { } @Test - public void fastBlockValidation_onSuccess() { + public void validateBlockForSyncing_onSuccess() { final boolean isValid = - mainnetBlockValidator.fastBlockValidation( + mainnetBlockValidator.validateBlockForSyncing( protocolContext, block, Collections.emptyList(), block.getBody().getRequests(), HeaderValidationMode.FULL, - HeaderValidationMode.FULL); + HeaderValidationMode.FULL, + BodyValidationMode.FULL); assertThat(isValid).isTrue(); assertNoBadBlocks(); } @Test - public void fastBlockValidation_onFailedHeaderValidation() { - final HeaderValidationMode validationMode = HeaderValidationMode.FULL; + public void validateBlockValidation_onFailedHeaderForSyncing() { + final HeaderValidationMode headerValidationMode = HeaderValidationMode.FULL; when(blockHeaderValidator.validateHeader( - any(BlockHeader.class), eq(protocolContext), eq(validationMode))) + any(BlockHeader.class), eq(protocolContext), eq(headerValidationMode))) .thenReturn(false); + final BodyValidationMode bodyValidationMode = BodyValidationMode.FULL; final boolean isValid = - mainnetBlockValidator.fastBlockValidation( + mainnetBlockValidator.validateBlockForSyncing( protocolContext, block, Collections.emptyList(), block.getBody().getRequests(), - validationMode, - validationMode); + headerValidationMode, + headerValidationMode, + bodyValidationMode); assertThat(isValid).isFalse(); assertBadBlockIsTracked(block); } @Test - public void fastBlockValidation_onFailedBodyValidation() { - final HeaderValidationMode validationMode = HeaderValidationMode.FULL; + public void validateBlockValidation_onFailedBodyForSyncing() { + final HeaderValidationMode headerValidationMode = HeaderValidationMode.FULL; + final BodyValidationMode bodyValidationMode = BodyValidationMode.FULL; when(blockBodyValidator.validateBodyLight( - eq(protocolContext), eq(block), any(), any(), eq(validationMode))) + eq(protocolContext), + eq(block), + any(), + any(), + eq(headerValidationMode), + eq(bodyValidationMode))) .thenReturn(false); final boolean isValid = - mainnetBlockValidator.fastBlockValidation( + mainnetBlockValidator.validateBlockForSyncing( protocolContext, block, Collections.emptyList(), block.getBody().getRequests(), - validationMode, - validationMode); + headerValidationMode, + headerValidationMode, + bodyValidationMode); assertThat(isValid).isFalse(); assertBadBlockIsTracked(block); diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockBodyValidatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockBodyValidatorTest.java index c116d545d80..c412a30756b 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockBodyValidatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/MainnetBlockBodyValidatorTest.java @@ -19,14 +19,22 @@ import static org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode.NONE; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.GWei; +import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.BlockDataGenerator; import org.hyperledger.besu.ethereum.core.BlockDataGenerator.BlockOptions; import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; import org.hyperledger.besu.ethereum.core.Withdrawal; import org.hyperledger.besu.ethereum.mainnet.requests.DepositRequestValidator; import org.hyperledger.besu.ethereum.mainnet.requests.RequestsValidatorCoordinator; @@ -93,7 +101,12 @@ void validatesWithdrawals() { assertThat( new MainnetBlockBodyValidator(protocolSchedule) .validateBodyLight( - blockchainSetupUtil.getProtocolContext(), block, emptyList(), any(), NONE)) + blockchainSetupUtil.getProtocolContext(), + block, + emptyList(), + any(), + NONE, + BodyValidationMode.FULL)) .isTrue(); } @@ -117,7 +130,12 @@ void validationFailsIfWithdrawalsValidationFails() { assertThat( new MainnetBlockBodyValidator(protocolSchedule) .validateBodyLight( - blockchainSetupUtil.getProtocolContext(), block, emptyList(), any(), NONE)) + blockchainSetupUtil.getProtocolContext(), + block, + emptyList(), + any(), + NONE, + BodyValidationMode.FULL)) .isFalse(); } @@ -141,7 +159,12 @@ void validationFailsIfWithdrawalsRootValidationFails() { assertThat( new MainnetBlockBodyValidator(protocolSchedule) .validateBodyLight( - blockchainSetupUtil.getProtocolContext(), block, emptyList(), any(), NONE)) + blockchainSetupUtil.getProtocolContext(), + block, + emptyList(), + any(), + NONE, + BodyValidationMode.FULL)) .isFalse(); } @@ -165,7 +188,99 @@ public void validationFailsIfWithdrawalRequestsValidationFails() { assertThat( new MainnetBlockBodyValidator(protocolSchedule) .validateBodyLight( - blockchainSetupUtil.getProtocolContext(), block, emptyList(), any(), NONE)) + blockchainSetupUtil.getProtocolContext(), + block, + emptyList(), + any(), + NONE, + BodyValidationMode.FULL)) .isFalse(); } + + @Test + @SuppressWarnings("unchecked") + public void noneValidationModeDoesNothing() { + final Block block = mock(Block.class); + final List receipts = mock(List.class); + + final MainnetBlockBodyValidator bodyValidator = new MainnetBlockBodyValidator(protocolSchedule); + + assertThat( + bodyValidator.validateBodyLight( + blockchainSetupUtil.getProtocolContext(), + block, + receipts, + Optional.empty(), + NONE, + BodyValidationMode.NONE)) + .isTrue(); + verifyNoInteractions(block); + verifyNoInteractions(receipts); + } + + @Test + public void lightValidationDoesNotCheckTransactionRootOrReceiptRoot() { + final Block block = + blockDataGenerator.block( + new BlockOptions() + .setBlockNumber(1) + .setGasUsed(0) + .hasTransactions(false) + .hasOmmers(false) + .setReceiptsRoot(BodyValidation.receiptsRoot(emptyList())) + .setLogsBloom(LogsBloomFilter.empty()) + .setParentHash(blockchainSetupUtil.getBlockchain().getChainHeadHash()) + .setWithdrawals(Optional.of(withdrawals))); + blockchainSetupUtil.getBlockchain().appendBlock(block, Collections.emptyList()); + + final MainnetBlockBodyValidator bodyValidator = new MainnetBlockBodyValidator(protocolSchedule); + final MainnetBlockBodyValidator bodyValidatorSpy = spy(bodyValidator); + + assertThat( + bodyValidatorSpy.validateBodyLight( + blockchainSetupUtil.getProtocolContext(), + block, + emptyList(), + Optional.empty(), + NONE, + BodyValidationMode.LIGHT)) + .isTrue(); + verify(bodyValidatorSpy, never()).validateReceiptsRoot(any(), any(), any()); + verify(bodyValidatorSpy, never()).validateTransactionsRoot(any(), any(), any()); + } + + @Test + public void fullValidationChecksTransactionRootAndReceiptRoot() { + final Block block = + blockDataGenerator.block( + new BlockOptions() + .setBlockNumber(1) + .setGasUsed(0) + .hasTransactions(false) + .hasOmmers(false) + .setReceiptsRoot(BodyValidation.receiptsRoot(emptyList())) + .setLogsBloom(LogsBloomFilter.empty()) + .setParentHash(blockchainSetupUtil.getBlockchain().getChainHeadHash()) + .setWithdrawals(Optional.of(withdrawals))); + blockchainSetupUtil.getBlockchain().appendBlock(block, Collections.emptyList()); + + final MainnetBlockBodyValidator bodyValidator = new MainnetBlockBodyValidator(protocolSchedule); + final MainnetBlockBodyValidator bodyValidatorSpy = spy(bodyValidator); + + assertThat( + bodyValidatorSpy.validateBodyLight( + blockchainSetupUtil.getProtocolContext(), + block, + emptyList(), + Optional.empty(), + NONE, + BodyValidationMode.FULL)) + .isTrue(); + final Hash receiptsRoot = BodyValidation.receiptsRoot(emptyList()); + final Hash transactionsRoot = BodyValidation.transactionsRoot(emptyList()); + verify(bodyValidatorSpy, times(1)) + .validateReceiptsRoot(block.getHeader(), receiptsRoot, receiptsRoot); + verify(bodyValidatorSpy, times(1)) + .validateTransactionsRoot(block.getHeader(), transactionsRoot, transactionsRoot); + } } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PragueRequestsValidatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PragueRequestsValidatorTest.java index 833e692d7f4..6c325b70a86 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PragueRequestsValidatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PragueRequestsValidatorTest.java @@ -106,7 +106,8 @@ void shouldValidateRequestsTest() { block, emptyList(), expectedRequests, - NONE)) + NONE, + BodyValidationMode.FULL)) .isFalse(); } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/FastSyncDownloadPipelineFactory.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/FastSyncDownloadPipelineFactory.java index 07e964426cc..87032b76e57 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/FastSyncDownloadPipelineFactory.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/FastSyncDownloadPipelineFactory.java @@ -37,6 +37,7 @@ import org.hyperledger.besu.ethereum.eth.sync.range.SyncTargetRangeSource; import org.hyperledger.besu.ethereum.eth.sync.state.SyncState; import org.hyperledger.besu.ethereum.eth.sync.state.SyncTarget; +import org.hyperledger.besu.ethereum.mainnet.BodyValidationMode; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.metrics.BesuMetricCategory; import org.hyperledger.besu.plugin.services.MetricsSystem; @@ -117,6 +118,10 @@ public Pipeline createDownloadPipelineForSyncTarget(final SyncT final int downloaderParallelism = syncConfig.getDownloaderParallelism(); final int headerRequestSize = syncConfig.getDownloaderHeaderRequestSize(); final int singleHeaderBufferSize = headerRequestSize * downloaderParallelism; + final BodyValidationMode bodyValidationMode = + protocolSchedule.anyMatch(scheduledProtocolSpec -> scheduledProtocolSpec.spec().isPoS()) + ? BodyValidationMode.NONE + : BodyValidationMode.LIGHT; final SyncTargetRangeSource checkpointRangeSource = new SyncTargetRangeSource( new RangeHeadersFetcher( @@ -148,7 +153,8 @@ public Pipeline createDownloadPipelineForSyncTarget(final SyncT attachedValidationPolicy, ommerValidationPolicy, ethContext, - fastSyncState.getPivotBlockHeader().get()); + fastSyncState.getPivotBlockHeader().get(), + bodyValidationMode); return PipelineBuilder.createPipelineFrom( "fetchCheckpoints", diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportBlocksStep.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportBlocksStep.java index c6945964cf0..20b9d84916a 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportBlocksStep.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportBlocksStep.java @@ -22,6 +22,7 @@ import org.hyperledger.besu.ethereum.eth.sync.ValidationPolicy; import org.hyperledger.besu.ethereum.eth.sync.tasks.exceptions.InvalidBlockException; import org.hyperledger.besu.ethereum.mainnet.BlockImportResult; +import org.hyperledger.besu.ethereum.mainnet.BodyValidationMode; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import java.util.List; @@ -45,6 +46,7 @@ public class ImportBlocksStep implements Consumer> { private long accumulatedTime = 0L; private OptionalLong logStartBlock = OptionalLong.empty(); private final BlockHeader pivotHeader; + private final BodyValidationMode bodyValidationMode; public ImportBlocksStep( final ProtocolSchedule protocolSchedule, @@ -52,13 +54,15 @@ public ImportBlocksStep( final ValidationPolicy headerValidationPolicy, final ValidationPolicy ommerValidationPolicy, final EthContext ethContext, - final BlockHeader pivotHeader) { + final BlockHeader pivotHeader, + final BodyValidationMode bodyValidationMode) { this.protocolSchedule = protocolSchedule; this.protocolContext = protocolContext; this.headerValidationPolicy = headerValidationPolicy; this.ommerValidationPolicy = ommerValidationPolicy; this.ethContext = ethContext; this.pivotHeader = pivotHeader; + this.bodyValidationMode = bodyValidationMode; } @Override @@ -106,20 +110,20 @@ protected static long getBlocksPercent(final long lastBlock, final long totalBlo if (totalBlocks == 0) { return 0; } - final long blocksPercent = (100 * lastBlock / totalBlocks); - return blocksPercent; + return (100 * lastBlock / totalBlocks); } protected boolean importBlock(final BlockWithReceipts blockWithReceipts) { final BlockImporter importer = protocolSchedule.getByBlockHeader(blockWithReceipts.getHeader()).getBlockImporter(); final BlockImportResult blockImportResult = - importer.fastImportBlock( + importer.importBlockForSyncing( protocolContext, blockWithReceipts.getBlock(), blockWithReceipts.getReceipts(), headerValidationPolicy.getValidationModeForNextBlock(), - ommerValidationPolicy.getValidationModeForNextBlock()); + ommerValidationPolicy.getValidationModeForNextBlock(), + bodyValidationMode); return blockImportResult.isImported(); } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportBlocksStepTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportBlocksStepTest.java index 70c9e10eba6..af4f45f6901 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportBlocksStepTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportBlocksStepTest.java @@ -33,6 +33,7 @@ import org.hyperledger.besu.ethereum.eth.sync.ValidationPolicy; import org.hyperledger.besu.ethereum.eth.sync.tasks.exceptions.InvalidBlockException; import org.hyperledger.besu.ethereum.mainnet.BlockImportResult; +import org.hyperledger.besu.ethereum.mainnet.BodyValidationMode; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; @@ -72,7 +73,8 @@ public void setUp() { validationPolicy, ommerValidationPolicy, null, - pivotHeader); + pivotHeader, + BodyValidationMode.FULL); } @Test @@ -84,12 +86,13 @@ public void shouldImportBlocks() { .collect(toList()); for (final BlockWithReceipts blockWithReceipts : blocksWithReceipts) { - when(blockImporter.fastImportBlock( + when(blockImporter.importBlockForSyncing( protocolContext, blockWithReceipts.getBlock(), blockWithReceipts.getReceipts(), FULL, - LIGHT)) + LIGHT, + BodyValidationMode.FULL)) .thenReturn(new BlockImportResult(true)); } importBlocksStep.accept(blocksWithReceipts); @@ -105,8 +108,13 @@ public void shouldThrowExceptionWhenValidationFails() { final Block block = gen.block(); final BlockWithReceipts blockWithReceipts = new BlockWithReceipts(block, gen.receipts(block)); - when(blockImporter.fastImportBlock( - protocolContext, block, blockWithReceipts.getReceipts(), FULL, LIGHT)) + when(blockImporter.importBlockForSyncing( + protocolContext, + block, + blockWithReceipts.getReceipts(), + FULL, + LIGHT, + BodyValidationMode.FULL)) .thenReturn(new BlockImportResult(false)); assertThatThrownBy(() -> importBlocksStep.accept(singletonList(blockWithReceipts))) .isInstanceOf(InvalidBlockException.class); From e721237c26b518d9b4f100b695df8ab7290034ee Mon Sep 17 00:00:00 2001 From: Matt Whitehead Date: Fri, 20 Sep 2024 09:38:57 +0100 Subject: [PATCH 12/37] Don't persist IBFT2 proposal blocks, just validate them (#7631) * Don't persist IBFT2 proposal blocks, just validate them Signed-off-by: Matthew Whitehead * Tidy up changelog Signed-off-by: Matthew Whitehead --------- Signed-off-by: Matthew Whitehead Signed-off-by: Matt Whitehead --- CHANGELOG.md | 2 +- .../hyperledger/besu/consensus/common/bft/RoundTimer.java | 2 +- .../besu/consensus/ibft/validation/MessageValidator.java | 8 +++++--- .../consensus/ibft/validation/MessageValidatorTest.java | 4 ++-- 4 files changed, 9 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c1ecca978ee..246df1d91cd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -15,7 +15,7 @@ - Fix mounted data path directory permissions for besu user [#7575](https://github.com/hyperledger/besu/pull/7575) - Fix for `debug_traceCall` to handle transactions without specified gas price. [#7510](https://github.com/hyperledger/besu/pull/7510) - Corrects a regression where custom plugin services are not initialized correctly. [#7625](https://github.com/hyperledger/besu/pull/7625) - +- Fix for IBFT2 chains using the BONSAI DB format [#7631](https://github.com/hyperledger/besu/pull/7631) ## 24.9.1 diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/RoundTimer.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/RoundTimer.java index 6a8b02991d6..a01dc652cff 100644 --- a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/RoundTimer.java +++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/RoundTimer.java @@ -81,7 +81,7 @@ public synchronized void startTimer(final ConsensusRoundIdentifier round) { // Once we are up to round 2 start logging round expiries if (round.getRoundNumber() >= 2) { LOG.info( - "QBFT round {} expired. Moved to round {} which will expire in {} seconds", + "BFT round {} expired. Moved to round {} which will expire in {} seconds", round.getRoundNumber() - 1, round.getRoundNumber(), (expiryTime / 1000)); diff --git a/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/validation/MessageValidator.java b/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/validation/MessageValidator.java index 783861cdfef..1d7b3da9d61 100644 --- a/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/validation/MessageValidator.java +++ b/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/validation/MessageValidator.java @@ -78,7 +78,9 @@ public boolean validateProposal(final Proposal msg) { return false; } - if (!validateBlock(msg.getBlock())) { + // We want to validate the block but not persist it yet as it's just a proposal. If it turns + // out to be an accepted block it will be persisted at block import time + if (!validateBlockWithoutPersisting(msg.getBlock())) { return false; } @@ -93,14 +95,14 @@ public boolean validateProposal(final Proposal msg) { msg.getSignedPayload(), msg.getBlock(), blockInterface); } - private boolean validateBlock(final Block block) { + private boolean validateBlockWithoutPersisting(final Block block) { final BlockValidator blockValidator = protocolSchedule.getByBlockHeader(block.getHeader()).getBlockValidator(); final var validationResult = blockValidator.validateAndProcessBlock( - protocolContext, block, HeaderValidationMode.LIGHT, HeaderValidationMode.FULL); + protocolContext, block, HeaderValidationMode.LIGHT, HeaderValidationMode.FULL, false); if (validationResult.isFailed()) { LOG.info( diff --git a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/validation/MessageValidatorTest.java b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/validation/MessageValidatorTest.java index f7fb7af3e94..2352642b87b 100644 --- a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/validation/MessageValidatorTest.java +++ b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/validation/MessageValidatorTest.java @@ -113,7 +113,7 @@ public void setup() { when(protocolSpec.getBlockValidator()).thenReturn(blockValidator); when(protocolSchedule.getByBlockHeader(any())).thenReturn(protocolSpec); - when(blockValidator.validateAndProcessBlock(any(), any(), any(), any())) + when(blockValidator.validateAndProcessBlock(any(), any(), any(), any(), eq(false))) .thenReturn(new BlockProcessingResult(Optional.empty())); when(roundChangeCertificateValidator.validateProposalMessageMatchesLatestPrepareCertificate( @@ -168,7 +168,7 @@ public void ifProposalConsistencyChecksFailProposalIsIllegal() { @Test public void blockValidationFailureFailsValidation() { - when(blockValidator.validateAndProcessBlock(any(), any(), any(), any())) + when(blockValidator.validateAndProcessBlock(any(), any(), any(), any(), eq(false))) .thenReturn(new BlockProcessingResult("Failed")); final Proposal proposalMsg = From 19d3ca84b282f4db26f95497187209ea7fb6c72d Mon Sep 17 00:00:00 2001 From: Matt Whitehead Date: Fri, 20 Sep 2024 10:12:11 +0100 Subject: [PATCH 13/37] Dev/test option for short BFT block periods (#7588) * Dev mode for short BFT block periods Signed-off-by: Matthew Whitehead * Refactoring Signed-off-by: Matthew Whitehead * Fix comment Signed-off-by: Matthew Whitehead * Refactor to make BFT block milliseconds an experimental QBFT config option Signed-off-by: Matthew Whitehead * Update Json BFT config options Signed-off-by: Matthew Whitehead --------- Signed-off-by: Matthew Whitehead --- .../controller/IbftBesuControllerBuilder.java | 6 +- .../controller/QbftBesuControllerBuilder.java | 6 +- .../besu/config/BftConfigOptions.java | 7 +++ .../org/hyperledger/besu/config/BftFork.java | 13 +++++ .../besu/config/JsonBftConfigOptions.java | 10 ++++ .../besu/consensus/common/bft/BlockTimer.java | 29 ++++++++-- .../common/bft/MutableBftConfigOptions.java | 17 ++++++ .../besu/consensus/common/bft/RoundTimer.java | 12 ++-- .../consensus/common/bft/RoundTimerTest.java | 3 +- .../ibft/support/TestContextBuilder.java | 3 +- ...ftBlockHeaderValidationRulesetFactory.java | 58 +++++++++++-------- .../ibft/IbftProtocolScheduleBuilder.java | 6 +- ...ockHeaderValidationRulesetFactoryTest.java | 6 +- .../blockcreation/BftBlockCreatorTest.java | 5 +- .../qbft/support/TestContextBuilder.java | 3 +- ...ftBlockHeaderValidationRulesetFactory.java | 50 ++++++++++------ .../qbft/QbftForksSchedulesFactory.java | 1 + .../qbft/QbftProtocolScheduleBuilder.java | 5 +- ...ockHeaderValidationRulesetFactoryTest.java | 6 +- 19 files changed, 181 insertions(+), 65 deletions(-) diff --git a/besu/src/main/java/org/hyperledger/besu/controller/IbftBesuControllerBuilder.java b/besu/src/main/java/org/hyperledger/besu/controller/IbftBesuControllerBuilder.java index b8d4d2645e0..58412029fc3 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/IbftBesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/IbftBesuControllerBuilder.java @@ -74,6 +74,7 @@ import org.hyperledger.besu.plugin.services.BesuEvents; import org.hyperledger.besu.util.Subscribers; +import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -182,7 +183,10 @@ protected MiningCoordinator createMiningCoordinator( Util.publicKeyToAddress(nodeKey.getPublicKey()), proposerSelector, uniqueMessageMulticaster, - new RoundTimer(bftEventQueue, bftConfig.getRequestTimeoutSeconds(), bftExecutors), + new RoundTimer( + bftEventQueue, + Duration.ofSeconds(bftConfig.getRequestTimeoutSeconds()), + bftExecutors), new BlockTimer(bftEventQueue, forksSchedule, bftExecutors, clock), blockCreatorFactory, clock); diff --git a/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java b/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java index 7961305c48e..3d3412d4860 100644 --- a/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java +++ b/besu/src/main/java/org/hyperledger/besu/controller/QbftBesuControllerBuilder.java @@ -84,6 +84,7 @@ import org.hyperledger.besu.plugin.services.BesuEvents; import org.hyperledger.besu.util.Subscribers; +import java.time.Duration; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -222,7 +223,10 @@ protected MiningCoordinator createMiningCoordinator( Util.publicKeyToAddress(nodeKey.getPublicKey()), proposerSelector, uniqueMessageMulticaster, - new RoundTimer(bftEventQueue, qbftConfig.getRequestTimeoutSeconds(), bftExecutors), + new RoundTimer( + bftEventQueue, + Duration.ofSeconds(qbftConfig.getRequestTimeoutSeconds()), + bftExecutors), new BlockTimer(bftEventQueue, qbftForksSchedule, bftExecutors, clock), blockCreatorFactory, clock); diff --git a/config/src/main/java/org/hyperledger/besu/config/BftConfigOptions.java b/config/src/main/java/org/hyperledger/besu/config/BftConfigOptions.java index c94752b598d..58df7be8490 100644 --- a/config/src/main/java/org/hyperledger/besu/config/BftConfigOptions.java +++ b/config/src/main/java/org/hyperledger/besu/config/BftConfigOptions.java @@ -37,6 +37,13 @@ public interface BftConfigOptions { */ int getBlockPeriodSeconds(); + /** + * Gets block period milliseconds. For TESTING only. If set then blockperiodseconds is ignored. + * + * @return the block period milliseconds + */ + long getBlockPeriodMilliseconds(); + /** * Gets request timeout seconds. * diff --git a/config/src/main/java/org/hyperledger/besu/config/BftFork.java b/config/src/main/java/org/hyperledger/besu/config/BftFork.java index 30f8e1c5d5b..fdaa8f2fa9d 100644 --- a/config/src/main/java/org/hyperledger/besu/config/BftFork.java +++ b/config/src/main/java/org/hyperledger/besu/config/BftFork.java @@ -21,6 +21,7 @@ import java.util.Locale; import java.util.Optional; import java.util.OptionalInt; +import java.util.OptionalLong; import com.fasterxml.jackson.annotation.JsonCreator; import com.fasterxml.jackson.databind.node.ArrayNode; @@ -40,6 +41,9 @@ public class BftFork implements Fork { /** The constant BLOCK_PERIOD_SECONDS_KEY. */ public static final String BLOCK_PERIOD_SECONDS_KEY = "blockperiodseconds"; + /** The constant BLOCK_PERIOD_MILLISECONDS_KEY. */ + public static final String BLOCK_PERIOD_MILLISECONDS_KEY = "xblockperiodmilliseconds"; + /** The constant BLOCK_REWARD_KEY. */ public static final String BLOCK_REWARD_KEY = "blockreward"; @@ -82,6 +86,15 @@ public OptionalInt getBlockPeriodSeconds() { return JsonUtil.getPositiveInt(forkConfigRoot, BLOCK_PERIOD_SECONDS_KEY); } + /** + * Gets block period milliseconds. Experimental for test scenarios only. + * + * @return the block period milliseconds + */ + public OptionalLong getBlockPeriodMilliseconds() { + return JsonUtil.getLong(forkConfigRoot, BLOCK_PERIOD_MILLISECONDS_KEY); + } + /** * Gets block reward wei. * diff --git a/config/src/main/java/org/hyperledger/besu/config/JsonBftConfigOptions.java b/config/src/main/java/org/hyperledger/besu/config/JsonBftConfigOptions.java index 95f2d9f7ce7..b1c8630b399 100644 --- a/config/src/main/java/org/hyperledger/besu/config/JsonBftConfigOptions.java +++ b/config/src/main/java/org/hyperledger/besu/config/JsonBftConfigOptions.java @@ -34,6 +34,7 @@ public class JsonBftConfigOptions implements BftConfigOptions { private static final long DEFAULT_EPOCH_LENGTH = 30_000; private static final int DEFAULT_BLOCK_PERIOD_SECONDS = 1; + private static final int DEFAULT_BLOCK_PERIOD_MILLISECONDS = 0; // Experimental for test only private static final int DEFAULT_ROUND_EXPIRY_SECONDS = 1; // In a healthy network this can be very small. This default limit will allow for suitable // protection for on a typical 20 node validator network with multiple rounds @@ -66,6 +67,12 @@ public int getBlockPeriodSeconds() { bftConfigRoot, "blockperiodseconds", DEFAULT_BLOCK_PERIOD_SECONDS); } + @Override + public long getBlockPeriodMilliseconds() { + return JsonUtil.getLong( + bftConfigRoot, "xblockperiodmilliseconds", DEFAULT_BLOCK_PERIOD_MILLISECONDS); + } + @Override public int getRequestTimeoutSeconds() { return JsonUtil.getInt(bftConfigRoot, "requesttimeoutseconds", DEFAULT_ROUND_EXPIRY_SECONDS); @@ -133,6 +140,9 @@ public Map asMap() { if (bftConfigRoot.has("blockperiodseconds")) { builder.put("blockPeriodSeconds", getBlockPeriodSeconds()); } + if (bftConfigRoot.has("xblockperiodmilliseconds")) { + builder.put("xBlockPeriodMilliSeconds", getBlockPeriodMilliseconds()); + } if (bftConfigRoot.has("requesttimeoutseconds")) { builder.put("requestTimeoutSeconds", getRequestTimeoutSeconds()); } diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java index 5649b69e8f1..49ef94e008c 100644 --- a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java +++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/BlockTimer.java @@ -24,9 +24,14 @@ import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + /** Class for starting and keeping organised block timers */ public class BlockTimer { + private static final Logger LOG = LoggerFactory.getLogger(BlockTimer.class); + private final ForksSchedule forksSchedule; private final BftExecutors bftExecutors; private Optional> currentTimerTask; @@ -79,12 +84,26 @@ public synchronized void startTimer( cancelTimer(); final long now = clock.millis(); + final long expiryTime; + + // Experimental option for test scenarios only. Not for production use. + final long blockPeriodMilliseconds = + forksSchedule.getFork(round.getSequenceNumber()).getValue().getBlockPeriodMilliseconds(); - // absolute time when the timer is supposed to expire - final int blockPeriodSeconds = - forksSchedule.getFork(round.getSequenceNumber()).getValue().getBlockPeriodSeconds(); - final long minimumTimeBetweenBlocksMillis = blockPeriodSeconds * 1000L; - final long expiryTime = chainHeadHeader.getTimestamp() * 1_000 + minimumTimeBetweenBlocksMillis; + if (blockPeriodMilliseconds > 0) { + // Experimental mode for setting < 1 second block periods e.g. for CI/CD pipelines + // running tests against Besu + expiryTime = clock.millis() + blockPeriodMilliseconds; + LOG.warn( + "Test-mode only xblockperiodmilliseconds has been set to {} millisecond blocks. Do not use in a production system.", + blockPeriodMilliseconds); + } else { + // absolute time when the timer is supposed to expire + final int blockPeriodSeconds = + forksSchedule.getFork(round.getSequenceNumber()).getValue().getBlockPeriodSeconds(); + final long minimumTimeBetweenBlocksMillis = blockPeriodSeconds * 1000L; + expiryTime = chainHeadHeader.getTimestamp() * 1_000 + minimumTimeBetweenBlocksMillis; + } if (expiryTime > now) { final long delay = expiryTime - now; diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/MutableBftConfigOptions.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/MutableBftConfigOptions.java index fd406ea5e12..7b27c7b4c44 100644 --- a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/MutableBftConfigOptions.java +++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/MutableBftConfigOptions.java @@ -31,6 +31,7 @@ public class MutableBftConfigOptions implements BftConfigOptions { private long epochLength; private int blockPeriodSeconds; + private long blockPeriodMilliseconds; private int requestTimeoutSeconds; private int gossipedHistoryLimit; private int messageQueueLimit; @@ -48,6 +49,7 @@ public class MutableBftConfigOptions implements BftConfigOptions { public MutableBftConfigOptions(final BftConfigOptions bftConfigOptions) { this.epochLength = bftConfigOptions.getEpochLength(); this.blockPeriodSeconds = bftConfigOptions.getBlockPeriodSeconds(); + this.blockPeriodMilliseconds = bftConfigOptions.getBlockPeriodMilliseconds(); this.requestTimeoutSeconds = bftConfigOptions.getRequestTimeoutSeconds(); this.gossipedHistoryLimit = bftConfigOptions.getGossipedHistoryLimit(); this.messageQueueLimit = bftConfigOptions.getMessageQueueLimit(); @@ -68,6 +70,11 @@ public int getBlockPeriodSeconds() { return blockPeriodSeconds; } + @Override + public long getBlockPeriodMilliseconds() { + return blockPeriodMilliseconds; + } + @Override public int getRequestTimeoutSeconds() { return requestTimeoutSeconds; @@ -131,6 +138,16 @@ public void setBlockPeriodSeconds(final int blockPeriodSeconds) { this.blockPeriodSeconds = blockPeriodSeconds; } + /** + * Sets block period milliseconds. Experimental for test scenarios. Not for use on production + * systems. + * + * @param blockPeriodMilliseconds the block period milliseconds + */ + public void setBlockPeriodMilliseconds(final long blockPeriodMilliseconds) { + this.blockPeriodMilliseconds = blockPeriodMilliseconds; + } + /** * Sets request timeout seconds. * diff --git a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/RoundTimer.java b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/RoundTimer.java index a01dc652cff..0302943fc12 100644 --- a/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/RoundTimer.java +++ b/consensus/common/src/main/java/org/hyperledger/besu/consensus/common/bft/RoundTimer.java @@ -16,6 +16,7 @@ import org.hyperledger.besu.consensus.common.bft.events.RoundExpiry; +import java.time.Duration; import java.util.Optional; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -31,21 +32,21 @@ public class RoundTimer { private final BftExecutors bftExecutors; private Optional> currentTimerTask; private final BftEventQueue queue; - private final long baseExpiryMillis; + private final Duration baseExpiryPeriod; /** * Construct a RoundTimer with primed executor service ready to start timers * * @param queue The queue in which to put round expiry events - * @param baseExpirySeconds The initial round length for round 0 + * @param baseExpiryPeriod The initial round length for round 0 * @param bftExecutors executor service that timers can be scheduled with */ public RoundTimer( - final BftEventQueue queue, final long baseExpirySeconds, final BftExecutors bftExecutors) { + final BftEventQueue queue, final Duration baseExpiryPeriod, final BftExecutors bftExecutors) { this.queue = queue; this.bftExecutors = bftExecutors; this.currentTimerTask = Optional.empty(); - this.baseExpiryMillis = baseExpirySeconds * 1000; + this.baseExpiryPeriod = baseExpiryPeriod; } /** Cancels the current running round timer if there is one */ @@ -71,7 +72,8 @@ public synchronized boolean isRunning() { public synchronized void startTimer(final ConsensusRoundIdentifier round) { cancelTimer(); - final long expiryTime = baseExpiryMillis * (long) Math.pow(2, round.getRoundNumber()); + final long expiryTime = + baseExpiryPeriod.toMillis() * (long) Math.pow(2, round.getRoundNumber()); final Runnable newTimerRunnable = () -> queue.add(new RoundExpiry(round)); diff --git a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/RoundTimerTest.java b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/RoundTimerTest.java index 0ebca51c9e2..8f437395471 100644 --- a/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/RoundTimerTest.java +++ b/consensus/common/src/test/java/org/hyperledger/besu/consensus/common/bft/RoundTimerTest.java @@ -25,6 +25,7 @@ import org.hyperledger.besu.consensus.common.bft.events.RoundExpiry; +import java.time.Duration; import java.util.concurrent.ScheduledFuture; import java.util.concurrent.TimeUnit; @@ -46,7 +47,7 @@ public void initialise() { bftExecutors = mock(BftExecutors.class); queue = new BftEventQueue(1000); queue.start(); - timer = new RoundTimer(queue, 1, bftExecutors); + timer = new RoundTimer(queue, Duration.ofSeconds(1), bftExecutors); } @Test diff --git a/consensus/ibft/src/integration-test/java/org/hyperledger/besu/consensus/ibft/support/TestContextBuilder.java b/consensus/ibft/src/integration-test/java/org/hyperledger/besu/consensus/ibft/support/TestContextBuilder.java index 5d2b02b1a7c..8896733548d 100644 --- a/consensus/ibft/src/integration-test/java/org/hyperledger/besu/consensus/ibft/support/TestContextBuilder.java +++ b/consensus/ibft/src/integration-test/java/org/hyperledger/besu/consensus/ibft/support/TestContextBuilder.java @@ -100,6 +100,7 @@ import org.hyperledger.besu.util.Subscribers; import java.time.Clock; +import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.util.ArrayList; @@ -403,7 +404,7 @@ private static ControllerAndState createControllerAndFinalState( Util.publicKeyToAddress(nodeKey.getPublicKey()), proposerSelector, multicaster, - new RoundTimer(bftEventQueue, ROUND_TIMER_SEC, bftExecutors), + new RoundTimer(bftEventQueue, Duration.ofSeconds(ROUND_TIMER_SEC), bftExecutors), new BlockTimer(bftEventQueue, forksSchedule, bftExecutors, TestClock.fixed()), blockCreatorFactory, clock); diff --git a/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java b/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java index 0e71fe81216..2367f90e2be 100644 --- a/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java +++ b/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/IbftBlockHeaderValidationRulesetFactory.java @@ -32,6 +32,7 @@ import org.hyperledger.besu.ethereum.mainnet.headervalidationrules.TimestampBoundedByFutureParameter; import org.hyperledger.besu.ethereum.mainnet.headervalidationrules.TimestampMoreRecentThanParent; +import java.time.Duration; import java.util.Optional; import org.apache.tuweni.units.bigints.UInt256; @@ -45,32 +46,43 @@ private IbftBlockHeaderValidationRulesetFactory() {} * Produces a BlockHeaderValidator configured for assessing bft block headers which are to form * part of the BlockChain (i.e. not proposed blocks, which do not contain commit seals) * - * @param secondsBetweenBlocks the minimum number of seconds which must elapse between blocks. + * @param minimumTimeBetweenBlocks the minimum time which must elapse between blocks. * @param baseFeeMarket an {@link Optional} wrapping {@link BaseFeeMarket} class if appropriate. * @return BlockHeaderValidator configured for assessing bft block headers */ public static BlockHeaderValidator.Builder blockHeaderValidator( - final long secondsBetweenBlocks, final Optional baseFeeMarket) { - return new BlockHeaderValidator.Builder() - .addRule(new AncestryValidationRule()) - .addRule(new GasUsageValidationRule()) - .addRule( - new GasLimitRangeAndDeltaValidationRule( - DEFAULT_MIN_GAS_LIMIT, DEFAULT_MAX_GAS_LIMIT, baseFeeMarket)) - .addRule(new TimestampBoundedByFutureParameter(1)) - .addRule(new TimestampMoreRecentThanParent(secondsBetweenBlocks)) - .addRule( - new ConstantFieldValidationRule<>( - "MixHash", BlockHeader::getMixHash, BftHelpers.EXPECTED_MIX_HASH)) - .addRule( - new ConstantFieldValidationRule<>( - "OmmersHash", BlockHeader::getOmmersHash, Hash.EMPTY_LIST_HASH)) - .addRule( - new ConstantFieldValidationRule<>( - "Difficulty", BlockHeader::getDifficulty, UInt256.ONE)) - .addRule(new ConstantFieldValidationRule<>("Nonce", BlockHeader::getNonce, 0L)) - .addRule(new BftValidatorsValidationRule()) - .addRule(new BftCoinbaseValidationRule()) - .addRule(new BftCommitSealsValidationRule()); + final Duration minimumTimeBetweenBlocks, final Optional baseFeeMarket) { + final BlockHeaderValidator.Builder ruleBuilder = + new BlockHeaderValidator.Builder() + .addRule(new AncestryValidationRule()) + .addRule(new GasUsageValidationRule()) + .addRule( + new GasLimitRangeAndDeltaValidationRule( + DEFAULT_MIN_GAS_LIMIT, DEFAULT_MAX_GAS_LIMIT, baseFeeMarket)) + .addRule(new TimestampBoundedByFutureParameter(1)) + .addRule( + new ConstantFieldValidationRule<>( + "MixHash", BlockHeader::getMixHash, BftHelpers.EXPECTED_MIX_HASH)) + .addRule( + new ConstantFieldValidationRule<>( + "OmmersHash", BlockHeader::getOmmersHash, Hash.EMPTY_LIST_HASH)) + .addRule( + new ConstantFieldValidationRule<>( + "Difficulty", BlockHeader::getDifficulty, UInt256.ONE)) + .addRule(new ConstantFieldValidationRule<>("Nonce", BlockHeader::getNonce, 0L)) + .addRule(new BftValidatorsValidationRule()) + .addRule(new BftCoinbaseValidationRule()) + .addRule(new BftCommitSealsValidationRule()); + + // Currently the minimum acceptable time between blocks is 1 second. The timestamp of an + // Ethereum header is stored as seconds since Unix epoch so blocks being produced more + // frequently than once a second cannot pass this validator. For non-production scenarios + // (e.g. for testing block production much more frequently than once a second) Besu has + // an experimental 'xblockperiodmilliseconds' option for BFT chains. If this is enabled + // we cannot apply the TimestampMoreRecentThanParent validation rule so we do not add it + if (minimumTimeBetweenBlocks.compareTo(Duration.ofSeconds(1)) >= 0) { + ruleBuilder.addRule(new TimestampMoreRecentThanParent(minimumTimeBetweenBlocks.getSeconds())); + } + return ruleBuilder; } } diff --git a/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/IbftProtocolScheduleBuilder.java b/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/IbftProtocolScheduleBuilder.java index 0789f2e8981..3adf5718955 100644 --- a/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/IbftProtocolScheduleBuilder.java +++ b/consensus/ibft/src/main/java/org/hyperledger/besu/consensus/ibft/IbftProtocolScheduleBuilder.java @@ -29,6 +29,7 @@ import org.hyperledger.besu.evm.internal.EvmConfiguration; import org.hyperledger.besu.plugin.services.MetricsSystem; +import java.time.Duration; import java.util.Optional; /** Defines the protocol behaviours for a blockchain using a BFT consensus mechanism. */ @@ -120,6 +121,9 @@ protected BlockHeaderValidator.Builder createBlockHeaderRuleset( Optional.of(feeMarket).filter(FeeMarket::implementsBaseFee).map(BaseFeeMarket.class::cast); return IbftBlockHeaderValidationRulesetFactory.blockHeaderValidator( - config.getBlockPeriodSeconds(), baseFeeMarket); + config.getBlockPeriodMilliseconds() > 0 + ? Duration.ofMillis(config.getBlockPeriodMilliseconds()) + : Duration.ofSeconds(config.getBlockPeriodSeconds()), + baseFeeMarket); } } diff --git a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java index a7deb9e1973..160fc2a2f39 100644 --- a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java +++ b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/IbftBlockHeaderValidationRulesetFactoryTest.java @@ -38,6 +38,7 @@ import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; +import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -93,7 +94,7 @@ public void bftValidateHeaderPassesWithBaseFee() { final BlockHeaderValidator validator = IbftBlockHeaderValidationRulesetFactory.blockHeaderValidator( - 5, Optional.of(FeeMarket.london(1))) + Duration.ofSeconds(5), Optional.of(FeeMarket.london(1))) .build(); assertThat( @@ -372,7 +373,8 @@ private BlockHeaderTestFixture getPresetHeaderBuilder( } public BlockHeaderValidator getBlockHeaderValidator() { - return IbftBlockHeaderValidationRulesetFactory.blockHeaderValidator(5, Optional.empty()) + return IbftBlockHeaderValidationRulesetFactory.blockHeaderValidator( + Duration.ofSeconds(5), Optional.empty()) .build(); } } diff --git a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/blockcreation/BftBlockCreatorTest.java b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/blockcreation/BftBlockCreatorTest.java index 8b8406b6385..5469717b13f 100644 --- a/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/blockcreation/BftBlockCreatorTest.java +++ b/consensus/ibft/src/test/java/org/hyperledger/besu/consensus/ibft/blockcreation/BftBlockCreatorTest.java @@ -64,6 +64,7 @@ import org.hyperledger.besu.testutil.DeterministicEthScheduler; import org.hyperledger.besu.testutil.TestClock; +import java.time.Duration; import java.time.ZoneId; import java.util.Collections; import java.util.List; @@ -105,7 +106,7 @@ public void createdBlockPassesValidationRulesAndHasAppropriateHashAndMixHash() { public BlockHeaderValidator.Builder createBlockHeaderRuleset( final BftConfigOptions config, final FeeMarket feeMarket) { return IbftBlockHeaderValidationRulesetFactory.blockHeaderValidator( - 5, Optional.empty()); + Duration.ofSeconds(5), Optional.empty()); } }; final GenesisConfigOptions configOptions = @@ -200,7 +201,7 @@ public BlockHeaderValidator.Builder createBlockHeaderRuleset( final BlockHeaderValidator rules = IbftBlockHeaderValidationRulesetFactory.blockHeaderValidator( - secondsBetweenBlocks, Optional.empty()) + Duration.ofSeconds(secondsBetweenBlocks), Optional.empty()) .build(); // NOTE: The header will not contain commit seals, so can only do light validation on header. diff --git a/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/support/TestContextBuilder.java b/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/support/TestContextBuilder.java index 8906f0de7f6..d90d5a15277 100644 --- a/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/support/TestContextBuilder.java +++ b/consensus/qbft/src/integration-test/java/org/hyperledger/besu/consensus/qbft/support/TestContextBuilder.java @@ -118,6 +118,7 @@ import java.io.IOException; import java.nio.file.Path; import java.time.Clock; +import java.time.Duration; import java.time.Instant; import java.time.ZoneId; import java.util.ArrayList; @@ -512,7 +513,7 @@ private static ControllerAndState createControllerAndFinalState( Util.publicKeyToAddress(nodeKey.getPublicKey()), proposerSelector, multicaster, - new RoundTimer(bftEventQueue, ROUND_TIMER_SEC, bftExecutors), + new RoundTimer(bftEventQueue, Duration.ofSeconds(ROUND_TIMER_SEC), bftExecutors), new BlockTimer(bftEventQueue, forksSchedule, bftExecutors, TestClock.fixed()), blockCreatorFactory, clock); diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java index 7320dfaaf7a..ac5ba3ac23a 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactory.java @@ -31,6 +31,7 @@ import org.hyperledger.besu.ethereum.mainnet.headervalidationrules.TimestampBoundedByFutureParameter; import org.hyperledger.besu.ethereum.mainnet.headervalidationrules.TimestampMoreRecentThanParent; +import java.time.Duration; import java.util.Optional; import org.apache.tuweni.units.bigints.UInt256; @@ -44,31 +45,42 @@ private QbftBlockHeaderValidationRulesetFactory() {} * Produces a BlockHeaderValidator configured for assessing bft block headers which are to form * part of the BlockChain (i.e. not proposed blocks, which do not contain commit seals) * - * @param secondsBetweenBlocks the minimum number of seconds which must elapse between blocks. + * @param minimumTimeBetweenBlocks the minimum amount of time that must elapse between blocks. * @param useValidatorContract whether validator selection is using a validator contract * @param baseFeeMarket an {@link Optional} wrapping {@link BaseFeeMarket} class if appropriate. * @return BlockHeaderValidator configured for assessing bft block headers */ public static BlockHeaderValidator.Builder blockHeaderValidator( - final long secondsBetweenBlocks, + final Duration minimumTimeBetweenBlocks, final boolean useValidatorContract, final Optional baseFeeMarket) { - return new BlockHeaderValidator.Builder() - .addRule(new AncestryValidationRule()) - .addRule(new GasUsageValidationRule()) - .addRule( - new GasLimitRangeAndDeltaValidationRule( - DEFAULT_MIN_GAS_LIMIT, DEFAULT_MAX_GAS_LIMIT, baseFeeMarket)) - .addRule(new TimestampBoundedByFutureParameter(1)) - .addRule(new TimestampMoreRecentThanParent(secondsBetweenBlocks)) - .addRule( - new ConstantFieldValidationRule<>( - "MixHash", BlockHeader::getMixHash, BftHelpers.EXPECTED_MIX_HASH)) - .addRule( - new ConstantFieldValidationRule<>( - "Difficulty", BlockHeader::getDifficulty, UInt256.ONE)) - .addRule(new QbftValidatorsValidationRule(useValidatorContract)) - .addRule(new BftCoinbaseValidationRule()) - .addRule(new BftCommitSealsValidationRule()); + BlockHeaderValidator.Builder ruleBuilder = + new BlockHeaderValidator.Builder() + .addRule(new AncestryValidationRule()) + .addRule(new GasUsageValidationRule()) + .addRule( + new GasLimitRangeAndDeltaValidationRule( + DEFAULT_MIN_GAS_LIMIT, DEFAULT_MAX_GAS_LIMIT, baseFeeMarket)) + .addRule(new TimestampBoundedByFutureParameter(1)) + .addRule( + new ConstantFieldValidationRule<>( + "MixHash", BlockHeader::getMixHash, BftHelpers.EXPECTED_MIX_HASH)) + .addRule( + new ConstantFieldValidationRule<>( + "Difficulty", BlockHeader::getDifficulty, UInt256.ONE)) + .addRule(new QbftValidatorsValidationRule(useValidatorContract)) + .addRule(new BftCoinbaseValidationRule()) + .addRule(new BftCommitSealsValidationRule()); + + // Currently the minimum acceptable time between blocks is 1 second. The timestamp of an + // Ethereum header is stored as seconds since Unix epoch so blocks being produced more + // frequently than once a second cannot pass this validator. For non-production scenarios + // (e.g. for testing block production much more frequently than once a second) Besu has + // an experimental 'xblockperiodmilliseconds' option for BFT chains. If this is enabled + // we cannot apply the TimestampMoreRecentThanParent validation rule so we do not add it + if (minimumTimeBetweenBlocks.compareTo(Duration.ofSeconds(1)) >= 0) { + ruleBuilder.addRule(new TimestampMoreRecentThanParent(minimumTimeBetweenBlocks.getSeconds())); + } + return ruleBuilder; } } diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftForksSchedulesFactory.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftForksSchedulesFactory.java index 90448f62015..0f0d467de37 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftForksSchedulesFactory.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftForksSchedulesFactory.java @@ -49,6 +49,7 @@ private static QbftConfigOptions createQbftConfigOptions( new MutableQbftConfigOptions(lastSpec.getValue()); fork.getBlockPeriodSeconds().ifPresent(bftConfigOptions::setBlockPeriodSeconds); + fork.getBlockPeriodMilliseconds().ifPresent(bftConfigOptions::setBlockPeriodMilliseconds); fork.getBlockRewardWei().ifPresent(bftConfigOptions::setBlockRewardWei); if (fork.isMiningBeneficiaryConfigured()) { diff --git a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftProtocolScheduleBuilder.java b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftProtocolScheduleBuilder.java index 44c7ddfba8c..e1cbc134b6f 100644 --- a/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftProtocolScheduleBuilder.java +++ b/consensus/qbft/src/main/java/org/hyperledger/besu/consensus/qbft/QbftProtocolScheduleBuilder.java @@ -33,6 +33,7 @@ import org.hyperledger.besu.evm.internal.EvmConfiguration; import org.hyperledger.besu.plugin.services.MetricsSystem; +import java.time.Duration; import java.util.Optional; /** Defines the protocol behaviours for a blockchain using a QBFT consensus mechanism. */ @@ -164,7 +165,9 @@ protected BlockHeaderValidator.Builder createBlockHeaderRuleset( Optional.of(feeMarket).filter(FeeMarket::implementsBaseFee).map(BaseFeeMarket.class::cast); return QbftBlockHeaderValidationRulesetFactory.blockHeaderValidator( - qbftConfigOptions.getBlockPeriodSeconds(), + qbftConfigOptions.getBlockPeriodMilliseconds() > 0 + ? Duration.ofMillis(qbftConfigOptions.getBlockPeriodMilliseconds()) + : Duration.ofSeconds(qbftConfigOptions.getBlockPeriodSeconds()), qbftConfigOptions.isValidatorContractMode(), baseFeeMarket); } diff --git a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactoryTest.java b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactoryTest.java index 4771cf91cbd..ae1fbc0f19b 100644 --- a/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactoryTest.java +++ b/consensus/qbft/src/test/java/org/hyperledger/besu/consensus/qbft/QbftBlockHeaderValidationRulesetFactoryTest.java @@ -34,6 +34,7 @@ import org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; +import java.time.Duration; import java.util.Collection; import java.util.List; import java.util.Optional; @@ -92,7 +93,7 @@ public void bftValidateHeaderPassesWithBaseFee() { final BlockHeaderValidator validator = QbftBlockHeaderValidationRulesetFactory.blockHeaderValidator( - 5, false, Optional.of(FeeMarket.london(1))) + Duration.ofSeconds(5), false, Optional.of(FeeMarket.london(1))) .build(); assertThat( @@ -366,7 +367,8 @@ blockHeader, parentHeader, protocolContext(validators), HeaderValidationMode.FUL } public BlockHeaderValidator getBlockHeaderValidator() { - return QbftBlockHeaderValidationRulesetFactory.blockHeaderValidator(5, false, Optional.empty()) + return QbftBlockHeaderValidationRulesetFactory.blockHeaderValidator( + Duration.ofSeconds(5), false, Optional.empty()) .build(); } } From 3dbe60617249446222fa786e9bb82ea3311a361a Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Fri, 20 Sep 2024 15:10:59 +0200 Subject: [PATCH 14/37] Wait for Besu to be up before running Goss Docker test 02 (#7655) Signed-off-by: Fabio Di Fabio --- docker/tests/02/goss_wait.yaml | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docker/tests/02/goss_wait.yaml diff --git a/docker/tests/02/goss_wait.yaml b/docker/tests/02/goss_wait.yaml new file mode 100644 index 00000000000..f6b397c6188 --- /dev/null +++ b/docker/tests/02/goss_wait.yaml @@ -0,0 +1,7 @@ +--- +# runtime docker tests for interfaces & ports +port: + tcp:30303: + listening: true + ip: + - 0.0.0.0 From 676e1f8c1344edd3009eba6f22a46287b4e98bdb Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Fri, 20 Sep 2024 15:52:12 +0200 Subject: [PATCH 15/37] Rebalance GHA runners to reduce ATs failure and speedup unit tests (#7656) Signed-off-by: Fabio Di Fabio --- .github/workflows/acceptance-tests.yml | 2 +- .github/workflows/pre-review.yml | 2 +- .github/workflows/reference-tests.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index a8f6981d896..27d0020b2c5 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -12,7 +12,7 @@ concurrency: env: GRADLE_OPTS: "-Xmx7g" - total-runners: 12 + total-runners: 14 jobs: acceptanceTestEthereum: diff --git a/.github/workflows/pre-review.yml b/.github/workflows/pre-review.yml index cba13f1ebda..35e8956f1db 100644 --- a/.github/workflows/pre-review.yml +++ b/.github/workflows/pre-review.yml @@ -12,7 +12,7 @@ concurrency: env: GRADLE_OPTS: "-Xmx6g -Dorg.gradle.parallel=true" - total-runners: 8 + total-runners: 10 jobs: repolint: diff --git a/.github/workflows/reference-tests.yml b/.github/workflows/reference-tests.yml index 4e458e057e3..435cbba0f8b 100644 --- a/.github/workflows/reference-tests.yml +++ b/.github/workflows/reference-tests.yml @@ -8,7 +8,7 @@ on: env: GRADLE_OPTS: "-Xmx6g -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.caching=true" - total-runners: 10 + total-runners: 8 concurrency: group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} From f1da4e77e6fc7bd9eefc61b0f82d367962debb1b Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Fri, 20 Sep 2024 16:31:37 +0200 Subject: [PATCH 16/37] Rebalance GHA runners to reduce ATs failure and speedup unit tests [part 2] (#7658) Signed-off-by: Fabio Di Fabio --- .github/workflows/acceptance-tests.yml | 2 +- .github/workflows/pre-review.yml | 2 +- .github/workflows/reference-tests.yml | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/acceptance-tests.yml b/.github/workflows/acceptance-tests.yml index 27d0020b2c5..e1549b2357d 100644 --- a/.github/workflows/acceptance-tests.yml +++ b/.github/workflows/acceptance-tests.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: true matrix: - runner_index: [0,1,2,3,4,5,6,7,8,9,10,11] + runner_index: [0,1,2,3,4,5,6,7,8,9,10,11,12,13] steps: - name: Checkout Repo uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 diff --git a/.github/workflows/pre-review.yml b/.github/workflows/pre-review.yml index 35e8956f1db..93bfcfa9f50 100644 --- a/.github/workflows/pre-review.yml +++ b/.github/workflows/pre-review.yml @@ -83,7 +83,7 @@ jobs: strategy: fail-fast: true matrix: - runner_index: [0,1,2,3,4,5,6,7] + runner_index: [0,1,2,3,4,5,6,7,8,9] steps: - name: Checkout Repo uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 diff --git a/.github/workflows/reference-tests.yml b/.github/workflows/reference-tests.yml index 435cbba0f8b..d27114ebc1e 100644 --- a/.github/workflows/reference-tests.yml +++ b/.github/workflows/reference-tests.yml @@ -24,7 +24,7 @@ jobs: strategy: fail-fast: true matrix: - runner_index: [1,2,3,4,5,6,7,8,9,10] + runner_index: [1,2,3,4,5,6,7,8] steps: - name: Checkout Repo uses: actions/checkout@b4ffde65f46336ab88eb53be808477a3936bae11 From 3e0e5cdc1ffa0888ebba96e0edb3a9bdf18eb17e Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Fri, 20 Sep 2024 17:11:56 +0200 Subject: [PATCH 17/37] Fix reading tx-pool-min-score option from configuration file (#7623) Signed-off-by: Fabio Di Fabio --- CHANGELOG.md | 1 + .../util/TomlConfigurationDefaultProvider.java | 6 +++++- .../options/TransactionPoolOptionsTest.java | 18 ++++++++++++++++++ 3 files changed, 24 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 246df1d91cd..96e1511eead 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -16,6 +16,7 @@ - Fix for `debug_traceCall` to handle transactions without specified gas price. [#7510](https://github.com/hyperledger/besu/pull/7510) - Corrects a regression where custom plugin services are not initialized correctly. [#7625](https://github.com/hyperledger/besu/pull/7625) - Fix for IBFT2 chains using the BONSAI DB format [#7631](https://github.com/hyperledger/besu/pull/7631) +- Fix reading `tx-pool-min-score` option from configuration file [#7623](https://github.com/hyperledger/besu/pull/7623) ## 24.9.1 diff --git a/besu/src/main/java/org/hyperledger/besu/cli/util/TomlConfigurationDefaultProvider.java b/besu/src/main/java/org/hyperledger/besu/cli/util/TomlConfigurationDefaultProvider.java index ca76b40b686..636be329d43 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/util/TomlConfigurationDefaultProvider.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/util/TomlConfigurationDefaultProvider.java @@ -120,7 +120,11 @@ private String getConfigurationValue(final OptionSpec optionSpec) { } private boolean isNumericType(final Class type) { - return type.equals(Integer.class) + return type.equals(Byte.class) + || type.equals(byte.class) + || type.equals(Short.class) + || type.equals(short.class) + || type.equals(Integer.class) || type.equals(int.class) || type.equals(Long.class) || type.equals(long.class) diff --git a/besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java b/besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java index eb48a3f2a5c..37acbe167ff 100644 --- a/besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java +++ b/besu/src/test/java/org/hyperledger/besu/cli/options/TransactionPoolOptionsTest.java @@ -427,6 +427,24 @@ public void minScoreWorks() { Byte.toString(minScore)); } + @Test + public void minScoreWorksConfigFile() throws IOException { + final byte minScore = -10; + final Path tempConfigFilePath = + createTempFile( + "config", + String.format( + """ + tx-pool-min-score=%s + """, + minScore)); + + internalTestSuccess( + config -> assertThat(config.getMinScore()).isEqualTo(minScore), + "--config-file", + tempConfigFilePath.toString()); + } + @Test public void minScoreNonByteValueReturnError() { final var overflowMinScore = Integer.toString(-300); From 6e246ca4b0be04fe14f67a25cde895c127feeec4 Mon Sep 17 00:00:00 2001 From: Cooper Mosawi Date: Sat, 21 Sep 2024 05:13:16 +1200 Subject: [PATCH 18/37] fix opentelemetry (#7649) Signed-off-by: Blue Co-authored-by: Fabio Di Fabio --- docs/tracing/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/tracing/README.md b/docs/tracing/README.md index 77deadd16aa..3a3f9fa435f 100644 --- a/docs/tracing/README.md +++ b/docs/tracing/README.md @@ -1,6 +1,6 @@ # Tracing -Hyperledger Besu integrates with the [open-telemetry](https://open-telemetry.io) project to integrate tracing reporting. +Hyperledger Besu integrates with the [open-telemetry](https://opentelemetry.io/) project to integrate tracing reporting. This allows to report all JSON-RPC traffic as traces. From 1751a77f98cc6cdf218cd80f429e15ccd8e01e67 Mon Sep 17 00:00:00 2001 From: daniellehrner Date: Sat, 21 Sep 2024 13:28:01 +0200 Subject: [PATCH 19/37] 7702 validation checks v2 (#7653) * yParity is valid up to 2**256 as well Signed-off-by: Daniel Lehrner --- .../besu/crypto/AbstractSECP256.java | 2 +- .../besu/crypto/CodeDelegationSignature.java | 15 ++++++++--- .../besu/crypto/SignatureAlgorithm.java | 2 +- .../crypto/CodeDelegationSignatureTest.java | 27 +++++++++++++------ .../CodeDelegationTransactionDecoder.java | 2 +- 5 files changed, 33 insertions(+), 15 deletions(-) diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java index ce376512668..bd450b206e9 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/AbstractSECP256.java @@ -214,7 +214,7 @@ public SECPSignature createSignature(final BigInteger r, final BigInteger s, fin @Override public CodeDelegationSignature createCodeDelegationSignature( - final BigInteger r, final BigInteger s, final long yParity) { + final BigInteger r, final BigInteger s, final BigInteger yParity) { return CodeDelegationSignature.create(r, s, yParity); } diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/CodeDelegationSignature.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/CodeDelegationSignature.java index 4bb2e4653e2..06ec72bf0a9 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/CodeDelegationSignature.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/CodeDelegationSignature.java @@ -42,18 +42,25 @@ public CodeDelegationSignature(final BigInteger r, final BigInteger s, final byt * @return the new CodeDelegationSignature */ public static CodeDelegationSignature create( - final BigInteger r, final BigInteger s, final long yParity) { + final BigInteger r, final BigInteger s, final BigInteger yParity) { checkNotNull(r); checkNotNull(s); if (r.compareTo(TWO_POW_256) >= 0) { - throw new IllegalArgumentException("Invalid 'r' value, should be < 2^256 but got " + r); + throw new IllegalArgumentException( + "Invalid 'r' value, should be < 2^256 but got " + r.toString(16)); } if (s.compareTo(TWO_POW_256) >= 0) { - throw new IllegalArgumentException("Invalid 's' value, should be < 2^256 but got " + s); + throw new IllegalArgumentException( + "Invalid 's' value, should be < 2^256 but got " + s.toString(16)); } - return new CodeDelegationSignature(r, s, (byte) yParity); + if (yParity.compareTo(TWO_POW_256) >= 0) { + throw new IllegalArgumentException( + "Invalid 'yParity' value, should be < 2^256 but got " + yParity.toString(16)); + } + + return new CodeDelegationSignature(r, s, yParity.byteValue()); } } diff --git a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java index 8e19b608544..4bf8d89c825 100644 --- a/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java +++ b/crypto/algorithms/src/main/java/org/hyperledger/besu/crypto/SignatureAlgorithm.java @@ -224,7 +224,7 @@ Optional recoverPublicKeyFromSignature( * @return the code delegation signature */ CodeDelegationSignature createCodeDelegationSignature( - final BigInteger r, final BigInteger s, final long yParity); + final BigInteger r, final BigInteger s, final BigInteger yParity); /** * Decode secp signature. diff --git a/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/CodeDelegationSignatureTest.java b/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/CodeDelegationSignatureTest.java index 1cc66966a78..332aa14893f 100644 --- a/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/CodeDelegationSignatureTest.java +++ b/crypto/algorithms/src/test/java/org/hyperledger/besu/crypto/CodeDelegationSignatureTest.java @@ -29,19 +29,19 @@ class CodeDelegationSignatureTest { void testValidInputs() { BigInteger r = BigInteger.ONE; BigInteger s = BigInteger.TEN; - long yParity = 1L; + BigInteger yParity = BigInteger.ONE; CodeDelegationSignature result = CodeDelegationSignature.create(r, s, yParity); assertThat(r).isEqualTo(result.getR()); assertThat(s).isEqualTo(result.getS()); - assertThat((byte) yParity).isEqualTo(result.getRecId()); + assertThat(yParity.byteValue()).isEqualTo(result.getRecId()); } @Test void testNullRValue() { BigInteger s = BigInteger.TEN; - long yParity = 0L; + BigInteger yParity = BigInteger.ZERO; assertThatExceptionOfType(NullPointerException.class) .isThrownBy(() -> CodeDelegationSignature.create(null, s, yParity)); @@ -50,7 +50,7 @@ void testNullRValue() { @Test void testNullSValue() { BigInteger r = BigInteger.ONE; - long yParity = 0L; + BigInteger yParity = BigInteger.ZERO; assertThatExceptionOfType(NullPointerException.class) .isThrownBy(() -> CodeDelegationSignature.create(r, null, yParity)); @@ -60,7 +60,7 @@ void testNullSValue() { void testRValueExceedsTwoPow256() { BigInteger r = TWO_POW_256; BigInteger s = BigInteger.TEN; - long yParity = 0L; + BigInteger yParity = BigInteger.ZERO; assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> CodeDelegationSignature.create(r, s, yParity)) @@ -71,23 +71,34 @@ void testRValueExceedsTwoPow256() { void testSValueExceedsTwoPow256() { BigInteger r = BigInteger.ONE; BigInteger s = TWO_POW_256; - long yParity = 0L; + BigInteger yParity = BigInteger.ZERO; assertThatExceptionOfType(IllegalArgumentException.class) .isThrownBy(() -> CodeDelegationSignature.create(r, s, yParity)) .withMessageContainingAll("Invalid 's' value, should be < 2^256"); } + @Test + void testYParityExceedsTwoPow256() { + BigInteger r = BigInteger.ONE; + BigInteger s = BigInteger.TWO; + BigInteger yParity = TWO_POW_256; + + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> CodeDelegationSignature.create(r, s, yParity)) + .withMessageContainingAll("Invalid 'yParity' value, should be < 2^256"); + } + @Test void testValidYParityZero() { BigInteger r = BigInteger.ONE; BigInteger s = BigInteger.TEN; - long yParity = 0L; + BigInteger yParity = BigInteger.ZERO; CodeDelegationSignature result = CodeDelegationSignature.create(r, s, yParity); assertThat(r).isEqualTo(result.getR()); assertThat(s).isEqualTo(result.getS()); - assertThat((byte) yParity).isEqualTo(result.getRecId()); + assertThat(yParity.byteValue()).isEqualTo(result.getRecId()); } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java index d3ef60bfc41..6448940d8d5 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/core/encoding/CodeDelegationTransactionDecoder.java @@ -81,7 +81,7 @@ public static CodeDelegation decodeInnerPayload(final RLPInput input) { final Address address = Address.wrap(input.readBytes()); final long nonce = input.readLongScalar(); - final long yParity = input.readUnsignedIntScalar(); + final BigInteger yParity = input.readUInt256Scalar().toUnsignedBigInteger(); final BigInteger r = input.readUInt256Scalar().toUnsignedBigInteger(); final BigInteger s = input.readUInt256Scalar().toUnsignedBigInteger(); From 9d689b940134b4f7a9b80020829d506417c2065e Mon Sep 17 00:00:00 2001 From: Sally MacFarlane Date: Sun, 22 Sep 2024 06:45:09 +1000 Subject: [PATCH 20/37] remove integration tests related to privacy (#7645) * remove integration tests related to privacy Signed-off-by: Sally MacFarlane --------- Signed-off-by: Sally MacFarlane --- .../hyperledger/besu/enclave/EnclaveTest.java | 215 --------------- .../enclave/TlsCertificateDefinition.java | 52 ---- .../besu/enclave/TlsEnabledEnclaveTest.java | 144 ----------- .../enclave/TlsEnabledHttpServerFactory.java | 109 -------- .../hyperledger/besu/enclave/TlsHelpers.java | 98 ------- ...vGetPrivateTransactionIntegrationTest.java | 192 -------------- ...acyPrecompiledContractIntegrationTest.java | 244 ------------------ testutil/src/main/resources/enclave_key_0.key | 1 - 8 files changed, 1055 deletions(-) delete mode 100644 enclave/src/integration-test/java/org/hyperledger/besu/enclave/EnclaveTest.java delete mode 100644 enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsCertificateDefinition.java delete mode 100644 enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledEnclaveTest.java delete mode 100644 enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledHttpServerFactory.java delete mode 100644 enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsHelpers.java delete mode 100644 ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/fork/frontier/PrivGetPrivateTransactionIntegrationTest.java delete mode 100644 ethereum/core/src/integration-test/java/org/hyperledger/besu/ethereum/mainnet/precompiles/privacy/PrivacyPrecompiledContractIntegrationTest.java delete mode 100644 testutil/src/main/resources/enclave_key_0.key diff --git a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/EnclaveTest.java b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/EnclaveTest.java deleted file mode 100644 index b9b657e6689..00000000000 --- a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/EnclaveTest.java +++ /dev/null @@ -1,215 +0,0 @@ -/* - * 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.enclave; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; - -import org.hyperledger.besu.enclave.types.PrivacyGroup; -import org.hyperledger.besu.enclave.types.ReceiveResponse; -import org.hyperledger.besu.enclave.types.SendResponse; -import org.hyperledger.enclave.testutil.EnclaveEncryptorType; -import org.hyperledger.enclave.testutil.EnclaveKeyConfiguration; -import org.hyperledger.enclave.testutil.TesseraTestHarness; -import org.hyperledger.enclave.testutil.TesseraTestHarnessFactory; - -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -import com.google.common.collect.Lists; -import io.vertx.core.Vertx; -import org.awaitility.Awaitility; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -public class EnclaveTest { - - @TempDir private static Path folder; - - private static final String PAYLOAD = "a wonderful transaction"; - private static final String MOCK_KEY = "iOCzoGo5kwtZU0J41Z9xnGXHN6ZNukIa9MspvHtu3Jk="; - private Enclave enclave; - private Vertx vertx; - private EnclaveFactory factory; - - private TesseraTestHarness testHarness; - - @BeforeEach - public void setUp() throws Exception { - vertx = Vertx.vertx(); - factory = new EnclaveFactory(vertx); - - testHarness = - TesseraTestHarnessFactory.create( - "enclave", - Files.createTempDirectory(folder, "enclave"), - new EnclaveKeyConfiguration( - new String[] {"enclave_key_0.pub"}, - new String[] {"enclave_key_0.key"}, - EnclaveEncryptorType.NOOP), - Optional.empty()); - - testHarness.start(); - - enclave = factory.createVertxEnclave(testHarness.clientUrl()); - } - - @AfterEach - public void tearDown() { - testHarness.close(); - vertx.close(); - } - - @Test - public void testUpCheck() { - assertThat(enclave.upCheck()).isTrue(); - } - - @Test - public void testReceiveThrowsWhenPayloadDoesNotExist() { - final String publicKey = testHarness.getDefaultPublicKey(); - - final Throwable t = catchThrowable(() -> enclave.receive(MOCK_KEY, publicKey)); - - assertThat(t.getMessage()).isEqualTo("Message with hash was not found"); - } - - @Test - public void testSendAndReceive() { - final List publicKeys = testHarness.getPublicKeys(); - - final SendResponse sr = - enclave.send(PAYLOAD, publicKeys.get(0), Lists.newArrayList(publicKeys.get(0))); - - final ReceiveResponse rr = enclave.receive(sr.getKey(), publicKeys.get(0)); - assertThat(rr).isNotNull(); - assertThat(new String(rr.getPayload(), UTF_8)).isEqualTo(PAYLOAD); - assertThat(rr.getPrivacyGroupId()).isNotNull(); - } - - @Test - public void testSendWithPrivacyGroupAndReceive() { - final List publicKeys = testHarness.getPublicKeys(); - - final PrivacyGroup privacyGroupResponse = - enclave.createPrivacyGroup(publicKeys, publicKeys.get(0), "", ""); - - final SendResponse sr = - enclave.send(PAYLOAD, publicKeys.get(0), privacyGroupResponse.getPrivacyGroupId()); - - final ReceiveResponse rr = enclave.receive(sr.getKey(), publicKeys.get(0)); - assertThat(rr).isNotNull(); - assertThat(new String(rr.getPayload(), UTF_8)).isEqualTo(PAYLOAD); - assertThat(rr.getPrivacyGroupId()).isNotNull(); - } - - @Test - public void testCreateAndDeletePrivacyGroup() { - final List publicKeys = testHarness.getPublicKeys(); - final String name = "testName"; - final String description = "testDesc"; - - final PrivacyGroup privacyGroupResponse = - enclave.createPrivacyGroup(publicKeys, publicKeys.get(0), name, description); - - assertThat(privacyGroupResponse.getPrivacyGroupId()).isNotNull(); - assertThat(privacyGroupResponse.getName()).isEqualTo(name); - assertThat(privacyGroupResponse.getDescription()).isEqualTo(description); - assertThat(privacyGroupResponse.getType()).isEqualByComparingTo(PrivacyGroup.Type.PANTHEON); - - final String response = - enclave.deletePrivacyGroup(privacyGroupResponse.getPrivacyGroupId(), publicKeys.get(0)); - - assertThat(privacyGroupResponse.getPrivacyGroupId()).isEqualTo(response); - } - - @Test - public void testCreateFindDeleteFindPrivacyGroup() { - final List publicKeys = testHarness.getPublicKeys(); - final String name = "name"; - final String description = "desc"; - - final PrivacyGroup privacyGroupResponse = - enclave.createPrivacyGroup(publicKeys, publicKeys.get(0), name, description); - - assertThat(privacyGroupResponse.getPrivacyGroupId()).isNotNull(); - assertThat(privacyGroupResponse.getName()).isEqualTo(name); - assertThat(privacyGroupResponse.getDescription()).isEqualTo(description); - assertThat(privacyGroupResponse.getType()).isEqualTo(PrivacyGroup.Type.PANTHEON); - - Awaitility.await() - .atMost(5, TimeUnit.SECONDS) - .untilAsserted( - () -> { - final PrivacyGroup[] findPrivacyGroupResponse = enclave.findPrivacyGroup(publicKeys); - - assertThat(findPrivacyGroupResponse.length).isEqualTo(1); - assertThat(findPrivacyGroupResponse[0].getPrivacyGroupId()) - .isEqualTo(privacyGroupResponse.getPrivacyGroupId()); - }); - - final String response = - enclave.deletePrivacyGroup(privacyGroupResponse.getPrivacyGroupId(), publicKeys.get(0)); - - assertThat(privacyGroupResponse.getPrivacyGroupId()).isEqualTo(response); - - Awaitility.await() - .atMost(5, TimeUnit.SECONDS) - .untilAsserted( - () -> { - final PrivacyGroup[] findPrivacyGroupResponse = enclave.findPrivacyGroup(publicKeys); - - assertThat(findPrivacyGroupResponse.length).isEqualTo(0); - }); - } - - @Test - public void testCreateDeleteRetrievePrivacyGroup() { - final List publicKeys = testHarness.getPublicKeys(); - final String name = "name"; - final String description = "desc"; - - final PrivacyGroup privacyGroupResponse = - enclave.createPrivacyGroup(publicKeys, publicKeys.get(0), name, description); - - assertThat(privacyGroupResponse.getPrivacyGroupId()).isNotNull(); - assertThat(privacyGroupResponse.getName()).isEqualTo(name); - assertThat(privacyGroupResponse.getDescription()).isEqualTo(description); - assertThat(privacyGroupResponse.getType()).isEqualTo(PrivacyGroup.Type.PANTHEON); - - final PrivacyGroup retrievePrivacyGroup = - enclave.retrievePrivacyGroup(privacyGroupResponse.getPrivacyGroupId()); - - assertThat(retrievePrivacyGroup).usingRecursiveComparison().isEqualTo(privacyGroupResponse); - - final String response = - enclave.deletePrivacyGroup(privacyGroupResponse.getPrivacyGroupId(), publicKeys.get(0)); - - assertThat(privacyGroupResponse.getPrivacyGroupId()).isEqualTo(response); - } - - @Test - public void upcheckReturnsFalseIfNoResponseReceived() throws URISyntaxException { - assertThat(factory.createVertxEnclave(new URI("http://8.8.8.8:65535")).upCheck()).isFalse(); - } -} diff --git a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsCertificateDefinition.java b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsCertificateDefinition.java deleted file mode 100644 index ad1271e9200..00000000000 --- a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsCertificateDefinition.java +++ /dev/null @@ -1,52 +0,0 @@ -/* - * 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.enclave; - -import java.io.File; -import java.net.URL; -import java.nio.file.Path; - -import com.google.common.io.Resources; - -public class TlsCertificateDefinition { - - private final File pkcs12File; - private final String password; - - public static TlsCertificateDefinition loadFromResource( - final String resourcePath, final String password) { - try { - final URL sslCertificate = Resources.getResource(resourcePath); - final Path keystorePath = Path.of(sslCertificate.getPath()); - - return new TlsCertificateDefinition(keystorePath.toFile(), password); - } catch (final Exception e) { - throw new RuntimeException("Failed to load TLS certificates", e); - } - } - - public TlsCertificateDefinition(final File pkcs12File, final String password) { - this.pkcs12File = pkcs12File; - this.password = password; - } - - public File getPkcs12File() { - return pkcs12File; - } - - public String getPassword() { - return password; - } -} diff --git a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledEnclaveTest.java b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledEnclaveTest.java deleted file mode 100644 index b5947793060..00000000000 --- a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledEnclaveTest.java +++ /dev/null @@ -1,144 +0,0 @@ -/* - * 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.enclave; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.fail; -import static org.hyperledger.besu.enclave.TlsHelpers.populateFingerprintFile; - -import java.io.IOException; -import java.net.URI; -import java.net.URISyntaxException; -import java.nio.charset.Charset; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.util.Optional; - -import io.vertx.core.Vertx; -import io.vertx.core.http.HttpServer; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class TlsEnabledEnclaveTest { - - private TlsEnabledHttpServerFactory serverFactory; - private Vertx vertx; - - final TlsCertificateDefinition httpServerCert = - TlsCertificateDefinition.loadFromResource("tls/cert1.pfx", "password"); - final TlsCertificateDefinition besuCert = - TlsCertificateDefinition.loadFromResource("tls/cert2.pfx", "password2"); - - public void shutdown() { - vertx.close(); - } - - @BeforeEach - public void setup() { - serverFactory = new TlsEnabledHttpServerFactory(); - this.vertx = Vertx.vertx(); - } - - @AfterEach - public void cleanup() { - serverFactory.shutdown(); - this.shutdown(); - } - - private Enclave createEnclave( - final int httpServerPort, final Path workDir, final boolean tlsEnabled) throws IOException { - - final Path serverFingerprintFile = workDir.resolve("server_known_clients"); - final Path besuCertPasswordFile = workDir.resolve("password_file"); - try { - populateFingerprintFile(serverFingerprintFile, httpServerCert, Optional.of(httpServerPort)); - Files.write(besuCertPasswordFile, besuCert.getPassword().getBytes(Charset.defaultCharset())); - - final EnclaveFactory factory = new EnclaveFactory(vertx); - if (tlsEnabled) { - final URI httpServerUri = new URI("https://localhost:" + httpServerPort); - return factory.createVertxEnclave( - httpServerUri, - besuCert.getPkcs12File().toPath(), - besuCertPasswordFile, - serverFingerprintFile); - } else { - return factory.createVertxEnclave(new URI("http://localhost:" + httpServerPort)); - } - } catch (KeyStoreException | NoSuchAlgorithmException | CertificateException e) { - fail("unable to populate fingerprint file"); - return null; - } catch (URISyntaxException e) { - fail("unable to create URI"); - return null; - } - } - - @Test - public void nonTlsEnclaveCannotConnectToTlsServer() throws IOException { - - Path workDir = Files.createTempDirectory("test-certs"); - - // Note: the HttpServer always responds with a JsonRpcSuccess, result="I'm up". - final HttpServer httpServer = serverFactory.create(httpServerCert, besuCert, workDir, true); - - final Enclave enclave = createEnclave(httpServer.actualPort(), workDir, false); - - assertThat(enclave.upCheck()).isEqualTo(false); - } - - @Test - public void nonTlsEnclaveCanConnectToNonTlsServer() throws IOException { - - Path workDir = Files.createTempDirectory("test-certs"); - - // Note: the HttpServer always responds with a JsonRpcSuccess, result="I'm up". - final HttpServer httpServer = serverFactory.create(httpServerCert, besuCert, workDir, false); - - final Enclave enclave = createEnclave(httpServer.actualPort(), workDir, false); - - assertThat(enclave.upCheck()).isEqualTo(true); - } - - @Test - public void tlsEnclaveCannotConnectToNonTlsServer() throws IOException { - - Path workDir = Files.createTempDirectory("test-certs"); - - // Note: the HttpServer always responds with a JsonRpcSuccess, result="I'm up!". - final HttpServer httpServer = serverFactory.create(httpServerCert, besuCert, workDir, false); - - final Enclave enclave = createEnclave(httpServer.actualPort(), workDir, true); - - assertThat(enclave.upCheck()).isEqualTo(false); - } - - @Test - public void tlsEnclaveCanConnectToTlsServer() throws IOException { - - Path workDir = Files.createTempDirectory("test-certs"); - - // Note: the HttpServer always responds with a JsonRpcSuccess, result="I'm up". - final HttpServer httpServer = serverFactory.create(httpServerCert, besuCert, workDir, true); - - final Enclave enclave = createEnclave(httpServer.actualPort(), workDir, true); - - assertThat(enclave.upCheck()).isEqualTo(true); - } -} diff --git a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledHttpServerFactory.java b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledHttpServerFactory.java deleted file mode 100644 index 7c67aeb7b9e..00000000000 --- a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsEnabledHttpServerFactory.java +++ /dev/null @@ -1,109 +0,0 @@ -/* - * 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.enclave; - -import static org.hyperledger.besu.enclave.TlsHelpers.populateFingerprintFile; - -import java.io.IOException; -import java.nio.file.Path; -import java.security.KeyStoreException; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateException; -import java.util.List; -import java.util.Optional; -import java.util.concurrent.CompletableFuture; -import java.util.concurrent.ExecutionException; - -import com.google.common.collect.Lists; -import io.netty.handler.codec.http.HttpHeaderValues; -import io.vertx.core.Vertx; -import io.vertx.core.http.ClientAuth; -import io.vertx.core.http.HttpMethod; -import io.vertx.core.http.HttpServer; -import io.vertx.core.http.HttpServerOptions; -import io.vertx.core.http.HttpServerResponse; -import io.vertx.core.net.PfxOptions; -import io.vertx.ext.web.Router; -import io.vertx.ext.web.RoutingContext; -import org.apache.tuweni.net.tls.VertxTrustOptions; - -class TlsEnabledHttpServerFactory { - - private final Vertx vertx; - private final List serversCreated = Lists.newArrayList(); - - TlsEnabledHttpServerFactory() { - this.vertx = Vertx.vertx(); - } - - void shutdown() { - serversCreated.forEach(HttpServer::close); - vertx.close(); - } - - HttpServer create( - final TlsCertificateDefinition serverCert, - final TlsCertificateDefinition acceptedClientCerts, - final Path workDir, - final boolean tlsEnabled) { - try { - - final Path serverFingerprintFile = workDir.resolve("server_known_clients"); - populateFingerprintFile(serverFingerprintFile, acceptedClientCerts, Optional.empty()); - - final HttpServerOptions web3HttpServerOptions = new HttpServerOptions(); - web3HttpServerOptions.setPort(0); - if (tlsEnabled) { - web3HttpServerOptions.setSsl(true); - web3HttpServerOptions.setClientAuth(ClientAuth.REQUIRED); - web3HttpServerOptions.setTrustOptions( - VertxTrustOptions.allowlistClients(serverFingerprintFile)); - web3HttpServerOptions.setPfxKeyCertOptions( - new PfxOptions() - .setPath(serverCert.getPkcs12File().toString()) - .setPassword(serverCert.getPassword())); - } - final Router router = Router.router(vertx); - router - .route(HttpMethod.GET, "/upcheck") - .produces(HttpHeaderValues.APPLICATION_JSON.toString()) - .handler(TlsEnabledHttpServerFactory::handleRequest); - - final HttpServer mockOrionHttpServer = vertx.createHttpServer(web3HttpServerOptions); - - final CompletableFuture serverConfigured = new CompletableFuture<>(); - mockOrionHttpServer.requestHandler(router).listen(result -> serverConfigured.complete(true)); - - serverConfigured.get(); - - serversCreated.add(mockOrionHttpServer); - return mockOrionHttpServer; - } catch (final KeyStoreException - | NoSuchAlgorithmException - | CertificateException - | IOException - | ExecutionException - | InterruptedException e) { - throw new RuntimeException("Failed to construct a TLS Enabled Server", e); - } - } - - private static void handleRequest(final RoutingContext context) { - final HttpServerResponse response = context.response(); - if (!response.closed()) { - response.end("I'm up!"); - } - } -} diff --git a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsHelpers.java b/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsHelpers.java deleted file mode 100644 index 09002f81151..00000000000 --- a/enclave/src/integration-test/java/org/hyperledger/besu/enclave/TlsHelpers.java +++ /dev/null @@ -1,98 +0,0 @@ -/* - * 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.enclave; - -import org.hyperledger.besu.crypto.MessageDigestFactory; - -import java.io.File; -import java.io.FileInputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.KeyStore; -import java.security.KeyStoreException; -import java.security.MessageDigest; -import java.security.NoSuchAlgorithmException; -import java.security.cert.CertificateEncodingException; -import java.security.cert.CertificateException; -import java.security.cert.X509Certificate; -import java.util.Enumeration; -import java.util.List; -import java.util.Locale; -import java.util.Optional; -import java.util.StringJoiner; - -import com.google.common.collect.Lists; - -public class TlsHelpers { - - private TlsHelpers() {} - - private static KeyStore loadP12KeyStore(final File pkcsFile, final String password) - throws KeyStoreException, NoSuchAlgorithmException, CertificateException { - final KeyStore store = KeyStore.getInstance("pkcs12"); - try (final InputStream keystoreStream = new FileInputStream(pkcsFile)) { - store.load(keystoreStream, password.toCharArray()); - } catch (IOException e) { - throw new RuntimeException("Unable to load keystore.", e); - } - return store; - } - - public static void populateFingerprintFile( - final Path knownClientsPath, - final TlsCertificateDefinition certDef, - final Optional serverPortToAppendToHostname) - throws KeyStoreException, NoSuchAlgorithmException, CertificateException, IOException { - - final List certs = getCertsFromPkcs12(certDef); - final StringBuilder fingerprintsToAdd = new StringBuilder(); - final String portFragment = serverPortToAppendToHostname.map(port -> ":" + port).orElse(""); - for (final X509Certificate cert : certs) { - final String fingerprint = generateFingerprint(cert); - fingerprintsToAdd.append(String.format("localhost%s %s%n", portFragment, fingerprint)); - fingerprintsToAdd.append(String.format("127.0.0.1%s %s%n", portFragment, fingerprint)); - } - Files.writeString(knownClientsPath, fingerprintsToAdd.toString()); - } - - @SuppressWarnings("JdkObsolete") // java.util.Enumeration is baked into the Keystore API - public static List getCertsFromPkcs12(final TlsCertificateDefinition certDef) - throws KeyStoreException, NoSuchAlgorithmException, CertificateException { - final List results = Lists.newArrayList(); - - final KeyStore p12 = loadP12KeyStore(certDef.getPkcs12File(), certDef.getPassword()); - final Enumeration aliases = p12.aliases(); - while (aliases.hasMoreElements()) { - results.add((X509Certificate) p12.getCertificate(aliases.nextElement())); - } - return results; - } - - private static String generateFingerprint(final X509Certificate cert) - throws NoSuchAlgorithmException, CertificateEncodingException { - final MessageDigest md = MessageDigestFactory.create(MessageDigestFactory.SHA256_ALG); - md.update(cert.getEncoded()); - final byte[] digest = md.digest(); - - final StringJoiner joiner = new StringJoiner(":"); - for (final byte b : digest) { - joiner.add(String.format("%02X", b)); - } - - return joiner.toString().toLowerCase(Locale.ROOT); - } -} diff --git a/ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/fork/frontier/PrivGetPrivateTransactionIntegrationTest.java b/ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/fork/frontier/PrivGetPrivateTransactionIntegrationTest.java deleted file mode 100644 index 9bb10629f31..00000000000 --- a/ethereum/api/src/integration-test/java/org/hyperledger/besu/ethereum/api/jsonrpc/methods/fork/frontier/PrivGetPrivateTransactionIntegrationTest.java +++ /dev/null @@ -1,192 +0,0 @@ -/* - * 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.api.jsonrpc.methods.fork.frontier; - -import static java.nio.charset.StandardCharsets.UTF_8; -import static org.assertj.core.api.Assertions.assertThat; -import static org.hyperledger.besu.ethereum.core.PrivateTransactionDataFixture.privateMarkerTransaction; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.crypto.KeyPair; -import org.hyperledger.besu.crypto.SignatureAlgorithm; -import org.hyperledger.besu.crypto.SignatureAlgorithmFactory; -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.datatypes.Wei; -import org.hyperledger.besu.enclave.Enclave; -import org.hyperledger.besu.enclave.EnclaveFactory; -import org.hyperledger.besu.enclave.types.SendResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequest; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.JsonRpcRequestContext; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.PrivacyIdProvider; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.privacy.methods.priv.PrivGetPrivateTransaction; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.response.JsonRpcSuccessResponse; -import org.hyperledger.besu.ethereum.api.jsonrpc.internal.results.privacy.PrivateTransactionLegacyResult; -import org.hyperledger.besu.ethereum.chain.Blockchain; -import org.hyperledger.besu.ethereum.chain.TransactionLocation; -import org.hyperledger.besu.ethereum.core.BlockHeader; -import org.hyperledger.besu.ethereum.core.Transaction; -import org.hyperledger.besu.ethereum.privacy.PrivacyController; -import org.hyperledger.besu.ethereum.privacy.PrivateTransaction; -import org.hyperledger.besu.ethereum.privacy.RestrictedDefaultPrivacyController; -import org.hyperledger.besu.ethereum.privacy.storage.PrivateStateStorage; -import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; -import org.hyperledger.besu.plugin.data.Restriction; -import org.hyperledger.enclave.testutil.EnclaveEncryptorType; -import org.hyperledger.enclave.testutil.EnclaveKeyConfiguration; -import org.hyperledger.enclave.testutil.TesseraTestHarness; -import org.hyperledger.enclave.testutil.TesseraTestHarnessFactory; - -import java.math.BigInteger; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Base64; -import java.util.Optional; - -import com.google.common.collect.Lists; -import io.vertx.core.Vertx; -import org.apache.tuweni.bytes.Bytes; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -public class PrivGetPrivateTransactionIntegrationTest { - - @TempDir private static Path folder; - private static final String ENCLAVE_PUBLIC_KEY = "A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="; - - private final PrivacyIdProvider privacyIdProvider = (user) -> ENCLAVE_PUBLIC_KEY; - private final PrivateStateStorage privateStateStorage = mock(PrivateStateStorage.class); - private final Blockchain blockchain = mock(Blockchain.class); - - private final Address sender = - Address.fromHexString("0x0000000000000000000000000000000000000003"); - - private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithmFactory.getInstance(); - - private final KeyPair KEY_PAIR = - signatureAlgorithm.createKeyPair( - signatureAlgorithm.createPrivateKey( - new BigInteger( - "8f2a55949038a9610f50fb23b5883af3b4ecb3c3bb792cbcefbd1542c692be63", 16))); - - private final PrivateTransaction privateTransaction = - PrivateTransaction.builder() - .nonce(0) - .gasPrice(Wei.of(1000)) - .gasLimit(3000000) - .to(null) - .value(Wei.ZERO) - .payload( - Bytes.fromHexString( - "0x608060405234801561001057600080fd5b5060d08061001f60003960" - + "00f3fe60806040526004361060485763ffffffff7c01000000" - + "00000000000000000000000000000000000000000000000000" - + "60003504166360fe47b18114604d5780636d4ce63c14607557" - + "5b600080fd5b348015605857600080fd5b5060736004803603" - + "6020811015606d57600080fd5b50356099565b005b34801560" - + "8057600080fd5b506087609e565b6040805191825251908190" - + "0360200190f35b600055565b6000549056fea165627a7a7230" - + "5820cb1d0935d14b589300b12fcd0ab849a7e9019c81da24d6" - + "daa4f6b2f003d1b0180029")) - .sender(sender) - .chainId(BigInteger.valueOf(2018)) - .privateFrom(Bytes.wrap(ENCLAVE_PUBLIC_KEY.getBytes(UTF_8))) - .privateFor( - Lists.newArrayList( - Bytes.wrap("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo=".getBytes(UTF_8)))) - .restriction(Restriction.RESTRICTED) - .signAndBuild(KEY_PAIR); - - private Vertx vertx = Vertx.vertx(); - private TesseraTestHarness testHarness; - private Enclave enclave; - private PrivacyController privacyController; - - @BeforeEach - public void setUp() throws Exception { - vertx = Vertx.vertx(); - - testHarness = - TesseraTestHarnessFactory.create( - "enclave", - Files.createTempDirectory(folder, "enclave"), - new EnclaveKeyConfiguration( - new String[] {"enclave_key_0.pub"}, - new String[] {"enclave_key_0.key"}, - EnclaveEncryptorType.NOOP), - Optional.empty()); - - testHarness.start(); - - final EnclaveFactory factory = new EnclaveFactory(vertx); - enclave = factory.createVertxEnclave(testHarness.clientUrl()); - - privacyController = - new RestrictedDefaultPrivacyController( - blockchain, privateStateStorage, enclave, null, null, null, null, null); - } - - @AfterEach - public void tearDown() { - testHarness.close(); - vertx.close(); - } - - @Test - public void returnsStoredPrivateTransaction() { - final PrivGetPrivateTransaction privGetPrivateTransaction = - new PrivGetPrivateTransaction(privacyController, privacyIdProvider); - - final Hash blockHash = Hash.ZERO; - final Transaction pmt = spy(privateMarkerTransaction()); - when(blockchain.getTransactionByHash(eq(pmt.getHash()))).thenReturn(Optional.of(pmt)); - when(blockchain.getTransactionLocation(eq(pmt.getHash()))) - .thenReturn(Optional.of(new TransactionLocation(blockHash, 0))); - - final BlockHeader blockHeader = mock(BlockHeader.class); - when(blockHeader.getHash()).thenReturn(blockHash); - when(blockchain.getBlockHeader(eq(blockHash))).thenReturn(Optional.of(blockHeader)); - - final BytesValueRLPOutput bvrlp = new BytesValueRLPOutput(); - privateTransaction.writeTo(bvrlp); - - final String payload = Base64.getEncoder().encodeToString(bvrlp.encoded().toArrayUnsafe()); - final ArrayList to = Lists.newArrayList("A1aVtMxLCUHmBVHXoZzzBgPbW/wj5axDpW9X8l91SGo="); - final SendResponse sendResponse = enclave.send(payload, ENCLAVE_PUBLIC_KEY, to); - - final Bytes hexKey = Bytes.fromBase64String(sendResponse.getKey()); - when(pmt.getPayload()).thenReturn(hexKey); - - final Object[] params = new Object[] {pmt.getHash()}; - - final JsonRpcRequestContext request = - new JsonRpcRequestContext(new JsonRpcRequest("1", "priv_getPrivateTransaction", params)); - - final JsonRpcSuccessResponse response = - (JsonRpcSuccessResponse) privGetPrivateTransaction.response(request); - final PrivateTransactionLegacyResult result = - (PrivateTransactionLegacyResult) response.getResult(); - - assertThat(new PrivateTransactionLegacyResult(this.privateTransaction)) - .usingRecursiveComparison() - .isEqualTo(result); - } -} diff --git a/ethereum/core/src/integration-test/java/org/hyperledger/besu/ethereum/mainnet/precompiles/privacy/PrivacyPrecompiledContractIntegrationTest.java b/ethereum/core/src/integration-test/java/org/hyperledger/besu/ethereum/mainnet/precompiles/privacy/PrivacyPrecompiledContractIntegrationTest.java deleted file mode 100644 index a5ee5f068ee..00000000000 --- a/ethereum/core/src/integration-test/java/org/hyperledger/besu/ethereum/mainnet/precompiles/privacy/PrivacyPrecompiledContractIntegrationTest.java +++ /dev/null @@ -1,244 +0,0 @@ -/* - * 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.mainnet.precompiles.privacy; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.catchThrowable; -import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.anyBoolean; -import static org.mockito.ArgumentMatchers.eq; -import static org.mockito.ArgumentMatchers.nullable; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -import org.hyperledger.besu.datatypes.Address; -import org.hyperledger.besu.datatypes.Hash; -import org.hyperledger.besu.enclave.Enclave; -import org.hyperledger.besu.enclave.EnclaveFactory; -import org.hyperledger.besu.enclave.types.SendResponse; -import org.hyperledger.besu.ethereum.core.Block; -import org.hyperledger.besu.ethereum.core.BlockDataGenerator; -import org.hyperledger.besu.ethereum.core.MutableWorldState; -import org.hyperledger.besu.ethereum.core.PrivateTransactionDataFixture; -import org.hyperledger.besu.ethereum.core.ProcessableBlockHeader; -import org.hyperledger.besu.ethereum.mainnet.PrivateStateUtils; -import org.hyperledger.besu.ethereum.privacy.PrivateStateGenesisAllocator; -import org.hyperledger.besu.ethereum.privacy.PrivateStateRootResolver; -import org.hyperledger.besu.ethereum.privacy.PrivateTransaction; -import org.hyperledger.besu.ethereum.privacy.PrivateTransactionProcessor; -import org.hyperledger.besu.ethereum.privacy.storage.PrivacyGroupHeadBlockMap; -import org.hyperledger.besu.ethereum.privacy.storage.PrivateMetadataUpdater; -import org.hyperledger.besu.ethereum.privacy.storage.PrivateStateStorage; -import org.hyperledger.besu.ethereum.processing.TransactionProcessingResult; -import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; -import org.hyperledger.besu.ethereum.worldstate.WorldStateArchive; -import org.hyperledger.besu.evm.frame.MessageFrame; -import org.hyperledger.besu.evm.gascalculator.SpuriousDragonGasCalculator; -import org.hyperledger.besu.evm.operation.BlockHashOperation.BlockHashLookup; -import org.hyperledger.besu.evm.precompile.PrecompiledContract; -import org.hyperledger.besu.evm.tracing.OperationTracer; -import org.hyperledger.besu.evm.worldstate.WorldUpdater; -import org.hyperledger.enclave.testutil.EnclaveEncryptorType; -import org.hyperledger.enclave.testutil.EnclaveKeyConfiguration; -import org.hyperledger.enclave.testutil.TesseraTestHarness; -import org.hyperledger.enclave.testutil.TesseraTestHarnessFactory; - -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Collections; -import java.util.List; -import java.util.Optional; - -import com.google.common.collect.Lists; -import io.vertx.core.Vertx; -import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; - -public class PrivacyPrecompiledContractIntegrationTest { - - // this tempDir is deliberately static - @TempDir private static Path folder; - - private static final Bytes VALID_PRIVATE_TRANSACTION_RLP = - Bytes.fromHexString( - "0xf90113800182520894095e7baea6a6c7c4c2dfeb977efac326af552d87" - + "a0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff" - + "ffff801ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d" - + "495a36649353a01fffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab94" - + "9f53faa07bd2c804ac41316156744d784c4355486d425648586f5a7a7a4267" - + "5062572f776a3561784470573958386c393153476f3df85aac41316156744d" - + "784c4355486d425648586f5a7a7a42675062572f776a356178447057395838" - + "6c393153476f3dac4b6f32625671442b6e4e6c4e594c35454537793349644f" - + "6e766966746a69697a706a52742b4854754642733d8a726573747269637465" - + "64"); - private static final String DEFAULT_OUTPUT = "0x01"; - - private static Enclave enclave; - private static MessageFrame messageFrame; - - private static TesseraTestHarness testHarness; - private static WorldStateArchive worldStateArchive; - private static PrivateStateStorage privateStateStorage; - private static final Vertx vertx = Vertx.vertx(); - - private PrivateTransactionProcessor mockPrivateTxProcessor() { - final PrivateTransactionProcessor mockPrivateTransactionProcessor = - mock(PrivateTransactionProcessor.class); - final TransactionProcessingResult result = - TransactionProcessingResult.successful( - null, 0, 0, Bytes.fromHexString(DEFAULT_OUTPUT), null); - when(mockPrivateTransactionProcessor.processTransaction( - nullable(WorldUpdater.class), - nullable(WorldUpdater.class), - nullable(ProcessableBlockHeader.class), - nullable(Hash.class), - nullable(PrivateTransaction.class), - nullable(Address.class), - nullable(OperationTracer.class), - nullable(BlockHashLookup.class), - nullable(Bytes.class))) - .thenReturn(result); - - return mockPrivateTransactionProcessor; - } - - @BeforeAll - public static void setUpOnce() throws Exception { - - testHarness = - TesseraTestHarnessFactory.create( - "enclave", - Files.createTempDirectory(folder, "enclave"), - new EnclaveKeyConfiguration( - new String[] {"enclave_key_0.pub"}, - new String[] {"enclave_key_1.key"}, - EnclaveEncryptorType.NOOP), - Optional.empty()); - - testHarness.start(); - - final EnclaveFactory factory = new EnclaveFactory(vertx); - enclave = factory.createVertxEnclave(testHarness.clientUrl()); - messageFrame = mock(MessageFrame.class); - final BlockDataGenerator blockGenerator = new BlockDataGenerator(); - final Block genesis = blockGenerator.genesisBlock(); - final Block block = - blockGenerator.block( - new BlockDataGenerator.BlockOptions().setParentHash(genesis.getHeader().getHash())); - when(messageFrame.getBlockValues()).thenReturn(block.getHeader()); - final PrivateMetadataUpdater privateMetadataUpdater = mock(PrivateMetadataUpdater.class); - when(privateMetadataUpdater.getPrivateBlockMetadata(any())).thenReturn(null); - when(privateMetadataUpdater.getPrivacyGroupHeadBlockMap()) - .thenReturn(PrivacyGroupHeadBlockMap.empty()); - when(messageFrame.getContextVariable( - eq(PrivateStateUtils.KEY_IS_PERSISTING_PRIVATE_STATE), anyBoolean())) - .thenReturn(false); - when(messageFrame.getContextVariable(eq(PrivateStateUtils.KEY_PRIVATE_METADATA_UPDATER))) - .thenReturn(privateMetadataUpdater); - when(messageFrame.hasContextVariable(eq(PrivateStateUtils.KEY_PRIVATE_METADATA_UPDATER))) - .thenReturn(true); - - worldStateArchive = mock(WorldStateArchive.class); - final MutableWorldState mutableWorldState = mock(MutableWorldState.class); - when(mutableWorldState.updater()).thenReturn(mock(WorldUpdater.class)); - when(worldStateArchive.getMutable()).thenReturn(mutableWorldState); - when(worldStateArchive.getMutable(any(), any())).thenReturn(Optional.of(mutableWorldState)); - - privateStateStorage = mock(PrivateStateStorage.class); - final PrivateStateStorage.Updater storageUpdater = mock(PrivateStateStorage.Updater.class); - when(privateStateStorage.getPrivacyGroupHeadBlockMap(any())) - .thenReturn(Optional.of(PrivacyGroupHeadBlockMap.empty())); - when(storageUpdater.putPrivateBlockMetadata( - nullable(Bytes32.class), nullable(Bytes32.class), any())) - .thenReturn(storageUpdater); - when(storageUpdater.putTransactionReceipt( - nullable(Bytes32.class), nullable(Bytes32.class), any())) - .thenReturn(storageUpdater); - when(privateStateStorage.updater()).thenReturn(storageUpdater); - } - - @AfterAll - public static void tearDownOnce() { - testHarness.stop(); - vertx.close(); - } - - @Test - public void testUpCheck() { - assertThat(enclave.upCheck()).isTrue(); - } - - @Test - public void testSendAndReceive() { - final List publicKeys = testHarness.getPublicKeys(); - - final PrivateTransaction privateTransaction = - PrivateTransactionDataFixture.privateContractDeploymentTransactionBesu(publicKeys.get(0)); - final BytesValueRLPOutput bytesValueRLPOutput = new BytesValueRLPOutput(); - privateTransaction.writeTo(bytesValueRLPOutput); - - final String s = bytesValueRLPOutput.encoded().toBase64String(); - final SendResponse sr = - enclave.send(s, publicKeys.get(0), Lists.newArrayList(publicKeys.get(0))); - - final PrivacyPrecompiledContract privacyPrecompiledContract = - new PrivacyPrecompiledContract( - new SpuriousDragonGasCalculator(), - enclave, - worldStateArchive, - new PrivateStateRootResolver(privateStateStorage), - new PrivateStateGenesisAllocator( - false, (privacyGroupId, blockNumber) -> Collections::emptyList), - false, - "IntegrationTest"); - - privacyPrecompiledContract.setPrivateTransactionProcessor(mockPrivateTxProcessor()); - - final PrecompiledContract.PrecompileContractResult result = - privacyPrecompiledContract.computePrecompile( - Bytes.fromBase64String(sr.getKey()), messageFrame); - final Bytes actual = result.getOutput(); - - assertThat(actual).isEqualTo(Bytes.fromHexString(DEFAULT_OUTPUT)); - } - - @Test - public void testNoPrivateKeyError() throws RuntimeException { - final List publicKeys = testHarness.getPublicKeys(); - publicKeys.add("noPrivateKey"); - - final String s = VALID_PRIVATE_TRANSACTION_RLP.toBase64String(); - - final Throwable thrown = catchThrowable(() -> enclave.send(s, publicKeys.get(0), publicKeys)); - - assertThat(thrown).hasMessageContaining("Index 9 out of bounds for length 9"); - } - - @Test - public void testWrongPrivateKeyError() throws RuntimeException { - final List publicKeys = testHarness.getPublicKeys(); - publicKeys.add("noPrivateKenoPrivateKenoPrivateKenoPrivateK"); - - final String s = VALID_PRIVATE_TRANSACTION_RLP.toBase64String(); - - final Throwable thrown = catchThrowable(() -> enclave.send(s, publicKeys.get(0), publicKeys)); - - assertThat(thrown).hasMessageContaining("Recipient not found for key:"); - } -} diff --git a/testutil/src/main/resources/enclave_key_0.key b/testutil/src/main/resources/enclave_key_0.key deleted file mode 100644 index eaae9b0867c..00000000000 --- a/testutil/src/main/resources/enclave_key_0.key +++ /dev/null @@ -1 +0,0 @@ -{"data":{"bytes":"hBsuQsGJzx4QHmFmBkNoI7YGnTmaZP4P+wBOdu56ljk="},"type":"unlocked"} \ No newline at end of file From 874cba016db3ef3c64df9d2ccf0b2b4a32d5d0f8 Mon Sep 17 00:00:00 2001 From: Matt Whitehead Date: Mon, 23 Sep 2024 11:58:47 +0100 Subject: [PATCH 21/37] Update protobuf to 32.25.5 to resolve CVE-2024-7254 (#7664) Signed-off-by: Matthew Whitehead --- gradle/verification-metadata.xml | 36 ++++++++++++++++++++++++++++++++ gradle/versions.gradle | 2 ++ 2 files changed, 38 insertions(+) diff --git a/gradle/verification-metadata.xml b/gradle/verification-metadata.xml index 3778a2d60ad..7f6a58b66e7 100644 --- a/gradle/verification-metadata.xml +++ b/gradle/verification-metadata.xml @@ -1023,6 +1023,11 @@ + + + + + @@ -1033,6 +1038,11 @@ + + + + + @@ -1049,6 +1059,14 @@ + + + + + + + + @@ -1062,6 +1080,14 @@ + + + + + + + + @@ -1080,6 +1106,11 @@ + + + + + @@ -1090,6 +1121,11 @@ + + + + + diff --git a/gradle/versions.gradle b/gradle/versions.gradle index b7a286e69a3..634c29b1e13 100644 --- a/gradle/versions.gradle +++ b/gradle/versions.gradle @@ -25,6 +25,8 @@ dependencyManagement { dependency 'com.github.ben-manes.caffeine:caffeine:3.1.8' + dependency 'com.google.protobuf:protobuf-java:3.25.5' + dependency 'com.github.oshi:oshi-core:6.6.3' dependency 'com.google.auto.service:auto-service:1.1.1' From 0d6395515890280c29ee2402c03b1ee81bde3bab Mon Sep 17 00:00:00 2001 From: Rafael Matias Date: Mon, 23 Sep 2024 14:34:20 +0200 Subject: [PATCH 22/37] Docker: Only switch user if the current user is root (#7654) * Update entrypoint script for Dockerfile to only switch user if its running as root Signed-off-by: Rafael Matias * make root user check at the beginning Signed-off-by: Rafael Matias --------- Signed-off-by: Rafael Matias --- besu/src/main/scripts/besu-entry.sh | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/besu/src/main/scripts/besu-entry.sh b/besu/src/main/scripts/besu-entry.sh index ed3687b2291..ee11bfbffc2 100755 --- a/besu/src/main/scripts/besu-entry.sh +++ b/besu/src/main/scripts/besu-entry.sh @@ -14,6 +14,14 @@ ## SPDX-License-Identifier: Apache-2.0 ## +# Construct the command as a single string +COMMAND="/opt/besu/bin/besu $@" + +# Check if current user is not root. If not, run the command as is. +if [ "$(id -u)" -ne 0 ]; then + exec /bin/bash -c "$COMMAND" +fi + # Run Besu first to get paths needing permission adjustment output=$(/opt/besu/bin/besu --print-paths-and-exit $BESU_USER_NAME "$@") @@ -41,9 +49,5 @@ echo "$output" | while IFS=: read -r prefix path accessType; do fi done -# Finally, run Besu with the actual arguments passed to the container -# Construct the command as a single string -COMMAND="/opt/besu/bin/besu $@" - # Switch to the besu user and execute the command -exec su -s /bin/bash $BESU_USER_NAME -c "$COMMAND" +exec su -s /bin/bash "$BESU_USER_NAME" -c "$COMMAND" From 4f07e76a6c26b294bcc8f50deefefc78438636dd Mon Sep 17 00:00:00 2001 From: Matilda-Clerke Date: Tue, 24 Sep 2024 13:04:03 +1000 Subject: [PATCH 23/37] 7311: Add feature toggle for enabling use of the peertask system where available (#7633) Signed-off-by: Matilda Clerke --- .../options/unstable/SynchronizerOptions.java | 18 +++++++++++++++++- besu/src/test/resources/everything_config.toml | 1 + .../eth/sync/SynchronizerConfiguration.java | 18 ++++++++++++++++-- 3 files changed, 34 insertions(+), 3 deletions(-) diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/unstable/SynchronizerOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/unstable/SynchronizerOptions.java index 95bbe0a2b1f..816d9df00a1 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/unstable/SynchronizerOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/unstable/SynchronizerOptions.java @@ -314,6 +314,13 @@ public void parseBlockPropagationRange(final String arg) { description = "Snap sync enabled for BFT chains (default: ${DEFAULT-VALUE})") private Boolean snapsyncBftEnabled = SnapSyncConfiguration.DEFAULT_SNAP_SYNC_BFT_ENABLED; + @CommandLine.Option( + names = {"--Xpeertask-system-enabled"}, + hidden = true, + description = + "Temporary feature toggle to enable using the new peertask system (default: ${DEFAULT-VALUE})") + private final Boolean isPeerTaskSystemEnabled = false; + private SynchronizerOptions() {} /** @@ -334,6 +341,15 @@ public boolean isSnapSyncBftEnabled() { return snapsyncBftEnabled; } + /** + * Flag to indicate whether the peer task system should be used where available + * + * @return true if the peer task system should be used where available + */ + public boolean isPeerTaskSystemEnabled() { + return isPeerTaskSystemEnabled; + } + /** * Create synchronizer options. * @@ -420,7 +436,7 @@ public SynchronizerConfiguration.Builder toDomainObject() { .isSnapSyncBftEnabled(snapsyncBftEnabled) .build()); builder.checkpointPostMergeEnabled(checkpointPostMergeSyncEnabled); - + builder.isPeerTaskSystemEnabled(isPeerTaskSystemEnabled); return builder; } diff --git a/besu/src/test/resources/everything_config.toml b/besu/src/test/resources/everything_config.toml index e3d7a3f28d3..d28152b47cd 100644 --- a/besu/src/test/resources/everything_config.toml +++ b/besu/src/test/resources/everything_config.toml @@ -226,6 +226,7 @@ Xsecp256k1-native-enabled=false Xaltbn128-native-enabled=false Xsnapsync-server-enabled=true Xbonsai-full-flat-db-enabled=true +Xpeertask-system-enabled=false # compatibility flags compatibility-eth64-forkid-enabled=false diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/SynchronizerConfiguration.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/SynchronizerConfiguration.java index d46da85dc48..d72f76c213c 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/SynchronizerConfiguration.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/SynchronizerConfiguration.java @@ -85,6 +85,7 @@ public class SynchronizerConfiguration { private final int maxTrailingPeers; private final long worldStateMinMillisBeforeStalling; private final long propagationManagerGetBlockTimeoutMillis; + private final boolean isPeerTaskSystemEnabled; private SynchronizerConfiguration( final int syncPivotDistance, @@ -108,7 +109,8 @@ private SynchronizerConfiguration( final int computationParallelism, final int maxTrailingPeers, final long propagationManagerGetBlockTimeoutMillis, - final boolean checkpointPostMergeEnabled) { + final boolean checkpointPostMergeEnabled, + final boolean isPeerTaskSystemEnabled) { this.syncPivotDistance = syncPivotDistance; this.fastSyncFullValidationRate = fastSyncFullValidationRate; this.syncMinimumPeerCount = syncMinimumPeerCount; @@ -131,6 +133,7 @@ private SynchronizerConfiguration( this.maxTrailingPeers = maxTrailingPeers; this.propagationManagerGetBlockTimeoutMillis = propagationManagerGetBlockTimeoutMillis; this.checkpointPostMergeEnabled = checkpointPostMergeEnabled; + this.isPeerTaskSystemEnabled = isPeerTaskSystemEnabled; } public static Builder builder() { @@ -256,6 +259,10 @@ public long getPropagationManagerGetBlockTimeoutMillis() { return propagationManagerGetBlockTimeoutMillis; } + public boolean isPeerTaskSystemEnabled() { + return isPeerTaskSystemEnabled; + } + public static class Builder { private SyncMode syncMode = SyncMode.FULL; private int syncMinimumPeerCount = DEFAULT_SYNC_MINIMUM_PEERS; @@ -280,6 +287,7 @@ public static class Builder { DEFAULT_WORLD_STATE_MAX_REQUESTS_WITHOUT_PROGRESS; private long worldStateMinMillisBeforeStalling = DEFAULT_WORLD_STATE_MIN_MILLIS_BEFORE_STALLING; private int worldStateTaskCacheSize = DEFAULT_WORLD_STATE_TASK_CACHE_SIZE; + private boolean isPeerTaskSystemEnabled = false; private long propagationManagerGetBlockTimeoutMillis = DEFAULT_PROPAGATION_MANAGER_GET_BLOCK_TIMEOUT_MILLIS; @@ -406,6 +414,11 @@ public Builder checkpointPostMergeEnabled(final boolean checkpointPostMergeEnabl return this; } + public Builder isPeerTaskSystemEnabled(final boolean isPeerTaskSystemEnabled) { + this.isPeerTaskSystemEnabled = isPeerTaskSystemEnabled; + return this; + } + public SynchronizerConfiguration build() { return new SynchronizerConfiguration( syncPivotDistance, @@ -429,7 +442,8 @@ public SynchronizerConfiguration build() { computationParallelism, maxTrailingPeers, propagationManagerGetBlockTimeoutMillis, - checkpointPostMergeEnabled); + checkpointPostMergeEnabled, + isPeerTaskSystemEnabled); } } } From 04ba15aa922659a1516589612c6057301804c4d5 Mon Sep 17 00:00:00 2001 From: Glory Agatevure Date: Tue, 24 Sep 2024 06:06:16 +0100 Subject: [PATCH 24/37] Add consolidationRequestContract in jsonGenesisConfig (#7647) * Include consolidationRequestContract in jsonGenesisConfigOptions Signed-off-by: gconnect * Update changelog and ran spotlessApply Signed-off-by: gconnect * Rename consolidationRequestPredeployAddress to consolidationRequestContractAddress Signed-off-by: gconnect * Create request contract addresses class Signed-off-by: gconnect * Update method calls Signed-off-by: gconnect * Refactor RequestContractAddresses class and update method calls and test Signed-off-by: gconnect --------- Signed-off-by: gconnect Co-authored-by: Gabriel-Trintinalia --- CHANGELOG.md | 2 + .../besu/config/GenesisConfigOptions.java | 7 +++ .../besu/config/JsonGenesisConfigOptions.java | 11 ++++ .../besu/config/StubGenesisConfigOptions.java | 5 ++ .../besu/config/GenesisConfigOptionsTest.java | 27 ++++++++ .../mainnet/MainnetProtocolSpecs.java | 16 ++--- .../ConsolidationRequestProcessor.java | 9 ++- .../requests/MainnetRequestsValidator.java | 21 ++++--- .../requests/RequestContractAddresses.java | 61 +++++++++++++++++++ .../mainnet/PragueRequestsValidatorTest.java | 11 +++- 10 files changed, 148 insertions(+), 22 deletions(-) create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/RequestContractAddresses.java diff --git a/CHANGELOG.md b/CHANGELOG.md index 96e1511eead..5ce29955d12 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,8 @@ # Changelog ## [Unreleased] +- Add configuration of Consolidation Request Contract Address via genesis configuration [#7647](https://github.com/hyperledger/besu/pull/7647) + ### Upcoming Breaking Changes - k8s (KUBERNETES) Nat method is now deprecated and will be removed in a future release diff --git a/config/src/main/java/org/hyperledger/besu/config/GenesisConfigOptions.java b/config/src/main/java/org/hyperledger/besu/config/GenesisConfigOptions.java index d6323ee9bfb..07ddd0d7eac 100644 --- a/config/src/main/java/org/hyperledger/besu/config/GenesisConfigOptions.java +++ b/config/src/main/java/org/hyperledger/besu/config/GenesisConfigOptions.java @@ -539,4 +539,11 @@ default boolean isConsensusMigration() { * @return the deposit address */ Optional

getDepositContractAddress(); + + /** + * The consolidation request contract address + * + * @return the consolidation request contract address + */ + Optional
getConsolidationRequestContractAddress(); } diff --git a/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java b/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java index 67114b29bf3..83b1f48fb48 100644 --- a/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java +++ b/config/src/main/java/org/hyperledger/besu/config/JsonGenesisConfigOptions.java @@ -52,6 +52,8 @@ public class JsonGenesisConfigOptions implements GenesisConfigOptions { private static final String WITHDRAWAL_REQUEST_CONTRACT_ADDRESS_KEY = "withdrawalrequestcontractaddress"; private static final String DEPOSIT_CONTRACT_ADDRESS_KEY = "depositcontractaddress"; + private static final String CONSOLIDATION_REQUEST_CONTRACT_ADDRESS_KEY = + "consolidationrequestcontractaddress"; private final ObjectNode configRoot; private final Map configOverrides = new TreeMap<>(String.CASE_INSENSITIVE_ORDER); @@ -453,6 +455,13 @@ public Optional
getDepositContractAddress() { return inputAddress.map(Address::fromHexString); } + @Override + public Optional
getConsolidationRequestContractAddress() { + Optional inputAddress = + JsonUtil.getString(configRoot, CONSOLIDATION_REQUEST_CONTRACT_ADDRESS_KEY); + return inputAddress.map(Address::fromHexString); + } + @Override public Map asMap() { final ImmutableMap.Builder builder = ImmutableMap.builder(); @@ -504,6 +513,8 @@ public Map asMap() { getWithdrawalRequestContractAddress() .ifPresent(l -> builder.put("withdrawalRequestContractAddress", l)); getDepositContractAddress().ifPresent(l -> builder.put("depositContractAddress", l)); + getConsolidationRequestContractAddress() + .ifPresent(l -> builder.put("consolidationRequestContractAddress", l)); if (isClique()) { builder.put("clique", getCliqueConfigOptions().asMap()); diff --git a/config/src/main/java/org/hyperledger/besu/config/StubGenesisConfigOptions.java b/config/src/main/java/org/hyperledger/besu/config/StubGenesisConfigOptions.java index efe56a086d0..ee9584fd3a4 100644 --- a/config/src/main/java/org/hyperledger/besu/config/StubGenesisConfigOptions.java +++ b/config/src/main/java/org/hyperledger/besu/config/StubGenesisConfigOptions.java @@ -467,6 +467,11 @@ public Optional
getDepositContractAddress() { return Optional.empty(); } + @Override + public Optional
getConsolidationRequestContractAddress() { + return Optional.empty(); + } + /** * Homestead block stub genesis config options. * diff --git a/config/src/test/java/org/hyperledger/besu/config/GenesisConfigOptionsTest.java b/config/src/test/java/org/hyperledger/besu/config/GenesisConfigOptionsTest.java index 219ea4fcf8a..69494a9c9c4 100644 --- a/config/src/test/java/org/hyperledger/besu/config/GenesisConfigOptionsTest.java +++ b/config/src/test/java/org/hyperledger/besu/config/GenesisConfigOptionsTest.java @@ -382,6 +382,33 @@ void asMapIncludesDepositContractAddress() { .containsValue(Address.ZERO); } + @Test + void shouldGetConsolidationRequestContractAddress() { + final GenesisConfigOptions config = + fromConfigOptions( + singletonMap( + "consolidationRequestContractAddress", + "0x00000000219ab540356cbb839cbe05303d7705fa")); + assertThat(config.getConsolidationRequestContractAddress()) + .hasValue(Address.fromHexString("0x00000000219ab540356cbb839cbe05303d7705fa")); + } + + @Test + void shouldNotHaveConsolidationRequestContractAddressWhenEmpty() { + final GenesisConfigOptions config = fromConfigOptions(emptyMap()); + assertThat(config.getConsolidationRequestContractAddress()).isEmpty(); + } + + @Test + void asMapIncludesConsolidationRequestContractAddress() { + final GenesisConfigOptions config = + fromConfigOptions(Map.of("consolidationRequestContractAddress", "0x0")); + + assertThat(config.asMap()) + .containsOnlyKeys("consolidationRequestContractAddress") + .containsValue(Address.ZERO); + } + private GenesisConfigOptions fromConfigOptions(final Map configOptions) { final ObjectNode rootNode = JsonUtil.createEmptyObjectNode(); final ObjectNode options = JsonUtil.objectNodeFromMap(configOptions); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java index e6d7a88cbdf..b5b2f678d67 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/MainnetProtocolSpecs.java @@ -14,10 +14,8 @@ */ package org.hyperledger.besu.ethereum.mainnet; -import static org.hyperledger.besu.ethereum.mainnet.requests.DepositRequestProcessor.DEFAULT_DEPOSIT_CONTRACT_ADDRESS; import static org.hyperledger.besu.ethereum.mainnet.requests.MainnetRequestsValidator.pragueRequestsProcessors; import static org.hyperledger.besu.ethereum.mainnet.requests.MainnetRequestsValidator.pragueRequestsValidator; -import static org.hyperledger.besu.ethereum.mainnet.requests.WithdrawalRequestProcessor.DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS; import org.hyperledger.besu.config.GenesisConfigOptions; import org.hyperledger.besu.config.PowAlgorithm; @@ -41,6 +39,7 @@ import org.hyperledger.besu.ethereum.mainnet.feemarket.BaseFeeMarket; import org.hyperledger.besu.ethereum.mainnet.feemarket.FeeMarket; import org.hyperledger.besu.ethereum.mainnet.parallelization.MainnetParallelBlockProcessor; +import org.hyperledger.besu.ethereum.mainnet.requests.RequestContractAddresses; import org.hyperledger.besu.ethereum.privacy.PrivateTransactionProcessor; import org.hyperledger.besu.ethereum.privacy.PrivateTransactionValidator; import org.hyperledger.besu.ethereum.privacy.storage.PrivateMetadataUpdater; @@ -767,12 +766,8 @@ static ProtocolSpecBuilder pragueDefinition( final boolean isParallelTxProcessingEnabled, final MetricsSystem metricsSystem) { - final Address withdrawalRequestContractAddress = - genesisConfigOptions - .getWithdrawalRequestContractAddress() - .orElse(DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS); - final Address depositContractAddress = - genesisConfigOptions.getDepositContractAddress().orElse(DEFAULT_DEPOSIT_CONTRACT_ADDRESS); + RequestContractAddresses requestContractAddresses = + RequestContractAddresses.fromGenesis(genesisConfigOptions); return cancunDefinition( chainId, @@ -794,10 +789,9 @@ static ProtocolSpecBuilder pragueDefinition( .precompileContractRegistryBuilder(MainnetPrecompiledContractRegistries::prague) // EIP-7002 Withdrawals / EIP-6610 Deposits / EIP-7685 Requests - .requestsValidator(pragueRequestsValidator(depositContractAddress)) + .requestsValidator(pragueRequestsValidator(requestContractAddresses)) // EIP-7002 Withdrawals / EIP-6610 Deposits / EIP-7685 Requests - .requestProcessorCoordinator( - pragueRequestsProcessors(withdrawalRequestContractAddress, depositContractAddress)) + .requestProcessorCoordinator(pragueRequestsProcessors(requestContractAddresses)) // change to accept EIP-7702 transactions .transactionValidatorFactoryBuilder( diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/ConsolidationRequestProcessor.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/ConsolidationRequestProcessor.java index 0a48d8278c9..641720670fb 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/ConsolidationRequestProcessor.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/ConsolidationRequestProcessor.java @@ -22,13 +22,18 @@ public class ConsolidationRequestProcessor extends AbstractSystemCallRequestProcessor { - public static final Address CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS = + public static final Address CONSOLIDATION_REQUEST_CONTRACT_ADDRESS = Address.fromHexString("0x00b42dbF2194e931E80326D950320f7d9Dbeac02"); private static final int ADDRESS_BYTES = 20; private static final int PUBLIC_KEY_BYTES = 48; private static final int CONSOLIDATION_REQUEST_BYTES_SIZE = ADDRESS_BYTES + PUBLIC_KEY_BYTES + PUBLIC_KEY_BYTES; + private final Address consolidationRequestContractAddress; + + public ConsolidationRequestProcessor(final Address consolidationRequestContractAddress) { + this.consolidationRequestContractAddress = consolidationRequestContractAddress; + } /** * Gets the call address for consolidation requests. @@ -37,7 +42,7 @@ public class ConsolidationRequestProcessor */ @Override protected Address getCallAddress() { - return CONSOLIDATION_REQUEST_PREDEPLOY_ADDRESS; + return consolidationRequestContractAddress; } /** diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/MainnetRequestsValidator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/MainnetRequestsValidator.java index 6e61a0343c3..9c86d18f7ad 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/MainnetRequestsValidator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/MainnetRequestsValidator.java @@ -14,27 +14,34 @@ */ package org.hyperledger.besu.ethereum.mainnet.requests; -import org.hyperledger.besu.datatypes.Address; import org.hyperledger.besu.datatypes.RequestType; public class MainnetRequestsValidator { public static RequestsValidatorCoordinator pragueRequestsValidator( - final Address depositContractAddress) { + final RequestContractAddresses requestContractAddresses) { return new RequestsValidatorCoordinator.Builder() .addValidator(RequestType.WITHDRAWAL, new WithdrawalRequestValidator()) .addValidator(RequestType.CONSOLIDATION, new ConsolidationRequestValidator()) - .addValidator(RequestType.DEPOSIT, new DepositRequestValidator(depositContractAddress)) + .addValidator( + RequestType.DEPOSIT, + new DepositRequestValidator(requestContractAddresses.getDepositContractAddress())) .build(); } public static RequestProcessorCoordinator pragueRequestsProcessors( - final Address withdrawalRequestContractAddress, final Address depositContractAddress) { + final RequestContractAddresses requestContractAddresses) { return new RequestProcessorCoordinator.Builder() .addProcessor( RequestType.WITHDRAWAL, - new WithdrawalRequestProcessor(withdrawalRequestContractAddress)) - .addProcessor(RequestType.CONSOLIDATION, new ConsolidationRequestProcessor()) - .addProcessor(RequestType.DEPOSIT, new DepositRequestProcessor(depositContractAddress)) + new WithdrawalRequestProcessor( + requestContractAddresses.getWithdrawalRequestContractAddress())) + .addProcessor( + RequestType.CONSOLIDATION, + new ConsolidationRequestProcessor( + requestContractAddresses.getConsolidationRequestContractAddress())) + .addProcessor( + RequestType.DEPOSIT, + new DepositRequestProcessor(requestContractAddresses.getDepositContractAddress())) .build(); } } diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/RequestContractAddresses.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/RequestContractAddresses.java new file mode 100644 index 00000000000..b75677dda79 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/mainnet/requests/RequestContractAddresses.java @@ -0,0 +1,61 @@ +/* + * Copyright contributors to Hyperledger Besu. + * + * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on + * an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the + * specific language governing permissions and limitations under the License. + * + * SPDX-License-Identifier: Apache-2.0 + */ +package org.hyperledger.besu.ethereum.mainnet.requests; + +import static org.hyperledger.besu.ethereum.mainnet.requests.ConsolidationRequestProcessor.CONSOLIDATION_REQUEST_CONTRACT_ADDRESS; +import static org.hyperledger.besu.ethereum.mainnet.requests.DepositRequestProcessor.DEFAULT_DEPOSIT_CONTRACT_ADDRESS; +import static org.hyperledger.besu.ethereum.mainnet.requests.WithdrawalRequestProcessor.DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS; + +import org.hyperledger.besu.config.GenesisConfigOptions; +import org.hyperledger.besu.datatypes.Address; + +public class RequestContractAddresses { + private final Address withdrawalRequestContractAddress; + private final Address depositContractAddress; + private final Address consolidationRequestContractAddress; + + public RequestContractAddresses( + final Address withdrawalRequestContractAddress, + final Address depositContractAddress, + final Address consolidationRequestContractAddress) { + this.withdrawalRequestContractAddress = withdrawalRequestContractAddress; + this.depositContractAddress = depositContractAddress; + this.consolidationRequestContractAddress = consolidationRequestContractAddress; + } + + public static RequestContractAddresses fromGenesis( + final GenesisConfigOptions genesisConfigOptions) { + return new RequestContractAddresses( + genesisConfigOptions + .getWithdrawalRequestContractAddress() + .orElse(DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS), + genesisConfigOptions.getDepositContractAddress().orElse(DEFAULT_DEPOSIT_CONTRACT_ADDRESS), + genesisConfigOptions + .getConsolidationRequestContractAddress() + .orElse(CONSOLIDATION_REQUEST_CONTRACT_ADDRESS)); + } + + public Address getWithdrawalRequestContractAddress() { + return withdrawalRequestContractAddress; + } + + public Address getDepositContractAddress() { + return depositContractAddress; + } + + public Address getConsolidationRequestContractAddress() { + return consolidationRequestContractAddress; + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PragueRequestsValidatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PragueRequestsValidatorTest.java index 6c325b70a86..6158ba44c37 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PragueRequestsValidatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/mainnet/PragueRequestsValidatorTest.java @@ -17,8 +17,10 @@ import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.ethereum.mainnet.HeaderValidationMode.NONE; +import static org.hyperledger.besu.ethereum.mainnet.requests.ConsolidationRequestProcessor.CONSOLIDATION_REQUEST_CONTRACT_ADDRESS; import static org.hyperledger.besu.ethereum.mainnet.requests.DepositRequestProcessor.DEFAULT_DEPOSIT_CONTRACT_ADDRESS; import static org.hyperledger.besu.ethereum.mainnet.requests.MainnetRequestsValidator.pragueRequestsValidator; +import static org.hyperledger.besu.ethereum.mainnet.requests.WithdrawalRequestProcessor.DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.lenient; @@ -31,6 +33,7 @@ import org.hyperledger.besu.ethereum.core.BlockchainSetupUtil; import org.hyperledger.besu.ethereum.core.Request; import org.hyperledger.besu.ethereum.core.WithdrawalRequest; +import org.hyperledger.besu.ethereum.mainnet.requests.RequestContractAddresses; import org.hyperledger.besu.ethereum.mainnet.requests.RequestsValidatorCoordinator; import org.hyperledger.besu.evm.log.LogsBloomFilter; @@ -52,9 +55,13 @@ class PragueRequestsValidatorTest { @Mock private ProtocolSchedule protocolSchedule; @Mock private ProtocolSpec protocolSpec; @Mock private WithdrawalsValidator withdrawalsValidator; + private final RequestContractAddresses requestContractAddresses = + new RequestContractAddresses( + DEFAULT_WITHDRAWAL_REQUEST_CONTRACT_ADDRESS, + DEFAULT_DEPOSIT_CONTRACT_ADDRESS, + CONSOLIDATION_REQUEST_CONTRACT_ADDRESS); - RequestsValidatorCoordinator requestValidator = - pragueRequestsValidator(DEFAULT_DEPOSIT_CONTRACT_ADDRESS); + RequestsValidatorCoordinator requestValidator = pragueRequestsValidator(requestContractAddresses); @BeforeEach public void setUp() { From 2aa3848950c86bbe720531539bdb990ab1fcaa7b Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Tue, 24 Sep 2024 11:49:17 +0200 Subject: [PATCH 25/37] Enable logging during unit tests (#7672) Signed-off-by: Fabio Di Fabio --- testutil/src/main/resources/{log4j2.xml => log4j2-test.xml} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename testutil/src/main/resources/{log4j2.xml => log4j2-test.xml} (100%) diff --git a/testutil/src/main/resources/log4j2.xml b/testutil/src/main/resources/log4j2-test.xml similarity index 100% rename from testutil/src/main/resources/log4j2.xml rename to testutil/src/main/resources/log4j2-test.xml From e0518c6d94bf1ae4f0e3742a9e3e51aac7e15fb3 Mon Sep 17 00:00:00 2001 From: Gabriel-Trintinalia Date: Tue, 24 Sep 2024 20:50:19 +1000 Subject: [PATCH 26/37] Force besu to stop on plugin initialization errors (#7662) Signed-off-by: Gabriel-Trintinalia --- CHANGELOG.md | 1 + .../dsl/node/ThreadBesuNodeRunner.java | 3 +- .../acceptance/plugins/TestPicoCLIPlugin.java | 51 +++- .../services/BesuPluginContextImplTest.java | 266 +++++++++++++++--- .../org/hyperledger/besu/cli/BesuCommand.java | 6 +- .../besu/cli/DefaultCommandValues.java | 3 + .../stable/PluginsConfigurationOptions.java | 30 +- .../besu/services/BesuPluginContextImpl.java | 91 ++++-- .../besu/cli/PluginsOptionsTest.java | 51 +++- .../src/test/resources/everything_config.toml | 3 +- .../core/plugins/PluginConfiguration.java | 18 +- 11 files changed, 424 insertions(+), 99 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 5ce29955d12..bb867af945e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ - k8s (KUBERNETES) Nat method is now deprecated and will be removed in a future release ### Breaking Changes +- Besu will now fail to start if any plugins encounter errors during initialization. To allow Besu to continue running despite plugin errors, use the `--plugin-continue-on-error` option. [#7662](https://github.com/hyperledger/besu/pull/7662) ### Additions and Improvements - Remove privacy test classes support [#7569](https://github.com/hyperledger/besu/pull/7569) diff --git a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java index 5d78f1460c4..90de0b0e952 100644 --- a/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java +++ b/acceptance-tests/dsl/src/main/java/org/hyperledger/besu/tests/acceptance/dsl/node/ThreadBesuNodeRunner.java @@ -503,8 +503,9 @@ public BesuPluginContextImpl providePluginContext( besuPluginContext.addService(PermissioningService.class, permissioningService); besuPluginContext.addService(PrivacyPluginService.class, new PrivacyPluginServiceImpl()); - besuPluginContext.registerPlugins( + besuPluginContext.initialize( new PluginConfiguration.Builder().pluginsDir(pluginsPath).build()); + besuPluginContext.registerPlugins(); commandLine.parseArgs(extraCLIOptions.toArray(new String[0])); // register built-in plugins diff --git a/acceptance-tests/test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestPicoCLIPlugin.java b/acceptance-tests/test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestPicoCLIPlugin.java index 0db5281537c..375fbd490ec 100644 --- a/acceptance-tests/test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestPicoCLIPlugin.java +++ b/acceptance-tests/test-plugins/src/main/java/org/hyperledger/besu/tests/acceptance/plugins/TestPicoCLIPlugin.java @@ -1,5 +1,5 @@ /* - * Copyright ConsenSys AG. + * Copyright contributors to Hyperledger Besu. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at @@ -32,16 +32,25 @@ public class TestPicoCLIPlugin implements BesuPlugin { private static final Logger LOG = LoggerFactory.getLogger(TestPicoCLIPlugin.class); + private static final String UNSET = "UNSET"; + private static final String FAIL_REGISTER = "FAILREGISTER"; + private static final String FAIL_BEFORE_EXTERNAL_SERVICES = "FAILBEFOREEXTERNALSERVICES"; + private static final String FAIL_START = "FAILSTART"; + private static final String FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP = + "FAILAFTEREXTERNALSERVICEPOSTMAINLOOP"; + private static final String FAIL_STOP = "FAILSTOP"; + private static final String PLUGIN_LIFECYCLE_PREFIX = "pluginLifecycle."; + @Option( names = {"--Xplugin-test-option"}, hidden = true, - defaultValue = "UNSET") + defaultValue = UNSET) String testOption = System.getProperty("testPicoCLIPlugin.testOption"); @Option( names = {"--plugin-test-stable-option"}, hidden = true, - defaultValue = "UNSET") + defaultValue = UNSET) String stableOption = ""; private String state = "uninited"; @@ -52,7 +61,7 @@ public void register(final BesuContext context) { LOG.info("Registering. Test Option is '{}'", testOption); state = "registering"; - if ("FAILREGISTER".equals(testOption)) { + if (FAIL_REGISTER.equals(testOption)) { state = "failregister"; throw new RuntimeException("I was told to fail at registration"); } @@ -66,12 +75,26 @@ public void register(final BesuContext context) { state = "registered"; } + @Override + public void beforeExternalServices() { + LOG.info("Before external services. Test Option is '{}'", testOption); + state = "beforeExternalServices"; + + if (FAIL_BEFORE_EXTERNAL_SERVICES.equals(testOption)) { + state = "failbeforeExternalServices"; + throw new RuntimeException("I was told to fail before external services"); + } + + writeSignal("beforeExternalServices"); + state = "beforeExternalServicesFinished"; + } + @Override public void start() { LOG.info("Starting. Test Option is '{}'", testOption); state = "starting"; - if ("FAILSTART".equals(testOption)) { + if (FAIL_START.equals(testOption)) { state = "failstart"; throw new RuntimeException("I was told to fail at startup"); } @@ -80,12 +103,26 @@ public void start() { state = "started"; } + @Override + public void afterExternalServicePostMainLoop() { + LOG.info("After external services post main loop. Test Option is '{}'", testOption); + state = "afterExternalServicePostMainLoop"; + + if (FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP.equals(testOption)) { + state = "failafterExternalServicePostMainLoop"; + throw new RuntimeException("I was told to fail after external services post main loop"); + } + + writeSignal("afterExternalServicePostMainLoop"); + state = "afterExternalServicePostMainLoopFinished"; + } + @Override public void stop() { LOG.info("Stopping. Test Option is '{}'", testOption); state = "stopping"; - if ("FAILSTOP".equals(testOption)) { + if (FAIL_STOP.equals(testOption)) { state = "failstop"; throw new RuntimeException("I was told to fail at stop"); } @@ -103,7 +140,7 @@ public String getState() { @SuppressWarnings("ResultOfMethodCallIgnored") private void writeSignal(final String signal) { try { - final File callbackFile = new File(callbackDir, "pluginLifecycle." + signal); + final File callbackFile = new File(callbackDir, PLUGIN_LIFECYCLE_PREFIX + signal); if (!callbackFile.getParentFile().exists()) { callbackFile.getParentFile().mkdirs(); callbackFile.getParentFile().deleteOnExit(); diff --git a/acceptance-tests/test-plugins/src/test/java/org/hyperledger/besu/services/BesuPluginContextImplTest.java b/acceptance-tests/test-plugins/src/test/java/org/hyperledger/besu/services/BesuPluginContextImplTest.java index 7e277041776..a6d167665ff 100644 --- a/acceptance-tests/test-plugins/src/test/java/org/hyperledger/besu/services/BesuPluginContextImplTest.java +++ b/acceptance-tests/test-plugins/src/test/java/org/hyperledger/besu/services/BesuPluginContextImplTest.java @@ -1,5 +1,5 @@ /* - * Copyright ConsenSys AG. + * Copyright contributors to Hyperledger Besu. * * Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with * the License. You may obtain a copy of the License at @@ -40,8 +40,31 @@ public class BesuPluginContextImplTest { private static final Path DEFAULT_PLUGIN_DIRECTORY = Paths.get("."); + private static final String TEST_PICO_CLI_PLUGIN = "TestPicoCLIPlugin"; + private static final String TEST_PICO_CLI_PLUGIN_TEST_OPTION = "testPicoCLIPlugin.testOption"; + private static final String FAIL_REGISTER = "FAILREGISTER"; + private static final String FAIL_START = "FAILSTART"; + private static final String FAIL_STOP = "FAILSTOP"; + private static final String FAIL_BEFORE_EXTERNAL_SERVICES = "FAILBEFOREEXTERNALSERVICES"; + private static final String FAIL_BEFORE_MAIN_LOOP = "FAILBEFOREMAINLOOP"; + private static final String FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP = + "FAILAFTEREXTERNALSERVICEPOSTMAINLOOP"; + private static final String NON_EXISTENT_PLUGIN = "NonExistentPlugin"; + private static final String REGISTERED = "registered"; + private static final String STARTED = "started"; + private static final String STOPPED = "stopped"; + private static final String FAIL_START_STATE = "failstart"; + private static final String FAIL_STOP_STATE = "failstop"; + private BesuPluginContextImpl contextImpl; + private static final PluginConfiguration DEFAULT_CONFIGURATION = + PluginConfiguration.builder() + .pluginsDir(DEFAULT_PLUGIN_DIRECTORY) + .externalPluginsEnabled(true) + .continueOnPluginError(true) + .build(); + @BeforeAll public static void createFakePluginDir() throws IOException { if (System.getProperty("besu.plugins.dir") == null) { @@ -53,7 +76,7 @@ public static void createFakePluginDir() throws IOException { @AfterEach public void clearTestPluginState() { - System.clearProperty("testPicoCLIPlugin.testOption"); + System.clearProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION); } @BeforeEach @@ -64,31 +87,31 @@ void setup() { @Test public void verifyEverythingGoesSmoothly() { assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); - contextImpl.registerPlugins( - PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build()); + contextImpl.initialize(DEFAULT_CONFIGURATION); + contextImpl.registerPlugins(); assertThat(contextImpl.getRegisteredPlugins()).isNotEmpty(); final Optional testPluginOptional = findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class); assertThat(testPluginOptional).isPresent(); final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get(); - assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered"); + assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED); contextImpl.beforeExternalServices(); contextImpl.startPlugins(); - assertThat(testPicoCLIPlugin.getState()).isEqualTo("started"); + assertThat(testPicoCLIPlugin.getState()).isEqualTo(STARTED); contextImpl.stopPlugins(); - assertThat(testPicoCLIPlugin.getState()).isEqualTo("stopped"); + assertThat(testPicoCLIPlugin.getState()).isEqualTo(STOPPED); } @Test public void registrationErrorsHandledSmoothly() { - System.setProperty("testPicoCLIPlugin.testOption", "FAILREGISTER"); + System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_REGISTER); assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); - contextImpl.registerPlugins( - PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build()); + contextImpl.initialize(DEFAULT_CONFIGURATION); + contextImpl.registerPlugins(); assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class); contextImpl.beforeExternalServices(); @@ -103,11 +126,11 @@ public void registrationErrorsHandledSmoothly() { @Test public void startErrorsHandledSmoothly() { - System.setProperty("testPicoCLIPlugin.testOption", "FAILSTART"); + System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_START); assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); - contextImpl.registerPlugins( - PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build()); + contextImpl.initialize(DEFAULT_CONFIGURATION); + contextImpl.registerPlugins(); assertThat(contextImpl.getRegisteredPlugins()) .extracting("class") .contains(TestPicoCLIPlugin.class); @@ -116,11 +139,11 @@ public void startErrorsHandledSmoothly() { findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class); assertThat(testPluginOptional).isPresent(); final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get(); - assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered"); + assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED); contextImpl.beforeExternalServices(); contextImpl.startPlugins(); - assertThat(testPicoCLIPlugin.getState()).isEqualTo("failstart"); + assertThat(testPicoCLIPlugin.getState()).isEqualTo(FAIL_START_STATE); assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class); contextImpl.stopPlugins(); @@ -129,11 +152,11 @@ public void startErrorsHandledSmoothly() { @Test public void stopErrorsHandledSmoothly() { - System.setProperty("testPicoCLIPlugin.testOption", "FAILSTOP"); + System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_STOP); assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); - contextImpl.registerPlugins( - PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build()); + contextImpl.initialize(DEFAULT_CONFIGURATION); + contextImpl.registerPlugins(); assertThat(contextImpl.getRegisteredPlugins()) .extracting("class") .contains(TestPicoCLIPlugin.class); @@ -142,22 +165,20 @@ public void stopErrorsHandledSmoothly() { findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class); assertThat(testPluginOptional).isPresent(); final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get(); - assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered"); + assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED); contextImpl.beforeExternalServices(); contextImpl.startPlugins(); - assertThat(testPicoCLIPlugin.getState()).isEqualTo("started"); + assertThat(testPicoCLIPlugin.getState()).isEqualTo(STARTED); contextImpl.stopPlugins(); - assertThat(testPicoCLIPlugin.getState()).isEqualTo("failstop"); + assertThat(testPicoCLIPlugin.getState()).isEqualTo(FAIL_STOP_STATE); } @Test public void lifecycleExceptions() throws Throwable { - final ThrowableAssert.ThrowingCallable registerPlugins = - () -> - contextImpl.registerPlugins( - PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build()); + contextImpl.initialize(DEFAULT_CONFIGURATION); + final ThrowableAssert.ThrowingCallable registerPlugins = () -> contextImpl.registerPlugins(); assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::startPlugins); assertThatExceptionOfType(IllegalStateException.class).isThrownBy(contextImpl::stopPlugins); @@ -179,30 +200,27 @@ public void lifecycleExceptions() throws Throwable { @Test public void shouldRegisterAllPluginsWhenNoPluginsOption() { - final PluginConfiguration config = - PluginConfiguration.builder().pluginsDir(DEFAULT_PLUGIN_DIRECTORY).build(); - assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); - contextImpl.registerPlugins(config); + contextImpl.initialize(DEFAULT_CONFIGURATION); + contextImpl.registerPlugins(); final Optional testPluginOptional = findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class); assertThat(testPluginOptional).isPresent(); final TestPicoCLIPlugin testPicoCLIPlugin = testPluginOptional.get(); - assertThat(testPicoCLIPlugin.getState()).isEqualTo("registered"); + assertThat(testPicoCLIPlugin.getState()).isEqualTo(REGISTERED); } @Test public void shouldRegisterOnlySpecifiedPluginWhenPluginsOptionIsSet() { - final PluginConfiguration config = createConfigurationForSpecificPlugin("TestPicoCLIPlugin"); - assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); - contextImpl.registerPlugins(config); + contextImpl.initialize(createConfigurationForSpecificPlugin(TEST_PICO_CLI_PLUGIN)); + contextImpl.registerPlugins(); final Optional requestedPlugin = findTestPlugin(contextImpl.getRegisteredPlugins(), TestPicoCLIPlugin.class); assertThat(requestedPlugin).isPresent(); - assertThat(requestedPlugin.get().getState()).isEqualTo("registered"); + assertThat(requestedPlugin.get().getState()).isEqualTo(REGISTERED); final Optional nonRequestedPlugin = findTestPlugin(contextImpl.getRegisteredPlugins(), TestBesuEventsPlugin.class); @@ -212,9 +230,9 @@ public void shouldRegisterOnlySpecifiedPluginWhenPluginsOptionIsSet() { @Test public void shouldNotRegisterUnspecifiedPluginsWhenPluginsOptionIsSet() { - final PluginConfiguration config = createConfigurationForSpecificPlugin("TestPicoCLIPlugin"); assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); - contextImpl.registerPlugins(config); + contextImpl.initialize(createConfigurationForSpecificPlugin(TEST_PICO_CLI_PLUGIN)); + contextImpl.registerPlugins(); final Optional nonRequestedPlugin = findTestPlugin(contextImpl.getRegisteredPlugins(), TestBesuEventsPlugin.class); @@ -223,13 +241,12 @@ public void shouldNotRegisterUnspecifiedPluginsWhenPluginsOptionIsSet() { @Test void shouldThrowExceptionIfExplicitlySpecifiedPluginNotFound() { - PluginConfiguration config = createConfigurationForSpecificPlugin("NonExistentPlugin"); - + contextImpl.initialize(createConfigurationForSpecificPlugin(NON_EXISTENT_PLUGIN)); String exceptionMessage = - assertThrows(NoSuchElementException.class, () -> contextImpl.registerPlugins(config)) + assertThrows(NoSuchElementException.class, () -> contextImpl.registerPlugins()) .getMessage(); final String expectedMessage = - "The following requested plugins were not found: NonExistentPlugin"; + "The following requested plugins were not found: " + NON_EXISTENT_PLUGIN; assertThat(exceptionMessage).isEqualTo(expectedMessage); assertThat(contextImpl.getRegisteredPlugins()).isEmpty(); } @@ -241,19 +258,180 @@ void shouldNotRegisterAnyPluginsIfExternalPluginsDisabled() { .pluginsDir(DEFAULT_PLUGIN_DIRECTORY) .externalPluginsEnabled(false) .build(); - contextImpl.registerPlugins(config); + contextImpl.initialize(config); + contextImpl.registerPlugins(); assertThat(contextImpl.getRegisteredPlugins().isEmpty()).isTrue(); } @Test void shouldRegisterPluginsIfExternalPluginsEnabled() { + contextImpl.initialize(DEFAULT_CONFIGURATION); + contextImpl.registerPlugins(); + assertThat(contextImpl.getRegisteredPlugins().isEmpty()).isFalse(); + } + + @Test + void shouldHaltOnRegisterErrorWhenFlagIsFalse() { + System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_REGISTER); + PluginConfiguration config = PluginConfiguration.builder() + .requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN))) .pluginsDir(DEFAULT_PLUGIN_DIRECTORY) - .externalPluginsEnabled(true) + .continueOnPluginError(false) .build(); - contextImpl.registerPlugins(config); - assertThat(contextImpl.getRegisteredPlugins().isEmpty()).isFalse(); + + contextImpl.initialize(config); + + String errorMessage = + String.format("Error registering plugin of type %s", TestPicoCLIPlugin.class.getName()); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> contextImpl.registerPlugins()) + .withMessageContaining(errorMessage); + } + + @Test + void shouldNotHaltOnRegisterErrorWhenFlagIsTrue() { + System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_REGISTER); + + PluginConfiguration config = + PluginConfiguration.builder() + .pluginsDir(DEFAULT_PLUGIN_DIRECTORY) + .continueOnPluginError(true) + .build(); + + contextImpl.initialize(config); + contextImpl.registerPlugins(); + assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class); + } + + @Test + void shouldHaltOnBeforeExternalServicesErrorWhenFlagIsFalse() { + System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_BEFORE_EXTERNAL_SERVICES); + + PluginConfiguration config = + PluginConfiguration.builder() + .requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN))) + .pluginsDir(DEFAULT_PLUGIN_DIRECTORY) + .continueOnPluginError(false) + .build(); + + contextImpl.initialize(config); + contextImpl.registerPlugins(); + + String errorMessage = + String.format( + "Error calling `beforeExternalServices` on plugin of type %s", + TestPicoCLIPlugin.class.getName()); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> contextImpl.beforeExternalServices()) + .withMessageContaining(errorMessage); + } + + @Test + void shouldNotHaltOnBeforeExternalServicesErrorWhenFlagIsTrue() { + System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_BEFORE_EXTERNAL_SERVICES); + + PluginConfiguration config = + PluginConfiguration.builder() + .requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN))) + .pluginsDir(DEFAULT_PLUGIN_DIRECTORY) + .continueOnPluginError(true) + .build(); + + contextImpl.initialize(config); + contextImpl.registerPlugins(); + contextImpl.beforeExternalServices(); + + assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class); + } + + @Test + void shouldHaltOnBeforeMainLoopErrorWhenFlagIsFalse() { + System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_START); + + PluginConfiguration config = + PluginConfiguration.builder() + .requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN))) + .pluginsDir(DEFAULT_PLUGIN_DIRECTORY) + .continueOnPluginError(false) + .build(); + + contextImpl.initialize(config); + contextImpl.registerPlugins(); + contextImpl.beforeExternalServices(); + + String errorMessage = + String.format("Error starting plugin of type %s", TestPicoCLIPlugin.class.getName()); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> contextImpl.startPlugins()) + .withMessageContaining(errorMessage); + } + + @Test + void shouldNotHaltOnBeforeMainLoopErrorWhenFlagIsTrue() { + System.setProperty(TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_BEFORE_MAIN_LOOP); + + PluginConfiguration config = + PluginConfiguration.builder() + .requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN))) + .pluginsDir(DEFAULT_PLUGIN_DIRECTORY) + .continueOnPluginError(true) + .build(); + + contextImpl.initialize(config); + contextImpl.registerPlugins(); + contextImpl.beforeExternalServices(); + contextImpl.startPlugins(); + + assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class); + } + + @Test + void shouldHaltOnAfterExternalServicePostMainLoopErrorWhenFlagIsFalse() { + System.setProperty( + TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP); + + PluginConfiguration config = + PluginConfiguration.builder() + .requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN))) + .pluginsDir(DEFAULT_PLUGIN_DIRECTORY) + .continueOnPluginError(false) + .build(); + + contextImpl.initialize(config); + contextImpl.registerPlugins(); + contextImpl.beforeExternalServices(); + contextImpl.startPlugins(); + + String errorMessage = + String.format( + "Error calling `afterExternalServicePostMainLoop` on plugin of type %s", + TestPicoCLIPlugin.class.getName()); + assertThatExceptionOfType(RuntimeException.class) + .isThrownBy(() -> contextImpl.afterExternalServicesMainLoop()) + .withMessageContaining(errorMessage); + } + + @Test + void shouldNotHaltOnAfterExternalServicePostMainLoopErrorWhenFlagIsTrue() { + System.setProperty( + TEST_PICO_CLI_PLUGIN_TEST_OPTION, FAIL_AFTER_EXTERNAL_SERVICE_POST_MAIN_LOOP); + + PluginConfiguration config = + PluginConfiguration.builder() + .requestedPlugins(List.of(new PluginInfo(TEST_PICO_CLI_PLUGIN))) + .pluginsDir(DEFAULT_PLUGIN_DIRECTORY) + .continueOnPluginError(true) + .build(); + + contextImpl.initialize(config); + contextImpl.registerPlugins(); + contextImpl.beforeExternalServices(); + contextImpl.startPlugins(); + contextImpl.afterExternalServicesMainLoop(); + + assertThat(contextImpl.getRegisteredPlugins()).isNotInstanceOfAny(TestPicoCLIPlugin.class); } private PluginConfiguration createConfigurationForSpecificPlugin(final String pluginName) { diff --git a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java index 79879538bd5..e93518cad43 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/BesuCommand.java @@ -118,7 +118,6 @@ import org.hyperledger.besu.ethereum.core.MiningParametersMetrics; import org.hyperledger.besu.ethereum.core.PrivacyParameters; import org.hyperledger.besu.ethereum.core.VersionMetadata; -import org.hyperledger.besu.ethereum.core.plugins.PluginConfiguration; import org.hyperledger.besu.ethereum.eth.sync.SyncMode; import org.hyperledger.besu.ethereum.eth.sync.SynchronizerConfiguration; import org.hyperledger.besu.ethereum.eth.transactions.ImmutableTransactionPoolConfiguration; @@ -1080,9 +1079,8 @@ private IExecutionStrategy createExecuteTask(final IExecutionStrategy nextStep) private IExecutionStrategy createPluginRegistrationTask(final IExecutionStrategy nextStep) { return parseResult -> { - PluginConfiguration configuration = - PluginsConfigurationOptions.fromCommandLine(parseResult.commandSpec().commandLine()); - besuPluginContext.registerPlugins(configuration); + besuPluginContext.initialize(PluginsConfigurationOptions.fromCommandLine(commandLine)); + besuPluginContext.registerPlugins(); commandLine.setExecutionStrategy(nextStep); return commandLine.execute(parseResult.originalArgs().toArray(new String[0])); }; diff --git a/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java b/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java index ba05f455246..8f83c71a787 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/DefaultCommandValues.java @@ -128,6 +128,9 @@ public interface DefaultCommandValues { /** The constant DEFAULT_PLUGINS_OPTION_NAME. */ String DEFAULT_PLUGINS_OPTION_NAME = "--plugins"; + /** The constant DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME. */ + String DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME = "--plugin-continue-on-error"; + /** The constant DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME. */ String DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME = "--Xplugins-external-enabled"; diff --git a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/PluginsConfigurationOptions.java b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/PluginsConfigurationOptions.java index 47df831c577..25893fff895 100644 --- a/besu/src/main/java/org/hyperledger/besu/cli/options/stable/PluginsConfigurationOptions.java +++ b/besu/src/main/java/org/hyperledger/besu/cli/options/stable/PluginsConfigurationOptions.java @@ -14,6 +14,7 @@ */ package org.hyperledger.besu.cli.options.stable; +import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_CONTINUE_ON_PLUGIN_ERROR_OPTION_NAME; import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_PLUGINS_EXTERNAL_ENABLED_OPTION_NAME; import static org.hyperledger.besu.cli.DefaultCommandValues.DEFAULT_PLUGINS_OPTION_NAME; @@ -27,7 +28,7 @@ import picocli.CommandLine; -/** The Plugins Options options. */ +/** The Plugins options. */ public class PluginsConfigurationOptions implements CLIOptions { @CommandLine.Option( @@ -44,9 +45,17 @@ public class PluginsConfigurationOptions implements CLIOptions