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)?
331            .with_context(|| format!("Required tipset missing from database, tipset key: {tsk}"))
332    }
333
334    /// Returns epoch of the tipset.
335    pub fn epoch(&self) -> ChainEpoch {
336        self.min_ticket_block().epoch
337    }
338    pub fn block_headers(&self) -> &NonEmpty<CachingBlockHeader> {
339        &self.headers
340    }
341    /// Returns the smallest ticket of all blocks in the tipset
342    pub fn min_ticket(&self) -> Option<&Ticket> {
343        self.min_ticket_block().ticket.as_ref()
344    }
345    /// Returns the block with the smallest ticket of all blocks in the tipset
346    pub fn min_ticket_block(&self) -> &CachingBlockHeader {
347        self.headers.first()
348    }
349    /// Returns the smallest timestamp of all blocks in the tipset
350    pub fn min_timestamp(&self) -> u64 {
351        self.headers
352            .iter()
353            .map(|block| block.timestamp)
354            .min()
355            .expect("tipset headers are non-empty")
356    }
357    /// Returns the number of blocks in the tipset.
358    pub fn len(&self) -> usize {
359        self.headers.len()
360    }
361    /// Returns a key for the tipset.
362    pub fn key(&self) -> &TipsetKey {
363        self.key
364            .get_or_init(|| TipsetKey::from(self.headers.iter_ne().map(|h| *h.cid()).collect_vec()))
365    }
366    /// Returns a non-empty collection of `CIDs` for the current tipset
367    pub fn cids(&self) -> NonEmpty<Cid> {
368        self.key().to_cids()
369    }
370    /// Returns the keys of the parents of the blocks in the tipset.
371    pub fn parents(&self) -> &TipsetKey {
372        &self.min_ticket_block().parents
373    }
374    /// Returns the state root for the tipset parent.
375    pub fn parent_state(&self) -> &Cid {
376        &self.min_ticket_block().state_root
377    }
378    /// Returns the message receipt root for the tipset parent.
379    pub fn parent_message_receipts(&self) -> &Cid {
380        &self.min_ticket_block().message_receipts
381    }
382    /// Returns the tipset's calculated weight
383    pub fn weight(&self) -> &BigInt {
384        &self.min_ticket_block().weight
385    }
386    /// Returns true if self wins according to the Filecoin tie-break rule
387    /// (FIP-0023)
388    #[cfg(test)]
389    pub fn break_weight_tie(&self, other: &Tipset) -> bool {
390        // blocks are already sorted by ticket
391        let broken = self
392            .block_headers()
393            .iter()
394            .zip(other.block_headers().iter())
395            .any(|(a, b)| {
396                const MSG: &str =
397                    "The function block_sanity_checks should have been called at this point.";
398                let ticket = a.ticket.as_ref().expect(MSG);
399                let other_ticket = b.ticket.as_ref().expect(MSG);
400                ticket.vrfproof < other_ticket.vrfproof
401            });
402        if broken {
403            tracing::info!("Weight tie broken in favour of {}", self.key());
404        } else {
405            tracing::info!("Weight tie left unbroken, default to {}", other.key());
406        }
407        broken
408    }
409
410    /// Returns an iterator of all tipsets, taking an owned [`Blockstore`]
411    pub fn chain_owned(self, store: impl Blockstore) -> impl Iterator<Item = Tipset> {
412        let mut tipset = Some(self);
413        std::iter::from_fn(move || {
414            let child = tipset.take()?;
415            tipset = Tipset::load_required(&store, child.parents()).ok();
416            Some(child)
417        })
418    }
419
420    /// Returns an iterator of all tipsets
421    pub fn chain(self, store: &impl Blockstore) -> impl Iterator<Item = Tipset> + '_ {
422        let mut tipset = Some(self);
423        std::iter::from_fn(move || {
424            let child = tipset.take()?;
425            tipset = Tipset::load_required(store, child.parents()).ok();
426            Some(child)
427        })
428    }
429
430    /// Fetch the genesis tipset for a given tipset.
431    pub async fn genesis(
432        &self,
433        store: impl Blockstore + Send + Sync + 'static,
434    ) -> anyhow::Result<Tipset> {
435        let this = self.shallow_clone();
436        tokio::task::spawn_blocking(move || this.genesis_blocking(&store)).await?
437    }
438
439    /// Fetch the genesis tipset for a given tipset.
440    /// This call can be expensive and blocking, use [`Self::genesis`]
441    /// in async contexts to avoid exhausting Tokio worker threads.
442    pub fn genesis_blocking(&self, store: &impl Blockstore) -> anyhow::Result<Tipset> {
443        // Scanning through millions of epochs to find the genesis is quite
444        // slow. Let's use a list of known blocks to short-circuit the search.
445        // The blocks are hash-chained together and known blocks are guaranteed
446        // to have a known genesis.
447        #[derive(Serialize, Deserialize)]
448        struct KnownHeaders {
449            calibnet: HashMap<ChainEpoch, String>,
450            mainnet: HashMap<ChainEpoch, String>,
451        }
452
453        static KNOWN_HEADERS: OnceLock<KnownHeaders> = OnceLock::new();
454        let headers = KNOWN_HEADERS.get_or_init(|| {
455            serde_yaml::from_str(include_str!("../../build/known_blocks.yaml"))
456                .expect("bundled known_blocks.yaml is valid")
457        });
458
459        for tipset in self.shallow_clone().chain(store) {
460            // Search for known calibnet and mainnet blocks
461            for (genesis_cid, known_blocks) in [
462                (*calibnet::GENESIS_CID, &headers.calibnet),
463                (*mainnet::GENESIS_CID, &headers.mainnet),
464            ] {
465                if let Some(known_block_cid) = known_blocks.get(&tipset.epoch())
466                    && known_block_cid == &tipset.min_ticket_block().cid().to_string()
467                {
468                    let genesis_block: CachingBlockHeader = store
469                        .get_cbor(&genesis_cid)?
470                        .context("Genesis block missing from database")?;
471                    return Ok(genesis_block.into());
472                }
473            }
474
475            // If no known blocks are found, we'll eventually hit the genesis tipset.
476            if tipset.epoch() == 0 {
477                return Ok(tipset);
478            }
479        }
480        anyhow::bail!("Genesis block not found")
481    }
482}
483
484impl TipsetLike for Tipset {
485    fn epoch(&self) -> ChainEpoch {
486        self.epoch()
487    }
488
489    fn key(&self) -> &TipsetKey {
490        self.key()
491    }
492
493    fn parents(&self) -> &TipsetKey {
494        self.parents()
495    }
496
497    fn parent_state(&self) -> &Cid {
498        self.parent_state()
499    }
500}
501
502/// `FullTipset` is an expanded version of a tipset that contains all the blocks
503/// and messages.
504#[derive(Debug, Clone, Eq)]
505pub struct FullTipset {
506    blocks: Arc<NonEmpty<Block>>,
507    // key is lazily initialized via `fn key()`.
508    key: Arc<OnceLock<TipsetKey>>,
509}
510
511impl TipsetLike for FullTipset {
512    fn epoch(&self) -> ChainEpoch {
513        self.epoch()
514    }
515
516    fn key(&self) -> &TipsetKey {
517        self.key()
518    }
519
520    fn parents(&self) -> &TipsetKey {
521        self.parents()
522    }
523
524    fn parent_state(&self) -> &Cid {
525        self.parent_state()
526    }
527}
528
529impl std::hash::Hash for FullTipset {
530    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
531        self.key().hash(state)
532    }
533}
534
535// Constructing a FullTipset from a single Block is infallible.
536impl From<Block> for FullTipset {
537    fn from(block: Block) -> Self {
538        FullTipset {
539            blocks: nonempty![block].into(),
540            key: OnceLock::new().into(),
541        }
542    }
543}
544
545impl PartialEq for FullTipset {
546    fn eq(&self, other: &Self) -> bool {
547        self.blocks.eq(&other.blocks)
548    }
549}
550
551impl FullTipset {
552    pub fn new(blocks: impl IntoIterator<Item = Block>) -> Result<Self, CreateTipsetError> {
553        let blocks = Arc::new(
554            NonEmpty::new(
555                // sort blocks on creation to allow for more seamless conversions between
556                // FullTipset and Tipset
557                blocks
558                    .into_iter()
559                    .sorted_by_cached_key(|it| it.header.tipset_sort_key())
560                    .collect(),
561            )
562            .map_err(|_| CreateTipsetError::Empty)?,
563        );
564
565        verify_block_headers(blocks.iter().map(|it| &it.header))?;
566
567        Ok(Self {
568            blocks,
569            key: Arc::new(OnceLock::new()),
570        })
571    }
572    /// Returns the first block of the tipset.
573    fn first_block(&self) -> &Block {
574        self.blocks.first()
575    }
576    /// Returns reference to all blocks in a full tipset.
577    pub fn blocks(&self) -> &NonEmpty<Block> {
578        &self.blocks
579    }
580    /// Returns all blocks in a full tipset.
581    pub fn into_blocks(self) -> NonEmpty<Block> {
582        Arc::unwrap_or_clone(self.blocks)
583    }
584    /// Converts the full tipset into a [Tipset] which removes the messages
585    /// attached.
586    pub fn into_tipset(self) -> Tipset {
587        Tipset::from(self)
588    }
589    /// Returns a key for the tipset.
590    pub fn key(&self) -> &TipsetKey {
591        self.key
592            .get_or_init(|| TipsetKey::from(self.blocks.iter_ne().map(|b| *b.cid()).collect_vec()))
593    }
594    /// Returns the state root for the tipset parent.
595    pub fn parent_state(&self) -> &Cid {
596        &self.first_block().header().state_root
597    }
598    /// Returns the keys of the parents of the blocks in the tipset.
599    pub fn parents(&self) -> &TipsetKey {
600        &self.first_block().header().parents
601    }
602    /// Returns epoch of the tipset.
603    pub fn epoch(&self) -> ChainEpoch {
604        self.first_block().header().epoch
605    }
606    /// Returns the tipset's calculated weight.
607    pub fn weight(&self) -> &BigInt {
608        &self.first_block().header().weight
609    }
610    /// Persists the tipset into the blockstore.
611    pub fn persist(&self, db: &impl Blockstore) -> anyhow::Result<()> {
612        for block in self.blocks() {
613            // To persist `TxMeta` that is required for loading tipset messages
614            TipsetValidator::validate_msg_root(db, block)?;
615            crate::chain::persist_objects(&db, std::iter::once(block.header()))?;
616            crate::chain::persist_objects(&db, block.bls_msgs().iter())?;
617            crate::chain::persist_objects(&db, block.secp_msgs().iter())?;
618        }
619        Ok(())
620    }
621}
622
623fn verify_block_headers<'a>(
624    headers: impl IntoIterator<Item = &'a CachingBlockHeader>,
625) -> Result<(), CreateTipsetError> {
626    use itertools::all;
627
628    let headers =
629        NonEmpty::new(headers.into_iter().collect()).map_err(|_| CreateTipsetError::Empty)?;
630    if !all(&headers, |it| it.ticket.is_some()) {
631        return Err(CreateTipsetError::MissingTicket);
632    }
633    if !all(&headers, |it| it.parents == headers.first().parents) {
634        return Err(CreateTipsetError::BadParents);
635    }
636    if !all(&headers, |it| it.state_root == headers.first().state_root) {
637        return Err(CreateTipsetError::BadStateRoot);
638    }
639    if !all(&headers, |it| it.epoch == headers.first().epoch) {
640        return Err(CreateTipsetError::BadEpoch);
641    }
642
643    if !headers.iter().map(|it| it.miner_address).all_unique() {
644        return Err(CreateTipsetError::DuplicateMiner);
645    }
646
647    Ok(())
648}
649
650#[cfg_vis::cfg_vis(doc, pub)]
651mod lotus_json {
652    //! [Tipset] isn't just plain old data - it has an invariant (all block headers are valid)
653    //! So there is custom de-serialization here
654
655    use super::*;
656    use crate::blocks::{CachingBlockHeader, Tipset};
657    use crate::lotus_json::*;
658    use nunny::Vec as NonEmpty;
659    use schemars::JsonSchema;
660    use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
661
662    #[derive(Debug, PartialEq, Clone, JsonSchema)]
663    #[schemars(rename = "Tipset")]
664    pub struct TipsetLotusJson(#[schemars(with = "TipsetLotusJsonInner")] Tipset);
665
666    #[derive(Serialize, Deserialize, JsonSchema)]
667    #[schemars(rename = "TipsetInner")]
668    #[serde(rename_all = "PascalCase")]
669    struct TipsetLotusJsonInner {
670        #[serde(with = "crate::lotus_json")]
671        #[schemars(with = "LotusJson<TipsetKey>")]
672        cids: TipsetKey,
673        #[serde(with = "crate::lotus_json")]
674        #[schemars(with = "LotusJson<NonEmpty<CachingBlockHeader>>")]
675        blocks: NonEmpty<CachingBlockHeader>,
676        height: ChainEpoch,
677    }
678
679    impl<'de> Deserialize<'de> for TipsetLotusJson {
680        fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
681        where
682            D: Deserializer<'de>,
683        {
684            let TipsetLotusJsonInner {
685                cids: _ignored0,
686                blocks,
687                height: _ignored1,
688            } = Deserialize::deserialize(deserializer)?;
689
690            Ok(Self(Tipset::new(blocks).map_err(D::Error::custom)?))
691        }
692    }
693
694    impl Serialize for TipsetLotusJson {
695        fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
696        where
697            S: Serializer,
698        {
699            let Self(tipset) = self;
700            TipsetLotusJsonInner {
701                cids: tipset.key().clone(),
702                height: tipset.epoch(),
703                blocks: tipset.block_headers().clone(),
704            }
705            .serialize(serializer)
706        }
707    }
708
709    impl HasLotusJson for Tipset {
710        type LotusJson = TipsetLotusJson;
711
712        #[cfg(test)]
713        fn snapshots() -> Vec<(serde_json::Value, Self)> {
714            use crate::blocks::header::RawBlockHeader;
715            use crate::test_utils::dummy_ticket;
716            use serde_json::json;
717            let header = CachingBlockHeader::new(RawBlockHeader {
718                ticket: dummy_ticket(0),
719                ..Default::default()
720            });
721            let header_cid = *header.cid();
722            vec![(
723                json!({
724                    "Blocks": [
725                        {
726                            "BeaconEntries": null,
727                            "ForkSignaling": 0,
728                            "Height": 0,
729                            "Messages": { "/": "baeaaaaa" },
730                            "Miner": "f00",
731                            "ParentBaseFee": "0",
732                            "ParentMessageReceipts": { "/": "baeaaaaa" },
733                            "ParentStateRoot": { "/":"baeaaaaa" },
734                            "ParentWeight": "0",
735                            "Parents": [{"/":"bafyreiaqpwbbyjo4a42saasj36kkrpv4tsherf2e7bvezkert2a7dhonoi"}],
736                            "Ticket": { "VRFProof": "AA==" },
737                            "Timestamp": 0,
738                            "WinPoStProof": null
739                        }
740                    ],
741                    "Cids": [
742                        { "/": header_cid.to_string() }
743                    ],
744                    "Height": 0
745                }),
746                Self::new(vec![header]).unwrap(),
747            )]
748        }
749
750        fn into_lotus_json(self) -> Self::LotusJson {
751            TipsetLotusJson(self)
752        }
753
754        fn from_lotus_json(TipsetLotusJson(tipset): Self::LotusJson) -> Self {
755            tipset
756        }
757    }
758
759    #[test]
760    fn snapshots() {
761        assert_all_snapshots::<Tipset>()
762    }
763
764    #[cfg(test)]
765    #[quickcheck_macros::quickcheck]
766    fn quickcheck(val: Tipset) {
767        assert_unchanged_via_json(val)
768    }
769}
770
771#[cfg(test)]
772mod test {
773    use super::*;
774    use crate::blocks::{
775        CachingBlockHeader, ElectionProof, Ticket, Tipset, TipsetKey, VRFProof,
776        header::RawBlockHeader,
777    };
778    use crate::db::MemoryDB;
779    use crate::shim::address::Address;
780    use crate::test_utils::dummy_ticket;
781    use cid::Cid;
782    use fvm_ipld_encoding::DAG_CBOR;
783    use num_bigint::BigInt;
784    use quickcheck::Arbitrary;
785    use quickcheck_macros::quickcheck;
786    use std::iter;
787
788    pub fn mock_block(id: u64, weight: u64, ticket_sequence: u64) -> CachingBlockHeader {
789        let addr = Address::new_id(id);
790        let cid =
791            Cid::try_from("bafyreicmaj5hhoy5mgqvamfhgexxyergw7hdeshizghodwkjg6qmpoco7i").unwrap();
792
793        let fmt_str = format!("===={ticket_sequence}=====");
794        let ticket = Ticket::new(VRFProof::new(fmt_str.clone().into_bytes()));
795        let election_proof = ElectionProof {
796            win_count: 0,
797            vrfproof: VRFProof::new(fmt_str.into_bytes()),
798        };
799        let weight_inc = BigInt::from(weight);
800        CachingBlockHeader::new(RawBlockHeader {
801            miner_address: addr,
802            election_proof: Some(election_proof),
803            ticket: Some(ticket),
804            message_receipts: cid,
805            messages: cid,
806            state_root: cid,
807            weight: weight_inc,
808            ..Default::default()
809        })
810    }
811
812    #[test]
813    fn test_break_weight_tie() {
814        let b1 = mock_block(1234561, 1, 1);
815        let ts1 = Tipset::from(&b1);
816
817        let b2 = mock_block(1234562, 1, 2);
818        let ts2 = Tipset::from(&b2);
819
820        let b3 = mock_block(1234563, 1, 1);
821        let ts3 = Tipset::from(&b3);
822
823        // All tipsets have the same weight (but it's not really important here)
824
825        // Can break weight tie
826        assert!(ts1.break_weight_tie(&ts2));
827        // Can not break weight tie (because of same min tickets)
828        assert!(!ts1.break_weight_tie(&ts3));
829
830        // Values are chosen so that Ticket(b4) < Ticket(b5) < Ticket(b1)
831        let b4 = mock_block(1234564, 1, 41);
832        let b5 = mock_block(1234565, 1, 45);
833        let ts4 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
834        let ts5 = Tipset::new(vec![b4.clone(), b5.clone(), b2]).unwrap();
835        // Can break weight tie with several min tickets the same
836        assert!(ts4.break_weight_tie(&ts5));
837
838        let ts6 = Tipset::new(vec![b4.clone(), b5.clone(), b1.clone()]).unwrap();
839        let ts7 = Tipset::new(vec![b4, b5, b1]).unwrap();
840        // Can not break weight tie with all min tickets the same
841        assert!(!ts6.break_weight_tie(&ts7));
842    }
843
844    #[test]
845    fn ensure_miner_addresses_are_distinct() {
846        let h0 = RawBlockHeader {
847            miner_address: Address::new_id(0),
848            ticket: dummy_ticket(0),
849            ..Default::default()
850        };
851        let h1 = RawBlockHeader {
852            miner_address: Address::new_id(0),
853            ticket: dummy_ticket(0),
854            ..Default::default()
855        };
856        assert_eq!(
857            Tipset::new([h0.clone(), h1.clone()]).unwrap_err(),
858            CreateTipsetError::DuplicateMiner
859        );
860
861        let h_unique = RawBlockHeader {
862            miner_address: Address::new_id(1),
863            ticket: dummy_ticket(0),
864            ..Default::default()
865        };
866
867        assert_eq!(
868            Tipset::new([h_unique, h0, h1]).unwrap_err(),
869            CreateTipsetError::DuplicateMiner
870        );
871    }
872
873    #[test]
874    fn ensure_epochs_are_equal() {
875        let h0 = RawBlockHeader {
876            miner_address: Address::new_id(0),
877            ticket: dummy_ticket(0),
878            epoch: 1,
879            ..Default::default()
880        };
881        let h1 = RawBlockHeader {
882            miner_address: Address::new_id(1),
883            ticket: dummy_ticket(0),
884            epoch: 2,
885            ..Default::default()
886        };
887        assert_eq!(
888            Tipset::new([h0, h1]).unwrap_err(),
889            CreateTipsetError::BadEpoch
890        );
891    }
892
893    #[test]
894    fn ensure_state_roots_are_equal() {
895        let h0 = RawBlockHeader {
896            miner_address: Address::new_id(0),
897            ticket: dummy_ticket(0),
898            state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
899            ..Default::default()
900        };
901        let h1 = RawBlockHeader {
902            miner_address: Address::new_id(1),
903            ticket: dummy_ticket(0),
904            state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[1])),
905            ..Default::default()
906        };
907        assert_eq!(
908            Tipset::new([h0, h1]).unwrap_err(),
909            CreateTipsetError::BadStateRoot
910        );
911    }
912
913    #[test]
914    fn ensure_parent_cids_are_equal() {
915        let h0 = RawBlockHeader {
916            miner_address: Address::new_id(0),
917            ticket: dummy_ticket(0),
918            ..Default::default()
919        };
920        let h1 = RawBlockHeader {
921            miner_address: Address::new_id(1),
922            ticket: dummy_ticket(0),
923            parents: TipsetKey::from(nonempty![Cid::new_v1(
924                DAG_CBOR,
925                MultihashCode::Identity.digest(&[])
926            )]),
927            ..Default::default()
928        };
929        assert_eq!(
930            Tipset::new([h0, h1]).unwrap_err(),
931            CreateTipsetError::BadParents
932        );
933    }
934
935    #[test]
936    fn ensure_there_are_blocks() {
937        assert_eq!(
938            Tipset::new(iter::empty::<RawBlockHeader>()).unwrap_err(),
939            CreateTipsetError::Empty
940        );
941    }
942
943    #[test]
944    fn ensure_tickets_are_present() {
945        let with_ticket = RawBlockHeader {
946            miner_address: Address::new_id(0),
947            ticket: dummy_ticket(0),
948            ..Default::default()
949        };
950        let without_ticket = RawBlockHeader {
951            miner_address: Address::new_id(1),
952            ticket: None,
953            ..Default::default()
954        };
955        assert_eq!(
956            Tipset::new([with_ticket, without_ticket]).unwrap_err(),
957            CreateTipsetError::MissingTicket
958        );
959    }
960
961    impl Arbitrary for TipsetKey {
962        fn arbitrary(g: &mut quickcheck::Gen) -> Self {
963            let blocks: nunny::Vec<Vec<u8>> = nunny::Vec::arbitrary(g);
964            let cids = nunny::Vec::new(
965                blocks
966                    .into_iter()
967                    .map(|b| {
968                        Cid::new_v1(
969                            fvm_ipld_encoding::DAG_CBOR,
970                            MultihashCode::Blake2b256.digest(&b),
971                        )
972                    })
973                    .collect_vec(),
974            )
975            .expect("infallible");
976            cids.into()
977        }
978    }
979
980    #[quickcheck]
981    fn tipset_key_bytes(tsk: TipsetKey) {
982        let bytes = tsk.bytes();
983        let tsk2 = TipsetKey::from_bytes(bytes).unwrap();
984        assert_eq!(tsk, tsk2);
985
986        let bs = MemoryDB::default();
987        let cid = tsk.save(&bs).unwrap();
988        let tsk3 = TipsetKey::load(&bs, &cid).unwrap();
989        assert_eq!(tsk, tsk3);
990    }
991}