fuel_block_producer/
block_producer.rs1use 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 pub lock: Mutex<()>,
73 pub dry_run_semaphore: Semaphore,
74}
75
76impl<Database> Producer<Database>
77where
78 Database: BlockProducerDatabase + 'static,
79{
80 pub async fn produce_and_execute_block(
82 &self,
83 height: BlockHeight,
84 max_gas: Word,
85 ) -> Result<UncommittedResult<ports::DBTransaction<Database>>> {
86 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 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 pub async fn dry_run(
126 &self,
127 transaction: Transaction,
128 height: Option<BlockHeight>,
129 utxo_validation: Option<bool>,
130 ) -> Result<Vec<Receipt>> {
131 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 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 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 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 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 if height == 0u32.into() {
213 Err(Error::GenesisBlock.into())
214 } else {
215 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 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}