Skip to main content

forest/message_pool/msgpool/
pending_store.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4//! Pending message storage and event broadcast.
5
6use hashbrown::HashMap;
7use parking_lot::RwLock as SyncRwLock;
8use tokio::sync::broadcast;
9
10use crate::message::SignedMessage;
11use crate::message_pool::errors::Error;
12use crate::message_pool::msgpool::events::{
13    MPOOL_UPDATE_CHANNEL_CAPACITY, MpoolSubscriber, MpoolUpdate,
14};
15use crate::message_pool::msgpool::msg_pool::TrustPolicy;
16use crate::message_pool::msgpool::msg_set::{MsgSet, MsgSetLimits, StrictnessPolicy};
17use crate::prelude::*;
18use crate::shim::address::Address;
19use crate::utils::broadcast::has_subscribers;
20
21/// Owns the per-actor [`MsgSet`] map and the [`MpoolUpdate`] broadcast
22/// channel. The single place where the pending map is mutated.
23pub(in crate::message_pool) struct PendingStore {
24    inner: Arc<Inner>,
25}
26
27impl ShallowClone for PendingStore {
28    fn shallow_clone(&self) -> Self {
29        Self {
30            inner: self.inner.shallow_clone(),
31        }
32    }
33}
34
35struct Inner {
36    /// Per-resolved-address pending messages.
37    pending: SyncRwLock<HashMap<Address, MsgSet>>,
38    /// Broadcast channel for [`MpoolUpdate`] events.
39    events: broadcast::Sender<MpoolUpdate>,
40    /// Per-actor pending-message caps captured once from the provider.
41    limits: MsgSetLimits,
42}
43
44impl PendingStore {
45    /// Construct an empty store with the given per-actor limits.
46    pub(in crate::message_pool) fn new(limits: MsgSetLimits) -> Self {
47        let (events, _) = broadcast::channel(MPOOL_UPDATE_CHANNEL_CAPACITY);
48        let inner = Arc::new(Inner {
49            pending: SyncRwLock::new(HashMap::new()),
50            events,
51            limits,
52        });
53        crate::metrics::register_collector(Box::new(InnerMetricsCollector(inner.shallow_clone())));
54        Self { inner }
55    }
56
57    /// Insert a signed message under its already-resolved sender address.
58    ///
59    /// On success, emits a single [`MpoolUpdate::Add`] carrying the inserted
60    /// message.
61    pub(in crate::message_pool) fn insert(
62        &self,
63        resolved_from: Address,
64        msg: SignedMessage,
65        state_sequence: u64,
66        trust: TrustPolicy,
67        strictness: StrictnessPolicy,
68    ) -> Result<(), Error> {
69        let event_msg = has_subscribers(&self.inner.events).then(|| msg.clone());
70
71        {
72            let mut pending = self.inner.pending.write();
73            let mset = pending
74                .entry(resolved_from)
75                .or_insert_with(|| MsgSet::new(state_sequence));
76            mset.add(msg, strictness, trust, self.inner.limits)?;
77        }
78
79        if let Some(m) = event_msg {
80            // send() only fails when there are zero receivers.
81            let _ = self.inner.events.send(MpoolUpdate::Add(m));
82        }
83        Ok(())
84    }
85
86    /// Remove the message at `sequence` for `from` (which must already be in
87    /// resolved-key form).
88    /// Returns the removed message if one was present. Emits a single
89    /// [`MpoolUpdate::Remove`] per actual removal
90    pub(in crate::message_pool) fn remove(
91        &self,
92        from: &Address,
93        sequence: u64,
94        applied: bool,
95    ) -> Option<SignedMessage> {
96        let removed = {
97            let mut pending = self.inner.pending.write();
98            let mset = pending.get_mut(from)?;
99            let removed = mset.rm(sequence, applied);
100            if mset.msgs.is_empty() {
101                pending.remove(from);
102            }
103            removed
104        };
105
106        if let Some(msg) = &removed
107            && has_subscribers(&self.inner.events)
108        {
109            let _ = self.inner.events.send(MpoolUpdate::Remove(msg.clone()));
110        }
111        removed
112    }
113
114    /// Shrinks the capacity of the internal map as much as possible.
115    pub(in crate::message_pool) fn shrink_to_fit(&self) {
116        self.inner.pending.write().shrink_to_fit();
117    }
118
119    /// Deep-clone of the pending map — one read-lock acquisition.
120    pub(in crate::message_pool) fn snapshot(&self) -> HashMap<Address, MsgSet> {
121        self.inner.pending.read().clone()
122    }
123
124    /// Deep-clone the [`MsgSet`] for a single sender, if present.
125    pub(in crate::message_pool) fn snapshot_for(&self, addr: &Address) -> Option<MsgSet> {
126        self.inner.pending.read().get(addr).cloned()
127    }
128
129    /// A subscribe-only handle to the [`MpoolUpdate`] bus that can mint
130    /// independent receivers on demand without exposing the send half.
131    pub(in crate::message_pool) fn subscriber(&self) -> MpoolSubscriber {
132        MpoolSubscriber::new(self.inner.events.clone())
133    }
134}
135
136#[derive(derive_more::Debug, derive_more::Deref)]
137struct InnerMetricsCollector(#[debug(skip)] Arc<Inner>);
138
139mod metrics_collection {
140    use super::*;
141    use prometheus_client::{
142        collector::Collector,
143        encoding::{DescriptorEncoder, EncodeMetric},
144        metrics::gauge::Gauge,
145        registry::Unit,
146    };
147
148    impl Collector for InnerMetricsCollector {
149        fn encode(&self, mut encoder: DescriptorEncoder) -> Result<(), std::fmt::Error> {
150            {
151                let size_in_bytes = {
152                    let g: Gauge = Default::default();
153                    g.set(self.pending.read().allocation_size() as i64);
154                    g
155                };
156                let size_metric_encoder = encoder.encode_descriptor(
157                    "mpool_pending_size",
158                    "Allocation size of message pool pending messages in bytes",
159                    Some(&Unit::Bytes),
160                    size_in_bytes.metric_type(),
161                )?;
162                size_in_bytes.encode(size_metric_encoder)?;
163            }
164            {
165                let len = {
166                    let g: Gauge = Default::default();
167                    g.set(self.pending.read().len() as i64);
168                    g
169                };
170                let size_metric_encoder = encoder.encode_descriptor(
171                    "mpool_pending_len",
172                    "Length of the message pool pending messages",
173                    None,
174                    len.metric_type(),
175                )?;
176                len.encode(size_metric_encoder)?;
177            }
178            {
179                let cap = {
180                    let g: Gauge = Default::default();
181                    g.set(self.pending.read().capacity() as i64);
182                    g
183                };
184                let size_metric_encoder = encoder.encode_descriptor(
185                    "mpool_pending_cap",
186                    "Capacity of the message pool pending messages",
187                    None,
188                    cap.metric_type(),
189                )?;
190                cap.encode(size_metric_encoder)?;
191            }
192
193            Ok(())
194        }
195    }
196}
197
198#[cfg(test)]
199mod tests {
200    use super::*;
201    use crate::message::MessageRead as _;
202    use crate::shim::econ::TokenAmount;
203    use crate::shim::message::Message as ShimMessage;
204    use tokio::sync::broadcast::error::TryRecvError;
205
206    /// Default limits used by `PendingStore` unit tests. Picked high enough
207    /// that nonce/gap behaviour, not capacity, drives the outcomes.
208    const TEST_LIMITS: MsgSetLimits = MsgSetLimits {
209        trusted: 1000,
210        untrusted: 1000,
211    };
212
213    fn make_smsg(from: Address, seq: u64, premium: u64) -> SignedMessage {
214        SignedMessage::mock_bls_signed_message(ShimMessage {
215            from,
216            sequence: seq,
217            gas_premium: TokenAmount::from_atto(premium),
218            gas_limit: 1_000_000,
219            ..ShimMessage::default()
220        })
221    }
222
223    fn assert_add(update: MpoolUpdate, expected_seq: u64) {
224        match update {
225            MpoolUpdate::Add(m) => assert_eq!(m.sequence(), expected_seq),
226            other => panic!("expected Add, got {other:?}"),
227        }
228    }
229
230    fn assert_remove(update: MpoolUpdate, expected_seq: u64) {
231        match update {
232            MpoolUpdate::Remove(m) => assert_eq!(m.sequence(), expected_seq),
233            other => panic!("expected Remove, got {other:?}"),
234        }
235    }
236
237    #[test]
238    fn insert_emits_add_and_stores_message() {
239        let store = PendingStore::new(TEST_LIMITS);
240        let mut rx = store.subscriber().subscribe();
241        let addr = Address::new_id(1);
242
243        store
244            .insert(
245                addr,
246                make_smsg(addr, 0, 100),
247                0,
248                TrustPolicy::Trusted,
249                StrictnessPolicy::Relaxed,
250            )
251            .unwrap();
252
253        assert_add(rx.try_recv().unwrap(), 0);
254        assert!(
255            matches!(rx.try_recv(), Err(TryRecvError::Empty)),
256            "expected empty channel"
257        );
258        assert_eq!(store.snapshot_for(&addr).unwrap().next_sequence, 1);
259    }
260
261    #[test]
262    fn rbf_replacement_emits_add_for_the_new_message() {
263        let store = PendingStore::new(TEST_LIMITS);
264        let mut rx = store.subscriber().subscribe();
265        let addr = Address::new_id(1);
266
267        store
268            .insert(
269                addr,
270                make_smsg(addr, 0, 100),
271                0,
272                TrustPolicy::Trusted,
273                StrictnessPolicy::Relaxed,
274            )
275            .unwrap();
276        store
277            .insert(
278                addr,
279                make_smsg(addr, 0, 200), // higher premium → RBF
280                0,
281                TrustPolicy::Trusted,
282                StrictnessPolicy::Relaxed,
283            )
284            .unwrap();
285
286        assert_add(rx.try_recv().unwrap(), 0);
287        assert_add(rx.try_recv().unwrap(), 0);
288        assert!(
289            matches!(rx.try_recv(), Err(TryRecvError::Empty)),
290            "expected empty channel"
291        );
292    }
293
294    #[test]
295    fn remove_emits_remove_once_then_is_idempotent() {
296        let store = PendingStore::new(TEST_LIMITS);
297        let mut rx = store.subscriber().subscribe();
298        let addr = Address::new_id(1);
299
300        store
301            .insert(
302                addr,
303                make_smsg(addr, 0, 100),
304                0,
305                TrustPolicy::Trusted,
306                StrictnessPolicy::Relaxed,
307            )
308            .unwrap();
309        let _add = rx.try_recv().unwrap();
310
311        assert!(store.remove(&addr, 0, true).is_some());
312        assert_remove(rx.try_recv().unwrap(), 0);
313
314        // Second remove is a no-op — sender is already gone.
315        assert!(store.remove(&addr, 0, true).is_none());
316        assert!(
317            matches!(rx.try_recv(), Err(TryRecvError::Empty)),
318            "expected empty channel"
319        );
320    }
321
322    #[test]
323    fn remove_of_unknown_sender_is_silent() {
324        let store = PendingStore::new(TEST_LIMITS);
325        let mut rx = store.subscriber().subscribe();
326        let addr = Address::new_id(42);
327
328        assert!(store.remove(&addr, 0, true).is_none());
329        assert!(
330            matches!(rx.try_recv(), Err(TryRecvError::Empty)),
331            "expected empty channel"
332        );
333    }
334
335    #[test]
336    fn insert_without_subscribers_skips_message_clone() {
337        // Regression guard for the has_subscribers fast-path: insert must
338        // succeed and the store must reflect the message even when the emit
339        // branch is elided entirely.
340        let store = PendingStore::new(TEST_LIMITS);
341        let addr = Address::new_id(1);
342
343        assert!(!has_subscribers(&store.inner.events));
344        store
345            .insert(
346                addr,
347                make_smsg(addr, 0, 100),
348                0,
349                TrustPolicy::Trusted,
350                StrictnessPolicy::Relaxed,
351            )
352            .unwrap();
353        assert_eq!(store.snapshot_for(&addr).unwrap().next_sequence, 1);
354    }
355
356    #[test]
357    fn snapshot_is_a_deep_copy() {
358        let store = PendingStore::new(TEST_LIMITS);
359        let addr = Address::new_id(1);
360        store
361            .insert(
362                addr,
363                make_smsg(addr, 0, 100),
364                0,
365                TrustPolicy::Trusted,
366                StrictnessPolicy::Relaxed,
367            )
368            .unwrap();
369
370        let mut snap = store.snapshot();
371        snap.clear();
372        assert!(
373            !store.snapshot().is_empty(),
374            "mutating the snapshot must not affect the store"
375        );
376    }
377
378    #[test]
379    fn clone_is_cheap_and_shares_state() {
380        // The handle pattern: cloning the store gives another view over the
381        // same pending map and the same broadcast channel.
382        let store = PendingStore::new(TEST_LIMITS);
383        let handle = store.shallow_clone();
384        let mut rx = handle.subscriber().subscribe();
385        let addr = Address::new_id(7);
386
387        store
388            .insert(
389                addr,
390                make_smsg(addr, 0, 100),
391                0,
392                TrustPolicy::Trusted,
393                StrictnessPolicy::Relaxed,
394            )
395            .unwrap();
396
397        assert_add(rx.try_recv().unwrap(), 0);
398        assert_eq!(handle.snapshot_for(&addr).unwrap().next_sequence, 1);
399    }
400
401    #[test]
402    fn remove_clears_empty_sender_bucket() {
403        let store = PendingStore::new(TEST_LIMITS);
404        let addr = Address::new_id(1);
405        store
406            .insert(
407                addr,
408                make_smsg(addr, 0, 100),
409                0,
410                TrustPolicy::Trusted,
411                StrictnessPolicy::Relaxed,
412            )
413            .unwrap();
414        store.remove(&addr, 0, true);
415        assert!(
416            store.snapshot().is_empty(),
417            "removing the last message for an actor should drop the bucket"
418        );
419    }
420}