clementine_core/
bitvm_client.rs

1use crate::actor::WinternitzDerivationPath;
2use crate::builder::address::taproot_builder_with_scripts;
3use crate::builder::script::{SpendableScript, WinternitzCommit};
4
5use crate::config::protocol::ProtocolParamset;
6use crate::constants::MAX_SCRIPT_REPLACEMENT_OPERATIONS;
7use crate::errors::BridgeError;
8use bitcoin::{self};
9use bitcoin::{ScriptBuf, XOnlyPublicKey};
10
11use bitvm::chunk::api::{
12    api_generate_full_tapscripts, api_generate_partial_script, Assertions, NUM_HASH, NUM_PUBS,
13    NUM_U256,
14};
15
16use bitvm::signatures::{Wots, Wots20};
17use borsh::{BorshDeserialize, BorshSerialize};
18use bridge_circuit_host::utils::{get_verifying_key, is_dev_mode};
19use eyre::Context;
20use sha2::{Digest, Sha256};
21use std::fs;
22use tokio::sync::Mutex;
23
24use once_cell::sync::OnceCell;
25use std::str::FromStr;
26use std::sync::{Arc, LazyLock};
27use std::time::Instant;
28
29/// Replacing bitvm scripts require cloning the scripts, which can be ~4GB. And this operation is done every deposit.
30/// So we ensure only 1 thread is doing this at a time to avoid OOM.
31pub static REPLACE_SCRIPTS_LOCK: LazyLock<Mutex<()>> = LazyLock::new(|| Mutex::new(()));
32
33lazy_static::lazy_static! {
34    /// Global secp context.
35    pub static ref SECP: bitcoin::secp256k1::Secp256k1<bitcoin::secp256k1::All> = bitcoin::secp256k1::Secp256k1::new();
36}
37
38lazy_static::lazy_static! {
39    /// This is an unspendable pubkey.
40    ///
41    /// See https://github.com/bitcoin/bips/blob/master/bip-0341.mediawiki#constructing-and-spending-taproot-outputs
42    ///
43    /// It is used to create a taproot address where the internal key is not spendable.
44    /// Here are the other protocols that use this key:
45    /// - Babylon:https://github.com/babylonlabs-io/btc-staking-ts/blob/v0.4.0-rc.2/src/constants/internalPubkey.ts
46    /// - Ark: https://github.com/ark-network/ark/blob/cba48925bcc836cc55f9bb482f2cd1b76d78953e/common/tree/validation.go#L47
47    /// - BitVM: https://github.com/BitVM/BitVM/blob/2dd2e0e799d2b9236dd894da3fee8c4c4893dcf1/bridge/src/scripts.rs#L16
48    /// - Best in Slot: https://github.com/bestinslot-xyz/brc20-programmable-module/blob/2113bdd73430a8c3757e537cb63124a6cb33dfab/src/evm/precompiles/get_locked_pkscript_precompile.rs#L53
49    /// - https://github.com/BlockstreamResearch/options/blob/36a77175919101393b49f1211732db762cc7dfc1/src/options_lib/src/contract.rs#L132
50    ///
51    pub static ref UNSPENDABLE_XONLY_PUBKEY: bitcoin::secp256k1::XOnlyPublicKey =
52        XOnlyPublicKey::from_str("50929b74c1a04954b78b4b6035e97a5e078a5a0f28ec96d547bfee9ace803ac0").expect("this key is valid");
53}
54
55/// Global BitVM cache wrapped in a OnceLock.
56///
57/// # Usage
58/// Use with `BITVM_CACHE.get_or_try_init(load_or_generate_bitvm_cache)` to get the cache or optionally load it.
59/// The cache will be initialized from a file, and if that fails, the fresh data will be generated.
60pub static BITVM_CACHE: OnceCell<BitvmCacheWithMetadata> = OnceCell::new();
61
62pub struct BitvmCacheWithMetadata {
63    pub bitvm_cache: BitvmCache,
64    pub sha256_bitvm_cache: [u8; 32],
65}
66
67/// Returns the BitVM cache file path from `BITVM_CACHE_PATH` env var or a default based on RISC0 dev/prod mode.
68fn get_cache_path() -> String {
69    std::env::var("BITVM_CACHE_PATH").unwrap_or_else(|_| {
70        if is_dev_mode() {
71            "bitvm_cache_dev.bin".to_string()
72        } else {
73            "bitvm_cache.bin".to_string()
74        }
75    })
76}
77
78pub fn load_or_generate_bitvm_cache() -> Result<BitvmCacheWithMetadata, BridgeError> {
79    let start = Instant::now();
80
81    let cache_path = get_cache_path();
82
83    let bitvm_cache = {
84        tracing::debug!("Attempting to load BitVM cache from file: {}", cache_path);
85
86        match BitvmCache::load_from_file(&cache_path) {
87            Ok(cache) => {
88                tracing::debug!("Loaded BitVM cache from file");
89
90                cache
91            }
92            Err(_) => {
93                tracing::info!("No BitVM cache found, generating fresh data");
94
95                let fresh_data = generate_fresh_data();
96
97                if let Err(e) = fresh_data.save_to_file(&cache_path) {
98                    tracing::error!(
99                        "Failed to save freshly generated BitVM cache to file: {}",
100                        e
101                    );
102                }
103                fresh_data
104            }
105        }
106    };
107
108    tracing::debug!("BitVM initialization took: {:?}", start.elapsed());
109
110    // calculate sha256 of disprove scripts, to be used in compatibility checks
111    let sha256_start = Instant::now();
112    let sha256_bitvm_cache = bitvm_cache.calculate_sha256()?;
113    tracing::info!(
114        "Time taken to calculate SHA256 of bitvm cache: {:?}",
115        sha256_start.elapsed()
116    );
117    tracing::info!("SHA256 of bitvm cache: {:?}", sha256_bitvm_cache);
118
119    Ok(BitvmCacheWithMetadata {
120        bitvm_cache,
121        sha256_bitvm_cache,
122    })
123}
124
125#[derive(BorshSerialize, BorshDeserialize, Debug)]
126pub struct BitvmCache {
127    pub disprove_scripts: Vec<Vec<u8>>,
128    pub replacement_places: Box<ClementineBitVMReplacementData>,
129}
130
131impl BitvmCache {
132    fn save_to_file(&self, path: &str) -> Result<(), BridgeError> {
133        let serialized = borsh::to_vec(self).map_err(|e| {
134            tracing::error!("Failed to serialize BitVM cache: {}", e);
135            BridgeError::ConfigError("Failed to serialize BitVM cache".to_string())
136        })?;
137
138        fs::write(path, serialized).map_err(|e| {
139            tracing::error!("Failed to save BitVM cache: {}", e);
140            BridgeError::ConfigError("Failed to save BitVM cache".to_string())
141        })
142    }
143
144    fn load_from_file(path: &str) -> Result<Self, BridgeError> {
145        let bytes = fs::read(path).map_err(|e| {
146            tracing::error!("Failed to read BitVM cache: {}", e);
147            BridgeError::ConfigError("No BitVM cache found".to_string())
148        })?;
149
150        tracing::info!("Loaded BitVM cache from file, read {} bytes", bytes.len());
151
152        Self::try_from_slice(&bytes).map_err(|e| {
153            tracing::error!("Failed to deserialize BitVM cache: {}", e);
154            BridgeError::ConfigError("Failed to deserialize BitVM cache".to_string())
155        })
156    }
157
158    fn calculate_sha256(&self) -> Result<[u8; 32], BridgeError> {
159        let mut hasher = Sha256::new();
160        hasher.update(
161            borsh::to_vec(&self)
162                .wrap_err("Failed to serialize BitVM cache while calculating sha256")?,
163        );
164        Ok(hasher.finalize().into())
165    }
166}
167
168fn generate_fresh_data() -> BitvmCache {
169    let vk = get_verifying_key();
170
171    let dummy_pks = ClementineBitVMPublicKeys::create_replacable();
172
173    let partial_scripts = api_generate_partial_script(&vk);
174
175    let scripts = partial_scripts
176        .iter()
177        .map(|s| s.clone().to_bytes())
178        .collect::<Vec<_>>();
179
180    for (script_idx, script) in scripts.iter().enumerate() {
181        let mut pos = 0;
182        while pos + 20 <= script.len() {
183            // Check if this window matches our pattern (255u8 in the end)
184            if script[pos + 4..pos + 20] == [255u8; 16] {
185                panic!("Dummy value found in script {script_idx}");
186            }
187            pos += 1;
188        }
189    }
190
191    let disprove_scripts = api_generate_full_tapscripts(dummy_pks.bitvm_pks, &partial_scripts);
192
193    let scripts: Vec<Vec<u8>> = disprove_scripts
194        .iter()
195        .map(|s| s.clone().to_bytes())
196        .collect();
197
198    // Build mapping of dummy keys to their positions
199    let mut replacement_places: ClementineBitVMReplacementData = Default::default();
200    // For each script
201    for (script_idx, script) in scripts.iter().enumerate() {
202        let mut pos = 0;
203        while pos + 20 <= script.len() {
204            // Check if this window matches our pattern (255u8 in the end)
205            if script[pos + 4..pos + 20] == [255u8; 16] {
206                let pk_type_idx = script[pos];
207                let pk_idx = u16::from_be_bytes([script[pos + 1], script[pos + 2]]);
208                let digit_idx = script[pos + 3];
209
210                match pk_type_idx {
211                    0 => {
212                        replacement_places.payout_tx_blockhash_pk[digit_idx as usize]
213                            .push((script_idx, pos));
214                    }
215                    1 => {
216                        replacement_places.latest_blockhash_pk[digit_idx as usize]
217                            .push((script_idx, pos));
218                    }
219                    2 => {
220                        replacement_places.challenge_sending_watchtowers_pk[digit_idx as usize]
221                            .push((script_idx, pos));
222                    }
223                    3 => {
224                        replacement_places.bitvm_pks.0[pk_idx as usize][digit_idx as usize]
225                            .push((script_idx, pos));
226                    }
227                    4 => {
228                        replacement_places.bitvm_pks.1[pk_idx as usize][digit_idx as usize]
229                            .push((script_idx, pos));
230                    }
231                    5 => {
232                        replacement_places.bitvm_pks.2[pk_idx as usize][digit_idx as usize]
233                            .push((script_idx, pos));
234                    }
235                    _ => {
236                        panic!("Invalid pk type index: {pk_type_idx}");
237                    }
238                }
239                pos += 20;
240            } else {
241                pos += 1;
242            }
243        }
244    }
245
246    BitvmCache {
247        disprove_scripts: scripts,
248        replacement_places: Box::new(replacement_places),
249    }
250}
251
252#[derive(Debug, Clone, BorshSerialize, BorshDeserialize, PartialEq, Eq)]
253pub struct ClementineBitVMPublicKeys {
254    pub combined_method_id_constant: [u8; 32],
255    pub deposit_constant: [u8; 32],
256    pub payout_tx_blockhash_pk: <Wots20 as Wots>::PublicKey,
257    pub latest_blockhash_pk: <Wots20 as Wots>::PublicKey,
258    pub challenge_sending_watchtowers_pk: <Wots20 as Wots>::PublicKey,
259    pub bitvm_pks: bitvm::chunk::api::PublicKeys,
260}
261
262#[derive(Debug, Clone, BorshSerialize, BorshDeserialize)]
263#[allow(clippy::type_complexity)]
264pub struct ClementineBitVMReplacementData {
265    pub payout_tx_blockhash_pk: [Vec<(usize, usize)>; 43],
266    pub latest_blockhash_pk: [Vec<(usize, usize)>; 43],
267    pub challenge_sending_watchtowers_pk: [Vec<(usize, usize)>; 43],
268    pub bitvm_pks: (
269        [[Vec<(usize, usize)>; 67]; NUM_PUBS],
270        [[Vec<(usize, usize)>; 67]; NUM_U256],
271        [[Vec<(usize, usize)>; 35]; NUM_HASH],
272    ),
273}
274
275impl Default for ClementineBitVMReplacementData {
276    fn default() -> Self {
277        Self {
278            payout_tx_blockhash_pk: std::array::from_fn(|_| Vec::new()),
279            latest_blockhash_pk: std::array::from_fn(|_| Vec::new()),
280            challenge_sending_watchtowers_pk: std::array::from_fn(|_| Vec::new()),
281            bitvm_pks: (
282                std::array::from_fn(|_| std::array::from_fn(|_| Vec::new())),
283                std::array::from_fn(|_| std::array::from_fn(|_| Vec::new())),
284                std::array::from_fn(|_| std::array::from_fn(|_| Vec::new())),
285            ),
286        }
287    }
288}
289
290impl ClementineBitVMPublicKeys {
291    pub fn get_replacable_value(pk_type_idx: u8, pk_idx: u16, digit_idx: u8) -> [u8; 20] {
292        let mut dummy_value = [255u8; 20];
293        dummy_value[0] = pk_type_idx;
294        dummy_value[1] = pk_idx.to_be_bytes()[0];
295        dummy_value[2] = pk_idx.to_be_bytes()[1];
296        dummy_value[3] = digit_idx;
297        dummy_value
298    }
299
300    pub fn get_replacable_wpks<const DIGIT_LEN: usize>(
301        pk_type_idx: u8,
302        pk_idx: u16,
303    ) -> [[u8; 20]; DIGIT_LEN] {
304        (0..DIGIT_LEN as u8)
305            .map(|digit_idx| Self::get_replacable_value(pk_type_idx, pk_idx, digit_idx))
306            .collect::<Vec<_>>()
307            .try_into()
308            .expect("Should be able to convert to array")
309    }
310
311    pub fn get_multiple_replacable_wpks<const DIGIT_LEN: usize, const PK_LEN: usize>(
312        pk_type_idx: u8,
313    ) -> [[[u8; 20]; DIGIT_LEN]; PK_LEN] {
314        (0..PK_LEN as u16)
315            .map(|pk_idx| Self::get_replacable_wpks(pk_type_idx, pk_idx))
316            .collect::<Vec<_>>()
317            .try_into()
318            .expect("Should be able to convert to array")
319    }
320
321    pub fn create_replacable() -> Self {
322        let combined_method_id_constant = [255u8; 32];
323        let deposit_constant = [255u8; 32];
324        let payout_tx_blockhash_pk = Self::get_replacable_wpks(0, 0);
325        let latest_blockhash_pk = Self::get_replacable_wpks(1, 0);
326        let challenge_sending_watchtowers_pk = Self::get_replacable_wpks(2, 0);
327        let bitvm_part_1 = Self::get_multiple_replacable_wpks(3);
328        let bitvm_part_2 = Self::get_multiple_replacable_wpks(4);
329        let bitvm_part_3 = Self::get_multiple_replacable_wpks(5);
330        let bitvm_pks = (bitvm_part_1, bitvm_part_2, bitvm_part_3);
331        Self {
332            combined_method_id_constant,
333            deposit_constant,
334            payout_tx_blockhash_pk,
335            latest_blockhash_pk,
336            challenge_sending_watchtowers_pk,
337            bitvm_pks,
338        }
339    }
340
341    pub fn from_flattened_vec(flattened_wpks: &[Vec<[u8; 20]>]) -> Self {
342        // These are dummy since they are coming from another source
343        let combined_method_id_constant = [255u8; 32];
344        let deposit_constant = [255u8; 32];
345
346        // Use the first element for payout_tx_blockhash_pk
347        let payout_tx_blockhash_pk = Self::vec_to_array::<43>(&flattened_wpks[0]);
348
349        // Use the second element for latest_blockhash_pk
350        let latest_blockhash_pk = Self::vec_to_array::<43>(&flattened_wpks[1]);
351
352        // Use the third element for challenge_sending_watchtowers_pk
353        let challenge_sending_watchtowers_pk = Self::vec_to_array::<43>(&flattened_wpks[2]);
354
355        // Create the nested arrays for bitvm_pks, starting from the fourth element
356        let bitvm_pks_1 =
357            Self::vec_slice_to_nested_array::<67, NUM_PUBS>(&flattened_wpks[3..3 + NUM_PUBS]);
358
359        let bitvm_pks_2 = Self::vec_slice_to_nested_array::<67, NUM_U256>(
360            &flattened_wpks[3 + NUM_PUBS..3 + NUM_PUBS + NUM_U256],
361        );
362
363        let bitvm_pks_3 = Self::vec_slice_to_nested_array::<35, NUM_HASH>(
364            &flattened_wpks[3 + NUM_PUBS + NUM_U256..3 + NUM_PUBS + NUM_U256 + NUM_HASH],
365        );
366
367        Self {
368            combined_method_id_constant,
369            deposit_constant,
370            payout_tx_blockhash_pk,
371            latest_blockhash_pk,
372            challenge_sending_watchtowers_pk,
373            bitvm_pks: (bitvm_pks_1, bitvm_pks_2, bitvm_pks_3),
374        }
375    }
376
377    pub fn to_flattened_vec(&self) -> Vec<Vec<[u8; 20]>> {
378        let mut flattened_wpks = Vec::new();
379
380        // Convert each array to Vec<[u8; 20]>
381        flattened_wpks.push(self.payout_tx_blockhash_pk.to_vec());
382        flattened_wpks.push(self.latest_blockhash_pk.to_vec());
383        flattened_wpks.push(self.challenge_sending_watchtowers_pk.to_vec());
384
385        // Convert and add each nested array from bitvm_pks
386        for arr in &self.bitvm_pks.0 {
387            flattened_wpks.push(arr.to_vec());
388        }
389
390        for arr in &self.bitvm_pks.1 {
391            flattened_wpks.push(arr.to_vec());
392        }
393
394        for arr in &self.bitvm_pks.2 {
395            flattened_wpks.push(arr.to_vec());
396        }
397
398        flattened_wpks
399    }
400
401    // Helper to convert Vec<[u8; 20]> to [[u8; 20]; N]
402    pub fn vec_to_array<const N: usize>(vec: &[[u8; 20]]) -> [[u8; 20]; N] {
403        let mut result = [[255u8; 20]; N];
404        for (i, item) in vec.iter().enumerate() {
405            if i < N {
406                result[i] = *item;
407            }
408        }
409        result
410    }
411
412    // Helper to convert &[Vec<[u8; 20]>] to [[[u8; 20]; INNER_LEN]; OUTER_LEN]
413    pub fn vec_slice_to_nested_array<const INNER_LEN: usize, const OUTER_LEN: usize>(
414        slice: &[Vec<[u8; 20]>],
415    ) -> [[[u8; 20]; INNER_LEN]; OUTER_LEN] {
416        let mut result = [[[255u8; 20]; INNER_LEN]; OUTER_LEN];
417        for (i, vec) in slice.iter().enumerate() {
418            if i < OUTER_LEN {
419                result[i] = Self::vec_to_array::<INNER_LEN>(vec);
420            }
421        }
422        result
423    }
424
425    pub const fn number_of_assert_txs() -> usize {
426        36
427    }
428
429    pub const fn number_of_flattened_wpks() -> usize {
430        381
431    }
432
433    pub fn get_assert_scripts(
434        &self,
435        xonly_public_key: XOnlyPublicKey,
436    ) -> Vec<std::sync::Arc<dyn SpendableScript>> {
437        let mut scripts = Vec::new();
438        let first_script: Arc<dyn SpendableScript> = Arc::new(WinternitzCommit::new(
439            vec![
440                (self.challenge_sending_watchtowers_pk.to_vec(), 40),
441                (self.bitvm_pks.0[0].to_vec(), 64),
442                (self.bitvm_pks.1[NUM_U256 - 4].to_vec(), 64),
443                (self.bitvm_pks.1[NUM_U256 - 3].to_vec(), 64),
444                (self.bitvm_pks.1[NUM_U256 - 2].to_vec(), 64),
445                (self.bitvm_pks.1[NUM_U256 - 1].to_vec(), 64),
446            ],
447            xonly_public_key,
448            4,
449        ));
450        scripts.push(first_script);
451        // iterate NUM_U256 5 by 5
452        let k1 = 5;
453        for i in (0..NUM_U256 - 4).step_by(k1) {
454            let last_idx = std::cmp::min(i + k1, NUM_U256);
455            let script: Arc<dyn SpendableScript> = Arc::new(WinternitzCommit::new(
456                self.bitvm_pks.1[i..last_idx]
457                    .iter()
458                    .map(|x| (x.to_vec(), 64))
459                    .collect::<Vec<_>>(),
460                xonly_public_key,
461                4,
462            ));
463            scripts.push(script);
464        }
465        let k2 = 11;
466        // iterate NUM_HASH 11 by 11
467        for i in (0..NUM_HASH).step_by(k2) {
468            let last_idx = std::cmp::min(i + k2, NUM_HASH);
469            let script: Arc<dyn SpendableScript> = Arc::new(WinternitzCommit::new(
470                self.bitvm_pks.2[i..last_idx]
471                    .iter()
472                    .map(|x| (x.to_vec(), 32))
473                    .collect::<Vec<_>>(),
474                xonly_public_key,
475                4,
476            ));
477            scripts.push(script);
478        }
479        scripts
480    }
481
482    pub fn get_assert_commit_data(
483        asserts: Assertions,
484        challenge_sending_watchtowers: &[u8; 20],
485    ) -> Vec<Vec<Vec<u8>>> {
486        let mut commit_data = Vec::new();
487        tracing::info!(
488            "Getting assert commit data, challenge_sending_watchtowers: {:?}",
489            challenge_sending_watchtowers
490        );
491        commit_data.push(vec![
492            challenge_sending_watchtowers.to_vec(),
493            asserts.0[0].to_vec(),
494            asserts.1[NUM_U256 - 4].to_vec(),
495            asserts.1[NUM_U256 - 3].to_vec(),
496            asserts.1[NUM_U256 - 2].to_vec(),
497            asserts.1[NUM_U256 - 1].to_vec(),
498        ]);
499        let k1 = 5;
500        for i in (0..NUM_U256 - 4).step_by(k1) {
501            let last_idx = std::cmp::min(i + k1, NUM_U256);
502            commit_data.push(
503                asserts.1[i..last_idx]
504                    .iter()
505                    .map(|x| x.to_vec())
506                    .collect::<Vec<_>>(),
507            );
508        }
509        let k2 = 11;
510        for i in (0..NUM_HASH).step_by(k2) {
511            let last_idx = std::cmp::min(i + k2, NUM_HASH);
512            commit_data.push(
513                asserts.2[i..last_idx]
514                    .iter()
515                    .map(|x| x.to_vec())
516                    .collect::<Vec<_>>(),
517            );
518        }
519        commit_data
520    }
521
522    pub fn get_latest_blockhash_derivation(
523        deposit_outpoint: bitcoin::OutPoint,
524        paramset: &'static ProtocolParamset,
525    ) -> WinternitzDerivationPath {
526        WinternitzDerivationPath::BitvmAssert(20 * 2, 1, 0, deposit_outpoint, paramset)
527    }
528
529    pub fn get_payout_tx_blockhash_derivation(
530        deposit_outpoint: bitcoin::OutPoint,
531        paramset: &'static ProtocolParamset,
532    ) -> WinternitzDerivationPath {
533        WinternitzDerivationPath::BitvmAssert(20 * 2, 0, 0, deposit_outpoint, paramset)
534    }
535
536    pub fn get_challenge_sending_watchtowers_derivation(
537        deposit_outpoint: bitcoin::OutPoint,
538        paramset: &'static ProtocolParamset,
539    ) -> WinternitzDerivationPath {
540        WinternitzDerivationPath::BitvmAssert(20 * 2, 2, 0, deposit_outpoint, paramset)
541    }
542
543    pub fn mini_assert_derivations_0(
544        deposit_outpoint: bitcoin::OutPoint,
545        paramset: &'static ProtocolParamset,
546    ) -> Vec<WinternitzDerivationPath> {
547        vec![
548            Self::get_challenge_sending_watchtowers_derivation(deposit_outpoint, paramset), // Will not go into BitVM disprove scripts
549            WinternitzDerivationPath::BitvmAssert(32 * 2, 3, 0, deposit_outpoint, paramset), // This is the Groth16 public output
550            // This is the extra 11th NUM_U256, after chunking by 5 for the first 2 asserts
551            WinternitzDerivationPath::BitvmAssert(
552                32 * 2,
553                4,
554                (NUM_U256 - 4) as u32,
555                deposit_outpoint,
556                paramset,
557            ),
558            // This is the extra 12th NUM_U256, after chunking by 5 for the first 2 asserts
559            WinternitzDerivationPath::BitvmAssert(
560                32 * 2,
561                4,
562                (NUM_U256 - 3) as u32,
563                deposit_outpoint,
564                paramset,
565            ),
566            // This is the extra 13th NUM_U256, after chunking by 6 for the first 2 asserts
567            WinternitzDerivationPath::BitvmAssert(
568                32 * 2,
569                4,
570                (NUM_U256 - 2) as u32,
571                deposit_outpoint,
572                paramset,
573            ),
574            // This is the extra 14th NUM_U256, after chunking by 6 for the first 2 asserts
575            WinternitzDerivationPath::BitvmAssert(
576                32 * 2,
577                4,
578                (NUM_U256 - 1) as u32,
579                deposit_outpoint,
580                paramset,
581            ),
582        ]
583    }
584
585    pub fn get_assert_derivations(
586        mini_assert_idx: usize,
587        deposit_outpoint: bitcoin::OutPoint,
588        paramset: &'static ProtocolParamset,
589    ) -> Vec<WinternitzDerivationPath> {
590        if mini_assert_idx == 0 {
591            Self::mini_assert_derivations_0(deposit_outpoint, paramset)
592        } else if (1..=2).contains(&mini_assert_idx) {
593            // for 1, we will have 5 derivations index starting from 0 to 4
594            // for 2, we will have 5 derivations index starting from 5 to 9
595            let derivations: u32 = (mini_assert_idx as u32 - 1) * 5;
596
597            let mut derivations_vec = vec![];
598            for i in 0..5 {
599                if derivations + i < NUM_U256 as u32 - 4 {
600                    derivations_vec.push(WinternitzDerivationPath::BitvmAssert(
601                        32 * 2,
602                        4,
603                        derivations + i,
604                        deposit_outpoint,
605                        paramset,
606                    ));
607                }
608            }
609            derivations_vec
610        } else {
611            let derivations: u32 = (mini_assert_idx as u32 - 3) * 11;
612            let mut derivations_vec = vec![];
613            for i in 0..11 {
614                if derivations + i < NUM_HASH as u32 {
615                    derivations_vec.push(WinternitzDerivationPath::BitvmAssert(
616                        16 * 2,
617                        5,
618                        derivations + i,
619                        deposit_outpoint,
620                        paramset,
621                    ));
622                }
623            }
624            derivations_vec
625        }
626    }
627
628    pub fn get_assert_taproot_leaf_hashes(
629        &self,
630        xonly_public_key: XOnlyPublicKey,
631    ) -> Vec<bitcoin::TapNodeHash> {
632        let assert_scripts = self.get_assert_scripts(xonly_public_key);
633        assert_scripts
634            .into_iter()
635            .map(|script| {
636                let taproot_builder = taproot_builder_with_scripts(&[script.to_script_buf()]);
637                taproot_builder
638                    .try_into_taptree()
639                    .expect("taproot builder always builds a full taptree")
640                    .root_hash()
641            })
642            .collect::<Vec<_>>()
643    }
644
645    pub fn get_g16_verifier_disprove_scripts(&self) -> Result<Vec<ScriptBuf>, BridgeError> {
646        if cfg!(debug_assertions) {
647            Ok(vec![ScriptBuf::from_bytes(vec![0x51])]) // OP_TRUE
648        } else {
649            Ok(replace_disprove_scripts(self)?)
650        }
651    }
652}
653
654pub fn replace_disprove_scripts(
655    pks: &ClementineBitVMPublicKeys,
656) -> Result<Vec<ScriptBuf>, BridgeError> {
657    let start = Instant::now();
658    tracing::info!("Starting script replacement");
659
660    let cache = &BITVM_CACHE
661        .get_or_try_init(load_or_generate_bitvm_cache)?
662        .bitvm_cache;
663    let replacement_places = &cache.replacement_places;
664
665    // Calculate estimated operations to prevent DoS attacks
666    let estimated_operations = calculate_replacement_operations(replacement_places);
667    tracing::info!(
668        "Estimated operations for script replacement: {}",
669        estimated_operations
670    );
671    if estimated_operations > MAX_SCRIPT_REPLACEMENT_OPERATIONS {
672        tracing::warn!(
673            "Rejecting script replacement: estimated {} operations exceeds limit of {}",
674            estimated_operations,
675            MAX_SCRIPT_REPLACEMENT_OPERATIONS
676        );
677        return Err(BridgeError::BitvmReplacementResourceExhaustion(
678            estimated_operations,
679        ));
680    }
681
682    tracing::info!("Estimated operations: {}", estimated_operations);
683
684    let mut result: Vec<Vec<u8>> = cache.disprove_scripts.clone();
685
686    for (digit, places) in replacement_places.payout_tx_blockhash_pk.iter().enumerate() {
687        for (script_idx, pos) in places.iter() {
688            result[*script_idx][*pos..*pos + 20]
689                .copy_from_slice(&pks.payout_tx_blockhash_pk[digit]);
690        }
691    }
692
693    for (digit, places) in replacement_places.latest_blockhash_pk.iter().enumerate() {
694        for (script_idx, pos) in places.iter() {
695            result[*script_idx][*pos..*pos + 20].copy_from_slice(&pks.latest_blockhash_pk[digit]);
696        }
697    }
698
699    for (digit, places) in replacement_places
700        .challenge_sending_watchtowers_pk
701        .iter()
702        .enumerate()
703    {
704        for (script_idx, pos) in places.iter() {
705            result[*script_idx][*pos..*pos + 20]
706                .copy_from_slice(&pks.challenge_sending_watchtowers_pk[digit]);
707        }
708    }
709
710    for (digit, places) in replacement_places.bitvm_pks.0.iter().enumerate() {
711        for (pk_idx, places) in places.iter().enumerate() {
712            for (script_idx, pos) in places.iter() {
713                result[*script_idx][*pos..*pos + 20]
714                    .copy_from_slice(&pks.bitvm_pks.0[digit][pk_idx]);
715            }
716        }
717    }
718
719    for (digit, places) in replacement_places.bitvm_pks.1.iter().enumerate() {
720        for (pk_idx, places) in places.iter().enumerate() {
721            for (script_idx, pos) in places.iter() {
722                result[*script_idx][*pos..*pos + 20]
723                    .copy_from_slice(&pks.bitvm_pks.1[digit][pk_idx]);
724            }
725        }
726    }
727
728    for (digit, places) in replacement_places.bitvm_pks.2.iter().enumerate() {
729        for (pk_idx, places) in places.iter().enumerate() {
730            for (script_idx, pos) in places.iter() {
731                result[*script_idx][*pos..*pos + 20]
732                    .copy_from_slice(&pks.bitvm_pks.2[digit][pk_idx]);
733            }
734        }
735    }
736
737    let result: Vec<ScriptBuf> = result.into_iter().map(ScriptBuf::from_bytes).collect();
738
739    let elapsed = start.elapsed();
740    tracing::info!(
741        "Script replacement completed in {:?} with {} operations",
742        elapsed,
743        estimated_operations
744    );
745
746    Ok(result)
747}
748
749/// Helper function to calculate the total number of replacement operations
750fn calculate_replacement_operations(replacement_places: &ClementineBitVMReplacementData) -> usize {
751    let mut total_operations = 0;
752
753    // Count payout_tx_blockhash_pk operations
754    for places in &replacement_places.payout_tx_blockhash_pk {
755        total_operations += places.len();
756    }
757
758    // Count latest_blockhash_pk operations
759    for places in &replacement_places.latest_blockhash_pk {
760        total_operations += places.len();
761    }
762
763    // Count challenge_sending_watchtowers_pk operations
764    for places in &replacement_places.challenge_sending_watchtowers_pk {
765        total_operations += places.len();
766    }
767
768    // Count bitvm_pks operations (this is typically the largest contributor)
769    for digit_places in &replacement_places.bitvm_pks.0 {
770        for places in digit_places {
771            total_operations += places.len();
772        }
773    }
774
775    for digit_places in &replacement_places.bitvm_pks.1 {
776        for places in digit_places {
777            total_operations += places.len();
778        }
779    }
780
781    for digit_places in &replacement_places.bitvm_pks.2 {
782        for places in digit_places {
783            total_operations += places.len();
784        }
785    }
786
787    total_operations
788}
789
790#[cfg(test)]
791mod tests {
792    use bitcoin::secp256k1::rand::thread_rng;
793    use bitcoin::{hashes::Hash, Txid};
794
795    use super::*;
796    use crate::{actor::Actor, test::common::create_test_config_with_thread_name};
797
798    #[test]
799    fn test_to_flattened_vec() {
800        let bitvm_pks = ClementineBitVMPublicKeys::create_replacable();
801        let flattened_vec = bitvm_pks.to_flattened_vec();
802        let from_vec_to_array = ClementineBitVMPublicKeys::from_flattened_vec(&flattened_vec);
803        assert_eq!(bitvm_pks, from_vec_to_array);
804    }
805
806    #[tokio::test]
807    async fn test_vec_to_array_with_actor_values() {
808        let config = create_test_config_with_thread_name().await;
809
810        let sk = bitcoin::secp256k1::SecretKey::new(&mut thread_rng());
811        let signer = Actor::new(sk, config.protocol_paramset().network);
812        let deposit_outpoint = bitcoin::OutPoint {
813            txid: Txid::all_zeros(),
814            vout: 0,
815        };
816        let bitvm_pks = signer
817            .generate_bitvm_pks_for_deposit(deposit_outpoint, config.protocol_paramset())
818            .unwrap();
819
820        let flattened_vec = bitvm_pks.to_flattened_vec();
821        let from_vec_to_array = ClementineBitVMPublicKeys::from_flattened_vec(&flattened_vec);
822        assert_eq!(bitvm_pks, from_vec_to_array);
823    }
824
825    #[tokio::test]
826    #[ignore = "This test is too slow to run on every commit"]
827    async fn test_generate_fresh_data() {
828        let bitvm_cache = generate_fresh_data();
829        bitvm_cache
830            .save_to_file("bitvm_cache_new.bin")
831            .expect("Failed to save BitVM cache");
832    }
833
834    #[tokio::test]
835    async fn test_bitvm_cache_matches_generated_data() {
836        if cfg!(debug_assertions) {
837            // only run this test in release mode
838            return;
839        }
840        let bitvm_cache = BitvmCache::load_from_file(&get_cache_path()).unwrap();
841        let sha256_bitvm_cache = bitvm_cache.calculate_sha256().unwrap();
842        let generated_data = generate_fresh_data();
843        let fresh_data_sha256 = generated_data.calculate_sha256().unwrap();
844        assert_eq!(sha256_bitvm_cache, fresh_data_sha256);
845    }
846}