From 9b67daaf3e67297ed355f6ff7ff66524a94dde53 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Fri, 27 Feb 2026 12:11:06 +0100 Subject: [PATCH 1/7] Pass agreed capabilities to PeerTask getRequestMessage and processResponse The PeerTask interface methods getRequestMessage and processResponse now receive the set of capabilities agreed with the peer, enabling implementations to dispatch to different protocol versions (e.g. eth/69 vs eth/70) based on what the peer supports. Signed-off-by: Fabio Di Fabio Co-Authored-By: Claude Sonnet 4.6 --- .../besu/ethereum/eth/manager/EthPeer.java | 8 - .../eth/manager/peertask/PeerTask.java | 7 +- .../manager/peertask/PeerTaskExecutor.java | 7 +- .../task/AbstractGetBodiesFromPeerTask.java | 4 +- .../peertask/task/GetBodiesFromPeerTask.java | 5 +- .../peertask/task/GetHeadersFromPeerTask.java | 7 +- .../GetPooledTransactionsFromPeerTask.java | 7 +- .../task/GetSyncBlockBodiesFromPeerTask.java | 5 +- .../task/GetSyncReceiptsFromPeerTask.java | 7 +- .../eth/messages/ReceiptsMessage.java | 51 +--- .../SnapSyncChainDownloadPipelineFactory.java | 2 +- .../besu/ethereum/eth/core/Utils.java | 47 +++- .../ethereum/eth/manager/EthPeerTest.java | 8 +- .../eth/manager/EthProtocolManagerTest.java | 7 +- .../ethereum/eth/manager/EthServerTest.java | 13 +- .../eth/manager/RespondingEthPeer.java | 18 +- .../peertask/PeerTaskExecutorTest.java | 251 +++++++++--------- .../task/GetBodiesFromPeerTaskTest.java | 10 +- .../task/GetHeadersFromPeerTaskTest.java | 92 ++++--- ...GetPooledTransactionsFromPeerTaskTest.java | 10 +- .../task/GetSyncReceiptsFromPeerTaskTest.java | 41 +-- .../eth/messages/MessageWrapperTest.java | 54 ++-- .../eth/messages/ReceiptsMessageTest.java | 9 +- .../fastsync/ImportSyncBlocksStepTest.java | 48 +--- 24 files changed, 377 insertions(+), 341 deletions(-) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeer.java index 3f7baba3565..0d8d9229339 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthPeer.java @@ -25,7 +25,6 @@ import org.hyperledger.besu.ethereum.eth.messages.GetBlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.GetNodeDataMessage; import org.hyperledger.besu.ethereum.eth.messages.GetPooledTransactionsMessage; -import org.hyperledger.besu.ethereum.eth.messages.GetReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.StatusMessage; import org.hyperledger.besu.ethereum.eth.messages.snap.GetAccountRangeMessage; import org.hyperledger.besu.ethereum.eth.messages.snap.GetByteCodesMessage; @@ -329,13 +328,6 @@ public RequestManager.ResponseStream getBodies(final List blockHashes) requestManagers.get(EthProtocol.NAME).get(EthProtocolMessages.GET_BLOCK_BODIES), message); } - public RequestManager.ResponseStream getReceipts(final List blockHashes) - throws PeerNotConnected { - final GetReceiptsMessage message = GetReceiptsMessage.create(blockHashes); - return sendRequest( - requestManagers.get(EthProtocol.NAME).get(EthProtocolMessages.GET_RECEIPTS), message); - } - public RequestManager.ResponseStream getNodeData(final Iterable nodeHashes) throws PeerNotConnected { final GetNodeDataMessage message = GetNodeDataMessage.create(nodeHashes); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTask.java index 0b6a84d4b0f..a4328d1e632 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTask.java @@ -15,10 +15,12 @@ package org.hyperledger.besu.ethereum.eth.manager.peertask; import org.hyperledger.besu.ethereum.eth.manager.EthPeerImmutableAttributes; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; import java.time.Duration; +import java.util.Set; import java.util.function.Predicate; /** @@ -39,17 +41,18 @@ public interface PeerTask { * * @return the request data to send to the EthPeer */ - MessageData getRequestMessage(); + MessageData getRequestMessage(final Set agreedCapabilities); /** * Parses and processes the MessageData response from the EthPeer * * @param messageData the response MessageData to be parsed + * @param agreedCapabilities the set of capabilities agreed with the peer * @return a T built from the response MessageData * @throws InvalidPeerTaskResponseException if the response messageData is invalid * @throws MalformedRlpFromPeerException if the peer sent malformed RLP */ - T processResponse(MessageData messageData) + T processResponse(MessageData messageData, final Set agreedCapabilities) throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException; /** diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskExecutor.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskExecutor.java index af51135f3c3..8bca8bae028 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskExecutor.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskExecutor.java @@ -16,6 +16,7 @@ import org.hyperledger.besu.ethereum.eth.manager.EthPeer; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection.PeerNotConnected; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason; @@ -30,6 +31,7 @@ import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.TimeoutException; import java.util.concurrent.atomic.AtomicInteger; @@ -126,7 +128,8 @@ public PeerTaskExecutorResult executeAgainstPeer( inflightRequestGauge.labels(inflightRequests::get, taskClassName); return inflightRequests; }); - MessageData requestMessageData = peerTask.getRequestMessage(); + final Set agreedCapabilities = peer.getAgreedCapabilities(); + MessageData requestMessageData = peerTask.getRequestMessage(agreedCapabilities); SubProtocol peerTaskSubProtocol = peerTask.getSubProtocol(); PeerTaskExecutorResult executorResult; int retriesRemaining = peerTask.getRetriesWithSamePeer(); @@ -144,7 +147,7 @@ public PeerTaskExecutorResult executeAgainstPeer( throw new InvalidPeerTaskResponseException("Null response"); } - result = peerTask.processResponse(responseMessageData); + result = peerTask.processResponse(responseMessageData, agreedCapabilities); } finally { inflightRequestCountForThisTaskClass.decrementAndGet(); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/AbstractGetBodiesFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/AbstractGetBodiesFromPeerTask.java index b14f0dfd3e3..1898ee8724a 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/AbstractGetBodiesFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/AbstractGetBodiesFromPeerTask.java @@ -21,10 +21,12 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskValidationResponse; import org.hyperledger.besu.ethereum.eth.messages.GetBlockBodiesMessage; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; import java.util.List; +import java.util.Set; import java.util.function.Predicate; /** @@ -66,7 +68,7 @@ public SubProtocol getSubProtocol() { } @Override - public MessageData getRequestMessage() { + public MessageData getRequestMessage(final Set agreedCapabilities) { return GetBlockBodiesMessage.create( blockHeaders.stream().map(BlockHeader::getBlockHash).toList()); } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBodiesFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBodiesFromPeerTask.java index 231f5832645..d27aea83c7d 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBodiesFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBodiesFromPeerTask.java @@ -21,10 +21,12 @@ import org.hyperledger.besu.ethereum.eth.manager.task.BodyIdentifier; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import java.util.ArrayList; import java.util.List; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,7 +56,8 @@ public GetBodiesFromPeerTask( } @Override - public List processResponse(final MessageData messageData) + public List processResponse( + final MessageData messageData, final Set agreedCapabilities) throws InvalidPeerTaskResponseException { // Blocks returned by this method are in the same order as the headers, but might not be // complete diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetHeadersFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetHeadersFromPeerTask.java index 141b94f1f8d..a56664cad58 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetHeadersFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetHeadersFromPeerTask.java @@ -26,12 +26,14 @@ import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockHeadersMessage; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; import java.time.Duration; import java.util.Comparator; import java.util.List; +import java.util.Set; import java.util.function.Predicate; import org.slf4j.Logger; @@ -156,7 +158,7 @@ public SubProtocol getSubProtocol() { } @Override - public MessageData getRequestMessage() { + public MessageData getRequestMessage(final Set agreedCapabilities) { if (blockHash != null) { return GetBlockHeadersMessage.create( blockHash, maxHeaders, skip, direction == Direction.REVERSE); @@ -167,7 +169,8 @@ public MessageData getRequestMessage() { } @Override - public List processResponse(final MessageData messageData) + public List processResponse( + final MessageData messageData, final Set agreedCapabilities) throws InvalidPeerTaskResponseException { if (messageData == null) { throw new InvalidPeerTaskResponseException("Response MessageData is null"); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetPooledTransactionsFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetPooledTransactionsFromPeerTask.java index 78d52772a81..58e3b63d2a1 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetPooledTransactionsFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetPooledTransactionsFromPeerTask.java @@ -23,10 +23,12 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskValidationResponse; import org.hyperledger.besu.ethereum.eth.messages.GetPooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.PooledTransactionsMessage; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; import java.util.List; +import java.util.Set; import java.util.function.Predicate; public class GetPooledTransactionsFromPeerTask implements PeerTask> { @@ -43,12 +45,13 @@ public SubProtocol getSubProtocol() { } @Override - public MessageData getRequestMessage() { + public MessageData getRequestMessage(final Set agreedCapabilities) { return GetPooledTransactionsMessage.create(hashes); } @Override - public List processResponse(final MessageData messageData) + public List processResponse( + final MessageData messageData, final Set agreedCapabilities) throws InvalidPeerTaskResponseException { final PooledTransactionsMessage pooledTransactionsMessage = PooledTransactionsMessage.readFrom(messageData); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncBlockBodiesFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncBlockBodiesFromPeerTask.java index 773c7d37435..b5fb644e9bd 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncBlockBodiesFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncBlockBodiesFromPeerTask.java @@ -21,10 +21,12 @@ import org.hyperledger.besu.ethereum.eth.manager.task.BodyIdentifier; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import java.util.ArrayList; import java.util.List; +import java.util.Set; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -54,7 +56,8 @@ public GetSyncBlockBodiesFromPeerTask( } @Override - public List processResponse(final MessageData messageData) + public List processResponse( + final MessageData messageData, final Set agreedCapabilities) throws InvalidPeerTaskResponseException { // Blocks returned by this method are in the same order as the headers, but might not be // complete diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java index 62cc2971e47..ffcc0713b2c 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java @@ -31,6 +31,7 @@ import org.hyperledger.besu.ethereum.eth.messages.GetReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; import org.hyperledger.besu.ethereum.rlp.RLPException; @@ -38,6 +39,7 @@ import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.Set; import java.util.function.Predicate; import com.google.common.annotations.VisibleForTesting; @@ -78,12 +80,13 @@ public SubProtocol getSubProtocol() { } @Override - public MessageData getRequestMessage() { + public MessageData getRequestMessage(final Set agreedCapabilities) { return GetReceiptsMessage.create(requestedHeaders.stream().map(BlockHeader::getHash).toList()); } @Override - public Map> processResponse(final MessageData messageData) + public Map> processResponse( + final MessageData messageData, final Set agreedCapabilities) throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { if (messageData == null) { throw new InvalidPeerTaskResponseException("Null message data"); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java index b5a0e9bb5bf..7e2eb3a4c39 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java @@ -15,21 +15,15 @@ package org.hyperledger.besu.ethereum.eth.messages; import org.hyperledger.besu.ethereum.core.SyncTransactionReceipt; -import org.hyperledger.besu.ethereum.core.TransactionReceipt; import org.hyperledger.besu.ethereum.core.encoding.receipt.SyncTransactionReceiptDecoder; -import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptDecoder; -import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; -import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.AbstractMessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; -import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.rlp.RLPInput; import java.util.ArrayList; import java.util.List; -import com.google.common.annotations.VisibleForTesting; import org.apache.tuweni.bytes.Bytes; public final class ReceiptsMessage extends AbstractMessageData { @@ -40,12 +34,8 @@ public final class ReceiptsMessage extends AbstractMessageData { private static final SyncTransactionReceiptDecoder DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER = new SyncTransactionReceiptDecoder(); - private final SyncTransactionReceiptDecoder syncTransactionReceiptDecoder; - - private ReceiptsMessage( - final Bytes data, final SyncTransactionReceiptDecoder syncTransactionReceiptDecoder) { + private ReceiptsMessage(final Bytes data) { super(data); - this.syncTransactionReceiptDecoder = syncTransactionReceiptDecoder; } public static ReceiptsMessage readFrom(final MessageData message) { @@ -57,23 +47,7 @@ public static ReceiptsMessage readFrom(final MessageData message) { throw new IllegalArgumentException( String.format("Message has code %d and thus is not a ReceiptsMessage.", code)); } - return new ReceiptsMessage(message.getData(), DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER); - } - - @VisibleForTesting - public static ReceiptsMessage create( - final List> receipts, - final TransactionReceiptEncodingConfiguration encodingConfiguration) { - final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); - tmp.startList(); - receipts.forEach( - (receiptSet) -> { - tmp.startList(); - receiptSet.forEach(r -> TransactionReceiptEncoder.writeTo(r, tmp, encodingConfiguration)); - tmp.endList(); - }); - tmp.endList(); - return new ReceiptsMessage(tmp.encoded(), DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER); + return new ReceiptsMessage(message.getData()); } /** @@ -84,7 +58,7 @@ public static ReceiptsMessage create( * @return A new ReceiptsMessage */ public static ReceiptsMessage createUnsafe(final Bytes data) { - return new ReceiptsMessage(data, DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER); + return new ReceiptsMessage(data); } @Override @@ -92,23 +66,6 @@ public int getCode() { return EthProtocolMessages.RECEIPTS; } - public List> receipts() { - final RLPInput input = new BytesValueRLPInput(data, false); - input.enterList(); - final List> receipts = new ArrayList<>(); - while (input.nextIsList()) { - final int setSize = input.enterList(); - final List receiptSet = new ArrayList<>(setSize); - for (int i = 0; i < setSize; i++) { - receiptSet.add(TransactionReceiptDecoder.readFrom(input, false)); - } - input.leaveList(); - receipts.add(receiptSet); - } - input.leaveList(); - return receipts; - } - public List> syncReceipts() { final RLPInput input = new BytesValueRLPInput(data, false); input.enterList(); @@ -118,7 +75,7 @@ public List> syncReceipts() { final List receiptSet = new ArrayList<>(setSize); for (int i = 0; i < setSize; i++) { receiptSet.add( - syncTransactionReceiptDecoder.decode( + DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER.decode( input.nextIsList() ? input.currentListAsBytes() : input.readBytes())); } input.leaveList(); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloadPipelineFactory.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloadPipelineFactory.java index 78dc683e239..f78434ce12b 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloadPipelineFactory.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/snapsync/SnapSyncChainDownloadPipelineFactory.java @@ -133,7 +133,7 @@ public Pipeline createBackwardHeaderDownloadPipeline(final ChainSyncState ethContext, headerRequestSize, anchorForHeaderDownload.getNumber(), - java.time.Duration.ofMillis(syncConfig.getBackwardHeadersDownloadStepTimeoutMillis())); + Duration.ofMillis(syncConfig.getBackwardHeadersDownloadStepTimeoutMillis())); final ImportHeadersStep importHeadersStep = new ImportHeadersStep( diff --git a/ethereum/eth/src/test-support/java/org/hyperledger/besu/ethereum/eth/core/Utils.java b/ethereum/eth/src/test-support/java/org/hyperledger/besu/ethereum/eth/core/Utils.java index 2e5088a2907..9b7046a510f 100644 --- a/ethereum/eth/src/test-support/java/org/hyperledger/besu/ethereum/eth/core/Utils.java +++ b/ethereum/eth/src/test-support/java/org/hyperledger/besu/ethereum/eth/core/Utils.java @@ -20,6 +20,7 @@ import org.hyperledger.besu.ethereum.core.SyncTransactionReceipt; import org.hyperledger.besu.ethereum.core.TransactionReceipt; import org.hyperledger.besu.ethereum.core.encoding.receipt.SyncTransactionReceiptEncoder; +import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptDecoder; import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; import org.hyperledger.besu.ethereum.mainnet.DefaultProtocolSchedule; @@ -31,6 +32,8 @@ import java.util.List; import java.util.Optional; +import org.apache.tuweni.bytes.Bytes; + public class Utils { private static final SyncTransactionReceiptEncoder SYNC_RECEIPT_ENCODER = new SyncTransactionReceiptEncoder(new SimpleNoCopyRlpEncoder()); @@ -40,7 +43,13 @@ public static SyncTransactionReceipt receiptToSyncReceipt( final TransactionReceiptEncodingConfiguration receiptEncodingConfiguration) { BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); TransactionReceiptEncoder.writeTo(receipt, rlpOutput, receiptEncodingConfiguration); - return new SyncTransactionReceipt(rlpOutput.encoded()); + // Read back the encoded item so the bytes match what the decoder extracts from wire messages: + // list items (FRONTIER / eth69) → currentListAsBytes(); bytes items (typed eth68) → + // readBytes(). + final BytesValueRLPInput rlpInput = new BytesValueRLPInput(rlpOutput.encoded(), false); + final Bytes rawBytes = + rlpInput.nextIsList() ? rlpInput.currentListAsBytes() : rlpInput.readBytes(); + return new SyncTransactionReceipt(rawBytes); } public static List receiptsToSyncReceipts( @@ -52,6 +61,27 @@ public static List receiptsToSyncReceipts( .toList(); } + public static TransactionReceipt syncReceiptToReceipt(final SyncTransactionReceipt syncReceipt) { + final Bytes rlpBytes = syncReceipt.getRlpBytes(); + // Flat receipts (Frontier / eth69): rlpBytes is a complete RLP list, usable directly. + // Typed receipts (EIP-2718+): rlpBytes is type || RLP(fields) and must be re-wrapped + // as an RLP bytes item so that TransactionReceiptDecoder.decodeTypedReceipt can call + // readBytes() to obtain the full typed-receipt payload. + if ((rlpBytes.get(0) & 0xFF) >= 0xC0) { + return TransactionReceiptDecoder.readFrom(new BytesValueRLPInput(rlpBytes, false), true); + } else { + final BytesValueRLPOutput wrapper = new BytesValueRLPOutput(); + wrapper.writeBytes(rlpBytes); + return TransactionReceiptDecoder.readFrom( + new BytesValueRLPInput(wrapper.encoded(), false), true); + } + } + + public static List syncReceiptsToReceipts( + final List syncReceipts) { + return syncReceipts.stream().map(Utils::syncReceiptToReceipt).toList(); + } + public static SyncBlock blockToSyncBlock(final Block block) { BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); block.getBody().writeWrappedBodyTo(rlpOutput); @@ -86,4 +116,19 @@ public static int compareSyncReceipts( .compareTo(SYNC_RECEIPT_ENCODER.encodeForRootCalculation(receipt2)); } } + + public static Bytes serializeReceiptsList( + final List> receipts, + final TransactionReceiptEncodingConfiguration encodingConfiguration) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + receipts.forEach( + (receiptSet) -> { + tmp.startList(); + receiptSet.forEach(r -> TransactionReceiptEncoder.writeTo(r, tmp, encodingConfiguration)); + tmp.endList(); + }); + tmp.endList(); + return tmp.encoded(); + } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthPeerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthPeerTest.java index fb1ab6b1c79..611d4a72cd2 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthPeerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthPeerTest.java @@ -19,6 +19,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.fail; +import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.mock; @@ -208,9 +209,10 @@ public void listenForMultipleStreams() throws PeerNotConnected { final EthMessage otherMessage = new EthMessage( peer, - ReceiptsMessage.create( - singletonList(gen.receipts(gen.block())), - TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION) + ReceiptsMessage.createUnsafe( + serializeReceiptsList( + singletonList(gen.receipts(gen.block())), + TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)) .wrapMessageData(BigInteger.ONE)); // Set up stream for headers diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java index e7a8bbb29df..bb0bfce03c3 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java @@ -46,6 +46,7 @@ import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; import org.hyperledger.besu.ethereum.eth.EthProtocolVersion; import org.hyperledger.besu.ethereum.eth.ImmutableEthProtocolConfiguration; +import org.hyperledger.besu.ethereum.eth.core.Utils; import org.hyperledger.besu.ethereum.eth.manager.MockPeerConnection.PeerSendHandler; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; @@ -934,7 +935,7 @@ public void respondToGetReceipts() throws ExecutionException, InterruptedExcepti final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(message.unwrapMessageData().getValue()); final List> receipts = - Lists.newArrayList(receiptsMessage.receipts()); + receiptsMessage.syncReceipts().stream().map(Utils::syncReceiptsToReceipts).toList(); assertThat(receipts).hasSize(blockCount); for (int i = 0; i < blockCount; i++) { assertThat(expectedReceipts.get(i)).isEqualTo(receipts.get(i)); @@ -988,7 +989,7 @@ public void respondToGetReceiptsWithinLimits() throws ExecutionException, Interr final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(message.unwrapMessageData().getValue()); final List> receipts = - Lists.newArrayList(receiptsMessage.receipts()); + receiptsMessage.syncReceipts().stream().map(Utils::syncReceiptsToReceipts).toList(); assertThat(receipts).hasSize(limit); for (int i = 0; i < limit; i++) { assertThat(expectedReceipts.get(i)).isEqualTo(receipts.get(i)); @@ -1036,7 +1037,7 @@ public void respondToGetReceiptsPartial() throws ExecutionException, Interrupted final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(message.unwrapMessageData().getValue()); final List> receipts = - Lists.newArrayList(receiptsMessage.receipts()); + receiptsMessage.syncReceipts().stream().map(Utils::syncReceiptsToReceipts).toList(); assertThat(receipts).hasSize(1); assertThat(expectedReceipts).isEqualTo(receipts.get(0)); done.complete(null); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java index 5817ea65300..877c640dbc5 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java @@ -16,6 +16,7 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -256,8 +257,10 @@ public void shouldLimitTxReceiptsByMessageSize() { // Check response final ReceiptsMessage expectedMsg = - ReceiptsMessage.create( - expectedResults, TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + ReceiptsMessage.createUnsafe( + serializeReceiptsList( + expectedResults, + TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION)); final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.LATEST); assertThat(result).contains(expectedMsg); } @@ -278,8 +281,10 @@ public void shouldLimitTxReceiptsByCount() { // Check response final ReceiptsMessage expectedMsg = - ReceiptsMessage.create( - expectedResults, TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + ReceiptsMessage.createUnsafe( + serializeReceiptsList( + expectedResults, + TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION)); final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.LATEST); assertThat(result).contains(expectedMsg); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RespondingEthPeer.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RespondingEthPeer.java index 1834ff4badf..447fe6da741 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RespondingEthPeer.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/RespondingEthPeer.java @@ -17,6 +17,7 @@ import static com.google.common.base.Preconditions.checkArgument; import static com.google.common.base.Preconditions.checkNotNull; import static org.hyperledger.besu.ethereum.core.InMemoryKeyValueStorageProvider.createInMemoryWorldStateArchive; +import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; import static org.mockito.Mockito.mock; import org.hyperledger.besu.datatypes.Hash; @@ -31,6 +32,7 @@ import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; import org.hyperledger.besu.ethereum.eth.EthProtocolVersion; +import org.hyperledger.besu.ethereum.eth.core.Utils; import org.hyperledger.besu.ethereum.eth.manager.snap.SnapProtocolManager; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; @@ -382,13 +384,14 @@ public static Responder partialResponder( case EthProtocolMessages.GET_RECEIPTS: final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(originalResponse); final List> originalReceipts = - Lists.newArrayList(receiptsMessage.receipts()); + receiptsMessage.syncReceipts().stream().map(Utils::syncReceiptsToReceipts).toList(); final List> partialReceipts = originalReceipts.subList(0, (int) (originalReceipts.size() * portion)); partialResponse = - ReceiptsMessage.create( - partialReceipts, - TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION); + ReceiptsMessage.createUnsafe( + serializeReceiptsList( + partialReceipts, + TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); break; case EthProtocolMessages.GET_NODE_DATA: final NodeDataMessage nodeDataMessage = NodeDataMessage.readFrom(originalResponse); @@ -423,9 +426,10 @@ public static Responder emptyResponder() { break; case EthProtocolMessages.GET_RECEIPTS: response = - ReceiptsMessage.create( - Collections.emptyList(), - TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION); + ReceiptsMessage.createUnsafe( + serializeReceiptsList( + Collections.emptyList(), + TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); break; case EthProtocolMessages.GET_NODE_DATA: response = NodeDataMessage.create(Collections.emptyList()); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskExecutorTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskExecutorTest.java index 127d6f854a8..6d0761c1e9e 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskExecutorTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskExecutorTest.java @@ -14,6 +14,16 @@ */ package org.hyperledger.besu.ethereum.eth.manager.peertask; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertSame; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.manager.EthPeer; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; @@ -23,13 +33,13 @@ import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; import java.util.Optional; +import java.util.Set; import java.util.concurrent.ExecutionException; import java.util.concurrent.TimeoutException; import java.util.function.Predicate; import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.mockito.Mock; @@ -52,6 +62,7 @@ public class PeerTaskExecutorTest { public void beforeTest() { mockCloser = MockitoAnnotations.openMocks(this); peerTaskExecutor = new PeerTaskExecutor(peerSelector, requestSender, new NoOpMetricsSystem()); + when(ethPeer.getAgreedCapabilities()).thenReturn(Set.of(EthProtocol.LATEST)); } @AfterEach @@ -70,24 +81,24 @@ public void testExecuteAgainstPeerWithNoRetriesAndSuccessfulFlow() Object responseObject = new Object(); - Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); - Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); - Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); - Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) + when(peerTask.getRequestMessage(any())).thenReturn(requestMessageData); + when(peerTask.getRetriesWithSamePeer()).thenReturn(0); + when(peerTask.getSubProtocol()).thenReturn(subprotocol); + when(subprotocol.getName()).thenReturn("subprotocol"); + when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) .thenReturn(responseMessageData); - Mockito.when(peerTask.processResponse(responseMessageData)).thenReturn(responseObject); - Mockito.when(peerTask.validateResult(responseObject)) + when(peerTask.processResponse(any(), any())).thenReturn(responseObject); + when(peerTask.validateResult(any())) .thenReturn(PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD); PeerTaskExecutorResult result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); - Mockito.verify(ethPeer).recordUsefulResponse(); + verify(ethPeer).recordUsefulResponse(); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.result().isPresent()); - Assertions.assertSame(responseObject, result.result().get()); - Assertions.assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); + assertNotNull(result); + assertTrue(result.result().isPresent()); + assertSame(responseObject, result.result().get()); + assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); } @Test @@ -101,25 +112,25 @@ public void testExecuteAgainstPeerWithNoRetriesAndPeerShouldBeDisconnected() Object responseObject = new Object(); - Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); - Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); - Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); - Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) + when(peerTask.getRequestMessage(any())).thenReturn(requestMessageData); + when(peerTask.getRetriesWithSamePeer()).thenReturn(0); + when(peerTask.getSubProtocol()).thenReturn(subprotocol); + when(subprotocol.getName()).thenReturn("subprotocol"); + when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) .thenReturn(responseMessageData); - Mockito.when(peerTask.processResponse(responseMessageData)).thenReturn(responseObject); - Mockito.when(peerTask.validateResult(responseObject)) + when(peerTask.processResponse(any(), any())).thenReturn(responseObject); + when(peerTask.validateResult(any())) .thenReturn(PeerTaskValidationResponse.NON_SEQUENTIAL_HEADERS_RETURNED); PeerTaskExecutorResult result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); - Mockito.verify(ethPeer) + verify(ethPeer) .disconnect(DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL_NON_SEQUENTIAL_HEADERS); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.result().isPresent()); - Assertions.assertSame(responseObject, result.result().get()); - Assertions.assertEquals(PeerTaskExecutorResponseCode.INVALID_RESPONSE, result.responseCode()); + assertNotNull(result); + assertTrue(result.result().isPresent()); + assertSame(responseObject, result.result().get()); + assertEquals(PeerTaskExecutorResponseCode.INVALID_RESPONSE, result.responseCode()); } @Test @@ -131,23 +142,22 @@ public void testExecuteAgainstPeerWithNoRetriesAndPeerSuppliedMalformedRlp() InvalidPeerTaskResponseException, MalformedRlpFromPeerException { - Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); - Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); - Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); - Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) + when(peerTask.getRequestMessage(any())).thenReturn(requestMessageData); + when(peerTask.getRetriesWithSamePeer()).thenReturn(0); + when(peerTask.getSubProtocol()).thenReturn(subprotocol); + when(subprotocol.getName()).thenReturn("subprotocol"); + when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) .thenReturn(responseMessageData); - Mockito.when(peerTask.processResponse(responseMessageData)) + when(peerTask.processResponse(any(), any())) .thenThrow(new MalformedRlpFromPeerException(new Exception(), Bytes.EMPTY)); PeerTaskExecutorResult result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); - Mockito.verify(ethPeer) - .disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); + verify(ethPeer).disconnect(DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); - Assertions.assertNotNull(result); - Assertions.assertFalse(result.result().isPresent()); - Assertions.assertEquals(PeerTaskExecutorResponseCode.INVALID_RESPONSE, result.responseCode()); + assertNotNull(result); + assertFalse(result.result().isPresent()); + assertEquals(PeerTaskExecutorResponseCode.INVALID_RESPONSE, result.responseCode()); } @Test @@ -161,21 +171,20 @@ public void testExecuteAgainstPeerWithNoRetriesAndPartialSuccessfulFlow() Object responseObject = new Object(); - Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); - Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); - Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); - Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) + when(peerTask.getRequestMessage(any())).thenReturn(requestMessageData); + when(peerTask.getRetriesWithSamePeer()).thenReturn(0); + when(peerTask.getSubProtocol()).thenReturn(subprotocol); + when(subprotocol.getName()).thenReturn("subprotocol"); + when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) .thenReturn(responseMessageData); - Mockito.when(peerTask.processResponse(responseMessageData)).thenReturn(responseObject); - Mockito.when(peerTask.validateResult(responseObject)) - .thenReturn(PeerTaskValidationResponse.NO_RESULTS_RETURNED); + when(peerTask.processResponse(any(), any())).thenReturn(responseObject); + when(peerTask.validateResult(any())).thenReturn(PeerTaskValidationResponse.NO_RESULTS_RETURNED); PeerTaskExecutorResult result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.result().isPresent()); - Assertions.assertEquals(PeerTaskExecutorResponseCode.INVALID_RESPONSE, result.responseCode()); + assertNotNull(result); + assertTrue(result.result().isPresent()); + assertEquals(PeerTaskExecutorResponseCode.INVALID_RESPONSE, result.responseCode()); } @Test @@ -190,28 +199,28 @@ public void testExecuteAgainstPeerWithRetriesAndSuccessfulFlowAfterFirstFailure( int requestMessageDataCode = 123; String protocolName = "snap"; - Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); - Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(2); + when(peerTask.getRequestMessage(any())).thenReturn(requestMessageData); + when(peerTask.getRetriesWithSamePeer()).thenReturn(2); - Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); - Mockito.when(subprotocol.getName()).thenReturn(protocolName); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) + when(peerTask.getSubProtocol()).thenReturn(subprotocol); + when(subprotocol.getName()).thenReturn(protocolName); + when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) .thenThrow(new TimeoutException()) .thenReturn(responseMessageData); - Mockito.when(requestMessageData.getCode()).thenReturn(requestMessageDataCode); - Mockito.when(peerTask.processResponse(responseMessageData)).thenReturn(responseObject); - Mockito.when(peerTask.validateResult(responseObject)) + when(requestMessageData.getCode()).thenReturn(requestMessageDataCode); + when(peerTask.processResponse(any(), any())).thenReturn(responseObject); + when(peerTask.validateResult(any())) .thenReturn(PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD); PeerTaskExecutorResult result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); - Mockito.verify(ethPeer).recordRequestTimeout(protocolName, requestMessageDataCode); - Mockito.verify(ethPeer).recordUsefulResponse(); + verify(ethPeer).recordRequestTimeout(protocolName, requestMessageDataCode); + verify(ethPeer).recordUsefulResponse(); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.result().isPresent()); - Assertions.assertSame(responseObject, result.result().get()); - Assertions.assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); + assertNotNull(result); + assertTrue(result.result().isPresent()); + assertSame(responseObject, result.result().get()); + assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); } @Test @@ -221,18 +230,18 @@ public void testExecuteAgainstPeerWithNoRetriesAndPeerNotConnected() InterruptedException, TimeoutException { - Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); - Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); - Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); - Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) + when(peerTask.getRequestMessage(any())).thenReturn(requestMessageData); + when(peerTask.getRetriesWithSamePeer()).thenReturn(0); + when(peerTask.getSubProtocol()).thenReturn(subprotocol); + when(subprotocol.getName()).thenReturn("subprotocol"); + when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) .thenThrow(new PeerConnection.PeerNotConnected("")); PeerTaskExecutorResult result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.result().isEmpty()); - Assertions.assertEquals(PeerTaskExecutorResponseCode.PEER_DISCONNECTED, result.responseCode()); + assertNotNull(result); + assertTrue(result.result().isEmpty()); + assertEquals(PeerTaskExecutorResponseCode.PEER_DISCONNECTED, result.responseCode()); } @Test @@ -244,21 +253,21 @@ public void testExecuteAgainstPeerWithNoRetriesAndTimeoutException() int requestMessageDataCode = 123; String protocolName = "snap"; - Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); - Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); - Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); - Mockito.when(subprotocol.getName()).thenReturn(protocolName); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) + when(peerTask.getRequestMessage(any())).thenReturn(requestMessageData); + when(peerTask.getRetriesWithSamePeer()).thenReturn(0); + when(peerTask.getSubProtocol()).thenReturn(subprotocol); + when(subprotocol.getName()).thenReturn(protocolName); + when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) .thenThrow(new TimeoutException()); - Mockito.when(requestMessageData.getCode()).thenReturn(requestMessageDataCode); + when(requestMessageData.getCode()).thenReturn(requestMessageDataCode); PeerTaskExecutorResult result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); - Mockito.verify(ethPeer).recordRequestTimeout(protocolName, requestMessageDataCode); + verify(ethPeer).recordRequestTimeout(protocolName, requestMessageDataCode); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.result().isEmpty()); - Assertions.assertEquals(PeerTaskExecutorResponseCode.TIMEOUT, result.responseCode()); + assertNotNull(result); + assertTrue(result.result().isEmpty()); + assertEquals(PeerTaskExecutorResponseCode.TIMEOUT, result.responseCode()); } @Test @@ -270,22 +279,21 @@ public void testExecuteAgainstPeerWithNoRetriesAndInvalidResponseMessage() InvalidPeerTaskResponseException, MalformedRlpFromPeerException { - Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); - Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); - Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); - Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) + when(peerTask.getRequestMessage(any())).thenReturn(requestMessageData); + when(peerTask.getRetriesWithSamePeer()).thenReturn(0); + when(peerTask.getSubProtocol()).thenReturn(subprotocol); + when(subprotocol.getName()).thenReturn("subprotocol"); + when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) .thenReturn(responseMessageData); - Mockito.when(peerTask.processResponse(responseMessageData)) - .thenThrow(new InvalidPeerTaskResponseException()); + when(peerTask.processResponse(any(), any())).thenThrow(new InvalidPeerTaskResponseException()); PeerTaskExecutorResult result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); - Mockito.verify(ethPeer).recordUselessResponse(null); + verify(ethPeer).recordUselessResponse(null); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.result().isEmpty()); - Assertions.assertEquals(PeerTaskExecutorResponseCode.INVALID_RESPONSE, result.responseCode()); + assertNotNull(result); + assertTrue(result.result().isEmpty()); + assertEquals(PeerTaskExecutorResponseCode.INVALID_RESPONSE, result.responseCode()); } @Test @@ -299,28 +307,27 @@ public void testExecuteWithNoRetriesAndSuccessFlow() MalformedRlpFromPeerException { Object responseObject = new Object(); - Mockito.when(peerSelector.getPeer(Mockito.any(Predicate.class))) - .thenReturn(Optional.of(ethPeer)); + when(peerSelector.getPeer(any(Predicate.class))).thenReturn(Optional.of(ethPeer)); - Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); - Mockito.when(peerTask.getRetriesWithOtherPeer()).thenReturn(0); - Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); - Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); - Mockito.when(subprotocol.getName()).thenReturn("subprotocol"); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) + when(peerTask.getRequestMessage(any())).thenReturn(requestMessageData); + when(peerTask.getRetriesWithOtherPeer()).thenReturn(0); + when(peerTask.getRetriesWithSamePeer()).thenReturn(0); + when(peerTask.getSubProtocol()).thenReturn(subprotocol); + when(subprotocol.getName()).thenReturn("subprotocol"); + when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) .thenReturn(responseMessageData); - Mockito.when(peerTask.processResponse(responseMessageData)).thenReturn(responseObject); - Mockito.when(peerTask.validateResult(responseObject)) + when(peerTask.processResponse(any(), any())).thenReturn(responseObject); + when(peerTask.validateResult(any())) .thenReturn(PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD); PeerTaskExecutorResult result = peerTaskExecutor.executeAgainstPeer(peerTask, ethPeer); - Mockito.verify(ethPeer).recordUsefulResponse(); + verify(ethPeer).recordUsefulResponse(); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.result().isPresent()); - Assertions.assertSame(responseObject, result.result().get()); - Assertions.assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); + assertNotNull(result); + assertTrue(result.result().isPresent()); + assertSame(responseObject, result.result().get()); + assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); } @Test @@ -337,32 +344,32 @@ public void testExecuteWithPeerSwitchingAndSuccessFlow() String protocolName = "snap"; EthPeer peer2 = Mockito.mock(EthPeer.class); - Mockito.when(peerSelector.getPeer(Mockito.any(Predicate.class))) + when(peerSelector.getPeer(any(Predicate.class))) .thenReturn(Optional.of(ethPeer)) .thenReturn(Optional.of(peer2)); - Mockito.when(peerTask.getRequestMessage()).thenReturn(requestMessageData); - Mockito.when(peerTask.getRetriesWithOtherPeer()).thenReturn(2); - Mockito.when(peerTask.getRetriesWithSamePeer()).thenReturn(0); - Mockito.when(peerTask.getSubProtocol()).thenReturn(subprotocol); - Mockito.when(subprotocol.getName()).thenReturn(protocolName); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) + when(peerTask.getRequestMessage(any())).thenReturn(requestMessageData); + when(peerTask.getRetriesWithOtherPeer()).thenReturn(2); + when(peerTask.getRetriesWithSamePeer()).thenReturn(0); + when(peerTask.getSubProtocol()).thenReturn(subprotocol); + when(subprotocol.getName()).thenReturn(protocolName); + when(requestSender.sendRequest(subprotocol, requestMessageData, ethPeer)) .thenThrow(new TimeoutException()); - Mockito.when(requestMessageData.getCode()).thenReturn(requestMessageDataCode); - Mockito.when(requestSender.sendRequest(subprotocol, requestMessageData, peer2)) + when(requestMessageData.getCode()).thenReturn(requestMessageDataCode); + when(requestSender.sendRequest(subprotocol, requestMessageData, peer2)) .thenReturn(responseMessageData); - Mockito.when(peerTask.processResponse(responseMessageData)).thenReturn(responseObject); - Mockito.when(peerTask.validateResult(responseObject)) + when(peerTask.processResponse(any(), any())).thenReturn(responseObject); + when(peerTask.validateResult(any())) .thenReturn(PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD); PeerTaskExecutorResult result = peerTaskExecutor.execute(peerTask); - Mockito.verify(ethPeer).recordRequestTimeout(protocolName, requestMessageDataCode); - Mockito.verify(peer2).recordUsefulResponse(); + verify(ethPeer).recordRequestTimeout(protocolName, requestMessageDataCode); + verify(peer2).recordUsefulResponse(); - Assertions.assertNotNull(result); - Assertions.assertTrue(result.result().isPresent()); - Assertions.assertSame(responseObject, result.result().get()); - Assertions.assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); + assertNotNull(result); + assertTrue(result.result().isPresent()); + assertSame(responseObject, result.result().get()); + assertEquals(PeerTaskExecutorResponseCode.SUCCESS, result.responseCode()); } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBodiesFromPeerTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBodiesFromPeerTaskTest.java index 0c787747620..6bb1429fc89 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBodiesFromPeerTaskTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetBodiesFromPeerTaskTest.java @@ -39,6 +39,7 @@ import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; @@ -46,6 +47,7 @@ import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.apache.tuweni.bytes.Bytes; @@ -66,6 +68,7 @@ public class GetBodiesFromPeerTaskTest { public static final List TRANSACTION_LIST = List.of(TX); public static final BlockBody BLOCK_BODY = new BlockBody(TRANSACTION_LIST, Collections.emptyList(), Optional.empty()); + private static final Set AGREED_CAPABILITIES = Set.of(EthProtocol.LATEST); private static ProtocolSchedule protocolSchedule; @BeforeAll @@ -90,7 +93,7 @@ public void testGetRequestMessage() { new GetBodiesFromPeerTask( List.of(mockBlockHeader(1), mockBlockHeader(2), mockBlockHeader(3)), protocolSchedule); - MessageData messageData = task.getRequestMessage(); + MessageData messageData = task.getRequestMessage(AGREED_CAPABILITIES); GetBlockBodiesMessage getBlockBodiesMessage = GetBlockBodiesMessage.readFrom(messageData); Assertions.assertEquals(EthProtocolMessages.GET_BLOCK_BODIES, getBlockBodiesMessage.getCode()); @@ -123,7 +126,8 @@ public void testParseResponseForInvalidResponse() { BlockBodiesMessage bodiesMessage = BlockBodiesMessage.create(List.of(BLOCK_BODY)); Assertions.assertThrows( - InvalidPeerTaskResponseException.class, () -> task.processResponse(bodiesMessage)); + InvalidPeerTaskResponseException.class, + () -> task.processResponse(bodiesMessage, Set.of())); } @Test @@ -136,7 +140,7 @@ public void testParseResponse() throws InvalidPeerTaskResponseException { final BlockBodiesMessage blockBodiesMessage = BlockBodiesMessage.create(List.of(BLOCK_BODY)); - List result = task.processResponse(blockBodiesMessage); + List result = task.processResponse(blockBodiesMessage, Set.of()); assertThat(result.size()).isEqualTo(1); assertThat(result.getFirst().getBody().getTransactions()).isEqualTo(TRANSACTION_LIST); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetHeadersFromPeerTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetHeadersFromPeerTaskTest.java index da4e9c2f546..280461c0f80 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetHeadersFromPeerTaskTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetHeadersFromPeerTaskTest.java @@ -14,7 +14,13 @@ */ package org.hyperledger.besu.ethereum.eth.manager.peertask.task; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.reset; +import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.chain.Blockchain; @@ -32,35 +38,36 @@ import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.plugin.services.storage.DataStorageFormat; import java.util.Collections; import java.util.List; +import java.util.Set; -import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; import org.junit.jupiter.params.provider.ValueSource; -import org.mockito.Mockito; public class GetHeadersFromPeerTaskTest { + private static final Set AGREED_CAPABILITIES = Set.of(EthProtocol.LATEST); - private final ProtocolSchedule protocolSchedule = Mockito.mock(ProtocolSchedule.class); + private final ProtocolSchedule protocolSchedule = mock(ProtocolSchedule.class); @Test public void testGetSubProtocol() { GetHeadersFromPeerTask task = new GetHeadersFromPeerTask(0, 1, 0, Direction.FORWARD, protocolSchedule); - Assertions.assertEquals(EthProtocol.get(), task.getSubProtocol()); + assertEquals(EthProtocol.get(), task.getSubProtocol()); } @Test public void testGetRequestMessageForHash() { GetHeadersFromPeerTask task = new GetHeadersFromPeerTask(Hash.ZERO, 0, 1, 0, Direction.FORWARD, protocolSchedule); - MessageData requestMessageData = task.getRequestMessage(); - Assertions.assertEquals( + MessageData requestMessageData = task.getRequestMessage(AGREED_CAPABILITIES); + assertEquals( "0xe4a00000000000000000000000000000000000000000000000000000000000000000018080", requestMessageData.getData().toHexString()); } @@ -69,16 +76,16 @@ public void testGetRequestMessageForHash() { public void testGetRequestMessageForBlockNumber() { GetHeadersFromPeerTask task = new GetHeadersFromPeerTask(123, 1, 0, Direction.FORWARD, protocolSchedule); - MessageData requestMessageData = task.getRequestMessage(); - Assertions.assertEquals("0xc47b018080", requestMessageData.getData().toHexString()); + MessageData requestMessageData = task.getRequestMessage(AGREED_CAPABILITIES); + assertEquals("0xc47b018080", requestMessageData.getData().toHexString()); } @Test public void testGetRequestMessageForHashWhenBlockNumberAlsoProvided() { GetHeadersFromPeerTask task = new GetHeadersFromPeerTask(Hash.ZERO, 123, 1, 0, Direction.FORWARD, protocolSchedule); - MessageData requestMessageData = task.getRequestMessage(); - Assertions.assertEquals( + MessageData requestMessageData = task.getRequestMessage(AGREED_CAPABILITIES); + assertEquals( "0xe4a00000000000000000000000000000000000000000000000000000000000000000018080", requestMessageData.getData().toHexString()); } @@ -87,9 +94,9 @@ public void testGetRequestMessageForHashWhenBlockNumberAlsoProvided() { public void testProcessResponseWithNullMessageData() { GetHeadersFromPeerTask task = new GetHeadersFromPeerTask(0, 1, 0, Direction.FORWARD, protocolSchedule); - Assertions.assertThrows( + assertThrows( InvalidPeerTaskResponseException.class, - () -> task.processResponse(null), + () -> task.processResponse(null, Set.of()), "Response MessageData is null"); } @@ -111,15 +118,16 @@ public void testProcessResponse() throws InvalidPeerTaskResponseException { Direction.FORWARD, blockchainSetupUtil.getProtocolSchedule()); - Assertions.assertEquals( - List.of(blockchain.getChainHeadHeader()), task.processResponse(responseMessage)); + assertEquals( + List.of(blockchain.getChainHeadHeader()), + task.processResponse(responseMessage, AGREED_CAPABILITIES)); } @ParameterizedTest @ValueSource(booleans = {false, true}) public void testGetPeerRequirementFilter(final boolean isPoS) { - Mockito.reset(protocolSchedule); - Mockito.when(protocolSchedule.anyMatch(Mockito.any())).thenReturn(isPoS); + reset(protocolSchedule); + when(protocolSchedule.anyMatch(any())).thenReturn(isPoS); GetHeadersFromPeerTask task = new GetHeadersFromPeerTask(5, 1, 0, Direction.FORWARD, protocolSchedule); @@ -127,11 +135,11 @@ public void testGetPeerRequirementFilter(final boolean isPoS) { EthPeer failForShortChainHeight = mockPeer(1); EthPeer successfulCandidate = mockPeer(5); - Assertions.assertEquals( + assertEquals( isPoS, task.getPeerRequirementFilter() .test(EthPeerImmutableAttributes.from(failForShortChainHeight))); - Assertions.assertTrue( + assertTrue( task.getPeerRequirementFilter().test(EthPeerImmutableAttributes.from(successfulCandidate))); } @@ -139,7 +147,7 @@ public void testGetPeerRequirementFilter(final boolean isPoS) { public void testValidateResultForEmptyResult() { GetHeadersFromPeerTask task = new GetHeadersFromPeerTask(5, 1, 0, Direction.FORWARD, protocolSchedule); - Assertions.assertEquals( + assertEquals( PeerTaskValidationResponse.NO_RESULTS_RETURNED, task.validateResult(Collections.emptyList())); } @@ -149,11 +157,11 @@ public void testShouldDisconnectPeerForTooManyHeadersReturned() { GetHeadersFromPeerTask task = new GetHeadersFromPeerTask(5, 1, 1, Direction.FORWARD, protocolSchedule); - BlockHeader header1 = Mockito.mock(BlockHeader.class); - BlockHeader header2 = Mockito.mock(BlockHeader.class); - BlockHeader header3 = Mockito.mock(BlockHeader.class); + BlockHeader header1 = mock(BlockHeader.class); + BlockHeader header2 = mock(BlockHeader.class); + BlockHeader header3 = mock(BlockHeader.class); - Assertions.assertEquals( + assertEquals( PeerTaskValidationResponse.TOO_MANY_RESULTS_RETURNED, task.validateResult(List.of(header1, header2, header3))); } @@ -165,32 +173,32 @@ public void testValidateResultForNonSequentialHeaders() { Hash block1Hash = Hash.fromHexStringLenient("01"); Hash block2Hash = Hash.fromHexStringLenient("02"); - BlockHeader header1 = Mockito.mock(BlockHeader.class); - Mockito.when(header1.getNumber()).thenReturn(1L); - Mockito.when(header1.getHash()).thenReturn(block1Hash); - BlockHeader header2 = Mockito.mock(BlockHeader.class); - Mockito.when(header2.getNumber()).thenReturn(2L); - Mockito.when(header2.getHash()).thenReturn(block2Hash); - Mockito.when(header2.getParentHash()).thenReturn(block1Hash); - BlockHeader header3 = Mockito.mock(BlockHeader.class); - Mockito.when(header3.getNumber()).thenReturn(3L); - Mockito.when(header3.getParentHash()).thenReturn(Hash.ZERO); - - Assertions.assertEquals( + BlockHeader header1 = mock(BlockHeader.class); + when(header1.getNumber()).thenReturn(1L); + when(header1.getHash()).thenReturn(block1Hash); + BlockHeader header2 = mock(BlockHeader.class); + when(header2.getNumber()).thenReturn(2L); + when(header2.getHash()).thenReturn(block2Hash); + when(header2.getParentHash()).thenReturn(block1Hash); + BlockHeader header3 = mock(BlockHeader.class); + when(header3.getNumber()).thenReturn(3L); + when(header3.getParentHash()).thenReturn(Hash.ZERO); + + assertEquals( PeerTaskValidationResponse.NON_SEQUENTIAL_HEADERS_RETURNED, task.validateResult(List.of(header1, header2, header3))); } private EthPeer mockPeer(final long chainHeight) { - EthPeer ethPeer = Mockito.mock(EthPeer.class); - ChainState chainState = Mockito.mock(ChainState.class); + EthPeer ethPeer = mock(EthPeer.class); + ChainState chainState = mock(ChainState.class); - Mockito.when(ethPeer.chainState()).thenReturn(chainState); - Mockito.when(chainState.getEstimatedHeight()).thenReturn(chainHeight); - Mockito.when(chainState.getEstimatedTotalDifficulty()).thenReturn(Difficulty.of(0)); - Mockito.when(ethPeer.getReputation()).thenReturn(new PeerReputation()); + when(ethPeer.chainState()).thenReturn(chainState); + when(chainState.getEstimatedHeight()).thenReturn(chainHeight); + when(chainState.getEstimatedTotalDifficulty()).thenReturn(Difficulty.of(0)); + when(ethPeer.getReputation()).thenReturn(new PeerReputation()); PeerConnection connection = mock(PeerConnection.class); - Mockito.when(ethPeer.getConnection()).thenReturn(connection); + when(ethPeer.getConnection()).thenReturn(connection); return ethPeer; } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetPooledTransactionsFromPeerTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetPooledTransactionsFromPeerTaskTest.java index 1058413e974..313627a933c 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetPooledTransactionsFromPeerTaskTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetPooledTransactionsFromPeerTaskTest.java @@ -17,12 +17,15 @@ import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.core.BlockDataGenerator; import org.hyperledger.besu.ethereum.core.Transaction; +import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.manager.peertask.InvalidPeerTaskResponseException; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskValidationResponse; import org.hyperledger.besu.ethereum.eth.messages.PooledTransactionsMessage; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import java.util.List; +import java.util.Set; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -30,13 +33,14 @@ public class GetPooledTransactionsFromPeerTaskTest { private static final BlockDataGenerator GENERATOR = new BlockDataGenerator(); + private static final Set AGREED_CAPABILITIES = Set.of(EthProtocol.LATEST); @Test public void testGetRequestMessage() { List hashes = List.of(Hash.EMPTY); GetPooledTransactionsFromPeerTask task = new GetPooledTransactionsFromPeerTask(hashes); - MessageData result = task.getRequestMessage(); + MessageData result = task.getRequestMessage(AGREED_CAPABILITIES); Assertions.assertEquals( "0xe1a0c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", @@ -52,7 +56,7 @@ public void testProcessResponse() throws InvalidPeerTaskResponseException { PooledTransactionsMessage pooledTransactionsMessage = PooledTransactionsMessage.create(List.of(transaction)); - List result = task.processResponse(pooledTransactionsMessage); + List result = task.processResponse(pooledTransactionsMessage, AGREED_CAPABILITIES); Assertions.assertEquals(List.of(transaction), result); } @@ -68,7 +72,7 @@ public void testProcessResponseWithIncorrectTransactionCount() { InvalidPeerTaskResponseException exception = Assertions.assertThrows( InvalidPeerTaskResponseException.class, - () -> task.processResponse(pooledTransactionsMessage)); + () -> task.processResponse(pooledTransactionsMessage, AGREED_CAPABILITIES)); Assertions.assertEquals( "Response transaction count does not match request hash count", exception.getMessage()); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java index 3340edb10a3..1d733f342a0 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java @@ -16,6 +16,7 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.ethereum.eth.core.Utils.receiptToSyncReceipt; +import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -47,12 +48,14 @@ import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; import org.hyperledger.besu.ethereum.rlp.SimpleNoCopyRlpEncoder; import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; +import java.util.Set; import org.apache.commons.lang3.StringUtils; import org.junit.jupiter.api.Assertions; @@ -63,6 +66,7 @@ import org.mockito.Mockito; public class GetSyncReceiptsFromPeerTaskTest { + private static final Set AGREED_CAPABILITIES = Set.of(EthProtocol.ETH69); private static ProtocolSchedule protocolSchedule; @BeforeAll @@ -123,7 +127,7 @@ public void testGetRequestMessage() { final var task = createTask(blocks, protocolSchedule); - final var messageData = task.getRequestMessage(); + final var messageData = task.getRequestMessage(AGREED_CAPABILITIES); final var getReceiptsMessage = GetReceiptsMessage.readFrom(messageData); assertEquals(EthProtocolMessages.GET_RECEIPTS, getReceiptsMessage.getCode()); @@ -146,7 +150,8 @@ public void testParseResponseWithNullResponseMessage() { final var task = createTask(List.of(syncBlock), protocolSchedule); Assertions.assertThrows( - InvalidPeerTaskResponseException.class, () -> task.processResponse(null)); + InvalidPeerTaskResponseException.class, + () -> task.processResponse(null, AGREED_CAPABILITIES)); } @Test @@ -182,12 +187,15 @@ public void testParseResponse() final var task = createTask(List.of(syncBlock1, syncBlock2, syncBlock3), protocolSchedule); ReceiptsMessage receiptsMessage = - ReceiptsMessage.create( - List.of( - List.of(receiptForBlock1), List.of(receiptForBlock2), List.of(receiptForBlock3)), - TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION); + ReceiptsMessage.createUnsafe( + serializeReceiptsList( + List.of( + List.of(receiptForBlock1), + List.of(receiptForBlock2), + List.of(receiptForBlock3)), + TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); - final var response = task.processResponse(receiptsMessage); + final var response = task.processResponse(receiptsMessage, AGREED_CAPABILITIES); assertThat(response).hasSize(3); assertThat(response.get(syncBlock1)) @@ -306,17 +314,20 @@ public void testParseResponseForInvalidResponse() { final var task = createTask(List.of(syncBlock1, syncBlock2, syncBlock3), protocolSchedule); final ReceiptsMessage receiptsMessage = - ReceiptsMessage.create( - List.of( - List.of(receiptForBlock1), - List.of(receiptForBlock2), - List.of(receiptForBlock3), + ReceiptsMessage.createUnsafe( + serializeReceiptsList( List.of( - new TransactionReceipt(1, 101112, Collections.emptyList(), Optional.empty()))), - TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION); + List.of(receiptForBlock1), + List.of(receiptForBlock2), + List.of(receiptForBlock3), + List.of( + new TransactionReceipt( + 1, 101112, Collections.emptyList(), Optional.empty()))), + TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); Assertions.assertThrows( - InvalidPeerTaskResponseException.class, () -> task.processResponse(receiptsMessage)); + InvalidPeerTaskResponseException.class, + () -> task.processResponse(receiptsMessage, AGREED_CAPABILITIES)); } @Test diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/MessageWrapperTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/MessageWrapperTest.java index 6ff9f8c6d89..17174c83620 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/MessageWrapperTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/MessageWrapperTest.java @@ -22,6 +22,7 @@ import static org.hyperledger.besu.ethereum.core.Transaction.REPLAY_PROTECTED_V_MIN; import static org.hyperledger.besu.ethereum.core.Transaction.REPLAY_UNPROTECTED_V_BASE; import static org.hyperledger.besu.ethereum.core.Transaction.REPLAY_UNPROTECTED_V_BASE_PLUS_1; +import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; import static org.junit.jupiter.api.Assertions.assertThrows; import org.hyperledger.besu.crypto.SECP256K1; @@ -187,33 +188,34 @@ public void Receipts() throws IOException { final var testJson = parseTestFile("ReceiptsPacket66.json"); final Bytes expected = Bytes.fromHexString(testJson.get("rlp").asText()); final ReceiptsMessage receiptsMessage = - ReceiptsMessage.create( - singletonList( + ReceiptsMessage.createUnsafe( + serializeReceiptsList( singletonList( - new TransactionReceipt( - TransactionType.FRONTIER, - 0, - 1, - singletonList( - new LogWithMetadata( - 0, - 0, - Hash.ZERO, - 0L, - Hash.ZERO, - 0, - Address.fromHexString("0x11"), - Bytes.fromHexString("0x0100ff"), - Stream.of( - "0x000000000000000000000000000000000000000000000000000000000000dead", - "0x000000000000000000000000000000000000000000000000000000000000beef") - .map(LogTopic::fromHexString) - .collect(toUnmodifiableList()), - false)), - LogsBloomFilter.fromHexString( - "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), - Optional.empty()))), - TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION); + singletonList( + new TransactionReceipt( + TransactionType.FRONTIER, + 0, + 1, + singletonList( + new LogWithMetadata( + 0, + 0, + Hash.ZERO, + 0L, + Hash.ZERO, + 0, + Address.fromHexString("0x11"), + Bytes.fromHexString("0x0100ff"), + Stream.of( + "0x000000000000000000000000000000000000000000000000000000000000dead", + "0x000000000000000000000000000000000000000000000000000000000000beef") + .map(LogTopic::fromHexString) + .collect(toUnmodifiableList()), + false)), + LogsBloomFilter.fromHexString( + "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000"), + Optional.empty()))), + TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); final Bytes actual = receiptsMessage.wrapMessageData(BigInteger.valueOf(1111)).getData(); assertThat(actual).isEqualTo(expected); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessageTest.java index ec3b143649a..46e022ac0e8 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessageTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessageTest.java @@ -14,9 +14,12 @@ */ package org.hyperledger.besu.ethereum.eth.messages; +import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; + import org.hyperledger.besu.ethereum.core.BlockDataGenerator; import org.hyperledger.besu.ethereum.core.TransactionReceipt; import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; +import org.hyperledger.besu.ethereum.eth.core.Utils; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; @@ -54,12 +57,14 @@ public void roundTripTest(final TransactionReceiptEncodingConfiguration encoding // Perform round-trip transformation // Create specific message, copy it to a generic message, then read back into a specific format - final MessageData initialMessage = ReceiptsMessage.create(receipts, encodingConfiguration); + final MessageData initialMessage = + ReceiptsMessage.createUnsafe(serializeReceiptsList(receipts, encodingConfiguration)); final MessageData raw = new RawMessage(EthProtocolMessages.RECEIPTS, initialMessage.getData()); final ReceiptsMessage message = ReceiptsMessage.readFrom(raw); // Read data back out after round trip and check they match originals. - final Iterator> readData = message.receipts().iterator(); + final Iterator> readData = + message.syncReceipts().stream().map(Utils::syncReceiptsToReceipts).iterator(); for (int i = 0; i < dataCount; ++i) { Assertions.assertThat(readData.next()).isEqualTo(receipts.get(i)); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportSyncBlocksStepTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportSyncBlocksStepTest.java index f648ccae1fa..7e69a831241 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportSyncBlocksStepTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/ImportSyncBlocksStepTest.java @@ -15,6 +15,8 @@ package org.hyperledger.besu.ethereum.eth.sync.fastsync; import static java.util.stream.Collectors.toList; +import static org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION; +import static org.hyperledger.besu.ethereum.eth.core.Utils.blocksToSyncBlocks; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -23,21 +25,11 @@ import org.hyperledger.besu.ethereum.core.Block; import org.hyperledger.besu.ethereum.core.BlockDataGenerator; import org.hyperledger.besu.ethereum.core.SyncBlock; -import org.hyperledger.besu.ethereum.core.SyncBlockBody; import org.hyperledger.besu.ethereum.core.SyncBlockWithReceipts; -import org.hyperledger.besu.ethereum.core.SyncTransactionReceipt; -import org.hyperledger.besu.ethereum.core.TransactionReceipt; -import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; -import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; +import org.hyperledger.besu.ethereum.eth.core.Utils; import org.hyperledger.besu.ethereum.eth.sync.state.SyncState; -import org.hyperledger.besu.ethereum.mainnet.DefaultProtocolSchedule; -import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; -import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; -import java.math.BigInteger; -import java.util.ArrayList; import java.util.List; -import java.util.Optional; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -66,15 +58,16 @@ public void setUp() { @Test public void shouldImportBlocks() { final List realBlocks = gen.blockSequence(5); - final List blocks = blockToSyncBlock(realBlocks); + final List blocks = blocksToSyncBlocks(realBlocks); final List blocksWithReceipts = blocks.stream() .map( block -> new SyncBlockWithReceipts( block, - receiptsToSyncReceipts( - gen.receipts(realBlocks.get(blocks.indexOf(block)))))) + Utils.receiptsToSyncReceipts( + gen.receipts(realBlocks.get(blocks.indexOf(block))), + ETH69_RECEIPT_CONFIGURATION))) .collect(toList()); importSyncBlocksStep.accept(blocksWithReceipts); @@ -82,31 +75,4 @@ public void shouldImportBlocks() { verify(blockchain).unsafeImportSyncBodiesAndReceipts(blocksWithReceipts, false); verify(syncState).setSyncProgress(0L, blocksWithReceipts.getLast().getNumber(), 10L); } - - private List blockToSyncBlock(final List blocks) { - final ArrayList syncBlocks = new ArrayList<>(blocks.size()); - for (final Block block : blocks) { - BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); - block.getBody().writeWrappedBodyTo(rlpOutput); - final BytesValueRLPInput input = new BytesValueRLPInput(rlpOutput.encoded(), false); - final SyncBlockBody syncBlockBody = - SyncBlockBody.readWrappedBodyFrom( - input, false, new DefaultProtocolSchedule(Optional.of(BigInteger.ONE))); - syncBlocks.add(new SyncBlock(block.getHeader(), syncBlockBody)); - } - return syncBlocks; - } - - private List receiptsToSyncReceipts( - final List receipts) { - return receipts.stream() - .map( - receipt -> { - BytesValueRLPOutput rlpOutput = new BytesValueRLPOutput(); - TransactionReceiptEncoder.writeTo( - receipt, rlpOutput, TransactionReceiptEncodingConfiguration.DEFAULT); - return new SyncTransactionReceipt(rlpOutput.encoded()); - }) - .collect(toList()); - } } From 2a90a738b0440e614b7e31c3c78a05dde37c2fcd Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Thu, 26 Feb 2026 12:07:53 +0100 Subject: [PATCH 2/7] Implement EIP-7975: eth/70 - partial block receipt lists Signed-off-by: Fabio Di Fabio --- CHANGELOG.md | 1 + .../api/jsonrpc/eth/eth_protocolVersion.json | 2 +- .../besu/ethereum/eth/EthProtocol.java | 11 +- .../besu/ethereum/eth/EthProtocolVersion.java | 3 +- .../eth/manager/EthProtocolManager.java | 1 + .../besu/ethereum/eth/manager/EthServer.java | 116 +++++++- .../task/GetSyncReceiptsFromPeerTask.java | 136 +++++++-- .../eth/messages/GetReceiptsMessage.java | 43 ++- .../eth/messages/ReceiptsMessage.java | 103 ++++++- .../fastsync/DownloadSyncReceiptsStep.java | 51 +++- .../eth/messages/GetReceiptsMessageTest.java | 4 +- .../DownloadSyncReceiptsStepTest.java | 258 +++++++++++++----- .../ethereum/p2p/rlpx/wire/MessageData.java | 9 +- .../rlpx/wire/messages/DisconnectMessage.java | 2 + .../hyperledger/besu/ethereum/rlp/RLP.java | 2 +- 15 files changed, 604 insertions(+), 138 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 1612108a27b..4191c6f7066 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -18,6 +18,7 @@ ### Additions and Improvements - Add IPv6 dual-stack support for DiscV5 peer discovery (enabled via `--Xv5-discovery-enabled`): new `--p2p-host-ipv6`, `--p2p-interface-ipv6`, and `--p2p-port-ipv6` CLI options enable a second UDP discovery socket; `--p2p-ipv6-outbound-enabled` controls whether IPv6 is preferred for outbound connections when a peer advertises both address families [#9763](https://github.com/hyperledger/besu/pull/9763); RLPx now also binds a second TCP socket on the IPv6 interface so IPv6-only peers can establish connections [#9873](https://github.com/hyperledger/besu/pull/9873) - Add blockTimestamp to transaction RPC results [#9887](https://github.com/hyperledger/besu/pull/9887) +- Support [EIP-7975](https://eips.ethereum.org/EIPS/eip-7975): eth/70 - partial block receipt lists ## 26.2.0 diff --git a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json index 42359634b44..2fe0c58d880 100644 --- a/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json +++ b/ethereum/api/src/test/resources/org/hyperledger/besu/ethereum/api/jsonrpc/eth/eth_protocolVersion.json @@ -8,7 +8,7 @@ "response": { "jsonrpc": "2.0", "id": 2, - "result": "0x45" + "result": "0x46" }, "statusCode": 200 } \ No newline at end of file diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java index 146d0fa3ae3..f2b4a4ba141 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocol.java @@ -24,13 +24,14 @@ /** * Eth protocol messages as defined in Ethereum Wire Protocol - * (ETH)} + * (ETH) */ public class EthProtocol implements SubProtocol { public static final String NAME = "eth"; private static final EthProtocol INSTANCE = new EthProtocol(); public static final Capability ETH68 = Capability.create(NAME, EthProtocolVersion.V68); public static final Capability ETH69 = Capability.create(NAME, EthProtocolVersion.V69); + public static final Capability ETH70 = Capability.create(NAME, EthProtocolVersion.V70); public static final BitSet REQUEST_ID_MESSAGES; static { @@ -52,7 +53,7 @@ public class EthProtocol implements SubProtocol { } // Latest version of the Eth protocol - public static final Capability LATEST = ETH69; + public static final Capability LATEST = ETH70; public static boolean requestIdCompatible(final int code) { return REQUEST_ID_MESSAGES.get(code); @@ -67,7 +68,7 @@ public String getName() { public int messageSpace(final int protocolVersion) { return switch (protocolVersion) { case EthProtocolVersion.V68 -> 17; - case EthProtocolVersion.V69 -> 18; + case EthProtocolVersion.V69, EthProtocolVersion.V70 -> 18; default -> 0; }; } @@ -106,4 +107,8 @@ public static EthProtocol get() { public static boolean isEth69Compatible(final Capability capability) { return NAME.equals(capability.getName()) && capability.getVersion() >= ETH69.getVersion(); } + + public static boolean isEth70Compatible(final Capability capability) { + return NAME.equals(capability.getName()) && capability.getVersion() >= ETH70.getVersion(); + } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java index ed35849e98d..555334df314 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/EthProtocolVersion.java @@ -27,6 +27,7 @@ public class EthProtocolVersion { public static final int V68 = 68; public static final int V69 = 69; + public static final int V70 = 70; /** eth/68 */ private static final List eth68Messages = @@ -76,7 +77,7 @@ public class EthProtocolVersion { public static List getSupportedMessages(final int protocolVersion) { return switch (protocolVersion) { case EthProtocolVersion.V68 -> eth68Messages; - case EthProtocolVersion.V69 -> eth69Messages; + case EthProtocolVersion.V69, EthProtocolVersion.V70 -> eth69Messages; default -> Collections.emptyList(); }; } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java index 0b245610fba..f6965ddbc14 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java @@ -175,6 +175,7 @@ private List calculateCapabilities( capabilities.add(EthProtocol.ETH68); capabilities.add(EthProtocol.ETH69); + capabilities.add(EthProtocol.ETH70); capabilities.removeIf(cap -> cap.getVersion() > ethProtocolConfiguration.getMaxEthCapability()); capabilities.removeIf(cap -> cap.getVersion() < ethProtocolConfiguration.getMinEthCapability()); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java index fb9663664fd..7f1de1e2cbc 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java @@ -14,6 +14,9 @@ */ package org.hyperledger.besu.ethereum.eth.manager; +import static org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason.INVALID_BLOCK_REQUESTED; +import static org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason.INVALID_FIRST_BLOCK_RECEIPT_INDEX; + import org.hyperledger.besu.datatypes.Hash; import org.hyperledger.besu.ethereum.chain.Blockchain; import org.hyperledger.besu.ethereum.core.BlockBody; @@ -96,13 +99,22 @@ private void registerResponseConstructors() { maxMessageSize)); ethMessages.registerResponseConstructor( EthProtocolMessages.GET_RECEIPTS, - (peer, messageData, capability) -> - constructGetReceiptsResponse( + (peer, messageData, capability) -> { + if (EthProtocol.isEth70Compatible(capability)) { + return constructGetReceiptsPaginatedResponse( + peer, blockchain, messageData, ethereumWireProtocolConfiguration.getMaxGetReceipts(), - maxMessageSize, - capability)); + maxMessageSize); + } + return constructGetReceiptsResponse( + blockchain, + messageData, + ethereumWireProtocolConfiguration.getMaxGetReceipts(), + maxMessageSize, + capability); + }); ethMessages.registerResponseConstructor( EthProtocolMessages.GET_NODE_DATA, (peer, messageData, capability) -> @@ -226,18 +238,18 @@ static MessageData constructGetReceiptsResponse( final int maxMessageSize, final Capability cap) { final GetReceiptsMessage getReceipts = GetReceiptsMessage.readFrom(message); - final Iterable hashes = getReceipts.hashes(); + final Iterable blockHashes = getReceipts.blockHashes(); int responseSizeEstimate = RLP.MAX_PREFIX_SIZE; final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); rlp.startList(); int count = 0; - for (final Hash hash : hashes) { + for (final Hash blockHash : blockHashes) { if (count >= requestLimit) { break; } count++; - final Optional> maybeReceipts = blockchain.getTxReceipts(hash); + final Optional> maybeReceipts = blockchain.getTxReceipts(blockHash); if (maybeReceipts.isEmpty()) { continue; } @@ -265,6 +277,96 @@ static MessageData constructGetReceiptsResponse( return ReceiptsMessage.createUnsafe(rlp.encoded()); } + static MessageData constructGetReceiptsPaginatedResponse( + final EthPeer peer, + final Blockchain blockchain, + final MessageData message, + final int requestLimit, + final int maxMessageSize) { + final GetReceiptsMessage getReceipts = GetReceiptsMessage.readFrom(message); + final List requestedBlockHashes = getReceipts.blockHashes(); + final List blockHashes; + if (requestedBlockHashes.size() > requestLimit) { + LOG.atDebug() + .setMessage( + "Requested receipts for {} blocks, more than allowed max number of {}, ignoring extra blocks") + .addArgument(requestedBlockHashes::size) + .addArgument(requestLimit) + .log(); + blockHashes = requestedBlockHashes.subList(0, requestLimit); + } else { + blockHashes = requestedBlockHashes; + } + + final var blockReceiptsRLPs = new ArrayList(blockHashes.size()); + + int skipBefore = getReceipts.firstBlockReceiptIndex(); + int responseSizeEstimate = RLP.MAX_PREFIX_SIZE; + boolean lastBlockIncomplete = false; + + for (final Hash blockHash : blockHashes) { + final Optional> maybeReceipts = blockchain.getTxReceipts(blockHash); + if (maybeReceipts.isEmpty()) { + LOG.debug("Invalid request from peer {}, block {} does not exists", peer, blockHash); + peer.disconnect(INVALID_BLOCK_REQUESTED); + return ReceiptsMessage.createUnsafe(Bytes.EMPTY, false); + } + + final List blockReceipts = maybeReceipts.get(); + final List requestedReceipts; + + if (skipBefore > blockReceipts.size()) { + LOG.debug( + "Invalid request from peer {}, firstBlockReceiptIndex {} is greater than the receipt count of {} for block {}", + peer, + skipBefore, + blockReceipts.size(), + blockHash); + peer.disconnect(INVALID_FIRST_BLOCK_RECEIPT_INDEX); + return ReceiptsMessage.createUnsafe(Bytes.EMPTY, false); + } + + if (skipBefore > 0) { + requestedReceipts = blockReceipts.subList(skipBefore, blockReceipts.size()); + skipBefore = 0; + } else { + requestedReceipts = blockReceipts; + } + + final BytesValueRLPOutput encodedBlockReceipts = new BytesValueRLPOutput(); + encodedBlockReceipts.startList(); + + for (final TransactionReceipt receipt : requestedReceipts) { + final BytesValueRLPOutput encodedReceipt = new BytesValueRLPOutput(); + TransactionReceiptEncoder.writeTo( + receipt, + encodedReceipt, + TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + if (responseSizeEstimate + encodedReceipt.encodedSize() + RLP.MAX_PREFIX_SIZE + > maxMessageSize) { + lastBlockIncomplete = true; + break; + } + responseSizeEstimate += encodedReceipt.encodedSize(); + encodedBlockReceipts.writeRaw(encodedReceipt.encoded()); + } + + encodedBlockReceipts.endList(); + blockReceiptsRLPs.add(encodedBlockReceipts); + if (lastBlockIncomplete) { + break; + } + } + + final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); + rlp.writeLongScalar(lastBlockIncomplete ? 1 : 0); + rlp.startList(); + blockReceiptsRLPs.forEach(r -> rlp.writeRaw(r.encoded())); + rlp.endList(); + + return ReceiptsMessage.createUnsafe(rlp.encoded(), lastBlockIncomplete); + } + static MessageData constructGetPooledTransactionsResponse( final TransactionPool transactionPool, final EthPeer peer, diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java index ffcc0713b2c..d276d407595 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java @@ -36,6 +36,7 @@ import org.hyperledger.besu.ethereum.p2p.rlpx.wire.SubProtocol; import org.hyperledger.besu.ethereum.rlp.RLPException; +import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; @@ -45,32 +46,30 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.tuweni.bytes.Bytes; -public class GetSyncReceiptsFromPeerTask - implements PeerTask>> { - - private final List requestedBlocks; +public class GetSyncReceiptsFromPeerTask implements PeerTask { + private final Request request; + protected final ProtocolSchedule protocolSchedule; private final List requestedHeaders; private final long requiredBlockchainHeight; private final boolean isPoS; private final SyncTransactionReceiptEncoder syncTransactionReceiptEncoder; public GetSyncReceiptsFromPeerTask( - final List blocks, + final Request request, final ProtocolSchedule protocolSchedule, final SyncTransactionReceiptEncoder syncTransactionReceiptEncoder) { - checkArgument(!blocks.isEmpty(), "Requested block list must not be empty"); - this.requestedBlocks = blocks; - this.requestedHeaders = blocks.stream().map(SyncBlock::getHeader).toList(); + checkArgument(!request.blocks.isEmpty(), "Requested block list must not be empty"); + this.request = request; + this.protocolSchedule = protocolSchedule; + this.requestedHeaders = request.blocks.stream().map(SyncBlock::getHeader).toList(); - // calculate the minimum required blockchain height a peer will need to be able to fulfil this - // request requiredBlockchainHeight = - requestedHeaders.stream() + this.requestedHeaders.stream() .mapToLong(BlockHeader::getNumber) .max() .orElse(BlockHeader.GENESIS_BLOCK_NUMBER); - isPoS = protocolSchedule.anyMatch((ps) -> ps.spec().isPoS()); + isPoS = protocolSchedule.anyMatch(ps -> ps.spec().isPoS()); this.syncTransactionReceiptEncoder = syncTransactionReceiptEncoder; } @@ -81,11 +80,14 @@ public SubProtocol getSubProtocol() { @Override public MessageData getRequestMessage(final Set agreedCapabilities) { - return GetReceiptsMessage.create(requestedHeaders.stream().map(BlockHeader::getHash).toList()); + final List blockHashes = requestedHeaders.stream().map(BlockHeader::getHash).toList(); + return agreedCapabilities.stream().anyMatch(EthProtocol::isEth70Compatible) + ? GetReceiptsMessage.create(blockHashes, request.firstBlockPartialReceipts.size()) + : GetReceiptsMessage.create(blockHashes); } @Override - public Map> processResponse( + public Response processResponse( final MessageData messageData, final Set agreedCapabilities) throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { if (messageData == null) { @@ -93,35 +95,85 @@ public Map> processResponse( } final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(messageData); try { - final List> receivedBlocks = receiptsMessage.syncReceipts(); - if (receivedBlocks.size() > requestedBlocks.size()) { + final boolean isEth70Response = receiptsMessage.lastBlockIncomplete().isPresent(); + + final List> receivedReceipts = + isEth70Response + ? completeFirstBlock(receiptsMessage.syncReceipts()) + : receiptsMessage.syncReceipts(); + + if (receivedReceipts.isEmpty()) { + throw new InvalidPeerTaskResponseException("No result returned"); + } + + if (receivedReceipts.size() > request.size()) { throw new InvalidPeerTaskResponseException("Too many result returned"); } - final Map> response = - HashMap.newHashMap(receivedBlocks.size()); - for (int i = 0; i < receivedBlocks.size(); i++) { - response.put(requestedBlocks.get(i), receivedBlocks.get(i)); + + final int endIndex; + final List lastBlockPartialReceipts; + if (isEth70Response && receiptsMessage.lastBlockIncomplete().get()) { + endIndex = receivedReceipts.size() - 1; + lastBlockPartialReceipts = receivedReceipts.getLast(); + } else { + endIndex = receivedReceipts.size(); + lastBlockPartialReceipts = List.of(); } - return response; + + final Map> receiptsByBlock = + HashMap.newHashMap(receivedReceipts.size()); + + for (int i = 0; i < endIndex; i++) { + receiptsByBlock.put(request.blocks.get(i), receivedReceipts.get(i)); + } + + return new Response(receiptsByBlock, lastBlockPartialReceipts); } catch (RLPException e) { // indicates a malformed or unexpected RLP result from the peer throw new MalformedRlpFromPeerException(e, messageData.getData()); } } + private List> completeFirstBlock( + final List> receivedReceipts) + throws InvalidPeerTaskResponseException { + if (request.firstBlockPartialReceipts.isEmpty()) { + // nothing to integrate returning as is + return receivedReceipts; + } + + if (receivedReceipts.isEmpty()) { + throw new InvalidPeerTaskResponseException("No result returned"); + } + + // add new receipts to the already present ones + final List cumulativeReceiptsForFirstBlock = + new ArrayList<>( + request.firstBlockPartialReceipts.size() + receivedReceipts.getFirst().size()); + cumulativeReceiptsForFirstBlock.addAll(request.firstBlockPartialReceipts); + cumulativeReceiptsForFirstBlock.addAll(receivedReceipts.getFirst()); + + // replace first list of receipts with the new cumulative list + final List> cumulativeReceipts = + new ArrayList<>(receivedReceipts.size()); + cumulativeReceipts.add(cumulativeReceiptsForFirstBlock); + cumulativeReceipts.addAll(receivedReceipts.subList(1, receivedReceipts.size())); + return cumulativeReceipts; + } + @Override public Predicate getPeerRequirementFilter() { return (ethPeer) -> isPoS || ethPeer.estimatedChainHeight() >= requiredBlockchainHeight; } @Override - public PeerTaskValidationResponse validateResult( - final Map> result) { + public PeerTaskValidationResponse validateResult(final Response result) { if (result.isEmpty()) { return PeerTaskValidationResponse.NO_RESULTS_RETURNED; } - for (final Map.Entry> entry : result.entrySet()) { + for (final Map.Entry> entry : + result.completeReceiptsByBlock.entrySet()) { final SyncBlock requestedBlock = entry.getKey(); final List receivedReceiptsForBlock = entry.getValue(); @@ -136,12 +188,21 @@ public PeerTaskValidationResponse validateResult( } } + if (!result.lastBlockPartialReceipts().isEmpty()) { + final SyncBlock lastBlockReceived = + request.blocks.get(result.completeReceiptsByBlock().size()); + if (result.lastBlockPartialReceipts().size() + > lastBlockReceived.getBody().getTransactionCount()) { + return PeerTaskValidationResponse.TOO_MANY_RESULTS_RETURNED; + } + } + return PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD; } private boolean receiptsRootMatches( final BlockHeader blockHeader, final List receipts) { - final Hash calculatedReceiptsRoot = + final var calculatedReceiptsRoot = Util.getRootFromListOfBytes( receipts.stream() .map( @@ -160,6 +221,29 @@ private boolean receiptsRootMatches( @VisibleForTesting public List getRequestedBlocks() { - return requestedBlocks; + return request.blocks(); + } + + public record Request( + List blocks, List firstBlockPartialReceipts) { + public boolean isEmpty() { + return blocks.isEmpty(); + } + + public int size() { + return blocks.size(); + } + } + + public record Response( + Map> completeReceiptsByBlock, + List lastBlockPartialReceipts) { + public boolean isEmpty() { + return completeReceiptsByBlock.isEmpty() && lastBlockPartialReceipts.isEmpty(); + } + + public int size() { + return completeReceiptsByBlock.size(); + } } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java index a8a0b7e601f..f3bca1f2fc4 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java @@ -27,6 +27,8 @@ import org.apache.tuweni.bytes.Bytes; public final class GetReceiptsMessage extends AbstractMessageData { + private List blockHashes; + private int firstBlockReceiptIndex; public static GetReceiptsMessage readFrom(final MessageData message) { if (message instanceof GetReceiptsMessage) { @@ -40,16 +42,32 @@ public static GetReceiptsMessage readFrom(final MessageData message) { return new GetReceiptsMessage(message.getData()); } - public static GetReceiptsMessage create(final Iterable hashes) { + public static GetReceiptsMessage create(final List blockHashes) { + return create(blockHashes, -1); + } + + public static GetReceiptsMessage create( + final List blockHashes, final int firstBlockReceiptIndex) { final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + if (firstBlockReceiptIndex >= 0) { + tmp.writeIntScalar(firstBlockReceiptIndex); + } tmp.startList(); - hashes.forEach(hash -> tmp.writeBytes(hash.getBytes())); + blockHashes.forEach(hash -> tmp.writeBytes(hash.getBytes())); tmp.endList(); - return new GetReceiptsMessage(tmp.encoded()); + return new GetReceiptsMessage(tmp.encoded(), blockHashes, firstBlockReceiptIndex); } private GetReceiptsMessage(final Bytes data) { super(data); + deserialize(data); + } + + private GetReceiptsMessage( + final Bytes data, final List blockHashes, final int firstBlockReceiptIndex) { + super(data); + this.blockHashes = blockHashes; + this.firstBlockReceiptIndex = firstBlockReceiptIndex; } @Override @@ -57,14 +75,23 @@ public int getCode() { return EthProtocolMessages.GET_RECEIPTS; } - public List hashes() { + public List blockHashes() { + return blockHashes; + } + + public int firstBlockReceiptIndex() { + return firstBlockReceiptIndex; + } + + private void deserialize(final Bytes data) { final RLPInput input = new BytesValueRLPInput(data, false); - input.enterList(); - final List hashes = new ArrayList<>(); + + this.firstBlockReceiptIndex = input.nextIsList() ? -1 : input.readIntScalar(); + + this.blockHashes = new ArrayList<>(input.enterList()); while (!input.isEndOfCurrentList()) { - hashes.add(Hash.wrap(input.readBytes32())); + blockHashes.add(Hash.wrap(input.readBytes32())); } input.leaveList(); - return hashes; } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java index 7e2eb3a4c39..a5b31188f9d 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java @@ -15,17 +15,27 @@ package org.hyperledger.besu.ethereum.eth.messages; import org.hyperledger.besu.ethereum.core.SyncTransactionReceipt; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; import org.hyperledger.besu.ethereum.core.encoding.receipt.SyncTransactionReceiptDecoder; +import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptDecoder; +import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; +import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.AbstractMessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.rlp.RLPInput; import java.util.ArrayList; import java.util.List; +import java.util.Optional; +import java.util.function.Function; +import javax.annotation.concurrent.NotThreadSafe; +import com.google.common.annotations.VisibleForTesting; import org.apache.tuweni.bytes.Bytes; +@NotThreadSafe public final class ReceiptsMessage extends AbstractMessageData { /** * This default decoder instance is used for performance reasons to avoid creating a new decoder @@ -34,8 +44,21 @@ public final class ReceiptsMessage extends AbstractMessageData { private static final SyncTransactionReceiptDecoder DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER = new SyncTransactionReceiptDecoder(); - private ReceiptsMessage(final Bytes data) { + private final SyncTransactionReceiptDecoder syncTransactionReceiptDecoder; + private List> receiptsByBlock; + private List> syncReceiptsByBlock; + private Optional lastBlockIncomplete; + + private ReceiptsMessage( + final Bytes data, + final SyncTransactionReceiptDecoder syncTransactionReceiptDecoder, + final List> receipts, + final Boolean lastBlockIncomplete) { super(data); + this.syncTransactionReceiptDecoder = syncTransactionReceiptDecoder; + this.receiptsByBlock = receipts; + this.lastBlockIncomplete = + (lastBlockIncomplete != null) ? Optional.of(lastBlockIncomplete) : null; } public static ReceiptsMessage readFrom(final MessageData message) { @@ -47,7 +70,25 @@ public static ReceiptsMessage readFrom(final MessageData message) { throw new IllegalArgumentException( String.format("Message has code %d and thus is not a ReceiptsMessage.", code)); } - return new ReceiptsMessage(message.getData()); + return new ReceiptsMessage( + message.getData(), DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER, null, null); + } + + @VisibleForTesting + public static ReceiptsMessage create( + final List> receipts, + final TransactionReceiptEncodingConfiguration encodingConfiguration) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.startList(); + receipts.forEach( + (receiptSet) -> { + tmp.startList(); + receiptSet.forEach(r -> TransactionReceiptEncoder.writeTo(r, tmp, encodingConfiguration)); + tmp.endList(); + }); + tmp.endList(); + return new ReceiptsMessage( + tmp.encoded(), DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER, receipts, null); } /** @@ -58,7 +99,12 @@ public static ReceiptsMessage readFrom(final MessageData message) { * @return A new ReceiptsMessage */ public static ReceiptsMessage createUnsafe(final Bytes data) { - return new ReceiptsMessage(data); + return new ReceiptsMessage(data, DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER, null, null); + } + + public static ReceiptsMessage createUnsafe(final Bytes data, final boolean lastBlockIncomplete) { + return new ReceiptsMessage( + data, DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER, null, lastBlockIncomplete); } @Override @@ -66,22 +112,57 @@ public int getCode() { return EthProtocolMessages.RECEIPTS; } + public List> receipts() { + if (receiptsByBlock == null) { + receiptsByBlock = + deserialize( + getData(), rlpInput -> TransactionReceiptDecoder.readFrom(rlpInput, false), false); + } + return receiptsByBlock; + } + public List> syncReceipts() { + if (syncReceiptsByBlock == null) { + syncReceiptsByBlock = + deserialize( + getData(), + rlpInput -> + syncTransactionReceiptDecoder.decode( + rlpInput.nextIsList() ? rlpInput.currentListAsBytes() : rlpInput.readBytes()), + false); + } + return syncReceiptsByBlock; + } + + public Optional lastBlockIncomplete() { + if (lastBlockIncomplete == null) { + deserialize(getData(), null, true); + } + return lastBlockIncomplete; + } + + private List> deserialize( + final Bytes data, + final Function deserializer, + final boolean onlyLastBlockIncomplete) { final RLPInput input = new BytesValueRLPInput(data, false); - input.enterList(); - final List> receiptsForBodies = new ArrayList<>(); + this.lastBlockIncomplete = + input.nextIsList() ? Optional.empty() : Optional.of(input.readLongScalar() == 1); + if (onlyLastBlockIncomplete) { + return null; + } + + final List> receiptsByBlock = new ArrayList<>(input.enterList()); while (input.nextIsList()) { final int setSize = input.enterList(); - final List receiptSet = new ArrayList<>(setSize); + final List receiptSet = new ArrayList<>(setSize); for (int i = 0; i < setSize; i++) { - receiptSet.add( - DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER.decode( - input.nextIsList() ? input.currentListAsBytes() : input.readBytes())); + receiptSet.add(deserializer.apply(input)); } input.leaveList(); - receiptsForBodies.add(receiptSet); + receiptsByBlock.add(receiptSet); } input.leaveList(); - return receiptsForBodies; + return receiptsByBlock; } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/DownloadSyncReceiptsStep.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/DownloadSyncReceiptsStep.java index 72e79a25ec7..f215793d4f6 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/DownloadSyncReceiptsStep.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/DownloadSyncReceiptsStep.java @@ -28,6 +28,8 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutorResponseCode; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutorResult; import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Request; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Response; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import java.time.Duration; @@ -76,12 +78,15 @@ public DownloadSyncReceiptsStep( public CompletableFuture> apply(final List blocks) { final int currTaskId = taskSequence.incrementAndGet(); final List blocksToRequest = prepareRequest(blocks); + final List firstBlockPartialReceipts = new ArrayList<>(); final Map> receiptsByRootHash = HashMap.newHashMap(blocksToRequest.size()); return ethScheduler .scheduleServiceTask( - () -> downloadReceipts(currTaskId, 0, blocksToRequest, receiptsByRootHash)) + () -> + downloadReceipts( + currTaskId, 0, blocksToRequest, firstBlockPartialReceipts, receiptsByRootHash)) .thenApply(receipts -> combineBlocksAndReceipts(blocks, receipts)) .orTimeout(timeoutDuration.toMillis(), TimeUnit.MILLISECONDS) .whenComplete( @@ -136,6 +141,7 @@ private CompletableFuture>> downloadRecei final int currTaskId, final int prevIterations, final List blocksToRequest, + final List firstBlockPartialReceipts, final Map> receiptsByRootHash) { final int initialBlockCount = blocksToRequest.size(); @@ -144,25 +150,28 @@ private CompletableFuture>> downloadRecei ++iteration; LOG.atTrace() - .setMessage("[{}:{}] Requesting receipts for {} blocks (initial {}): {}") + .setMessage( + "[{}:{}] Requesting receipts for {} blocks, partial receipts fetched for first block {} (initial {}): {}") .addArgument(currTaskId) .addArgument(iteration) .addArgument(blocksToRequest::size) + .addArgument(firstBlockPartialReceipts::size) .addArgument(initialBlockCount) .addArgument(() -> formatBlockDetails(blocksToRequest)) .log(); final GetSyncReceiptsFromPeerTask task = new GetSyncReceiptsFromPeerTask( - blocksToRequest, protocolSchedule, syncTransactionReceiptEncoder); + new Request(blocksToRequest, firstBlockPartialReceipts), + protocolSchedule, + syncTransactionReceiptEncoder); - final PeerTaskExecutorResult>> getReceiptsResult = - peerTaskExecutor.execute(task); + final PeerTaskExecutorResult getReceiptsResult = peerTaskExecutor.execute(task); final PeerTaskExecutorResponseCode responseCode = getReceiptsResult.responseCode(); if (responseCode == SUCCESS) { - final Map> receiptsByBlock = + final Response response = getReceiptsResult .result() .orElseThrow( @@ -170,10 +179,23 @@ private CompletableFuture>> downloadRecei new IllegalStateException( "Task validation failure, it must flag empty result as failure")); + final Map> receiptsByBlock = + response.completeReceiptsByBlock(); + final List lastBlockPartialReceipts = + response.lastBlockPartialReceipts(); + + firstBlockPartialReceipts.clear(); + if (!lastBlockPartialReceipts.isEmpty()) { + firstBlockPartialReceipts.addAll(lastBlockPartialReceipts); + } + LOG.atTrace() - .setMessage("[{}:{}] Received response for {} blocks (requested {}, initial {}): {}") + .setMessage( + "[{}:{}] Received complete response for {} blocks, last block partial receipts {}, completed blocks {} (requested {}, initial {}): {}") .addArgument(currTaskId) .addArgument(iteration) + .addArgument(response::size) + .addArgument(lastBlockPartialReceipts::size) .addArgument(receiptsByBlock::size) .addArgument(blocksToRequest::size) .addArgument(initialBlockCount) @@ -181,11 +203,10 @@ private CompletableFuture>> downloadRecei .log(); receiptsByBlock.forEach( - (syncBlock, syncTransactionReceipts) -> - receiptsByRootHash.put( - syncBlock.getHeader().getReceiptsRoot(), syncTransactionReceipts)); - - blocksToRequest.removeAll(receiptsByBlock.keySet()); + (block, receipts) -> { + receiptsByRootHash.put(block.getHeader().getReceiptsRoot(), receipts); + blocksToRequest.remove(block); + }); } else { LOG.atTrace() .setMessage( @@ -205,7 +226,11 @@ private CompletableFuture>> downloadRecei ethScheduler.scheduleServiceTask( () -> downloadReceipts( - currTaskId, passIterations, blocksToRequest, receiptsByRootHash)), + currTaskId, + passIterations, + blocksToRequest, + firstBlockPartialReceipts, + receiptsByRootHash)), RETRY_DELAY); } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java index bb3de01f1ea..40d3bfe2eff 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java @@ -41,13 +41,13 @@ public void roundTripTest() { // Perform round-trip transformation // Create GetReceipts message, copy it to a generic message, then read back into a GetReceipts // message - final MessageData initialMessage = GetReceiptsMessage.create(hashes); + final MessageData initialMessage = GetReceiptsMessage.create(hashes, 1); final MessageData raw = new RawMessage(EthProtocolMessages.GET_RECEIPTS, initialMessage.getData()); final GetReceiptsMessage message = GetReceiptsMessage.readFrom(raw); // Read hashes back out after round trip and check they match originals. - final Iterator readData = message.hashes().iterator(); + final Iterator readData = message.blockHashes().iterator(); for (int i = 0; i < hashCount; ++i) { Assertions.assertThat(readData.next()).isEqualTo(hashes.get(i)); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/DownloadSyncReceiptsStepTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/DownloadSyncReceiptsStepTest.java index fadb29bdbac..0281442e75a 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/DownloadSyncReceiptsStepTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/DownloadSyncReceiptsStepTest.java @@ -43,6 +43,7 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutor; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskExecutorResult; import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Response; import org.hyperledger.besu.ethereum.mainnet.DefaultProtocolSchedule; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.metrics.noop.NoOpMetricsSystem; @@ -88,25 +89,29 @@ public void setUp() { @Test public void shouldDownloadReceiptsForBlocksWithTransactions() throws ExecutionException, InterruptedException { - // skip genesis block, since we do not need to retrieve receipt for it final List blockWithTxs = gen.blockSequence(3).subList(1, 3); - final var receiptsPerBlock = + final List> returnedReceiptsByBlock = blockWithTxs.stream() .map(gen::receipts) .map(rs -> receiptsToSyncReceipts(rs, ETH69_RECEIPT_CONFIGURATION)) .toList(); final var syncBlocks = blocksToSyncBlocks(blockWithTxs); - final Map> returnedReceiptsByBlock = new HashMap<>(); - for (int i = 0; i < syncBlocks.size(); i++) { - returnedReceiptsByBlock.put(syncBlocks.get(i), receiptsPerBlock.get(i)); - } - // Mock the peer task executor to return receipts for both blocks - final var executorResult = - new PeerTaskExecutorResult<>(Optional.of(returnedReceiptsByBlock), SUCCESS, emptyList()); + final PeerTaskExecutorResult executorResult = + new PeerTaskExecutorResult<>( + Optional.of( + new Response( + Map.of( + syncBlocks.get(0), + returnedReceiptsByBlock.get(0), + syncBlocks.get(1), + returnedReceiptsByBlock.get(1)), + List.of())), + SUCCESS, + emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(executorResult); @@ -119,7 +124,7 @@ public void shouldDownloadReceiptsForBlocksWithTransactions() assertThat(blocksWithReceipts).hasSize(2); for (int i = 0; i < blocksWithReceipts.size(); i++) { assertThat(blocksWithReceipts.get(i).getBlock()).isEqualTo(syncBlocks.get(i)); - assertThat(blocksWithReceipts.get(i).getReceipts()).isEqualTo(receiptsPerBlock.get(i)); + assertThat(blocksWithReceipts.get(i).getReceipts()).isEqualTo(returnedReceiptsByBlock.get(i)); } // Verify the task was executed once @@ -139,21 +144,23 @@ public void shouldSkipDownloadForBlocksWithEmptyReceiptsRoot() gen.setBlockOptionsSupplier(() -> BlockOptions.create().hasTransactions(true)); final Block block3_withTxs = gen.blockSequence(block2_withoutTxs, 1).getFirst(); - final var blocks = List.of(block1_withTxs, block2_withoutTxs, block3_withTxs); + final List blocks = List.of(block1_withTxs, block2_withoutTxs, block3_withTxs); // we must not request receipt for block2, so only return receipts for the 2 blocks with txs - final var receiptsForBlock1 = + final List receiptsForBlock1 = receiptsToSyncReceipts(gen.receipts(block1_withTxs), ETH69_RECEIPT_CONFIGURATION); - final var receiptsForBlock3 = + final List receiptsForBlock3 = receiptsToSyncReceipts(gen.receipts(block3_withTxs), ETH69_RECEIPT_CONFIGURATION); - final var syncBlocks = blocksToSyncBlocks(blocks); + final List syncBlocks = blocksToSyncBlocks(blocks); - final Map> returnedReceiptsByBlock = - Map.of(syncBlocks.get(0), receiptsForBlock1, syncBlocks.get(2), receiptsForBlock3); + final Response taskResponse = + new Response( + Map.of(syncBlocks.get(0), receiptsForBlock1, syncBlocks.get(2), receiptsForBlock3), + List.of()); - final var executorResult = - new PeerTaskExecutorResult<>(Optional.of(returnedReceiptsByBlock), SUCCESS, emptyList()); + final PeerTaskExecutorResult executorResult = + new PeerTaskExecutorResult<>(Optional.of(taskResponse), SUCCESS, emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(executorResult); @@ -178,6 +185,120 @@ public void shouldSkipDownloadForBlocksWithEmptyReceiptsRoot() verify(peerTaskExecutor, times(1)).execute(any(GetSyncReceiptsFromPeerTask.class)); } + @Test + public void shouldHandlePartialReceiptsFromFirstBlock() + throws ExecutionException, InterruptedException { + + // Given: blocks with 3 transactions each, excluding genesis block + gen.setBlockOptionsSupplier( + () -> BlockOptions.create().hasTransactions(true).transactionCount(3)); + final List blocks = gen.blockSequence(3).subList(1, 3); + + final List syncBlocks = blocksToSyncBlocks(blocks); + + final List> returnedReceiptsByBlock = + blocks.stream() + .map(gen::receipts) + .map(rs -> receiptsToSyncReceipts(rs, ETH69_RECEIPT_CONFIGURATION)) + .toList(); + + // First call returns partial receipts for first block + final List firstBlockReceipts = returnedReceiptsByBlock.getFirst(); + final List secondBlockReceipts = returnedReceiptsByBlock.get(1); + final List partialReceipts = + firstBlockReceipts.subList(0, firstBlockReceipts.size() / 2); + + final PeerTaskExecutorResult firstExecutorResult = + new PeerTaskExecutorResult<>( + Optional.of(new Response(Map.of(), partialReceipts)), SUCCESS, emptyList()); + + // Second call returns combined receipts (partial + remaining) for first block and second block + // Note: processResponse in AbstractGetReceiptsFromPeerTask combines firstBlockPartialReceipts + // with newly received receipts, so the result contains the full receipt list + final PeerTaskExecutorResult secondExecutorResult = + new PeerTaskExecutorResult<>( + Optional.of( + new Response( + Map.of( + syncBlocks.getFirst(), + firstBlockReceipts, + syncBlocks.get(1), + secondBlockReceipts), + List.of())), + SUCCESS, + emptyList()); + + when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) + .thenReturn(firstExecutorResult) + .thenReturn(secondExecutorResult); + + // When: downloading receipts + final CompletableFuture> result = + downloadSyncReceiptsStep.apply(syncBlocks); + + // Then: should return blocks with complete receipts + final List blocksWithReceipts = result.get(); + assertThat(blocksWithReceipts).hasSize(2); + assertThat(blocksWithReceipts.get(0).getReceipts()).isEqualTo(firstBlockReceipts); + assertThat(blocksWithReceipts.get(1).getReceipts()).isEqualTo(secondBlockReceipts); + + // Verify the task was executed twice + verify(peerTaskExecutor, times(2)).execute(any(GetSyncReceiptsFromPeerTask.class)); + } + + @Test + public void shouldHandlePartialReceiptsFromBlockAdvanced() + throws ExecutionException, InterruptedException { + + // Given: block with 3 transactions + gen.setBlockOptionsSupplier( + () -> BlockOptions.create().hasTransactions(true).transactionCount(3)); + final Block block = gen.block(); + + final List syncBlocks = blocksToSyncBlocks(List.of(block)); + + final List returnedReceipts = + receiptsToSyncReceipts(gen.receipts(block), ETH69_RECEIPT_CONFIGURATION); + + // Receipts for the block are split in three responses + // Note: processResponse combines partial receipts, so each result contains cumulative receipts + final List firstCallReturnedReceipts = returnedReceipts.subList(0, 1); + final List secondCallReturnedReceipts = returnedReceipts.subList(0, 2); + final List thirdCallReturnedReceipts = returnedReceipts; + + final PeerTaskExecutorResult firstExecutorResult = + new PeerTaskExecutorResult<>( + Optional.of(new Response(Map.of(), firstCallReturnedReceipts)), SUCCESS, emptyList()); + + final PeerTaskExecutorResult secondExecutorResult = + new PeerTaskExecutorResult<>( + Optional.of(new Response(Map.of(), secondCallReturnedReceipts)), SUCCESS, emptyList()); + + final PeerTaskExecutorResult thirdExecutorResult = + new PeerTaskExecutorResult<>( + Optional.of( + new Response(Map.of(syncBlocks.getFirst(), thirdCallReturnedReceipts), List.of())), + SUCCESS, + emptyList()); + + when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) + .thenReturn(firstExecutorResult) + .thenReturn(secondExecutorResult) + .thenReturn(thirdExecutorResult); + + // When: downloading receipts + final CompletableFuture> result = + downloadSyncReceiptsStep.apply(syncBlocks); + + // Then: should return block with complete receipts + final List blocksWithReceipts = result.get(); + assertThat(blocksWithReceipts).hasSize(1); + assertThat(blocksWithReceipts.getFirst().getReceipts()).isEqualTo(returnedReceipts); + + // Verify the task was executed twice + verify(peerTaskExecutor, times(3)).execute(any(GetSyncReceiptsFromPeerTask.class)); + } + @Test public void shouldRetryUntilAllReceiptsDownloaded() throws ExecutionException, InterruptedException { @@ -185,26 +306,35 @@ public void shouldRetryUntilAllReceiptsDownloaded() final List blocks = gen.blockSequence(4).subList(1, 4); final List syncBlocks = blocksToSyncBlocks(blocks); - final var receiptsPerBlock = + final List> receiptsPerBlock = blocks.stream() .map(gen::receipts) .map(rs -> receiptsToSyncReceipts(rs, ETH69_RECEIPT_CONFIGURATION)) .toList(); // First call returns first block only - final var firstExecutorResult = + final PeerTaskExecutorResult firstExecutorResult = new PeerTaskExecutorResult<>( - Optional.of(Map.of(syncBlocks.get(0), receiptsPerBlock.get(0))), SUCCESS, emptyList()); + Optional.of( + new Response(Map.of(syncBlocks.get(0), receiptsPerBlock.get(0)), List.of())), + SUCCESS, + emptyList()); // Second call returns second block only - final var secondExecutorResult = + final PeerTaskExecutorResult secondExecutorResult = new PeerTaskExecutorResult<>( - Optional.of(Map.of(syncBlocks.get(1), receiptsPerBlock.get(1))), SUCCESS, emptyList()); + Optional.of( + new Response(Map.of(syncBlocks.get(1), receiptsPerBlock.get(1)), List.of())), + SUCCESS, + emptyList()); // Third call returns third block only - final var thirdExecutorResult = + final PeerTaskExecutorResult thirdExecutorResult = new PeerTaskExecutorResult<>( - Optional.of(Map.of(syncBlocks.get(2), receiptsPerBlock.get(2))), SUCCESS, emptyList()); + Optional.of( + new Response(Map.of(syncBlocks.get(2), receiptsPerBlock.get(2)), List.of())), + SUCCESS, + emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(firstExecutorResult) @@ -287,13 +417,13 @@ public void shouldRetryAfterSingleFailureAndEventuallySucceed() } // First call returns failure (e.g., peer disconnected) - final var failureResult = - new PeerTaskExecutorResult>>( - Optional.empty(), PEER_DISCONNECTED, emptyList()); + final PeerTaskExecutorResult failureResult = + new PeerTaskExecutorResult<>(Optional.empty(), PEER_DISCONNECTED, emptyList()); // Second call returns success with all receipts - final var successResult = - new PeerTaskExecutorResult<>(Optional.of(returnedReceiptsByBlock), SUCCESS, emptyList()); + final PeerTaskExecutorResult successResult = + new PeerTaskExecutorResult<>( + Optional.of(new Response(returnedReceiptsByBlock, emptyList())), SUCCESS, emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(failureResult) @@ -329,20 +459,19 @@ public void shouldRetryMultipleTimesAfterConsecutiveFailures() .toList(); // First three calls return different failures - final var timeoutResult = - new PeerTaskExecutorResult>>( - Optional.empty(), TIMEOUT, emptyList()); - final var noPeerResult = - new PeerTaskExecutorResult>>( - Optional.empty(), NO_PEER_AVAILABLE, emptyList()); - final var disconnectedResult = - new PeerTaskExecutorResult>>( - Optional.empty(), PEER_DISCONNECTED, emptyList()); + final PeerTaskExecutorResult timeoutResult = + new PeerTaskExecutorResult<>(Optional.empty(), TIMEOUT, emptyList()); + final PeerTaskExecutorResult noPeerResult = + new PeerTaskExecutorResult<>(Optional.empty(), NO_PEER_AVAILABLE, emptyList()); + final PeerTaskExecutorResult disconnectedResult = + new PeerTaskExecutorResult<>(Optional.empty(), PEER_DISCONNECTED, emptyList()); // Fourth call returns success - final var successResult = + final PeerTaskExecutorResult successResult = new PeerTaskExecutorResult<>( - Optional.of(Map.of(syncBlocks.getFirst(), receiptsPerBlock.getFirst())), + Optional.of( + new Response( + Map.of(syncBlocks.getFirst(), receiptsPerBlock.getFirst()), List.of())), SUCCESS, emptyList()); @@ -360,6 +489,7 @@ public void shouldRetryMultipleTimesAfterConsecutiveFailures() final List blocksWithReceipts = result.get(); assertThat(blocksWithReceipts).hasSize(1); assertThat(blocksWithReceipts.getFirst().getBlock()).isEqualTo(syncBlocks.getFirst()); + assertThat(blocksWithReceipts.getFirst().getReceipts()).isEqualTo(receiptsPerBlock.getFirst()); // Verify the task was executed 4 times (3 failures + 1 success) @@ -373,9 +503,8 @@ public void shouldThrowIllegalStateExceptionWhenSuccessWithEmptyResult() { final List syncBlocks = blocksToSyncBlocks(blocks); // Mock returns SUCCESS but with empty Optional (should never happen in practice) - final var invalidResult = - new PeerTaskExecutorResult>>( - Optional.empty(), SUCCESS, emptyList()); + final PeerTaskExecutorResult invalidResult = + new PeerTaskExecutorResult<>(Optional.empty(), SUCCESS, emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(invalidResult); @@ -417,16 +546,17 @@ public void shouldDeduplicateBlocksWithSameReceiptRoot() final List syncBlocks = blocksToSyncBlocks(blocks); // Only 2 unique receipt requests should be made (for block1 and block3) - final var receiptsForBlock1 = + final List receiptsForBlock1 = receiptsToSyncReceipts(gen.receipts(block1), ETH69_RECEIPT_CONFIGURATION); - final var receiptsForBlock3 = + final List receiptsForBlock3 = receiptsToSyncReceipts(gen.receipts(block3), ETH69_RECEIPT_CONFIGURATION); final Map> returnedReceiptsByBlock = Map.of(syncBlocks.get(0), receiptsForBlock1, syncBlocks.get(2), receiptsForBlock3); - final var executorResult = - new PeerTaskExecutorResult<>(Optional.of(returnedReceiptsByBlock), SUCCESS, emptyList()); + final PeerTaskExecutorResult executorResult = + new PeerTaskExecutorResult<>( + Optional.of(new Response(returnedReceiptsByBlock, List.of())), SUCCESS, emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(executorResult); @@ -462,34 +592,37 @@ public void shouldHandlePartialSuccessThenFailureThenSuccess() final List blocks = gen.blockSequence(5).subList(1, 5); final List syncBlocks = blocksToSyncBlocks(blocks); - final var allReceiptsPerBlock = + final List> allReceiptsPerBlock = blocks.stream() .map(gen::receipts) .map(rs -> receiptsToSyncReceipts(rs, ETH69_RECEIPT_CONFIGURATION)) .toList(); // First call returns partial success (first 2 blocks only) - final var firstSuccessResult = + final PeerTaskExecutorResult firstSuccessResult = new PeerTaskExecutorResult<>( Optional.of( - Map.of( - syncBlocks.get(0), allReceiptsPerBlock.get(0), - syncBlocks.get(1), allReceiptsPerBlock.get(1))), + new Response( + Map.of( + syncBlocks.get(0), allReceiptsPerBlock.get(0), + syncBlocks.get(1), allReceiptsPerBlock.get(1)), + List.of())), SUCCESS, emptyList()); // Second call for remaining blocks returns failure - final var failureResult = - new PeerTaskExecutorResult>>( - Optional.empty(), TIMEOUT, emptyList()); + final PeerTaskExecutorResult failureResult = + new PeerTaskExecutorResult<>(Optional.empty(), TIMEOUT, emptyList()); // Third call (retry) returns the remaining 2 blocks successfully - final var secondSuccessResult = + final PeerTaskExecutorResult secondSuccessResult = new PeerTaskExecutorResult<>( Optional.of( - Map.of( - syncBlocks.get(2), allReceiptsPerBlock.get(2), - syncBlocks.get(3), allReceiptsPerBlock.get(3))), + new Response( + Map.of( + syncBlocks.get(2), allReceiptsPerBlock.get(2), + syncBlocks.get(3), allReceiptsPerBlock.get(3)), + List.of())), SUCCESS, emptyList()); @@ -541,9 +674,8 @@ public void shouldTimeoutAfterConfiguredDuration() throws Exception { Duration.ofMillis(100)); // Mock continuous failures that would retry indefinitely without timeout - final var failureResult = - new PeerTaskExecutorResult>>( - Optional.empty(), TIMEOUT, emptyList()); + final PeerTaskExecutorResult failureResult = + new PeerTaskExecutorResult<>(Optional.empty(), TIMEOUT, emptyList()); when(peerTaskExecutor.execute(any(GetSyncReceiptsFromPeerTask.class))) .thenReturn(failureResult); diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/MessageData.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/MessageData.java index 35a45c5015d..37f2c278ff2 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/MessageData.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/MessageData.java @@ -20,6 +20,7 @@ import java.math.BigInteger; import java.util.AbstractMap; +import java.util.ArrayList; import java.util.Map; import org.apache.tuweni.bytes.Bytes; @@ -63,9 +64,13 @@ default Map.Entry unwrapMessageData() { final RLPInput messageDataRLP = RLP.input(getData()); messageDataRLP.enterList(); final BigInteger requestId = messageDataRLP.readBigIntegerScalar(); - final Bytes message = messageDataRLP.readAsRlp().raw(); + final var params = new ArrayList(); + while (!messageDataRLP.isEndOfCurrentList()) { + params.add(messageDataRLP.readAsRlp().raw()); + } messageDataRLP.leaveList(); - return new AbstractMap.SimpleImmutableEntry<>(requestId, new RawMessage(getCode(), message)); + return new AbstractMap.SimpleImmutableEntry<>( + requestId, new RawMessage(getCode(), Bytes.concatenate(params))); } /** diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java index c5011e17d63..a76f181e570 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java @@ -143,6 +143,8 @@ public enum DisconnectReason { UNEXPECTED_ID((byte) 0x09, "Unexpected ID"), LOCAL_IDENTITY((byte) 0x0a, "Local identity"), TIMEOUT((byte) 0x0b, "Timeout"), + INVALID_BLOCK_REQUESTED((byte) 0x0f, "Invalid block requested"), + INVALID_FIRST_BLOCK_RECEIPT_INDEX((byte) 0x0f, "Invalid first block receipt index"), SUBPROTOCOL_TRIGGERED((byte) 0x10, "Sub protocol triggered"), SUBPROTOCOL_TRIGGERED_MISMATCHED_NETWORK((byte) 0x10, "Mismatched network id"), SUBPROTOCOL_TRIGGERED_MISMATCHED_FORKID((byte) 0x10, "Mismatched fork id"), diff --git a/ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLP.java b/ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLP.java index b3c18749ddb..964f6406fcd 100644 --- a/ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLP.java +++ b/ethereum/rlp/src/main/java/org/hyperledger/besu/ethereum/rlp/RLP.java @@ -30,7 +30,7 @@ private RLP() {} public static final Bytes EMPTY_LIST; - // RLP encoding requires payloads to be less thatn 2^64 bytes in length + // RLP encoding requires payloads to be less than 2^64 bytes in length // As a result, the longest RLP strings will have a prefix composed of 1 byte encoding the type // of string followed by at most 8 bytes describing the length of the string public static final int MAX_PREFIX_SIZE = 9; From 26df56c2d133349188ac9c9d6c379429e66c8b95 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Fri, 27 Feb 2026 11:28:38 +0100 Subject: [PATCH 3/7] Better separation of the code between different versions of the protocol that way it will be eaiser and safer to remove a version when it will be deprecated, reducing the surface for bug in the latest version. Signed-off-by: Fabio Di Fabio --- .../besu/ethereum/eth/manager/EthServer.java | 19 +- .../peertask/PeerTaskValidationResponse.java | 3 +- .../task/GetSyncReceiptsFromPeerTask.java | 93 ++- .../messages/GetPaginatedReceiptsMessage.java | 75 ++ .../eth/messages/GetReceiptsMessage.java | 45 +- .../messages/PaginatedReceiptsMessage.java | 62 ++ .../eth/messages/ReceiptsMessage.java | 99 +-- .../eth/manager/EthProtocolManagerTest.java | 10 +- .../ethereum/eth/manager/EthServerTest.java | 4 +- .../task/GetSyncReceiptsFromPeerTaskTest.java | 671 ++++++++++++------ .../GetPaginatedReceiptsMessageTest.java | 95 +++ .../eth/messages/GetReceiptsMessageTest.java | 2 +- .../PaginatedReceiptsMessageTest.java | 110 +++ .../fastsync/FastSyncChainDownloaderTest.java | 3 +- .../rlpx/wire/messages/DisconnectMessage.java | 1 + 15 files changed, 912 insertions(+), 380 deletions(-) create mode 100644 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java create mode 100644 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessage.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessageTest.java create mode 100644 ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessageTest.java diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java index 7f1de1e2cbc..7bd823e42bd 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java @@ -35,9 +35,11 @@ import org.hyperledger.besu.ethereum.eth.messages.GetBlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.GetNodeDataMessage; +import org.hyperledger.besu.ethereum.eth.messages.GetPaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetPooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.NodeDataMessage; +import org.hyperledger.besu.ethereum.eth.messages.PaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.PooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; @@ -101,7 +103,7 @@ private void registerResponseConstructors() { EthProtocolMessages.GET_RECEIPTS, (peer, messageData, capability) -> { if (EthProtocol.isEth70Compatible(capability)) { - return constructGetReceiptsPaginatedResponse( + return constructGetPaginatedReceiptsResponse( peer, blockchain, messageData, @@ -277,14 +279,15 @@ static MessageData constructGetReceiptsResponse( return ReceiptsMessage.createUnsafe(rlp.encoded()); } - static MessageData constructGetReceiptsPaginatedResponse( + static MessageData constructGetPaginatedReceiptsResponse( final EthPeer peer, final Blockchain blockchain, final MessageData message, final int requestLimit, final int maxMessageSize) { - final GetReceiptsMessage getReceipts = GetReceiptsMessage.readFrom(message); - final List requestedBlockHashes = getReceipts.blockHashes(); + final GetPaginatedReceiptsMessage getPaginatedReceipts = + GetPaginatedReceiptsMessage.readFrom(message); + final List requestedBlockHashes = getPaginatedReceipts.blockHashes(); final List blockHashes; if (requestedBlockHashes.size() > requestLimit) { LOG.atDebug() @@ -300,7 +303,7 @@ static MessageData constructGetReceiptsPaginatedResponse( final var blockReceiptsRLPs = new ArrayList(blockHashes.size()); - int skipBefore = getReceipts.firstBlockReceiptIndex(); + int skipBefore = getPaginatedReceipts.firstBlockReceiptIndex(); int responseSizeEstimate = RLP.MAX_PREFIX_SIZE; boolean lastBlockIncomplete = false; @@ -309,7 +312,7 @@ static MessageData constructGetReceiptsPaginatedResponse( if (maybeReceipts.isEmpty()) { LOG.debug("Invalid request from peer {}, block {} does not exists", peer, blockHash); peer.disconnect(INVALID_BLOCK_REQUESTED); - return ReceiptsMessage.createUnsafe(Bytes.EMPTY, false); + return PaginatedReceiptsMessage.createUnsafe(Bytes.EMPTY, false); } final List blockReceipts = maybeReceipts.get(); @@ -323,7 +326,7 @@ static MessageData constructGetReceiptsPaginatedResponse( blockReceipts.size(), blockHash); peer.disconnect(INVALID_FIRST_BLOCK_RECEIPT_INDEX); - return ReceiptsMessage.createUnsafe(Bytes.EMPTY, false); + return PaginatedReceiptsMessage.createUnsafe(Bytes.EMPTY, false); } if (skipBefore > 0) { @@ -364,7 +367,7 @@ static MessageData constructGetReceiptsPaginatedResponse( blockReceiptsRLPs.forEach(r -> rlp.writeRaw(r.encoded())); rlp.endList(); - return ReceiptsMessage.createUnsafe(rlp.encoded(), lastBlockIncomplete); + return PaginatedReceiptsMessage.createUnsafe(rlp.encoded(), lastBlockIncomplete); } static MessageData constructGetPooledTransactionsResponse( diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java index 9c0fae1a8bb..5693f506b77 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/PeerTaskValidationResponse.java @@ -24,7 +24,8 @@ public enum PeerTaskValidationResponse { RESULTS_DO_NOT_MATCH_QUERY(null, true), NON_SEQUENTIAL_HEADERS_RETURNED( DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL_NON_SEQUENTIAL_HEADERS, true), - RESULTS_VALID_AND_GOOD(null, false); + RESULTS_VALID_AND_GOOD(null, false), + INVALID_RECEIPT_RETURNED(DisconnectMessage.DisconnectReason.INVALID_RECEIPT_RECEIVED, true); private final Optional disconnectReason; private final boolean recordUselessResponse; diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java index d276d407595..6c25e0f7a6b 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java @@ -28,7 +28,9 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.MalformedRlpFromPeerException; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTask; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskValidationResponse; +import org.hyperledger.besu.ethereum.eth.messages.GetPaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetReceiptsMessage; +import org.hyperledger.besu.ethereum.eth.messages.PaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.mainnet.ProtocolSchedule; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; @@ -45,6 +47,7 @@ import com.google.common.annotations.VisibleForTesting; import org.apache.tuweni.bytes.Bytes; +import org.jspecify.annotations.Nullable; public class GetSyncReceiptsFromPeerTask implements PeerTask { private final Request request; @@ -82,7 +85,7 @@ public SubProtocol getSubProtocol() { public MessageData getRequestMessage(final Set agreedCapabilities) { final List blockHashes = requestedHeaders.stream().map(BlockHeader::getHash).toList(); return agreedCapabilities.stream().anyMatch(EthProtocol::isEth70Compatible) - ? GetReceiptsMessage.create(blockHashes, request.firstBlockPartialReceipts.size()) + ? GetPaginatedReceiptsMessage.create(blockHashes, request.firstBlockPartialReceipts.size()) : GetReceiptsMessage.create(blockHashes); } @@ -93,14 +96,39 @@ public Response processResponse( if (messageData == null) { throw new InvalidPeerTaskResponseException("Null message data"); } + if (agreedCapabilities.stream().anyMatch(EthProtocol::isEth70Compatible)) { + return processPaginatedResponse(messageData); + } + return processNotPaginatedResponse(messageData); + } + + private Response processNotPaginatedResponse(final MessageData messageData) + throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { final ReceiptsMessage receiptsMessage = ReceiptsMessage.readFrom(messageData); try { - final boolean isEth70Response = receiptsMessage.lastBlockIncomplete().isPresent(); + final List> receivedBlocks = receiptsMessage.syncReceipts(); + if (receivedBlocks.size() > request.size()) { + throw new InvalidPeerTaskResponseException("Too many result returned"); + } + final Map> receiptsByBlock = + HashMap.newHashMap(receivedBlocks.size()); + for (int i = 0; i < receivedBlocks.size(); i++) { + receiptsByBlock.put(request.blocks.get(i), receivedBlocks.get(i)); + } + return new Response(receiptsByBlock, List.of()); + } catch (RLPException e) { + // indicates a malformed or unexpected RLP result from the peer + throw new MalformedRlpFromPeerException(e, messageData.getData()); + } + } + private Response processPaginatedResponse(final MessageData messageData) + throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { + final PaginatedReceiptsMessage paginatedReceiptsMessage = + PaginatedReceiptsMessage.readFrom(messageData); + try { final List> receivedReceipts = - isEth70Response - ? completeFirstBlock(receiptsMessage.syncReceipts()) - : receiptsMessage.syncReceipts(); + completeFirstBlock(paginatedReceiptsMessage.syncReceipts()); if (receivedReceipts.isEmpty()) { throw new InvalidPeerTaskResponseException("No result returned"); @@ -112,7 +140,7 @@ public Response processResponse( final int endIndex; final List lastBlockPartialReceipts; - if (isEth70Response && receiptsMessage.lastBlockIncomplete().get()) { + if (paginatedReceiptsMessage.lastBlockIncomplete()) { endIndex = receivedReceipts.size() - 1; lastBlockPartialReceipts = receivedReceipts.getLast(); } else { @@ -142,10 +170,6 @@ private List> completeFirstBlock( return receivedReceipts; } - if (receivedReceipts.isEmpty()) { - throw new InvalidPeerTaskResponseException("No result returned"); - } - // add new receipts to the already present ones final List cumulativeReceiptsForFirstBlock = new ArrayList<>( @@ -189,20 +213,55 @@ public PeerTaskValidationResponse validateResult(final Response result) { } if (!result.lastBlockPartialReceipts().isEmpty()) { - final SyncBlock lastBlockReceived = - request.blocks.get(result.completeReceiptsByBlock().size()); - if (result.lastBlockPartialReceipts().size() - > lastBlockReceived.getBody().getTransactionCount()) { - return PeerTaskValidationResponse.TOO_MANY_RESULTS_RETURNED; - } + final PeerTaskValidationResponse tooManyResultsReturned = validatePartialReceiptList(result); + if (tooManyResultsReturned != null) return tooManyResultsReturned; } return PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD; } + private @Nullable PeerTaskValidationResponse validatePartialReceiptList(final Response result) { + final SyncBlock lastBlockReceived = request.blocks.get(result.completeReceiptsByBlock().size()); + final List lastBlockPartialReceipts = result.lastBlockPartialReceipts(); + + if (lastBlockPartialReceipts.size() > lastBlockReceived.getBody().getTransactionCount()) { + return PeerTaskValidationResponse.TOO_MANY_RESULTS_RETURNED; + } + + final long txGasLimitUpperBound = calculateTxGasLimitUpperBound(lastBlockReceived); + + // check that each receipt in partial list is within size bounds + long cumulativeReceiptSize = 0; + for (int i = 0; i < lastBlockPartialReceipts.size(); i++) { + final SyncTransactionReceipt receipt = lastBlockPartialReceipts.get(i); + final long receiptSize = receipt.getRlpBytes().size(); + if (receiptSize > txGasLimitUpperBound / 8) { + return PeerTaskValidationResponse.INVALID_RECEIPT_RETURNED; + } + cumulativeReceiptSize += receiptSize; + if (cumulativeReceiptSize > lastBlockReceived.getHeader().getGasLimit() / 8) { + return PeerTaskValidationResponse.INVALID_RECEIPT_RETURNED; + } + } + return null; + } + + private long calculateTxGasLimitUpperBound(final SyncBlock lastBlockReceived) { + // to avoid having to deserialize the tx to get the actual gas limit we approximate it + // giving an upper bound, for everything before Osaka we use the block gas limit of 45M + // for Osaka onward we can use the max gas limit allowed per tx as specified by the protocol + // schedule + return Math.min( + protocolSchedule + .getByBlockHeader(lastBlockReceived.getHeader()) + .getGasLimitCalculator() + .transactionGasLimitCap(), + 45_000_000L); + } + private boolean receiptsRootMatches( final BlockHeader blockHeader, final List receipts) { - final var calculatedReceiptsRoot = + final Hash calculatedReceiptsRoot = Util.getRootFromListOfBytes( receipts.stream() .map( diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java new file mode 100644 index 00000000000..34f98a69c69 --- /dev/null +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java @@ -0,0 +1,75 @@ +/* + * 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.eth.messages; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; +import org.hyperledger.besu.ethereum.rlp.RLPInput; + +import java.util.List; + +import org.apache.tuweni.bytes.Bytes; + +public final class GetPaginatedReceiptsMessage extends GetReceiptsMessage { + private int firstBlockReceiptIndex; + + private GetPaginatedReceiptsMessage(final Bytes data) { + super(data); + deserialize(data); + } + + private GetPaginatedReceiptsMessage( + final Bytes data, final List blockHashes, final int firstBlockReceiptIndex) { + super(data, blockHashes); + this.firstBlockReceiptIndex = firstBlockReceiptIndex; + } + + public static GetPaginatedReceiptsMessage readFrom(final MessageData message) { + if (message instanceof GetPaginatedReceiptsMessage) { + return (GetPaginatedReceiptsMessage) message; + } + final int code = message.getCode(); + if (code != EthProtocolMessages.GET_RECEIPTS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a GetReceipts.", code)); + } + return new GetPaginatedReceiptsMessage(message.getData()); + } + + public static GetPaginatedReceiptsMessage create( + final List blockHashes, final int firstBlockReceiptIndex) { + final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); + tmp.writeIntScalar(firstBlockReceiptIndex); + tmp.startList(); + blockHashes.forEach(hash -> tmp.writeBytes(hash.getBytes())); + tmp.endList(); + return new GetPaginatedReceiptsMessage(tmp.encoded(), blockHashes, firstBlockReceiptIndex); + } + + public int firstBlockReceiptIndex() { + return firstBlockReceiptIndex; + } + + @Override + protected void deserialize(final Bytes data) { + final RLPInput input = new BytesValueRLPInput(data, false); + + this.firstBlockReceiptIndex = input.readIntScalar(); + + deserializeBlockHashList(input); + } +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java index f3bca1f2fc4..053430b7650 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java @@ -26,9 +26,18 @@ import org.apache.tuweni.bytes.Bytes; -public final class GetReceiptsMessage extends AbstractMessageData { +public class GetReceiptsMessage extends AbstractMessageData { private List blockHashes; - private int firstBlockReceiptIndex; + + protected GetReceiptsMessage(final Bytes data) { + super(data); + deserialize(data); + } + + protected GetReceiptsMessage(final Bytes data, final List blockHashes) { + super(data); + this.blockHashes = blockHashes; + } public static GetReceiptsMessage readFrom(final MessageData message) { if (message instanceof GetReceiptsMessage) { @@ -43,31 +52,11 @@ public static GetReceiptsMessage readFrom(final MessageData message) { } public static GetReceiptsMessage create(final List blockHashes) { - return create(blockHashes, -1); - } - - public static GetReceiptsMessage create( - final List blockHashes, final int firstBlockReceiptIndex) { final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); - if (firstBlockReceiptIndex >= 0) { - tmp.writeIntScalar(firstBlockReceiptIndex); - } tmp.startList(); blockHashes.forEach(hash -> tmp.writeBytes(hash.getBytes())); tmp.endList(); - return new GetReceiptsMessage(tmp.encoded(), blockHashes, firstBlockReceiptIndex); - } - - private GetReceiptsMessage(final Bytes data) { - super(data); - deserialize(data); - } - - private GetReceiptsMessage( - final Bytes data, final List blockHashes, final int firstBlockReceiptIndex) { - super(data); - this.blockHashes = blockHashes; - this.firstBlockReceiptIndex = firstBlockReceiptIndex; + return new GetReceiptsMessage(tmp.encoded(), blockHashes); } @Override @@ -79,15 +68,13 @@ public List blockHashes() { return blockHashes; } - public int firstBlockReceiptIndex() { - return firstBlockReceiptIndex; - } - - private void deserialize(final Bytes data) { + protected void deserialize(final Bytes data) { final RLPInput input = new BytesValueRLPInput(data, false); - this.firstBlockReceiptIndex = input.nextIsList() ? -1 : input.readIntScalar(); + deserializeBlockHashList(input); + } + protected void deserializeBlockHashList(final RLPInput input) { this.blockHashes = new ArrayList<>(input.enterList()); while (!input.isEndOfCurrentList()) { blockHashes.add(Hash.wrap(input.readBytes32())); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessage.java new file mode 100644 index 00000000000..e03b758814c --- /dev/null +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessage.java @@ -0,0 +1,62 @@ +/* + * 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.eth.messages; + +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; +import org.hyperledger.besu.ethereum.rlp.RLPInput; + +import org.apache.tuweni.bytes.Bytes; + +public final class PaginatedReceiptsMessage extends ReceiptsMessage { + private Boolean lastBlockIncomplete; + + private PaginatedReceiptsMessage(final Bytes data, final Boolean lastBlockIncomplete) { + super(data); + this.lastBlockIncomplete = lastBlockIncomplete; + } + + public static PaginatedReceiptsMessage readFrom(final MessageData message) { + if (message instanceof PaginatedReceiptsMessage) { + return (PaginatedReceiptsMessage) message; + } + final int code = message.getCode(); + if (code != EthProtocolMessages.RECEIPTS) { + throw new IllegalArgumentException( + String.format("Message has code %d and thus is not a ReceiptsMessage.", code)); + } + + return new PaginatedReceiptsMessage(message.getData(), null); + } + + public static PaginatedReceiptsMessage createUnsafe( + final Bytes data, final boolean lastBlockIncomplete) { + return new PaginatedReceiptsMessage(data, lastBlockIncomplete); + } + + public boolean lastBlockIncomplete() { + if (lastBlockIncomplete == null) { + deserialize(); + } + return lastBlockIncomplete; + } + + @Override + protected void deserialize() { + final RLPInput input = new BytesValueRLPInput(data, false); + lastBlockIncomplete = input.readLongScalar() == 1; + deserializeReceiptLists(input); + } +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java index a5b31188f9d..70350fc3ff1 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/ReceiptsMessage.java @@ -15,28 +15,18 @@ package org.hyperledger.besu.ethereum.eth.messages; import org.hyperledger.besu.ethereum.core.SyncTransactionReceipt; -import org.hyperledger.besu.ethereum.core.TransactionReceipt; import org.hyperledger.besu.ethereum.core.encoding.receipt.SyncTransactionReceiptDecoder; -import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptDecoder; -import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; -import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.AbstractMessageData; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; import org.hyperledger.besu.ethereum.rlp.BytesValueRLPInput; -import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; import org.hyperledger.besu.ethereum.rlp.RLPInput; import java.util.ArrayList; import java.util.List; -import java.util.Optional; -import java.util.function.Function; -import javax.annotation.concurrent.NotThreadSafe; -import com.google.common.annotations.VisibleForTesting; import org.apache.tuweni.bytes.Bytes; -@NotThreadSafe -public final class ReceiptsMessage extends AbstractMessageData { +public class ReceiptsMessage extends AbstractMessageData { /** * This default decoder instance is used for performance reasons to avoid creating a new decoder * for every ReceiptsMessage @@ -44,21 +34,10 @@ public final class ReceiptsMessage extends AbstractMessageData { private static final SyncTransactionReceiptDecoder DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER = new SyncTransactionReceiptDecoder(); - private final SyncTransactionReceiptDecoder syncTransactionReceiptDecoder; - private List> receiptsByBlock; private List> syncReceiptsByBlock; - private Optional lastBlockIncomplete; - private ReceiptsMessage( - final Bytes data, - final SyncTransactionReceiptDecoder syncTransactionReceiptDecoder, - final List> receipts, - final Boolean lastBlockIncomplete) { + protected ReceiptsMessage(final Bytes data) { super(data); - this.syncTransactionReceiptDecoder = syncTransactionReceiptDecoder; - this.receiptsByBlock = receipts; - this.lastBlockIncomplete = - (lastBlockIncomplete != null) ? Optional.of(lastBlockIncomplete) : null; } public static ReceiptsMessage readFrom(final MessageData message) { @@ -70,25 +49,7 @@ public static ReceiptsMessage readFrom(final MessageData message) { throw new IllegalArgumentException( String.format("Message has code %d and thus is not a ReceiptsMessage.", code)); } - return new ReceiptsMessage( - message.getData(), DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER, null, null); - } - - @VisibleForTesting - public static ReceiptsMessage create( - final List> receipts, - final TransactionReceiptEncodingConfiguration encodingConfiguration) { - final BytesValueRLPOutput tmp = new BytesValueRLPOutput(); - tmp.startList(); - receipts.forEach( - (receiptSet) -> { - tmp.startList(); - receiptSet.forEach(r -> TransactionReceiptEncoder.writeTo(r, tmp, encodingConfiguration)); - tmp.endList(); - }); - tmp.endList(); - return new ReceiptsMessage( - tmp.encoded(), DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER, receipts, null); + return new ReceiptsMessage(message.getData()); } /** @@ -99,12 +60,7 @@ public static ReceiptsMessage create( * @return A new ReceiptsMessage */ public static ReceiptsMessage createUnsafe(final Bytes data) { - return new ReceiptsMessage(data, DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER, null, null); - } - - public static ReceiptsMessage createUnsafe(final Bytes data, final boolean lastBlockIncomplete) { - return new ReceiptsMessage( - data, DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER, null, lastBlockIncomplete); + return new ReceiptsMessage(data); } @Override @@ -112,57 +68,32 @@ public int getCode() { return EthProtocolMessages.RECEIPTS; } - public List> receipts() { - if (receiptsByBlock == null) { - receiptsByBlock = - deserialize( - getData(), rlpInput -> TransactionReceiptDecoder.readFrom(rlpInput, false), false); - } - return receiptsByBlock; - } - public List> syncReceipts() { if (syncReceiptsByBlock == null) { - syncReceiptsByBlock = - deserialize( - getData(), - rlpInput -> - syncTransactionReceiptDecoder.decode( - rlpInput.nextIsList() ? rlpInput.currentListAsBytes() : rlpInput.readBytes()), - false); + deserialize(); } return syncReceiptsByBlock; } - public Optional lastBlockIncomplete() { - if (lastBlockIncomplete == null) { - deserialize(getData(), null, true); - } - return lastBlockIncomplete; - } - - private List> deserialize( - final Bytes data, - final Function deserializer, - final boolean onlyLastBlockIncomplete) { + protected void deserialize() { final RLPInput input = new BytesValueRLPInput(data, false); - this.lastBlockIncomplete = - input.nextIsList() ? Optional.empty() : Optional.of(input.readLongScalar() == 1); - if (onlyLastBlockIncomplete) { - return null; - } + deserializeReceiptLists(input); + } - final List> receiptsByBlock = new ArrayList<>(input.enterList()); + protected void deserializeReceiptLists(final RLPInput input) { + final List> receiptsByBlock = new ArrayList<>(input.enterList()); while (input.nextIsList()) { final int setSize = input.enterList(); - final List receiptSet = new ArrayList<>(setSize); + final List receiptSet = new ArrayList<>(setSize); for (int i = 0; i < setSize; i++) { - receiptSet.add(deserializer.apply(input)); + receiptSet.add( + DEFAULT_SYNC_TRANSACTION_RECEIPT_DECODER.decode( + input.nextIsList() ? input.currentListAsBytes() : input.readBytes())); } input.leaveList(); receiptsByBlock.add(receiptSet); } input.leaveList(); - return receiptsByBlock; + syncReceiptsByBlock = receiptsByBlock; } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java index bb0bfce03c3..3571f054c19 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManagerTest.java @@ -945,7 +945,7 @@ public void respondToGetReceipts() throws ExecutionException, InterruptedExcepti // Run test final PeerConnection peer = setupPeer(ethManager, onSend); - ethManager.processMessage(EthProtocol.LATEST, new DefaultMessage(peer, messageData)); + ethManager.processMessage(EthProtocol.ETH69, new DefaultMessage(peer, messageData)); done.get(); } } @@ -999,7 +999,7 @@ public void respondToGetReceiptsWithinLimits() throws ExecutionException, Interr // Run test final PeerConnection peer = setupPeer(ethManager, onSend); - ethManager.processMessage(EthProtocol.LATEST, new DefaultMessage(peer, messageData)); + ethManager.processMessage(EthProtocol.ETH69, new DefaultMessage(peer, messageData)); done.get(); } } @@ -1045,7 +1045,7 @@ public void respondToGetReceiptsPartial() throws ExecutionException, Interrupted // Run test final PeerConnection peer = setupPeer(ethManager, onSend); - ethManager.processMessage(EthProtocol.LATEST, new DefaultMessage(peer, messageData)); + ethManager.processMessage(EthProtocol.ETH69, new DefaultMessage(peer, messageData)); done.get(); } } @@ -1294,8 +1294,8 @@ public void transactionMessagesGoToTheCorrectExecutor() { @Test public void shouldUseRightCapabilityDependingOnSyncMode() { - assertHighestCapability(SyncMode.SNAP, EthProtocol.ETH69); - assertHighestCapability(SyncMode.FULL, EthProtocol.ETH69); + assertHighestCapability(SyncMode.SNAP, EthProtocol.LATEST); + assertHighestCapability(SyncMode.FULL, EthProtocol.LATEST); } @Test diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java index 877c640dbc5..977977326b4 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java @@ -261,7 +261,7 @@ public void shouldLimitTxReceiptsByMessageSize() { serializeReceiptsList( expectedResults, TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION)); - final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.LATEST); + final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.ETH69); assertThat(result).contains(expectedMsg); } @@ -285,7 +285,7 @@ public void shouldLimitTxReceiptsByCount() { serializeReceiptsList( expectedResults, TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION)); - final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.LATEST); + final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.ETH69); assertThat(result).contains(expectedMsg); } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java index 1d733f342a0..f8da0991440 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTaskTest.java @@ -14,16 +14,20 @@ */ package org.hyperledger.besu.ethereum.eth.manager.peertask.task; +import static java.util.Collections.emptyList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hyperledger.besu.ethereum.eth.core.Utils.receiptToSyncReceipt; import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.reset; import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.GasLimitCalculator; import org.hyperledger.besu.ethereum.core.BlockHeader; import org.hyperledger.besu.ethereum.core.Difficulty; import org.hyperledger.besu.ethereum.core.SyncBlock; @@ -41,7 +45,10 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.InvalidPeerTaskResponseException; import org.hyperledger.besu.ethereum.eth.manager.peertask.MalformedRlpFromPeerException; import org.hyperledger.besu.ethereum.eth.manager.peertask.PeerTaskValidationResponse; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Request; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Response; import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages; +import org.hyperledger.besu.ethereum.eth.messages.GetPaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.mainnet.BodyValidation; @@ -49,30 +56,42 @@ import org.hyperledger.besu.ethereum.mainnet.ProtocolSpec; import org.hyperledger.besu.ethereum.p2p.rlpx.connections.PeerConnection; import org.hyperledger.besu.ethereum.p2p.rlpx.wire.Capability; +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.SimpleNoCopyRlpEncoder; -import java.util.Collections; +import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Optional; import java.util.Set; +import java.util.stream.IntStream; +import java.util.stream.Stream; import org.apache.commons.lang3.StringUtils; +import org.apache.tuweni.bytes.Bytes; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; import org.junit.jupiter.params.provider.ValueSource; import org.mockito.Mockito; public class GetSyncReceiptsFromPeerTaskTest { - private static final Set AGREED_CAPABILITIES = Set.of(EthProtocol.ETH69); + private static final Set AGREED_CAPABILITIES_ETH69 = Set.of(EthProtocol.ETH69); + private static final Set AGREED_CAPABILITIES_LATEST = Set.of(EthProtocol.LATEST); private static ProtocolSchedule protocolSchedule; @BeforeAll public static void setup() { protocolSchedule = mock(ProtocolSchedule.class); final ProtocolSpec protocolSpec = mock(ProtocolSpec.class); + final GasLimitCalculator gasLimitCalculator = mock(GasLimitCalculator.class); + when(gasLimitCalculator.transactionGasLimitCap()).thenReturn(Long.MAX_VALUE); + when(protocolSpec.getGasLimitCalculator()).thenReturn(gasLimitCalculator); when(protocolSpec.isPoS()).thenReturn(false); when(protocolSchedule.getByBlockHeader(Mockito.any())).thenReturn(protocolSpec); when(protocolSchedule.anyMatch(Mockito.any())).thenReturn(false); @@ -80,133 +99,227 @@ public static void setup() { @Test public void testGetSubProtocol() { - final BlockHeader blockHeader = mockBlockHeader(1); - final TransactionReceipt receipt = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader.getReceiptsRoot()).thenReturn(BodyValidation.receiptsRoot(List.of(receipt))); - final SyncBlockBody syncBlockBody = mock(SyncBlockBody.class); - when(syncBlockBody.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock = new SyncBlock(blockHeader, syncBlockBody); - - final var task = createTask(List.of(syncBlock), protocolSchedule); + final var task = + createTask(new Request(List.of(mockBlock(1, 1).block), List.of()), protocolSchedule); assertEquals(EthProtocol.get(), task.getSubProtocol()); } @Test - public void testGetRequestMessage() { - final BlockHeader blockHeader1 = mockBlockHeader(1); - final TransactionReceipt receipt1_forBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - final TransactionReceipt receipt2_forBlock1 = - new TransactionReceipt(1, 321, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receipt1_forBlock1, receipt2_forBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(2); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); - - BlockHeader blockHeader2 = mockBlockHeader(2); - TransactionReceipt receiptForBlock2 = - new TransactionReceipt(1, 456, Collections.emptyList(), Optional.empty()); - when(blockHeader2.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock2))); - final SyncBlockBody syncBlockBody2 = mock(SyncBlockBody.class); - when(syncBlockBody2.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock2 = new SyncBlock(blockHeader2, syncBlockBody2); - - BlockHeader blockHeader3 = mockBlockHeader(3); - TransactionReceipt receiptForBlock3 = - new TransactionReceipt(1, 789, Collections.emptyList(), Optional.empty()); - when(blockHeader3.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock3))); - final SyncBlockBody syncBlockBody3 = mock(SyncBlockBody.class); - when(syncBlockBody3.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock3 = new SyncBlock(blockHeader3, syncBlockBody3); - - final List blocks = List.of(syncBlock1, syncBlock2, syncBlock3); - - final var task = createTask(blocks, protocolSchedule); - - final var messageData = task.getRequestMessage(AGREED_CAPABILITIES); - final var getReceiptsMessage = GetReceiptsMessage.readFrom(messageData); + public void testGetRequestMessageETH69() { + final List mockedBlocks = + List.of(mockBlock(1, 2), mockBlock(2, 1), mockBlock(3, 1)); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(mockedBlocks.stream().map(MockedBlock::block).toList(), List.of()), + protocolSchedule); + + final MessageData messageData = task.getRequestMessage(AGREED_CAPABILITIES_ETH69); + final GetReceiptsMessage getReceiptsMessage = GetReceiptsMessage.readFrom(messageData); assertEquals(EthProtocolMessages.GET_RECEIPTS, getReceiptsMessage.getCode()); - List hashesInMessage = getReceiptsMessage.hashes(); - List expectedHashes = blocks.stream().map(SyncBlock::getHash).toList(); + final List hashesInMessage = getReceiptsMessage.blockHashes(); + final List expectedHashes = + mockedBlocks.stream() + .map(MockedBlock::block) + .map(SyncBlock::getHeader) + .map(BlockHeader::getHash) + .toList(); assertThat(expectedHashes).containsExactlyElementsOf(hashesInMessage); } @Test - public void testParseResponseWithNullResponseMessage() { - final BlockHeader blockHeader = mockBlockHeader(1); - final TransactionReceipt receipt = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader.getReceiptsRoot()).thenReturn(BodyValidation.receiptsRoot(List.of(receipt))); - final SyncBlockBody syncBlockBody = mock(SyncBlockBody.class); - when(syncBlockBody.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock = new SyncBlock(blockHeader, syncBlockBody); + public void testGetRequestMessageLatest() { + final List mockedBlocks = + List.of(mockBlock(1, 2), mockBlock(2, 1), mockBlock(3, 1)); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request( + mockedBlocks.stream().map(MockedBlock::block).toList(), + List.of(toResponseReceipt(mockedBlocks.getFirst().receipts.getFirst()))), + protocolSchedule); + + final MessageData messageData = task.getRequestMessage(AGREED_CAPABILITIES_LATEST); + final GetPaginatedReceiptsMessage getReceiptsMessage = + GetPaginatedReceiptsMessage.readFrom(messageData); + + assertEquals(EthProtocolMessages.GET_RECEIPTS, getReceiptsMessage.getCode()); - final var task = createTask(List.of(syncBlock), protocolSchedule); - Assertions.assertThrows( - InvalidPeerTaskResponseException.class, - () -> task.processResponse(null, AGREED_CAPABILITIES)); + final List hashesInMessage = getReceiptsMessage.blockHashes(); + final List expectedHashes = + mockedBlocks.stream() + .map(MockedBlock::block) + .map(SyncBlock::getHeader) + .map(BlockHeader::getHash) + .toList(); + + assertThat(expectedHashes).containsExactlyElementsOf(hashesInMessage); + + assertThat(getReceiptsMessage.firstBlockReceiptIndex()).isEqualTo(1); + } + + @Test + public void testParseResponseWithNullResponseMessage() { + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(mockBlock(1, 2).block), List.of()), protocolSchedule); + assertThrows( + InvalidPeerTaskResponseException.class, () -> task.processResponse(null, Set.of())); } @Test public void testParseResponse() throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { - BlockHeader blockHeader1 = mockBlockHeader(1); - TransactionReceipt receiptForBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); - - BlockHeader blockHeader2 = mockBlockHeader(2); - TransactionReceipt receiptForBlock2 = - new TransactionReceipt(1, 456, Collections.emptyList(), Optional.empty()); - when(blockHeader2.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock2))); - final SyncBlockBody syncBlockBody2 = mock(SyncBlockBody.class); - when(syncBlockBody2.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock2 = new SyncBlock(blockHeader2, syncBlockBody2); - - BlockHeader blockHeader3 = mockBlockHeader(3); - TransactionReceipt receiptForBlock3 = - new TransactionReceipt(1, 789, Collections.emptyList(), Optional.empty()); - when(blockHeader3.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock3))); - final SyncBlockBody syncBlockBody3 = mock(SyncBlockBody.class); - when(syncBlockBody3.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock3 = new SyncBlock(blockHeader3, syncBlockBody3); - - final var task = createTask(List.of(syncBlock1, syncBlock2, syncBlock3), protocolSchedule); - - ReceiptsMessage receiptsMessage = + final List mockedBlocks = + List.of(mockBlock(1, 1), mockBlock(2, 1), mockBlock(3, 1), mockBlock(4, 0)); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(mockedBlocks.stream().map(MockedBlock::block).toList(), List.of()), + protocolSchedule); + + final ReceiptsMessage receiptsMessage = ReceiptsMessage.createUnsafe( serializeReceiptsList( - List.of( - List.of(receiptForBlock1), - List.of(receiptForBlock2), - List.of(receiptForBlock3)), + mockedBlocks.stream().map(MockedBlock::receipts).toList(), TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); + final MessageData rawMsg = + new RawMessage(EthProtocolMessages.RECEIPTS, receiptsMessage.getData()); + + final Response response = task.processResponse(rawMsg, AGREED_CAPABILITIES_ETH69); + + assertThat(response.completeReceiptsByBlock().values()) + .usingElementComparator(this::receiptsComparator) + .containsExactlyInAnyOrder( + toResponseReceipts(mockedBlocks.get(0).receipts), + toResponseReceipts(mockedBlocks.get(1).receipts), + toResponseReceipts(mockedBlocks.get(2).receipts), + toResponseReceipts(mockedBlocks.get(3).receipts)); + } + + /** Builds a MessageData in eth/70 wire format: {@code } */ + private MessageData buildEth70ReceiptsMessage( + final List> receiptsByBlock, final boolean lastBlockIncomplete) { + final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); + rlp.writeLongScalar(lastBlockIncomplete ? 1 : 0); + final Bytes serializedReceiptsList = + serializeReceiptsList( + receiptsByBlock, TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION); + return new RawMessage( + EthProtocolMessages.RECEIPTS, Bytes.concatenate(rlp.encoded(), serializedReceiptsList)); + } + + @Test + public void testParseResponseWithEth70PaginatedLastBlockIncomplete() + throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { + // Block 1 has 2 receipts (complete), block 2 has 3 receipts but only 1 is returned (partial) + final MockedBlock block1 = mockBlock(1, 2); + final MockedBlock block2 = mockBlock(2, 3); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(block1.block, block2.block), List.of()), protocolSchedule); + + // Server returns block1 fully and 1 receipt from block2 (lastBlockIncomplete=true) + final MessageData receiptsMessage = + buildEth70ReceiptsMessage( + List.of(block1.receipts, List.of(block2.receipts.getFirst())), true); + + final Response response = task.processResponse(receiptsMessage, AGREED_CAPABILITIES_LATEST); + + // Block1 is complete → in completeReceiptsByBlock + assertThat(response.completeReceiptsByBlock()).hasSize(1); + assertThat(response.completeReceiptsByBlock()).containsKey(block1.block); + + // Block2 is partial → in lastBlockPartialReceipts + assertThat(response.lastBlockPartialReceipts()).hasSize(1); + assertThat(response.lastBlockPartialReceipts().getFirst().getRlpBytes()) + .isEqualTo(toResponseReceipt(block2.receipts.getFirst()).getRlpBytes()); + } + + @Test + public void testParseResponseWithEth70AllReceiptsCompleteLastBlockIncompleteFalse() + throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { + final MockedBlock block1 = mockBlock(1, 1); + final MockedBlock block2 = mockBlock(2, 2); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(block1.block, block2.block), List.of()), protocolSchedule); + + // Server returns both blocks fully (lastBlockIncomplete=false) + final MessageData receiptsMessage = + buildEth70ReceiptsMessage(List.of(block1.receipts, block2.receipts), false); + + final Response response = task.processResponse(receiptsMessage, AGREED_CAPABILITIES_LATEST); + + assertThat(response.lastBlockPartialReceipts()).isEmpty(); + assertThat(response.completeReceiptsByBlock()).containsOnlyKeys(block1.block, block2.block); + } + + @Test + public void testParseResponseCombinesPartialReceiptsFromPreviousRequest() + throws InvalidPeerTaskResponseException, MalformedRlpFromPeerException { + // Block has 3 receipts; previous request delivered receipts[0], now we get receipts[1..2] + final MockedBlock block = mockBlock(1, 3); + + final List alreadyFetched = + List.of(toResponseReceipt(block.receipts.getFirst())); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(block.block), alreadyFetched), protocolSchedule); + + // Server returns receipts[1..2] (eth/70 format, lastBlockIncomplete=false) + final MessageData receiptsMessage = + buildEth70ReceiptsMessage(List.of(block.receipts.subList(1, 3)), false); + + final Response response = task.processResponse(receiptsMessage, AGREED_CAPABILITIES_LATEST); + + // completeFirstBlock() should prepend alreadyFetched and produce all 3 receipts + assertThat(response.lastBlockPartialReceipts()).isEmpty(); + assertThat(response.completeReceiptsByBlock()).containsKey(block.block); + assertThat(response.completeReceiptsByBlock().get(block.block)).hasSize(3); + } + + @Test + public void testParseResponseWithEth70LastBlockIncompleteTrueAndEmptyListThrows() { + final MockedBlock block = mockBlock(1, 2); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(block.block), List.of()), protocolSchedule); + + // Malicious server sends lastBlockIncomplete=1 but empty receipt list + final MessageData receiptsMessage = buildEth70ReceiptsMessage(List.of(), true); + + assertThatThrownBy(() -> task.processResponse(receiptsMessage, AGREED_CAPABILITIES_LATEST)) + .isInstanceOf(InvalidPeerTaskResponseException.class); + } + + @Test + public void testParseResponseFailsWhenReceiptsForTooManyBlocksAreReturned() { + final List mockedBlocks = + List.of(mockBlock(1, 1), mockBlock(2, 1), mockBlock(3, 1)); - final var response = task.processResponse(receiptsMessage, AGREED_CAPABILITIES); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(mockedBlocks.stream().map(MockedBlock::block).toList(), List.of()), + protocolSchedule); - assertThat(response).hasSize(3); - assertThat(response.get(syncBlock1)) - .usingElementComparator(Utils::compareSyncReceipts) - .containsExactly(toResponseReceipt(receiptForBlock1)); - assertThat(response.get(syncBlock2)) - .usingElementComparator(Utils::compareSyncReceipts) - .containsExactly(toResponseReceipt(receiptForBlock2)); - assertThat(response.get(syncBlock3)) - .usingElementComparator(Utils::compareSyncReceipts) - .containsExactly(toResponseReceipt(receiptForBlock3)); + final MockedBlock extraMockedBlock = mockBlock(4, 1); + + final ReceiptsMessage receiptsMessage = + ReceiptsMessage.createUnsafe( + serializeReceiptsList( + Stream.concat(mockedBlocks.stream(), Stream.of(extraMockedBlock)) + .map(MockedBlock::receipts) + .toList(), + TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); + final MessageData rawMsg = + new RawMessage(EthProtocolMessages.RECEIPTS, receiptsMessage.getData()); + + assertThatThrownBy(() -> task.processResponse(rawMsg, AGREED_CAPABILITIES_ETH69)) + .isInstanceOf(InvalidPeerTaskResponseException.class) + .hasMessageContaining("Too many result returned"); } @ParameterizedTest @@ -214,26 +327,12 @@ public void testParseResponse() public void testGetPeerRequirementFilter(final boolean isPoS) { reset(protocolSchedule); when(protocolSchedule.anyMatch(any())).thenReturn(isPoS); + final List mockedBlocks = List.of(mockBlock(1, 1), mockBlock(2, 1)); - BlockHeader blockHeader1 = mockBlockHeader(1); - TransactionReceipt receiptForBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); - - BlockHeader blockHeader2 = mockBlockHeader(2); - TransactionReceipt receiptForBlock2 = - new TransactionReceipt(1, 456, Collections.emptyList(), Optional.empty()); - when(blockHeader2.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock2))); - final SyncBlockBody syncBlockBody2 = mock(SyncBlockBody.class); - when(syncBlockBody2.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock2 = new SyncBlock(blockHeader2, syncBlockBody2); - - final var task = createTask(List.of(syncBlock1, syncBlock2), protocolSchedule); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(mockedBlocks.stream().map(MockedBlock::block).toList(), List.of()), + protocolSchedule); EthPeer failForShortChainHeight = mockPeer(1); EthPeer successfulCandidate = mockPeer(5); @@ -246,148 +345,207 @@ public void testGetPeerRequirementFilter(final boolean isPoS) { task.getPeerRequirementFilter().test(EthPeerImmutableAttributes.from(successfulCandidate))); } + @ParameterizedTest + @ValueSource(booleans = {false, true}) + public void validateResultFailsWhenNoResultAreReturned( + final boolean hasFirstBlockPartialReceipts) { + final MockedBlock mockedBlock = mockBlock(1, 1); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request( + List.of(mockedBlock.block), + hasFirstBlockPartialReceipts + ? toResponseReceipts(mockedBlock.receipts) + : emptyList()), + protocolSchedule); + + assertEquals( + PeerTaskValidationResponse.NO_RESULTS_RETURNED, + task.validateResult(new Response(Map.of(), List.of()))); + } + + static List validateResultProvider() { + return List.of( + Arguments.of(false, false), + Arguments.of(false, true), + Arguments.of(true, false), + Arguments.of(true, true)); + } + + @ParameterizedTest + @MethodSource("validateResultProvider") + public void testValidateResultForFullSuccess( + final boolean hasFirstBlockPartialReceipts, final boolean lastBlockIncomplete) { + final MockedBlock mockedBlock = mockBlock(1, 1); + final MockedBlock lastMockedBlock = mockBlock(2, 3); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request( + List.of(mockedBlock.block, lastMockedBlock.block), + hasFirstBlockPartialReceipts + ? List.of(toResponseReceipt(lastMockedBlock.receipts.getFirst())) + : emptyList()), + protocolSchedule); + + final List expectedLastBlockPartialReceipts = + lastBlockIncomplete + ? toResponseReceipts(lastMockedBlock.receipts).subList(0, 1) + : List.of(); + + final Map> expectedCompletedBlocks = new HashMap<>(); + expectedCompletedBlocks.put(mockedBlock.block, toResponseReceipts(mockedBlock.receipts)); + if (!lastBlockIncomplete) { + expectedCompletedBlocks.put( + lastMockedBlock.block, toResponseReceipts(lastMockedBlock.receipts)); + } + + assertEquals( + PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD, + task.validateResult( + new Response(expectedCompletedBlocks, expectedLastBlockPartialReceipts))); + } + @Test - public void validateResultFailsWhenNoResultAreReturned() { - final BlockHeader blockHeader = mockBlockHeader(1); - final TransactionReceipt receiptForBlock = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock))); - final SyncBlockBody syncBlockBody = mock(SyncBlockBody.class); - when(syncBlockBody.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock = new SyncBlock(blockHeader, syncBlockBody); + public void validateResultFailsReceiptRootDoesNotMatch() { + final MockedBlock mockedRequestedBlock = mockBlock(1, 1); + + final GetSyncReceiptsFromPeerTask task = + createTask(new Request(List.of(mockedRequestedBlock.block), List.of()), protocolSchedule); - final var task = createTask(List.of(syncBlock), protocolSchedule); + final List anotherBlockReceipts = mockBlock(2, 1).receipts; - assertEquals(PeerTaskValidationResponse.NO_RESULTS_RETURNED, task.validateResult(Map.of())); + assertEquals( + PeerTaskValidationResponse.RESULTS_DO_NOT_MATCH_QUERY, + task.validateResult( + new Response( + // for the requested block, receipts returned are from another block + Map.of(mockedRequestedBlock.block, toResponseReceipts(anotherBlockReceipts)), + List.of()))); } @Test - public void testValidateResultForFullSuccess() { - final BlockHeader blockHeader = mockBlockHeader(1); - final TransactionReceipt receiptForBlock = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock))); - final SyncBlockBody syncBlockBody = mock(SyncBlockBody.class); - when(syncBlockBody.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock = new SyncBlock(blockHeader, syncBlockBody); - - final var task = createTask(List.of(syncBlock), protocolSchedule); + public void validateResultSuccessWhenPartialBlockIsIncomplete() { + final MockedBlock mockedBlock = mockBlock(1, 3); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request( + List.of(mockedBlock.block), + List.of(toResponseReceipt(mockedBlock.receipts.getFirst()))), + protocolSchedule); assertEquals( PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD, - task.validateResult(Map.of(syncBlock, List.of(toResponseReceipt(receiptForBlock))))); + task.validateResult( + new Response(Map.of(), toResponseReceipts(mockedBlock.receipts).subList(0, 2)))); } @Test - public void testParseResponseForInvalidResponse() { - // Too many block-lists in the response (4 for 3 requested blocks) must be rejected at parse - // time by processResponse, not deferred to validateResult. - final BlockHeader blockHeader1 = mockBlockHeader(1); - final TransactionReceipt receiptForBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); - - final BlockHeader blockHeader2 = mockBlockHeader(2); - final TransactionReceipt receiptForBlock2 = - new TransactionReceipt(1, 456, Collections.emptyList(), Optional.empty()); - when(blockHeader2.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock2))); - final SyncBlockBody syncBlockBody2 = mock(SyncBlockBody.class); - when(syncBlockBody2.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock2 = new SyncBlock(blockHeader2, syncBlockBody2); - - final BlockHeader blockHeader3 = mockBlockHeader(3); - final TransactionReceipt receiptForBlock3 = - new TransactionReceipt(1, 789, Collections.emptyList(), Optional.empty()); - when(blockHeader3.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock3))); - final SyncBlockBody syncBlockBody3 = mock(SyncBlockBody.class); - when(syncBlockBody3.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock3 = new SyncBlock(blockHeader3, syncBlockBody3); - - final var task = createTask(List.of(syncBlock1, syncBlock2, syncBlock3), protocolSchedule); + public void validateResultFailsWhenPartialReceiptSizeExceedsTxGasLimitBound() { + // txGasLimitCap=800 → per-receipt threshold = 100 bytes; a 101-byte receipt must be rejected + final MockedBlock block = mockBlockWithGasLimit(1, 1, 30_000_000L); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(List.of(block.block), List.of()), + createProtocolScheduleWithTxGasLimitCap(800L)); - final ReceiptsMessage receiptsMessage = - ReceiptsMessage.createUnsafe( - serializeReceiptsList( - List.of( - List.of(receiptForBlock1), - List.of(receiptForBlock2), - List.of(receiptForBlock3), - List.of( - new TransactionReceipt( - 1, 101112, Collections.emptyList(), Optional.empty()))), - TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); + final SyncTransactionReceipt oversizedReceipt = + new SyncTransactionReceipt(Bytes.of(new byte[101])); - Assertions.assertThrows( - InvalidPeerTaskResponseException.class, - () -> task.processResponse(receiptsMessage, AGREED_CAPABILITIES)); + assertEquals( + PeerTaskValidationResponse.INVALID_RECEIPT_RETURNED, + task.validateResult(new Response(Map.of(), List.of(oversizedReceipt)))); } @Test - public void validateResultFailsWhenTooManyBlocksReturned() { - // A block with 1 transaction that receives 2 receipts must be rejected. - final BlockHeader blockHeader1 = mockBlockHeader(1); - final TransactionReceipt receiptForBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); - - final TransactionReceipt extraReceipt = - new TransactionReceipt(1, 321, Collections.emptyList(), Optional.empty()); - - final var task = createTask(List.of(syncBlock1), protocolSchedule); + public void validateResultFailsWhenCumulativePartialReceiptSizeExceedsBlockGasLimitBound() { + // blockGasLimit=800 → cumulative threshold = 100 bytes; two 60-byte receipts (total 120) must + // be rejected even though each individually is within the per-receipt bound + final MockedBlock block = mockBlockWithGasLimit(1, 2, 800L); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(List.of(block.block), List.of()), + createProtocolScheduleWithTxGasLimitCap(Long.MAX_VALUE)); + + final List partialReceipts = + List.of( + new SyncTransactionReceipt(Bytes.of(new byte[60])), + new SyncTransactionReceipt(Bytes.of(new byte[60]))); assertEquals( - PeerTaskValidationResponse.TOO_MANY_RESULTS_RETURNED, - task.validateResult( - Map.of( - syncBlock1, - List.of(toResponseReceipt(receiptForBlock1), toResponseReceipt(extraReceipt))))); + PeerTaskValidationResponse.INVALID_RECEIPT_RETURNED, + task.validateResult(new Response(Map.of(), partialReceipts))); } @Test - public void validateResultFailsReceiptRootDoesNotMatch() { - final BlockHeader blockHeader1 = mockBlockHeader(1); - final TransactionReceipt receiptForBlock1 = - new TransactionReceipt(1, 123, Collections.emptyList(), Optional.empty()); - when(blockHeader1.getReceiptsRoot()) - .thenReturn(BodyValidation.receiptsRoot(List.of(receiptForBlock1))); - final SyncBlockBody syncBlockBody1 = mock(SyncBlockBody.class); - when(syncBlockBody1.getTransactionCount()).thenReturn(1); - final SyncBlock syncBlock1 = new SyncBlock(blockHeader1, syncBlockBody1); + public void validateResultPassesWhenPartialReceiptSizesAreWithinBounds() { + // txGasLimitCap=800 → per-receipt threshold=100; blockGasLimit=800 → cumulative threshold=100 + // Two 40-byte receipts: each 40 < 100, cumulative 80 < 100 → valid + final MockedBlock block = mockBlockWithGasLimit(1, 2, 800L); + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(List.of(block.block), List.of()), + createProtocolScheduleWithTxGasLimitCap(800L)); + + final List partialReceipts = + List.of( + new SyncTransactionReceipt(Bytes.of(new byte[40])), + new SyncTransactionReceipt(Bytes.of(new byte[40]))); - final TransactionReceipt returnedReceiptForBlock1 = - new TransactionReceipt(1, 321, Collections.emptyList(), Optional.empty()); + assertEquals( + PeerTaskValidationResponse.RESULTS_VALID_AND_GOOD, + task.validateResult(new Response(Map.of(), partialReceipts))); + } - final var task = createTask(List.of(syncBlock1), protocolSchedule); + @Test + public void validateResultChecksAllPartialReceiptsEvenWhenFirstRequestBlockIsComplete() { + // Regression test: when the first block in the request becomes complete and the partial + // block is a later one, the size loop must start from index 0 (not + // firstBlockPartialReceipts.size()) so none of the later block's receipts are skipped. + final MockedBlock blockA = mockBlock(1, 1); + final MockedBlock blockB = mockBlockWithGasLimit(2, 1, 800L); + final SyncTransactionReceipt receiptForA = toResponseReceipt(blockA.receipts.getFirst()); + + final GetSyncReceiptsFromPeerTask task = + createTask( + new Request(List.of(blockA.block, blockB.block), List.of(receiptForA)), + createProtocolScheduleWithTxGasLimitCap(800L)); + + // blockA is complete (receipt root matches), blockB has a single oversized partial receipt + final SyncTransactionReceipt oversizedReceiptForB = + new SyncTransactionReceipt(Bytes.of(new byte[101])); assertEquals( - PeerTaskValidationResponse.RESULTS_DO_NOT_MATCH_QUERY, + PeerTaskValidationResponse.INVALID_RECEIPT_RETURNED, task.validateResult( - Map.of(syncBlock1, List.of(toResponseReceipt(returnedReceiptForBlock1))))); + new Response( + Map.of(blockA.block, List.of(receiptForA)), List.of(oversizedReceiptForB)))); } - private static BlockHeader mockBlockHeader(final long blockNumber) { + private static BlockHeader mockBlockHeader( + final long blockNumber, final List receipts) { BlockHeader blockHeader = mock(BlockHeader.class); when(blockHeader.getNumber()).thenReturn(blockNumber); - // second to last hex digit indicates the blockNumber, last hex digit indicates the usage of the - // hash + // second to last hex digit indicates the blockNumber, + // last hex digit indicates the usage of the hash when(blockHeader.getHash()) .thenReturn(Hash.fromHexString(StringUtils.repeat("00", 31) + blockNumber + "1")); + when(blockHeader.getReceiptsRoot()).thenReturn(BodyValidation.receiptsRoot(receipts)); + when(blockHeader.getGasLimit()).thenReturn(30_000_000L); return blockHeader; } + private static List mockTransactionReceipts( + final long blockNumber, final int count) { + + return IntStream.rangeClosed(1, count) + .mapToObj( + i -> new TransactionReceipt(1, 123L * i + blockNumber, emptyList(), Optional.empty())) + .toList(); + } + private EthPeer mockPeer(final long chainHeight) { EthPeer ethPeer = mock(EthPeer.class); ChainState chainState = mock(ChainState.class); @@ -402,12 +560,61 @@ private EthPeer mockPeer(final long chainHeight) { } private GetSyncReceiptsFromPeerTask createTask( - final List blocks, final ProtocolSchedule protocolSchedule) { + final Request request, final ProtocolSchedule protocolSchedule) { return new GetSyncReceiptsFromPeerTask( - blocks, protocolSchedule, new SyncTransactionReceiptEncoder(new SimpleNoCopyRlpEncoder())); + request, protocolSchedule, new SyncTransactionReceiptEncoder(new SimpleNoCopyRlpEncoder())); } private SyncTransactionReceipt toResponseReceipt(final TransactionReceipt receipt) { return receiptToSyncReceipt(receipt, TransactionReceiptEncodingConfiguration.DEFAULT); } + + private List toResponseReceipts(final List receipts) { + return receipts.stream().map(this::toResponseReceipt).toList(); + } + + private int receiptsComparator( + final List receipts1, final List receipts2) { + if (receipts1.size() != receipts2.size()) { + return receipts1.size() - receipts2.size(); + } + for (int i = 0; i < receipts1.size(); i++) { + if (Utils.compareSyncReceipts(receipts1.get(i), receipts2.get(i)) != 0) { + // quick tiebreak since we are not interested in the order here + return receipts1.hashCode() - receipts2.hashCode(); + } + } + return 0; + } + + private MockedBlock mockBlock(final long number, final int txCount) { + final SyncBlockBody body = mock(SyncBlockBody.class); + when(body.getTransactionCount()).thenReturn(txCount); + final List receipts = mockTransactionReceipts(number, txCount); + return new MockedBlock(new SyncBlock(mockBlockHeader(number, receipts), body), receipts); + } + + private MockedBlock mockBlockWithGasLimit( + final long number, final int txCount, final long blockGasLimit) { + final SyncBlockBody body = mock(SyncBlockBody.class); + when(body.getTransactionCount()).thenReturn(txCount); + final List receipts = mockTransactionReceipts(number, txCount); + final BlockHeader header = mockBlockHeader(number, receipts); + when(header.getGasLimit()).thenReturn(blockGasLimit); + return new MockedBlock(new SyncBlock(header, body), receipts); + } + + private ProtocolSchedule createProtocolScheduleWithTxGasLimitCap(final long txGasLimitCap) { + final ProtocolSchedule ps = mock(ProtocolSchedule.class); + final ProtocolSpec spec = mock(ProtocolSpec.class); + final GasLimitCalculator calc = mock(GasLimitCalculator.class); + when(calc.transactionGasLimitCap()).thenReturn(txGasLimitCap); + when(spec.getGasLimitCalculator()).thenReturn(calc); + when(spec.isPoS()).thenReturn(false); + when(ps.getByBlockHeader(any())).thenReturn(spec); + when(ps.anyMatch(any())).thenReturn(false); + return ps; + } + + private record MockedBlock(SyncBlock block, List receipts) {} } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessageTest.java new file mode 100644 index 00000000000..d4b8e65180b --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessageTest.java @@ -0,0 +1,95 @@ +/* + * 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.eth.messages; + +import org.hyperledger.besu.datatypes.Hash; +import org.hyperledger.besu.ethereum.core.BlockDataGenerator; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.MessageData; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; + +import java.util.ArrayList; +import java.util.Iterator; +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.Test; + +public final class GetPaginatedReceiptsMessageTest { + + @Test + public void roundTripTestWithZeroIndex() { + roundTripTest(0); + } + + @Test + public void roundTripTestWithNonZeroIndex() { + roundTripTest(5); + } + + private void roundTripTest(final int firstBlockReceiptIndex) { + // Generate some hashes + final BlockDataGenerator gen = new BlockDataGenerator(1); + final List hashes = new ArrayList<>(); + final int hashCount = 20; + for (int i = 0; i < hashCount; ++i) { + hashes.add(gen.hash()); + } + + // Perform round-trip transformation + // Create message, copy it to a generic message, then read back into a GetPaginatedReceipts + // message + final MessageData initialMessage = + GetPaginatedReceiptsMessage.create(hashes, firstBlockReceiptIndex); + final MessageData raw = + new RawMessage(EthProtocolMessages.GET_RECEIPTS, initialMessage.getData()); + final GetPaginatedReceiptsMessage message = GetPaginatedReceiptsMessage.readFrom(raw); + + // Read data back out after round trip and check they match originals. + Assertions.assertThat(message.firstBlockReceiptIndex()).isEqualTo(firstBlockReceiptIndex); + final Iterator readData = message.blockHashes().iterator(); + for (int i = 0; i < hashCount; ++i) { + Assertions.assertThat(readData.next()).isEqualTo(hashes.get(i)); + } + Assertions.assertThat(readData.hasNext()).isFalse(); + } + + @Test + public void readFromReturnsSameInstanceIfAlreadyCorrectType() { + final BlockDataGenerator gen = new BlockDataGenerator(1); + final GetPaginatedReceiptsMessage original = + GetPaginatedReceiptsMessage.create(List.of(gen.hash()), 0); + + final GetPaginatedReceiptsMessage result = GetPaginatedReceiptsMessage.readFrom(original); + + Assertions.assertThat(result).isSameAs(original); + } + + @Test + public void readFromThrowsOnWrongMessageCode() { + final BlockDataGenerator gen = new BlockDataGenerator(1); + final MessageData wrongCodeMessage = + new RawMessage(EthProtocolMessages.GET_BLOCK_HEADERS, gen.hash().getBytes()); + + Assertions.assertThatThrownBy(() -> GetPaginatedReceiptsMessage.readFrom(wrongCodeMessage)) + .isInstanceOf(IllegalArgumentException.class); + } + + @Test + public void getCodeReturnsGetReceipts() { + final GetPaginatedReceiptsMessage message = GetPaginatedReceiptsMessage.create(List.of(), 0); + + Assertions.assertThat(message.getCode()).isEqualTo(EthProtocolMessages.GET_RECEIPTS); + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java index 40d3bfe2eff..7f347b235e3 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessageTest.java @@ -41,7 +41,7 @@ public void roundTripTest() { // Perform round-trip transformation // Create GetReceipts message, copy it to a generic message, then read back into a GetReceipts // message - final MessageData initialMessage = GetReceiptsMessage.create(hashes, 1); + final MessageData initialMessage = GetReceiptsMessage.create(hashes); final MessageData raw = new RawMessage(EthProtocolMessages.GET_RECEIPTS, initialMessage.getData()); final GetReceiptsMessage message = GetReceiptsMessage.readFrom(raw); diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessageTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessageTest.java new file mode 100644 index 00000000000..57671663cf3 --- /dev/null +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/messages/PaginatedReceiptsMessageTest.java @@ -0,0 +1,110 @@ +/* + * 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.eth.messages; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.hyperledger.besu.ethereum.core.BlockDataGenerator; +import org.hyperledger.besu.ethereum.core.TransactionReceipt; +import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncoder; +import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.RawMessage; +import org.hyperledger.besu.ethereum.rlp.BytesValueRLPOutput; + +import java.util.List; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +public final class PaginatedReceiptsMessageTest { + + static List testDeserializeFromWireWithLastBlockIncomplete() { + return List.of(Arguments.of(0, false), Arguments.of(1, true)); + } + + @ParameterizedTest + @MethodSource("testDeserializeFromWireWithLastBlockIncomplete") + public void testDeserializeFromWireWithLastBlockIncomplete( + final int value, final boolean expected) { + final BlockDataGenerator gen = new BlockDataGenerator(1); + final BytesValueRLPOutput blockRlp = new BytesValueRLPOutput(); + blockRlp.startList(); + TransactionReceiptEncoder.writeTo( + gen.receipt(), + blockRlp, + TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + blockRlp.endList(); + + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeLongScalar(value); + out.startList(); + out.writeRaw(blockRlp.encoded()); + out.endList(); + + final PaginatedReceiptsMessage decoded = + PaginatedReceiptsMessage.readFrom( + new RawMessage(EthProtocolMessages.RECEIPTS, out.encoded())); + assertThat(decoded.lastBlockIncomplete()).isEqualTo(expected); + } + + @Test + public void testCreateUnsafePreservesLastBlockIncomplete() { + final BlockDataGenerator gen = new BlockDataGenerator(1); + final List> receipts = List.of(List.of(gen.receipt())); + + final BytesValueRLPOutput blockReceipts = new BytesValueRLPOutput(); + blockReceipts.startList(); + receipts + .getFirst() + .forEach( + r -> + TransactionReceiptEncoder.writeTo( + r, + blockReceipts, + TransactionReceiptEncodingConfiguration.DEFAULT_NETWORK_CONFIGURATION)); + blockReceipts.endList(); + + final BytesValueRLPOutput messageData = new BytesValueRLPOutput(); + messageData.writeLongScalar(0); + messageData.startList(); + messageData.writeRaw(blockReceipts.encoded()); + messageData.endList(); + + // createUnsafe stores the flag directly without re-parsing + final PaginatedReceiptsMessage messageComplete = + PaginatedReceiptsMessage.createUnsafe(messageData.encoded(), false); + assertThat(messageComplete.lastBlockIncomplete()).isFalse(); + + final PaginatedReceiptsMessage messageIncomplete = + PaginatedReceiptsMessage.createUnsafe(messageData.encoded(), true); + assertThat(messageIncomplete.lastBlockIncomplete()).isTrue(); + } + + @Test + public void testMinimalEncoding() { + // Test minimal encoding without actual receipts to isolate the flag logic + final BytesValueRLPOutput out = new BytesValueRLPOutput(); + out.writeLongScalar(1); // Flag = true + out.startList(); // Empty list of blocks + out.endList(); + + final PaginatedReceiptsMessage message = + PaginatedReceiptsMessage.readFrom( + new RawMessage(EthProtocolMessages.RECEIPTS, out.encoded())); + assertThat(message.lastBlockIncomplete()).isTrue(); + } +} diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/FastSyncChainDownloaderTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/FastSyncChainDownloaderTest.java index 960ff23f0ce..0c4daed48b0 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/FastSyncChainDownloaderTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/sync/fastsync/FastSyncChainDownloaderTest.java @@ -44,6 +44,7 @@ import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetHeadersFromPeerTaskExecutorAnswer; import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncBlockBodiesFromPeerTask; import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask; +import org.hyperledger.besu.ethereum.eth.manager.peertask.task.GetSyncReceiptsFromPeerTask.Response; import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages; import org.hyperledger.besu.ethereum.eth.messages.GetBlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.sync.ChainDownloader; @@ -168,7 +169,7 @@ public void setup(final DataStorageFormat storageFormat) { .toList())); return new PeerTaskExecutorResult<>( - Optional.of(getReceiptsFromPeerTaskResult), + Optional.of(new Response(getReceiptsFromPeerTaskResult, List.of())), PeerTaskExecutorResponseCode.SUCCESS, Collections.emptyList()); }); diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java index a76f181e570..5241be82a1e 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java @@ -144,6 +144,7 @@ public enum DisconnectReason { LOCAL_IDENTITY((byte) 0x0a, "Local identity"), TIMEOUT((byte) 0x0b, "Timeout"), INVALID_BLOCK_REQUESTED((byte) 0x0f, "Invalid block requested"), + INVALID_RECEIPT_RECEIVED((byte) 0x0f, "Invalid receipt received"), INVALID_FIRST_BLOCK_RECEIPT_INDEX((byte) 0x0f, "Invalid first block receipt index"), SUBPROTOCOL_TRIGGERED((byte) 0x10, "Sub protocol triggered"), SUBPROTOCOL_TRIGGERED_MISMATCHED_NETWORK((byte) 0x10, "Mismatched network id"), From 9e56a6b01fd1bee97a5e3f0ec7a5e24f8ec0811f Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Thu, 5 Mar 2026 11:55:36 +0100 Subject: [PATCH 4/7] more unit tests for EthServer paginated receipts response Signed-off-by: Fabio Di Fabio --- .../ethereum/eth/manager/EthServerTest.java | 166 ++++++++++++++++++ 1 file changed, 166 insertions(+) diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java index 977977326b4..8d70676e149 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java @@ -17,8 +17,11 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; +import static org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason.INVALID_BLOCK_REQUESTED; +import static org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason.INVALID_FIRST_BLOCK_RECEIPT_INDEX; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Hash; @@ -39,9 +42,11 @@ import org.hyperledger.besu.ethereum.eth.messages.GetBlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.GetNodeDataMessage; +import org.hyperledger.besu.ethereum.eth.messages.GetPaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetPooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.GetReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.NodeDataMessage; +import org.hyperledger.besu.ethereum.eth.messages.PaginatedReceiptsMessage; import org.hyperledger.besu.ethereum.eth.messages.PooledTransactionsMessage; import org.hyperledger.besu.ethereum.eth.messages.ReceiptsMessage; import org.hyperledger.besu.ethereum.eth.transactions.TransactionPool; @@ -336,6 +341,137 @@ public void shouldLimitTransactionsByCount() { assertThat(result).contains(expectedMsg); } + @Test + public void shouldReturnAllPaginatedReceiptsForKnownBlocks() { + final Map> receiptsByHash = setupBlockReceipts(3); + final List hashes = new ArrayList<>(receiptsByHash.keySet()); + setupEthServer(); + + final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, 0); + final EthMessage ethMsg = new EthMessage(ethPeer, msg); + + final List> expectedReceipts = + hashes.stream().map(receiptsByHash::get).collect(Collectors.toList()); + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(expectedReceipts, false), false); + + final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldLimitPaginatedReceiptsByCount() { + final int limit = 6; + final Map> receiptsByHash = setupBlockReceipts(10); + final List hashes = new ArrayList<>(receiptsByHash.keySet()); + setupEthServer(b -> b.maxGetReceipts(limit)); + + final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, 0); + final EthMessage ethMsg = new EthMessage(ethPeer, msg); + + final List> expectedReceipts = + hashes.stream().limit(limit).map(receiptsByHash::get).collect(Collectors.toList()); + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(expectedReceipts, false), false); + + final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldLimitPaginatedReceiptsByMessageSize() { + // Use explicit receipts (not blockSequence) to avoid blocks with 0 transactions, which would + // be silently included in the response without setting lastBlockIncomplete, causing an + // extra empty-list block entry and making the expected vs actual sizes diverge. + final Hash block0Hash = dataGenerator.hash(); + final Hash block1Hash = dataGenerator.hash(); + final TransactionReceipt receipt0 = dataGenerator.receipt(); + final TransactionReceipt receipt1 = dataGenerator.receipt(); + when(blockchain.getTxReceipts(block0Hash)).thenReturn(Optional.of(List.of(receipt0))); + when(blockchain.getTxReceipts(block1Hash)).thenReturn(Optional.of(List.of(receipt1))); + + // Size limit: exactly fits receipt0 from block 0 but not receipt1 from block 1. + // The server check is: responseSizeEstimate + receiptSize + MAX_PREFIX_SIZE > maxMessageSize. + // With sizeLimit = 2*MAX_PREFIX + size(receipt0): + // receipt0 check: MAX_PREFIX + size(receipt0) + MAX_PREFIX = sizeLimit → passes (not >) + // receipt1 check: MAX_PREFIX + size(receipt0) + size(receipt1) + MAX_PREFIX > sizeLimit + // = size(receipt1) > 0 → always true → lastBlockIncomplete = true + final int sizeLimit = 2 * RLP.MAX_PREFIX_SIZE + calculatePaginatedReceiptEncodedSize(receipt0); + setupEthServer(b -> b.maxMessageSize(sizeLimit)); + + final List hashes = List.of(block0Hash, block1Hash); + final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, 0); + + // block 0: receipt0 fits; block 1: starts but receipt1 doesn't fit → empty list + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(List.of(List.of(receipt0), List.of()), true), true); + + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldPaginateWithFirstBlockReceiptIndex() { + // Create receipts directly rather than deriving them from blockSequence, which can produce + // blocks with 0 transactions (and thus 0 receipts), causing subList(firstIndex, 0) to throw. + final Hash firstBlockHash = dataGenerator.hash(); + final Hash secondBlockHash = dataGenerator.hash(); + final List firstBlockReceipts = + List.of(dataGenerator.receipt(), dataGenerator.receipt()); + final List secondBlockReceipts = + List.of(dataGenerator.receipt(), dataGenerator.receipt()); + when(blockchain.getTxReceipts(firstBlockHash)).thenReturn(Optional.of(firstBlockReceipts)); + when(blockchain.getTxReceipts(secondBlockHash)).thenReturn(Optional.of(secondBlockReceipts)); + setupEthServer(); + + final int firstIndex = 1; + // Use List.of to guarantee ordering: firstBlockHash is always the block that gets paginated + final List hashes = List.of(firstBlockHash, secondBlockHash); + final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, firstIndex); + final EthMessage ethMsg = new EthMessage(ethPeer, msg); + + // firstIndex skips the first receipt of the first block only; subsequent blocks are unaffected + final List> expectedReceipts = + List.of( + firstBlockReceipts.subList(firstIndex, firstBlockReceipts.size()), secondBlockReceipts); + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(expectedReceipts, false), false); + + final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldDisconnectPeerForUnknownBlockInPaginatedRequest() { + final Hash unknownHash = dataGenerator.hash(); + when(blockchain.getTxReceipts(unknownHash)).thenReturn(Optional.empty()); + setupEthServer(); + + final GetPaginatedReceiptsMessage msg = + GetPaginatedReceiptsMessage.create(List.of(unknownHash), 0); + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + + verify(ethPeer).disconnect(INVALID_BLOCK_REQUESTED); + } + + @Test + public void shouldDisconnectPeerForInvalidFirstBlockReceiptIndex() { + // Blocks have 2 receipts each; requesting firstBlockReceiptIndex = 3 is out of range + final Map> receiptsByHash = setupBlockReceipts(1); + final List hashes = new ArrayList<>(receiptsByHash.keySet()); + setupEthServer(); + + final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, 3); + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + + verify(ethPeer).disconnect(INVALID_FIRST_BLOCK_RECEIPT_INDEX); + } + private void setupEthServer() { setupEthServer(Function.identity()); } @@ -428,4 +564,34 @@ private int calculateRlpEncodedSize(final List receipts) { rlp.endList(); return rlp.encodedSize(); } + + private int calculatePaginatedReceiptEncodedSize(final TransactionReceipt receipt) { + final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); + TransactionReceiptEncoder.writeTo( + receipt, rlp, TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + return rlp.encodedSize(); + } + + private Bytes serializePaginatedReceiptsList( + final List> receipts, final boolean lastBlockIncomplete) { + final BytesValueRLPOutput rlp = new BytesValueRLPOutput(); + rlp.writeLongScalar(lastBlockIncomplete ? 1 : 0); + rlp.startList(); + for (final List blockReceipts : receipts) { + final BytesValueRLPOutput encodedBlockReceipts = new BytesValueRLPOutput(); + encodedBlockReceipts.startList(); + for (final TransactionReceipt receipt : blockReceipts) { + final BytesValueRLPOutput encodedReceipt = new BytesValueRLPOutput(); + TransactionReceiptEncoder.writeTo( + receipt, + encodedReceipt, + TransactionReceiptEncodingConfiguration.ETH69_RECEIPT_CONFIGURATION); + encodedBlockReceipts.writeRaw(encodedReceipt.encoded()); + } + encodedBlockReceipts.endList(); + rlp.writeRaw(encodedBlockReceipts.encoded()); + } + rlp.endList(); + return rlp.encoded(); + } } From 0b7cc36cf035bb0cd839d260f03d03e963a80834 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Fri, 6 Mar 2026 11:22:07 +0100 Subject: [PATCH 5/7] Apply suggestions from code review Signed-off-by: Fabio Di Fabio --- .../besu/ethereum/eth/manager/EthServer.java | 7 +++-- .../task/GetSyncReceiptsFromPeerTask.java | 27 ++++++++++--------- .../messages/GetPaginatedReceiptsMessage.java | 1 - .../sync/common/DownloadSyncReceiptsStep.java | 2 +- .../ethereum/eth/manager/EthServerTest.java | 18 +++++++++---- .../rlpx/wire/messages/DisconnectMessage.java | 1 - 6 files changed, 31 insertions(+), 25 deletions(-) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java index 7bd823e42bd..1b6aa8735c4 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java @@ -14,7 +14,6 @@ */ package org.hyperledger.besu.ethereum.eth.manager; -import static org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason.INVALID_BLOCK_REQUESTED; import static org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason.INVALID_FIRST_BLOCK_RECEIPT_INDEX; import org.hyperledger.besu.datatypes.Hash; @@ -310,9 +309,9 @@ static MessageData constructGetPaginatedReceiptsResponse( for (final Hash blockHash : blockHashes) { final Optional> maybeReceipts = blockchain.getTxReceipts(blockHash); if (maybeReceipts.isEmpty()) { - LOG.debug("Invalid request from peer {}, block {} does not exists", peer, blockHash); - peer.disconnect(INVALID_BLOCK_REQUESTED); - return PaginatedReceiptsMessage.createUnsafe(Bytes.EMPTY, false); + LOG.debug( + "Invalid request from peer {}, block {} does not exists, returning", peer, blockHash); + break; } final List blockReceipts = maybeReceipts.get(); diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java index 6c25e0f7a6b..c419f1195c9 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/peertask/task/GetSyncReceiptsFromPeerTask.java @@ -128,7 +128,7 @@ private Response processPaginatedResponse(final MessageData messageData) PaginatedReceiptsMessage.readFrom(messageData); try { final List> receivedReceipts = - completeFirstBlock(paginatedReceiptsMessage.syncReceipts()); + paginatedReceiptsMessage.syncReceipts(); if (receivedReceipts.isEmpty()) { throw new InvalidPeerTaskResponseException("No result returned"); @@ -138,21 +138,24 @@ private Response processPaginatedResponse(final MessageData messageData) throw new InvalidPeerTaskResponseException("Too many result returned"); } + final List> cumulativeReceivedReceipts = + completeFirstBlock(receivedReceipts); + final int endIndex; final List lastBlockPartialReceipts; if (paginatedReceiptsMessage.lastBlockIncomplete()) { - endIndex = receivedReceipts.size() - 1; - lastBlockPartialReceipts = receivedReceipts.getLast(); + endIndex = cumulativeReceivedReceipts.size() - 1; + lastBlockPartialReceipts = cumulativeReceivedReceipts.getLast(); } else { - endIndex = receivedReceipts.size(); + endIndex = cumulativeReceivedReceipts.size(); lastBlockPartialReceipts = List.of(); } final Map> receiptsByBlock = - HashMap.newHashMap(receivedReceipts.size()); + HashMap.newHashMap(cumulativeReceivedReceipts.size()); for (int i = 0; i < endIndex; i++) { - receiptsByBlock.put(request.blocks.get(i), receivedReceipts.get(i)); + receiptsByBlock.put(request.blocks.get(i), cumulativeReceivedReceipts.get(i)); } return new Response(receiptsByBlock, lastBlockPartialReceipts); @@ -163,8 +166,7 @@ private Response processPaginatedResponse(final MessageData messageData) } private List> completeFirstBlock( - final List> receivedReceipts) - throws InvalidPeerTaskResponseException { + final List> receivedReceipts) { if (request.firstBlockPartialReceipts.isEmpty()) { // nothing to integrate returning as is return receivedReceipts; @@ -247,10 +249,9 @@ public PeerTaskValidationResponse validateResult(final Response result) { } private long calculateTxGasLimitUpperBound(final SyncBlock lastBlockReceived) { - // to avoid having to deserialize the tx to get the actual gas limit we approximate it - // giving an upper bound, for everything before Osaka we use the block gas limit of 45M - // for Osaka onward we can use the max gas limit allowed per tx as specified by the protocol - // schedule + // to avoid having to deserialize the tx to get the actual gas limit we use an upper bound, + // for everything before Osaka we use the block gas limit of 45M and for Osaka onward + // we can use the max gas limit allowed per tx as specified by the protocol schedule return Math.min( protocolSchedule .getByBlockHeader(lastBlockReceived.getHeader()) @@ -301,7 +302,7 @@ public boolean isEmpty() { return completeReceiptsByBlock.isEmpty() && lastBlockPartialReceipts.isEmpty(); } - public int size() { + public int completeCount() { return completeReceiptsByBlock.size(); } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java index 34f98a69c69..d6e8cee51e6 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java @@ -29,7 +29,6 @@ public final class GetPaginatedReceiptsMessage extends GetReceiptsMessage { private GetPaginatedReceiptsMessage(final Bytes data) { super(data); - deserialize(data); } private GetPaginatedReceiptsMessage( diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStep.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStep.java index 64aeb0785a2..458b600424d 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStep.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/sync/common/DownloadSyncReceiptsStep.java @@ -203,7 +203,7 @@ private CompletableFuture>> downloadRecei "[{}:{}] Received complete response for {} blocks, last block partial receipts {}, completed blocks {} (requested {}, initial {}): {}") .addArgument(currTaskId) .addArgument(iteration) - .addArgument(response::size) + .addArgument(response::completeCount) .addArgument(lastBlockPartialReceipts::size) .addArgument(receiptsByBlock::size) .addArgument(blocksToRequest::size) diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java index 8d70676e149..c33c9bbfb09 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java @@ -17,7 +17,6 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; -import static org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason.INVALID_BLOCK_REQUESTED; import static org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason.INVALID_FIRST_BLOCK_RECEIPT_INDEX; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; @@ -447,16 +446,25 @@ public void shouldPaginateWithFirstBlockReceiptIndex() { } @Test - public void shouldDisconnectPeerForUnknownBlockInPaginatedRequest() { + public void shouldReturnCollectedReceiptsUpToUnknownBlockInPaginatedRequest() { + // Setup one known block followed by an unknown one + final Hash knownHash = dataGenerator.hash(); + final TransactionReceipt receipt = dataGenerator.receipt(); + when(blockchain.getTxReceipts(knownHash)).thenReturn(Optional.of(List.of(receipt))); final Hash unknownHash = dataGenerator.hash(); when(blockchain.getTxReceipts(unknownHash)).thenReturn(Optional.empty()); setupEthServer(); final GetPaginatedReceiptsMessage msg = - GetPaginatedReceiptsMessage.create(List.of(unknownHash), 0); - ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + GetPaginatedReceiptsMessage.create(List.of(knownHash, unknownHash), 0); + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); - verify(ethPeer).disconnect(INVALID_BLOCK_REQUESTED); + // Server stops at the unknown block and returns what was collected before it + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(List.of(List.of(receipt)), false), false); + assertThat(result).contains(expectedMsg); } @Test diff --git a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java index 5241be82a1e..bb007f58ea8 100644 --- a/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java +++ b/ethereum/p2p/src/main/java/org/hyperledger/besu/ethereum/p2p/rlpx/wire/messages/DisconnectMessage.java @@ -143,7 +143,6 @@ public enum DisconnectReason { UNEXPECTED_ID((byte) 0x09, "Unexpected ID"), LOCAL_IDENTITY((byte) 0x0a, "Local identity"), TIMEOUT((byte) 0x0b, "Timeout"), - INVALID_BLOCK_REQUESTED((byte) 0x0f, "Invalid block requested"), INVALID_RECEIPT_RECEIVED((byte) 0x0f, "Invalid receipt received"), INVALID_FIRST_BLOCK_RECEIPT_INDEX((byte) 0x0f, "Invalid first block receipt index"), SUBPROTOCOL_TRIGGERED((byte) 0x10, "Sub protocol triggered"), From 8811a53c6821f0daba1659ba73bbff1258065535 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Tue, 10 Mar 2026 10:27:16 +0100 Subject: [PATCH 6/7] Apply suggestions from code review Signed-off-by: Fabio Di Fabio --- .../eth/manager/EthProtocolManager.java | 11 ++ .../besu/ethereum/eth/manager/EthServer.java | 13 +- .../ProtocolViolationException.java | 35 +++++ .../messages/GetPaginatedReceiptsMessage.java | 20 +-- .../eth/messages/GetReceiptsMessage.java | 23 +--- .../ethereum/eth/manager/EthServerTest.java | 120 ++++++++++++++---- 6 files changed, 160 insertions(+), 62 deletions(-) create mode 100644 ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/exceptions/ProtocolViolationException.java diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java index f6965ddbc14..86143925ca2 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthProtocolManager.java @@ -23,6 +23,7 @@ import org.hyperledger.besu.ethereum.core.Difficulty; import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; +import org.hyperledger.besu.ethereum.eth.manager.exceptions.ProtocolViolationException; import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages; import org.hyperledger.besu.ethereum.eth.messages.StatusMessage; import org.hyperledger.besu.ethereum.eth.peervalidation.PeerValidator; @@ -322,6 +323,16 @@ public void processMessage(final Capability capability, final Message message) { ethPeer.disconnect( DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL_MALFORMED_MESSAGE_RECEIVED); + } catch (final ProtocolViolationException e) { + LOG.atDebug() + .setMessage("Received invalid message {} ({}), disconnecting: {}, {}") + .addArgument(messageData::getData) + .addArgument(e::getReason) + .addArgument(ethPeer::toString) + .addArgument(e::toString) + .log(); + + ethPeer.disconnect(e.getReason()); } maybeResponseData.ifPresent( responseData -> { diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java index 1b6aa8735c4..b33cf88e7fb 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java @@ -28,6 +28,7 @@ import org.hyperledger.besu.ethereum.core.encoding.receipt.TransactionReceiptEncodingConfiguration; import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; +import org.hyperledger.besu.ethereum.eth.manager.exceptions.ProtocolViolationException; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.EthProtocolMessages; @@ -318,14 +319,10 @@ static MessageData constructGetPaginatedReceiptsResponse( final List requestedReceipts; if (skipBefore > blockReceipts.size()) { - LOG.debug( - "Invalid request from peer {}, firstBlockReceiptIndex {} is greater than the receipt count of {} for block {}", - peer, - skipBefore, - blockReceipts.size(), - blockHash); - peer.disconnect(INVALID_FIRST_BLOCK_RECEIPT_INDEX); - return PaginatedReceiptsMessage.createUnsafe(Bytes.EMPTY, false); + throw new ProtocolViolationException( + "Invalid request from peer %s, firstBlockReceiptIndex %d is greater than or equal the receipt count of %d for block %s" + .formatted(peer, skipBefore, blockReceipts.size(), blockHash), + INVALID_FIRST_BLOCK_RECEIPT_INDEX); } if (skipBefore > 0) { diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/exceptions/ProtocolViolationException.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/exceptions/ProtocolViolationException.java new file mode 100644 index 00000000000..d3ae3ef1388 --- /dev/null +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/exceptions/ProtocolViolationException.java @@ -0,0 +1,35 @@ +/* + * 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.eth.manager.exceptions; + +import org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage; + +public class ProtocolViolationException extends RuntimeException { + private final DisconnectMessage.DisconnectReason reason; + + public ProtocolViolationException(final String message) { + this(message, DisconnectMessage.DisconnectReason.BREACH_OF_PROTOCOL); + } + + public ProtocolViolationException( + final String message, final DisconnectMessage.DisconnectReason reason) { + super(message); + this.reason = reason; + } + + public DisconnectMessage.DisconnectReason getReason() { + return reason; + } +} diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java index d6e8cee51e6..b43b7eff53d 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetPaginatedReceiptsMessage.java @@ -25,11 +25,7 @@ import org.apache.tuweni.bytes.Bytes; public final class GetPaginatedReceiptsMessage extends GetReceiptsMessage { - private int firstBlockReceiptIndex; - - private GetPaginatedReceiptsMessage(final Bytes data) { - super(data); - } + private final int firstBlockReceiptIndex; private GetPaginatedReceiptsMessage( final Bytes data, final List blockHashes, final int firstBlockReceiptIndex) { @@ -46,7 +42,10 @@ public static GetPaginatedReceiptsMessage readFrom(final MessageData message) { throw new IllegalArgumentException( String.format("Message has code %d and thus is not a GetReceipts.", code)); } - return new GetPaginatedReceiptsMessage(message.getData()); + final RLPInput input = new BytesValueRLPInput(message.getData(), false); + final int firstBlockReceiptIndex = input.readIntScalar(); + final List blockHashes = parseBlockHashes(input); + return new GetPaginatedReceiptsMessage(message.getData(), blockHashes, firstBlockReceiptIndex); } public static GetPaginatedReceiptsMessage create( @@ -62,13 +61,4 @@ public static GetPaginatedReceiptsMessage create( public int firstBlockReceiptIndex() { return firstBlockReceiptIndex; } - - @Override - protected void deserialize(final Bytes data) { - final RLPInput input = new BytesValueRLPInput(data, false); - - this.firstBlockReceiptIndex = input.readIntScalar(); - - deserializeBlockHashList(input); - } } diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java index 053430b7650..4a70434e920 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/messages/GetReceiptsMessage.java @@ -27,12 +27,7 @@ import org.apache.tuweni.bytes.Bytes; public class GetReceiptsMessage extends AbstractMessageData { - private List blockHashes; - - protected GetReceiptsMessage(final Bytes data) { - super(data); - deserialize(data); - } + private final List blockHashes; protected GetReceiptsMessage(final Bytes data, final List blockHashes) { super(data); @@ -48,7 +43,8 @@ public static GetReceiptsMessage readFrom(final MessageData message) { throw new IllegalArgumentException( String.format("Message has code %d and thus is not a GetReceipts.", code)); } - return new GetReceiptsMessage(message.getData()); + final RLPInput input = new BytesValueRLPInput(message.getData(), false); + return new GetReceiptsMessage(message.getData(), parseBlockHashes(input)); } public static GetReceiptsMessage create(final List blockHashes) { @@ -68,17 +64,12 @@ public List blockHashes() { return blockHashes; } - protected void deserialize(final Bytes data) { - final RLPInput input = new BytesValueRLPInput(data, false); - - deserializeBlockHashList(input); - } - - protected void deserializeBlockHashList(final RLPInput input) { - this.blockHashes = new ArrayList<>(input.enterList()); + protected static List parseBlockHashes(final RLPInput input) { + final List hashes = new ArrayList<>(input.enterList()); while (!input.isEndOfCurrentList()) { - blockHashes.add(Hash.wrap(input.readBytes32())); + hashes.add(Hash.wrap(input.readBytes32())); } input.leaveList(); + return hashes; } } diff --git a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java index c33c9bbfb09..67cf81fe3e9 100644 --- a/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java +++ b/ethereum/eth/src/test/java/org/hyperledger/besu/ethereum/eth/manager/EthServerTest.java @@ -16,11 +16,10 @@ import static java.util.Collections.singletonList; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import static org.hyperledger.besu.ethereum.eth.core.Utils.serializeReceiptsList; -import static org.hyperledger.besu.ethereum.p2p.rlpx.wire.messages.DisconnectMessage.DisconnectReason.INVALID_FIRST_BLOCK_RECEIPT_INDEX; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import org.hyperledger.besu.datatypes.Hash; @@ -36,6 +35,7 @@ import org.hyperledger.besu.ethereum.eth.EthProtocol; import org.hyperledger.besu.ethereum.eth.EthProtocolConfiguration; import org.hyperledger.besu.ethereum.eth.ImmutableEthProtocolConfiguration; +import org.hyperledger.besu.ethereum.eth.manager.exceptions.ProtocolViolationException; import org.hyperledger.besu.ethereum.eth.messages.BlockBodiesMessage; import org.hyperledger.besu.ethereum.eth.messages.BlockHeadersMessage; import org.hyperledger.besu.ethereum.eth.messages.GetBlockBodiesMessage; @@ -342,40 +342,55 @@ public void shouldLimitTransactionsByCount() { @Test public void shouldReturnAllPaginatedReceiptsForKnownBlocks() { - final Map> receiptsByHash = setupBlockReceipts(3); - final List hashes = new ArrayList<>(receiptsByHash.keySet()); + // Use explicit receipts to guarantee each block has at least 1 receipt: blocks with 0 + // receipts from blockSequence would trigger skipBefore(0) >= size(0) → ProtocolViolation. + final Hash hash0 = dataGenerator.hash(); + final Hash hash1 = dataGenerator.hash(); + final Hash hash2 = dataGenerator.hash(); + final List receipts0 = List.of(dataGenerator.receipt()); + final List receipts1 = List.of(dataGenerator.receipt()); + final List receipts2 = List.of(dataGenerator.receipt()); + when(blockchain.getTxReceipts(hash0)).thenReturn(Optional.of(receipts0)); + when(blockchain.getTxReceipts(hash1)).thenReturn(Optional.of(receipts1)); + when(blockchain.getTxReceipts(hash2)).thenReturn(Optional.of(receipts2)); setupEthServer(); + final List hashes = List.of(hash0, hash1, hash2); final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, 0); - final EthMessage ethMsg = new EthMessage(ethPeer, msg); - final List> expectedReceipts = - hashes.stream().map(receiptsByHash::get).collect(Collectors.toList()); final PaginatedReceiptsMessage expectedMsg = PaginatedReceiptsMessage.createUnsafe( - serializePaginatedReceiptsList(expectedReceipts, false), false); + serializePaginatedReceiptsList(List.of(receipts0, receipts1, receipts2), false), false); - final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.ETH70); + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); assertThat(result).contains(expectedMsg); } @Test public void shouldLimitPaginatedReceiptsByCount() { final int limit = 6; - final Map> receiptsByHash = setupBlockReceipts(10); - final List hashes = new ArrayList<>(receiptsByHash.keySet()); + final int blockCount = 10; + // Explicit receipts (1 per block) to avoid 0-receipt blocks from blockSequence. + final List hashes = new ArrayList<>(); + final List> allReceipts = new ArrayList<>(); + for (int i = 0; i < blockCount; i++) { + final Hash hash = dataGenerator.hash(); + final List receipts = List.of(dataGenerator.receipt()); + hashes.add(hash); + allReceipts.add(receipts); + when(blockchain.getTxReceipts(hash)).thenReturn(Optional.of(receipts)); + } setupEthServer(b -> b.maxGetReceipts(limit)); final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, 0); - final EthMessage ethMsg = new EthMessage(ethPeer, msg); - final List> expectedReceipts = - hashes.stream().limit(limit).map(receiptsByHash::get).collect(Collectors.toList()); final PaginatedReceiptsMessage expectedMsg = PaginatedReceiptsMessage.createUnsafe( - serializePaginatedReceiptsList(expectedReceipts, false), false); + serializePaginatedReceiptsList(allReceipts.subList(0, limit), false), false); - final Optional result = ethMessages.dispatch(ethMsg, EthProtocol.ETH70); + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); assertThat(result).contains(expectedMsg); } @@ -468,16 +483,75 @@ public void shouldReturnCollectedReceiptsUpToUnknownBlockInPaginatedRequest() { } @Test - public void shouldDisconnectPeerForInvalidFirstBlockReceiptIndex() { - // Blocks have 2 receipts each; requesting firstBlockReceiptIndex = 3 is out of range - final Map> receiptsByHash = setupBlockReceipts(1); - final List hashes = new ArrayList<>(receiptsByHash.keySet()); + public void shouldThrowProtocolViolationForInvalidFirstBlockReceiptIndex() { + // Block has 2 receipts; firstBlockReceiptIndex = 3 triggers skipBefore(3) > size(2) → throws + final Hash blockHash = dataGenerator.hash(); + final List receipts = + List.of(dataGenerator.receipt(), dataGenerator.receipt()); + when(blockchain.getTxReceipts(blockHash)).thenReturn(Optional.of(receipts)); + setupEthServer(); + + final GetPaginatedReceiptsMessage msg = + GetPaginatedReceiptsMessage.create(List.of(blockHash), 3); + + assertThatThrownBy(() -> ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70)) + .isInstanceOf(ProtocolViolationException.class); + } + + @Test + public void shouldTreatFirstBlockReceiptIndexEqualToSizeAsValidAndReturnEmptyFirstBlock() { + // skipBefore == blockReceipts.size() is valid (condition is strictly >): + // the first block contributes an empty receipt list and subsequent blocks are returned + // normally. + final Hash block0Hash = dataGenerator.hash(); + final Hash block1Hash = dataGenerator.hash(); + final List block0Receipts = + List.of(dataGenerator.receipt(), dataGenerator.receipt()); + final List block1Receipts = List.of(dataGenerator.receipt()); + when(blockchain.getTxReceipts(block0Hash)).thenReturn(Optional.of(block0Receipts)); + when(blockchain.getTxReceipts(block1Hash)).thenReturn(Optional.of(block1Receipts)); setupEthServer(); - final GetPaginatedReceiptsMessage msg = GetPaginatedReceiptsMessage.create(hashes, 3); - ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + // firstBlockReceiptIndex == size(block0) == 2: skip all receipts → empty list for block0 + final GetPaginatedReceiptsMessage msg = + GetPaginatedReceiptsMessage.create(List.of(block0Hash, block1Hash), 2); + + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(List.of(List.of(), block1Receipts), false), false); + + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); + } + + @Test + public void shouldTruncateResponseWhenFirstBlockIndexAndMessageSizeLimitInteract() { + // Covers the interaction between firstBlockReceiptIndex > 0 (skipping receipts in block 0) + // and the message size limit cutting off block 1 mid-way ("double pagination"). + final Hash block0Hash = dataGenerator.hash(); + final Hash block1Hash = dataGenerator.hash(); + final TransactionReceipt r0 = dataGenerator.receipt(); // skipped via firstBlockReceiptIndex + final TransactionReceipt r1 = dataGenerator.receipt(); // included from block 0 + final TransactionReceipt r2 = dataGenerator.receipt(); // would be from block 1, doesn't fit + when(blockchain.getTxReceipts(block0Hash)).thenReturn(Optional.of(List.of(r0, r1))); + when(blockchain.getTxReceipts(block1Hash)).thenReturn(Optional.of(List.of(r2))); + + // Size limit: fits only r1 (from block 0 after skipping r0); r2 from block 1 won't fit. + final int sizeLimit = 2 * RLP.MAX_PREFIX_SIZE + calculatePaginatedReceiptEncodedSize(r1); + setupEthServer(b -> b.maxMessageSize(sizeLimit)); + + final GetPaginatedReceiptsMessage msg = + GetPaginatedReceiptsMessage.create(List.of(block0Hash, block1Hash), 1); - verify(ethPeer).disconnect(INVALID_FIRST_BLOCK_RECEIPT_INDEX); + // block 0: r0 skipped, r1 fits; block 1: starts but r2 doesn't fit → empty list + final PaginatedReceiptsMessage expectedMsg = + PaginatedReceiptsMessage.createUnsafe( + serializePaginatedReceiptsList(List.of(List.of(r1), List.of()), true), true); + + final Optional result = + ethMessages.dispatch(new EthMessage(ethPeer, msg), EthProtocol.ETH70); + assertThat(result).contains(expectedMsg); } private void setupEthServer() { From 074c228de70f133a2a7923a230aaa3317b17e226 Mon Sep 17 00:00:00 2001 From: Fabio Di Fabio Date: Wed, 11 Mar 2026 12:05:45 +0100 Subject: [PATCH 7/7] Add trace logs to EthServer for paginated receipts response Signed-off-by: Fabio Di Fabio --- .../besu/ethereum/eth/manager/EthServer.java | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java index b33cf88e7fb..0578a4c9c97 100644 --- a/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java +++ b/ethereum/eth/src/main/java/org/hyperledger/besu/ethereum/eth/manager/EthServer.java @@ -304,6 +304,10 @@ static MessageData constructGetPaginatedReceiptsResponse( final var blockReceiptsRLPs = new ArrayList(blockHashes.size()); int skipBefore = getPaginatedReceipts.firstBlockReceiptIndex(); + LOG.trace( + "Paginated receipt request for {} blocks with first block receipt index {}", + blockHashes.size(), + skipBefore); int responseSizeEstimate = RLP.MAX_PREFIX_SIZE; boolean lastBlockIncomplete = false; @@ -363,7 +367,13 @@ static MessageData constructGetPaginatedReceiptsResponse( blockReceiptsRLPs.forEach(r -> rlp.writeRaw(r.encoded())); rlp.endList(); - return PaginatedReceiptsMessage.createUnsafe(rlp.encoded(), lastBlockIncomplete); + final Bytes encodedResponse = rlp.encoded(); + LOG.trace( + "Returning paginated receipts for {} blocks, with last block incomplete {}, enconded size {}", + blockHashes.size(), + lastBlockIncomplete, + encodedResponse.size()); + return PaginatedReceiptsMessage.createUnsafe(encodedResponse, lastBlockIncomplete); } static MessageData constructGetPooledTransactionsResponse(