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 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 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(¶ms.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(¶ms.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 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(¶ms) {
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 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 #[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 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(¶ms2).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(¶ms2).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(¶ms2);
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(¶ms2);
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 params2.protocol_paramset = create_test_protocol_paramset_different();
445
446 let result = params1.is_compatible(¶ms2);
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(¶ms2);
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(¶ms2);
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(¶ms2);
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(¶ms2);
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(¶ms2);
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 assert!(params1.is_compatible(¶ms2).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 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 let result = params1.is_compatible(¶ms2);
603 assert!(result.is_ok());
604 let result = params1.is_compatible(¶ms3);
605 assert!(result.is_ok());
606 let result = params1.is_compatible(¶ms4);
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(¶ms6);
616 assert!(result.is_ok());
617 let result = params5.is_compatible(¶ms7);
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(¶ms2);
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}