Skip to main content

tempo_alloy/rpc/
compat.rs

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                // Create an empty signature for the transaction.
27                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            // Try to recover the fee payer address from the signature.
105            // If recovery fails (e.g. dummy signature during gas estimation / fill),
106            // keep it unresolved so estimation/fill can continue with sender-paid semantics.
107            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                // Create mock signature for gas estimation
146                // If key_type is not provided, default to secp256k1
147                // For Keychain signatures, use the caller's address as the root key address
148                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                    // Use a zero sentinel for expiring nonce hash during simulation.
181                    // The real hash is keccak256(encode_for_signing || sender), but we
182                    // don't have a signed transaction here. B256::ZERO lets the handler's
183                    // replay check proceed (it runs against ephemeral simulation state
184                    // that is discarded). Gas accuracy is unaffected — the 13k
185                    // EXPIRING_NONCE_GAS is charged based on nonce_key, not the hash value.
186                    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
200/// Creates a mock AA signature for gas estimation based on key type hints
201///
202/// - `key_type`: The primitive signature type (secp256k1, P256, WebAuthn)
203/// - `key_data`: Type-specific data (e.g., WebAuthn size)
204/// - `key_id`: If Some, wraps the signature in a Keychain wrapper (+3,000 gas for key validation)
205/// - `caller_addr`: The transaction caller address (used as root key address for Keychain)
206/// - `is_t1c`: Whether T1C is active — determines keychain signature version (V1 pre-T1C, V2 post-T1C)
207fn 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        // For Keychain signatures, the root_key_address is the caller (account owner).
220        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
231/// Creates a mock primitive signature for gas estimation
232fn 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            // Create a dummy secp256k1 signature (65 bytes)
243            PrimitiveSignature::Secp256k1(Signature::new(
244                alloy_primitives::U256::ZERO,
245                alloy_primitives::U256::ZERO,
246                false,
247            ))
248        }
249        SignatureType::P256 => {
250            // Create a dummy P256 signature
251            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            // Create a dummy WebAuthn signature with the specified size
261            // key_data contains the total size of webauthn_data (excluding 128 bytes for public keys)
262            // Default: 800 bytes if no key_data provided
263
264            // Base clientDataJSON template (50 bytes): {"type":"webauthn.get","challenge":"","origin":""}
265            // Authenticator data (37 bytes): 32 rpIdHash + 1 flags + 4 signCount
266            // Minimum total: 87 bytes
267            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(); // 87 bytes
270            const DEFAULT_WEBAUTHN_SIZE: usize = 800; // Default when no key_data provided
271            const MAX_WEBAUTHN_SIZE: usize = 8192; // Maximum realistic WebAuthn signature size
272
273            // Parse size from key_data, or use default
274            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, // Fallback default
280                }
281            } else {
282                DEFAULT_WEBAUTHN_SIZE // Default size when no key_data provided
283            };
284
285            // Clamp size to safe bounds to prevent DoS via unbounded allocation
286            let size = size.clamp(MIN_WEBAUTHN_SIZE, MAX_WEBAUTHN_SIZE);
287
288            // Construct authenticatorData (37 bytes)
289            let mut webauthn_data = vec![0u8; AUTH_DATA_SIZE];
290            webauthn_data[32] = 0x01; // UP flag set
291
292            // Construct clientDataJSON with padding in origin field if needed
293            let additional_bytes = size - MIN_WEBAUTHN_SIZE;
294            let client_json = if additional_bytes > 0 {
295                // Add padding bytes to origin field
296                // {"type":"webauthn.get","challenge":"","origin":"XXXXX"}
297                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        // Attempt to create a signature with u32::MAX size (would be ~4GB without fix)
394        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        // Extract webauthn_data and verify it's clamped to MAX_WEBAUTHN_SIZE (8192)
399        let PrimitiveSignature::WebAuthn(webauthn_sig) = sig else {
400            panic!("Expected WebAuthn signature");
401        };
402
403        // The webauthn_data should be at most MAX_WEBAUTHN_SIZE bytes
404        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        // Attempt to create a signature with size 0
414        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        // Should be at least MIN_WEBAUTHN_SIZE (87 bytes)
422        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        // No key_data should use default size (800)
432        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        // Default is 800 bytes
439        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        // Build a TempoTransaction so we can compute fee_payer_signature_hash
449        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        // Request with ONLY fee_payer_signature as the Tempo-specific field
473        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}