tempo_precompiles/validator_config/
mod.rs

1pub mod dispatch;
2
3use tempo_contracts::precompiles::VALIDATOR_CONFIG_ADDRESS;
4pub use tempo_contracts::precompiles::{IValidatorConfig, ValidatorConfigError};
5use tempo_precompiles_macros::{Storable, contract};
6
7use crate::{
8    error::TempoPrecompileError,
9    storage::{Mapping, PrecompileStorageProvider, Slot, VecSlotExt},
10};
11use alloy::primitives::{Address, B256, Bytes};
12use revm::state::Bytecode;
13use tracing::trace;
14
15/// Validator information
16#[derive(Debug, Storable)]
17struct Validator {
18    public_key: B256,
19    active: bool,
20    index: u64,
21    validator_address: Address,
22    /// Address where other validators can connect to this validator.
23    /// Format: `<hostname|ip>:<port>`
24    inbound_address: String,
25    /// IP address for firewall whitelisting by other validators.
26    /// Format: `<ip>:<port>` - must be an IP address, not a hostname.
27    outbound_address: String,
28}
29
30/// Helper type to easily interact with the `validators_array`
31type ValidatorsArray = Slot<Vec<Address>>;
32
33/// Validator Config precompile for managing consensus validators
34#[contract]
35pub struct ValidatorConfig {
36    owner: Address,
37    validator_count: u64,
38    validators_array: Vec<Address>,
39    validators: Mapping<Address, Validator>,
40}
41
42impl<'a, S: PrecompileStorageProvider> ValidatorConfig<'a, S> {
43    pub fn new(storage: &'a mut S) -> Self {
44        Self::_new(VALIDATOR_CONFIG_ADDRESS, storage)
45    }
46
47    /// Initialize the precompile with an owner
48    pub fn initialize(&mut self, owner: Address) -> Result<(), TempoPrecompileError> {
49        trace!(address=%self.address, %owner, "Initializing validator config precompile");
50
51        // must ensure the account is not empty, by setting some code
52        self.storage.set_code(
53            self.address,
54            Bytecode::new_legacy(Bytes::from_static(&[0xef])),
55        )?;
56
57        self.sstore_owner(owner)?;
58
59        Ok(())
60    }
61
62    /// Internal helper to get owner
63    pub fn owner(&mut self) -> Result<Address, TempoPrecompileError> {
64        self.sload_owner()
65    }
66
67    /// Check if caller is the owner
68    pub fn check_owner(&mut self, caller: Address) -> Result<(), TempoPrecompileError> {
69        if self.owner()? != caller {
70            return Err(ValidatorConfigError::unauthorized())?;
71        }
72        Ok(())
73    }
74
75    /// Change the owner (owner only)
76    pub fn change_owner(
77        &mut self,
78        sender: Address,
79        call: IValidatorConfig::changeOwnerCall,
80    ) -> Result<(), TempoPrecompileError> {
81        self.check_owner(sender)?;
82        self.sstore_owner(call.newOwner)
83    }
84
85    /// Get the current validator count
86    fn validator_count(&mut self) -> Result<u64, TempoPrecompileError> {
87        self.sload_validator_count()
88    }
89
90    /// Check if a validator exists by checking if their publicKey is non-zero
91    /// Since ed25519 keys cannot be zero, this is a reliable existence check
92    fn validator_exists(&mut self, validator: Address) -> Result<bool, TempoPrecompileError> {
93        let validator = self.sload_validators(validator)?;
94        Ok(!validator.public_key.is_zero())
95    }
96
97    /// Get all validators (view function)
98    pub fn get_validators(
99        &mut self,
100        _call: IValidatorConfig::getValidatorsCall,
101    ) -> Result<Vec<IValidatorConfig::Validator>, TempoPrecompileError> {
102        let count = self.validator_count()?;
103        let mut validators = Vec::new();
104
105        let validators_array = ValidatorsArray::new(slots::VALIDATORS_ARRAY);
106        for i in 0..count {
107            // Read validator address from the array at index i
108            let validator_address = validators_array.read_at(self, i as usize)?;
109
110            let Validator {
111                public_key,
112                active,
113                index,
114                validator_address: _,
115                inbound_address,
116                outbound_address,
117            } = self.sload_validators(validator_address)?;
118
119            validators.push(IValidatorConfig::Validator {
120                publicKey: public_key,
121                active,
122                index,
123                validatorAddress: validator_address,
124                inboundAddress: inbound_address,
125                outboundAddress: outbound_address,
126            });
127        }
128
129        Ok(validators)
130    }
131
132    /// Add a new validator (owner only)
133    pub fn add_validator(
134        &mut self,
135        sender: Address,
136        call: IValidatorConfig::addValidatorCall,
137    ) -> Result<(), TempoPrecompileError> {
138        // Only owner can create validators
139        self.check_owner(sender)?;
140
141        // Check if validator already exists
142        if self.validator_exists(call.newValidatorAddress)? {
143            return Err(ValidatorConfigError::validator_already_exists())?;
144        }
145
146        // Validate addresses
147        ensure_address_is_ip_port(&call.inboundAddress).map_err(|err| {
148            ValidatorConfigError::not_host_port(
149                "inboundAddress".to_string(),
150                call.inboundAddress.clone(),
151                format!("{err:?}"),
152            )
153        })?;
154
155        ensure_address_is_ip_port(&call.outboundAddress).map_err(|err| {
156            ValidatorConfigError::not_ip_port(
157                "outboundAddress".to_string(),
158                call.outboundAddress.clone(),
159                format!("{err:?}"),
160            )
161        })?;
162
163        // Store the new validator in the validators mapping
164        let count = self.validator_count()?;
165        let validator = Validator {
166            public_key: call.publicKey,
167            active: call.active,
168            index: count,
169            validator_address: call.newValidatorAddress,
170            inbound_address: call.inboundAddress,
171            outbound_address: call.outboundAddress,
172        };
173        self.sstore_validators(call.newValidatorAddress, validator)?;
174
175        // Add the validator public key to the validators array
176        ValidatorsArray::new(slots::VALIDATORS_ARRAY).push(self, call.newValidatorAddress)?;
177
178        // Increment the validator count
179        self.sstore_validator_count(
180            count
181                .checked_add(1)
182                .ok_or(TempoPrecompileError::under_overflow())?,
183        )?;
184
185        Ok(())
186    }
187
188    /// Update validator information (and optionally rotate to new address)
189    pub fn update_validator(
190        &mut self,
191        sender: Address,
192        call: IValidatorConfig::updateValidatorCall,
193    ) -> Result<(), TempoPrecompileError> {
194        // Validator can update their own info
195        if !self.validator_exists(sender)? {
196            return Err(ValidatorConfigError::validator_not_found())?;
197        }
198
199        // Load the current validator info
200        let old_validator = self.sload_validators(sender)?;
201
202        // Check if rotating to a new address
203        if call.newValidatorAddress != sender {
204            if self.validator_exists(call.newValidatorAddress)? {
205                return Err(ValidatorConfigError::validator_already_exists())?;
206            }
207
208            // Update the validators array to point at the new validator address
209            ValidatorsArray::new(slots::VALIDATORS_ARRAY).write_at(
210                self,
211                old_validator.index as usize,
212                call.newValidatorAddress,
213            )?;
214
215            // Clear the old validator
216            self.clear_validators(sender)?;
217        }
218
219        ensure_address_is_ip_port(&call.inboundAddress).map_err(|err| {
220            ValidatorConfigError::not_host_port(
221                "inboundAddress".to_string(),
222                call.inboundAddress.clone(),
223                format!("{err:?}"),
224            )
225        })?;
226
227        ensure_address_is_ip_port(&call.outboundAddress).map_err(|err| {
228            ValidatorConfigError::not_ip_port(
229                "outboundAddress".to_string(),
230                call.outboundAddress.clone(),
231                format!("{err:?}"),
232            )
233        })?;
234
235        let updated_validator = Validator {
236            public_key: call.publicKey,
237            active: old_validator.active,
238            index: old_validator.index,
239            validator_address: call.newValidatorAddress,
240            inbound_address: call.inboundAddress,
241            outbound_address: call.outboundAddress,
242        };
243
244        self.sstore_validators(call.newValidatorAddress, updated_validator)?;
245
246        Ok(())
247    }
248
249    /// Change validator active status (owner only)
250    pub fn change_validator_status(
251        &mut self,
252        sender: Address,
253        call: IValidatorConfig::changeValidatorStatusCall,
254    ) -> Result<(), TempoPrecompileError> {
255        self.check_owner(sender)?;
256
257        if !self.validator_exists(call.validator)? {
258            return Err(ValidatorConfigError::validator_not_found())?;
259        }
260
261        let mut validator = self.sload_validators(call.validator)?;
262        validator.active = call.active;
263        self.sstore_validators(call.validator, validator)?;
264
265        Ok(())
266    }
267}
268
269#[derive(Debug, thiserror::Error)]
270#[error("input was not of the form `<ip>:<port>`")]
271pub struct IpWithPortParseError {
272    #[from]
273    source: std::net::AddrParseError,
274}
275
276pub fn ensure_address_is_ip_port(input: &str) -> Result<(), IpWithPortParseError> {
277    // Only accept IP addresses (v4 or v6) with port
278    input.parse::<std::net::SocketAddr>()?;
279    Ok(())
280}
281
282#[cfg(test)]
283mod tests {
284    use super::*;
285    use crate::storage::hashmap::HashMapStorageProvider;
286    use alloy::primitives::Address;
287    use alloy_primitives::FixedBytes;
288
289    #[test]
290    fn test_owner_initialization_and_change() {
291        let mut storage = HashMapStorageProvider::new(1);
292        let owner1 = Address::from([0x11; 20]);
293        let owner2 = Address::from([0x22; 20]);
294
295        let mut validator_config = ValidatorConfig::new(&mut storage);
296
297        // Initialize with owner1
298        validator_config.initialize(owner1).unwrap();
299
300        // Check that owner is owner1
301        let current_owner = validator_config.owner().unwrap();
302        assert_eq!(
303            current_owner, owner1,
304            "Owner should be owner1 after initialization"
305        );
306
307        // Change owner to owner2
308        validator_config
309            .change_owner(
310                owner1,
311                IValidatorConfig::changeOwnerCall { newOwner: owner2 },
312            )
313            .expect("Should change owner");
314
315        // Check that owner is now owner2
316        let current_owner = validator_config.owner().unwrap();
317        assert_eq!(current_owner, owner2, "Owner should be owner2 after change");
318    }
319
320    #[test]
321    fn test_owner_only_functions() {
322        let mut storage = HashMapStorageProvider::new(1);
323        let owner1 = Address::from([0x11; 20]);
324        let owner2 = Address::from([0x22; 20]);
325        let validator1 = Address::from([0x33; 20]);
326
327        let mut validator_config = ValidatorConfig::new(&mut storage);
328
329        // Initialize with owner1
330        validator_config.initialize(owner1).unwrap();
331
332        // Owner1 adds a validator - should succeed
333        let public_key = FixedBytes::<32>::from([0x44; 32]);
334        let result = validator_config.add_validator(
335            owner1,
336            IValidatorConfig::addValidatorCall {
337                newValidatorAddress: validator1,
338                publicKey: public_key,
339                inboundAddress: "192.168.1.1:8000".to_string(),
340                active: true,
341                outboundAddress: "192.168.1.1:9000".to_string(),
342            },
343        );
344        assert!(result.is_ok(), "Owner should be able to add validator");
345
346        // Verify validator was added
347        let validators = validator_config
348            .get_validators(IValidatorConfig::getValidatorsCall {})
349            .expect("Should get validators");
350        assert_eq!(validators.len(), 1, "Should have 1 validator");
351        assert_eq!(validators[0].validatorAddress, validator1);
352        assert_eq!(validators[0].publicKey, public_key);
353        assert!(validators[0].active, "New validator should be active");
354
355        // Owner1 changes validator status - should succeed
356        let result = validator_config.change_validator_status(
357            owner1,
358            IValidatorConfig::changeValidatorStatusCall {
359                validator: validator1,
360                active: false,
361            },
362        );
363        assert!(
364            result.is_ok(),
365            "Owner should be able to change validator status"
366        );
367
368        // Verify status was changed
369        let validators = validator_config
370            .get_validators(IValidatorConfig::getValidatorsCall {})
371            .expect("Should get validators");
372        assert!(!validators[0].active, "Validator should be inactive");
373
374        // Owner2 (non-owner) tries to add validator - should fail
375        let validator2 = Address::from([0x55; 20]);
376        let result = validator_config.add_validator(
377            owner2,
378            IValidatorConfig::addValidatorCall {
379                newValidatorAddress: validator2,
380                publicKey: FixedBytes::<32>::from([0x66; 32]),
381                inboundAddress: "192.168.1.2:8000".to_string(),
382                active: true,
383                outboundAddress: "192.168.1.2:9000".to_string(),
384            },
385        );
386        assert!(
387            result.is_err(),
388            "Non-owner should not be able to add validator"
389        );
390        assert_eq!(
391            result.unwrap_err(),
392            ValidatorConfigError::unauthorized().into(),
393            "Should return Unauthorized error"
394        );
395
396        // Owner2 (non-owner) tries to change validator status - should fail
397        let result = validator_config.change_validator_status(
398            owner2,
399            IValidatorConfig::changeValidatorStatusCall {
400                validator: validator1,
401                active: true,
402            },
403        );
404        assert!(
405            result.is_err(),
406            "Non-owner should not be able to change validator status"
407        );
408        assert_eq!(
409            result.unwrap_err(),
410            ValidatorConfigError::unauthorized().into(),
411            "Should return Unauthorized error"
412        );
413    }
414
415    #[test]
416    fn test_validator_lifecycle() {
417        let mut storage = HashMapStorageProvider::new(1);
418        let owner = Address::from([0x01; 20]);
419
420        let mut validator_config = ValidatorConfig::new(&mut storage);
421        validator_config.initialize(owner).unwrap();
422
423        let validator1 = Address::from([0x11; 20]);
424        let public_key1 = FixedBytes::<32>::from([0x21; 32]);
425        let inbound1 = "192.168.1.1:8000".to_string();
426        let outbound1 = "192.168.1.1:9000".to_string();
427        validator_config
428            .add_validator(
429                owner,
430                IValidatorConfig::addValidatorCall {
431                    newValidatorAddress: validator1,
432                    publicKey: public_key1,
433                    inboundAddress: inbound1.clone(),
434                    active: true,
435                    outboundAddress: outbound1,
436                },
437            )
438            .expect("should add validator1");
439
440        // Try adding duplicate validator - should fail
441        let result = validator_config.add_validator(
442            owner,
443            IValidatorConfig::addValidatorCall {
444                newValidatorAddress: validator1,
445                publicKey: FixedBytes::<32>::from([0x22; 32]),
446                inboundAddress: "192.168.1.1:8000".to_string(),
447                active: true,
448                outboundAddress: "192.168.1.1:9000".to_string(),
449            },
450        );
451        assert!(result.is_err(), "Should not allow duplicate validator");
452        assert_eq!(
453            result.unwrap_err(),
454            ValidatorConfigError::validator_already_exists().into(),
455            "Should return ValidatorAlreadyExists error"
456        );
457
458        // Add 4 more unique validators
459        let validator2 = Address::from([0x12; 20]);
460        let public_key2 = FixedBytes::<32>::from([0x22; 32]);
461        validator_config
462            .add_validator(
463                owner,
464                IValidatorConfig::addValidatorCall {
465                    newValidatorAddress: validator2,
466                    publicKey: public_key2,
467                    inboundAddress: "192.168.1.2:8000".to_string(),
468                    active: true,
469                    outboundAddress: "192.168.1.2:9000".to_string(),
470                },
471            )
472            .expect("Should add validator2");
473
474        let validator3 = Address::from([0x13; 20]);
475        let public_key3 = FixedBytes::<32>::from([0x23; 32]);
476        validator_config
477            .add_validator(
478                owner,
479                IValidatorConfig::addValidatorCall {
480                    newValidatorAddress: validator3,
481                    publicKey: public_key3,
482                    inboundAddress: "192.168.1.3:8000".to_string(),
483                    active: false,
484                    outboundAddress: "192.168.1.3:9000".to_string(),
485                },
486            )
487            .expect("Should add validator3");
488
489        let validator4 = Address::from([0x14; 20]);
490        let public_key4 = FixedBytes::<32>::from([0x24; 32]);
491        validator_config
492            .add_validator(
493                owner,
494                IValidatorConfig::addValidatorCall {
495                    newValidatorAddress: validator4,
496                    publicKey: public_key4,
497                    inboundAddress: "192.168.1.4:8000".to_string(),
498                    active: true,
499                    outboundAddress: "192.168.1.4:9000".to_string(),
500                },
501            )
502            .expect("Should add validator4");
503
504        let validator5 = Address::from([0x15; 20]);
505        let public_key5 = FixedBytes::<32>::from([0x25; 32]);
506        validator_config
507            .add_validator(
508                owner,
509                IValidatorConfig::addValidatorCall {
510                    newValidatorAddress: validator5,
511                    publicKey: public_key5,
512                    inboundAddress: "192.168.1.5:8000".to_string(),
513                    active: true,
514                    outboundAddress: "192.168.1.5:9000".to_string(),
515                },
516            )
517            .expect("Should add validator5");
518
519        // Get all validators
520        let mut validators = validator_config
521            .get_validators(IValidatorConfig::getValidatorsCall {})
522            .expect("Should get validators");
523
524        // Verify count
525        assert_eq!(validators.len(), 5, "Should have 5 validators");
526
527        // Sort by validator address for consistent checking
528        validators.sort_by_key(|v| v.validatorAddress);
529
530        // Verify each validator
531        assert_eq!(validators[0].validatorAddress, validator1);
532        assert_eq!(validators[0].publicKey, public_key1);
533        assert_eq!(validators[0].inboundAddress, inbound1);
534        assert!(validators[0].active);
535
536        assert_eq!(validators[1].validatorAddress, validator2);
537        assert_eq!(validators[1].publicKey, public_key2);
538        assert_eq!(validators[1].inboundAddress, "192.168.1.2:8000");
539        assert!(validators[1].active);
540
541        assert_eq!(validators[2].validatorAddress, validator3);
542        assert_eq!(validators[2].publicKey, public_key3);
543        assert_eq!(validators[2].inboundAddress, "192.168.1.3:8000");
544        assert!(!validators[2].active);
545
546        assert_eq!(validators[3].validatorAddress, validator4);
547        assert_eq!(validators[3].publicKey, public_key4);
548        assert_eq!(validators[3].inboundAddress, "192.168.1.4:8000");
549        assert!(validators[3].active);
550
551        assert_eq!(validators[4].validatorAddress, validator5);
552        assert_eq!(validators[4].publicKey, public_key5);
553        assert_eq!(validators[4].inboundAddress, "192.168.1.5:8000");
554        assert!(validators[4].active);
555
556        // Validator1 updates from long to short address (tests update_string slot clearing)
557        let public_key1_new = FixedBytes::<32>::from([0x31; 32]);
558        let short_inbound1 = "10.0.0.1:8000".to_string();
559        let short_outbound1 = "10.0.0.1:9000".to_string();
560        validator_config
561            .update_validator(
562                validator1,
563                IValidatorConfig::updateValidatorCall {
564                    newValidatorAddress: validator1,
565                    publicKey: public_key1_new,
566                    inboundAddress: short_inbound1.clone(),
567                    outboundAddress: short_outbound1,
568                },
569            )
570            .expect("Should update validator1");
571
572        // Validator2 rotates to new address, keeps IP and publicKey
573        let validator2_new = Address::from([0x22; 20]);
574        validator_config
575            .update_validator(
576                validator2,
577                IValidatorConfig::updateValidatorCall {
578                    newValidatorAddress: validator2_new,
579                    publicKey: public_key2,
580                    inboundAddress: "192.168.1.2:8000".to_string(),
581                    outboundAddress: "192.168.1.2:9000".to_string(),
582                },
583            )
584            .expect("Should rotate validator2 address");
585
586        // Validator3 rotates to new address with long host (tests delete_string on old slot)
587        let validator3_new = Address::from([0x23; 20]);
588        let long_inbound3 = "192.169.1.3:8000".to_string();
589        let long_outbound3 = "192.168.1.3:9000".to_string();
590        validator_config
591            .update_validator(
592                validator3,
593                IValidatorConfig::updateValidatorCall {
594                    newValidatorAddress: validator3_new,
595                    publicKey: public_key3,
596                    inboundAddress: long_inbound3.clone(),
597                    outboundAddress: long_outbound3,
598                },
599            )
600            .expect("Should rotate validator3 address and update IP");
601
602        // Get all validators again
603        let mut validators = validator_config
604            .get_validators(IValidatorConfig::getValidatorsCall {})
605            .expect("Should get validators");
606
607        // Should still have 5 validators
608        assert_eq!(validators.len(), 5, "Should still have 5 validators");
609
610        // Sort by validator address
611        validators.sort_by_key(|v| v.validatorAddress);
612
613        // Verify validator1 - updated from long to short address
614        assert_eq!(validators[0].validatorAddress, validator1);
615        assert_eq!(
616            validators[0].publicKey, public_key1_new,
617            "PublicKey should be updated"
618        );
619        assert_eq!(
620            validators[0].inboundAddress, short_inbound1,
621            "Address should be updated to short"
622        );
623        assert!(validators[0].active);
624
625        // Verify validator4 - unchanged
626        assert_eq!(validators[1].validatorAddress, validator4);
627        assert_eq!(validators[1].publicKey, public_key4);
628        assert_eq!(validators[1].inboundAddress, "192.168.1.4:8000");
629        assert!(validators[1].active);
630
631        // Verify validator5 - unchanged
632        assert_eq!(validators[2].validatorAddress, validator5);
633        assert_eq!(validators[2].publicKey, public_key5);
634        assert_eq!(validators[2].inboundAddress, "192.168.1.5:8000");
635        assert!(validators[2].active);
636
637        // Verify validator2_new - rotated address, kept IP and publicKey
638        assert_eq!(validators[3].validatorAddress, validator2_new);
639        assert_eq!(
640            validators[3].publicKey, public_key2,
641            "PublicKey should be same"
642        );
643        assert_eq!(
644            validators[3].inboundAddress, "192.168.1.2:8000",
645            "IP should be same"
646        );
647        assert!(validators[3].active);
648
649        // Verify validator3_new - rotated address with long host, kept publicKey
650        assert_eq!(validators[4].validatorAddress, validator3_new);
651        assert_eq!(
652            validators[4].publicKey, public_key3,
653            "PublicKey should be same"
654        );
655        assert_eq!(
656            validators[4].inboundAddress, long_inbound3,
657            "Address should be updated to long"
658        );
659        assert!(!validators[4].active);
660    }
661
662    #[test]
663    fn test_owner_cannot_update_validator() {
664        let mut storage = HashMapStorageProvider::new(1);
665        let owner = Address::from([0x01; 20]);
666        let validator = Address::from([0x11; 20]);
667
668        let mut validator_config = ValidatorConfig::new(&mut storage);
669        validator_config.initialize(owner).unwrap();
670
671        // Owner adds a validator
672        let public_key = FixedBytes::<32>::from([0x21; 32]);
673        validator_config
674            .add_validator(
675                owner,
676                IValidatorConfig::addValidatorCall {
677                    newValidatorAddress: validator,
678                    publicKey: public_key,
679                    inboundAddress: "192.168.1.1:8000".to_string(),
680                    active: true,
681                    outboundAddress: "192.168.1.1:9000".to_string(),
682                },
683            )
684            .expect("Should add validator");
685
686        // Owner tries to update validator - should fail
687        let result = validator_config.update_validator(
688            owner,
689            IValidatorConfig::updateValidatorCall {
690                newValidatorAddress: validator,
691                publicKey: FixedBytes::<32>::from([0x22; 32]),
692                inboundAddress: "10.0.0.1:8000".to_string(),
693                outboundAddress: "10.0.0.1:9000".to_string(),
694            },
695        );
696
697        assert!(
698            result.is_err(),
699            "Owner should not be able to update validator"
700        );
701        assert_eq!(
702            result.unwrap_err(),
703            ValidatorConfigError::validator_not_found().into(),
704            "Should return ValidatorNotFound error"
705        );
706    }
707
708    #[test]
709    fn test_validator_rotation_clears_all_slots() {
710        let mut storage = HashMapStorageProvider::new(1);
711        let owner = Address::from([0x01; 20]);
712        let validator1 = Address::from([0x11; 20]);
713        let validator2 = Address::from([0x22; 20]);
714
715        let mut validator_config = ValidatorConfig::new(&mut storage);
716        validator_config.initialize(owner).unwrap();
717
718        // Add validator with long inbound address that uses multiple slots
719        let long_inbound = "192.168.1.1:8000".to_string();
720        let long_outbound = "192.168.1.1:9000".to_string();
721        let public_key = FixedBytes::<32>::from([0x21; 32]);
722
723        validator_config
724            .add_validator(
725                owner,
726                IValidatorConfig::addValidatorCall {
727                    newValidatorAddress: validator1,
728                    publicKey: public_key,
729                    inboundAddress: long_inbound,
730                    active: true,
731                    outboundAddress: long_outbound,
732                },
733            )
734            .expect("Should add validator with long addresses");
735
736        // Rotate to new address with shorter addresses
737        validator_config
738            .update_validator(
739                validator1,
740                IValidatorConfig::updateValidatorCall {
741                    newValidatorAddress: validator2,
742                    publicKey: public_key,
743                    inboundAddress: "10.0.0.1:8000".to_string(),
744                    outboundAddress: "10.0.0.1:9000".to_string(),
745                },
746            )
747            .expect("Should rotate validator");
748
749        // Verify old slots are cleared by checking storage directly
750        let validator = validator_config
751            .sload_validators(validator1)
752            .expect("Could not load validator");
753
754        // Assert all validator fields are cleared/zeroed
755        assert_eq!(
756            validator.public_key,
757            B256::ZERO,
758            "Old validator public key should be cleared"
759        );
760        assert_eq!(
761            validator.validator_address,
762            Address::ZERO,
763            "Old validator address should be cleared"
764        );
765        assert_eq!(validator.index, 0, "Old validator index should be cleared");
766        assert!(!validator.active, "Old validator should be inactive");
767        assert_eq!(
768            validator.inbound_address,
769            String::default(),
770            "Old validator inbound address should be cleared"
771        );
772        assert_eq!(
773            validator.outbound_address,
774            String::default(),
775            "Old validator outbound address should be cleared"
776        );
777    }
778
779    #[test]
780    fn ipv4_with_port_is_host_port() {
781        ensure_address_is_ip_port("127.0.0.1:8000").unwrap();
782    }
783
784    #[test]
785    fn ipv6_with_port_is_host_port() {
786        ensure_address_is_ip_port("[::1]:8000").unwrap();
787    }
788}