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