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