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