Skip to main content

tempo_precompiles/signature_verifier/
mod.rs

1pub mod dispatch;
2
3use crate::{SIGNATURE_VERIFIER_ADDRESS, error::Result};
4use alloy::primitives::{Address, B256, Bytes};
5use tempo_contracts::precompiles::SignatureVerifierError;
6use tempo_precompiles_macros::contract;
7use tempo_primitives::transaction::{SignatureType, tt_signature::PrimitiveSignature};
8
9/// Gas cost for secp256k1 signature verification.
10const SECP256K1_VERIFY_GAS: u64 = 3_000;
11
12/// Gas cost for P256 signature verification.
13const P256_VERIFY_GAS: u64 = 8_000;
14
15/// Gas cost for WebAuthn signature verification.
16const WEBAUTHN_VERIFY_GAS: u64 = 8_000;
17
18#[contract(addr = SIGNATURE_VERIFIER_ADDRESS)]
19pub struct SignatureVerifier {}
20
21impl SignatureVerifier {
22    pub fn initialize(&mut self) -> Result<()> {
23        self.__initialize()
24    }
25
26    pub fn recover(&mut self, hash: B256, signature: Bytes) -> Result<Address> {
27        // Parse and validate signature (handles size checks + type disambiguation).
28        let sig = PrimitiveSignature::from_bytes(&signature)
29            .map_err(|_| SignatureVerifierError::invalid_format())?;
30
31        // Charge verification gas before performing verification.
32        let verify_gas = match sig.signature_type() {
33            SignatureType::Secp256k1 => SECP256K1_VERIFY_GAS,
34            SignatureType::P256 => P256_VERIFY_GAS,
35            SignatureType::WebAuthn => WEBAUTHN_VERIFY_GAS,
36        };
37        self.storage.deduct_gas(verify_gas)?;
38
39        // Verify and recover signer.
40        sig.recover_signer(&hash)
41            .map_err(|_| SignatureVerifierError::invalid_signature().into())
42    }
43}
44
45#[cfg(test)]
46mod tests {
47    use super::*;
48    use crate::storage::{StorageCtx, hashmap::HashMapStorageProvider};
49    use alloy_signer::SignerSync;
50    use alloy_signer_local::PrivateKeySigner;
51    use tempo_chainspec::hardfork::TempoHardfork;
52    use tempo_primitives::transaction::tt_signature::{
53        SIGNATURE_TYPE_P256, SIGNATURE_TYPE_WEBAUTHN,
54    };
55
56    fn sign_recover(hash: B256, signature: Vec<u8>) -> Result<Address> {
57        SignatureVerifier::new().recover(hash, Bytes::from(signature))
58    }
59
60    #[test]
61    fn test_verify_secp256k1_valid() -> eyre::Result<()> {
62        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
63        StorageCtx::enter(&mut storage, || {
64            let signer = PrivateKeySigner::random();
65            let hash = B256::from([0xAA; 32]);
66            let sig = signer.sign_hash_sync(&hash)?;
67            let sig_bytes = sig.as_bytes().to_vec();
68            assert_eq!(sig_bytes.len(), 65);
69
70            let result = sign_recover(hash, sig_bytes)?;
71            assert_eq!(result, signer.address());
72            Ok(())
73        })
74    }
75
76    #[test]
77    fn test_verify_p256_valid() -> eyre::Result<()> {
78        use p256::{ecdsa::SigningKey, elliptic_curve::rand_core::OsRng};
79        use tempo_primitives::transaction::tt_signature::{derive_p256_address, normalize_p256_s};
80
81        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
82        StorageCtx::enter(&mut storage, || {
83            let signing_key = SigningKey::random(&mut OsRng);
84            let verifying_key = signing_key.verifying_key();
85            let encoded = verifying_key.to_encoded_point(false);
86            let pub_key_x =
87                B256::from_slice(encoded.x().ok_or_else(|| eyre::eyre!("missing x coord"))?);
88            let pub_key_y =
89                B256::from_slice(encoded.y().ok_or_else(|| eyre::eyre!("missing y coord"))?);
90            let expected_address = derive_p256_address(&pub_key_x, &pub_key_y);
91
92            let hash = B256::from([0xBB; 32]);
93            let (signature, _) = signing_key.sign_prehash_recoverable(hash.as_slice())?;
94            let r = B256::from_slice(&signature.r().to_bytes());
95            let s =
96                normalize_p256_s(&signature.s().to_bytes()).expect("p256 crate produces valid s");
97
98            // Build encoded P256 signature: 0x01 || r || s || x || y || prehash(0)
99            let mut sig_bytes = Vec::new();
100            sig_bytes.push(SIGNATURE_TYPE_P256);
101            sig_bytes.extend_from_slice(r.as_slice());
102            sig_bytes.extend_from_slice(s.as_slice());
103            sig_bytes.extend_from_slice(pub_key_x.as_slice());
104            sig_bytes.extend_from_slice(pub_key_y.as_slice());
105            sig_bytes.push(0); // pre_hash = false
106            assert_eq!(sig_bytes.len(), 130);
107
108            let result = sign_recover(hash, sig_bytes)?;
109            assert_eq!(result, expected_address);
110            Ok(())
111        })
112    }
113
114    #[test]
115    fn test_verify_empty_signature_reverts() -> eyre::Result<()> {
116        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
117        StorageCtx::enter(&mut storage, || {
118            let result = sign_recover(B256::ZERO, vec![]);
119            assert!(result.is_err());
120            Ok(())
121        })
122    }
123
124    #[test]
125    fn test_verify_secp256k1_wrong_length_reverts() -> eyre::Result<()> {
126        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
127        StorageCtx::enter(&mut storage, || {
128            // 64 bytes — not 65
129            let result = sign_recover(B256::ZERO, vec![0u8; 64]);
130            assert!(result.is_err());
131            // 66 bytes — not 65
132            let result = sign_recover(B256::ZERO, vec![0u8; 66]);
133            assert!(result.is_err());
134            Ok(())
135        })
136    }
137
138    #[test]
139    fn test_verify_p256_wrong_length_reverts() -> eyre::Result<()> {
140        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
141        StorageCtx::enter(&mut storage, || {
142            // 0x01 prefix + 128 bytes (should be 129)
143            let mut sig = vec![SIGNATURE_TYPE_P256];
144            sig.extend_from_slice(&[0u8; 128]);
145            let result = sign_recover(B256::ZERO, sig);
146            assert!(result.is_err());
147            Ok(())
148        })
149    }
150
151    #[test]
152    fn test_verify_webauthn_too_short_reverts() -> eyre::Result<()> {
153        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
154        StorageCtx::enter(&mut storage, || {
155            // 0x02 prefix + 127 bytes (min is 128)
156            let mut sig = vec![SIGNATURE_TYPE_WEBAUTHN];
157            sig.extend_from_slice(&[0u8; 127]);
158            let result = sign_recover(B256::ZERO, sig);
159            assert!(result.is_err());
160            Ok(())
161        })
162    }
163
164    #[test]
165    fn test_verify_webauthn_too_long_reverts() -> eyre::Result<()> {
166        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
167        StorageCtx::enter(&mut storage, || {
168            // 0x02 prefix + 2049 bytes (max is 2048)
169            let mut sig = vec![SIGNATURE_TYPE_WEBAUTHN];
170            sig.extend_from_slice(&[0u8; 2049]);
171            let result = sign_recover(B256::ZERO, sig);
172            assert!(result.is_err());
173            Ok(())
174        })
175    }
176
177    #[test]
178    fn test_verify_unknown_type_reverts() -> eyre::Result<()> {
179        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
180        StorageCtx::enter(&mut storage, || {
181            let mut sig = vec![0x05];
182            sig.extend_from_slice(&[0u8; 129]);
183            let result = sign_recover(B256::ZERO, sig);
184            assert!(result.is_err());
185            Ok(())
186        })
187    }
188
189    #[test]
190    fn test_verify_invalid_secp256k1_signature_reverts() -> eyre::Result<()> {
191        let mut storage = HashMapStorageProvider::new_with_spec(1, TempoHardfork::T3);
192        StorageCtx::enter(&mut storage, || {
193            let result = sign_recover(B256::ZERO, vec![0u8; 65]);
194            assert!(result.is_err());
195            Ok(())
196        })
197    }
198}