sc_consensus_nakamoto/
block_import.rs

1//! This module provides the implementation for importing Bitcoin blocks into Subcoin.
2//!
3//! Each Bitcoin block is converted into a Substrate block and imported into the database.
4//! The Bitcoin header is included in the Substrate header as a `DigestItem`, each Bitcoin
5//! transaction is wrapped into an unsigned extrinsic defined in pallet-bitcoin.
6//!
7//! Key components:
8//!
9//! - [`BitcoinBlockImporter`]
10//!   The main struct responsible for importing Bitcoin blocks and managing the import process.
11//!
12//! - [`BitcoinBlockImport`]
13//!   An async trait for importing Bitcoin blocks, which is implemented by [`BitcoinBlockImporter`].
14//!
15//! - [`ImportConfig`]
16//!   Configuration for block import, including network type, verification level, and execution options.
17//!
18//! - [`ImportStatus`]
19//!   An enum representing the result of an import operation, with variants for different import outcomes.
20
21use crate::ScriptEngine;
22use crate::metrics::Metrics;
23use crate::verification::{BlockVerification, BlockVerifier};
24use bitcoin::blockdata::block::Header as BitcoinHeader;
25use bitcoin::hashes::Hash;
26use bitcoin::{Block as BitcoinBlock, BlockHash, Network, Work};
27use codec::Encode;
28use sc_client_api::{AuxStore, Backend, BlockBackend, HeaderBackend, StorageProvider};
29use sc_consensus::{
30    BlockImport, BlockImportParams, ForkChoiceStrategy, ImportResult, ImportedAux, StateAction,
31    StorageChanges,
32};
33use sp_api::{ApiExt, CallApiAt, CallContext, Core, ProvideRuntimeApi};
34use sp_blockchain::HashAndNumber;
35use sp_consensus::{BlockOrigin, BlockStatus};
36use sp_runtime::traits::{
37    Block as BlockT, Hash as HashT, HashingFor, Header as HeaderT, NumberFor,
38};
39use sp_runtime::{SaturatedConversion, Saturating};
40use std::marker::PhantomData;
41use std::sync::Arc;
42use std::time::Instant;
43use subcoin_primitives::runtime::SubcoinApi;
44use subcoin_primitives::{
45    BackendExt, BitcoinTransactionAdapter, CoinStorageKey, substrate_header_digest,
46};
47use substrate_prometheus_endpoint::Registry;
48
49/// Block import configuration.
50#[derive(Debug, Clone)]
51pub struct ImportConfig {
52    /// Bitcoin network type.
53    pub network: Network,
54    /// Specify the block verification level.
55    pub block_verification: BlockVerification,
56    /// Whether to execute the transactions in the block.
57    pub execute_block: bool,
58    /// Bitcoin script interpreter.
59    pub script_engine: ScriptEngine,
60}
61
62#[derive(Debug, Default)]
63struct Stats {
64    /// Total transactions processed.
65    total_txs: usize,
66    /// Total blocks processed.
67    total_blocks: usize,
68    // Total block execution times in milliseconds.
69    total_execution_time: usize,
70}
71
72impl Stats {
73    fn transaction_per_millisecond(&self) -> f64 {
74        self.total_txs as f64 / self.total_execution_time as f64
75    }
76
77    fn block_per_second(&self) -> f64 {
78        self.total_blocks as f64 * 1000f64 / self.total_execution_time as f64
79    }
80
81    fn record_new_block_execution<Block: BlockT>(
82        &mut self,
83        block_number: NumberFor<Block>,
84        block_hash: Block::Hash,
85        tx_count: usize,
86        execution_time: u128,
87    ) {
88        self.total_txs += tx_count;
89        self.total_blocks += 1;
90        self.total_execution_time += execution_time as usize;
91        let tx_per_ms = self.transaction_per_millisecond();
92        let block_per_second = self.block_per_second();
93
94        tracing::debug!(
95            "Executed block#{block_number} ({tx_count} txs) ({block_hash}) \
96            {tx_per_ms:.2} tx/ms, {block_per_second:.2} block/s, execution time: {execution_time} ms",
97        );
98    }
99}
100
101/// Bitcoin specific block import implementation.
102pub struct BitcoinBlockImporter<Block, Client, BE, BI, TransactionAdapter> {
103    client: Arc<Client>,
104    inner: BI,
105    stats: Stats,
106    config: ImportConfig,
107    verifier: BlockVerifier<Block, Client, BE>,
108    metrics: Option<Metrics>,
109    last_block_execution_report: Instant,
110    _phantom: PhantomData<TransactionAdapter>,
111}
112
113impl<Block, Client, BE, BI, TransactionAdapter>
114    BitcoinBlockImporter<Block, Client, BE, BI, TransactionAdapter>
115where
116    Block: BlockT,
117    BE: Backend<Block> + 'static,
118    Client: HeaderBackend<Block>
119        + BlockBackend<Block>
120        + AuxStore
121        + ProvideRuntimeApi<Block>
122        + StorageProvider<Block, BE>
123        + CallApiAt<Block>
124        + Send
125        + 'static,
126    Client::Api: Core<Block> + SubcoinApi<Block>,
127    BI: BlockImport<Block> + Send + Sync + 'static,
128    TransactionAdapter: BitcoinTransactionAdapter<Block>,
129{
130    /// Constructs a new instance of [`BitcoinBlockImporter`].
131    pub fn new(
132        client: Arc<Client>,
133        block_import: BI,
134        config: ImportConfig,
135        coin_storage_key: Arc<dyn CoinStorageKey>,
136        registry: Option<&Registry>,
137    ) -> Self {
138        let verifier = BlockVerifier::new(
139            client.clone(),
140            config.network,
141            config.block_verification,
142            coin_storage_key,
143            config.script_engine,
144        );
145        let metrics = match registry {
146            Some(registry) => Metrics::register(registry)
147                .map_err(|err| {
148                    tracing::error!("Failed to registry metrics: {err:?}");
149                })
150                .ok(),
151            None => None,
152        };
153        Self {
154            client,
155            inner: block_import,
156            stats: Stats::default(),
157            config,
158            verifier,
159            metrics,
160            last_block_execution_report: Instant::now(),
161            _phantom: Default::default(),
162        }
163    }
164
165    #[inline]
166    fn substrate_block_hash(&self, bitcoin_block_hash: BlockHash) -> Option<Block::Hash> {
167        BackendExt::<Block>::substrate_block_hash_for(&self.client, bitcoin_block_hash)
168    }
169
170    // Returns the corresponding substrate block info for given bitcoin block hash if any.
171    fn fetch_substrate_block_info(
172        &self,
173        bitcoin_block_hash: BlockHash,
174    ) -> sp_blockchain::Result<Option<HashAndNumber<Block>>> {
175        let Some(substrate_block_hash) = self.substrate_block_hash(bitcoin_block_hash) else {
176            return Ok(None);
177        };
178
179        let block_number =
180            self.client
181                .number(substrate_block_hash)?
182                .ok_or(sp_blockchain::Error::UnknownBlock(format!(
183                    "Substate block hash mapping exists, \
184                    but block#{substrate_block_hash} is not in chain"
185                )))?;
186
187        Ok(Some(HashAndNumber {
188            number: block_number,
189            hash: substrate_block_hash,
190        }))
191    }
192
193    fn execute_block_at(
194        &mut self,
195        block_number: NumberFor<Block>,
196        parent_hash: Block::Hash,
197        block: Block,
198    ) -> sp_blockchain::Result<(
199        Block::Hash,
200        sp_state_machine::StorageChanges<HashingFor<Block>>,
201    )> {
202        let timer = std::time::Instant::now();
203
204        let transactions_count = block.extrinsics().len();
205        let block_size = block.encoded_size();
206
207        let mut runtime_api = self.client.runtime_api();
208        runtime_api.set_call_context(CallContext::Onchain);
209
210        runtime_api.execute_block_without_state_root_check(parent_hash, block)?;
211
212        let state = self.client.state_at(parent_hash)?;
213
214        let storage_changes = runtime_api
215            .into_storage_changes(&state, parent_hash)
216            .map_err(sp_blockchain::Error::StorageChanges)?;
217
218        let state_root = storage_changes.transaction_storage_root;
219
220        if let Some(metrics) = &self.metrics {
221            const BLOCK_EXECUTION_REPORT_INTERVAL: u128 = 50;
222
223            // Executing blocks before 200000 is pretty fast, it becomes
224            // increasingly slower beyond this point, we only cares about
225            // the slow block executions.
226            if self.last_block_execution_report.elapsed().as_millis()
227                > BLOCK_EXECUTION_REPORT_INTERVAL
228            {
229                let block_number: u32 = block_number.saturated_into();
230                let execution_time = timer.elapsed().as_millis();
231                metrics.report_block_execution(
232                    block_number.saturated_into(),
233                    transactions_count,
234                    block_size,
235                    execution_time,
236                );
237                self.last_block_execution_report = Instant::now();
238            }
239        }
240
241        Ok((state_root, storage_changes))
242    }
243
244    fn prepare_substrate_block_import(
245        &mut self,
246        block: BitcoinBlock,
247        substrate_parent_block: HashAndNumber<Block>,
248        origin: BlockOrigin,
249    ) -> sp_blockchain::Result<BlockImportParams<Block>> {
250        let HashAndNumber {
251            number: parent_block_number,
252            hash: parent_hash,
253        } = substrate_parent_block;
254
255        let block_number = parent_block_number.saturating_add(1u32.into());
256
257        let extrinsics = block
258            .txdata
259            .clone()
260            .into_iter()
261            .map(TransactionAdapter::bitcoin_transaction_into_extrinsic)
262            .collect::<Vec<_>>();
263
264        let extrinsics_root = HashingFor::<Block>::ordered_trie_root(
265            extrinsics.iter().map(|xt| xt.encode()).collect(),
266            sp_core::storage::StateVersion::V0,
267        );
268
269        let digest = substrate_header_digest(&block.header);
270
271        // We know everything for the Substrate header except the state root, which will be
272        // obtained after executing the block manually.
273        let mut header = <<Block as BlockT>::Header as HeaderT>::new(
274            block_number,
275            extrinsics_root,
276            Default::default(),
277            parent_hash,
278            digest,
279        );
280
281        // subcoin bootstrap node must execute every block.
282        let state_action = if self.config.execute_block {
283            let now = std::time::Instant::now();
284
285            let tx_count = extrinsics.len();
286
287            let (state_root, storage_changes) = self.execute_block_at(
288                block_number,
289                parent_hash,
290                Block::new(header.clone(), extrinsics.clone()),
291            )?;
292
293            let execution_time = now.elapsed().as_millis();
294
295            self.stats.record_new_block_execution::<Block>(
296                block_number,
297                header.hash(),
298                tx_count,
299                execution_time,
300            );
301
302            // Now it's a normal Substrate header after setting the state root.
303            header.set_state_root(state_root);
304
305            StateAction::ApplyChanges(StorageChanges::<Block>::Changes(storage_changes))
306        } else {
307            StateAction::Skip
308        };
309
310        let substrate_block_hash = header.hash();
311        let bitcoin_block_hash = block.header.block_hash();
312
313        let mut block_import_params = BlockImportParams::new(origin, header);
314        let (total_work, fork_choice) = calculate_chain_work_and_fork_choice(
315            &self.client,
316            &block.header,
317            block_number.saturated_into(),
318        )?;
319        block_import_params.fork_choice = Some(fork_choice);
320        block_import_params.body = Some(extrinsics);
321        block_import_params.state_action = state_action;
322
323        write_aux_storage(
324            &mut block_import_params,
325            bitcoin_block_hash,
326            substrate_block_hash,
327            total_work,
328        );
329
330        Ok(block_import_params)
331    }
332}
333
334pub(crate) fn calculate_chain_work_and_fork_choice<Block, Client>(
335    client: &Arc<Client>,
336    block_header: &BitcoinHeader,
337    block_number: u32,
338) -> sp_blockchain::Result<(Work, ForkChoiceStrategy)>
339where
340    Block: BlockT,
341    Client: HeaderBackend<Block> + AuxStore,
342{
343    let prev_blockhash = block_header.prev_blockhash;
344
345    let parent_work = if block_number == 1u32 {
346        // Genesis block's work is hardcoded.
347        client
348            .block_header(prev_blockhash)
349            .expect("Genesis header must exist; qed")
350            .work()
351    } else {
352        crate::aux_schema::load_chain_work(client.as_ref(), prev_blockhash)?
353    };
354
355    let total_work = parent_work + block_header.work();
356
357    let fork_choice = {
358        let info = client.info();
359
360        let parent_hash = client.substrate_block_hash_for(prev_blockhash).ok_or(
361            sp_blockchain::Error::Backend(format!(
362                "Missing substrate block hash for #{prev_blockhash}"
363            )),
364        )?;
365
366        let last_best_work = if info.best_hash == parent_hash {
367            parent_work
368        } else {
369            let bitcoin_block_hash = client
370                .bitcoin_block_hash_for(info.best_hash)
371                .expect("Best bitcoin hash must exist; qed");
372            crate::aux_schema::load_chain_work(client.as_ref(), bitcoin_block_hash)?
373        };
374
375        ForkChoiceStrategy::Custom(total_work > last_best_work)
376    };
377
378    Ok((total_work, fork_choice))
379}
380
381/// Writes storage into the auxiliary data of the `block_import_params`, which
382/// will be stored in the aux-db within the [`BitcoinBlockImport::import_block`]
383/// or Substrate import queue's verification process.
384///
385/// The auxiliary data stored includes:
386/// - A mapping between a Bitcoin block hash and a Substrate block hash.
387/// - Total accumulated proof-of-work up to the given block.
388pub(crate) fn write_aux_storage<Block: BlockT>(
389    block_import_params: &mut BlockImportParams<Block>,
390    bitcoin_block_hash: BlockHash,
391    substrate_block_hash: Block::Hash,
392    chain_work: Work,
393) {
394    block_import_params.auxiliary.push((
395        bitcoin_block_hash.to_byte_array().to_vec(),
396        Some(substrate_block_hash.encode()),
397    ));
398    crate::aux_schema::write_chain_work(bitcoin_block_hash, chain_work, |(k, v)| {
399        block_import_params.auxiliary.push((k, Some(v)))
400    });
401}
402
403/// Result of the operation of importing a Bitcoin block.
404///
405/// Same semantic with [`sc_consensus::ImportResult`] with additional information
406/// in the [`ImportStatus::Imported`] variant.
407#[derive(Debug, Clone)]
408pub enum ImportStatus {
409    /// Block was imported successfully.
410    Imported {
411        block_number: u32,
412        block_hash: BlockHash,
413        aux: ImportedAux,
414    },
415    /// Already in the blockchain.
416    AlreadyInChain(u32),
417    /// Block parent is not in the chain.
418    UnknownParent,
419    /// Parent state is missing.
420    MissingState,
421    /// Block or parent is known to be bad.
422    KnownBad,
423}
424
425impl ImportStatus {
426    /// Returns `true` if the import status is [`Self::UnknownParent`].
427    pub fn is_unknown_parent(&self) -> bool {
428        matches!(self, Self::UnknownParent)
429    }
430}
431
432/// A trait for importing Bitcoin blocks.
433#[async_trait::async_trait]
434pub trait BitcoinBlockImport: Send + Sync + 'static {
435    /// Import a Bitcoin block.
436    async fn import_block(
437        &mut self,
438        block: BitcoinBlock,
439        origin: BlockOrigin,
440    ) -> Result<ImportStatus, sp_consensus::Error>;
441}
442
443#[async_trait::async_trait]
444impl<Block, Client, BE, BI, TransactionAdapter> BitcoinBlockImport
445    for BitcoinBlockImporter<Block, Client, BE, BI, TransactionAdapter>
446where
447    Block: BlockT,
448    BE: Backend<Block> + Send + Sync + 'static,
449    Client: HeaderBackend<Block>
450        + BlockBackend<Block>
451        + StorageProvider<Block, BE>
452        + AuxStore
453        + ProvideRuntimeApi<Block>
454        + CallApiAt<Block>
455        + Send
456        + 'static,
457    Client::Api: Core<Block> + SubcoinApi<Block>,
458    BI: BlockImport<Block> + Send + Sync + 'static,
459    TransactionAdapter: BitcoinTransactionAdapter<Block> + Send + Sync + 'static,
460{
461    async fn import_block(
462        &mut self,
463        block: BitcoinBlock,
464        origin: BlockOrigin,
465    ) -> Result<ImportStatus, sp_consensus::Error> {
466        if let Some(block_number) = self.client.block_number(block.block_hash()) {
467            return Ok(ImportStatus::AlreadyInChain(block_number));
468        }
469
470        let bitcoin_parent_hash = block.header.prev_blockhash;
471
472        let import_err = sp_consensus::Error::ClientImport;
473
474        let Some(substrate_parent_block) = self
475            .fetch_substrate_block_info(bitcoin_parent_hash)
476            .map_err(|err| import_err(err.to_string()))?
477        else {
478            // The parent block must exist to proceed, otherwise it's an orphan bolck.
479            return Ok(ImportStatus::UnknownParent);
480        };
481
482        if self.config.execute_block {
483            // Ensure the parent block has been imported and the parent state exists.
484            match self
485                .client
486                .block_status(substrate_parent_block.hash)
487                .map_err(|err| import_err(err.to_string()))?
488            {
489                BlockStatus::InChainWithState | BlockStatus::Queued => {}
490                BlockStatus::Unknown => return Ok(ImportStatus::UnknownParent),
491                BlockStatus::InChainPruned => return Ok(ImportStatus::MissingState),
492                BlockStatus::KnownBad => return Ok(ImportStatus::KnownBad),
493            }
494        }
495
496        let block_number = substrate_parent_block.number.saturated_into::<u32>() + 1u32;
497        let block_hash = block.block_hash();
498
499        // Consensus-level Bitcoin block verification.
500        self.verifier
501            .verify_block(block_number, &block)
502            .map_err(|err| import_err(format!("{err:?}")))?;
503
504        let block_import_params = self
505            .prepare_substrate_block_import(block, substrate_parent_block, origin)
506            .map_err(|err| import_err(err.to_string()))?;
507
508        self.inner
509            .import_block(block_import_params)
510            .await
511            .map(|import_result| match import_result {
512                ImportResult::Imported(aux) => ImportStatus::Imported {
513                    block_number,
514                    block_hash,
515                    aux,
516                },
517                ImportResult::AlreadyInChain => ImportStatus::AlreadyInChain(block_number),
518                ImportResult::KnownBad => ImportStatus::KnownBad,
519                ImportResult::UnknownParent => ImportStatus::UnknownParent,
520                ImportResult::MissingState => ImportStatus::MissingState,
521            })
522            .map_err(|err| import_err(err.to_string()))
523    }
524}