Skip to main content

tempo_precompiles/validator_config_v2/
dispatch.rs

1//! ABI dispatch for the [`ValidatorConfigV2`] precompile (T2+).
2
3use super::*;
4use crate::{Precompile, charge_input_cost, dispatch_call, mutate, mutate_void, view};
5use alloy::{primitives::Address, sol_types::SolInterface};
6use revm::precompile::PrecompileResult;
7use tempo_contracts::precompiles::IValidatorConfigV2::IValidatorConfigV2Calls;
8
9impl Precompile for ValidatorConfigV2 {
10    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
11        if let Some(err) = charge_input_cost(&mut self.storage, calldata) {
12            return err;
13        }
14
15        // Pre-T2: behave like an empty contract (call succeeds, no execution)
16        if !self.storage.spec().is_t2() {
17            return Ok(self.storage.success_output(Default::default()));
18        }
19
20        dispatch_call(
21            calldata,
22            &[],
23            IValidatorConfigV2Calls::abi_decode,
24            |call| match call {
25                IValidatorConfigV2Calls::owner(call) => view(call, |_| self.owner()),
26                IValidatorConfigV2Calls::getActiveValidators(call) => {
27                    view(call, |_| self.get_active_validators())
28                }
29                IValidatorConfigV2Calls::getInitializedAtHeight(call) => {
30                    view(call, |_| self.get_initialized_at_height())
31                }
32                IValidatorConfigV2Calls::validatorCount(call) => {
33                    view(call, |_| self.validator_count())
34                }
35                IValidatorConfigV2Calls::validatorByIndex(call) => {
36                    view(call, |c| self.validator_by_index(c.index))
37                }
38                IValidatorConfigV2Calls::validatorByAddress(call) => {
39                    view(call, |c| self.validator_by_address(c.validatorAddress))
40                }
41                IValidatorConfigV2Calls::validatorByPublicKey(call) => {
42                    view(call, |c| self.validator_by_public_key(c.publicKey))
43                }
44                IValidatorConfigV2Calls::getNextNetworkIdentityRotationEpoch(call) => {
45                    view(call, |_| self.get_next_network_identity_rotation_epoch())
46                }
47                IValidatorConfigV2Calls::isInitialized(call) => {
48                    view(call, |_| self.is_initialized())
49                }
50
51                IValidatorConfigV2Calls::addValidator(call) => {
52                    mutate(call, msg_sender, |s, c| self.add_validator(s, c))
53                }
54                IValidatorConfigV2Calls::deactivateValidator(call) => {
55                    mutate_void(call, msg_sender, |s, c| self.deactivate_validator(s, c))
56                }
57                IValidatorConfigV2Calls::rotateValidator(call) => {
58                    mutate_void(call, msg_sender, |s, c| self.rotate_validator(s, c))
59                }
60                IValidatorConfigV2Calls::setFeeRecipient(call) => {
61                    mutate_void(call, msg_sender, |s, c| self.set_fee_recipient(s, c))
62                }
63                IValidatorConfigV2Calls::setIpAddresses(call) => {
64                    mutate_void(call, msg_sender, |s, c| self.set_ip_addresses(s, c))
65                }
66                IValidatorConfigV2Calls::transferValidatorOwnership(call) => {
67                    mutate_void(call, msg_sender, |s, c| {
68                        self.transfer_validator_ownership(s, c)
69                    })
70                }
71                IValidatorConfigV2Calls::transferOwnership(call) => {
72                    mutate_void(call, msg_sender, |s, c| self.transfer_ownership(s, c))
73                }
74                IValidatorConfigV2Calls::setNetworkIdentityRotationEpoch(call) => {
75                    mutate_void(call, msg_sender, |s, c| {
76                        self.set_network_identity_rotation_epoch(s, c)
77                    })
78                }
79                IValidatorConfigV2Calls::migrateValidator(call) => {
80                    mutate_void(call, msg_sender, |s, c| self.migrate_validator(s, c))
81                }
82                IValidatorConfigV2Calls::initializeIfMigrated(call) => {
83                    mutate_void(call, msg_sender, |s, _| self.initialize_if_migrated(s))
84                }
85            },
86        )
87    }
88}
89
90#[cfg(test)]
91mod tests {
92    use super::*;
93    use crate::{
94        expect_precompile_revert,
95        storage::{StorageCtx, hashmap::HashMapStorageProvider},
96        test_util::{assert_full_coverage, check_selector_coverage},
97    };
98    use alloy::{
99        primitives::{Address, FixedBytes},
100        sol_types::{SolCall, SolValue},
101    };
102    use tempo_chainspec::hardfork::TempoHardfork;
103    use tempo_contracts::precompiles::{
104        IValidatorConfigV2, IValidatorConfigV2::IValidatorConfigV2Calls, ValidatorConfigV2Error,
105    };
106
107    #[test]
108    fn test_pre_t2_returns_empty_success() -> eyre::Result<()> {
109        let owner = Address::random();
110
111        // Pre-T2 (T1): calling the precompile should succeed with empty output
112        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
113        StorageCtx::enter(&mut storage, || -> eyre::Result<()> {
114            let mut vc = ValidatorConfigV2::new();
115            vc.initialize(owner)?;
116
117            // Any call should succeed with empty bytes
118            let owner_call = IValidatorConfigV2::ownerCall {};
119            let calldata = owner_call.abi_encode();
120            let result = vc.call(&calldata, owner)?;
121
122            assert!(!result.is_revert(), "Pre-T2 call should not revert");
123            assert!(
124                result.bytes.is_empty(),
125                "Pre-T2 call should return empty bytes"
126            );
127
128            Ok(())
129        })?;
130
131        // Pre-T2 (T0): same behavior
132        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
133        StorageCtx::enter(&mut storage, || -> eyre::Result<()> {
134            let mut vc = ValidatorConfigV2::new();
135            vc.initialize(owner)?;
136
137            let calldata = IValidatorConfigV2::ownerCall {}.abi_encode();
138            let result = vc.call(&calldata, owner)?;
139
140            assert!(!result.is_revert());
141            assert!(result.bytes.is_empty());
142
143            // Even empty calldata should succeed
144            let result = vc.call(&[], owner)?;
145            assert!(!result.is_revert());
146            assert!(result.bytes.is_empty());
147
148            Ok(())
149        })?;
150
151        Ok(())
152    }
153
154    #[test]
155    fn test_t2_dispatch_works() -> eyre::Result<()> {
156        let owner = Address::random();
157
158        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
159        StorageCtx::enter(&mut storage, || -> eyre::Result<()> {
160            let mut vc = ValidatorConfigV2::new();
161            vc.initialize(owner)?;
162
163            // owner() should work in T2
164            let calldata = IValidatorConfigV2::ownerCall {}.abi_encode();
165            let result = vc.call(&calldata, owner)?;
166
167            assert!(!result.is_revert());
168            let decoded = Address::abi_decode(&result.bytes)?;
169            assert_eq!(decoded, owner);
170
171            Ok(())
172        })
173    }
174
175    #[test]
176    fn test_add_validator_dispatch() -> eyre::Result<()> {
177        use commonware_codec::Encode;
178        use commonware_cryptography::{Signer, ed25519::PrivateKey};
179
180        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
181        let owner = Address::random();
182        let validator_addr = Address::random();
183        StorageCtx::enter(&mut storage, || {
184            let mut vc = ValidatorConfigV2::new();
185            vc.initialize(owner)?;
186
187            // Generate real Ed25519 key pair
188            let seed = rand_08::random::<u64>();
189            let private_key = PrivateKey::from_seed(seed);
190            let public_key_obj = private_key.public_key();
191
192            // Build message (remove lines with "TEMPO" prefix)
193            let mut msg_data = Vec::new();
194            msg_data.extend_from_slice(&1u64.to_be_bytes());
195            msg_data.extend_from_slice(
196                tempo_contracts::precompiles::VALIDATOR_CONFIG_V2_ADDRESS.as_slice(),
197            );
198            msg_data.extend_from_slice(validator_addr.as_slice());
199            msg_data.push(u8::try_from("192.168.1.1:8000".len()).unwrap());
200            msg_data.extend_from_slice(b"192.168.1.1:8000");
201            msg_data.push(u8::try_from("192.168.1.1".len()).unwrap());
202            msg_data.extend_from_slice(b"192.168.1.1");
203            msg_data.extend_from_slice(validator_addr.as_slice());
204            let message = alloy::primitives::keccak256(&msg_data);
205
206            // Sign with namespace
207            let signature = private_key.sign(VALIDATOR_NS_ADD, message.as_slice());
208
209            // Encode public key
210            let pubkey_bytes = public_key_obj.encode();
211            let mut pubkey_array = [0u8; 32];
212            pubkey_array.copy_from_slice(&pubkey_bytes);
213            let public_key = FixedBytes::<32>::from(pubkey_array);
214
215            let add_call = IValidatorConfigV2::addValidatorCall {
216                validatorAddress: validator_addr,
217                publicKey: public_key,
218                ingress: "192.168.1.1:8000".to_string(),
219                egress: "192.168.1.1".to_string(),
220                feeRecipient: validator_addr,
221                signature: signature.encode().to_vec().into(),
222            };
223            let calldata = add_call.abi_encode();
224
225            let result = vc.call(&calldata, owner)?;
226            assert!(!result.is_revert());
227
228            assert_eq!(vc.validator_count()?, 1);
229            let v = vc.validator_by_index(0)?;
230            assert_eq!(v.validatorAddress, validator_addr);
231            assert_eq!(v.publicKey, public_key);
232
233            Ok(())
234        })
235    }
236
237    #[test]
238    fn test_unauthorized_add_validator_dispatch() -> eyre::Result<()> {
239        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
240        let owner = Address::random();
241        let non_owner = Address::random();
242        let validator_addr = Address::random();
243        StorageCtx::enter(&mut storage, || {
244            let mut vc = ValidatorConfigV2::new();
245            vc.initialize(owner)?;
246
247            let add_call = IValidatorConfigV2::addValidatorCall {
248                validatorAddress: validator_addr,
249                publicKey: FixedBytes::<32>::from([0x42; 32]),
250                ingress: "192.168.1.1:8000".to_string(),
251                egress: "192.168.1.1".to_string(),
252                feeRecipient: validator_addr,
253                signature: vec![0u8; 64].into(),
254            };
255            let calldata = add_call.abi_encode();
256
257            let result = vc.call(&calldata, non_owner);
258            expect_precompile_revert(&result, ValidatorConfigV2Error::unauthorized());
259
260            Ok(())
261        })
262    }
263
264    #[test]
265    fn test_function_selector_dispatch() -> eyre::Result<()> {
266        let sender = Address::random();
267        let owner = Address::random();
268
269        // T2: invalid selector returns reverted output
270        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
271        StorageCtx::enter(&mut storage, || -> eyre::Result<()> {
272            let mut vc = ValidatorConfigV2::new();
273            vc.initialize(owner)?;
274
275            let result = vc.call(&[0x12, 0x34, 0x56, 0x78], sender)?;
276            assert!(result.is_revert());
277
278            // Insufficient calldata also returns reverted output
279            let result = vc.call(&[0x12, 0x34], sender)?;
280            assert!(result.is_revert());
281
282            Ok(())
283        })
284    }
285
286    #[test]
287    fn test_selector_coverage() -> eyre::Result<()> {
288        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T2);
289        StorageCtx::enter(&mut storage, || {
290            let mut vc = ValidatorConfigV2::new();
291
292            let unsupported = check_selector_coverage(
293                &mut vc,
294                IValidatorConfigV2Calls::SELECTORS,
295                "IValidatorConfigV2",
296                IValidatorConfigV2Calls::name_by_selector,
297            );
298
299            assert_full_coverage([unsupported]);
300
301            Ok(())
302        })
303    }
304}