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 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(¶ms.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(¶ms.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 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(¶ms) {
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 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 #[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 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(¶ms2).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(¶ms2).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(¶ms2);
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(¶ms2);
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 params2.protocol_paramset = create_test_protocol_paramset_different();
419
420 let result = params1.is_compatible(¶ms2);
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(¶ms2);
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(¶ms2);
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(¶ms2);
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(¶ms2);
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(¶ms2);
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 assert!(params1.is_compatible(¶ms2).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 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 let result = params1.is_compatible(¶ms2);
577 assert!(result.is_ok());
578 let result = params1.is_compatible(¶ms3);
579 assert!(result.is_ok());
580 let result = params1.is_compatible(¶ms4);
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(¶ms6);
590 assert!(result.is_ok());
591 let result = params5.is_compatible(¶ms7);
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(¶ms2);
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}