diff --git a/crates/blockchain/dev/block_producer.rs b/crates/blockchain/dev/block_producer.rs index 7cbe818e7..2de974b90 100644 --- a/crates/blockchain/dev/block_producer.rs +++ b/crates/blockchain/dev/block_producer.rs @@ -62,6 +62,7 @@ pub async fn start_block_producer( execution_payload_response.execution_payload, execution_payload_response .blobs_bundle + .unwrap_or_default() .commitments .iter() .map(|commitment| { diff --git a/crates/blockchain/dev/utils/engine_client/mod.rs b/crates/blockchain/dev/utils/engine_client/mod.rs index 47dd69f55..05f0987da 100644 --- a/crates/blockchain/dev/utils/engine_client/mod.rs +++ b/crates/blockchain/dev/utils/engine_client/mod.rs @@ -7,12 +7,15 @@ use ethereum_types::H256; use ethrex_rpc::{ engine::{ fork_choice::ForkChoiceUpdatedV3, - payload::{GetPayloadV3Request, NewPayloadV3Request}, + payload::{ + GetPayloadRequest, GetPayloadRequestVersion, GetPayloadV3Request, NewPayloadRequest, + NewPayloadRequestVersion, NewPayloadV3Request, + }, ExchangeCapabilitiesRequest, }, types::{ fork_choice::{ForkChoiceResponse, ForkChoiceState, PayloadAttributesV3}, - payload::{ExecutionPayloadResponse, ExecutionPayloadV3, PayloadStatus}, + payload::{ExecutionPayload, ExecutionPayloadResponse, PayloadStatus}, }, utils::{RpcErrorResponse, RpcRequest, RpcSuccessResponse}, }; @@ -79,15 +82,10 @@ impl EngineClient { state: ForkChoiceState, payload_attributes: Option, ) -> Result { - let request = ForkChoiceUpdatedV3 { + let request = RpcRequest::from(ForkChoiceUpdatedV3 { fork_choice_state: state, - payload_attributes: Ok(payload_attributes), - } - .try_into() - .map_err(|e| { - ForkChoiceUpdateError::ConversionError(format!("Failed to convert to RPC request: {e}")) - }) - .map_err(EngineClientError::from)?; + payload_attributes, + }); match self.send_request(request).await { Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) @@ -104,7 +102,11 @@ impl EngineClient { &self, payload_id: u64, ) -> Result { - let request = GetPayloadV3Request { payload_id }.into(); + let request = GetPayloadV3Request(GetPayloadRequest { + payload_id, + version: GetPayloadRequestVersion::V3, + }) + .into(); match self.send_request(request).await { Ok(RpcResponse::Success(result)) => serde_json::from_value(result.result) @@ -119,14 +121,18 @@ impl EngineClient { pub async fn engine_new_payload_v3( &self, - execution_payload: ExecutionPayloadV3, + execution_payload: ExecutionPayload, expected_blob_versioned_hashes: Vec, parent_beacon_block_root: H256, ) -> Result { let request = NewPayloadV3Request { - payload: execution_payload, - expected_blob_versioned_hashes, - parent_beacon_block_root, + new_payload_request: NewPayloadRequest { + payload: execution_payload, + version: NewPayloadRequestVersion::V3 { + expected_blob_versioned_hashes, + parent_beacon_block_root, + }, + }, } .into(); diff --git a/crates/blockchain/payload.rs b/crates/blockchain/payload.rs index a9e841fb8..287889084 100644 --- a/crates/blockchain/payload.rs +++ b/crates/blockchain/payload.rs @@ -96,7 +96,9 @@ pub fn create_payload(args: &BuildPayloadArgs, storage: &Store) -> Result, +} + +impl RpcHandler for ForkChoiceUpdatedV2 { + fn parse(params: &Option>) -> Result { + let (fork_choice_state, payload_attributes) = parse(params)?; + + Ok(ForkChoiceUpdatedV2 { + fork_choice_state, + payload_attributes, + }) + } + + fn handle(&self, context: RpcApiContext) -> Result { + let (head_block_opt, mut response) = + handle_forkchoice(&self.fork_choice_state, context.clone(), 2)?; + if let (Some(head_block), Some(attributes)) = (head_block_opt, &self.payload_attributes) { + validate_v2(attributes, head_block, &context)?; + let payload_id = build_payload(attributes, context, &self.fork_choice_state, 2)?; + response.set_id(payload_id); + } + + serde_json::to_value(response).map_err(|error| RpcErr::Internal(error.to_string())) + } +} + #[derive(Debug)] pub struct ForkChoiceUpdatedV3 { pub fork_choice_state: ForkChoiceState, - #[allow(unused)] - pub payload_attributes: Result, String>, + pub payload_attributes: Option, } -impl TryFrom for RpcRequest { - type Error = String; - - fn try_from(val: ForkChoiceUpdatedV3) -> Result { - match val.payload_attributes { - Ok(attrs) => Ok(RpcRequest { - method: "engine_forkchoiceUpdatedV3".to_string(), - params: Some(vec![ - serde_json::json!(val.fork_choice_state), - serde_json::json!(attrs), - ]), - ..Default::default() - }), - Err(err) => Err(err), +impl From for RpcRequest { + fn from(val: ForkChoiceUpdatedV3) -> Self { + RpcRequest { + method: "engine_forkchoiceUpdatedV3".to_string(), + params: Some(vec![ + serde_json::json!(val.fork_choice_state), + serde_json::json!(val.payload_attributes), + ]), + ..Default::default() } } } impl RpcHandler for ForkChoiceUpdatedV3 { - // TODO(#853): Allow fork choice to be executed even if fork choice updated v3 was not correctly parsed. fn parse(params: &Option>) -> Result { - let params = params - .as_ref() - .ok_or(RpcErr::BadParams("No params provided".to_owned()))?; - if params.len() != 2 { - return Err(RpcErr::BadParams("Expected 2 params".to_owned())); - } + let (fork_choice_state, payload_attributes) = parse(params)?; + Ok(ForkChoiceUpdatedV3 { - fork_choice_state: serde_json::from_value(params[0].clone())?, - payload_attributes: serde_json::from_value(params[1].clone()) - .map_err(|e| e.to_string()), + fork_choice_state, + payload_attributes, }) } fn handle(&self, context: RpcApiContext) -> Result { - info!( - "New fork choice request with head: {}, safe: {}, finalized: {}.", - self.fork_choice_state.head_block_hash, - self.fork_choice_state.safe_block_hash, - self.fork_choice_state.finalized_block_hash - ); - - let head_block = match apply_fork_choice( - &context.storage, - self.fork_choice_state.head_block_hash, - self.fork_choice_state.safe_block_hash, - self.fork_choice_state.finalized_block_hash, - ) { - Ok(head) => head, - Err(error) => { - let fork_choice_response = match error { - InvalidForkChoice::NewHeadAlreadyCanonical => { - ForkChoiceResponse::from(PayloadStatus::valid_with_hash( - latest_canonical_block_hash(&context.storage).unwrap(), - )) - } - InvalidForkChoice::Syncing => { - // Start sync - let current_number = context.storage.get_latest_block_number()?.unwrap(); - let Some(current_head) = - context.storage.get_canonical_block_hash(current_number)? - else { - return Err(RpcErr::Internal( - "Missing latest canonical block".to_owned(), - )); - }; - let sync_head = self.fork_choice_state.head_block_hash; - tokio::spawn(async move { - // If we can't get hold of the syncer, then it means that there is an active sync in process - if let Ok(mut syncer) = context.syncer.try_lock() { - syncer - .start_sync(current_head, sync_head, context.storage.clone()) - .await - } - }); - ForkChoiceResponse::from(PayloadStatus::syncing()) - } - reason => { - warn!("Invalid fork choice state. Reason: {:#?}", reason); - return Err(RpcErr::InvalidForkChoiceState(reason.to_string())); - } - }; - return serde_json::to_value(fork_choice_response) - .map_err(|error| RpcErr::Internal(error.to_string())); - } + let (head_block_opt, mut response) = + handle_forkchoice(&self.fork_choice_state, context.clone(), 2)?; + if let (Some(head_block), Some(attributes)) = (head_block_opt, &self.payload_attributes) { + validate_v3(attributes, head_block, &context)?; + let payload_id = build_payload(attributes, context, &self.fork_choice_state, 3)?; + response.set_id(payload_id); + } + + serde_json::to_value(response).map_err(|error| RpcErr::Internal(error.to_string())) + } +} + +fn parse( + params: &Option>, +) -> Result<(ForkChoiceState, Option), RpcErr> { + let params = params + .as_ref() + .ok_or(RpcErr::BadParams("No params provided".to_owned()))?; + if params.len() != 2 { + return Err(RpcErr::BadParams("Expected 2 params".to_owned())); + } + + let forkchoice_state: ForkChoiceState = serde_json::from_value(params[0].clone())?; + // if there is an error when parsing, set to None + let payload_attributes: Option = + if let Ok(attributes) = serde_json::from_value::(params[1].clone()) { + Some(attributes) + } else { + None }; - // Build block from received payload. This step is skipped if applying the fork choice state failed - let mut response = ForkChoiceResponse::from(PayloadStatus::valid_with_hash( - self.fork_choice_state.head_block_hash, - )); + Ok((forkchoice_state, payload_attributes)) +} - match &self.payload_attributes { - // Payload may be invalid but we had to apply fork choice state nevertheless. - Err(e) => return Err(RpcErr::InvalidPayloadAttributes(e.into())), - Ok(None) => (), - Ok(Some(attributes)) => { - info!("Fork choice updated includes payload attributes. Creating a new payload."); - let chain_config = context.storage.get_chain_config()?; - if !chain_config.is_cancun_activated(attributes.timestamp) { - return Err(RpcErr::UnsuportedFork( - "forkChoiceV3 used to build pre-Cancun payload".to_string(), - )); +fn handle_forkchoice( + fork_choice_state: &ForkChoiceState, + context: RpcApiContext, + version: usize, +) -> Result<(Option, ForkChoiceResponse), RpcErr> { + info!( + "New fork choice request v{} with head: {}, safe: {}, finalized: {}.", + version, + fork_choice_state.head_block_hash, + fork_choice_state.safe_block_hash, + fork_choice_state.finalized_block_hash + ); + + match apply_fork_choice( + &context.storage, + fork_choice_state.head_block_hash, + fork_choice_state.safe_block_hash, + fork_choice_state.finalized_block_hash, + ) { + Ok(head) => Ok(( + Some(head), + ForkChoiceResponse::from(PayloadStatus::valid_with_hash( + fork_choice_state.head_block_hash, + )), + )), + Err(forkchoice_error) => { + let forkchoice_response = match forkchoice_error { + InvalidForkChoice::NewHeadAlreadyCanonical => { + ForkChoiceResponse::from(PayloadStatus::valid_with_hash( + latest_canonical_block_hash(&context.storage).unwrap(), + )) + } + InvalidForkChoice::Syncing => { + // Start sync + let current_number = context.storage.get_latest_block_number()?.unwrap(); + let Some(current_head) = + context.storage.get_canonical_block_hash(current_number)? + else { + return Err(RpcErr::Internal( + "Missing latest canonical block".to_owned(), + )); + }; + let sync_head = fork_choice_state.head_block_hash; + tokio::spawn(async move { + // If we can't get hold of the syncer, then it means that there is an active sync in process + if let Ok(mut syncer) = context.syncer.try_lock() { + syncer + .start_sync(current_head, sync_head, context.storage.clone()) + .await + } + }); + ForkChoiceResponse::from(PayloadStatus::syncing()) } - if attributes.timestamp <= head_block.timestamp { - return Err(RpcErr::InvalidPayloadAttributes( - "invalid timestamp".to_string(), - )); + reason => { + warn!("Invalid fork choice state. Reason: {:#?}", reason); + return Err(RpcErr::InvalidForkChoiceState(reason.to_string())); } - let args = BuildPayloadArgs { - parent: self.fork_choice_state.head_block_hash, - timestamp: attributes.timestamp, - fee_recipient: attributes.suggested_fee_recipient, - random: attributes.prev_randao, - withdrawals: attributes.withdrawals.clone(), - beacon_root: Some(attributes.parent_beacon_block_root), - version: 3, - }; - let payload_id = args.id(); - response.set_id(payload_id); - let payload = match create_payload(&args, &context.storage) { - Ok(payload) => payload, - Err(ChainError::EvmError(error)) => return Err(error.into()), - // Parent block is guaranteed to be present at this point, - // so the only errors that may be returned are internal storage errors - Err(error) => return Err(RpcErr::Internal(error.to_string())), - }; - context.storage.add_payload(payload_id, payload)?; - } + }; + Ok((None, forkchoice_response)) } + } +} - serde_json::to_value(response).map_err(|error| RpcErr::Internal(error.to_string())) +fn validate_v3( + attributes: &PayloadAttributesV3, + head_block: BlockHeader, + context: &RpcApiContext, +) -> Result<(), RpcErr> { + let chain_config = context.storage.get_chain_config()?; + if attributes.parent_beacon_block_root.is_none() { + return Err(RpcErr::InvalidPayloadAttributes( + "Null Parent Beacon Root".to_string(), + )); + } + if !chain_config.is_cancun_activated(attributes.timestamp) { + return Err(RpcErr::UnsuportedFork( + "forkChoiceV3 used to build pre-Cancun payload".to_string(), + )); } + if attributes.timestamp <= head_block.timestamp { + return Err(RpcErr::InvalidPayloadAttributes( + "invalid timestamp".to_string(), + )); + } + Ok(()) +} + +fn validate_v2( + attributes: &PayloadAttributesV3, + head_block: BlockHeader, + context: &RpcApiContext, +) -> Result<(), RpcErr> { + let chain_config = context.storage.get_chain_config()?; + if attributes.parent_beacon_block_root.is_some() { + return Err(RpcErr::InvalidPayloadAttributes( + "forkChoiceV2 with Beacon Root".to_string(), + )); + } + if !chain_config.is_shanghai_activated(attributes.timestamp) { + return Err(RpcErr::UnsuportedFork( + "forkChoiceV2 used to build pre-Shanghai payload".to_string(), + )); + } + if chain_config.is_cancun_activated(attributes.timestamp) { + return Err(RpcErr::UnsuportedFork( + "forkChoiceV2 used to build Cancun payload".to_string(), + )); + } + if attributes.timestamp <= head_block.timestamp { + return Err(RpcErr::InvalidPayloadAttributes( + "invalid timestamp".to_string(), + )); + } + Ok(()) +} + +fn build_payload( + attributes: &PayloadAttributesV3, + context: RpcApiContext, + fork_choice_state: &ForkChoiceState, + version: u8, +) -> Result { + info!("Fork choice updated includes payload attributes. Creating a new payload."); + let args = BuildPayloadArgs { + parent: fork_choice_state.head_block_hash, + timestamp: attributes.timestamp, + fee_recipient: attributes.suggested_fee_recipient, + random: attributes.prev_randao, + withdrawals: attributes.withdrawals.clone(), + beacon_root: attributes.parent_beacon_block_root, + version, + }; + let payload_id = args.id(); + let payload = match create_payload(&args, &context.storage) { + Ok(payload) => payload, + Err(ChainError::EvmError(error)) => return Err(error.into()), + // Parent block is guaranteed to be present at this point, + // so the only errors that may be returned are internal storage errors + Err(error) => return Err(RpcErr::Internal(error.to_string())), + }; + context.storage.add_payload(payload_id, payload)?; + + Ok(payload_id) } diff --git a/crates/networking/rpc/engine/payload.rs b/crates/networking/rpc/engine/payload.rs index 9d7eb0089..2906cbf6a 100644 --- a/crates/networking/rpc/engine/payload.rs +++ b/crates/networking/rpc/engine/payload.rs @@ -1,59 +1,108 @@ use ethrex_blockchain::add_block; use ethrex_blockchain::error::ChainError; use ethrex_blockchain::payload::build_payload; -use ethrex_core::types::Fork; +use ethrex_core::types::{BlobsBundle, Block, Fork}; use ethrex_core::{H256, U256}; use serde_json::Value; use tracing::{error, info, warn}; -use crate::types::payload::ExecutionPayloadResponse; +use crate::types::payload::{ExecutionPayload, ExecutionPayloadResponse, PayloadStatus}; use crate::utils::RpcRequest; -use crate::RpcApiContext; -use crate::{ - types::payload::{ExecutionPayloadV3, PayloadStatus}, - RpcErr, RpcHandler, -}; +use crate::{RpcApiContext, RpcErr, RpcHandler}; -pub struct NewPayloadV3Request { - pub payload: ExecutionPayloadV3, - pub expected_blob_versioned_hashes: Vec, - pub parent_beacon_block_root: H256, +#[derive(Debug)] +pub enum NewPayloadRequestVersion { + V1, + V2, + V3 { + expected_blob_versioned_hashes: Vec, + parent_beacon_block_root: H256, + }, } -pub struct GetPayloadV3Request { - pub payload_id: u64, +pub struct NewPayloadRequest { + pub payload: ExecutionPayload, + pub version: NewPayloadRequestVersion, } -impl From for RpcRequest { - fn from(val: NewPayloadV3Request) -> Self { - RpcRequest { - method: "engine_newPayloadV3".to_string(), - params: Some(vec![ - serde_json::json!(val.payload), - serde_json::json!(val.expected_blob_versioned_hashes), - serde_json::json!(val.parent_beacon_block_root), - ]), - ..Default::default() +impl NewPayloadRequest { + fn from(&self) -> RpcRequest { + match &self.version { + NewPayloadRequestVersion::V1 => todo!(), + NewPayloadRequestVersion::V2 => RpcRequest { + method: "engine_newPayloadV2".to_string(), + params: Some(vec![serde_json::json!(self.payload)]), + ..Default::default() + }, + NewPayloadRequestVersion::V3 { + expected_blob_versioned_hashes, + parent_beacon_block_root, + } => RpcRequest { + method: "engine_newPayloadV3".to_string(), + params: Some(vec![ + serde_json::json!(self.payload), + serde_json::json!(expected_blob_versioned_hashes), + serde_json::json!(parent_beacon_block_root), + ]), + ..Default::default() + }, } } -} -impl RpcHandler for NewPayloadV3Request { - fn parse(params: &Option>) -> Result { - let params = params - .as_ref() - .ok_or(RpcErr::BadParams("No params provided".to_owned()))?; - if params.len() != 3 { - return Err(RpcErr::BadParams("Expected 3 params".to_owned())); + fn parent_beacon_block_root(&self) -> Option { + match self.version { + NewPayloadRequestVersion::V1 => None, + NewPayloadRequestVersion::V2 => None, + NewPayloadRequestVersion::V3 { + parent_beacon_block_root, + .. + } => Some(parent_beacon_block_root), + } + } + + fn validate_execution_payload(&self) -> Result<(), RpcErr> { + match self.version { + NewPayloadRequestVersion::V1 => Ok(()), + NewPayloadRequestVersion::V2 => Ok(()), + NewPayloadRequestVersion::V3 { .. } => { + if self.payload.excess_blob_gas.is_none() { + return Err(RpcErr::WrongParam("excess_blob_gas".to_string())); + } + if self.payload.blob_gas_used.is_none() { + return Err(RpcErr::WrongParam("blob_gas_used".to_string())); + } + Ok(()) + } + } + } + + fn valid_fork(&self) -> Fork { + match self.version { + NewPayloadRequestVersion::V1 => Fork::Paris, + NewPayloadRequestVersion::V2 => Fork::Shanghai, + NewPayloadRequestVersion::V3 { .. } => Fork::Cancun, + } + } + + fn are_blob_versioned_hashes_invalid(&self, block: &Block) -> bool { + match &self.version { + NewPayloadRequestVersion::V1 => false, + NewPayloadRequestVersion::V2 => false, + NewPayloadRequestVersion::V3 { + expected_blob_versioned_hashes, + .. + } => { + // Concatenate blob versioned hashes lists (tx.blob_versioned_hashes) of each blob transaction included in the payload, respecting the order of inclusion + // and check that the resulting array matches expected_blob_versioned_hashes + let blob_versioned_hashes: Vec = block + .body + .transactions + .iter() + .flat_map(|tx| tx.blob_versioned_hashes()) + .collect(); + *expected_blob_versioned_hashes != blob_versioned_hashes + } } - Ok(NewPayloadV3Request { - payload: serde_json::from_value(params[0].clone()) - .map_err(|_| RpcErr::WrongParam("payload".to_string()))?, - expected_blob_versioned_hashes: serde_json::from_value(params[1].clone()) - .map_err(|_| RpcErr::WrongParam("expected_blob_versioned_hashes".to_string()))?, - parent_beacon_block_root: serde_json::from_value(params[2].clone()) - .map_err(|_| RpcErr::WrongParam("parent_beacon_block_root".to_string()))?, - }) } fn handle(&self, context: RpcApiContext) -> Result { @@ -65,7 +114,7 @@ impl RpcHandler for NewPayloadV3Request { let block = match self .payload .clone() - .into_block(self.parent_beacon_block_root) + .into_block(self.parent_beacon_block_root()) { Ok(block) => block, Err(error) => { @@ -76,11 +125,12 @@ impl RpcHandler for NewPayloadV3Request { }; // Payload Validation + self.validate_execution_payload()?; - // Check timestamp is post Cancun fork + // Check timestamp is post valid fork let chain_config = storage.get_chain_config()?; let current_fork = chain_config.get_fork(block.header.timestamp); - if current_fork < Fork::Cancun { + if current_fork < self.valid_fork() { return Err(RpcErr::UnsuportedFork(format!("{current_fork:?}"))); } @@ -93,15 +143,7 @@ impl RpcHandler for NewPayloadV3Request { } info!("Block hash {block_hash} is valid"); - // Concatenate blob versioned hashes lists (tx.blob_versioned_hashes) of each blob transaction included in the payload, respecting the order of inclusion - // and check that the resulting array matches expected_blob_versioned_hashes - let blob_versioned_hashes: Vec = block - .body - .transactions - .iter() - .flat_map(|tx| tx.blob_versioned_hashes()) - .collect(); - if self.expected_blob_versioned_hashes != blob_versioned_hashes { + if self.are_blob_versioned_hashes_invalid(&block) { let result = PayloadStatus::invalid_with_err("Invalid blob_versioned_hashes"); return serde_json::to_value(result) .map_err(|error| RpcErr::Internal(error.to_string())); @@ -156,18 +198,126 @@ impl RpcHandler for NewPayloadV3Request { } } -impl From for RpcRequest { - fn from(val: GetPayloadV3Request) -> Self { - RpcRequest { - method: "engine_getPayloadV3".to_string(), - params: Some(vec![serde_json::json!(U256::from(val.payload_id))]), - ..Default::default() +pub struct NewPayloadV3Request { + pub new_payload_request: NewPayloadRequest, +} + +impl From for RpcRequest { + fn from(val: NewPayloadV3Request) -> Self { + val.new_payload_request.from() + } +} + +impl RpcHandler for NewPayloadV3Request { + fn parse(params: &Option>) -> Result { + let params = params + .as_ref() + .ok_or(RpcErr::BadParams("No params provided".to_owned()))?; + if params.len() != 3 { + return Err(RpcErr::BadParams("Expected 3 params".to_owned())); } + Ok(NewPayloadV3Request { + new_payload_request: NewPayloadRequest { + payload: serde_json::from_value(params[0].clone()) + .map_err(|_| RpcErr::WrongParam("payload".to_string()))?, + version: NewPayloadRequestVersion::V3 { + expected_blob_versioned_hashes: serde_json::from_value(params[1].clone()) + .map_err(|_| { + RpcErr::WrongParam("expected_blob_versioned_hashes".to_string()) + })?, + parent_beacon_block_root: serde_json::from_value(params[2].clone()) + .map_err(|_| RpcErr::WrongParam("parent_beacon_block_root".to_string()))?, + }, + }, + }) + } + + fn handle(&self, context: RpcApiContext) -> Result { + self.new_payload_request.handle(context) } } -impl RpcHandler for GetPayloadV3Request { +pub struct NewPayloadV2Request { + pub new_payload_request: NewPayloadRequest, +} + +impl From for RpcRequest { + fn from(val: NewPayloadV2Request) -> Self { + val.new_payload_request.from() + } +} + +impl RpcHandler for NewPayloadV2Request { fn parse(params: &Option>) -> Result { + let params = params + .as_ref() + .ok_or(RpcErr::BadParams("No params provided".to_owned()))?; + if params.len() != 1 { + return Err(RpcErr::BadParams("Expected 1 params".to_owned())); + } + Ok(NewPayloadV2Request { + new_payload_request: NewPayloadRequest { + payload: serde_json::from_value(params[0].clone()) + .map_err(|_| RpcErr::WrongParam("payload".to_string()))?, + version: NewPayloadRequestVersion::V2, + }, + }) + } + + fn handle(&self, context: RpcApiContext) -> Result { + self.new_payload_request.handle(context) + } +} + +#[derive(Clone)] +pub enum GetPayloadRequestVersion { + V1 = 1, + V2 = 2, + V3 = 3, +} + +pub struct GetPayloadRequest { + pub payload_id: u64, + pub version: GetPayloadRequestVersion, +} + +impl GetPayloadRequest { + fn method(&self) -> String { + format!("engine_getPayloadV{}", self.version.clone() as usize) + } + + fn valid_fork(&self) -> Fork { + match self.version { + GetPayloadRequestVersion::V1 => Fork::Paris, + GetPayloadRequestVersion::V2 => Fork::Shanghai, + GetPayloadRequestVersion::V3 => Fork::Cancun, + } + } + + fn build_response( + &self, + execution_payload: ExecutionPayload, + payload_blobs_bundle: BlobsBundle, + block_value: U256, + ) -> ExecutionPayloadResponse { + let (blobs_bundle, should_override_builder) = + if let GetPayloadRequestVersion::V3 = self.version { + (Some(payload_blobs_bundle), Some(false)) + } else { + (None, None) + }; + ExecutionPayloadResponse { + execution_payload, + block_value, + blobs_bundle, + should_override_builder, + } + } + + fn parse( + params: &Option>, + version: GetPayloadRequestVersion, + ) -> Result { let params = params .as_ref() .ok_or(RpcErr::BadParams("No params provided".to_owned()))?; @@ -187,7 +337,18 @@ impl RpcHandler for GetPayloadV3Request { let Ok(payload_id) = u64::from_str_radix(hex_str, 16) else { return Err(RpcErr::BadHexFormat(0)); }; - Ok(GetPayloadV3Request { payload_id }) + Ok(GetPayloadRequest { + payload_id, + version, + }) + } + + fn from(&self) -> RpcRequest { + RpcRequest { + method: self.method(), + params: Some(vec![serde_json::json!(U256::from(self.payload_id))]), + ..Default::default() + } } fn handle(&self, context: RpcApiContext) -> Result { @@ -198,14 +359,61 @@ impl RpcHandler for GetPayloadV3Request { self.payload_id ))); }; + // Check timestamp matches valid fork + let chain_config = &context.storage.get_chain_config()?; + let current_fork = chain_config.get_fork(payload.header.timestamp); + info!("Current Fork: {:?}", current_fork); + if current_fork != self.valid_fork() { + return Err(RpcErr::UnsuportedFork(format!("{current_fork:?}"))); + } + let (blobs_bundle, block_value) = build_payload(&mut payload, &context.storage) .map_err(|err| RpcErr::Internal(err.to_string()))?; - serde_json::to_value(ExecutionPayloadResponse { - execution_payload: ExecutionPayloadV3::from_block(payload), - block_value, - blobs_bundle, - should_override_builder: false, - }) - .map_err(|error| RpcErr::Internal(error.to_string())) + let execution_payload = ExecutionPayload::from_block(payload); + + serde_json::to_value(self.build_response(execution_payload, blobs_bundle, block_value)) + .map_err(|error| RpcErr::Internal(error.to_string())) + } +} + +pub struct GetPayloadV3Request(pub GetPayloadRequest); + +impl From for RpcRequest { + fn from(val: GetPayloadV3Request) -> Self { + GetPayloadRequest::from(&val.0) + } +} + +impl RpcHandler for GetPayloadV3Request { + fn parse(params: &Option>) -> Result { + Ok(Self(GetPayloadRequest::parse( + params, + GetPayloadRequestVersion::V3, + )?)) + } + + fn handle(&self, context: RpcApiContext) -> Result { + GetPayloadRequest::handle(&self.0, context) + } +} + +pub struct GetPayloadV2Request(pub GetPayloadRequest); + +impl From for RpcRequest { + fn from(val: GetPayloadV2Request) -> Self { + GetPayloadRequest::from(&val.0) + } +} + +impl RpcHandler for GetPayloadV2Request { + fn parse(params: &Option>) -> Result { + Ok(Self(GetPayloadRequest::parse( + params, + GetPayloadRequestVersion::V2, + )?)) + } + + fn handle(&self, context: RpcApiContext) -> Result { + GetPayloadRequest::handle(&self.0, context) } } diff --git a/crates/networking/rpc/rpc.rs b/crates/networking/rpc/rpc.rs index 80581d2ab..931278cc2 100644 --- a/crates/networking/rpc/rpc.rs +++ b/crates/networking/rpc/rpc.rs @@ -7,8 +7,8 @@ use axum_extra::{ use bytes::Bytes; use engine::{ exchange_transition_config::ExchangeTransitionConfigV1Req, - fork_choice::ForkChoiceUpdatedV3, - payload::{GetPayloadV3Request, NewPayloadV3Request}, + fork_choice::{ForkChoiceUpdatedV2, ForkChoiceUpdatedV3}, + payload::{GetPayloadV2Request, GetPayloadV3Request, NewPayloadV2Request, NewPayloadV3Request}, ExchangeCapabilitiesRequest, }; use eth::{ @@ -253,12 +253,15 @@ pub fn map_debug_requests(req: &RpcRequest, context: RpcApiContext) -> Result Result { match req.method.as_str() { "engine_exchangeCapabilities" => ExchangeCapabilitiesRequest::call(req, context), + "engine_forkchoiceUpdatedV2" => ForkChoiceUpdatedV2::call(req, context), "engine_forkchoiceUpdatedV3" => ForkChoiceUpdatedV3::call(req, context), "engine_newPayloadV3" => NewPayloadV3Request::call(req, context), + "engine_newPayloadV2" => NewPayloadV2Request::call(req, context), "engine_exchangeTransitionConfigurationV1" => { ExchangeTransitionConfigV1Req::call(req, context) } "engine_getPayloadV3" => GetPayloadV3Request::call(req, context), + "engine_getPayloadV2" => GetPayloadV2Request::call(req, context), unknown_engine_method => Err(RpcErr::MethodNotFound(unknown_engine_method.to_owned())), } } diff --git a/crates/networking/rpc/types/fork_choice.rs b/crates/networking/rpc/types/fork_choice.rs index 4cfcf2bd3..8d31e8eff 100644 --- a/crates/networking/rpc/types/fork_choice.rs +++ b/crates/networking/rpc/types/fork_choice.rs @@ -20,7 +20,7 @@ pub struct PayloadAttributesV3 { pub prev_randao: H256, pub suggested_fee_recipient: Address, pub withdrawals: Vec, - pub parent_beacon_block_root: H256, + pub parent_beacon_block_root: Option, } #[derive(Debug, Serialize, Deserialize)] diff --git a/crates/networking/rpc/types/payload.rs b/crates/networking/rpc/types/payload.rs index b35825039..415e1d6c3 100644 --- a/crates/networking/rpc/types/payload.rs +++ b/crates/networking/rpc/types/payload.rs @@ -13,7 +13,7 @@ use ethrex_core::{ #[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] -pub struct ExecutionPayloadV3 { +pub struct ExecutionPayload { parent_hash: H256, fee_recipient: Address, state_root: H256, @@ -35,10 +35,19 @@ pub struct ExecutionPayloadV3 { pub block_hash: H256, transactions: Vec, withdrawals: Vec, - #[serde(with = "serde_utils::u64::hex_str")] - blob_gas_used: u64, - #[serde(with = "serde_utils::u64::hex_str")] - excess_blob_gas: u64, + // ExecutionPayloadV3 fields. Optional since we support V2 too + #[serde( + skip_serializing_if = "Option::is_none", + with = "serde_utils::u64::hex_str_opt", + default + )] + pub blob_gas_used: Option, + #[serde( + skip_serializing_if = "Option::is_none", + with = "serde_utils::u64::hex_str_opt", + default + )] + pub excess_blob_gas: Option, } #[derive(Clone, Debug)] @@ -78,10 +87,13 @@ impl EncodedTransaction { } } -impl ExecutionPayloadV3 { - /// Converts an `ExecutionPayloadV3` into a block (aka a BlockHeader and BlockBody) - /// using the parentBeaconBlockRoot received along with the payload in the rpc call `engine_newPayloadV3` - pub fn into_block(self, parent_beacon_block_root: H256) -> Result { +impl ExecutionPayload { + /// Converts an `ExecutionPayload` into a block (aka a BlockHeader and BlockBody) + /// using the parentBeaconBlockRoot received along with the payload in the rpc call `engine_newPayloadV2/V3` + pub fn into_block( + self, + parent_beacon_block_root: Option, + ) -> Result { let body = BlockBody { transactions: self .transactions @@ -112,9 +124,9 @@ impl ExecutionPayloadV3 { withdrawals_root: Some(compute_withdrawals_root( &body.withdrawals.clone().unwrap_or_default(), )), - blob_gas_used: Some(self.blob_gas_used), - excess_blob_gas: Some(self.excess_blob_gas), - parent_beacon_block_root: Some(parent_beacon_block_root), + blob_gas_used: self.blob_gas_used, + excess_blob_gas: self.excess_blob_gas, + parent_beacon_block_root, }; Ok(Block::new(header, body)) @@ -142,8 +154,8 @@ impl ExecutionPayloadV3 { .map(EncodedTransaction::encode) .collect(), withdrawals: block.body.withdrawals.unwrap_or_default(), - blob_gas_used: block.header.blob_gas_used.unwrap_or_default(), - excess_blob_gas: block.header.excess_blob_gas.unwrap_or_default(), + blob_gas_used: block.header.blob_gas_used, + excess_blob_gas: block.header.excess_blob_gas, } } } @@ -224,11 +236,19 @@ impl PayloadStatus { #[derive(Clone, Debug, Serialize, Deserialize)] #[serde(rename_all = "camelCase")] pub struct ExecutionPayloadResponse { - pub execution_payload: ExecutionPayloadV3, // We only handle v3 payloads + pub execution_payload: ExecutionPayload, + // Total fees consumed by the block (fees paid) + pub block_value: U256, + pub blobs_bundle: Option, + pub should_override_builder: Option, // TODO: look into this +} + +#[derive(Clone, Debug, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionPayloadResponseV2 { + pub execution_payload: ExecutionPayload, // Total fees consumed by the block (fees paid) pub block_value: U256, - pub blobs_bundle: BlobsBundle, - pub should_override_builder: bool, // TODO: look into this } #[cfg(test)] @@ -239,7 +259,7 @@ mod test { fn deserialize_payload_into_block() { // Payload extracted from running kurtosis, only some transactions are included to reduce it's size. let json = r#"{"baseFeePerGas":"0x342770c0","blobGasUsed":"0x0","blockHash":"0x4029a2342bb6d54db91457bc8e442be22b3481df8edea24cc721f9d0649f65be","blockNumber":"0x1","excessBlobGas":"0x0","extraData":"0xd883010e06846765746888676f312e32322e34856c696e7578","feeRecipient":"0x8943545177806ed17b9f23f0a21ee5948ecaa776","gasLimit":"0x17dd79d","gasUsed":"0x401640","logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","parentHash":"0x2971eefd1f71f3548728cad87c16cc91b979ef035054828c59a02e49ae300a84","prevRandao":"0x2971eefd1f71f3548728cad87c16cc91b979ef035054828c59a02e49ae300a84","receiptsRoot":"0x0185e8473b81c3a504c4919249a94a94965a2f61c06367ee6ffb88cb7a3ef02b","stateRoot":"0x0eb8fd0af53174e65bb660d0904e5016425a713d8f11c767c26148b526fc05f3","timestamp":"0x66846fb2","transactions":["0xf86d80843baa0c4082f618946177843db3138ae69679a54b95cf345ed759450d870aa87bee538000808360306ba0151ccc02146b9b11adf516e6787b59acae3e76544fdcd75e77e67c6b598ce65da064c5dd5aae2fbb535830ebbdad0234975cd7ece3562013b63ea18cc0df6c97d4","0xf86d01843baa0c4082f61894687704db07e902e9a8b3754031d168d46e3d586e870aa87bee538000808360306ba0f6c479c3e9135a61d7cca17b7354ddc311cda2d8df265d0378f940bdefd62b54a077786891b0b6bcd438d8c24d00fa6628bc2f1caa554f9dec0a96daa4f40eb0d7","0xf86d02843baa0c4082f6189415e6a5a2e131dd5467fa1ff3acd104f45ee5940b870aa87bee538000808360306ca084469ec8ee41e9104cbe3ad7e7fe4225de86076dd2783749b099a4d155900305a07e64e8848c692f0fc251e78e6f3c388eb303349f3e247481366517c2a5ae2d89","0xf86d03843baa0c4082f6189480c4c7125967139acaa931ee984a9db4100e0f3b870aa87bee538000808360306ba021d2d8a35b8da03d7e0b494f71c9ed1c28a195b94c298407b81d65163a79fbdaa024a9bfcf5bbe75ba35130fa784ab88cd21c12c4e7daf3464de91bc1ed07d1bf6","0xf86d04843baa0c4082f61894d08a63244fcd28b0aec5075052cdce31ba04fead870aa87bee538000808360306ca07ee42fee5e426595056ad406aa65a3c7adb1d3d77279f56ebe2410bcf5118b2ca07b8a0e1d21578e9043a7331f60bafc71d15788d1a2d70d00b3c46e0856ff56d2","0xf86d05843baa0c4082f618940b06ef8be65fcda88f2dbae5813480f997ee8e35870aa87bee538000808360306ba0620669c8d6a781d3131bca874152bf833622af0edcd2247eab1b086875d5242ba01632353388f46946b5ce037130e92128e5837fe35d6c7de2b9e56a0f8cc1f5e6", "0x02f8ef83301824048413f157f8842daf517a830186a094000000000000000000000000000000000000000080b8807a0a600060a0553db8600060c855c77fb29ecd7661d8aefe101a0db652a728af0fded622ff55d019b545d03a7532932a60ad52604260cd5360bf60ce53609460cf53603e60d05360f560d153bc596000609e55600060c6556000601f556000609155535660556057536055605853606e60595360e7605a5360d0605b5360eb60c080a03acb03b1fc20507bc66210f7e18ff5af65038fb22c626ae488ad9513d9b6debca05d38459e9d2a221eb345b0c2761b719b313d062ff1ea3d10cf5b8762c44385a6"],"withdrawals":[]}"#; - let payload: ExecutionPayloadV3 = serde_json::from_str(json).unwrap(); - assert!(payload.into_block(H256::zero()).is_ok()); + let payload: ExecutionPayload = serde_json::from_str(json).unwrap(); + assert!(payload.into_block(Some(H256::zero())).is_ok()); } } diff --git a/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs b/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs index c58e66dd2..e0d5f6ada 100644 --- a/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs +++ b/crates/vm/levm/src/opcode_handlers/stack_memory_storage_flow.rs @@ -178,7 +178,7 @@ impl VM { let mut gas_refunds = U256::zero(); if new_storage_slot_value != storage_slot.current_value { if storage_slot.current_value == storage_slot.original_value { - if storage_slot.original_value.is_zero() && new_storage_slot_value.is_zero() { + if !storage_slot.original_value.is_zero() && new_storage_slot_value.is_zero() { gas_refunds = gas_refunds .checked_add(U256::from(4800)) .ok_or(VMError::GasRefundsOverflow)?; diff --git a/crates/vm/levm/src/vm.rs b/crates/vm/levm/src/vm.rs index 7a2a58a43..63e7cc5ce 100644 --- a/crates/vm/levm/src/vm.rs +++ b/crates/vm/levm/src/vm.rs @@ -626,6 +626,12 @@ impl VM { .checked_mul(self.env.gas_price) .ok_or(VMError::GasLimitPriceProductOverflow)?, )?; + self.increase_account_balance( + sender, + U256::from(report.gas_refunded) + .checked_mul(self.env.gas_price) + .ok_or(VMError::GasLimitPriceProductOverflow)?, + )?; // Send coinbase fee let priority_fee_per_gas = self