tempo_primitives/transaction/
tt_signed.rs1use 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#[derive(Clone, Debug)]
30pub struct AASigned {
31 tx: TempoTransaction,
33 signature: TempoSignature,
35 #[doc(alias = "tx_hash", alias = "transaction_hash")]
37 hash: OnceLock<B256>,
38}
39
40impl AASigned {
41 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 pub const fn new_unhashed(tx: TempoTransaction, signature: TempoSignature) -> Self {
57 Self {
58 tx,
59 signature,
60 hash: OnceLock::new(),
61 }
62 }
63
64 #[doc(alias = "transaction")]
66 pub const fn tx(&self) -> &TempoTransaction {
67 &self.tx
68 }
69
70 pub const fn tx_mut(&mut self) -> &mut TempoTransaction {
72 &mut self.tx
73 }
74
75 pub const fn signature(&self) -> &TempoSignature {
77 &self.signature
78 }
79
80 pub fn strip_signature(self) -> TempoTransaction {
82 self.tx
83 }
84
85 #[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 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 pub fn signature_hash(&self) -> B256 {
101 self.tx.signature_hash()
102 }
103
104 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 #[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 pub fn rlp_encode(&self, out: &mut dyn BufMut) {
136 self.rlp_header().encode(out);
138
139 self.tx.rlp_encode_fields_default(out);
141
142 self.signature.encode(out);
144 }
145
146 pub fn into_parts(self) -> (TempoTransaction, TempoSignature, B256) {
148 let hash = *self.hash();
149 (self.tx, self.signature, hash)
150 }
151
152 fn rlp_encoded_length(&self) -> usize {
154 self.rlp_header().length_with_payload()
155 }
156
157 fn eip2718_encoded_length(&self) -> usize {
159 1 + self.rlp_encoded_length()
160 }
161
162 pub fn eip2718_encode(&self, out: &mut dyn BufMut) {
164 out.put_u8(TEMPO_TX_TYPE_ID);
166 self.rlp_encode(out);
168 }
169
170 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 let tx = TempoTransaction::rlp_decode_fields(buf)?;
184
185 let sig_bytes: Bytes = Decodable::decode(buf)?;
187
188 let consumed = remaining - buf.len();
190 if consumed != header.payload_length {
191 return Err(alloy_rlp::Error::UnexpectedLength);
192 }
193
194 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 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 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 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 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 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 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 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 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 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 let signed = AASigned::new_unhashed(tx.clone(), sig.clone());
510
511 let hash1 = *signed.hash();
513 let hash2 = *signed.hash();
515 assert_eq!(hash1, hash2, "hash should be deterministic");
516 assert_ne!(hash1, B256::ZERO);
517
518 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 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 let mut buf = Vec::new();
546 signed.rlp_encode(&mut buf);
547
548 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 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 assert_eq!(signed.trie_hash(), *signed.hash());
564
565 let fallback_result = AASigned::fallback_decode(&mut [].as_ref());
567 assert!(fallback_result.is_err());
568
569 assert_eq!(signed.encode_2718_len(), eip_buf.len());
571 }
572
573 #[test]
574 fn test_rlp_decode_error_paths() {
575 let result = AASigned::rlp_decode(&mut [].as_ref());
577 assert!(result.is_err());
578
579 let result = AASigned::rlp_decode(&mut [0x80].as_ref());
581 assert!(result.is_err());
582
583 let result = AASigned::rlp_decode(&mut [0xc1, 0x00].as_ref()); assert!(result.is_err());
586
587 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, 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 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 assert_ne!(signed1.hash(), signed2.hash(), "tx hashes must differ");
626
627 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 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 let signature = sign_hash(&signing_key, &sig_hash);
693 let signed = AASigned::new_unhashed(tx.clone(), signature);
694
695 let recovered = signed.recover_signer().unwrap();
697 assert_eq!(recovered, expected_address);
698
699 let recovered_unchecked = signed.recover_signer_unchecked().unwrap();
701 assert_eq!(recovered_unchecked, expected_address);
702
703 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}