Skip to main content

forest/message_pool/msgpool/
msg_set.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4//! Per-sender message set.
5//!
6//! [`MsgSet`] owns the pending messages for a single sender address and tracks
7//! the next sequence expected for the gap-filling / replace-by-fee rules.
8
9use ahash::{HashMap, HashMapExt};
10
11use crate::message::{MessageRead, SignedMessage};
12use crate::message_pool::errors::Error;
13use crate::message_pool::metrics;
14use crate::message_pool::msg_pool::TrustPolicy;
15use crate::message_pool::msgpool::utils::compute_rbf_min_premium;
16
17/// Maximum allowed nonce gap for trusted message inserts under [`StrictnessPolicy::Strict`].
18pub(in crate::message_pool) const MAX_NONCE_GAP: u64 = 4;
19
20/// Per-actor pending-message limits.
21#[derive(Clone, Copy, Debug)]
22pub struct MsgSetLimits {
23    /// Cap applied when a message is inserted via the trusted path.
24    pub trusted: u64,
25    /// Cap applied when a message is inserted via the untrusted path.
26    pub untrusted: u64,
27}
28
29impl MsgSetLimits {
30    pub fn new(trusted: u64, untrusted: u64) -> Self {
31        Self { trusted, untrusted }
32    }
33}
34
35/// Strictness policy for pending insertion.
36#[derive(Clone, Copy, Debug, PartialEq, Eq)]
37pub enum StrictnessPolicy {
38    Strict,
39    Relaxed,
40}
41
42/// Simple structure that contains a hash-map of messages where k: message nonce,
43/// v: a message which corresponds to that nonce.
44#[derive(Clone, Default, Debug)]
45pub struct MsgSet {
46    pub(in crate::message_pool) msgs: HashMap<u64, SignedMessage>,
47    pub(in crate::message_pool) next_sequence: u64,
48}
49
50impl MsgSet {
51    /// Generate a new `MsgSet` with an empty hash-map and setting the sequence
52    /// specifically.
53    pub fn new(sequence: u64) -> Self {
54        MsgSet {
55            msgs: HashMap::new(),
56            next_sequence: sequence,
57        }
58    }
59
60    /// Insert a message into this set, maintaining `next_sequence`.
61    ///
62    /// - If the message nonce equals `next_sequence`, advance past any
63    ///   consecutive existing messages (gap-filling loop).
64    /// - If the nonce exceeds `next_sequence + max_nonce_gap` and [`StrictnessPolicy::Strict`],
65    ///   reject with [`Error::NonceGap`].
66    /// - Replace-by-fee for an existing nonce is rejected when strict and
67    ///   a nonce gap is present.
68    ///
69    /// [`StrictnessPolicy`] and [`TrustPolicy`] are independent: strictness controls
70    /// whether nonce gap checks run, while [`TrustPolicy`] sets `max_nonce_gap`
71    /// ([`MAX_NONCE_GAP`] for trusted, `0` for untrusted) and selects which cap
72    /// in [`MsgSetLimits`] applies.
73    pub(in crate::message_pool) fn add(
74        &mut self,
75        m: SignedMessage,
76        strictness: StrictnessPolicy,
77        trust: TrustPolicy,
78        limits: MsgSetLimits,
79    ) -> Result<(), Error> {
80        let strict = matches!(strictness, StrictnessPolicy::Strict);
81        let trusted = matches!(trust, TrustPolicy::Trusted);
82        let max_nonce_gap: u64 = if trusted { MAX_NONCE_GAP } else { 0 };
83        let max_actor_pending_messages = if trusted {
84            limits.trusted
85        } else {
86            limits.untrusted
87        };
88
89        let mut next_nonce = self.next_sequence;
90        let nonce_gap = if m.sequence() == next_nonce {
91            next_nonce += 1;
92            while self.msgs.contains_key(&next_nonce) {
93                next_nonce += 1;
94            }
95            false
96        } else if strict && m.sequence() > next_nonce + max_nonce_gap {
97            tracing::debug!(
98                nonce = m.sequence(),
99                next_nonce,
100                "message nonce has too big a gap from expected nonce"
101            );
102            return Err(Error::NonceGap);
103        } else {
104            m.sequence() > next_nonce
105        };
106
107        let has_existing = if let Some(exms) = self.msgs.get(&m.sequence()) {
108            if strict && nonce_gap {
109                tracing::debug!(
110                    nonce = m.sequence(),
111                    next_nonce,
112                    "rejecting replace by fee because of nonce gap"
113                );
114                return Err(Error::NonceGap);
115            }
116            if m.cid() != exms.cid() {
117                let premium = &exms.message().gas_premium;
118                let min_price = compute_rbf_min_premium(premium);
119                if m.message().gas_premium < min_price {
120                    return Err(Error::GasPriceTooLow);
121                }
122            } else {
123                return Err(Error::DuplicateSequence);
124            }
125            true
126        } else {
127            false
128        };
129
130        // Only check the limit when adding a new message, not when replacing an existing one (RBF)
131        if !has_existing && self.msgs.len() as u64 >= max_actor_pending_messages {
132            return Err(Error::TooManyPendingMessages(
133                m.message.from().to_string(),
134                trusted,
135            ));
136        }
137
138        if strict && nonce_gap {
139            tracing::debug!(
140                from = %m.from(),
141                nonce = m.sequence(),
142                next_nonce,
143                "adding nonce-gapped message"
144            );
145        }
146
147        self.next_sequence = next_nonce;
148        if self.msgs.insert(m.sequence(), m).is_none() {
149            metrics::MPOOL_MESSAGE_TOTAL.inc();
150        }
151        Ok(())
152    }
153
154    /// Remove the message at `sequence` and adjust `next_sequence`.
155    ///
156    /// - **Applied** (included on-chain): advance `next_sequence` to
157    ///   `sequence + 1`, then run the gap-filling loop to advance past any
158    ///   consecutive pending messages.
159    /// - **Pruned** (evicted): rewind `next_sequence` to `sequence` if the
160    ///   removal creates a gap.
161    ///
162    /// Returns the removed message when one was present.
163    /// If the sequence was not in the set, no event is removed and [`None`] is returned.
164    pub fn rm(&mut self, sequence: u64, applied: bool) -> Option<SignedMessage> {
165        let Some(removed) = self.msgs.remove(&sequence) else {
166            if applied && sequence >= self.next_sequence {
167                self.next_sequence = sequence + 1;
168                while self.msgs.contains_key(&self.next_sequence) {
169                    self.next_sequence += 1;
170                }
171            }
172            return None;
173        };
174        metrics::MPOOL_MESSAGE_TOTAL.dec();
175
176        // adjust next sequence
177        if applied {
178            // we removed a (known) message because it was applied in a tipset and we need to
179            // advance past it and any consecutive pending successors
180            if sequence >= self.next_sequence {
181                self.next_sequence = sequence + 1;
182                while self.msgs.contains_key(&self.next_sequence) {
183                    self.next_sequence += 1;
184                }
185            }
186        } else if sequence < self.next_sequence {
187            // we removed a message because it was pruned
188            // we have to adjust the sequence if it creates a gap or rewinds state
189            self.next_sequence = sequence;
190        }
191        Some(removed)
192    }
193}
194
195#[cfg(test)]
196mod tests {
197    use super::*;
198    use crate::shim::address::Address;
199    use crate::shim::econ::TokenAmount;
200    use crate::shim::message::Message as ShimMessage;
201
202    fn make_smsg(from: Address, seq: u64, premium: u64) -> SignedMessage {
203        SignedMessage::mock_bls_signed_message(ShimMessage {
204            from,
205            sequence: seq,
206            gas_premium: TokenAmount::from_atto(premium),
207            gas_limit: 1_000_000,
208            ..ShimMessage::default()
209        })
210    }
211
212    // Test that RBF (Replace By Fee) is allowed even when at max_actor_pending_messages capacity
213    // This matches Lotus behavior where the check is: https://github.com/filecoin-project/lotus/blob/5f32d00550ddd2f2d0f9abe97dbae07615f18547/chain/messagepool/messagepool.go#L296-L299
214    #[test]
215    fn rbf_at_capacity() {
216        let limits = MsgSetLimits::new(10, 10);
217        let mut mset = MsgSet::new(0);
218
219        // Fill up to capacity (10 messages)
220        for i in 0..10 {
221            let res = mset.add(
222                make_smsg(Address::default(), i, 100),
223                StrictnessPolicy::Relaxed,
224                TrustPolicy::Trusted,
225                limits,
226            );
227            assert!(res.is_ok(), "Failed to add message {i}");
228        }
229
230        // Should reject adding a NEW message (sequence 10) when at capacity
231        let res = mset.add(
232            make_smsg(Address::default(), 10, 100),
233            StrictnessPolicy::Relaxed,
234            TrustPolicy::Trusted,
235            limits,
236        );
237        assert!(matches!(res, Err(Error::TooManyPendingMessages(_, _))));
238
239        // Should ALLOW replacing an existing message (RBF) even when at capacity
240        // Replace message with sequence 5 with higher gas premium
241        let res = mset.add(
242            make_smsg(Address::default(), 5, 200),
243            StrictnessPolicy::Relaxed,
244            TrustPolicy::Trusted,
245            limits,
246        );
247        assert!(res.is_ok(), "RBF should be allowed at capacity");
248    }
249
250    #[test]
251    fn gap_filling_advances_next_sequence() {
252        let limits = MsgSetLimits::new(1000, 1000);
253        let mut mset = MsgSet::new(0);
254
255        mset.add(
256            make_smsg(Address::default(), 0, 100),
257            StrictnessPolicy::Relaxed,
258            TrustPolicy::Trusted,
259            limits,
260        )
261        .unwrap();
262        assert_eq!(mset.next_sequence, 1);
263
264        mset.add(
265            make_smsg(Address::default(), 2, 100),
266            StrictnessPolicy::Relaxed,
267            TrustPolicy::Trusted,
268            limits,
269        )
270        .unwrap();
271        assert_eq!(mset.next_sequence, 1, "gap at 1, so next_sequence stays");
272
273        mset.add(
274            make_smsg(Address::default(), 1, 100),
275            StrictnessPolicy::Relaxed,
276            TrustPolicy::Trusted,
277            limits,
278        )
279        .unwrap();
280        assert_eq!(
281            mset.next_sequence, 3,
282            "filling the gap should advance past all consecutive messages"
283        );
284    }
285
286    #[test]
287    fn trusted_allows_any_nonce_gap() {
288        let limits = MsgSetLimits::new(1000, 1000);
289        let mut mset = MsgSet::new(0);
290
291        mset.add(
292            make_smsg(Address::default(), 0, 100),
293            StrictnessPolicy::Relaxed,
294            TrustPolicy::Trusted,
295            limits,
296        )
297        .unwrap();
298        let res = mset.add(
299            make_smsg(Address::default(), 10, 100),
300            StrictnessPolicy::Relaxed,
301            TrustPolicy::Trusted,
302            limits,
303        );
304        assert!(
305            res.is_ok(),
306            "trusted adds skip nonce gap enforcement (StrictnessPolicy::Relaxed)"
307        );
308    }
309
310    #[test]
311    fn strict_allows_small_nonce_gap() {
312        let limits = MsgSetLimits::new(1000, 1000);
313        let mut mset = MsgSet::new(0);
314
315        // Strict + trusted -> max_nonce_gap=4 (non-local add path)
316        mset.add(
317            make_smsg(Address::default(), 0, 100),
318            StrictnessPolicy::Strict,
319            TrustPolicy::Trusted,
320            limits,
321        )
322        .unwrap();
323        let res = mset.add(
324            make_smsg(Address::default(), 3, 100),
325            StrictnessPolicy::Strict,
326            TrustPolicy::Trusted,
327            limits,
328        );
329        assert!(
330            res.is_ok(),
331            "strict+trusted: gap of 2 (within MAX_NONCE_GAP=4) should succeed"
332        );
333    }
334
335    #[test]
336    fn strict_rejects_large_nonce_gap() {
337        let limits = MsgSetLimits::new(1000, 1000);
338        let mut mset = MsgSet::new(0);
339
340        // Strict + trusted -> max_nonce_gap=4
341        mset.add(
342            make_smsg(Address::default(), 0, 100),
343            StrictnessPolicy::Strict,
344            TrustPolicy::Trusted,
345            limits,
346        )
347        .unwrap();
348        let res = mset.add(
349            make_smsg(Address::default(), 6, 100),
350            StrictnessPolicy::Strict,
351            TrustPolicy::Trusted,
352            limits,
353        );
354        assert_eq!(
355            res,
356            Err(Error::NonceGap),
357            "strict+trusted: gap of 5 (exceeds MAX_NONCE_GAP=4) should be rejected"
358        );
359    }
360
361    #[test]
362    fn strict_untrusted_rejects_any_gap() {
363        let limits = MsgSetLimits::new(1000, 1000);
364        let mut mset = MsgSet::new(0);
365
366        // Strict + untrusted -> max_nonce_gap=0
367        mset.add(
368            make_smsg(Address::default(), 0, 100),
369            StrictnessPolicy::Strict,
370            TrustPolicy::Untrusted,
371            limits,
372        )
373        .unwrap();
374        let res = mset.add(
375            make_smsg(Address::default(), 2, 100),
376            StrictnessPolicy::Strict,
377            TrustPolicy::Untrusted,
378            limits,
379        );
380        assert_eq!(
381            res,
382            Err(Error::NonceGap),
383            "strict+untrusted: any gap (maxNonceGap=0) is rejected"
384        );
385    }
386
387    #[test]
388    fn non_strict_untrusted_skips_gap_check() {
389        let limits = MsgSetLimits::new(1000, 1000);
390        let mut mset = MsgSet::new(0);
391
392        // Relaxed + untrusted -> gap check skipped (PushUntrusted path)
393        mset.add(
394            make_smsg(Address::default(), 0, 100),
395            StrictnessPolicy::Relaxed,
396            TrustPolicy::Untrusted,
397            limits,
398        )
399        .unwrap();
400        let res = mset.add(
401            make_smsg(Address::default(), 5, 100),
402            StrictnessPolicy::Relaxed,
403            TrustPolicy::Untrusted,
404            limits,
405        );
406        assert!(
407            res.is_ok(),
408            "non-strict untrusted (PushUntrusted) skips gap enforcement"
409        );
410    }
411
412    #[test]
413    fn strict_rbf_during_gap_rejected() {
414        let limits = MsgSetLimits::new(1000, 1000);
415        let mut mset = MsgSet::new(0);
416
417        // Set up a gap using relaxed trusted (local push path)
418        mset.add(
419            make_smsg(Address::default(), 0, 100),
420            StrictnessPolicy::Relaxed,
421            TrustPolicy::Trusted,
422            limits,
423        )
424        .unwrap();
425        mset.add(
426            make_smsg(Address::default(), 2, 100),
427            StrictnessPolicy::Relaxed,
428            TrustPolicy::Trusted,
429            limits,
430        )
431        .unwrap();
432
433        // Strict RBF at nonce 2 should be rejected due to gap at nonce 1
434        let res = mset.add(
435            make_smsg(Address::default(), 2, 200),
436            StrictnessPolicy::Strict,
437            TrustPolicy::Trusted,
438            limits,
439        );
440        assert_eq!(
441            res,
442            Err(Error::NonceGap),
443            "strict RBF should be rejected when nonce gap exists"
444        );
445    }
446
447    #[test]
448    fn rbf_without_gap_still_works() {
449        let limits = MsgSetLimits::new(1000, 1000);
450        let mut mset = MsgSet::new(0);
451
452        mset.add(
453            make_smsg(Address::default(), 0, 100),
454            StrictnessPolicy::Relaxed,
455            TrustPolicy::Trusted,
456            limits,
457        )
458        .unwrap();
459        mset.add(
460            make_smsg(Address::default(), 1, 100),
461            StrictnessPolicy::Relaxed,
462            TrustPolicy::Trusted,
463            limits,
464        )
465        .unwrap();
466        mset.add(
467            make_smsg(Address::default(), 2, 100),
468            StrictnessPolicy::Relaxed,
469            TrustPolicy::Trusted,
470            limits,
471        )
472        .unwrap();
473
474        let res = mset.add(
475            make_smsg(Address::default(), 1, 200),
476            StrictnessPolicy::Relaxed,
477            TrustPolicy::Trusted,
478            limits,
479        );
480        assert!(res.is_ok(), "RBF without a nonce gap should succeed");
481    }
482
483    #[test]
484    fn rm_applied_advances_next_sequence() {
485        let limits = MsgSetLimits::new(1000, 1000);
486        let mut mset = MsgSet::new(0);
487
488        mset.add(
489            make_smsg(Address::default(), 0, 100),
490            StrictnessPolicy::Relaxed,
491            TrustPolicy::Trusted,
492            limits,
493        )
494        .unwrap();
495        assert_eq!(mset.next_sequence, 1);
496
497        // applied=true, and sequence >= next_sequence path: remove advances
498        mset.rm(0, true);
499        assert_eq!(
500            mset.next_sequence, 1,
501            "applied rm at seq < next_sequence does not advance further"
502        );
503
504        // applied=true with an unknown sequence ahead of current: advances
505        mset.rm(5, true);
506        assert_eq!(
507            mset.next_sequence, 6,
508            "applied rm of unknown seq >= next_sequence advances to seq+1"
509        );
510    }
511
512    #[test]
513    fn rm_applied_known_advances_past_pending_successors() {
514        let limits = MsgSetLimits::new(1000, 1000);
515        // next_sequence starts at 5, so nonces 6 and 7 are added as gapped,
516        // leaving next_sequence behind at 5.
517        let mut mset = MsgSet::new(5);
518
519        for seq in [6, 7] {
520            mset.add(
521                make_smsg(Address::default(), seq, 100),
522                StrictnessPolicy::Relaxed,
523                TrustPolicy::Trusted,
524                limits,
525            )
526            .unwrap();
527        }
528        assert_eq!(
529            mset.next_sequence, 5,
530            "gapped adds leave next_sequence at 5"
531        );
532
533        // Nonce 6 (known) is applied while 7 is still pending.
534        mset.rm(6, true);
535        assert_eq!(
536            mset.next_sequence, 8,
537            "must advance past the still-pending nonce 7, not stop at 7"
538        );
539    }
540
541    #[test]
542    fn rm_pruned_rewinds_next_sequence_on_gap() {
543        let limits = MsgSetLimits::new(1000, 1000);
544        let mut mset = MsgSet::new(0);
545
546        // Fill 0..=2 so next_sequence=3
547        for i in 0..3 {
548            mset.add(
549                make_smsg(Address::default(), i, 100),
550                StrictnessPolicy::Relaxed,
551                TrustPolicy::Trusted,
552                limits,
553            )
554            .unwrap();
555        }
556        assert_eq!(mset.next_sequence, 3);
557
558        // applied=false (prune) of seq=1 (< next_sequence): rewind to 1
559        mset.rm(1, false);
560        assert_eq!(
561            mset.next_sequence, 1,
562            "pruned rm creating a gap rewinds next_sequence"
563        );
564    }
565}