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