use std::sync::atomic::{AtomicBool, Ordering};
use std::{cell::Ref, sync::Arc};
use crate::blocks::{CachingBlockHeader, Tipset};
use crate::chain::{
index::{ChainIndex, ResolveNullTipset},
store::ChainStore,
};
use crate::interpreter::errors::Error;
use crate::networks::ChainConfig;
use crate::prelude::*;
use crate::shim::{
actors::{MinerActorStateLoad as _, miner},
address::Address,
clock::ChainEpoch,
consensus::{ConsensusFault, ConsensusFaultType},
externs::Rand,
gas::{Gas, GasTracker, price_list_by_network_version},
state_tree::StateTree,
version::NetworkVersion,
};
use crate::utils::encoding::from_slice_with_fallback;
use anyhow::bail;
use fvm_ipld_blockstore::tracking::{BSStats, TrackingBlockstore};
pub struct ForestExterns {
pub(super) rand: Box<dyn Rand>,
heaviest_tipset: Tipset,
epoch: ChainEpoch,
root: Cid,
chain_index: ChainIndex,
chain_config: Arc<ChainConfig>,
bail: AtomicBool,
}
impl ForestExterns {
pub fn new(
rand: Box<dyn Rand>,
heaviest_tipset: Tipset,
epoch: ChainEpoch,
root: Cid,
chain_index: ChainIndex,
chain_config: Arc<ChainConfig>,
) -> Self {
Self {
rand,
heaviest_tipset,
epoch,
root,
chain_index,
chain_config,
bail: AtomicBool::new(false),
}
}
pub fn bail(&self) -> bool {
self.bail.load(Ordering::Relaxed)
}
fn get_lookback_tipset_state_root_for_round(&self, height: ChainEpoch) -> anyhow::Result<Cid> {
let (_, st) = ChainStore::get_lookback_tipset_for_round_blocking(
&self.chain_index,
&self.chain_config,
&self.heaviest_tipset,
height,
)?;
Ok(st)
}
fn worker_key_at_lookback(
&self,
miner_addr: &Address,
height: ChainEpoch,
) -> anyhow::Result<(Address, i64)> {
if height < self.epoch - self.chain_config.policy.chain_finality {
bail!(
"cannot get worker key (current epoch: {}, height: {height}, miner_addr: {miner_addr})",
self.epoch
);
}
let prev_root = self.get_lookback_tipset_state_root_for_round(height)?;
let lb_state = StateTree::new_from_root(self.chain_index.db(), &prev_root)?;
let actor = lb_state.get_actor(miner_addr)?.ok_or_else(|| {
anyhow::anyhow!(
"actor not found, current epoch: {}, height: {height}, miner_addr: {miner_addr}",
self.epoch
)
})?;
let tbs = TrackingBlockstore::new(self.chain_index.db());
let ms = miner::State::load(&tbs, actor.code, actor.state)?;
let worker = ms.info(&tbs)?.worker;
let state = StateTree::new_from_root(self.chain_index.db(), &self.root)?;
let addr = state.resolve_to_deterministic_address(&tbs, worker)?;
let network_version = self.chain_config.network_version(self.epoch);
let gas_used = cal_gas_used_from_stats(tbs.stats.borrow(), network_version)?;
Ok((addr, gas_used.round_up() as i64))
}
fn verify_block_signature(&self, bh: &CachingBlockHeader) -> anyhow::Result<i64, Error> {
let (worker_addr, gas_used) = self.worker_key_at_lookback(&bh.miner_address, bh.epoch)?;
bh.verify_signature_against(&worker_addr)?;
Ok(gas_used)
}
pub(super) fn verify_consensus_fault(
&self,
h1: &[u8],
h2: &[u8],
extra: &[u8],
) -> anyhow::Result<(Option<ConsensusFault>, i64)> {
self.verify_consensus_fault_impl(h1, h2, extra)
.inspect_err(|e| {
tracing::warn!(
"verify_consensus_fault failed, ts@{}: {}, error: {e:#?}",
self.heaviest_tipset.epoch(),
self.heaviest_tipset.key()
);
})
}
fn verify_consensus_fault_impl(
&self,
h1: &[u8],
h2: &[u8],
extra: &[u8],
) -> anyhow::Result<(Option<ConsensusFault>, i64)> {
let mut total_gas: i64 = 0;
if h1 == h2 {
bail!(
"no consensus fault: submitted blocks are the same: {:?}, {:?}",
h1,
h2
);
};
let bh_1 = from_slice_with_fallback::<CachingBlockHeader>(h1)?;
let bh_2 = from_slice_with_fallback::<CachingBlockHeader>(h2)?;
if bh_1.cid() == bh_2.cid() {
bail!("no consensus fault: submitted blocks are the same");
}
if bh_1.miner_address != bh_2.miner_address {
bail!(
"no consensus fault: blocks not mined by same miner: {:?}, {:?}",
bh_1.miner_address,
bh_2.miner_address
);
};
if bh_2.epoch < bh_1.epoch {
bail!(
"first block must not be of higher height than second: {:?}, {:?}",
bh_1.epoch,
bh_2.epoch
);
};
let mut fault_type: Option<ConsensusFaultType> = None;
if bh_1.epoch == bh_2.epoch {
fault_type = Some(ConsensusFaultType::DoubleForkMining);
};
if bh_1.parents == bh_2.parents && bh_1.epoch != bh_2.epoch {
fault_type = Some(ConsensusFaultType::TimeOffsetMining);
};
if !extra.is_empty() {
let bh_3 = from_slice_with_fallback::<CachingBlockHeader>(extra)?;
if bh_1.parents == bh_3.parents
&& bh_1.epoch == bh_3.epoch
&& bh_2.parents.contains(*bh_3.cid())
&& !bh_2.parents.contains(*bh_1.cid())
{
fault_type = Some(ConsensusFaultType::ParentGrinding);
}
};
match fault_type {
None => {
Ok((None, total_gas))
}
Some(fault_type) => {
for block_header in [&bh_1, &bh_2] {
let res = self.verify_block_signature(block_header);
match res {
Err(Error::Signature(_)) => return Ok((None, total_gas)),
Err(Error::Lookup(_)) => return Ok((None, total_gas)),
Ok(gas_used) => total_gas += gas_used,
}
}
let ret = Some(ConsensusFault {
target: bh_1.miner_address,
epoch: bh_2.epoch,
fault_type,
});
Ok((ret, total_gas))
}
}
}
pub(super) fn get_tipset_cid(&self, epoch: ChainEpoch) -> anyhow::Result<Cid> {
self.get_tipset_cid_impl(epoch).inspect_err(|e| {
tracing::warn!(
"get_tipset_cid failed, ts@{}: {}, error: {e:#?}",
self.heaviest_tipset.epoch(),
self.heaviest_tipset.key()
);
})
}
fn get_tipset_cid_impl(&self, epoch: ChainEpoch) -> anyhow::Result<Cid> {
let ts = self
.chain_index
.load_required_tipset_by_height(
epoch,
self.heaviest_tipset.clone(),
ResolveNullTipset::TakeOlder,
)
.context("Failed to get tipset cid")?;
ts.key().cid()
}
}
pub fn cal_gas_used_from_stats(
stats: Ref<BSStats>,
network_version: NetworkVersion,
) -> anyhow::Result<Gas> {
let price_list = price_list_by_network_version(network_version);
let gas_tracker = GasTracker::new(Gas::new(i64::MAX as u64).into(), Gas::new(0).into(), false);
for _ in 0..stats.r {
gas_tracker
.apply_charge(price_list.on_block_open_base().into())?
.stop();
}
if stats.w > 0 {
gas_tracker
.apply_charge(price_list.on_block_link(stats.bw).into())?
.stop();
for _ in 1..stats.w {
gas_tracker
.apply_charge(price_list.on_block_link(0).into())?
.stop();
}
}
Ok(gas_tracker.gas_used().into())
}
#[cfg(test)]
mod tests {
use super::*;
use std::cell::RefCell;
#[test]
fn test_cal_gas_used_from_stats_1_read() {
test_cal_gas_used_from_stats_inner(1, &[])
}
#[test]
fn test_cal_gas_used_from_stats_1_write() {
test_cal_gas_used_from_stats_inner(0, &[100])
}
#[test]
fn test_cal_gas_used_from_stats_multi_read() {
test_cal_gas_used_from_stats_inner(10, &[])
}
#[test]
fn test_cal_gas_used_from_stats_multi_write() {
test_cal_gas_used_from_stats_inner(0, &[100, 101, 102, 103, 104, 105, 106, 107, 108, 109])
}
#[test]
fn test_cal_gas_used_from_stats_1_read_1_write() {
test_cal_gas_used_from_stats_inner(1, &[100])
}
#[test]
fn test_cal_gas_used_from_stats_multi_read_multi_write() {
test_cal_gas_used_from_stats_inner(10, &[100, 101, 102, 103, 104, 105, 106, 107, 108, 109])
}
fn test_cal_gas_used_from_stats_inner(read_count: usize, write_bytes: &[usize]) {
let network_version = NetworkVersion::V8;
let stats = BSStats {
r: read_count,
w: write_bytes.len(),
br: 0, bw: write_bytes.iter().sum(),
};
let result =
cal_gas_used_from_stats(RefCell::new(stats).borrow(), network_version).unwrap();
let price_list = price_list_by_network_version(network_version);
let tracker = GasTracker::new(Gas::new(u64::MAX).into(), Gas::new(0).into(), false);
std::iter::repeat_n((), read_count).for_each(|_| {
tracker
.apply_charge(price_list.on_block_open_base().into())
.unwrap()
.stop();
});
for &bytes in write_bytes {
tracker
.apply_charge(price_list.on_block_link(bytes).into())
.unwrap()
.stop();
}
let expected = tracker.gas_used();
assert_eq!(result, expected.into());
}
}