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