subcoin_primitives/
lib.rs

1//! Primitives for the client.
2
3pub mod consensus;
4pub mod tx_pool;
5
6use bitcoin::blockdata::block::Header as BitcoinHeader;
7use bitcoin::consensus::{Decodable, Encodable};
8use bitcoin::constants::genesis_block;
9use bitcoin::hashes::Hash;
10use bitcoin::{
11    Amount, Block as BitcoinBlock, BlockHash, ScriptBuf, Transaction, TxOut, Txid, Weight,
12};
13use codec::{Decode, Encode};
14use sc_client_api::AuxStore;
15use sp_blockchain::HeaderBackend;
16use sp_runtime::generic::{Digest, DigestItem};
17use sp_runtime::traits::{Block as BlockT, Header as HeaderT};
18use std::sync::Arc;
19use subcoin_runtime_primitives::{NAKAMOTO_HASH_ENGINE_ID, NAKAMOTO_HEADER_ENGINE_ID};
20
21pub use subcoin_runtime_primitives as runtime;
22
23type Height = u32;
24
25/// 6 blocks is the standard confirmation period in the Bitcoin community.
26pub const CONFIRMATION_DEPTH: u32 = 6u32;
27
28/// The maximum allowed weight for a block, see BIP 141 (network rule).
29pub const MAX_BLOCK_WEIGHT: Weight = Weight::MAX_BLOCK;
30
31/// Returns the encoded Bitcoin genesis block.
32///
33/// Used in the Substrate genesis block construction.
34pub fn raw_genesis_tx(network: bitcoin::Network) -> Vec<u8> {
35    let mut data = Vec::new();
36
37    genesis_block(network)
38        .txdata
39        .into_iter()
40        .next()
41        .expect("Bitcoin genesis tx must exist; qed")
42        .consensus_encode(&mut data)
43        .expect("Genesis tx must be valid; qed");
44
45    data
46}
47
48/// Returns the encoded Bitcoin genesis block.
49pub fn bitcoin_genesis_tx() -> Vec<u8> {
50    raw_genesis_tx(bitcoin::Network::Bitcoin)
51}
52
53/// Represents an indexed Bitcoin block, identified by its block number and hash.
54#[derive(Debug, Clone, Copy)]
55pub struct IndexedBlock {
56    /// Block number.
57    pub number: u32,
58    /// Block hash.
59    pub hash: BlockHash,
60}
61
62impl std::fmt::Display for IndexedBlock {
63    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
64        write!(f, "#{},{}", self.number, self.hash)
65    }
66}
67
68impl Default for IndexedBlock {
69    fn default() -> Self {
70        Self {
71            number: 0u32,
72            hash: BlockHash::all_zeros(),
73        }
74    }
75}
76
77/// Trait for converting between Substrate extrinsics and Bitcoin transactions.
78pub trait BitcoinTransactionAdapter<Block: BlockT> {
79    /// Converts Substrate extrinsic to Bitcoin transaction.
80    fn extrinsic_to_bitcoin_transaction(extrinsics: &Block::Extrinsic) -> Transaction;
81
82    /// Converts a Bitcoin transaction into a Substrate extrinsic.
83    fn bitcoin_transaction_into_extrinsic(btc_tx: Transaction) -> Block::Extrinsic;
84}
85
86/// Trait for interfacing with the Bitcoin storage.
87///
88/// Th essence of this trait is the mapping of the hashes between the substrate block
89/// and the corresponding bitcoin block.
90///
91/// The mapping is stored in the client's auxiliary database.
92pub trait BackendExt<Block: BlockT> {
93    /// Whether the specified Bitcoin block exists in the system.
94    fn block_exists(&self, bitcoin_block_hash: BlockHash) -> bool;
95
96    /// Returns the number for given bitcoin block hash.
97    ///
98    /// Returns `None` if the header is not in the chain.
99    fn block_number(&self, bitcoin_block_hash: BlockHash) -> Option<Height>;
100
101    /// Returns the bitcoin block hash for given block number.
102    ///
103    /// Returns `None` if the header is not in the chain.
104    fn block_hash(&self, block_number: u32) -> Option<BlockHash>;
105
106    /// Returns the header for given bitcoin block hash.
107    fn block_header(&self, bitcoin_block_hash: BlockHash) -> Option<BitcoinHeader>;
108
109    /// Returns `Some(BlockHash)` if a corresponding Bitcoin block hash is found, otherwise returns `None`.
110    fn bitcoin_block_hash_for(
111        &self,
112        substrate_block_hash: <Block as BlockT>::Hash,
113    ) -> Option<BlockHash>;
114
115    /// Returns `Some(Block::Hash)` if a corresponding Substrate block hash is found, otherwise returns `None`.
116    fn substrate_block_hash_for(
117        &self,
118        bitcoin_block_hash: BlockHash,
119    ) -> Option<<Block as BlockT>::Hash>;
120}
121
122impl<Block, Client> BackendExt<Block> for Arc<Client>
123where
124    Block: BlockT,
125    Client: HeaderBackend<Block> + AuxStore,
126{
127    fn block_exists(&self, bitcoin_block_hash: BlockHash) -> bool {
128        self.get_aux(bitcoin_block_hash.as_ref())
129            .ok()
130            .flatten()
131            .is_some()
132    }
133
134    fn block_number(&self, bitcoin_block_hash: BlockHash) -> Option<Height> {
135        self.substrate_block_hash_for(bitcoin_block_hash)
136            .and_then(|substrate_block_hash| self.number(substrate_block_hash).ok().flatten())
137            .map(|number| {
138                number
139                    .try_into()
140                    .unwrap_or_else(|_| panic!("BlockNumber must fit into u32; qed"))
141            })
142    }
143
144    fn block_hash(&self, number: u32) -> Option<BlockHash> {
145        self.hash(number.into())
146            .ok()
147            .flatten()
148            .and_then(|substrate_block_hash| self.bitcoin_block_hash_for(substrate_block_hash))
149    }
150
151    fn block_header(&self, bitcoin_block_hash: BlockHash) -> Option<BitcoinHeader> {
152        self.substrate_block_hash_for(bitcoin_block_hash)
153            .and_then(|substrate_block_hash| self.header(substrate_block_hash).ok().flatten())
154            .and_then(|header| extract_bitcoin_block_header::<Block>(&header).ok())
155    }
156
157    fn bitcoin_block_hash_for(
158        &self,
159        substrate_block_hash: <Block as BlockT>::Hash,
160    ) -> Option<BlockHash> {
161        self.header(substrate_block_hash)
162            .ok()
163            .flatten()
164            .and_then(|substrate_header| {
165                extract_bitcoin_block_hash::<Block>(&substrate_header).ok()
166            })
167    }
168
169    fn substrate_block_hash_for(
170        &self,
171        bitcoin_block_hash: BlockHash,
172    ) -> Option<<Block as BlockT>::Hash> {
173        self.get_aux(bitcoin_block_hash.as_ref())
174            .map_err(|err| {
175                tracing::error!(
176                    ?bitcoin_block_hash,
177                    "Failed to fetch substrate block hash: {err:?}"
178                );
179            })
180            .ok()
181            .flatten()
182            .and_then(|substrate_hash| Decode::decode(&mut substrate_hash.as_slice()).ok())
183    }
184}
185
186/// Number of blocks for median time calculation (BIP113).
187pub const MEDIAN_TIME_SPAN: usize = 11;
188
189/// A trait to extend the Substrate Client.
190pub trait ClientExt<Block> {
191    /// Returns the number of best block.
192    fn best_number(&self) -> u32;
193
194    /// Calculate median time past for a given block (BIP113).
195    ///
196    /// Returns the median timestamp of the last 11 blocks (including the given block).
197    fn calculate_median_time_past(&self, block_hash: BlockHash) -> Option<i64>;
198
199    /// Get block metadata (height and median time past) for a given block hash.
200    ///
201    /// Returns None if the block is not found in the chain.
202    fn get_block_metadata(&self, block_hash: BlockHash) -> Option<BlockMetadata>;
203
204    /// Check if a block is on the active (best) chain.
205    ///
206    /// Returns false if the block is not found or is on a stale fork.
207    fn is_block_on_active_chain(&self, block_hash: BlockHash) -> bool;
208}
209
210impl<Block, Client> ClientExt<Block> for Arc<Client>
211where
212    Block: BlockT,
213    Client: HeaderBackend<Block> + AuxStore,
214{
215    fn best_number(&self) -> u32 {
216        self.info()
217            .best_number
218            .try_into()
219            .unwrap_or_else(|_| panic!("BlockNumber must fit into u32; qed"))
220    }
221
222    fn calculate_median_time_past(&self, block_hash: BlockHash) -> Option<i64> {
223        let mut timestamps = Vec::with_capacity(MEDIAN_TIME_SPAN);
224
225        let header = self.block_header(block_hash)?;
226        timestamps.push(header.time as i64);
227
228        let zero_hash = BlockHash::all_zeros();
229        let mut prev_hash = header.prev_blockhash;
230
231        // Collect timestamps from previous blocks
232        for _ in 0..MEDIAN_TIME_SPAN - 1 {
233            if prev_hash == zero_hash {
234                break;
235            }
236
237            let header = self.block_header(prev_hash)?;
238            timestamps.push(header.time as i64);
239            prev_hash = header.prev_blockhash;
240        }
241
242        timestamps.sort_unstable();
243        Some(timestamps[timestamps.len() / 2])
244    }
245
246    fn get_block_metadata(&self, block_hash: BlockHash) -> Option<BlockMetadata> {
247        let height = self.block_number(block_hash)?;
248        let median_time_past = self.calculate_median_time_past(block_hash)?;
249
250        Some(BlockMetadata {
251            height,
252            median_time_past,
253        })
254    }
255
256    fn is_block_on_active_chain(&self, block_hash: BlockHash) -> bool {
257        self.block_number(block_hash)
258            .and_then(|height| self.block_hash(height))
259            .map(|canonical_hash| canonical_hash == block_hash)
260            .unwrap_or(false)
261    }
262}
263
264/// Deals with the storage key for UTXO in the state.
265pub trait CoinStorageKey: Send + Sync {
266    /// Returns the storage key for the given output specified by (txid, vout).
267    fn storage_key(&self, txid: bitcoin::Txid, vout: u32) -> Vec<u8>;
268
269    /// Returns the final storage prefix for Coins.
270    fn storage_prefix(&self) -> [u8; 32];
271}
272
273/// Represents a Bitcoin block locator, used to sync blockchain data between nodes.
274#[derive(Debug, Clone)]
275pub struct BlockLocator {
276    /// The latest block number.
277    pub latest_block: u32,
278    /// A vector of block hashes, starting from the latest block and going backwards.
279    pub locator_hashes: Vec<BlockHash>,
280}
281
282impl BlockLocator {
283    pub fn empty() -> Self {
284        Self {
285            latest_block: 0u32,
286            locator_hashes: Vec::new(),
287        }
288    }
289}
290
291/// A trait for retrieving block locators.
292pub trait BlockLocatorProvider<Block: BlockT> {
293    /// Retrieve a block locator from given height.
294    ///
295    /// If `from` is None, the block locator is generated from the current best block.
296    fn block_locator(
297        &self,
298        from: Option<Height>,
299        search_pending_block: impl Fn(Height) -> Option<BlockHash>,
300    ) -> BlockLocator;
301}
302
303impl<Block, Client> BlockLocatorProvider<Block> for Arc<Client>
304where
305    Block: BlockT,
306    Client: HeaderBackend<Block> + AuxStore,
307{
308    fn block_locator(
309        &self,
310        from: Option<Height>,
311        search_pending_block: impl Fn(Height) -> Option<BlockHash>,
312    ) -> BlockLocator {
313        let mut locator_hashes = Vec::new();
314
315        let from = from.unwrap_or_else(|| self.best_number());
316
317        for height in locator_indexes(from) {
318            // if height < last_checkpoint {
319            // Don't go past the latest checkpoint. We never want to accept a fork
320            // older than our last checkpoint.
321            // break;
322            // }
323
324            if let Some(bitcoin_hash) = search_pending_block(height) {
325                locator_hashes.push(bitcoin_hash);
326                continue;
327            }
328
329            let Ok(Some(hash)) = self.hash(height.into()) else {
330                continue;
331            };
332
333            if let Ok(Some(header)) = self.header(hash) {
334                let maybe_bitcoin_block_hash =
335                    BackendExt::<Block>::bitcoin_block_hash_for(self, header.hash());
336                if let Some(bitcoin_block_hash) = maybe_bitcoin_block_hash {
337                    locator_hashes.push(bitcoin_block_hash);
338                }
339            }
340        }
341
342        BlockLocator {
343            latest_block: from,
344            locator_hashes,
345        }
346    }
347}
348
349/// Get the locator indexes starting from a given height, and going backwards, exponentially
350/// backing off.
351fn locator_indexes(mut from: Height) -> Vec<Height> {
352    let mut indexes = Vec::new();
353    let mut step = 1;
354
355    while from > 0 {
356        // For the first 8 blocks, don't skip any heights.
357        if indexes.len() >= 8 {
358            step *= 2;
359        }
360        indexes.push(from as Height);
361        from = from.saturating_sub(step);
362    }
363
364    // Always include genesis.
365    indexes.push(0);
366
367    indexes
368}
369
370/// Represents the index of a transaction.
371#[derive(Debug, Clone, Encode, Decode)]
372pub struct TxPosition {
373    /// Number of the block including the transaction.
374    pub block_number: u32,
375    /// Position of the transaction within the block.
376    pub index: u32,
377}
378
379/// Interface for retriving the position of given transaction ID.
380pub trait TransactionIndex {
381    /// Returns the position of given transaction ID if any.
382    fn tx_index(&self, txid: Txid) -> sp_blockchain::Result<Option<TxPosition>>;
383}
384
385/// Dummy implementor of [`TransactionIndex`].
386pub struct NoTransactionIndex;
387
388impl TransactionIndex for NoTransactionIndex {
389    fn tx_index(&self, _txid: Txid) -> sp_blockchain::Result<Option<TxPosition>> {
390        Ok(None)
391    }
392}
393
394/// Constructs a Substrate header digest from a Bitcoin header.
395///
396/// NOTE: The bitcoin block hash digest is stored in the reversed byte order, making it
397/// user-friendly on polkadot.js.org.
398pub fn substrate_header_digest(bitcoin_header: &BitcoinHeader) -> Digest {
399    let mut raw_bitcoin_block_hash = bitcoin_header.block_hash().to_byte_array().to_vec();
400    raw_bitcoin_block_hash.reverse();
401
402    let mut encoded_bitcoin_header = Vec::with_capacity(32);
403    bitcoin_header
404        .consensus_encode(&mut encoded_bitcoin_header)
405        .expect("Bitcoin header must be valid; qed");
406
407    // Store the Bitcoin block hash and the bitcoin header itself in the header digest.
408    //
409    // Storing the Bitcoin block hash redundantly is used to retrieve it quickly without
410    // decoding the entire bitcoin header later.
411    Digest {
412        logs: vec![
413            DigestItem::PreRuntime(NAKAMOTO_HASH_ENGINE_ID, raw_bitcoin_block_hash),
414            DigestItem::PreRuntime(NAKAMOTO_HEADER_ENGINE_ID, encoded_bitcoin_header),
415        ],
416    }
417}
418
419/// Error type of Subcoin header.
420#[derive(Debug, Clone)]
421pub enum HeaderError {
422    MultiplePreRuntimeDigests,
423    MissingBitcoinBlockHashDigest,
424    InvalidBitcoinBlockHashDigest,
425    MissingBitcoinBlockHeader,
426    InvalidBitcoinBlockHeader(String),
427}
428
429/// Extracts the Bitcoin block hash from the given Substrate header.
430pub fn extract_bitcoin_block_hash<Block: BlockT>(
431    header: &Block::Header,
432) -> Result<BlockHash, HeaderError> {
433    let mut pre_digest: Option<_> = None;
434
435    for log in header.digest().logs() {
436        tracing::trace!("Checking log {:?}, looking for pre runtime digest", log);
437        match (log, pre_digest.is_some()) {
438            (DigestItem::PreRuntime(NAKAMOTO_HASH_ENGINE_ID, _), true) => {
439                return Err(HeaderError::MultiplePreRuntimeDigests);
440            }
441            (DigestItem::PreRuntime(NAKAMOTO_HASH_ENGINE_ID, v), false) => {
442                pre_digest.replace(v);
443            }
444            (_, _) => tracing::trace!("Ignoring digest not meant for us"),
445        }
446    }
447
448    let mut raw_bitcoin_block_hash = pre_digest
449        .ok_or(HeaderError::MissingBitcoinBlockHashDigest)?
450        .to_vec();
451    raw_bitcoin_block_hash.reverse();
452
453    BlockHash::from_slice(&raw_bitcoin_block_hash)
454        .map_err(|_| HeaderError::InvalidBitcoinBlockHashDigest)
455}
456
457/// Extracts the Bitcoin block header from the given Substrate header.
458pub fn extract_bitcoin_block_header<Block: BlockT>(
459    header: &Block::Header,
460) -> Result<BitcoinHeader, HeaderError> {
461    let mut pre_digest: Option<_> = None;
462
463    for log in header.digest().logs() {
464        tracing::trace!("Checking log {:?}, looking for pre runtime digest", log);
465        match (log, pre_digest.is_some()) {
466            (DigestItem::PreRuntime(NAKAMOTO_HEADER_ENGINE_ID, _), true) => {
467                return Err(HeaderError::MultiplePreRuntimeDigests);
468            }
469            (DigestItem::PreRuntime(NAKAMOTO_HEADER_ENGINE_ID, v), false) => {
470                pre_digest.replace(v);
471            }
472            (_, _) => tracing::trace!("Ignoring digest not meant for us"),
473        }
474    }
475
476    let bitcoin_block_header = pre_digest.ok_or(HeaderError::MissingBitcoinBlockHeader)?;
477
478    BitcoinHeader::consensus_decode(&mut bitcoin_block_header.as_slice())
479        .map_err(|err| HeaderError::InvalidBitcoinBlockHeader(err.to_string()))
480}
481
482/// Converts a Substrate block to a Bitcoin block.
483pub fn convert_to_bitcoin_block<
484    Block: BlockT,
485    TransactionAdapter: BitcoinTransactionAdapter<Block>,
486>(
487    substrate_block: Block,
488) -> Result<BitcoinBlock, HeaderError> {
489    let header = extract_bitcoin_block_header::<Block>(substrate_block.header())?;
490
491    let txdata = substrate_block
492        .extrinsics()
493        .iter()
494        .map(TransactionAdapter::extrinsic_to_bitcoin_transaction)
495        .collect();
496
497    Ok(BitcoinBlock { header, txdata })
498}
499
500/// Marker height for coins that exist only in the mempool.
501pub const MEMPOOL_HEIGHT: u32 = 0x7FFFFFFF;
502
503/// UTXO coin with metadata for mempool validation.
504#[derive(Debug, Clone)]
505pub struct PoolCoin {
506    /// The transaction output.
507    pub output: TxOut,
508    /// Block height where this coin was created (MEMPOOL_HEIGHT for mempool coins).
509    pub height: u32,
510    /// Whether this coin is from a coinbase transaction.
511    pub is_coinbase: bool,
512    /// Median Time Past of the block containing this coin (for BIP68 validation).
513    pub median_time_past: i64,
514}
515
516impl From<subcoin_runtime_primitives::Coin> for PoolCoin {
517    fn from(coin: subcoin_runtime_primitives::Coin) -> Self {
518        Self {
519            output: TxOut {
520                value: Amount::from_sat(coin.amount),
521                script_pubkey: ScriptBuf::from_bytes(coin.script_pubkey),
522            },
523            height: coin.height,
524            is_coinbase: coin.is_coinbase,
525            median_time_past: 0, // Runtime coins don't have MTP; set to 0
526        }
527    }
528}
529
530/// Block metadata for BIP68 validation.
531#[derive(Debug, Clone, Copy)]
532pub struct BlockMetadata {
533    /// Block height.
534    pub height: u32,
535    /// Median Time Past of this block.
536    pub median_time_past: i64,
537}