Skip to main content

forest/message_pool/msgpool/
msg_pool.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4// Contains the implementation of Message Pool component.
5// The Message Pool is the component of forest that handles pending messages for
6// inclusion in the chain. Messages are added either directly for locally
7// published messages or through pubsub propagation.
8
9use crate::blocks::{CachingBlockHeader, Tipset, TipsetKey};
10use crate::chain::{HeadChanges, MINIMUM_BASE_FEE};
11use crate::eth::is_valid_eth_tx_for_sending;
12use crate::libp2p::{NetworkMessage, PUBSUB_MSG_STR, Topic};
13use crate::message::{ChainMessage, MessageRead as _, SignedMessage, valid_for_block_inclusion};
14use crate::message_pool::{
15    config::MpoolConfig,
16    errors::Error,
17    msgpool::{
18        BASE_FEE_LOWER_BOUND_FACTOR_CONSERVATIVE, events::MpoolSubscriber,
19        pending_store::PendingStore, recover_sig, republish::RepublishState,
20    },
21    provider::{Provider, ProviderExt},
22    utils::get_base_fee_lower_bound,
23};
24use crate::networks::{ChainConfig, NEWEST_NETWORK_VERSION};
25use crate::prelude::*;
26use crate::rpc::eth::types::EthAddress;
27use crate::shim::{
28    address::{Address, Protocol},
29    crypto::{Signature, SignatureType},
30    econ::TokenAmount,
31    gas::{Gas, price_list_by_network_version},
32    state_tree::ActorState,
33};
34use crate::state_manager::IdToAddressCache;
35use crate::state_manager::utils::is_valid_for_sending;
36use crate::utils::cache::SizeTrackingCache;
37use ahash::HashSet;
38use futures::StreamExt;
39use fvm_ipld_encoding::to_vec;
40use get_size2::GetSize;
41use itertools::Itertools;
42use nonzero_ext::nonzero;
43use parking_lot::RwLock as SyncRwLock;
44use std::num::NonZeroUsize;
45use std::time::Duration;
46use tokio::{sync::broadcast::error::RecvError, task::JoinSet, time::interval};
47use tracing::warn;
48
49/// Maximum size of a serialized message in bytes. Anti-DoS measure to keep
50/// the pool from ingesting pathologically large messages.
51const MAX_MESSAGE_SIZE: usize = 64 << 10; // 64 KiB
52
53pub(in crate::message_pool) const MAX_ACTOR_PENDING_MESSAGES: u64 = 1000;
54pub(in crate::message_pool) const MAX_UNTRUSTED_ACTOR_PENDING_MESSAGES: u64 = 100;
55
56// Cache sizes have been taken from the lotus implementation
57const BLS_SIG_CACHE_SIZE: NonZeroUsize = nonzero!(40000usize);
58const SIG_VAL_CACHE_SIZE: NonZeroUsize = nonzero!(32000usize);
59const KEY_CACHE_SIZE: NonZeroUsize = nonzero!(1_048_576usize);
60const STATE_NONCE_CACHE_SIZE: NonZeroUsize = nonzero!(32768usize);
61
62#[derive(Clone, Debug, Hash, PartialEq, Eq, GetSize)]
63pub(in crate::message_pool) struct StateNonceCacheKey {
64    tipset_key: TipsetKey,
65    addr: Address,
66}
67
68/// Trust policy for whether a message is from a trusted or untrusted source.
69/// Untrusted sources are subject to stricter limits.
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub(in crate::message_pool) enum TrustPolicy {
72    Trusted,
73    Untrusted,
74}
75
76pub use super::msg_set::{MsgSetLimits, StrictnessPolicy};
77
78/// Caches owned by [`MessagePool`].
79pub(in crate::message_pool) struct Caches {
80    pub(in crate::message_pool) bls_sig: SizeTrackingCache<CidWrapper, Signature>,
81    pub(in crate::message_pool) sig_val: SizeTrackingCache<CidWrapper, ()>,
82    pub(in crate::message_pool) key: IdToAddressCache,
83    pub(in crate::message_pool) state_nonce: SizeTrackingCache<StateNonceCacheKey, u64>,
84}
85
86impl Caches {
87    pub(in crate::message_pool) fn new() -> Self {
88        Self {
89            bls_sig: SizeTrackingCache::new_with_metrics("bls_sig", BLS_SIG_CACHE_SIZE),
90            sig_val: SizeTrackingCache::new_with_metrics("sig_val", SIG_VAL_CACHE_SIZE),
91            key: SizeTrackingCache::new_with_metrics("mpool_key", KEY_CACHE_SIZE),
92            state_nonce: SizeTrackingCache::new_with_metrics("state_nonce", STATE_NONCE_CACHE_SIZE),
93        }
94    }
95}
96
97impl ShallowClone for Caches {
98    fn shallow_clone(&self) -> Self {
99        Self {
100            bls_sig: self.bls_sig.shallow_clone(),
101            sig_val: self.sig_val.shallow_clone(),
102            key: self.key.shallow_clone(),
103            state_nonce: self.state_nonce.shallow_clone(),
104        }
105    }
106}
107
108/// This contains all necessary information needed for the message pool.
109/// Keeps track of messages to apply, as well as context needed for verifying
110/// transactions.
111pub struct MessagePool<T> {
112    /// Pending messages, keyed by resolved-key address, together with the
113    /// broadcast channel for [`MpoolUpdate`](super::events::MpoolUpdate) events. See [`PendingStore`].
114    pub(in crate::message_pool) pending: PendingStore,
115    pub(in crate::message_pool) caches: Caches,
116    /// Resolved-key senders of locally submitted messages.
117    pub(in crate::message_pool) local_addrs: Arc<SyncRwLock<HashSet<Address>>>,
118    /// The current tipset (a set of blocks)
119    pub(in crate::message_pool) cur_tipset: Arc<SyncRwLock<Tipset>>,
120    /// The underlying provider
121    pub(in crate::message_pool) api: Arc<T>,
122    /// Sender half to send messages to other components
123    pub(in crate::message_pool) network_sender: flume::Sender<NetworkMessage>,
124    /// Republish coordination state
125    pub(in crate::message_pool) republish: Arc<RepublishState>,
126    /// Configurable parameters of the message pool.
127    pub(in crate::message_pool) config: Arc<MpoolConfig>,
128    /// Chain configuration
129    pub(in crate::message_pool) chain_config: Arc<ChainConfig>,
130}
131
132impl<T> ShallowClone for MessagePool<T> {
133    fn shallow_clone(&self) -> Self {
134        Self {
135            pending: self.pending.shallow_clone(),
136            caches: self.caches.shallow_clone(),
137            local_addrs: self.local_addrs.shallow_clone(),
138            cur_tipset: self.cur_tipset.shallow_clone(),
139            api: self.api.shallow_clone(),
140            network_sender: self.network_sender.clone(),
141            republish: self.republish.shallow_clone(),
142            config: self.config.shallow_clone(),
143            chain_config: self.chain_config.shallow_clone(),
144        }
145    }
146}
147
148/// Resolve an address to its key form, checking the cache first.
149/// Non-ID addresses are returned unchanged.
150pub(in crate::message_pool) async fn resolve_to_key<T: Provider + Send + Sync + 'static>(
151    api: &Arc<T>,
152    key_cache: &IdToAddressCache,
153    addr: &Address,
154    cur_ts: &Tipset,
155) -> Result<Address, Error> {
156    let id = addr.id().ok();
157    if let Some(id) = &id
158        && let Some(resolved) = key_cache.get(id)
159    {
160        return Ok(resolved);
161    }
162    let resolved = api
163        .resolve_to_deterministic_address_at_finality_async(*addr, cur_ts.clone())
164        .await?;
165    if let Some(id) = id {
166        key_cache.insert(id, resolved);
167    }
168    Ok(resolved)
169}
170
171impl<T> MessagePool<T>
172where
173    T: Provider,
174{
175    /// Gets the current tipset
176    pub fn current_tipset(&self) -> Tipset {
177        self.cur_tipset.read().clone()
178    }
179
180    pub(in crate::message_pool) async fn resolve_to_key(
181        &self,
182        addr: &Address,
183        cur_ts: &Tipset,
184    ) -> Result<Address, Error>
185    where
186        T: Send + Sync + 'static,
187    {
188        resolve_to_key(&self.api, &self.caches.key, addr, cur_ts).await
189    }
190
191    /// Record the resolved-key sender of a locally-submitted message so the
192    /// republish loop can find it on its next sweep.
193    async fn add_local(&self, m: &SignedMessage) -> Result<(), Error>
194    where
195        T: Send + Sync + 'static,
196    {
197        let cur_ts = self.current_tipset();
198        let resolved = self.resolve_to_key(&m.from(), &cur_ts).await?;
199        self.local_addrs.write().insert(resolved);
200        Ok(())
201    }
202
203    /// Push a signed message to the `MessagePool`. Records the sender as
204    /// local and broadcasts on gossip if validation marks it publishable.
205    async fn push_internal(
206        &self,
207        msg: SignedMessage,
208        trust_policy: TrustPolicy,
209    ) -> Result<Cid, Error>
210    where
211        T: Send + Sync + 'static,
212    {
213        let cid = msg.cid();
214        let publish = self.add_to_pool(msg.clone(), true, trust_policy).await?;
215        self.add_local(&msg).await?;
216        if publish {
217            self.publish_pubsub(&msg).await?;
218        }
219        Ok(cid)
220    }
221
222    /// Broadcast a signed message on the network's `gossipsub` topic.
223    pub(in crate::message_pool) async fn publish_pubsub(
224        &self,
225        msg: &SignedMessage,
226    ) -> Result<(), Error> {
227        let message = to_vec(msg)?;
228        let network_name = self.chain_config.network.genesis_name();
229        self.network_sender
230            .send_async(NetworkMessage::PubsubMessage {
231                topic: Topic::new(format!("{PUBSUB_MSG_STR}/{network_name}")),
232                message,
233            })
234            .await
235            .map_err(|_| Error::Other("Network receiver dropped".to_string()))
236    }
237
238    /// Push a signed message to the `MessagePool` from an trusted source.
239    pub async fn push(&self, msg: SignedMessage) -> Result<Cid, Error>
240    where
241        T: Send + Sync + 'static,
242    {
243        self.push_internal(msg, TrustPolicy::Trusted).await
244    }
245
246    /// Push a signed message to the `MessagePool` from an untrusted source.
247    pub async fn push_untrusted(&self, msg: SignedMessage) -> Result<Cid, Error>
248    where
249        T: Send + Sync + 'static,
250    {
251        self.push_internal(msg, TrustPolicy::Untrusted).await
252    }
253
254    /// Insert a message received via gossip. Runs full validation. Does
255    /// not publish back to the network.
256    pub async fn add(&self, msg: SignedMessage) -> Result<(), Error>
257    where
258        T: Send + Sync + 'static,
259    {
260        self.add_to_pool(msg, false, TrustPolicy::Trusted).await?;
261        Ok(())
262    }
263
264    /// Message validation.
265    ///
266    /// Returns `publish: bool` — `true` when the message should be gossiped
267    /// after insertion; `false` when a local sender's message failed the
268    /// soft base-fee floor (kept locally, not broadcast).
269    pub(in crate::message_pool) async fn validate_for_pool(
270        &self,
271        msg: &SignedMessage,
272        cur_ts: &Tipset,
273        local: bool,
274    ) -> Result<bool, Error>
275    where
276        T: Send + Sync + 'static,
277    {
278        validate_static(msg)?;
279        validate_signature(msg, &self.caches.sig_val, self.chain_config.eth_chain_id)?;
280
281        let expected_sequence = self.get_state_sequence(&msg.from(), cur_ts).await?;
282        let sender_actor = self.api.get_actor_after(&msg.from(), cur_ts)?;
283
284        validate_with_state(
285            msg,
286            &self.chain_config,
287            cur_ts,
288            &sender_actor,
289            expected_sequence,
290            local,
291        )
292    }
293
294    /// Validate `msg` and insert it into the pending pool.
295    ///
296    /// Returns `publish: bool` (see [`Self::validate_for_pool`]).
297    pub(in crate::message_pool) async fn add_to_pool(
298        &self,
299        msg: SignedMessage,
300        local: bool,
301        trust_policy: TrustPolicy,
302    ) -> Result<bool, Error>
303    where
304        T: Send + Sync + 'static,
305    {
306        let cur_ts = self.current_tipset();
307        let publish = self.validate_for_pool(&msg, &cur_ts, local).await?;
308        let strictness = if local {
309            StrictnessPolicy::Relaxed
310        } else {
311            StrictnessPolicy::Strict
312        };
313        self.add_to_pool_unchecked(&cur_ts, msg, trust_policy, strictness)
314            .await?;
315        Ok(publish)
316    }
317
318    /// Insert a message into the pending pool *without* running validation
319    /// (size, sig, base-fee, sender-actor checks). The reorg replay path
320    /// uses this directly to restore reverted messages even when they no
321    /// longer pass the add-time filters.
322    pub(in crate::message_pool) async fn add_to_pool_unchecked(
323        &self,
324        cur_ts: &Tipset,
325        msg: SignedMessage,
326        trust_policy: TrustPolicy,
327        strictness: StrictnessPolicy,
328    ) -> Result<(), Error>
329    where
330        T: Send + Sync + 'static,
331    {
332        if msg.signature().signature_type() == SignatureType::Bls {
333            self.caches
334                .bls_sig
335                .insert(msg.cid().into(), msg.signature().clone());
336        }
337
338        self.api
339            .put_message(&ChainMessage::Signed(msg.clone().into()))?;
340        self.api
341            .put_message(&ChainMessage::Unsigned(msg.message().clone().into()))?;
342
343        let sequence = self.get_state_sequence(&msg.from(), cur_ts).await?;
344        let resolved_from = self.resolve_to_key(&msg.from(), cur_ts).await?;
345        self.pending
346            .insert(resolved_from, msg, sequence, trust_policy, strictness)
347    }
348
349    /// Get the sequence for a given address, return Error if there is a failure
350    /// to retrieve the respective sequence.
351    pub async fn get_sequence(&self, addr: &Address) -> Result<u64, Error>
352    where
353        T: Send + Sync + 'static,
354    {
355        let cur_ts = self.current_tipset();
356
357        let sequence = self.get_state_sequence(addr, &cur_ts).await?;
358
359        let resolved = self.resolve_to_key(addr, &cur_ts).await.ok();
360        let mset = resolved
361            .and_then(|r| self.pending.snapshot_for(&r))
362            .or_else(|| self.pending.snapshot_for(addr));
363        match mset {
364            Some(mset) => {
365                if sequence > mset.next_sequence {
366                    return Ok(sequence);
367                }
368                Ok(mset.next_sequence)
369            }
370            None => Ok(sequence),
371        }
372    }
373
374    /// Get the state nonce for an address in `cur_ts`, accounting for
375    /// messages already included in that tipset. Cached by `(TipsetKey,
376    /// Address)`.
377    pub(in crate::message_pool) async fn get_state_sequence(
378        &self,
379        addr: &Address,
380        cur_ts: &Tipset,
381    ) -> Result<u64, Error>
382    where
383        T: Send + Sync + 'static,
384    {
385        let nk = StateNonceCacheKey {
386            tipset_key: cur_ts.key().clone(),
387            addr: *addr,
388        };
389
390        if let Some(cached) = self.caches.state_nonce.get(&nk) {
391            return Ok(cached);
392        }
393
394        let actor = self.api.get_actor_after(addr, cur_ts)?;
395        let mut next_nonce = actor.sequence;
396
397        let resolved = self
398            .resolve_to_key(addr, cur_ts)
399            .await
400            .inspect_err(|e| tracing::warn!(%addr, "failed to resolve address to key: {e:#}"));
401        let messages = self
402            .api
403            .messages_for_tipset(cur_ts)
404            .inspect_err(|e| tracing::warn!("failed to get messages for tipset: {e:#}"));
405        if let (Ok(resolved), Ok(messages)) = (resolved, messages) {
406            for msg in messages.iter() {
407                if let Ok(from) = self.resolve_to_key(&msg.from(), cur_ts).await.inspect_err(
408                    |e| tracing::warn!(from = %msg.from(), "failed to resolve message sender: {e:#}"),
409                ) && from == resolved
410                {
411                    let n = msg.sequence() + 1;
412                    if n > next_nonce {
413                        next_nonce = n;
414                    }
415                }
416            }
417        }
418
419        self.caches.state_nonce.insert(nk, next_nonce);
420        Ok(next_nonce)
421    }
422
423    /// Return a tuple that contains a vector of all signed messages and the
424    /// current tipset for self.
425    pub fn pending(&self) -> (Vec<SignedMessage>, Tipset) {
426        let snapshot = self.pending.snapshot();
427        let len = snapshot.values().map(|mset| mset.msgs.len()).sum();
428        let mut out = Vec::with_capacity(len);
429
430        for mset in snapshot.into_values() {
431            out.extend(
432                mset.msgs
433                    .into_values()
434                    .sorted_unstable_by_key(|m| m.message().sequence),
435            );
436        }
437
438        let cur_ts = self.current_tipset();
439
440        (out, cur_ts)
441    }
442
443    /// Return a Vector of signed messages for a given from address. This vector
444    /// will be sorted by each `message`'s sequence. If no corresponding
445    /// messages found, return None result type.
446    pub async fn pending_for(&self, a: &Address) -> Option<Vec<SignedMessage>>
447    where
448        T: Send + Sync + 'static,
449    {
450        let cur_ts = self.current_tipset();
451        let resolved = self
452            .resolve_to_key(a, &cur_ts)
453            .await
454            .inspect_err(|e| tracing::debug!(%a, "pending_for: failed to resolve address: {e:#}"))
455            .ok()?;
456        let mset = self.pending.snapshot_for(&resolved)?;
457        if mset.msgs.is_empty() {
458            return None;
459        }
460
461        Some(
462            mset.msgs
463                .into_values()
464                .sorted_by_key(|v| v.message().sequence)
465                .collect(),
466        )
467    }
468
469    /// A subscribe-only handle to the [`MpoolUpdate`](super::events::MpoolUpdate) bus, the single entry
470    /// point for observing insertions into and removals from the pending pool.
471    pub fn subscriber(&self) -> MpoolSubscriber {
472        self.pending.subscriber()
473    }
474
475    /// Return Vector of signed messages given a block header for self.
476    pub fn messages_for_blocks<'a>(
477        &self,
478        blks: impl Iterator<Item = &'a CachingBlockHeader>,
479    ) -> Result<Vec<SignedMessage>, Error> {
480        let mut msg_vec: Vec<SignedMessage> = Vec::new();
481
482        for block in blks {
483            let (umsg, mut smsgs) = self.api.messages_for_block(block)?;
484
485            msg_vec.append(smsgs.as_mut());
486            for msg in umsg {
487                let smsg = recover_sig(&self.caches.bls_sig, msg)?;
488                msg_vec.push(smsg)
489            }
490        }
491        Ok(msg_vec)
492    }
493
494    pub fn gas_limit_overestimation(&self) -> f64 {
495        self.config.gas_limit_overestimation
496    }
497
498    pub fn config(&self) -> MpoolConfig {
499        (*self.config).clone()
500    }
501}
502
503impl<T> MessagePool<T>
504where
505    T: Provider + Send + Sync + 'static,
506{
507    /// Creates a new `MessagePool` instance.
508    pub fn new(
509        api: T,
510        network_sender: flume::Sender<NetworkMessage>,
511        config: MpoolConfig,
512        chain_config: Arc<ChainConfig>,
513        services: &mut JoinSet<anyhow::Result<()>>,
514    ) -> Result<Self, Error>
515    where
516        T: Provider,
517    {
518        // Per-actor limits are constant for the lifetime of this pool; capture
519        // them once here rather than re-reading on every insert.
520        let pending = PendingStore::new(MsgSetLimits::new(
521            api.max_actor_pending_messages(),
522            api.max_untrusted_actor_pending_messages(),
523        ));
524        let cur_tipset = Arc::new(SyncRwLock::new(api.get_heaviest_tipset()));
525        let republish_interval =
526            u64::from(10 * chain_config.block_delay_secs + chain_config.propagation_delay_secs);
527        let (republish, repub_trigger_rx) = RepublishState::new();
528
529        let mp = MessagePool {
530            pending,
531            caches: Caches::new(),
532            local_addrs: Arc::new(SyncRwLock::new(HashSet::default())),
533            republish: Arc::new(republish),
534            cur_tipset,
535            api: Arc::new(api),
536            network_sender,
537            config: Arc::new(config),
538            chain_config,
539        };
540
541        // Reacts to new HeadChanges
542        {
543            let mp = mp.shallow_clone();
544            let mut head_changes_rx = mp.api.subscribe_head_changes();
545            services.spawn(async move {
546                loop {
547                    match head_changes_rx.recv().await {
548                        Ok(HeadChanges { reverts, applies }) => {
549                            if let Err(e) = mp.apply_head_change(reverts, applies).await {
550                                tracing::warn!("Error changing head: {e}");
551                            }
552                        }
553                        Err(RecvError::Lagged(e)) => {
554                            warn!("Head change subscriber lagged: skipping {e} events");
555                        }
556                        Err(RecvError::Closed) => {
557                            break Ok(());
558                        }
559                    }
560                }
561            });
562        }
563
564        // Reacts to republishing requests
565        {
566            let mp = mp.shallow_clone();
567            services.spawn(async move {
568                let mut repub_trigger_rx = repub_trigger_rx.stream();
569                let mut interval = interval(Duration::from_secs(republish_interval));
570                loop {
571                    tokio::select! {
572                        _ = interval.tick() => (),
573                        _ = repub_trigger_rx.next() => (),
574                    }
575                    if let Err(e) = mp.run_republish_cycle().await {
576                        warn!("Failed to republish pending messages: {}", e.to_string());
577                    }
578                }
579            });
580        }
581
582        Ok(mp)
583    }
584}
585
586fn validate_static(msg: &SignedMessage) -> Result<(), Error> {
587    if to_vec(msg)?.len() > MAX_MESSAGE_SIZE {
588        return Err(Error::MessageTooBig);
589    }
590    let to = msg.message().to();
591    if to.protocol() == Protocol::Delegated {
592        EthAddress::from_filecoin_address(&to).context(format!(
593            "message recipient {to} is a delegated address but not a valid Eth Address"
594        ))?;
595    }
596    valid_for_block_inclusion(msg.message(), Gas::new(0), NEWEST_NETWORK_VERSION)?;
597    if msg.gas_fee_cap().atto() < &MINIMUM_BASE_FEE.into() {
598        return Err(Error::GasFeeCapTooLow);
599    }
600    Ok(())
601}
602
603fn validate_signature(
604    msg: &SignedMessage,
605    sig_val_cache: &SizeTrackingCache<CidWrapper, ()>,
606    eth_chain_id: u64,
607) -> Result<(), Error> {
608    let cid = msg.cid();
609    if sig_val_cache.get(&cid).is_some() {
610        return Ok(());
611    }
612    msg.verify(eth_chain_id)
613        .map_err(|e| Error::Other(e.to_string()))?;
614    sig_val_cache.insert(cid.into(), ());
615    Ok(())
616}
617
618/// Check the message against the pre-resolved chain state.
619fn validate_with_state(
620    msg: &SignedMessage,
621    chain_config: &ChainConfig,
622    cur_ts: &Tipset,
623    sender_actor: &ActorState,
624    expected_sequence: u64,
625    local: bool,
626) -> Result<bool, Error> {
627    if expected_sequence > msg.message().sequence {
628        return Err(Error::SequenceTooLow);
629    }
630
631    // The message can only be included in the next epoch and beyond, hence the +1.
632    let nv_next = chain_config.network_version(cur_ts.epoch() + 1);
633    if msg.is_delegated() && !is_valid_eth_tx_for_sending(chain_config.eth_chain_id, nv_next, msg) {
634        return Err(Error::Other(
635            "Invalid Ethereum message for the current network version".to_owned(),
636        ));
637    }
638    if !is_valid_for_sending(nv_next, sender_actor) {
639        return Err(Error::Other(
640            "Sender actor is not a valid top-level sender".to_owned(),
641        ));
642    }
643
644    let nv_cur = chain_config.network_version(cur_ts.epoch());
645    let min_gas = price_list_by_network_version(nv_cur).on_chain_message(msg.chain_length()?);
646    valid_for_block_inclusion(msg.message(), min_gas.total(), NEWEST_NETWORK_VERSION)?;
647
648    let publish = check_base_fee_floor(msg, cur_ts, local)?;
649
650    let balance = TokenAmount::from(&sender_actor.balance);
651    let required = msg.required_funds();
652    if balance < required {
653        return Err(Error::NotEnoughFunds { balance, required });
654    }
655
656    Ok(publish)
657}
658
659/// Base-Fee floor check.
660pub(in crate::message_pool) fn check_base_fee_floor(
661    msg: &SignedMessage,
662    cur_ts: &Tipset,
663    local: bool,
664) -> Result<bool, Error> {
665    let base_fee = &cur_ts.block_headers().first().parent_base_fee;
666    let lb = get_base_fee_lower_bound(base_fee, BASE_FEE_LOWER_BOUND_FACTOR_CONSERVATIVE);
667    if msg.gas_fee_cap() >= lb {
668        return Ok(local);
669    }
670    if local {
671        warn!(
672            "local message will not be immediately published because GasFeeCap doesn't meet the lower bound for inclusion in the next 20 blocks (GasFeeCap: {}, baseFeeLowerBound: {})",
673            msg.gas_fee_cap(),
674            lb
675        );
676        return Ok(false);
677    }
678    Err(Error::SoftValidationFailure(format!(
679        "GasFeeCap doesn't meet base fee lower bound for inclusion in the next 20 blocks (GasFeeCap: {}, baseFeeLowerBound:{})",
680        msg.gas_fee_cap(),
681        lb
682    )))
683}
684
685#[cfg(test)]
686mod tests {
687    use crate::blocks::RawBlockHeader;
688    use crate::chain::ChainStore;
689    use crate::db::{DbImpl, MemoryDB};
690    use crate::message_pool::provider::Provider;
691    use crate::message_pool::test_provider::TestApi;
692    use crate::networks::ChainConfig;
693    use crate::shim::econ::TokenAmount;
694    use crate::shim::state_tree::{ActorState, StateTree, StateTreeVersion};
695    use crate::test_utils::dummy_ticket;
696    use crate::utils::db::CborStoreExt as _;
697
698    use super::*;
699    use crate::shim::message::Message as ShimMessage;
700
701    use tokio::task::JoinSet;
702
703    fn make_smsg(from: Address, seq: u64, premium: u64) -> SignedMessage {
704        SignedMessage::mock_bls_signed_message(ShimMessage {
705            from,
706            sequence: seq,
707            gas_premium: TokenAmount::from_atto(premium),
708            gas_limit: 1_000_000,
709            ..ShimMessage::default()
710        })
711    }
712
713    fn make_test_mpool(api: TestApi) -> (MessagePool<TestApi>, JoinSet<anyhow::Result<()>>) {
714        let (tx, _rx) = flume::bounded(50);
715        let mut services = JoinSet::new();
716        let mpool = MessagePool::new(
717            api,
718            tx,
719            Default::default(),
720            Default::default(),
721            &mut services,
722        )
723        .unwrap();
724        (mpool, services)
725    }
726
727    // Regression test for https://github.com/ChainSafe/forest/pull/6118 which fixed a bogus 100M
728    // gas limit. There are no limits on a single message.
729    #[tokio::test]
730    async fn add_to_pool_unchecked_accepts_high_gas_limit() {
731        let api = TestApi::default();
732        let (mpool, _services) = make_test_mpool(api);
733        let cur_ts = mpool.current_tipset();
734        let message = ShimMessage {
735            gas_limit: 666_666_666,
736            ..ShimMessage::default()
737        };
738        let msg = SignedMessage::mock_bls_signed_message(message);
739        let res = mpool
740            .add_to_pool_unchecked(
741                &cur_ts,
742                msg,
743                TrustPolicy::Trusted,
744                StrictnessPolicy::Relaxed,
745            )
746            .await;
747        assert!(res.is_ok());
748    }
749
750    #[tokio::test]
751    async fn test_resolve_to_key_returns_non_id_unchanged() {
752        let api = TestApi::default();
753        let (mpool, _services) = make_test_mpool(api);
754        let ts = mpool.current_tipset();
755
756        let bls_addr = Address::new_bls(&[1u8; 48]).unwrap();
757        let result = mpool.resolve_to_key(&bls_addr, &ts).await.unwrap();
758        assert_eq!(result, bls_addr);
759        assert_eq!(
760            mpool.caches.key.len(),
761            0,
762            "cache should not be populated for non-ID addresses"
763        );
764    }
765
766    #[tokio::test]
767    async fn test_resolve_to_key_resolves_id_and_caches() {
768        let api = TestApi::default();
769        let id_addr = Address::new_id(100);
770        let key_addr = Address::new_bls(&[5u8; 48]).unwrap();
771        api.set_key_address_mapping(&id_addr, &key_addr);
772
773        let (mpool, _services) = make_test_mpool(api);
774        let ts = mpool.current_tipset();
775
776        let result = mpool.resolve_to_key(&id_addr, &ts).await.unwrap();
777        assert_eq!(result, key_addr);
778        assert_eq!(
779            mpool.caches.key.len(),
780            1,
781            "cache should have one entry after resolution"
782        );
783
784        // Second call should hit the cache (no API call needed)
785        let result2 = mpool.resolve_to_key(&id_addr, &ts).await.unwrap();
786        assert_eq!(result2, key_addr);
787    }
788
789    #[tokio::test]
790    async fn test_add_to_pool_unchecked_keys_pending_by_resolved_address() {
791        let api = TestApi::default();
792        let id_addr = Address::new_id(200);
793        let key_addr = Address::new_bls(&[7u8; 48]).unwrap();
794        api.set_key_address_mapping(&id_addr, &key_addr);
795        api.set_state_sequence(&key_addr, 0);
796
797        let (mpool, _services) = make_test_mpool(api);
798        let cur_ts = mpool.current_tipset();
799
800        let message = ShimMessage {
801            from: id_addr,
802            gas_limit: 1_000_000,
803            ..ShimMessage::default()
804        };
805        let msg = SignedMessage::mock_bls_signed_message(message);
806
807        mpool
808            .add_to_pool_unchecked(
809                &cur_ts,
810                msg,
811                TrustPolicy::Trusted,
812                StrictnessPolicy::Relaxed,
813            )
814            .await
815            .unwrap();
816
817        assert!(
818            mpool.pending.snapshot_for(&key_addr).is_some(),
819            "pending should be keyed by the resolved key address"
820        );
821        assert!(
822            mpool.pending.snapshot_for(&id_addr).is_none(),
823            "pending should NOT have an entry under the raw ID address"
824        );
825    }
826
827    #[tokio::test]
828    async fn test_get_sequence_works_with_both_address_forms() {
829        let api = TestApi::default();
830        let id_addr = Address::new_id(300);
831        let key_addr = Address::new_bls(&[9u8; 48]).unwrap();
832        api.set_key_address_mapping(&id_addr, &key_addr);
833        api.set_state_sequence(&key_addr, 0);
834
835        let (mpool, _services) = make_test_mpool(api);
836        let cur_ts = mpool.current_tipset();
837
838        // Add two messages from the ID address
839        for seq in 0..2 {
840            let message = ShimMessage {
841                from: id_addr,
842                sequence: seq,
843                gas_limit: 1_000_000,
844                ..ShimMessage::default()
845            };
846            let msg = SignedMessage::mock_bls_signed_message(message);
847            mpool
848                .add_to_pool_unchecked(
849                    &cur_ts,
850                    msg,
851                    TrustPolicy::Trusted,
852                    StrictnessPolicy::Relaxed,
853                )
854                .await
855                .unwrap();
856        }
857
858        let state_seq = mpool
859            .api
860            .get_actor_after(&id_addr, &cur_ts)
861            .unwrap()
862            .sequence;
863        let resolved_for_id = mpool.resolve_to_key(&id_addr, &cur_ts).await.unwrap();
864        let resolved_for_key = mpool.resolve_to_key(&key_addr, &cur_ts).await.unwrap();
865        assert_eq!(resolved_for_id, resolved_for_key);
866
867        let next_seq = mpool
868            .pending
869            .snapshot_for(&resolved_for_id)
870            .unwrap()
871            .next_sequence;
872        let expected = std::cmp::max(state_seq, next_seq);
873        assert_eq!(expected, 2, "should reflect both pending messages");
874    }
875
876    #[tokio::test]
877    async fn test_get_state_sequence_accounts_for_tipset_messages() {
878        use crate::message_pool::test_provider::mock_block;
879
880        let api = TestApi::default();
881        let sender = Address::new_bls(&[3u8; 48]).unwrap();
882        api.set_state_sequence(&sender, 5);
883
884        let block = mock_block(1, 1);
885        api.inner.lock().set_block_messages(
886            &block,
887            vec![make_smsg(sender, 5, 100), make_smsg(sender, 7, 100)],
888        );
889        let ts = Tipset::from(block);
890
891        let (mpool, _services) = make_test_mpool(api);
892
893        let nonce = mpool.get_state_sequence(&sender, &ts).await.unwrap();
894        assert_eq!(
895            nonce, 8,
896            "should account for non-consecutive tipset message at nonce 7"
897        );
898    }
899
900    #[tokio::test]
901    async fn test_get_state_sequence_ignores_other_addresses() {
902        use crate::message_pool::test_provider::mock_block;
903
904        let api = TestApi::default();
905        let addr_a = Address::new_bls(&[4u8; 48]).unwrap();
906        let addr_b = Address::new_bls(&[5u8; 48]).unwrap();
907        api.set_state_sequence(&addr_a, 0);
908        api.set_state_sequence(&addr_b, 0);
909
910        let block = mock_block(1, 1);
911        api.inner.lock().set_block_messages(
912            &block,
913            vec![
914                make_smsg(addr_b, 0, 100),
915                make_smsg(addr_b, 1, 100),
916                make_smsg(addr_b, 2, 100),
917            ],
918        );
919        let ts = Tipset::from(block);
920
921        let (mpool, _services) = make_test_mpool(api);
922
923        let nonce_a = mpool.get_state_sequence(&addr_a, &ts).await.unwrap();
924        assert_eq!(
925            nonce_a, 0,
926            "addr_a nonce should be unaffected by addr_b's messages"
927        );
928
929        let nonce_b = mpool.get_state_sequence(&addr_b, &ts).await.unwrap();
930        assert_eq!(
931            nonce_b, 3,
932            "addr_b nonce should reflect its tipset messages"
933        );
934    }
935
936    #[tokio::test]
937    async fn test_get_state_sequence_cache_hit() {
938        use crate::message_pool::test_provider::mock_block;
939
940        let api = TestApi::default();
941        let sender = Address::new_bls(&[6u8; 48]).unwrap();
942        api.set_state_sequence(&sender, 5);
943
944        let block = mock_block(1, 1);
945        api.inner
946            .lock()
947            .set_block_messages(&block, vec![make_smsg(sender, 5, 100)]);
948        let ts = Tipset::from(block);
949
950        let (mpool, _services) = make_test_mpool(api);
951
952        let nonce1 = mpool.get_state_sequence(&sender, &ts).await.unwrap();
953        assert_eq!(nonce1, 6);
954
955        // Mutate the underlying state; the cache should still return the old value.
956        mpool.api.set_state_sequence(&sender, 99);
957        let nonce2 = mpool.get_state_sequence(&sender, &ts).await.unwrap();
958        assert_eq!(
959            nonce2, 6,
960            "second call should return the cached value, not re-read state"
961        );
962    }
963
964    #[tokio::test]
965    async fn test_get_state_sequence_cache_miss_on_different_tipset() {
966        use crate::message_pool::test_provider::mock_block;
967
968        let api = TestApi::default();
969        let sender = Address::new_bls(&[7u8; 48]).unwrap();
970        api.set_state_sequence(&sender, 10);
971
972        let (mpool, _services) = make_test_mpool(api);
973
974        let block_a = mock_block(1, 1);
975        let ts_a = Tipset::from(&block_a);
976
977        let nonce_a = mpool.get_state_sequence(&sender, &ts_a).await.unwrap();
978        assert_eq!(nonce_a, 10);
979
980        // Different tipset should be a cache miss and re-read state.
981        mpool.api.set_state_sequence(&sender, 20);
982        let block_b = mock_block(2, 2);
983        let ts_b = Tipset::from(&block_b);
984
985        let nonce_b = mpool.get_state_sequence(&sender, &ts_b).await.unwrap();
986        assert_eq!(
987            nonce_b, 20,
988            "different tipset should miss the cache and read fresh state"
989        );
990    }
991
992    #[test]
993    fn resolve_to_key_uses_finality_lookback() {
994        let db: DbImpl = Arc::new(MemoryDB::default()).into();
995
996        let mut cfg = ChainConfig::default();
997        cfg.policy.chain_finality = 1;
998        let cfg = Arc::new(cfg);
999
1000        let bls_a = Address::new_bls(&[8u8; 48]).unwrap();
1001        let bls_b = Address::new_bls(&[9u8; 48]).unwrap();
1002
1003        // root_a: only contains f0300
1004        let mut st_a = StateTree::new(&db, StateTreeVersion::V5).unwrap();
1005        st_a.set_actor(
1006            &Address::new_id(300),
1007            ActorState::new_empty(Cid::default(), Some(bls_a)),
1008        )
1009        .unwrap();
1010        let root_a = st_a.flush().unwrap();
1011
1012        // root_b: only contains f0400
1013        let mut st_b = StateTree::new(&db, StateTreeVersion::V5).unwrap();
1014        st_b.set_actor(
1015            &Address::new_id(400),
1016            ActorState::new_empty(Cid::default(), Some(bls_b)),
1017        )
1018        .unwrap();
1019        let root_b = st_b.flush().unwrap();
1020
1021        let genesis = Tipset::from(CachingBlockHeader::new(RawBlockHeader {
1022            ticket: dummy_ticket(0),
1023            state_root: root_a,
1024            ..Default::default()
1025        }));
1026        db.put_cbor_default(genesis.block_headers().first())
1027            .unwrap();
1028
1029        let ts1 = Tipset::from(CachingBlockHeader::new(RawBlockHeader {
1030            parents: genesis.key().clone(),
1031            ticket: dummy_ticket(1),
1032            epoch: 1,
1033            state_root: root_a,
1034            timestamp: 1,
1035            ..Default::default()
1036        }));
1037        db.put_cbor_default(ts1.block_headers().first()).unwrap();
1038
1039        let head = Tipset::from(CachingBlockHeader::new(RawBlockHeader {
1040            parents: ts1.key().clone(),
1041            ticket: dummy_ticket(2),
1042            epoch: 2,
1043            state_root: root_b,
1044            timestamp: 2,
1045            ..Default::default()
1046        }));
1047        db.put_cbor_default(head.block_headers().first()).unwrap();
1048
1049        let cs = ChainStore::new(db, cfg, genesis.block_headers().first().clone()).unwrap();
1050
1051        // f0300 exists in lookback state (root_a) → resolves successfully.
1052        let result = Provider::resolve_to_deterministic_address_at_finality(
1053            &cs,
1054            &Address::new_id(300),
1055            &head,
1056        )
1057        .unwrap();
1058        assert_eq!(result, bls_a);
1059
1060        // f0400 exists only in head state (root_b), not in lookback → fails.
1061        Provider::resolve_to_deterministic_address_at_finality(&cs, &Address::new_id(400), &head)
1062            .expect_err("actor only in head state must not resolve via finality lookback");
1063    }
1064}