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;
11use crate::config::BridgeConfig;
12use crate::deposit::SecurityCouncil;
13use crate::errors::BridgeError;
14use crate::operator::Operator;
15use crate::rpc::clementine::CompatibilityParamsRpc;
16use crate::verifier::Verifier;
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::{
196        config::protocol::{REGTEST_PARAMSET, TESTNET4_TEST_PARAMSET},
197        rpc::clementine::{entity_data_with_id::DataResult, Empty},
198        test::common::{
199            citrea::MockCitreaClient, create_actors, create_regtest_rpc,
200            create_test_config_with_thread_name,
201        },
202    };
203    use bitcoin::XOnlyPublicKey;
204    use std::str::FromStr;
205
206    #[allow(dead_code)]
207    struct MockActorWithConfig {
208        config: BridgeConfig,
209    }
210
211    impl ActorWithConfig for MockActorWithConfig {
212        fn get_config(&self) -> &BridgeConfig {
213            &self.config
214        }
215    }
216
217    #[test]
218    fn test_mock_actor_get_compatibility_params() {
219        let config = BridgeConfig::default();
220        let actor = MockActorWithConfig { config };
221
222        let params = actor.get_compatibility_params().unwrap();
223
224        assert_eq!(
225            params.protocol_paramset,
226            actor.config.protocol_paramset.clone()
227        );
228        assert_eq!(params.security_council, actor.config.security_council);
229        assert_eq!(params.citrea_chain_id, actor.config.citrea_chain_id);
230        assert_eq!(
231            params.bridge_circuit_constant,
232            *actor
233                .config
234                .protocol_paramset
235                .bridge_circuit_constant()
236                .unwrap()
237        );
238        assert_eq!(
239            params.sha256_bitvm_cache,
240            BITVM_CACHE
241                .get_or_try_init(load_or_generate_bitvm_cache)
242                .unwrap()
243                .sha256_bitvm_cache
244        );
245        assert_eq!(
246            params.clementine_version,
247            env!("CARGO_PKG_VERSION").to_string()
248        );
249    }
250
251    #[test]
252    fn test_mock_actor_is_compatible_success() {
253        let config = BridgeConfig::default();
254        let actor = MockActorWithConfig { config };
255
256        let own = actor.get_compatibility_params().unwrap();
257        let others = vec![
258            ("aggregator".to_string(), own.clone()),
259            ("verifier".to_string(), own),
260        ];
261        assert!(actor.is_compatible(others).is_ok());
262    }
263
264    #[test]
265    fn test_mock_actor_is_compatible_failure() {
266        let config = BridgeConfig::default();
267        let actor = MockActorWithConfig { config };
268
269        let mut other = actor.get_compatibility_params().unwrap();
270        // introduce mismatches
271        other.citrea_chain_id += 1;
272        other.security_council = create_test_security_council_different();
273
274        let result = actor.is_compatible(vec![("verifier-1".to_string(), other)]);
275        assert!(result.is_err());
276        let msg = result.unwrap_err().to_string();
277        assert!(msg.contains("verifier-1:"));
278        assert!(msg.contains("Citrea chain ID mismatch"));
279        assert!(msg.contains("Security council mismatch"));
280    }
281
282    // serial test because it calculates sha256 of the bitvm cache for all actors
283    #[tokio::test]
284    async fn test_get_compatibility_data_from_entities() {
285        let mut config = create_test_config_with_thread_name().await;
286        let _regtest = create_regtest_rpc(&mut config).await;
287        let actors = create_actors::<MockCitreaClient>(&config).await;
288        let mut aggregator = actors.get_aggregator();
289        // Load cache here to calculate sha256 of the bitvm cache for all actors before get_compatibility call to avoid timeout in debug mode
290        BITVM_CACHE
291            .get_or_try_init(load_or_generate_bitvm_cache)
292            .unwrap();
293        let entity_comp_data = aggregator
294            .get_compatibility_data_from_entities(Empty {})
295            .await
296            .unwrap()
297            .into_inner();
298
299        tracing::info!("Entity compatibility data: {:?}", entity_comp_data);
300
301        let mut errors = Vec::new();
302        for entity in entity_comp_data.entities_compatibility_data {
303            let data = entity.data_result.unwrap();
304            match data {
305                DataResult::Data(_) => {}
306                DataResult::Error(err) => {
307                    errors.push(format!(
308                        "Entity {:?} returned an error: {:?}",
309                        entity.entity_id.unwrap(),
310                        err
311                    ));
312                }
313            }
314        }
315        if !errors.is_empty() {
316            panic!("Errors: {}", errors.join(", "));
317        }
318    }
319
320    fn create_test_protocol_paramset() -> ProtocolParamset {
321        REGTEST_PARAMSET
322    }
323
324    fn create_test_protocol_paramset_different() -> ProtocolParamset {
325        TESTNET4_TEST_PARAMSET
326    }
327
328    fn create_test_security_council() -> SecurityCouncil {
329        SecurityCouncil {
330            pks: vec![
331                XOnlyPublicKey::from_str(
332                    "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
333                )
334                .unwrap(),
335                XOnlyPublicKey::from_str(
336                    "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
337                )
338                .unwrap(),
339            ],
340            threshold: 1,
341        }
342    }
343
344    fn create_test_security_council_different() -> SecurityCouncil {
345        SecurityCouncil {
346            pks: vec![
347                XOnlyPublicKey::from_str(
348                    "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
349                )
350                .unwrap(),
351                XOnlyPublicKey::from_str(
352                    "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
353                )
354                .unwrap(),
355            ],
356            threshold: 2,
357        }
358    }
359
360    fn create_test_compatibility_params(version: &str) -> CompatibilityParams {
361        let protocol_paramset = create_test_protocol_paramset();
362        CompatibilityParams {
363            bridge_circuit_constant: *protocol_paramset.bridge_circuit_constant().unwrap(),
364            sha256_bitvm_cache: [0u8; 32],
365            protocol_paramset,
366            security_council: create_test_security_council(),
367            citrea_chain_id: 1234,
368            clementine_version: version.to_string(),
369        }
370    }
371
372    #[test]
373    fn test_compatible_identical_params() {
374        let params1 = create_test_compatibility_params("1.2.3");
375        let params2 = create_test_compatibility_params("1.2.3");
376
377        assert!(params1.is_compatible(&params2).is_ok());
378    }
379
380    #[test]
381    fn test_compatible_different_patch_versions() {
382        let params1 = create_test_compatibility_params("1.2.3");
383        let params2 = create_test_compatibility_params("1.2.5");
384
385        assert!(params1.is_compatible(&params2).is_ok());
386    }
387
388    #[test]
389    fn test_incompatible_different_security_council() {
390        let params1 = create_test_compatibility_params("1.2.3");
391        let mut params2 = create_test_compatibility_params("1.2.3");
392        params2.security_council = create_test_security_council_different();
393
394        let result = params1.is_compatible(&params2);
395        assert!(result.is_err());
396        let err_msg = result.unwrap_err().to_string();
397        assert!(err_msg.contains("Security council mismatch"));
398    }
399
400    #[test]
401    fn test_incompatible_different_citrea_chain_id() {
402        let params1 = create_test_compatibility_params("1.2.3");
403        let mut params2 = create_test_compatibility_params("1.2.3");
404        params2.citrea_chain_id = 5678;
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("Citrea chain ID mismatch"));
410    }
411
412    #[test]
413    fn test_incompatible_different_protocol_paramset() {
414        let params1 = create_test_compatibility_params("1.2.3");
415        let mut params2 = create_test_compatibility_params("1.2.3");
416        // Change a field in the protocol paramset
417        params2.protocol_paramset = create_test_protocol_paramset_different();
418
419        let result = params1.is_compatible(&params2);
420        assert!(result.is_err());
421        let err_msg = result.unwrap_err().to_string();
422        assert!(err_msg.contains("Protocol paramset mismatch"));
423    }
424
425    #[test]
426    fn test_incompatible_different_bridge_circuit_constant() {
427        let params1 = create_test_compatibility_params("1.2.3");
428        let mut params2 = create_test_compatibility_params("1.2.3");
429        params2.bridge_circuit_constant = [1u8; 32];
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("Bridge circuit constant mismatch"));
435    }
436
437    #[test]
438    fn test_incompatible_different_sha256_bitvm_cache() {
439        let params1 = create_test_compatibility_params("1.2.3");
440        let mut params2 = create_test_compatibility_params("1.2.3");
441        params2.sha256_bitvm_cache = [2u8; 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("BitVM cache SHA256 mismatch"));
447    }
448
449    #[test]
450    fn test_incompatible_multiple_reasons() {
451        let params1 = create_test_compatibility_params("1.2.3");
452        let mut params2 = create_test_compatibility_params("2.0.0");
453        params2.citrea_chain_id = 5678;
454        params2.security_council = create_test_security_council_different();
455
456        let result = params1.is_compatible(&params2);
457        assert!(result.is_err());
458        let err_msg = result.unwrap_err().to_string();
459        assert!(err_msg.contains("Security council mismatch"));
460        assert!(err_msg.contains("Citrea chain ID mismatch"));
461        assert!(err_msg.contains("Clementine version mismatch"));
462    }
463
464    #[test]
465    fn test_invalid_version_format_self() {
466        let params1 = create_test_compatibility_params("invalid-version");
467        let params2 = create_test_compatibility_params("1.2.3");
468
469        let result = params1.is_compatible(&params2);
470        assert!(result.is_err());
471    }
472
473    #[test]
474    fn test_invalid_version_format_other() {
475        let params1 = create_test_compatibility_params("1.2.3");
476        let params2 = create_test_compatibility_params("not-a-version");
477
478        let result = params1.is_compatible(&params2);
479        assert!(result.is_err());
480    }
481
482    #[test]
483    fn test_version_with_build_metadata() {
484        let params1 = create_test_compatibility_params("1.2.3+build123");
485        let params2 = create_test_compatibility_params("1.2.5+build456");
486
487        // Build metadata should be ignored, and patch versions can differ
488        assert!(params1.is_compatible(&params2).is_ok());
489    }
490
491    #[test]
492    fn test_compatibility_params_to_rpc_conversion() {
493        let params = create_test_compatibility_params("1.2.3");
494
495        let rpc_params: CompatibilityParamsRpc = params.clone().try_into().unwrap();
496
497        assert_eq!(rpc_params.citrea_chain_id, 1234);
498        assert_eq!(rpc_params.clementine_version, params.clementine_version);
499        assert!(!rpc_params.protocol_paramset.is_empty());
500        assert!(!rpc_params.security_council.is_empty());
501        assert_eq!(
502            rpc_params.bridge_circuit_constant,
503            params.bridge_circuit_constant.to_vec()
504        );
505        assert_eq!(
506            rpc_params.sha256_bitvm_cache,
507            params.sha256_bitvm_cache.to_vec()
508        );
509    }
510
511    #[test]
512    fn test_compatibility_params_rpc_parsing() {
513        let params = create_test_compatibility_params("1.2.3");
514
515        let rpc_params: CompatibilityParamsRpc = params.clone().try_into().unwrap();
516        let params_back: CompatibilityParams = rpc_params.try_into().unwrap();
517
518        assert_eq!(params.protocol_paramset, params_back.protocol_paramset);
519        assert_eq!(params.security_council, params_back.security_council);
520        assert_eq!(params.citrea_chain_id, params_back.citrea_chain_id);
521        assert_eq!(
522            params.bridge_circuit_constant,
523            params_back.bridge_circuit_constant
524        );
525        assert_eq!(params.sha256_bitvm_cache, params_back.sha256_bitvm_cache);
526        assert_eq!(params.clementine_version, params_back.clementine_version);
527    }
528
529    #[test]
530    fn test_security_council_string_parsing() {
531        let council = create_test_security_council();
532        let council_str = council.to_string();
533        let council_back: SecurityCouncil = council_str.parse().unwrap();
534
535        assert_eq!(council, council_back);
536    }
537
538    #[test]
539    fn test_security_council_multiple_keys() {
540        let council = SecurityCouncil {
541            pks: vec![
542                XOnlyPublicKey::from_str(
543                    "79be667ef9dcbbac55a06295ce870b07029bfcdb2dce28d959f2815b16f81798",
544                )
545                .unwrap(),
546                XOnlyPublicKey::from_str(
547                    "c6047f9441ed7d6d3045406e95c07cd85c778e4b8cef3ca7abac09b95c709ee5",
548                )
549                .unwrap(),
550                XOnlyPublicKey::from_str(
551                    "f9308a019258c31049344f85f89d5229b531c845836f99b08601f113bce036f9",
552                )
553                .unwrap(),
554            ],
555            threshold: 2,
556        };
557
558        let council_str = council.to_string();
559        let council_back: SecurityCouncil = council_str.parse().unwrap();
560
561        assert_eq!(council, council_back);
562        assert_eq!(council_back.threshold, 2);
563        assert_eq!(council_back.pks.len(), 3);
564    }
565
566    #[test]
567    fn test_actor_with_config_compatibility_success() {
568        // Create two sets of compatible params
569        let params1 = create_test_compatibility_params("1.2.3");
570        let params2 = create_test_compatibility_params("1.2.5");
571        let params3 = create_test_compatibility_params("1.3.7");
572        let params4 = create_test_compatibility_params("2.0.1");
573
574        // Test the is_compatible method on the params
575        let result = params1.is_compatible(&params2);
576        assert!(result.is_ok());
577        let result = params1.is_compatible(&params3);
578        assert!(result.is_ok());
579        let result = params1.is_compatible(&params4);
580        assert!(result.is_err());
581        let err_msg = result.unwrap_err().to_string();
582        assert!(err_msg.contains("Clementine version mismatch"));
583
584        let params5 = create_test_compatibility_params("0.6.8");
585        let params6 = create_test_compatibility_params("0.6.7");
586        let params7 = create_test_compatibility_params("0.7.0");
587
588        let result = params5.is_compatible(&params6);
589        assert!(result.is_ok());
590        let result = params5.is_compatible(&params7);
591        assert!(result.is_err());
592        let err_msg = result.unwrap_err().to_string();
593        assert!(err_msg.contains("Clementine version mismatch"));
594    }
595
596    #[test]
597    fn test_actor_with_config_compatibility_failure() {
598        let params1 = create_test_compatibility_params("1.2.3");
599        let mut params2 = create_test_compatibility_params("2.0.0");
600        params2.citrea_chain_id = 9999;
601
602        let result = params1.is_compatible(&params2);
603        assert!(result.is_err());
604        let err_msg = result.unwrap_err().to_string();
605        assert!(err_msg.contains("Citrea chain ID mismatch"));
606        assert!(err_msg.contains("Clementine version mismatch"));
607    }
608}