#![allow(clippy::too_many_arguments)]
#![allow(clippy::type_complexity)]
use super::*;
use ledger_coinbase::{CoinbasePuzzle, EpochChallenge};
use synthesizer_program::FinalizeOperation;
use std::collections::HashSet;
#[cfg(not(feature = "serial"))]
use rayon::prelude::*;
impl<N: Network> Block<N> {
pub fn verify(
&self,
previous_block: &Block<N>,
current_state_root: N::StateRoot,
current_committee: &Committee<N>,
current_puzzle: &CoinbasePuzzle<N>,
current_epoch_challenge: &EpochChallenge<N>,
current_timestamp: i64,
ratified_finalize_operations: Vec<FinalizeOperation<N>>,
) -> Result<()> {
self.verify_hash(previous_block.height(), previous_block.hash())?;
let (expected_round, expected_height, expected_timestamp) =
self.verify_authority(previous_block.round(), previous_block.height(), current_committee)?;
let (
expected_cumulative_weight,
expected_cumulative_proof_target,
expected_coinbase_target,
expected_proof_target,
expected_last_coinbase_target,
expected_last_coinbase_timestamp,
expected_block_reward,
expected_puzzle_reward,
) = self.verify_solutions(previous_block, current_puzzle, current_epoch_challenge)?;
self.verify_ratifications(expected_block_reward, expected_puzzle_reward)?;
self.verify_transactions()?;
let expected_previous_state_root = current_state_root;
let expected_transactions_root = self.compute_transactions_root()?;
let expected_finalize_root = self.compute_finalize_root(ratified_finalize_operations)?;
let expected_ratifications_root = self.compute_ratifications_root()?;
let expected_solutions_root = self.compute_solutions_root()?;
let expected_subdag_root = self.compute_subdag_root()?;
self.header.verify(
expected_previous_state_root,
expected_transactions_root,
expected_finalize_root,
expected_ratifications_root,
expected_solutions_root,
expected_subdag_root,
expected_round,
expected_height,
expected_cumulative_weight,
expected_cumulative_proof_target,
expected_coinbase_target,
expected_proof_target,
expected_last_coinbase_target,
expected_last_coinbase_timestamp,
expected_timestamp,
current_timestamp,
)
}
}
impl<N: Network> Block<N> {
fn verify_hash(&self, previous_height: u32, previous_hash: N::BlockHash) -> Result<(), Error> {
let expected_height = previous_height.saturating_add(1);
ensure!(
self.previous_hash == previous_hash,
"Previous block hash is incorrect in block {expected_height} (found '{}', expected '{}')",
self.previous_hash,
previous_hash
);
let Ok(header_root) = self.header.to_root() else {
bail!("Failed to compute the Merkle root of the block header");
};
let candidate_hash = match N::hash_bhp1024(&to_bits_le![previous_hash, header_root]) {
Ok(candidate_hash) => candidate_hash,
Err(error) => bail!("Failed to compute the block hash for block {expected_height} - {error}"),
};
ensure!(
*self.block_hash == candidate_hash,
"Block hash is incorrect in block {expected_height} (found '{}', expected '{}')",
self.block_hash,
Into::<N::BlockHash>::into(candidate_hash)
);
Ok(())
}
fn verify_authority(
&self,
previous_round: u64,
previous_height: u32,
current_committee: &Committee<N>,
) -> Result<(u64, u32, i64)> {
let expected_height = previous_height.saturating_add(1);
match expected_height == 0 {
true => ensure!(self.authority.is_beacon(), "The genesis block must be a beacon block"),
false => {
#[cfg(not(any(test, feature = "test")))]
ensure!(self.authority.is_quorum(), "The next block must be a quorum block");
}
}
let expected_round = match &self.authority {
Authority::Beacon(..) => previous_round.saturating_add(1),
Authority::Quorum(subdag) => {
ensure!(
subdag.anchor_round() > previous_round,
"Subdag anchor round is not after previous block round in block {} (found '{}', expected after '{}')",
expected_height,
subdag.anchor_round(),
previous_round
);
subdag.anchor_round()
}
};
ensure!(
expected_round >= current_committee.starting_round(),
"Block {} has an invalid round (found '{expected_round}', expected at least '{}')",
expected_height,
current_committee.starting_round()
);
match &self.authority {
Authority::Beacon(signature) => {
let signer = signature.to_address();
ensure!(
current_committee.members().contains_key(&signer),
"Beacon block {expected_height} has a signer not in the committee (found '{signer}')",
);
ensure!(
signature.verify(&signer, &[*self.block_hash]),
"Signature is invalid in block {expected_height}"
);
}
Authority::Quorum(subdag) => {
let expected_leader = current_committee.get_leader(expected_round)?;
ensure!(
subdag.leader_address() == expected_leader,
"Quorum block {expected_height} is authored by an unexpected leader (found: {}, expected: {expected_leader})",
subdag.leader_address()
);
Self::check_subdag_transmissions(
subdag,
&self.solutions,
&self.transactions,
&self.aborted_transaction_ids,
)?;
}
}
let expected_timestamp = match &self.authority {
Authority::Beacon(..) => self.timestamp(),
Authority::Quorum(subdag) => subdag.timestamp(),
};
Ok((expected_round, expected_height, expected_timestamp))
}
fn verify_ratifications(&self, expected_block_reward: u64, expected_puzzle_reward: u64) -> Result<()> {
let height = self.height();
ensure!(!self.ratifications.len() >= 2, "Block {height} must contain at least 2 ratifications");
let mut ratifications_iter = self.ratifications.iter();
let block_reward = match ratifications_iter.next() {
Some(Ratify::BlockReward(block_reward)) => *block_reward,
_ => bail!("Block {height} is invalid - the first ratification must be a block reward"),
};
let puzzle_reward = match ratifications_iter.next() {
Some(Ratify::PuzzleReward(puzzle_reward)) => *puzzle_reward,
_ => bail!("Block {height} is invalid - the second ratification must be a puzzle reward"),
};
ensure!(
block_reward == expected_block_reward,
"Block {height} has an invalid block reward (found '{block_reward}', expected '{expected_block_reward}')",
);
ensure!(
puzzle_reward == expected_puzzle_reward,
"Block {height} has an invalid puzzle reward (found '{puzzle_reward}', expected '{expected_puzzle_reward}')",
);
Ok(())
}
fn verify_solutions(
&self,
previous_block: &Block<N>,
current_puzzle: &CoinbasePuzzle<N>,
current_epoch_challenge: &EpochChallenge<N>,
) -> Result<(u128, u128, u64, u64, u64, i64, u64, u64)> {
let height = self.height();
let timestamp = self.timestamp();
let (combined_proof_target, expected_cumulative_proof_target, is_coinbase_target_reached) = match &self
.solutions
{
Some(coinbase) => {
ensure!(
coinbase.len() <= N::MAX_PROVER_SOLUTIONS,
"Block {height} contains too many prover solutions (found '{}', expected '{}')",
coinbase.len(),
N::MAX_PROVER_SOLUTIONS
);
if height > block_height_at_year(N::BLOCK_TIME, 10) {
bail!("Solutions are no longer accepted after the block height at year 10.");
}
if let Err(e) =
current_puzzle.check_solutions(coinbase, current_epoch_challenge, previous_block.proof_target())
{
bail!("Block {height} contains an invalid puzzle proof - {e}");
}
let combined_proof_target = coinbase.to_combined_proof_target()?;
if self.cumulative_proof_target() >= previous_block.coinbase_target() as u128 {
bail!(
"The cumulative proof target in block {height} must be less than the previous coinbase target"
)
}
let cumulative_proof_target =
previous_block.cumulative_proof_target().saturating_add(combined_proof_target);
let is_coinbase_target_reached = cumulative_proof_target >= previous_block.coinbase_target() as u128;
let expected_cumulative_proof_target = match is_coinbase_target_reached {
true => 0u128,
false => cumulative_proof_target,
};
(combined_proof_target, expected_cumulative_proof_target, is_coinbase_target_reached)
}
None => {
let combined_proof_target = 0;
let expected_cumulative_proof_target = previous_block.cumulative_proof_target();
(combined_proof_target, expected_cumulative_proof_target, false)
}
};
let expected_cumulative_weight = previous_block.cumulative_weight().saturating_add(combined_proof_target);
let expected_coinbase_target = coinbase_target(
previous_block.last_coinbase_target(),
previous_block.last_coinbase_timestamp(),
timestamp,
N::ANCHOR_TIME,
N::NUM_BLOCKS_PER_EPOCH,
N::GENESIS_COINBASE_TARGET,
)?;
let expected_proof_target = proof_target(expected_coinbase_target, N::GENESIS_PROOF_TARGET);
let expected_last_coinbase_target = match is_coinbase_target_reached {
true => expected_coinbase_target,
false => previous_block.last_coinbase_target(),
};
let expected_last_coinbase_timestamp = match is_coinbase_target_reached {
true => timestamp,
false => previous_block.last_coinbase_timestamp(),
};
let expected_coinbase_reward = coinbase_reward(
height,
N::STARTING_SUPPLY,
N::ANCHOR_HEIGHT,
N::BLOCK_TIME,
combined_proof_target,
u64::try_from(previous_block.cumulative_proof_target())?,
previous_block.coinbase_target(),
)?;
let expected_transaction_fees =
self.transactions.iter().map(|tx| Ok(*tx.priority_fee_amount()?)).sum::<Result<u64>>()?;
let expected_block_reward =
block_reward(N::STARTING_SUPPLY, N::BLOCK_TIME, expected_coinbase_reward, expected_transaction_fees);
let expected_puzzle_reward = puzzle_reward(expected_coinbase_reward);
Ok((
expected_cumulative_weight,
expected_cumulative_proof_target,
expected_coinbase_target,
expected_proof_target,
expected_last_coinbase_target,
expected_last_coinbase_timestamp,
expected_block_reward,
expected_puzzle_reward,
))
}
fn verify_transactions(&self) -> Result<()> {
let height = self.height();
ensure!(!self.transactions.is_empty(), "Block {height} must contain at least 1 transaction");
if self.transactions.len() + self.aborted_transaction_ids.len() > Transactions::<N>::MAX_TRANSACTIONS {
bail!("Cannot validate a block with more than {} transactions", Transactions::<N>::MAX_TRANSACTIONS);
}
if has_duplicates(self.transaction_ids().chain(self.aborted_transaction_ids.iter())) {
bail!("Found a duplicate transaction in block {height}");
}
if has_duplicates(self.transition_ids()) {
bail!("Found a duplicate transition in block {height}");
}
if has_duplicates(self.input_ids()) {
bail!("Found a duplicate input ID in block {height}");
}
if has_duplicates(self.serial_numbers()) {
bail!("Found a duplicate serial number in block {height}");
}
if has_duplicates(self.tags()) {
bail!("Found a duplicate tag in block {height}");
}
if has_duplicates(self.output_ids()) {
bail!("Found a duplicate output ID in block {height}");
}
if has_duplicates(self.commitments()) {
bail!("Found a duplicate commitment in block {height}");
}
if has_duplicates(self.nonces()) {
bail!("Found a duplicate nonce in block {height}");
}
if has_duplicates(self.transition_public_keys()) {
bail!("Found a duplicate transition public key in block {height}");
}
if has_duplicates(self.transition_commitments()) {
bail!("Found a duplicate transition commitment in block {height}");
}
Ok(())
}
}
impl<N: Network> Block<N> {
fn compute_transactions_root(&self) -> Result<Field<N>> {
match self.transactions.to_transactions_root() {
Ok(transactions_root) => Ok(transactions_root),
Err(error) => bail!("Failed to compute the transactions root for block {} - {error}", self.height()),
}
}
fn compute_finalize_root(&self, ratified_finalize_operations: Vec<FinalizeOperation<N>>) -> Result<Field<N>> {
match self.transactions.to_finalize_root(ratified_finalize_operations) {
Ok(finalize_root) => Ok(finalize_root),
Err(error) => bail!("Failed to compute the finalize root for block {} - {error}", self.height()),
}
}
fn compute_ratifications_root(&self) -> Result<Field<N>> {
match self.ratifications.to_ratifications_root() {
Ok(ratifications_root) => Ok(ratifications_root),
Err(error) => bail!("Failed to compute the ratifications root for block {} - {error}", self.height()),
}
}
fn compute_solutions_root(&self) -> Result<Field<N>> {
match self.solutions {
Some(ref coinbase) => coinbase.to_accumulator_point(),
None => Ok(Field::zero()),
}
}
fn compute_subdag_root(&self) -> Result<Field<N>> {
match self.authority {
Authority::Quorum(ref subdag) => subdag.to_subdag_root(),
Authority::Beacon(_) => Ok(Field::zero()),
}
}
pub(super) fn check_subdag_transmissions(
subdag: &Subdag<N>,
solutions: &Option<CoinbaseSolution<N>>,
transactions: &Transactions<N>,
aborted_transaction_ids: &[N::TransactionID],
) -> Result<()> {
let mut solutions = solutions.as_ref().map(|s| s.deref()).into_iter().flatten().peekable();
let unconfirmed_transaction_ids = cfg_iter!(transactions)
.map(|confirmed| confirmed.to_unconfirmed_transaction_id())
.collect::<Result<Vec<_>>>()?;
let mut unconfirmed_transaction_ids = unconfirmed_transaction_ids.iter().peekable();
let mut seen_transmission_ids = HashSet::new();
let mut aborted_or_existing_solution_ids = Vec::new();
let mut aborted_or_existing_transaction_ids = Vec::new();
for transmission_id in subdag.transmission_ids() {
if !seen_transmission_ids.insert(transmission_id) {
continue;
}
match transmission_id {
TransmissionID::Ratification => {}
TransmissionID::Solution(commitment) => {
match solutions.peek() {
Some((_, solution)) if solution.commitment() == *commitment => {
solutions.next();
}
_ => aborted_or_existing_solution_ids.push(commitment),
}
}
TransmissionID::Transaction(transaction_id) => {
match unconfirmed_transaction_ids.peek() {
Some(expected_id) if transaction_id == *expected_id => {
unconfirmed_transaction_ids.next();
}
_ => aborted_or_existing_transaction_ids.push(*transaction_id),
}
}
}
}
ensure!(solutions.next().is_none(), "There exists more solutions than expected.");
ensure!(unconfirmed_transaction_ids.next().is_none(), "There exists more transactions than expected.");
for aborted_transaction_id in aborted_transaction_ids {
if !aborted_or_existing_transaction_ids.contains(aborted_transaction_id) {
bail!(
"Block contains an aborted transaction ID that is not found in the subdag (found '{aborted_transaction_id}')"
);
}
}
Ok(())
}
}