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