Skip to content

Commit 9987d48

Browse files
committed
fix: optimize tipset_by_height
1 parent 1e0a5c5 commit 9987d48

13 files changed

Lines changed: 175 additions & 90 deletions

File tree

src/blocks/checkpoints.rs

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
// Copyright 2019-2026 ChainSafe Systems
2+
// SPDX-License-Identifier: Apache-2.0, MIT
3+
4+
//! Scanning through millions of epochs to find the genesis is quite
5+
//! slow. We use a list of known blocks to short-circuit the search.
6+
//! The blocks are hash-chained together and known blocks are guaranteed
7+
//! to have a known genesis.
8+
9+
use crate::{
10+
blocks::{CachingBlockHeader, Tipset},
11+
networks::NetworkChain,
12+
shim::clock::ChainEpoch,
13+
};
14+
use ahash::HashMap;
15+
use anyhow::Context as _;
16+
use cid::Cid;
17+
use fvm_ipld_blockstore::Blockstore;
18+
use itertools::Itertools;
19+
use serde::{Deserialize, Serialize};
20+
use serde_with::{DisplayFromStr, serde_as};
21+
use std::sync::{LazyLock, OnceLock};
22+
23+
#[serde_as]
24+
#[derive(Serialize, Deserialize)]
25+
pub struct KnownBlocks {
26+
#[serde_as(as = "HashMap<_, DisplayFromStr>")]
27+
pub calibnet: HashMap<ChainEpoch, Cid>,
28+
#[serde_as(as = "HashMap<_, DisplayFromStr>")]
29+
pub mainnet: HashMap<ChainEpoch, Cid>,
30+
}
31+
32+
pub static KNOWN_BLOCKS: LazyLock<KnownBlocks> = LazyLock::new(|| {
33+
serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).expect("infallible")
34+
});
35+
36+
pub fn known_tipsets(
37+
bs: &impl Blockstore,
38+
network: &NetworkChain,
39+
) -> anyhow::Result<&'static Vec<Tipset>> {
40+
static CACHE_CALIBNET: OnceLock<Vec<Tipset>> = OnceLock::new();
41+
static CACHE_MAINNET: OnceLock<Vec<Tipset>> = OnceLock::new();
42+
let (cache, known_blocks) = match network {
43+
NetworkChain::Calibnet => (&CACHE_CALIBNET, &KNOWN_BLOCKS.calibnet),
44+
NetworkChain::Mainnet => (&CACHE_MAINNET, &KNOWN_BLOCKS.mainnet),
45+
_ => anyhow::bail!("unsupported network {network}"),
46+
};
47+
if let Some(v) = cache.get() {
48+
Ok(v)
49+
} else {
50+
let tipsets = known_blocks_to_known_tipsets(bs, known_blocks)?;
51+
_ = cache.set(tipsets);
52+
cache.get().context("infallible")
53+
}
54+
}
55+
56+
fn known_blocks_to_known_tipsets(
57+
bs: &impl Blockstore,
58+
blocks: &HashMap<ChainEpoch, Cid>,
59+
) -> anyhow::Result<Vec<Tipset>> {
60+
let mut tipsets: Vec<Tipset> = blocks
61+
.values()
62+
.map(|&b| block_cid_to_required_parent_tipset(bs, b))
63+
.try_collect()?;
64+
tipsets.sort_by_key(|ts| ts.epoch());
65+
Ok(tipsets)
66+
}
67+
68+
fn block_cid_to_parent_tipset(bs: &impl Blockstore, block: Cid) -> anyhow::Result<Option<Tipset>> {
69+
if let Some(block) = CachingBlockHeader::load(bs, block)? {
70+
Tipset::load(bs, &block.parents)
71+
} else {
72+
Ok(None)
73+
}
74+
}
75+
76+
fn block_cid_to_required_parent_tipset(bs: &impl Blockstore, block: Cid) -> anyhow::Result<Tipset> {
77+
block_cid_to_parent_tipset(bs, block)?
78+
.with_context(|| format!("failed to load parent tipset of block {block}"))
79+
}
80+
81+
#[cfg(test)]
82+
mod tests {
83+
use super::*;
84+
85+
#[test]
86+
fn test_known_blocks() {
87+
assert!(!KNOWN_BLOCKS.calibnet.is_empty());
88+
assert!(!KNOWN_BLOCKS.mainnet.is_empty());
89+
}
90+
}

src/blocks/mod.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use thiserror::Error;
66
mod block;
77
#[cfg(test)]
88
mod chain4u;
9+
pub mod checkpoints;
910
mod election_proof;
1011
mod gossip_block;
1112
mod header;

src/blocks/tipset.rs

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,6 @@ use crate::{
1919
multihash::MultihashCode,
2020
},
2121
};
22-
use ahash::HashMap;
2322
use anyhow::Context as _;
2423
use cid::Cid;
2524
use fvm_ipld_blockstore::Blockstore;
@@ -412,29 +411,20 @@ impl Tipset {
412411

413412
/// Fetch the genesis block header for a given tipset.
414413
pub fn genesis(&self, store: &impl Blockstore) -> anyhow::Result<CachingBlockHeader> {
415-
// Scanning through millions of epochs to find the genesis is quite
416-
// slow. Let's use a list of known blocks to short-circuit the search.
417-
// The blocks are hash-chained together and known blocks are guaranteed
418-
// to have a known genesis.
419-
#[derive(Serialize, Deserialize)]
420-
struct KnownHeaders {
421-
calibnet: HashMap<ChainEpoch, String>,
422-
mainnet: HashMap<ChainEpoch, String>,
423-
}
424-
425-
static KNOWN_HEADERS: OnceLock<KnownHeaders> = OnceLock::new();
426-
let headers = KNOWN_HEADERS.get_or_init(|| {
427-
serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).unwrap()
428-
});
429-
430414
for tipset in self.clone().chain(store) {
431415
// Search for known calibnet and mainnet blocks
432416
for (genesis_cid, known_blocks) in [
433-
(*calibnet::GENESIS_CID, &headers.calibnet),
434-
(*mainnet::GENESIS_CID, &headers.mainnet),
417+
(
418+
*calibnet::GENESIS_CID,
419+
&super::checkpoints::KNOWN_BLOCKS.calibnet,
420+
),
421+
(
422+
*mainnet::GENESIS_CID,
423+
&super::checkpoints::KNOWN_BLOCKS.mainnet,
424+
),
435425
] {
436426
if let Some(known_block_cid) = known_blocks.get(&tipset.epoch())
437-
&& known_block_cid == &tipset.min_ticket_block().cid().to_string()
427+
&& known_block_cid == tipset.min_ticket_block().cid()
438428
{
439429
return store
440430
.get_cbor(&genesis_cid)?

src/chain/store/chain_store.rs

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ use super::{
66
index::{ChainIndex, ResolveNullTipset},
77
tipset_tracker::TipsetTracker,
88
};
9-
use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite};
109
use crate::message::{ChainMessage, Message as MessageTrait, SignedMessage};
1110
use crate::networks::{ChainConfig, Height};
1211
use crate::rpc::eth::{eth_tx_from_signed_eth_message, types::EthHash};
@@ -17,6 +16,10 @@ use crate::shim::{
1716
};
1817
use crate::state_manager::StateOutput;
1918
use crate::utils::db::{BlockstoreExt, CborStoreExt};
19+
use crate::{
20+
blocks::checkpoints::known_tipsets,
21+
libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite},
22+
};
2023
use crate::{
2124
blocks::{CachingBlockHeader, Tipset, TipsetKey, TxMeta},
2225
db::HeaviestTipsetKeyProvider,
@@ -240,6 +243,32 @@ where
240243
Tipset::from(self.genesis_block_header())
241244
}
242245

246+
/// Find tipset at epoch `to` in the chain
247+
pub fn tipset_by_height(
248+
&self,
249+
to: ChainEpoch,
250+
from: Option<Tipset>,
251+
resolve: ResolveNullTipset,
252+
) -> Result<Tipset, Error> {
253+
let best_known_from = if let Ok(known_tipsets) =
254+
known_tipsets(self.blockstore(), &self.chain_config().network)
255+
&& let Some(ts) = known_tipsets.iter().find(|ts| ts.epoch() > to).cloned()
256+
{
257+
Some(ts)
258+
} else {
259+
None
260+
};
261+
let from = match (from, best_known_from) {
262+
// prefer `b` when `b` is closer to `to`
263+
(Some(a), Some(b)) if b.epoch() < a.epoch() => b,
264+
// prefer `a` when presents
265+
(Some(a), _) => a,
266+
(None, Some(b)) => b,
267+
// fallback to chain head
268+
(None, None) => self.heaviest_tipset(),
269+
};
270+
self.chain_index().tipset_by_height(to, from, resolve)
271+
}
243272
/// Subscribes head changes.
244273
pub fn subscribe_head_changes(&self) -> broadcast::Receiver<HeadChanges> {
245274
self.head_changes_tx.subscribe()

src/dev/subcommands/state_cmd.rs

Lines changed: 4 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -85,16 +85,9 @@ impl ComputeCommand {
8585
let (ts, ts_next) = {
8686
// We don't want to track all entries that are visited by `tipset_by_height`
8787
db.pause_tracking();
88-
let ts = chain_store.chain_index().tipset_by_height(
89-
epoch,
90-
chain_store.heaviest_tipset(),
91-
ResolveNullTipset::TakeOlder,
92-
)?;
93-
let ts_next = chain_store.chain_index().tipset_by_height(
94-
epoch + 1,
95-
chain_store.heaviest_tipset(),
96-
ResolveNullTipset::TakeNewer,
97-
)?;
88+
let ts = chain_store.tipset_by_height(epoch, None, ResolveNullTipset::TakeOlder)?;
89+
let ts_next =
90+
chain_store.tipset_by_height(epoch + 1, None, ResolveNullTipset::TakeNewer)?;
9891
db.resume_tracking();
9992
SettingsStoreExt::write_obj(
10093
&db.tracker,
@@ -215,11 +208,7 @@ impl ValidateCommand {
215208
let ts = {
216209
// We don't want to track all entries that are visited by `tipset_by_height`
217210
db.pause_tracking();
218-
let ts = chain_store.chain_index().tipset_by_height(
219-
epoch,
220-
chain_store.heaviest_tipset(),
221-
ResolveNullTipset::TakeOlder,
222-
)?;
211+
let ts = chain_store.tipset_by_height(epoch, None, ResolveNullTipset::TakeOlder)?;
223212
db.resume_tracking();
224213
SettingsStoreExt::write_obj(&db.tracker, crate::db::setting_keys::HEAD_KEY, ts.key())?;
225214
// Only track the desired tipset

src/rpc/methods/chain.rs

Lines changed: 18 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -178,9 +178,9 @@ impl RpcMethod<0> for ChainGetFinalizedTipset {
178178
Err(_) => {
179179
// fallback to ec finality
180180
tracing::warn!("F3 finalization unavailable, falling back to EC finality");
181-
let ec_tipset = ctx.chain_index().tipset_by_height(
181+
let ec_tipset = ctx.chain_store().tipset_by_height(
182182
ec_finality_epoch,
183-
head,
183+
None,
184184
ResolveNullTipset::TakeOlder,
185185
)?;
186186
Ok(ec_tipset)
@@ -438,8 +438,8 @@ impl RpcMethod<1> for ForestChainExport {
438438

439439
let head = ctx.chain_store().load_required_tipset_or_heaviest(&tsk)?;
440440
let start_ts =
441-
ctx.chain_index()
442-
.tipset_by_height(epoch, head, ResolveNullTipset::TakeOlder)?;
441+
ctx.chain_store()
442+
.tipset_by_height(epoch, Some(head), ResolveNullTipset::TakeOlder)?;
443443

444444
let options = Some(ExportOptions {
445445
skip_checksum,
@@ -629,10 +629,9 @@ impl RpcMethod<1> for ForestChainExportDiff {
629629
);
630630
}
631631

632-
let head = ctx.chain_store().heaviest_tipset();
633632
let start_ts =
634-
ctx.chain_index()
635-
.tipset_by_height(from, head, ResolveNullTipset::TakeOlder)?;
633+
ctx.chain_store()
634+
.tipset_by_height(from, None, ResolveNullTipset::TakeOlder)?;
636635

637636
crate::tool::subcommands::archive_cmd::do_export(
638637
&ctx.store_owned(),
@@ -929,9 +928,9 @@ impl RpcMethod<2> for ChainGetTipSetByHeight {
929928
let ts = ctx
930929
.chain_store()
931930
.load_required_tipset_or_heaviest(&tipset_key)?;
932-
let tss = ctx
933-
.chain_index()
934-
.tipset_by_height(height, ts, ResolveNullTipset::TakeOlder)?;
931+
let tss =
932+
ctx.chain_store()
933+
.tipset_by_height(height, Some(ts), ResolveNullTipset::TakeOlder)?;
935934
Ok(tss)
936935
}
937936
}
@@ -959,9 +958,9 @@ impl RpcMethod<2> for ChainGetTipSetAfterHeight {
959958
let ts = ctx
960959
.chain_store()
961960
.load_required_tipset_or_heaviest(&tipset_key)?;
962-
let tss = ctx
963-
.chain_index()
964-
.tipset_by_height(height, ts, ResolveNullTipset::TakeNewer)?;
961+
let tss =
962+
ctx.chain_store()
963+
.tipset_by_height(height, Some(ts), ResolveNullTipset::TakeNewer)?;
965964
Ok(tss)
966965
}
967966
}
@@ -1102,9 +1101,9 @@ impl ChainGetTipSetV2 {
11021101
if finalized.epoch() >= safe_height {
11031102
Ok(finalized)
11041103
} else {
1105-
Ok(ctx.chain_index().tipset_by_height(
1104+
Ok(ctx.chain_store().tipset_by_height(
11061105
safe_height,
1107-
head,
1106+
None,
11081107
ResolveNullTipset::TakeOlder,
11091108
)?)
11101109
}
@@ -1140,9 +1139,9 @@ impl ChainGetTipSetV2 {
11401139
pub fn get_ec_finalized_tipset(ctx: &Ctx<impl Blockstore>) -> anyhow::Result<Tipset> {
11411140
let head = ctx.chain_store().heaviest_tipset();
11421141
let ec_finality_epoch = (head.epoch() - ctx.chain_config().policy.chain_finality).max(0);
1143-
Ok(ctx.chain_index().tipset_by_height(
1142+
Ok(ctx.chain_store().tipset_by_height(
11441143
ec_finality_epoch,
1145-
head,
1144+
None,
11461145
ResolveNullTipset::TakeOlder,
11471146
)?)
11481147
}
@@ -1160,9 +1159,9 @@ impl ChainGetTipSetV2 {
11601159
// Get tipset by height.
11611160
if let Some(height) = &selector.height {
11621161
let anchor = Self::get_tipset_by_anchor(ctx, height.anchor.as_ref()).await?;
1163-
let ts = ctx.chain_index().tipset_by_height(
1162+
let ts = ctx.chain_store().tipset_by_height(
11641163
height.at,
1165-
anchor,
1164+
Some(anchor),
11661165
height.resolve_null_tipset_policy(),
11671166
)?;
11681167
return Ok(ts);

src/rpc/methods/eth.rs

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -932,9 +932,7 @@ fn resolve_block_number_tipset<DB: Blockstore>(
932932
if height > head.epoch() - 1 {
933933
bail!("requested a future epoch (beyond \"latest\")");
934934
}
935-
Ok(chain
936-
.chain_index()
937-
.tipset_by_height(height, head, resolve)?)
935+
Ok(chain.tipset_by_height(height, None, resolve)?)
938936
}
939937

940938
fn resolve_block_hash_tipset<DB: Blockstore>(
@@ -947,10 +945,7 @@ fn resolve_block_hash_tipset<DB: Blockstore>(
947945
// verify that the tipset is in the canonical chain
948946
if require_canonical {
949947
// walk up the current chain (our head) until we reach ts.epoch()
950-
let walk_ts =
951-
chain
952-
.chain_index()
953-
.tipset_by_height(ts.epoch(), chain.heaviest_tipset(), resolve)?;
948+
let walk_ts = chain.tipset_by_height(ts.epoch(), None, resolve)?;
954949
// verify that it equals the expected tipset
955950
if walk_ts != ts {
956951
bail!("tipset is not canonical");

src/rpc/methods/eth/filter/mod.rs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -408,9 +408,9 @@ impl EthEventHandler {
408408
*range.end()
409409
};
410410

411-
let max_tipset = ctx.chain_index().tipset_by_height(
411+
let max_tipset = ctx.chain_store().tipset_by_height(
412412
max_height,
413-
ctx.chain_store().heaviest_tipset(),
413+
None,
414414
ResolveNullTipset::TakeOlder,
415415
)?;
416416
for tipset in max_tipset

src/rpc/methods/eth/tipset_resolver.rs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -157,9 +157,9 @@ where
157157
pub fn get_ec_safe_tipset(&self) -> anyhow::Result<Tipset> {
158158
let head = self.ctx.chain_store().heaviest_tipset();
159159
let safe_height = (head.epoch() - SAFE_HEIGHT_DISTANCE).max(0);
160-
Ok(self.ctx.chain_index().tipset_by_height(
160+
Ok(self.ctx.chain_store().tipset_by_height(
161161
safe_height,
162-
head,
162+
None,
163163
ResolveNullTipset::TakeOlder,
164164
)?)
165165
}
@@ -171,9 +171,9 @@ where
171171
let head = self.ctx.chain_store().heaviest_tipset();
172172
let ec_finality_epoch =
173173
(head.epoch() - self.ctx.chain_config().policy.chain_finality).max(0);
174-
Ok(self.ctx.chain_index().tipset_by_height(
174+
Ok(self.ctx.chain_store().tipset_by_height(
175175
ec_finality_epoch,
176-
head,
176+
None,
177177
ResolveNullTipset::TakeOlder,
178178
)?)
179179
}

0 commit comments

Comments
 (0)