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