Files
nanoreth/crates/consensus/auto-seal/src/task.rs
2024-04-19 11:35:20 +00:00

255 lines
11 KiB
Rust

use crate::{mode::MiningMode, Storage};
use futures_util::{future::BoxFuture, FutureExt};
use reth_beacon_consensus::{BeaconEngineMessage, ForkchoiceStatus};
use reth_engine_primitives::EngineTypes;
use reth_evm::ConfigureEvm;
use reth_interfaces::consensus::ForkchoiceState;
use reth_primitives::{Block, ChainSpec, IntoRecoveredTransaction, SealedBlockWithSenders};
use reth_provider::{CanonChainTracker, CanonStateNotificationSender, Chain, StateProviderFactory};
use reth_stages_api::PipelineEvent;
use reth_transaction_pool::{TransactionPool, ValidPoolTransaction};
use std::{
collections::VecDeque,
future::Future,
pin::Pin,
sync::Arc,
task::{Context, Poll},
};
use tokio::sync::{mpsc::UnboundedSender, oneshot};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tracing::{debug, error, warn};
/// A Future that listens for new ready transactions and puts new blocks into storage
pub struct MiningTask<Client, Pool: TransactionPool, EvmConfig, Engine: EngineTypes> {
/// The configured chain spec
chain_spec: Arc<ChainSpec>,
/// The client used to interact with the state
client: Client,
/// The active miner
miner: MiningMode,
/// Single active future that inserts a new block into `storage`
insert_task: Option<BoxFuture<'static, Option<UnboundedReceiverStream<PipelineEvent>>>>,
/// Shared storage to insert new blocks
storage: Storage,
/// Pool where transactions are stored
pool: Pool,
/// backlog of sets of transactions ready to be mined
queued: VecDeque<Vec<Arc<ValidPoolTransaction<<Pool as TransactionPool>::Transaction>>>>,
// TODO: ideally this would just be a sender of hashes
to_engine: UnboundedSender<BeaconEngineMessage<Engine>>,
/// Used to notify consumers of new blocks
canon_state_notification: CanonStateNotificationSender,
/// The pipeline events to listen on
pipe_line_events: Option<UnboundedReceiverStream<PipelineEvent>>,
/// The type that defines how to configure the EVM.
evm_config: EvmConfig,
}
// === impl MiningTask ===
impl<EvmConfig, Client, Pool: TransactionPool, Engine: EngineTypes>
MiningTask<Client, Pool, EvmConfig, Engine>
{
/// Creates a new instance of the task
#[allow(clippy::too_many_arguments)]
pub(crate) fn new(
chain_spec: Arc<ChainSpec>,
miner: MiningMode,
to_engine: UnboundedSender<BeaconEngineMessage<Engine>>,
canon_state_notification: CanonStateNotificationSender,
storage: Storage,
client: Client,
pool: Pool,
evm_config: EvmConfig,
) -> Self {
Self {
chain_spec,
client,
miner,
insert_task: None,
storage,
pool,
to_engine,
canon_state_notification,
queued: Default::default(),
pipe_line_events: None,
evm_config,
}
}
/// Sets the pipeline events to listen on.
pub fn set_pipeline_events(&mut self, events: UnboundedReceiverStream<PipelineEvent>) {
self.pipe_line_events = Some(events);
}
}
impl<EvmConfig, Client, Pool, Engine> Future for MiningTask<Client, Pool, EvmConfig, Engine>
where
Client: StateProviderFactory + CanonChainTracker + Clone + Unpin + 'static,
Pool: TransactionPool + Unpin + 'static,
<Pool as TransactionPool>::Transaction: IntoRecoveredTransaction,
Engine: EngineTypes + 'static,
EvmConfig: ConfigureEvm + Clone + Unpin + Send + Sync + 'static,
{
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
// this drives block production and
loop {
if let Poll::Ready(transactions) = this.miner.poll(&this.pool, cx) {
// miner returned a set of transaction that we feed to the producer
this.queued.push_back(transactions);
}
if this.insert_task.is_none() {
if this.queued.is_empty() {
// nothing to insert
break
}
// ready to queue in new insert task
let storage = this.storage.clone();
let transactions = this.queued.pop_front().expect("not empty");
let to_engine = this.to_engine.clone();
let client = this.client.clone();
let chain_spec = Arc::clone(&this.chain_spec);
let pool = this.pool.clone();
let events = this.pipe_line_events.take();
let canon_state_notification = this.canon_state_notification.clone();
let evm_config = this.evm_config.clone();
// Create the mining future that creates a block, notifies the engine that drives
// the pipeline
this.insert_task = Some(Box::pin(async move {
let mut storage = storage.write().await;
let (transactions, senders): (Vec<_>, Vec<_>) = transactions
.into_iter()
.map(|tx| {
let recovered = tx.to_recovered_transaction();
let signer = recovered.signer();
(recovered.into_signed(), signer)
})
.unzip();
match storage.build_and_execute(
transactions.clone(),
&client,
chain_spec,
evm_config,
) {
Ok((new_header, bundle_state)) => {
// clear all transactions from pool
pool.remove_transactions(
transactions.iter().map(|tx| tx.hash()).collect(),
);
let state = ForkchoiceState {
head_block_hash: new_header.hash(),
finalized_block_hash: new_header.hash(),
safe_block_hash: new_header.hash(),
};
drop(storage);
// TODO: make this a future
// await the fcu call rx for SYNCING, then wait for a VALID response
loop {
// send the new update to the engine, this will trigger the engine
// to download and execute the block we just inserted
let (tx, rx) = oneshot::channel();
let _ = to_engine.send(BeaconEngineMessage::ForkchoiceUpdated {
state,
payload_attrs: None,
tx,
});
debug!(target: "consensus::auto", ?state, "Sent fork choice update");
match rx.await.unwrap() {
Ok(fcu_response) => {
match fcu_response.forkchoice_status() {
ForkchoiceStatus::Valid => break,
ForkchoiceStatus::Invalid => {
error!(target: "consensus::auto", ?fcu_response, "Forkchoice update returned invalid response");
return None
}
ForkchoiceStatus::Syncing => {
debug!(target: "consensus::auto", ?fcu_response, "Forkchoice update returned SYNCING, waiting for VALID");
// wait for the next fork choice update
continue
}
}
}
Err(err) => {
error!(target: "consensus::auto", %err, "Autoseal fork choice update failed");
return None
}
}
}
// seal the block
let block = Block {
header: new_header.clone().unseal(),
body: transactions,
ommers: vec![],
withdrawals: None,
};
let sealed_block = block.seal_slow();
let sealed_block_with_senders =
SealedBlockWithSenders::new(sealed_block, senders)
.expect("senders are valid");
// update canon chain for rpc
client.set_canonical_head(new_header.clone());
client.set_safe(new_header.clone());
client.set_finalized(new_header.clone());
debug!(target: "consensus::auto", header=?sealed_block_with_senders.hash(), "sending block notification");
let chain = Arc::new(Chain::new(
vec![sealed_block_with_senders],
bundle_state,
None,
));
// send block notification
let _ = canon_state_notification
.send(reth_provider::CanonStateNotification::Commit { new: chain });
}
Err(err) => {
warn!(target: "consensus::auto", %err, "failed to execute block")
}
}
events
}));
}
if let Some(mut fut) = this.insert_task.take() {
match fut.poll_unpin(cx) {
Poll::Ready(events) => {
this.pipe_line_events = events;
}
Poll::Pending => {
this.insert_task = Some(fut);
break
}
}
}
}
Poll::Pending
}
}
impl<Client, Pool: TransactionPool, EvmConfig: std::fmt::Debug, Engine: EngineTypes> std::fmt::Debug
for MiningTask<Client, Pool, EvmConfig, Engine>
{
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("MiningTask").finish_non_exhaustive()
}
}