Skip to main content

tempo_precompiles/validator_config/
dispatch.rs

1//! ABI dispatch for the [`ValidatorConfig`] (V1) precompile.
2
3use super::ValidatorConfig;
4use crate::{
5    Precompile, dispatch_call, error::TempoPrecompileError, input_cost, mutate_void,
6    unknown_selector, view,
7};
8use alloy::{
9    primitives::Address,
10    sol_types::{SolCall, SolInterface},
11};
12use revm::precompile::{PrecompileError, PrecompileResult};
13use tempo_contracts::precompiles::IValidatorConfig::{
14    IValidatorConfigCalls, changeValidatorStatusByIndexCall,
15};
16
17impl Precompile for ValidatorConfig {
18    fn call(&mut self, calldata: &[u8], msg_sender: Address) -> PrecompileResult {
19        self.storage
20            .deduct_gas(input_cost(calldata.len()))
21            .map_err(|_| PrecompileError::OutOfGas)?;
22
23        dispatch_call(
24            calldata,
25            IValidatorConfigCalls::abi_decode,
26            |call| match call {
27                // View functions
28                IValidatorConfigCalls::owner(call) => view(call, |_| self.owner()),
29                IValidatorConfigCalls::getValidators(call) => view(call, |_| self.get_validators()),
30                IValidatorConfigCalls::getNextFullDkgCeremony(call) => {
31                    view(call, |_| self.get_next_full_dkg_ceremony())
32                }
33                IValidatorConfigCalls::validatorsArray(call) => view(call, |c| {
34                    let index =
35                        u64::try_from(c.index).map_err(|_| TempoPrecompileError::array_oob())?;
36                    self.validators_array(index)
37                }),
38                IValidatorConfigCalls::validators(call) => {
39                    view(call, |c| self.validators(c.validator))
40                }
41                IValidatorConfigCalls::validatorCount(call) => {
42                    view(call, |_| self.validator_count())
43                }
44
45                // Mutate functions
46                IValidatorConfigCalls::addValidator(call) => {
47                    mutate_void(call, msg_sender, |s, c| self.add_validator(s, c))
48                }
49                IValidatorConfigCalls::updateValidator(call) => {
50                    mutate_void(call, msg_sender, |s, c| self.update_validator(s, c))
51                }
52                IValidatorConfigCalls::changeValidatorStatus(call) => {
53                    mutate_void(call, msg_sender, |s, c| self.change_validator_status(s, c))
54                }
55                IValidatorConfigCalls::changeValidatorStatusByIndex(call) => {
56                    // T1+: changeValidatorStatusByIndex is only available in T1+
57                    if !self.storage.spec().is_t1() {
58                        return unknown_selector(
59                            changeValidatorStatusByIndexCall::SELECTOR,
60                            self.storage.gas_used(),
61                        );
62                    }
63                    mutate_void(call, msg_sender, |s, c| {
64                        self.change_validator_status_by_index(s, c)
65                    })
66                }
67                IValidatorConfigCalls::changeOwner(call) => {
68                    mutate_void(call, msg_sender, |s, c| self.change_owner(s, c))
69                }
70                IValidatorConfigCalls::setNextFullDkgCeremony(call) => {
71                    mutate_void(call, msg_sender, |s, c| {
72                        self.set_next_full_dkg_ceremony(s, c)
73                    })
74                }
75            },
76        )
77    }
78}
79
80#[cfg(test)]
81mod tests {
82    use super::*;
83    use crate::{
84        expect_precompile_revert,
85        storage::{StorageCtx, hashmap::HashMapStorageProvider},
86        test_util::{assert_full_coverage, check_selector_coverage},
87    };
88    use alloy::{
89        primitives::{Address, FixedBytes},
90        sol_types::{SolCall, SolValue},
91    };
92    use tempo_chainspec::hardfork::TempoHardfork;
93    use tempo_contracts::precompiles::{
94        IValidatorConfig, IValidatorConfig::IValidatorConfigCalls, ValidatorConfigError,
95    };
96
97    #[test]
98    fn test_function_selector_dispatch() -> eyre::Result<()> {
99        let sender = Address::random();
100        let owner = Address::random();
101
102        // T1: invalid selector returns reverted output
103        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
104        StorageCtx::enter(&mut storage, || -> eyre::Result<()> {
105            let mut validator_config = ValidatorConfig::new();
106            validator_config.initialize(owner)?;
107
108            let result = validator_config.call(&[0x12, 0x34, 0x56, 0x78], sender)?;
109            assert!(result.reverted);
110
111            // T1: insufficient calldata also returns reverted output
112            let result = validator_config.call(&[0x12, 0x34], sender)?;
113            assert!(result.reverted);
114
115            Ok(())
116        })?;
117
118        // Pre-T1 (T0): insufficient calldata returns error
119        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
120        StorageCtx::enter(&mut storage, || {
121            let mut validator_config = ValidatorConfig::new();
122            validator_config.initialize(owner)?;
123
124            let result = validator_config.call(&[0x12, 0x34], sender);
125            assert!(matches!(result, Err(PrecompileError::Other(_))));
126
127            Ok(())
128        })
129    }
130
131    #[test]
132    fn test_owner_view_dispatch() -> eyre::Result<()> {
133        let mut storage = HashMapStorageProvider::new(1);
134        let sender = Address::random();
135        let owner = Address::random();
136        StorageCtx::enter(&mut storage, || {
137            let mut validator_config = ValidatorConfig::new();
138
139            // Initialize with owner
140            validator_config.initialize(owner)?;
141
142            // Call owner() via dispatch
143            let owner_call = IValidatorConfig::ownerCall {};
144            let calldata = owner_call.abi_encode();
145
146            let result = validator_config.call(&calldata, sender)?;
147            // HashMapStorageProvider does not do gas accounting, so we expect 0 here.
148            assert_eq!(result.gas_used, 0);
149
150            // Verify we get the correct owner
151            let decoded = Address::abi_decode(&result.bytes)?;
152            assert_eq!(decoded, owner);
153
154            Ok(())
155        })
156    }
157
158    #[test]
159    fn test_add_validator_dispatch() -> eyre::Result<()> {
160        let mut storage = HashMapStorageProvider::new(1);
161        let owner = Address::random();
162        let validator_addr = Address::random();
163        StorageCtx::enter(&mut storage, || {
164            let mut validator_config = ValidatorConfig::new();
165
166            // Initialize with owner
167            validator_config.initialize(owner)?;
168
169            // Add validator via dispatch
170            let public_key = FixedBytes::<32>::from([0x42; 32]);
171            let add_call = IValidatorConfig::addValidatorCall {
172                newValidatorAddress: validator_addr,
173                publicKey: public_key,
174                active: true,
175                inboundAddress: "192.168.1.1:8000".to_string(),
176                outboundAddress: "192.168.1.1:9000".to_string(),
177            };
178            let calldata = add_call.abi_encode();
179
180            let result = validator_config.call(&calldata, owner)?;
181
182            // HashMapStorageProvider does not have gas accounting, so we expect 0
183            assert_eq!(result.gas_used, 0);
184
185            // Verify validator was added by calling getValidators
186            let validators = validator_config.get_validators()?;
187            assert_eq!(validators.len(), 1);
188            assert_eq!(validators[0].validatorAddress, validator_addr);
189            assert_eq!(validators[0].publicKey, public_key);
190            assert_eq!(validators[0].inboundAddress, "192.168.1.1:8000");
191            assert_eq!(validators[0].outboundAddress, "192.168.1.1:9000");
192            assert!(validators[0].active);
193
194            Ok(())
195        })
196    }
197
198    #[test]
199    fn test_unauthorized_add_validator_dispatch() -> eyre::Result<()> {
200        let mut storage = HashMapStorageProvider::new(1);
201        let owner = Address::random();
202        let non_owner = Address::random();
203        let validator_addr = Address::random();
204        StorageCtx::enter(&mut storage, || {
205            let mut validator_config = ValidatorConfig::new();
206
207            // Initialize with owner
208            validator_config.initialize(owner)?;
209
210            // Try to add validator as non-owner
211            let public_key = FixedBytes::<32>::from([0x42; 32]);
212            let add_call = IValidatorConfig::addValidatorCall {
213                newValidatorAddress: validator_addr,
214                publicKey: public_key,
215                active: true,
216                inboundAddress: "192.168.1.1:8000".to_string(),
217                outboundAddress: "192.168.1.1:9000".to_string(),
218            };
219            let calldata = add_call.abi_encode();
220
221            let result = validator_config.call(&calldata, non_owner);
222            expect_precompile_revert(&result, ValidatorConfigError::unauthorized());
223
224            Ok(())
225        })
226    }
227
228    #[test]
229    fn test_selector_coverage() -> eyre::Result<()> {
230        let mut storage = HashMapStorageProvider::new(1);
231        StorageCtx::enter(&mut storage, || {
232            let mut validator_config = ValidatorConfig::new();
233
234            let unsupported = check_selector_coverage(
235                &mut validator_config,
236                IValidatorConfigCalls::SELECTORS,
237                "IValidatorConfig",
238                IValidatorConfigCalls::name_by_selector,
239            );
240
241            assert_full_coverage([unsupported]);
242
243            Ok(())
244        })
245    }
246
247    #[test]
248    fn test_change_validator_status_by_index_t1_gating() -> eyre::Result<()> {
249        use alloy::sol_types::SolError;
250        use tempo_contracts::precompiles::UnknownFunctionSelector;
251
252        let owner = Address::random();
253        let validator = Address::random();
254        let public_key = FixedBytes::<32>::from([0x42; 32]);
255
256        // T0: changeValidatorStatusByIndex returns UnknownFunctionSelector
257        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T0);
258        StorageCtx::enter(&mut storage, || -> eyre::Result<()> {
259            let mut validator_config = ValidatorConfig::new();
260            validator_config.initialize(owner)?;
261
262            // Add a validator first
263            validator_config.add_validator(
264                owner,
265                IValidatorConfig::addValidatorCall {
266                    newValidatorAddress: validator,
267                    publicKey: public_key,
268                    active: true,
269                    inboundAddress: "192.168.1.1:8000".to_string(),
270                    outboundAddress: "192.168.1.1:9000".to_string(),
271                },
272            )?;
273
274            // Try to call changeValidatorStatusByIndex in T0 - should return UnknownFunctionSelector
275            let call = IValidatorConfig::changeValidatorStatusByIndexCall {
276                index: 0,
277                active: false,
278            };
279            let calldata = call.abi_encode();
280            let result = validator_config.call(&calldata, owner)?;
281
282            assert!(result.reverted);
283            let decoded = UnknownFunctionSelector::abi_decode(&result.bytes)?;
284            assert_eq!(
285                decoded.selector.0,
286                IValidatorConfig::changeValidatorStatusByIndexCall::SELECTOR
287            );
288
289            Ok(())
290        })?;
291
292        // T1: changeValidatorStatusByIndex works
293        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T1);
294        StorageCtx::enter(&mut storage, || -> eyre::Result<()> {
295            let mut validator_config = ValidatorConfig::new();
296            validator_config.initialize(owner)?;
297
298            // Add a validator first
299            validator_config.add_validator(
300                owner,
301                IValidatorConfig::addValidatorCall {
302                    newValidatorAddress: validator,
303                    publicKey: public_key,
304                    active: true,
305                    inboundAddress: "192.168.1.1:8000".to_string(),
306                    outboundAddress: "192.168.1.1:9000".to_string(),
307                },
308            )?;
309
310            // changeValidatorStatusByIndex should work in T1
311            let call = IValidatorConfig::changeValidatorStatusByIndexCall {
312                index: 0,
313                active: false,
314            };
315            let calldata = call.abi_encode();
316            let result = validator_config.call(&calldata, owner)?;
317
318            assert!(
319                !result.reverted,
320                "changeValidatorStatusByIndex should succeed in T1"
321            );
322
323            // Verify the status was changed
324            let validators = validator_config.get_validators()?;
325            assert!(!validators[0].active, "Validator should be inactive");
326
327            Ok(())
328        })
329    }
330}