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