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