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