Skip to main content

tempo_precompiles/signature_verifier/
dispatch.rs

1use super::SignatureVerifier;
2use crate::{Precompile, charge_input_cost, dispatch_call, view};
3use alloy::{primitives::Address, sol_types::SolInterface};
4use revm::precompile::PrecompileResult;
5use tempo_contracts::precompiles::{
6    ISignatureVerifier::ISignatureVerifierCalls as ISVCalls, SignatureVerifierError,
7};
8use tempo_primitives::MAX_WEBAUTHN_SIGNATURE_LENGTH;
9
10/// Maximum valid calldata size: `verify(address,bytes32,bytes)` with a WebAuthn signature is the
11/// worst case. ABI encoding pads the dynamic `bytes` field independently, so only round the
12/// dynamic portion: selector(4) + args(4×32) + padded_sig_bytes.
13const MAX_CALLDATA_LEN: usize =
14    4 + 32 * 4 + (MAX_WEBAUTHN_SIGNATURE_LENGTH + 1).next_multiple_of(32);
15
16impl Precompile for SignatureVerifier {
17    fn call(&mut self, calldata: &[u8], _msg_sender: Address) -> PrecompileResult {
18        if let Some(err) = charge_input_cost(&mut self.storage, calldata) {
19            return err;
20        }
21
22        if calldata.len() > MAX_CALLDATA_LEN {
23            return Ok(self
24                .storage
25                .abi_revert(SignatureVerifierError::invalid_format()));
26        }
27
28        dispatch_call(calldata, &[], ISVCalls::abi_decode, |call| match call {
29            ISVCalls::recover(call) => view(call, |c| self.recover(c.hash, c.signature)),
30            ISVCalls::verify(call) => view(call, |c| {
31                self.recover(c.hash, c.signature).map(|sig| sig == c.signer)
32            }),
33        })
34    }
35}
36
37#[cfg(test)]
38mod tests {
39    use super::*;
40    use crate::{
41        Precompile, expect_precompile_revert,
42        storage::{StorageCtx, hashmap::HashMapStorageProvider},
43        test_util::{assert_full_coverage, check_selector_coverage},
44    };
45    use alloy::{primitives::B256, sol_types::SolCall};
46    use alloy_signer::SignerSync;
47    use alloy_signer_local::PrivateKeySigner;
48    use tempo_chainspec::hardfork::TempoHardfork;
49    use tempo_contracts::precompiles::ISignatureVerifier;
50
51    #[test]
52    fn test_signature_verifier_selector_coverage() -> eyre::Result<()> {
53        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
54        StorageCtx::enter(&mut storage, || {
55            let mut verifier = SignatureVerifier::new();
56
57            let unsupported = check_selector_coverage(
58                &mut verifier,
59                ISVCalls::SELECTORS,
60                "ISignatureVerifier",
61                ISVCalls::name_by_selector,
62            );
63
64            assert_full_coverage([unsupported]);
65            Ok(())
66        })
67    }
68
69    #[test]
70    fn test_verify_returns_true_for_correct_signer() -> eyre::Result<()> {
71        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
72        StorageCtx::enter(&mut storage, || {
73            let signer = PrivateKeySigner::random();
74            let hash = B256::from([0xAA; 32]);
75            let sig = signer.sign_hash_sync(&hash)?;
76
77            let calldata = ISignatureVerifier::verifyCall {
78                signer: signer.address(),
79                hash,
80                signature: sig.as_bytes().to_vec().into(),
81            }
82            .abi_encode();
83
84            let output = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
85            let ret = ISignatureVerifier::verifyCall::abi_decode_returns(&output.bytes)?;
86            assert!(ret, "verify should return true for the correct signer");
87            Ok(())
88        })
89    }
90
91    #[test]
92    fn test_verify_returns_false_for_wrong_signer() -> eyre::Result<()> {
93        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
94        StorageCtx::enter(&mut storage, || {
95            let signer = PrivateKeySigner::random();
96            let hash = B256::from([0xBB; 32]);
97            let sig = signer.sign_hash_sync(&hash)?;
98
99            let calldata = ISignatureVerifier::verifyCall {
100                signer: Address::random(),
101                hash,
102                signature: sig.as_bytes().to_vec().into(),
103            }
104            .abi_encode();
105
106            let output = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
107            let ret = ISignatureVerifier::verifyCall::abi_decode_returns(&output.bytes)?;
108            assert!(!ret, "verify should return false for a wrong signer");
109            Ok(())
110        })
111    }
112
113    #[test]
114    fn test_oversized_calldata_reverts_with_invalid_format() -> eyre::Result<()> {
115        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
116        StorageCtx::enter(&mut storage, || {
117            let calldata = vec![0u8; MAX_CALLDATA_LEN + 1];
118            let result = SignatureVerifier::new().call(&calldata, Address::ZERO);
119
120            expect_precompile_revert(&result, SignatureVerifierError::invalid_format());
121            Ok(())
122        })
123    }
124
125    #[test]
126    fn test_max_webauthn_verify_passes_size_guard() -> eyre::Result<()> {
127        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
128        StorageCtx::enter(&mut storage, || {
129            let mut sig = vec![0x02u8];
130            sig.extend_from_slice(&[0u8; MAX_WEBAUTHN_SIGNATURE_LENGTH]);
131
132            let calldata = ISignatureVerifier::verifyCall {
133                signer: Address::ZERO,
134                hash: B256::ZERO,
135                signature: sig.into(),
136            }
137            .abi_encode();
138
139            let result = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
140            // Should NOT be rejected by the size guard, should fail later at signature validation
141            assert!(
142                SignatureVerifierError::abi_decode(&result.bytes)
143                    .map(|e| e != SignatureVerifierError::invalid_format())
144                    .unwrap_or(true),
145                "max-size WebAuthn calldata was wrongly rejected by size guard"
146            );
147            Ok(())
148        })
149    }
150
151    #[test]
152    fn test_max_calldata_is_not_rejected() -> eyre::Result<()> {
153        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
154        StorageCtx::enter(&mut storage, || {
155            // Exactly MAX_CALLDATA_LEN bytes should pass the size guard (and fail at ABI
156            // decode instead). A zeroed selector is unknown, so we expect an
157            // UnknownFunctionSelector revert — not InvalidFormat.
158            let calldata = vec![0u8; MAX_CALLDATA_LEN];
159            let result = SignatureVerifier::new().call(&calldata, Address::ZERO)?;
160
161            assert!(result.is_revert());
162            assert!(
163                SignatureVerifierError::abi_decode(&result.bytes).is_err(),
164                "should not be an InvalidFormat revert"
165            );
166            Ok(())
167        })
168    }
169}