Skip to content

feat: engine_newPayloadV3: validate, execute & store block #222

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 29 commits into from
Aug 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
29 commits
Select commit Hold shift + click to select a range
2f4b506
wip
fmoletta Aug 2, 2024
d08ee6b
Insert cancun time from genesis + update newpayloadv3 parsing
fmoletta Aug 2, 2024
14b8bae
fmt
fmoletta Aug 2, 2024
f395676
Merge branch 'main' of https://github.com/lambdaclass/ethrex into sto…
fmoletta Aug 2, 2024
507e969
Update
fmoletta Aug 2, 2024
a618b61
Merge branch 'main' of https://github.com/lambdaclass/ethrex into sto…
fmoletta Aug 5, 2024
688fb6e
replace update_x with set_chain_config method for StoreEngine
fmoletta Aug 5, 2024
bbb4e52
Merge branch 'main' of https://github.com/lambdaclass/ethrex into sto…
fmoletta Aug 5, 2024
e81c9cd
Add payload status methods + validate block header
fmoletta Aug 5, 2024
938ae60
Fix
fmoletta Aug 5, 2024
7c43792
Execute & store block
fmoletta Aug 5, 2024
00ab2e4
Store block number
fmoletta Aug 5, 2024
72c4d29
Remove unwrap
Aug 6, 2024
5ce36ce
Fix root computation
Aug 6, 2024
2cea78c
Use base fee for block 0 on validation
Aug 7, 2024
934a3fd
Compute roots for genesis block
Aug 7, 2024
b085177
Set initial base fee for genesis block
Aug 7, 2024
904f86a
Update default values for genesis block
Aug 7, 2024
38b2ef7
Revert change to block validation
Aug 7, 2024
96f0da7
Fix gas limit for beacon root contract call
Aug 7, 2024
abf5a8f
Add more tracing
Aug 7, 2024
ee655a0
clippy
Aug 7, 2024
4b03b11
Update test
Aug 7, 2024
45afb13
Merge branch 'main' of github.com:lambdaclass/ethereum_rust into vali…
fmoletta Aug 7, 2024
e39caf5
Add test
fmoletta Aug 7, 2024
762e9aa
Use 30_000_000 as gas_limit for both tx and block env when executing …
fmoletta Aug 8, 2024
54bcb27
Merge branch 'main' into validate-payload
fmoletta Aug 8, 2024
c0fa93d
Merge branch 'main' into validate-payload
fmoletta Aug 8, 2024
14d19a9
Fix typo
fmoletta Aug 8, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 11 additions & 11 deletions crates/core/types/block.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,18 +203,18 @@ impl BlockBody {
withdrawals: Some(Vec::new()),
}
}
}

pub fn compute_transactions_root(&self) -> H256 {
let mut trie = PatriciaMerkleTree::<Vec<u8>, Vec<u8>, Keccak256>::new();
for (idx, tx) in self.transactions.iter().enumerate() {
// Key: RLP(tx_index)
// Value: tx_type || RLP(tx) if tx_type != 0
// RLP(tx) else
trie.insert(idx.encode_to_vec(), tx.encode_to_vec());
}
let &root = trie.compute_hash();
H256(root.into())
pub fn compute_transactions_root(transactions: &[Transaction]) -> H256 {
let mut trie = PatriciaMerkleTree::<Vec<u8>, Vec<u8>, Keccak256>::new();
for (idx, tx) in transactions.iter().enumerate() {
// Key: RLP(tx_index)
// Value: tx_type || RLP(tx) if tx_type != 0
// RLP(tx) else
trie.insert(idx.encode_to_vec(), tx.encode_to_vec());
}
let &root = trie.compute_hash();
H256(root.into())
}

pub fn compute_receipts_root(receipts: &[Receipt]) -> H256 {
Expand Down Expand Up @@ -442,7 +442,7 @@ mod serializable {
BlockBodyWrapper::OnlyHashes(OnlyHashesBlockBody {
transactions: body.transactions.iter().map(|t| t.compute_hash()).collect(),
uncles: body.ommers,
withdrawals: body.withdrawals.unwrap(),
withdrawals: body.withdrawals.unwrap_or_default(),
})
};
let hash = header.compute_block_hash();
Expand Down
1 change: 1 addition & 0 deletions crates/core/types/constants.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,3 +4,4 @@ pub const BASE_FEE_MAX_CHANGE_DENOMINATOR: u64 = 8;
pub const GAS_LIMIT_ADJUSTMENT_FACTOR: u64 = 1024;
pub const GAS_LIMIT_MINIMUM: u64 = 5000;
pub const GWEI_TO_WEI: u64 = 1_000_000_000;
pub const INITIAL_BASE_FEE: u64 = 1_000_000_000; //Initial base fee as defined in [EIP-1559](https://eips.ethereum.org/EIPS/eip-1559)
62 changes: 50 additions & 12 deletions crates/core/types/engine/payload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ use crate::rlp::decode::RLPDecode;
use crate::{rlp::error::RLPDecodeError, serde_utils};

use crate::types::{
compute_withdrawals_root, BlockBody, BlockHeader, Transaction, Withdrawal, DEFAULT_OMMERS_HASH,
compute_transactions_root, compute_withdrawals_root, Block, BlockBody, BlockHash, BlockHeader,
Transaction, Withdrawal, DEFAULT_OMMERS_HASH,
};

#[allow(unused)]
Expand Down Expand Up @@ -71,11 +72,8 @@ 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<(BlockHeader, BlockBody), RLPDecodeError> {
let block_body = BlockBody {
pub fn into_block(self, parent_beacon_block_root: H256) -> Result<Block, RLPDecodeError> {
let body = BlockBody {
transactions: self
.transactions
.iter()
Expand All @@ -84,13 +82,13 @@ impl ExecutionPayloadV3 {
ommers: vec![],
withdrawals: Some(self.withdrawals),
};
Ok((
BlockHeader {
Ok(Block {
header: BlockHeader {
parent_hash: self.parent_hash,
ommers_hash: *DEFAULT_OMMERS_HASH,
coinbase: self.fee_recipient,
state_root: self.state_root,
transactions_root: block_body.compute_transactions_root(),
transactions_root: compute_transactions_root(&body.transactions),
receipt_root: self.receipts_root,
logs_bloom: self.logs_bloom,
difficulty: 0.into(),
Expand All @@ -103,14 +101,14 @@ impl ExecutionPayloadV3 {
nonce: 0,
base_fee_per_gas: self.base_fee_per_gas,
withdrawals_root: Some(compute_withdrawals_root(
&block_body.withdrawals.clone().unwrap(),
&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),
},
block_body,
))
body,
})
}
}

Expand All @@ -132,6 +130,46 @@ pub enum PayloadValidationStatus {
Accepted,
}

impl PayloadStatus {
// Convenience methods to create payload status

/// Creates a PayloadStatus with invalid status and error message
pub fn invalid_with_err(error: &str) -> Self {
PayloadStatus {
status: PayloadValidationStatus::Invalid,
latest_valid_hash: None,
validation_error: Some(error.to_string()),
}
}

/// Creates a PayloadStatus with invalid status and latest valid hash
pub fn invalid_with_hash(hash: BlockHash) -> Self {
PayloadStatus {
status: PayloadValidationStatus::Invalid,
latest_valid_hash: Some(hash),
validation_error: None,
}
}

/// Creates a PayloadStatus with syncing status and no other info
pub fn syncing() -> Self {
PayloadStatus {
status: PayloadValidationStatus::Syncing,
latest_valid_hash: None,
validation_error: None,
}
}

/// Creates a PayloadStatus with valid status and latest valid hash
pub fn valid_with_hash(hash: BlockHash) -> Self {
PayloadStatus {
status: PayloadValidationStatus::Valid,
latest_valid_hash: Some(hash),
validation_error: None,
}
}
}

#[cfg(test)]
mod test {
use super::*;
Expand Down
51 changes: 34 additions & 17 deletions crates/core/types/genesis.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ use std::collections::HashMap;
use crate::rlp::encode::RLPEncode as _;

use super::{
code_hash, AccountInfo, AccountState, Block, BlockBody, BlockHeader, DEFAULT_OMMERS_HASH,
code_hash, compute_receipts_root, compute_transactions_root, compute_withdrawals_root,
AccountInfo, AccountState, Block, BlockBody, BlockHeader, DEFAULT_OMMERS_HASH,
INITIAL_BASE_FEE,
};

#[allow(unused)]
Expand Down Expand Up @@ -109,8 +111,8 @@ impl Genesis {
ommers_hash: *DEFAULT_OMMERS_HASH,
coinbase: self.coinbase,
state_root: self.compute_state_root(),
transactions_root: H256::zero(),
receipt_root: H256::zero(),
transactions_root: compute_transactions_root(&[]),
receipt_root: compute_receipts_root(&[]),
logs_bloom: Bloom::zero(),
difficulty: self.difficulty,
number: 0,
Expand All @@ -120,19 +122,19 @@ impl Genesis {
extra_data: Bytes::new(),
prev_randao: self.mixhash,
nonce: self.nonce,
base_fee_per_gas: 0,
withdrawals_root: None,
blob_gas_used: None,
excess_blob_gas: None,
parent_beacon_block_root: None,
base_fee_per_gas: INITIAL_BASE_FEE,
withdrawals_root: Some(compute_withdrawals_root(&[])),
blob_gas_used: Some(0),
excess_blob_gas: Some(0),
parent_beacon_block_root: Some(H256::zero()),
}
}

fn get_block_body(&self) -> BlockBody {
BlockBody {
transactions: vec![],
ommers: vec![],
withdrawals: None,
withdrawals: Some(vec![]),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Im surprised these changes don't break any of the spec test. Does the rlp and the state root remain equal with these values (empty vec vs None, 0 vs None)?

Copy link
Contributor Author

@fmoletta fmoletta Aug 8, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The state root shouldn't be affected (as it is built from the accounts in the alloc). The rlp does change, and we do need it to change as with the current version block validation failed when running with kurtosis as the genesis block's hash didn't match the parent_block_hash of the following block

}
}

Expand Down Expand Up @@ -272,8 +274,8 @@ mod tests {
H256::from_str("0x2dab6a1d6d638955507777aecea699e6728825524facbd446bd4e86d44fa5ecd")
.unwrap()
);
assert_eq!(header.transactions_root, H256::from([0; 32]));
assert_eq!(header.receipt_root, H256::from([0; 32]));
assert_eq!(header.transactions_root, compute_transactions_root(&[]));
assert_eq!(header.receipt_root, compute_receipts_root(&[]));
assert_eq!(header.logs_bloom, Bloom::default());
assert_eq!(header.difficulty, U256::from(1));
assert_eq!(header.gas_limit, 25_000_000);
Expand All @@ -282,13 +284,28 @@ mod tests {
assert_eq!(header.extra_data, Bytes::default());
assert_eq!(header.prev_randao, H256::from([0; 32]));
assert_eq!(header.nonce, 4660);
assert_eq!(header.base_fee_per_gas, 0);
assert_eq!(header.withdrawals_root, None);
assert_eq!(header.blob_gas_used, None);
assert_eq!(header.excess_blob_gas, None);
assert_eq!(header.parent_beacon_block_root, None);
assert_eq!(header.base_fee_per_gas, INITIAL_BASE_FEE);
assert_eq!(header.withdrawals_root, Some(compute_withdrawals_root(&[])));
assert_eq!(header.blob_gas_used, Some(0));
assert_eq!(header.excess_blob_gas, Some(0));
assert_eq!(header.parent_beacon_block_root, Some(H256::zero()));
assert!(body.transactions.is_empty());
assert!(body.ommers.is_empty());
assert_eq!(body.withdrawals, None);
assert!(body.withdrawals.is_some_and(|w| w.is_empty()));
}

#[test]
// Parses genesis received by kurtosis and checks that the hash matches the next block's parent hash
fn read_and_compute_hash() {
let file = File::open("../../test_data/genesis.json").expect("Failed to open genesis file");
let reader = BufReader::new(file);
let genesis: Genesis =
serde_json::from_reader(reader).expect("Failed to deserialize genesis file");
let genesis_block_hash = genesis.get_block().header.compute_block_hash();
assert_eq!(
genesis_block_hash,
H256::from_str("0xcb5306dd861d0f2c1f9952fbfbc75a46d0b6ce4f37bea370c3471fe8410bf40b")
.unwrap()
)
}
}
4 changes: 2 additions & 2 deletions crates/core/types/transaction.rs
Original file line number Diff line number Diff line change
Expand Up @@ -914,7 +914,7 @@ mod serde_impl {

#[cfg(test)]
mod tests {
use crate::types::{compute_receipts_root, BlockBody, Receipt};
use crate::types::{compute_receipts_root, compute_transactions_root, BlockBody, Receipt};

use super::*;
use hex_literal::hex;
Expand All @@ -941,7 +941,7 @@ mod tests {
body.transactions.push(Transaction::LegacyTransaction(tx));
let expected_root =
hex!("8151d548273f6683169524b66ca9fe338b9ce42bc3540046c828fd939ae23bcb");
let result = body.compute_transactions_root();
let result = compute_transactions_root(&body.transactions);

assert_eq!(result, expected_root.into());
}
Expand Down
1 change: 1 addition & 0 deletions crates/evm/evm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,7 @@ pub fn beacon_root_contract_call(
};
let mut block_env = block_env(header);
block_env.basefee = RevmU256::ZERO;
block_env.gas_limit = RevmU256::from(30_000_000);

let mut evm = Evm::builder()
.with_db(&mut state.0)
Expand Down
75 changes: 42 additions & 33 deletions crates/rpc/engine/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
use ethereum_rust_core::{
types::{ExecutionPayloadV3, PayloadStatus, PayloadValidationStatus},
types::{validate_block_header, ExecutionPayloadV3, PayloadStatus},
H256,
};
use ethereum_rust_evm::{evm_state, execute_block, SpecId};
use ethereum_rust_storage::Store;
use serde_json::{json, Value};
use tracing::info;
Expand Down Expand Up @@ -50,57 +51,65 @@ pub fn new_payload_v3(
storage: Store,
) -> Result<PayloadStatus, RpcErr> {
let block_hash = request.payload.block_hash;
info!("Received new payload with block hash: {block_hash}");

info!("Received new payload with block hash: {}", block_hash);

let (block_header, block_body) =
match request.payload.into_block(request.parent_beacon_block_root) {
Ok(block) => block,
Err(error) => {
return Ok(PayloadStatus {
status: PayloadValidationStatus::Invalid,
latest_valid_hash: Some(H256::zero()),
validation_error: Some(error.to_string()),
})
}
};
let block = match request.payload.into_block(request.parent_beacon_block_root) {
Ok(block) => block,
Err(error) => return Ok(PayloadStatus::invalid_with_err(&error.to_string())),
};

// Payload Validation

// Check timestamp does not fall within the time frame of the Cancun fork
match storage.get_cancun_time().map_err(|_| RpcErr::Internal)? {
Some(cancun_time) if block_header.timestamp > cancun_time => {}
Some(cancun_time) if block.header.timestamp > cancun_time => {}
_ => return Err(RpcErr::UnsuportedFork),
}

// Check that block_hash is valid
let actual_block_hash = block_header.compute_block_hash();
let actual_block_hash = block.header.compute_block_hash();
if block_hash != actual_block_hash {
return Ok(PayloadStatus {
status: PayloadValidationStatus::Invalid,
latest_valid_hash: None,
validation_error: Some("Invalid block hash".to_string()),
});
return Ok(PayloadStatus::invalid_with_err("Invalid block hash"));
}
info!("Block hash {} is valid", block_hash);
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<H256> = block_body
let blob_versioned_hashes: Vec<H256> = block
.body
.transactions
.iter()
.flat_map(|tx| tx.blob_versioned_hashes())
.collect();
if request.expected_blob_versioned_hashes != blob_versioned_hashes {
return Ok(PayloadStatus {
status: PayloadValidationStatus::Invalid,
latest_valid_hash: None,
validation_error: Some("Invalid blob_versioned_hashes".to_string()),
});
return Ok(PayloadStatus::invalid_with_err(
"Invalid blob_versioned_hashes",
));
}

Ok(PayloadStatus {
status: PayloadValidationStatus::Valid,
latest_valid_hash: Some(block_hash),
validation_error: None,
})
// Fetch parent block header and validate current header
if let Some(parent_header) = storage
.get_block_header(block.header.number.saturating_sub(1))
.map_err(|_| RpcErr::Internal)?
{
if !validate_block_header(&block.header, &parent_header) {
return Ok(PayloadStatus::invalid_with_hash(
parent_header.compute_block_hash(),
));
}
} else {
return Ok(PayloadStatus::syncing());
}

// Execute and store the block
info!("Executing payload with block hash: {block_hash}");
execute_block(&block, &mut evm_state(storage.clone()), SpecId::CANCUN)
.map_err(|_| RpcErr::Vm)?;
info!("Block with hash {block_hash} executed succesfully");
storage
.add_block_number(block_hash, block.header.number)
.map_err(|_| RpcErr::Internal)?;
storage.add_block(block).map_err(|_| RpcErr::Internal)?;
info!("Block with hash {block_hash} added to storage");

Ok(PayloadStatus::valid_with_hash(block_hash))
}
Loading