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