Skip to main content

tempo_precompiles/validator_config_v2/
mod.rs

1//! Validator Config V2 precompile – index-canonical, on-chain, [consensus] validator registry with
2//! signature-gated operations, IP uniqueness enforcement, and migration support from V1.
3//!
4//! [consensus]: <https://docs.tempo.xyz/protocol/blockspace/consensus>
5
6pub mod dispatch;
7
8pub use tempo_contracts::precompiles::{IValidatorConfigV2, ValidatorConfigV2Error};
9use tempo_contracts::precompiles::{VALIDATOR_CONFIG_V2_ADDRESS, ValidatorConfigV2Event};
10use tempo_precompiles_macros::{Storable, contract};
11
12use crate::{
13    error::{Result, TempoPrecompileError},
14    ip_validation::{IpWithPortParseError, ensure_address_is_ip, ensure_address_is_ip_port},
15    storage::{Handler, Mapping},
16    validator_config::ValidatorConfig,
17};
18use alloy::primitives::{Address, B256, Keccak256};
19use commonware_codec::DecodeExt;
20use commonware_cryptography::{
21    Verifier,
22    ed25519::{PublicKey, Signature},
23};
24use tracing::trace;
25
26/// Signature namespace for `addValidator` operations.
27pub const VALIDATOR_NS_ADD: &[u8] = b"TEMPO_VALIDATOR_CONFIG_V2_ADD_VALIDATOR";
28/// Signature namespace for `rotateValidator` operations.
29pub const VALIDATOR_NS_ROTATE: &[u8] = b"TEMPO_VALIDATOR_CONFIG_V2_ROTATE_VALIDATOR";
30
31/// Distinguishes `addValidator` from `rotateValidator` signatures at the type level.
32enum SignatureKind {
33    Add { fee_recipient: Address },
34    Rotate,
35}
36
37/// Contract-level configuration: ownership, initialization state, and migration bookkeeping.
38#[derive(Debug, Storable)]
39struct Config {
40    /// Contract admin address.
41    owner: Address,
42    /// `true` once the contract is fully initialized (post-migration or direct init).
43    is_init: bool,
44    /// Block height at which initialization completed.
45    init_at_height: u64,
46    /// Number of V1 validators skipped during migration (bad pubkey, duplicate, etc).
47    /// Packed alongside `is_init` and `init_at_height` since all are migration lifecycle state.
48    migration_skipped_count: u8,
49    /// Snapshotted V1 validator count, captured on the first `migrateValidator` call.
50    /// Used for index validation so V1 mutations cannot break migration ordering.
51    v1_validator_count: u8,
52}
53
54impl Config {
55    fn new(owner: Address, is_init: bool, init_at_height: u64) -> Self {
56        Self {
57            owner,
58            is_init,
59            init_at_height,
60            migration_skipped_count: 0,
61            v1_validator_count: 0,
62        }
63    }
64
65    fn is_owner(&self, addr: Address) -> bool {
66        self.owner == addr
67    }
68
69    fn require_init(self) -> Result<Self> {
70        if self.is_init {
71            return Ok(self);
72        }
73        Err(ValidatorConfigV2Error::not_initialized())?
74    }
75
76    fn require_not_init(self) -> Result<Self> {
77        if self.is_init {
78            Err(ValidatorConfigV2Error::already_initialized())?
79        }
80        Ok(self)
81    }
82
83    fn require_owner(self, caller: Address) -> Result<Self> {
84        if !self.is_owner(caller) {
85            Err(ValidatorConfigV2Error::unauthorized())?
86        }
87        Ok(self)
88    }
89
90    fn require_owner_or_validator(self, caller: Address, validator: Address) -> Result<Self> {
91        if caller != validator && !self.is_owner(caller) {
92            Err(ValidatorConfigV2Error::unauthorized())?
93        }
94        Ok(self)
95    }
96}
97
98/// A single entry in the `validators` vector.
99///
100/// ## Lifecycle
101///
102/// A record is created in one of three ways:
103/// - `add_validator`: active entry (`deactivated_at_height = 0`, `active_idx != 0`)
104/// - `migrate_validator`: active or born-deactivated, depending on V1 state
105/// - `rotate_validator`: appends a born-deactivated snapshot of the old identity
106///   and overwrites the original slot in-place with the new identity
107///
108/// A record is deactivated via `deactivate_validator`, which sets
109/// `deactivated_at_height` to the current block height and clears `active_idx` to 0.
110/// This transition is one-way — a deactivated record never becomes active again.
111#[derive(Debug, Storable)]
112struct ValidatorRecord {
113    /// Ed25519 communication public key (unique across all records, reserved forever).
114    public_key: B256,
115    /// Ethereum-style address of the validator (unique among active validators).
116    validator_address: Address,
117    /// Inbound address for peer connections (`<ip>:<port>`).
118    ingress: String,
119    /// Outbound IP for firewall whitelisting (`<ip>`).
120    egress: String,
121    /// Address that receives execution-layer fees for this validator.
122    fee_recipient: Address,
123    /// Position in the `validators` array. Stable across rotations for the in-place slot;
124    /// snapshots get the tail position at the time they were appended.
125    index: u64,
126    /// 1-indexed position in `active_indices` (0 = not active).
127    /// Used as a backpointer for O(1) swap-and-pop removal on deactivation.
128    active_idx: u64,
129    /// Block height at which this record was created (or overwritten during rotation).
130    added_at_height: u64,
131    /// Block height at which this record was deactivated (0 = still active).
132    deactivated_at_height: u64,
133}
134
135/// Validator Config V2 precompile — manages consensus validators with append-only,
136/// delete-once semantics.
137///
138/// Replaces V1's mutable state with immutable height-based tracking (`addedAtHeight`,
139/// `deactivatedAtHeight`) to enable historical validator set reconstruction without
140/// requiring historical state access.
141///
142/// ## Storage design
143///
144/// The `validators` vec is the source of truth (append-only). Two auxiliary mappings
145/// provide O(1) lookups:
146/// - `address_to_index`: validator address -> 1-indexed position (0 = not found)
147/// - `pubkey_to_index`: public key -> 1-indexed position (0 = not found)
148///
149/// A separate `active_indices` vec stores 1-indexed global positions of active validators,
150/// enabling O(active_count) enumeration without scanning deactivated entries. Each validator
151/// stores an `active_idx` backpointer into this vec for O(1) swap-and-pop removal.
152///
153/// The struct fields define the on-chain storage layout; the `#[contract]` macro generates the
154/// storage handlers which provide an ergonomic way to interact with the EVM state.
155#[contract(addr = VALIDATOR_CONFIG_V2_ADDRESS)]
156pub struct ValidatorConfigV2 {
157    /// Contract-level config: ownership, initialization state, and migration bookkeeping.
158    config: Config,
159    /// Append-only array of all validators ever registered (including deactivated snapshots).
160    validators: Vec<ValidatorRecord>,
161    /// Validator address → 1-indexed position in `validators` (0 = not found).
162    /// After deactivation the mapping still points to the old (now-deactivated) entry.
163    /// Overwritten when the address is reused by a new validator, or when ownership is transferred.
164    address_to_index: Mapping<Address, u64>,
165    /// Ed25519 public key -> 1-indexed position in `validators` (0 = not found).
166    /// Public keys are reserved forever — even deactivated entries keep their mapping.
167    pubkey_to_index: Mapping<B256, u64>,
168    /// Epoch at which a DKG ceremony will run that rotates the network identity.
169    next_network_identity_rotation_epoch: u64,
170    /// Prevents two active validators from sharing the same ingress IP address.
171    active_ingress_ips: Mapping<B256, bool>,
172    /// Compact list of 1-indexed global positions of currently active validators.
173    /// Order is NOT stable (swap-and-pop on deactivation).
174    active_indices: Vec<u64>,
175}
176
177impl ValidatorConfigV2 {
178    /// Initializes the validator config V2 precompile.
179    ///
180    /// The contract is fully operational immediately: `is_init` is set to `true` and all mutating
181    /// functions (`add_validator`, `rotate_validator`, etc.) are unlocked.
182    ///
183    /// For V1 migration, the contract is NOT initialized — instead `migrate_validator` manually
184    /// copies validators and `initialize_if_migrated` flips `is_init` once all have been migrated.
185    pub fn initialize(&mut self, owner: Address) -> Result<()> {
186        trace!(address=%self.address, %owner, "Initializing validator config v2 precompile");
187        self.__initialize()?;
188
189        let config = Config::new(owner, true, self.storage.block_number());
190
191        self.config.write(config)
192    }
193
194    // =========================================================================
195    // Config accessors and guards — each reads config once (1 SLOAD)
196    // =========================================================================
197
198    /// Returns the current owner of the contract.
199    pub fn owner(&self) -> Result<Address> {
200        self.config.owner.read()
201    }
202
203    /// Returns the block height at which the contract was initialized.
204    ///
205    /// Only meaningful when [`is_initialized`](Self::is_initialized) returns `true`.
206    pub fn get_initialized_at_height(&self) -> Result<u64> {
207        self.config.init_at_height.read()
208    }
209
210    /// Returns whether V2 has been initialized (either directly or via migration).
211    ///
212    /// When `false`, the CL reads from V1 and mutating operations (except
213    /// `deactivate_validator` and migration functions) are blocked.
214    pub fn is_initialized(&self) -> Result<bool> {
215        self.config.is_init.read()
216    }
217
218    /// Returns the total number of validators ever added, including deactivated
219    /// entries and rotation snapshots.
220    pub fn validator_count(&self) -> Result<u64> {
221        Ok(self.validators.len()? as u64)
222    }
223
224    /// Returns the validator at the given global index, or errors if the index
225    /// is out of bounds or the validator has been deactivated.
226    fn get_active_validator(&self, idx: u64) -> Result<ValidatorRecord> {
227        if idx >= self.validators.len()? as u64 {
228            Err(ValidatorConfigV2Error::validator_not_found())?
229        }
230        let v = self.validators[idx as usize].read()?;
231        if v.deactivated_at_height != 0 {
232            Err(ValidatorConfigV2Error::validator_already_deactivated())?
233        }
234        Ok(v)
235    }
236
237    fn read_validator_at(&self, index: u64) -> Result<IValidatorConfigV2::Validator> {
238        debug_assert!(index < self.validator_count()?, "OOB index");
239
240        let v = self.validators[index as usize].read()?;
241        Ok(IValidatorConfigV2::Validator {
242            publicKey: v.public_key,
243            validatorAddress: v.validator_address,
244            ingress: v.ingress,
245            egress: v.egress,
246            feeRecipient: v.fee_recipient,
247            index: v.index,
248            addedAtHeight: v.added_at_height,
249            deactivatedAtHeight: v.deactivated_at_height,
250        })
251    }
252
253    /// Returns the validator registry at the given global index in the `validators` array.
254    ///
255    /// # Errors
256    /// - `ValidatorNotFound` — `index` is out of bounds
257    pub fn validator_by_index(&self, index: u64) -> Result<IValidatorConfigV2::Validator> {
258        if index >= self.validator_count()? {
259            Err(ValidatorConfigV2Error::validator_not_found())?
260        }
261        self.read_validator_at(index)
262    }
263
264    /// Looks up a validator registry by its address.
265    ///
266    /// Returns the current entry for the address (after any rotations or transfers).
267    ///
268    /// # Errors
269    /// - `ValidatorNotFound` — the address has never been registered
270    pub fn validator_by_address(&self, addr: Address) -> Result<IValidatorConfigV2::Validator> {
271        let idx1 = self.address_to_index[addr].read()?;
272        if idx1 == 0 {
273            Err(ValidatorConfigV2Error::validator_not_found())?
274        }
275        self.read_validator_at(idx1 - 1)
276    }
277
278    /// Looks up a validator by its Ed25519 public key.
279    ///
280    /// For rotated validators, the old public key resolves to the deactivated snapshot, while the
281    /// new key resolves to the in-place active entry.
282    ///
283    /// # Errors
284    /// - `ValidatorNotFound` — the public key has never been registered
285    pub fn validator_by_public_key(&self, pubkey: B256) -> Result<IValidatorConfigV2::Validator> {
286        let idx1 = self.pubkey_to_index[pubkey].read()?;
287        if idx1 == 0 {
288            Err(ValidatorConfigV2Error::validator_not_found())?
289        }
290        self.read_validator_at(idx1 - 1)
291    }
292
293    /// Returns only active validators (where `deactivatedAtHeight == 0`).
294    ///
295    /// NOTE: the order of returned validator records is NOT stable and should NOT be relied upon.
296    pub fn get_active_validators(&self) -> Result<Vec<IValidatorConfigV2::Validator>> {
297        let count = self.active_indices.len()?;
298        let mut out = Vec::new();
299        for i in 0..count {
300            let global_idx1 = self.active_indices[i].read()?;
301            out.push(self.read_validator_at(global_idx1 - 1)?);
302        }
303        Ok(out)
304    }
305
306    /// Returns the epoch at which a network identity rotation will be triggered.
307    ///
308    /// See [`set_network_identity_rotation_epoch`](Self::set_network_identity_rotation_epoch).
309    pub fn get_next_network_identity_rotation_epoch(&self) -> Result<u64> {
310        self.next_network_identity_rotation_epoch.read()
311    }
312
313    fn validate_endpoints(ingress: &str, egress: &str) -> Result<()> {
314        ensure_address_is_ip_port(ingress).map_err(|err| {
315            TempoPrecompileError::from(ValidatorConfigV2Error::not_ip_port(
316                ingress.to_string(),
317                err.to_string(),
318            ))
319        })?;
320
321        ensure_address_is_ip(egress).map_err(|err| {
322            TempoPrecompileError::from(ValidatorConfigV2Error::not_ip(
323                egress.to_string(),
324                err.to_string(),
325            ))
326        })
327    }
328
329    /// Parses `ingress` as an `<ip>:<port>` pair and returns the hash of the
330    /// ingress' binary representation.
331    ///
332    /// For V4 addresses, that's `keccak256(octets(ip) || big_endian(port))`.
333    ///
334    /// For V6 addresses, that's `keccak256(octets(ip) || big_endian(scope_id) || big_endian(port))`.
335    fn ingress_key(ingress: &str) -> Result<B256> {
336        let ingress = ingress
337            .parse::<std::net::SocketAddr>()
338            .map_err(IpWithPortParseError::from)
339            .map_err(|err| {
340                TempoPrecompileError::from(ValidatorConfigV2Error::not_ip_port(
341                    ingress.to_string(),
342                    err.to_string(),
343                ))
344            })?;
345        let mut hasher = Keccak256::new();
346        match ingress {
347            std::net::SocketAddr::V4(v4) => {
348                hasher.update(v4.ip().octets());
349                hasher.update(v4.port().to_be_bytes());
350            }
351            std::net::SocketAddr::V6(v6) => {
352                hasher.update(v6.ip().octets());
353                hasher.update(v6.scope_id().to_be_bytes());
354                hasher.update(v6.port().to_be_bytes());
355            }
356        }
357        Ok(hasher.finalize())
358    }
359
360    fn require_unique_ingress(&self, ingress: &str) -> Result<B256> {
361        let ingress_hash = Self::ingress_key(ingress)?;
362        if self.active_ingress_ips[ingress_hash].read()? {
363            Err(ValidatorConfigV2Error::ingress_already_exists(
364                ingress.to_string(),
365            ))?
366        }
367        Ok(ingress_hash)
368    }
369
370    fn update_ingress_ip_tracking(&mut self, old_ingress: &str, new_ingress: &str) -> Result<()> {
371        let old_ingress_hash = Self::ingress_key(old_ingress)?;
372        let new_ingress_hash = Self::ingress_key(new_ingress)?;
373
374        if old_ingress_hash != new_ingress_hash {
375            if self.active_ingress_ips[new_ingress_hash].read()? {
376                Err(ValidatorConfigV2Error::ingress_already_exists(
377                    new_ingress.to_string(),
378                ))?
379            }
380            self.active_ingress_ips[old_ingress_hash].delete()?;
381            self.active_ingress_ips[new_ingress_hash].write(true)?;
382        }
383
384        Ok(())
385    }
386
387    #[allow(clippy::too_many_arguments)]
388    fn append_validator(
389        &mut self,
390        addr: Address,
391        pubkey: B256,
392        ingress: String,
393        egress: String,
394        fee_recipient: Address,
395        added_at_height: u64,
396        deactivated_at_height: u64,
397    ) -> Result<u64> {
398        let count = self.validator_count()?;
399        let mut active_idx = 0u64;
400
401        if deactivated_at_height == 0 {
402            self.active_indices.push(count + 1)?; // 1-indexed
403            active_idx = self.active_indices.len()? as u64; // 1-indexed
404        }
405
406        let v = ValidatorRecord {
407            public_key: pubkey,
408            validator_address: addr,
409            ingress,
410            egress,
411            fee_recipient,
412            index: count,
413            active_idx,
414            added_at_height,
415            deactivated_at_height,
416        };
417
418        self.validators.push(v)?;
419
420        self.pubkey_to_index[pubkey].write(count + 1)?;
421        // for any dups the prev entries must be deactivated since we check above
422        self.address_to_index[addr].write(count + 1)?;
423
424        Ok(count)
425    }
426
427    /// Allows reusing addresses of deactivated validators.
428    fn require_new_address(&self, addr: Address) -> Result<()> {
429        if addr.is_zero() {
430            Err(ValidatorConfigV2Error::invalid_validator_address())?
431        }
432        let idx1 = self.address_to_index[addr].read()?;
433        if idx1 != 0
434            && self.validators[(idx1 - 1) as usize]
435                .deactivated_at_height
436                .read()?
437                == 0
438        {
439            Err(ValidatorConfigV2Error::address_already_has_validator())?
440        }
441        Ok(())
442    }
443
444    fn require_new_pubkey(&self, pubkey: B256) -> Result<()> {
445        if pubkey.is_zero() {
446            Err(ValidatorConfigV2Error::invalid_public_key())?
447        }
448        if self.pubkey_to_index[pubkey].read()? != 0 {
449            Err(ValidatorConfigV2Error::public_key_already_exists())?
450        }
451        Ok(())
452    }
453
454    /// Verifies a validator signature for add or rotate operations.
455    ///
456    /// Constructs the message according to the validator config v2 specification and verifies
457    /// the Ed25519 signature using the appropriate namespace.
458    fn verify_validator_signature(
459        &self,
460        kind: SignatureKind,
461        pubkey: &B256,
462        signature: &[u8],
463        validator_address: Address,
464        ingress: &str,
465        egress: &str,
466    ) -> Result<()> {
467        let sig = Signature::decode(signature)
468            .map_err(|_| ValidatorConfigV2Error::invalid_signature_format())?;
469
470        let mut hasher = Keccak256::new();
471        hasher.update(self.storage.chain_id().to_be_bytes());
472        hasher.update(VALIDATOR_CONFIG_V2_ADDRESS.as_slice());
473        hasher.update(validator_address.as_slice());
474        hasher.update([
475            u8::try_from(ingress.len()).expect("validator ingress length must fit in uint8")
476        ]);
477        hasher.update(ingress.as_bytes());
478        hasher.update([
479            u8::try_from(egress.len()).expect("validator egress length must fit in uint8")
480        ]);
481        hasher.update(egress.as_bytes());
482
483        let namespace = match kind {
484            SignatureKind::Add { fee_recipient } => {
485                hasher.update(fee_recipient.as_slice());
486                VALIDATOR_NS_ADD
487            }
488            SignatureKind::Rotate => VALIDATOR_NS_ROTATE,
489        };
490        let message = hasher.finalize();
491
492        let public_key = PublicKey::decode(pubkey.as_slice())
493            .map_err(|_| ValidatorConfigV2Error::invalid_public_key())?;
494        if !public_key.verify(namespace, message.as_slice(), &sig) {
495            Err(ValidatorConfigV2Error::invalid_signature())?
496        }
497
498        Ok(())
499    }
500
501    // =========================================================================
502    // Owner-only mutating functions
503    // =========================================================================
504
505    /// Adds a new validator to the set (owner only).
506    ///
507    /// Requires a valid Ed25519 signature, using the [`VALIDATOR_NS_ADD`] namespace, over
508    /// `keccak256(chainId || contractAddr || validatorAddr || len(ingress) || ingress || len(egress) || egress || feeRecipient)`
509    /// which proves that the caller controls the private key corresponding to `publicKey`.
510    ///
511    /// # Errors
512    /// - `NotInitialized` — the contract has not been initialized
513    /// - `Unauthorized` — `sender` is not the owner
514    /// - `InvalidPublicKey` — `publicKey` is zero or not a valid Ed25519 key
515    /// - `PublicKeyAlreadyExists` — the public key is already registered
516    /// - `InvalidValidatorAddress` — `validatorAddress` is zero
517    /// - `AddressAlreadyHasValidator` — the address belongs to an active validator
518    /// - `NotIpPort` / `NotIp` — endpoints fail validation
519    /// - `IngressAlreadyExists` — the new ingress is already in use
520    /// - `InvalidSignature` — signature verification fails
521    pub fn add_validator(
522        &mut self,
523        sender: Address,
524        call: IValidatorConfigV2::addValidatorCall,
525    ) -> Result<u64> {
526        self.config.read()?.require_init()?.require_owner(sender)?;
527        self.require_new_pubkey(call.publicKey)?;
528        self.require_new_address(call.validatorAddress)?;
529        Self::validate_endpoints(&call.ingress, &call.egress)?;
530        let ingress_hash = self.require_unique_ingress(&call.ingress)?;
531
532        self.verify_validator_signature(
533            SignatureKind::Add {
534                fee_recipient: call.feeRecipient,
535            },
536            &call.publicKey,
537            &call.signature,
538            call.validatorAddress,
539            &call.ingress,
540            &call.egress,
541        )?;
542
543        let block_height = self.storage.block_number();
544
545        self.active_ingress_ips[ingress_hash].write(true)?;
546
547        let index = self.append_validator(
548            call.validatorAddress,
549            call.publicKey,
550            call.ingress.clone(),
551            call.egress.clone(),
552            call.feeRecipient,
553            block_height,
554            0,
555        )?;
556
557        self.emit_event(ValidatorConfigV2Event::validator_added(
558            index,
559            call.validatorAddress,
560            call.publicKey,
561            call.ingress,
562            call.egress,
563            call.feeRecipient,
564        ))?;
565
566        Ok(index)
567    }
568
569    /// Deactivates a validator by setting its `deactivatedAtHeight` to the current
570    /// block height (owner or the validator itself).
571    ///
572    /// The validator's entry remains in storage for historical queries and its
573    /// public key stays reserved forever. The ingress IP is freed for reuse.
574    ///
575    /// Does NOT require initialization — can be called during the migration window.
576    ///
577    /// Uses swap-and-pop on `active_indices` for O(1) removal.
578    ///
579    /// # Errors
580    /// - `ValidatorNotFound` — `idx` is out of bounds
581    /// - `ValidatorAlreadyDeleted` — the validator is already deactivated
582    /// - `Unauthorized` — `sender` is neither the owner nor the validator
583    pub fn deactivate_validator(
584        &mut self,
585        sender: Address,
586        call: IValidatorConfigV2::deactivateValidatorCall,
587    ) -> Result<()> {
588        let v = self.get_active_validator(call.idx)?;
589        self.config
590            .read()?
591            .require_owner_or_validator(sender, v.validator_address)?;
592
593        self.active_ingress_ips[Self::ingress_key(&v.ingress)?].delete()?;
594
595        let block_height = self.storage.block_number();
596        self.validators[call.idx as usize]
597            .deactivated_at_height
598            .write(block_height)?;
599
600        // Swap-and-pop for active_indices
601        let active_index = (v.active_idx - 1) as usize;
602        let last_pos = self.active_indices.len()? - 1;
603
604        if active_index != last_pos {
605            let moved_val = self.active_indices[last_pos].read()?;
606            self.active_indices[active_index].write(moved_val)?;
607            self.validators[(moved_val - 1) as usize]
608                .active_idx
609                .write((active_index + 1) as u64)?;
610        }
611        self.active_indices.pop()?;
612        self.validators[call.idx as usize].active_idx.write(0)?;
613
614        self.emit_event(ValidatorConfigV2Event::validator_deactivated(
615            call.idx,
616            v.validator_address,
617        ))
618    }
619
620    /// Transfers ownership of the contract to a new address (owner only).
621    ///
622    /// # Errors
623    /// - `InvalidOwner` — `newOwner` is `address(0)`
624    /// - `Unauthorized` — `sender` is not the owner
625    /// - `NotInitialized` — the contract has not been initialized
626    pub fn transfer_ownership(
627        &mut self,
628        sender: Address,
629        call: IValidatorConfigV2::transferOwnershipCall,
630    ) -> Result<()> {
631        if call.newOwner.is_zero() {
632            Err(ValidatorConfigV2Error::invalid_owner())?
633        }
634        let mut config = self.config.read()?.require_init()?.require_owner(sender)?;
635        let old_owner = config.owner;
636        config.owner = call.newOwner;
637        self.config.write(config)?;
638
639        self.emit_event(ValidatorConfigV2Event::ownership_transferred(
640            old_owner,
641            call.newOwner,
642        ))
643    }
644
645    /// Sets the epoch at which a rotation of the network identity will be triggered.
646    ///
647    /// If `E` is ahead of the network's current epoch, the network will perform a
648    /// Distribute-Key-Generation (DKG) ceremony to rotate its identity at the new epoch `E`.
649    /// - If the DKG ceremony is successful, then epoch `E+1` will run with a new network identity.
650    /// - If `E` is not ahead of the network epoch this value is ignored.
651    pub fn set_network_identity_rotation_epoch(
652        &mut self,
653        sender: Address,
654        call: IValidatorConfigV2::setNetworkIdentityRotationEpochCall,
655    ) -> Result<()> {
656        self.config.read()?.require_init()?.require_owner(sender)?;
657        let previous_epoch = self.next_network_identity_rotation_epoch.read()?;
658        self.next_network_identity_rotation_epoch
659            .write(call.epoch)?;
660        self.emit_event(ValidatorConfigV2Event::network_identity_rotation_epoch_set(
661            previous_epoch,
662            call.epoch,
663        ))
664    }
665
666    // =========================================================================
667    // Dual-auth functions (owner or validator)
668    // =========================================================================
669
670    /// Rotates a validator to a new identity (owner or the validator itself).
671    ///
672    /// Atomically:
673    /// 1. Appends a deactivated snapshot of the old identity to the tail of `validators`
674    /// 2. Overwrites the slot in-place with new pubkey, endpoints, and `addedAtHeight = now`
675    ///
676    /// The validator's global index, `active_idx`, `address_to_index` pointer, and
677    /// position in `active_indices` are all preserved — only `pubkey_to_index` is
678    /// updated (old key -> snapshot, new key -> original slot).
679    ///
680    /// Requires a valid Ed25519 signature, using the [`VALIDATOR_NS_ROTATE`] namespace, over
681    /// `keccak256(chainId || contractAddr || validatorAddr || len(ingress) || ingress || len(egress) || egress)`
682    /// which proves that the caller controls the private key corresponding to `publicKey`.
683    ///
684    /// # Errors
685    /// - `ValidatorNotFound` / `ValidatorAlreadyDeleted` — `idx` is invalid
686    /// - `NotInitialized` / `Unauthorized` — auth failure
687    /// - `InvalidPublicKey` / `PublicKeyAlreadyExists` — the new key is invalid
688    /// - `NotIpPort` / `NotIp` / `IngressAlreadyExists` — endpoint validation failure
689    /// - `InvalidSignature` — signature verification fails
690    pub fn rotate_validator(
691        &mut self,
692        sender: Address,
693        call: IValidatorConfigV2::rotateValidatorCall,
694    ) -> Result<()> {
695        let v = self.get_active_validator(call.idx)?;
696        self.config
697            .read()?
698            .require_init()?
699            .require_owner_or_validator(sender, v.validator_address)?;
700        self.require_new_pubkey(call.publicKey)?;
701        Self::validate_endpoints(&call.ingress, &call.egress)?;
702
703        self.require_unique_ingress(&call.ingress)?;
704        self.verify_validator_signature(
705            SignatureKind::Rotate,
706            &call.publicKey,
707            &call.signature,
708            v.validator_address,
709            &call.ingress,
710            &call.egress,
711        )?;
712
713        let block_height = self.storage.block_number();
714
715        self.update_ingress_ip_tracking(&v.ingress, &call.ingress)?;
716
717        // Append deactivated snapshot of the old validator
718        let appended_idx = self.validators.len()? as u64;
719        let snapshot = ValidatorRecord {
720            public_key: v.public_key,
721            validator_address: v.validator_address,
722            ingress: v.ingress,
723            egress: v.egress,
724            fee_recipient: v.fee_recipient,
725            index: appended_idx,
726            active_idx: 0,
727            added_at_height: v.added_at_height,
728            deactivated_at_height: block_height,
729        };
730        self.validators.push(snapshot)?;
731
732        // Update pubkey_to_index: old pubkey → appended_idx + 1
733        self.pubkey_to_index[v.public_key].write(appended_idx + 1)?;
734
735        // Modify in-place at the original index
736        let mut updated = self.validators[call.idx as usize].read()?;
737        updated.public_key = call.publicKey;
738        updated.ingress = call.ingress.clone();
739        updated.egress = call.egress.clone();
740        updated.added_at_height = block_height;
741        self.validators[call.idx as usize].write(updated)?;
742
743        // Set pubkey_to_index for new pubkey
744        self.pubkey_to_index[call.publicKey].write(call.idx + 1)?;
745
746        self.emit_event(ValidatorConfigV2Event::validator_rotated(
747            call.idx,
748            appended_idx,
749            v.validator_address,
750            v.public_key,
751            call.publicKey,
752            call.ingress,
753            call.egress,
754            sender,
755        ))
756    }
757
758    /// Updates the fee recipient address for a validator (owner or the validator itself).
759    ///
760    /// # Errors
761    /// - `NotInitialized` — the contract has not been initialized
762    /// - `ValidatorNotFound` / `ValidatorAlreadyDeleted` — `idx` is invalid
763    /// - `Unauthorized` — `sender` is neither the owner nor the validator
764    pub fn set_fee_recipient(
765        &mut self,
766        sender: Address,
767        call: IValidatorConfigV2::setFeeRecipientCall,
768    ) -> Result<()> {
769        let mut v = self.get_active_validator(call.idx)?;
770        self.config
771            .read()?
772            .require_init()?
773            .require_owner_or_validator(sender, v.validator_address)?;
774
775        v.fee_recipient = call.feeRecipient;
776        self.validators[call.idx as usize].write(v)?;
777
778        self.emit_event(ValidatorConfigV2Event::fee_recipient_updated(
779            call.idx,
780            call.feeRecipient,
781            sender,
782        ))
783    }
784
785    /// Updates a validator's ingress and egress addresses (owner or the validator itself).
786    ///
787    /// # Errors
788    /// - `NotInitialized` — the contract has not been initialized
789    /// - `ValidatorNotFound` / `ValidatorAlreadyDeleted` — `idx` is invalid
790    /// - `Unauthorized` — `sender` is neither the owner nor the validator
791    /// - `NotIpPort` / `NotIp` — the new endpoints fail validation
792    /// - `IngressAlreadyExists` — the new ingress is already in use.
793    pub fn set_ip_addresses(
794        &mut self,
795        sender: Address,
796        call: IValidatorConfigV2::setIpAddressesCall,
797    ) -> Result<()> {
798        let mut v = self.get_active_validator(call.idx)?;
799        self.config
800            .read()?
801            .require_init()?
802            .require_owner_or_validator(sender, v.validator_address)?;
803
804        Self::validate_endpoints(&call.ingress, &call.egress)?;
805
806        self.update_ingress_ip_tracking(&v.ingress, &call.ingress)?;
807
808        v.ingress = call.ingress.clone();
809        v.egress = call.egress.clone();
810        self.validators[call.idx as usize].write(v)?;
811
812        self.emit_event(ValidatorConfigV2Event::ip_addresses_updated(
813            call.idx,
814            call.ingress,
815            call.egress,
816            sender,
817        ))
818    }
819
820    /// Transfers a validator entry to a new address (owner or the validator itself).
821    ///
822    /// Updates the validator's address in the lookup maps: deletes the old `address_to_index`
823    /// entry and creates a new one pointing to the same slot.
824    ///
825    /// # Errors
826    /// - `ValidatorNotFound` / `ValidatorAlreadyDeleted` — `idx` is invalid
827    /// - `NotInitialized` / `Unauthorized` — auth failure
828    /// - `InvalidValidatorAddress` — `newAddress` is zero
829    /// - `AddressAlreadyHasValidator` — `newAddress` belongs to an active validator
830    pub fn transfer_validator_ownership(
831        &mut self,
832        sender: Address,
833        call: IValidatorConfigV2::transferValidatorOwnershipCall,
834    ) -> Result<()> {
835        let mut v = self.get_active_validator(call.idx)?;
836        self.config
837            .read()?
838            .require_init()?
839            .require_owner_or_validator(sender, v.validator_address)?;
840        self.require_new_address(call.newAddress)?;
841
842        let old_address = v.validator_address;
843        v.validator_address = call.newAddress;
844        self.validators[call.idx as usize].write(v)?;
845
846        self.address_to_index[old_address].delete()?;
847        self.address_to_index[call.newAddress].write(call.idx + 1)?;
848
849        self.emit_event(ValidatorConfigV2Event::validator_ownership_transferred(
850            call.idx,
851            old_address,
852            call.newAddress,
853            sender,
854        ))
855    }
856
857    // =========================================================================
858    // Migration
859    // =========================================================================
860
861    /// On the very first migration call the V2 owner is still zero, so we copy it from V1
862    /// before checking authorization. Returns the (potentially updated) config for reuse.
863    ///
864    /// # Errors
865    /// - `AlreadyInitialized` — V2 is already initialized
866    /// - `Unauthorized` — `caller` is not the owner (after copying from V1 if needed)
867    fn require_migration_owner(&mut self, caller: Address) -> Result<Config> {
868        let mut config = self.config.read()?.require_not_init()?;
869
870        if config.owner.is_zero() {
871            let v1 = v1();
872            config.owner = v1.owner()?;
873            let v1_count = v1.validator_count()?;
874            if v1_count == 0 {
875                Err(ValidatorConfigV2Error::empty_v_1_validator_set())?
876            }
877            config.v1_validator_count = v1_count as u8;
878            self.config.write(Config {
879                owner: config.owner,
880                is_init: false,
881                init_at_height: 0,
882                migration_skipped_count: 0,
883                v1_validator_count: config.v1_validator_count,
884            })?;
885        }
886
887        config.require_owner(caller)
888    }
889
890    /// Migrates a single validator from V1 to V2 (owner only).
891    ///
892    /// Must be called once per V1 validator, in reverse index order.
893    /// On the first call, copies the owner from V1 if V2's owner is `address(0)`.
894    ///
895    /// Validators are skipped (not reverted) when:
896    /// - The public key is not a valid Ed25519 key
897    /// - The egress address cannot be parsed as a `SocketAddr`
898    /// - The public key or ingress IP is a duplicate of an already-migrated entry
899    ///
900    /// Active V1 validators get `deactivatedAtHeight = 0`; inactive ones get
901    /// `deactivatedAtHeight = block.number`.
902    ///
903    /// # Errors
904    /// - `Unauthorized` — `sender` is not the owner
905    /// - `AlreadyInitialized` — V2 is already initialized
906    /// - `InvalidMigrationIndex` — `idx` is out of order
907    pub fn migrate_validator(
908        &mut self,
909        sender: Address,
910        call: IValidatorConfigV2::migrateValidatorCall,
911    ) -> Result<()> {
912        let config = self.require_migration_owner(sender)?;
913        let block_height = self.storage.block_number();
914
915        let v1 = v1();
916        let v1_count = u64::from(config.v1_validator_count);
917        let migrated = self.validator_count()?;
918        let skipped = config.migration_skipped_count;
919
920        let total_processed = migrated + u64::from(skipped);
921        if total_processed >= v1_count || call.idx != v1_count - total_processed - 1 {
922            Err(ValidatorConfigV2Error::invalid_migration_index())?
923        }
924
925        let v1_val = v1.validators(v1.validators_array(call.idx)?)?;
926
927        // Closure to skipping a validator when one of the checks fails
928        let skip = |s: &mut Self| {
929            s.emit_event(ValidatorConfigV2Event::skipped_validator_migration(
930                call.idx,
931                v1_val.validatorAddress,
932                v1_val.publicKey,
933            ))?;
934            s.config
935                .migration_skipped_count
936                .write(skipped.saturating_add(1))
937        };
938
939        // Skip if public key decoding fails
940        if PublicKey::decode(v1_val.publicKey.as_slice()).is_err() {
941            return skip(self);
942        }
943
944        // Skip if egress decoding fails
945        let egress = match v1_val.outboundAddress.parse::<std::net::SocketAddr>() {
946            Ok(sa) => sa.ip().to_string(),
947            Err(_) => return skip(self),
948        };
949
950        // Skip if public key is a duplicate of an existing entry
951        if self.pubkey_to_index[v1_val.publicKey].read()? != 0 {
952            return skip(self);
953        }
954
955        // Skip if address is a duplicate of an existing entry
956        let addr_idx = self.address_to_index[v1_val.validatorAddress].read()?;
957        if addr_idx != 0
958            && self.validators[(addr_idx - 1) as usize]
959                .deactivated_at_height
960                .read()?
961                == 0
962        {
963            Err(ValidatorConfigV2Error::address_already_has_validator())?
964        }
965
966        let now_active = v1_val.active;
967        let ingress_hash = Self::ingress_key(&v1_val.inboundAddress)?;
968
969        // Skip if ingress ip hash is a duplicate of an existing entry
970        if now_active && self.active_ingress_ips[ingress_hash].read()? {
971            return skip(self);
972        }
973
974        let migrated_idx = self.append_validator(
975            v1_val.validatorAddress,
976            v1_val.publicKey,
977            v1_val.inboundAddress,
978            egress,
979            Address::ZERO,
980            block_height,
981            if now_active { 0 } else { block_height },
982        )?;
983
984        if now_active {
985            self.active_ingress_ips[ingress_hash].write(true)?;
986        }
987
988        self.emit_event(ValidatorConfigV2Event::validator_migrated(
989            migrated_idx,
990            v1_val.validatorAddress,
991            v1_val.publicKey,
992        ))
993    }
994
995    /// Finalizes V1 -> V2 migration by setting `is_init = true`.
996    ///
997    /// Should only be called after all V1 validators have been migrated via
998    /// [`migrate_validator`](Self::migrate_validator). Copies `nextDkgCeremony`
999    /// from V1. After this call, the CL reads from V2 instead of V1 and all
1000    /// mutating functions are unlocked.
1001    ///
1002    /// # Errors
1003    /// - `Unauthorized` — `sender` is not the owner
1004    /// - `AlreadyInitialized` — V2 is already initialized
1005    /// - `MigrationNotComplete` — `validator_count + skipped < v1.validator_count`
1006    pub fn initialize_if_migrated(&mut self, sender: Address) -> Result<()> {
1007        let mut config = self.require_migration_owner(sender)?;
1008
1009        // NOTE: this count comparison is sufficient because `add_validator` and
1010        // `rotate_validator` are blocked until the contract is initialized.
1011        if config.v1_validator_count == 0
1012            || self.validator_count()? + u64::from(config.migration_skipped_count)
1013                < u64::from(config.v1_validator_count)
1014        {
1015            Err(ValidatorConfigV2Error::migration_not_complete())?
1016        }
1017
1018        let v1 = v1();
1019        let v1_next_dkg = v1.get_next_full_dkg_ceremony()?;
1020        self.next_network_identity_rotation_epoch
1021            .write(v1_next_dkg)?;
1022
1023        trace!(address=%self.address, "Initializing validator config v2 precompile after migration");
1024
1025        // Initialize the precompile config
1026        let height = self.storage.block_number();
1027        config.init_at_height = height;
1028        config.is_init = true;
1029        self.config.write(config)?;
1030
1031        self.emit_event(ValidatorConfigV2Event::initialized(height))
1032    }
1033}
1034
1035fn v1() -> ValidatorConfig {
1036    ValidatorConfig::new()
1037}
1038
1039#[cfg(test)]
1040mod tests {
1041    use super::*;
1042    use crate::storage::{StorageCtx, hashmap::HashMapStorageProvider};
1043    use alloy::primitives::Address;
1044    use alloy_primitives::FixedBytes;
1045    use commonware_codec::Encode;
1046    use commonware_cryptography::{Signer, ed25519::PrivateKey};
1047
1048    /// Generate a test Ed25519 key pair and create a valid signature
1049    fn make_test_keypair_and_signature(
1050        validator_address: Address,
1051        ingress: &str,
1052        egress: &str,
1053        kind: SignatureKind,
1054    ) -> (FixedBytes<32>, Vec<u8>) {
1055        // Generate a random private key for testing
1056        let seed = rand_08::random::<u64>();
1057        let private_key = PrivateKey::from_seed(seed);
1058        let public_key = private_key.public_key();
1059
1060        let mut hasher = Keccak256::new();
1061        hasher.update(1u64.to_be_bytes());
1062        hasher.update(VALIDATOR_CONFIG_V2_ADDRESS.as_slice());
1063        hasher.update(validator_address.as_slice());
1064        hasher.update([
1065            u8::try_from(ingress.len()).expect("validator ingress length must fit in uint8")
1066        ]);
1067        hasher.update(ingress.as_bytes());
1068        hasher.update([
1069            u8::try_from(egress.len()).expect("validator egress length must fit in uint8")
1070        ]);
1071        hasher.update(egress.as_bytes());
1072        let namespace = match kind {
1073            SignatureKind::Add { fee_recipient } => {
1074                hasher.update(fee_recipient.as_slice());
1075                VALIDATOR_NS_ADD
1076            }
1077            SignatureKind::Rotate => VALIDATOR_NS_ROTATE,
1078        };
1079        let message = hasher.finalize();
1080
1081        // Sign with namespace
1082        let signature = private_key.sign(namespace, message.as_slice());
1083
1084        // Encode public key to bytes
1085        let pubkey_bytes = public_key.encode();
1086        let mut pubkey_array = [0u8; 32];
1087        pubkey_array.copy_from_slice(&pubkey_bytes);
1088
1089        (
1090            FixedBytes::<32>::from(pubkey_array),
1091            signature.encode().to_vec(),
1092        )
1093    }
1094
1095    fn make_add_call(
1096        addr: Address,
1097        pubkey: FixedBytes<32>,
1098        ingress: &str,
1099        egress: &str,
1100        fee_recipient: Address,
1101        signature: Vec<u8>,
1102    ) -> IValidatorConfigV2::addValidatorCall {
1103        IValidatorConfigV2::addValidatorCall {
1104            validatorAddress: addr,
1105            publicKey: pubkey,
1106            ingress: ingress.to_string(),
1107            egress: egress.to_string(),
1108            feeRecipient: fee_recipient,
1109            signature: signature.into(),
1110        }
1111    }
1112
1113    /// Helper to make a complete add call with generated keys
1114    fn make_valid_add_call(
1115        addr: Address,
1116        ingress: &str,
1117        egress: &str,
1118        fee_recipient: Address,
1119    ) -> IValidatorConfigV2::addValidatorCall {
1120        let (pubkey, signature) = make_test_keypair_and_signature(
1121            addr,
1122            ingress,
1123            egress,
1124            SignatureKind::Add { fee_recipient },
1125        );
1126        make_add_call(addr, pubkey, ingress, egress, fee_recipient, signature)
1127    }
1128
1129    #[test]
1130    fn test_owner_initialization() -> eyre::Result<()> {
1131        let mut storage = HashMapStorageProvider::new(1);
1132        let owner = Address::random();
1133        StorageCtx::enter(&mut storage, || {
1134            let mut vc = ValidatorConfigV2::new();
1135            vc.initialize(owner)?;
1136
1137            assert_eq!(vc.owner()?, owner);
1138            assert!(vc.is_initialized()?);
1139            assert_eq!(vc.get_initialized_at_height()?, 0);
1140            assert_eq!(vc.validator_count()?, 0);
1141
1142            Ok(())
1143        })
1144    }
1145
1146    #[test]
1147    fn test_add_validator() -> eyre::Result<()> {
1148        let mut storage = HashMapStorageProvider::new(1);
1149        let owner = Address::random();
1150        let validator = Address::random();
1151        StorageCtx::enter(&mut storage, || {
1152            let mut vc = ValidatorConfigV2::new();
1153            vc.initialize(owner)?;
1154
1155            let (pubkey, signature) = make_test_keypair_and_signature(
1156                validator,
1157                "192.168.1.1:8000",
1158                "192.168.1.1",
1159                SignatureKind::Add {
1160                    fee_recipient: validator,
1161                },
1162            );
1163            vc.storage.set_block_number(200);
1164            vc.add_validator(
1165                owner,
1166                make_add_call(
1167                    validator,
1168                    pubkey,
1169                    "192.168.1.1:8000",
1170                    "192.168.1.1",
1171                    validator,
1172                    signature,
1173                ),
1174            )?;
1175
1176            assert_eq!(vc.validator_count()?, 1);
1177
1178            let v = vc.validator_by_index(0)?;
1179            assert_eq!(v.publicKey, pubkey);
1180            assert_eq!(v.validatorAddress, validator);
1181            assert_eq!(v.addedAtHeight, 200);
1182            assert_eq!(v.deactivatedAtHeight, 0);
1183            assert_eq!(v.index, 0);
1184
1185            let v2 = vc.validator_by_address(validator)?;
1186            assert_eq!(v2.publicKey, pubkey);
1187
1188            let v3 = vc.validator_by_public_key(pubkey)?;
1189            assert_eq!(v3.validatorAddress, validator);
1190
1191            Ok(())
1192        })
1193    }
1194
1195    #[test]
1196    fn test_add_validator_rejects_unauthorized() -> eyre::Result<()> {
1197        let mut storage = HashMapStorageProvider::new(1);
1198        let owner = Address::random();
1199        let non_owner = Address::random();
1200        StorageCtx::enter(&mut storage, || {
1201            let mut vc = ValidatorConfigV2::new();
1202            vc.initialize(owner)?;
1203
1204            let result = vc.add_validator(
1205                non_owner,
1206                make_valid_add_call(
1207                    Address::random(),
1208                    "192.168.1.1:8000",
1209                    "192.168.1.1",
1210                    Address::random(),
1211                ),
1212            );
1213            assert_eq!(result, Err(ValidatorConfigV2Error::unauthorized().into()));
1214
1215            Ok(())
1216        })
1217    }
1218
1219    #[test]
1220    fn test_add_validator_rejects_zero_pubkey() -> eyre::Result<()> {
1221        let mut storage = HashMapStorageProvider::new(1);
1222        let owner = Address::random();
1223        StorageCtx::enter(&mut storage, || {
1224            let mut vc = ValidatorConfigV2::new();
1225            vc.initialize(owner)?;
1226
1227            let result = vc.add_validator(
1228                owner,
1229                make_add_call(
1230                    Address::random(),
1231                    FixedBytes::<32>::ZERO,
1232                    "192.168.1.1:8000",
1233                    "192.168.1.1",
1234                    Address::random(),
1235                    vec![0u8; 64],
1236                ),
1237            );
1238            assert_eq!(
1239                result,
1240                Err(ValidatorConfigV2Error::invalid_public_key().into())
1241            );
1242
1243            Ok(())
1244        })
1245    }
1246
1247    #[test]
1248    fn test_add_validator_rejects_duplicate_address() -> eyre::Result<()> {
1249        let mut storage = HashMapStorageProvider::new(1);
1250        let owner = Address::random();
1251        let validator = Address::random();
1252        StorageCtx::enter(&mut storage, || {
1253            let mut vc = ValidatorConfigV2::new();
1254            vc.initialize(owner)?;
1255
1256            vc.storage.set_block_number(200);
1257            vc.add_validator(
1258                owner,
1259                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
1260            )?;
1261
1262            vc.storage.set_block_number(201);
1263            let result = vc.add_validator(
1264                owner,
1265                make_valid_add_call(validator, "192.168.1.2:8000", "192.168.1.2", validator),
1266            );
1267            assert_eq!(
1268                result,
1269                Err(ValidatorConfigV2Error::address_already_has_validator().into())
1270            );
1271
1272            Ok(())
1273        })
1274    }
1275
1276    #[test]
1277    fn test_add_validator_rejects_duplicate_pubkey() -> eyre::Result<()> {
1278        let mut storage = HashMapStorageProvider::new(1);
1279        let owner = Address::random();
1280        StorageCtx::enter(&mut storage, || {
1281            let mut vc = ValidatorConfigV2::new();
1282            vc.initialize(owner)?;
1283
1284            // First validator
1285            let addr1 = Address::random();
1286            let (pubkey, sig1) = make_test_keypair_and_signature(
1287                addr1,
1288                "192.168.1.1:8000",
1289                "192.168.1.1",
1290                SignatureKind::Add {
1291                    fee_recipient: addr1,
1292                },
1293            );
1294            vc.storage.set_block_number(200);
1295            vc.add_validator(
1296                owner,
1297                make_add_call(
1298                    addr1,
1299                    pubkey,
1300                    "192.168.1.1:8000",
1301                    "192.168.1.1",
1302                    addr1,
1303                    sig1,
1304                ),
1305            )?;
1306
1307            // Try to add second validator with same public key (but different signature for different address)
1308            let addr2 = Address::random();
1309            let (_, sig2) = make_test_keypair_and_signature(
1310                addr2,
1311                "192.168.1.2:8000",
1312                "192.168.1.2",
1313                SignatureKind::Add {
1314                    fee_recipient: addr2,
1315                },
1316            );
1317            vc.storage.set_block_number(201);
1318            let result = vc.add_validator(
1319                owner,
1320                make_add_call(
1321                    addr2,
1322                    pubkey,
1323                    "192.168.1.2:8000",
1324                    "192.168.1.2",
1325                    addr2,
1326                    sig2,
1327                ),
1328            );
1329            assert_eq!(
1330                result,
1331                Err(ValidatorConfigV2Error::public_key_already_exists().into())
1332            );
1333
1334            Ok(())
1335        })
1336    }
1337
1338    #[test]
1339    fn test_deactivate_validator() -> eyre::Result<()> {
1340        let mut storage = HashMapStorageProvider::new(1);
1341        let owner = Address::random();
1342        let validator = Address::random();
1343        StorageCtx::enter(&mut storage, || {
1344            let mut vc = ValidatorConfigV2::new();
1345            vc.initialize(owner)?;
1346
1347            vc.storage.set_block_number(200);
1348            vc.add_validator(
1349                owner,
1350                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
1351            )?;
1352
1353            vc.storage.set_block_number(300);
1354            vc.deactivate_validator(
1355                owner,
1356                IValidatorConfigV2::deactivateValidatorCall { idx: 0 },
1357            )?;
1358
1359            let v = vc.validator_by_index(0)?;
1360            assert_eq!(v.deactivatedAtHeight, 300);
1361
1362            // Double deactivation fails
1363            vc.storage.set_block_number(301);
1364            let result = vc.deactivate_validator(
1365                owner,
1366                IValidatorConfigV2::deactivateValidatorCall { idx: 0 },
1367            );
1368            assert_eq!(
1369                result,
1370                Err(ValidatorConfigV2Error::validator_already_deactivated().into())
1371            );
1372
1373            Ok(())
1374        })
1375    }
1376
1377    #[test]
1378    fn test_deactivate_validator_dual_auth() -> eyre::Result<()> {
1379        let mut storage = HashMapStorageProvider::new(1);
1380        let owner = Address::random();
1381        let v1 = Address::random();
1382        let v2 = Address::random();
1383        let third_party = Address::random();
1384        StorageCtx::enter(&mut storage, || {
1385            let mut vc = ValidatorConfigV2::new();
1386            vc.initialize(owner)?;
1387
1388            vc.storage.set_block_number(200);
1389            vc.add_validator(
1390                owner,
1391                make_valid_add_call(v1, "192.168.1.1:8000", "192.168.1.1", v1),
1392            )?;
1393            vc.add_validator(
1394                owner,
1395                make_valid_add_call(v2, "192.168.1.2:8000", "192.168.1.2", v2),
1396            )?;
1397
1398            // Third party cannot deactivate
1399            vc.storage.set_block_number(300);
1400            let result = vc.deactivate_validator(
1401                third_party,
1402                IValidatorConfigV2::deactivateValidatorCall { idx: 0 },
1403            );
1404            assert_eq!(result, Err(ValidatorConfigV2Error::unauthorized().into()));
1405
1406            // Validator can deactivate itself
1407            vc.deactivate_validator(v1, IValidatorConfigV2::deactivateValidatorCall { idx: 0 })?;
1408            assert_eq!(vc.validator_by_index(0)?.deactivatedAtHeight, 300);
1409
1410            // Owner can deactivate another validator
1411            vc.storage.set_block_number(301);
1412            vc.deactivate_validator(
1413                owner,
1414                IValidatorConfigV2::deactivateValidatorCall { idx: 1 },
1415            )?;
1416            assert_eq!(vc.validator_by_index(1)?.deactivatedAtHeight, 301);
1417
1418            Ok(())
1419        })
1420    }
1421
1422    #[test]
1423    fn test_rotate_validator() -> eyre::Result<()> {
1424        let mut storage = HashMapStorageProvider::new(1);
1425        let owner = Address::random();
1426        let validator = Address::random();
1427        StorageCtx::enter(&mut storage, || {
1428            let mut vc = ValidatorConfigV2::new();
1429            vc.initialize(owner)?;
1430
1431            // Add initial validator and track the old key
1432            let (old_pubkey, old_sig) = make_test_keypair_and_signature(
1433                validator,
1434                "192.168.1.1:8000",
1435                "192.168.1.1",
1436                SignatureKind::Add {
1437                    fee_recipient: validator,
1438                },
1439            );
1440            vc.storage.set_block_number(200);
1441            vc.add_validator(
1442                owner,
1443                make_add_call(
1444                    validator,
1445                    old_pubkey,
1446                    "192.168.1.1:8000",
1447                    "192.168.1.1",
1448                    validator,
1449                    old_sig,
1450                ),
1451            )?;
1452
1453            // Rotate to new key
1454            let (new_pubkey, new_sig) = make_test_keypair_and_signature(
1455                validator,
1456                "10.0.0.1:8000",
1457                "10.0.0.1",
1458                SignatureKind::Rotate,
1459            );
1460            vc.storage.set_block_number(300);
1461            vc.rotate_validator(
1462                owner,
1463                IValidatorConfigV2::rotateValidatorCall {
1464                    idx: 0,
1465                    publicKey: new_pubkey,
1466                    ingress: "10.0.0.1:8000".to_string(),
1467                    egress: "10.0.0.1".to_string(),
1468                    signature: new_sig.into(),
1469                },
1470            )?;
1471
1472            // Should now have 2 entries
1473            assert_eq!(vc.validator_count()?, 2);
1474
1475            // Original slot updated in-place with new key
1476            let updated = vc.validator_by_index(0)?;
1477            assert_eq!(updated.deactivatedAtHeight, 0);
1478            assert_eq!(updated.publicKey, new_pubkey);
1479            assert_eq!(updated.validatorAddress, validator);
1480            assert_eq!(updated.addedAtHeight, 300);
1481
1482            // Appended snapshot of old validator deactivated
1483            let snapshot = vc.validator_by_index(1)?;
1484            assert_eq!(snapshot.deactivatedAtHeight, 300);
1485            assert_eq!(snapshot.publicKey, old_pubkey);
1486
1487            // address_to_index still points to the original slot
1488            let by_addr = vc.validator_by_address(validator)?;
1489            assert_eq!(by_addr.publicKey, new_pubkey);
1490
1491            // Old pubkey resolves to the appended snapshot
1492            let by_old_pk = vc.validator_by_public_key(old_pubkey)?;
1493            assert_eq!(by_old_pk.deactivatedAtHeight, 300);
1494
1495            Ok(())
1496        })
1497    }
1498
1499    #[test]
1500    fn test_get_active_validators() -> eyre::Result<()> {
1501        let mut storage = HashMapStorageProvider::new(1);
1502        let owner = Address::random();
1503        let v1 = Address::random();
1504        let v2 = Address::random();
1505        StorageCtx::enter(&mut storage, || {
1506            let mut vc = ValidatorConfigV2::new();
1507            vc.initialize(owner)?;
1508
1509            vc.storage.set_block_number(200);
1510            vc.add_validator(
1511                owner,
1512                make_valid_add_call(v1, "192.168.1.1:8000", "192.168.1.1", v1),
1513            )?;
1514            vc.storage.set_block_number(201);
1515            vc.add_validator(
1516                owner,
1517                make_valid_add_call(v2, "192.168.1.2:8000", "192.168.1.2", v2),
1518            )?;
1519
1520            assert_eq!(vc.get_active_validators()?.len(), 2);
1521
1522            vc.storage.set_block_number(300);
1523            vc.deactivate_validator(
1524                owner,
1525                IValidatorConfigV2::deactivateValidatorCall { idx: 0 },
1526            )?;
1527
1528            let active = vc.get_active_validators()?;
1529            assert_eq!(active.len(), 1);
1530            assert_eq!(active[0].validatorAddress, v2);
1531
1532            assert_eq!(vc.validator_count()?, 2);
1533
1534            Ok(())
1535        })
1536    }
1537
1538    #[test]
1539    fn test_set_fee_recipient() -> eyre::Result<()> {
1540        let mut storage = HashMapStorageProvider::new(1);
1541        let owner = Address::random();
1542        let validator = Address::random();
1543        StorageCtx::enter(&mut storage, || {
1544            let mut vc = ValidatorConfigV2::new();
1545            vc.initialize(owner)?;
1546
1547            vc.storage.set_block_number(200);
1548            vc.add_validator(
1549                owner,
1550                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
1551            )?;
1552
1553            let fee_recipient_1 = Address::random();
1554            vc.set_fee_recipient(
1555                owner,
1556                IValidatorConfigV2::setFeeRecipientCall {
1557                    idx: 0,
1558                    feeRecipient: fee_recipient_1,
1559                },
1560            )?;
1561
1562            let v = vc.validator_by_address(validator)?;
1563            assert_eq!(v.feeRecipient, fee_recipient_1);
1564
1565            // Validator can update its own
1566            let fee_recipient_2 = Address::random();
1567            vc.set_fee_recipient(
1568                validator,
1569                IValidatorConfigV2::setFeeRecipientCall {
1570                    idx: 0,
1571                    feeRecipient: fee_recipient_2,
1572                },
1573            )?;
1574
1575            let v = vc.validator_by_address(validator)?;
1576            assert_eq!(v.feeRecipient, fee_recipient_2);
1577
1578            Ok(())
1579        })
1580    }
1581
1582    #[test]
1583    fn test_set_ip_addresses() -> eyre::Result<()> {
1584        let mut storage = HashMapStorageProvider::new(1);
1585        let owner = Address::random();
1586        let validator = Address::random();
1587        StorageCtx::enter(&mut storage, || {
1588            let mut vc = ValidatorConfigV2::new();
1589            vc.initialize(owner)?;
1590
1591            vc.storage.set_block_number(200);
1592            vc.add_validator(
1593                owner,
1594                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
1595            )?;
1596
1597            vc.set_ip_addresses(
1598                owner,
1599                IValidatorConfigV2::setIpAddressesCall {
1600                    idx: 0,
1601                    ingress: "10.0.0.1:8000".to_string(),
1602                    egress: "10.0.0.1".to_string(),
1603                },
1604            )?;
1605
1606            let v = vc.validator_by_address(validator)?;
1607            assert_eq!(v.ingress, "10.0.0.1:8000");
1608            assert_eq!(v.egress, "10.0.0.1");
1609
1610            // Validator can update its own
1611            vc.set_ip_addresses(
1612                validator,
1613                IValidatorConfigV2::setIpAddressesCall {
1614                    idx: 0,
1615                    ingress: "10.0.0.2:8000".to_string(),
1616                    egress: "10.0.0.2".to_string(),
1617                },
1618            )?;
1619
1620            let v = vc.validator_by_address(validator)?;
1621            assert_eq!(v.ingress, "10.0.0.2:8000");
1622
1623            Ok(())
1624        })
1625    }
1626
1627    #[test]
1628    fn test_transfer_ownership() -> eyre::Result<()> {
1629        let mut storage = HashMapStorageProvider::new(1);
1630        let owner = Address::random();
1631        let new_owner = Address::random();
1632        let non_owner = Address::random();
1633        StorageCtx::enter(&mut storage, || {
1634            let mut vc = ValidatorConfigV2::new();
1635
1636            // Rejects pre-init
1637            let result = vc.transfer_ownership(
1638                owner,
1639                IValidatorConfigV2::transferOwnershipCall {
1640                    newOwner: new_owner,
1641                },
1642            );
1643            assert_eq!(
1644                result,
1645                Err(ValidatorConfigV2Error::not_initialized().into())
1646            );
1647
1648            vc.initialize(owner)?;
1649
1650            // Rejects zero address
1651            let result = vc.transfer_ownership(
1652                owner,
1653                IValidatorConfigV2::transferOwnershipCall {
1654                    newOwner: Address::ZERO,
1655                },
1656            );
1657            assert_eq!(result, Err(ValidatorConfigV2Error::invalid_owner().into()));
1658            assert_eq!(vc.owner()?, owner);
1659
1660            // Rejects non-owner
1661            let result = vc.transfer_ownership(
1662                non_owner,
1663                IValidatorConfigV2::transferOwnershipCall {
1664                    newOwner: new_owner,
1665                },
1666            );
1667            assert_eq!(result, Err(ValidatorConfigV2Error::unauthorized().into()));
1668
1669            // Succeeds for owner
1670            vc.transfer_ownership(
1671                owner,
1672                IValidatorConfigV2::transferOwnershipCall {
1673                    newOwner: new_owner,
1674                },
1675            )?;
1676            assert_eq!(vc.owner()?, new_owner);
1677
1678            // Old owner can no longer transfer
1679            let result = vc.transfer_ownership(
1680                owner,
1681                IValidatorConfigV2::transferOwnershipCall {
1682                    newOwner: Address::random(),
1683                },
1684            );
1685            assert_eq!(result, Err(ValidatorConfigV2Error::unauthorized().into()));
1686
1687            Ok(())
1688        })
1689    }
1690
1691    #[test]
1692    fn test_transfer_validator_ownership() -> eyre::Result<()> {
1693        let mut storage = HashMapStorageProvider::new(1);
1694        let owner = Address::random();
1695        let validator = Address::random();
1696        let new_address = Address::random();
1697        StorageCtx::enter(&mut storage, || {
1698            let mut vc = ValidatorConfigV2::new();
1699            vc.initialize(owner)?;
1700
1701            let (pubkey, sig) = make_test_keypair_and_signature(
1702                validator,
1703                "192.168.1.1:8000",
1704                "192.168.1.1",
1705                SignatureKind::Add {
1706                    fee_recipient: validator,
1707                },
1708            );
1709            vc.storage.set_block_number(200);
1710            vc.add_validator(
1711                owner,
1712                make_add_call(
1713                    validator,
1714                    pubkey,
1715                    "192.168.1.1:8000",
1716                    "192.168.1.1",
1717                    validator,
1718                    sig,
1719                ),
1720            )?;
1721
1722            vc.transfer_validator_ownership(
1723                owner,
1724                IValidatorConfigV2::transferValidatorOwnershipCall {
1725                    idx: 0,
1726                    newAddress: new_address,
1727                },
1728            )?;
1729
1730            // Old address lookup gone
1731            let result = vc.validator_by_address(validator);
1732            assert_eq!(
1733                result,
1734                Err(ValidatorConfigV2Error::validator_not_found().into())
1735            );
1736
1737            // New address works
1738            let v = vc.validator_by_address(new_address)?;
1739            assert_eq!(v.publicKey, pubkey);
1740            assert_eq!(v.validatorAddress, new_address);
1741
1742            Ok(())
1743        })
1744    }
1745
1746    #[test]
1747    fn test_transfer_validator_ownership_rejects_deactivated() -> eyre::Result<()> {
1748        let mut storage = HashMapStorageProvider::new(1);
1749        let owner = Address::random();
1750        let validator = Address::random();
1751        StorageCtx::enter(&mut storage, || {
1752            let mut vc = ValidatorConfigV2::new();
1753            vc.initialize(owner)?;
1754
1755            vc.storage.set_block_number(200);
1756            vc.add_validator(
1757                owner,
1758                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
1759            )?;
1760
1761            vc.storage.set_block_number(300);
1762            vc.deactivate_validator(
1763                owner,
1764                IValidatorConfigV2::deactivateValidatorCall { idx: 0 },
1765            )?;
1766
1767            let result = vc.transfer_validator_ownership(
1768                owner,
1769                IValidatorConfigV2::transferValidatorOwnershipCall {
1770                    idx: 0,
1771                    newAddress: Address::random(),
1772                },
1773            );
1774            assert_eq!(
1775                result,
1776                Err(ValidatorConfigV2Error::validator_already_deactivated().into())
1777            );
1778
1779            Ok(())
1780        })
1781    }
1782
1783    #[test]
1784    fn test_set_network_identity_rotation_epoch() -> eyre::Result<()> {
1785        let mut storage = HashMapStorageProvider::new(1);
1786        let owner = Address::random();
1787        StorageCtx::enter(&mut storage, || {
1788            let mut vc = ValidatorConfigV2::new();
1789            vc.initialize(owner)?;
1790
1791            assert_eq!(vc.get_next_network_identity_rotation_epoch()?, 0);
1792
1793            vc.set_network_identity_rotation_epoch(
1794                owner,
1795                IValidatorConfigV2::setNetworkIdentityRotationEpochCall { epoch: 42 },
1796            )?;
1797            assert_eq!(vc.get_next_network_identity_rotation_epoch()?, 42);
1798
1799            let non_owner = Address::random();
1800            let result = vc.set_network_identity_rotation_epoch(
1801                non_owner,
1802                IValidatorConfigV2::setNetworkIdentityRotationEpochCall { epoch: 100 },
1803            );
1804            assert_eq!(result, Err(ValidatorConfigV2Error::unauthorized().into()));
1805
1806            Ok(())
1807        })
1808    }
1809
1810    #[test]
1811    fn test_not_initialized_errors() -> eyre::Result<()> {
1812        let mut storage = HashMapStorageProvider::new(1);
1813        let owner = Address::random();
1814        StorageCtx::enter(&mut storage, || {
1815            let mut vc = ValidatorConfigV2::new();
1816
1817            let result = vc.add_validator(
1818                owner,
1819                make_valid_add_call(
1820                    Address::random(),
1821                    "192.168.1.1:8000",
1822                    "192.168.1.1",
1823                    Address::random(),
1824                ),
1825            );
1826            assert_eq!(
1827                result,
1828                Err(ValidatorConfigV2Error::not_initialized().into())
1829            );
1830
1831            Ok(())
1832        })
1833    }
1834
1835    #[test]
1836    fn test_egress_validates_ip_without_port() -> eyre::Result<()> {
1837        let mut storage = HashMapStorageProvider::new(1);
1838        let owner = Address::random();
1839        StorageCtx::enter(&mut storage, || {
1840            let mut vc = ValidatorConfigV2::new();
1841            vc.initialize(owner)?;
1842
1843            let addr1 = Address::random();
1844            let (pubkey1, sig1) = make_test_keypair_and_signature(
1845                addr1,
1846                "192.168.1.1:8000",
1847                "192.168.1.1:9000",
1848                SignatureKind::Add {
1849                    fee_recipient: addr1,
1850                },
1851            );
1852
1853            // IP:port for egress should fail (egress validation happens before signature)
1854            let result = vc.add_validator(
1855                owner,
1856                make_add_call(
1857                    addr1,
1858                    pubkey1,
1859                    "192.168.1.1:8000",
1860                    "192.168.1.1:9000",
1861                    addr1,
1862                    sig1,
1863                ),
1864            );
1865            assert!(result.is_err(), "egress with port should be rejected");
1866
1867            // Plain IP for egress should succeed
1868            vc.storage.set_block_number(200);
1869            let result = vc.add_validator(
1870                owner,
1871                make_valid_add_call(
1872                    Address::random(),
1873                    "192.168.1.1:8000",
1874                    "192.168.1.1",
1875                    Address::random(),
1876                ),
1877            );
1878            assert!(result.is_ok(), "egress with plain IP should succeed");
1879
1880            Ok(())
1881        })
1882    }
1883
1884    #[test]
1885    fn test_migration_from_v1() -> eyre::Result<()> {
1886        let mut storage = HashMapStorageProvider::new(1);
1887        let owner = Address::random();
1888        let v1_addr = Address::random();
1889        let v2_addr = Address::random();
1890
1891        StorageCtx::enter(&mut storage, || {
1892            // Set up V1 with some validators
1893            let mut v1 = v1();
1894            v1.initialize(owner)?;
1895
1896            v1.add_validator(
1897                owner,
1898                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
1899                    newValidatorAddress: v1_addr,
1900                    publicKey: FixedBytes::<32>::from([0x11; 32]),
1901                    active: true,
1902                    inboundAddress: "192.168.1.1:8000".to_string(),
1903                    outboundAddress: "192.168.1.1:9000".to_string(),
1904                },
1905            )?;
1906
1907            v1.add_validator(
1908                owner,
1909                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
1910                    newValidatorAddress: v2_addr,
1911                    publicKey: FixedBytes::<32>::from([0x22; 32]),
1912                    active: false,
1913                    inboundAddress: "192.168.1.2:8000".to_string(),
1914                    outboundAddress: "192.168.1.2:9000".to_string(),
1915                },
1916            )?;
1917
1918            // Now migrate to V2 (not initialized — migration mode)
1919            let mut v2 = ValidatorConfigV2::new();
1920
1921            // Migrate second validator first (reverse order)
1922            v2.storage.set_block_number(100);
1923            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 1 })?;
1924
1925            assert_eq!(v2.validator_count()?, 1);
1926            let migrated = v2.validator_by_index(0)?;
1927            assert_eq!(migrated.validatorAddress, v2_addr);
1928            assert_eq!(migrated.publicKey, FixedBytes::<32>::from([0x22; 32]));
1929            assert_eq!(migrated.deactivatedAtHeight, 100);
1930
1931            // Migrate first validator
1932            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 0 })?;
1933
1934            assert_eq!(v2.validator_count()?, 2);
1935
1936            // Initialize V2
1937            v2.storage.set_block_number(400);
1938            v2.initialize_if_migrated(owner)?;
1939
1940            assert!(v2.is_initialized()?);
1941
1942            // Migration should be blocked after initialization
1943            v2.storage.set_block_number(100);
1944            let result =
1945                v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 0 });
1946            assert_eq!(
1947                result,
1948                Err(ValidatorConfigV2Error::already_initialized().into())
1949            );
1950
1951            Ok(())
1952        })
1953    }
1954
1955    /// V1 stores outboundAddress as ip:port, but V2 egress is plain IP.
1956    /// Migration must strip the port so migrated data satisfies V2 validation.
1957    #[test]
1958    fn test_migration_strips_port_from_v1_outbound_address() -> eyre::Result<()> {
1959        let mut storage = HashMapStorageProvider::new(1);
1960        let owner = Address::random();
1961        let v1_addr = Address::random();
1962
1963        StorageCtx::enter(&mut storage, || {
1964            // V1 validator with outboundAddress = ip:port
1965            let mut v1 = v1();
1966            v1.initialize(owner)?;
1967            v1.add_validator(
1968                owner,
1969                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
1970                    newValidatorAddress: v1_addr,
1971                    publicKey: FixedBytes::<32>::from([0x11; 32]),
1972                    active: true,
1973                    inboundAddress: "192.168.1.1:8000".to_string(),
1974                    outboundAddress: "192.168.1.1:9000".to_string(),
1975                },
1976            )?;
1977
1978            // Migrate to V2 (not initialized — migration mode)
1979            let mut v2 = ValidatorConfigV2::new();
1980
1981            v2.storage.set_block_number(100);
1982            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 0 })?;
1983            v2.storage.set_block_number(400);
1984            v2.initialize_if_migrated(owner)?;
1985
1986            // Egress should be plain IP (port stripped from V1's "192.168.1.1:9000")
1987            let migrated = v2.validator_by_index(0)?;
1988            assert_eq!(
1989                migrated.egress, "192.168.1.1",
1990                "migration should strip port from V1 outboundAddress"
1991            );
1992
1993            // Ingress preserved as-is (both V1 and V2 use ip:port)
1994            assert_eq!(migrated.ingress, "192.168.1.1:8000");
1995
1996            // setIpAddresses should accept the migrated egress value
1997            v2.set_ip_addresses(
1998                owner,
1999                IValidatorConfigV2::setIpAddressesCall {
2000                    idx: 0,
2001                    ingress: "192.168.1.1:8000".to_string(),
2002                    egress: migrated.egress,
2003                },
2004            )?;
2005
2006            Ok(())
2007        })
2008    }
2009
2010    #[test]
2011    fn test_migration_out_of_order_fails() -> eyre::Result<()> {
2012        let mut storage = HashMapStorageProvider::new(1);
2013        let owner = Address::random();
2014
2015        StorageCtx::enter(&mut storage, || {
2016            // Set up V1 with validators
2017            let mut v1 = v1();
2018            v1.initialize(owner)?;
2019
2020            v1.add_validator(
2021                owner,
2022                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2023                    newValidatorAddress: Address::random(),
2024                    publicKey: FixedBytes::<32>::from([0x11; 32]),
2025                    active: true,
2026                    inboundAddress: "192.168.1.1:8000".to_string(),
2027                    outboundAddress: "192.168.1.1:9000".to_string(),
2028                },
2029            )?;
2030
2031            v1.add_validator(
2032                owner,
2033                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2034                    newValidatorAddress: Address::random(),
2035                    publicKey: FixedBytes::<32>::from([0x22; 32]),
2036                    active: true,
2037                    inboundAddress: "192.168.1.2:8000".to_string(),
2038                    outboundAddress: "192.168.1.2:9000".to_string(),
2039                },
2040            )?;
2041
2042            // Try to migrate out of order (should start at idx 1, not idx 0)
2043            let mut v2 = ValidatorConfigV2::new();
2044            v2.storage.set_block_number(100);
2045            let result =
2046                v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 0 });
2047
2048            assert_eq!(
2049                result,
2050                Err(ValidatorConfigV2Error::invalid_migration_index().into())
2051            );
2052
2053            Ok(())
2054        })
2055    }
2056
2057    #[test]
2058    fn test_initialize_before_migration_complete_fails() -> eyre::Result<()> {
2059        let mut storage = HashMapStorageProvider::new(1);
2060        let owner = Address::random();
2061
2062        StorageCtx::enter(&mut storage, || {
2063            // Set up V1 with 2 validators
2064            let mut v1 = v1();
2065            v1.initialize(owner)?;
2066
2067            v1.add_validator(
2068                owner,
2069                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2070                    newValidatorAddress: Address::random(),
2071                    publicKey: FixedBytes::<32>::from([0x11; 32]),
2072                    active: true,
2073                    inboundAddress: "192.168.1.1:8000".to_string(),
2074                    outboundAddress: "192.168.1.1:9000".to_string(),
2075                },
2076            )?;
2077
2078            v1.add_validator(
2079                owner,
2080                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2081                    newValidatorAddress: Address::random(),
2082                    publicKey: FixedBytes::<32>::from([0x22; 32]),
2083                    active: true,
2084                    inboundAddress: "192.168.1.2:8000".to_string(),
2085                    outboundAddress: "192.168.1.2:9000".to_string(),
2086                },
2087            )?;
2088
2089            // Only migrate second validator (reverse order starts at idx 1)
2090            let mut v2 = ValidatorConfigV2::new();
2091            v2.storage.set_block_number(100);
2092            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 1 })?;
2093
2094            // Try to initialize with incomplete migration
2095            v2.storage.set_block_number(400);
2096            let result = v2.initialize_if_migrated(owner);
2097
2098            assert_eq!(
2099                result,
2100                Err(ValidatorConfigV2Error::migration_not_complete().into())
2101            );
2102
2103            Ok(())
2104        })
2105    }
2106
2107    #[test]
2108    fn test_add_validator_reuses_deactivated_address() -> eyre::Result<()> {
2109        let mut storage = HashMapStorageProvider::new(1);
2110        let owner = Address::random();
2111        let validator_addr = Address::random();
2112        StorageCtx::enter(&mut storage, || {
2113            let mut vc = ValidatorConfigV2::new();
2114            vc.initialize(owner)?;
2115
2116            // Add first validator
2117            let (pubkey1, sig1) = make_test_keypair_and_signature(
2118                validator_addr,
2119                "192.168.1.1:8000",
2120                "192.168.1.1",
2121                SignatureKind::Add {
2122                    fee_recipient: validator_addr,
2123                },
2124            );
2125            vc.storage.set_block_number(200);
2126            vc.add_validator(
2127                owner,
2128                make_add_call(
2129                    validator_addr,
2130                    pubkey1,
2131                    "192.168.1.1:8000",
2132                    "192.168.1.1",
2133                    validator_addr,
2134                    sig1,
2135                ),
2136            )?;
2137
2138            // Deactivate it
2139            vc.storage.set_block_number(300);
2140            vc.deactivate_validator(
2141                owner,
2142                IValidatorConfigV2::deactivateValidatorCall { idx: 0 },
2143            )?;
2144
2145            // Now add new validator with SAME address but different pubkey - should succeed
2146            let (pubkey2, sig2) = make_test_keypair_and_signature(
2147                validator_addr,
2148                "192.168.1.2:8000",
2149                "192.168.1.2",
2150                SignatureKind::Add {
2151                    fee_recipient: validator_addr,
2152                },
2153            );
2154            vc.storage.set_block_number(400);
2155            vc.add_validator(
2156                owner,
2157                make_add_call(
2158                    validator_addr,
2159                    pubkey2,
2160                    "192.168.1.2:8000",
2161                    "192.168.1.2",
2162                    validator_addr,
2163                    sig2,
2164                ),
2165            )?;
2166
2167            // Should have 2 validators
2168            assert_eq!(vc.validator_count()?, 2);
2169
2170            // First one is deactivated
2171            let v1 = vc.validator_by_index(0)?;
2172            assert_eq!(v1.validatorAddress, validator_addr);
2173            assert_eq!(v1.publicKey, pubkey1);
2174            assert_eq!(v1.deactivatedAtHeight, 300);
2175
2176            // Second one is active with same address
2177            let v2 = vc.validator_by_index(1)?;
2178            assert_eq!(v2.validatorAddress, validator_addr);
2179            assert_eq!(v2.publicKey, pubkey2);
2180            assert_eq!(v2.deactivatedAtHeight, 0);
2181
2182            // Lookup by address returns the NEW active validator
2183            let by_addr = vc.validator_by_address(validator_addr)?;
2184            assert_eq!(by_addr.publicKey, pubkey2);
2185            assert_eq!(by_addr.deactivatedAtHeight, 0);
2186
2187            // Lookup by old pubkey returns old deactivated validator
2188            let by_old_pk = vc.validator_by_public_key(pubkey1)?;
2189            assert_eq!(by_old_pk.deactivatedAtHeight, 300);
2190
2191            // Lookup by new pubkey returns new active validator
2192            let by_new_pk = vc.validator_by_public_key(pubkey2)?;
2193            assert_eq!(by_new_pk.deactivatedAtHeight, 0);
2194
2195            Ok(())
2196        })
2197    }
2198
2199    #[test]
2200    fn test_add_validator_rejects_duplicate_ingress() -> eyre::Result<()> {
2201        let mut storage = HashMapStorageProvider::new(1);
2202        let owner = Address::random();
2203        StorageCtx::enter(&mut storage, || {
2204            let mut vc = ValidatorConfigV2::new();
2205            vc.initialize(owner)?;
2206
2207            vc.storage.set_block_number(200);
2208            vc.add_validator(
2209                owner,
2210                make_valid_add_call(
2211                    Address::random(),
2212                    "192.168.1.1:8000",
2213                    "192.168.1.1",
2214                    Address::random(),
2215                ),
2216            )?;
2217
2218            vc.storage.set_block_number(201);
2219            let result = vc.add_validator(
2220                owner,
2221                make_valid_add_call(
2222                    Address::random(),
2223                    "192.168.1.1:8000",
2224                    "192.168.2.1",
2225                    Address::random(),
2226                ),
2227            );
2228
2229            assert!(result.is_err());
2230            Ok(())
2231        })
2232    }
2233
2234    #[test]
2235    fn test_ingress_reuse_after_deactivation() -> eyre::Result<()> {
2236        let mut storage = HashMapStorageProvider::new(1);
2237        let owner = Address::random();
2238        let v1 = Address::random();
2239        StorageCtx::enter(&mut storage, || {
2240            let mut vc = ValidatorConfigV2::new();
2241            vc.initialize(owner)?;
2242
2243            vc.storage.set_block_number(200);
2244            vc.add_validator(
2245                owner,
2246                make_valid_add_call(v1, "192.168.1.1:8000", "192.168.1.1", v1),
2247            )?;
2248
2249            vc.storage.set_block_number(300);
2250            vc.deactivate_validator(
2251                owner,
2252                IValidatorConfigV2::deactivateValidatorCall { idx: 0 },
2253            )?;
2254
2255            // Should allow IP reuse after deactivation
2256            vc.storage.set_block_number(400);
2257            vc.add_validator(
2258                owner,
2259                make_valid_add_call(
2260                    Address::random(),
2261                    "192.168.1.1:8000",
2262                    "192.168.1.1",
2263                    Address::random(),
2264                ),
2265            )?;
2266
2267            Ok(())
2268        })
2269    }
2270
2271    #[test]
2272    fn test_ingress_reuse_after_rotation() -> eyre::Result<()> {
2273        let mut storage = HashMapStorageProvider::new(1);
2274        let owner = Address::random();
2275        let v1 = Address::random();
2276        StorageCtx::enter(&mut storage, || {
2277            let mut vc = ValidatorConfigV2::new();
2278            vc.initialize(owner)?;
2279
2280            vc.storage.set_block_number(200);
2281            vc.add_validator(
2282                owner,
2283                make_valid_add_call(v1, "[2001:db8::1]:8000", "2001:db8::1", Address::random()),
2284            )?;
2285
2286            vc.storage.set_block_number(300);
2287
2288            // Rotate to different ingress
2289            let (new_pubkey, new_sig) = make_test_keypair_and_signature(
2290                v1,
2291                "[2001:db8::1]:8001",
2292                "2001:db8::1",
2293                SignatureKind::Rotate,
2294            );
2295            vc.rotate_validator(
2296                owner,
2297                IValidatorConfigV2::rotateValidatorCall {
2298                    idx: 0,
2299                    publicKey: new_pubkey,
2300                    ingress: "[2001:db8::1]:8001".to_string(),
2301                    egress: "2001:db8::1".to_string(),
2302                    signature: new_sig.into(),
2303                },
2304            )?;
2305            let v = vc.validator_by_address(v1)?;
2306            assert_eq!(v.ingress, "[2001:db8::1]:8001");
2307
2308            // Should allow ingress reuse after rotation.
2309            vc.storage.set_block_number(400);
2310            vc.set_ip_addresses(
2311                owner,
2312                IValidatorConfigV2::setIpAddressesCall {
2313                    idx: 0,
2314                    ingress: "[2001:db8::1]:8000".to_string(),
2315                    egress: "2001:db8::1".to_string(),
2316                },
2317            )?;
2318            let v = vc.validator_by_address(v1)?;
2319            assert_eq!(v.ingress, "[2001:db8::1]:8000");
2320
2321            Ok(())
2322        })
2323    }
2324
2325    #[test]
2326    fn test_set_ip_addresses_rejects_pre_init() -> eyre::Result<()> {
2327        let mut storage = HashMapStorageProvider::new(1);
2328        let owner = Address::random();
2329        let v1_addr = Address::random();
2330
2331        StorageCtx::enter(&mut storage, || {
2332            // Setup V1
2333            let mut v1 = v1();
2334            v1.initialize(owner)?;
2335            v1.add_validator(
2336                owner,
2337                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2338                    newValidatorAddress: v1_addr,
2339                    publicKey: FixedBytes::<32>::from([0x11; 32]),
2340                    active: true,
2341                    inboundAddress: "192.168.1.1:8000".to_string(),
2342                    outboundAddress: "192.168.1.1:9000".to_string(),
2343                },
2344            )?;
2345
2346            // Migrate to V2
2347            let mut v2 = ValidatorConfigV2::new();
2348            v2.storage.set_block_number(100);
2349            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 0 })?;
2350
2351            let result = v2.set_ip_addresses(
2352                owner,
2353                IValidatorConfigV2::setIpAddressesCall {
2354                    idx: 0,
2355                    ingress: "10.0.0.1:8000".to_string(),
2356                    egress: "10.0.0.1".to_string(),
2357                },
2358            );
2359
2360            assert_eq!(
2361                result,
2362                Err(ValidatorConfigV2Error::not_initialized().into())
2363            );
2364            Ok(())
2365        })
2366    }
2367
2368    #[test]
2369    fn test_rotate_removes_and_checks_ips() -> eyre::Result<()> {
2370        let mut storage = HashMapStorageProvider::new(1);
2371        let owner = Address::random();
2372        let v1 = Address::random();
2373        let v2 = Address::random();
2374
2375        StorageCtx::enter(&mut storage, || {
2376            let mut vc = ValidatorConfigV2::new();
2377            vc.initialize(owner)?;
2378
2379            vc.storage.set_block_number(200);
2380            vc.add_validator(
2381                owner,
2382                make_valid_add_call(v1, "192.168.1.1:8000", "192.168.1.1", v1),
2383            )?;
2384            vc.add_validator(
2385                owner,
2386                make_valid_add_call(v2, "192.168.2.1:8000", "192.168.2.1", v2),
2387            )?;
2388
2389            // Rotate v1 to v2's IPs should fail
2390            let (new_pk, sig) = make_test_keypair_and_signature(
2391                v1,
2392                "192.168.2.1:8000",
2393                "192.168.2.1",
2394                SignatureKind::Rotate,
2395            );
2396
2397            vc.storage.set_block_number(300);
2398            let result = vc.rotate_validator(
2399                owner,
2400                IValidatorConfigV2::rotateValidatorCall {
2401                    idx: 0,
2402                    publicKey: new_pk,
2403                    ingress: "192.168.2.1:8000".to_string(),
2404                    egress: "192.168.2.1".to_string(),
2405                    signature: sig.into(),
2406                },
2407            );
2408
2409            assert!(result.is_err());
2410            Ok(())
2411        })
2412    }
2413
2414    #[test]
2415    fn test_migrate_skips_duplicate_ingress() -> eyre::Result<()> {
2416        let mut storage = HashMapStorageProvider::new(1);
2417        let owner = Address::random();
2418
2419        StorageCtx::enter(&mut storage, || {
2420            let mut v1 = v1();
2421            v1.initialize(owner)?;
2422            v1.add_validator(
2423                owner,
2424                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2425                    newValidatorAddress: Address::random(),
2426                    publicKey: FixedBytes::<32>::from([0x11; 32]),
2427                    active: true,
2428                    inboundAddress: "192.168.1.1:8000".to_string(),
2429                    outboundAddress: "192.168.1.1:9000".to_string(),
2430                },
2431            )?;
2432            v1.add_validator(
2433                owner,
2434                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2435                    newValidatorAddress: Address::random(),
2436                    publicKey: FixedBytes::<32>::from([0x22; 32]),
2437                    active: true,
2438                    inboundAddress: "192.168.1.1:8000".to_string(),
2439                    outboundAddress: "192.168.2.1:9000".to_string(),
2440                },
2441            )?;
2442
2443            let mut v2 = ValidatorConfigV2::new();
2444            v2.storage.set_block_number(100);
2445            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 1 })?;
2446
2447            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 0 })?;
2448            assert_eq!(v2.validator_count()?, 1);
2449            assert_eq!(v2.config.migration_skipped_count.read()?, 1);
2450
2451            v2.storage.set_block_number(400);
2452            v2.initialize_if_migrated(owner)?;
2453            assert!(v2.is_initialized()?);
2454
2455            Ok(())
2456        })
2457    }
2458
2459    #[test]
2460    fn test_migrate_skips_invalid_ed25519_pubkey() -> eyre::Result<()> {
2461        let mut storage = HashMapStorageProvider::new(1);
2462        let owner = Address::random();
2463
2464        StorageCtx::enter(&mut storage, || {
2465            let mut v1 = v1();
2466            v1.initialize(owner)?;
2467            v1.add_validator(
2468                owner,
2469                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2470                    newValidatorAddress: Address::random(),
2471                    publicKey: FixedBytes::<32>::from([0xDD; 32]),
2472                    active: true,
2473                    inboundAddress: "192.168.1.1:8000".to_string(),
2474                    outboundAddress: "192.168.1.1:9000".to_string(),
2475                },
2476            )?;
2477            v1.add_validator(
2478                owner,
2479                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2480                    newValidatorAddress: Address::random(),
2481                    publicKey: FixedBytes::<32>::from([0x22; 32]),
2482                    active: true,
2483                    inboundAddress: "192.168.1.2:8000".to_string(),
2484                    outboundAddress: "192.168.1.2:9000".to_string(),
2485                },
2486            )?;
2487
2488            let mut v2 = ValidatorConfigV2::new();
2489            v2.storage.set_block_number(100);
2490
2491            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 1 })?;
2492            assert_eq!(v2.validator_count()?, 1);
2493
2494            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 0 })?;
2495            assert_eq!(v2.validator_count()?, 1);
2496            assert_eq!(v2.config.migration_skipped_count.read()?, 1);
2497
2498            v2.storage.set_block_number(400);
2499            v2.initialize_if_migrated(owner)?;
2500            assert!(v2.is_initialized()?);
2501
2502            Ok(())
2503        })
2504    }
2505
2506    #[test]
2507    fn test_migrate_overwrites_duplicate_pubkey() -> eyre::Result<()> {
2508        let mut storage = HashMapStorageProvider::new(1);
2509        let owner = Address::random();
2510        let addr1 = Address::random();
2511        let addr2 = Address::random();
2512
2513        StorageCtx::enter(&mut storage, || {
2514            let mut v1 = v1();
2515            v1.initialize(owner)?;
2516            v1.add_validator(
2517                owner,
2518                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2519                    newValidatorAddress: addr1,
2520                    publicKey: FixedBytes::<32>::from([0x11; 32]),
2521                    active: true,
2522                    inboundAddress: "192.168.1.1:8000".to_string(),
2523                    outboundAddress: "192.168.1.1:9000".to_string(),
2524                },
2525            )?;
2526            v1.add_validator(
2527                owner,
2528                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
2529                    newValidatorAddress: addr2,
2530                    publicKey: FixedBytes::<32>::from([0x11; 32]),
2531                    active: true,
2532                    inboundAddress: "192.168.1.2:8000".to_string(),
2533                    outboundAddress: "192.168.1.2:9000".to_string(),
2534                },
2535            )?;
2536
2537            let mut v2 = ValidatorConfigV2::new();
2538            v2.storage.set_block_number(100);
2539
2540            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 1 })?;
2541            assert_eq!(v2.validator_count()?, 1);
2542
2543            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 0 })?;
2544            assert_eq!(v2.validator_count()?, 1);
2545            assert_eq!(v2.config.migration_skipped_count.read()?, 1);
2546
2547            let migrated = v2.validator_by_index(0)?;
2548            assert_eq!(migrated.validatorAddress, addr2);
2549            assert_eq!(migrated.ingress, "192.168.1.2:8000");
2550            assert_eq!(migrated.egress, "192.168.1.2");
2551
2552            v2.storage.set_block_number(400);
2553            v2.initialize_if_migrated(owner)?;
2554            assert!(v2.is_initialized()?);
2555
2556            Ok(())
2557        })
2558    }
2559
2560    #[test]
2561    fn test_add_validator_rejects_third_party() -> eyre::Result<()> {
2562        let mut storage = HashMapStorageProvider::new(1);
2563        let owner = Address::random();
2564        let validator = Address::random();
2565        let third_party = Address::random();
2566        StorageCtx::enter(&mut storage, || {
2567            let mut vc = ValidatorConfigV2::new();
2568            vc.initialize(owner)?;
2569
2570            vc.storage.set_block_number(200);
2571            vc.add_validator(
2572                owner,
2573                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
2574            )?;
2575
2576            // Third party is neither owner nor validator_owner — should be rejected
2577            let result = vc.set_ip_addresses(
2578                third_party,
2579                IValidatorConfigV2::setIpAddressesCall {
2580                    idx: 0,
2581                    ingress: "10.0.0.1:8000".to_string(),
2582                    egress: "10.0.0.1".to_string(),
2583                },
2584            );
2585            assert_eq!(result, Err(ValidatorConfigV2Error::unauthorized().into()));
2586
2587            let result = vc.transfer_validator_ownership(
2588                third_party,
2589                IValidatorConfigV2::transferValidatorOwnershipCall {
2590                    idx: 0,
2591                    newAddress: Address::random(),
2592                },
2593            );
2594            assert_eq!(result, Err(ValidatorConfigV2Error::unauthorized().into()));
2595
2596            Ok(())
2597        })
2598    }
2599
2600    #[test]
2601    fn test_rotate_validator_to_different_ingress() -> eyre::Result<()> {
2602        let mut storage = HashMapStorageProvider::new(1);
2603        let owner = Address::random();
2604        let validator = Address::random();
2605        StorageCtx::enter(&mut storage, || {
2606            let mut vc = ValidatorConfigV2::new();
2607            vc.initialize(owner)?;
2608
2609            vc.storage.set_block_number(200);
2610            vc.add_validator(
2611                owner,
2612                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
2613            )?;
2614
2615            // Rotate keeping the same ingress/egress — should succeed
2616            let (new_pubkey, new_sig) = make_test_keypair_and_signature(
2617                validator,
2618                "192.168.1.1:8001",
2619                "192.168.1.1",
2620                SignatureKind::Rotate,
2621            );
2622            vc.storage.set_block_number(300);
2623            vc.rotate_validator(
2624                owner,
2625                IValidatorConfigV2::rotateValidatorCall {
2626                    idx: 0,
2627                    publicKey: new_pubkey,
2628                    ingress: "192.168.1.1:8001".to_string(),
2629                    egress: "192.168.1.1".to_string(),
2630                    signature: new_sig.into(),
2631                },
2632            )?;
2633
2634            assert_eq!(vc.validator_count()?, 2);
2635            assert_eq!(vc.validator_by_index(0)?.deactivatedAtHeight, 0);
2636            assert_eq!(vc.validator_by_index(1)?.deactivatedAtHeight, 300);
2637            assert_eq!(vc.validator_by_address(validator)?.publicKey, new_pubkey);
2638            assert_eq!(
2639                vc.validator_by_address(validator)?.ingress,
2640                "192.168.1.1:8001"
2641            );
2642            assert_eq!(vc.validator_by_address(validator)?.egress, "192.168.1.1");
2643
2644            Ok(())
2645        })
2646    }
2647
2648    #[test]
2649    fn test_rotate_validator_rejects_same_ingress() -> eyre::Result<()> {
2650        let mut storage = HashMapStorageProvider::new(1);
2651        let owner = Address::random();
2652        let validator = Address::random();
2653        StorageCtx::enter(&mut storage, || {
2654            let mut vc = ValidatorConfigV2::new();
2655            vc.initialize(owner)?;
2656
2657            vc.storage.set_block_number(200);
2658            vc.add_validator(
2659                owner,
2660                make_valid_add_call(
2661                    validator,
2662                    "192.168.1.1:8000",
2663                    "192.168.1.1",
2664                    Address::random(),
2665                ),
2666            )?;
2667
2668            // Rotate keeping the same ingress/egress — should succeed
2669            let (new_pubkey, new_sig) = make_test_keypair_and_signature(
2670                validator,
2671                "192.168.1.1:8000",
2672                "192.168.1.1",
2673                SignatureKind::Rotate,
2674            );
2675            vc.storage.set_block_number(300);
2676            assert!(
2677                vc.rotate_validator(
2678                    owner,
2679                    IValidatorConfigV2::rotateValidatorCall {
2680                        idx: 0,
2681                        publicKey: new_pubkey,
2682                        ingress: "192.168.1.1:8000".to_string(),
2683                        egress: "192.168.1.1".to_string(),
2684                        signature: new_sig.into(),
2685                    },
2686                )
2687                .is_err()
2688            );
2689            Ok(())
2690        })
2691    }
2692
2693    #[test]
2694    fn test_set_ip_addresses_ingress_only() -> eyre::Result<()> {
2695        let mut storage = HashMapStorageProvider::new(1);
2696        let owner = Address::random();
2697        let validator = Address::random();
2698        StorageCtx::enter(&mut storage, || {
2699            let mut vc = ValidatorConfigV2::new();
2700            vc.initialize(owner)?;
2701
2702            vc.storage.set_block_number(200);
2703            vc.add_validator(
2704                owner,
2705                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
2706            )?;
2707
2708            // Change ingress only, keep egress the same
2709            vc.set_ip_addresses(
2710                owner,
2711                IValidatorConfigV2::setIpAddressesCall {
2712                    idx: 0,
2713                    ingress: "10.0.0.1:8000".to_string(),
2714                    egress: "192.168.1.1".to_string(),
2715                },
2716            )?;
2717
2718            let v = vc.validator_by_address(validator)?;
2719            assert_eq!(v.ingress, "10.0.0.1:8000");
2720            assert_eq!(v.egress, "192.168.1.1");
2721
2722            Ok(())
2723        })
2724    }
2725
2726    #[test]
2727    fn test_set_ip_addresses_ingress_port_only() -> eyre::Result<()> {
2728        let mut storage = HashMapStorageProvider::new(1);
2729        let owner = Address::random();
2730        let validator = Address::random();
2731        StorageCtx::enter(&mut storage, || {
2732            let mut vc = ValidatorConfigV2::new();
2733            vc.initialize(owner)?;
2734
2735            vc.storage.set_block_number(200);
2736            vc.add_validator(
2737                owner,
2738                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
2739            )?;
2740
2741            // Change ingress only, keep egress the same
2742            vc.set_ip_addresses(
2743                owner,
2744                IValidatorConfigV2::setIpAddressesCall {
2745                    idx: 0,
2746                    ingress: "192.168.1.1:8001".to_string(),
2747                    egress: "192.168.1.1".to_string(),
2748                },
2749            )?;
2750
2751            let v = vc.validator_by_address(validator)?;
2752            assert_eq!(v.ingress, "192.168.1.1:8001");
2753            assert_eq!(v.egress, "192.168.1.1");
2754
2755            Ok(())
2756        })
2757    }
2758
2759    #[test]
2760    fn test_set_ip_addresses_egress_only() -> eyre::Result<()> {
2761        let mut storage = HashMapStorageProvider::new(1);
2762        let owner = Address::random();
2763        let validator = Address::random();
2764        StorageCtx::enter(&mut storage, || {
2765            let mut vc = ValidatorConfigV2::new();
2766            vc.initialize(owner)?;
2767
2768            vc.storage.set_block_number(200);
2769            vc.add_validator(
2770                owner,
2771                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
2772            )?;
2773
2774            // Change egress only, keep ingress the same
2775            vc.set_ip_addresses(
2776                owner,
2777                IValidatorConfigV2::setIpAddressesCall {
2778                    idx: 0,
2779                    ingress: "192.168.1.1:8000".to_string(),
2780                    egress: "10.0.0.1".to_string(),
2781                },
2782            )?;
2783
2784            let v = vc.validator_by_address(validator)?;
2785            assert_eq!(v.ingress, "192.168.1.1:8000");
2786            assert_eq!(v.egress, "10.0.0.1");
2787
2788            Ok(())
2789        })
2790    }
2791
2792    #[test]
2793    fn test_set_ip_addresses_rejects_duplicate_ingress() -> eyre::Result<()> {
2794        let mut storage = HashMapStorageProvider::new(1);
2795        let owner = Address::random();
2796        let v1 = Address::random();
2797        let v2 = Address::random();
2798        StorageCtx::enter(&mut storage, || {
2799            let mut vc = ValidatorConfigV2::new();
2800            vc.initialize(owner)?;
2801
2802            vc.storage.set_block_number(200);
2803            vc.add_validator(
2804                owner,
2805                make_valid_add_call(v1, "192.168.1.1:8000", "192.168.1.1", v1),
2806            )?;
2807            vc.add_validator(
2808                owner,
2809                make_valid_add_call(v2, "192.168.2.1:8000", "192.168.2.1", v2),
2810            )?;
2811
2812            let result = vc.set_ip_addresses(
2813                owner,
2814                IValidatorConfigV2::setIpAddressesCall {
2815                    idx: 1,
2816                    ingress: "192.168.1.1:8000".to_string(),
2817                    egress: "192.168.2.1".to_string(),
2818                },
2819            );
2820
2821            assert!(result.is_err());
2822            Ok(())
2823        })
2824    }
2825
2826    #[test]
2827    fn test_set_ip_addresses_allows_same_ip_different_port() -> eyre::Result<()> {
2828        let mut storage = HashMapStorageProvider::new(1);
2829        let owner = Address::random();
2830        let validator = Address::random();
2831        StorageCtx::enter(&mut storage, || {
2832            let mut vc = ValidatorConfigV2::new();
2833            vc.initialize(owner)?;
2834
2835            vc.storage.set_block_number(200);
2836            vc.add_validator(
2837                owner,
2838                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
2839            )?;
2840
2841            vc.set_ip_addresses(
2842                owner,
2843                IValidatorConfigV2::setIpAddressesCall {
2844                    idx: 0,
2845                    ingress: "192.168.1.1:9000".to_string(),
2846                    egress: "192.168.1.1".to_string(),
2847                },
2848            )?;
2849
2850            let v = vc.validator_by_address(validator)?;
2851            assert_eq!(v.ingress, "192.168.1.1:9000");
2852
2853            Ok(())
2854        })
2855    }
2856
2857    #[test]
2858    fn test_transfer_validator_ownership_by_validator() -> eyre::Result<()> {
2859        let mut storage = HashMapStorageProvider::new(1);
2860        let owner = Address::random();
2861        let validator = Address::random();
2862        let new_address = Address::random();
2863        StorageCtx::enter(&mut storage, || {
2864            let mut vc = ValidatorConfigV2::new();
2865            vc.initialize(owner)?;
2866
2867            vc.storage.set_block_number(200);
2868            vc.add_validator(
2869                owner,
2870                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
2871            )?;
2872
2873            // Validator transfers its own ownership (not owner)
2874            vc.transfer_validator_ownership(
2875                validator,
2876                IValidatorConfigV2::transferValidatorOwnershipCall {
2877                    idx: 0,
2878                    newAddress: new_address,
2879                },
2880            )?;
2881
2882            let result = vc.validator_by_address(validator);
2883            assert_eq!(
2884                result,
2885                Err(ValidatorConfigV2Error::validator_not_found().into())
2886            );
2887
2888            let v = vc.validator_by_address(new_address)?;
2889            assert_eq!(v.validatorAddress, new_address);
2890
2891            Ok(())
2892        })
2893    }
2894
2895    #[test]
2896    fn test_add_validator_rejects_deleted_pubkey() -> eyre::Result<()> {
2897        let mut storage = HashMapStorageProvider::new(1);
2898        let owner = Address::random();
2899        StorageCtx::enter(&mut storage, || {
2900            let mut vc = ValidatorConfigV2::new();
2901            vc.initialize(owner)?;
2902
2903            let addr1 = Address::random();
2904            let (pubkey, sig) = make_test_keypair_and_signature(
2905                addr1,
2906                "192.168.1.1:8000",
2907                "192.168.1.1",
2908                SignatureKind::Add {
2909                    fee_recipient: addr1,
2910                },
2911            );
2912            vc.storage.set_block_number(200);
2913            vc.add_validator(
2914                owner,
2915                make_add_call(addr1, pubkey, "192.168.1.1:8000", "192.168.1.1", addr1, sig),
2916            )?;
2917
2918            // Deactivate
2919            vc.storage.set_block_number(300);
2920            vc.deactivate_validator(
2921                owner,
2922                IValidatorConfigV2::deactivateValidatorCall { idx: 0 },
2923            )?;
2924
2925            // Try to add a new validator reusing the deleted pubkey — should fail
2926            let addr2 = Address::random();
2927            let result = vc.add_validator(
2928                owner,
2929                make_add_call(
2930                    addr2,
2931                    pubkey,
2932                    "192.168.2.1:8000",
2933                    "192.168.2.1",
2934                    addr2,
2935                    vec![0u8; 64],
2936                ),
2937            );
2938            assert_eq!(
2939                result,
2940                Err(ValidatorConfigV2Error::public_key_already_exists().into())
2941            );
2942
2943            Ok(())
2944        })
2945    }
2946
2947    #[test]
2948    fn test_add_validator_with_ipv6() -> eyre::Result<()> {
2949        let mut storage = HashMapStorageProvider::new(1);
2950        let owner = Address::random();
2951        let validator = Address::random();
2952        StorageCtx::enter(&mut storage, || {
2953            let mut vc = ValidatorConfigV2::new();
2954            vc.initialize(owner)?;
2955
2956            // Add validator with IPv6 ingress
2957            vc.storage.set_block_number(200);
2958            vc.add_validator(
2959                owner,
2960                make_valid_add_call(validator, "[::1]:8000", "::1", validator),
2961            )?;
2962
2963            assert_eq!(vc.validator_count()?, 1);
2964            let v = vc.validator_by_index(0)?;
2965            assert_eq!(v.validatorAddress, validator);
2966            assert_eq!(v.ingress, "[::1]:8000");
2967            assert_eq!(v.egress, "::1");
2968
2969            Ok(())
2970        })
2971    }
2972
2973    #[test]
2974    fn test_add_validator_rejects_duplicate_ingress_ipv6() -> eyre::Result<()> {
2975        let mut storage = HashMapStorageProvider::new(1);
2976        let owner = Address::random();
2977        StorageCtx::enter(&mut storage, || {
2978            let mut vc = ValidatorConfigV2::new();
2979            vc.initialize(owner)?;
2980
2981            vc.storage.set_block_number(200);
2982            vc.add_validator(
2983                owner,
2984                make_valid_add_call(
2985                    Address::random(),
2986                    "[2001:db8::1]:8000",
2987                    "2001:db8::1",
2988                    Address::random(),
2989                ),
2990            )?;
2991
2992            // Try to add another validator with same IPv6 IP (different port)
2993            vc.storage.set_block_number(201);
2994            let result = vc.add_validator(
2995                owner,
2996                make_valid_add_call(
2997                    Address::random(),
2998                    "[2001:db8::1]:8000",
2999                    "2001:db8::2",
3000                    Address::random(),
3001                ),
3002            );
3003
3004            assert!(result.is_err());
3005            Ok(())
3006        })
3007    }
3008
3009    #[test]
3010    fn test_ipv6_reuse_after_deactivation() -> eyre::Result<()> {
3011        let mut storage = HashMapStorageProvider::new(1);
3012        let owner = Address::random();
3013        let v1 = Address::random();
3014        StorageCtx::enter(&mut storage, || {
3015            let mut vc = ValidatorConfigV2::new();
3016            vc.initialize(owner)?;
3017
3018            vc.storage.set_block_number(200);
3019            vc.add_validator(
3020                owner,
3021                make_valid_add_call(v1, "[2001:db8::1]:8000", "2001:db8::1", v1),
3022            )?;
3023
3024            vc.storage.set_block_number(300);
3025            vc.deactivate_validator(
3026                owner,
3027                IValidatorConfigV2::deactivateValidatorCall { idx: 0 },
3028            )?;
3029
3030            // Should allow IPv6 reuse after deactivation
3031            vc.storage.set_block_number(400);
3032            vc.add_validator(
3033                owner,
3034                make_valid_add_call(
3035                    Address::random(),
3036                    "[2001:db8::1]:8000",
3037                    "2001:db8::1",
3038                    Address::random(),
3039                ),
3040            )?;
3041
3042            Ok(())
3043        })
3044    }
3045
3046    #[test]
3047    fn test_rotate_validator_with_ipv6() -> eyre::Result<()> {
3048        let mut storage = HashMapStorageProvider::new(1);
3049        let owner = Address::random();
3050        let validator = Address::random();
3051        StorageCtx::enter(&mut storage, || {
3052            let mut vc = ValidatorConfigV2::new();
3053            vc.initialize(owner)?;
3054
3055            // Add initial validator with IPv4
3056            vc.storage.set_block_number(200);
3057            vc.add_validator(
3058                owner,
3059                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", validator),
3060            )?;
3061
3062            // Rotate to IPv6
3063            let (new_pubkey, new_sig) = make_test_keypair_and_signature(
3064                validator,
3065                "[2001:db8::1]:8000",
3066                "2001:db8::1",
3067                SignatureKind::Rotate,
3068            );
3069            vc.storage.set_block_number(300);
3070            vc.rotate_validator(
3071                owner,
3072                IValidatorConfigV2::rotateValidatorCall {
3073                    idx: 0,
3074                    publicKey: new_pubkey,
3075                    ingress: "[2001:db8::1]:8000".to_string(),
3076                    egress: "2001:db8::1".to_string(),
3077                    signature: new_sig.into(),
3078                },
3079            )?;
3080
3081            assert_eq!(vc.validator_count()?, 2);
3082            let updated = vc.validator_by_index(0)?;
3083            assert_eq!(updated.deactivatedAtHeight, 0);
3084            assert_eq!(updated.ingress, "[2001:db8::1]:8000");
3085            assert_eq!(updated.egress, "2001:db8::1");
3086
3087            let snapshot = vc.validator_by_index(1)?;
3088            assert_eq!(snapshot.deactivatedAtHeight, 300);
3089
3090            Ok(())
3091        })
3092    }
3093
3094    #[test]
3095    fn test_ipv6_canonical_representation() -> eyre::Result<()> {
3096        let mut storage = HashMapStorageProvider::new(1);
3097        let owner = Address::random();
3098        StorageCtx::enter(&mut storage, || {
3099            let mut vc = ValidatorConfigV2::new();
3100            vc.initialize(owner)?;
3101
3102            // Add validator with compressed IPv6 notation
3103            vc.storage.set_block_number(200);
3104            vc.add_validator(
3105                owner,
3106                make_valid_add_call(Address::random(), "[::1]:8000", "::1", Address::random()),
3107            )?;
3108
3109            // Try to add another validator with expanded IPv6 notation of same IP
3110            // This should fail because [::1] and [0:0:0:0:0:0:0:1] are the same IP
3111            vc.storage.set_block_number(201);
3112            let result = vc.add_validator(
3113                owner,
3114                make_valid_add_call(
3115                    Address::random(),
3116                    "[0:0:0:0:0:0:0:1]:8000",
3117                    "::1",
3118                    Address::random(),
3119                ),
3120            );
3121
3122            assert!(
3123                result.is_err(),
3124                "Different IPv6 notations of same IP should be rejected"
3125            );
3126
3127            // No scope and %0 are the same - should fail.
3128            vc.storage.set_block_number(202);
3129            let result = vc.add_validator(
3130                owner,
3131                make_valid_add_call(Address::random(), "[::1%0]:8000", "::1", Address::random()),
3132            );
3133
3134            assert!(
3135                result.is_err(),
3136                "Different IPv6 notations of same IP should be rejected"
3137            );
3138
3139            // Same IP/Port but different port should succeed.
3140            vc.storage.set_block_number(203);
3141            let result = vc.add_validator(
3142                owner,
3143                make_valid_add_call(Address::random(), "[::1%1]:8000", "::1", Address::random()),
3144            );
3145            assert!(result.is_ok());
3146
3147            Ok(())
3148        })
3149    }
3150
3151    #[test]
3152    fn test_add_validator_rejects_wrong_key_signature() -> eyre::Result<()> {
3153        let mut storage = HashMapStorageProvider::new(1);
3154        let owner = Address::random();
3155        let validator = Address::random();
3156        let fee_recipient = Address::random();
3157        StorageCtx::enter(&mut storage, || {
3158            let mut vc = ValidatorConfigV2::new();
3159            vc.initialize(owner)?;
3160
3161            // Generate a valid keypair for a different key
3162            let (pubkey, _) = make_test_keypair_and_signature(
3163                validator,
3164                "192.168.1.1:8000",
3165                "192.168.1.1",
3166                SignatureKind::Add { fee_recipient },
3167            );
3168
3169            // Generate signature from a completely different key
3170            let (_, wrong_sig) = make_test_keypair_and_signature(
3171                validator,
3172                "192.168.1.1:8000",
3173                "192.168.1.1",
3174                SignatureKind::Add { fee_recipient },
3175            );
3176
3177            vc.storage.set_block_number(200);
3178            let result = vc.add_validator(
3179                owner,
3180                make_add_call(
3181                    validator,
3182                    pubkey,
3183                    "192.168.1.1:8000",
3184                    "192.168.1.1",
3185                    fee_recipient,
3186                    wrong_sig,
3187                ),
3188            );
3189            assert_eq!(
3190                result,
3191                Err(ValidatorConfigV2Error::invalid_signature().into())
3192            );
3193
3194            Ok(())
3195        })
3196    }
3197
3198    #[test]
3199    fn test_add_validator_rejects_wrong_namespace_signature() -> eyre::Result<()> {
3200        let mut storage = HashMapStorageProvider::new(1);
3201        let owner = Address::random();
3202        let validator = Address::random();
3203        let fee_recipient = Address::random();
3204        StorageCtx::enter(&mut storage, || {
3205            let mut vc = ValidatorConfigV2::new();
3206            vc.initialize(owner)?;
3207
3208            // Sign with ROTATE namespace, but try to ADD
3209            let (pubkey, sig) = make_test_keypair_and_signature(
3210                validator,
3211                "192.168.1.1:8000",
3212                "192.168.1.1",
3213                SignatureKind::Rotate,
3214            );
3215
3216            vc.storage.set_block_number(200);
3217            let result = vc.add_validator(
3218                owner,
3219                make_add_call(
3220                    validator,
3221                    pubkey,
3222                    "192.168.1.1:8000",
3223                    "192.168.1.1",
3224                    fee_recipient,
3225                    sig,
3226                ),
3227            );
3228            assert_eq!(
3229                result,
3230                Err(ValidatorConfigV2Error::invalid_signature().into())
3231            );
3232
3233            Ok(())
3234        })
3235    }
3236
3237    #[test]
3238    fn test_rotate_validator_rejects_wrong_key_signature() -> eyre::Result<()> {
3239        let mut storage = HashMapStorageProvider::new(1);
3240        let owner = Address::random();
3241        let validator = Address::random();
3242        let fee_recipient = Address::random();
3243        StorageCtx::enter(&mut storage, || {
3244            let mut vc = ValidatorConfigV2::new();
3245            vc.initialize(owner)?;
3246
3247            // Add a valid validator first
3248            vc.storage.set_block_number(200);
3249            vc.add_validator(
3250                owner,
3251                make_valid_add_call(validator, "192.168.1.1:8000", "192.168.1.1", fee_recipient),
3252            )?;
3253
3254            // Generate a new pubkey for rotation
3255            let (new_pubkey, _) = make_test_keypair_and_signature(
3256                validator,
3257                "10.0.0.1:8000",
3258                "10.0.0.1",
3259                SignatureKind::Rotate,
3260            );
3261
3262            // Sign with a different key
3263            let (_, wrong_sig) = make_test_keypair_and_signature(
3264                validator,
3265                "10.0.0.1:8000",
3266                "10.0.0.1",
3267                SignatureKind::Rotate,
3268            );
3269
3270            vc.storage.set_block_number(300);
3271            let result = vc.rotate_validator(
3272                owner,
3273                IValidatorConfigV2::rotateValidatorCall {
3274                    idx: 0,
3275                    publicKey: new_pubkey,
3276                    ingress: "10.0.0.1:8000".to_string(),
3277                    egress: "10.0.0.1".to_string(),
3278                    signature: wrong_sig.into(),
3279                },
3280            );
3281            assert_eq!(
3282                result,
3283                Err(ValidatorConfigV2Error::invalid_signature().into())
3284            );
3285
3286            Ok(())
3287        })
3288    }
3289
3290    #[test]
3291    fn test_add_validator_rejects_malformed_signature() -> eyre::Result<()> {
3292        let mut storage = HashMapStorageProvider::new(1);
3293        let owner = Address::random();
3294        let validator = Address::random();
3295        let fee_recipient = Address::random();
3296        StorageCtx::enter(&mut storage, || {
3297            let mut vc = ValidatorConfigV2::new();
3298            vc.initialize(owner)?;
3299
3300            let (pubkey, _) = make_test_keypair_and_signature(
3301                validator,
3302                "192.168.1.1:8000",
3303                "192.168.1.1",
3304                SignatureKind::Add { fee_recipient },
3305            );
3306
3307            vc.storage.set_block_number(200);
3308            let result = vc.add_validator(
3309                owner,
3310                make_add_call(
3311                    validator,
3312                    pubkey,
3313                    "192.168.1.1:8000",
3314                    "192.168.1.1",
3315                    fee_recipient,
3316                    vec![0xde, 0xad],
3317                ),
3318            );
3319            assert_eq!(
3320                result,
3321                Err(ValidatorConfigV2Error::invalid_signature_format().into())
3322            );
3323
3324            Ok(())
3325        })
3326    }
3327
3328    #[test]
3329    fn test_ipv4_ipv6_different_ips() -> eyre::Result<()> {
3330        let mut storage = HashMapStorageProvider::new(1);
3331        let owner = Address::random();
3332        StorageCtx::enter(&mut storage, || {
3333            let mut vc = ValidatorConfigV2::new();
3334            vc.initialize(owner)?;
3335
3336            // Add IPv4 validator
3337            vc.storage.set_block_number(200);
3338            vc.add_validator(
3339                owner,
3340                make_valid_add_call(
3341                    Address::random(),
3342                    "192.168.1.1:8000",
3343                    "192.168.1.1",
3344                    Address::random(),
3345                ),
3346            )?;
3347
3348            // Add IPv6 validator - should succeed (different IP)
3349            vc.storage.set_block_number(201);
3350            vc.add_validator(
3351                owner,
3352                make_valid_add_call(
3353                    Address::random(),
3354                    "[2001:db8::1]:8000",
3355                    "2001:db8::1",
3356                    Address::random(),
3357                ),
3358            )?;
3359
3360            assert_eq!(vc.validator_count()?, 2);
3361            Ok(())
3362        })
3363    }
3364
3365    #[test]
3366    fn test_event_emission_owner_and_validator_actions() -> eyre::Result<()> {
3367        let mut storage = HashMapStorageProvider::new(1);
3368        let owner = Address::random();
3369        let validator = Address::random();
3370        let new_validator_address = Address::random();
3371
3372        StorageCtx::enter(&mut storage, || {
3373            let mut vc = ValidatorConfigV2::new();
3374            vc.initialize(owner)?;
3375
3376            let (pubkey, signature) = make_test_keypair_and_signature(
3377                validator,
3378                "192.168.1.1:8000",
3379                "192.168.1.1",
3380                SignatureKind::Add {
3381                    fee_recipient: validator,
3382                },
3383            );
3384
3385            vc.storage.set_block_number(100);
3386            vc.add_validator(
3387                owner,
3388                make_add_call(
3389                    validator,
3390                    pubkey,
3391                    "192.168.1.1:8000",
3392                    "192.168.1.1",
3393                    validator,
3394                    signature,
3395                ),
3396            )?;
3397            vc.assert_emitted_events(vec![ValidatorConfigV2Event::validator_added(
3398                0,
3399                validator,
3400                pubkey,
3401                "192.168.1.1:8000".to_string(),
3402                "192.168.1.1".to_string(),
3403                validator,
3404            )]);
3405
3406            vc.clear_emitted_events();
3407            vc.set_ip_addresses(
3408                validator,
3409                IValidatorConfigV2::setIpAddressesCall {
3410                    idx: 0,
3411                    ingress: "10.0.0.1:8000".to_string(),
3412                    egress: "10.0.0.1".to_string(),
3413                },
3414            )?;
3415            vc.assert_emitted_events(vec![ValidatorConfigV2Event::ip_addresses_updated(
3416                0,
3417                "10.0.0.1:8000".to_string(),
3418                "10.0.0.1".to_string(),
3419                validator,
3420            )]);
3421
3422            vc.clear_emitted_events();
3423            vc.transfer_validator_ownership(
3424                owner,
3425                IValidatorConfigV2::transferValidatorOwnershipCall {
3426                    idx: 0,
3427                    newAddress: new_validator_address,
3428                },
3429            )?;
3430            vc.assert_emitted_events(vec![
3431                ValidatorConfigV2Event::validator_ownership_transferred(
3432                    0,
3433                    validator,
3434                    new_validator_address,
3435                    owner,
3436                ),
3437            ]);
3438
3439            vc.clear_emitted_events();
3440            vc.deactivate_validator(
3441                new_validator_address,
3442                IValidatorConfigV2::deactivateValidatorCall { idx: 0 },
3443            )?;
3444            vc.assert_emitted_events(vec![ValidatorConfigV2Event::validator_deactivated(
3445                0,
3446                new_validator_address,
3447            )]);
3448
3449            vc.clear_emitted_events();
3450            let new_owner = Address::random();
3451            vc.transfer_ownership(
3452                owner,
3453                IValidatorConfigV2::transferOwnershipCall {
3454                    newOwner: new_owner,
3455                },
3456            )?;
3457            vc.assert_emitted_events(vec![ValidatorConfigV2Event::ownership_transferred(
3458                owner, new_owner,
3459            )]);
3460
3461            Ok(())
3462        })
3463    }
3464
3465    #[test]
3466    fn test_event_emission_rotate_and_next_dkg() -> eyre::Result<()> {
3467        let mut storage = HashMapStorageProvider::new(1);
3468        let owner = Address::random();
3469        let validator = Address::random();
3470
3471        StorageCtx::enter(&mut storage, || {
3472            let mut vc = ValidatorConfigV2::new();
3473            vc.initialize(owner)?;
3474
3475            let (old_pubkey, old_sig) = make_test_keypair_and_signature(
3476                validator,
3477                "192.168.1.1:8000",
3478                "192.168.1.1",
3479                SignatureKind::Add {
3480                    fee_recipient: validator,
3481                },
3482            );
3483
3484            vc.storage.set_block_number(200);
3485            vc.add_validator(
3486                owner,
3487                make_add_call(
3488                    validator,
3489                    old_pubkey,
3490                    "192.168.1.1:8000",
3491                    "192.168.1.1",
3492                    validator,
3493                    old_sig,
3494                ),
3495            )?;
3496
3497            vc.clear_emitted_events();
3498            let (new_pubkey, new_sig) = make_test_keypair_and_signature(
3499                validator,
3500                "10.0.0.2:8000",
3501                "10.0.0.2",
3502                SignatureKind::Rotate,
3503            );
3504            vc.storage.set_block_number(300);
3505            vc.rotate_validator(
3506                owner,
3507                IValidatorConfigV2::rotateValidatorCall {
3508                    idx: 0,
3509                    publicKey: new_pubkey,
3510                    ingress: "10.0.0.2:8000".to_string(),
3511                    egress: "10.0.0.2".to_string(),
3512                    signature: new_sig.into(),
3513                },
3514            )?;
3515            vc.assert_emitted_events(vec![ValidatorConfigV2Event::validator_rotated(
3516                0,
3517                1,
3518                validator,
3519                old_pubkey,
3520                new_pubkey,
3521                "10.0.0.2:8000".to_string(),
3522                "10.0.0.2".to_string(),
3523                owner,
3524            )]);
3525
3526            vc.clear_emitted_events();
3527            vc.set_network_identity_rotation_epoch(
3528                owner,
3529                IValidatorConfigV2::setNetworkIdentityRotationEpochCall { epoch: 42 },
3530            )?;
3531            vc.assert_emitted_events(vec![
3532                ValidatorConfigV2Event::network_identity_rotation_epoch_set(0, 42),
3533            ]);
3534
3535            Ok(())
3536        })
3537    }
3538
3539    #[test]
3540    fn test_event_emission_migration_and_initialize() -> eyre::Result<()> {
3541        let mut storage = HashMapStorageProvider::new(1);
3542        let owner = Address::random();
3543        let v1_addr = Address::random();
3544        let v1_pk = FixedBytes::<32>::from([0x11; 32]);
3545
3546        StorageCtx::enter(&mut storage, || {
3547            let mut v1 = v1();
3548            v1.initialize(owner)?;
3549            v1.add_validator(
3550                owner,
3551                tempo_contracts::precompiles::IValidatorConfig::addValidatorCall {
3552                    newValidatorAddress: v1_addr,
3553                    publicKey: v1_pk,
3554                    active: true,
3555                    inboundAddress: "192.168.1.1:8000".to_string(),
3556                    outboundAddress: "192.168.1.1:9000".to_string(),
3557                },
3558            )?;
3559
3560            let mut v2 = ValidatorConfigV2::new();
3561            v2.storage.set_block_number(500);
3562            v2.migrate_validator(owner, IValidatorConfigV2::migrateValidatorCall { idx: 0 })?;
3563            v2.assert_emitted_events(vec![ValidatorConfigV2Event::validator_migrated(
3564                0, v1_addr, v1_pk,
3565            )]);
3566
3567            v2.clear_emitted_events();
3568            v2.storage.set_block_number(700);
3569            v2.initialize_if_migrated(owner)?;
3570            v2.assert_emitted_events(vec![ValidatorConfigV2Event::initialized(700)]);
3571
3572            Ok(())
3573        })
3574    }
3575}