Verify Versioned Hashes During Optimistic Sync (#4832)

* Convert NewPayloadRequest to use Reference

* Refactor for Clarity

* Verify Versioned Hashes

* Added Tests for Version Hash Verification

* Added Moar Tests

* Fix Problems Caused By  Merge

* Update to use Alloy Instead of Reth Crates (#14)

* Update beacon_node/execution_layer/src/engine_api/new_payload_request.rs

Co-authored-by: realbigsean <seananderson33@GMAIL.com>

* Faster Versioned Hash Extraction

* Update to rust 1.75 & Pin alloy-consensus
This commit is contained in:
ethDreamer
2024-02-18 23:40:45 +11:00
committed by GitHub
parent 1711b80779
commit a264afd19f
16 changed files with 957 additions and 222 deletions

View File

@@ -51,3 +51,5 @@ hash-db = "0.15.2"
pretty_reqwest_error = { workspace = true }
arc-swap = "1.6.0"
eth2_network_config = { workspace = true }
alloy-rlp = "0.3"
alloy-consensus = { git = "https://github.com/alloy-rs/alloy.git", rev = "974d488bab5e21e9f17452a39a4bfa56677367b2" }

View File

@@ -1,92 +1,61 @@
use crate::{
json_structures::JsonWithdrawal,
keccak::{keccak256, KeccakHasher},
metrics, Error, ExecutionLayer,
};
use ethers_core::utils::rlp::RlpStream;
use keccak_hash::KECCAK_EMPTY_LIST_RLP;
use triehash::ordered_trie_root;
use types::{
map_execution_block_header_fields_base, Address, BeaconBlockRef, EthSpec, ExecutionBlockHash,
map_execution_block_header_fields_base, Address, EthSpec, ExecutionBlockHash,
ExecutionBlockHeader, ExecutionPayloadRef, Hash256, Hash64, Uint256,
};
impl<T: EthSpec> ExecutionLayer<T> {
/// Calculate the block hash of an execution block.
///
/// Return `(block_hash, transactions_root)`, where `transactions_root` is the root of the RLP
/// transactions.
pub fn calculate_execution_block_hash(
payload: ExecutionPayloadRef<T>,
parent_beacon_block_root: Hash256,
) -> (ExecutionBlockHash, Hash256) {
// Calculate the transactions root.
// We're currently using a deprecated Parity library for this. We should move to a
// better alternative when one appears, possibly following Reth.
let rlp_transactions_root = ordered_trie_root::<KeccakHasher, _>(
payload.transactions().iter().map(|txn_bytes| &**txn_bytes),
);
/// Calculate the block hash of an execution block.
///
/// Return `(block_hash, transactions_root)`, where `transactions_root` is the root of the RLP
/// transactions.
pub fn calculate_execution_block_hash<T: EthSpec>(
payload: ExecutionPayloadRef<T>,
parent_beacon_block_root: Option<Hash256>,
) -> (ExecutionBlockHash, Hash256) {
// Calculate the transactions root.
// We're currently using a deprecated Parity library for this. We should move to a
// better alternative when one appears, possibly following Reth.
let rlp_transactions_root = ordered_trie_root::<KeccakHasher, _>(
payload.transactions().iter().map(|txn_bytes| &**txn_bytes),
);
// Calculate withdrawals root (post-Capella).
let rlp_withdrawals_root = if let Ok(withdrawals) = payload.withdrawals() {
Some(ordered_trie_root::<KeccakHasher, _>(
withdrawals.iter().map(|withdrawal| {
rlp_encode_withdrawal(&JsonWithdrawal::from(withdrawal.clone()))
}),
))
} else {
None
};
// Calculate withdrawals root (post-Capella).
let rlp_withdrawals_root = if let Ok(withdrawals) = payload.withdrawals() {
Some(ordered_trie_root::<KeccakHasher, _>(
withdrawals
.iter()
.map(|withdrawal| rlp_encode_withdrawal(&JsonWithdrawal::from(withdrawal.clone()))),
))
} else {
None
};
let rlp_blob_gas_used = payload.blob_gas_used().ok();
let rlp_excess_blob_gas = payload.excess_blob_gas().ok();
let rlp_blob_gas_used = payload.blob_gas_used().ok();
let rlp_excess_blob_gas = payload.excess_blob_gas().ok();
// Calculate parent beacon block root (post-Deneb).
let rlp_parent_beacon_block_root = rlp_excess_blob_gas
.as_ref()
.map(|_| parent_beacon_block_root);
// Construct the block header.
let exec_block_header = ExecutionBlockHeader::from_payload(
payload,
KECCAK_EMPTY_LIST_RLP.as_fixed_bytes().into(),
rlp_transactions_root,
rlp_withdrawals_root,
rlp_blob_gas_used,
rlp_excess_blob_gas,
parent_beacon_block_root,
);
// Construct the block header.
let exec_block_header = ExecutionBlockHeader::from_payload(
payload,
KECCAK_EMPTY_LIST_RLP.as_fixed_bytes().into(),
rlp_transactions_root,
rlp_withdrawals_root,
rlp_blob_gas_used,
rlp_excess_blob_gas,
rlp_parent_beacon_block_root,
);
// Hash the RLP encoding of the block header.
let rlp_block_header = rlp_encode_block_header(&exec_block_header);
(
ExecutionBlockHash::from_root(keccak256(&rlp_block_header)),
rlp_transactions_root,
)
}
/// Verify `payload.block_hash` locally within Lighthouse.
///
/// No remote calls to the execution client will be made, so this is quite a cheap check.
pub fn verify_payload_block_hash(&self, block: BeaconBlockRef<T>) -> Result<(), Error> {
let payload = block.execution_payload()?.execution_payload_ref();
let parent_beacon_block_root = block.parent_root();
let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH);
let (header_hash, rlp_transactions_root) =
Self::calculate_execution_block_hash(payload, parent_beacon_block_root);
if header_hash != payload.block_hash() {
return Err(Error::BlockHashMismatch {
computed: header_hash,
payload: payload.block_hash(),
transactions_root: rlp_transactions_root,
});
}
Ok(())
}
// Hash the RLP encoding of the block header.
let rlp_block_header = rlp_encode_block_header(&exec_block_header);
(
ExecutionBlockHash::from_root(keccak256(&rlp_block_header)),
rlp_transactions_root,
)
}
/// RLP encode a withdrawal.

View File

@@ -17,7 +17,6 @@ pub use json_structures::{JsonWithdrawal, TransitionConfigurationV1};
use pretty_reqwest_error::PrettyReqwestError;
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash;
use std::convert::TryFrom;
use strum::IntoStaticStr;
use superstruct::superstruct;
@@ -26,14 +25,16 @@ pub use types::{
ExecutionPayloadRef, FixedVector, ForkName, Hash256, Transactions, Uint256, VariableList,
Withdrawal, Withdrawals,
};
use types::{
BeaconStateError, ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadMerge,
KzgProofs, VersionedHash,
};
use types::{ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadMerge, KzgProofs};
pub mod auth;
pub mod http;
pub mod json_structures;
mod new_payload_request;
pub use new_payload_request::{
NewPayloadRequest, NewPayloadRequestCapella, NewPayloadRequestDeneb, NewPayloadRequestMerge,
};
pub const LATEST_TAG: &str = "latest";
@@ -571,110 +572,6 @@ impl<E: EthSpec> ExecutionPayloadBodyV1<E> {
}
}
#[superstruct(
variants(Merge, Capella, Deneb),
variant_attributes(derive(Clone, Debug, PartialEq),),
map_into(ExecutionPayload),
map_ref_into(ExecutionPayloadRef),
cast_error(
ty = "BeaconStateError",
expr = "BeaconStateError::IncorrectStateVariant"
),
partial_getter_error(
ty = "BeaconStateError",
expr = "BeaconStateError::IncorrectStateVariant"
)
)]
#[derive(Clone, Debug, PartialEq)]
pub struct NewPayloadRequest<E: EthSpec> {
#[superstruct(only(Merge), partial_getter(rename = "execution_payload_merge"))]
pub execution_payload: ExecutionPayloadMerge<E>,
#[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))]
pub execution_payload: ExecutionPayloadCapella<E>,
#[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))]
pub execution_payload: ExecutionPayloadDeneb<E>,
#[superstruct(only(Deneb))]
pub versioned_hashes: Vec<VersionedHash>,
#[superstruct(only(Deneb))]
pub parent_beacon_block_root: Hash256,
}
impl<E: EthSpec> NewPayloadRequest<E> {
pub fn parent_hash(&self) -> ExecutionBlockHash {
match self {
Self::Merge(payload) => payload.execution_payload.parent_hash,
Self::Capella(payload) => payload.execution_payload.parent_hash,
Self::Deneb(payload) => payload.execution_payload.parent_hash,
}
}
pub fn block_hash(&self) -> ExecutionBlockHash {
match self {
Self::Merge(payload) => payload.execution_payload.block_hash,
Self::Capella(payload) => payload.execution_payload.block_hash,
Self::Deneb(payload) => payload.execution_payload.block_hash,
}
}
pub fn block_number(&self) -> u64 {
match self {
Self::Merge(payload) => payload.execution_payload.block_number,
Self::Capella(payload) => payload.execution_payload.block_number,
Self::Deneb(payload) => payload.execution_payload.block_number,
}
}
pub fn into_execution_payload(self) -> ExecutionPayload<E> {
map_new_payload_request_into_execution_payload!(self, |request, cons| {
cons(request.execution_payload)
})
}
}
impl<'a, E: EthSpec> TryFrom<BeaconBlockRef<'a, E>> for NewPayloadRequest<E> {
type Error = BeaconStateError;
fn try_from(block: BeaconBlockRef<'a, E>) -> Result<Self, Self::Error> {
match block {
BeaconBlockRef::Base(_) | BeaconBlockRef::Altair(_) => {
Err(Self::Error::IncorrectStateVariant)
}
BeaconBlockRef::Merge(block_ref) => Ok(Self::Merge(NewPayloadRequestMerge {
execution_payload: block_ref.body.execution_payload.execution_payload.clone(),
})),
BeaconBlockRef::Capella(block_ref) => Ok(Self::Capella(NewPayloadRequestCapella {
execution_payload: block_ref.body.execution_payload.execution_payload.clone(),
})),
BeaconBlockRef::Deneb(block_ref) => Ok(Self::Deneb(NewPayloadRequestDeneb {
execution_payload: block_ref.body.execution_payload.execution_payload.clone(),
versioned_hashes: block_ref
.body
.blob_kzg_commitments
.iter()
.map(kzg_commitment_to_versioned_hash)
.collect(),
parent_beacon_block_root: block_ref.parent_root,
})),
}
}
}
impl<E: EthSpec> TryFrom<ExecutionPayload<E>> for NewPayloadRequest<E> {
type Error = BeaconStateError;
fn try_from(payload: ExecutionPayload<E>) -> Result<Self, Self::Error> {
match payload {
ExecutionPayload::Merge(payload) => Ok(Self::Merge(NewPayloadRequestMerge {
execution_payload: payload,
})),
ExecutionPayload::Capella(payload) => Ok(Self::Capella(NewPayloadRequestCapella {
execution_payload: payload,
})),
ExecutionPayload::Deneb(_) => Err(Self::Error::IncorrectStateVariant),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct EngineCapabilities {
pub new_payload_v1: bool,

View File

@@ -803,10 +803,10 @@ impl HttpJsonRpc {
pub async fn new_payload_v3<T: EthSpec>(
&self,
new_payload_request_deneb: NewPayloadRequestDeneb<T>,
new_payload_request_deneb: NewPayloadRequestDeneb<'_, T>,
) -> Result<PayloadStatusV1, Error> {
let params = json!([
JsonExecutionPayload::V3(new_payload_request_deneb.execution_payload.into()),
JsonExecutionPayload::V3(new_payload_request_deneb.execution_payload.clone().into()),
new_payload_request_deneb.versioned_hashes,
new_payload_request_deneb.parent_beacon_block_root,
]);
@@ -1079,7 +1079,7 @@ impl HttpJsonRpc {
// new_payload that the execution engine supports
pub async fn new_payload<T: EthSpec>(
&self,
new_payload_request: NewPayloadRequest<T>,
new_payload_request: NewPayloadRequest<'_, T>,
) -> Result<PayloadStatusV1, Error> {
let engine_capabilities = self.get_engine_capabilities(None).await?;
match new_payload_request {

View File

@@ -0,0 +1,332 @@
use crate::{block_hash::calculate_execution_block_hash, metrics, Error};
use crate::versioned_hashes::verify_versioned_hashes;
use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash;
use superstruct::superstruct;
use types::{
BeaconBlockRef, BeaconStateError, EthSpec, ExecutionBlockHash, ExecutionPayload,
ExecutionPayloadRef, Hash256, VersionedHash,
};
use types::{ExecutionPayloadCapella, ExecutionPayloadDeneb, ExecutionPayloadMerge};
#[superstruct(
variants(Merge, Capella, Deneb),
variant_attributes(derive(Clone, Debug, PartialEq),),
map_into(ExecutionPayload),
map_ref_into(ExecutionPayloadRef),
cast_error(
ty = "BeaconStateError",
expr = "BeaconStateError::IncorrectStateVariant"
),
partial_getter_error(
ty = "BeaconStateError",
expr = "BeaconStateError::IncorrectStateVariant"
)
)]
#[derive(Clone, Debug, PartialEq)]
pub struct NewPayloadRequest<'block, E: EthSpec> {
#[superstruct(only(Merge), partial_getter(rename = "execution_payload_merge"))]
pub execution_payload: &'block ExecutionPayloadMerge<E>,
#[superstruct(only(Capella), partial_getter(rename = "execution_payload_capella"))]
pub execution_payload: &'block ExecutionPayloadCapella<E>,
#[superstruct(only(Deneb), partial_getter(rename = "execution_payload_deneb"))]
pub execution_payload: &'block ExecutionPayloadDeneb<E>,
#[superstruct(only(Deneb))]
pub versioned_hashes: Vec<VersionedHash>,
#[superstruct(only(Deneb))]
pub parent_beacon_block_root: Hash256,
}
impl<'block, E: EthSpec> NewPayloadRequest<'block, E> {
pub fn parent_hash(&self) -> ExecutionBlockHash {
match self {
Self::Merge(payload) => payload.execution_payload.parent_hash,
Self::Capella(payload) => payload.execution_payload.parent_hash,
Self::Deneb(payload) => payload.execution_payload.parent_hash,
}
}
pub fn block_hash(&self) -> ExecutionBlockHash {
match self {
Self::Merge(payload) => payload.execution_payload.block_hash,
Self::Capella(payload) => payload.execution_payload.block_hash,
Self::Deneb(payload) => payload.execution_payload.block_hash,
}
}
pub fn block_number(&self) -> u64 {
match self {
Self::Merge(payload) => payload.execution_payload.block_number,
Self::Capella(payload) => payload.execution_payload.block_number,
Self::Deneb(payload) => payload.execution_payload.block_number,
}
}
pub fn execution_payload_ref(&self) -> ExecutionPayloadRef<'block, E> {
match self {
Self::Merge(request) => ExecutionPayloadRef::Merge(request.execution_payload),
Self::Capella(request) => ExecutionPayloadRef::Capella(request.execution_payload),
Self::Deneb(request) => ExecutionPayloadRef::Deneb(request.execution_payload),
}
}
pub fn into_execution_payload(self) -> ExecutionPayload<E> {
match self {
Self::Merge(request) => ExecutionPayload::Merge(request.execution_payload.clone()),
Self::Capella(request) => ExecutionPayload::Capella(request.execution_payload.clone()),
Self::Deneb(request) => ExecutionPayload::Deneb(request.execution_payload.clone()),
}
}
/// Performs the required verifications of the payload when the chain is optimistically syncing.
///
/// ## Specification
///
/// Performs the verifications in the `verify_and_notify_new_payload` function:
///
/// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.2/specs/deneb/beacon-chain.md#modified-verify_and_notify_new_payload
pub fn perform_optimistic_sync_verifications(&self) -> Result<(), Error> {
self.verfiy_payload_block_hash()?;
self.verify_versioned_hashes()?;
Ok(())
}
/// Verify the block hash is consistent locally within Lighthouse.
///
/// ## Specification
///
/// Equivalent to `is_valid_block_hash` in the spec:
/// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.2/specs/deneb/beacon-chain.md#is_valid_block_hash
pub fn verfiy_payload_block_hash(&self) -> Result<(), Error> {
let payload = self.execution_payload_ref();
let parent_beacon_block_root = self.parent_beacon_block_root().ok().cloned();
let _timer = metrics::start_timer(&metrics::EXECUTION_LAYER_VERIFY_BLOCK_HASH);
let (header_hash, rlp_transactions_root) =
calculate_execution_block_hash(payload, parent_beacon_block_root);
if header_hash != self.block_hash() {
return Err(Error::BlockHashMismatch {
computed: header_hash,
payload: payload.block_hash(),
transactions_root: rlp_transactions_root,
});
}
Ok(())
}
/// Verify the versioned hashes computed by the blob transactions match the versioned hashes computed from the commitments.
///
/// ## Specification
///
/// Equivalent to `is_valid_versioned_hashes` in the spec:
/// https://github.com/ethereum/consensus-specs/blob/v1.4.0-beta.2/specs/deneb/beacon-chain.md#is_valid_versioned_hashes
pub fn verify_versioned_hashes(&self) -> Result<(), Error> {
if let Ok(versioned_hashes) = self.versioned_hashes() {
verify_versioned_hashes(self.execution_payload_ref(), versioned_hashes)
.map_err(Error::VerifyingVersionedHashes)?;
}
Ok(())
}
}
impl<'a, E: EthSpec> TryFrom<BeaconBlockRef<'a, E>> for NewPayloadRequest<'a, E> {
type Error = BeaconStateError;
fn try_from(block: BeaconBlockRef<'a, E>) -> Result<Self, Self::Error> {
match block {
BeaconBlockRef::Base(_) | BeaconBlockRef::Altair(_) => {
Err(Self::Error::IncorrectStateVariant)
}
BeaconBlockRef::Merge(block_ref) => Ok(Self::Merge(NewPayloadRequestMerge {
execution_payload: &block_ref.body.execution_payload.execution_payload,
})),
BeaconBlockRef::Capella(block_ref) => Ok(Self::Capella(NewPayloadRequestCapella {
execution_payload: &block_ref.body.execution_payload.execution_payload,
})),
BeaconBlockRef::Deneb(block_ref) => Ok(Self::Deneb(NewPayloadRequestDeneb {
execution_payload: &block_ref.body.execution_payload.execution_payload,
versioned_hashes: block_ref
.body
.blob_kzg_commitments
.iter()
.map(kzg_commitment_to_versioned_hash)
.collect(),
parent_beacon_block_root: block_ref.parent_root,
})),
}
}
}
impl<'a, E: EthSpec> TryFrom<ExecutionPayloadRef<'a, E>> for NewPayloadRequest<'a, E> {
type Error = BeaconStateError;
fn try_from(payload: ExecutionPayloadRef<'a, E>) -> Result<Self, Self::Error> {
match payload {
ExecutionPayloadRef::Merge(payload) => Ok(Self::Merge(NewPayloadRequestMerge {
execution_payload: payload,
})),
ExecutionPayloadRef::Capella(payload) => Ok(Self::Capella(NewPayloadRequestCapella {
execution_payload: payload,
})),
ExecutionPayloadRef::Deneb(_) => Err(Self::Error::IncorrectStateVariant),
}
}
}
#[cfg(test)]
mod test {
use crate::versioned_hashes::Error as VersionedHashError;
use crate::{Error, NewPayloadRequest};
use state_processing::per_block_processing::deneb::kzg_commitment_to_versioned_hash;
use types::{BeaconBlock, ExecPayload, ExecutionBlockHash, Hash256, MainnetEthSpec};
#[test]
fn test_optimistic_sync_verifications_valid_block() {
let beacon_block = get_valid_beacon_block();
let new_payload_request = NewPayloadRequest::try_from(beacon_block.to_ref())
.expect("should create new payload request");
assert!(
new_payload_request
.perform_optimistic_sync_verifications()
.is_ok(),
"validations should pass"
);
}
#[test]
fn test_optimistic_sync_verifications_bad_block_hash() {
let mut beacon_block = get_valid_beacon_block();
let correct_block_hash = beacon_block
.body()
.execution_payload()
.expect("should get payload")
.block_hash();
let invalid_block_hash = ExecutionBlockHash(Hash256::repeat_byte(0x42));
// now mutate the block hash
beacon_block
.body_mut()
.execution_payload_deneb_mut()
.expect("should get payload")
.execution_payload
.block_hash = invalid_block_hash;
let new_payload_request = NewPayloadRequest::try_from(beacon_block.to_ref())
.expect("should create new payload request");
let verification_result = new_payload_request.perform_optimistic_sync_verifications();
println!("verification_result: {:?}", verification_result);
let got_expected_result = match verification_result {
Err(Error::BlockHashMismatch {
computed, payload, ..
}) => computed == correct_block_hash && payload == invalid_block_hash,
_ => false,
};
assert!(got_expected_result, "should return expected error");
}
#[test]
fn test_optimistic_sync_verifications_bad_versioned_hashes() {
let mut beacon_block = get_valid_beacon_block();
let mut commitments: Vec<_> = beacon_block
.body()
.blob_kzg_commitments()
.expect("should get commitments")
.clone()
.into();
let correct_versioned_hash = kzg_commitment_to_versioned_hash(
commitments.last().expect("should get last commitment"),
);
// mutate the last commitment
commitments
.last_mut()
.expect("should get last commitment")
.0[0] = 0x42;
// calculate versioned hash from mutated commitment
let bad_versioned_hash = kzg_commitment_to_versioned_hash(
commitments.last().expect("should get last commitment"),
);
*beacon_block
.body_mut()
.blob_kzg_commitments_mut()
.expect("should get commitments") = commitments.into();
let new_payload_request = NewPayloadRequest::try_from(beacon_block.to_ref())
.expect("should create new payload request");
let verification_result = new_payload_request.perform_optimistic_sync_verifications();
println!("verification_result: {:?}", verification_result);
let got_expected_result = match verification_result {
Err(Error::VerifyingVersionedHashes(VersionedHashError::VersionHashMismatch {
expected,
found,
})) => expected == bad_versioned_hash && found == correct_versioned_hash,
_ => false,
};
assert!(got_expected_result, "should return expected error");
}
fn get_valid_beacon_block() -> BeaconBlock<MainnetEthSpec> {
BeaconBlock::Deneb(serde_json::from_str(r#"{
"slot": "88160",
"proposer_index": "583",
"parent_root": "0x60770cd86a497ca3aa2e91f1687aa3ebafac87af52c30a920b5f40bd9e930eb6",
"state_root": "0x4a0e0abbcbcf576f2cb7387c4289ab13b8a128e32127642f056143d6164941a6",
"body": {
"randao_reveal": "0xb5253d5739496abc4f67c7c92e39e46cca452c2fdfc5275e3e0426a012aa62df82f47f7dece348e28db4bb212f0e793d187120bbd47b8031ed79344116eb4128f0ce0b05ba18cd615bb13966c1bd7d89e23cc769c8e4d8e4a63755f623ac3bed",
"eth1_data": {
"deposit_root": "0xe4785ac914d8673797f886e3151ce2647f81ae070c7ddb6845e65fd1c47d1222",
"deposit_count": "1181",
"block_hash": "0x010671bdfbfce6b0071984a06a7ded6deef13b4f8fdbae402c606a7a0c8780d1"
},
"graffiti": "0x6c6f6465737461722f6765746800000000000000000000000000000000000000",
"proposer_slashings": [],
"attester_slashings": [],
"attestations": [],
"deposits": [],
"voluntary_exits": [],
"sync_aggregate": {
"sync_committee_bits": "0xfebffffffebfff7fff7f7fffbbefffff6affffffffbfffffefffebfffdbf77fff7fd77ffffefffdff7ffffeffffffe7e5ffffffdefffff7ffbffff7fffffffff",
"sync_committee_signature": "0x91939b5baf2a6f52d405b6dd396f5346ec435eca7d25912c91cc6a2f7030d870d68bebe4f2b21872a06929ff4cf3e5e9191053cb43eb24ebe34b9a75fb88a3acd06baf329c87f68bd664b49891260c698d7bca0f5365870b5b2b3a76f582156c"
},
"execution_payload": {
"parent_hash": "0xa6f3ed782a992f79ad38da2af91b3e8923c71b801c50bc9033bb35a2e1da885f",
"fee_recipient": "0xf97e180c050e5ab072211ad2c213eb5aee4df134",
"state_root": "0x3bfd1a7f309ed35048c349a8daf01815bdc09a6d5df86ea77d1056f248ba2017",
"receipts_root": "0xcb5b8ffea57cd0fa87194d49bc8bb7fad08c93c9934b886489503c328d15fd36",
"logs_bloom": "0x002000000000000000000000800000000000000000001040000000000000000000000001000000000000000000000000000000000000100000000020000c0800000000000000008000000008000000200000800000000000000000000000000000000000000000008000000000008000000000000000000002000010000000000000000000000000000000000000000000000000000000080000004000000000800000000000000000000100000000000000000000000000000000000800000000000102000000000000000000000000000000080000001000000000000000000000000000000000000000000000000000000000000000000000000000000000",
"prev_randao": "0xb2693020177d99ffbd4c267023be172d759e7306ff51b0e7d677d3148fbd7f1d",
"block_number": "74807",
"gas_limit": "30000000",
"gas_used": "128393",
"timestamp": "1697039520",
"extra_data": "0xd883010d03846765746888676f312e32312e31856c696e7578",
"base_fee_per_gas": "7",
"block_hash": "0xc64f3a43c64aeb98518a237f6279fa03095b9f95ca673c860ad7f16fb9340062",
"transactions": [
"0x02f9017a8501a1f0ff4382317585012a05f2008512a05f2000830249f094c1b0bc605e2c808aa0867bfc98e51a1fe3e9867f80b901040cc7326300000000000000000000000000000000000000000000000000000000000000a0000000000000000000000000000000000000000000000000036e534e16b8920d000000000000000000000000fb3e9c7cb92443931ee6b5b9728598d4eb9618c1000000000000000000000000fc7360b3b28cf4204268a8354dbec60720d155d2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009a054a063f0fe7b9c68de8df91aaa5e96c15ab540000000000000000000000000c8d41b8fcc066cdabaf074d78e5153e8ce018a9c080a07dd9be0d014ffcd5b6883d0917c66b74ba51f0d976c8fc5674af192af6fa9450a02dad2c660974c125f5f22b1e6e8862a292e08cc2b4cafda35af650ee62868a43",
"0x03f8db8501a1f0ff430d84773594008504a817c8008252089454e594b6de0aa4b0188cd1549dd7ba715a455d078080c08504a817c800f863a001253ce00f525e3495cffa0b865eadb90a4c5ee812185cc796af74b6ec0a5dd7a0010720372a4d7dcab84413ed0cfc164fb91fb6ef1562ec2f7a82e912a1d9e129a0015a73e97950397896ed2c47dcab7c0360220bcfb413a8f210a7b6e6264e698880a04402cb0f13c17ef41dca106b1e1520c7aadcbe62984d81171e29914f587d67c1a02db62a8edb581917958e4a3884e7eececbaec114c5ee496e238033e896f997ac"
],
"withdrawals": [],
"blob_gas_used": "393216",
"excess_blob_gas": "58720256"
},
"bls_to_execution_changes": [],
"blob_kzg_commitments": [
"0xa7accb7a25224a8c2e0cee9cd569fc1798665bfbfe780e08945fa9098ec61da4061f5b04e750a88d3340a801850a54fa",
"0xac7b47f99836510ae9076dc5f5da1f370679dea1d47073307a14cbb125cdc7822ae619637135777cb40e13d897fd00a7",
"0x997794110b9655833a88ad5a4ec40a3dc7964877bfbeb04ca1abe1d51bdc43e20e4c5757028896d298d7da954a6f14a1"
]
}
}"#).expect("should decode"))
}
}

View File

@@ -61,6 +61,7 @@ mod metrics;
pub mod payload_cache;
mod payload_status;
pub mod test_utils;
mod versioned_hashes;
/// Indicates the default jwt authenticated execution endpoint.
pub const DEFAULT_EXECUTION_ENDPOINT: &str = "http://localhost:8551/";
@@ -141,6 +142,7 @@ pub enum Error {
InvalidBlobConversion(String),
BeaconStateError(BeaconStateError),
PayloadTypeMismatch,
VerifyingVersionedHashes(versioned_hashes::Error),
}
impl From<BeaconStateError> for Error {
@@ -1321,7 +1323,7 @@ impl<T: EthSpec> ExecutionLayer<T> {
/// Maps to the `engine_newPayload` JSON-RPC call.
pub async fn notify_new_payload(
&self,
new_payload_request: NewPayloadRequest<T>,
new_payload_request: NewPayloadRequest<'_, T>,
) -> Result<PayloadStatus, Error> {
let _timer = metrics::start_timer_vec(
&metrics::EXECUTION_LAYER_REQUEST_TIMES,

View File

@@ -699,7 +699,7 @@ pub fn generate_blobs<E: EthSpec>(
Ok((bundle, transactions.into()))
}
fn static_valid_tx<T: EthSpec>() -> Result<Transaction<T::MaxBytesPerTransaction>, String> {
pub fn static_valid_tx<T: EthSpec>() -> Result<Transaction<T::MaxBytesPerTransaction>, String> {
// This is a real transaction hex encoded, but we don't care about the contents of the transaction.
let transaction: EthersTransaction = serde_json::from_str(
r#"{

View File

@@ -244,7 +244,7 @@ impl<T: EthSpec> MockExecutionLayer<T> {
// TODO: again consider forks
let status = self
.el
.notify_new_payload(payload.try_into().unwrap())
.notify_new_payload(payload.to_ref().try_into().unwrap())
.await
.unwrap();
assert_eq!(status, PayloadStatus::Valid);

View File

@@ -25,8 +25,8 @@ use warp::{http::StatusCode, Filter, Rejection};
use crate::EngineCapabilities;
pub use execution_block_generator::{
generate_blobs, generate_genesis_block, generate_genesis_header, generate_pow_block, Block,
ExecutionBlockGenerator,
generate_blobs, generate_genesis_block, generate_genesis_header, generate_pow_block,
static_valid_tx, Block, ExecutionBlockGenerator,
};
pub use hook::Hook;
pub use mock_builder::{MockBuilder, Operation};

View File

@@ -0,0 +1,135 @@
extern crate alloy_consensus;
extern crate alloy_rlp;
use alloy_consensus::TxEnvelope;
use alloy_rlp::Decodable;
use types::{EthSpec, ExecutionPayloadRef, Hash256, Unsigned, VersionedHash};
#[derive(Debug)]
pub enum Error {
DecodingTransaction(String),
LengthMismatch { expected: usize, found: usize },
VersionHashMismatch { expected: Hash256, found: Hash256 },
}
pub fn verify_versioned_hashes<E: EthSpec>(
execution_payload: ExecutionPayloadRef<E>,
expected_versioned_hashes: &[VersionedHash],
) -> Result<(), Error> {
let versioned_hashes =
extract_versioned_hashes_from_transactions::<E>(execution_payload.transactions())?;
if versioned_hashes.len() != expected_versioned_hashes.len() {
return Err(Error::LengthMismatch {
expected: expected_versioned_hashes.len(),
found: versioned_hashes.len(),
});
}
for (found, expected) in versioned_hashes
.iter()
.zip(expected_versioned_hashes.iter())
{
if found != expected {
return Err(Error::VersionHashMismatch {
expected: *expected,
found: *found,
});
}
}
Ok(())
}
pub fn extract_versioned_hashes_from_transactions<E: EthSpec>(
transactions: &types::Transactions<E>,
) -> Result<Vec<VersionedHash>, Error> {
let mut versioned_hashes = Vec::new();
for tx in transactions {
match beacon_tx_to_tx_envelope(tx)? {
TxEnvelope::Eip4844(signed_tx_eip4844) => {
versioned_hashes.extend(
signed_tx_eip4844
.tx()
.blob_versioned_hashes
.iter()
.map(|fb| Hash256::from(fb.0)),
);
}
// enumerating all variants explicitly to make pattern irrefutable
// in case new types are added in the future which also have blobs
TxEnvelope::Legacy(_)
| TxEnvelope::TaggedLegacy(_)
| TxEnvelope::Eip2930(_)
| TxEnvelope::Eip1559(_) => {}
}
}
Ok(versioned_hashes)
}
pub fn beacon_tx_to_tx_envelope<N: Unsigned>(
tx: &types::Transaction<N>,
) -> Result<TxEnvelope, Error> {
let tx_bytes = Vec::from(tx.clone());
TxEnvelope::decode(&mut tx_bytes.as_slice())
.map_err(|e| Error::DecodingTransaction(e.to_string()))
}
#[cfg(test)]
mod test {
use super::*;
use crate::test_utils::static_valid_tx;
use alloy_consensus::{TxKind, TxLegacy};
type E = types::MainnetEthSpec;
#[test]
fn test_decode_static_transaction() {
let valid_tx = static_valid_tx::<E>().expect("should give me known valid transaction");
let tx_envelope = beacon_tx_to_tx_envelope(&valid_tx).expect("should decode tx");
let TxEnvelope::Legacy(signed_tx) = tx_envelope else {
panic!("should decode to legacy transaction");
};
assert!(matches!(
signed_tx.tx(),
TxLegacy {
chain_id: Some(0x01),
nonce: 0x15,
gas_price: 0x4a817c800,
to: TxKind::Call(..),
..
}
));
}
#[test]
fn test_extract_versioned_hashes() {
use serde::Deserialize;
#[derive(Deserialize)]
#[serde(transparent)]
struct TestTransactions<E: EthSpec>(
#[serde(with = "ssz_types::serde_utils::list_of_hex_var_list")] types::Transactions<E>,
);
let TestTransactions(raw_transactions): TestTransactions<E> = serde_json::from_str(r#"[
"0x03f901388501a1f0ff430f843b9aca00843b9aca0082520894e7249813d8ccf6fa95a2203f46a64166073d58878080c002f8c6a0012e98362c814f1724262c0d211a1463418a5f6382a8d457b37a2698afbe7b5ea00100ef985761395dfa8ed5ce91f3f2180b612401909e4cb8f33b90c8a454d9baa0013d45411623b90d90f916e4025ada74b453dd4ca093c017c838367c9de0f801a001753e2af0b1e70e7ef80541355b2a035cc9b2c177418bb2a4402a9b346cf84da0011789b520a8068094a92aa0b04db8d8ef1c6c9818947c5210821732b8744049a0011c4c4f95597305daa5f62bf5f690e37fa11f5de05a95d05cac4e2119e394db80a0ccd86a742af0e042d08cbb35d910ddc24bbc6538f9e53be6620d4b6e1bb77662a01a8bacbc614940ac2f5c23ffc00a122c9f085046883de65c88ab0edb859acb99",
"0x02f9017a8501a1f0ff4382363485012a05f2008512a05f2000830249f094c1b0bc605e2c808aa0867bfc98e51a1fe3e9867f80b901040cc7326300000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000009445a285baa43e00000000000000000000000000c500931f24edb821cef6e28f7adb33b38578c82000000000000000000000000fc7360b3b28cf4204268a8354dbec60720d155d2000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000009a054a063f0fe7b9c68de8df91aaa5e96c15ab540000000000000000000000000c8d41b8fcc066cdabaf074d78e5153e8ce018a9c080a008e14475c1173cd9f5740c24c08b793f9e16c36c08fa73769db95050e31e3396a019767dcdda26c4a774ca28c9df15d0c20e43bd07bd33ee0f84d6096cb5a1ebed"
]"#).expect("should get raw transactions");
let expected_versioned_hashes = vec![
"0x012e98362c814f1724262c0d211a1463418a5f6382a8d457b37a2698afbe7b5e",
"0x0100ef985761395dfa8ed5ce91f3f2180b612401909e4cb8f33b90c8a454d9ba",
"0x013d45411623b90d90f916e4025ada74b453dd4ca093c017c838367c9de0f801",
"0x01753e2af0b1e70e7ef80541355b2a035cc9b2c177418bb2a4402a9b346cf84d",
"0x011789b520a8068094a92aa0b04db8d8ef1c6c9818947c5210821732b8744049",
"0x011c4c4f95597305daa5f62bf5f690e37fa11f5de05a95d05cac4e2119e394db",
]
.into_iter()
.map(|tx| Hash256::from_slice(&hex::decode(&tx[2..]).expect("should decode hex")))
.collect::<Vec<_>>();
let versioned_hashes = extract_versioned_hashes_from_transactions::<E>(&raw_transactions)
.expect("should get versioned hashes");
assert_eq!(versioned_hashes, expected_versioned_hashes);
}
}