subcoin_primitives/
lib.rs

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