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
330impl alloy_consensus::transaction::SignerRecoverable for AASigned {
331    fn recover_signer(
332        &self,
333    ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
334        let sig_hash = self.signature_hash();
335        self.signature.recover_signer(&sig_hash)
336    }
337
338    fn recover_signer_unchecked(
339        &self,
340    ) -> Result<alloy_primitives::Address, alloy_consensus::crypto::RecoveryError> {
341        // For Tempo transactions, verified and unverified recovery are the same
342        // since signature verification happens during recover_signer
343        self.recover_signer()
344    }
345}
346
347impl Encodable2718 for AASigned {
348    fn encode_2718_len(&self) -> usize {
349        self.eip2718_encoded_length()
350    }
351
352    fn encode_2718(&self, out: &mut dyn alloy_rlp::BufMut) {
353        self.eip2718_encode(out)
354    }
355
356    fn trie_hash(&self) -> B256 {
357        *self.hash()
358    }
359}
360
361impl Decodable2718 for AASigned {
362    fn typed_decode(ty: u8, buf: &mut &[u8]) -> Eip2718Result<Self> {
363        if ty != TEMPO_TX_TYPE_ID {
364            return Err(Eip2718Error::UnexpectedType(ty));
365        }
366        Self::rlp_decode(buf).map_err(Into::into)
367    }
368
369    fn fallback_decode(_: &mut &[u8]) -> Eip2718Result<Self> {
370        Err(Eip2718Error::UnexpectedType(0))
371    }
372}
373
374#[cfg(any(test, feature = "arbitrary"))]
375impl<'a> arbitrary::Arbitrary<'a> for AASigned {
376    fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result<Self> {
377        let tx = TempoTransaction::arbitrary(u)?;
378        let signature = TempoSignature::arbitrary(u)?;
379        Ok(Self::new_unhashed(tx, signature))
380    }
381}
382
383#[cfg(feature = "serde")]
384mod serde_impl {
385    use super::*;
386    use alloc::borrow::Cow;
387    use serde::{Deserialize, Deserializer, Serialize, Serializer};
388
389    #[derive(Serialize, Deserialize)]
390    struct AASignedHelper<'a> {
391        #[serde(flatten)]
392        tx: Cow<'a, TempoTransaction>,
393        signature: Cow<'a, TempoSignature>,
394        hash: Cow<'a, B256>,
395    }
396
397    impl Serialize for super::AASigned {
398        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
399        where
400            S: Serializer,
401        {
402            if let TempoSignature::Keychain(keychain_sig) = &self.signature {
403                // Initialize the `key_id` field for keychain signatures so that it's serialized.
404                let _ = keychain_sig.key_id(&self.signature_hash());
405            }
406            AASignedHelper {
407                tx: Cow::Borrowed(&self.tx),
408                signature: Cow::Borrowed(&self.signature),
409                hash: Cow::Borrowed(self.hash()),
410            }
411            .serialize(serializer)
412        }
413    }
414
415    impl<'de> Deserialize<'de> for super::AASigned {
416        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
417        where
418            D: Deserializer<'de>,
419        {
420            AASignedHelper::deserialize(deserializer).map(|value| {
421                Self::new_unchecked(
422                    value.tx.into_owned(),
423                    value.signature.into_owned(),
424                    value.hash.into_owned(),
425                )
426            })
427        }
428    }
429
430    #[cfg(test)]
431    mod tests {
432        use crate::transaction::{
433            tempo_transaction::{Call, TempoTransaction},
434            tt_signature::{PrimitiveSignature, TempoSignature},
435        };
436        use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
437
438        #[test]
439        fn test_serde_output() {
440            // Create a simple Tempo transaction
441            let tx = TempoTransaction {
442                chain_id: 1337,
443                fee_token: None,
444                max_priority_fee_per_gas: 1000000000,
445                max_fee_per_gas: 2000000000,
446                gas_limit: 21000,
447                calls: vec![Call {
448                    to: TxKind::Call(Address::repeat_byte(0x42)),
449                    value: U256::from(1000),
450                    input: Bytes::from(vec![1, 2, 3, 4]),
451                }],
452                nonce_key: U256::ZERO,
453                nonce: 5,
454                ..Default::default()
455            };
456
457            // Create a secp256k1 signature
458            let signature = TempoSignature::Primitive(PrimitiveSignature::Secp256k1(
459                Signature::test_signature(),
460            ));
461
462            let aa_signed = super::super::AASigned::new_unhashed(tx, signature);
463
464            // Serialize to JSON
465            let json = serde_json::to_string_pretty(&aa_signed).unwrap();
466
467            println!("\n=== AASigned JSON Output ===");
468            println!("{json}");
469            println!("============================\n");
470
471            // Also test deserialization round-trip
472            let deserialized: super::super::AASigned = serde_json::from_str(&json).unwrap();
473            assert_eq!(aa_signed.tx(), deserialized.tx());
474        }
475    }
476}
477
478#[cfg(test)]
479mod tests {
480    use super::*;
481    use crate::transaction::{
482        tempo_transaction::Call,
483        tt_authorization::tests::{generate_secp256k1_keypair, sign_hash},
484        tt_signature::PrimitiveSignature,
485    };
486    use alloy_consensus::transaction::SignerRecoverable;
487    use alloy_primitives::{Address, Bytes, Signature, TxKind, U256};
488
489    fn make_tx() -> TempoTransaction {
490        TempoTransaction {
491            chain_id: 1,
492            gas_limit: 21000,
493            calls: vec![Call {
494                to: TxKind::Call(Address::repeat_byte(0x42)),
495                value: U256::ZERO,
496                input: Bytes::new(),
497            }],
498            ..Default::default()
499        }
500    }
501
502    #[test]
503    fn test_hash_and_transaction_trait() {
504        let tx = make_tx();
505        let sig =
506            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
507
508        // new_unhashed: hash not computed yet
509        let signed = AASigned::new_unhashed(tx.clone(), sig.clone());
510
511        // First call computes hash
512        let hash1 = *signed.hash();
513        // Second call returns cached hash (same reference)
514        let hash2 = *signed.hash();
515        assert_eq!(hash1, hash2, "hash should be deterministic");
516        assert_ne!(hash1, B256::ZERO);
517
518        // new_unchecked: hash provided directly
519        let known_hash = B256::random();
520        let signed_unchecked = AASigned::new_unchecked(tx.clone(), sig.clone(), known_hash);
521        assert_eq!(
522            *signed_unchecked.hash(),
523            known_hash,
524            "new_unchecked should use provided hash"
525        );
526
527        // into_parts returns the hash
528        let signed_for_parts = AASigned::new_unhashed(tx.clone(), sig.clone());
529        let (returned_tx, returned_sig, returned_hash) = signed_for_parts.into_parts();
530        assert_eq!(returned_tx, tx);
531        assert_eq!(returned_sig, sig);
532        assert_eq!(returned_hash, hash1);
533    }
534
535    #[test]
536    fn test_rlp_encode_decode_roundtrip() {
537        use alloy_eips::eip2718::Encodable2718;
538
539        let tx = make_tx();
540        let sig =
541            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
542        let signed = AASigned::new_unhashed(tx, sig);
543
544        // Encode
545        let mut buf = Vec::new();
546        signed.rlp_encode(&mut buf);
547
548        // Decode
549        let decoded = AASigned::rlp_decode(&mut buf.as_slice()).unwrap();
550        assert_eq!(decoded.tx(), signed.tx());
551        assert_eq!(decoded.signature(), signed.signature());
552
553        // EIP-2718 encode/decode
554        let mut eip_buf = Vec::new();
555        signed.eip2718_encode(&mut eip_buf);
556        assert_eq!(eip_buf[0], TEMPO_TX_TYPE_ID);
557
558        let decoded_2718 =
559            AASigned::typed_decode(TEMPO_TX_TYPE_ID, &mut eip_buf[1..].as_ref()).unwrap();
560        assert_eq!(decoded_2718.tx(), signed.tx());
561
562        // trie_hash equals hash
563        assert_eq!(signed.trie_hash(), *signed.hash());
564
565        // fallback_decode returns error (Tempo txs must be typed)
566        let fallback_result = AASigned::fallback_decode(&mut [].as_ref());
567        assert!(fallback_result.is_err());
568
569        // encode_2718_len matches actual encoded length
570        assert_eq!(signed.encode_2718_len(), eip_buf.len());
571    }
572
573    #[test]
574    fn test_rlp_decode_error_paths() {
575        // Empty buffer
576        let result = AASigned::rlp_decode(&mut [].as_ref());
577        assert!(result.is_err());
578
579        // Not a list (string header)
580        let result = AASigned::rlp_decode(&mut [0x80].as_ref());
581        assert!(result.is_err());
582
583        // Payload length exceeds buffer
584        let result = AASigned::rlp_decode(&mut [0xc1, 0x00].as_ref()); // list of 1 byte but only 0 available
585        assert!(result.is_err());
586
587        // Wrong type for typed_decode
588        let result = AASigned::typed_decode(0x00, &mut [].as_ref());
589        assert!(result.is_err());
590    }
591
592    #[test]
593    fn test_expiring_nonce_hash_invariant_to_fee_payer() {
594        let sender = Address::repeat_byte(0x01);
595
596        let make_sponsored_tx = |fee_payer_sig: Signature| -> TempoTransaction {
597            TempoTransaction {
598                chain_id: 1,
599                gas_limit: 1_000_000,
600                nonce_key: U256::MAX, // TEMPO_EXPIRING_NONCE_KEY
601                nonce: 0,
602                fee_token: Some(Address::repeat_byte(0xFE)),
603                fee_payer_signature: Some(fee_payer_sig),
604                valid_before: Some(core::num::NonZeroU64::new(100).unwrap()),
605                calls: vec![Call {
606                    to: TxKind::Call(Address::repeat_byte(0x42)),
607                    value: U256::ZERO,
608                    input: Bytes::new(),
609                }],
610                ..Default::default()
611            }
612        };
613
614        let sig =
615            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
616
617        // Two txs identical except for fee_payer_signature
618        let tx1 = make_sponsored_tx(Signature::new(U256::from(1), U256::from(2), false));
619        let tx2 = make_sponsored_tx(Signature::new(U256::from(3), U256::from(4), true));
620
621        let signed1 = AASigned::new_unhashed(tx1, sig.clone());
622        let signed2 = AASigned::new_unhashed(tx2, sig);
623
624        // tx_hash MUST differ (fee_payer_signature is part of the envelope)
625        assert_ne!(signed1.hash(), signed2.hash(), "tx hashes must differ");
626
627        // expiring_nonce_hash MUST be identical (invariant to fee payer)
628        let hash1 = signed1.expiring_nonce_hash(sender);
629        let hash2 = signed2.expiring_nonce_hash(sender);
630        assert_eq!(
631            hash1, hash2,
632            "expiring_nonce_hash must be invariant to fee payer signature changes"
633        );
634        assert_ne!(hash1, B256::ZERO);
635    }
636
637    #[test]
638    fn test_expiring_nonce_hash_unique_per_sender() {
639        let tx = TempoTransaction {
640            chain_id: 1,
641            gas_limit: 1_000_000,
642            nonce_key: U256::MAX,
643            nonce: 0,
644            valid_before: Some(core::num::NonZeroU64::new(100).unwrap()),
645            calls: vec![Call {
646                to: TxKind::Call(Address::repeat_byte(0x42)),
647                value: U256::ZERO,
648                input: Bytes::new(),
649            }],
650            ..Default::default()
651        };
652        let sig =
653            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
654        let signed = AASigned::new_unhashed(tx, sig);
655
656        let sender_a = Address::repeat_byte(0x01);
657        let sender_b = Address::repeat_byte(0x02);
658
659        assert_ne!(
660            signed.expiring_nonce_hash(sender_a),
661            signed.expiring_nonce_hash(sender_b),
662            "different senders must produce different expiring_nonce_hash"
663        );
664    }
665
666    #[test]
667    fn test_expiring_nonce_hash_deterministic() {
668        let tx = make_tx();
669        let sig =
670            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
671        let signed = AASigned::new_unhashed(tx, sig);
672        let sender = Address::repeat_byte(0xAB);
673
674        let h1 = signed.expiring_nonce_hash(sender);
675        let h2 = signed.expiring_nonce_hash(sender);
676        assert_eq!(h1, h2, "expiring_nonce_hash must be deterministic");
677    }
678
679    #[test]
680    fn test_recover_signer() {
681        let (signing_key, expected_address) = generate_secp256k1_keypair();
682
683        let tx = make_tx();
684
685        // Create signed transaction with placeholder sig to get sig_hash
686        let placeholder =
687            TempoSignature::Primitive(PrimitiveSignature::Secp256k1(Signature::test_signature()));
688        let temp_signed = AASigned::new_unhashed(tx.clone(), placeholder);
689        let sig_hash = temp_signed.signature_hash();
690
691        // Sign the correct hash
692        let signature = sign_hash(&signing_key, &sig_hash);
693        let signed = AASigned::new_unhashed(tx.clone(), signature);
694
695        // Recovery should succeed with correct address
696        let recovered = signed.recover_signer().unwrap();
697        assert_eq!(recovered, expected_address);
698
699        // recover_signer_unchecked should give same result
700        let recovered_unchecked = signed.recover_signer_unchecked().unwrap();
701        assert_eq!(recovered_unchecked, expected_address);
702
703        // Wrong signature yields wrong address
704        let wrong_sig = sign_hash(&signing_key, &B256::random());
705        let bad_signed = AASigned::new_unhashed(tx, wrong_sig);
706        let bad_recovered = bad_signed.recover_signer().unwrap();
707        assert_ne!(bad_recovered, expected_address);
708    }
709}