forest/blocks/
tipset.rs

1// Copyright 2019-2025 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4use std::{
5    fmt,
6    sync::{Arc, OnceLock},
7};
8
9use super::{Block, CachingBlockHeader, RawBlockHeader, Ticket};
10use crate::{
11    chain_sync::TipsetValidator,
12    cid_collections::SmallCidNonEmptyVec,
13    networks::{calibnet, mainnet},
14    shim::clock::ChainEpoch,
15    utils::{cid::CidCborExt, get_size::nunny_vec_heap_size_helper},
16};
17use ahash::HashMap;
18use anyhow::Context as _;
19use cid::Cid;
20use fvm_ipld_blockstore::Blockstore;
21use fvm_ipld_encoding::CborStore;
22use get_size2::GetSize;
23use itertools::Itertools as _;
24use num::BigInt;
25use nunny::{Vec as NonEmpty, vec as nonempty};
26use serde::{Deserialize, Serialize};
27use thiserror::Error;
28use tracing::info;
29
30/// A set of `CIDs` forming a unique key for a Tipset.
31/// Equal keys will have equivalent iteration order, but note that the `CIDs`
32/// are *not* maintained in the same order as the canonical iteration order of
33/// blocks in a tipset (which is by ticket)
34#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, PartialOrd, Ord, GetSize)]
35#[cfg_attr(test, derive(derive_quickcheck_arbitrary::Arbitrary))]
36pub struct TipsetKey(SmallCidNonEmptyVec);
37
38impl TipsetKey {
39    // Special encoding to match Lotus.
40    pub fn cid(&self) -> anyhow::Result<Cid> {
41        use fvm_ipld_encoding::RawBytes;
42
43        let mut bytes = Vec::new();
44        for cid in self.to_cids() {
45            bytes.append(&mut cid.to_bytes())
46        }
47        Ok(Cid::from_cbor_blake2b256(&RawBytes::new(bytes))?)
48    }
49
50    /// Returns `true` if the tipset key contains the given CID.
51    pub fn contains(&self, cid: Cid) -> bool {
52        self.0.contains(cid)
53    }
54
55    /// Returns a non-empty collection of `CID`
56    pub fn into_cids(self) -> NonEmpty<Cid> {
57        self.0.into_cids()
58    }
59
60    /// Returns a non-empty collection of `CID`
61    pub fn to_cids(&self) -> NonEmpty<Cid> {
62        self.0.clone().into_cids()
63    }
64
65    /// Returns an iterator of `CID`s.
66    pub fn iter(&self) -> impl Iterator<Item = Cid> + '_ {
67        self.0.iter()
68    }
69
70    /// Returns the number of `CID`s
71    pub fn len(&self) -> usize {
72        self.0.len()
73    }
74
75    // To suppress `#[warn(clippy::len_without_is_empty)]`
76    pub fn is_empty(&self) -> bool {
77        false
78    }
79
80    /// Terse representation of the tipset key.
81    /// `bafy2bzaceaqrqoasufr7gdwrbhvlfy2xmc4e5sdzekjgyha2kldxigu73gilo`
82    /// becomes `eaq...ilo`. The `bafy2bzac` prefix is removed.
83    pub fn terse(&self) -> String {
84        fn terse_cid(cid: Cid) -> String {
85            let s = cid::multibase::encode(
86                cid::multibase::Base::Base32Lower,
87                cid.to_bytes().as_slice(),
88            );
89            format!("{}...{}", &s[9..12], &s[s.len() - 3..])
90        }
91        self.to_cids()
92            .into_iter()
93            .map(terse_cid)
94            .collect::<Vec<_>>()
95            .join(", ")
96    }
97
98    /// Formats tipset key to match the Lotus display.
99    pub fn format_lotus(&self) -> String {
100        format!("{{{}}}", self.to_cids().into_iter().join(","))
101    }
102}
103
104impl From<NonEmpty<Cid>> for TipsetKey {
105    fn from(mut value: NonEmpty<Cid>) -> Self {
106        // When `value.capacity() > value.len()`, it takes more heap memory.
107        // Always shrink it since `TipsetKey` is immutable and used in caches.
108        value.shrink_to_fit();
109        Self(value.into())
110    }
111}
112
113impl fmt::Display for TipsetKey {
114    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
115        let s = self
116            .to_cids()
117            .into_iter()
118            .map(|cid| cid.to_string())
119            .collect::<Vec<_>>()
120            .join(", ");
121        write!(f, "[{s}]")
122    }
123}
124
125impl<'a> IntoIterator for &'a TipsetKey {
126    type Item = <&'a SmallCidNonEmptyVec as IntoIterator>::Item;
127
128    type IntoIter = <&'a SmallCidNonEmptyVec as IntoIterator>::IntoIter;
129
130    fn into_iter(self) -> Self::IntoIter {
131        (&self.0).into_iter()
132    }
133}
134
135impl IntoIterator for TipsetKey {
136    type Item = <SmallCidNonEmptyVec as IntoIterator>::Item;
137
138    type IntoIter = <SmallCidNonEmptyVec as IntoIterator>::IntoIter;
139
140    fn into_iter(self) -> Self::IntoIter {
141        self.0.into_iter()
142    }
143}
144
145/// An immutable set of blocks at the same height with the same parent set.
146/// Blocks in a tipset are canonically ordered by ticket size.
147///
148/// Represents non-null tipsets, see the documentation on [`crate::state_manager::apply_block_messages`]
149/// for more.
150#[derive(Clone, Debug, GetSize)]
151pub struct Tipset {
152    /// Sorted
153    #[get_size(size_fn = nunny_vec_heap_size_helper)]
154    headers: NonEmpty<CachingBlockHeader>,
155    // key is lazily initialized via `fn key()`.
156    key: OnceLock<TipsetKey>,
157}
158
159impl From<RawBlockHeader> for Tipset {
160    fn from(value: RawBlockHeader) -> Self {
161        Self::from(CachingBlockHeader::from(value))
162    }
163}
164
165impl From<&CachingBlockHeader> for Tipset {
166    fn from(value: &CachingBlockHeader) -> Self {
167        value.clone().into()
168    }
169}
170
171impl From<CachingBlockHeader> for Tipset {
172    fn from(value: CachingBlockHeader) -> Self {
173        Self {
174            headers: nonempty![value],
175            key: OnceLock::new(),
176        }
177    }
178}
179
180impl From<NonEmpty<CachingBlockHeader>> for Tipset {
181    fn from(headers: NonEmpty<CachingBlockHeader>) -> Self {
182        Self {
183            headers,
184            key: OnceLock::new(),
185        }
186    }
187}
188
189impl PartialEq for Tipset {
190    fn eq(&self, other: &Self) -> bool {
191        self.headers.eq(&other.headers)
192    }
193}
194
195#[cfg(test)]
196impl quickcheck::Arbitrary for Tipset {
197    fn arbitrary(g: &mut quickcheck::Gen) -> Self {
198        // TODO(forest): https://github.com/ChainSafe/forest/issues/3570
199        //               Support random generation of tipsets with multiple blocks.
200        Tipset::from(CachingBlockHeader::arbitrary(g))
201    }
202}
203
204impl From<FullTipset> for Tipset {
205    fn from(full_tipset: FullTipset) -> Self {
206        let key = full_tipset.key;
207        let headers = full_tipset
208            .blocks
209            .into_iter_ne()
210            .map(|block| block.header)
211            .collect_vec();
212
213        Tipset { headers, key }
214    }
215}
216
217#[derive(Error, Debug, PartialEq)]
218pub enum CreateTipsetError {
219    #[error("tipsets must not be empty")]
220    Empty,
221    #[error(
222        "parent CID is inconsistent. All block headers in a tipset must agree on their parent tipset"
223    )]
224    BadParents,
225    #[error(
226        "state root is inconsistent. All block headers in a tipset must agree on their parent state root"
227    )]
228    BadStateRoot,
229    #[error("epoch is inconsistent. All block headers in a tipset must agree on their epoch")]
230    BadEpoch,
231    #[error("duplicate miner address. All miners in a tipset must be unique.")]
232    DuplicateMiner,
233}
234
235#[allow(clippy::len_without_is_empty)]
236impl Tipset {
237    /// Builds a new Tipset from a collection of blocks.
238    /// A valid tipset contains a non-empty collection of blocks that have
239    /// distinct miners and all specify identical epoch, parents, weight,
240    /// height, state root, receipt root; content-id for headers are
241    /// supposed to be distinct but until encoding is added will be equal.
242    pub fn new<H: Into<CachingBlockHeader>>(
243        headers: impl IntoIterator<Item = H>,
244    ) -> Result<Self, CreateTipsetError> {
245        let mut headers = NonEmpty::new(
246            headers
247                .into_iter()
248                .map(Into::<CachingBlockHeader>::into)
249                .sorted_by_cached_key(|it| it.tipset_sort_key())
250                .collect(),
251        )
252        .map_err(|_| CreateTipsetError::Empty)?;
253        headers.shrink_to_fit();
254        verify_block_headers(&headers)?;
255
256        Ok(Self {
257            headers,
258            key: OnceLock::new(),
259        })
260    }
261
262    /// Fetch a tipset from the blockstore. This call fails if the tipset is
263    /// present but invalid. If the tipset is missing, None is returned.
264    pub fn load(store: &impl Blockstore, tsk: &TipsetKey) -> anyhow::Result<Option<Tipset>> {
265        Ok(tsk
266            .to_cids()
267            .into_iter()
268            .map(|key| CachingBlockHeader::load(store, key))
269            .collect::<anyhow::Result<Option<Vec<_>>>>()?
270            .map(Tipset::new)
271            .transpose()?)
272    }
273
274    /// Fetch a tipset from the blockstore. This calls fails if the tipset is
275    /// missing or invalid.
276    pub fn load_required(store: &impl Blockstore, tsk: &TipsetKey) -> anyhow::Result<Tipset> {
277        Tipset::load(store, tsk)?.context("Required tipset missing from database")
278    }
279
280    /// Constructs and returns a full tipset if messages from storage exists
281    pub fn fill_from_blockstore(&self, store: &impl Blockstore) -> Option<FullTipset> {
282        // Find tipset messages. If any are missing, return `None`.
283        let blocks = self
284            .block_headers()
285            .iter()
286            .cloned()
287            .map(|header| {
288                let (bls_messages, secp_messages) =
289                    crate::chain::store::block_messages(store, &header).ok()?;
290                Some(Block {
291                    header,
292                    bls_messages,
293                    secp_messages,
294                })
295            })
296            .collect::<Option<Vec<_>>>()?;
297
298        // the given tipset has already been verified, so this cannot fail
299        Some(
300            FullTipset::new(blocks)
301                .expect("block headers have already been verified so this check cannot fail"),
302        )
303    }
304
305    /// Returns epoch of the tipset.
306    pub fn epoch(&self) -> ChainEpoch {
307        self.min_ticket_block().epoch
308    }
309    pub fn block_headers(&self) -> &NonEmpty<CachingBlockHeader> {
310        &self.headers
311    }
312    pub fn into_block_headers(self) -> NonEmpty<CachingBlockHeader> {
313        self.headers
314    }
315    /// Returns the smallest ticket of all blocks in the tipset
316    pub fn min_ticket(&self) -> Option<&Ticket> {
317        self.min_ticket_block().ticket.as_ref()
318    }
319    /// Returns the block with the smallest ticket of all blocks in the tipset
320    pub fn min_ticket_block(&self) -> &CachingBlockHeader {
321        self.headers.first()
322    }
323    /// Returns the smallest timestamp of all blocks in the tipset
324    pub fn min_timestamp(&self) -> u64 {
325        self.headers
326            .iter()
327            .map(|block| block.timestamp)
328            .min()
329            .unwrap()
330    }
331    /// Returns the number of blocks in the tipset.
332    pub fn len(&self) -> usize {
333        self.headers.len()
334    }
335    /// Returns a key for the tipset.
336    pub fn key(&self) -> &TipsetKey {
337        self.key
338            .get_or_init(|| TipsetKey::from(self.headers.iter_ne().map(|h| *h.cid()).collect_vec()))
339    }
340    /// Returns a non-empty collection of `CIDs` for the current tipset
341    pub fn cids(&self) -> NonEmpty<Cid> {
342        self.key().to_cids()
343    }
344    /// Returns the keys of the parents of the blocks in the tipset.
345    pub fn parents(&self) -> &TipsetKey {
346        &self.min_ticket_block().parents
347    }
348    /// Returns the state root for the tipset parent.
349    pub fn parent_state(&self) -> &Cid {
350        &self.min_ticket_block().state_root
351    }
352    /// Returns the tipset's calculated weight
353    pub fn weight(&self) -> &BigInt {
354        &self.min_ticket_block().weight
355    }
356    /// Returns true if self wins according to the Filecoin tie-break rule
357    /// (FIP-0023)
358    pub fn break_weight_tie(&self, other: &Tipset) -> bool {
359        // blocks are already sorted by ticket
360        let broken = self
361            .block_headers()
362            .iter()
363            .zip(other.block_headers().iter())
364            .any(|(a, b)| {
365                const MSG: &str =
366                    "The function block_sanity_checks should have been called at this point.";
367                let ticket = a.ticket.as_ref().expect(MSG);
368                let other_ticket = b.ticket.as_ref().expect(MSG);
369                ticket.vrfproof < other_ticket.vrfproof
370            });
371        if broken {
372            info!("Weight tie broken in favour of {}", self.key());
373        } else {
374            info!("Weight tie left unbroken, default to {}", other.key());
375        }
376        broken
377    }
378
379    /// Returns an iterator of all tipsets, taking an owned [`Blockstore`]
380    pub fn chain_owned(self, store: impl Blockstore) -> impl Iterator<Item = Tipset> {
381        let mut tipset = Some(self);
382        std::iter::from_fn(move || {
383            let child = tipset.take()?;
384            tipset = Tipset::load_required(&store, child.parents()).ok();
385            Some(child)
386        })
387    }
388
389    /// Returns an iterator of all tipsets
390    pub fn chain(self, store: &impl Blockstore) -> impl Iterator<Item = Tipset> + '_ {
391        let mut tipset = Some(self);
392        std::iter::from_fn(move || {
393            let child = tipset.take()?;
394            tipset = Tipset::load_required(store, child.parents()).ok();
395            Some(child)
396        })
397    }
398
399    /// Returns an iterator of all tipsets
400    pub fn chain_arc(
401        self: Arc<Self>,
402        store: &impl Blockstore,
403    ) -> impl Iterator<Item = Arc<Tipset>> + '_ {
404        let mut tipset = Some(self);
405        std::iter::from_fn(move || {
406            let child = tipset.take()?;
407            tipset = Tipset::load_required(store, child.parents())
408                .ok()
409                .map(Arc::new);
410            Some(child)
411        })
412    }
413
414    /// Fetch the genesis block header for a given tipset.
415    pub fn genesis(&self, store: &impl Blockstore) -> anyhow::Result<CachingBlockHeader> {
416        // Scanning through millions of epochs to find the genesis is quite
417        // slow. Let's use a list of known blocks to short-circuit the search.
418        // The blocks are hash-chained together and known blocks are guaranteed
419        // to have a known genesis.
420        #[derive(Serialize, Deserialize)]
421        struct KnownHeaders {
422            calibnet: HashMap<ChainEpoch, String>,
423            mainnet: HashMap<ChainEpoch, String>,
424        }
425
426        static KNOWN_HEADERS: OnceLock<KnownHeaders> = OnceLock::new();
427        let headers = KNOWN_HEADERS.get_or_init(|| {
428            serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).unwrap()
429        });
430
431        for tipset in self.clone().chain(store) {
432            // Search for known calibnet and mainnet blocks
433            for (genesis_cid, known_blocks) in [
434                (*calibnet::GENESIS_CID, &headers.calibnet),
435                (*mainnet::GENESIS_CID, &headers.mainnet),
436            ] {
437                if let Some(known_block_cid) = known_blocks.get(&tipset.epoch())
438                    && known_block_cid == &tipset.min_ticket_block().cid().to_string()
439                {
440                    return store
441                        .get_cbor(&genesis_cid)?
442                        .context("Genesis block missing from database");
443                }
444            }
445
446            // If no known blocks are found, we'll eventually hit the genesis tipset.
447            if tipset.epoch() == 0 {
448                return Ok(tipset.min_ticket_block().clone());
449            }
450        }
451        anyhow::bail!("Genesis block not found")
452    }
453
454    /// Check if `self` is the child of `other`
455    pub fn is_child_of(&self, other: &Self) -> bool {
456        // Note: the extra `&& self.epoch() > other.epoch()` check in lotus is dropped
457        // See <https://github.com/filecoin-project/lotus/blob/01ec22974942fb7328a1e665704c6cfd75d93372/chain/types/tipset.go#L258>
458        self.parents() == other.key()
459    }
460}
461
462/// `FullTipset` is an expanded version of a tipset that contains all the blocks
463/// and messages.
464#[derive(Debug, Clone, Eq)]
465pub struct FullTipset {
466    blocks: NonEmpty<Block>,
467    // key is lazily initialized via `fn key()`.
468    key: OnceLock<TipsetKey>,
469}
470
471impl std::hash::Hash for FullTipset {
472    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
473        self.key().hash(state)
474    }
475}
476
477// Constructing a FullTipset from a single Block is infallible.
478impl From<Block> for FullTipset {
479    fn from(block: Block) -> Self {
480        FullTipset {
481            blocks: nonempty![block],
482            key: OnceLock::new(),
483        }
484    }
485}
486
487impl PartialEq for FullTipset {
488    fn eq(&self, other: &Self) -> bool {
489        self.blocks.eq(&other.blocks)
490    }
491}
492
493impl FullTipset {
494    pub fn new(blocks: impl IntoIterator<Item = Block>) -> Result<Self, CreateTipsetError> {
495        let blocks = NonEmpty::new(
496            // sort blocks on creation to allow for more seamless conversions between
497            // FullTipset and Tipset
498            blocks
499                .into_iter()
500                .sorted_by_cached_key(|it| it.header.tipset_sort_key())
501                .collect(),
502        )
503        .map_err(|_| CreateTipsetError::Empty)?;
504
505        verify_block_headers(blocks.iter().map(|it| &it.header))?;
506
507        Ok(Self {
508            blocks,
509            key: OnceLock::new(),
510        })
511    }
512    /// Returns the first block of the tipset.
513    fn first_block(&self) -> &Block {
514        self.blocks.first()
515    }
516    /// Returns reference to all blocks in a full tipset.
517    pub fn blocks(&self) -> &NonEmpty<Block> {
518        &self.blocks
519    }
520    /// Returns all blocks in a full tipset.
521    pub fn into_blocks(self) -> NonEmpty<Block> {
522        self.blocks
523    }
524    /// Converts the full tipset into a [Tipset] which removes the messages
525    /// attached.
526    pub fn into_tipset(self) -> Tipset {
527        Tipset::from(self)
528    }
529    /// Returns a key for the tipset.
530    pub fn key(&self) -> &TipsetKey {
531        self.key
532            .get_or_init(|| TipsetKey::from(self.blocks.iter_ne().map(|b| *b.cid()).collect_vec()))
533    }
534    /// Returns the state root for the tipset parent.
535    pub fn parent_state(&self) -> &Cid {
536        &self.first_block().header().state_root
537    }
538    /// Returns the keys of the parents of the blocks in the tipset.
539    pub fn parents(&self) -> &TipsetKey {
540        &self.first_block().header().parents
541    }
542    /// Returns epoch of the tipset.
543    pub fn epoch(&self) -> ChainEpoch {
544        self.first_block().header().epoch
545    }
546    /// Returns the tipset's calculated weight.
547    pub fn weight(&self) -> &BigInt {
548        &self.first_block().header().weight
549    }
550    /// Persists the tipset into the blockstore.
551    pub fn persist(&self, db: &impl Blockstore) -> anyhow::Result<()> {
552        for block in self.blocks() {
553            // To persist `TxMeta` that is required for loading tipset messages
554            TipsetValidator::validate_msg_root(db, block)?;
555            crate::chain::persist_objects(&db, std::iter::once(block.header()))?;
556            crate::chain::persist_objects(&db, block.bls_msgs().iter())?;
557            crate::chain::persist_objects(&db, block.secp_msgs().iter())?;
558        }
559        Ok(())
560    }
561}
562
563fn verify_block_headers<'a>(
564    headers: impl IntoIterator<Item = &'a CachingBlockHeader>,
565) -> Result<(), CreateTipsetError> {
566    use itertools::all;
567
568    let headers =
569        NonEmpty::new(headers.into_iter().collect()).map_err(|_| CreateTipsetError::Empty)?;
570    if !all(&headers, |it| it.parents == headers.first().parents) {
571        return Err(CreateTipsetError::BadParents);
572    }
573    if !all(&headers, |it| it.state_root == headers.first().state_root) {
574        return Err(CreateTipsetError::BadStateRoot);
575    }
576    if !all(&headers, |it| it.epoch == headers.first().epoch) {
577        return Err(CreateTipsetError::BadEpoch);
578    }
579
580    if !headers.iter().map(|it| it.miner_address).all_unique() {
581        return Err(CreateTipsetError::DuplicateMiner);
582    }
583
584    Ok(())
585}
586
587#[cfg_vis::cfg_vis(doc, pub)]
588mod lotus_json {
589    //! [Tipset] isn't just plain old data - it has an invariant (all block headers are valid)
590    //! So there is custom de-serialization here
591
592    use crate::blocks::{CachingBlockHeader, Tipset};
593    use crate::lotus_json::*;
594    use nunny::Vec as NonEmpty;
595    use schemars::JsonSchema;
596    use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
597
598    use super::TipsetKey;
599
600    #[derive(Debug, PartialEq, Clone, JsonSchema)]
601    #[schemars(rename = "Tipset")]
602    pub struct TipsetLotusJson(#[schemars(with = "TipsetLotusJsonInner")] Tipset);
603
604    #[derive(Serialize, Deserialize, JsonSchema)]
605    #[schemars(rename = "TipsetInner")]
606    #[serde(rename_all = "PascalCase")]
607    struct TipsetLotusJsonInner {
608        #[serde(with = "crate::lotus_json")]
609        #[schemars(with = "LotusJson<TipsetKey>")]
610        cids: TipsetKey,
611        #[serde(with = "crate::lotus_json")]
612        #[schemars(with = "LotusJson<NonEmpty<CachingBlockHeader>>")]
613        blocks: NonEmpty<CachingBlockHeader>,
614        height: i64,
615    }
616
617    impl<'de> Deserialize<'de> for TipsetLotusJson {
618        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
619        where
620            D: Deserializer<'de>,
621        {
622            let TipsetLotusJsonInner {
623                cids: _ignored0,
624                blocks,
625                height: _ignored1,
626            } = Deserialize::deserialize(deserializer)?;
627
628            Ok(Self(Tipset::new(blocks).map_err(D::Error::custom)?))
629        }
630    }
631
632    impl Serialize for TipsetLotusJson {
633        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
634        where
635            S: Serializer,
636        {
637            let Self(tipset) = self;
638            TipsetLotusJsonInner {
639                cids: tipset.key().clone(),
640                blocks: tipset.clone().into_block_headers(),
641                height: tipset.epoch(),
642            }
643            .serialize(serializer)
644        }
645    }
646
647    impl HasLotusJson for Tipset {
648        type LotusJson = TipsetLotusJson;
649
650        #[cfg(test)]
651        fn snapshots() -> Vec<(serde_json::Value, Self)> {
652            use serde_json::json;
653            vec![(
654                json!({
655                    "Blocks": [
656                        {
657                            "BeaconEntries": null,
658                            "ForkSignaling": 0,
659                            "Height": 0,
660                            "Messages": { "/": "baeaaaaa" },
661                            "Miner": "f00",
662                            "ParentBaseFee": "0",
663                            "ParentMessageReceipts": { "/": "baeaaaaa" },
664                            "ParentStateRoot": { "/":"baeaaaaa" },
665                            "ParentWeight": "0",
666                            "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
667                            "Timestamp": 0,
668                            "WinPoStProof": null
669                        }
670                    ],
671                    "Cids": [
672                        { "/": "bafy2bzaceag62hjj3o43lf6oyeox3fvg5aqkgl5zagbwpjje3ajwg6yw4iixk" }
673                    ],
674                    "Height": 0
675                }),
676                Self::new(vec![CachingBlockHeader::default()]).unwrap(),
677            )]
678        }
679
680        fn into_lotus_json(self) -> Self::LotusJson {
681            TipsetLotusJson(self)
682        }
683
684        fn from_lotus_json(TipsetLotusJson(tipset): Self::LotusJson) -> Self {
685            tipset
686        }
687    }
688
689    #[test]
690    fn snapshots() {
691        assert_all_snapshots::<Tipset>()
692    }
693
694    #[cfg(test)]
695    quickcheck::quickcheck! {
696        fn quickcheck(val: Tipset) -> () {
697            assert_unchanged_via_json(val)
698        }
699    }
700}
701
702#[cfg(test)]
703mod test {
704    use super::*;
705    use crate::blocks::VRFProof;
706    use crate::blocks::{
707        CachingBlockHeader, ElectionProof, Ticket, Tipset, TipsetKey, header::RawBlockHeader,
708    };
709    use crate::shim::address::Address;
710    use crate::utils::multihash::prelude::*;
711    use cid::Cid;
712    use fvm_ipld_encoding::DAG_CBOR;
713    use num_bigint::BigInt;
714    use std::iter;
715
716    pub fn mock_block(id: u64, weight: u64, ticket_sequence: u64) -> CachingBlockHeader {
717        let addr = Address::new_id(id);
718        let cid =
719            Cid::try_from("bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i").unwrap();
720
721        let fmt_str = format!("===={ticket_sequence}=====");
722        let ticket = Ticket::new(VRFProof::new(fmt_str.clone().into_bytes()));
723        let election_proof = ElectionProof {
724            win_count: 0,
725            vrfproof: VRFProof::new(fmt_str.into_bytes()),
726        };
727        let weight_inc = BigInt::from(weight);
728        CachingBlockHeader::new(RawBlockHeader {
729            miner_address: addr,
730            election_proof: Some(election_proof),
731            ticket: Some(ticket),
732            message_receipts: cid,
733            messages: cid,
734            state_root: cid,
735            weight: weight_inc,
736            ..Default::default()
737        })
738    }
739
740    #[test]
741    fn test_break_weight_tie() {
742        let b1 = mock_block(1234561, 1, 1);
743        let ts1 = Tipset::from(&b1);
744
745        let b2 = mock_block(1234562, 1, 2);
746        let ts2 = Tipset::from(&b2);
747
748        let b3 = mock_block(1234563, 1, 1);
749        let ts3 = Tipset::from(&b3);
750
751        // All tipsets have the same weight (but it's not really important here)
752
753        // Can break weight tie
754        assert!(ts1.break_weight_tie(&ts2));
755        // Can not break weight tie (because of same min tickets)
756        assert!(!ts1.break_weight_tie(&ts3));
757
758        // Values are chosen so that Ticket(b4) < Ticket(b5) < Ticket(b1)
759        let b4 = mock_block(1234564, 1, 41);
760        let b5 = mock_block(1234565, 1, 45);
761        let ts4 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
762        let ts5 = Tipset::new(vec![b4.clone(), b5.clone(), b2]).unwrap();
763        // Can break weight tie with several min tickets the same
764        assert!(ts4.break_weight_tie(&ts5));
765
766        let ts6 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
767        let ts7 = Tipset::new(vec![b4, b5, b1]).unwrap();
768        // Can not break weight tie with all min tickets the same
769        assert!(!ts6.break_weight_tie(&ts7));
770    }
771
772    #[test]
773    fn ensure_miner_addresses_are_distinct() {
774        let h0 = RawBlockHeader {
775            miner_address: Address::new_id(0),
776            ..Default::default()
777        };
778        let h1 = RawBlockHeader {
779            miner_address: Address::new_id(0),
780            ..Default::default()
781        };
782        assert_eq!(
783            Tipset::new([h0.clone(), h1.clone()]).unwrap_err(),
784            CreateTipsetError::DuplicateMiner
785        );
786
787        let h_unique = RawBlockHeader {
788            miner_address: Address::new_id(1),
789            ..Default::default()
790        };
791
792        assert_eq!(
793            Tipset::new([h_unique, h0, h1]).unwrap_err(),
794            CreateTipsetError::DuplicateMiner
795        );
796    }
797
798    #[test]
799    fn ensure_epochs_are_equal() {
800        let h0 = RawBlockHeader {
801            miner_address: Address::new_id(0),
802            epoch: 1,
803            ..Default::default()
804        };
805        let h1 = RawBlockHeader {
806            miner_address: Address::new_id(1),
807            epoch: 2,
808            ..Default::default()
809        };
810        assert_eq!(
811            Tipset::new([h0, h1]).unwrap_err(),
812            CreateTipsetError::BadEpoch
813        );
814    }
815
816    #[test]
817    fn ensure_state_roots_are_equal() {
818        let h0 = RawBlockHeader {
819            miner_address: Address::new_id(0),
820            state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
821            ..Default::default()
822        };
823        let h1 = RawBlockHeader {
824            miner_address: Address::new_id(1),
825            state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[1])),
826            ..Default::default()
827        };
828        assert_eq!(
829            Tipset::new([h0, h1]).unwrap_err(),
830            CreateTipsetError::BadStateRoot
831        );
832    }
833
834    #[test]
835    fn ensure_parent_cids_are_equal() {
836        let h0 = RawBlockHeader {
837            miner_address: Address::new_id(0),
838            ..Default::default()
839        };
840        let h1 = RawBlockHeader {
841            miner_address: Address::new_id(1),
842            parents: TipsetKey::from(nonempty![Cid::new_v1(
843                DAG_CBOR,
844                MultihashCode::Identity.digest(&[])
845            )]),
846            ..Default::default()
847        };
848        assert_eq!(
849            Tipset::new([h0, h1]).unwrap_err(),
850            CreateTipsetError::BadParents
851        );
852    }
853
854    #[test]
855    fn ensure_there_are_blocks() {
856        assert_eq!(
857            Tipset::new(iter::empty::<RawBlockHeader>()).unwrap_err(),
858            CreateTipsetError::Empty
859        );
860    }
861}