diff --git a/crates/blockchain/payload.rs b/crates/blockchain/payload.rs index a1a458cf3c..1019a7bd73 100644 --- a/crates/blockchain/payload.rs +++ b/crates/blockchain/payload.rs @@ -8,11 +8,18 @@ use ethereum_rust_core::{ }, Address, Bloom, Bytes, H256, U256, }; +use ethereum_rust_evm::{ + beacon_root_contract_call, evm_state, get_state_transitions, process_withdrawals, spec_id, + SpecId, +}; use ethereum_rust_rlp::encode::RLPEncode; -use ethereum_rust_storage::{error::StoreError, Store}; +use ethereum_rust_storage::Store; use sha3::{Digest, Keccak256}; -use crate::constants::{GAS_LIMIT_BOUND_DIVISOR, MIN_GAS_LIMIT, TARGET_BLOB_GAS_PER_BLOCK}; +use crate::{ + constants::{GAS_LIMIT_BOUND_DIVISOR, MIN_GAS_LIMIT, TARGET_BLOB_GAS_PER_BLOCK}, + error::ChainError, +}; pub struct BuildPayloadArgs { pub parent: BlockHash, @@ -44,17 +51,15 @@ impl BuildPayloadArgs { /// Builds a new payload based on the payload arguments // Basic payload block building, can and should be improved -pub fn build_payload(args: &BuildPayloadArgs, storage: &Store) -> Result { +pub fn build_payload(args: &BuildPayloadArgs, storage: &Store) -> Result { // TODO: check where we should get builder values from const DEFAULT_BUILDER_GAS_CEIL: u64 = 30_000_000; - // Presence of a parent block should have been checked or guaranteed before calling this function - // So we can treat a missing parent block as an internal storage error let parent_block = storage .get_block_header_by_hash(args.parent)? - .ok_or_else(|| StoreError::Custom("unexpected missing parent block".to_string()))?; + .ok_or_else(|| ChainError::ParentNotFound)?; let chain_config = storage.get_chain_config()?; let gas_limit = calc_gas_limit(parent_block.gas_limit, DEFAULT_BUILDER_GAS_CEIL); - Ok(Block { + let mut payload = Block { header: BlockHeader { parent_hash: args.parent, ommers_hash: *DEFAULT_OMMERS_HASH, @@ -96,7 +101,19 @@ pub fn build_payload(args: &BuildPayloadArgs, storage: &Store) -> Result u64 { @@ -127,3 +144,20 @@ fn calc_excess_blob_gas(parent_excess_blob_gas: u64, parent_blob_gas_used: u64) excess_blob_gas - TARGET_BLOB_GAS_PER_BLOCK } } + +/// Calculates the total fees paid by the payload block +/// Only potential errors are storage errors which should be treated as internal errors by rpc providers +pub fn payload_block_value(block: &Block, storage: &Store) -> Option { + let mut total_fee = U256::zero(); + let mut last_cummulative_gas_used = 0; + for (index, tx) in block.body.transactions.iter().enumerate() { + // Execution already succeded by this point so we can asume the fee is valid + let fee = tx.effective_gas_tip(block.header.base_fee_per_gas)?; + let receipt = storage + .get_receipt(block.header.number, index as u64) + .ok()??; + total_fee += U256::from(fee) * (receipt.cumulative_gas_used - last_cummulative_gas_used); + last_cummulative_gas_used = receipt.cumulative_gas_used; + } + Some(total_fee) +} diff --git a/crates/core/serde_utils.rs b/crates/core/serde_utils.rs index 21c0d6b684..2728df9747 100644 --- a/crates/core/serde_utils.rs +++ b/crates/core/serde_utils.rs @@ -230,6 +230,8 @@ pub mod bytes { } pub mod vec { + use serde::ser::SerializeSeq; + use super::*; pub fn deserialize<'de, D>(d: D) -> Result, D::Error> @@ -246,6 +248,17 @@ pub mod bytes { } Ok(output) } + + pub fn serialize(value: &Vec, serializer: S) -> Result + where + S: Serializer, + { + let mut seq_serializer = serializer.serialize_seq(Some(value.len()))?; + for encoded in value { + seq_serializer.serialize_element(&format!("0x{}", hex::encode(encoded)))?; + } + seq_serializer.end() + } } } diff --git a/crates/core/types/transaction.rs b/crates/core/types/transaction.rs index 4646e2a62c..348938682f 100644 --- a/crates/core/types/transaction.rs +++ b/crates/core/types/transaction.rs @@ -1,3 +1,5 @@ +use std::cmp::min; + use bytes::Bytes; use ethereum_types::{Address, H256, U256}; use secp256k1::{ecdsa::RecoveryId, Message, SECP256K1}; @@ -607,6 +609,23 @@ impl Transaction { pub fn compute_hash(&self) -> H256 { keccak_hash::keccak(self.encode_canonical_to_vec()) } + + fn gas_tip_cap(&self) -> u64 { + self.max_priority_fee().unwrap_or(self.gas_price()) + } + + fn gas_fee_cap(&self) -> u64 { + self.max_fee_per_gas().unwrap_or(self.gas_price()) + } + + pub fn effective_gas_tip(&self, base_fee: Option) -> Option { + let Some(base_fee) = base_fee else { + return Some(self.gas_tip_cap()); + }; + self.gas_fee_cap() + .checked_sub(base_fee) + .map(|tip| min(tip, self.gas_fee_cap())) + } } fn recover_address( diff --git a/crates/rpc/engine/fork_choice.rs b/crates/rpc/engine/fork_choice.rs index 244bc05fcd..3296cc60db 100644 --- a/crates/rpc/engine/fork_choice.rs +++ b/crates/rpc/engine/fork_choice.rs @@ -1,4 +1,7 @@ -use ethereum_rust_blockchain::payload::{build_payload, BuildPayloadArgs}; +use ethereum_rust_blockchain::{ + error::ChainError, + payload::{build_payload, BuildPayloadArgs}, +}; use ethereum_rust_core::{types::BlockHeader, H256, U256}; use ethereum_rust_storage::{error::StoreError, Store}; use serde_json::Value; @@ -104,7 +107,13 @@ impl RpcHandler for ForkChoiceUpdatedV3 { }; let payload_id = args.id(); response.set_id(payload_id); - let payload = build_payload(&args, &storage)?; + let payload = match build_payload(&args, &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 + _ => return Err(RpcErr::Internal), + }; storage.add_payload(payload_id, payload)?; } diff --git a/crates/rpc/engine/payload.rs b/crates/rpc/engine/payload.rs index 9feb6e6929..af01040e0b 100644 --- a/crates/rpc/engine/payload.rs +++ b/crates/rpc/engine/payload.rs @@ -1,4 +1,5 @@ use ethereum_rust_blockchain::error::ChainError; +use ethereum_rust_blockchain::payload::payload_block_value; use ethereum_rust_blockchain::{add_block, latest_valid_hash}; use ethereum_rust_core::types::Fork; use ethereum_rust_core::H256; @@ -6,6 +7,7 @@ use ethereum_rust_storage::Store; use serde_json::Value; use tracing::{info, warn}; +use crate::types::payload::ExecutionPayloadResponse; use crate::{ types::payload::{ExecutionPayloadV3, PayloadStatus}, RpcErr, RpcHandler, @@ -17,6 +19,10 @@ pub struct NewPayloadV3Request { pub parent_beacon_block_root: H256, } +pub struct GetPayloadV3Request { + pub payload_id: u64, +} + impl RpcHandler for NewPayloadV3Request { fn parse(params: &Option>) -> Result { let params = params.as_ref().ok_or(RpcErr::BadParams)?; @@ -96,7 +102,7 @@ impl RpcHandler for NewPayloadV3Request { // Execute and store the block info!("Executing payload with block hash: {block_hash}"); - let result = match add_block(&block, &storage) { + let payload_status = match add_block(&block, &storage) { Err(ChainError::NonCanonicalParent) => Ok(PayloadStatus::syncing()), Err(ChainError::ParentNotFound) => Ok(PayloadStatus::invalid_with_err( "Could not reference parent block with parent_hash", @@ -110,12 +116,50 @@ impl RpcHandler for NewPayloadV3Request { Err(ChainError::StoreError(_)) => Err(RpcErr::Internal), Ok(()) => { info!("Block with hash {block_hash} executed succesfully"); + // TODO: We don't have a way to fetch blocks by number if they are not canonical + // so we need to set it as canonical in order to run basic test suites + // We should remove this line once the issue is solved + storage.set_canonical_block(block.header.number, block_hash)?; info!("Block with hash {block_hash} added to storage"); Ok(PayloadStatus::valid_with_hash(block_hash)) } }?; - serde_json::to_value(result).map_err(|_| RpcErr::Internal) + serde_json::to_value(payload_status).map_err(|_| RpcErr::Internal) + } +} + +impl RpcHandler for GetPayloadV3Request { + fn parse(params: &Option>) -> Result { + let params = params.as_ref().ok_or(RpcErr::BadParams)?; + if params.len() != 1 { + return Err(RpcErr::BadParams); + }; + let Ok(hex_str) = serde_json::from_value::(params[0].clone()) else { + return Err(RpcErr::BadParams); + }; + // Check that the hex string is 0x prefixed + let Some(hex_str) = hex_str.strip_prefix("0x") else { + return Err(RpcErr::BadHexFormat(0)); + }; + // Parse hex string + let Ok(payload_id) = u64::from_str_radix(hex_str, 16) else { + return Err(RpcErr::BadHexFormat(0)); + }; + Ok(GetPayloadV3Request { payload_id }) + } + + fn handle(&self, storage: Store) -> Result { + info!("Requested payload with id: {:#018x}", self.payload_id); + let Some(payload) = storage.get_payload(self.payload_id)? else { + return Err(RpcErr::UnknownPayload); + }; + let block_value = payload_block_value(&payload, &storage).ok_or(RpcErr::Internal)?; + serde_json::to_value(ExecutionPayloadResponse::new( + ExecutionPayloadV3::from_block(payload), + block_value, + )) + .map_err(|_| RpcErr::Internal) } } diff --git a/crates/rpc/rpc.rs b/crates/rpc/rpc.rs index 3add92eb93..1fc1a4f30d 100644 --- a/crates/rpc/rpc.rs +++ b/crates/rpc/rpc.rs @@ -9,8 +9,10 @@ use axum_extra::{ TypedHeader, }; use engine::{ - exchange_transition_config::ExchangeTransitionConfigV1Req, fork_choice::ForkChoiceUpdatedV3, - payload::NewPayloadV3Request, ExchangeCapabilitiesRequest, + exchange_transition_config::ExchangeTransitionConfigV1Req, + fork_choice::ForkChoiceUpdatedV3, + payload::{GetPayloadV3Request, NewPayloadV3Request}, + ExchangeCapabilitiesRequest, }; use eth::{ account::{ @@ -213,6 +215,7 @@ pub fn map_engine_requests(req: &RpcRequest, storage: Store) -> Result { ExchangeTransitionConfigV1Req::call(req, storage) } + "engine_getPayloadV3" => GetPayloadV3Request::call(req, storage), _ => Err(RpcErr::MethodNotFound), } } diff --git a/crates/rpc/types/account_proof.rs b/crates/rpc/types/account_proof.rs index a404d11853..332026b30b 100644 --- a/crates/rpc/types/account_proof.rs +++ b/crates/rpc/types/account_proof.rs @@ -24,7 +24,7 @@ pub struct StorageProof { pub value: U256, } -fn serialize_proofs(value: &Vec>, serializer: S) -> Result +pub fn serialize_proofs(value: &Vec>, serializer: S) -> Result where S: Serializer, { diff --git a/crates/rpc/types/payload.rs b/crates/rpc/types/payload.rs index b005dac808..fd4fa9cbae 100644 --- a/crates/rpc/types/payload.rs +++ b/crates/rpc/types/payload.rs @@ -8,10 +8,10 @@ use ethereum_rust_core::{ compute_transactions_root, compute_withdrawals_root, Block, BlockBody, BlockHash, BlockHeader, Transaction, Withdrawal, DEFAULT_OMMERS_HASH, }, - Address, Bloom, H256, + Address, Bloom, H256, U256, }; -#[derive(Clone, Debug, Deserialize)] +#[derive(Clone, Debug, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExecutionPayloadV3 { parent_hash: H256, @@ -55,6 +55,15 @@ impl<'de> Deserialize<'de> for EncodedTransaction { } } +impl Serialize for EncodedTransaction { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serde_utils::bytes::serialize(&self.0, serializer) + } +} + impl EncodedTransaction { /// Based on [EIP-2718] /// Transactions can be encoded in the following formats: @@ -63,6 +72,10 @@ impl EncodedTransaction { fn decode(&self) -> Result { Transaction::decode_canonical(self.0.as_ref()) } + + fn encode(tx: &Transaction) -> Self { + Self(Bytes::from(tx.encode_canonical_to_vec())) + } } impl ExecutionPayloadV3 { @@ -106,6 +119,33 @@ impl ExecutionPayloadV3 { body, }) } + + pub fn from_block(block: Block) -> Self { + Self { + parent_hash: block.header.parent_hash, + fee_recipient: block.header.coinbase, + state_root: block.header.state_root, + receipts_root: block.header.receipts_root, + logs_bloom: block.header.logs_bloom, + prev_randao: block.header.prev_randao, + block_number: block.header.number, + gas_limit: block.header.gas_limit, + gas_used: block.header.gas_used, + timestamp: block.header.timestamp, + extra_data: block.header.extra_data.clone(), + base_fee_per_gas: block.header.base_fee_per_gas.unwrap_or_default(), + block_hash: block.header.compute_block_hash(), + transactions: block + .body + .transactions + .iter() + .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(), + } + } } #[derive(Debug, Deserialize, Serialize)] @@ -173,6 +213,41 @@ impl PayloadStatus { } } +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct ExecutionPayloadResponse { + pub execution_payload: ExecutionPayloadV3, // We only handle v3 payloads + // Total fees consumed by the block (fees paid) + pub block_value: U256, + pub blobs_bundle: BlobsBundle, +} + +#[derive(Clone, Debug, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct BlobsBundle { + #[serde(with = "serde_utils::bytes::vec")] + commitments: Vec, + #[serde(serialize_with = "super::account_proof::serialize_proofs")] + pub proofs: Vec>, + #[serde(with = "serde_utils::bytes::vec")] + pub blobs: Vec, +} + +// TODO: Fill BlobsBundle +impl ExecutionPayloadResponse { + pub fn new(payload: ExecutionPayloadV3, block_value: U256) -> Self { + Self { + execution_payload: payload, + block_value, + blobs_bundle: BlobsBundle { + commitments: vec![], + proofs: vec![], + blobs: vec![], + }, + } + } +} + #[cfg(test)] mod test { use super::*; diff --git a/crates/rpc/utils.rs b/crates/rpc/utils.rs index 2331a16ea6..91a0edb982 100644 --- a/crates/rpc/utils.rs +++ b/crates/rpc/utils.rs @@ -18,6 +18,7 @@ pub enum RpcErr { Halt { reason: String, gas_used: u64 }, AuthenticationError(AuthenticationError), InvalidForkChoiceState(String), + UnknownPayload, } impl From for RpcErrorMetadata { @@ -92,6 +93,11 @@ impl From for RpcErrorMetadata { data: Some(data), message: "Invalid forkchoice state".to_string(), }, + RpcErr::UnknownPayload => RpcErrorMetadata { + code: -38001, + data: None, + message: "Unknown payload".to_string(), + }, } } } diff --git a/crates/storage/storage.rs b/crates/storage/storage.rs index 12662b1cab..dbd3693036 100644 --- a/crates/storage/storage.rs +++ b/crates/storage/storage.rs @@ -110,23 +110,21 @@ impl Store { block_hash: BlockHash, block_header: BlockHeader, ) -> Result<(), StoreError> { - self.engine - .clone() - .add_block_header(block_hash, block_header) + self.engine.add_block_header(block_hash, block_header) } pub fn get_block_header( &self, block_number: BlockNumber, ) -> Result, StoreError> { - self.engine.clone().get_block_header(block_number) + self.engine.get_block_header(block_number) } pub fn get_block_header_by_hash( &self, block_hash: BlockHash, ) -> Result, StoreError> { - self.engine.clone().get_block_header_by_hash(block_hash) + self.engine.get_block_header_by_hash(block_hash) } pub fn add_block_body( @@ -134,14 +132,14 @@ impl Store { block_hash: BlockHash, block_body: BlockBody, ) -> Result<(), StoreError> { - self.engine.clone().add_block_body(block_hash, block_body) + self.engine.add_block_body(block_hash, block_body) } pub fn get_block_body( &self, block_number: BlockNumber, ) -> Result, StoreError> { - self.engine.clone().get_block_body(block_number) + self.engine.get_block_body(block_number) } pub fn add_block_number( @@ -158,7 +156,7 @@ impl Store { &self, block_hash: BlockHash, ) -> Result, StoreError> { - self.engine.clone().get_block_number(block_hash) + self.engine.get_block_number(block_hash) } pub fn add_block_total_difficulty( @@ -208,11 +206,11 @@ impl Store { } fn add_account_code(&self, code_hash: H256, code: Bytes) -> Result<(), StoreError> { - self.engine.clone().add_account_code(code_hash, code) + self.engine.add_account_code(code_hash, code) } pub fn get_account_code(&self, code_hash: H256) -> Result, StoreError> { - self.engine.clone().get_account_code(code_hash) + self.engine.get_account_code(code_hash) } pub fn get_code_by_account_address( @@ -336,7 +334,7 @@ impl Store { index: Index, receipt: Receipt, ) -> Result<(), StoreError> { - self.engine.clone().add_receipt(block_hash, index, receipt) + self.engine.add_receipt(block_hash, index, receipt) } pub fn get_receipt( @@ -344,7 +342,7 @@ impl Store { block_number: BlockNumber, index: Index, ) -> Result, StoreError> { - self.engine.clone().get_receipt(block_number, index) + self.engine.get_receipt(block_number, index) } pub fn add_block(&self, block: Block) -> Result<(), StoreError> {