From 6d591e1d1eb0436b255182050c8940924d488ab2 Mon Sep 17 00:00:00 2001 From: Chen Kai <281165273grape@gmail.com> Date: Tue, 20 Feb 2024 19:46:30 +0800 Subject: [PATCH] feat:blockv3 support Signed-off-by: Chen Kai <281165273grape@gmail.com> --- hildr-node/build.gradle | 2 +- .../io/optimism/engine/ExecutionPayload.java | 193 ++++---- .../network/AbstractTopicHandler.java | 440 ++++++++++++++++++ .../optimism/network/BlockV1TopicHandler.java | 261 +---------- .../optimism/network/BlockV2TopicHandler.java | 272 +---------- .../optimism/network/BlockV3TopicHandler.java | 41 ++ .../io/optimism/network/BlockVersion.java | 64 +++ .../network/ExecutionPayloadEnvelop.java | 34 ++ .../optimism/network/ExecutionPayloadSSZ.java | 89 ++-- .../io/optimism/network/OpStackNetwork.java | 6 + .../io/optimism/engine/EngineApiTest.java | 4 +- .../network/AbstractTopicHandlerTest.java | 236 ++++++++++ .../network/ExecutionPayloadEnvelopTest.java | 43 ++ .../network/ExecutionPayloadSSZTest.java | 21 +- 14 files changed, 1066 insertions(+), 640 deletions(-) create mode 100644 hildr-node/src/main/java/io/optimism/network/AbstractTopicHandler.java create mode 100644 hildr-node/src/main/java/io/optimism/network/BlockV3TopicHandler.java create mode 100644 hildr-node/src/main/java/io/optimism/network/BlockVersion.java create mode 100644 hildr-node/src/main/java/io/optimism/network/ExecutionPayloadEnvelop.java create mode 100644 hildr-node/src/test/java/io/optimism/network/AbstractTopicHandlerTest.java create mode 100644 hildr-node/src/test/java/io/optimism/network/ExecutionPayloadEnvelopTest.java diff --git a/hildr-node/build.gradle b/hildr-node/build.gradle index 4df89303..fd6ae1bf 100644 --- a/hildr-node/build.gradle +++ b/hildr-node/build.gradle @@ -74,7 +74,7 @@ dependencies { implementation("com.squareup.okhttp3:okhttp:5.0.0-alpha.2") implementation("com.squareup.okhttp3:logging-interceptor:5.0.0-alpha.2") - implementation('org.web3j:core:4.10.3') { + implementation('org.web3j:core:4.11.0') { exclude group: 'org.bouncycastle', module: 'bcprov-jdk15on' exclude group: 'com.squareup.okhttp3', module: 'okhttp' exclude group: 'com.squareup.okhttp3', module: 'logging-interceptor' diff --git a/hildr-node/src/main/java/io/optimism/engine/ExecutionPayload.java b/hildr-node/src/main/java/io/optimism/engine/ExecutionPayload.java index ed94f571..983905ef 100644 --- a/hildr-node/src/main/java/io/optimism/engine/ExecutionPayload.java +++ b/hildr-node/src/main/java/io/optimism/engine/ExecutionPayload.java @@ -13,21 +13,23 @@ /** * The type ExecutionPayload. * - * @param parentHash A 32 byte hash of the parent payload. - * @param feeRecipient A 20 byte hash (aka Address) for the feeRecipient field of the new payload. - * @param stateRoot A 32 byte state root hash. - * @param receiptsRoot A 32 byte receipt root hash. - * @param logsBloom A 32 byte logs bloom filter. - * @param prevRandao A 32 byte beacon chain randomness value. - * @param blockNumber A 64-bit number for the current block index. - * @param gasLimit A 64-bit value for the gas limit. - * @param gasUsed A 64-bit value for the gas used. - * @param timestamp A 64-bit value for the timestamp field of the new payload. - * @param extraData 0 to 32 byte value for extra data. + * @param parentHash A 32 byte hash of the parent payload. + * @param feeRecipient A 20 byte hash (aka Address) for the feeRecipient field of the new payload. + * @param stateRoot A 32 byte state root hash. + * @param receiptsRoot A 32 byte receipt root hash. + * @param logsBloom A 32 byte logs bloom filter. + * @param prevRandao A 32 byte beacon chain randomness value. + * @param blockNumber A 64-bit number for the current block index. + * @param gasLimit A 64-bit value for the gas limit. + * @param gasUsed A 64-bit value for the gas used. + * @param timestamp A 64-bit value for the timestamp field of the new payload. + * @param extraData 0 to 32 byte value for extra data. * @param baseFeePerGas 256 bits for the base fee per gas. - * @param blockHash The 32 byte block hash. - * @param transactions An array of transaction objects where each object is a byte list. - * @param withdrawals An array of withdrawal objects where each object is a byte list. + * @param blockHash The 32 byte block hash. + * @param transactions An array of transaction objects where each object is a byte list. + * @param withdrawals An array of withdrawal objects where each object is a byte list. + * @param blobGasUsed The gas used by the blob. + * @param excessBlobGas The excess gas used by the blob. * @author grapebaba * @since 0.1.0 */ @@ -45,27 +47,31 @@ public record ExecutionPayload( String extraData, BigInteger baseFeePerGas, String blockHash, + List transactions, List withdrawals, - List transactions) { + BigInteger blobGasUsed, + BigInteger excessBlobGas) { /** * The type Execution payload res. * - * @param parentHash A 32 byte hash of the parent payload. - * @param feeRecipient A 20 byte hash (aka Address) for the feeRecipient field of the new payload. - * @param stateRoot A 32 byte state root hash. - * @param receiptsRoot A 32 byte receipt root hash. - * @param logsBloom A 32 byte logs bloom filter. - * @param prevRandao A 32 byte beacon chain randomness value. - * @param blockNumber A 64-bit number for the current block index. - * @param gasLimit A 64-bit value for the gas limit. - * @param gasUsed A 64-bit value for the gas used. - * @param timestamp A 64-bit value for the timestamp field of the new payload. - * @param extraData 0 to 32 byte value for extra data. + * @param parentHash A 32 byte hash of the parent payload. + * @param feeRecipient A 20 byte hash (aka Address) for the feeRecipient field of the new payload. + * @param stateRoot A 32 byte state root hash. + * @param receiptsRoot A 32 byte receipt root hash. + * @param logsBloom A 32 byte logs bloom filter. + * @param prevRandao A 32 byte beacon chain randomness value. + * @param blockNumber A 64-bit number for the current block index. + * @param gasLimit A 64-bit value for the gas limit. + * @param gasUsed A 64-bit value for the gas used. + * @param timestamp A 64-bit value for the timestamp field of the new payload. + * @param extraData 0 to 32 byte value for extra data. * @param baseFeePerGas 256 bits for the base fee per gas. - * @param blockHash The 32 byte block hash. - * @param withdrawals An array of withdrawal objects where each object is a byte list. - * @param transactions An array of transaction objects where each object is a byte list. + * @param blockHash The 32 byte block hash. + * @param withdrawals An array of withdrawal objects where each object is a byte list. + * @param transactions An array of transaction objects where each object is a byte list. + * @param blobGasUsed The gas used by the blob. + * @param excessBlobGas The excess gas used by the blob. */ public record ExecutionPayloadRes( String parentHash, @@ -81,8 +87,10 @@ public record ExecutionPayloadRes( String extraData, String baseFeePerGas, String blockHash, + List transactions, List withdrawals, - List transactions) { + String blobGasUsed, + String excessBlobGas) { /** * To execution payload execution payload. @@ -104,8 +112,10 @@ public ExecutionPayload toExecutionPayload() { extraData, Numeric.decodeQuantity(baseFeePerGas), blockHash, + transactions, withdrawals, - transactions); + StringUtils.isEmpty(blobGasUsed) ? null : Numeric.decodeQuantity(blobGasUsed), + StringUtils.isEmpty(excessBlobGas) ? null : Numeric.decodeQuantity(excessBlobGas)); } } @@ -134,8 +144,11 @@ public static ExecutionPayload from(EthBlock.Block block) { block.getExtraData(), block.getBaseFeePerGas(), block.getHash(), + encodedTxs, block.getWithdrawals(), - encodedTxs); + // TODO: + null, + null); } /** @@ -159,30 +172,34 @@ public static ExecutionPayload from(ExecutionPayloadSSZ payload) { Numeric.toHexString(payload.extraData().toArray()), payload.baseFeePerGas().toBigInteger(), Numeric.toHexString(payload.blockHash().toArray()), - payload.withdrawals(), payload.transactions().stream() .map(bytes -> Numeric.toHexString(bytes.toArray())) - .collect(Collectors.toList())); + .collect(Collectors.toList()), + payload.withdrawals(), + payload.blobGasUsed() == null ? null : BigInteger.valueOf(payload.blobGasUsed()), + payload.excessBlobGas() == null ? null : BigInteger.valueOf(payload.excessBlobGas())); } /** * The type Execution payload req. * - * @param parentHash A 32 byte hash of the parent payload. - * @param feeRecipient A 20 byte hash (aka Address) for the feeRecipient field of the new payload. - * @param stateRoot A 32 byte state root hash. - * @param receiptsRoot A 32 byte receipt root hash. - * @param logsBloom A 32 byte logs bloom filter. - * @param prevRandao A 32 byte beacon chain randomness value. - * @param blockNumber A 64-bit number for the current block index. - * @param gasLimit A 64-bit value for the gas limit. - * @param gasUsed A 64-bit value for the gas used. - * @param timestamp A 64-bit value for the timestamp field of the new payload. - * @param extraData 0 to 32 byte value for extra data. + * @param parentHash A 32 byte hash of the parent payload. + * @param feeRecipient A 20 byte hash (aka Address) for the feeRecipient field of the new payload. + * @param stateRoot A 32 byte state root hash. + * @param receiptsRoot A 32 byte receipt root hash. + * @param logsBloom A 32 byte logs bloom filter. + * @param prevRandao A 32 byte beacon chain randomness value. + * @param blockNumber A 64-bit number for the current block index. + * @param gasLimit A 64-bit value for the gas limit. + * @param gasUsed A 64-bit value for the gas used. + * @param timestamp A 64-bit value for the timestamp field of the new payload. + * @param extraData 0 to 32 byte value for extra data. * @param baseFeePerGas 256 bits for the base fee per gas. - * @param blockHash The 32 byte block hash. - * @param withdrawals The withdrawals list. - * @param transactions An array of transaction objects where each object is a byte list. + * @param blockHash The 32 byte block hash. + * @param withdrawals The withdrawals list. + * @param transactions An array of transaction objects where each object is a byte list. + * @param blobGasUsed The gas used by the blob. + * @param excessBlobGas The excess gas used by the blob. */ public record ExecutionPayloadReq( String parentHash, @@ -198,8 +215,10 @@ public record ExecutionPayloadReq( String extraData, String baseFeePerGas, String blockHash, + List transactions, List withdrawals, - List transactions) {} + String blobGasUsed, + String excessBlobGas) {} /** * To req execution payload req. @@ -221,8 +240,10 @@ public ExecutionPayloadReq toReq() { extraData, Numeric.toHexStringWithPrefix(baseFeePerGas), blockHash, + transactions, withdrawals, - transactions); + blobGasUsed == null ? null : Numeric.toHexStringWithPrefix(blobGasUsed), + excessBlobGas == null ? null : Numeric.toHexStringWithPrefix(excessBlobGas)); } /** @@ -231,23 +252,23 @@ public ExecutionPayloadReq toReq() { *

L2 extended payload attributes for Optimism. For more details, visit the [Optimism specs](...). * - * @param timestamp 64 bit value for the timestamp field of the new payload. - * @param prevRandao 32 byte value for the prevRandao field of the new payload. + * @param timestamp 64 bit value for the timestamp field of the new payload. + * @param prevRandao 32 byte value for the prevRandao field of the new payload. * @param suggestedFeeRecipient 20 bytes suggested value for the feeRecipient field of the new - * payload. - * @param transactions List of transactions to be included in the new payload. - * @param withdrawals List of withdrawals to be included in the new payload. - * @param noTxPool Boolean value indicating whether the payload should be built without including - * transactions from the txpool. - * @param gasLimit 64 bit value for the gasLimit field of the new payload.The gasLimit is optional - * w.r.t. compatibility with L1, but required when used as rollup.This field overrides the gas - * limit used during block-building.If not specified as rollup, a STATUS_INVALID is returned. - * @param epoch The batch epoch number from derivation. This value is not expected by the engine - * is skipped during serialization and deserialization. - * @param l1InclusionBlock The L1 block number when this batch was first fully derived. This value - * is not expected by the engine and is skipped during serialization and deserialization. - * @param seqNumber The L2 sequence number of the block. This value is not expected by the engine - * and is skipped during serialization and deserialization. + * payload. + * @param transactions List of transactions to be included in the new payload. + * @param withdrawals List of withdrawals to be included in the new payload. + * @param noTxPool Boolean value indicating whether the payload should be built without including + * transactions from the txpool. + * @param gasLimit 64 bit value for the gasLimit field of the new payload.The gasLimit is optional + * w.r.t. compatibility with L1, but required when used as rollup.This field overrides the gas + * limit used during block-building.If not specified as rollup, a STATUS_INVALID is returned. + * @param epoch The batch epoch number from derivation. This value is not expected by the engine + * is skipped during serialization and deserialization. + * @param l1InclusionBlock The L1 block number when this batch was first fully derived. This value + * is not expected by the engine and is skipped during serialization and deserialization. + * @param seqNumber The L2 sequence number of the block. This value is not expected by the engine + * and is skipped during serialization and deserialization. * @author zhouop0 * @since 0.1.0 */ @@ -266,8 +287,8 @@ public record PayloadAttributes( /** * The type Epoch req. * - * @param number the number - * @param hash the hash + * @param number the number + * @param hash the hash * @param timestamp the timestamp */ public record EpochReq(String number, String hash, String timestamp) {} @@ -275,13 +296,13 @@ public record EpochReq(String number, String hash, String timestamp) {} /** * The type Payload attributes req. * - * @param timestamp the timestamp - * @param prevRandao the prev randao + * @param timestamp the timestamp + * @param prevRandao the prev randao * @param suggestedFeeRecipient the suggested fee recipient - * @param transactions the transactions - * @param withdrawals the withdrawals - * @param noTxPool the no tx pool - * @param gasLimit the gas limit + * @param transactions the transactions + * @param withdrawals the withdrawals + * @param noTxPool the no tx pool + * @param gasLimit the gas limit */ public record PayloadAttributesReq( String timestamp, @@ -316,15 +337,25 @@ public PayloadAttributesReq toReq() { * @since 0.1.0 */ public enum Status { - /** Valid status. */ + /** + * Valid status. + */ VALID, - /** Invalid status. */ + /** + * Invalid status. + */ INVALID, - /** Syncing status. */ + /** + * Syncing status. + */ SYNCING, - /** Accepted status. */ + /** + * Accepted status. + */ ACCEPTED, - /** Invalid block hash status. */ + /** + * Invalid block hash status. + */ INVALID_BLOCK_HASH, } @@ -345,7 +376,9 @@ public static class PayloadStatus { private String latestValidHash; private String validationError; - /** PayloadStatus constructor. */ + /** + * PayloadStatus constructor. + */ public PayloadStatus() {} /** diff --git a/hildr-node/src/main/java/io/optimism/network/AbstractTopicHandler.java b/hildr-node/src/main/java/io/optimism/network/AbstractTopicHandler.java new file mode 100644 index 00000000..a57e8397 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/network/AbstractTopicHandler.java @@ -0,0 +1,440 @@ +package io.optimism.network; + +import static org.web3j.crypto.Sign.recoverFromSignature; +import static org.web3j.utils.Assertions.verifyPrecondition; + +import com.google.common.cache.Cache; +import com.google.common.cache.CacheBuilder; +import io.libp2p.core.pubsub.ValidationResult; +import io.optimism.engine.ExecutionPayload; +import java.math.BigInteger; +import java.security.SignatureException; +import java.time.Instant; +import java.util.concurrent.CompletionStage; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.concurrent.RejectedExecutionException; +import org.apache.commons.lang3.ArrayUtils; +import org.apache.commons.lang3.exception.ExceptionUtils; +import org.apache.tuweni.bytes.Bytes; +import org.apache.tuweni.bytes.Bytes32; +import org.apache.tuweni.units.bigints.UInt64; +import org.bouncycastle.util.Arrays; +import org.jctools.queues.MessagePassingQueue; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.web3j.crypto.ECDSASignature; +import org.web3j.crypto.Hash; +import org.web3j.crypto.Keys; +import org.web3j.crypto.Sign; +import org.web3j.utils.Numeric; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.infrastructure.async.SafeFuture; +import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessage; +import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessageFactory; +import tech.pegasys.teku.networking.p2p.libp2p.config.LibP2PParamsFactory; +import tech.pegasys.teku.service.serviceutils.ServiceCapacityExceededException; +import tech.pegasys.teku.statetransition.validation.InternalValidationResult; + +/** + * The type Abstract topic handler. + * + * @author grapebaba + * @since 0.2.6 + */ +public abstract class AbstractTopicHandler implements NamedTopicHandler { + + private static final Logger LOGGER = LoggerFactory.getLogger(AbstractTopicHandler.class); + + private final PreparedGossipMessageFactory preparedGossipMessageFactory; + + private final String topic; + + private final AsyncRunner asyncRunner; + + private final UInt64 chainId; + + private final String unsafeBlockSigner; + + private final MessagePassingQueue unsafeBlockQueue; + + private final BlockVersion version; + + private final Cache> cache; + + /** + * Instantiates a new Abstract topic handler. + * + * @param preparedGossipMessageFactory the prepared gossip message factory + * @param topic the topic + * @param asyncRunner the async runner + * @param chainId the chain id + * @param unsafeBlockSigner the unsafe block signer + * @param unsafeBlockQueue the unsafe block queue + * @param version the version + */ + protected AbstractTopicHandler( + PreparedGossipMessageFactory preparedGossipMessageFactory, + String topic, + AsyncRunner asyncRunner, + UInt64 chainId, + String unsafeBlockSigner, + MessagePassingQueue unsafeBlockQueue, + BlockVersion version) { + this.preparedGossipMessageFactory = preparedGossipMessageFactory; + this.topic = topic; + this.asyncRunner = asyncRunner; + this.chainId = chainId; + this.unsafeBlockSigner = unsafeBlockSigner; + this.unsafeBlockQueue = unsafeBlockQueue; + this.version = version; + this.cache = CacheBuilder.from("maximumSize=1000").build(); + } + + @Override + public PreparedGossipMessage prepareMessage(Bytes payload) { + return preparedGossipMessageFactory.create(topic, payload, null); + } + + @Override + public SafeFuture handleMessage(PreparedGossipMessage message) { + return SafeFuture.of(() -> deserialize(message)) + .thenCompose(deserialized -> + asyncRunner.runAsync(() -> checkBlock(deserialized).thenApply(internalValidation -> { + processMessage(internalValidation, message, deserialized); + return fromInternalValidationResult(internalValidation); + }))) + .exceptionally(error -> handleMessageProcessingError(message, error)); + } + + /** + * Gets topic. + * + * @return the topic + */ + @Override + public String getTopic() { + return topic; + } + + @Override + public int getMaxMessageSize() { + return LibP2PParamsFactory.MAX_COMPRESSED_GOSSIP_SIZE; + } + + /** + * The type BlockMessage. + * + * @param payloadEnvelop the payloadEnvelop + * @param signature the signature + * @param payloadHash the payload hash + */ + public record BlockMessage( + ExecutionPayloadEnvelop payloadEnvelop, Sign.SignatureData signature, byte[] payloadHash) { + + /** + * From block message. + * + * @param data the data + * @param version the version + * @return the block message + */ + public static BlockMessage from(byte[] data, BlockVersion version) { + Sign.SignatureData signature = new Sign.SignatureData( + data[64], + Bytes.wrap(data, 0, 32).toArray(), + Bytes.wrap(data, 32, 32).toArray()); + Bytes payload = Bytes.wrap(ArrayUtils.subarray(data, 65, data.length)); + ExecutionPayloadEnvelop executionPayloadEnvelop; + if (version == BlockVersion.V3) { + executionPayloadEnvelop = ExecutionPayloadEnvelop.from(payload); + } else { + ExecutionPayloadSSZ executionPayloadSSZ = ExecutionPayloadSSZ.from(payload, version); + ExecutionPayload executionPayload = ExecutionPayload.from(executionPayloadSSZ); + executionPayloadEnvelop = new ExecutionPayloadEnvelop(null, executionPayload); + } + byte[] payloadHash = Hash.sha3(payload.toArray()); + + return new BlockMessage(executionPayloadEnvelop, signature, payloadHash); + } + } + + private CompletionStage deserialize(PreparedGossipMessage message) throws DecodingException { + return SafeFuture.completedFuture(decode(message)); + } + + private BlockMessage decode(PreparedGossipMessage message) throws DecodingException { + LOGGER.debug("Received gossip message {} on topic: {}", message, topic); + try { + return BlockMessage.from( + message.getDecodedMessage().getDecodedMessageOrElseThrow().toArray(), version); + } catch (PreparedGossipMessage.GossipDecodingException e) { + LOGGER.error("Failed to decode gossip message", e); + throw new DecodingException("Failed to decode gossip message", e); + } + } + + SafeFuture checkBlock(BlockMessage blockMessage) { + LOGGER.debug("Checking block {}", blockMessage); + + // [REJECT] if the signature by the sequencer is not valid + byte[] msg = signatureMessage(chainId, blockMessage.payloadHash); + try { + BigInteger fromPub = signedMessageHashToKey(msg, blockMessage.signature()); + String from = Numeric.prependHexPrefix(Keys.getAddress(fromPub)); + + if (!from.equalsIgnoreCase(this.unsafeBlockSigner)) { + LOGGER.warn("Block signature is invalid, from: {}, expected: {}", from, this.unsafeBlockSigner); + return SafeFuture.completedFuture( + InternalValidationResult.reject("Block signature is invalid: %s", blockMessage.signature())); + } + + } catch (SignatureException e) { + return SafeFuture.completedFuture( + InternalValidationResult.reject("Block signature is invalid: %s", blockMessage.signature())); + } + + ExecutionPayload executionPayload = blockMessage.payloadEnvelop.executionPayload(); + long now = Instant.now().getEpochSecond(); + + // [REJECT] if the `payload.timestamp` is more than 5 seconds into the future + if (executionPayload.timestamp().longValue() > now + 5) { + LOGGER.warn( + "Block timestamp is too far in the future: {}, now: {}", + executionPayload.timestamp().longValue(), + now); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Block timestamp is too far in the future: %d", + executionPayload.timestamp().longValue())); + } + + // [REJECT] if the `payload.timestamp` is older than 60 seconds in the past + if (executionPayload.timestamp().longValue() < now - 60) { + LOGGER.warn( + "Block timestamp is too far in the past: {}, now: {}", + executionPayload.timestamp().longValue(), + now); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Block timestamp is too far in the past: %d", + executionPayload.timestamp().longValue())); + } + + // TODO: block_hash check + + // [REJECT] if a V1 Block has withdrawals + if (!version.hasWithdrawals() && executionPayload.withdrawals() != null) { + LOGGER.warn("Block withdrawals is not empty"); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Payload is on v1 topic, but has withdrawals. Bad hash: %s, Withdrawals count: %d", + executionPayload.blockHash(), executionPayload.withdrawals().size())); + } + + // [REJECT] if a V2/V3 Block does not have withdrawals + if (version.hasWithdrawals() && executionPayload.withdrawals() == null) { + LOGGER.warn("Block withdrawals is null"); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Payload is on v2/v3 topic, but does not have withdrawals. Bad Hash: %s", + executionPayload.blockHash())); + } + + // [REJECT] if a V2/V3 Block has non-empty withdrawals + if (version.hasWithdrawals() && !executionPayload.withdrawals().isEmpty()) { + LOGGER.warn("Block withdrawals is not empty"); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Payload is on v2/v3 topic, but has withdrawals. Bad hash: %s, Withdrawals count: %d", + executionPayload.blockHash(), executionPayload.withdrawals().size())); + } + + // [REJECT] if the block is on a topic <= V2 and has a blob gas value set + if (!version.hasBlobProperties() && executionPayload.blobGasUsed() != null) { + LOGGER.warn("Block has blob gas value set"); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Payload is on v1/v2 topic, but has blob gas value set. Bad hash: %s, Blob gas: %d", + executionPayload.blockHash(), executionPayload.blobGasUsed().longValue())); + } + + // [REJECT] if the block is on a topic <= V2 and has an excess blob gas value set + if (!version.hasBlobProperties() && executionPayload.excessBlobGas() != null) { + LOGGER.warn("Block has excess blob gas value set"); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Payload is on v1/v2 topic, but has excess blob gas value set. Bad hash: %s, Excess blob gas: %d", + executionPayload.blockHash(), + executionPayload.excessBlobGas().longValue())); + } + + // [REJECT] if the block is on a topic >= V3 and does not have a blob gas value + if (version.hasBlobProperties() && executionPayload.blobGasUsed() == null) { + LOGGER.warn("Block has no blob gas value set"); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Payload is on v3 topic, but has no blob gas value set. Bad hash: %s", + executionPayload.blockHash())); + } + + // [REJECT] if the block is on a topic >= V3 and does not have an excess blob gas value + if (version.hasBlobProperties() && executionPayload.excessBlobGas() == null) { + LOGGER.warn("Block has no excess blob gas value set"); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Payload is on v3 topic, but has no excess blob gas value set. Bad hash: %s", + executionPayload.blockHash())); + } + + // [REJECT] if the block is on a topic >= V3 and has a blob gas used value that is not zero + if (version.hasBlobProperties() && executionPayload.blobGasUsed().longValue() != 0) { + LOGGER.warn("Block has non-zero blob gas value set"); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Payload is on v3 topic, but has non-zero blob gas value set. Bad hash: %s, Blob gas: %d", + executionPayload.blockHash(), executionPayload.blobGasUsed().longValue())); + } + + // [REJECT] if the block is on a topic >= V3 and has an excess blob gas value that is not zero + if (version.hasBlobProperties() && executionPayload.excessBlobGas().longValue() != 0) { + LOGGER.warn("Block has non-zero excess blob gas value set"); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Payload is on v3 topic, but has non-zero excess blob gas value set. Bad hash: %s, Excess blob gas: %d", + executionPayload.blockHash(), + executionPayload.excessBlobGas().longValue())); + } + + // [REJECT] if the block is on a topic >= V3 and the parent beacon block root is nil + if (version.hasParentBeaconBlockRoot() && blockMessage.payloadEnvelop.parentBeaconBlockRoot() == null) { + LOGGER.warn("Block has nil parent beacon block root"); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "Payload is on v3 topic, but has nil parent beacon block root. Bad hash: %s", + executionPayload.blockHash())); + } + + var exist = this.cache.getIfPresent(executionPayload.blockNumber()); + if (exist == null) { + exist = new CopyOnWriteArrayList<>(); + this.cache.put(executionPayload.blockNumber(), exist); + } + + // [REJECT] if more than 5 blocks have been seen with the same block height + if (exist.size() > 5) { + LOGGER.warn("seen too many different blocks at same height: {}", executionPayload.blockNumber()); + return SafeFuture.completedFuture(InternalValidationResult.reject( + "seen too many different blocks at same height: %d", executionPayload.blockNumber())); + } + + // [IGNORE] if the block has already been seen + if (exist.contains(executionPayload.blockHash())) { + LOGGER.warn("Block has been processed before"); + return SafeFuture.completedFuture(InternalValidationResult.ignore( + "Payload has been processed before. Bad hash: %s", executionPayload.blockHash())); + } + + exist.add(executionPayload.blockHash()); + return SafeFuture.completedFuture(InternalValidationResult.ACCEPT); + } + + /** + * Signature message byte [ ]. + * + * @param chainId the chain id + * @param payloadHash the payload hash + * @return the byte [ ] + */ + protected byte[] signatureMessage(UInt64 chainId, byte[] payloadHash) { + Bytes32 domain = Bytes32.ZERO; + byte[] chainIdBytes = Numeric.toBytesPadded(chainId.toBigInteger(), 32); + return Hash.sha3(Arrays.concatenate(domain.toArray(), chainIdBytes, payloadHash)); + } + + /** + * Process message. + * + * @param internalValidationResult the internal validation result + * @param message the message + * @param blockMessage the block message + */ + protected void processMessage( + final InternalValidationResult internalValidationResult, + final PreparedGossipMessage message, + final BlockMessage blockMessage) { + switch (internalValidationResult.code()) { + case REJECT -> LOGGER.warn( + "Rejecting gossip message on topic {}, reason: {}, decoded message: {}", + topic, + internalValidationResult.getDescription(), + message.getDecodedMessage().getDecodedMessage().orElseThrow()); + case IGNORE -> LOGGER.debug("Ignoring message for topic: {}", topic); + case SAVE_FOR_FUTURE -> LOGGER.debug("Deferring message for topic: {}", topic); + case ACCEPT -> { + LOGGER.debug("Accepting message for topic: {}", topic); + this.unsafeBlockQueue.offer(blockMessage.payloadEnvelop.executionPayload()); + } + default -> throw new UnsupportedOperationException( + String.format("Unexpected validation result: %s", internalValidationResult)); + } + } + + /** + * Handle message processing error validation result. + * + * @param message the message + * @param err the err + * @return the validation result + */ + protected ValidationResult handleMessageProcessingError(final PreparedGossipMessage message, final Throwable err) { + final ValidationResult response; + if (ExceptionUtils.hasCause(err, DecodingException.class)) { + LOGGER.warn( + "Failed to decode gossip message on topic {}, raw message: {}, error: {}", + topic, + message.getOriginalMessage(), + err); + response = ValidationResult.Invalid; + } else if (ExceptionUtils.hasCause(err, RejectedExecutionException.class)) { + LOGGER.warn("Discarding gossip message for topic {} because the executor queue is full", topic); + response = ValidationResult.Ignore; + } else if (ExceptionUtils.hasCause(err, ServiceCapacityExceededException.class)) { + LOGGER.warn( + "Discarding gossip message for topic {} because the signature verification queue is full", topic); + response = ValidationResult.Ignore; + } else { + LOGGER.warn("Encountered exception while processing message for topic {}, error: {}", topic, err); + response = ValidationResult.Invalid; + } + + return response; + } + + /** + * From internal validation result validation result. + * + * @param result the result + * @return the validation result + */ + protected static ValidationResult fromInternalValidationResult(InternalValidationResult result) { + return switch (result.code()) { + case ACCEPT -> ValidationResult.Valid; + case SAVE_FOR_FUTURE, IGNORE -> ValidationResult.Ignore; + case REJECT -> ValidationResult.Invalid; + }; + } + + /** + * Signed message hash to key big integer. + * + * @param messageHash the message hash + * @param signatureData the signature data + * @return the big integer + * @throws SignatureException the signature exception + */ + protected static BigInteger signedMessageHashToKey(byte[] messageHash, Sign.SignatureData signatureData) + throws SignatureException { + + byte[] r = signatureData.getR(); + byte[] s = signatureData.getS(); + verifyPrecondition(r != null && r.length == 32, "r must be 32 bytes"); + verifyPrecondition(s != null && s.length == 32, "s must be 32 bytes"); + + ECDSASignature sig = + new ECDSASignature(new BigInteger(1, signatureData.getR()), new BigInteger(1, signatureData.getS())); + + BigInteger key = recoverFromSignature(signatureData.getV()[0], sig, messageHash); + if (key == null) { + throw new SignatureException("Could not recover public key from signature"); + } + return key; + } +} diff --git a/hildr-node/src/main/java/io/optimism/network/BlockV1TopicHandler.java b/hildr-node/src/main/java/io/optimism/network/BlockV1TopicHandler.java index 36e465bc..593b976c 100644 --- a/hildr-node/src/main/java/io/optimism/network/BlockV1TopicHandler.java +++ b/hildr-node/src/main/java/io/optimism/network/BlockV1TopicHandler.java @@ -1,37 +1,10 @@ package io.optimism.network; -import static org.web3j.crypto.Sign.recoverFromSignature; -import static org.web3j.utils.Assertions.verifyPrecondition; - -import io.libp2p.core.pubsub.ValidationResult; import io.optimism.engine.ExecutionPayload; -import java.math.BigInteger; -import java.security.SignatureException; -import java.time.Instant; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.RejectedExecutionException; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt64; -import org.bouncycastle.util.Arrays; import org.jctools.queues.MessagePassingQueue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.web3j.crypto.ECDSASignature; -import org.web3j.crypto.Hash; -import org.web3j.crypto.Keys; -import org.web3j.crypto.Sign; -import org.web3j.utils.Numeric; import tech.pegasys.teku.infrastructure.async.AsyncRunner; -import tech.pegasys.teku.infrastructure.async.SafeFuture; -import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessage; -import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessage.GossipDecodingException; import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessageFactory; -import tech.pegasys.teku.networking.p2p.libp2p.config.LibP2PParamsFactory; -import tech.pegasys.teku.service.serviceutils.ServiceCapacityExceededException; -import tech.pegasys.teku.statetransition.validation.InternalValidationResult; /** * The type BlockV1TopicHandler. @@ -39,35 +12,16 @@ * @author grapebaba * @since 0.1.1 */ -@SuppressWarnings({"checkstyle:VariableDeclarationUsageDistance", "checkstyle:AbbreviationAsWordInName"}) -public class BlockV1TopicHandler implements NamedTopicHandler { - // All fields (4s are offsets to dynamic data) - - // MAX_TRANSACTIONS_PER_PAYLOAD in consensus spec - - private static final Logger LOGGER = LoggerFactory.getLogger(BlockV1TopicHandler.class); - private final PreparedGossipMessageFactory preparedGossipMessageFactory; - - private final String topic; - - private final AsyncRunner asyncRunner; - - private final UInt64 chainId; - - private final String unsafeBlockSigner; - - private final MessagePassingQueue unsafeBlockQueue; +public class BlockV1TopicHandler extends AbstractTopicHandler { /** - * Instantiates a new Block topic handler. + * Instantiates a new BlockV1TopicHandler. * * @param preparedGossipMessageFactory the prepared gossip message factory * @param asyncRunner the async runner * @param chainId the chain id * @param unsafeBlockSigner the unsafe block signer * @param unsafeBlockQueue the unsafe block queue - * @author grapebaba - * @since 0.1.1 */ public BlockV1TopicHandler( PreparedGossipMessageFactory preparedGossipMessageFactory, @@ -75,208 +29,13 @@ public BlockV1TopicHandler( UInt64 chainId, String unsafeBlockSigner, MessagePassingQueue unsafeBlockQueue) { - this.preparedGossipMessageFactory = preparedGossipMessageFactory; - this.topic = String.format("/optimism/%s/0/blocks", chainId.toString()); - this.asyncRunner = asyncRunner; - this.chainId = chainId; - this.unsafeBlockSigner = unsafeBlockSigner; - this.unsafeBlockQueue = unsafeBlockQueue; - } - - @Override - public PreparedGossipMessage prepareMessage(Bytes payload) { - return preparedGossipMessageFactory.create(topic, payload, null); - } - - @Override - public SafeFuture handleMessage(PreparedGossipMessage message) { - return SafeFuture.of(() -> deserialize(message)) - .thenCompose(deserialized -> - asyncRunner.runAsync(() -> checkBlock(deserialized).thenApply(internalValidation -> { - processMessage(internalValidation, message); - return fromInternalValidationResult(internalValidation); - }))) - .exceptionally(error -> handleMessageProcessingError(message, error)); - } - - /** - * Gets topic. - * - * @return the topic - */ - @Override - public String getTopic() { - return topic; - } - - @Override - public int getMaxMessageSize() { - return LibP2PParamsFactory.MAX_COMPRESSED_GOSSIP_SIZE; - } - - /** - * The type BlockMessage. - * - * @param payload the payload - * @param signature the signature - * @param payloadHash the payload hash - */ - public record BlockMessage(ExecutionPayload payload, Sign.SignatureData signature, byte[] payloadHash) { - - /** - * From block message. - * - * @param data the data - * @return the block message - */ - public static BlockMessage from(byte[] data) { - Sign.SignatureData signature = new Sign.SignatureData( - data[64], - Bytes.wrap(data, 0, 32).toArray(), - Bytes.wrap(data, 32, 32).toArray()); - Bytes payload = Bytes.wrap(ArrayUtils.subarray(data, 65, data.length)); - ExecutionPayloadSSZ executionPayloadSSZ = ExecutionPayloadSSZ.from(payload, 0); - ExecutionPayload executionPayload = ExecutionPayload.from(executionPayloadSSZ); - byte[] payloadHash = Hash.sha3(payload.toArray()); - - return new BlockMessage(executionPayload, signature, payloadHash); - } - } - - private CompletionStage deserialize(PreparedGossipMessage message) throws DecodingException { - return SafeFuture.completedFuture(decode(message)); - } - - private BlockMessage decode(PreparedGossipMessage message) throws DecodingException { - LOGGER.debug("Received gossip message {} on topic: {}", message, topic); - try { - return BlockMessage.from( - message.getDecodedMessage().getDecodedMessageOrElseThrow().toArray()); - } catch (GossipDecodingException e) { - LOGGER.error("Failed to decode gossip message", e); - throw new DecodingException("Failed to decode gossip message", e); - } - } - - private SafeFuture checkBlock(BlockMessage blockMessage) { - LOGGER.debug("Checking block {}", blockMessage); - long now = Instant.now().getEpochSecond(); - if (blockMessage.payload.timestamp().longValue() > now + 5) { - LOGGER.debug( - "Block timestamp is too far in the future: {}, now: {}", - blockMessage.payload.timestamp().longValue(), - now); - return SafeFuture.completedFuture(InternalValidationResult.reject( - "Block timestamp is too far in the future: %d", - blockMessage.payload.timestamp().longValue())); - } - - if (blockMessage.payload.timestamp().longValue() < now - 60) { - LOGGER.debug( - "Block timestamp is too far in the past: {}, now: {}", - blockMessage.payload.timestamp().longValue(), - now); - return SafeFuture.completedFuture(InternalValidationResult.reject( - "Block timestamp is too far in the past: %d", - blockMessage.payload.timestamp().longValue())); - } - - if (blockMessage.payload.withdrawals() != null) { - LOGGER.debug("Block contains withdrawals"); - return SafeFuture.completedFuture(InternalValidationResult.reject( - "Payload is on v1 topic, but have withdrawals. Bad Hash: %s", blockMessage.payload.blockHash())); - } - - byte[] msg = signatureMessage(chainId, blockMessage.payloadHash); - - try { - BigInteger fromPub = signedMessageHashToKey(msg, blockMessage.signature()); - String from = Numeric.prependHexPrefix(Keys.getAddress(fromPub)); - - if (!from.equalsIgnoreCase(this.unsafeBlockSigner)) { - LOGGER.debug("Block signature is invalid, from: {}, expected: {}", from, this.unsafeBlockSigner); - return SafeFuture.completedFuture( - InternalValidationResult.reject("Block signature is invalid: %s", blockMessage.signature())); - } - - } catch (SignatureException e) { - return SafeFuture.completedFuture( - InternalValidationResult.reject("Block signature is invalid: %s", blockMessage.signature())); - } - - this.unsafeBlockQueue.offer(blockMessage.payload); - return SafeFuture.completedFuture(InternalValidationResult.ACCEPT); - } - - private byte[] signatureMessage(UInt64 chainId, byte[] payloadHash) { - Bytes32 domain = Bytes32.ZERO; - byte[] chainIdBytes = Numeric.toBytesPadded(chainId.toBigInteger(), 32); - return Hash.sha3(Arrays.concatenate(domain.toArray(), chainIdBytes, payloadHash)); - } - - private void processMessage( - final InternalValidationResult internalValidationResult, final PreparedGossipMessage message) { - switch (internalValidationResult.code()) { - case REJECT -> LOGGER.warn( - "Rejecting gossip message on topic {}, reason: {}, decoded message: {}", - topic, - internalValidationResult.getDescription(), - message.getDecodedMessage().getDecodedMessage().orElseThrow()); - case IGNORE -> LOGGER.debug("Ignoring message for topic: {}", topic); - case SAVE_FOR_FUTURE -> LOGGER.debug("Deferring message for topic: {}", topic); - case ACCEPT -> LOGGER.debug("Accepting message for topic: {}", topic); - default -> throw new UnsupportedOperationException( - String.format("Unexpected validation result: %s", internalValidationResult)); - } - } - - private ValidationResult handleMessageProcessingError(final PreparedGossipMessage message, final Throwable err) { - final ValidationResult response; - if (ExceptionUtils.hasCause(err, DecodingException.class)) { - LOGGER.warn( - "Failed to decode gossip message on topic {}, raw message: {}, error: {}", - topic, - message.getOriginalMessage(), - err); - response = ValidationResult.Invalid; - } else if (ExceptionUtils.hasCause(err, RejectedExecutionException.class)) { - LOGGER.warn("Discarding gossip message for topic {} because the executor queue is full", topic); - response = ValidationResult.Ignore; - } else if (ExceptionUtils.hasCause(err, ServiceCapacityExceededException.class)) { - LOGGER.warn( - "Discarding gossip message for topic {} because the signature verification queue is full", topic); - response = ValidationResult.Ignore; - } else { - LOGGER.warn("Encountered exception while processing message for topic {}, error: {}", topic, err); - response = ValidationResult.Invalid; - } - - return response; - } - - private static ValidationResult fromInternalValidationResult(InternalValidationResult result) { - return switch (result.code()) { - case ACCEPT -> ValidationResult.Valid; - case SAVE_FOR_FUTURE, IGNORE -> ValidationResult.Ignore; - case REJECT -> ValidationResult.Invalid; - }; - } - - private static BigInteger signedMessageHashToKey(byte[] messageHash, Sign.SignatureData signatureData) - throws SignatureException { - - byte[] r = signatureData.getR(); - byte[] s = signatureData.getS(); - verifyPrecondition(r != null && r.length == 32, "r must be 32 bytes"); - verifyPrecondition(s != null && s.length == 32, "s must be 32 bytes"); - - ECDSASignature sig = - new ECDSASignature(new BigInteger(1, signatureData.getR()), new BigInteger(1, signatureData.getS())); - - BigInteger key = recoverFromSignature(signatureData.getV()[0], sig, messageHash); - if (key == null) { - throw new SignatureException("Could not recover public key from signature"); - } - return key; + super( + preparedGossipMessageFactory, + String.format("/optimism/%s/%d/blocks", chainId.toString(), BlockVersion.V1.getVersion()), + asyncRunner, + chainId, + unsafeBlockSigner, + unsafeBlockQueue, + BlockVersion.V1); } } diff --git a/hildr-node/src/main/java/io/optimism/network/BlockV2TopicHandler.java b/hildr-node/src/main/java/io/optimism/network/BlockV2TopicHandler.java index aa795c0a..30693409 100644 --- a/hildr-node/src/main/java/io/optimism/network/BlockV2TopicHandler.java +++ b/hildr-node/src/main/java/io/optimism/network/BlockV2TopicHandler.java @@ -1,73 +1,27 @@ package io.optimism.network; -import static org.web3j.crypto.Sign.recoverFromSignature; -import static org.web3j.utils.Assertions.verifyPrecondition; - -import io.libp2p.core.pubsub.ValidationResult; import io.optimism.engine.ExecutionPayload; -import java.math.BigInteger; -import java.security.SignatureException; -import java.time.Instant; -import java.util.concurrent.CompletionStage; -import java.util.concurrent.RejectedExecutionException; -import org.apache.commons.lang3.ArrayUtils; -import org.apache.commons.lang3.exception.ExceptionUtils; -import org.apache.tuweni.bytes.Bytes; -import org.apache.tuweni.bytes.Bytes32; import org.apache.tuweni.units.bigints.UInt64; -import org.bouncycastle.util.Arrays; import org.jctools.queues.MessagePassingQueue; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.web3j.crypto.ECDSASignature; -import org.web3j.crypto.Hash; -import org.web3j.crypto.Keys; -import org.web3j.crypto.Sign; -import org.web3j.utils.Numeric; import tech.pegasys.teku.infrastructure.async.AsyncRunner; -import tech.pegasys.teku.infrastructure.async.SafeFuture; -import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessage; -import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessage.GossipDecodingException; import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessageFactory; -import tech.pegasys.teku.networking.p2p.libp2p.config.LibP2PParamsFactory; -import tech.pegasys.teku.service.serviceutils.ServiceCapacityExceededException; -import tech.pegasys.teku.statetransition.validation.InternalValidationResult; /** * The type BlockV2TopicHandler. * * @author grapebaba - * @since 0.2.0 + * @since 0.2.4 */ -@SuppressWarnings({"checkstyle:VariableDeclarationUsageDistance", "checkstyle:AbbreviationAsWordInName"}) -public class BlockV2TopicHandler implements NamedTopicHandler { - // All fields (4s are offsets to dynamic data) - - // MAX_TRANSACTIONS_PER_PAYLOAD in consensus spec - - private static final Logger LOGGER = LoggerFactory.getLogger(BlockV1TopicHandler.class); - private final PreparedGossipMessageFactory preparedGossipMessageFactory; - - private final String topic; - - private final AsyncRunner asyncRunner; - - private final UInt64 chainId; - - private final String unsafeBlockSigner; - - private final MessagePassingQueue unsafeBlockQueue; +public class BlockV2TopicHandler extends AbstractTopicHandler { /** - * Instantiates a new Block topic handler. + * Instantiates a new BlockV2TopicHandler. * * @param preparedGossipMessageFactory the prepared gossip message factory * @param asyncRunner the async runner * @param chainId the chain id * @param unsafeBlockSigner the unsafe block signer * @param unsafeBlockQueue the unsafe block queue - * @author grapebaba - * @since 0.2.0 */ public BlockV2TopicHandler( PreparedGossipMessageFactory preparedGossipMessageFactory, @@ -75,217 +29,13 @@ public BlockV2TopicHandler( UInt64 chainId, String unsafeBlockSigner, MessagePassingQueue unsafeBlockQueue) { - this.preparedGossipMessageFactory = preparedGossipMessageFactory; - this.topic = String.format("/optimism/%s/1/blocks", chainId.toString()); - this.asyncRunner = asyncRunner; - this.chainId = chainId; - this.unsafeBlockSigner = unsafeBlockSigner; - this.unsafeBlockQueue = unsafeBlockQueue; - } - - @Override - public PreparedGossipMessage prepareMessage(Bytes payload) { - return preparedGossipMessageFactory.create(topic, payload, null); - } - - @Override - public SafeFuture handleMessage(PreparedGossipMessage message) { - return SafeFuture.of(() -> deserialize(message)) - .thenCompose(deserialized -> - asyncRunner.runAsync(() -> checkBlock(deserialized).thenApply(internalValidation -> { - processMessage(internalValidation, message); - return fromInternalValidationResult(internalValidation); - }))) - .exceptionally(error -> handleMessageProcessingError(message, error)); - } - - /** - * Gets topic. - * - * @return the topic - */ - @Override - public String getTopic() { - return topic; - } - - @Override - public int getMaxMessageSize() { - return LibP2PParamsFactory.MAX_COMPRESSED_GOSSIP_SIZE; - } - - /** - * The type BlockMessage. - * - * @param payload the payload - * @param signature the signature - * @param payloadHash the payload hash - */ - public record BlockMessage(ExecutionPayload payload, Sign.SignatureData signature, byte[] payloadHash) { - - /** - * From block message. - * - * @param data the data - * @return the block message - */ - public static BlockMessage from(byte[] data) { - Sign.SignatureData signature = new Sign.SignatureData( - data[64], - Bytes.wrap(data, 0, 32).toArray(), - Bytes.wrap(data, 32, 32).toArray()); - Bytes payload = Bytes.wrap(ArrayUtils.subarray(data, 65, data.length)); - ExecutionPayloadSSZ executionPayloadSSZ = ExecutionPayloadSSZ.from(payload, 1); - ExecutionPayload executionPayload = ExecutionPayload.from(executionPayloadSSZ); - byte[] payloadHash = Hash.sha3(payload.toArray()); - - return new BlockMessage(executionPayload, signature, payloadHash); - } - } - - private CompletionStage deserialize(PreparedGossipMessage message) throws DecodingException { - return SafeFuture.completedFuture(decode(message)); - } - - private BlockMessage decode(PreparedGossipMessage message) throws DecodingException { - LOGGER.debug("Received gossip message {} on topic: {}", message, topic); - try { - return BlockMessage.from( - message.getDecodedMessage().getDecodedMessageOrElseThrow().toArray()); - } catch (GossipDecodingException e) { - LOGGER.error("Failed to decode gossip message", e); - throw new DecodingException("Failed to decode gossip message", e); - } - } - - private SafeFuture checkBlock(BlockMessage blockMessage) { - LOGGER.debug("Checking block {}", blockMessage); - long now = Instant.now().getEpochSecond(); - if (blockMessage.payload.timestamp().longValue() > now + 5) { - LOGGER.debug( - "Block timestamp is too far in the future: {}, now: {}", - blockMessage.payload.timestamp().longValue(), - now); - return SafeFuture.completedFuture(InternalValidationResult.reject( - "Block timestamp is too far in the future: %d", - blockMessage.payload.timestamp().longValue())); - } - - if (blockMessage.payload.timestamp().longValue() < now - 60) { - LOGGER.debug( - "Block timestamp is too far in the past: {}, now: {}", - blockMessage.payload.timestamp().longValue(), - now); - return SafeFuture.completedFuture(InternalValidationResult.reject( - "Block timestamp is too far in the past: %d", - blockMessage.payload.timestamp().longValue())); - } - - if (blockMessage.payload.withdrawals() == null) { - LOGGER.debug("Block withdrawals is null"); - return SafeFuture.completedFuture(InternalValidationResult.reject( - "Payload is on v2 topic, but does not have withdrawals. Bad Hash: %s", - blockMessage.payload.blockHash())); - } - - if (!blockMessage.payload.withdrawals().isEmpty()) { - LOGGER.debug("Block withdrawals is not empty"); - return SafeFuture.completedFuture(InternalValidationResult.reject( - "Payload is on v2 topic, but does not have withdrawals. Bad hash: %s, Withdrawl count: %d", - blockMessage.payload.blockHash(), - blockMessage.payload.withdrawals().size())); - } - - byte[] msg = signatureMessage(chainId, blockMessage.payloadHash); - - try { - BigInteger fromPub = signedMessageHashToKey(msg, blockMessage.signature()); - String from = Numeric.prependHexPrefix(Keys.getAddress(fromPub)); - - if (!from.equalsIgnoreCase(this.unsafeBlockSigner)) { - LOGGER.debug("Block signature is invalid, from: {}, expected: {}", from, this.unsafeBlockSigner); - return SafeFuture.completedFuture( - InternalValidationResult.reject("Block signature is invalid: %s", blockMessage.signature())); - } - - } catch (SignatureException e) { - return SafeFuture.completedFuture( - InternalValidationResult.reject("Block signature is invalid: %s", blockMessage.signature())); - } - - this.unsafeBlockQueue.offer(blockMessage.payload); - return SafeFuture.completedFuture(InternalValidationResult.ACCEPT); - } - - private byte[] signatureMessage(UInt64 chainId, byte[] payloadHash) { - Bytes32 domain = Bytes32.ZERO; - byte[] chainIdBytes = Numeric.toBytesPadded(chainId.toBigInteger(), 32); - return Hash.sha3(Arrays.concatenate(domain.toArray(), chainIdBytes, payloadHash)); - } - - private void processMessage( - final InternalValidationResult internalValidationResult, final PreparedGossipMessage message) { - switch (internalValidationResult.code()) { - case REJECT -> LOGGER.warn( - "Rejecting gossip message on topic {}, reason: {}, decoded message: {}", - topic, - internalValidationResult.getDescription(), - message.getDecodedMessage().getDecodedMessage().orElseThrow()); - case IGNORE -> LOGGER.debug("Ignoring message for topic: {}", topic); - case SAVE_FOR_FUTURE -> LOGGER.debug("Deferring message for topic: {}", topic); - case ACCEPT -> LOGGER.debug("Accepting message for topic: {}", topic); - default -> throw new UnsupportedOperationException( - String.format("Unexpected validation result: %s", internalValidationResult)); - } - } - - private ValidationResult handleMessageProcessingError(final PreparedGossipMessage message, final Throwable err) { - final ValidationResult response; - if (ExceptionUtils.hasCause(err, DecodingException.class)) { - LOGGER.warn( - "Failed to decode gossip message on topic {}, raw message: {}, error: {}", - topic, - message.getOriginalMessage(), - err); - response = ValidationResult.Invalid; - } else if (ExceptionUtils.hasCause(err, RejectedExecutionException.class)) { - LOGGER.warn("Discarding gossip message for topic {} because the executor queue is full", topic); - response = ValidationResult.Ignore; - } else if (ExceptionUtils.hasCause(err, ServiceCapacityExceededException.class)) { - LOGGER.warn( - "Discarding gossip message for topic {} because the signature verification queue is full", topic); - response = ValidationResult.Ignore; - } else { - LOGGER.warn("Encountered exception while processing message for topic {}, error: {}", topic, err); - response = ValidationResult.Invalid; - } - - return response; - } - - private static ValidationResult fromInternalValidationResult(InternalValidationResult result) { - return switch (result.code()) { - case ACCEPT -> ValidationResult.Valid; - case SAVE_FOR_FUTURE, IGNORE -> ValidationResult.Ignore; - case REJECT -> ValidationResult.Invalid; - }; - } - - private static BigInteger signedMessageHashToKey(byte[] messageHash, Sign.SignatureData signatureData) - throws SignatureException { - - byte[] r = signatureData.getR(); - byte[] s = signatureData.getS(); - verifyPrecondition(r != null && r.length == 32, "r must be 32 bytes"); - verifyPrecondition(s != null && s.length == 32, "s must be 32 bytes"); - - ECDSASignature sig = - new ECDSASignature(new BigInteger(1, signatureData.getR()), new BigInteger(1, signatureData.getS())); - - BigInteger key = recoverFromSignature(signatureData.getV()[0], sig, messageHash); - if (key == null) { - throw new SignatureException("Could not recover public key from signature"); - } - return key; + super( + preparedGossipMessageFactory, + String.format("/optimism/%s/%d/blocks", chainId.toString(), BlockVersion.V2.getVersion()), + asyncRunner, + chainId, + unsafeBlockSigner, + unsafeBlockQueue, + BlockVersion.V2); } } diff --git a/hildr-node/src/main/java/io/optimism/network/BlockV3TopicHandler.java b/hildr-node/src/main/java/io/optimism/network/BlockV3TopicHandler.java new file mode 100644 index 00000000..d4ef7031 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/network/BlockV3TopicHandler.java @@ -0,0 +1,41 @@ +package io.optimism.network; + +import io.optimism.engine.ExecutionPayload; +import org.apache.tuweni.units.bigints.UInt64; +import org.jctools.queues.MessagePassingQueue; +import tech.pegasys.teku.infrastructure.async.AsyncRunner; +import tech.pegasys.teku.networking.p2p.gossip.PreparedGossipMessageFactory; + +/** + * The type BlockV3TopicHandler. + * + * @author grapebaba + * @since 0.2.6 + */ +public class BlockV3TopicHandler extends AbstractTopicHandler { + + /** + * Instantiates a new BlockV3TopicHandler. + * + * @param preparedGossipMessageFactory the prepared gossip message factory + * @param asyncRunner the async runner + * @param chainId the chain id + * @param unsafeBlockSigner the unsafe block signer + * @param unsafeBlockQueue the unsafe block queue + */ + public BlockV3TopicHandler( + PreparedGossipMessageFactory preparedGossipMessageFactory, + AsyncRunner asyncRunner, + UInt64 chainId, + String unsafeBlockSigner, + MessagePassingQueue unsafeBlockQueue) { + super( + preparedGossipMessageFactory, + String.format("/optimism/%s/%d/blocks", chainId.toString(), BlockVersion.V3.getVersion()), + asyncRunner, + chainId, + unsafeBlockSigner, + unsafeBlockQueue, + BlockVersion.V3); + } +} diff --git a/hildr-node/src/main/java/io/optimism/network/BlockVersion.java b/hildr-node/src/main/java/io/optimism/network/BlockVersion.java new file mode 100644 index 00000000..d9f30f56 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/network/BlockVersion.java @@ -0,0 +1,64 @@ +package io.optimism.network; + +/** + * The type BlockVersion. + * + * @author grapebaba + * @since 0.2.6 + */ +public enum BlockVersion { + /** + * V1 block version. + */ + V1(0), + /** + * V2 block version. + */ + V2(1), + /** + * V3 block version. + */ + V3(2); + + private final int version; + + BlockVersion(int version) { + this.version = version; + } + + /** + * Has withdrawals boolean. + * + * @return the boolean + */ + boolean hasWithdrawals() { + return this == V2 || this == V3; + } + + /** + * Has blob properties boolean. + * + * @return the boolean + */ + boolean hasBlobProperties() { + return this == V3; + } + + /** + * Has parent beacon block root boolean. + * + * @return the boolean + */ + boolean hasParentBeaconBlockRoot() { + return this == V3; + } + + /** + * Gets version. + * + * @return the version + */ + int getVersion() { + return version; + } +} diff --git a/hildr-node/src/main/java/io/optimism/network/ExecutionPayloadEnvelop.java b/hildr-node/src/main/java/io/optimism/network/ExecutionPayloadEnvelop.java new file mode 100644 index 00000000..b7f65557 --- /dev/null +++ b/hildr-node/src/main/java/io/optimism/network/ExecutionPayloadEnvelop.java @@ -0,0 +1,34 @@ +package io.optimism.network; + +import io.optimism.engine.ExecutionPayload; +import org.apache.tuweni.bytes.Bytes; + +/** + * The type ExecutionPayloadEnvelop. + * + * @param parentBeaconBlockRoot the parent beacon block root + * @param executionPayload the execution payload + * @author grapebaba + * @since 0.2.6 + */ +public record ExecutionPayloadEnvelop(Bytes parentBeaconBlockRoot, ExecutionPayload executionPayload) { + + /** + * From execution payload envelop. + * + * @param data the data + * @return the execution payload envelop + */ + public static ExecutionPayloadEnvelop from(Bytes data) { + if (data.size() < 32) { + throw new IllegalArgumentException( + "scope too small to decode execution payloadEnvelop envelope: %d".formatted(data.size())); + } + + Bytes parentBeaconBlockRoot = data.slice(0, 32); + + ExecutionPayloadSSZ executionPayloadSSZ = ExecutionPayloadSSZ.from(data.slice(32), BlockVersion.V3); + ExecutionPayload executionPayload = ExecutionPayload.from(executionPayloadSSZ); + return new ExecutionPayloadEnvelop(parentBeaconBlockRoot, executionPayload); + } +} diff --git a/hildr-node/src/main/java/io/optimism/network/ExecutionPayloadSSZ.java b/hildr-node/src/main/java/io/optimism/network/ExecutionPayloadSSZ.java index 5ba80ed9..f7252a93 100644 --- a/hildr-node/src/main/java/io/optimism/network/ExecutionPayloadSSZ.java +++ b/hildr-node/src/main/java/io/optimism/network/ExecutionPayloadSSZ.java @@ -26,8 +26,10 @@ * @param extraData the extra data * @param baseFeePerGas the base fee per gas * @param blockHash the block hash - * @param withdrawals the withdrawals * @param transactions the transactions + * @param withdrawals the withdrawals + * @param blobGasUsed the blob gas used + * @param excessBlobGas the excess blob gas * @author grapebaba * @since 0.2.0 */ @@ -45,8 +47,10 @@ public record ExecutionPayloadSSZ( Bytes extraData, UInt256 baseFeePerGas, Bytes blockHash, + List transactions, List withdrawals, - List transactions) { + Long blobGasUsed, + Long excessBlobGas) { /** * The constant EXECUTION_PAYLOAD_FIXED_PART. */ @@ -54,9 +58,11 @@ public record ExecutionPayloadSSZ( private static final int EXECUTION_PAYLOAD_FIXED_PART_V1 = 32 + 20 + 32 + 32 + 256 + 32 + 8 + 8 + 8 + 8 + 4 + 32 + 32 + 4; - // additional 4 bytes for withdrawals offset + // V1 + Withdrawals offset private static final int EXECUTION_PAYLOAD_FIXED_PART_V2 = EXECUTION_PAYLOAD_FIXED_PART_V1 + 4; + // V2 + BlobGasUed + ExcessBlobGas + private static final int EXECUTION_PAYLOAD_FIXED_PART_V3 = EXECUTION_PAYLOAD_FIXED_PART_V2 + 8 + 8; private static final int WITHDRAWAL_SIZE = 8 + 8 + 20 + 8; private static final int MAX_WITHDRAWALS_PER_PAYLOAD = 1 << 4; @@ -73,7 +79,7 @@ public record ExecutionPayloadSSZ( * @param version the version * @return the execution payload ssz */ - public static ExecutionPayloadSSZ from(Bytes data, int version) { + public static ExecutionPayloadSSZ from(Bytes data, BlockVersion version) { final int dataSize = data.size(); final int fixedPart = executionPayloadFixedPart(version); if (dataSize < fixedPart) { @@ -120,26 +126,31 @@ public static ExecutionPayloadSSZ from(Bytes data, int version) { } offset += 4; - if (version == 0 && offset != fixedPart) { - throw new IllegalArgumentException( - String.format("unexpected offset: %d <> %d, version: %d", offset, fixedPart, version)); + if (version == BlockVersion.V1 && offset != fixedPart) { + throw new IllegalArgumentException(String.format( + "unexpected offset: %d <> %d, version: %d", offset, fixedPart, version.getVersion())); } long withdrawalsOffset = dataSize; - if (version == 1) { + if (version.hasWithdrawals()) { withdrawalsOffset = sszReader.readUInt32(); + offset += 4; if (withdrawalsOffset < transactionsOffset) { throw new IllegalArgumentException(String.format( "unexpected withdrawals offset: %d < %d", withdrawalsOffset, transactionsOffset)); } - offset += 4; } - if (version == 1 && offset != fixedPart) { - throw new IllegalArgumentException( - String.format("unexpected offset: %d <> %d, version: %d", offset, fixedPart, version)); + Long blobGasUsed = null; + Long ExcessBlobGas = null; + if (version == BlockVersion.V3) { + blobGasUsed = sszReader.readUInt64(); + offset += 8; + ExcessBlobGas = sszReader.readUInt64(); } + // var _ = offset; // for future extensions: we keep the offset accurate for extensions + if (transactionsOffset > extraDataOffset + 32 || transactionsOffset > dataSize) { throw new IllegalArgumentException( String.format("extra-data is too large: %d", transactionsOffset - extraDataOffset)); @@ -150,11 +161,16 @@ public static ExecutionPayloadSSZ from(Bytes data, int version) { extraData = sszReader.readFixedBytes((int) (transactionsOffset - extraDataOffset)); } - Bytes transactionsData = sszReader.readFixedBytes((int) (withdrawalsOffset - transactionsOffset)); - List transactions = unmarshalTransactions(transactionsData); + List transactions; + if (sszReader.isComplete()) { + transactions = List.of(); + } else { + Bytes transactionsData = sszReader.readFixedBytes((int) (withdrawalsOffset - transactionsOffset)); + transactions = unmarshalTransactions(transactionsData); + } List withdrawals = null; - if (version == 1) { + if (version.hasWithdrawals()) { if (withdrawalsOffset > dataSize) { throw new IllegalArgumentException( String.format("withdrawals is too large: %d", withdrawalsOffset - dataSize)); @@ -182,8 +198,10 @@ public static ExecutionPayloadSSZ from(Bytes data, int version) { extraData, baseFeePerGas, blockHash, + transactions, withdrawals, - transactions); + blobGasUsed, + ExcessBlobGas); }); } @@ -285,26 +303,29 @@ private static List unmarshalTransactions(Bytes data) { @Override public String toString() { - return "ExecutionPayloadSSZ{" + "parentHash=" - + parentHash + ", feeRecipient=" - + feeRecipient + ", stateRoot=" - + stateRoot + ", receiptsRoot=" - + receiptsRoot + ", logsBloom=" - + logsBloom + ", prevRandao=" - + prevRandao + ", blockNumber=" - + blockNumber + ", gasLimit=" - + gasLimit + ", gasUsed=" - + gasUsed + ", timestamp=" - + timestamp + ", extraData=" - + extraData + ", baseFeePerGas=" - + baseFeePerGas + ", blockHash=" - + blockHash + ", withdrawals=" - + withdrawals + ", transactions=" - + transactions + '}'; + return "ExecutionPayloadSSZ{parentHash=%s, feeRecipient=%s, stateRoot=%s, receiptsRoot=%s, logsBloom=%s, prevRandao=%s, blockNumber=%d, gasLimit=%d, gasUsed=%d, timestamp=%d, extraData=%s, baseFeePerGas=%s, blockHash=%s, withdrawals=%s, transactions=%s}" + .formatted( + parentHash, + feeRecipient, + stateRoot, + receiptsRoot, + logsBloom, + prevRandao, + blockNumber, + gasLimit, + gasUsed, + timestamp, + extraData, + baseFeePerGas, + blockHash, + withdrawals, + transactions); } - private static int executionPayloadFixedPart(int version) { - if (version == 1) { + private static int executionPayloadFixedPart(BlockVersion version) { + if (version == BlockVersion.V3) { + return EXECUTION_PAYLOAD_FIXED_PART_V3; + } else if (version == BlockVersion.V2) { return EXECUTION_PAYLOAD_FIXED_PART_V2; } else { return EXECUTION_PAYLOAD_FIXED_PART_V1; diff --git a/hildr-node/src/main/java/io/optimism/network/OpStackNetwork.java b/hildr-node/src/main/java/io/optimism/network/OpStackNetwork.java index 34857943..f91f870e 100644 --- a/hildr-node/src/main/java/io/optimism/network/OpStackNetwork.java +++ b/hildr-node/src/main/java/io/optimism/network/OpStackNetwork.java @@ -100,6 +100,12 @@ public OpStackNetwork(Config.ChainConfig config, MessagePassingQueue + * All tests are timestamp sensitive and are disabled by default. + * + * @author grapebaba + * @since 0.2.6 + */ +class AbstractTopicHandlerTest { + + @Test + @Disabled + void testV3RejectNonZeroExcessGas() throws ExecutionException, InterruptedException { + String data = + "0xf104f0436c000b5448b55b27c4a06a556d4eecd6e502995ffbcd738843263d7ed92489e66d3776f1c829e0b43c7d354840b1b8983752bd797278d246b7d4ec0e100c6ae9010000006a03000412346a1d00fe0100fe0100fe0100fe0100fe0100fe01004201000c6e75d465219504100201067601007c752580fe037c2d682ee9b17b7e7b477cda0569cf4422284a570e918d526d283f0144010411011c0100000000000000"; + + final MetricsSystem metricsSystem = new PrometheusMetricsSystem( + ImmutableSet.builder() + .addAll(EnumSet.allOf(StandardMetricCategory.class)) + .addAll(EnumSet.allOf(HildrNodeMetricsCategory.class)) + .build(), + true); + final AsyncRunner gossipAsyncRunner = AsyncRunnerFactory.createDefault( + new MetricTrackingExecutorFactory(metricsSystem)) + .create("hildr_node_gossip", 20); + MpscUnboundedXaddArrayQueue unsafeBlockQueue = new MpscUnboundedXaddArrayQueue<>(1024 * 64); + BlockV3TopicHandler blockV3TopicHandler = new BlockV3TopicHandler( + new SnappyPreparedGossipMessageFactory(), + gossipAsyncRunner, + UInt64.valueOf(100L), + "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", + unsafeBlockQueue); + + PreparedGossipMessage preparedGossipMessage = blockV3TopicHandler.prepareMessage(Bytes.fromHexString(data)); + SafeFuture future = blockV3TopicHandler.handleMessage(preparedGossipMessage); + ValidationResult res = future.get(); + assertEquals(ValidationResult.Invalid, res); + } + + @Test + @Disabled + void testV3RejectNonZeroBlobGasUsed() throws ExecutionException, InterruptedException { + String data = + "0xf104f0436cd94b95a37c662c0d7a22346105f4af7d7dac0929d2e5a2047fd945f7cc37db2598623bb0355244e691c2d8deb925b0fc691b9b6875fc64e34b80f0cdb7649a000000006a03000412346a1d00fe0100fe0100fe0100fe0100fe0100fe01004201000cbd77d465219504100201067601007cf5248c2d75a4532d5c6d2bd780a59f0ba5edfcb9393ebb2a5ded6acd8ff3eb5a014401043c01000000000000000000000000000000"; + + final MetricsSystem metricsSystem = new PrometheusMetricsSystem( + ImmutableSet.builder() + .addAll(EnumSet.allOf(StandardMetricCategory.class)) + .addAll(EnumSet.allOf(HildrNodeMetricsCategory.class)) + .build(), + true); + final AsyncRunner gossipAsyncRunner = AsyncRunnerFactory.createDefault( + new MetricTrackingExecutorFactory(metricsSystem)) + .create("hildr_node_gossip", 20); + MpscUnboundedXaddArrayQueue unsafeBlockQueue = new MpscUnboundedXaddArrayQueue<>(1024 * 64); + BlockV3TopicHandler blockV3TopicHandler = new BlockV3TopicHandler( + new SnappyPreparedGossipMessageFactory(), + gossipAsyncRunner, + UInt64.valueOf(100L), + "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", + unsafeBlockQueue); + + PreparedGossipMessage preparedGossipMessage = blockV3TopicHandler.prepareMessage(Bytes.fromHexString(data)); + SafeFuture future = blockV3TopicHandler.handleMessage(preparedGossipMessage); + ValidationResult res = future.get(); + assertEquals(ValidationResult.Invalid, res); + } + + @Test + @Disabled + void testV3Valid() throws ExecutionException, InterruptedException { + String data = + "0xf104f0434442b9eb38b259f5b23826e6b623e829d2fb878dac70187a1aecf42a3f9bedfd29793d1fcb5822324be0d3e12340a95855553a65d64b83e5579dffb31470df5d010000006a03000412346a1d00fe0100fe0100fe0100fe0100fe0100fe01004201000cc588d465219504100201067601007cfece77b89685f60e3663b6e0faf2de0734674eb91339700c4858c773a8ff921e014401043e0100"; + + final MetricsSystem metricsSystem = new PrometheusMetricsSystem( + ImmutableSet.builder() + .addAll(EnumSet.allOf(StandardMetricCategory.class)) + .addAll(EnumSet.allOf(HildrNodeMetricsCategory.class)) + .build(), + true); + final AsyncRunner gossipAsyncRunner = AsyncRunnerFactory.createDefault( + new MetricTrackingExecutorFactory(metricsSystem)) + .create("hildr_node_gossip", 20); + MpscUnboundedXaddArrayQueue unsafeBlockQueue = new MpscUnboundedXaddArrayQueue<>(1024 * 64); + BlockV3TopicHandler blockV3TopicHandler = new BlockV3TopicHandler( + new SnappyPreparedGossipMessageFactory(), + gossipAsyncRunner, + UInt64.valueOf(100L), + "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", + unsafeBlockQueue); + + PreparedGossipMessage preparedGossipMessage = blockV3TopicHandler.prepareMessage(Bytes.fromHexString(data)); + SafeFuture future = blockV3TopicHandler.handleMessage(preparedGossipMessage); + ValidationResult res = future.get(); + assertEquals(ValidationResult.Valid, res); + } + + @Test + @Disabled + void testV2Valid() throws ExecutionException, InterruptedException { + String data = + "0xc104f0433805080eb36c0b130a7cc1dc74c3f721af4e249aa6f61bb89d1557143e971bb738a3f3b98df7c457e74048e9d2d7e5cd82bb45e3760467e2270e9db86d1271a700000000fe0300fe0300fe0300fe0300fe0300fe0300a203000c6b89d46525ad000205067201009cda69cb5b9b73fc4eb2458b37d37f04ff507fe6c9cd2ab704a05ea9dae3cd61760002000000020000"; + + final MetricsSystem metricsSystem = new PrometheusMetricsSystem( + ImmutableSet.builder() + .addAll(EnumSet.allOf(StandardMetricCategory.class)) + .addAll(EnumSet.allOf(HildrNodeMetricsCategory.class)) + .build(), + true); + final AsyncRunner gossipAsyncRunner = AsyncRunnerFactory.createDefault( + new MetricTrackingExecutorFactory(metricsSystem)) + .create("hildr_node_gossip", 20); + MpscUnboundedXaddArrayQueue unsafeBlockQueue = new MpscUnboundedXaddArrayQueue<>(1024 * 64); + BlockV2TopicHandler blockV2TopicHandler = new BlockV2TopicHandler( + new SnappyPreparedGossipMessageFactory(), + gossipAsyncRunner, + UInt64.valueOf(100L), + "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", + unsafeBlockQueue); + + PreparedGossipMessage preparedGossipMessage = blockV2TopicHandler.prepareMessage(Bytes.fromHexString(data)); + SafeFuture future = blockV2TopicHandler.handleMessage(preparedGossipMessage); + ValidationResult res = future.get(); + assertEquals(ValidationResult.Valid, res); + } + + @Test + @Disabled + void testV2NonZeroWithdrawals() { + String data = + "0xed04f0430cc8e59221e3d3eeb5c42307557aae2cb4823de2950cf54229ec29453662c92e748f4249507ee1f1c95d1f6ed96377244e5ee0b13f39a58c7d34a0579570579001000000fe0300fe0300fe0300fe0300fe0300fe0300a203000ccd89d46525ad000205067201007cc6713cb4dfd6e18fac545e2a48dd42ce51dcae91533d96048ff379e9e8e1f62d05440c020000010d4611086e0100"; + + final MetricsSystem metricsSystem = new PrometheusMetricsSystem( + ImmutableSet.builder() + .addAll(EnumSet.allOf(StandardMetricCategory.class)) + .addAll(EnumSet.allOf(HildrNodeMetricsCategory.class)) + .build(), + true); + final AsyncRunner gossipAsyncRunner = AsyncRunnerFactory.createDefault( + new MetricTrackingExecutorFactory(metricsSystem)) + .create("hildr_node_gossip", 20); + MpscUnboundedXaddArrayQueue unsafeBlockQueue = new MpscUnboundedXaddArrayQueue<>(1024 * 64); + BlockV2TopicHandler blockV2TopicHandler = new BlockV2TopicHandler( + new SnappyPreparedGossipMessageFactory(), + gossipAsyncRunner, + UInt64.valueOf(100L), + "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", + unsafeBlockQueue); + + PreparedGossipMessage preparedGossipMessage = blockV2TopicHandler.prepareMessage(Bytes.fromHexString(data)); + SafeFuture future = blockV2TopicHandler.handleMessage(preparedGossipMessage); + ValidationResult res = future.join(); + assertEquals(ValidationResult.Invalid, res); + } + + @Test + void testV2NonZeroBlobProperties() throws ExecutionException, InterruptedException { + String data = + "0xd104f0438a265ca8d1047e6e6dc04f88b82e87b4ac7d083fc271e153a32b183e3d6f0fa7139fa2808187f0ad12291e7c9f0a2296ac90339ddb1711d31b360a41ce67069000000000fe0300fe0300fe0300fe0300fe0300fe0300a203000c828ad46521ad04100201067601007c095842332e4409ec736246c082fc1e2a217d17af81ce0bbda3ed1bdf42f828c8014401043e0100"; + + final MetricsSystem metricsSystem = new PrometheusMetricsSystem( + ImmutableSet.builder() + .addAll(EnumSet.allOf(StandardMetricCategory.class)) + .addAll(EnumSet.allOf(HildrNodeMetricsCategory.class)) + .build(), + true); + final AsyncRunner gossipAsyncRunner = AsyncRunnerFactory.createDefault( + new MetricTrackingExecutorFactory(metricsSystem)) + .create("hildr_node_gossip", 20); + MpscUnboundedXaddArrayQueue unsafeBlockQueue = new MpscUnboundedXaddArrayQueue<>(1024 * 64); + BlockV2TopicHandler blockV2TopicHandler = new BlockV2TopicHandler( + new SnappyPreparedGossipMessageFactory(), + gossipAsyncRunner, + UInt64.valueOf(100L), + "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", + unsafeBlockQueue); + + PreparedGossipMessage preparedGossipMessage = blockV2TopicHandler.prepareMessage(Bytes.fromHexString(data)); + SafeFuture future = blockV2TopicHandler.handleMessage(preparedGossipMessage); + + ValidationResult res = future.get(); + assertEquals(ValidationResult.Invalid, res); + } + + @Test + void testV3RejectExecutionPayload() throws ExecutionException, InterruptedException { + String data = + "0xd104f043d7f7992d2817a9dd744273494896a1ef6214d9f80e95db22e132a5f95f6a5a1b67fa7e5b7813fcf090eb2ad0870e852400e8c919cc77b9905416674b3d51e3a100000000fe0300fe0300fe0300fe0300fe0300fe0300a203000c6b8ed46521ad04100201067601007c98759d697467f0fd82dc95a7999225840106105c71d3f6022f0b7a7b87791855014401043e0100"; + + final MetricsSystem metricsSystem = new PrometheusMetricsSystem( + ImmutableSet.builder() + .addAll(EnumSet.allOf(StandardMetricCategory.class)) + .addAll(EnumSet.allOf(HildrNodeMetricsCategory.class)) + .build(), + true); + final AsyncRunner gossipAsyncRunner = AsyncRunnerFactory.createDefault( + new MetricTrackingExecutorFactory(metricsSystem)) + .create("hildr_node_gossip", 20); + MpscUnboundedXaddArrayQueue unsafeBlockQueue = new MpscUnboundedXaddArrayQueue<>(1024 * 64); + BlockV3TopicHandler blockV3TopicHandler = new BlockV3TopicHandler( + new SnappyPreparedGossipMessageFactory(), + gossipAsyncRunner, + UInt64.valueOf(100L), + "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc", + unsafeBlockQueue); + + PreparedGossipMessage preparedGossipMessage = blockV3TopicHandler.prepareMessage(Bytes.fromHexString(data)); + SafeFuture future = blockV3TopicHandler.handleMessage(preparedGossipMessage); + ValidationResult res = future.get(); + assertEquals(ValidationResult.Invalid, res); + } +} diff --git a/hildr-node/src/test/java/io/optimism/network/ExecutionPayloadEnvelopTest.java b/hildr-node/src/test/java/io/optimism/network/ExecutionPayloadEnvelopTest.java new file mode 100644 index 00000000..1509e836 --- /dev/null +++ b/hildr-node/src/test/java/io/optimism/network/ExecutionPayloadEnvelopTest.java @@ -0,0 +1,43 @@ +package io.optimism.network; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import java.math.BigInteger; +import org.apache.tuweni.bytes.Bytes; +import org.junit.jupiter.api.Test; + +/** + * The type ExecutionPayloadEnvelopTest. + * + * @author grapebaba + * @since 0.2.6 + */ +class ExecutionPayloadEnvelopTest { + + @Test + void testValidExecutionPayloadEnvelop() { + String data = + "00000000000000000000000000000000000000000000000000000000000001230000000000000000000000000000000000000000000000000000000000000123000000000000000000000000000000000000045600000000000000000000000000000000000000000000000000000000000007890000000000000000000000000000000000000000000000000000000000000abc0d0e0f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111de000000000000004d01000000000000bc010000000000002b0200000000000010020000090300000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000008881202000018020000000000000000000000000000000000006666040000009999"; + ExecutionPayloadEnvelop executionPayloadEnvelop = ExecutionPayloadEnvelop.from(Bytes.fromHexString(data)); + + assertEquals( + "0x0000000000000000000000000000000000000000000000000000000000000123", + executionPayloadEnvelop.parentBeaconBlockRoot().toHexString()); + + assertEquals(BigInteger.ZERO, executionPayloadEnvelop.executionPayload().blobGasUsed()); + assertEquals(BigInteger.ZERO, executionPayloadEnvelop.executionPayload().excessBlobGas()); + assertEquals( + BigInteger.valueOf(222L), + executionPayloadEnvelop.executionPayload().blockNumber()); + assertEquals( + BigInteger.valueOf(555L), + executionPayloadEnvelop.executionPayload().timestamp()); + assertEquals( + "0x0000000000000000000000000000000000000000000000000000000000000123", + executionPayloadEnvelop.executionPayload().parentHash()); + assertEquals( + "0x0000000000000000000000000000000000000000000000000000000000000789", + executionPayloadEnvelop.executionPayload().stateRoot()); + assertEquals("0x6666", executionPayloadEnvelop.executionPayload().extraData()); + } +} diff --git a/hildr-node/src/test/java/io/optimism/network/ExecutionPayloadSSZTest.java b/hildr-node/src/test/java/io/optimism/network/ExecutionPayloadSSZTest.java index 3e2771ad..1b96fe1b 100644 --- a/hildr-node/src/test/java/io/optimism/network/ExecutionPayloadSSZTest.java +++ b/hildr-node/src/test/java/io/optimism/network/ExecutionPayloadSSZTest.java @@ -18,8 +18,8 @@ public class ExecutionPayloadSSZTest { public void zeroWithdrawalsSucceeds() { String hex = "0x0000000000000000000000000000000000000000000000000000000000000123000000000000000000000000000000000000045600000000000000000000000000000000000000000000000000000000000007890000000000000000000000000000000000000000000000000000000000000abc0d0e0f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111de000000000000004d01000000000000bc010000000000002b020000000000000002000009030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000888000200000402000004000000"; - int version = 1; - ExecutionPayloadSSZ payload = ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), version); + ExecutionPayloadSSZ payload = + ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), BlockVersion.V2); Assertions.assertEquals(payload.baseFeePerGas(), UInt256.valueOf(777L)); } @@ -27,9 +27,8 @@ public void zeroWithdrawalsSucceeds() { public void zeroWithdrawalsFailsToDeserialize() { String hex = "0x0000000000000000000000000000000000000000000000000000000000000123000000000000000000000000000000000000045600000000000000000000000000000000000000000000000000000000000007890000000000000000000000000000000000000000000000000000000000000abc0d0e0f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111de000000000000004d01000000000000bc010000000000002b020000000000000002000009030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000888000200000402000004000000"; - int version = 0; Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> { - ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), version); + ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), BlockVersion.V1); }); } @@ -37,8 +36,8 @@ public void zeroWithdrawalsFailsToDeserialize() { public void withdrawalsSucceeds() { String hex = "0x0000000000000000000000000000000000000000000000000000000000000123000000000000000000000000000000000000045600000000000000000000000000000000000000000000000000000000000007890000000000000000000000000000000000000000000000000000000000000abc0d0e0f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111de000000000000004d01000000000000bc010000000000002b020000000000000002000009030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000888000200000402000004000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000"; - int version = 1; - ExecutionPayloadSSZ payload = ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), version); + ExecutionPayloadSSZ payload = + ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), BlockVersion.V2); Assertions.assertEquals(payload.baseFeePerGas(), UInt256.valueOf(777L)); } @@ -46,9 +45,8 @@ public void withdrawalsSucceeds() { public void withdrawalsFailsToDeserialize() { String hex = "0x0000000000000000000000000000000000000000000000000000000000000123000000000000000000000000000000000000045600000000000000000000000000000000000000000000000000000000000007890000000000000000000000000000000000000000000000000000000000000abc0d0e0f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111de000000000000004d01000000000000bc010000000000002b020000000000000002000009030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000888000200000402000004000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000"; - int version = 0; Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> { - ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), version); + ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), BlockVersion.V1); }); } @@ -56,8 +54,8 @@ public void withdrawalsFailsToDeserialize() { public void maxWithdrawalsSucceeds() { String hex = "0x0000000000000000000000000000000000000000000000000000000000000123000000000000000000000000000000000000045600000000000000000000000000000000000000000000000000000000000007890000000000000000000000000000000000000000000000000000000000000abc0d0e0f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111de000000000000004d01000000000000bc010000000000002b020000000000000002000009030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000888000200000402000004000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000"; - int version = 1; - ExecutionPayloadSSZ payload = ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), version); + ExecutionPayloadSSZ payload = + ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), BlockVersion.V2); Assertions.assertEquals(payload.baseFeePerGas(), UInt256.valueOf(777L)); } @@ -65,9 +63,8 @@ public void maxWithdrawalsSucceeds() { public void tooManyWithdrawalsErrors() { String hex = "0x0000000000000000000000000000000000000000000000000000000000000123000000000000000000000000000000000000045600000000000000000000000000000000000000000000000000000000000007890000000000000000000000000000000000000000000000000000000000000abc0d0e0f000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000111de000000000000004d01000000000000bc010000000000002b020000000000000002000009030000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000888000200000402000004000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000db030000000000008e0200000000000000000000000000000000000000000000000008984101000000000000"; - int version = 1; Assertions.assertThrowsExactly(IllegalArgumentException.class, () -> { - ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), version); + ExecutionPayloadSSZ.from(Bytes.wrap(Numeric.hexStringToByteArray(hex)), BlockVersion.V2); }); } }