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