1use crate::rpc::{TempoHeaderResponse, TempoTransactionRequest};
2use alloy_consensus::{EthereumTxEnvelope, TxEip4844, error::ValueError};
3use alloy_network::{NetworkTransactionBuilder, TxSigner};
4use alloy_primitives::{Address, B256, Bytes, Signature};
5use core::num::NonZeroU64;
6use reth_evm::EvmEnv;
7use reth_primitives_traits::SealedHeader;
8use reth_rpc_convert::{
9 FromConsensusHeader, SignTxRequestError, SignableTxRequest, TryIntoSimTx, TryIntoTxEnv,
10};
11use reth_rpc_eth_types::EthApiError;
12use tempo_chainspec::hardfork::TempoHardfork;
13use tempo_evm::TempoBlockEnv;
14use tempo_primitives::{
15 SignatureType, TempoHeader, TempoSignature, TempoTxEnvelope, TempoTxType,
16 transaction::{Call, RecoveredTempoAuthorization},
17};
18use tempo_revm::{TempoBatchCallEnv, TempoTxEnv};
19
20impl TryIntoSimTx<TempoTxEnvelope> for TempoTransactionRequest {
21 fn try_into_sim_tx(self) -> Result<TempoTxEnvelope, ValueError<Self>> {
22 match self.output_tx_type() {
23 TempoTxType::AA => {
24 let tx = self.build_aa()?;
25
26 let signature = TempoSignature::default();
28
29 Ok(tx.into_signed(signature).into())
30 }
31 TempoTxType::Legacy
32 | TempoTxType::Eip2930
33 | TempoTxType::Eip1559
34 | TempoTxType::Eip7702 => {
35 let Self {
36 inner,
37 fee_token,
38 nonce_key,
39 calls,
40 key_type,
41 key_data,
42 key_id,
43 tempo_authorization_list,
44 key_authorization,
45 valid_before,
46 valid_after,
47 fee_payer_signature,
48 } = self;
49 let envelope = match TryIntoSimTx::<EthereumTxEnvelope<TxEip4844>>::try_into_sim_tx(
50 inner.clone(),
51 ) {
52 Ok(inner) => inner,
53 Err(e) => {
54 return Err(e.map(|inner| Self {
55 inner,
56 fee_token,
57 nonce_key,
58 calls,
59 key_type,
60 key_data,
61 key_id,
62 tempo_authorization_list,
63 key_authorization,
64 valid_before,
65 valid_after,
66 fee_payer_signature,
67 }));
68 }
69 };
70
71 Ok(envelope.try_into().map_err(
72 |e: ValueError<EthereumTxEnvelope<TxEip4844>>| {
73 e.map(|_inner| Self {
74 inner,
75 fee_token,
76 nonce_key,
77 calls,
78 key_type,
79 key_data,
80 key_id,
81 tempo_authorization_list,
82 key_authorization,
83 valid_before,
84 valid_after,
85 fee_payer_signature,
86 })
87 },
88 )?)
89 }
90 }
91 }
92}
93
94impl TryIntoTxEnv<TempoTxEnv, TempoHardfork, TempoBlockEnv> for TempoTransactionRequest {
95 type Err = EthApiError;
96
97 fn try_into_tx_env(
98 self,
99 evm_env: &EvmEnv<TempoHardfork, TempoBlockEnv>,
100 ) -> Result<TempoTxEnv, Self::Err> {
101 let caller_addr = self.inner.from.unwrap_or_default();
102
103 let fee_payer = if self.fee_payer_signature.is_some() {
104 let recovered = self
108 .clone()
109 .build_aa()
110 .ok()
111 .and_then(|tx| tx.recover_fee_payer(caller_addr).ok());
112 Some(recovered)
113 } else {
114 None
115 };
116
117 let Self {
118 inner,
119 fee_token,
120 calls,
121 key_type,
122 key_data,
123 key_id,
124 tempo_authorization_list,
125 nonce_key,
126 key_authorization,
127 valid_before,
128 valid_after,
129 fee_payer_signature: _,
130 } = self;
131
132 Ok(TempoTxEnv {
133 fee_token,
134 is_system_tx: false,
135 fee_payer,
136 tempo_tx_env: if !calls.is_empty()
137 || !tempo_authorization_list.is_empty()
138 || nonce_key.is_some()
139 || key_authorization.is_some()
140 || key_id.is_some()
141 || fee_payer.is_some()
142 || valid_before.is_some()
143 || valid_after.is_some()
144 {
145 let key_type = key_type.unwrap_or(SignatureType::Secp256k1);
149 let mock_signature = create_mock_tempo_sig(
150 &key_type,
151 key_data.as_ref(),
152 key_id,
153 caller_addr,
154 evm_env.spec_id().is_t1c(),
155 );
156
157 let mut calls = calls;
158 if let Some(to) = &inner.to {
159 calls.push(Call {
160 to: *to,
161 value: inner.value.unwrap_or_default(),
162 input: inner.input.clone().into_input().unwrap_or_default(),
163 });
164 }
165 if calls.is_empty() {
166 return Err(EthApiError::InvalidParams("empty calls list".to_string()));
167 }
168
169 Some(Box::new(TempoBatchCallEnv {
170 aa_calls: calls,
171 signature: mock_signature,
172 tempo_authorization_list: tempo_authorization_list
173 .into_iter()
174 .map(RecoveredTempoAuthorization::new)
175 .collect(),
176 nonce_key: nonce_key.unwrap_or_default(),
177 key_authorization,
178 signature_hash: B256::ZERO,
179 tx_hash: B256::ZERO,
180 expiring_nonce_hash: Some(B256::ZERO),
187 valid_before: valid_before.map(NonZeroU64::get),
188 valid_after: valid_after.map(NonZeroU64::get),
189 subblock_transaction: false,
190 override_key_id: key_id,
191 expiring_nonce_idx: None,
192 }))
193 } else {
194 None
195 },
196 inner: inner.try_into_tx_env(evm_env)?,
197 })
198 }
199}
200
201fn create_mock_tempo_sig(
209 key_type: &SignatureType,
210 key_data: Option<&Bytes>,
211 key_id: Option<Address>,
212 caller_addr: alloy_primitives::Address,
213 is_t1c: bool,
214) -> TempoSignature {
215 use tempo_primitives::transaction::tt_signature::{KeychainSignature, TempoSignature};
216
217 let inner_sig = create_mock_primitive_signature(key_type, key_data.cloned());
218
219 if key_id.is_some() {
220 let keychain_sig = if is_t1c {
222 KeychainSignature::new(caller_addr, inner_sig)
223 } else {
224 KeychainSignature::new_v1(caller_addr, inner_sig)
225 };
226 TempoSignature::Keychain(keychain_sig)
227 } else {
228 TempoSignature::Primitive(inner_sig)
229 }
230}
231
232fn create_mock_primitive_signature(
234 sig_type: &SignatureType,
235 key_data: Option<Bytes>,
236) -> tempo_primitives::transaction::tt_signature::PrimitiveSignature {
237 use tempo_primitives::transaction::tt_signature::{
238 P256SignatureWithPreHash, PrimitiveSignature, WebAuthnSignature,
239 };
240
241 match sig_type {
242 SignatureType::Secp256k1 => {
243 PrimitiveSignature::Secp256k1(Signature::new(
245 alloy_primitives::U256::ZERO,
246 alloy_primitives::U256::ZERO,
247 false,
248 ))
249 }
250 SignatureType::P256 => {
251 PrimitiveSignature::P256(P256SignatureWithPreHash {
253 r: alloy_primitives::B256::ZERO,
254 s: alloy_primitives::B256::ZERO,
255 pub_key_x: alloy_primitives::B256::ZERO,
256 pub_key_y: alloy_primitives::B256::ZERO,
257 pre_hash: false,
258 })
259 }
260 SignatureType::WebAuthn => {
261 const BASE_CLIENT_JSON: &str = r#"{"type":"webauthn.get","challenge":"","origin":""}"#;
269 const AUTH_DATA_SIZE: usize = 37;
270 const MIN_WEBAUTHN_SIZE: usize = AUTH_DATA_SIZE + BASE_CLIENT_JSON.len(); const DEFAULT_WEBAUTHN_SIZE: usize = 800; const MAX_WEBAUTHN_SIZE: usize = 8192; let size = if let Some(data) = key_data.as_ref() {
276 match data.len() {
277 1 => data[0] as usize,
278 2 => u16::from_be_bytes([data[0], data[1]]) as usize,
279 4 => u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize,
280 _ => DEFAULT_WEBAUTHN_SIZE, }
282 } else {
283 DEFAULT_WEBAUTHN_SIZE };
285
286 let size = size.clamp(MIN_WEBAUTHN_SIZE, MAX_WEBAUTHN_SIZE);
288
289 let mut webauthn_data = vec![0u8; AUTH_DATA_SIZE];
291 webauthn_data[32] = 0x01; let additional_bytes = size - MIN_WEBAUTHN_SIZE;
295 let client_json = if additional_bytes > 0 {
296 let padding = "x".repeat(additional_bytes);
299 format!(r#"{{"type":"webauthn.get","challenge":"","origin":"{padding}"}}"#,)
300 } else {
301 BASE_CLIENT_JSON.to_string()
302 };
303
304 webauthn_data.extend_from_slice(client_json.as_bytes());
305 let webauthn_data = Bytes::from(webauthn_data);
306
307 PrimitiveSignature::WebAuthn(WebAuthnSignature {
308 webauthn_data,
309 r: alloy_primitives::B256::ZERO,
310 s: alloy_primitives::B256::ZERO,
311 pub_key_x: alloy_primitives::B256::ZERO,
312 pub_key_y: alloy_primitives::B256::ZERO,
313 })
314 }
315 }
316}
317
318impl SignableTxRequest<TempoTxEnvelope> for TempoTransactionRequest {
319 async fn try_build_and_sign(
320 self,
321 signer: impl TxSigner<Signature> + Send,
322 ) -> Result<TempoTxEnvelope, SignTxRequestError> {
323 if self.output_tx_type() == TempoTxType::AA {
324 let mut tx = self
325 .build_aa()
326 .map_err(|_| SignTxRequestError::InvalidTransactionRequest)?;
327 let signature = signer.sign_transaction(&mut tx).await?;
328 Ok(tx.into_signed(signature.into()).into())
329 } else {
330 SignableTxRequest::<TempoTxEnvelope>::try_build_and_sign(self.inner, signer).await
331 }
332 }
333}
334
335impl FromConsensusHeader<TempoHeader> for TempoHeaderResponse {
336 fn from_consensus_header(header: SealedHeader<TempoHeader>, block_size: usize) -> Self {
337 Self {
338 timestamp_millis: header.timestamp_millis(),
339 inner: FromConsensusHeader::from_consensus_header(header, block_size),
340 }
341 }
342}
343
344#[cfg(test)]
345mod tests {
346 use super::*;
347 use alloy_primitives::{TxKind, U256, address};
348 use alloy_rpc_types_eth::TransactionRequest;
349 use alloy_signer::SignerSync;
350 use alloy_signer_local::PrivateKeySigner;
351 use reth_rpc_convert::TryIntoTxEnv;
352 use tempo_primitives::{
353 TempoTransaction,
354 transaction::{Call, tt_signature::PrimitiveSignature},
355 };
356
357 #[test]
358 fn test_estimate_gas_when_calls_set() {
359 let existing_call = Call {
360 to: TxKind::Call(address!("0x1111111111111111111111111111111111111111")),
361 value: alloy_primitives::U256::from(1),
362 input: Bytes::from(vec![0xaa]),
363 };
364
365 let req = TempoTransactionRequest {
366 inner: TransactionRequest {
367 to: Some(TxKind::Call(address!(
368 "0x2222222222222222222222222222222222222222"
369 ))),
370 value: Some(alloy_primitives::U256::from(2)),
371 input: alloy_rpc_types_eth::TransactionInput::new(Bytes::from(vec![0xbb])),
372 nonce: Some(0),
373 gas: Some(100_000),
374 max_fee_per_gas: Some(1_000_000_000),
375 max_priority_fee_per_gas: Some(1_000_000),
376 ..Default::default()
377 },
378 calls: vec![existing_call],
379 nonce_key: Some(alloy_primitives::U256::ZERO),
380 ..Default::default()
381 };
382
383 let built_calls = req.clone().build_aa().expect("build_aa").calls;
384
385 let evm_env = EvmEnv::default();
386 let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
387 let estimated_calls = tx_env.tempo_tx_env.expect("tempo_tx_env").aa_calls;
388
389 assert_eq!(estimated_calls, built_calls);
390 }
391
392 #[test]
393 fn test_webauthn_size_clamped_to_max() {
394 let malicious_key_data = Bytes::from(0xFFFFFFFFu32.to_be_bytes().to_vec());
396 let sig =
397 create_mock_primitive_signature(&SignatureType::WebAuthn, Some(malicious_key_data));
398
399 let PrimitiveSignature::WebAuthn(webauthn_sig) = sig else {
401 panic!("Expected WebAuthn signature");
402 };
403
404 assert!(
406 webauthn_sig.webauthn_data.len() <= 8192,
407 "WebAuthn data size {} exceeds maximum 8192",
408 webauthn_sig.webauthn_data.len()
409 );
410 }
411
412 #[test]
413 fn test_webauthn_size_respects_minimum() {
414 let key_data = Bytes::from(vec![0u8]);
416 let sig = create_mock_primitive_signature(&SignatureType::WebAuthn, Some(key_data));
417
418 let PrimitiveSignature::WebAuthn(webauthn_sig) = sig else {
419 panic!("Expected WebAuthn signature");
420 };
421
422 assert!(
424 webauthn_sig.webauthn_data.len() >= 87,
425 "WebAuthn data size {} is below minimum 87",
426 webauthn_sig.webauthn_data.len()
427 );
428 }
429
430 #[test]
431 fn test_webauthn_default_size() {
432 let sig = create_mock_primitive_signature(&SignatureType::WebAuthn, None);
434
435 let PrimitiveSignature::WebAuthn(webauthn_sig) = sig else {
436 panic!("Expected WebAuthn signature");
437 };
438
439 assert_eq!(webauthn_sig.webauthn_data.len(), 800);
441 }
442
443 #[test]
444 fn test_estimate_gas_fee_payer_signature_only_produces_aa_env() {
445 let sponsor = PrivateKeySigner::random();
446 let sender = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
447 let target = address!("0x2222222222222222222222222222222222222222");
448
449 let tx = TempoTransaction {
451 chain_id: 4217,
452 nonce: 0,
453 fee_payer_signature: None,
454 valid_before: None,
455 valid_after: None,
456 gas_limit: 100_000,
457 max_fee_per_gas: 1_000_000_000,
458 max_priority_fee_per_gas: 1_000_000,
459 fee_token: None,
460 access_list: Default::default(),
461 calls: vec![Call {
462 to: target.into(),
463 value: Default::default(),
464 input: Default::default(),
465 }],
466 tempo_authorization_list: vec![],
467 nonce_key: Default::default(),
468 key_authorization: None,
469 };
470 let hash = tx.fee_payer_signature_hash(sender);
471 let fee_payer_sig = sponsor.sign_hash_sync(&hash).expect("sign");
472
473 let req = TempoTransactionRequest {
475 inner: TransactionRequest {
476 from: Some(sender),
477 to: Some(TxKind::Call(target)),
478 nonce: Some(0),
479 gas: Some(100_000),
480 max_fee_per_gas: Some(1_000_000_000),
481 max_priority_fee_per_gas: Some(1_000_000),
482 chain_id: Some(4217),
483 ..Default::default()
484 },
485 fee_payer_signature: Some(fee_payer_sig),
486 ..Default::default()
487 };
488
489 let evm_env = EvmEnv::default();
490 let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
491
492 assert!(
493 tx_env.tempo_tx_env.is_some(),
494 "fee_payer_signature alone must produce an AA tx env"
495 );
496 assert_eq!(
497 tx_env.fee_payer,
498 Some(Some(sponsor.address())),
499 "fee_payer should recover sponsor address"
500 );
501 }
502
503 #[test]
504 fn test_aa_roundtrip_via_tx_env() {
505 use alloy_primitives::U256;
506
507 let calls = vec![
508 Call {
509 to: address!("0x1111111111111111111111111111111111111111").into(),
510 value: U256::ZERO,
511 input: Bytes::from(vec![0xaa]),
512 },
513 Call {
514 to: address!("0x2222222222222222222222222222222222222222").into(),
515 value: U256::ZERO,
516 input: Bytes::from(vec![0xbb]),
517 },
518 ];
519
520 let tx = TempoTransaction {
521 chain_id: 4217,
522 nonce: 1,
523 gas_limit: 100_000,
524 max_fee_per_gas: 1_000_000_000,
525 max_priority_fee_per_gas: 1_000_000,
526 calls: calls.clone(),
527 ..Default::default()
528 };
529
530 let req: TempoTransactionRequest = tx.into();
531
532 let evm_env = EvmEnv::default();
533 let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
534 let aa_calls = tx_env.tempo_tx_env.expect("tempo_tx_env").aa_calls;
535
536 assert_eq!(
537 aa_calls, calls,
538 "roundtrip via try_into_tx_env must preserve exact call list"
539 );
540 }
541
542 #[test]
543 fn test_estimate_gas_invalid_fee_payer_signature_keeps_unresolved_fee_payer() {
544 let sender = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
545 let target = address!("0x2222222222222222222222222222222222222222");
546
547 let req = TempoTransactionRequest {
548 inner: TransactionRequest {
549 from: Some(sender),
550 to: Some(TxKind::Call(target)),
551 nonce: Some(0),
552 gas: Some(100_000),
553 max_fee_per_gas: Some(1_000_000_000),
554 max_priority_fee_per_gas: Some(1_000_000),
555 chain_id: Some(4217),
556 ..Default::default()
557 },
558 fee_payer_signature: Some(Signature::new(U256::ZERO, U256::ZERO, false)),
559 ..Default::default()
560 };
561
562 let evm_env = EvmEnv::default();
563 let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
564
565 assert!(
566 tx_env.tempo_tx_env.is_some(),
567 "fee_payer_signature alone must produce an AA tx env"
568 );
569 assert_eq!(
570 tx_env.fee_payer,
571 Some(None),
572 "invalid fee_payer_signature should remain unresolved"
573 );
574 }
575
576 #[tokio::test]
577 async fn test_signable_tx_request_preserves_tempo_fields() {
578 let signer = PrivateKeySigner::random();
579
580 let call = Call {
581 to: alloy_primitives::TxKind::Call(address!(
582 "0x1111111111111111111111111111111111111111"
583 )),
584 value: alloy_primitives::U256::from(1),
585 input: Bytes::from(vec![0xaa]),
586 };
587
588 let fee_token = address!("0x20c0000000000000000000000000000000000000");
589 let nonce_key = alloy_primitives::U256::from(42);
590
591 let req = TempoTransactionRequest {
592 inner: TransactionRequest {
593 nonce: Some(0),
594 gas: Some(100_000),
595 max_fee_per_gas: Some(1_000_000_000),
596 max_priority_fee_per_gas: Some(1_000_000),
597 chain_id: Some(4217),
598 ..Default::default()
599 },
600 calls: vec![call.clone()],
601 fee_token: Some(fee_token),
602 nonce_key: Some(nonce_key),
603 ..Default::default()
604 };
605
606 let envelope = SignableTxRequest::<TempoTxEnvelope>::try_build_and_sign(req, &signer)
607 .await
608 .expect("should build and sign");
609
610 match &envelope {
611 TempoTxEnvelope::AA(signed) => {
612 let tx = signed.tx();
613 assert_eq!(tx.fee_token, Some(fee_token), "fee_token must be preserved");
614 assert_eq!(tx.nonce_key, nonce_key, "nonce_key must be preserved");
615 assert_eq!(tx.calls, vec![call], "calls must be preserved");
616 }
617 other => panic!(
618 "Expected AA envelope for request with Tempo fields, got {:?}",
619 other.tx_type()
620 ),
621 }
622 }
623}