Skip to main content

tempo_primitives/transaction/
tt_signed.rs

1use super::{
2    tempo_transaction::{TEMPO_TX_TYPE_ID, TempoTransaction},
3    tt_signature::TempoSignature,
4};
5use alloc::vec::Vec;
6use alloy_consensus::{SignableTransaction, Transaction, transaction::TxHashRef};
7use alloy_eips::{
8    Decodable2718, Encodable2718, Typed2718,
9    eip2718::{Eip2718Error, Eip2718Result},
10    eip2930::AccessList,
11    eip7702::SignedAuthorization,
12};
13use alloy_primitives::{Address, B256, Bytes, TxKind, U256};
14use alloy_rlp::{BufMut, Decodable, Encodable};
15use core::{
16    fmt::Debug,
17    hash::{Hash, Hasher},
18};
19
20#[cfg(not(feature = "std"))]
21use once_cell::race::OnceBox as OnceLock;
22#[cfg(feature = "std")]
23use std::sync::OnceLock;
24
25/// A transaction with an AA signature and hash seal.
26///
27/// This wraps a TempoTransaction transaction with its multi-signature-type signature
28/// (secp256k1, P256, Webauthn, Keychain) and provides cached hashes.
29#[derive(Clone, Debug)]
30pub struct AASigned {
31    /// The inner Tempo transaction
32    tx: TempoTransaction,
33    /// The signature (can be secp256k1, P256, Webauthn, Keychain)
34    signature: TempoSignature,
35    /// Cached transaction hash
36    #[doc(alias = "tx_hash", alias = "transaction_hash")]
37    hash: OnceLock<B256>,
38}
39
40impl AASigned {
41    /// Instantiate from a transaction and signature with a known hash.
42    /// Does not verify the signature.
43    pub fn new_unchecked(tx: TempoTransaction, signature: TempoSignature, hash: B256) -> Self {
44        let value = OnceLock::new();
45        #[allow(clippy::useless_conversion)]
46        value.get_or_init(|| hash.into());
47        Self {
48            tx,
49            signature,
50            hash: value,
51        }
52    }
53
54    /// Instantiate from a transaction and signature without computing the hash.
55    /// Does not verify the signature.
56    pub const fn new_unhashed(tx: TempoTransaction, signature: TempoSignature) -> Self {
57        Self {
58            tx,
59            signature,
60            hash: OnceLock::new(),
61        }
62    }
63
64    /// Returns a reference to the transaction.
65    #[doc(alias = "transaction")]
66    pub const fn tx(&self) -> &TempoTransaction {
67        &self.tx
68    }
69
70    /// Returns a mutable reference to the transaction.
71    pub const fn tx_mut(&mut self) -> &mut TempoTransaction {
72        &mut self.tx
73    }
74
75    /// Returns a reference to the signature.
76    pub const fn signature(&self) -> &TempoSignature {
77        &self.signature
78    }
79
80    /// Returns the transaction without signature.
81    pub fn strip_signature(self) -> TempoTransaction {
82        self.tx
83    }
84
85    /// Returns a reference to the transaction hash, computing it if needed.
86    #[doc(alias = "tx_hash", alias = "transaction_hash")]
87    pub fn hash(&self) -> &B256 {
88        #[allow(clippy::useless_conversion)]
89        self.hash.get_or_init(|| self.compute_hash().into())
90    }
91
92    /// Calculate the transaction hash
93    fn compute_hash(&self) -> B256 {
94        let mut buf = Vec::new();
95        self.eip2718_encode(&mut buf);
96        alloy_primitives::keccak256(&buf)
97    }
98
99    /// Calculate the signing hash for the transaction.
100    pub fn signature_hash(&self) -> B256 {
101        self.tx.signature_hash()
102    }
103
104    /// Calculate the expiring nonce dedup hash for replay protection.
105    ///
106    /// This hash is `keccak256(encode_for_signing || sender)`. It is:
107    /// - **Invariant to fee payer changes**: the fee payer signature and fee token are excluded
108    ///   (since `encode_for_signing` doesn't commit to them when a fee payer is present).
109    /// - **Unique per sender**: different signers produce different recovered addresses, so the
110    ///   hash differs even for identical transaction payloads.
111    ///
112    /// This prevents a replay attack where two different fee payers sign the same sender-signed
113    /// transaction, producing different `tx_hash` values that would bypass `tx_hash`-based
114    /// replay protection.
115    pub fn expiring_nonce_hash(&self, sender: Address) -> B256 {
116        let mut buf =
117            Vec::with_capacity(self.tx.payload_len_for_signature() + sender.as_slice().len());
118        self.tx.encode_for_signing(&mut buf);
119        buf.extend_from_slice(sender.as_slice());
120        alloy_primitives::keccak256(&buf)
121    }
122
123    /// Returns the RLP header for the transaction and signature, encapsulating both
124    /// payload length calculation and header creation
125    #[inline]
126    fn rlp_header(&self) -> alloy_rlp::Header {
127        let payload_length = self.tx.rlp_encoded_fields_length_default() + self.signature.length();
128        alloy_rlp::Header {
129            list: true,
130            payload_length,
131        }
132    }
133
134    /// Encode the transaction fields and signature as RLP list (without type byte)
135    pub fn rlp_encode(&self, out: &mut dyn BufMut) {
136        // RLP header
137        self.rlp_header().encode(out);
138
139        // Encode transaction fields
140        self.tx.rlp_encode_fields_default(out);
141
142        // Encode signature
143        self.signature.encode(out);
144    }
145
146    /// Splits the transaction into parts.
147    pub fn into_parts(self) -> (TempoTransaction, TempoSignature, B256) {
148        let hash = *self.hash();
149        (self.tx, self.signature, hash)
150    }
151
152    /// Get the length of the transaction when RLP encoded.
153    fn rlp_encoded_length(&self) -> usize {
154        self.rlp_header().length_with_payload()
155    }
156
157    /// Get the length of the transaction when EIP-2718 encoded (includes type byte).
158    fn eip2718_encoded_length(&self) -> usize {
159        1 + self.rlp_encoded_length()
160    }
161
162    /// EIP-2718 encode the signed transaction.
163    pub fn eip2718_encode(&self, out: &mut dyn BufMut) {
164        // Type byte
165        out.put_u8(TEMPO_TX_TYPE_ID);
166        // RLP fields
167        self.rlp_encode(out);
168    }
169
170    /// Decode the RLP fields (without type byte).
171    pub fn rlp_decode(buf: &mut &[u8]) -> alloy_rlp::Result<Self> {
172        let header = alloy_rlp::Header::decode(buf)?;
173        if !header.list {
174            return Err(alloy_rlp::Error::UnexpectedString);
175        }
176        let remaining = buf.len();
177
178        if header.payload_length > remaining {
179            return Err(alloy_rlp::Error::InputTooShort);
180        }
181
182        // Decode transaction fields directly from the buffer
183        let tx = TempoTransaction::rlp_decode_fields(buf)?;
184
185        // Decode signature bytes
186        let sig_bytes: Bytes = Decodable::decode(buf)?;
187
188        // Check that we consumed the expected amount
189        let consumed = remaining - buf.len();
190        if consumed != header.payload_length {
191            return Err(alloy_rlp::Error::UnexpectedLength);
192        }
193
194        // Parse signature
195        let signature = TempoSignature::from_bytes(&sig_bytes).map_err(alloy_rlp::Error::Custom)?;
196
197        Ok(Self::new_unhashed(tx, signature))
198    }
199}
200
201impl TxHashRef for AASigned {
202    fn tx_hash(&self) -> &B256 {
203        self.hash()
204    }
205}
206
207impl Typed2718 for AASigned {
208    fn ty(&self) -> u8 {
209        TEMPO_TX_TYPE_ID
210    }
211}
212
213impl Transaction for AASigned {
214    #[inline]
215    fn chain_id(&self) -> Option<u64> {
216        self.tx.chain_id()
217    }
218
219    #[inline]
220    fn nonce(&self) -> u64 {
221        self.tx.nonce()
222    }
223
224    #[inline]
225    fn gas_limit(&self) -> u64 {
226        self.tx.gas_limit()
227    }
228
229    #[inline]
230    fn gas_price(&self) -> Option<u128> {
231        self.tx.gas_price()
232    }
233
234    #[inline]
235    fn max_fee_per_gas(&self) -> u128 {
236        self.tx.max_fee_per_gas()
237    }
238
239    #[inline]
240    fn max_priority_fee_per_gas(&self) -> Option<u128> {
241        self.tx.max_priority_fee_per_gas()
242    }
243
244    #[inline]
245    fn max_fee_per_blob_gas(&self) -> Option<u128> {
246        None
247    }
248
249    #[inline]
250    fn priority_fee_or_price(&self) -> u128 {
251        self.tx.priority_fee_or_price()
252    }
253
254    fn effective_gas_price(&self, base_fee: Option<u64>) -> u128 {
255        self.tx.effective_gas_price(base_fee)
256    }
257
258    #[inline]
259    fn is_dynamic_fee(&self) -> bool {
260        true
261    }
262
263    #[inline]
264    fn kind(&self) -> TxKind {
265        // Return first call's `to` or Create if empty
266        self.tx
267            .calls
268            .first()
269            .map(|c| c.to)
270            .unwrap_or(TxKind::Create)
271    }
272
273    #[inline]
274    fn is_create(&self) -> bool {
275        self.kind().is_create()
276    }
277
278    #[inline]
279    fn value(&self) -> U256 {
280        // Return sum of all call values
281        self.tx
282            .calls
283            .iter()
284            .fold(U256::ZERO, |acc, call| acc + call.value)
285    }
286
287    #[inline]
288    fn input(&self) -> &Bytes {
289        // Return first call's input or empty
290        static EMPTY_BYTES: Bytes = Bytes::new();
291        self.tx
292            .calls
293            .first()
294            .map(|c| &c.input)
295            .unwrap_or(&EMPTY_BYTES)
296    }
297
298    #[inline]
299    fn access_list(&self) -> Option<&AccessList> {
300        Some(&self.tx.access_list)
301    }
302
303    #[inline]
304    fn blob_versioned_hashes(&self) -> Option<&[B256]> {
305        None
306    }
307
308    #[inline]
309    fn authorization_list(&self) -> Option<&[SignedAuthorization]> {
310        None
311    }
312}
313
314impl Hash for AASigned {
315    fn hash<H: Hasher>(&self, state: &mut H) {
316        self.hash().hash(state);
317        self.tx.hash(state);
318        self.signature.hash(state);
319    }
320}
321
322impl PartialEq for AASigned {
323    fn eq(&self, other: &Self) -> bool {
324        self.hash() == other.hash() && self.tx == other.tx && self.signature == other.signature
325    }
326}
327
328impl Eq for AASigned {}
329
330#[cfg(feature = "reth")]
331impl reth_primitives_traits::InMemorySize for AASigned {
332    fn size(&self) -> usize {
333        size_of::<Self>() + self.tx.size() + self.signature.size()
334    }
335}
336
337impl alloy_consensus::transaction::SignerRecoverable for AASigned {
338    fn recover_signer(
339        &self,
340    ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
341        let sig_hash = self.signature_hash();
342        self.signature.recover_signer(&sig_hash)
343    }
344
345    fn recover_signer_unchecked(
346        &self,
347    ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
348        // For Tempo transactions, verified and unverified recovery are the same
349        // since signature verification happens during recover_signer
350        self.recover_signer()
351    }
352}
353
354impl Encodable2718 for AASigned {
355    fn encode_2718_len(&self) -> usize {
356        self.eip2718_encoded_length()
357    }
358
359    fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
360        self.eip2718_encode(out)
361    }
362
363    fn trie_hash(&self) -> B256 {
364        *self.hash()
365    }
366}
367
368impl Decodable2718 for AASigned {
369    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
370        if ty != TEMPO_TX_TYPE_ID {
371            return Err(Eip2718Error::UnexpectedType(ty));
372        }
373        Self::rlp_decode(buf).map_err(Into::into)
374    }
375
376    fn fallback_decode(_: &mut &[u8]) -> Eip2718Result<Self> {
377        Err(Eip2718Error::UnexpectedType(0))
378    }
379}
380
381#[cfg(any(test, feature = "arbitrary"))]
382impl<'a> arbitrary::Arbitrary<'a> for AASigned {
383    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
384        let tx = TempoTransaction::arbitrary(u)?;
385        let signature = TempoSignature::arbitrary(u)?;
386        Ok(Self::new_unhashed(tx, signature))
387    }
388}
389
390#[cfg(feature = "serde")]
391mod serde_impl {
392    use super::*;
393    use alloc::borrow::Cow;
394    use serde::{Deserialize, Deserializer, Serialize, Serializer};
395
396    #[derive(Serialize, Deserialize)]
397    struct AASignedHelper<'a> {
398        #[serde(flatten)]
399        tx: Cow<'a, TempoTransaction>,
400        signature: Cow<'a, TempoSignature>,
401        hash: Cow<'a, B256>,
402    }
403
404    impl Serialize for super::AASigned {
405        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
406        where
407            S: Serializer,
408        {
409            if let TempoSignature::Keychain(keychain_sig) = &self.signature {
410                // Initialize the `key_id` field for keychain signatures so that it's serialized.
411                let _ = keychain_sig.key_id(&self.signature_hash());
412            }
413            AASignedHelper {
414                tx: Cow::Borrowed(&self.tx),
415                signature: Cow::Borrowed(&self.signature),
416                hash: Cow::Borrowed(self.hash()),
417            }
418            .serialize(serializer)
419        }
420    }
421
422    impl<'de> Deserialize<'de> for super::AASigned {
423        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
424        where
425            D: Deserializer<'de>,
426        {
427            AASignedHelper::deserialize(deserializer).map(|value| {
428                Self::new_unchecked(
429                    value.tx.into_owned(),
430                    value.signature.into_owned(),
431                    value.hash.into_owned(),
432                )
433            })
434        }
435    }
436
437    #[cfg(test)]
438    mod tests {
439        use crate::transaction::{
440            tempo_transaction::{Call, TempoTransaction},
441            tt_signature::{PrimitiveSignature, TempoSignature},
442        };
443        use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
444
445        #[test]
446        fn test_serde_output() {
447            // Create a simple Tempo transaction
448            let tx = TempoTransaction {
449                chain_id: 1337,
450                fee_token: None,
451                max_priority_fee_per_gas: 1000000000,
452                max_fee_per_gas: 2000000000,
453                gas_limit: 21000,
454                calls: vec![Call {
455                    to: TxKind::Call(Address::repeat_byte(0x42)),
456                    value: U256::from(1000),
457                    input: Bytes::from(vec![1, 2, 3, 4]),
458                }],
459                nonce_key: U256::ZERO,
460                nonce: 5,
461                ..Default::default()
462            };
463
464            // Create a secp256k1 signature
465            let signature = TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
466                Signature::test_signature(),
467            ));
468
469            let aa_signed = super::super::AASigned::new_unhashed(tx, signature);
470
471            // Serialize to JSON
472            let json = serde_json::to_string_pretty(&aa_signed).unwrap();
473
474            println!("\n=== AASigned JSON Output ===");
475            println!("{json}");
476            println!("============================\n");
477
478            // Also test deserialization round-trip
479            let deserialized: super::super::AASigned = serde_json::from_str(&json).unwrap();
480            assert_eq!(aa_signed.tx(), deserialized.tx());
481        }
482    }
483}
484
485#[cfg(test)]
486mod tests {
487    use super::*;
488    use crate::transaction::{
489        tempo_transaction::Call,
490        tt_authorization::tests::{generate_secp256k1_keypair, sign_hash},
491        tt_signature::PrimitiveSignature,
492    };
493    use alloy_consensus::transaction::SignerRecoverable;
494    use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
495
496    fn make_tx() -> TempoTransaction {
497        TempoTransaction {
498            chain_id: 1,
499            gas_limit: 21000,
500            calls: vec![Call {
501                to: TxKind::Call(Address::repeat_byte(0x42)),
502                value: U256::ZERO,
503                input: Bytes::new(),
504            }],
505            ..Default::default()
506        }
507    }
508
509    #[test]
510    fn test_hash_and_transaction_trait() {
511        let tx = make_tx();
512        let sig =
513            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
514
515        // new_unhashed: hash not computed yet
516        let signed = AASigned::new_unhashed(tx.clone(), sig.clone());
517
518        // First call computes hash
519        let hash1 = *signed.hash();
520        // Second call returns cached hash (same reference)
521        let hash2 = *signed.hash();
522        assert_eq!(hash1, hash2, "hash should be deterministic");
523        assert_ne!(hash1, B256::ZERO);
524
525        // new_unchecked: hash provided directly
526        let known_hash = B256::random();
527        let signed_unchecked = AASigned::new_unchecked(tx.clone(), sig.clone(), known_hash);
528        assert_eq!(
529            *signed_unchecked.hash(),
530            known_hash,
531            "new_unchecked should use provided hash"
532        );
533
534        // into_parts returns the hash
535        let signed_for_parts = AASigned::new_unhashed(tx.clone(), sig.clone());
536        let (returned_tx, returned_sig, returned_hash) = signed_for_parts.into_parts();
537        assert_eq!(returned_tx, tx);
538        assert_eq!(returned_sig, sig);
539        assert_eq!(returned_hash, hash1);
540    }
541
542    #[test]
543    fn test_rlp_encode_decode_roundtrip() {
544        use alloy_eips::eip2718::Encodable2718;
545
546        let tx = make_tx();
547        let sig =
548            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
549        let signed = AASigned::new_unhashed(tx, sig);
550
551        // Encode
552        let mut buf = Vec::new();
553        signed.rlp_encode(&mut buf);
554
555        // Decode
556        let decoded = AASigned::rlp_decode(&mut buf.as_slice()).unwrap();
557        assert_eq!(decoded.tx(), signed.tx());
558        assert_eq!(decoded.signature(), signed.signature());
559
560        // EIP-2718 encode/decode
561        let mut eip_buf = Vec::new();
562        signed.eip2718_encode(&mut eip_buf);
563        assert_eq!(eip_buf[0], TEMPO_TX_TYPE_ID);
564
565        let decoded_2718 =
566            AASigned::typed_decode(TEMPO_TX_TYPE_ID, &mut eip_buf[1..].as_ref()).unwrap();
567        assert_eq!(decoded_2718.tx(), signed.tx());
568
569        // trie_hash equals hash
570        assert_eq!(signed.trie_hash(), *signed.hash());
571
572        // fallback_decode returns error (Tempo txs must be typed)
573        let fallback_result = AASigned::fallback_decode(&mut [].as_ref());
574        assert!(fallback_result.is_err());
575
576        // encode_2718_len matches actual encoded length
577        assert_eq!(signed.encode_2718_len(), eip_buf.len());
578    }
579
580    #[test]
581    fn test_rlp_decode_error_paths() {
582        // Empty buffer
583        let result = AASigned::rlp_decode(&mut [].as_ref());
584        assert!(result.is_err());
585
586        // Not a list (string header)
587        let result = AASigned::rlp_decode(&mut [0x80].as_ref());
588        assert!(result.is_err());
589
590        // Payload length exceeds buffer
591        let result = AASigned::rlp_decode(&mut [0xc1, 0x00].as_ref()); // list of 1 byte but only 0 available
592        assert!(result.is_err());
593
594        // Wrong type for typed_decode
595        let result = AASigned::typed_decode(0x00, &mut [].as_ref());
596        assert!(result.is_err());
597    }
598
599    #[test]
600    fn test_expiring_nonce_hash_invariant_to_fee_payer() {
601        let sender = Address::repeat_byte(0x01);
602
603        let make_sponsored_tx = |fee_payer_sig: Signature| -> TempoTransaction {
604            TempoTransaction {
605                chain_id: 1,
606                gas_limit: 1_000_000,
607                nonce_key: U256::MAX, // TEMPO_EXPIRING_NONCE_KEY
608                nonce: 0,
609                fee_token: Some(Address::repeat_byte(0xFE)),
610                fee_payer_signature: Some(fee_payer_sig),
611                valid_before: Some(100),
612                calls: vec![Call {
613                    to: TxKind::Call(Address::repeat_byte(0x42)),
614                    value: U256::ZERO,
615                    input: Bytes::new(),
616                }],
617                ..Default::default()
618            }
619        };
620
621        let sig =
622            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
623
624        // Two txs identical except for fee_payer_signature
625        let tx1 = make_sponsored_tx(Signature::new(U256::from(1), U256::from(2), false));
626        let tx2 = make_sponsored_tx(Signature::new(U256::from(3), U256::from(4), true));
627
628        let signed1 = AASigned::new_unhashed(tx1, sig.clone());
629        let signed2 = AASigned::new_unhashed(tx2, sig);
630
631        // tx_hash MUST differ (fee_payer_signature is part of the envelope)
632        assert_ne!(signed1.hash(), signed2.hash(), "tx hashes must differ");
633
634        // expiring_nonce_hash MUST be identical (invariant to fee payer)
635        let hash1 = signed1.expiring_nonce_hash(sender);
636        let hash2 = signed2.expiring_nonce_hash(sender);
637        assert_eq!(
638            hash1, hash2,
639            "expiring_nonce_hash must be invariant to fee payer signature changes"
640        );
641        assert_ne!(hash1, B256::ZERO);
642    }
643
644    #[test]
645    fn test_expiring_nonce_hash_unique_per_sender() {
646        let tx = TempoTransaction {
647            chain_id: 1,
648            gas_limit: 1_000_000,
649            nonce_key: U256::MAX,
650            nonce: 0,
651            valid_before: Some(100),
652            calls: vec![Call {
653                to: TxKind::Call(Address::repeat_byte(0x42)),
654                value: U256::ZERO,
655                input: Bytes::new(),
656            }],
657            ..Default::default()
658        };
659        let sig =
660            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
661        let signed = AASigned::new_unhashed(tx, sig);
662
663        let sender_a = Address::repeat_byte(0x01);
664        let sender_b = Address::repeat_byte(0x02);
665
666        assert_ne!(
667            signed.expiring_nonce_hash(sender_a),
668            signed.expiring_nonce_hash(sender_b),
669            "different senders must produce different expiring_nonce_hash"
670        );
671    }
672
673    #[test]
674    fn test_expiring_nonce_hash_deterministic() {
675        let tx = make_tx();
676        let sig =
677            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
678        let signed = AASigned::new_unhashed(tx, sig);
679        let sender = Address::repeat_byte(0xAB);
680
681        let h1 = signed.expiring_nonce_hash(sender);
682        let h2 = signed.expiring_nonce_hash(sender);
683        assert_eq!(h1, h2, "expiring_nonce_hash must be deterministic");
684    }
685
686    #[test]
687    fn test_recover_signer() {
688        let (signing_key, expected_address) = generate_secp256k1_keypair();
689
690        let tx = make_tx();
691
692        // Create signed transaction with placeholder sig to get sig_hash
693        let placeholder =
694            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
695        let temp_signed = AASigned::new_unhashed(tx.clone(), placeholder);
696        let sig_hash = temp_signed.signature_hash();
697
698        // Sign the correct hash
699        let signature = sign_hash(&signing_key, &sig_hash);
700        let signed = AASigned::new_unhashed(tx.clone(), signature);
701
702        // Recovery should succeed with correct address
703        let recovered = signed.recover_signer().unwrap();
704        assert_eq!(recovered, expected_address);
705
706        // recover_signer_unchecked should give same result
707        let recovered_unchecked = signed.recover_signer_unchecked().unwrap();
708        assert_eq!(recovered_unchecked, expected_address);
709
710        // Wrong signature yields wrong address
711        let wrong_sig = sign_hash(&signing_key, &B256::random());
712        let bad_signed = AASigned::new_unhashed(tx, wrong_sig);
713        let bad_recovered = bad_signed.recover_signer().unwrap();
714        assert_ne!(bad_recovered, expected_address);
715    }
716}