Skip to main content

forest/blocks/
tipset.rs

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