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;
#[auto_impl::auto_impl(&, Arc)]
#[delegatable_trait]
pub trait MessageRead {
fn vm_message(&self) -> &Message;
fn chain_length(&self) -> anyhow::Result<usize>;
fn from(&self) -> Address;
fn to(&self) -> Address;
fn sequence(&self) -> u64;
fn value(&self) -> &TokenAmount;
fn gas_limit(&self) -> u64;
fn required_funds(&self) -> TokenAmount;
fn gas_fee_cap(&self) -> &TokenAmount;
fn gas_premium(&self) -> &TokenAmount;
fn effective_gas_premium(&self, base_fee: &TokenAmount) -> TokenAmount {
let available = self.gas_fee_cap() - base_fee;
available.clamp(TokenAmount::zero(), self.gas_premium().clone())
}
}
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}"
);
}
#[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);
}
#[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));
}
}