clementine_core/
compatibility.rs

1//! # Compatibility Module
2//! This module contains the logic for checking compatibility between actors in the system.
3
4use eyre::Context;
5use semver::VersionReq;
6
7use crate::aggregator::Aggregator;
8use crate::bitvm_client::{load_or_generate_bitvm_cache, BITVM_CACHE};
9use crate::citrea::CitreaClientT;
10use crate::config::protocol::{ProtocolParamset, ProtocolParamsetExt};
11use crate::config::BridgeConfig;
12use crate::deposit::SecurityCouncil;
13use crate::operator::Operator;
14use crate::rpc::clementine::CompatibilityParamsRpc;
15use crate::verifier::Verifier;
16use clementine_errors::BridgeError;
17
18/// Parameters related to protocol configuration that can affect contract transactions, Citrea syncing, and version compatibility. This must not include sensitive information.
19#[derive(Clone, Debug)]
20pub struct CompatibilityParams {
21    pub protocol_paramset: ProtocolParamset,
22    pub security_council: SecurityCouncil,
23    pub citrea_chain_id: u32,
24    pub clementine_version: String,
25    pub bridge_circuit_constant: [u8; 32],
26    pub sha256_bitvm_cache: [u8; 32],
27}
28
29impl CompatibilityParams {
30    /// Returns an error with reason if not compatible, otherwise returns Ok(())
31    /// For Protocol paramset, security council and citrea chain ID, we only check if they are different.
32    /// For Clementine version, we allow different patch versions, but not different major or minor versions.
33    pub fn is_compatible(&self, other: &CompatibilityParams) -> Result<(), BridgeError> {
34        let mut reasons = Vec::new();
35        if self.protocol_paramset != other.protocol_paramset {
36            reasons.push(format!(
37                "Protocol paramset mismatch: self={:?}, other={:?}",
38                self.protocol_paramset, other.protocol_paramset
39            ));
40        }
41        if self.security_council != other.security_council {
42            reasons.push(format!(
43                "Security council mismatch: self={:?}, other={:?}",
44                self.security_council, other.security_council
45            ));
46        }
47        if self.citrea_chain_id != other.citrea_chain_id {
48            reasons.push(format!(
49                "Citrea chain ID mismatch: self={}, other={}",
50                self.citrea_chain_id, other.citrea_chain_id
51            ));
52        }
53        if self.bridge_circuit_constant != other.bridge_circuit_constant {
54            reasons.push(format!(
55                "Bridge circuit constant mismatch: self={:?}, other={:?}",
56                hex::encode(self.bridge_circuit_constant),
57                hex::encode(other.bridge_circuit_constant)
58            ));
59        }
60        if self.sha256_bitvm_cache != other.sha256_bitvm_cache {
61            reasons.push(format!(
62                "BitVM cache SHA256 mismatch: self={:?}, other={:?}",
63                hex::encode(self.sha256_bitvm_cache),
64                hex::encode(other.sha256_bitvm_cache)
65            ));
66        }
67        if std::env::var("DISABLE_VERSION_CHECK")
68            .ok()
69            .map(|x| x.to_lowercase())
70            .filter(|x| x == "1" || x == "true" || x == "yes")
71            .is_none()
72        {
73            let own_version =
74                semver::Version::parse(&self.clementine_version).wrap_err(format!(
75                    "Failed to parse own Clementine version {}",
76                    self.clementine_version
77                ))?;
78            let other_version =
79                semver::Version::parse(&other.clementine_version).wrap_err(format!(
80                    "Failed to parse other Clementine version {}",
81                    other.clementine_version
82                ))?;
83            let min_version = std::cmp::min(&own_version, &other_version);
84            let max_version = std::cmp::max(&own_version, &other_version);
85            let version_req =
86            VersionReq::parse(&format!("^{min_version}")).wrap_err(format!(
87                "Failed to parse version requirement for Clementine version mismatch: self={own_version:?}, other={other_version:?}",
88            ))?;
89            if !version_req.matches(max_version) {
90                reasons.push(format!(
91                    "Clementine version mismatch: self={own_version:?}, other={other_version:?}",
92                ));
93            }
94        } else {
95            tracing::warn!("Version check is disabled");
96        }
97        if reasons.is_empty() {
98            Ok(())
99        } else {
100            Err(BridgeError::ClementineNotCompatible(reasons.join(", ")))
101        }
102    }
103}
104
105impl TryFrom<CompatibilityParams> for CompatibilityParamsRpc {
106    type Error = eyre::Report;
107
108    fn try_from(params: CompatibilityParams) -> Result<Self, Self::Error> {
109        Ok(CompatibilityParamsRpc {
110            protocol_paramset: serde_json::to_string(&params.protocol_paramset)
111                .wrap_err("Failed to serialize protocol paramset")?,
112            security_council: params.security_council.to_string(),
113            citrea_chain_id: params.citrea_chain_id,
114            clementine_version: params.clementine_version,
115            bridge_circuit_constant: params.bridge_circuit_constant.to_vec(),
116            sha256_bitvm_cache: params.sha256_bitvm_cache.to_vec(),
117        })
118    }
119}
120
121impl TryFrom<CompatibilityParamsRpc> for CompatibilityParams {
122    type Error = eyre::Report;
123
124    fn try_from(params: CompatibilityParamsRpc) -> Result<Self, Self::Error> {
125        Ok(CompatibilityParams {
126            protocol_paramset: serde_json::from_str(&params.protocol_paramset)
127                .wrap_err("Failed to deserialize protocol paramset")?,
128            security_council: params
129                .security_council
130                .parse()
131                .wrap_err("Failed to deserialize security council")?,
132            citrea_chain_id: params.citrea_chain_id,
133            clementine_version: params.clementine_version,
134            bridge_circuit_constant: params.bridge_circuit_constant.try_into().map_err(|_| {
135                eyre::eyre!("Failed to convert bridge circuit constant to [u8; 32]")
136            })?,
137            sha256_bitvm_cache: params
138                .sha256_bitvm_cache
139                .try_into()
140                .map_err(|_| eyre::eyre!("Failed to convert sha256 bitvm cache to [u8; 32]"))?,
141        })
142    }
143}
144
145pub trait ActorWithConfig {
146    fn get_config(&self) -> &BridgeConfig;
147
148    fn get_compatibility_params(&self) -> Result<CompatibilityParams, BridgeError> {
149        let config = self.get_config();
150        Ok(CompatibilityParams {
151            protocol_paramset: config.protocol_paramset.clone(),
152            security_council: config.security_council.clone(),
153            citrea_chain_id: config.citrea_chain_id,
154            clementine_version: env!("CARGO_PKG_VERSION").to_string(),
155            bridge_circuit_constant: *config.protocol_paramset.bridge_circuit_constant()?,
156            sha256_bitvm_cache: BITVM_CACHE
157                .get_or_try_init(load_or_generate_bitvm_cache)?
158                .sha256_bitvm_cache,
159        })
160    }
161
162    /// Returns an error with reason if not compatible, otherwise returns Ok(())
163    fn is_compatible(&self, others: Vec<(String, CompatibilityParams)>) -> Result<(), BridgeError> {
164        let own_params = self.get_compatibility_params()?;
165        let mut reasons = Vec::new();
166        for (id, params) in others {
167            if let Err(e) = own_params.is_compatible(&params) {
168                reasons.push(format!("{id}: {e}"));
169            }
170        }
171        if reasons.is_empty() {
172            Ok(())
173        } else {
174            Err(BridgeError::ClementineNotCompatible(reasons.join(", ")))
175        }
176    }
177}
178
179impl<C> ActorWithConfig for Operator<C>
180where
181    C: CitreaClientT,
182{
183    fn get_config(&self) -> &BridgeConfig {
184        &self.config
185    }
186}
187
188impl<C> ActorWithConfig for Verifier<C>
189where
190    C: CitreaClientT,
191{
192    fn get_config(&self) -> &BridgeConfig {
193        &self.config
194    }
195}
196
197impl ActorWithConfig for Aggregator {
198    fn get_config(&self) -> &BridgeConfig {
199        &self.config
200    }
201}
202
203#[cfg(test)]
204mod tests {
205    use super::*;
206    use crate::config::protocol::ProtocolParamsetExt;
207    use crate::{
208        config::protocol::{REGTEST_PARAMSET, TESTNET4_TEST_PARAMSET},
209        rpc::clementine::{entity_data_with_id::DataResult, Empty},
210        test::common::{
211            citrea::MockCitreaClient, create_actors, create_regtest_rpc,
212            create_test_config_with_thread_name,
213        },
214    };
215    use bitcoin::XOnlyPublicKey;
216    use std::str::FromStr;
217
218    #[allow(dead_code)]
219    struct MockActorWithConfig {
220        config: BridgeConfig,
221    }
222
223    impl ActorWithConfig for MockActorWithConfig {
224        fn get_config(&self) -> &BridgeConfig {
225            &self.config
226        }
227    }
228
229    #[test]
230    fn test_mock_actor_get_compatibility_params() {
231        let config = BridgeConfig::default();
232        let actor = MockActorWithConfig { config };
233
234        let params = actor.get_compatibility_params().unwrap();
235
236        assert_eq!(
237            params.protocol_paramset,
238            actor.config.protocol_paramset.clone()
239        );
240        assert_eq!(params.security_council, actor.config.security_council);
241        assert_eq!(params.citrea_chain_id, actor.config.citrea_chain_id);
242        assert_eq!(
243            params.bridge_circuit_constant,
244            *actor
245                .config
246                .protocol_paramset
247                .bridge_circuit_constant()
248                .unwrap()
249        );
250        assert_eq!(
251            params.sha256_bitvm_cache,
252            BITVM_CACHE
253                .get_or_try_init(load_or_generate_bitvm_cache)
254                .unwrap()
255                .sha256_bitvm_cache
256        );
257        assert_eq!(
258            params.clementine_version,
259            env!("CARGO_PKG_VERSION").to_string()
260        );
261    }
262
263    #[test]
264    fn test_mock_actor_is_compatible_success() {
265        let config = BridgeConfig::default();
266        let actor = MockActorWithConfig { config };
267
268        let own = actor.get_compatibility_params().unwrap();
269        let others = vec![
270            ("aggregator".to_string(), own.clone()),
271            ("verifier".to_string(), own),
272        ];
273        assert!(actor.is_compatible(others).is_ok());
274    }
275
276    #[test]
277    fn test_mock_actor_is_compatible_failure() {
278        let config = BridgeConfig::default();
279        let actor = MockActorWithConfig { config };
280
281        let mut other = actor.get_compatibility_params().unwrap();
282        // introduce mismatches
283        other.citrea_chain_id += 1;
284        other.security_council = create_test_security_council_different();
285
286        let result = actor.is_compatible(vec![("verifier-1".to_string(), other)]);
287        assert!(result.is_err());
288        let msg = result.unwrap_err().to_string();
289        assert!(msg.contains("verifier-1:"));
290        assert!(msg.contains("Citrea chain ID mismatch"));
291        assert!(msg.contains("Security council mismatch"));
292    }
293
294    // serial test because it calculates sha256 of the bitvm cache for all actors
295    #[tokio::test]
296    async fn test_get_compatibility_data_from_entities() {
297        let mut config = create_test_config_with_thread_name().await;
298        let _regtest = create_regtest_rpc(&mut config).await;
299        let actors = create_actors::<MockCitreaClient>(&config).await;
300        let mut aggregator = actors.get_aggregator();
301        // Load cache here to calculate sha256 of the bitvm cache for all actors before get_compatibility call to avoid timeout in debug mode
302        BITVM_CACHE
303            .get_or_try_init(load_or_generate_bitvm_cache)
304            .unwrap();
305        let entity_comp_data = aggregator
306            .get_compatibility_data_from_entities(Empty {})
307            .await
308            .unwrap()
309            .into_inner();
310
311        tracing::info!("Entity compatibility data: {:?}", entity_comp_data);
312
313        let mut errors = Vec::new();
314        for entity in entity_comp_data.entities_compatibility_data {
315            let data = entity.data_result.unwrap();
316            match data {
317                DataResult::Data(_) => {}
318                DataResult::Error(err) => {
319                    errors.push(format!(
320                        "Entity {:?} returned an error: {:?}",
321                        entity.entity_id.unwrap(),
322                        err
323                    ));
324                }
325            }
326        }
327        if !errors.is_empty() {
328            panic!("Errors: {}", errors.join(", "));
329        }
330    }
331
332    fn create_test_protocol_paramset() -> ProtocolParamset {
333        REGTEST_PARAMSET
334    }
335
336    fn create_test_protocol_paramset_different() -> ProtocolParamset {
337        TESTNET4_TEST_PARAMSET
338    }
339
340    fn create_test_security_council() -> SecurityCouncil {
341        SecurityCouncil {
342            pks: vec![
343                XOnlyPublicKey::from_str(
344                    "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
345                )
346                .unwrap(),
347                XOnlyPublicKey::from_str(
348                    "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
349                )
350                .unwrap(),
351            ],
352            threshold: 1,
353        }
354    }
355
356    fn create_test_security_council_different() -> SecurityCouncil {
357        SecurityCouncil {
358            pks: vec![
359                XOnlyPublicKey::from_str(
360                    "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
361                )
362                .unwrap(),
363                XOnlyPublicKey::from_str(
364                    "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
365                )
366                .unwrap(),
367            ],
368            threshold: 2,
369        }
370    }
371
372    fn create_test_compatibility_params(version: &str) -> CompatibilityParams {
373        let protocol_paramset = create_test_protocol_paramset();
374        CompatibilityParams {
375            bridge_circuit_constant: *protocol_paramset.bridge_circuit_constant().unwrap(),
376            sha256_bitvm_cache: [0u8; 32],
377            protocol_paramset,
378            security_council: create_test_security_council(),
379            citrea_chain_id: 1234,
380            clementine_version: version.to_string(),
381        }
382    }
383
384    #[test]
385    fn test_compatible_identical_params() {
386        let params1 = create_test_compatibility_params("1.2.3");
387        let params2 = create_test_compatibility_params("1.2.3");
388
389        assert!(params1.is_compatible(&params2).is_ok());
390    }
391
392    #[test]
393    fn test_compatible_different_patch_versions() {
394        let params1 = create_test_compatibility_params("1.2.3");
395        let params2 = create_test_compatibility_params("1.2.5");
396
397        assert!(params1.is_compatible(&params2).is_ok());
398    }
399
400    #[test]
401    fn test_incompatible_different_security_council() {
402        let params1 = create_test_compatibility_params("1.2.3");
403        let mut params2 = create_test_compatibility_params("1.2.3");
404        params2.security_council = create_test_security_council_different();
405
406        let result = params1.is_compatible(&params2);
407        assert!(result.is_err());
408        let err_msg = result.unwrap_err().to_string();
409        assert!(err_msg.contains("Security council mismatch"));
410    }
411
412    #[test]
413    fn test_incompatible_different_citrea_chain_id() {
414        let params1 = create_test_compatibility_params("1.2.3");
415        let mut params2 = create_test_compatibility_params("1.2.3");
416        params2.citrea_chain_id = 5678;
417
418        let result = params1.is_compatible(&params2);
419        assert!(result.is_err());
420        let err_msg = result.unwrap_err().to_string();
421        assert!(err_msg.contains("Citrea chain ID mismatch"));
422    }
423
424    #[test]
425    fn test_incompatible_different_protocol_paramset() {
426        let params1 = create_test_compatibility_params("1.2.3");
427        let mut params2 = create_test_compatibility_params("1.2.3");
428        // Change a field in the protocol paramset
429        params2.protocol_paramset = create_test_protocol_paramset_different();
430
431        let result = params1.is_compatible(&params2);
432        assert!(result.is_err());
433        let err_msg = result.unwrap_err().to_string();
434        assert!(err_msg.contains("Protocol paramset mismatch"));
435    }
436
437    #[test]
438    fn test_incompatible_different_bridge_circuit_constant() {
439        let params1 = create_test_compatibility_params("1.2.3");
440        let mut params2 = create_test_compatibility_params("1.2.3");
441        params2.bridge_circuit_constant = [1u8; 32];
442
443        let result = params1.is_compatible(&params2);
444        assert!(result.is_err());
445        let err_msg = result.unwrap_err().to_string();
446        assert!(err_msg.contains("Bridge circuit constant mismatch"));
447    }
448
449    #[test]
450    fn test_incompatible_different_sha256_bitvm_cache() {
451        let params1 = create_test_compatibility_params("1.2.3");
452        let mut params2 = create_test_compatibility_params("1.2.3");
453        params2.sha256_bitvm_cache = [2u8; 32];
454
455        let result = params1.is_compatible(&params2);
456        assert!(result.is_err());
457        let err_msg = result.unwrap_err().to_string();
458        assert!(err_msg.contains("BitVM cache SHA256 mismatch"));
459    }
460
461    #[test]
462    fn test_incompatible_multiple_reasons() {
463        let params1 = create_test_compatibility_params("1.2.3");
464        let mut params2 = create_test_compatibility_params("2.0.0");
465        params2.citrea_chain_id = 5678;
466        params2.security_council = create_test_security_council_different();
467
468        let result = params1.is_compatible(&params2);
469        assert!(result.is_err());
470        let err_msg = result.unwrap_err().to_string();
471        assert!(err_msg.contains("Security council mismatch"));
472        assert!(err_msg.contains("Citrea chain ID mismatch"));
473        assert!(err_msg.contains("Clementine version mismatch"));
474    }
475
476    #[test]
477    fn test_invalid_version_format_self() {
478        let params1 = create_test_compatibility_params("invalid-version");
479        let params2 = create_test_compatibility_params("1.2.3");
480
481        let result = params1.is_compatible(&params2);
482        assert!(result.is_err());
483    }
484
485    #[test]
486    fn test_invalid_version_format_other() {
487        let params1 = create_test_compatibility_params("1.2.3");
488        let params2 = create_test_compatibility_params("not-a-version");
489
490        let result = params1.is_compatible(&params2);
491        assert!(result.is_err());
492    }
493
494    #[test]
495    fn test_version_with_build_metadata() {
496        let params1 = create_test_compatibility_params("1.2.3+build123");
497        let params2 = create_test_compatibility_params("1.2.5+build456");
498
499        // Build metadata should be ignored, and patch versions can differ
500        assert!(params1.is_compatible(&params2).is_ok());
501    }
502
503    #[test]
504    fn test_compatibility_params_to_rpc_conversion() {
505        let params = create_test_compatibility_params("1.2.3");
506
507        let rpc_params: CompatibilityParamsRpc = params.clone().try_into().unwrap();
508
509        assert_eq!(rpc_params.citrea_chain_id, 1234);
510        assert_eq!(rpc_params.clementine_version, params.clementine_version);
511        assert!(!rpc_params.protocol_paramset.is_empty());
512        assert!(!rpc_params.security_council.is_empty());
513        assert_eq!(
514            rpc_params.bridge_circuit_constant,
515            params.bridge_circuit_constant.to_vec()
516        );
517        assert_eq!(
518            rpc_params.sha256_bitvm_cache,
519            params.sha256_bitvm_cache.to_vec()
520        );
521    }
522
523    #[test]
524    fn test_compatibility_params_rpc_parsing() {
525        let params = create_test_compatibility_params("1.2.3");
526
527        let rpc_params: CompatibilityParamsRpc = params.clone().try_into().unwrap();
528        let params_back: CompatibilityParams = rpc_params.try_into().unwrap();
529
530        assert_eq!(params.protocol_paramset, params_back.protocol_paramset);
531        assert_eq!(params.security_council, params_back.security_council);
532        assert_eq!(params.citrea_chain_id, params_back.citrea_chain_id);
533        assert_eq!(
534            params.bridge_circuit_constant,
535            params_back.bridge_circuit_constant
536        );
537        assert_eq!(params.sha256_bitvm_cache, params_back.sha256_bitvm_cache);
538        assert_eq!(params.clementine_version, params_back.clementine_version);
539    }
540
541    #[test]
542    fn test_security_council_string_parsing() {
543        let council = create_test_security_council();
544        let council_str = council.to_string();
545        let council_back: SecurityCouncil = council_str.parse().unwrap();
546
547        assert_eq!(council, council_back);
548    }
549
550    #[test]
551    fn test_security_council_multiple_keys() {
552        let council = SecurityCouncil {
553            pks: vec![
554                XOnlyPublicKey::from_str(
555                    "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
556                )
557                .unwrap(),
558                XOnlyPublicKey::from_str(
559                    "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
560                )
561                .unwrap(),
562                XOnlyPublicKey::from_str(
563                    "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9",
564                )
565                .unwrap(),
566            ],
567            threshold: 2,
568        };
569
570        let council_str = council.to_string();
571        let council_back: SecurityCouncil = council_str.parse().unwrap();
572
573        assert_eq!(council, council_back);
574        assert_eq!(council_back.threshold, 2);
575        assert_eq!(council_back.pks.len(), 3);
576    }
577
578    #[test]
579    fn test_actor_with_config_compatibility_success() {
580        // Create two sets of compatible params
581        let params1 = create_test_compatibility_params("1.2.3");
582        let params2 = create_test_compatibility_params("1.2.5");
583        let params3 = create_test_compatibility_params("1.3.7");
584        let params4 = create_test_compatibility_params("2.0.1");
585
586        // Test the is_compatible method on the params
587        let result = params1.is_compatible(&params2);
588        assert!(result.is_ok());
589        let result = params1.is_compatible(&params3);
590        assert!(result.is_ok());
591        let result = params1.is_compatible(&params4);
592        assert!(result.is_err());
593        let err_msg = result.unwrap_err().to_string();
594        assert!(err_msg.contains("Clementine version mismatch"));
595
596        let params5 = create_test_compatibility_params("0.6.8");
597        let params6 = create_test_compatibility_params("0.6.7");
598        let params7 = create_test_compatibility_params("0.7.0");
599
600        let result = params5.is_compatible(&params6);
601        assert!(result.is_ok());
602        let result = params5.is_compatible(&params7);
603        assert!(result.is_err());
604        let err_msg = result.unwrap_err().to_string();
605        assert!(err_msg.contains("Clementine version mismatch"));
606    }
607
608    #[test]
609    fn test_actor_with_config_compatibility_failure() {
610        let params1 = create_test_compatibility_params("1.2.3");
611        let mut params2 = create_test_compatibility_params("2.0.0");
612        params2.citrea_chain_id = 9999;
613
614        let result = params1.is_compatible(&params2);
615        assert!(result.is_err());
616        let err_msg = result.unwrap_err().to_string();
617        assert!(err_msg.contains("Citrea chain ID mismatch"));
618        assert!(err_msg.contains("Clementine version mismatch"));
619    }
620}