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