From f92780859d7c3a31929b370cf6fc23e70f8ffb47 Mon Sep 17 00:00:00 2001 From: Gabriel-Trintinalia Date: Thu, 6 Mar 2025 15:13:51 +1100 Subject: [PATCH] eth_simulateV1 - Fill gaps between block calls (#8375) Signed-off-by: Gabriel-Trintinalia --- .../services/BlockSimulatorServiceImpl.java | 7 +- .../transaction/BlockSimulationParameter.java | 55 +++++ .../ethereum/transaction/BlockSimulator.java | 36 ++- .../ethereum/transaction/BlockStateCall.java | 10 +- .../ethereum/transaction/BlockStateCalls.java | 135 ++++++++++ .../transaction/BlockSimulatorTest.java | 17 +- .../transaction/BlockStateCallsTest.java | 230 ++++++++++++++++++ plugin-api/build.gradle | 2 +- .../besu/plugin/data/BlockOverrides.java | 22 +- 9 files changed, 485 insertions(+), 29 deletions(-) create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationParameter.java create mode 100644 ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCalls.java create mode 100644 ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockStateCallsTest.java diff --git a/besu/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java b/besu/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java index ca463cdaa..60494d177 100644 --- a/besu/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java +++ b/besu/src/main/java/org/hyperledger/besu/services/BlockSimulatorServiceImpl.java @@ -21,6 +21,7 @@ import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.MiningConfiguration; import org.hyperledger.besu.ethereum.core.MutableWorldState; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.transaction.BlockSimulationParameter; import org.hyperledger.besu.ethereum.transaction.BlockSimulationResult; import org.hyperledger.besu.ethereum.transaction.BlockSimulator; import org.hyperledger.besu.ethereum.transaction.BlockStateCall; @@ -116,10 +117,12 @@ public class BlockSimulatorServiceImpl implements BlockSimulationService { List callParameters = transactions.stream().map(CallParameter::fromTransaction).toList(); BlockStateCall blockStateCall = - new BlockStateCall(callParameters, blockOverrides, stateOverrides, true); + new BlockStateCall(callParameters, blockOverrides, stateOverrides); try (final MutableWorldState ws = getWorldState(header, persistWorldState)) { + BlockSimulationParameter blockSimulationParameter = + new BlockSimulationParameter(blockStateCall, true); List results = - blockSimulator.process(header, List.of(blockStateCall), ws); + blockSimulator.process(header, blockSimulationParameter, ws); BlockSimulationResult result = results.getFirst(); if (persistWorldState) { ws.persist(result.getBlock().getHeader()); diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationParameter.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationParameter.java new file mode 100644 index 000000000..ab96a91b8 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulationParameter.java @@ -0,0 +1,55 @@ +/* + * Copyright contributors to 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.transaction; + +import static com.google.common.base.Preconditions.checkNotNull; + +import java.util.List; + +public class BlockSimulationParameter { + + static final BlockSimulationParameter EMPTY = new BlockSimulationParameter(List.of(), false); + + final List blockStateCalls; + private final boolean validation; + + public BlockSimulationParameter(final List blockStateCalls) { + this.blockStateCalls = blockStateCalls; + this.validation = false; + } + + public BlockSimulationParameter(final BlockStateCall blockStateCall) { + this(List.of(blockStateCall)); + } + + public BlockSimulationParameter(final BlockStateCall blockStateCall, final boolean validation) { + this(List.of(blockStateCall), validation); + } + + public BlockSimulationParameter( + final List blockStateCalls, final boolean validation) { + checkNotNull(blockStateCalls); + this.blockStateCalls = blockStateCalls; + this.validation = validation; + } + + public List getBlockStateCalls() { + return blockStateCalls; + } + + public boolean isValidation() { + return validation; + } +} diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java index b6e59df4a..c4fcd43af 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockSimulator.java @@ -14,6 +14,7 @@ */ package org.hyperledger.besu.ethereum.transaction; +import static org.hyperledger.besu.ethereum.transaction.BlockStateCalls.fillBlockStateCalls; import static org.hyperledger.besu.ethereum.trie.diffbased.common.provider.WorldStateQueryParams.withBlockHeaderAndNoUpdateNodeHead; import org.hyperledger.besu.datatypes.Address; @@ -90,11 +91,11 @@ public class BlockSimulator { * Processes a list of BlockStateCalls sequentially, collecting the results. * * @param header The block header for all simulations. - * @param blockStateCalls The list of BlockStateCalls to process. + * @param blockSimulationParameter The blockSimulationParameter containing the block state calls. * @return A list of BlockSimulationResult objects from processing each BlockStateCall. */ public List process( - final BlockHeader header, final List blockStateCalls) { + final BlockHeader header, final BlockSimulationParameter blockSimulationParameter) { try (final MutableWorldState ws = worldStateArchive .getWorldState(withBlockHeaderAndNoUpdateNodeHead(header)) @@ -102,7 +103,7 @@ public class BlockSimulator { () -> new IllegalArgumentException( "Public world state not available for block " + header.toLogString()))) { - return process(header, blockStateCalls, ws); + return process(header, blockSimulationParameter, ws); } catch (IllegalArgumentException e) { throw e; } catch (final Exception e) { @@ -114,18 +115,24 @@ public class BlockSimulator { * Processes a list of BlockStateCalls sequentially, collecting the results. * * @param header The block header for all simulations. - * @param blockStateCalls The list of BlockStateCalls to process. + * @param blockSimulationParameter The blockSimulationParameter containing the block state calls. * @param worldState The initial MutableWorldState to start with. * @return A list of BlockSimulationResult objects from processing each BlockStateCall. */ public List process( final BlockHeader header, - final List blockStateCalls, + final BlockSimulationParameter blockSimulationParameter, final MutableWorldState worldState) { List simulationResults = new ArrayList<>(); + + // Fill gaps between blocks + List blockStateCalls = + fillBlockStateCalls(blockSimulationParameter.getBlockStateCalls(), header); + for (BlockStateCall blockStateCall : blockStateCalls) { BlockSimulationResult simulationResult = - processSingleBlockStateCall(header, blockStateCall, worldState); + processBlockStateCall( + header, blockStateCall, worldState, blockSimulationParameter.isValidation()); simulationResults.add(simulationResult); } return simulationResults; @@ -139,8 +146,11 @@ public class BlockSimulator { * @param ws The MutableWorldState to use for the simulation. * @return A BlockSimulationResult from processing the BlockStateCall. */ - private BlockSimulationResult processSingleBlockStateCall( - final BlockHeader header, final BlockStateCall blockStateCall, final MutableWorldState ws) { + private BlockSimulationResult processBlockStateCall( + final BlockHeader header, + final BlockStateCall blockStateCall, + final MutableWorldState ws, + final boolean shouldValidate) { BlockOverrides blockOverrides = blockStateCall.getBlockOverrides(); long timestamp = blockOverrides.getTimestamp().orElse(header.getTimestamp() + 1); ProtocolSpec newProtocolSpec = protocolSchedule.getForNextBlockHeader(header, timestamp); @@ -159,7 +169,12 @@ public class BlockSimulator { List transactionSimulatorResults = processTransactions( - blockHeader, blockStateCall, ws, miningBeneficiaryCalculator, blockHashLookup); + blockHeader, + blockStateCall, + ws, + shouldValidate, + miningBeneficiaryCalculator, + blockHashLookup); return finalizeBlock( blockHeader, blockStateCall, ws, newProtocolSpec, transactionSimulatorResults); @@ -170,6 +185,7 @@ public class BlockSimulator { final BlockHeader blockHeader, final BlockStateCall blockStateCall, final MutableWorldState ws, + final boolean shouldValidate, final MiningBeneficiaryCalculator miningBeneficiaryCalculator, final BlockHashLookup blockHashLookup) { @@ -182,7 +198,7 @@ public class BlockSimulator { transactionSimulator.processWithWorldUpdater( callParameter, Optional.empty(), // We have already applied state overrides on block level - buildTransactionValidationParams(blockStateCall.isValidate()), + buildTransactionValidationParams(shouldValidate), OperationTracer.NO_TRACING, blockHeader, transactionUpdater, diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCall.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCall.java index 9f6f46fd7..8c2074bc3 100644 --- a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCall.java +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCall.java @@ -29,22 +29,18 @@ public class BlockStateCall { private final StateOverrideMap stateOverrideMap; - private final boolean validation; - public BlockStateCall( final List calls, final BlockOverrides blockOverrides, - final StateOverrideMap stateOverrideMap, - final boolean validation) { + final StateOverrideMap stateOverrideMap) { this.calls = calls != null ? calls : new ArrayList<>(); this.blockOverrides = blockOverrides != null ? blockOverrides : BlockOverrides.builder().build(); this.stateOverrideMap = stateOverrideMap; - this.validation = validation; } - public boolean isValidate() { - return validation; + public BlockStateCall(final BlockOverrides blockOverrides) { + this(null, blockOverrides, null); } public BlockOverrides getBlockOverrides() { diff --git a/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCalls.java b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCalls.java new file mode 100644 index 000000000..28aa337c2 --- /dev/null +++ b/ethereum/core/src/main/java/org/hyperledger/besu/ethereum/transaction/BlockStateCalls.java @@ -0,0 +1,135 @@ +/* + * Copyright contributors to 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.transaction; + +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.plugin.data.BlockOverrides; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +/** + * A class that manages a chain of BlockStateCalls. It fills gaps between blocks and sets the + * correct block number and timestamp when they are not set. + */ +public class BlockStateCalls { + private static final long MAX_BLOCK_CALL_SIZE = 256; + private static final long TIMESTAMP_INCREMENT = 12; + + /** + * Normalizes a list of BlockStateCalls by filling gaps and setting the correct block number and + * timestamp. + * + * @param blockStateCalls the list of BlockStateCalls to normalize + * @param header the initial block header + * @return a normalized list of BlockStateCalls + */ + public static List fillBlockStateCalls( + final List blockStateCalls, final BlockHeader header) { + long lastPresentBlockNumber = findLastBlockNumber(blockStateCalls); + if (lastPresentBlockNumber > header.getNumber() + MAX_BLOCK_CALL_SIZE) { + throw new IllegalArgumentException( + String.format( + "Block number %d exceeds the limit of %d (header: %d + MAX_BLOCK_CALL_SIZE: %d)", + lastPresentBlockNumber, + header.getNumber() + MAX_BLOCK_CALL_SIZE, + header.getNumber(), + MAX_BLOCK_CALL_SIZE)); + } + List filledCalls = new ArrayList<>(); + long currentBlock = header.getNumber(); + long currentTimestamp = header.getTimestamp(); + for (BlockStateCall blockStateCall : blockStateCalls) { + long nextBlockNumber = + blockStateCall.getBlockOverrides().getBlockNumber().orElse(currentBlock + 1); + List intermediateBlocks = + new ArrayList<>( + generateIntermediateBlocks(nextBlockNumber, currentBlock, currentTimestamp)); + // Add intermediate blocks + for (BlockStateCall intermediateBlock : intermediateBlocks) { + add(filledCalls, intermediateBlock, currentBlock, currentTimestamp); + currentBlock = intermediateBlock.getBlockOverrides().getBlockNumber().orElseThrow(); + currentTimestamp = intermediateBlock.getBlockOverrides().getTimestamp().orElseThrow(); + } + // set the block number and timestamp if they are not set + if (blockStateCall.getBlockOverrides().getBlockNumber().isEmpty()) { + blockStateCall.getBlockOverrides().setBlockNumber(currentBlock + 1); + } + if (blockStateCall.getBlockOverrides().getTimestamp().isEmpty()) { + blockStateCall.getBlockOverrides().setTimestamp(currentTimestamp + TIMESTAMP_INCREMENT); + } + // Add the current block + add(filledCalls, blockStateCall, currentBlock, currentTimestamp); + currentBlock = blockStateCall.getBlockOverrides().getBlockNumber().orElseThrow(); + currentTimestamp = blockStateCall.getBlockOverrides().getTimestamp().orElseThrow(); + } + return filledCalls; + } + + private static void add( + final List blockStateCalls, + final BlockStateCall blockStateCall, + final long currentBlockNumber, + final long currentTimestamp) { + long blockNumber = blockStateCall.getBlockOverrides().getBlockNumber().orElseThrow(); + long timestamp = blockStateCall.getBlockOverrides().getTimestamp().orElseThrow(); + if (blockNumber <= currentBlockNumber) { + throw new IllegalArgumentException( + String.format( + "Block number is invalid. Trying to add a call at block number %s, while current block number is %s.", + blockNumber, currentBlockNumber)); + } + if (timestamp <= currentTimestamp) { + throw new IllegalArgumentException( + String.format( + "Timestamp is invalid. Trying to add a call at timestamp %s, while current timestamp is %s.", + timestamp, currentTimestamp)); + } + blockStateCalls.add(blockStateCall); + } + + private static List generateIntermediateBlocks( + final long targetBlockNumber, final long startBlockNumber, final long startTimestamp) { + List intermediateBlocks = new ArrayList<>(); + long blockNumberDiff = targetBlockNumber - startBlockNumber; + for (int i = 1; i < blockNumberDiff; i++) { + long nextBlockNumber = startBlockNumber + i; + long nextTimestamp = startTimestamp + TIMESTAMP_INCREMENT * i; + var nextBlockOverrides = + BlockOverrides.builder().blockNumber(nextBlockNumber).timestamp(nextTimestamp).build(); + intermediateBlocks.add(new BlockStateCall(nextBlockOverrides)); + } + return intermediateBlocks; + } + + private static long findLastBlockNumber(final List blockStateCalls) { + var lastPresentBlockNumber = + blockStateCalls.stream() + .map(blockStateCall -> blockStateCall.getBlockOverrides().getBlockNumber()) + .filter(Optional::isPresent) + .mapToLong(Optional::get) + .max() + .orElse(-1); + long callsAfterLastPresentBlockNumber = + blockStateCalls.stream() + .filter( + blockStateCall -> + blockStateCall.getBlockOverrides().getBlockNumber().orElse(Long.MAX_VALUE) + > lastPresentBlockNumber) + .count(); + return lastPresentBlockNumber + callsAfterLastPresentBlockNumber; + } +} diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java index 48899a5db..d8b0be31f 100644 --- a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockSimulatorTest.java @@ -52,7 +52,6 @@ import org.hyperledger.besu.evm.worldstate.WorldUpdater; import org.hyperledger.besu.plugin.data.BlockOverrides; import java.math.BigInteger; -import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -112,7 +111,7 @@ public class BlockSimulatorTest { .thenReturn(Optional.of(mutableWorldState)); List results = - blockSimulator.process(blockHeader, Collections.emptyList()); + blockSimulator.process(blockHeader, BlockSimulationParameter.EMPTY); assertNotNull(results); verify(worldStateArchive).getWorldState(withBlockHeaderAndNoUpdateNodeHead(blockHeader)); } @@ -125,7 +124,7 @@ public class BlockSimulatorTest { IllegalArgumentException exception = assertThrows( IllegalArgumentException.class, - () -> blockSimulator.process(blockHeader, Collections.emptyList())); + () -> blockSimulator.process(blockHeader, BlockSimulationParameter.EMPTY)); assertEquals( String.format("Public world state not available for block %s", blockHeader.toLogString()), @@ -136,7 +135,7 @@ public class BlockSimulatorTest { public void shouldStopWhenTransactionSimulationIsInvalid() { CallParameter callParameter = mock(CallParameter.class); - BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null, true); + BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null); TransactionSimulatorResult transactionSimulatorResult = mock(TransactionSimulatorResult.class); when(transactionSimulatorResult.isInvalid()).thenReturn(true); @@ -157,7 +156,9 @@ public class BlockSimulatorTest { BlockSimulationException exception = assertThrows( BlockSimulationException.class, - () -> blockSimulator.process(blockHeader, List.of(blockStateCall), mutableWorldState)); + () -> + blockSimulator.process( + blockHeader, new BlockSimulationParameter(blockStateCall), mutableWorldState)); assertEquals( "Transaction simulator result is invalid: Invalid Transaction", exception.getMessage()); @@ -167,7 +168,7 @@ public class BlockSimulatorTest { public void shouldStopWhenTransactionSimulationIsEmpty() { CallParameter callParameter = mock(CallParameter.class); - BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null, true); + BlockStateCall blockStateCall = new BlockStateCall(List.of(callParameter), null, null); when(transactionSimulator.processWithWorldUpdater( any(), @@ -183,7 +184,9 @@ public class BlockSimulatorTest { BlockSimulationException exception = assertThrows( BlockSimulationException.class, - () -> blockSimulator.process(blockHeader, List.of(blockStateCall), mutableWorldState)); + () -> + blockSimulator.process( + blockHeader, new BlockSimulationParameter(blockStateCall), mutableWorldState)); assertEquals("Transaction simulator result is empty", exception.getMessage()); } diff --git a/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockStateCallsTest.java b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockStateCallsTest.java new file mode 100644 index 000000000..ffc9e56cf --- /dev/null +++ b/ethereum/core/src/test/java/org/hyperledger/besu/ethereum/transaction/BlockStateCallsTest.java @@ -0,0 +1,230 @@ +/* + * Copyright contributors to 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.transaction; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.core.BlockHeader; +import org.hyperledger.besu.plugin.data.BlockOverrides; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class BlockStateCallsTest { + private BlockHeader mockBlockHeader; + private static final long MAX_BLOCK_CALL_SIZE = 256; + private final long headerTimestamp = 1000L; + + @BeforeEach + void setUp() { + mockBlockHeader = mock(BlockHeader.class); + when(mockBlockHeader.getTimestamp()).thenReturn(headerTimestamp); + when(mockBlockHeader.getNumber()).thenReturn(1L); + } + + /** Tests that gaps between block numbers are filled correctly when adding a BlockStateCall. */ + @Test + void shouldFillGapsBetweenBlockNumbers() { + // BlockHeader is at block number 1 + // BlockStateCall is at block number 4 + // Should fill gaps between 1 and 4 with block numbers 2 and 3 + + BlockStateCall blockStateCall = createBlockStateCall(4L, 1036L); + + List blockStateCalls = + BlockStateCalls.fillBlockStateCalls(List.of(blockStateCall), mockBlockHeader); + assertEquals(3, blockStateCalls.size()); + assertEquals(2L, blockStateCalls.get(0).getBlockOverrides().getBlockNumber().orElseThrow()); + assertEquals(1012L, blockStateCalls.get(0).getBlockOverrides().getTimestamp().orElseThrow()); + assertEquals(3L, blockStateCalls.get(1).getBlockOverrides().getBlockNumber().orElseThrow()); + assertEquals(1024L, blockStateCalls.get(1).getBlockOverrides().getTimestamp().orElseThrow()); + assertEquals(4L, blockStateCalls.get(2).getBlockOverrides().getBlockNumber().orElseThrow()); + assertEquals(1036L, blockStateCalls.get(2).getBlockOverrides().getTimestamp().orElseThrow()); + } + + /** + * Tests that the block number is updated correctly if it is not present in the BlockStateCall. + */ + @Test + void shouldUpdateBlockNumberIfNotPresent() { + // BlockHeader is at block number 1 + // BlockStateCall does not have a block number set + // Should set block number to 2 + + long expectedBlockNumber = 2L; + BlockStateCall blockStateCall = createBlockStateCall(null, null); + List blockStateCalls = + BlockStateCalls.fillBlockStateCalls(List.of(blockStateCall), mockBlockHeader); + + assertEquals(1, blockStateCalls.size()); + assertEquals( + expectedBlockNumber, + blockStateCalls.getFirst().getBlockOverrides().getBlockNumber().orElseThrow()); + } + + /** Tests that the timestamp is updated correctly if it is not present in the BlockStateCall. */ + @Test + void shouldUpdateTimestampIfNotPresent() { + // BlockHeader is at block number 1 and timestamp 1000 + // BlockStateCall does not have a timestamp set + // Should set timestamp to 1024 + + long blockNumber = 3L; + long expectedTimestamp = headerTimestamp + (blockNumber - 1L) * 12; + BlockStateCall blockStateCall = createBlockStateCall(blockNumber, null); + List blockStateCalls = + BlockStateCalls.fillBlockStateCalls(List.of(blockStateCall), mockBlockHeader); + assertEquals( + expectedTimestamp, + blockStateCalls.getLast().getBlockOverrides().getTimestamp().orElseThrow()); + } + + /** + * Tests that a list of BlockStateCalls is normalized correctly by filling gaps and setting block + * numbers and timestamps. + */ + @Test + void shouldFillBlockStateCalls() { + // BlockHeader is at block number 1 and timestamp 1000 + // BlockStateCalls are at block numbers 3 and 5 + // Should fill gaps between 1 and 3 and 3 and 5 with block numbers 2 and 4 + + List blockStateCalls = new ArrayList<>(); + blockStateCalls.add(createBlockStateCall(3L, 1024L)); + blockStateCalls.add(createBlockStateCall(5L, 1048L)); + + var normalizedCalls = BlockStateCalls.fillBlockStateCalls(blockStateCalls, mockBlockHeader); + + assertEquals(4, normalizedCalls.size()); + assertEquals(2L, normalizedCalls.get(0).getBlockOverrides().getBlockNumber().orElseThrow()); + assertEquals(1012L, normalizedCalls.get(0).getBlockOverrides().getTimestamp().orElseThrow()); + assertEquals(3L, normalizedCalls.get(1).getBlockOverrides().getBlockNumber().orElseThrow()); + assertEquals(1024L, normalizedCalls.get(1).getBlockOverrides().getTimestamp().orElseThrow()); + assertEquals(4L, normalizedCalls.get(2).getBlockOverrides().getBlockNumber().orElseThrow()); + assertEquals(1036L, normalizedCalls.get(2).getBlockOverrides().getTimestamp().orElseThrow()); + assertEquals(5L, normalizedCalls.get(3).getBlockOverrides().getBlockNumber().orElseThrow()); + assertEquals(1048L, normalizedCalls.get(3).getBlockOverrides().getTimestamp().orElseThrow()); + } + + /** + * Tests that an exception is thrown when a BlockStateCall with a block number less than or equal + * to the last block number is added. + */ + @Test + void shouldThrowExceptionForInvalidBlockNumber() { + // BlockHeader is at block number 1 + // BlockStateCall block number is 1 + // Should throw an exception because the block number is not greater than 1 + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + BlockStateCalls.fillBlockStateCalls( + List.of(createBlockStateCall(1L, 1012L)), mockBlockHeader)); + String expectedMessage = + String.format( + "Block number is invalid. Trying to add a call at block number %s, while current block number is %s.", + 1L, 1L); + assertEquals(expectedMessage, exception.getMessage()); + } + + /** + * Tests that an exception is thrown when a BlockStateCall with a timestamp less than or equal to + * the last timestamp is added. + */ + @Test + void shouldThrowExceptionForInvalidTimestamp() { + // BlockHeader is at block number 1 and timestamp 1000 + // BlockStateCall is at block number 2 and timestamp 1000 + // Should throw an exception because the timestamp is not greater than the 1000 + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + BlockStateCalls.fillBlockStateCalls( + List.of(createBlockStateCall(2L, headerTimestamp)), mockBlockHeader)); + String expectedMessage = + String.format( + "Timestamp is invalid. Trying to add a call at timestamp %s, while current timestamp is %s.", + headerTimestamp, headerTimestamp); // next timestamp + assertEquals(expectedMessage, exception.getMessage()); + } + + /** + * Tests that the chain is normalized by adding intermediate blocks and then fails when adding the + * last call due to an invalid timestamp. + */ + @Test + void shouldNormalizeChainAndFailOnInvalidTimestamp() { + // BlockHeader is at block number 1 and timestamp 1000 + // BlockStateCall is at block number 3 and timestamp 1012 + // Should throw an exception because the timestamp is not greater than 1012 + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> + BlockStateCalls.fillBlockStateCalls( + List.of(createBlockStateCall(3L, 1012L)), mockBlockHeader)); + assertEquals( + "Timestamp is invalid. Trying to add a call at timestamp 1012, while current timestamp is 1012.", + exception.getMessage()); + } + + /** Tests that an exception is thrown when the maximum number of BlockStateCalls is exceeded. */ + @Test + void shouldThrowExceptionWhenExceedingMaxBlocks() { + long maxAllowedBlockNumber = MAX_BLOCK_CALL_SIZE + 1; + long invalidBlockNumber = maxAllowedBlockNumber + 1; + BlockStateCall blockStateCall = createBlockStateCall(invalidBlockNumber, null); + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> BlockStateCalls.fillBlockStateCalls(List.of(blockStateCall), mockBlockHeader)); + String expectedMessage = + String.format( + "Block number %d exceeds the limit of %d (header: %d + MAX_BLOCK_CALL_SIZE: %d)", + invalidBlockNumber, maxAllowedBlockNumber, 1L, MAX_BLOCK_CALL_SIZE); + assertEquals(expectedMessage, exception.getMessage()); + } + + @Test + void shouldThrowExceptionWhenFillBlockStateCallsExceedsMaxBlockCallSize() { + List blockStateCalls = new ArrayList<>(); + blockStateCalls.add(createBlockStateCall(101L, 1609459212L)); + blockStateCalls.add(createBlockStateCall(257L, 1609459248L)); + blockStateCalls.add(createBlockStateCall(null, 1609459224L)); + + IllegalArgumentException exception = + assertThrows( + IllegalArgumentException.class, + () -> BlockStateCalls.fillBlockStateCalls(blockStateCalls, mockBlockHeader)); + assertEquals( + "Block number 258 exceeds the limit of 257 (header: 1 + MAX_BLOCK_CALL_SIZE: 256)", + exception.getMessage()); + } + + private BlockStateCall createBlockStateCall(final Long blockNumber, final Long timestamp) { + BlockOverrides blockOverrides = + BlockOverrides.builder().blockNumber(blockNumber).timestamp(timestamp).build(); + return new BlockStateCall(Collections.emptyList(), blockOverrides, null); + } +} diff --git a/plugin-api/build.gradle b/plugin-api/build.gradle index fcb26f533..eff967c15 100644 --- a/plugin-api/build.gradle +++ b/plugin-api/build.gradle @@ -71,7 +71,7 @@ Calculated : ${currentHash} tasks.register('checkAPIChanges', FileStateChecker) { description = "Checks that the API for the Plugin-API project does not change without deliberate thought" files = sourceSets.main.allJava.files - knownHash = 'D2ZMRGb2HS/9FgDE2mcizdxTQhFsGD4LS9lgngG/TnU=' + knownHash = 'Mp4ewH82ykJPHzMATeiPenUJ4TTMyDEad1MI8idyhWk=' } check.dependsOn('checkAPIChanges') diff --git a/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/BlockOverrides.java b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/BlockOverrides.java index 8d9f08a50..d172d9ec7 100644 --- a/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/BlockOverrides.java +++ b/plugin-api/src/main/java/org/hyperledger/besu/plugin/data/BlockOverrides.java @@ -28,8 +28,8 @@ import org.apache.tuweni.bytes.Bytes32; /** BlockOverrides represents the block overrides for a block. */ public class BlockOverrides { - private final Optional timestamp; - private final Optional blockNumber; + private Optional timestamp; + private Optional blockNumber; private final Optional blockHash; private final Optional prevRandao; private final Optional gasLimit; @@ -224,6 +224,24 @@ public class BlockOverrides { return blockHashLookup; } + /** + * Sets the timestamp. + * + * @param timestamp the timestamp to set + */ + public void setTimestamp(final Long timestamp) { + this.timestamp = Optional.ofNullable(timestamp); + } + + /** + * Sets the block number. + * + * @param blockNumber the block number to set + */ + public void setBlockNumber(final Long blockNumber) { + this.blockNumber = Optional.ofNullable(blockNumber); + } + /** * Creates a new Builder instance. *