forest-filecoin 0.38.0

Rust Filecoin implementation.
// Copyright 2019-2026 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

pub mod chain_message;
pub mod signed_message;

use crate::shim::{address::Address, econ::TokenAmount, message::Message};
use crate::shim::{gas::Gas, version::NetworkVersion};
use ambassador::delegatable_trait;
pub use chain_message::ChainMessage;
use num::Zero;
pub use signed_message::SignedMessage;

/// Message interface to make read-only interactions with Signed and unsigned messages in a generic
/// context.
#[auto_impl::auto_impl(&, Arc)]
#[delegatable_trait]
pub trait MessageRead {
    /// Returns the message the VM executes. For a signed message this is the inner unsigned
    /// message.
    ///
    /// Mirrors Lotus [`ChainMsg.VMMessage`](https://github.com/filecoin-project/lotus/blob/a56f7b7399c2804cdab1a230b0efbd92b1694617/chain/types/message.go#L134-L136).
    fn vm_message(&self) -> &Message;
    /// Returns the number of bytes this message contributes to the chain, which is what on-chain
    /// gas is charged for. A `Secp256k1` or delegated signature counts towards it; a BLS signature
    /// does not, because it is aggregated into the block header.
    ///
    /// Mirrors Lotus [`ChainMsg.ChainLength`](https://github.com/filecoin-project/lotus/blob/a56f7b7399c2804cdab1a230b0efbd92b1694617/chain/types/signedmessage.go#L86-L98).
    fn chain_length(&self) -> anyhow::Result<usize>;
    /// Returns the from address of the message.
    fn from(&self) -> Address;
    /// Returns the destination address of the message.
    fn to(&self) -> Address;
    /// Returns the message sequence or nonce.
    fn sequence(&self) -> u64;
    /// Returns the amount sent in message.
    fn value(&self) -> &TokenAmount;
    /// Returns the gas limit for the message.
    fn gas_limit(&self) -> u64;
    /// Returns the required funds for the message.
    fn required_funds(&self) -> TokenAmount;
    /// gets gas fee cap for the message.
    fn gas_fee_cap(&self) -> &TokenAmount;
    /// gets gas premium for the message.
    fn gas_premium(&self) -> &TokenAmount;
    /// This method returns the effective gas premium claimable by the miner
    /// given the supplied base fee. This method is not used anywhere except the `Eth` API.
    ///
    /// Filecoin clamps the gas premium at `gas_fee_cap` - `base_fee`, if lower than the
    /// specified premium. Returns 0 if `gas_fee_cap` is less than `base_fee`.
    fn effective_gas_premium(&self, base_fee: &TokenAmount) -> TokenAmount {
        let available = self.gas_fee_cap() - base_fee;
        // It's possible that storage providers may include messages with gasFeeCap less than the baseFee
        // In such cases, their reward should be viewed as zero
        available.clamp(TokenAmount::zero(), self.gas_premium().clone())
    }
}

/// Semantic validation and validates the message has enough gas.
pub fn valid_for_block_inclusion(
    msg: &Message,
    min_gas: Gas,
    version: NetworkVersion,
) -> anyhow::Result<()> {
    use crate::shim::address::ZERO_ADDRESS;
    use crate::shim::econ::{BLOCK_GAS_LIMIT, TOTAL_FILECOIN};
    if msg.version() != 0 {
        anyhow::bail!("Message version: {} not supported", msg.version());
    }
    if msg.to() == *ZERO_ADDRESS && version >= NetworkVersion::V7 {
        anyhow::bail!("invalid 'to' address");
    }
    if msg.value().is_negative() {
        anyhow::bail!("message value cannot be negative");
    }
    if *msg.value() > *TOTAL_FILECOIN {
        anyhow::bail!("message value cannot be greater than total FIL supply");
    }
    if msg.gas_fee_cap().is_negative() {
        anyhow::bail!("gas_fee_cap cannot be negative");
    }
    if msg.gas_premium().is_negative() {
        anyhow::bail!("gas_premium cannot be negative");
    }
    if msg.gas_premium() > msg.gas_fee_cap() {
        anyhow::bail!("gas_fee_cap less than gas_premium");
    }
    if msg.gas_limit() > BLOCK_GAS_LIMIT {
        anyhow::bail!(
            "gas_limit {} cannot be greater than block gas limit",
            msg.gas_limit()
        );
    }

    if Gas::new(msg.gas_limit()) < min_gas {
        anyhow::bail!(
            "gas_limit {} cannot be less than cost {} of storing a message on chain",
            msg.gas_limit(),
            min_gas
        );
    }

    Ok(())
}

#[cfg(test)]
mod tests {
    mod builder_test;

    use rstest::rstest;

    use super::*;
    use crate::shim::crypto::{SECP_SIG_LEN, Signature};
    use crate::shim::gas::price_list_by_network_version;

    #[test]
    fn gas_limit_below_min_gas_rejected_for_block_inclusion() {
        let msg = Message::builder().gas_limit(0).build();
        let err = valid_for_block_inclusion(&msg, Gas::new(1), NetworkVersion::V0)
            .expect_err("gas_limit below min_gas must be rejected");
        assert!(
            err.to_string().contains("less than cost"),
            "expected the gas-limit floor to reject, got: {err}"
        );
    }

    /// The floor is charged over the signed encoding for `Secp256k1` and delegated messages, so a
    /// gas limit that only covers the unsigned encoding must be rejected.
    ///
    /// See Lotus [`checkMsg`](https://github.com/filecoin-project/lotus/blob/a56f7b7399c2804cdab1a230b0efbd92b1694617/chain/consensus/common.go#L217-L223).
    #[rstest]
    #[case(Signature::new_secp256k1(vec![0; SECP_SIG_LEN]))]
    #[case(Signature::new_delegated(vec![0; SECP_SIG_LEN]))]
    fn block_inclusion_floor_counts_signature_bytes(#[case] signature: Signature) {
        let network_version = NetworkVersion::V29;
        let price_list = price_list_by_network_version(network_version);
        let signed = SignedMessage::new_unchecked(
            Message::builder()
                .to(Address::new_id(1))
                .from(Address::new_id(2))
                .build(),
            signature,
        );

        let floor = |len| price_list.on_chain_message(len).total();
        let signed_floor = floor(signed.chain_length().unwrap());
        let unsigned_floor = floor(signed.vm_message().chain_length().unwrap());
        assert!(
            signed_floor > unsigned_floor,
            "signature bytes must raise the floor, got {signed_floor} and {unsigned_floor}"
        );

        let underpaying = signed
            .message()
            .clone()
            .into_builder()
            .gas_limit(unsigned_floor.round_up())
            .build();
        assert!(
            valid_for_block_inclusion(&underpaying, unsigned_floor, network_version).is_ok(),
            "this is the message the unsigned floor used to accept"
        );
        assert!(
            valid_for_block_inclusion(&underpaying, signed_floor, network_version).is_err(),
            "a gas limit covering only the unsigned encoding must be rejected"
        );

        let paying = signed
            .message()
            .clone()
            .into_builder()
            .gas_limit(signed_floor.round_up())
            .build();
        valid_for_block_inclusion(&paying, signed_floor, network_version)
            .expect("a gas limit covering the signed encoding must be accepted");
    }

    #[test]
    fn vm_message_is_the_unsigned_message() {
        let message = Message::builder()
            .to(Address::new_id(1))
            .from(Address::new_id(2))
            .build();
        let signed = SignedMessage::new_unchecked(
            message.clone(),
            Signature::new_secp256k1(vec![0; SECP_SIG_LEN]),
        );

        assert_eq!(message.vm_message(), &message);
        assert_eq!(signed.vm_message(), &message);
    }

    // Test cases from the FIP-0115
    // <https://github.com/filecoin-project/FIPs/blob/b84b89a34ccb3d239493392a7867d6b082193b38/FIPS/fip-0115.md#premium>>
    #[rstest]
    #[case::fee_cap_equals_base_fee(8, 8, 8, 0)]
    #[case::premium_below_headroom(8, 16, 7, 7)]
    #[case::premium_well_below_headroom(8, 19, 10, 10)]
    #[case::fee_cap_below_base_fee(123456, 123455, 123455, 0)]
    #[case::premium_capped_by_headroom(123456, 1234567, 1111112, 1111111)]
    fn test_effective_gas_premium(
        #[case] base_fee: u64,
        #[case] gas_fee_cap: u64,
        #[case] gas_premium: u64,
        #[case] expected: u64,
    ) {
        let msg = Message::builder()
            .gas_fee_cap(TokenAmount::from_atto(gas_fee_cap))
            .gas_premium(TokenAmount::from_atto(gas_premium))
            .build();

        let result = msg.effective_gas_premium(&TokenAmount::from_atto(base_fee));
        assert_eq!(result, TokenAmount::from_atto(expected));
    }
}