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
15const 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 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 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}