diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTask.java index 43cfeadf89c..413cd1da469 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTask.java @@ -32,12 +32,14 @@ import org.hyperledger.besu.plugin.services.MetricsSystem; import java.util.Collection; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import org.jetbrains.annotations.NotNull; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -74,12 +76,38 @@ private CompleteBlocksTask( this.blocks = headers.stream() .filter(this::hasEmptyBody) - .collect(toMap(BlockHeader::getNumber, header -> new Block(header, BlockBody.empty()))); + .collect( + toMap( + BlockHeader::getNumber, + header -> + new Block( + header, + createEmptyBodyBasedOnProtocolSchedule(protocolSchedule, header)))); + } + + @NotNull + private BlockBody createEmptyBodyBasedOnProtocolSchedule( + final ProtocolSchedule protocolSchedule, final BlockHeader header) { + return new BlockBody( + Collections.emptyList(), + Collections.emptyList(), + isWithdrawalsEnabled(protocolSchedule, header) + ? Optional.of(Collections.emptyList()) + : Optional.empty()); + } + + private boolean isWithdrawalsEnabled( + final ProtocolSchedule protocolSchedule, final BlockHeader header) { + return protocolSchedule.getByBlockHeader(header).getWithdrawalsProcessor().isPresent(); } private boolean hasEmptyBody(final BlockHeader header) { return header.getOmmersHash().equals(Hash.EMPTY_LIST_HASH) - && header.getTransactionsRoot().equals(Hash.EMPTY_TRIE_HASH); + && header.getTransactionsRoot().equals(Hash.EMPTY_TRIE_HASH) + && header + .getWithdrawalsRoot() + .map(wsRoot -> wsRoot.equals(Hash.EMPTY_TRIE_HASH)) + .orElse(true); } public static CompleteBlocksTask forHeaders( diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java index 5193dd99865..097a8e814d0 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/tasks/CompleteBlocksTaskTest.java @@ -17,26 +17,42 @@ import static java.util.Arrays.asList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.spy; +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.chain.Blockchain; import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.BlockBody; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.BlockHeaderTestFixture; +import org.hyperledger.besu.ethereum.core.Withdrawal; import org.hyperledger.besu.ethereum.eth.manager.EthProtocolManagerTestUtil; import org.hyperledger.besu.ethereum.eth.manager.RespondingEthPeer; import org.hyperledger.besu.ethereum.eth.manager.ethtaskutils.RetryingMessageTaskTest; import org.hyperledger.besu.ethereum.eth.manager.exceptions.MaxRetriesReachedException; import org.hyperledger.besu.ethereum.eth.manager.task.EthTask; import org.hyperledger.besu.ethereum.eth.messages.GetBlockBodiesMessage; +import org.hyperledger.besu.ethereum.mainnet.BodyValidation; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; +import org.hyperledger.besu.ethereum.mainnet.WithdrawalsProcessor; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import java.util.ArrayList; +import java.util.Collections; import java.util.List; +import java.util.Optional; import java.util.concurrent.CompletableFuture; import java.util.stream.Collectors; +import org.apache.tuweni.units.bigints.UInt64; import org.junit.Test; +import org.mockito.Mockito; public class CompleteBlocksTaskTest extends RetryingMessageTaskTest> { @@ -79,6 +95,114 @@ public void shouldCompleteWithoutPeersWhenAllBlocksAreEmpty() { assertThat(task.run()).isCompletedWithValue(blocks); } + @Test + public void shouldCreateWithdrawalsAwareEmptyBlock_whenWithdrawalsAreEnabled() { + final ProtocolSchedule mockProtocolSchedule = Mockito.mock(ProtocolSchedule.class); + final ProtocolSpec mockParisSpec = Mockito.mock(ProtocolSpec.class); + final ProtocolSpec mockShanghaiSpec = Mockito.mock(ProtocolSpec.class); + final WithdrawalsProcessor mockWithdrawalsProcessor = Mockito.mock(WithdrawalsProcessor.class); + + final BlockHeader header1 = + new BlockHeaderTestFixture().number(1).withdrawalsRoot(null).buildHeader(); + final BlockHeader header2 = + new BlockHeaderTestFixture().number(2).withdrawalsRoot(Hash.EMPTY_TRIE_HASH).buildHeader(); + + when(mockProtocolSchedule.getByBlockHeader((eq(header1)))).thenReturn(mockParisSpec); + when(mockParisSpec.getWithdrawalsProcessor()).thenReturn(Optional.empty()); + when(mockProtocolSchedule.getByBlockHeader((eq(header2)))).thenReturn(mockShanghaiSpec); + when(mockShanghaiSpec.getWithdrawalsProcessor()) + .thenReturn(Optional.of(mockWithdrawalsProcessor)); + + final Block block1 = + new Block( + header1, + new BlockBody(Collections.emptyList(), Collections.emptyList(), Optional.empty())); + final Block block2 = + new Block( + header2, + new BlockBody( + Collections.emptyList(), + Collections.emptyList(), + Optional.of(Collections.emptyList()))); + + final List expectedBlocks = asList(block1, block2); + final EthTask> task = + CompleteBlocksTask.forHeaders( + mockProtocolSchedule, + ethContext, + List.of(header1, header2), + maxRetries, + new NoOpMetricsSystem()); + assertThat(task.run()).isCompletedWithValue(expectedBlocks); + } + + @Test + public void shouldCompleteBlockThatOnlyContainsWithdrawals_whenWithdrawalsAreEnabled() { + final ProtocolSchedule mockProtocolSchedule = Mockito.mock(ProtocolSchedule.class); + final ProtocolSpec mockParisSpec = Mockito.mock(ProtocolSpec.class); + final ProtocolSpec mockShanghaiSpec = Mockito.mock(ProtocolSpec.class); + final WithdrawalsProcessor mockWithdrawalsProcessor = Mockito.mock(WithdrawalsProcessor.class); + + final Withdrawal withdrawal = + new Withdrawal(UInt64.ONE, UInt64.ONE, Address.fromHexString("0x1"), GWei.ONE); + final List withdrawals = List.of(withdrawal); + final Hash withdrawalsRoot = BodyValidation.withdrawalsRoot(withdrawals); + + final BlockHeader header1 = new BlockHeaderTestFixture().number(1).buildHeader(); + final BlockHeader header2 = + new BlockHeaderTestFixture().number(2).withdrawalsRoot(withdrawalsRoot).buildHeader(); + final BlockHeader header3 = + new BlockHeaderTestFixture().number(3).withdrawalsRoot(Hash.EMPTY_TRIE_HASH).buildHeader(); + + final Block block1 = new Block(header1, BlockBody.empty()); + final Block block2 = + new Block( + header2, + new BlockBody( + Collections.emptyList(), Collections.emptyList(), Optional.of(withdrawals))); + final Block block3 = + new Block( + header3, + new BlockBody( + Collections.emptyList(), + Collections.emptyList(), + Optional.of(Collections.emptyList()))); + final List expected = asList(block1, block2, block3); + + final RespondingEthPeer respondingPeer = + EthProtocolManagerTestUtil.createPeer(ethProtocolManager, 1000); + final RespondingEthPeer.Responder responder = responderForFakeBlocks(expected); + + when(mockProtocolSchedule.getByBlockHeader((eq(header1)))).thenReturn(mockParisSpec); + when(mockParisSpec.getWithdrawalsProcessor()).thenReturn(Optional.empty()); + when(mockProtocolSchedule.getByBlockHeader((eq(header3)))).thenReturn(mockShanghaiSpec); + when(mockShanghaiSpec.getWithdrawalsProcessor()) + .thenReturn(Optional.of(mockWithdrawalsProcessor)); + + final EthTask> task = + CompleteBlocksTask.forHeaders( + mockProtocolSchedule, + ethContext, + List.of(header1, header2, header3), + maxRetries, + new NoOpMetricsSystem()); + + final CompletableFuture> runningTask = task.run(); + + assertThat(runningTask).isNotDone(); + respondingPeer.respond(responder); + assertThat(runningTask).isCompletedWithValue(expected); + } + + private RespondingEthPeer.Responder responderForFakeBlocks(final List blocks) { + final Blockchain mockBlockchain = spy(blockchain); + for (Block block : blocks) { + when(mockBlockchain.getBlockBody(block.getHash())).thenReturn(Optional.of(block.getBody())); + } + + return RespondingEthPeer.blockchainResponder(mockBlockchain); + } + @SuppressWarnings("unchecked") @Test public void shouldReduceTheBlockSegmentSizeAfterEachRetry() {