Skip to main content

forest/message_pool/
msg_chain.rs

1// Copyright 2019-2026 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3#![allow(clippy::indexing_slicing)]
4use std::{
5    cmp::Ordering,
6    mem,
7    ops::{Index, IndexMut},
8};
9
10use crate::message::{MessageRead as _, SignedMessage};
11use crate::networks::ChainConfig;
12use crate::shim::{
13    address::Address,
14    econ::TokenAmount,
15    gas::{Gas, price_list_by_network_version},
16};
17use crate::{
18    blocks::{BLOCK_MESSAGE_LIMIT, Tipset},
19    shim::crypto::SignatureType,
20};
21use ahash::HashMap;
22use num_traits::Zero;
23use slotmap::{SlotMap, new_key_type};
24use tracing::warn;
25
26use super::errors::Error;
27use crate::message_pool::{
28    provider::Provider,
29    utils::{get_gas_perf, get_gas_reward},
30};
31
32new_key_type! {
33    pub struct NodeKey;
34}
35
36/// Chains is an abstraction of a list of message chain nodes.
37/// It wraps a `SlotMap` instance. `key_vec` is an additional requirement in
38/// order to satisfy optimal `msg` selection use cases, such as iteration in
39/// insertion order. The `SlotMap` serves as a lookup table for nodes to get
40/// around the borrow checker rules. Each `MsgChainNode` contains only pointers
41/// as `NodeKey` to the entries in the map With this design, we get around the
42/// borrow checker rule issues when implementing the optimal selection
43/// algorithm.
44pub(in crate::message_pool) struct Chains {
45    pub map: SlotMap<NodeKey, MsgChainNode>,
46    pub key_vec: Vec<NodeKey>,
47}
48
49impl Chains {
50    // Sort by effective perf with cmp_effective
51    pub(in crate::message_pool) fn sort_effective(&mut self) {
52        let mut chains = mem::take(&mut self.key_vec);
53        chains.sort_by(|a, b| {
54            let a = self.map.get(*a).expect("key_vec keys reference live nodes");
55            let b = self.map.get(*b).expect("key_vec keys reference live nodes");
56            a.cmp_effective(b)
57        });
58        let _ = mem::replace(&mut self.key_vec, chains);
59    }
60
61    // Sort by effective `perf` on a range
62    pub(in crate::message_pool) fn sort_range_effective(
63        &mut self,
64        range: std::ops::RangeFrom<usize>,
65    ) {
66        let mut chains = mem::take(&mut self.key_vec);
67        chains[range].sort_by(|a, b| {
68            self.map
69                .get(*a)
70                .expect("key_vec keys reference live nodes")
71                .cmp_effective(self.map.get(*b).expect("key_vec keys reference live nodes"))
72        });
73        let _ = mem::replace(&mut self.key_vec, chains);
74    }
75
76    /// Retrieves the `msg` chain node by the given `NodeKey` along with the
77    /// data required from previous chain (if exists) to set effective
78    /// performance of this node.
79    pub(in crate::message_pool) fn get_mut_with_prev_eff(
80        &mut self,
81        k: NodeKey,
82    ) -> (Option<&mut MsgChainNode>, Option<(f64, u64)>) {
83        let node = self.map.get(k);
84        let prev = if let Some(node) = node {
85            if let Some(prev_key) = node.prev {
86                let prev_node = self
87                    .map
88                    .get(prev_key)
89                    .expect("prev key references a live node");
90                Some((prev_node.eff_perf, prev_node.gas_limit))
91            } else {
92                None
93            }
94        } else {
95            None
96        };
97
98        let node = self.map.get_mut(k);
99        (node, prev)
100    }
101
102    /// Retrieves the `msg` chain node by the given `NodeKey`
103    pub(in crate::message_pool) fn get(&self, k: NodeKey) -> Option<&MsgChainNode> {
104        self.map.get(k)
105    }
106}
107
108impl Chains {
109    pub(in crate::message_pool) fn new() -> Self {
110        Self {
111            map: SlotMap::with_key(),
112            key_vec: vec![],
113        }
114    }
115
116    /// Pushes a `msg` chain node into slot map and places the key in the
117    /// `node_vec` passed as parameter.
118    pub(in crate::message_pool) fn push_with(
119        &mut self,
120        cur_chain: MsgChainNode,
121        node_vec: &mut Vec<NodeKey>,
122    ) {
123        let key = self.map.insert(cur_chain);
124        node_vec.push(key);
125    }
126
127    /// Sorts the chains with `compare` method. If rev is true, sorts in
128    /// descending order.
129    pub(in crate::message_pool) fn sort(&mut self, rev: bool) {
130        // replace dance to get around borrow checker
131        let mut chains = mem::take(&mut self.key_vec);
132        chains.sort_by(|a, b| {
133            let a = self.map.get(*a).expect("key_vec keys reference live nodes");
134            let b = self.map.get(*b).expect("key_vec keys reference live nodes");
135            if rev { b.compare(a) } else { a.compare(b) }
136        });
137        let _ = mem::replace(&mut self.key_vec, chains);
138    }
139
140    /// Retrieves the `msg` chain node by the given `NodeKey`
141    pub(in crate::message_pool) fn get_mut(&mut self, k: NodeKey) -> Option<&mut MsgChainNode> {
142        self.map.get_mut(k)
143    }
144
145    /// Retrieves the `msg` chain node at the given index
146    pub(in crate::message_pool) fn get_mut_at(&mut self, i: usize) -> Option<&mut MsgChainNode> {
147        let key = self.key_vec.get(i)?;
148        self.get_mut(*key)
149    }
150
151    // Retrieves a msg chain node at the given index in the provided NodeKey vec
152    pub(in crate::message_pool) fn get_from(&self, i: usize, vec: &[NodeKey]) -> &MsgChainNode {
153        #[allow(clippy::indexing_slicing)]
154        self.map
155            .get(vec[i])
156            .expect("node vec keys reference live nodes")
157    }
158
159    // Retrieves a msg chain node at the given index in the provided NodeKey vec
160    pub(in crate::message_pool) fn get_mut_from(
161        &mut self,
162        i: usize,
163        vec: &[NodeKey],
164    ) -> &mut MsgChainNode {
165        #[allow(clippy::indexing_slicing)]
166        self.map
167            .get_mut(vec[i])
168            .expect("node vec keys reference live nodes")
169    }
170
171    // Retrieves the node key at the given index
172    pub(in crate::message_pool) fn get_key_at(&self, i: usize) -> Option<NodeKey> {
173        self.key_vec.get(i).copied()
174    }
175
176    /// Retrieves the `msg` chain node at the given index. Returns `None` if index is out-of-bounds.
177    pub(in crate::message_pool) fn get_at(&self, i: usize) -> Option<&MsgChainNode> {
178        self.map.get(self.get_key_at(i)?)
179    }
180
181    /// Retrieves the amount of items.
182    pub(in crate::message_pool) fn len(&self) -> usize {
183        self.map.len()
184    }
185
186    /// Returns true is the chain is empty and otherwise. We check the map as
187    /// the source of truth as `key_vec` can be extended time to time.
188    pub(in crate::message_pool) fn is_empty(&self) -> bool {
189        self.map.is_empty()
190    }
191
192    /// Removes messages from the given index and resets effective `perfs`
193    #[tracing::instrument(skip_all, level = "debug")]
194    pub(in crate::message_pool) fn trim_msgs_at(
195        &mut self,
196        idx: usize,
197        gas_limit: u64,
198        msg_limit: usize,
199        base_fee: &TokenAmount,
200    ) {
201        let prev = match idx {
202            0 => None,
203            _ => self
204                .get_at(idx - 1)
205                .map(|prev| (prev.eff_perf, prev.gas_limit)),
206        };
207        let chain_node = self
208            .get_mut_at(idx)
209            .expect("caller validates idx is a live chain");
210        let mut i = chain_node.msgs.len() as i64 - 1;
211
212        while i >= 0
213            && (chain_node.gas_limit > gas_limit
214                || chain_node.gas_perf < 0.0
215                || i >= msg_limit as i64)
216        {
217            #[allow(clippy::indexing_slicing)]
218            let msg = &chain_node.msgs[i as usize];
219            let gas_reward = get_gas_reward(msg, base_fee);
220            chain_node.gas_reward -= gas_reward;
221            chain_node.gas_limit = chain_node.gas_limit.saturating_sub(msg.gas_limit());
222            if chain_node.gas_limit > 0 {
223                chain_node.gas_perf = get_gas_perf(&chain_node.gas_reward, chain_node.gas_limit);
224                if chain_node.bp != 0.0 {
225                    chain_node.set_eff_perf(prev);
226                }
227            } else {
228                chain_node.gas_perf = 0.0;
229                chain_node.eff_perf = 0.0;
230            }
231            i -= 1;
232        }
233
234        if i < 0 {
235            chain_node.msgs.clear();
236            chain_node.valid = false;
237        } else {
238            chain_node.msgs.truncate(i as usize + 1);
239        }
240
241        let next = chain_node.next;
242        if next.is_some() {
243            self.invalidate(next);
244        }
245    }
246
247    pub(in crate::message_pool) fn bubble_down_after_trim(&mut self, from: usize) {
248        let mut j = from;
249        while j < self.key_vec.len().saturating_sub(1) {
250            #[allow(clippy::indexing_slicing)]
251            if self[j].compare(&self[j + 1]) == Ordering::Less {
252                break;
253            }
254            self.key_vec.swap(j, j + 1);
255            j += 1;
256        }
257    }
258
259    pub(in crate::message_pool) fn invalidate(&mut self, mut key: Option<NodeKey>) {
260        let mut next_keys = vec![];
261
262        while let Some(nk) = key {
263            let chain_node = self.map.get(nk).expect("chain keys reference live nodes");
264            next_keys.push(nk);
265            key = chain_node.next;
266        }
267
268        for k in next_keys.iter().rev() {
269            if let Some(node) = self.map.get_mut(*k) {
270                node.valid = false;
271                node.msgs.clear();
272                node.next = None;
273            }
274        }
275    }
276
277    /// Drops nodes which are no longer valid after the merge step
278    pub(in crate::message_pool) fn drop_invalid(&mut self, key_vec: &mut Vec<NodeKey>) {
279        let mut valid_keys = vec![];
280        for k in key_vec.iter() {
281            if self
282                .map
283                .get(*k)
284                .map(|n| n.valid)
285                .expect("node vec keys reference live nodes")
286            {
287                valid_keys.push(*k);
288            } else {
289                self.map.remove(*k);
290            }
291        }
292
293        *key_vec = valid_keys;
294    }
295}
296
297impl Index<usize> for Chains {
298    type Output = MsgChainNode;
299    fn index(&self, i: usize) -> &Self::Output {
300        self.get_at(i).expect("index out of bounds")
301    }
302}
303
304impl IndexMut<usize> for Chains {
305    fn index_mut(&mut self, i: usize) -> &mut Self::Output {
306        #[allow(clippy::indexing_slicing)]
307        self.map
308            .get_mut(self.key_vec[i])
309            .expect("key_vec keys reference live nodes")
310    }
311}
312
313/// Represents a node in the `MsgChain`.
314#[derive(Clone, Debug)]
315pub struct MsgChainNode {
316    pub msgs: Vec<SignedMessage>,
317    pub gas_reward: TokenAmount,
318    pub gas_limit: u64,
319    pub gas_perf: f64,
320    pub eff_perf: f64,
321    pub bp: f64,
322    pub parent_offset: f64,
323    pub valid: bool,
324    pub merged: bool,
325    pub next: Option<NodeKey>,
326    pub prev: Option<NodeKey>,
327    pub sig_type: Option<SignatureType>,
328}
329
330impl MsgChainNode {
331    pub fn compare(&self, other: &Self) -> Ordering {
332        if approx_cmp(self.gas_perf, other.gas_perf) == Ordering::Greater
333            || approx_cmp(self.gas_perf, other.gas_perf) == Ordering::Equal
334                && self.gas_reward.cmp(&other.gas_reward) == Ordering::Greater
335        {
336            return Ordering::Greater;
337        }
338
339        Ordering::Less
340    }
341
342    pub fn set_eff_perf(&mut self, prev: Option<(f64, u64)>) {
343        let mut eff_perf = self.gas_perf * self.bp;
344        if let Some(prev) = prev
345            && eff_perf > 0.0
346        {
347            let prev_eff_perf = prev.0;
348            let prev_gas_limit = prev.1;
349            let eff_perf_with_parent = (eff_perf * self.gas_limit as f64
350                + prev_eff_perf * prev_gas_limit as f64)
351                / (self.gas_limit + prev_gas_limit) as f64;
352            self.parent_offset = eff_perf - eff_perf_with_parent;
353            eff_perf = eff_perf_with_parent;
354        }
355        self.eff_perf = eff_perf;
356    }
357}
358
359impl MsgChainNode {
360    pub(in crate::message_pool) fn cmp_effective(&self, other: &Self) -> Ordering {
361        // Highest priority: merged
362        // Comment from Lotus: move merged chains to the front so we can discard them earlier
363        // Note: both cases need to be checked to ensure total ordering. Without it, the standard
364        // libraries' sorting methods may panic (since Rust 1.81).
365        match (self.merged, other.merged) {
366            (true, false) => return Ordering::Greater,
367            (false, true) => return Ordering::Less,
368            _ => {}
369        }
370
371        if self.gas_perf >= 0.0 && other.gas_perf < 0.0
372            || self.eff_perf > other.eff_perf
373            || (approx_cmp(self.eff_perf, other.eff_perf) == Ordering::Equal
374                && self.gas_perf > other.gas_perf)
375            || (approx_cmp(self.eff_perf, other.eff_perf) == Ordering::Equal
376                && approx_cmp(self.gas_perf, other.gas_perf) == Ordering::Equal
377                && self.gas_reward > other.gas_reward)
378        {
379            return Ordering::Greater;
380        }
381
382        Ordering::Less
383    }
384
385    pub fn set_null_effective_perf(&mut self) {
386        if self.gas_perf < 0.0 {
387            self.eff_perf = self.gas_perf;
388        } else {
389            self.eff_perf = 0.0;
390        }
391    }
392}
393
394impl std::default::Default for MsgChainNode {
395    fn default() -> Self {
396        Self {
397            msgs: vec![],
398            gas_reward: TokenAmount::zero(),
399            gas_limit: 0,
400            gas_perf: 0.0,
401            eff_perf: 0.0,
402            bp: 0.0,
403            parent_offset: 0.0,
404            valid: true,
405            merged: false,
406            next: None,
407            prev: None,
408            sig_type: None,
409        }
410    }
411}
412
413pub(in crate::message_pool) fn create_message_chains<T>(
414    api: &T,
415    actor: &Address,
416    mset: &HashMap<u64, SignedMessage>,
417    base_fee: &TokenAmount,
418    ts: &Tipset,
419    chains: &mut Chains,
420    chain_config: &ChainConfig,
421) -> Result<(), Error>
422where
423    T: Provider,
424{
425    // collect all messages and sort
426    let mut msgs: Vec<SignedMessage> = mset.values().cloned().collect();
427    msgs.sort_by_key(|v| v.sequence());
428
429    // sanity checks:
430    // - there can be no gaps in nonces, starting from the current actor nonce if
431    //   there is a gap, drop messages after the gap, we can't include them
432    // - all messages must have minimum gas and the total gas for the candidate
433    //   messages cannot exceed the block limit; drop all messages that exceed the
434    //   limit
435    // - the total gasReward cannot exceed the actor's balance; drop all messages
436    //   that exceed the balance
437    let Ok(actor_state) = api.get_actor_after(actor, ts) else {
438        tracing::warn!("failed to load actor state, not building chain for {actor}");
439        return Ok(());
440    };
441    let mut cur_seq = actor_state.sequence;
442    let mut balance: TokenAmount = TokenAmount::from(&actor_state.balance);
443
444    let mut gas_limit = 0;
445    let mut skip = 0;
446    let mut i = 0;
447    let mut rewards = Vec::with_capacity(msgs.len());
448
449    while let Some(m) = msgs.get(i) {
450        if m.sequence() < cur_seq {
451            warn!(
452                "encountered message from actor {} with nonce {} less than the current nonce {}",
453                actor,
454                m.sequence(),
455                cur_seq
456            );
457            skip += 1;
458            i += 1;
459            continue;
460        }
461
462        if m.sequence() != cur_seq {
463            break;
464        }
465        cur_seq += 1;
466
467        let network_version = chain_config.network_version(ts.epoch());
468
469        let min_gas = price_list_by_network_version(network_version)
470            .on_chain_message(m.chain_length()?)
471            .total();
472
473        if Gas::new(m.gas_limit()) < min_gas {
474            break;
475        }
476        gas_limit += m.gas_limit();
477        if gas_limit > crate::shim::econ::BLOCK_GAS_LIMIT {
478            break;
479        }
480
481        let required = m.required_funds();
482        if balance < required {
483            break;
484        }
485
486        balance -= required;
487        let value = m.value();
488        balance -= value;
489
490        let gas_reward = get_gas_reward(m, base_fee);
491        rewards.push(gas_reward);
492        i += 1;
493    }
494
495    // check we have a sane set of messages to construct the chains
496    let mut msgs = if i > skip {
497        #[allow(clippy::indexing_slicing)]
498        msgs[skip..i].to_vec()
499    } else {
500        return Ok(());
501    };
502
503    // if we have more messages from this sender than can fit in a block, drop the extra ones
504    if msgs.len() > BLOCK_MESSAGE_LIMIT {
505        warn!(
506            "dropping {} messages from {actor} as they exceed the block message limit of {BLOCK_MESSAGE_LIMIT}",
507            msgs.len() - BLOCK_MESSAGE_LIMIT,
508        );
509        msgs.truncate(BLOCK_MESSAGE_LIMIT);
510    };
511
512    let mut cur_chain = MsgChainNode::default();
513    let mut node_vec = vec![];
514
515    let new_chain = |m: SignedMessage, reward: &TokenAmount| -> MsgChainNode {
516        let gl = m.gas_limit();
517        let sig_type = Some(m.signature().sig_type);
518        MsgChainNode {
519            msgs: vec![m],
520            gas_reward: reward.clone(),
521            gas_limit: gl,
522            gas_perf: get_gas_perf(reward, gl),
523            eff_perf: 0.0,
524            bp: 0.0,
525            parent_offset: 0.0,
526            valid: true,
527            merged: false,
528            prev: None,
529            next: None,
530            sig_type,
531        }
532    };
533
534    // creates msg chain nodes in chunks based on gas_perf obtained from the current
535    // chain's gas limit.
536    for (i, (m, reward)) in msgs.into_iter().zip(rewards.iter()).enumerate() {
537        if i == 0 {
538            cur_chain = new_chain(m, reward);
539            continue;
540        }
541
542        let gas_reward = &cur_chain.gas_reward + reward;
543        let gas_limit = cur_chain.gas_limit + m.gas_limit();
544        let gas_perf = get_gas_perf(&gas_reward, gas_limit);
545
546        // try to add the message to the current chain -- if it decreases the gasPerf,
547        // then make a new chain
548        if gas_perf < cur_chain.gas_perf {
549            chains.push_with(cur_chain, &mut node_vec);
550            cur_chain = new_chain(m, reward);
551        } else {
552            cur_chain.msgs.push(m);
553            cur_chain.gas_reward = gas_reward;
554            cur_chain.gas_limit = gas_limit;
555            cur_chain.gas_perf = gas_perf;
556        }
557    }
558
559    chains.push_with(cur_chain, &mut node_vec);
560
561    // merge chains to maintain the invariant: higher gas perf nodes on the front.
562    loop {
563        let mut merged = 0;
564        for i in (1..node_vec.len()).rev() {
565            if chains.get_from(i, &node_vec).gas_perf >= chains.get_from(i - 1, &node_vec).gas_perf
566            {
567                // copy messages
568                let chain_i_msg = chains.get_from(i, &node_vec).msgs.clone();
569                chains
570                    .get_mut_from(i - 1, &node_vec)
571                    .msgs
572                    .extend(chain_i_msg);
573
574                // set gas reward
575                let chain_i_gas_reward = chains.get_from(i, &node_vec).gas_reward.clone();
576                chains.get_mut_from(i - 1, &node_vec).gas_reward += chain_i_gas_reward;
577
578                // set gas limit
579                let chain_i_gas_limit = chains.get_from(i, &node_vec).gas_limit;
580                chains.get_mut_from(i - 1, &node_vec).gas_limit += chain_i_gas_limit;
581
582                // set gas perf
583                let chain_i_gas_perf = get_gas_perf(
584                    &chains.get_from(i - 1, &node_vec).gas_reward,
585                    chains.get_from(i - 1, &node_vec).gas_limit,
586                );
587                chains.get_mut_from(i - 1, &node_vec).gas_perf = chain_i_gas_perf;
588                // invalidate the current chain as it is merged with the prev chain
589                chains.get_mut_from(i, &node_vec).valid = false;
590                merged += 1;
591            }
592        }
593
594        if merged == 0 {
595            break;
596        }
597
598        chains.drop_invalid(&mut node_vec);
599    }
600
601    if node_vec.len() > 1 {
602        for (&k1, &k2) in node_vec.iter().zip(node_vec.iter().skip(1)) {
603            // link next pointers
604            let n1 = chains
605                .get_mut(k1)
606                .ok_or_else(|| Error::Other(format!("{k1:?} should present in `chains`")))?;
607            n1.next = Some(k2);
608            // Should we link or clear n1.prev as well?
609
610            // link prev pointers
611            let n2 = chains
612                .get_mut(k2)
613                .ok_or_else(|| Error::Other(format!("{k2:?} should present in `chains`")))?;
614            n2.prev = Some(k1);
615            // Should we link or clear n2.next as well?
616        }
617    }
618
619    // Update the main chain key_vec with this node_vec
620    chains.key_vec.extend(node_vec);
621
622    Ok(())
623}
624
625fn approx_cmp(a: f64, b: f64) -> Ordering {
626    if (a - b).abs() <= (a * f64::EPSILON).abs() {
627        Ordering::Equal
628    } else {
629        a.total_cmp(&b)
630    }
631}
632
633#[cfg(test)]
634mod tests {
635    use super::*;
636
637    #[test]
638    fn approx_cmp_nan_does_not_panic() {
639        for (a, b) in [
640            (f64::NAN, 1.0),
641            (1.0, f64::NAN),
642            (f64::NAN, f64::NAN),
643            (f64::INFINITY, f64::NAN),
644        ] {
645            let _ = approx_cmp(a, b);
646        }
647    }
648}