1use alloy_primitives::Address;
6use blueprint_core::{error, info, warn};
7use blueprint_keystore::backends::eigenlayer::EigenlayerBackend;
8use blueprint_runner::config::BlueprintEnvironment;
9use chrono::Utc;
10use serde::{Deserialize, Serialize};
11use std::collections::HashMap;
12use std::path::{Path, PathBuf};
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum RegistrationStatus {
18 Active,
20 Deregistered,
22 Pending,
24}
25
26#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum RuntimeTarget {
37 Native,
40 Hypervisor,
43 Container,
46}
47
48impl Default for RuntimeTarget {
49 fn default() -> Self {
50 Self::Hypervisor
52 }
53}
54
55impl std::fmt::Display for RuntimeTarget {
56 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
57 match self {
58 Self::Native => write!(f, "native"),
59 Self::Hypervisor => write!(f, "hypervisor"),
60 Self::Container => write!(f, "container"),
61 }
62 }
63}
64
65impl std::str::FromStr for RuntimeTarget {
66 type Err = String;
67
68 fn from_str(s: &str) -> Result<Self, Self::Err> {
69 match s.to_lowercase().as_str() {
70 "native" => Ok(Self::Native),
71 "hypervisor" | "vm" => Ok(Self::Hypervisor),
72 "container" | "docker" | "kata" => Ok(Self::Container),
73 _ => Err(format!(
74 "Invalid runtime target: '{s}'. Valid options: 'native', 'hypervisor', 'container'"
75 )),
76 }
77 }
78}
79
80#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct AvsRegistrationConfig {
83 pub service_manager: Address,
85 pub registry_coordinator: Address,
87 pub operator_state_retriever: Address,
89 pub strategy_manager: Address,
91 pub delegation_manager: Address,
93 pub avs_directory: Address,
95 pub rewards_coordinator: Address,
97 pub permission_controller: Address,
99 pub allocation_manager: Address,
101 pub strategy_address: Address,
103 pub stake_registry: Address,
105
106 pub blueprint_path: PathBuf,
108
109 #[serde(skip_serializing_if = "Option::is_none")]
113 pub container_image: Option<String>,
114
115 #[serde(default)]
117 pub runtime_target: RuntimeTarget,
118
119 #[serde(default = "default_allocation_delay")]
121 pub allocation_delay: u32,
122 #[serde(default = "default_deposit_amount")]
124 pub deposit_amount: u128,
125 #[serde(default = "default_stake_amount")]
127 pub stake_amount: u64,
128
129 #[serde(default = "default_operator_sets")]
131 pub operator_sets: Vec<u32>,
132}
133
134fn default_allocation_delay() -> u32 {
135 0
136}
137
138fn default_deposit_amount() -> u128 {
139 5_000_000_000_000_000_000_000
140}
141
142fn default_stake_amount() -> u64 {
143 1_000_000_000_000_000_000
144}
145
146fn default_operator_sets() -> Vec<u32> {
147 vec![0]
148}
149
150#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct AvsRegistration {
153 pub operator_address: Address,
155 pub registered_at: String,
157 pub status: RegistrationStatus,
159 pub config: AvsRegistrationConfig,
161}
162
163impl AvsRegistrationConfig {
164 pub fn validate(&self) -> Result<(), String> {
175 if !self.blueprint_path.exists() {
177 return Err(format!(
178 "Blueprint path does not exist: {}",
179 self.blueprint_path.display()
180 ));
181 }
182
183 if !self.blueprint_path.is_dir() && !self.blueprint_path.is_file() {
185 return Err(format!(
186 "Blueprint path is neither a file nor directory: {}",
187 self.blueprint_path.display()
188 ));
189 }
190
191 if self.blueprint_path.is_file() && self.runtime_target != RuntimeTarget::Container {
193 return Err(format!(
194 "Pre-compiled binaries are not yet supported for {:?} runtime. \
195 Please use one of these options:\n\
196 1. Provide a Rust project directory (containing Cargo.toml)\n\
197 2. Use Container runtime (--runtime container) with a container image",
198 self.runtime_target
199 ));
200 }
201
202 match self.runtime_target {
204 RuntimeTarget::Native => {
205 #[cfg(not(debug_assertions))]
207 {
208 warn!(
209 "Native runtime selected - this provides NO ISOLATION and should only be used for testing!"
210 );
211 }
212 }
213 RuntimeTarget::Hypervisor => {
214 #[cfg(not(target_os = "linux"))]
216 {
217 return Err(
218 "Hypervisor runtime requires Linux/KVM. Use 'native' for local testing on macOS/Windows."
219 .to_string(),
220 );
221 }
222 }
223 RuntimeTarget::Container => {
224 if self.container_image.is_none() {
226 return Err(
227 "Container runtime requires 'container_image' field in config. \
228 Example: \"ghcr.io/my-org/my-avs:latest\""
229 .to_string(),
230 );
231 }
232
233 if let Some(ref image) = self.container_image {
235 if image.trim().is_empty() {
236 return Err("Container image cannot be empty".to_string());
237 }
238
239 let parts: Vec<&str> = image.split(':').collect();
241 if parts.len() != 2 || parts[0].is_empty() || parts[1].is_empty() {
242 return Err(format!(
243 "Container image must be 'name:tag' or 'registry/name:tag' format (got: '{image}'). \
244 Example: \"ghcr.io/my-org/my-avs:latest\" or \"my-image:latest\""
245 ));
246 }
247
248 if parts[0].contains("://") {
250 return Err(
251 "Container image should not include protocol (http:// or https://)"
252 .to_string(),
253 );
254 }
255 }
256 }
257 }
258
259 Ok(())
260 }
261}
262
263impl AvsRegistration {
264 pub fn new(operator_address: Address, config: AvsRegistrationConfig) -> Self {
266 Self {
267 operator_address,
268 registered_at: Utc::now().to_rfc3339(),
269 status: RegistrationStatus::Active,
270 config,
271 }
272 }
273
274 pub fn avs_id(&self) -> Address {
276 self.config.service_manager
277 }
278
279 pub fn blueprint_id(&self) -> u64 {
281 let bytes = self.config.service_manager.as_slice();
282 u64::from_be_bytes([
283 bytes[0], bytes[1], bytes[2], bytes[3], bytes[4], bytes[5], bytes[6], bytes[7],
284 ])
285 }
286}
287
288#[derive(Debug, Clone, Serialize, Deserialize, Default)]
290pub struct AvsRegistrations {
291 #[serde(default)]
293 pub registrations: HashMap<String, AvsRegistration>,
294}
295
296impl AvsRegistrations {
297 pub fn add(&mut self, registration: AvsRegistration) {
299 let key = format!("{:#x}", registration.config.service_manager);
300 self.registrations.insert(key, registration);
301 }
302
303 pub fn remove(&mut self, service_manager: Address) -> Option<AvsRegistration> {
305 let key = format!("{service_manager:#x}");
306 self.registrations.remove(&key)
307 }
308
309 pub fn get(&self, service_manager: Address) -> Option<&AvsRegistration> {
311 let key = format!("{service_manager:#x}");
312 self.registrations.get(&key)
313 }
314
315 pub fn get_mut(&mut self, service_manager: Address) -> Option<&mut AvsRegistration> {
317 let key = format!("{service_manager:#x}");
318 self.registrations.get_mut(&key)
319 }
320
321 pub fn active(&self) -> impl Iterator<Item = &AvsRegistration> {
323 self.registrations
324 .values()
325 .filter(|r| r.status == RegistrationStatus::Active)
326 }
327
328 pub fn mark_deregistered(&mut self, service_manager: Address) -> bool {
330 if let Some(reg) = self.get_mut(service_manager) {
331 reg.status = RegistrationStatus::Deregistered;
332 true
333 } else {
334 false
335 }
336 }
337}
338
339pub struct RegistrationStateManager {
341 state_file: PathBuf,
342 registrations: AvsRegistrations,
343}
344
345impl RegistrationStateManager {
346 pub fn load() -> Result<Self, crate::error::Error> {
354 let state_file = Self::default_state_file()?;
355 Self::load_from_file(&state_file)
356 }
357
358 pub fn load_or_create() -> Result<Self, crate::error::Error> {
367 match Self::load() {
368 Ok(manager) => Ok(manager),
369 Err(_) => {
370 let state_file = Self::default_state_file()?;
372 info!(
373 "Creating new registration state file at {}",
374 state_file.display()
375 );
376 Ok(Self {
377 state_file,
378 registrations: AvsRegistrations::default(),
379 })
380 }
381 }
382 }
383
384 pub fn load_from_file(path: &Path) -> Result<Self, crate::error::Error> {
386 let registrations = if path.exists() {
387 let contents = std::fs::read_to_string(path).map_err(|e| {
388 crate::error::Error::Other(format!("Failed to read registration state: {e}"))
389 })?;
390
391 serde_json::from_str(&contents).map_err(|e| {
392 crate::error::Error::Other(format!("Failed to parse registration state: {e}"))
393 })?
394 } else {
395 info!(
396 "No existing registration state found at {}, creating new",
397 path.display()
398 );
399 AvsRegistrations::default()
400 };
401
402 Ok(Self {
403 state_file: path.to_path_buf(),
404 registrations,
405 })
406 }
407
408 fn default_state_file() -> Result<PathBuf, crate::error::Error> {
410 let home = dirs::home_dir()
411 .ok_or_else(|| crate::error::Error::Other("Cannot determine home directory".into()))?;
412
413 let tangle_dir = home.join(".tangle");
414 std::fs::create_dir_all(&tangle_dir).map_err(|e| {
415 crate::error::Error::Other(format!("Failed to create .tangle directory: {e}"))
416 })?;
417
418 Ok(tangle_dir.join("eigenlayer_registrations.json"))
419 }
420
421 pub fn save(&self) -> Result<(), crate::error::Error> {
423 let contents = serde_json::to_string_pretty(&self.registrations).map_err(|e| {
424 crate::error::Error::Other(format!("Failed to serialize registrations: {e}"))
425 })?;
426
427 std::fs::write(&self.state_file, contents).map_err(|e| {
428 crate::error::Error::Other(format!("Failed to write registration state: {e}"))
429 })?;
430
431 info!("Saved registration state to {}", self.state_file.display());
432 Ok(())
433 }
434
435 pub fn registrations(&self) -> &AvsRegistrations {
437 &self.registrations
438 }
439
440 pub fn registrations_mut(&mut self) -> &mut AvsRegistrations {
442 &mut self.registrations
443 }
444
445 pub fn register(&mut self, registration: AvsRegistration) -> Result<(), crate::error::Error> {
447 info!(
448 "Registering AVS {} for operator {:#x}",
449 registration.config.service_manager, registration.operator_address
450 );
451
452 self.registrations.add(registration);
453 self.save()
454 }
455
456 pub fn deregister(&mut self, service_manager: Address) -> Result<(), crate::error::Error> {
458 info!("Deregistering AVS {:#x}", service_manager);
459
460 if self.registrations.mark_deregistered(service_manager) {
461 self.save()
462 } else {
463 Err(crate::error::Error::Other(format!(
464 "AVS {service_manager:#x} not found in registrations"
465 )))
466 }
467 }
468
469 pub async fn verify_on_chain(
473 &self,
474 service_manager: Address,
475 env: &BlueprintEnvironment,
476 ) -> Result<bool, crate::error::Error> {
477 let registration = self.registrations.get(service_manager).ok_or_else(|| {
478 crate::error::Error::Other(format!(
479 "AVS {service_manager:#x} not found in local registrations"
480 ))
481 })?;
482
483 use blueprint_keystore::backends::Backend;
485 use blueprint_keystore::crypto::k256::K256Ecdsa;
486
487 let ecdsa_public = env
488 .keystore()
489 .first_local::<K256Ecdsa>()
490 .map_err(|e| crate::error::Error::Other(format!("Keystore error: {e}")))?;
491
492 let ecdsa_secret = env
493 .keystore()
494 .expose_ecdsa_secret(&ecdsa_public)
495 .map_err(|e| crate::error::Error::Other(format!("Keystore error: {e}")))?
496 .ok_or_else(|| crate::error::Error::Other("No ECDSA secret found".into()))?;
497
498 let operator_address = ecdsa_secret.alloy_address().map_err(|e| {
499 crate::error::Error::Other(format!("Failed to get operator address: {e}"))
500 })?;
501
502 let avs_registry_reader =
504 eigensdk::client_avsregistry::reader::AvsRegistryChainReader::new(
505 registration.config.registry_coordinator,
506 registration.config.operator_state_retriever,
507 env.http_rpc_endpoint.to_string(),
508 )
509 .await
510 .map_err(|e| {
511 crate::error::Error::Other(format!("Failed to create AVS registry reader: {e}"))
512 })?;
513
514 avs_registry_reader
516 .is_operator_registered(operator_address)
517 .await
518 .map_err(|e| {
519 crate::error::Error::Other(format!("Failed to check registration status: {e}"))
520 })
521 }
522
523 pub async fn reconcile_with_chain(
531 &mut self,
532 env: &BlueprintEnvironment,
533 ) -> Result<usize, crate::error::Error> {
534 let mut reconciled = 0;
535 let service_managers: Vec<Address> = self
536 .registrations
537 .active()
538 .map(|r| r.config.service_manager)
539 .collect();
540
541 for service_manager in service_managers {
542 match self.verify_on_chain(service_manager, env).await {
543 Ok(is_registered) => {
544 if !is_registered {
545 warn!(
546 "AVS {:#x} is registered locally but not on-chain, marking as deregistered",
547 service_manager
548 );
549 self.registrations.mark_deregistered(service_manager);
550 reconciled += 1;
551 }
552 }
553 Err(e) => {
554 error!(
555 "Failed to verify AVS {:#x} on-chain: {}",
556 service_manager, e
557 );
558 }
559 }
560 }
561
562 if reconciled > 0 {
563 self.save()?;
564 }
565
566 Ok(reconciled)
567 }
568}
569
570#[cfg(test)]
571mod tests {
572 use super::*;
573
574 #[test]
575 fn test_validation_nonexistent_path() {
576 let config = AvsRegistrationConfig {
577 service_manager: Address::ZERO,
578 registry_coordinator: Address::ZERO,
579 operator_state_retriever: Address::ZERO,
580 strategy_manager: Address::ZERO,
581 delegation_manager: Address::ZERO,
582 avs_directory: Address::ZERO,
583 rewards_coordinator: Address::ZERO,
584 permission_controller: Address::ZERO,
585 allocation_manager: Address::ZERO,
586 strategy_address: Address::ZERO,
587 stake_registry: Address::ZERO,
588 blueprint_path: PathBuf::from("/nonexistent/path/to/blueprint"),
589 runtime_target: RuntimeTarget::Native,
590 allocation_delay: 0,
591 deposit_amount: 1000,
592 stake_amount: 100,
593 operator_sets: vec![0],
594 container_image: None,
595 };
596
597 let result = config.validate();
598 assert!(result.is_err());
599 assert!(result.unwrap_err().contains("does not exist"));
600 }
601
602 #[test]
603 fn test_validation_valid_config() {
604 let temp_dir = tempfile::tempdir().unwrap();
605 let blueprint_path = temp_dir.path().join("test_blueprint");
606 std::fs::File::create(&blueprint_path).unwrap();
607
608 let config = AvsRegistrationConfig {
609 service_manager: Address::ZERO,
610 registry_coordinator: Address::ZERO,
611 operator_state_retriever: Address::ZERO,
612 strategy_manager: Address::ZERO,
613 delegation_manager: Address::ZERO,
614 avs_directory: Address::ZERO,
615 rewards_coordinator: Address::ZERO,
616 permission_controller: Address::ZERO,
617 allocation_manager: Address::ZERO,
618 strategy_address: Address::ZERO,
619 stake_registry: Address::ZERO,
620 blueprint_path,
621 runtime_target: RuntimeTarget::Native,
622 allocation_delay: 0,
623 deposit_amount: 1000,
624 stake_amount: 100,
625 operator_sets: vec![0],
626 container_image: None,
627 };
628
629 let result = config.validate();
630 assert!(result.is_ok());
631 }
632
633 #[test]
634 fn test_registration_serialization() {
635 let config = AvsRegistrationConfig {
636 service_manager: Address::from([1u8; 20]),
637 registry_coordinator: Address::from([2u8; 20]),
638 operator_state_retriever: Address::from([3u8; 20]),
639 strategy_manager: Address::from([4u8; 20]),
640 delegation_manager: Address::from([5u8; 20]),
641 avs_directory: Address::from([6u8; 20]),
642 rewards_coordinator: Address::from([7u8; 20]),
643 permission_controller: Address::from([8u8; 20]),
644 allocation_manager: Address::from([9u8; 20]),
645 strategy_address: Address::from([10u8; 20]),
646 stake_registry: Address::from([11u8; 20]),
647 blueprint_path: PathBuf::from("/path/to/blueprint"),
648 runtime_target: RuntimeTarget::Native,
649 allocation_delay: 0,
650 deposit_amount: 5000,
651 stake_amount: 1000,
652 operator_sets: vec![0],
653 container_image: None,
654 };
655
656 let registration = AvsRegistration::new(Address::from([12u8; 20]), config);
657
658 let serialized = serde_json::to_string(®istration).unwrap();
659 let deserialized: AvsRegistration = serde_json::from_str(&serialized).unwrap();
660
661 assert_eq!(registration.operator_address, deserialized.operator_address);
662 assert_eq!(registration.status, deserialized.status);
663 }
664
665 #[test]
666 fn test_registrations_management() {
667 let mut registrations = AvsRegistrations::default();
668
669 let config = AvsRegistrationConfig {
670 service_manager: Address::from([1u8; 20]),
671 registry_coordinator: Address::from([2u8; 20]),
672 operator_state_retriever: Address::from([3u8; 20]),
673 strategy_manager: Address::from([4u8; 20]),
674 delegation_manager: Address::from([5u8; 20]),
675 avs_directory: Address::from([6u8; 20]),
676 rewards_coordinator: Address::from([7u8; 20]),
677 permission_controller: Address::from([8u8; 20]),
678 allocation_manager: Address::from([9u8; 20]),
679 strategy_address: Address::from([10u8; 20]),
680 stake_registry: Address::from([11u8; 20]),
681 blueprint_path: PathBuf::from("/path/to/blueprint"),
682 runtime_target: RuntimeTarget::Native,
683 allocation_delay: 0,
684 deposit_amount: 5000,
685 stake_amount: 1000,
686 operator_sets: vec![0],
687 container_image: None,
688 };
689
690 let registration = AvsRegistration::new(Address::from([12u8; 20]), config);
691 let service_manager = registration.config.service_manager;
692
693 registrations.add(registration);
694 assert!(registrations.get(service_manager).is_some());
695
696 assert!(registrations.mark_deregistered(service_manager));
697 assert_eq!(
698 registrations.get(service_manager).unwrap().status,
699 RegistrationStatus::Deregistered
700 );
701
702 assert_eq!(registrations.active().count(), 0);
703 }
704}