1use super::{
2 tempo_transaction::{TEMPO_TX_TYPE_ID, TempoTransaction},
3 tt_signature::TempoSignature,
4 unique_tx_identifier_from_signable,
5};
6use alloc::vec::Vec;
7use alloy_consensus::{SignableTransaction, Transaction, transaction::TxHashRef};
8use alloy_eips::{
9 Decodable2718, Encodable2718, Typed2718,
10 eip2718::{Eip2718Error, Eip2718Result},
11 eip2930::AccessList,
12 eip7702::SignedAuthorization,
13};
14use alloy_primitives::{Address, B256, Bytes, Keccak256, TxKind, U256};
15use alloy_rlp::{BufMut, Decodable, Encodable};
16use core::{
17 fmt::Debug,
18 hash::{Hash, Hasher},
19};
20
21#[cfg(not(feature = "std"))]
22use once_cell::race::OnceBox as OnceLock;
23#[cfg(feature = "std")]
24use std::sync::OnceLock;
25
26#[derive(Clone, Debug)]
31pub struct AASigned {
32 tx: TempoTransaction,
34 signature: TempoSignature,
36 #[doc(alias = "tx_hash", alias = "transaction_hash")]
38 hash: OnceLock<B256>,
39 signature_hash: OnceLock<B256>,
41 expiring_nonce_hash: OnceLock<(Address, B256)>,
43}
44
45impl AASigned {
46 pub fn new_unchecked(tx: TempoTransaction, signature: TempoSignature, hash: B256) -> Self {
49 let value = OnceLock::new();
50 #[allow(clippy::useless_conversion)]
51 value.get_or_init(|| hash.into());
52 Self {
53 tx,
54 signature,
55 hash: value,
56 signature_hash: OnceLock::new(),
57 expiring_nonce_hash: OnceLock::new(),
58 }
59 }
60
61 pub const fn new_unhashed(tx: TempoTransaction, signature: TempoSignature) -> Self {
64 Self {
65 tx,
66 signature,
67 hash: OnceLock::new(),
68 signature_hash: OnceLock::new(),
69 expiring_nonce_hash: OnceLock::new(),
70 }
71 }
72
73 #[doc(alias = "transaction")]
75 pub const fn tx(&self) -> &TempoTransaction {
76 &self.tx
77 }
78
79 pub const fn tx_mut(&mut self) -> &mut TempoTransaction {
81 &mut self.tx
82 }
83
84 pub const fn signature(&self) -> &TempoSignature {
86 &self.signature
87 }
88
89 pub fn strip_signature(self) -> TempoTransaction {
91 self.tx
92 }
93
94 #[doc(alias = "tx_hash", alias = "transaction_hash")]
96 pub fn hash(&self) -> &B256 {
97 #[allow(clippy::useless_conversion)]
98 self.hash.get_or_init(|| self.compute_hash().into())
99 }
100
101 fn compute_hash(&self) -> B256 {
103 let mut buf = Vec::new();
104 self.eip2718_encode(&mut buf);
105 alloy_primitives::keccak256(&buf)
106 }
107
108 pub fn signature_hash(&self) -> B256 {
110 #[allow(clippy::useless_conversion)]
111 *self
112 .signature_hash
113 .get_or_init(|| self.tx.signature_hash().into())
114 }
115
116 pub fn recover_signer_with_expiring_nonce_hash(
122 &self,
123 ) -> Result<(Address, Option<B256>), alloy_consensus::crypto::RecoveryError> {
124 if !self.tx.is_expiring_nonce_tx() {
125 let signer =
126 <Self as alloy_consensus::transaction::SignerRecoverable>::recover_signer(self)?;
127 return Ok((signer, None));
128 }
129
130 let mut buf = Vec::with_capacity(self.tx.payload_len_for_signature());
131 self.tx.encode_for_signing(&mut buf);
132
133 let mut hasher = Keccak256::new();
134 hasher.update(&buf);
135
136 #[allow(clippy::useless_conversion)]
137 let signature_hash = *self
138 .signature_hash
139 .get_or_init(|| hasher.clone().finalize().into());
140 let signer = self.signature.recover_signer(&signature_hash)?;
141
142 if let Some((cached_sender, cached_hash)) = self.expiring_nonce_hash.get()
143 && *cached_sender == signer
144 {
145 return Ok((signer, Some(*cached_hash)));
146 }
147
148 hasher.update(signer.as_slice());
149 let expiring_nonce_hash = hasher.finalize();
150 #[allow(clippy::useless_conversion)]
151 let _ = self
152 .expiring_nonce_hash
153 .set((signer, expiring_nonce_hash).into());
154
155 Ok((signer, Some(expiring_nonce_hash)))
156 }
157
158 pub fn expiring_nonce_hash(&self, sender: Address) -> B256 {
166 let cached = self.expiring_nonce_hash.get_or_init(|| {
167 let hash = unique_tx_identifier_from_signable(&self.tx, sender);
168 #[allow(clippy::useless_conversion)]
169 (sender, hash).into()
170 });
171 if cached.0 == sender {
172 cached.1
173 } else {
174 unique_tx_identifier_from_signable(&self.tx, sender)
175 }
176 }
177
178 #[inline]
181 fn rlp_header(&self) -> alloy_rlp::Header {
182 let payload_length = self.tx.rlp_encoded_fields_length_default() + self.signature.length();
183 alloy_rlp::Header {
184 list: true,
185 payload_length,
186 }
187 }
188
189 pub fn rlp_encode(&self, out: &mut dyn BufMut) {
191 self.rlp_header().encode(out);
193
194 self.tx.rlp_encode_fields_default(out);
196
197 self.signature.encode(out);
199 }
200
201 pub fn encode_for_fee_payer_service(&self, out: &mut dyn BufMut) {
203 let payload_length =
204 self.tx.rlp_encoded_fields_length(|_| 1, true) + self.signature.length();
205
206 out.put_u8(TEMPO_TX_TYPE_ID);
207 alloy_rlp::Header {
208 list: true,
209 payload_length,
210 }
211 .encode(out);
212 self.tx
213 .rlp_encode_fields(out, |_, out| out.put_u8(0x00), true);
214 self.signature.encode(out);
215 }
216
217 pub fn into_parts(self) -> (TempoTransaction, TempoSignature, B256) {
219 let hash = *self.hash();
220 (self.tx, self.signature, hash)
221 }
222
223 fn rlp_encoded_length(&self) -> usize {
225 self.rlp_header().length_with_payload()
226 }
227
228 fn eip2718_encoded_length(&self) -> usize {
230 1 + self.rlp_encoded_length()
231 }
232
233 pub fn eip2718_encode(&self, out: &mut dyn BufMut) {
235 out.put_u8(TEMPO_TX_TYPE_ID);
237 self.rlp_encode(out);
239 }
240
241 pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
243 let header = alloy_rlp::Header::decode(buf)?;
244 if !header.list {
245 return Err(alloy_rlp::Error::UnexpectedString);
246 }
247 let remaining = buf.len();
248
249 if header.payload_length > remaining {
250 return Err(alloy_rlp::Error::InputTooShort);
251 }
252
253 let tx = TempoTransaction::rlp_decode_fields(buf)?;
255
256 let sig_bytes: Bytes = Decodable::decode(buf)?;
258
259 let consumed = remaining - buf.len();
261 if consumed != header.payload_length {
262 return Err(alloy_rlp::Error::UnexpectedLength);
263 }
264
265 let signature = TempoSignature::from_bytes(&sig_bytes).map_err(alloy_rlp::Error::Custom)?;
267
268 Ok(Self::new_unhashed(tx, signature))
269 }
270}
271
272impl TxHashRef for AASigned {
273 fn tx_hash(&self) -> &B256 {
274 self.hash()
275 }
276}
277
278impl Typed2718 for AASigned {
279 fn ty(&self) -> u8 {
280 TEMPO_TX_TYPE_ID
281 }
282}
283
284impl Transaction for AASigned {
285 #[inline]
286 fn chain_id(&self) -> Option<u64> {
287 self.tx.chain_id()
288 }
289
290 #[inline]
291 fn nonce(&self) -> u64 {
292 self.tx.nonce()
293 }
294
295 #[inline]
296 fn gas_limit(&self) -> u64 {
297 self.tx.gas_limit()
298 }
299
300 #[inline]
301 fn gas_price(&self) -> Option<u128> {
302 self.tx.gas_price()
303 }
304
305 #[inline]
306 fn max_fee_per_gas(&self) -> u128 {
307 self.tx.max_fee_per_gas()
308 }
309
310 #[inline]
311 fn max_priority_fee_per_gas(&self) -> Option<u128> {
312 self.tx.max_priority_fee_per_gas()
313 }
314
315 #[inline]
316 fn max_fee_per_blob_gas(&self) -> Option<u128> {
317 None
318 }
319
320 #[inline]
321 fn priority_fee_or_price(&self) -> u128 {
322 self.tx.priority_fee_or_price()
323 }
324
325 fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
326 self.tx.effective_gas_price(base_fee)
327 }
328
329 #[inline]
330 fn is_dynamic_fee(&self) -> bool {
331 true
332 }
333
334 #[inline]
335 fn kind(&self) -> TxKind {
336 self.tx
338 .calls
339 .first()
340 .map(|c| c.to)
341 .unwrap_or(TxKind::Create)
342 }
343
344 #[inline]
345 fn is_create(&self) -> bool {
346 self.kind().is_create()
347 }
348
349 #[inline]
350 fn value(&self) -> U256 {
351 self.tx
353 .calls
354 .iter()
355 .fold(U256::ZERO, |acc, call| acc + call.value)
356 }
357
358 #[inline]
359 fn input(&self) -> &Bytes {
360 static EMPTY_BYTES: Bytes = Bytes::new();
362 self.tx
363 .calls
364 .first()
365 .map(|c| &c.input)
366 .unwrap_or(&EMPTY_BYTES)
367 }
368
369 #[inline]
370 fn access_list(&self) -> Option<&AccessList> {
371 Some(&self.tx.access_list)
372 }
373
374 #[inline]
375 fn blob_versioned_hashes(&self) -> Option<&[B256]> {
376 None
377 }
378
379 #[inline]
380 fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
381 None
382 }
383}
384
385impl Hash for AASigned {
386 fn hash<H: Hasher>(&self, state: &mut H) {
387 self.hash().hash(state);
388 self.tx.hash(state);
389 self.signature.hash(state);
390 }
391}
392
393impl PartialEq for AASigned {
394 fn eq(&self, other: &Self) -> bool {
395 self.hash() == other.hash() && self.tx == other.tx && self.signature == other.signature
396 }
397}
398
399impl Eq for AASigned {}
400
401impl alloy_consensus::transaction::SignerRecoverable for AASigned {
402 fn recover_signer(
403 &self,
404 ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
405 let sig_hash = self.signature_hash();
406 self.signature.recover_signer(&sig_hash)
407 }
408
409 fn recover_signer_unchecked(
410 &self,
411 ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
412 self.recover_signer()
415 }
416}
417
418impl Encodable2718 for AASigned {
419 fn encode_2718_len(&self) -> usize {
420 self.eip2718_encoded_length()
421 }
422
423 fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
424 self.eip2718_encode(out)
425 }
426
427 fn trie_hash(&self) -> B256 {
428 *self.hash()
429 }
430}
431
432impl Decodable2718 for AASigned {
433 fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
434 if ty != TEMPO_TX_TYPE_ID {
435 return Err(Eip2718Error::UnexpectedType(ty));
436 }
437 Self::rlp_decode(buf).map_err(Into::into)
438 }
439
440 fn fallback_decode(_: &mut &[u8]) -> Eip2718Result<Self> {
441 Err(Eip2718Error::UnexpectedType(0))
442 }
443}
444
445#[cfg(any(test, feature = "arbitrary"))]
446impl<'a> arbitrary::Arbitrary<'a> for AASigned {
447 fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
448 let tx = TempoTransaction::arbitrary(u)?;
449 let signature = TempoSignature::arbitrary(u)?;
450 Ok(Self::new_unhashed(tx, signature))
451 }
452}
453
454#[cfg(feature = "serde")]
455mod serde_impl {
456 use super::*;
457 use alloc::borrow::Cow;
458 use serde::{Deserialize, Deserializer, Serialize, Serializer};
459
460 #[derive(Serialize, Deserialize)]
461 struct AASignedHelper<'a> {
462 #[serde(flatten)]
463 tx: Cow<'a, TempoTransaction>,
464 signature: Cow<'a, TempoSignature>,
465 hash: Cow<'a, B256>,
466 }
467
468 impl Serialize for super::AASigned {
469 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
470 where
471 S: Serializer,
472 {
473 if let TempoSignature::Keychain(keychain_sig) = &self.signature {
474 let _ = keychain_sig.key_id(&self.signature_hash());
476 }
477 AASignedHelper {
478 tx: Cow::Borrowed(&self.tx),
479 signature: Cow::Borrowed(&self.signature),
480 hash: Cow::Borrowed(self.hash()),
481 }
482 .serialize(serializer)
483 }
484 }
485
486 impl<'de> Deserialize<'de> for super::AASigned {
487 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
488 where
489 D: Deserializer<'de>,
490 {
491 AASignedHelper::deserialize(deserializer).map(|value| {
492 Self::new_unchecked(
493 value.tx.into_owned(),
494 value.signature.into_owned(),
495 value.hash.into_owned(),
496 )
497 })
498 }
499 }
500
501 #[cfg(test)]
502 mod tests {
503 use crate::transaction::{
504 tempo_transaction::{Call, TempoTransaction},
505 tt_signature::{PrimitiveSignature, TempoSignature},
506 };
507 use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
508
509 #[test]
510 fn test_serde_output() {
511 let tx = TempoTransaction {
513 chain_id: 1337,
514 fee_token: None,
515 max_priority_fee_per_gas: 1000000000,
516 max_fee_per_gas: 2000000000,
517 gas_limit: 21000,
518 calls: vec![Call {
519 to: TxKind::Call(Address::repeat_byte(0x42)),
520 value: U256::from(1000),
521 input: Bytes::from(vec![1, 2, 3, 4]),
522 }],
523 nonce_key: U256::ZERO,
524 nonce: 5,
525 ..Default::default()
526 };
527
528 let signature = TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
530 Signature::test_signature(),
531 ));
532
533 let aa_signed = super::super::AASigned::new_unhashed(tx, signature);
534
535 let json = serde_json::to_string_pretty(&aa_signed).unwrap();
537 assert!(
538 !json.contains("signature_hash"),
539 "signature_hash cache must not be serialized"
540 );
541
542 println!("\n=== AASigned JSON Output ===");
543 println!("{json}");
544 println!("============================\n");
545
546 let deserialized: super::super::AASigned = serde_json::from_str(&json).unwrap();
548 assert_eq!(aa_signed.tx(), deserialized.tx());
549 assert_eq!(aa_signed.signature_hash(), deserialized.signature_hash());
550 }
551 }
552}
553
554#[cfg(test)]
555mod tests {
556 use super::*;
557 use crate::transaction::{
558 tempo_transaction::Call,
559 tt_authorization::tests::{generate_secp256k1_keypair, sign_hash},
560 tt_signature::PrimitiveSignature,
561 };
562 use alloy_consensus::transaction::SignerRecoverable;
563 use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
564 use alloy_signer_local::PrivateKeySigner;
565 use core::num::NonZeroU64;
566 use proptest::prelude::*;
567
568 fn make_tx() -> TempoTransaction {
569 TempoTransaction {
570 chain_id: 1,
571 gas_limit: 21000,
572 calls: vec![Call {
573 to: TxKind::Call(Address::repeat_byte(0x42)),
574 value: U256::ZERO,
575 input: Bytes::new(),
576 }],
577 ..Default::default()
578 }
579 }
580
581 fn signed_pair_for_tx(tx: TempoTransaction) -> (AASigned, AASigned) {
582 let signer = PrivateKeySigner::from_bytes(&B256::with_last_byte(1)).unwrap();
583 let signature = sign_hash(&signer, &tx.signature_hash());
584 (
585 AASigned::new_unhashed(tx.clone(), signature.clone()),
586 AASigned::new_unhashed(tx, signature),
587 )
588 }
589
590 fn arb_address() -> impl Strategy<Value = Address> {
591 any::<[u8; 20]>().prop_map(|bytes| Address::from_slice(&bytes))
592 }
593
594 fn arb_fee_payer_signature() -> impl Strategy<Value = Signature> {
595 (any::<u64>(), any::<u64>(), any::<bool>()).prop_map(|(r, s, parity)| {
596 Signature::new(
597 U256::from(r).saturating_add(U256::ONE),
598 U256::from(s).saturating_add(U256::ONE),
599 parity,
600 )
601 })
602 }
603
604 fn arb_call() -> impl Strategy<Value = Call> {
605 (
606 arb_address(),
607 any::<u64>(),
608 proptest::collection::vec(any::<u8>(), 0..128),
609 )
610 .prop_map(|(to, value, input)| Call {
611 to: TxKind::Call(to),
612 value: U256::from(value),
613 input: Bytes::from(input),
614 })
615 }
616
617 fn arb_valid_window() -> impl Strategy<Value = (Option<NonZeroU64>, Option<NonZeroU64>)> {
618 prop::option::of((1u64..1_000_000, 1u64..1_000)).prop_map(|window| {
619 window.map_or((None, None), |(valid_after, offset)| {
620 let valid_after = NonZeroU64::new(valid_after).unwrap();
621 let valid_before = NonZeroU64::new(valid_after.get() + offset).unwrap();
622 (Some(valid_after), Some(valid_before))
623 })
624 })
625 }
626
627 fn arb_tempo_tx() -> impl Strategy<Value = TempoTransaction> {
628 (
629 any::<u64>(),
630 prop::option::of(arb_address()),
631 any::<u128>(),
632 any::<u128>(),
633 any::<u64>(),
634 proptest::collection::vec(arb_call(), 1..8),
635 any::<u64>(),
636 prop::option::of(arb_fee_payer_signature()),
637 arb_valid_window(),
638 )
639 .prop_map(
640 |(
641 chain_id,
642 fee_token,
643 max_priority_fee_per_gas,
644 max_fee_per_gas,
645 gas_limit,
646 calls,
647 nonce,
648 fee_payer_signature,
649 (valid_after, valid_before),
650 )| TempoTransaction {
651 chain_id,
652 fee_token,
653 max_priority_fee_per_gas,
654 max_fee_per_gas,
655 gas_limit,
656 calls,
657 nonce_key: U256::ZERO,
658 nonce,
659 fee_payer_signature,
660 valid_before,
661 valid_after,
662 ..Default::default()
663 },
664 )
665 }
666
667 #[test]
668 fn test_hash_and_transaction_trait() {
669 let tx = make_tx();
670 let sig =
671 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
672
673 let signed = AASigned::new_unhashed(tx.clone(), sig.clone());
675
676 let hash1 = *signed.hash();
678 let hash2 = *signed.hash();
680 assert_eq!(hash1, hash2, "hash should be deterministic");
681 assert_ne!(hash1, B256::ZERO);
682
683 let known_hash = B256::random();
685 let signed_unchecked = AASigned::new_unchecked(tx.clone(), sig.clone(), known_hash);
686 assert_eq!(
687 *signed_unchecked.hash(),
688 known_hash,
689 "new_unchecked should use provided hash"
690 );
691
692 let signed_for_parts = AASigned::new_unhashed(tx.clone(), sig.clone());
694 let (returned_tx, returned_sig, returned_hash) = signed_for_parts.into_parts();
695 assert_eq!(returned_tx, tx);
696 assert_eq!(returned_sig, sig);
697 assert_eq!(returned_hash, hash1);
698 }
699
700 #[test]
701 fn test_rlp_encode_decode_roundtrip() {
702 use alloy_eips::eip2718::Encodable2718;
703
704 let tx = make_tx();
705 let sig =
706 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
707 let signed = AASigned::new_unhashed(tx, sig);
708
709 let mut buf = Vec::new();
711 signed.rlp_encode(&mut buf);
712 let encoded_before_cache = buf.clone();
713 let _ = signed.signature_hash();
714 let mut encoded_after_cache = Vec::new();
715 signed.rlp_encode(&mut encoded_after_cache);
716 assert_eq!(
717 encoded_before_cache, encoded_after_cache,
718 "signature_hash cache must not change RLP encoding"
719 );
720
721 let decoded = AASigned::rlp_decode(&mut buf.as_slice()).unwrap();
723 assert_eq!(decoded.tx(), signed.tx());
724 assert_eq!(decoded.signature(), signed.signature());
725
726 let mut eip_buf = Vec::new();
728 signed.eip2718_encode(&mut eip_buf);
729 assert_eq!(eip_buf[0], TEMPO_TX_TYPE_ID);
730
731 let decoded_2718 =
732 AASigned::typed_decode(TEMPO_TX_TYPE_ID, &mut eip_buf[1..].as_ref()).unwrap();
733 assert_eq!(decoded_2718.tx(), signed.tx());
734
735 assert_eq!(signed.trie_hash(), *signed.hash());
737
738 let fallback_result = AASigned::fallback_decode(&mut [].as_ref());
740 assert!(fallback_result.is_err());
741
742 assert_eq!(signed.encode_2718_len(), eip_buf.len());
744 }
745
746 #[test]
747 fn test_rlp_decode_error_paths() {
748 let result = AASigned::rlp_decode(&mut [].as_ref());
750 assert!(result.is_err());
751
752 let result = AASigned::rlp_decode(&mut [0x80].as_ref());
754 assert!(result.is_err());
755
756 let result = AASigned::rlp_decode(&mut [0xc1, 0x00].as_ref()); assert!(result.is_err());
759
760 let result = AASigned::typed_decode(0x00, &mut [].as_ref());
762 assert!(result.is_err());
763 }
764
765 #[test]
766 fn test_expiring_nonce_hash_invariant_to_fee_payer() {
767 let sender = Address::repeat_byte(0x01);
768
769 let make_sponsored_tx = |fee_payer_sig: Signature| -> TempoTransaction {
770 TempoTransaction {
771 chain_id: 1,
772 gas_limit: 1_000_000,
773 nonce_key: U256::MAX, nonce: 0,
775 fee_token: Some(Address::repeat_byte(0xFE)),
776 fee_payer_signature: Some(fee_payer_sig),
777 valid_before: Some(core::num::NonZeroU64::new(100).unwrap()),
778 calls: vec![Call {
779 to: TxKind::Call(Address::repeat_byte(0x42)),
780 value: U256::ZERO,
781 input: Bytes::new(),
782 }],
783 ..Default::default()
784 }
785 };
786
787 let sig =
788 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
789
790 let tx1 = make_sponsored_tx(Signature::new(U256::from(1), U256::from(2), false));
792 let tx2 = make_sponsored_tx(Signature::new(U256::from(3), U256::from(4), true));
793
794 let signed1 = AASigned::new_unhashed(tx1, sig.clone());
795 let signed2 = AASigned::new_unhashed(tx2, sig);
796
797 assert_ne!(signed1.hash(), signed2.hash(), "tx hashes must differ");
799
800 let hash1 = signed1.expiring_nonce_hash(sender);
802 let hash2 = signed2.expiring_nonce_hash(sender);
803 assert_eq!(
804 hash1, hash2,
805 "expiring_nonce_hash must be invariant to fee payer signature changes"
806 );
807 assert_ne!(hash1, B256::ZERO);
808 }
809
810 #[test]
811 fn test_expiring_nonce_hash_unique_per_sender() {
812 let tx = TempoTransaction {
813 chain_id: 1,
814 gas_limit: 1_000_000,
815 nonce_key: U256::MAX,
816 nonce: 0,
817 valid_before: Some(core::num::NonZeroU64::new(100).unwrap()),
818 calls: vec![Call {
819 to: TxKind::Call(Address::repeat_byte(0x42)),
820 value: U256::ZERO,
821 input: Bytes::new(),
822 }],
823 ..Default::default()
824 };
825 let sig =
826 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
827 let signed = AASigned::new_unhashed(tx, sig);
828
829 let sender_a = Address::repeat_byte(0x01);
830 let sender_b = Address::repeat_byte(0x02);
831
832 assert_ne!(
833 signed.expiring_nonce_hash(sender_a),
834 signed.expiring_nonce_hash(sender_b),
835 "different senders must produce different expiring_nonce_hash"
836 );
837 }
838
839 #[test]
840 fn test_expiring_nonce_hash_deterministic() {
841 let tx = make_tx();
842 let sig =
843 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
844 let signed = AASigned::new_unhashed(tx, sig);
845 let sender = Address::repeat_byte(0xAB);
846
847 let h1 = signed.expiring_nonce_hash(sender);
848 let h2 = signed.expiring_nonce_hash(sender);
849 assert_eq!(h1, h2, "expiring_nonce_hash must be deterministic");
850 }
851
852 #[test]
853 fn test_recover_signer() {
854 let (signing_key, expected_address) = generate_secp256k1_keypair();
855
856 let tx = make_tx();
857
858 let placeholder =
860 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
861 let temp_signed = AASigned::new_unhashed(tx.clone(), placeholder);
862 let sig_hash = temp_signed.signature_hash();
863
864 let signature = sign_hash(&signing_key, &sig_hash);
866 let signed = AASigned::new_unhashed(tx.clone(), signature);
867
868 let recovered = signed.recover_signer().unwrap();
870 assert_eq!(recovered, expected_address);
871
872 let recovered_unchecked = signed.recover_signer_unchecked().unwrap();
874 assert_eq!(recovered_unchecked, expected_address);
875
876 let wrong_sig = sign_hash(&signing_key, &B256::random());
878 let bad_signed = AASigned::new_unhashed(tx, wrong_sig);
879 let bad_recovered = bad_signed.recover_signer().unwrap();
880 assert_ne!(bad_recovered, expected_address);
881 }
882
883 #[test]
884 fn test_recover_signer_with_expiring_nonce_hash() {
885 let (signing_key, expected_address) = generate_secp256k1_keypair();
886
887 let mut tx = make_tx();
888 tx.nonce_key = U256::MAX;
889
890 let placeholder =
891 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
892 let temp_signed = AASigned::new_unhashed(tx.clone(), placeholder);
893 let sig_hash = temp_signed.signature_hash();
894
895 let signature = sign_hash(&signing_key, &sig_hash);
896 let signed = AASigned::new_unhashed(tx, signature);
897
898 let (recovered, expiring_nonce_hash) =
899 signed.recover_signer_with_expiring_nonce_hash().unwrap();
900 assert_eq!(recovered, expected_address);
901 assert_eq!(
902 expiring_nonce_hash,
903 Some(signed.expiring_nonce_hash(expected_address))
904 );
905 assert_eq!(signed.signature_hash(), sig_hash);
906 }
907
908 #[test]
909 fn test_recover_signer_with_expiring_nonce_hash_non_expiring() {
910 let (signing_key, expected_address) = generate_secp256k1_keypair();
911
912 let tx = make_tx();
913
914 let placeholder =
915 TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
916 let temp_signed = AASigned::new_unhashed(tx.clone(), placeholder);
917 let sig_hash = temp_signed.signature_hash();
918
919 let signature = sign_hash(&signing_key, &sig_hash);
920 let signed = AASigned::new_unhashed(tx, signature);
921
922 let (recovered, expiring_nonce_hash) =
923 signed.recover_signer_with_expiring_nonce_hash().unwrap();
924 assert_eq!(recovered, expected_address);
925 assert_eq!(expiring_nonce_hash, None);
926 }
927
928 proptest! {
929 #[test]
930 fn proptest_recover_signer_with_expiring_nonce_hash_matches_individuals(mut tx in arb_tempo_tx()) {
931 tx.nonce_key = U256::MAX;
932 let (individual, helper) = signed_pair_for_tx(tx);
933
934 let individual_signer = individual.recover_signer().unwrap();
935 let individual_expiring_nonce_hash = individual.expiring_nonce_hash(individual_signer);
936 let (helper_signer, helper_expiring_nonce_hash) =
937 helper.recover_signer_with_expiring_nonce_hash().unwrap();
938
939 prop_assert_eq!(helper_signer, individual_signer);
940 prop_assert_eq!(helper_expiring_nonce_hash, Some(individual_expiring_nonce_hash));
941 }
942
943 #[test]
944 fn proptest_recover_signer_with_expiring_nonce_hash_matches_individuals_for_non_expiring(mut tx in arb_tempo_tx()) {
945 tx.nonce_key = U256::ZERO;
946 let (individual, helper) = signed_pair_for_tx(tx);
947
948 let individual_signer = individual.recover_signer().unwrap();
949 let (helper_signer, helper_expiring_nonce_hash) =
950 helper.recover_signer_with_expiring_nonce_hash().unwrap();
951
952 prop_assert_eq!(helper_signer, individual_signer);
953 prop_assert_eq!(helper_expiring_nonce_hash, None);
954 }
955 }
956}