Skip to content

Commit

Permalink
feat: add rpc endpoint engine_getPayloadV3 + fixes to block buildin…
Browse files Browse the repository at this point in the history
…g pipeline (lambdaclass#571)

Depends on lambdaclass#540 
**Motivation**
Being able to run the full basic block building pipeline
(forkChoiceUpdated -> getPayload -> newPayload)
CURRENT_STATUS: I could successfully run some tests that ran the above
sequence, we still need to add transactions sent by sendRawTransaction
endpoint to the payload in order to fully build blocks
<!-- Why does this pull request exist? What are its goals? -->

**Description**
* Add rpc endpoint `engine_getPayloadV3`
* Compute new state root (after applying beacon_root contract &
withdrawals) when building a new payload (`engine_forkChoiceUpdated`)
* Set new block as canonical in `engine_newPayloadV3` (TEMP FIX for not
being able to fetch non canonical blocks)

Missing work (for a later PR)
* Fetch transactions from the mempool and add them to the block
* Populate BlobsBundle 

<!-- A clear and concise general description of the changes this PR
introduces -->

<!-- Link to issues: Resolves lambdaclass#111, Resolves lambdaclass#222 -->

Closes None, but is part of lambdaclass#344
  • Loading branch information
fmoletta authored Sep 30, 2024
1 parent cdacba0 commit d7a872d
Show file tree
Hide file tree
Showing 10 changed files with 230 additions and 29 deletions.
50 changes: 42 additions & 8 deletions crates/blockchain/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<Block, StoreError> {
pub fn build_payload(args: &BuildPayloadArgs, storage: &Store) -> Result<Block, ChainError> {
// 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,
Expand Down Expand Up @@ -96,7 +101,19 @@ pub fn build_payload(args: &BuildPayloadArgs, storage: &Store) -> Result<Block,
ommers: Vec::new(),
withdrawals: Some(args.withdrawals.clone()),
},
})
};
// Apply withdrawals & call beacon root contract, and obtain the new state root
let spec_id = spec_id(storage, args.timestamp)?;
let mut evm_state = evm_state(storage.clone(), parent_block.number);
if args.beacon_root.is_some() && spec_id == SpecId::CANCUN {
beacon_root_contract_call(&mut evm_state, &payload.header, spec_id)?;
}
process_withdrawals(&mut evm_state, &args.withdrawals)?;
let account_updates = get_state_transitions(&mut evm_state);
payload.header.state_root = storage
.apply_account_updates(parent_block.number, &account_updates)?
.unwrap_or_default();
Ok(payload)
}

fn calc_gas_limit(parent_gas_limit: u64, desired_limit: u64) -> u64 {
Expand Down Expand Up @@ -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<U256> {
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)
}
13 changes: 13 additions & 0 deletions crates/core/serde_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,8 @@ pub mod bytes {
}

pub mod vec {
use serde::ser::SerializeSeq;

use super::*;

pub fn deserialize<'de, D>(d: D) -> Result<Vec<Bytes>, D::Error>
Expand All @@ -246,6 +248,17 @@ pub mod bytes {
}
Ok(output)
}

pub fn serialize<S>(value: &Vec<Bytes>, serializer: S) -> Result<S::Ok, S::Error>
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()
}
}
}

Expand Down
19 changes: 19 additions & 0 deletions crates/core/types/transaction.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::cmp::min;

use bytes::Bytes;
use ethereum_types::{Address, H256, U256};
use secp256k1::{ecdsa::RecoveryId, Message, SECP256K1};
Expand Down Expand Up @@ -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<u64>) -> Option<u64> {
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(
Expand Down
13 changes: 11 additions & 2 deletions crates/rpc/engine/fork_choice.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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)?;
}

Expand Down
48 changes: 46 additions & 2 deletions crates/rpc/engine/payload.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
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;
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,
Expand All @@ -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<Vec<Value>>) -> Result<Self, RpcErr> {
let params = params.as_ref().ok_or(RpcErr::BadParams)?;
Expand Down Expand Up @@ -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",
Expand All @@ -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<Vec<Value>>) -> Result<Self, RpcErr> {
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::<String>(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<Value, RpcErr> {
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)
}
}
7 changes: 5 additions & 2 deletions crates/rpc/rpc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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::{
Expand Down Expand Up @@ -213,6 +215,7 @@ pub fn map_engine_requests(req: &RpcRequest, storage: Store) -> Result<Value, Rp
"engine_exchangeTransitionConfigurationV1" => {
ExchangeTransitionConfigV1Req::call(req, storage)
}
"engine_getPayloadV3" => GetPayloadV3Request::call(req, storage),
_ => Err(RpcErr::MethodNotFound),
}
}
Expand Down
2 changes: 1 addition & 1 deletion crates/rpc/types/account_proof.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ pub struct StorageProof {
pub value: U256,
}

fn serialize_proofs<S>(value: &Vec<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
pub fn serialize_proofs<S>(value: &Vec<Vec<u8>>, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
Expand Down
79 changes: 77 additions & 2 deletions crates/rpc/types/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -55,6 +55,15 @@ impl<'de> Deserialize<'de> for EncodedTransaction {
}
}

impl Serialize for EncodedTransaction {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
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:
Expand All @@ -63,6 +72,10 @@ impl EncodedTransaction {
fn decode(&self) -> Result<Transaction, RLPDecodeError> {
Transaction::decode_canonical(self.0.as_ref())
}

fn encode(tx: &Transaction) -> Self {
Self(Bytes::from(tx.encode_canonical_to_vec()))
}
}

impl ExecutionPayloadV3 {
Expand Down Expand Up @@ -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)]
Expand Down Expand Up @@ -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<Bytes>,
#[serde(serialize_with = "super::account_proof::serialize_proofs")]
pub proofs: Vec<Vec<u8>>,
#[serde(with = "serde_utils::bytes::vec")]
pub blobs: Vec<Bytes>,
}

// 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::*;
Expand Down
Loading

0 comments on commit d7a872d

Please sign in to comment.