1use 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#[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 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(¶ms.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(¶ms.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 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(¶ms) {
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 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 #[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 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(¶ms2).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(¶ms2).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(¶ms2);
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(¶ms2);
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 params2.protocol_paramset = create_test_protocol_paramset_different();
430
431 let result = params1.is_compatible(¶ms2);
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(¶ms2);
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(¶ms2);
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(¶ms2);
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(¶ms2);
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(¶ms2);
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 assert!(params1.is_compatible(¶ms2).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 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 let result = params1.is_compatible(¶ms2);
588 assert!(result.is_ok());
589 let result = params1.is_compatible(¶ms3);
590 assert!(result.is_ok());
591 let result = params1.is_compatible(¶ms4);
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(¶ms6);
601 assert!(result.is_ok());
602 let result = params5.is_compatible(¶ms7);
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(¶ms2);
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}