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