use std::collections::HashMap;
use fil_actors_runtime::{parse_uint_key, runtime::Policy, Map, MessageAccumulator, Multimap};
use fvm_ipld_blockstore::Blockstore;
use fvm_ipld_encoding::RawBytes;
use fvm_shared::{
address::Address,
clock::ChainEpoch,
sector::{SealVerifyInfo, StoragePower},
HAMT_BIT_WIDTH,
};
use num_traits::{Signed, Zero};
use crate::{
consensus_miner_min_power, Claim, CronEvent, State, CRON_QUEUE_AMT_BITWIDTH,
CRON_QUEUE_HAMT_BITWIDTH, MAX_MINER_PROVE_COMMITS_PER_EPOCH,
PROOF_VALIDATION_BATCH_AMT_BITWIDTH,
};
pub struct MinerCronEvent {
pub epoch: ChainEpoch,
pub payload: RawBytes,
}
type CronEventsByAddress = HashMap<Address, Vec<MinerCronEvent>>;
type ClaimsByAddress = HashMap<Address, Claim>;
type ProofsByAddress = HashMap<Address, SealVerifyInfo>;
pub struct StateSummary {
pub crons: CronEventsByAddress,
pub claims: ClaimsByAddress,
pub proofs: ProofsByAddress,
}
pub fn check_state_invariants<BS: Blockstore>(
policy: &Policy,
state: &State,
store: &BS,
) -> (StateSummary, MessageAccumulator) {
let acc = MessageAccumulator::default();
acc.require(
!state.total_raw_byte_power.is_negative(),
format!("total raw power is negative {}", state.total_raw_byte_power),
);
acc.require(
!state.total_quality_adj_power.is_negative(),
format!("total qa power is negative {}", state.total_quality_adj_power),
);
acc.require(
!state.total_bytes_committed.is_negative(),
format!("total raw power committed is negative {}", state.total_bytes_committed),
);
acc.require(
!state.total_qa_bytes_committed.is_negative(),
format!("total qa power committed is negative {}", state.total_qa_bytes_committed),
);
acc.require(
state.total_raw_byte_power <= state.total_quality_adj_power,
format!(
"total raw power {} is greater than total quality adjusted power {}",
state.total_raw_byte_power, state.total_quality_adj_power
),
);
acc.require(
state.total_bytes_committed <= state.total_qa_bytes_committed,
format!(
"committed raw power {} is greater than committed quality adjusted power {}",
state.total_bytes_committed, state.total_qa_bytes_committed
),
);
acc.require(
state.total_raw_byte_power <= state.total_bytes_committed,
format!(
"total raw power {} is greater than raw power committed {}",
state.total_raw_byte_power, state.total_bytes_committed
),
);
acc.require(
state.total_quality_adj_power <= state.total_qa_bytes_committed,
format!(
"total qa power {} is greater than qa power committed {}",
state.total_quality_adj_power, state.total_qa_bytes_committed
),
);
let crons = check_cron_invariants(state, store, &acc);
let claims = check_claims_invariants(policy, state, store, &acc);
let proofs = check_proofs_invariants(state, store, &claims, &acc);
(StateSummary { crons, claims, proofs }, acc)
}
fn check_cron_invariants<BS: Blockstore>(
state: &State,
store: &BS,
acc: &MessageAccumulator,
) -> CronEventsByAddress {
let mut cron_events_by_address = CronEventsByAddress::new();
match Multimap::from_root(
store,
&state.cron_event_queue,
CRON_QUEUE_HAMT_BITWIDTH,
CRON_QUEUE_AMT_BITWIDTH,
) {
Ok(queue) => {
let ret = queue.for_all::<_, CronEvent>(|key, events| {
let epoch = match parse_uint_key(key) {
Ok(key) => key,
Err(e) => {
acc.add(format!("non-int key in cron array: {e}"));
return Ok(());
}
} as i64;
acc.require(
epoch >= state.first_cron_epoch,
format!(
"cron event at epoch {epoch} before first_cron_epoch {}",
state.first_cron_epoch
),
);
events
.for_each(|_, event| {
cron_events_by_address.entry(event.miner_addr).or_insert(Vec::new()).push(
MinerCronEvent { epoch, payload: event.callback_payload.clone() },
);
Ok(())
})
.map_err(|e| {
anyhow::anyhow!("error iterating cron events for epoch {}: {}", epoch, e)
})
});
acc.require_no_error(ret, "error iterating cron tasks");
}
Err(e) => acc.add(format!("error loading cron event queue: {e}")),
}
cron_events_by_address
}
fn check_claims_invariants<BS: Blockstore>(
policy: &Policy,
state: &State,
store: &BS,
acc: &MessageAccumulator,
) -> ClaimsByAddress {
let mut claims_by_address = ClaimsByAddress::new();
let mut committed_raw_power = StoragePower::zero();
let mut committed_qa_power = StoragePower::zero();
let mut raw_power = StoragePower::zero();
let mut qa_power = StoragePower::zero();
let mut claims_with_sufficient_power_count = 0;
match Map::<_, Claim>::load(&state.claims, store) {
Ok(claims) => {
let ret = claims.for_each(|key, claim| {
let address = Address::from_bytes(key)?;
claims_by_address.insert(address, claim.clone());
committed_raw_power += &claim.raw_byte_power;
committed_qa_power += &claim.quality_adj_power;
let min_power =
match consensus_miner_min_power(policy, claim.window_post_proof_type) {
Ok(power) => power,
Err(e) => {
acc.add(format!(
"could not get consensus miner min power for miner {address}: {e}"
));
return Ok(());
}
};
if claim.raw_byte_power >= min_power {
claims_with_sufficient_power_count += 1;
raw_power += &claim.raw_byte_power;
qa_power += &claim.quality_adj_power;
}
Ok(())
});
acc.require_no_error(ret, "error iterating power claims");
}
Err(e) => acc.add(format!("error loading power claims: {e}")),
};
acc.require(committed_raw_power == state.total_bytes_committed, format!("sum of raw power in claims {committed_raw_power} does not match recorded bytes committed {}", state.total_bytes_committed));
acc.require(committed_qa_power == state.total_qa_bytes_committed, format!("sum of qa power in claims {committed_qa_power} does not match recorded qa power committed {}", state.total_qa_bytes_committed));
acc.require(claims_with_sufficient_power_count == state.miner_above_min_power_count, format!("claims with sufficient power {claims_with_sufficient_power_count} does not match miner_above_min_power_count {}", state.miner_above_min_power_count));
acc.require(
state.total_raw_byte_power == raw_power,
format!(
"recorded raw power {} does not match raw power in claims {raw_power}",
state.total_raw_byte_power
),
);
acc.require(
state.total_quality_adj_power == qa_power,
format!(
"recorded qa power {} does not match qa power in claims {qa_power}",
state.total_quality_adj_power
),
);
claims_by_address
}
fn check_proofs_invariants<BS: Blockstore>(
state: &State,
store: &BS,
claims: &ClaimsByAddress,
acc: &MessageAccumulator,
) -> ProofsByAddress {
if state.proof_validation_batch.is_none() {
return ProofsByAddress::default();
}
let mut proofs_by_address = ProofsByAddress::new();
match Multimap::from_root(
store,
&state.proof_validation_batch.unwrap(),
HAMT_BIT_WIDTH,
PROOF_VALIDATION_BATCH_AMT_BITWIDTH,
) {
Ok(queue) => {
let ret = queue.for_all::<_, SealVerifyInfo>(|key, infos| {
let address = Address::from_bytes(key)?;
let claim = if let Some(claim) = claims.get(&address) {
claim
} else {
acc.add(format!("miner {address} has proofs awaiting validation but no claim"));
return Ok(())
};
let ret = infos.for_each(|_, info| {
match info.registered_proof.registered_window_post_proof() {
Ok(sector_window_post_proof_type) => {
acc.require(claim.window_post_proof_type == sector_window_post_proof_type, format!("miner submitted proof with proof type {:?} different from claim {:?}", sector_window_post_proof_type, claim.window_post_proof_type));
},
Err(e) => acc.add(format!("Invalid PoSt proof: {e}"))
}
proofs_by_address.insert(address, info.clone());
Ok(())
});
if ret.is_err() {
return ret.map_err(|e| anyhow::anyhow!("error iterating proof validation batch for address {}: {}", address, e));
}
acc.require(proofs_by_address.len() as u64 <= MAX_MINER_PROVE_COMMITS_PER_EPOCH, format!("miner {address} has submitted too many proofs ({}) for batch verification", proofs_by_address.len()));
Ok(())
});
acc.require_no_error(ret, "error iterating proof validation queue");
}
Err(e) => acc.add(format!("error loading proof validation queue: {e}")),
}
proofs_by_address
}