Skip to content

Commit

Permalink
test: add engine tree test, FCU triggers reorg with all the blocks pr…
Browse files Browse the repository at this point in the history
…esent (#9943)
  • Loading branch information
fgimenez authored Aug 1, 2024
1 parent cd0ec57 commit 0a1be8c
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 8 deletions.
40 changes: 40 additions & 0 deletions crates/chain-state/src/test_utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -225,6 +225,46 @@ impl TestBlockBuilder {
block
})
}

/// Returns the execution outcome for a block created with this builder.
/// In order to properly include the bundle state, the signer balance is
/// updated.
pub fn get_execution_outcome(&mut self, block: SealedBlockWithSenders) -> ExecutionOutcome {
let receipts = block
.body
.iter()
.enumerate()
.map(|(idx, tx)| Receipt {
tx_type: tx.tx_type(),
success: true,
cumulative_gas_used: (idx as u64 + 1) * 21_000,
..Default::default()
})
.collect::<Vec<_>>();

let mut bundle_state_builder = BundleState::builder(block.number..=block.number);

for tx in &block.body {
self.signer_execute_account_info.balance -= Self::single_tx_cost();
bundle_state_builder = bundle_state_builder.state_present_account_info(
self.signer,
AccountInfo {
nonce: tx.nonce(),
balance: self.signer_execute_account_info.balance,
..Default::default()
},
);
}

let execution_outcome = ExecutionOutcome::new(
bundle_state_builder.build(),
vec![vec![None]].into(),
block.number,
Vec::new(),
);

execution_outcome.with_receipts(Receipts::from(receipts))
}
}
/// A test `ChainEventSubscriptions`
#[derive(Clone, Debug, Default)]
Expand Down
201 changes: 195 additions & 6 deletions crates/engine/tree/src/tree/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -147,6 +147,22 @@ impl TreeState {
}
}

/// Determines if the given block is part of a fork by walking back the
/// chain from the given hash and checking if the current canonical head
/// is part of it.
fn is_fork(&self, block_hash: B256) -> bool {
let target_hash = self.canonical_block_hash();
let mut current_hash = block_hash;

while let Some(current_block) = self.block_by_hash(current_hash) {
if current_block.hash() == target_hash {
return false
}
current_hash = current_block.header.parent_hash;
}
true
}

/// Remove all blocks up to the given block number.
pub(crate) fn remove_before(&mut self, upper_bound: Bound<BlockNumber>) {
let mut numbers_to_remove = Vec::new();
Expand Down Expand Up @@ -1163,6 +1179,8 @@ where
/// This is invoked on a valid forkchoice update, or if we can make the target block canonical.
fn on_canonical_chain_update(&mut self, chain_update: NewCanonicalChain) {
trace!(target: "engine", new_blocks = %chain_update.new_block_count(), reorged_blocks = %chain_update.reorged_block_count() ,"applying new chain update");
let start = Instant::now();

// update the tracked canonical head
self.state.tree_state.set_canonical_head(chain_update.tip().num_hash());

Expand All @@ -1171,10 +1189,16 @@ where

// update the tracked in-memory state with the new chain
self.canonical_in_memory_state.update_chain(chain_update);
self.canonical_in_memory_state.set_canonical_head(tip);
self.canonical_in_memory_state.set_canonical_head(tip.clone());

// sends an event to all active listeners about the new canonical chain
self.canonical_in_memory_state.notify_canon_state(notification);

// emit event
self.emit_event(BeaconConsensusEngineEvent::CanonicalChainCommitted(
Box::new(tip),
start.elapsed(),
));
}

/// This handles downloaded blocks that are shown to be disconnected from the canonical chain.
Expand Down Expand Up @@ -1287,6 +1311,7 @@ where
return Ok(InsertPayloadOk::AlreadySeen(BlockStatus::Valid(attachment)))
}

let start = Instant::now();
// validate block consensus rules
self.validate_block(&block)?;

Expand All @@ -1312,6 +1337,7 @@ where

let block_number = block.number;
let block_hash = block.hash();
let sealed_block = Arc::new(block.block.clone());
let block = block.unseal();
let output = executor.execute((&block, U256::MAX).into())?;
self.consensus.validate_block_post_execution(
Expand All @@ -1331,7 +1357,7 @@ where
}

let executed = ExecutedBlock {
block: Arc::new(block.block.seal(block_hash)),
block: sealed_block.clone(),
senders: Arc::new(block.senders),
execution_output: Arc::new(ExecutionOutcome::new(
output.state,
Expand All @@ -1351,7 +1377,16 @@ where

self.state.tree_state.insert_executed(executed);

// emit insert event
let engine_event = if self.state.tree_state.is_fork(block_hash) {
BeaconConsensusEngineEvent::ForkBlockAdded(sealed_block)
} else {
BeaconConsensusEngineEvent::CanonicalBlockAdded(sealed_block, start.elapsed())
};
self.emit_event(EngineApiEvent::BeaconConsensus(engine_event));

let attachment = BlockAttachment::Canonical; // TODO: remove or revise attachment

Ok(InsertPayloadOk::Inserted(BlockStatus::Valid(attachment)))
}

Expand Down Expand Up @@ -1826,13 +1861,13 @@ mod tests {
use super::*;
use crate::persistence::PersistenceAction;
use alloy_rlp::Decodable;
use reth_beacon_consensus::EthBeaconConsensus;
use reth_beacon_consensus::{EthBeaconConsensus, ForkchoiceStatus};
use reth_chain_state::{test_utils::TestBlockBuilder, BlockState};
use reth_chainspec::{ChainSpec, HOLESKY, MAINNET};
use reth_ethereum_engine_primitives::EthEngineTypes;
use reth_evm::test_utils::MockExecutorProvider;
use reth_payload_builder::PayloadServiceCommand;
use reth_primitives::Bytes;
use reth_primitives::{Address, Bytes};
use reth_provider::test_utils::MockEthProvider;
use reth_rpc_types_compat::engine::block_to_payload_v1;
use std::{
Expand All @@ -1849,6 +1884,8 @@ mod tests {
action_rx: Receiver<PersistenceAction>,
payload_command_rx: UnboundedReceiver<PayloadServiceCommand<EthEngineTypes>>,
chain_spec: Arc<ChainSpec>,
executor_provider: MockExecutorProvider,
block_builder: TestBlockBuilder,
}

impl TestHarness {
Expand All @@ -1859,7 +1896,7 @@ mod tests {
let consensus = Arc::new(EthBeaconConsensus::new(chain_spec.clone()));

let provider = MockEthProvider::default();
let executor_factory = MockExecutorProvider::default();
let executor_provider = MockExecutorProvider::default();

let payload_validator = ExecutionPayloadValidator::new(chain_spec.clone());

Expand All @@ -1875,7 +1912,7 @@ mod tests {

let tree = EngineApiTreeHandlerImpl::new(
provider,
executor_factory,
executor_provider.clone(),
consensus,
payload_validator,
to_tree_rx,
Expand All @@ -1888,6 +1925,9 @@ mod tests {
TreeConfig::default(),
);

let block_builder = TestBlockBuilder::default()
.with_chain_spec((*chain_spec).clone())
.with_signer(Address::random());
Self {
tree,
to_tree_tx,
Expand All @@ -1896,6 +1936,8 @@ mod tests {
action_rx,
payload_command_rx,
chain_spec,
executor_provider,
block_builder,
}
}

Expand Down Expand Up @@ -1939,6 +1981,23 @@ mod tests {
self.tree.backfill_sync_state = state;
self
}

fn extend_execution_outcome(
&self,
execution_outcomes: impl IntoIterator<Item = impl Into<ExecutionOutcome>>,
) {
self.executor_provider.extend(execution_outcomes);
}

fn insert_block(
&mut self,
block: SealedBlockWithSenders,
) -> Result<InsertPayloadOk, InsertBlockErrorTwo> {
let execution_outcome = self.block_builder.get_execution_outcome(block.clone());
self.extend_execution_outcome([execution_outcome]);
self.tree.provider.add_state_root(block.state_root);
self.tree.insert_block(block)
}
}

#[tokio::test]
Expand Down Expand Up @@ -2342,4 +2401,134 @@ mod tests {
_ => panic!("Unexpected event: {:#?}", event),
}
}

#[tokio::test]
async fn test_engine_tree_fcu_canon_chain_insertion() {
let chain_spec = MAINNET.clone();
let mut test_harness = TestHarness::new(chain_spec.clone());

let base_chain: Vec<_> = test_harness.block_builder.get_executed_blocks(0..1).collect();
test_harness = test_harness.with_blocks(base_chain.clone());

// create FCU for the tip of the base chain
let fcu_state = ForkchoiceState {
head_block_hash: base_chain.last().unwrap().block().hash(),
safe_block_hash: B256::ZERO,
finalized_block_hash: B256::ZERO,
};

let (tx, rx) = oneshot::channel();
test_harness.tree.on_engine_message(FromEngine::Request(
BeaconEngineMessage::ForkchoiceUpdated { state: fcu_state, payload_attrs: None, tx },
));

let response = rx.await.unwrap().unwrap().await.unwrap();
assert!(response.payload_status.is_valid());

// check for ForkchoiceUpdated event
let event = test_harness.from_tree_rx.recv().await.unwrap();
match event {
EngineApiEvent::BeaconConsensus(BeaconConsensusEngineEvent::ForkchoiceUpdated(
state,
status,
)) => {
assert_eq!(state, fcu_state);
assert_eq!(status, ForkchoiceStatus::Valid);
}
_ => panic!("Unexpected event: {:#?}", event),
}

// extend main chain
let main_chain = test_harness.block_builder.create_fork(base_chain[0].block(), 3);

for (index, block) in main_chain.iter().enumerate() {
test_harness.insert_block(block.clone()).unwrap();
}

// check for CanonicalBlockAdded events, we expect main_chain.len() blocks added
for index in 0..main_chain.len() {
let event = test_harness.from_tree_rx.recv().await.unwrap();
match event {
EngineApiEvent::BeaconConsensus(
BeaconConsensusEngineEvent::CanonicalBlockAdded(block, _),
) => {
assert!(main_chain.iter().any(|b| b.hash() == block.hash()));
}
_ => panic!("Unexpected event: {:#?}", event),
}
}
}

#[tokio::test]
async fn test_engine_tree_fcu_reorg_with_all_blocks() {
let chain_spec = MAINNET.clone();
let mut test_harness = TestHarness::new(chain_spec.clone());

let main_chain: Vec<_> = test_harness.block_builder.get_executed_blocks(0..5).collect();
test_harness = test_harness.with_blocks(main_chain.clone());

let fork_chain = test_harness.block_builder.create_fork(main_chain[2].block(), 3);
// add fork blocks to the tree
for (index, block) in fork_chain.iter().enumerate() {
test_harness.insert_block(block.clone()).unwrap();
}

// create FCU for the tip of the fork
let fcu_state = ForkchoiceState {
head_block_hash: fork_chain.last().unwrap().hash(),
safe_block_hash: B256::ZERO,
finalized_block_hash: B256::ZERO,
};

let (tx, rx) = oneshot::channel();
test_harness.tree.on_engine_message(FromEngine::Request(
BeaconEngineMessage::ForkchoiceUpdated { state: fcu_state, payload_attrs: None, tx },
));

let response = rx.await.unwrap().unwrap().await.unwrap();
assert!(response.payload_status.is_valid());

// check for ForkBlockAdded events, we expect fork_chain.len() blocks added
for index in 0..fork_chain.len() {
let event = test_harness.from_tree_rx.recv().await.unwrap();
match event {
EngineApiEvent::BeaconConsensus(BeaconConsensusEngineEvent::ForkBlockAdded(
block,
)) => {
assert!(fork_chain.iter().any(|b| b.hash() == block.hash()));
}
_ => panic!("Unexpected event: {:#?}", event),
}
}

// check for CanonicalChainCommitted event
let event = test_harness.from_tree_rx.recv().await.unwrap();
match event {
EngineApiEvent::BeaconConsensus(
BeaconConsensusEngineEvent::CanonicalChainCommitted(header, _),
) => {
assert_eq!(header.hash(), fork_chain.last().unwrap().hash());
}
_ => panic!("Unexpected event: {:#?}", event),
}

// check for ForkchoiceUpdated event
let event = test_harness.from_tree_rx.recv().await.unwrap();
match event {
EngineApiEvent::BeaconConsensus(BeaconConsensusEngineEvent::ForkchoiceUpdated(
state,
status,
)) => {
assert_eq!(state, fcu_state);
assert_eq!(status, ForkchoiceStatus::Valid);
}
_ => panic!("Unexpected event: {:#?}", event),
}

// new head is the tip of the fork chain
assert_eq!(
test_harness.tree.state.tree_state.canonical_head().hash,
fork_chain.last().unwrap().hash()
);
}
}
Loading

0 comments on commit 0a1be8c

Please sign in to comment.