Skip to main content

tempo_precompiles/validator_config/
mod.rs

1//! Validator Config (V1) precompile – manages the on-chain [consensus] validator set.
2//! Will be migrated to Validator Config V2 post T2 hardfork
3//!
4//! [consensus]: <https://docs.tempo.xyz/protocol/blockspace/consensus>
5
6pub mod dispatch;
7
8use tempo_contracts::precompiles::VALIDATOR_CONFIG_ADDRESS;
9pub use tempo_contracts::precompiles::{IValidatorConfig, ValidatorConfigError};
10use tempo_precompiles_macros::{Storable, contract};
11
12use crate::{
13    error::{Result, TempoPrecompileError},
14    ip_validation::ensure_address_is_ip_port,
15    storage::{Handler, Mapping},
16};
17use alloy::primitives::{Address, B256};
18use tracing::trace;
19
20/// On-chain record for a single consensus validator.
21#[derive(Debug, Storable)]
22struct Validator {
23    /// Ed25519 public key (zero ⇒ validator does not exist).
24    public_key: B256,
25    /// Whether the validator participates in consensus.
26    active: bool,
27    /// Position in the `validators_array` vector.
28    index: u64,
29    /// Ethereum address that identifies this validator.
30    validator_address: Address,
31    /// Address where other validators can connect to this validator. Format: `<hostname|ip>:<port>`
32    inbound_address: String,
33    /// IP address for firewall whitelisting by other validators.
34    /// Format: `<ip>:<port>` - must be an IP address, not a hostname.
35    outbound_address: String,
36}
37
38/// Validator Config precompile for managing consensus validators.
39///
40/// The struct fields define the on-chain storage layout; the `#[contract]` macro generates the
41/// storage handlers which provide an ergonomic way to interact with the EVM state.
42#[contract(addr = VALIDATOR_CONFIG_ADDRESS)]
43pub struct ValidatorConfig {
44    /// Contract admin who can add/update/deactivate validators.
45    owner: Address,
46    /// Ordered list of validator addresses (append-only).
47    validators_array: Vec<Address>,
48    /// Validator address → full [`Validator`] record.
49    validators: Mapping<Address, Validator>,
50    /// The epoch at which a fresh DKG ceremony will be triggered.
51    next_dkg_ceremony: u64,
52}
53
54impl ValidatorConfig {
55    /// Initializes the validator config (V1) precompile with an owner.
56    pub fn initialize(&mut self, owner: Address) -> Result<()> {
57        trace!(address=%self.address, %owner, "Initializing validator config precompile");
58
59        // must ensure the account is not empty, by setting some code
60        self.__initialize()?;
61        self.owner.write(owner)
62    }
63
64    /// Returns the current contract owner address.
65    pub fn owner(&self) -> Result<Address> {
66        self.owner.read()
67    }
68
69    /// Returns `Ok(())` if `caller` is the owner, otherwise reverts with `unauthorized`.
70    pub fn check_owner(&self, caller: Address) -> Result<()> {
71        if self.owner()? != caller {
72            return Err(ValidatorConfigError::unauthorized())?;
73        }
74
75        Ok(())
76    }
77
78    /// Transfers contract ownership to `newOwner`. Owner-only.
79    ///
80    /// # Errors
81    /// - `unauthorized` — if `sender` is not the contract owner
82    pub fn change_owner(
83        &mut self,
84        sender: Address,
85        call: IValidatorConfig::changeOwnerCall,
86    ) -> Result<()> {
87        self.check_owner(sender)?;
88        self.owner.write(call.newOwner)
89    }
90
91    /// Returns the total number of registered validators.
92    pub fn validator_count(&self) -> Result<u64> {
93        self.validators_array.len().map(|c| c as u64)
94    }
95
96    /// Returns the validator address stored at `index` in the ordered array.
97    ///
98    /// # Errors
99    /// - `Panic(ArrayOutOfBounds)` — if `index` is out of range
100    pub fn validators_array(&self, index: u64) -> Result<Address> {
101        match self.validators_array.at(index as usize)? {
102            Some(elem) => elem.read(),
103            None => Err(TempoPrecompileError::array_oob()),
104        }
105    }
106
107    /// Returns the full [`IValidatorConfig::Validator`] record for the given address.
108    pub fn validators(&self, validator: Address) -> Result<IValidatorConfig::Validator> {
109        let validator_info = self.validators[validator].read()?;
110        Ok(IValidatorConfig::Validator {
111            publicKey: validator_info.public_key,
112            active: validator_info.active,
113            index: validator_info.index,
114            validatorAddress: validator_info.validator_address,
115            inboundAddress: validator_info.inbound_address,
116            outboundAddress: validator_info.outbound_address,
117        })
118    }
119
120    /// Check if a validator exists by checking if their publicKey is non-zero
121    /// Since ed25519 keys cannot be zero, this is a reliable existence check
122    fn validator_exists(&self, validator: Address) -> Result<bool> {
123        let validator = self.validators[validator].read()?;
124        Ok(!validator.public_key.is_zero())
125    }
126
127    /// Returns all registered validators in index order.
128    pub fn get_validators(&self) -> Result<Vec<IValidatorConfig::Validator>> {
129        let count = self.validators_array.len()?;
130        let mut validators = Vec::with_capacity(count);
131
132        for i in 0..count {
133            // Read validator address from the array at index i
134            let validator_address = self.validators_array[i].read()?;
135
136            let Validator {
137                public_key,
138                active,
139                index,
140                validator_address: _,
141                inbound_address,
142                outbound_address,
143            } = self.validators[validator_address].read()?;
144
145            validators.push(IValidatorConfig::Validator {
146                publicKey: public_key,
147                active,
148                index,
149                validatorAddress: validator_address,
150                inboundAddress: inbound_address,
151                outboundAddress: outbound_address,
152            });
153        }
154
155        Ok(validators)
156    }
157
158    /// Registers a new validator with the given key, addresses, and status. Owner-only.
159    ///
160    /// Validates the public key, checks for duplicates, and ensures both inbound and outbound
161    /// addresses are valid `<ip>:<port>` pairs before appending to the registry.
162    ///
163    /// # Errors
164    /// - `invalid_public_key` — if `publicKey` is zero (sentinel for non-existence)
165    /// - `unauthorized` — if `sender` is not the contract owner
166    /// - `validator_already_exists` — if a validator with `newValidatorAddress` already exists
167    /// - `not_host_port` — if `inboundAddress` is not a valid `<ip>:<port>`
168    /// - `not_ip_port` — if `outboundAddress` is not a valid `<ip>:<port>`
169    pub fn add_validator(
170        &mut self,
171        sender: Address,
172        call: IValidatorConfig::addValidatorCall,
173    ) -> Result<()> {
174        // Reject zero public key - zero is used as sentinel value for non-existence
175        if call.publicKey.is_zero() {
176            return Err(ValidatorConfigError::invalid_public_key())?;
177        }
178
179        // Only owner can create validators
180        self.check_owner(sender)?;
181
182        // Check if validator already exists
183        if self.validator_exists(call.newValidatorAddress)? {
184            return Err(ValidatorConfigError::validator_already_exists())?;
185        }
186
187        // Validate addresses.
188        // T2+: use stable Display formatting for errors.
189        // Pre-T2: preserve legacy Debug formatting for consensus compatibility.
190        if self.storage.spec().is_t2() {
191            ensure_address_is_ip_port(&call.inboundAddress).map_err(|err| {
192                ValidatorConfigError::not_host_port(
193                    "inboundAddress".to_string(),
194                    call.inboundAddress.clone(),
195                    err.to_string(),
196                )
197            })?;
198            ensure_address_is_ip_port(&call.outboundAddress).map_err(|err| {
199                ValidatorConfigError::not_ip_port(
200                    "outboundAddress".to_string(),
201                    call.outboundAddress.clone(),
202                    err.to_string(),
203                )
204            })?;
205        } else {
206            ensure_address_is_ip_port(&call.inboundAddress).map_err(|err| {
207                ValidatorConfigError::not_host_port(
208                    "inboundAddress".to_string(),
209                    call.inboundAddress.clone(),
210                    format!("{err:?}"),
211                )
212            })?;
213            ensure_address_is_ip_port(&call.outboundAddress).map_err(|err| {
214                ValidatorConfigError::not_ip_port(
215                    "outboundAddress".to_string(),
216                    call.outboundAddress.clone(),
217                    format!("{err:?}"),
218                )
219            })?;
220        }
221
222        // Store the new validator in the validators mapping
223        let count = self.validator_count()?;
224        let validator = Validator {
225            public_key: call.publicKey,
226            active: call.active,
227            index: count,
228            validator_address: call.newValidatorAddress,
229            inbound_address: call.inboundAddress,
230            outbound_address: call.outboundAddress,
231        };
232        self.validators[call.newValidatorAddress].write(validator)?;
233
234        // Add the validator public key to the validators array
235        self.validators_array.push(call.newValidatorAddress)
236    }
237
238    /// Updates validator information and optionally rotates to a new address.
239    ///
240    /// If `newValidatorAddress` differs from `sender`, the old record is cleared and the array
241    /// entry is updated to point at the new address.
242    ///
243    /// # Security Note
244    ///
245    /// The field `validator_address` must never be set to a user-controllable address.
246    ///
247    /// This function allows validators to update their own public key mid-epoch, which could
248    /// cause a chain halt at the next epoch boundary if exploited (the DKG manager panics when
249    /// the original key stored in the boundary header cannot be mapped to the modified registry).
250    /// By setting validator addresses to non-user-controllable addresses in the genesis config,
251    /// only the contract owner (admin) can effectively call this function.
252    ///
253    /// # Errors
254    /// - `invalid_public_key` — if `publicKey` is zero (sentinel for non-existence)
255    /// - `validator_not_found` — if `sender` is not a registered validator
256    /// - `validator_already_exists` — if rotating to an address that already has a validator
257    /// - `not_host_port` — if `inboundAddress` is not a valid `<ip>:<port>`
258    /// - `not_ip_port` — if `outboundAddress` is not a valid `<ip>:<port>`
259    pub fn update_validator(
260        &mut self,
261        sender: Address,
262        call: IValidatorConfig::updateValidatorCall,
263    ) -> Result<()> {
264        // Reject zero public key - zero is used as sentinel value for non-existence
265        if call.publicKey.is_zero() {
266            return Err(ValidatorConfigError::invalid_public_key())?;
267        }
268
269        // Validator can update their own info
270        if !self.validator_exists(sender)? {
271            return Err(ValidatorConfigError::validator_not_found())?;
272        }
273
274        // Load the current validator info
275        let old_validator = self.validators[sender].read()?;
276
277        // Check if rotating to a new address
278        if call.newValidatorAddress != sender {
279            if self.validator_exists(call.newValidatorAddress)? {
280                return Err(ValidatorConfigError::validator_already_exists())?;
281            }
282
283            // Update the validators array to point at the new validator address
284            self.validators_array[old_validator.index as usize].write(call.newValidatorAddress)?;
285
286            // Clear the old validator
287            self.validators[sender].delete()?;
288        }
289
290        if self.storage.spec().is_t2() {
291            ensure_address_is_ip_port(&call.inboundAddress).map_err(|err| {
292                ValidatorConfigError::not_host_port(
293                    "inboundAddress".to_string(),
294                    call.inboundAddress.clone(),
295                    err.to_string(),
296                )
297            })?;
298            ensure_address_is_ip_port(&call.outboundAddress).map_err(|err| {
299                ValidatorConfigError::not_ip_port(
300                    "outboundAddress".to_string(),
301                    call.outboundAddress.clone(),
302                    err.to_string(),
303                )
304            })?;
305        } else {
306            ensure_address_is_ip_port(&call.inboundAddress).map_err(|err| {
307                ValidatorConfigError::not_host_port(
308                    "inboundAddress".to_string(),
309                    call.inboundAddress.clone(),
310                    format!("{err:?}"),
311                )
312            })?;
313            ensure_address_is_ip_port(&call.outboundAddress).map_err(|err| {
314                ValidatorConfigError::not_ip_port(
315                    "outboundAddress".to_string(),
316                    call.outboundAddress.clone(),
317                    format!("{err:?}"),
318                )
319            })?;
320        }
321
322        let updated_validator = Validator {
323            public_key: call.publicKey,
324            active: old_validator.active,
325            index: old_validator.index,
326            validator_address: call.newValidatorAddress,
327            inbound_address: call.inboundAddress,
328            outbound_address: call.outboundAddress,
329        };
330
331        self.validators[call.newValidatorAddress].write(updated_validator)
332    }
333
334    /// Sets a validator's active flag by address. Owner-only.
335    ///
336    /// Deprecated: prefer [`Self::change_validator_status_by_index`] to prevent front-running.
337    ///
338    /// # Errors
339    /// - `unauthorized` — if `sender` is not the contract owner
340    /// - `validator_not_found` — if no validator exists at `call.validator`
341    pub fn change_validator_status(
342        &mut self,
343        sender: Address,
344        call: IValidatorConfig::changeValidatorStatusCall,
345    ) -> Result<()> {
346        self.check_owner(sender)?;
347
348        if !self.validator_exists(call.validator)? {
349            return Err(ValidatorConfigError::validator_not_found())?;
350        }
351
352        let mut validator = self.validators[call.validator].read()?;
353        validator.active = call.active;
354        self.validators[call.validator].write(validator)
355    }
356
357    /// Sets a validator's active flag by array index. Owner-only, T1+.
358    ///
359    /// Added in T1 to prevent front-running attacks where a validator rotates its address.
360    ///
361    /// # Errors
362    /// - `unauthorized` — if `sender` is not the contract owner
363    /// - `validator_not_found` — if `call.index` is out of bounds
364    pub fn change_validator_status_by_index(
365        &mut self,
366        sender: Address,
367        call: IValidatorConfig::changeValidatorStatusByIndexCall,
368    ) -> Result<()> {
369        self.check_owner(sender)?;
370
371        // Look up validator address by index
372        let validator_address = match self.validators_array.at(call.index as usize)? {
373            Some(elem) => elem.read()?,
374            None => return Err(ValidatorConfigError::validator_not_found())?,
375        };
376
377        let mut validator = self.validators[validator_address].read()?;
378        validator.active = call.active;
379        self.validators[validator_address].write(validator)
380    }
381
382    /// Returns the epoch at which a fresh DKG ceremony will be triggered.
383    ///
384    /// The fresh DKG ceremony runs in epoch N, and epoch N+1 uses the new DKG polynomial.
385    pub fn get_next_full_dkg_ceremony(&self) -> Result<u64> {
386        self.next_dkg_ceremony.read()
387    }
388
389    /// Alias for [`Self::get_next_full_dkg_ceremony`].
390    pub fn next_dkg_ceremony(&self) -> Result<u64> {
391        self.next_dkg_ceremony.read()
392    }
393
394    /// Sets the epoch at which a fresh DKG ceremony will be triggered. Owner-only.
395    ///
396    /// Epoch N runs the ceremony, and epoch N+1 uses the new DKG polynomial.
397    ///
398    /// # Errors
399    /// - `unauthorized` — if `sender` is not the contract owner
400    pub fn set_next_full_dkg_ceremony(
401        &mut self,
402        sender: Address,
403        call: IValidatorConfig::setNextFullDkgCeremonyCall,
404    ) -> Result<()> {
405        self.check_owner(sender)?;
406        self.next_dkg_ceremony.write(call.epoch)
407    }
408}
409
410#[cfg(test)]
411mod tests {
412    use super::*;
413    use crate::storage::{StorageCtx, hashmap::HashMapStorageProvider};
414    use alloy::primitives::Address;
415    use alloy_primitives::FixedBytes;
416
417    #[test]
418    fn test_owner_initialization_and_change() -> eyre::Result<()> {
419        let mut storage = HashMapStorageProvider::new(1);
420        let owner1 = Address::random();
421        let owner2 = Address::random();
422        StorageCtx::enter(&mut storage, || {
423            let mut validator_config = ValidatorConfig::new();
424
425            // Initialize with owner1
426            validator_config.initialize(owner1)?;
427
428            // Check that owner is owner1
429            let current_owner = validator_config.owner()?;
430            assert_eq!(
431                current_owner, owner1,
432                "Owner should be owner1 after initialization"
433            );
434
435            // Change owner to owner2
436            validator_config.change_owner(
437                owner1,
438                IValidatorConfig::changeOwnerCall { newOwner: owner2 },
439            )?;
440
441            // Check that owner is now owner2
442            let current_owner = validator_config.owner()?;
443            assert_eq!(current_owner, owner2, "Owner should be owner2 after change");
444
445            Ok(())
446        })
447    }
448
449    #[test]
450    fn test_owner_only_functions() -> eyre::Result<()> {
451        let mut storage = HashMapStorageProvider::new(1);
452        let owner1 = Address::random();
453        let owner2 = Address::random();
454        let validator1 = Address::random();
455        let validator2 = Address::random();
456        StorageCtx::enter(&mut storage, || {
457            let mut validator_config = ValidatorConfig::new();
458
459            // Initialize with owner1
460            validator_config.initialize(owner1)?;
461
462            // Owner1 adds a validator - should succeed
463            let public_key = FixedBytes::<32>::from([0x44; 32]);
464            validator_config.add_validator(
465                owner1,
466                IValidatorConfig::addValidatorCall {
467                    newValidatorAddress: validator1,
468                    publicKey: public_key,
469                    inboundAddress: "192.168.1.1:8000".to_string(),
470                    active: true,
471                    outboundAddress: "192.168.1.1:9000".to_string(),
472                },
473            )?;
474
475            // Verify validator was added
476            let validators = validator_config.get_validators()?;
477            assert_eq!(validators.len(), 1, "Should have 1 validator");
478            assert_eq!(validators[0].validatorAddress, validator1);
479            assert_eq!(validators[0].publicKey, public_key);
480            assert!(validators[0].active, "New validator should be active");
481
482            // Owner1 changes validator status - should succeed
483            validator_config.change_validator_status(
484                owner1,
485                IValidatorConfig::changeValidatorStatusCall {
486                    validator: validator1,
487                    active: false,
488                },
489            )?;
490
491            // Verify status was changed
492            let validators = validator_config.get_validators()?;
493            assert!(!validators[0].active, "Validator should be inactive");
494
495            // Owner2 (non-owner) tries to add validator - should fail
496            let res = validator_config.add_validator(
497                owner2,
498                IValidatorConfig::addValidatorCall {
499                    newValidatorAddress: validator2,
500                    publicKey: FixedBytes::<32>::from([0x66; 32]),
501                    inboundAddress: "192.168.1.2:8000".to_string(),
502                    active: true,
503                    outboundAddress: "192.168.1.2:9000".to_string(),
504                },
505            );
506            assert_eq!(res, Err(ValidatorConfigError::unauthorized().into()));
507
508            // Owner2 (non-owner) tries to change validator status - should fail
509            let res = validator_config.change_validator_status(
510                owner2,
511                IValidatorConfig::changeValidatorStatusCall {
512                    validator: validator1,
513                    active: true,
514                },
515            );
516            assert_eq!(res, Err(ValidatorConfigError::unauthorized().into()));
517
518            Ok(())
519        })
520    }
521
522    #[test]
523    fn test_validator_lifecycle() -> eyre::Result<()> {
524        let mut storage = HashMapStorageProvider::new(1);
525        StorageCtx::enter(&mut storage, || {
526            let owner = Address::from([0x01; 20]);
527
528            let mut validator_config = ValidatorConfig::new();
529            validator_config.initialize(owner)?;
530
531            let validator1 = Address::from([0x11; 20]);
532            let public_key1 = FixedBytes::<32>::from([0x21; 32]);
533            let inbound1 = "192.168.1.1:8000".to_string();
534            let outbound1 = "192.168.1.1:9000".to_string();
535            validator_config.add_validator(
536                owner,
537                IValidatorConfig::addValidatorCall {
538                    newValidatorAddress: validator1,
539                    publicKey: public_key1,
540                    inboundAddress: inbound1.clone(),
541                    active: true,
542                    outboundAddress: outbound1,
543                },
544            )?;
545
546            // Try adding duplicate validator - should fail
547            let result = validator_config.add_validator(
548                owner,
549                IValidatorConfig::addValidatorCall {
550                    newValidatorAddress: validator1,
551                    publicKey: FixedBytes::<32>::from([0x22; 32]),
552                    inboundAddress: "192.168.1.1:8000".to_string(),
553                    active: true,
554                    outboundAddress: "192.168.1.1:9000".to_string(),
555                },
556            );
557            assert_eq!(
558                result,
559                Err(ValidatorConfigError::validator_already_exists().into()),
560                "Should return ValidatorAlreadyExists error"
561            );
562
563            // Add 4 more unique validators
564            let validator2 = Address::from([0x12; 20]);
565            let public_key2 = FixedBytes::<32>::from([0x22; 32]);
566            validator_config.add_validator(
567                owner,
568                IValidatorConfig::addValidatorCall {
569                    newValidatorAddress: validator2,
570                    publicKey: public_key2,
571                    inboundAddress: "192.168.1.2:8000".to_string(),
572                    active: true,
573                    outboundAddress: "192.168.1.2:9000".to_string(),
574                },
575            )?;
576
577            let validator3 = Address::from([0x13; 20]);
578            let public_key3 = FixedBytes::<32>::from([0x23; 32]);
579            validator_config.add_validator(
580                owner,
581                IValidatorConfig::addValidatorCall {
582                    newValidatorAddress: validator3,
583                    publicKey: public_key3,
584                    inboundAddress: "192.168.1.3:8000".to_string(),
585                    active: false,
586                    outboundAddress: "192.168.1.3:9000".to_string(),
587                },
588            )?;
589
590            let validator4 = Address::from([0x14; 20]);
591            let public_key4 = FixedBytes::<32>::from([0x24; 32]);
592            validator_config.add_validator(
593                owner,
594                IValidatorConfig::addValidatorCall {
595                    newValidatorAddress: validator4,
596                    publicKey: public_key4,
597                    inboundAddress: "192.168.1.4:8000".to_string(),
598                    active: true,
599                    outboundAddress: "192.168.1.4:9000".to_string(),
600                },
601            )?;
602
603            let validator5 = Address::from([0x15; 20]);
604            let public_key5 = FixedBytes::<32>::from([0x25; 32]);
605            validator_config.add_validator(
606                owner,
607                IValidatorConfig::addValidatorCall {
608                    newValidatorAddress: validator5,
609                    publicKey: public_key5,
610                    inboundAddress: "192.168.1.5:8000".to_string(),
611                    active: true,
612                    outboundAddress: "192.168.1.5:9000".to_string(),
613                },
614            )?;
615
616            // Get all validators
617            let mut validators = validator_config.get_validators()?;
618
619            // Verify count
620            assert_eq!(validators.len(), 5, "Should have 5 validators");
621
622            // Sort by validator address for consistent checking
623            validators.sort_by_key(|v| v.validatorAddress);
624
625            // Verify each validator
626            assert_eq!(validators[0].validatorAddress, validator1);
627            assert_eq!(validators[0].publicKey, public_key1);
628            assert_eq!(validators[0].inboundAddress, inbound1);
629            assert!(validators[0].active);
630
631            assert_eq!(validators[1].validatorAddress, validator2);
632            assert_eq!(validators[1].publicKey, public_key2);
633            assert_eq!(validators[1].inboundAddress, "192.168.1.2:8000");
634            assert!(validators[1].active);
635
636            assert_eq!(validators[2].validatorAddress, validator3);
637            assert_eq!(validators[2].publicKey, public_key3);
638            assert_eq!(validators[2].inboundAddress, "192.168.1.3:8000");
639            assert!(!validators[2].active);
640
641            assert_eq!(validators[3].validatorAddress, validator4);
642            assert_eq!(validators[3].publicKey, public_key4);
643            assert_eq!(validators[3].inboundAddress, "192.168.1.4:8000");
644            assert!(validators[3].active);
645
646            assert_eq!(validators[4].validatorAddress, validator5);
647            assert_eq!(validators[4].publicKey, public_key5);
648            assert_eq!(validators[4].inboundAddress, "192.168.1.5:8000");
649            assert!(validators[4].active);
650
651            // Validator1 updates from long to short address (tests update_string slot clearing)
652            let public_key1_new = FixedBytes::<32>::from([0x31; 32]);
653            let short_inbound1 = "10.0.0.1:8000".to_string();
654            let short_outbound1 = "10.0.0.1:9000".to_string();
655            validator_config.update_validator(
656                validator1,
657                IValidatorConfig::updateValidatorCall {
658                    newValidatorAddress: validator1,
659                    publicKey: public_key1_new,
660                    inboundAddress: short_inbound1.clone(),
661                    outboundAddress: short_outbound1,
662                },
663            )?;
664
665            // Validator2 rotates to new address, keeps IP and publicKey
666            let validator2_new = Address::from([0x22; 20]);
667            validator_config.update_validator(
668                validator2,
669                IValidatorConfig::updateValidatorCall {
670                    newValidatorAddress: validator2_new,
671                    publicKey: public_key2,
672                    inboundAddress: "192.168.1.2:8000".to_string(),
673                    outboundAddress: "192.168.1.2:9000".to_string(),
674                },
675            )?;
676
677            // Validator3 rotates to new address with long host (tests delete_string on old slot)
678            let validator3_new = Address::from([0x23; 20]);
679            let long_inbound3 = "192.169.1.3:8000".to_string();
680            let long_outbound3 = "192.168.1.3:9000".to_string();
681            validator_config.update_validator(
682                validator3,
683                IValidatorConfig::updateValidatorCall {
684                    newValidatorAddress: validator3_new,
685                    publicKey: public_key3,
686                    inboundAddress: long_inbound3.clone(),
687                    outboundAddress: long_outbound3,
688                },
689            )?;
690
691            // Get all validators again
692            let mut validators = validator_config.get_validators()?;
693
694            // Should still have 5 validators
695            assert_eq!(validators.len(), 5, "Should still have 5 validators");
696
697            // Sort by validator address
698            validators.sort_by_key(|v| v.validatorAddress);
699
700            // Verify validator1 - updated from long to short address
701            assert_eq!(validators[0].validatorAddress, validator1);
702            assert_eq!(
703                validators[0].publicKey, public_key1_new,
704                "PublicKey should be updated"
705            );
706            assert_eq!(
707                validators[0].inboundAddress, short_inbound1,
708                "Address should be updated to short"
709            );
710            assert!(validators[0].active);
711
712            // Verify validator4 - unchanged
713            assert_eq!(validators[1].validatorAddress, validator4);
714            assert_eq!(validators[1].publicKey, public_key4);
715            assert_eq!(validators[1].inboundAddress, "192.168.1.4:8000");
716            assert!(validators[1].active);
717
718            // Verify validator5 - unchanged
719            assert_eq!(validators[2].validatorAddress, validator5);
720            assert_eq!(validators[2].publicKey, public_key5);
721            assert_eq!(validators[2].inboundAddress, "192.168.1.5:8000");
722            assert!(validators[2].active);
723
724            // Verify validator2_new - rotated address, kept IP and publicKey
725            assert_eq!(validators[3].validatorAddress, validator2_new);
726            assert_eq!(
727                validators[3].publicKey, public_key2,
728                "PublicKey should be same"
729            );
730            assert_eq!(
731                validators[3].inboundAddress, "192.168.1.2:8000",
732                "IP should be same"
733            );
734            assert!(validators[3].active);
735
736            // Verify validator3_new - rotated address with long host, kept publicKey
737            assert_eq!(validators[4].validatorAddress, validator3_new);
738            assert_eq!(
739                validators[4].publicKey, public_key3,
740                "PublicKey should be same"
741            );
742            assert_eq!(
743                validators[4].inboundAddress, long_inbound3,
744                "Address should be updated to long"
745            );
746            assert!(!validators[4].active);
747
748            Ok(())
749        })
750    }
751
752    #[test]
753    fn test_owner_cannot_update_validator() -> eyre::Result<()> {
754        let mut storage = HashMapStorageProvider::new(1);
755        let owner = Address::random();
756        let validator = Address::random();
757        StorageCtx::enter(&mut storage, || {
758            let mut validator_config = ValidatorConfig::new();
759            validator_config.initialize(owner)?;
760
761            // Owner adds a validator
762            let public_key = FixedBytes::<32>::from([0x21; 32]);
763            validator_config.add_validator(
764                owner,
765                IValidatorConfig::addValidatorCall {
766                    newValidatorAddress: validator,
767                    publicKey: public_key,
768                    inboundAddress: "192.168.1.1:8000".to_string(),
769                    active: true,
770                    outboundAddress: "192.168.1.1:9000".to_string(),
771                },
772            )?;
773
774            // Owner tries to update validator - should fail
775            let result = validator_config.update_validator(
776                owner,
777                IValidatorConfig::updateValidatorCall {
778                    newValidatorAddress: validator,
779                    publicKey: FixedBytes::<32>::from([0x22; 32]),
780                    inboundAddress: "10.0.0.1:8000".to_string(),
781                    outboundAddress: "10.0.0.1:9000".to_string(),
782                },
783            );
784
785            assert_eq!(
786                result,
787                Err(ValidatorConfigError::validator_not_found().into()),
788                "Should return ValidatorNotFound error"
789            );
790
791            Ok(())
792        })
793    }
794
795    #[test]
796    fn test_validator_rotation_clears_all_slots() -> eyre::Result<()> {
797        let mut storage = HashMapStorageProvider::new(1);
798        let owner = Address::random();
799        let validator1 = Address::random();
800        let validator2 = Address::random();
801        StorageCtx::enter(&mut storage, || {
802            let mut validator_config = ValidatorConfig::new();
803            validator_config.initialize(owner)?;
804
805            // Add validator with long inbound address that uses multiple slots
806            let long_inbound = "192.168.1.1:8000".to_string();
807            let long_outbound = "192.168.1.1:9000".to_string();
808            let public_key = FixedBytes::<32>::from([0x21; 32]);
809
810            validator_config.add_validator(
811                owner,
812                IValidatorConfig::addValidatorCall {
813                    newValidatorAddress: validator1,
814                    publicKey: public_key,
815                    inboundAddress: long_inbound,
816                    active: true,
817                    outboundAddress: long_outbound,
818                },
819            )?;
820
821            // Rotate to new address with shorter addresses
822            validator_config.update_validator(
823                validator1,
824                IValidatorConfig::updateValidatorCall {
825                    newValidatorAddress: validator2,
826                    publicKey: public_key,
827                    inboundAddress: "10.0.0.1:8000".to_string(),
828                    outboundAddress: "10.0.0.1:9000".to_string(),
829                },
830            )?;
831
832            // Verify old slots are cleared by checking storage directly
833            let validator = validator_config.validators[validator1].read()?;
834
835            // Assert all validator fields are cleared/zeroed
836            assert_eq!(
837                validator.public_key,
838                B256::ZERO,
839                "Old validator public key should be cleared"
840            );
841            assert_eq!(
842                validator.validator_address,
843                Address::ZERO,
844                "Old validator address should be cleared"
845            );
846            assert_eq!(validator.index, 0, "Old validator index should be cleared");
847            assert!(!validator.active, "Old validator should be inactive");
848            assert_eq!(
849                validator.inbound_address,
850                String::default(),
851                "Old validator inbound address should be cleared"
852            );
853            assert_eq!(
854                validator.outbound_address,
855                String::default(),
856                "Old validator outbound address should be cleared"
857            );
858
859            Ok(())
860        })
861    }
862
863    #[test]
864    fn test_next_dkg_ceremony() -> eyre::Result<()> {
865        let mut storage = HashMapStorageProvider::new(1);
866        let owner = Address::random();
867        let non_owner = Address::random();
868        StorageCtx::enter(&mut storage, || {
869            let mut validator_config = ValidatorConfig::new();
870            validator_config.initialize(owner)?;
871
872            // Default value is 0
873            assert_eq!(validator_config.get_next_full_dkg_ceremony()?, 0);
874
875            // Owner can set the value
876            validator_config.set_next_full_dkg_ceremony(
877                owner,
878                IValidatorConfig::setNextFullDkgCeremonyCall { epoch: 42 },
879            )?;
880            assert_eq!(validator_config.get_next_full_dkg_ceremony()?, 42);
881
882            // Non-owner cannot set the value
883            let result = validator_config.set_next_full_dkg_ceremony(
884                non_owner,
885                IValidatorConfig::setNextFullDkgCeremonyCall { epoch: 100 },
886            );
887            assert_eq!(result, Err(ValidatorConfigError::unauthorized().into()));
888
889            // Value unchanged after failed attempt
890            assert_eq!(validator_config.get_next_full_dkg_ceremony()?, 42);
891
892            Ok(())
893        })
894    }
895
896    #[test]
897    fn test_ipv4_with_port_is_host_port() {
898        ensure_address_is_ip_port("127.0.0.1:8000").unwrap();
899    }
900
901    #[test]
902    fn test_ipv6_with_port_is_host_port() {
903        ensure_address_is_ip_port("[::1]:8000").unwrap();
904    }
905
906    #[test]
907    fn test_add_validator_rejects_zero_public_key() -> eyre::Result<()> {
908        let mut storage = HashMapStorageProvider::new(1);
909        let owner = Address::random();
910        let validator = Address::random();
911        StorageCtx::enter(&mut storage, || {
912            let mut validator_config = ValidatorConfig::new();
913            validator_config.initialize(owner)?;
914
915            let zero_public_key = FixedBytes::<32>::ZERO;
916            let result = validator_config.add_validator(
917                owner,
918                IValidatorConfig::addValidatorCall {
919                    newValidatorAddress: validator,
920                    publicKey: zero_public_key,
921                    inboundAddress: "192.168.1.1:8000".to_string(),
922                    active: true,
923                    outboundAddress: "192.168.1.1:9000".to_string(),
924                },
925            );
926
927            assert_eq!(
928                result,
929                Err(ValidatorConfigError::invalid_public_key().into()),
930                "Should reject zero public key"
931            );
932
933            // Verify no validator was added
934            let validators = validator_config.get_validators()?;
935            assert_eq!(validators.len(), 0, "Should have no validators");
936
937            Ok(())
938        })
939    }
940
941    #[test]
942    fn test_update_validator_rejects_zero_public_key() -> eyre::Result<()> {
943        let mut storage = HashMapStorageProvider::new(1);
944        let owner = Address::random();
945        let validator = Address::random();
946        StorageCtx::enter(&mut storage, || {
947            let mut validator_config = ValidatorConfig::new();
948            validator_config.initialize(owner)?;
949
950            let original_public_key = FixedBytes::<32>::from([0x44; 32]);
951            validator_config.add_validator(
952                owner,
953                IValidatorConfig::addValidatorCall {
954                    newValidatorAddress: validator,
955                    publicKey: original_public_key,
956                    inboundAddress: "192.168.1.1:8000".to_string(),
957                    active: true,
958                    outboundAddress: "192.168.1.1:9000".to_string(),
959                },
960            )?;
961
962            let zero_public_key = FixedBytes::<32>::ZERO;
963            let result = validator_config.update_validator(
964                validator,
965                IValidatorConfig::updateValidatorCall {
966                    newValidatorAddress: validator,
967                    publicKey: zero_public_key,
968                    inboundAddress: "192.168.1.1:8000".to_string(),
969                    outboundAddress: "192.168.1.1:9000".to_string(),
970                },
971            );
972
973            assert_eq!(
974                result,
975                Err(ValidatorConfigError::invalid_public_key().into()),
976                "Should reject zero public key in update"
977            );
978
979            // Verify original public key is preserved
980            let validators = validator_config.get_validators()?;
981            assert_eq!(validators.len(), 1, "Should still have 1 validator");
982            assert_eq!(
983                validators[0].publicKey, original_public_key,
984                "Original public key should be preserved"
985            );
986
987            Ok(())
988        })
989    }
990
991    #[test]
992    fn test_validators_array_returns_correct_address() -> eyre::Result<()> {
993        let mut storage = HashMapStorageProvider::new(1);
994        let owner = Address::random();
995        let validator1 = Address::random();
996        let validator2 = Address::random();
997        StorageCtx::enter(&mut storage, || {
998            let mut validator_config = ValidatorConfig::new();
999            validator_config.initialize(owner)?;
1000
1001            let public_key1 = FixedBytes::<32>::from([0x11; 32]);
1002            let public_key2 = FixedBytes::<32>::from([0x22; 32]);
1003
1004            // Add validators
1005            validator_config.add_validator(
1006                owner,
1007                IValidatorConfig::addValidatorCall {
1008                    newValidatorAddress: validator1,
1009                    publicKey: public_key1,
1010                    inboundAddress: "192.168.1.1:8000".to_string(),
1011                    active: true,
1012                    outboundAddress: "192.168.1.1:9000".to_string(),
1013                },
1014            )?;
1015
1016            validator_config.add_validator(
1017                owner,
1018                IValidatorConfig::addValidatorCall {
1019                    newValidatorAddress: validator2,
1020                    publicKey: public_key2,
1021                    inboundAddress: "192.168.1.2:8000".to_string(),
1022                    active: true,
1023                    outboundAddress: "192.168.1.2:9000".to_string(),
1024                },
1025            )?;
1026
1027            // validators_array should return the actual addresses, not default
1028            assert_eq!(validator_config.validators_array(0)?, validator1);
1029            assert_eq!(validator_config.validators_array(1)?, validator2);
1030
1031            // Verify they're not default
1032            assert_ne!(validator_config.validators_array(0)?, Address::ZERO);
1033            assert_ne!(validator_config.validators_array(1)?, Address::ZERO);
1034
1035            Ok(())
1036        })
1037    }
1038
1039    #[test]
1040    fn test_next_dkg_ceremony_returns_correct_value() -> eyre::Result<()> {
1041        let mut storage = HashMapStorageProvider::new(1);
1042        let owner = Address::random();
1043        StorageCtx::enter(&mut storage, || {
1044            let mut validator_config = ValidatorConfig::new();
1045            validator_config.initialize(owner)?;
1046
1047            // Default should be 0
1048            assert_eq!(validator_config.get_next_full_dkg_ceremony()?, 0);
1049
1050            // Set to a specific value
1051            validator_config.set_next_full_dkg_ceremony(
1052                owner,
1053                IValidatorConfig::setNextFullDkgCeremonyCall { epoch: 100 },
1054            )?;
1055
1056            // Should return the set value, not 0 or 1
1057            let result = validator_config.get_next_full_dkg_ceremony()?;
1058            assert_eq!(result, 100);
1059            assert_ne!(result, 0);
1060            assert_ne!(result, 1);
1061
1062            Ok(())
1063        })
1064    }
1065}