1use crate::rpc::{TempoHeaderResponse, TempoTransactionRequest};
2use alloy_consensus::{EthereumTxEnvelope, TxEip4844, error::ValueError};
3use alloy_network::{TransactionBuilder, TxSigner};
4use alloy_primitives::{Address, B256, Bytes, Signature};
5use reth_evm::EvmEnv;
6use reth_primitives_traits::SealedHeader;
7use reth_rpc_convert::{
8 SignTxRequestError, SignableTxRequest, TryIntoSimTx, TryIntoTxEnv,
9 transaction::FromConsensusHeader,
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,
188 valid_after,
189 subblock_transaction: false,
190 override_key_id: key_id,
191 }))
192 } else {
193 None
194 },
195 inner: inner.try_into_tx_env(evm_env)?,
196 })
197 }
198}
199
200fn create_mock_tempo_sig(
208 key_type: &SignatureType,
209 key_data: Option<&Bytes>,
210 key_id: Option<Address>,
211 caller_addr: alloy_primitives::Address,
212 is_t1c: bool,
213) -> TempoSignature {
214 use tempo_primitives::transaction::tt_signature::{KeychainSignature, TempoSignature};
215
216 let inner_sig = create_mock_primitive_signature(key_type, key_data.cloned());
217
218 if key_id.is_some() {
219 let keychain_sig = if is_t1c {
221 KeychainSignature::new(caller_addr, inner_sig)
222 } else {
223 KeychainSignature::new_v1(caller_addr, inner_sig)
224 };
225 TempoSignature::Keychain(keychain_sig)
226 } else {
227 TempoSignature::Primitive(inner_sig)
228 }
229}
230
231fn create_mock_primitive_signature(
233 sig_type: &SignatureType,
234 key_data: Option<Bytes>,
235) -> tempo_primitives::transaction::tt_signature::PrimitiveSignature {
236 use tempo_primitives::transaction::tt_signature::{
237 P256SignatureWithPreHash, PrimitiveSignature, WebAuthnSignature,
238 };
239
240 match sig_type {
241 SignatureType::Secp256k1 => {
242 PrimitiveSignature::Secp256k1(Signature::new(
244 alloy_primitives::U256::ZERO,
245 alloy_primitives::U256::ZERO,
246 false,
247 ))
248 }
249 SignatureType::P256 => {
250 PrimitiveSignature::P256(P256SignatureWithPreHash {
252 r: alloy_primitives::B256::ZERO,
253 s: alloy_primitives::B256::ZERO,
254 pub_key_x: alloy_primitives::B256::ZERO,
255 pub_key_y: alloy_primitives::B256::ZERO,
256 pre_hash: false,
257 })
258 }
259 SignatureType::WebAuthn => {
260 const BASE_CLIENT_JSON: &str = r#"{"type":"webauthn.get","challenge":"","origin":""}"#;
268 const AUTH_DATA_SIZE: usize = 37;
269 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() {
275 match data.len() {
276 1 => data[0] as usize,
277 2 => u16::from_be_bytes([data[0], data[1]]) as usize,
278 4 => u32::from_be_bytes([data[0], data[1], data[2], data[3]]) as usize,
279 _ => DEFAULT_WEBAUTHN_SIZE, }
281 } else {
282 DEFAULT_WEBAUTHN_SIZE };
284
285 let size = size.clamp(MIN_WEBAUTHN_SIZE, MAX_WEBAUTHN_SIZE);
287
288 let mut webauthn_data = vec![0u8; AUTH_DATA_SIZE];
290 webauthn_data[32] = 0x01; let additional_bytes = size - MIN_WEBAUTHN_SIZE;
294 let client_json = if additional_bytes > 0 {
295 let padding = "x".repeat(additional_bytes);
298 format!(r#"{{"type":"webauthn.get","challenge":"","origin":"{padding}"}}"#,)
299 } else {
300 BASE_CLIENT_JSON.to_string()
301 };
302
303 webauthn_data.extend_from_slice(client_json.as_bytes());
304 let webauthn_data = Bytes::from(webauthn_data);
305
306 PrimitiveSignature::WebAuthn(WebAuthnSignature {
307 webauthn_data,
308 r: alloy_primitives::B256::ZERO,
309 s: alloy_primitives::B256::ZERO,
310 pub_key_x: alloy_primitives::B256::ZERO,
311 pub_key_y: alloy_primitives::B256::ZERO,
312 })
313 }
314 }
315}
316
317impl SignableTxRequest<TempoTxEnvelope> for TempoTransactionRequest {
318 async fn try_build_and_sign(
319 self,
320 signer: impl TxSigner<Signature> + Send,
321 ) -> Result<TempoTxEnvelope, SignTxRequestError> {
322 if self.output_tx_type() == TempoTxType::AA {
323 let mut tx = self
324 .build_aa()
325 .map_err(|_| SignTxRequestError::InvalidTransactionRequest)?;
326 let signature = signer.sign_transaction(&mut tx).await?;
327 Ok(tx.into_signed(signature.into()).into())
328 } else {
329 SignableTxRequest::<TempoTxEnvelope>::try_build_and_sign(self.inner, signer).await
330 }
331 }
332}
333
334impl FromConsensusHeader<TempoHeader> for TempoHeaderResponse {
335 fn from_consensus_header(header: SealedHeader<TempoHeader>, block_size: usize) -> Self {
336 Self {
337 timestamp_millis: header.timestamp_millis(),
338 inner: FromConsensusHeader::from_consensus_header(header, block_size),
339 }
340 }
341}
342
343#[cfg(test)]
344mod tests {
345 use super::*;
346 use alloy_primitives::{TxKind, U256, address};
347 use alloy_rpc_types_eth::TransactionRequest;
348 use alloy_signer::SignerSync;
349 use alloy_signer_local::PrivateKeySigner;
350 use reth_rpc_convert::TryIntoTxEnv;
351 use tempo_primitives::{
352 TempoTransaction,
353 transaction::{Call, tt_signature::PrimitiveSignature},
354 };
355
356 #[test]
357 fn test_estimate_gas_when_calls_set() {
358 let existing_call = Call {
359 to: TxKind::Call(address!("0x1111111111111111111111111111111111111111")),
360 value: alloy_primitives::U256::from(1),
361 input: Bytes::from(vec![0xaa]),
362 };
363
364 let req = TempoTransactionRequest {
365 inner: TransactionRequest {
366 to: Some(TxKind::Call(address!(
367 "0x2222222222222222222222222222222222222222"
368 ))),
369 value: Some(alloy_primitives::U256::from(2)),
370 input: alloy_rpc_types_eth::TransactionInput::new(Bytes::from(vec![0xbb])),
371 nonce: Some(0),
372 gas: Some(100_000),
373 max_fee_per_gas: Some(1_000_000_000),
374 max_priority_fee_per_gas: Some(1_000_000),
375 ..Default::default()
376 },
377 calls: vec![existing_call],
378 nonce_key: Some(alloy_primitives::U256::ZERO),
379 ..Default::default()
380 };
381
382 let built_calls = req.clone().build_aa().expect("build_aa").calls;
383
384 let evm_env = EvmEnv::default();
385 let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
386 let estimated_calls = tx_env.tempo_tx_env.expect("tempo_tx_env").aa_calls;
387
388 assert_eq!(estimated_calls, built_calls);
389 }
390
391 #[test]
392 fn test_webauthn_size_clamped_to_max() {
393 let malicious_key_data = Bytes::from(0xFFFFFFFFu32.to_be_bytes().to_vec());
395 let sig =
396 create_mock_primitive_signature(&SignatureType::WebAuthn, Some(malicious_key_data));
397
398 let PrimitiveSignature::WebAuthn(webauthn_sig) = sig else {
400 panic!("Expected WebAuthn signature");
401 };
402
403 assert!(
405 webauthn_sig.webauthn_data.len() <= 8192,
406 "WebAuthn data size {} exceeds maximum 8192",
407 webauthn_sig.webauthn_data.len()
408 );
409 }
410
411 #[test]
412 fn test_webauthn_size_respects_minimum() {
413 let key_data = Bytes::from(vec![0u8]);
415 let sig = create_mock_primitive_signature(&SignatureType::WebAuthn, Some(key_data));
416
417 let PrimitiveSignature::WebAuthn(webauthn_sig) = sig else {
418 panic!("Expected WebAuthn signature");
419 };
420
421 assert!(
423 webauthn_sig.webauthn_data.len() >= 87,
424 "WebAuthn data size {} is below minimum 87",
425 webauthn_sig.webauthn_data.len()
426 );
427 }
428
429 #[test]
430 fn test_webauthn_default_size() {
431 let sig = create_mock_primitive_signature(&SignatureType::WebAuthn, None);
433
434 let PrimitiveSignature::WebAuthn(webauthn_sig) = sig else {
435 panic!("Expected WebAuthn signature");
436 };
437
438 assert_eq!(webauthn_sig.webauthn_data.len(), 800);
440 }
441
442 #[test]
443 fn test_estimate_gas_fee_payer_signature_only_produces_aa_env() {
444 let sponsor = PrivateKeySigner::random();
445 let sender = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
446 let target = address!("0x2222222222222222222222222222222222222222");
447
448 let tx = TempoTransaction {
450 chain_id: 4217,
451 nonce: 0,
452 fee_payer_signature: None,
453 valid_before: None,
454 valid_after: None,
455 gas_limit: 100_000,
456 max_fee_per_gas: 1_000_000_000,
457 max_priority_fee_per_gas: 1_000_000,
458 fee_token: None,
459 access_list: Default::default(),
460 calls: vec![Call {
461 to: target.into(),
462 value: Default::default(),
463 input: Default::default(),
464 }],
465 tempo_authorization_list: vec![],
466 nonce_key: Default::default(),
467 key_authorization: None,
468 };
469 let hash = tx.fee_payer_signature_hash(sender);
470 let fee_payer_sig = sponsor.sign_hash_sync(&hash).expect("sign");
471
472 let req = TempoTransactionRequest {
474 inner: TransactionRequest {
475 from: Some(sender),
476 to: Some(TxKind::Call(target)),
477 nonce: Some(0),
478 gas: Some(100_000),
479 max_fee_per_gas: Some(1_000_000_000),
480 max_priority_fee_per_gas: Some(1_000_000),
481 chain_id: Some(4217),
482 ..Default::default()
483 },
484 fee_payer_signature: Some(fee_payer_sig),
485 ..Default::default()
486 };
487
488 let evm_env = EvmEnv::default();
489 let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
490
491 assert!(
492 tx_env.tempo_tx_env.is_some(),
493 "fee_payer_signature alone must produce an AA tx env"
494 );
495 assert_eq!(
496 tx_env.fee_payer,
497 Some(Some(sponsor.address())),
498 "fee_payer should recover sponsor address"
499 );
500 }
501
502 #[test]
503 fn test_estimate_gas_invalid_fee_payer_signature_keeps_unresolved_fee_payer() {
504 let sender = address!("0xaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa");
505 let target = address!("0x2222222222222222222222222222222222222222");
506
507 let req = TempoTransactionRequest {
508 inner: TransactionRequest {
509 from: Some(sender),
510 to: Some(TxKind::Call(target)),
511 nonce: Some(0),
512 gas: Some(100_000),
513 max_fee_per_gas: Some(1_000_000_000),
514 max_priority_fee_per_gas: Some(1_000_000),
515 chain_id: Some(4217),
516 ..Default::default()
517 },
518 fee_payer_signature: Some(Signature::new(U256::ZERO, U256::ZERO, false)),
519 ..Default::default()
520 };
521
522 let evm_env = EvmEnv::default();
523 let tx_env = req.try_into_tx_env(&evm_env).expect("try_into_tx_env");
524
525 assert!(
526 tx_env.tempo_tx_env.is_some(),
527 "fee_payer_signature alone must produce an AA tx env"
528 );
529 assert_eq!(
530 tx_env.fee_payer,
531 Some(None),
532 "invalid fee_payer_signature should remain unresolved"
533 );
534 }
535
536 #[tokio::test]
537 async fn test_signable_tx_request_preserves_tempo_fields() {
538 let signer = PrivateKeySigner::random();
539
540 let call = Call {
541 to: alloy_primitives::TxKind::Call(address!(
542 "0x1111111111111111111111111111111111111111"
543 )),
544 value: alloy_primitives::U256::from(1),
545 input: Bytes::from(vec![0xaa]),
546 };
547
548 let fee_token = address!("0x20c0000000000000000000000000000000000000");
549 let nonce_key = alloy_primitives::U256::from(42);
550
551 let req = TempoTransactionRequest {
552 inner: TransactionRequest {
553 nonce: Some(0),
554 gas: Some(100_000),
555 max_fee_per_gas: Some(1_000_000_000),
556 max_priority_fee_per_gas: Some(1_000_000),
557 chain_id: Some(4217),
558 ..Default::default()
559 },
560 calls: vec![call.clone()],
561 fee_token: Some(fee_token),
562 nonce_key: Some(nonce_key),
563 ..Default::default()
564 };
565
566 let envelope = SignableTxRequest::<TempoTxEnvelope>::try_build_and_sign(req, &signer)
567 .await
568 .expect("should build and sign");
569
570 match &envelope {
571 TempoTxEnvelope::AA(signed) => {
572 let tx = signed.tx();
573 assert_eq!(tx.fee_token, Some(fee_token), "fee_token must be preserved");
574 assert_eq!(tx.nonce_key, nonce_key, "nonce_key must be preserved");
575 assert_eq!(tx.calls, vec![call], "calls must be preserved");
576 }
577 other => panic!(
578 "Expected AA envelope for request with Tempo fields, got {:?}",
579 other.tx_type()
580 ),
581 }
582 }
583}