blueprint_eigenlayer_extra/
registration.rs

1/// EigenLayer AVS registration state management
2///
3/// This module provides persistent storage and querying of EigenLayer AVS registrations.
4/// It maintains a local state file and provides methods to reconcile with on-chain state.
5use 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/// Status of an AVS registration
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
16#[serde(rename_all = "lowercase")]
17pub enum RegistrationStatus {
18    /// AVS is active and should be running
19    Active,
20    /// AVS has been deregistered locally (not running)
21    Deregistered,
22    /// AVS registration is pending on-chain confirmation
23    Pending,
24}
25
26/// Runtime target for AVS blueprint execution
27///
28/// Maps 1:1 to the manager's Runtime enum without any Tangle dependencies.
29///
30/// Supports three runtime modes:
31/// - Native: Direct process execution (blueprint_path)
32/// - Hypervisor: VM-based isolation (blueprint_path)
33/// - Container: Docker/Kata containers (container_image)
34#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
35#[serde(rename_all = "lowercase")]
36pub enum RuntimeTarget {
37    /// Native process (no sandbox) - for testing only
38    /// WARNING: No isolation, fastest startup, use for local testing only
39    Native,
40    /// cloud-hypervisor VM sandbox (default, production-ready)
41    /// Provides strong isolation via hardware virtualization (requires Linux/KVM)
42    Hypervisor,
43    /// Container runtime (Docker/Kata)
44    /// Requires container_image field in config
45    Container,
46}
47
48impl Default for RuntimeTarget {
49    fn default() -> Self {
50        // Default to hypervisor for production safety
51        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/// Configuration for an AVS registration
81#[derive(Debug, Clone, Serialize, Deserialize)]
82pub struct AvsRegistrationConfig {
83    /// Service manager contract address (unique identifier for AVS)
84    pub service_manager: Address,
85    /// Registry coordinator contract address
86    pub registry_coordinator: Address,
87    /// Operator state retriever contract address
88    pub operator_state_retriever: Address,
89    /// Strategy manager contract address
90    pub strategy_manager: Address,
91    /// Delegation manager contract address
92    pub delegation_manager: Address,
93    /// AVS directory contract address
94    pub avs_directory: Address,
95    /// Rewards coordinator contract address
96    pub rewards_coordinator: Address,
97    /// Permission controller contract address
98    pub permission_controller: Address,
99    /// Allocation manager contract address
100    pub allocation_manager: Address,
101    /// Strategy address for staking
102    pub strategy_address: Address,
103    /// Stake registry address
104    pub stake_registry: Address,
105
106    /// Path to the blueprint binary or source (for Native/Hypervisor runtimes)
107    pub blueprint_path: PathBuf,
108
109    /// Container image (for Container runtime only)
110    /// Format: "registry/image:tag" or "image:tag"
111    /// Example: "ghcr.io/my-org/my-avs:latest"
112    #[serde(skip_serializing_if = "Option::is_none")]
113    pub container_image: Option<String>,
114
115    /// Runtime target for blueprint execution
116    #[serde(default)]
117    pub runtime_target: RuntimeTarget,
118
119    /// Allocation delay (in blocks)
120    #[serde(default = "default_allocation_delay")]
121    pub allocation_delay: u32,
122    /// Deposit amount (in wei)
123    #[serde(default = "default_deposit_amount")]
124    pub deposit_amount: u128,
125    /// Stake amount (in wei)
126    #[serde(default = "default_stake_amount")]
127    pub stake_amount: u64,
128
129    /// Operator sets to register for (default: [0])
130    #[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/// A registered AVS entry
151#[derive(Debug, Clone, Serialize, Deserialize)]
152pub struct AvsRegistration {
153    /// Operator address that registered
154    pub operator_address: Address,
155    /// When this registration was created (ISO 8601)
156    pub registered_at: String,
157    /// Current status
158    pub status: RegistrationStatus,
159    /// AVS configuration
160    pub config: AvsRegistrationConfig,
161}
162
163impl AvsRegistrationConfig {
164    /// Validate the registration configuration
165    ///
166    /// Checks that:
167    /// - Blueprint path exists and is accessible
168    /// - Runtime target is supported on current platform
169    /// - Required feature flags are enabled
170    ///
171    /// # Errors
172    ///
173    /// Returns error if configuration is invalid
174    pub fn validate(&self) -> Result<(), String> {
175        // Check blueprint path exists
176        if !self.blueprint_path.exists() {
177            return Err(format!(
178                "Blueprint path does not exist: {}",
179                self.blueprint_path.display()
180            ));
181        }
182
183        // Check if path is a valid file or directory
184        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        // Pre-compiled binaries not yet supported for Native/Hypervisor runtimes
192        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        // Check runtime target compatibility
203        match self.runtime_target {
204            RuntimeTarget::Native => {
205                // Native always works but warn in production
206                #[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                // Hypervisor requires Linux (check at compile time for better UX)
215                #[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                // Container runtime requires container_image field
225                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                // Validate image format
234                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                    // Validate format: should be "name:tag" or "registry/name:tag"
240                    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                    // Check for common mistakes
249                    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    /// Create a new AVS registration
265    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    /// Get a unique identifier for this AVS (based on service manager address)
275    pub fn avs_id(&self) -> Address {
276        self.config.service_manager
277    }
278
279    /// Get the blueprint ID for manager tracking (hash of service manager)
280    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/// Collection of AVS registrations
289#[derive(Debug, Clone, Serialize, Deserialize, Default)]
290pub struct AvsRegistrations {
291    /// Map of service_manager_address -> registration
292    #[serde(default)]
293    pub registrations: HashMap<String, AvsRegistration>,
294}
295
296impl AvsRegistrations {
297    /// Add a new registration
298    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    /// Remove a registration by service manager address
304    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    /// Get a registration by service manager address
310    pub fn get(&self, service_manager: Address) -> Option<&AvsRegistration> {
311        let key = format!("{service_manager:#x}");
312        self.registrations.get(&key)
313    }
314
315    /// Get a mutable reference to a registration
316    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    /// Get all active registrations
322    pub fn active(&self) -> impl Iterator<Item = &AvsRegistration> {
323        self.registrations
324            .values()
325            .filter(|r| r.status == RegistrationStatus::Active)
326    }
327
328    /// Mark a registration as deregistered
329    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
339/// Manager for AVS registration state
340pub struct RegistrationStateManager {
341    state_file: PathBuf,
342    registrations: AvsRegistrations,
343}
344
345impl RegistrationStateManager {
346    /// Load registration state from the default location
347    ///
348    /// The state file is stored at `~/.tangle/eigenlayer_registrations.json`
349    ///
350    /// # Errors
351    ///
352    /// Returns error if home directory cannot be determined or file cannot be read
353    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    /// Load registration state from the default location, or create a new empty state if it doesn't exist
359    ///
360    /// This is useful for commands that need to read or create registrations without failing
361    /// when the state file doesn't exist yet (e.g., first-time registration).
362    ///
363    /// # Errors
364    ///
365    /// Returns error if home directory cannot be determined or if directory creation fails
366    pub fn load_or_create() -> Result<Self, crate::error::Error> {
367        match Self::load() {
368            Ok(manager) => Ok(manager),
369            Err(_) => {
370                // Failed to load - create a new empty state
371                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    /// Load registration state from a specific file
385    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    /// Get the default state file path
409    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    /// Save registration state to disk
422    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    /// Get all registrations
436    pub fn registrations(&self) -> &AvsRegistrations {
437        &self.registrations
438    }
439
440    /// Get mutable access to registrations
441    pub fn registrations_mut(&mut self) -> &mut AvsRegistrations {
442        &mut self.registrations
443    }
444
445    /// Add a new registration and save to disk
446    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    /// Mark an AVS as deregistered and save to disk
457    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    /// Verify registration status on-chain
470    ///
471    /// Queries the EigenLayer contracts to check if the operator is still registered
472    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        // Get operator address from keystore
484        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        // Create AVS registry reader
503        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        // Check if operator is registered
515        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    /// Reconcile local state with on-chain state
524    ///
525    /// For each locally registered AVS:
526    /// - Queries on-chain to verify registration status
527    /// - Marks as deregistered if not found on-chain
528    ///
529    /// Returns the number of reconciled entries
530    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(&registration).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}