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