Skip to content

Commit

Permalink
refactor: simplify ChainIndex, hoist out checkpoints (#3220)
Browse files Browse the repository at this point in the history
Co-authored-by: Aatif Syed <38045910+aatifsyed@users.noreply.github.com>
  • Loading branch information
lemmih and aatifsyed authored Jul 19, 2023
1 parent df5c5a3 commit 68a0fd8
Show file tree
Hide file tree
Showing 21 changed files with 397 additions and 489 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,9 @@

- [#3189](https://github.com/ChainSafe/forest/issues/3189): Changed the database
organisation to use multiple columns. The database will need to be recreated.
- [#3220](https://github.com/ChainSafe/forest/pull/3220): Removed the
`forest-cli chain validate-tipset-checkpoints` and
`forest-cli chain tipset-hash` commands.

### Added

Expand Down
52 changes: 52 additions & 0 deletions build/known_blocks.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
# This file maps epochs to block headers for calibnet and mainnet. Forest use
# this mapping to quickly identify the origin network of a tipset.
#
# Block headers can be inspected on filfox:
# - https://filfox.info/en/block/bafy2bzacebnfm6dvxo7sm5thcxnv3kttoamb53uxycnvtdxgk5mh7d73qlly2
# - https://calibration.filfox.info/en/block/bafy2bzacedhkkz76zdekpexha55b42eop42e24ajmajm26wws4nbvtq7louvu
#
# This file was generated with `forest-cli archive checkpoints`
calibnet:
691200: bafy2bzacedhkkz76zdekpexha55b42eop42e24ajmajm26wws4nbvtq7louvu
604800: bafy2bzacec7prcdtac2blaeblzsorwpndtbjsz3k7tiyvwmellajinaaavpyo
518400: bafy2bzacecforpkd3h6hb46r5u3sqipcpzhws7itojwr65fj75nujftheeqya
432000: bafy2bzaceb2bryss7ixglbvpjyr2wcsdh3v4stgr7jarhbxdkzrzgfc7jo54s
345600: bafy2bzaceczzut4ogoavwc3hhrbucgzwm4ty5ifrbozhuce4mm3iqcapbg2l2
259200: bafy2bzaceajlesebltywlns4ctneyqtva7zut4fwrbihafm6c3rfydy5i4bw2
172800: bafy2bzacebluce5la2lbyjlgmup6mx4xytnnilsf4oy6v4omon2ivyduqwlsq
86400: bafy2bzaceatx7tlwdhez6vyias5qlhaxa54vjftigbuqzfsmdqduc6jdiclzc
mainnet:
2937600: bafy2bzacec3jsm2p6v4vtyim6cehl33bbf3qa4whdptjmbkm4bcmsrkow2ddk
2851200: bafy2bzacecs64y3uj6r7ikajjiahkgzrdqb246dprus2jipgsnck5amq2ekpa
2764800: bafy2bzaceco5ad55adwn5qyzmjzthgpxulciqlj5ydj5jdqomcmya5w4laqw2
2678400: bafy2bzacecoypmmzncy4sxno6kbqwps4ylr7tqf5qcdrxvewiycmg7z5a4akg
2592000: bafy2bzacec6gkk66djzdlvwpm3ij4dmgl27jxt5qhusgslhcprrrue46gugei
2505600: bafy2bzaceahcpwgeu7echjebrcdc2kow6ma4qws5kljyydrujrnat7mo4goe4
2419200: bafy2bzacechvixq5nbkhhlcdxgtcax6u5ti4owzywt2jaxxmky3wedlodhtfm
2332800: bafy2bzaceapj6btckm2uurejddv4unroox4sq3ntorseizn72ph6wbnhpxnck
2246400: bafy2bzacebr2jcw3esw6kvd7oi6h76c4zj2t3ncmab2psgpchsk6vswmshmoi
2160000: bafy2bzacedrntfl4tsa4xb2jkqpegbkt6t2cbnj6osfsha2hwhuury4s5wn2c
2073600: bafy2bzacedct5jwgjedkrvskseihsab7koqxolhnbveihtygob24sbmf26zrk
1987200: bafy2bzacec7yhpo4o7jhdfcbnl4mevulp2ifxg6ojhub6xfrg3jdfajuxx3oc
1900800: bafy2bzacedvvcatpaowihbobz53s6oiwcq5sujdrrswztf65nhgoxqr77rlio
1814400: bafy2bzaced4dyvpbogdif4xfnqiqxu2pjb3xqjkzs7muycfgq3snv7ksorvnc
1728000: bafy2bzaceca3bvwdu67rlxh6fr4fbtggii3pjt4tcrtfmsqzt5epc4y6emqxa
1641600: bafy2bzacecsq3z3xpfe3dszo4ftt53g44dex2ubya3f3wtz7vkbsnpksri6cy
1555200: bafy2bzacebnfm6dvxo7sm5thcxnv3kttoamb53uxycnvtdxgk5mh7d73qlly2
1468800: bafy2bzacedo7ufcauavu27ptpbjdxquvd6zzlnv7wduysrgms3wrhfdxmifwu
1382400: bafy2bzacea7z2hklqikueykuf5ayb6ulmnijj3nuhtg3dya4ttojmr5m2rug6
1296000: bafy2bzacecky7lbzjkqeqcoglrgfgto76ztwgb5gxaqmrsxfvjqsjr7r427uk
1209600: bafy2bzacediunbjqtmbxdp7xynkifhcvgzv5uhjwhb6aocgbskd55d7zw6uv6
1123200: bafy2bzaceax6x7iildnssg6s3rzinru7odinha6jfwmk5rxpgqxh6rjkh2ysm
1036800: bafy2bzacebg5enb2gsbydtpegmjvwifhq7bsgibllqrczhn56aartok2hsz5c
950400: bafy2bzacecwpmsp4reyhbim2xpulhxnsp3l6duo267vsbwsiyyiszpgw5isfi
864000: bafy2bzacea7yrzlkpee6fnw2ckoxv3rxxidq7hlhk732znxqwtoqsqzgw5sf4
777600: bafy2bzacealxm7nc25xfo7jzzaal2ruakr3n2xwcq6wt7c3ntj4ksczlumf56
691200: bafy2bzaceavrlfce6qxtlrahevzfijpzrpgt2uwkfae3333z4gafd4lj7wodk
604800: bafy2bzacedexv5uhodwwhaxs7hqo7fgwx33oo22pswxc73gyrawdmnbzl3h7y
518400: bafy2bzacedhzi7hqtwelbwcejvq45xvcqlcrymersv2dlfqh3niqs4ohdpm5i
432000: bafy2bzacebutfwbn55lid7g4nsxr4dwje7tcie4npyidsfjvvc5ohnem4yaue
345600: bafy2bzaceavqxds47ihtcrfiihpxzlef6jvhbdnbpfzxsk6h4lczokxkl7bcw
259200: bafy2bzacecebsgo4guk6t6tukqux265cbzcav6pg6bihlay2z3y3mp3ositws
172800: bafy2bzaceajphhjy3saqf573ewxhfyu7dwwb7yodcwass64kevbktlk4wcawu
86400: bafy2bzacec7ywitfdjr5hkglybvgavg3hzo7oluwnmfo55aa6h2qw2lyz4nac
1 change: 0 additions & 1 deletion scripts/gen_coverage_report.sh
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,6 @@ SNAPSHOT_PATH=$(find "$TMP_DIR" -name \*.car | head -n 1)
cov forest --chain calibnet --encrypt-keystore false --import-snapshot "$SNAPSHOT_PATH" --height=-200 --detach --track-peak-rss --save-token "$TOKEN_PATH"
cov forest-cli sync wait
cov forest-cli sync status
cov forest-cli chain validate-tipset-checkpoints
cov forest-cli --chain calibnet db gc
cov forest-cli --chain calibnet db stats
cov forest-cli snapshot export
Expand Down
17 changes: 0 additions & 17 deletions scripts/tests/calibnet_other_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,6 @@ source "$(dirname "$0")/harness.sh"

forest_init

echo "Validating checkpoint tipset hashes"
$FOREST_CLI_PATH chain validate-tipset-checkpoints

echo "Running database garbage collection"
forest_check_db_stats
$FOREST_CLI_PATH db gc
Expand All @@ -20,20 +17,6 @@ forest_check_db_stats
echo "Testing js console"
$FOREST_CLI_PATH attach --exec 'showPeers()'

# Get the checkpoint hash at epoch 424000. This output isn't affected by the
# number of recent state roots we store (2k at the time of writing) and this
# output should never change.
echo "Checkpoint hash test"
EXPECTED_HASH="Chain: calibnet
Epoch: 424000
Checkpoint hash: 8cab45fd441c1fb68d2fd7e45d5e9ef9a5d3b45f68b414ab3e244233dd8e37fc4dacffc8966b2dc8804d4abf92c8a57efda743e26db6805a77a4feac19478293"
ACTUAL_HASH=$($FOREST_CLI_PATH --chain calibnet chain tipset-hash 424000)
if [[ $EXPECTED_HASH != "$ACTUAL_HASH" ]]; then
printf "Invalid tipset hash:\n%s" "$ACTUAL_HASH"
printf "Expected:\n%s" "$EXPECTED_HASH"
exit 1
fi

echo "Test dev commands (which could brick the node/cause subsequent snapshots to fail)"

echo "Test subcommand: chain set-head"
Expand Down
4 changes: 4 additions & 0 deletions scripts/tests/forest_cli_check.sh
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,10 @@ pushd "$(mktemp --directory)"
if "$FOREST_CLI_PATH" snapshot validate --check-network mainnet "$validate_me"; then
exit 1
fi

: : check that it contains at least one expected checkpoint
# If calibnet is reset or the checkpoint interval is changed, this check has to be updated
"$FOREST_CLI_PATH" archive checkpoints "$validate_me" | grep bafy2bzaceatx7tlwdhez6vyias5qlhaxa54vjftigbuqzfsmdqduc6jdiclzc
rm -- *
popd

9 changes: 5 additions & 4 deletions src/blocks/header/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,9 +36,9 @@ pub mod json;
/// use cid::multihash::MultihashDigest;
///
/// BlockHeader::builder()
/// .message_receipts(Cid::new_v1(DAG_CBOR, Identity.digest(&[]))) // required
/// .state_root(Cid::new_v1(DAG_CBOR, Identity.digest(&[]))) // required
/// .miner_address(Address::new_id(0)) // required
/// .message_receipts(Cid::new_v1(DAG_CBOR, Identity.digest(&[]))) // optional
/// .state_root(Cid::new_v1(DAG_CBOR, Identity.digest(&[]))) // optional
/// .miner_address(Address::new_id(0)) // optional
/// .messages(Cid::new_v1(DAG_CBOR, Identity.digest(&[]))) // optional
/// .beacon_entries(Vec::new()) // optional
/// .winning_post_proof(Vec::new()) // optional
Expand All @@ -54,7 +54,7 @@ pub mod json;
/// .build()
/// .unwrap();
/// ```
#[derive(Clone, Debug, Builder)]
#[derive(Clone, Debug, Default, Builder)]
#[builder(name = "BlockHeaderBuilder")]
pub struct BlockHeader {
// CHAIN LINKING
Expand Down Expand Up @@ -84,6 +84,7 @@ pub struct BlockHeader {

// MINER INFO
/// `miner_address` is the address of the miner actor that mined this block
#[builder(default)]
miner_address: Address,

// STATE
Expand Down
52 changes: 48 additions & 4 deletions src/blocks/tipset.rs
Original file line number Diff line number Diff line change
@@ -1,19 +1,20 @@
// Copyright 2019-2023 ChainSafe Systems
// SPDX-License-Identifier: Apache-2.0, MIT

use std::fmt;
use std::{fmt, sync::OnceLock};

use crate::shim::address::Address;
use crate::shim::clock::ChainEpoch;
use crate::networks::{calibnet, mainnet};
use crate::shim::{address::Address, clock::ChainEpoch};
use crate::utils::cid::CidCborExt;
use ahash::{HashSet, HashSetExt};
use ahash::{HashMap, HashSet, HashSetExt};
use anyhow::Context as _;
use cid::Cid;
use fvm_ipld_blockstore::Blockstore;
use fvm_ipld_encoding::CborStore;
use num::BigInt;
use once_cell::sync::OnceCell;
use serde::{Deserialize, Serialize};
use std::str::FromStr;
use tracing::info;

use super::{Block, BlockHeader, Error, Ticket};
Expand Down Expand Up @@ -265,6 +266,49 @@ impl Tipset {
})
})
}

/// Fetch the genesis block header for a given tipset.
pub fn genesis(&self, store: impl Blockstore) -> anyhow::Result<BlockHeader> {
// Scanning through millions of epochs to find the genesis is quite
// slow. Let's use a list of known blocks to short-circuit the search.
// The blocks are hash-chained together and known blocks are guaranteed
// to have a known genesis.
#[derive(Serialize, Deserialize)]
struct KnownHeaders {
calibnet: HashMap<ChainEpoch, String>,
mainnet: HashMap<ChainEpoch, String>,
}

static KNOWN_HEADERS: OnceLock<KnownHeaders> = OnceLock::new();
let headers = KNOWN_HEADERS.get_or_init(|| {
serde_yaml::from_str(include_str!("../../build/known_blocks.yaml")).unwrap()
});

let calibnet_cid = Cid::from_str(calibnet::GENESIS_CID).unwrap();
let mainnet_cid = Cid::from_str(mainnet::GENESIS_CID).unwrap();

for tipset in self.clone().chain(&store) {
// Search for known calibnet and mainnet blocks
for (genesis_cid, known_blocks) in [
(calibnet_cid, &headers.calibnet),
(mainnet_cid, &headers.mainnet),
] {
if let Some(known_block_cid) = known_blocks.get(&tipset.epoch()) {
if known_block_cid == &tipset.min_ticket_block().cid().to_string() {
return store
.get_cbor(&genesis_cid)?
.ok_or_else(|| anyhow::anyhow!("Genesis block missing from database"));
}
}
}

// If no known blocks are found, we'll eventually hit the genesis tipset.
if tipset.epoch() == 0 {
return Ok(tipset.min_ticket_block().clone());
}
}
anyhow::bail!("Genesis block not found")
}
}

/// `FullTipset` is an expanded version of a tipset that contains all the blocks
Expand Down
107 changes: 11 additions & 96 deletions src/chain/store/chain_store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use crate::interpreter::BlockMessages;
use crate::ipld::{walk_snapshot, WALK_SNAPSHOT_PROGRESS_EXPORT};
use crate::libp2p_bitswap::{BitswapStoreRead, BitswapStoreReadWrite};
use crate::message::{ChainMessage, Message as MessageTrait, SignedMessage};
use crate::networks::{ChainConfig, NetworkChain};
use crate::networks::ChainConfig;
use crate::shim::clock::ChainEpoch;
use crate::shim::{
address::Address, econ::TokenAmount, executor::Receipt, message::Message,
Expand Down Expand Up @@ -43,7 +43,7 @@ use tokio::sync::{
use tracing::{debug, info, warn};

use super::{
index::{checkpoint_tipsets, ChainIndex},
index::{ChainIndex, ResolveNullTipset},
tipset_tracker::TipsetTracker,
Error,
};
Expand Down Expand Up @@ -74,7 +74,7 @@ pub struct ChainStore<DB> {
pub db: Arc<DB>,

/// Used as a cache for tipset `lookbacks`.
chain_index: ChainIndex<DB>,
pub chain_index: ChainIndex<DB>,

/// Tracks blocks for the purpose of forming tipsets.
tipset_tracker: TipsetTracker<DB>,
Expand Down Expand Up @@ -255,11 +255,6 @@ where
self.chain_index.load_tipset(tsk)
}

/// Returns Tipset key hash from key-value store from provided CIDs
pub fn tipset_hash_from_keys(&self, tsk: &TipsetKeys) -> String {
checkpoint_tipsets::tipset_hash(tsk)
}

/// Determines if provided tipset is heavier than existing known heaviest
/// tipset
fn update_heaviest<S>(&self, ts: Arc<Tipset>) -> Result<(), Error>
Expand Down Expand Up @@ -300,92 +295,6 @@ where
let _did_work = file.remove(cid);
}

/// Returns the tipset behind `tsk` at a given `height`.
/// If the given height is a null round:
/// - If `prev` is `true`, the tipset before the null round is returned.
/// - If `prev` is `false`, the tipset following the null round is returned.
#[tracing::instrument(skip(self, ts))]
pub fn tipset_by_height(
&self,
height: ChainEpoch,
ts: Arc<Tipset>,
prev: bool,
) -> Result<Arc<Tipset>, Error> {
if height > ts.epoch() {
return Err(Error::Other(
"searching for tipset that has a height less than starting point".to_owned(),
));
}
if height == ts.epoch() {
return Ok(ts);
}

let mut lbts = self.chain_index.get_tipset_by_height(ts.clone(), height)?;

if lbts.epoch() < height {
warn!(
"chain index returned the wrong tipset at height {}, using slow retrieval",
height
);
lbts = self
.chain_index
.get_tipset_by_height_without_cache(ts, height)?;
}

if lbts.epoch() == height || !prev {
Ok(lbts)
} else {
self.chain_index.load_tipset(lbts.parents())
}
}

pub fn validate_tipset_checkpoints(
&self,
from: Arc<Tipset>,
network: &NetworkChain,
) -> Result<(), Error> {
info!(
"Validating {network} tipset checkpoint hashes from: {}",
from.epoch()
);

let Some(mut hashes) = checkpoint_tipsets::get_tipset_hashes(network) else {
info!("No checkpoint tipsets found for network: {network}, skipping validation.");
return Ok(());
};

let mut ts = from;
let tipset_hash = checkpoint_tipsets::tipset_hash(ts.key());
hashes.remove(&tipset_hash);

loop {
let pts = self.chain_index.load_tipset(ts.parents())?;
let tipset_hash = checkpoint_tipsets::tipset_hash(ts.key());
hashes.remove(&tipset_hash);

ts = pts;

if ts.epoch() == 0 {
break;
}
}

if !hashes.is_empty() {
return Err(Error::Other(format!(
"Found tipset hash(es) on {network} that are no longer valid: {hashes:?}"
)));
}

if !checkpoint_tipsets::validate_genesis_cid(&ts, network) {
return Err(Error::Other(format!(
"Genesis cid {:?} on {network} network does not match with one stored in checkpoint registry",
ts.key().cid()
)));
}

Ok(())
}

/// Finds the latest beacon entry given a tipset up to 20 tipsets behind
pub fn latest_beacon_entry(&self, ts: &Tipset) -> Result<BeaconEntry, Error> {
let check_for_beacon_entry = |ts: &Tipset| {
Expand Down Expand Up @@ -594,7 +503,8 @@ where
round: ChainEpoch,
) -> Result<TipsetKeys, Error> {
let ts = self
.tipset_by_height(round, tipset, true)
.chain_index
.tipset_by_height(round, tipset, ResolveNullTipset::TakeOlder)
.map_err(|e| Error::Other(format!("Could not get tipset by height {e:?}")))?;
Ok(ts.key().clone())
}
Expand Down Expand Up @@ -647,7 +557,12 @@ where
}

let next_ts = self
.tipset_by_height(lbr + 1, heaviest_tipset.clone(), false)
.chain_index
.tipset_by_height(
lbr + 1,
heaviest_tipset.clone(),
ResolveNullTipset::TakeNewer,
)
.map_err(|e| Error::Other(format!("Could not get tipset by height {e:?}")))?;
if lbr > next_ts.epoch() {
return Err(Error::Other(format!(
Expand Down
Loading

0 comments on commit 68a0fd8

Please sign in to comment.