Skip to content

Commit f79ff79

Browse files
authored
fix: optimize logic of looking up genesis from a tipset (#7290)
1 parent 8b85c84 commit f79ff79

39 files changed

Lines changed: 354 additions & 182 deletions

File tree

src/beacon/mock_beacon.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
use super::DrandNetwork;
55
use crate::beacon::{Beacon, BeaconEntry};
6+
use crate::prelude::*;
67
use crate::shim::version::NetworkVersion;
78
use crate::utils::encoding::blake2b_256;
89
use byteorder::{BigEndian, ByteOrder};
@@ -45,7 +46,11 @@ impl Beacon for MockBeacon {
4546
Ok(Self::entry_for_index(round))
4647
}
4748

48-
fn max_beacon_round_for_epoch(&self, _network_version: NetworkVersion, fil_epoch: i64) -> u64 {
49+
fn max_beacon_round_for_epoch(
50+
&self,
51+
_network_version: NetworkVersion,
52+
fil_epoch: ChainEpoch,
53+
) -> u64 {
4954
fil_epoch as u64
5055
}
5156
}

src/blocks/tipset.rs

Lines changed: 27 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33

44
use std::{
55
fmt,
6-
sync::{Arc, LazyLock, OnceLock},
6+
sync::{LazyLock, OnceLock},
77
};
88

99
use super::{Block, CachingBlockHeader, RawBlockHeader, Ticket};
@@ -12,7 +12,6 @@ use crate::{
1212
cid_collections::SmallCidNonEmptyVec,
1313
networks::{calibnet, mainnet},
1414
prelude::*,
15-
shim::clock::ChainEpoch,
1615
utils::{
1716
cid::CidCborExt,
1817
db::{CborStoreExt, car_stream::CarBlock},
@@ -195,6 +194,12 @@ impl get_size2::GetSize for Tipset {
195194
}
196195
}
197196

197+
impl From<&RawBlockHeader> for Tipset {
198+
fn from(value: &RawBlockHeader) -> Self {
199+
value.clone().into()
200+
}
201+
}
202+
198203
impl From<RawBlockHeader> for Tipset {
199204
fn from(value: RawBlockHeader) -> Self {
200205
Self::from(CachingBlockHeader::from(value))
@@ -421,8 +426,19 @@ impl Tipset {
421426
})
422427
}
423428

424-
/// Fetch the genesis block header for a given tipset.
425-
pub fn genesis(&self, store: &impl Blockstore) -> anyhow::Result<CachingBlockHeader> {
429+
/// Fetch the genesis tipset for a given tipset.
430+
pub async fn genesis(
431+
&self,
432+
store: impl Blockstore + Send + Sync + 'static,
433+
) -> anyhow::Result<Tipset> {
434+
let this = self.shallow_clone();
435+
tokio::task::spawn_blocking(move || this.genesis_blocking(&store)).await?
436+
}
437+
438+
/// Fetch the genesis tipset for a given tipset.
439+
/// This call can be expensive and blocking, use [`Self::genesis`]
440+
/// in async contexts to avoid exhausting Tokio worker threads.
441+
pub fn genesis_blocking(&self, store: &impl Blockstore) -> anyhow::Result<Tipset> {
426442
// Scanning through millions of epochs to find the genesis is quite
427443
// slow. Let's use a list of known blocks to short-circuit the search.
428444
// The blocks are hash-chained together and known blocks are guaranteed
@@ -438,7 +454,7 @@ impl Tipset {
438454
serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).unwrap()
439455
});
440456

441-
for tipset in self.clone().chain(store) {
457+
for tipset in self.shallow_clone().chain(store) {
442458
// Search for known calibnet and mainnet blocks
443459
for (genesis_cid, known_blocks) in [
444460
(*calibnet::GENESIS_CID, &headers.calibnet),
@@ -447,15 +463,16 @@ impl Tipset {
447463
if let Some(known_block_cid) = known_blocks.get(&tipset.epoch())
448464
&& known_block_cid == &tipset.min_ticket_block().cid().to_string()
449465
{
450-
return store
466+
let genesis_block: CachingBlockHeader = store
451467
.get_cbor(&genesis_cid)?
452-
.context("Genesis block missing from database");
468+
.context("Genesis block missing from database")?;
469+
return Ok(genesis_block.into());
453470
}
454471
}
455472

456473
// If no known blocks are found, we'll eventually hit the genesis tipset.
457474
if tipset.epoch() == 0 {
458-
return Ok(tipset.min_ticket_block().clone());
475+
return Ok(tipset);
459476
}
460477
}
461478
anyhow::bail!("Genesis block not found")
@@ -633,14 +650,13 @@ mod lotus_json {
633650
//! [Tipset] isn't just plain old data - it has an invariant (all block headers are valid)
634651
//! So there is custom de-serialization here
635652
653+
use super::*;
636654
use crate::blocks::{CachingBlockHeader, Tipset};
637655
use crate::lotus_json::*;
638656
use nunny::Vec as NonEmpty;
639657
use schemars::JsonSchema;
640658
use serde::{Deserialize, Deserializer, Serialize, Serializer, de::Error as _};
641659

642-
use super::TipsetKey;
643-
644660
#[derive(Debug, PartialEq, Clone, JsonSchema)]
645661
#[schemars(rename = "Tipset")]
646662
pub struct TipsetLotusJson(#[schemars(with = "TipsetLotusJsonInner")] Tipset);
@@ -655,7 +671,7 @@ mod lotus_json {
655671
#[serde(with = "crate::lotus_json")]
656672
#[schemars(with = "LotusJson<NonEmpty<CachingBlockHeader>>")]
657673
blocks: NonEmpty<CachingBlockHeader>,
658-
height: i64,
674+
height: ChainEpoch,
659675
}
660676

661677
impl<'de> Deserialize<'de> for TipsetLotusJson {

src/chain/ec_finality/calculator/mod.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ mod skellam;
1818
#[cfg(test)]
1919
mod tests;
2020

21-
use anyhow::Context as _;
21+
use crate::prelude::*;
2222
use std::sync::LazyLock;
2323

2424
// `BISECT_LOW` and `BISECT_HIGH` define the search range for the bisect algorithm
@@ -46,11 +46,11 @@ pub static DEFAULT_GUARANTEE: LazyLock<f64> =
4646
#[allow(dead_code)]
4747
pub fn calc_validator_prob(
4848
chain: &[i64],
49-
finality: i64,
49+
finality: ChainEpoch,
5050
blocks_per_epoch: f64,
5151
byzantine_fraction: f64,
52-
current_epoch: i64,
53-
target_epoch: i64,
52+
current_epoch: ChainEpoch,
53+
target_epoch: ChainEpoch,
5454
) -> anyhow::Result<f64> {
5555
if current_epoch <= target_epoch || target_epoch < 0 || current_epoch >= chain.len() as i64 {
5656
return Ok(1.0);

src/chain/store/chain_store.rs

Lines changed: 21 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,8 @@ pub struct ChainStore {
7878
/// Tracks blocks for the purpose of forming tipsets.
7979
tipset_tracker: TipsetTracker<DbImpl>,
8080

81-
genesis_block_header: Arc<CachingBlockHeader>,
81+
/// Genesis tipset.
82+
genesis: Tipset,
8283

8384
/// validated blocks
8485
pub(crate) validated_blocks: SizeTrackingCache<CidWrapper, ()>,
@@ -99,7 +100,7 @@ impl ShallowClone for ChainStore {
99100
ec_calculator_finalized_epoch: self.ec_calculator_finalized_epoch.shallow_clone(),
100101
chain_index: self.chain_index.shallow_clone(),
101102
tipset_tracker: self.tipset_tracker.shallow_clone(),
102-
genesis_block_header: self.genesis_block_header.shallow_clone(),
103+
genesis: self.genesis.shallow_clone(),
103104
validated_blocks: self.validated_blocks.shallow_clone(),
104105
chain_config: self.chain_config.shallow_clone(),
105106
messages_in_tipset_cache: self.messages_in_tipset_cache.shallow_clone(),
@@ -111,9 +112,11 @@ impl ChainStore {
111112
pub fn new(
112113
db: impl Into<DbImpl>,
113114
chain_config: Arc<ChainConfig>,
114-
genesis_block_header: CachingBlockHeader,
115+
genesis: impl Into<Tipset>,
115116
) -> anyhow::Result<Self> {
116117
let db = db.into();
118+
let genesis = genesis.into();
119+
anyhow::ensure!(genesis.epoch() == 0, "genesis tipset must be at epoch 0");
117120
let (publisher, _) = broadcast::channel(SINK_CAP);
118121
let head = if let Some(head_tsk) = db
119122
.heaviest_tipset_key()
@@ -122,11 +125,11 @@ impl ChainStore {
122125
Tipset::load_required(&db, &head_tsk)
123126
.with_context(|| format!("failed to load head tipset with key {head_tsk}"))?
124127
} else {
125-
Tipset::from(&genesis_block_header)
128+
genesis.shallow_clone()
126129
};
127130
let heaviest_tipset = Arc::new(ArcSwap::from_pointee(head.shallow_clone()));
128131
let f3_finalized_tipset: Arc<ArcSwapOption<Tipset>> = Default::default();
129-
let chain_index = ChainIndex::new(db.shallow_clone());
132+
let chain_index = ChainIndex::new(db.shallow_clone(), genesis.shallow_clone());
130133
let ec_calculator_finalized_epoch = Arc::new(AtomicI64::new(
131134
ChainGetTipSetFinalityStatus::get_ec_finality_epoch(&chain_index, &chain_config, &head),
132135
));
@@ -150,7 +153,7 @@ impl ChainStore {
150153
heaviest_tipset,
151154
f3_finalized_tipset,
152155
ec_calculator_finalized_epoch,
153-
genesis_block_header: genesis_block_header.into(),
156+
genesis,
154157
validated_blocks: SizeTrackingCache::new_with_metrics(
155158
"validated_blocks",
156159
VALIDATED_BLOCKS_CACHE_SIZE,
@@ -259,20 +262,21 @@ impl ChainStore {
259262
self.tipset_tracker.expand(header)
260263
}
261264

265+
/// Returns the genesis block header.
262266
pub fn genesis_block_header(&self) -> &CachingBlockHeader {
263-
&self.genesis_block_header
267+
self.genesis.min_ticket_block()
268+
}
269+
270+
/// Returns the genesis tipset.
271+
pub fn genesis_tipset(&self) -> Tipset {
272+
self.genesis.shallow_clone()
264273
}
265274

266275
/// Returns the currently tracked heaviest tipset.
267276
pub fn heaviest_tipset(&self) -> Tipset {
268277
self.heaviest_tipset.load().as_ref().shallow_clone()
269278
}
270279

271-
/// Returns the genesis tipset.
272-
pub fn genesis_tipset(&self) -> Tipset {
273-
Tipset::from(self.genesis_block_header())
274-
}
275-
276280
/// Subscribes head changes.
277281
pub fn subscribe_head_changes(&self) -> broadcast::Receiver<HeadChanges> {
278282
self.head_changes_tx.subscribe()
@@ -407,13 +411,10 @@ impl ChainStore {
407411

408412
// More null blocks than lookback
409413
if lbr >= heaviest_tipset.epoch() {
410-
// This situation is extremely rare so it's fine to compute the
411-
// state-root without caching.
412-
let genesis_timestamp = heaviest_tipset.genesis(chain_index.db())?.timestamp;
414+
let genesis_timestamp = chain_index.genesis().min_ticket_block().timestamp;
413415
let beacon = Arc::new(chain_config.get_beacon_schedule(genesis_timestamp));
414416
let ExecutedTipset { state_root, .. } =
415417
crate::state_manager::apply_block_messages_blocking(
416-
genesis_timestamp,
417418
chain_index.shallow_clone(),
418419
chain_config.shallow_clone(),
419420
beacon,
@@ -726,14 +727,16 @@ mod tests {
726727
let gen_block = CachingBlockHeader::new(RawBlockHeader {
727728
miner_address: Address::new_id(0),
728729
state_root: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
729-
epoch: 1,
730+
epoch: 0,
730731
weight: 2u32.into(),
731732
messages: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
732733
message_receipts: Cid::new_v1(DAG_CBOR, MultihashCode::Identity.digest(&[])),
733734
..Default::default()
734735
});
735-
let cs = ChainStore::new(db, chain_config, gen_block.clone()).unwrap();
736+
let gen_ts = Tipset::from(&gen_block);
737+
let cs = ChainStore::new(db, chain_config, gen_ts.shallow_clone()).unwrap();
736738

739+
assert_eq!(cs.genesis_tipset(), gen_ts);
737740
assert_eq!(cs.genesis_block_header(), &gen_block);
738741
}
739742

src/chain/store/index.rs

Lines changed: 14 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ pub struct ChainIndex {
2626
ts_cache: TipsetCache,
2727
/// `Blockstore` pointer needed to load tipsets from cold storage.
2828
db: DbImpl,
29+
/// Genesis tipset
30+
genesis: Tipset,
2931
/// check whether a tipset is finalized
3032
is_tipset_finalized: Option<IsTipsetFinalizedFn>,
3133
}
@@ -35,6 +37,7 @@ impl ShallowClone for ChainIndex {
3537
Self {
3638
ts_cache: self.ts_cache.shallow_clone(),
3739
db: self.db.shallow_clone(),
40+
genesis: self.genesis.shallow_clone(),
3841
is_tipset_finalized: self.is_tipset_finalized.clone(),
3942
}
4043
}
@@ -52,12 +55,14 @@ pub enum ResolveNullTipset {
5255
}
5356

5457
impl ChainIndex {
55-
pub fn new(db: impl Into<DbImpl>) -> Self {
58+
pub fn new(db: impl Into<DbImpl>, genesis: Tipset) -> Self {
59+
assert!(genesis.epoch() == 0, "genesis tipset must be at epoch 0");
5660
let db = db.into();
5761
let ts_cache = SizeTrackingCache::new_with_metrics("tipset", DEFAULT_TIPSET_CACHE_SIZE);
5862
Self {
5963
ts_cache,
6064
db,
65+
genesis,
6166
is_tipset_finalized: None,
6267
}
6368
}
@@ -75,6 +80,10 @@ impl ChainIndex {
7580
self.db().shallow_clone()
7681
}
7782

83+
pub fn genesis(&self) -> &Tipset {
84+
&self.genesis
85+
}
86+
7887
/// Loads a tipset from memory given the tipset keys and cache. Semantically
7988
/// identical to [`Tipset::load`] but the result is cached.
8089
pub fn load_tipset(&self, tsk: &TipsetKey) -> Result<Option<Tipset>, Error> {
@@ -169,7 +178,7 @@ impl ChainIndex {
169178
}
170179

171180
if to == 0 {
172-
return Ok(Some(Tipset::from(from.genesis(&self.db)?)));
181+
return Ok(Some(self.genesis.shallow_clone()));
173182
}
174183

175184
let from_epoch = from.epoch();
@@ -327,7 +336,7 @@ mod tests {
327336
persist_tipset(&epoch3, &db);
328337
persist_tipset(&epoch4, &db);
329338

330-
let index = ChainIndex::new(db);
339+
let index = ChainIndex::new(db, genesis);
331340
// epoch 2 is null. ResolveNullTipset decided whether to return epoch 1 or epoch 3
332341
assert_eq!(
333342
index
@@ -365,7 +374,7 @@ mod tests {
365374
persist_tipset(&epoch2b, &db);
366375
persist_tipset(&epoch3b, &db);
367376

368-
let index = ChainIndex::new(db);
377+
let index = ChainIndex::new(db, genesis);
369378
// The chain as forked, epoch 2 and 3 are ambiguous
370379
assert_eq!(
371380
index
@@ -394,7 +403,7 @@ mod tests {
394403
persist_tipset(&genesis, &db);
395404
persist_tipset(&epoch3, &db);
396405

397-
let index = ChainIndex::new(db);
406+
let index = ChainIndex::new(db, genesis);
398407
assert!(
399408
index
400409
.tipset_by_height(2, epoch3, ResolveNullTipset::TakeOlder)

src/chain_sync/chain_follower.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1168,7 +1168,7 @@ mod tests {
11681168
[genesis_header = dummy_node(&db, 0)]
11691169
};
11701170

1171-
let cs = ChainStore::new(db, Default::default(), genesis_header.clone().into()).unwrap();
1171+
let cs = ChainStore::new(db, Default::default(), genesis_header).unwrap();
11721172

11731173
cs.set_heaviest_tipset(cs.genesis_tipset()).unwrap();
11741174

src/cli/subcommands/info_cmd.rs

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ impl InfoCommand {
182182

183183
#[cfg(test)]
184184
mod tests {
185+
use super::*;
185186
use crate::blocks::RawBlockHeader;
186187
use crate::blocks::{CachingBlockHeader, Tipset};
187188
use crate::shim::clock::EPOCH_DURATION_SECONDS;
@@ -190,8 +191,6 @@ mod tests {
190191
use quickcheck_macros::quickcheck;
191192
use std::{str::FromStr, time::Duration};
192193

193-
use super::{NodeStatusInfo, SyncStatus};
194-
195194
fn mock_tipset_at(seconds_since_unix_epoch: u64) -> Tipset {
196195
CachingBlockHeader::new(RawBlockHeader {
197196
miner_address: Address::from_str("f2kmbjvz7vagl2z6pfrbjoggrkjofxspp7cqtw2zy").unwrap(),
@@ -205,7 +204,7 @@ mod tests {
205204
NodeStatusInfo {
206205
lag: 0,
207206
health: 90.,
208-
epoch: i64::MAX,
207+
epoch: ChainEpoch::MAX,
209208
base_fee: TokenAmount::from_whole(1),
210209
sync_status: SyncStatus::Ok,
211210
start_time: DateTime::<chrono::Utc>::MIN_UTC,

0 commit comments

Comments
 (0)