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