Skip to main content

forest/eth/
transaction.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use super::{derive_eip_155_chain_id, validate_eip155_chain_id};
5use crate::eth::{LEGACY_V_VALUE_27, LEGACY_V_VALUE_28};
6use crate::rpc::eth::ApiEthTx;
7use crate::shim::crypto::Signature;
8use crate::shim::fvm_shared_latest;
9use anyhow::{Context, bail, ensure};
10use bytes::Bytes;
11use cbor4ii::core::{Value, dec::Decode as _, utils::SliceReader};
12use fvm_shared4::METHOD_CONSTRUCTOR;
13use num::{BigInt, Signed as _, bigint::Sign};
14use num_derive::FromPrimitive;
15use num_traits::cast::ToPrimitive;
16use rlp::Rlp;
17use spire_enum::prelude::delegated_enum;
18
19use crate::{
20    message::{MessageRead as _, SignedMessage},
21    rpc::eth::types::EthAddress,
22    shim::{address::Address, crypto::SignatureType, message::Message, version::NetworkVersion},
23};
24
25use super::{
26    EIP_1559_TX_TYPE, EIP_2930_TX_TYPE, EthChainId,
27    eip_155_transaction::{
28        EIP_155_SIG_PREFIX, EthLegacyEip155TxArgs, EthLegacyEip155TxArgsBuilder,
29        calc_valid_eip155_sig_len,
30    },
31    eip_1559_transaction::{EIP_1559_SIG_LEN, EthEip1559TxArgs, EthEip1559TxArgsBuilder},
32    homestead_transaction::{
33        EthLegacyHomesteadTxArgs, EthLegacyHomesteadTxArgsBuilder, HOMESTEAD_SIG_LEN,
34        HOMESTEAD_SIG_PREFIX,
35    },
36};
37// As per `ref-fvm`, which hardcodes it as well.
38#[derive(FromPrimitive)]
39#[repr(u64)]
40pub enum EAMMethod {
41    Constructor = METHOD_CONSTRUCTOR,
42    Create = 2,
43    Create2 = 3,
44    CreateExternal = 4,
45}
46
47#[derive(FromPrimitive)]
48#[repr(u64)]
49pub enum EVMMethod {
50    Constructor = METHOD_CONSTRUCTOR,
51    Resurrect = 2,
52    GetBytecode = 3,
53    GetBytecodeHash = 4,
54    GetStorageAt = 5,
55    InvokeContractDelegate = 6,
56    // As per `ref-fvm`:
57    // it is very unfortunate but the hasher creates a circular dependency, so we use the raw
58    // number.
59    // InvokeContract = frc42_dispatch::method_hash!("InvokeEVM"),
60    InvokeContract = 3844450837,
61}
62
63/// Ethereum transaction which can be of different types.
64/// The currently supported types are defined in [FIP-0091](https://github.com/filecoin-project/FIPs/blob/020bcb412ee20a2879b4a710337959c51b938d3b/FIPS/fip-0091.md).
65#[delegated_enum(impl_conversions)]
66#[derive(Debug)]
67pub enum EthTx {
68    Homestead(EthLegacyHomesteadTxArgs),
69    Eip1559(EthEip1559TxArgs),
70    Eip155(EthLegacyEip155TxArgs),
71}
72
73impl EthTx {
74    /// Creates an Ethereum transaction from a signed Filecoin message.
75    /// The transaction type is determined based on the signature, as defined in FIP-0091.
76    pub fn from_signed_message(
77        eth_chain_id: EthChainId,
78        msg: &SignedMessage,
79    ) -> anyhow::Result<Self> {
80        Self::ensure_signed_message_valid(msg)?;
81
82        // now we need to determine the transaction type based on the signature length
83        let sig_len = msg.signature().bytes().len();
84
85        // valid signature lengths are based on the chain ID, so we need to calculate it. This
86        // shouldn't be a resource-intensive operation, but if it becomes one, we can do some
87        // memoization.
88        let valid_eip_155_signature_lengths = calc_valid_eip155_sig_len(eth_chain_id);
89
90        let tx: Self = if sig_len == EIP_1559_SIG_LEN {
91            EthEip1559TxArgsBuilder::default()
92                .chain_id(eth_chain_id)
93                .unsigned_message(msg.message())?
94                .build()?
95                .with_signature(msg.signature())?
96                .into()
97        } else if sig_len == HOMESTEAD_SIG_LEN
98            || sig_len == valid_eip_155_signature_lengths.0 as usize
99            || sig_len == valid_eip_155_signature_lengths.1 as usize
100        {
101            // process based on the first byte of the signature
102            match *msg.signature().bytes().first().expect("infallible") {
103                HOMESTEAD_SIG_PREFIX => EthLegacyHomesteadTxArgsBuilder::default()
104                    .unsigned_message(msg.message())?
105                    .build()?
106                    .with_signature(msg.signature())?
107                    .into(),
108                EIP_155_SIG_PREFIX => EthLegacyEip155TxArgsBuilder::default()
109                    .chain_id(eth_chain_id)
110                    .unsigned_message(msg.message())?
111                    .build()?
112                    .with_signature(msg.signature())?
113                    .into(),
114                _ => bail!("unsupported signature prefix"),
115            }
116        } else {
117            bail!("unsupported signature length: {sig_len}");
118        };
119
120        Ok(tx)
121    }
122
123    pub fn eth_hash(&self) -> anyhow::Result<keccak_hash::H256> {
124        Ok(keccak_hash::keccak(self.rlp_signed_message()?))
125    }
126
127    pub fn get_signed_message(&self, eth_chain_id: EthChainId) -> anyhow::Result<SignedMessage> {
128        let from = self.sender(eth_chain_id)?;
129        let msg = match self {
130            Self::Homestead(tx) => tx.get_signed_message(from)?,
131            Self::Eip1559(tx) => tx.get_signed_message(from, eth_chain_id)?,
132            Self::Eip155(tx) => tx.get_signed_message(from, eth_chain_id)?,
133        };
134        Ok(msg)
135    }
136
137    pub fn get_unsigned_message(
138        &self,
139        from: Address,
140        eth_chain_id: EthChainId,
141    ) -> anyhow::Result<Message> {
142        let msg = match self {
143            Self::Homestead(tx) => tx.get_unsigned_message(from)?,
144            Self::Eip1559(tx) => tx.get_unsigned_message(from, eth_chain_id)?,
145            Self::Eip155(tx) => tx.get_unsigned_message(from, eth_chain_id)?,
146        };
147        Ok(msg)
148    }
149
150    pub fn rlp_unsigned_message(&self, eth_chain_id: EthChainId) -> anyhow::Result<Vec<u8>> {
151        match self {
152            Self::Homestead(tx) => tx.rlp_unsigned_message(),
153            Self::Eip1559(tx) => tx.rlp_unsigned_message(),
154            Self::Eip155(tx) => tx.rlp_unsigned_message(eth_chain_id),
155        }
156    }
157
158    pub fn rlp_signed_message(&self) -> anyhow::Result<Vec<u8>> {
159        delegate_eth_tx!(self.rlp_signed_message())
160    }
161
162    fn signature(&self, eth_chain_id: EthChainId) -> anyhow::Result<Signature> {
163        match self {
164            Self::Homestead(tx) => tx.signature(),
165            Self::Eip1559(tx) => tx.signature(),
166            Self::Eip155(tx) => tx.signature(eth_chain_id),
167        }
168    }
169
170    pub fn to_verifiable_signature(
171        &self,
172        sig: Vec<u8>,
173        eth_chain_id: EthChainId,
174    ) -> anyhow::Result<Vec<u8>> {
175        match self {
176            Self::Homestead(tx) => tx.to_verifiable_signature(sig),
177            Self::Eip1559(tx) => tx.to_verifiable_signature(sig),
178            Self::Eip155(tx) => tx.to_verifiable_signature(sig, eth_chain_id),
179        }
180    }
181
182    /// Checks if the transaction is EIP-1559
183    pub fn is_eip1559(&self) -> bool {
184        matches!(self, EthTx::Eip1559(_))
185    }
186
187    /// Validates that the signed Filecoin message is a valid Ethereum transaction.
188    /// Note: only basic checks are done. The signature and payload are not verified.
189    fn ensure_signed_message_valid(msg: &SignedMessage) -> anyhow::Result<()> {
190        ensure!(
191            msg.signature().signature_type() == SignatureType::Delegated,
192            "Signature is not delegated type"
193        );
194
195        ensure!(
196            msg.message().version == 0,
197            "unsupported msg version: {}",
198            msg.message().version
199        );
200
201        EthAddress::from_filecoin_address(&msg.from())?;
202
203        Ok(())
204    }
205
206    fn sender(&self, eth_chain_id: EthChainId) -> anyhow::Result<Address> {
207        let hash = keccak_hash::keccak(self.rlp_unsigned_message(eth_chain_id)?);
208        let sig = self.signature(eth_chain_id)?;
209        let sig_data = self.to_verifiable_signature(sig.bytes().to_vec(), eth_chain_id)?[..]
210            .try_into()
211            .expect("Incorrect signature length");
212        let pubkey =
213            fvm_shared_latest::crypto::signature::ops::recover_secp_public_key(&hash.0, &sig_data)?;
214        let eth_addr = EthAddress::eth_address_from_uncompressed_public_key(&pubkey)?;
215        eth_addr.to_filecoin_address()
216    }
217}
218
219impl From<EthTx> for ApiEthTx {
220    fn from(value: EthTx) -> Self {
221        delegate_eth_tx!(value.into())
222    }
223}
224
225/// Checks if a signed Filecoin message is valid for sending to Ethereum.
226pub fn is_valid_eth_tx_for_sending(
227    eth_chain_id: EthChainId,
228    network_version: NetworkVersion,
229    message: &SignedMessage,
230) -> bool {
231    let eth_tx = EthTx::from_signed_message(eth_chain_id, message);
232
233    if let Ok(eth_tx) = eth_tx {
234        // EIP-1559 transactions are valid for all network versions.
235        // Legacy transactions are only valid for network versions >= V23.
236        network_version >= NetworkVersion::V23 || eth_tx.is_eip1559()
237    } else {
238        false
239    }
240}
241
242/// Extracts the Ethereum transaction parameters and recipient from a Filecoin message.
243pub fn get_eth_params_and_recipient(
244    msg: &Message,
245) -> anyhow::Result<(Vec<u8>, Option<EthAddress>)> {
246    let mut to = None;
247    let mut params = vec![];
248
249    ensure!(msg.version == 0, "unsupported msg version: {}", msg.version);
250
251    if !msg.params().bytes().is_empty() {
252        let mut reader = SliceReader::new(msg.params().bytes());
253        match Value::decode(&mut reader) {
254            Ok(Value::Bytes(bytes)) => params = bytes,
255            _ => bail!("failed to read params byte array"),
256        }
257    }
258
259    if msg.to == Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR {
260        if msg.method_num() != EAMMethod::CreateExternal as u64 {
261            bail!("unsupported EAM method");
262        }
263    } else if msg.method_num() == EVMMethod::InvokeContract as u64 {
264        let addr = EthAddress::from_filecoin_address(&msg.to)?;
265        to = Some(addr);
266    } else {
267        bail!(
268            "invalid methodnum {}: only allowed method is InvokeContract({})",
269            msg.method_num(),
270            EVMMethod::InvokeContract as u64
271        );
272    }
273
274    Ok((params, to))
275}
276
277pub fn format_u64(value: u64) -> Bytes {
278    if value != 0 {
279        let i = (value.leading_zeros() / 8) as usize;
280        let bytes = value.to_be_bytes();
281        // `leading_zeros` for a positive `u64` returns a number in the range [1-63]
282        // `i` is in the range [1-7], and `bytes` is an array of size 8
283        // therefore, getting the slice from `i` to end should never fail
284        bytes.get(i..).expect("failed to get slice").to_vec().into()
285    } else {
286        // If all bytes are zero, return an empty slice
287        Bytes::new()
288    }
289}
290
291pub fn format_bigint(value: &BigInt) -> anyhow::Result<Bytes> {
292    Ok(if value.is_positive() {
293        value.to_bytes_be().1.into()
294    } else {
295        if value.is_negative() {
296            bail!("can't format a negative number");
297        }
298        // If all bytes are zero, return an empty slice
299        Bytes::new()
300    })
301}
302
303pub fn format_address(value: Option<&EthAddress>) -> Bytes {
304    if let Some(addr) = value {
305        addr.0.as_bytes().to_vec().into()
306    } else {
307        Bytes::new()
308    }
309}
310
311/// Pads data with leading zeros to the specified length
312pub fn pad_leading_zeros(data: Vec<u8>, length: usize) -> Vec<u8> {
313    if data.len() >= length {
314        return data;
315    }
316    let mut zeros = vec![0; length - data.len()];
317    zeros.extend(data);
318    zeros
319}
320
321/// Parses raw bytes into an eth transaction
322pub fn parse_eth_transaction(data: &[u8]) -> anyhow::Result<EthTx> {
323    ensure!(!data.is_empty(), "eth transaction data is empty");
324
325    match data.first() {
326        Some(&EIP_2930_TX_TYPE) => {
327            // EIP-2930
328            Err(anyhow::anyhow!("EIP-2930 transaction is not supported"))
329        }
330        Some(&EIP_1559_TX_TYPE) => {
331            parse_eip1559_tx(data).context("Failed to parse EIP-1559 transaction")
332        }
333        Some(tx_type) if *tx_type > 0x7f => parse_legacy_tx(data)
334            .map_err(|err| anyhow::anyhow!("failed to parse legacy transaction: {}", err)),
335        _ => Err(anyhow::anyhow!("unsupported transaction type")),
336    }
337}
338
339fn parse_eip1559_tx(data: &[u8]) -> anyhow::Result<EthTx> {
340    // Decode RLP data, skipping the first byte (EIP_1559_TX_TYPE)
341    let decoded = Rlp::new(data.get(1..).context("failed to get range of values")?);
342    ensure!(
343        decoded.item_count()? == 12,
344        "not an EIP-1559 transaction: should have 12 elements in the Rlp list"
345    );
346
347    let chain_id = decoded.at(0)?.as_val::<u64>()?;
348    let nonce = decoded.at(1)?.as_val::<u64>()?;
349    let max_priority_fee_per_gas = BigInt::from_bytes_be(Sign::Plus, decoded.at(2)?.data()?);
350    let max_fee_per_gas = BigInt::from_bytes_be(Sign::Plus, decoded.at(3)?.data()?);
351    let gas_limit = decoded.at(4)?.as_val::<u64>()?;
352
353    let addr_data = decoded.at(5)?.data()?;
354    let to = (!addr_data.is_empty())
355        .then(|| EthAddress::try_from(addr_data))
356        .transpose()?;
357
358    let value = BigInt::from_bytes_be(Sign::Plus, decoded.at(6)?.data()?);
359    let input = decoded.at(7)?.data()?.to_vec();
360
361    // Ensure access list is empty (should be an empty list)
362    ensure!(
363        decoded.at(8)?.item_count()? == 0,
364        "access list should be an empty list"
365    );
366
367    let v = BigInt::from_bytes_be(Sign::Plus, decoded.at(9)?.data()?);
368    let r = BigInt::from_bytes_be(Sign::Plus, decoded.at(10)?.data()?);
369    let s = BigInt::from_bytes_be(Sign::Plus, decoded.at(11)?.data()?);
370
371    // EIP-1559 transactions only support 0 or 1 for v
372    ensure!(
373        v == BigInt::from(0) || v == BigInt::from(1),
374        "EIP-1559 transactions only support 0 or 1 for v"
375    );
376
377    // Construct and return the Eth1559TxArgs struct
378    let tx_args = EthEip1559TxArgs {
379        chain_id,
380        nonce,
381        to,
382        max_priority_fee_per_gas,
383        max_fee_per_gas,
384        gas_limit,
385        value,
386        input,
387        v,
388        r,
389        s,
390    };
391
392    Ok(tx_args.into())
393}
394
395fn parse_legacy_tx(data: &[u8]) -> anyhow::Result<EthTx> {
396    // Decode RLP data
397    let decoded = Rlp::new(data);
398    if decoded.item_count()? != 9 {
399        bail!("not a Legacy transaction: should have 9 elements in the rlp list");
400    }
401
402    // Parse transaction fields
403    let nonce = decoded.at(0)?.as_val::<u64>()?;
404    let gas_price = BigInt::from_bytes_be(Sign::Plus, decoded.at(1)?.data()?);
405    let gas_limit = decoded.at(2)?.as_val::<u64>()?;
406
407    let addr_data = decoded.at(3)?.data()?;
408    let to = (!addr_data.is_empty())
409        .then(|| EthAddress::try_from(addr_data))
410        .transpose()?;
411
412    let value = BigInt::from_bytes_be(Sign::Plus, decoded.at(4)?.data()?);
413    let input = decoded.at(5)?.data()?.to_vec();
414
415    // Parse signature fields
416    let v = BigInt::from_bytes_be(Sign::Plus, decoded.at(6)?.data()?);
417    let r = BigInt::from_bytes_be(Sign::Plus, decoded.at(7)?.data()?);
418    let s = BigInt::from_bytes_be(Sign::Plus, decoded.at(8)?.data()?);
419
420    // Derive chain ID from the 'v' field
421    let chain_id = derive_eip_155_chain_id(&v)?
422        .to_u64()
423        .context("unable to convert derived chain to `u64`")?;
424
425    // Check if the transaction is a legacy Homestead transaction
426    if chain_id == 0 {
427        // Validate that 'v' is either 27 or 28
428        ensure!(
429            v == BigInt::from(LEGACY_V_VALUE_27) || v == BigInt::from(LEGACY_V_VALUE_28),
430            "legacy homestead transactions only support 27 or 28 for v, got {}",
431            v
432        );
433
434        let tx_args = EthLegacyHomesteadTxArgs {
435            nonce,
436            gas_price,
437            gas_limit,
438            to,
439            value,
440            input,
441            v,
442            r,
443            s,
444        };
445        return Ok(tx_args.into());
446    }
447
448    // For EIP-155 transactions, validate chain ID protection
449    validate_eip155_chain_id(chain_id, &v)?;
450
451    Ok(EthLegacyEip155TxArgs {
452        chain_id,
453        nonce,
454        gas_price,
455        gas_limit,
456        to,
457        value,
458        input,
459        v,
460        r,
461        s,
462    }
463    .into())
464}
465
466#[derive(Debug)]
467pub struct MethodInfo {
468    pub to: Address,
469    pub method: u64,
470    pub params: Vec<u8>,
471}
472
473/// Retrieves method info
474pub fn get_filecoin_method_info(
475    recipient: Option<&EthAddress>,
476    input: &[u8],
477) -> anyhow::Result<MethodInfo> {
478    let params = if !input.is_empty() {
479        cbor4ii::serde::to_vec(
480            Vec::with_capacity(input.len()),
481            &Value::Bytes(input.to_vec()),
482        )
483        .context("failed to encode params")?
484    } else {
485        Vec::new()
486    };
487
488    let (to, method) = match recipient {
489        None => {
490            // If recipient is None, use Ethereum Address Manager Actor and CreateExternal method
491            (
492                Address::ETHEREUM_ACCOUNT_MANAGER_ACTOR,
493                EAMMethod::CreateExternal as u64,
494            )
495        }
496        Some(recipient) => {
497            // Otherwise, use InvokeContract method and convert EthAddress to Filecoin address
498            let to = recipient
499                .to_filecoin_address()
500                .context("failed to convert EthAddress to Filecoin address")?;
501            (to, EVMMethod::InvokeContract as u64)
502        }
503    };
504
505    Ok(MethodInfo { to, method, params })
506}
507
508#[cfg(test)]
509pub(crate) mod tests {
510    use super::*;
511    use crate::{
512        networks::{calibnet, mainnet},
513        shim::{crypto::Signature, econ::TokenAmount},
514    };
515    use num::{BigInt, Num as _, Zero as _, traits::FromBytes as _};
516    use num_bigint::ToBigUint as _;
517    use quickcheck_macros::quickcheck;
518    use std::str::FromStr as _;
519
520    const ETH_ADDR_LEN: usize = 20;
521
522    pub fn create_message() -> Message {
523        let from = EthAddress::from_str("0xff38c072f286e3b20b3954ca9f99c05fbecc64aa")
524            .unwrap()
525            .to_filecoin_address()
526            .unwrap();
527
528        let to = Address::new_id(1);
529        Message {
530            version: 0,
531            to,
532            from,
533            value: TokenAmount::from_atto(10),
534            gas_fee_cap: TokenAmount::from_atto(11),
535            gas_premium: TokenAmount::from_atto(12),
536            gas_limit: 13,
537            sequence: 14,
538            method_num: EVMMethod::InvokeContract as u64,
539            params: Default::default(),
540        }
541    }
542
543    pub fn create_eip_1559_signed_message() -> SignedMessage {
544        let mut eip_1559_sig = vec![0u8; EIP_1559_SIG_LEN];
545        eip_1559_sig[0] = EIP_155_SIG_PREFIX;
546
547        SignedMessage {
548            message: create_message(),
549            signature: Signature::new(SignatureType::Delegated, eip_1559_sig),
550        }
551    }
552
553    pub fn create_homestead_signed_message() -> SignedMessage {
554        let mut homestead_sig = vec![0u8; HOMESTEAD_SIG_LEN];
555        homestead_sig[0] = HOMESTEAD_SIG_PREFIX;
556        homestead_sig[HOMESTEAD_SIG_LEN - 1] = 27;
557
558        SignedMessage {
559            message: create_message(),
560            signature: Signature::new(SignatureType::Delegated, homestead_sig),
561        }
562    }
563
564    #[test]
565    fn test_ensure_signed_message_valid() {
566        let create_empty_delegated_message = || SignedMessage {
567            message: create_message(),
568            signature: Signature::new(SignatureType::Delegated, vec![]),
569        };
570        // ok
571        let msg = create_empty_delegated_message();
572        EthTx::ensure_signed_message_valid(&msg).unwrap();
573
574        // wrong signature type
575        let mut msg = create_empty_delegated_message();
576        msg.signature = Signature::new(SignatureType::Bls, vec![]);
577        assert!(EthTx::ensure_signed_message_valid(&msg).is_err());
578
579        // unsupported version
580        let mut msg = create_empty_delegated_message();
581        msg.message.version = 1;
582        assert!(EthTx::ensure_signed_message_valid(&msg).is_err());
583
584        // invalid delegated address namespace
585        let mut msg = create_empty_delegated_message();
586        msg.message.from = Address::new_delegated(0x42, &[0xff; ETH_ADDR_LEN]).unwrap();
587        assert!(EthTx::ensure_signed_message_valid(&msg).is_err());
588    }
589
590    #[test]
591    fn test_eth_transaction_from_signed_filecoin_message_valid_eip1559() {
592        let msg = create_eip_1559_signed_message();
593
594        let tx = EthTx::from_signed_message(mainnet::ETH_CHAIN_ID, &msg).unwrap();
595        if let EthTx::Eip1559(tx) = tx {
596            assert_eq!(tx.chain_id, mainnet::ETH_CHAIN_ID);
597            assert_eq!(tx.value, msg.message.value.into());
598            assert_eq!(tx.max_fee_per_gas, msg.message.gas_fee_cap.into());
599            assert_eq!(tx.max_priority_fee_per_gas, msg.message.gas_premium.into());
600            assert_eq!(tx.gas_limit, msg.message.gas_limit);
601            assert_eq!(tx.nonce, msg.message.sequence);
602            assert_eq!(
603                tx.to.unwrap(),
604                EthAddress::from_filecoin_address(&msg.message.to).unwrap()
605            );
606            assert!(tx.input.is_empty());
607        } else {
608            panic!("invalid transaction type");
609        }
610    }
611
612    #[test]
613    fn test_eth_transaction_from_signed_filecoin_message_valid_homestead() {
614        let msg = create_homestead_signed_message();
615        let tx = EthTx::from_signed_message(mainnet::ETH_CHAIN_ID, &msg).unwrap();
616        if let EthTx::Homestead(tx) = tx {
617            assert_eq!(tx.value, msg.message.value.into());
618            assert_eq!(tx.gas_limit, msg.message.gas_limit);
619            assert_eq!(tx.nonce, msg.message.sequence);
620            assert_eq!(
621                tx.to.unwrap(),
622                EthAddress::from_filecoin_address(&msg.message.to).unwrap()
623            );
624            assert_eq!(tx.gas_price, msg.message.gas_fee_cap.into());
625            assert!(tx.input.is_empty());
626        } else {
627            panic!("invalid transaction type");
628        }
629    }
630
631    #[test]
632    fn test_eth_transaction_from_signed_filecoin_message_valid_eip_155() {
633        let eth_chain_id = mainnet::ETH_CHAIN_ID;
634
635        // we need some reverse math here to get the correct V value
636        // from which the chain ID is derived.
637        let v = (2 * eth_chain_id + 35).to_biguint().unwrap().to_bytes_be();
638        let eip_155_sig_len = calc_valid_eip155_sig_len(eth_chain_id).0 as usize;
639        let mut eip_155_sig = vec![0u8; eip_155_sig_len - v.len()];
640        eip_155_sig[0] = EIP_155_SIG_PREFIX;
641        eip_155_sig.extend(v);
642
643        let msg = SignedMessage {
644            message: create_message(),
645            signature: Signature::new(SignatureType::Delegated, eip_155_sig),
646        };
647
648        let tx = EthTx::from_signed_message(mainnet::ETH_CHAIN_ID, &msg).unwrap();
649        if let EthTx::Eip155(tx) = tx {
650            assert_eq!(tx.value, msg.message.value.into());
651            assert_eq!(tx.gas_limit, msg.message.gas_limit);
652            assert_eq!(tx.nonce, msg.message.sequence);
653            assert_eq!(
654                tx.to.unwrap(),
655                EthAddress::from_filecoin_address(&msg.message.to).unwrap()
656            );
657            assert_eq!(tx.gas_price, msg.message.gas_fee_cap.into());
658            assert!(tx.input.is_empty());
659        } else {
660            panic!("invalid transaction type");
661        }
662    }
663
664    #[test]
665    fn test_eth_transaction_from_signed_filecoin_message_empty_signature() {
666        let msg = SignedMessage {
667            message: create_message(),
668            signature: Signature::new(SignatureType::Delegated, vec![]),
669        };
670
671        assert!(EthTx::from_signed_message(mainnet::ETH_CHAIN_ID, &msg).is_err());
672    }
673
674    #[test]
675    fn test_eth_transaction_from_signed_filecoin_message_invalid_signature() {
676        let msg = SignedMessage {
677            message: create_message(),
678            signature: Signature::new(
679                SignatureType::Delegated,
680                b"Ph'nglui mglw'nafh Cthulhu R'lyeh wgah'nagl fhtagn".to_vec(),
681            ),
682        };
683
684        assert!(EthTx::from_signed_message(mainnet::ETH_CHAIN_ID, &msg).is_err());
685    }
686
687    #[test]
688    fn test_is_valid_eth_tx_for_sending_eip1559_always_valid() {
689        let msg = create_eip_1559_signed_message();
690        assert!(is_valid_eth_tx_for_sending(
691            mainnet::ETH_CHAIN_ID,
692            NetworkVersion::V22,
693            &msg
694        ));
695
696        assert!(is_valid_eth_tx_for_sending(
697            mainnet::ETH_CHAIN_ID,
698            NetworkVersion::V23,
699            &msg
700        ));
701    }
702
703    #[test]
704    fn test_is_valid_eth_tx_for_sending_legacy_valid_post_nv23() {
705        let msg = create_homestead_signed_message();
706        assert!(is_valid_eth_tx_for_sending(
707            mainnet::ETH_CHAIN_ID,
708            NetworkVersion::V23,
709            &msg
710        ));
711    }
712
713    #[test]
714    fn test_is_valid_eth_tx_for_sending_legacy_invalid_pre_nv23() {
715        let msg = create_homestead_signed_message();
716        assert!(!is_valid_eth_tx_for_sending(
717            mainnet::ETH_CHAIN_ID,
718            NetworkVersion::V22,
719            &msg
720        ));
721    }
722
723    #[test]
724    fn test_is_valid_eth_tx_for_sending_invalid_non_delegated() {
725        let msg = create_message();
726        let msg = SignedMessage {
727            message: msg,
728            signature: Signature::new_secp256k1(vec![]),
729        };
730        assert!(!is_valid_eth_tx_for_sending(
731            mainnet::ETH_CHAIN_ID,
732            NetworkVersion::V22,
733            &msg
734        ));
735        assert!(!is_valid_eth_tx_for_sending(
736            mainnet::ETH_CHAIN_ID,
737            NetworkVersion::V23,
738            &msg
739        ));
740    }
741
742    #[test]
743    fn test_eip_1559() {
744        let mut tx_args=EthEip1559TxArgsBuilder::default()
745            .chain_id(314159_u64)
746            .nonce(486_u64)
747            .to(Some(
748                ethereum_types::H160::from_str("0xeb4a9cdb9f42d3a503d580a39b6e3736eb21fffd")
749                    .unwrap()
750                    .into(),
751            ))
752            .value(BigInt::from(0))
753            .max_fee_per_gas(BigInt::from(1500000120))
754            .max_priority_fee_per_gas(BigInt::from(1500000000))
755            .gas_limit(37442471_u64)
756            .input(hex::decode("383487be000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000660d4d120000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000003b6261666b726569656f6f75326d36356276376561786e7767656d7562723675787269696867366474646e6c7a663469616f37686c6e6a6d647372750000000000").unwrap())
757            .build()
758            .unwrap();
759        tx_args.v = BigInt::from_str("1").unwrap();
760        tx_args.r = BigInt::from_str(
761            "84103132941276310528712440865285269631208564772362393569572880532520338257200",
762        )
763        .unwrap();
764        tx_args.s = BigInt::from_str(
765            "7820796778417228639067439047870612492553874254089570360061550763595363987236",
766        )
767        .unwrap();
768        let tx = EthTx::from(tx_args);
769        let sig = tx.signature(calibnet::ETH_CHAIN_ID);
770        assert!(sig.is_ok());
771        assert!(
772            tx.to_verifiable_signature(sig.unwrap().bytes().to_vec(), calibnet::ETH_CHAIN_ID)
773                .is_ok()
774        );
775        assert!(tx.rlp_unsigned_message(calibnet::ETH_CHAIN_ID).is_ok());
776        assert!(tx.get_signed_message(calibnet::ETH_CHAIN_ID).is_ok());
777        let expected_hash = ethereum_types::H256::from_str(
778            "0x9f2e70d5737c6b798eccea14895893fb48091ab3c59d0fe95508dc7efdae2e5f",
779        )
780        .unwrap();
781        assert_eq!(expected_hash, tx.eth_hash().unwrap());
782    }
783
784    #[test]
785    fn test_legacy_eip_155() {
786        // https://calibration.filfox.info/en/message/bafy2bzacebazsfc63saveaopjjgsz3yoic3izod4k5wo3pg4fswmpdqny5zlc?t=1
787        let mut tx_args = EthLegacyEip155TxArgsBuilder::default()
788            .chain_id(314159_u64)
789            .nonce(0x4_u64)
790            .to(Some(
791                ethereum_types::H160::from_str("0xd0fb381fc644cdd5d694d35e1afb445527b9244b")
792                    .unwrap()
793                    .into(),
794            ))
795            .value(BigInt::from(0))
796            .gas_limit(0x19ca81cc_u64)
797            .gas_price(BigInt::from(0x40696))
798            .input(hex::decode("d5b3d76d00000000000000000000000000000000000000000000000045466fa6fdcb80000000000000000000000000000000000000000000000000000000002e90edd0000000000000000000000000000000000000000000000000000000000000015180").unwrap())
799            .build()
800            .unwrap();
801        tx_args.v = BigInt::from_str_radix("99681", 16).unwrap();
802        tx_args.r = BigInt::from_str_radix(
803            "580b1d36c5a8c8c1c550fb45b0a6ff21aaa517be036385541621961b5d873796",
804            16,
805        )
806        .unwrap();
807        tx_args.s = BigInt::from_str_radix(
808            "55e8447d58d64ebc3038d9882886bbc3b0228c7ac77c71f4e811b97ed3f14b5a",
809            16,
810        )
811        .unwrap();
812        let tx = EthTx::from(tx_args);
813        let sig = tx.signature(calibnet::ETH_CHAIN_ID);
814        assert!(sig.is_ok());
815        assert!(
816            tx.to_verifiable_signature(sig.unwrap().bytes().to_vec(), calibnet::ETH_CHAIN_ID)
817                .is_ok()
818        );
819        assert!(tx.rlp_unsigned_message(calibnet::ETH_CHAIN_ID).is_ok());
820        assert!(tx.get_signed_message(calibnet::ETH_CHAIN_ID).is_ok());
821        let expected_hash = ethereum_types::H256::from_str(
822            "0x3ebc897150feeff6caa1b2e5992e347e8409e9e35fa30f7f5f8fcda3f7c965c7",
823        )
824        .unwrap();
825        assert_eq!(expected_hash, tx.eth_hash().unwrap());
826    }
827
828    #[test]
829    fn test_legacy_homestead() {
830        // https://calibration.filfox.info/en/message/bafy2bzacebazsfc63saveaopjjgsz3yoic3izod4k5wo3pg4fswmpdqny5zlc?t=1
831        let mut tx_args = EthLegacyHomesteadTxArgsBuilder::default()
832            .nonce(0x4_u64)
833            .to(Some(
834                ethereum_types::H160::from_str("0xd0fb381fc644cdd5d694d35e1afb445527b9244b")
835                    .unwrap()
836                    .into(),
837            ))
838            .value(BigInt::from(0))
839            .gas_limit(0x19ca81cc_u64)
840            .gas_price(BigInt::from(0x40696))
841            .input(hex::decode("d5b3d76d00000000000000000000000000000000000000000000000045466fa6fdcb80000000000000000000000000000000000000000000000000000000002e90edd0000000000000000000000000000000000000000000000000000000000000015180").unwrap())
842            .build()
843            .unwrap();
844        // Note that the `v` value in this test case is invalid for homestead
845        // when it's normally assigned in `with_signature`
846        tx_args.v = BigInt::from_str_radix("99681", 16).unwrap();
847        tx_args.r = BigInt::from_str_radix(
848            "580b1d36c5a8c8c1c550fb45b0a6ff21aaa517be036385541621961b5d873796",
849            16,
850        )
851        .unwrap();
852        tx_args.s = BigInt::from_str_radix(
853            "55e8447d58d64ebc3038d9882886bbc3b0228c7ac77c71f4e811b97ed3f14b5a",
854            16,
855        )
856        .unwrap();
857        let tx = EthTx::from(tx_args.clone());
858        let expected_hash = ethereum_types::H256::from_str(
859            "0x3ebc897150feeff6caa1b2e5992e347e8409e9e35fa30f7f5f8fcda3f7c965c7",
860        )
861        .unwrap();
862        assert_eq!(expected_hash, tx.eth_hash().unwrap());
863        // Note: `v` value 27 is for homestead
864        tx_args.v = BigInt::from_str_radix("1b", 16).unwrap();
865        let tx = EthTx::from(tx_args.clone());
866        let sig = tx.signature(calibnet::ETH_CHAIN_ID);
867        assert!(sig.is_ok());
868        assert!(
869            tx.to_verifiable_signature(sig.unwrap().bytes().to_vec(), calibnet::ETH_CHAIN_ID)
870                .is_ok()
871        );
872        assert!(tx.rlp_unsigned_message(calibnet::ETH_CHAIN_ID).is_ok());
873        assert!(tx.get_signed_message(calibnet::ETH_CHAIN_ID).is_ok());
874    }
875
876    #[quickcheck]
877    fn u64_roundtrip(i: u64) {
878        let bm = format_u64(i);
879        if i == 0 {
880            assert!(bm.is_empty());
881        } else {
882            // check that buffer doesn't start with zero
883            assert!(!bm.starts_with(&[0]));
884
885            // roundtrip
886            let mut padded = [0u8; 8];
887            let bytes: &[u8] = &bm.slice(..);
888            padded[8 - bytes.len()..].copy_from_slice(bytes);
889            assert_eq!(i, u64::from_be_bytes(padded));
890        }
891    }
892
893    #[quickcheck]
894    fn bigint_roundtrip(bi: num_bigint::BigInt) {
895        match format_bigint(&bi) {
896            Ok(bm) => {
897                if bi.is_zero() {
898                    assert!(bm.is_empty());
899                } else {
900                    // check that buffer doesn't start with zero
901                    assert!(!bm.starts_with(&[0]));
902
903                    // roundtrip
904                    let unsigned = num_bigint::BigUint::from_be_bytes(&bm.slice(..));
905                    assert_eq!(bi, unsigned.into());
906                }
907            }
908            Err(_) => {
909                // fails in case of negative number
910                assert!(bi.is_negative());
911            }
912        }
913    }
914
915    #[test]
916    fn test_pad_leading_zeros() {
917        // Case 1: Data is shorter than the target length
918        let data = vec![1, 2, 3];
919        let padded = pad_leading_zeros(data, 5);
920        assert_eq!(padded, vec![0, 0, 1, 2, 3]);
921
922        // Case 2: Data is already the target length
923        let data = vec![4, 5, 6];
924        let padded = pad_leading_zeros(data, 3);
925        assert_eq!(padded, vec![4, 5, 6]);
926
927        // Case 3: Data is longer than the target length (no padding should happen)
928        let data = vec![7, 8, 9, 10];
929        let padded = pad_leading_zeros(data, 3); // length is smaller
930        assert_eq!(padded, vec![7, 8, 9, 10]); // Should return unchanged
931
932        // Case 4: Data is empty, and the target length is greater than zero
933        let data = vec![];
934        let padded = pad_leading_zeros(data, 4);
935        assert_eq!(padded, vec![0, 0, 0, 0]);
936    }
937
938    #[test]
939    fn test_parse_eth_transaction() {
940        // Legacy transaction
941        let raw_tx = hex::decode(
942            "f8cc04830406968419ca81cc94d0fb381fc644cdd5d694d35e1afb445527b9244b80b864d5b3d76d00000000000000000000000000000000000000000000000045466fa6fdcb80000000000000000000000000000000000000000000000000000000002e90edd000000000000000000000000000000000000000000000000000000000000001518083099681a0580b1d36c5a8c8c1c550fb45b0a6ff21aaa517be036385541621961b5d873796a055e8447d58d64ebc3038d9882886bbc3b0228c7ac77c71f4e811b97ed3f14b5a",
943        )
944        .expect("Invalid hex");
945        let eth_tx = parse_eth_transaction(&raw_tx);
946        assert!(eth_tx.is_ok());
947
948        // EIP-1559 transaction
949        let raw_tx = hex::decode(
950            "02f901368304cb2f8201e68459682f008459682f7884023b53a794eb4a9cdb9f42d3a503d580a39b6e3736eb21fffd80b8c4383487be000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000660d4d120000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000003b6261666b726569656f6f75326d36356276376561786e7767656d7562723675787269696867366474646e6c7a663469616f37686c6e6a6d647372750000000000c001a0b9f0afb3fa8821fa414bac6056e613c61a8263ca341b59539096dbbc8600f530a0114a6a032347e132f115accc7664ccc61549be28f5b844c3fc170006feb72f24",
951        )
952        .expect("Invalid hex");
953        let eth_tx = parse_eth_transaction(&raw_tx);
954        assert!(eth_tx.is_ok());
955    }
956
957    #[test]
958    fn test_derive_sender() {
959        // Legacy transaction
960        let raw_tx = hex::decode(
961            "f8cc04830406968419ca81cc94d0fb381fc644cdd5d694d35e1afb445527b9244b80b864d5b3d76d00000000000000000000000000000000000000000000000045466fa6fdcb80000000000000000000000000000000000000000000000000000000002e90edd000000000000000000000000000000000000000000000000000000000000001518083099681a0580b1d36c5a8c8c1c550fb45b0a6ff21aaa517be036385541621961b5d873796a055e8447d58d64ebc3038d9882886bbc3b0228c7ac77c71f4e811b97ed3f14b5a",
962        )
963        .expect("Invalid hex");
964        let eth_tx = parse_eth_transaction(&raw_tx).unwrap();
965        let from = eth_tx.sender(calibnet::ETH_CHAIN_ID).unwrap();
966        assert_eq!(
967            EthAddress::from_filecoin_address(&from).unwrap(),
968            EthAddress::from_str("0xEb1D0C87B7e33D0Ab44a397b675F0897295491C2").unwrap()
969        );
970
971        // EIP-1559 transaction
972        let raw_tx = hex::decode(
973            "02f901368304cb2f8201e68459682f008459682f7884023b53a794eb4a9cdb9f42d3a503d580a39b6e3736eb21fffd80b8c4383487be000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000660d4d120000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000003b6261666b726569656f6f75326d36356276376561786e7767656d7562723675787269696867366474646e6c7a663469616f37686c6e6a6d647372750000000000c001a0b9f0afb3fa8821fa414bac6056e613c61a8263ca341b59539096dbbc8600f530a0114a6a032347e132f115accc7664ccc61549be28f5b844c3fc170006feb72f24",
974        )
975        .expect("Invalid hex");
976        let eth_tx = parse_eth_transaction(&raw_tx).unwrap();
977        let from = eth_tx.sender(calibnet::ETH_CHAIN_ID).unwrap();
978        assert_eq!(
979            EthAddress::from_filecoin_address(&from).unwrap(),
980            EthAddress::from_str("0x4fda4174D5D07C906395bfB77806287cc65Fd129").unwrap()
981        );
982    }
983
984    #[test]
985    fn test_parse_legacy_tx() {
986        // Raw transaction hex for a Legacy transaction
987        let raw_tx = hex::decode(
988            "f8cc04830406968419ca81cc94d0fb381fc644cdd5d694d35e1afb445527b9244b80b864d5b3d76d00000000000000000000000000000000000000000000000045466fa6fdcb80000000000000000000000000000000000000000000000000000000002e90edd000000000000000000000000000000000000000000000000000000000000001518083099681a0580b1d36c5a8c8c1c550fb45b0a6ff21aaa517be036385541621961b5d873796a055e8447d58d64ebc3038d9882886bbc3b0228c7ac77c71f4e811b97ed3f14b5a",
989        )
990        .expect("Invalid hex");
991
992        let result = parse_legacy_tx(&raw_tx);
993        assert!(result.is_ok());
994        let txn = result.unwrap();
995
996        if let EthTx::Eip155(tx) = txn {
997            assert_eq!(tx.chain_id, 314159);
998            assert_eq!(tx.nonce, 4);
999            assert_eq!(tx.gas_price, BigInt::from(263830u64));
1000            assert_eq!(tx.gas_limit, 432701900);
1001            assert_eq!(
1002                tx.to.unwrap(),
1003                EthAddress::from_str("0xd0fb381fc644cdd5d694d35e1afb445527b9244b").unwrap()
1004            );
1005            assert_eq!(tx.value, BigInt::from(0));
1006            assert_eq!(
1007                tx.v,
1008                BigInt::from_str_radix("099681", 16).expect("Invalid hex string")
1009            );
1010            assert_eq!(
1011                tx.r,
1012                BigInt::from_str_radix(
1013                    "580b1d36c5a8c8c1c550fb45b0a6ff21aaa517be036385541621961b5d873796",
1014                    16
1015                )
1016                .expect("Invalid hex string")
1017            );
1018            assert_eq!(
1019                tx.s,
1020                BigInt::from_str_radix(
1021                    "55e8447d58d64ebc3038d9882886bbc3b0228c7ac77c71f4e811b97ed3f14b5a",
1022                    16
1023                )
1024                .expect("Invalid hex string")
1025            );
1026        } else {
1027            panic!("Expected EIP-1559 transaction");
1028        }
1029    }
1030
1031    #[test]
1032    fn test_parse_eip1559_tx() {
1033        // Raw transaction hex for the EIP-1559 transaction
1034        let raw_tx = hex::decode(
1035            "02f901368304cb2f8201e68459682f008459682f7884023b53a794eb4a9cdb9f42d3a503d580a39b6e3736eb21fffd80b8c4383487be000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000660d4d120000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000003b6261666b726569656f6f75326d36356276376561786e7767656d7562723675787269696867366474646e6c7a663469616f37686c6e6a6d647372750000000000c001a0b9f0afb3fa8821fa414bac6056e613c61a8263ca341b59539096dbbc8600f530a0114a6a032347e132f115accc7664ccc61549be28f5b844c3fc170006feb72f24"
1036        ).expect("Invalid hex");
1037
1038        let result = parse_eip1559_tx(&raw_tx);
1039        assert!(result.is_ok());
1040
1041        let txn = result.unwrap();
1042
1043        if let EthTx::Eip1559(tx) = txn {
1044            assert_eq!(tx.chain_id, 314159);
1045            assert_eq!(tx.nonce, 486);
1046            assert_eq!(tx.max_fee_per_gas, BigInt::from(1500000120u64));
1047            assert_eq!(tx.max_priority_fee_per_gas, BigInt::from(1500000000u64));
1048            assert_eq!(tx.gas_limit, 37442471);
1049            assert_eq!(
1050                tx.to.unwrap(),
1051                EthAddress::from_str("0xeb4a9cdb9f42d3a503d580a39b6e3736eb21fffd").unwrap()
1052            );
1053            assert_eq!(tx.value, BigInt::from(0));
1054            assert_eq!(
1055                tx.v,
1056                BigInt::from_str_radix("01", 16).expect("Invalid hex string")
1057            );
1058            assert_eq!(
1059                tx.r,
1060                BigInt::from_str_radix(
1061                    "b9f0afb3fa8821fa414bac6056e613c61a8263ca341b59539096dbbc8600f530",
1062                    16
1063                )
1064                .expect("Invalid hex string")
1065            );
1066            assert_eq!(
1067                tx.s,
1068                BigInt::from_str_radix(
1069                    "114a6a032347e132f115accc7664ccc61549be28f5b844c3fc170006feb72f24",
1070                    16
1071                )
1072                .expect("Invalid hex string")
1073            );
1074        } else {
1075            panic!("Expected EIP-1559 transaction");
1076        }
1077    }
1078}