Skip to main content

tempo_precompiles/signature_verifier/
dispatch.rs

1use super::SignatureVerifier;
2use crate::{Precompile, SelectorSchedule, charge_input_cost, dispatch_call, view};
3use alloy::{
4    primitives::Address,
5    sol_types::{SolCall, SolInterface},
6};
7use revm::precompile::PrecompileResult;
8use tempo_chainspec::hardfork::TempoHardfork;
9use tempo_contracts::precompiles::{
10    ISignatureVerifier::{self, ISignatureVerifierCalls as ISVCalls},
11    SignatureVerifierError,
12};
13use tempo_primitives::MAX_WEBAUTHN_SIGNATURE_LENGTH;
14
15/// Maximum valid calldata size: `verify(address,bytes32,bytes)` with a WebAuthn signature is the
16/// worst case. ABI encoding pads the dynamic `bytes` field independently, so only round the
17/// dynamic portion: selector(4) + args(4×32) + padded_sig_bytes.
18const MAX_CALLDATA_LEN: usize =
19    4 + 32 * 4 + (MAX_WEBAUTHN_SIGNATURE_LENGTH + 1).next_multiple_of(32);
20
21const T6_ADDED: &[[u8; 4]] = &[
22    ISignatureVerifier::verifyKeychainCall::SELECTOR,
23    ISignatureVerifier::verifyKeychainAdminCall::SELECTOR,
24];
25
26impl Precompile for SignatureVerifier {
27    fn call(&mut self, calldata: &[u8], _msg_sender: Address) -> PrecompileResult {
28        if let Some(err) = charge_input_cost(&mut self.storage, calldata) {
29            return err;
30        }
31
32        if calldata.len() > MAX_CALLDATA_LEN {
33            return Ok(self
34                .storage
35                .abi_revert(SignatureVerifierError::invalid_format()));
36        }
37
38        dispatch_call(
39            calldata,
40            &[SelectorSchedule::new(TempoHardfork::T6).with_added(T6_ADDED)],
41            ISVCalls::abi_decode,
42            |call| match call {
43                ISVCalls::recover(call) => view(call, |c| self.recover(c.hash, c.signature)),
44                ISVCalls::verify(call) => view(call, |c| {
45                    self.recover(c.hash, c.signature).map(|sig| sig == c.signer)
46                }),
47                ISVCalls::verifyKeychain(call) => view(call, |c| {
48                    self.verify_keychain(c.account, c.hash, c.signature)
49                }),
50                ISVCalls::verifyKeychainAdmin(call) => view(call, |c| {
51                    self.verify_keychain_admin(c.account, c.hash, c.signature)
52                }),
53            },
54        )
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61    use crate::{
62        Precompile,
63        account_keychain::{AccountKeychain, KeyRestrictions, SignatureType},
64        expect_precompile_revert,
65        storage::{StorageCtx, hashmap::HashMapStorageProvider},
66        test_util::{assert_full_coverage, check_selector_coverage},
67    };
68    use alloy::{
69        primitives::B256,
70        sol_types::{SolCall, SolError},
71    };
72    use alloy_signer::SignerSync;
73    use alloy_signer_local::PrivateKeySigner;
74    use tempo_chainspec::hardfork::TempoHardfork;
75    use tempo_contracts::precompiles::{ISignatureVerifier, UnknownFunctionSelector};
76    use tempo_primitives::transaction::tt_signature::{
77        KeychainSignature, PrimitiveSignature, TempoSignature,
78    };
79
80    fn call_verify_keychain(
81        account: Address,
82        hash: B256,
83        signature: Vec<u8>,
84    ) -> eyre::Result<bool> {
85        let calldata = ISignatureVerifier::verifyKeychainCall {
86            account,
87            hash,
88            signature: signature.into(),
89        }
90        .abi_encode();
91
92        let output = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
93        let ret = ISignatureVerifier::verifyKeychainCall::abi_decode_returns(&output.bytes)?;
94        Ok(ret)
95    }
96
97    fn call_verify_keychain_admin(
98        account: Address,
99        hash: B256,
100        signature: Vec<u8>,
101    ) -> eyre::Result<bool> {
102        let calldata = ISignatureVerifier::verifyKeychainAdminCall {
103            account,
104            hash,
105            signature: signature.into(),
106        }
107        .abi_encode();
108
109        let output = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
110        let ret = ISignatureVerifier::verifyKeychainAdminCall::abi_decode_returns(&output.bytes)?;
111        Ok(ret)
112    }
113
114    fn keychain_signature(
115        account: Address,
116        key: &PrivateKeySigner,
117        hash: B256,
118    ) -> eyre::Result<Vec<u8>> {
119        let signing_hash = KeychainSignature::signing_hash(hash, account);
120        let inner = key.sign_hash_sync(&signing_hash)?;
121        Ok(TempoSignature::Keychain(KeychainSignature::new(
122            account,
123            PrimitiveSignature::Secp256k1(inner),
124        ))
125        .to_bytes()
126        .to_vec())
127    }
128
129    #[test]
130    fn test_signature_verifier_selector_coverage() -> eyre::Result<()> {
131        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
132        StorageCtx::enter(&mut storage, || {
133            let mut verifier = SignatureVerifier::new();
134
135            let unsupported = check_selector_coverage(
136                &mut verifier,
137                ISVCalls::SELECTORS,
138                "ISignatureVerifier",
139                ISVCalls::name_by_selector,
140            );
141
142            assert_full_coverage([unsupported]);
143            Ok(())
144        })
145    }
146
147    #[test]
148    fn test_verify_keychain_selector_rejected_before_t6() -> eyre::Result<()> {
149        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
150        StorageCtx::enter(&mut storage, || {
151            let calldata = ISignatureVerifier::verifyKeychainCall {
152                account: Address::random(),
153                hash: B256::ZERO,
154                signature: vec![0u8; 65].into(),
155            }
156            .abi_encode();
157
158            let result = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
159            assert!(result.is_revert());
160            assert!(
161                UnknownFunctionSelector::abi_decode(&result.bytes).is_ok(),
162                "verifyKeychain should be selector-gated before T6"
163            );
164            Ok(())
165        })
166    }
167
168    #[test]
169    fn test_verify_keychain_admin_selector_rejected_before_t6() -> eyre::Result<()> {
170        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T5);
171        StorageCtx::enter(&mut storage, || {
172            let calldata = ISignatureVerifier::verifyKeychainAdminCall {
173                account: Address::random(),
174                hash: B256::ZERO,
175                signature: vec![0u8; 65].into(),
176            }
177            .abi_encode();
178
179            let result = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
180            assert!(result.is_revert());
181            assert!(
182                UnknownFunctionSelector::abi_decode(&result.bytes).is_ok(),
183                "verifyKeychainAdmin should be selector-gated before T6"
184            );
185            Ok(())
186        })
187    }
188
189    #[test]
190    fn test_verify_returns_true_for_correct_signer() -> eyre::Result<()> {
191        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
192        StorageCtx::enter(&mut storage, || {
193            let signer = PrivateKeySigner::random();
194            let hash = B256::from([0xAA; 32]);
195            let sig = signer.sign_hash_sync(&hash)?;
196
197            let calldata = ISignatureVerifier::verifyCall {
198                signer: signer.address(),
199                hash,
200                signature: sig.as_bytes().to_vec().into(),
201            }
202            .abi_encode();
203
204            let output = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
205            let ret = ISignatureVerifier::verifyCall::abi_decode_returns(&output.bytes)?;
206            assert!(ret, "verify should return true for the correct signer");
207            Ok(())
208        })
209    }
210
211    #[test]
212    fn test_verify_returns_false_for_wrong_signer() -> eyre::Result<()> {
213        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
214        StorageCtx::enter(&mut storage, || {
215            let signer = PrivateKeySigner::random();
216            let hash = B256::from([0xBB; 32]);
217            let sig = signer.sign_hash_sync(&hash)?;
218
219            let calldata = ISignatureVerifier::verifyCall {
220                signer: Address::random(),
221                hash,
222                signature: sig.as_bytes().to_vec().into(),
223            }
224            .abi_encode();
225
226            let output = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
227            let ret = ISignatureVerifier::verifyCall::abi_decode_returns(&output.bytes)?;
228            assert!(!ret, "verify should return false for a wrong signer");
229            Ok(())
230        })
231    }
232
233    #[test]
234    fn test_verify_keychain_returns_true_for_active_key() -> eyre::Result<()> {
235        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
236        StorageCtx::enter(&mut storage, || {
237            let account = Address::random();
238            let access_key = PrivateKeySigner::random();
239
240            let mut keychain = AccountKeychain::new();
241            keychain.initialize()?;
242            keychain.set_tx_origin(account)?;
243            keychain.authorize_key(
244                account,
245                access_key.address(),
246                SignatureType::Secp256k1,
247                KeyRestrictions {
248                    expiry: u64::MAX,
249                    enforceLimits: false,
250                    limits: vec![],
251                    allowAnyCalls: true,
252                    allowedCalls: vec![],
253                },
254                None,
255            )?;
256
257            let hash = B256::from([0x44; 32]);
258            let signature = keychain_signature(account, &access_key, hash)?;
259
260            let ret = call_verify_keychain(account, hash, signature)?;
261            assert!(ret, "active keychain key should verify");
262            Ok(())
263        })
264    }
265
266    #[test]
267    fn test_verify_keychain_returns_false_for_missing_key() -> eyre::Result<()> {
268        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
269        StorageCtx::enter(&mut storage, || {
270            let account = Address::random();
271            let access_key = PrivateKeySigner::random();
272            let hash = B256::from([0x55; 32]);
273            let signature = keychain_signature(account, &access_key, hash)?;
274
275            let ret = call_verify_keychain(account, hash, signature)?;
276            assert!(!ret, "unknown keychain key should not verify");
277            Ok(())
278        })
279    }
280
281    #[test]
282    fn test_verify_keychain_returns_false_for_root_key_without_access_key() -> eyre::Result<()> {
283        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
284        StorageCtx::enter(&mut storage, || {
285            let root = PrivateKeySigner::random();
286            let account = root.address();
287            let hash = B256::from([0x56; 32]);
288            let signature = keychain_signature(account, &root, hash)?;
289
290            let ret = call_verify_keychain(account, hash, signature)?;
291            assert!(
292                !ret,
293                "root key should not verify as an active access key unless explicitly stored"
294            );
295            Ok(())
296        })
297    }
298
299    #[test]
300    fn test_verify_keychain_returns_false_for_account_mismatch() -> eyre::Result<()> {
301        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
302        StorageCtx::enter(&mut storage, || {
303            let account = Address::random();
304            let access_key = PrivateKeySigner::random();
305
306            let mut keychain = AccountKeychain::new();
307            keychain.initialize()?;
308            keychain.set_tx_origin(account)?;
309            keychain.authorize_key(
310                account,
311                access_key.address(),
312                SignatureType::Secp256k1,
313                KeyRestrictions {
314                    expiry: u64::MAX,
315                    enforceLimits: false,
316                    limits: vec![],
317                    allowAnyCalls: true,
318                    allowedCalls: vec![],
319                },
320                None,
321            )?;
322
323            let hash = B256::from([0x57; 32]);
324            let signature = keychain_signature(account, &access_key, hash)?;
325
326            let ret = call_verify_keychain(Address::random(), hash, signature)?;
327            assert!(
328                !ret,
329                "keychain signature should not verify for a different account"
330            );
331            Ok(())
332        })
333    }
334
335    #[test]
336    fn test_verify_keychain_admin_returns_true_for_admin_key() -> eyre::Result<()> {
337        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
338        StorageCtx::enter(&mut storage, || {
339            let account = Address::random();
340            let admin = PrivateKeySigner::random();
341
342            let mut keychain = AccountKeychain::new();
343            keychain.initialize()?;
344            keychain.set_tx_origin(account)?;
345            keychain.authorize_admin_key(
346                account,
347                admin.address(),
348                SignatureType::Secp256k1,
349                None,
350            )?;
351
352            let hash = B256::from([0x66; 32]);
353            let signature = keychain_signature(account, &admin, hash)?;
354
355            let ret = call_verify_keychain_admin(account, hash, signature)?;
356            assert!(ret, "active admin keychain key should verify as admin");
357            Ok(())
358        })
359    }
360
361    #[test]
362    fn test_verify_keychain_admin_returns_true_for_root_key() -> eyre::Result<()> {
363        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
364        StorageCtx::enter(&mut storage, || {
365            let root = PrivateKeySigner::random();
366            let account = root.address();
367            let hash = B256::from([0x67; 32]);
368            let signature = keychain_signature(account, &root, hash)?;
369
370            let ret = call_verify_keychain_admin(account, hash, signature)?;
371            assert!(ret, "root keychain key should verify as admin");
372            Ok(())
373        })
374    }
375
376    #[test]
377    fn test_verify_keychain_admin_returns_false_for_account_mismatch() -> eyre::Result<()> {
378        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
379        StorageCtx::enter(&mut storage, || {
380            let account = Address::random();
381            let admin = PrivateKeySigner::random();
382
383            let mut keychain = AccountKeychain::new();
384            keychain.initialize()?;
385            keychain.set_tx_origin(account)?;
386            keychain.authorize_admin_key(
387                account,
388                admin.address(),
389                SignatureType::Secp256k1,
390                None,
391            )?;
392
393            let hash = B256::from([0x68; 32]);
394            let signature = keychain_signature(account, &admin, hash)?;
395
396            let ret = call_verify_keychain_admin(Address::random(), hash, signature)?;
397            assert!(
398                !ret,
399                "admin keychain signature should not verify for a different account"
400            );
401            Ok(())
402        })
403    }
404
405    #[test]
406    fn test_verify_keychain_admin_returns_false_for_non_admin_key() -> eyre::Result<()> {
407        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
408        StorageCtx::enter(&mut storage, || {
409            let account = Address::random();
410            let access_key = PrivateKeySigner::random();
411
412            let mut keychain = AccountKeychain::new();
413            keychain.initialize()?;
414            keychain.set_tx_origin(account)?;
415            keychain.authorize_key(
416                account,
417                access_key.address(),
418                SignatureType::Secp256k1,
419                KeyRestrictions {
420                    expiry: u64::MAX,
421                    enforceLimits: false,
422                    limits: vec![],
423                    allowAnyCalls: true,
424                    allowedCalls: vec![],
425                },
426                None,
427            )?;
428
429            let hash = B256::from([0x77; 32]);
430            let signature = keychain_signature(account, &access_key, hash)?;
431
432            let ret = call_verify_keychain_admin(account, hash, signature)?;
433            assert!(!ret, "non-admin keychain key should not verify as admin");
434            Ok(())
435        })
436    }
437
438    #[test]
439    fn test_verify_keychain_reverts_for_non_keychain_signature() -> eyre::Result<()> {
440        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T6);
441        StorageCtx::enter(&mut storage, || {
442            let signer = PrivateKeySigner::random();
443            let hash = B256::from([0x88; 32]);
444            let sig = signer.sign_hash_sync(&hash)?;
445
446            let calldata = ISignatureVerifier::verifyKeychainCall {
447                account: signer.address(),
448                hash,
449                signature: sig.as_bytes().to_vec().into(),
450            }
451            .abi_encode();
452
453            let result = SignatureVerifier::new().call(&calldata, Address::ZERO);
454            expect_precompile_revert(&result, SignatureVerifierError::invalid_format());
455            Ok(())
456        })
457    }
458
459    #[test]
460    fn test_oversized_calldata_reverts_with_invalid_format() -> eyre::Result<()> {
461        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
462        StorageCtx::enter(&mut storage, || {
463            let calldata = vec![0u8; MAX_CALLDATA_LEN + 1];
464            let result = SignatureVerifier::new().call(&calldata, Address::ZERO);
465
466            expect_precompile_revert(&result, SignatureVerifierError::invalid_format());
467            Ok(())
468        })
469    }
470
471    #[test]
472    fn test_max_webauthn_verify_passes_size_guard() -> eyre::Result<()> {
473        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
474        StorageCtx::enter(&mut storage, || {
475            let mut sig = vec![0x02u8];
476            sig.extend_from_slice(&[0u8; MAX_WEBAUTHN_SIGNATURE_LENGTH]);
477
478            let calldata = ISignatureVerifier::verifyCall {
479                signer: Address::ZERO,
480                hash: B256::ZERO,
481                signature: sig.into(),
482            }
483            .abi_encode();
484
485            let result = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
486            // Should NOT be rejected by the size guard, should fail later at signature validation
487            assert!(
488                SignatureVerifierError::abi_decode(&result.bytes)
489                    .map(|e| e != SignatureVerifierError::invalid_format())
490                    .unwrap_or(true),
491                "max-size WebAuthn calldata was wrongly rejected by size guard"
492            );
493            Ok(())
494        })
495    }
496
497    #[test]
498    fn test_max_calldata_is_not_rejected() -> eyre::Result<()> {
499        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
500        StorageCtx::enter(&mut storage, || {
501            // Exactly MAX_CALLDATA_LEN bytes should pass the size guard (and fail at ABI
502            // decode instead). A zeroed selector is unknown, so we expect an
503            // UnknownFunctionSelector revert — not InvalidFormat.
504            let calldata = vec![0u8; MAX_CALLDATA_LEN];
505            let result = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
506
507            assert!(result.is_revert());
508            assert!(
509                SignatureVerifierError::abi_decode(&result.bytes).is_err(),
510                "should not be an InvalidFormat revert"
511            );
512            Ok(())
513        })
514    }
515}