pallet_bitcoin/
lib.rs

1//! # Bitcoin Pallet
2//!
3//! This pallet is designed to be minimalist, containing only one storage item for maintaining
4//! the state of the UTXO (Unspent Transaction Output) set by processing the inputs and outputs
5//! of each Bitcoin transaction wrapped in [`Call::transact`]. There is no verification logic
6//! within the pallet, all validation work should be performed outside the runtime. This approach
7//! simplifies off-runtime execution, allowing for easier syncing performance optimization off
8//! chain.
9
10// Ensure we're `no_std` when compiling for Wasm.
11#![cfg_attr(not(feature = "std"), no_std)]
12
13#[cfg(test)]
14mod mock;
15#[cfg(test)]
16mod tests;
17pub mod types;
18
19use self::types::{OutPoint, Transaction, Txid};
20use frame_support::dispatch::DispatchResult;
21use frame_support::weights::Weight;
22use sp_runtime::SaturatedConversion;
23use sp_runtime::traits::BlockNumberProvider;
24use sp_std::prelude::*;
25use sp_std::vec::Vec;
26use subcoin_runtime_primitives::Coin;
27
28// Re-export pallet items so that they can be accessed from the crate namespace.
29pub use pallet::*;
30
31/// Transaction output index.
32pub type OutputIndex = u32;
33
34/// Weight functions needed for `pallet_bitcoin`.
35pub trait WeightInfo {
36    /// Calculates the weight of [`Call::transact`].
37    fn transact(btc_tx: &Transaction) -> Weight;
38}
39
40impl WeightInfo for () {
41    fn transact(_: &Transaction) -> Weight {
42        Weight::zero()
43    }
44}
45
46/// A struct that implements the [`WeightInfo`] trait for Bitcoin transactions.
47pub struct BitcoinTransactionWeight;
48
49impl WeightInfo for BitcoinTransactionWeight {
50    fn transact(btc_tx: &Transaction) -> Weight {
51        let btc_weight = bitcoin::Transaction::from(btc_tx.clone()).weight().to_wu();
52        Weight::from_parts(btc_weight, 0u64)
53    }
54}
55
56#[frame_support::pallet]
57pub mod pallet {
58    use super::*;
59    use frame_support::pallet_prelude::*;
60    use frame_system::pallet_prelude::*;
61
62    #[pallet::config]
63    pub trait Config: frame_system::Config {
64        type WeightInfo: WeightInfo;
65    }
66
67    #[pallet::pallet]
68    pub struct Pallet<T>(_);
69
70    #[pallet::call(weight(<T as Config>::WeightInfo))]
71    impl<T: Config> Pallet<T> {
72        /// An unsigned extrinsic for embedding a Bitcoin transaction into the Substrate block.
73        #[pallet::call_index(0)]
74        #[pallet::weight((T::WeightInfo::transact(btc_tx), DispatchClass::Normal, Pays::No))]
75        pub fn transact(origin: OriginFor<T>, btc_tx: Transaction) -> DispatchResult {
76            ensure_none(origin)?;
77
78            Self::process_bitcoin_transaction(btc_tx.into());
79
80            Ok(())
81        }
82    }
83
84    #[pallet::event]
85    pub enum Event<T: Config> {}
86
87    /// UTXO set.
88    ///
89    /// (Txid, OutputIndex(vout), Coin)
90    ///
91    /// Note: There is a special case in Bitcoin that the outputs in the genesis block are excluded from the UTXO set.
92    #[pallet::storage]
93    pub type Coins<T> =
94        StorageDoubleMap<_, Identity, Txid, Identity, OutputIndex, Coin, OptionQuery>;
95
96    /// Size of the entire UTXO set.
97    #[pallet::storage]
98    pub type CoinsCount<T> = StorageValue<_, u64, ValueQuery>;
99}
100
101/// Returns the storage key for the referenced output.
102pub fn coin_storage_key<T: Config>(bitcoin_txid: bitcoin::Txid, vout: OutputIndex) -> Vec<u8> {
103    use frame_support::storage::generator::StorageDoubleMap;
104
105    let txid = Txid::from_bitcoin_txid(bitcoin_txid);
106    Coins::<T>::storage_double_map_final_key(txid, vout)
107}
108
109/// Returns the final storage prefix for the storage item `Coins`.
110pub fn coin_storage_prefix<T: Config>() -> [u8; 32] {
111    use frame_support::StoragePrefixedMap;
112
113    Coins::<T>::final_prefix()
114}
115
116impl<T: Config> Pallet<T> {
117    pub fn coins_count() -> u64 {
118        CoinsCount::<T>::get()
119    }
120
121    fn process_bitcoin_transaction(tx: bitcoin::Transaction) {
122        let txid = tx.compute_txid();
123        let is_coinbase = tx.is_coinbase();
124        let height = frame_system::Pallet::<T>::current_block_number();
125
126        // Check if transaction falls under the BIP30 exception heights
127        const BIP30_EXCEPTION_HEIGHTS: &[u32] = &[91722, 91812];
128        if is_coinbase && BIP30_EXCEPTION_HEIGHTS.contains(&height.saturated_into()) {
129            // Skip adding the outputs to UTXO set if it is a BIP30 exception.
130            return;
131        }
132
133        // Collect all new coins to be added to the UTXO set, skipping OP_RETURN outputs.
134        let new_coins: Vec<_> = tx
135            .output
136            .into_iter()
137            .enumerate()
138            .filter_map(|(index, txout)| {
139                let out_point = bitcoin::OutPoint {
140                    txid,
141                    vout: index as u32,
142                };
143
144                // OP_RETURN outputs are not added to the UTXO set.
145                //
146                // TODO: handle OP_RETURN data properly as they are super valuable.
147                if txout.script_pubkey.is_op_return() {
148                    None
149                } else {
150                    let coin = Coin {
151                        is_coinbase,
152                        amount: txout.value.to_sat(),
153                        script_pubkey: txout.script_pubkey.into_bytes(),
154                        height: height.saturated_into(),
155                    };
156                    Some((out_point, coin))
157                }
158            })
159            .collect();
160
161        let num_created = new_coins.len();
162
163        if is_coinbase {
164            // Insert new UTXOs for coinbase transaction.
165            for (out_point, coin) in new_coins {
166                let OutPoint { txid, output_index } = OutPoint::from(out_point);
167                Coins::<T>::insert(txid, output_index, coin);
168            }
169            CoinsCount::<T>::mutate(|v| {
170                *v += num_created as u64;
171            });
172            return;
173        }
174
175        let num_consumed = tx.input.len();
176
177        // Process the inputs to remove consumed UTXOs.
178        for input in tx.input {
179            let previous_output = input.previous_output;
180            let OutPoint { txid, output_index } = OutPoint::from(previous_output);
181            if let Some(_spent) = Coins::<T>::take(txid, output_index) {
182            } else {
183                panic!("Corruputed state, UTXO {previous_output:?} not found");
184            }
185        }
186
187        // Insert new UTXOs for non-coinbase transaction.
188        for (out_point, coin) in new_coins {
189            let OutPoint { txid, output_index } = OutPoint::from(out_point);
190            Coins::<T>::insert(txid, output_index, coin);
191        }
192
193        CoinsCount::<T>::mutate(|v| {
194            *v = *v + num_created as u64 - num_consumed as u64;
195        });
196    }
197}