fuel_block_producer/
block_producer.rs

1use crate::{
2    ports,
3    ports::BlockProducerDatabase,
4    Config,
5};
6use anyhow::{
7    anyhow,
8    Context,
9    Result,
10};
11use fuel_core_interfaces::{
12    common::{
13        crypto::ephemeral_merkle_root,
14        fuel_tx::{
15            Receipt,
16            Transaction,
17            Word,
18        },
19        fuel_types::Bytes32,
20        tai64::Tai64,
21    },
22    executor::{
23        ExecutionBlock,
24        UncommittedResult,
25    },
26    model::{
27        BlockHeight,
28        DaBlockHeight,
29        FuelApplicationHeader,
30        FuelConsensusHeader,
31        PartialFuelBlock,
32        PartialFuelBlockHeader,
33    },
34};
35use std::sync::Arc;
36use thiserror::Error;
37use tokio::{
38    sync::{
39        Mutex,
40        Semaphore,
41    },
42    task::spawn_blocking,
43};
44use tracing::debug;
45
46#[cfg(test)]
47mod tests;
48
49#[derive(Error, Debug)]
50pub enum Error {
51    #[error(
52        "0 is an invalid block height for production. It is reserved for genesis data."
53    )]
54    GenesisBlock,
55    #[error("Previous block height {0} doesn't exist")]
56    MissingBlock(BlockHeight),
57    #[error("Best finalized da_height {best} is behind previous block da_height {previous_block}")]
58    InvalidDaFinalizationState {
59        best: DaBlockHeight,
60        previous_block: DaBlockHeight,
61    },
62}
63
64pub struct Producer<Database> {
65    pub config: Config,
66    pub db: Database,
67    pub txpool: Box<dyn ports::TxPool>,
68    pub executor: Arc<dyn ports::Executor<Database>>,
69    pub relayer: Box<dyn ports::Relayer>,
70    // use a tokio lock since we want callers to yield until the previous block
71    // execution has completed (which may take a while).
72    pub lock: Mutex<()>,
73    pub dry_run_semaphore: Semaphore,
74}
75
76impl<Database> Producer<Database>
77where
78    Database: BlockProducerDatabase + 'static,
79{
80    /// Produces and execute block for the specified height
81    pub async fn produce_and_execute_block(
82        &self,
83        height: BlockHeight,
84        max_gas: Word,
85    ) -> Result<UncommittedResult<ports::DBTransaction<Database>>> {
86        //  - get previous block info (hash, root, etc)
87        //  - select best da_height from relayer
88        //  - get available txs from txpool
89        //  - select best txs based on factors like:
90        //      1. fees
91        //      2. parallel throughput
92        //  - Execute block with production mode to correctly malleate txs outputs and block headers
93
94        // prevent simultaneous block production calls, the guard will drop at the end of this fn.
95        let _production_guard = self.lock.lock().await;
96
97        let best_transactions = self.txpool.get_includable_txs(height, max_gas).await?;
98
99        let header = self.new_header(height).await?;
100        let block = PartialFuelBlock::new(
101            header,
102            best_transactions
103                .into_iter()
104                .map(|tx| tx.as_ref().into())
105                .collect(),
106        );
107
108        // Store the context string incase we error.
109        let context_string = format!(
110            "Failed to produce block {:?} due to execution failure",
111            block
112        );
113        let result = self
114            .executor
115            .execute_without_commit(ExecutionBlock::Production(block))
116            .context(context_string)?;
117
118        debug!("Produced block with result: {:?}", result.result());
119        Ok(result)
120    }
121
122    /// Simulate a transaction without altering any state. Does not aquire the production lock
123    /// since it is basically a "read only" operation and shouldn't get in the way of normal
124    /// production.
125    pub async fn dry_run(
126        &self,
127        transaction: Transaction,
128        height: Option<BlockHeight>,
129        utxo_validation: Option<bool>,
130    ) -> Result<Vec<Receipt>> {
131        // setup the block with the provided tx and optional height
132        // dry_run execute tx on the executor
133        // return the receipts
134        let _permit = self.dry_run_semaphore.acquire().await;
135
136        let height = match height {
137            None => self.db.current_block_height()?,
138            Some(height) => height,
139        } + 1u64.into();
140
141        let is_script = transaction.is_script();
142        let header = self.new_header(height).await?;
143        let block =
144            PartialFuelBlock::new(header, vec![transaction].into_iter().collect());
145
146        let executor = self.executor.clone();
147        // use the blocking threadpool for dry_run to avoid clogging up the main async runtime
148        let res: Vec<_> = spawn_blocking(move || -> Result<Vec<Receipt>> {
149            Ok(executor
150                .dry_run(ExecutionBlock::Production(block), utxo_validation)?
151                .into_iter()
152                .flatten()
153                .collect())
154        })
155        .await??;
156        if is_script && res.is_empty() {
157            return Err(anyhow!("Expected at least one set of receipts"))
158        }
159        Ok(res)
160    }
161}
162
163impl<Database> Producer<Database>
164where
165    Database: BlockProducerDatabase,
166{
167    /// Create the header for a new block at the provided height
168    async fn new_header(&self, height: BlockHeight) -> Result<PartialFuelBlockHeader> {
169        let previous_block_info = self.previous_block_info(height)?;
170        let new_da_height = self
171            .select_new_da_height(previous_block_info.da_height)
172            .await?;
173
174        Ok(PartialFuelBlockHeader {
175            application: FuelApplicationHeader {
176                da_height: new_da_height,
177                generated: Default::default(),
178            },
179            consensus: FuelConsensusHeader {
180                // TODO: this needs to be updated using a proper BMT MMR
181                prev_root: previous_block_info.prev_root,
182                height,
183                time: Tai64::now(),
184                generated: Default::default(),
185            },
186            metadata: None,
187        })
188    }
189
190    async fn select_new_da_height(
191        &self,
192        previous_da_height: DaBlockHeight,
193    ) -> Result<DaBlockHeight> {
194        let best_height = self.relayer.get_best_finalized_da_height().await?;
195        if best_height < previous_da_height {
196            // If this happens, it could mean a block was erroneously imported
197            // without waiting for our relayer's da_height to catch up to imported da_height.
198            return Err(Error::InvalidDaFinalizationState {
199                best: best_height,
200                previous_block: previous_da_height,
201            }
202            .into())
203        }
204        Ok(best_height)
205    }
206
207    fn previous_block_info(&self, height: BlockHeight) -> Result<PreviousBlockInfo> {
208        // TODO: It is not guaranteed that the genesis height is `0` height. Update the code to
209        //  use a genesis height from the database. If the `height` less than genesis height ->
210        //  return a new error.
211        // block 0 is reserved for genesis
212        if height == 0u32.into() {
213            Err(Error::GenesisBlock.into())
214        } else {
215            // get info from previous block height
216            let prev_height = height - 1u32.into();
217            let previous_block = self
218                .db
219                .get_block(prev_height)?
220                .ok_or(Error::MissingBlock(prev_height))?;
221            // TODO: this should use a proper BMT MMR
222            let hash = previous_block.id();
223            let prev_root = ephemeral_merkle_root(
224                vec![*previous_block.header.prev_root(), hash.into()].iter(),
225            );
226
227            Ok(PreviousBlockInfo {
228                prev_root,
229                da_height: previous_block.header.da_height,
230            })
231        }
232    }
233}
234
235struct PreviousBlockInfo {
236    prev_root: Bytes32,
237    da_height: DaBlockHeight,
238}