use std::{collections::HashSet, sync::Arc};
use chrono::{DateTime, Utc};
use mset::MultiSet;
use zebra_chain::{
amount::{
Amount, DeferredPoolBalanceChange, Error as AmountError, NegativeAllowed, NonNegative,
},
block::{Block, Hash, Header, Height},
parameters::{
subsidy::{
founders_reward, founders_reward_address, funding_stream_values, FundingStreamReceiver,
ParameterSubsidy, SubsidyError,
},
Network, NetworkUpgrade,
},
transaction::{self, Transaction},
transparent::{Address, Output},
work::{
difficulty::{ExpandedDifficulty, ParameterDifficulty as _},
equihash,
},
};
use crate::{error::*, funding_stream_address};
pub fn coinbase_is_first(block: &Block) -> Result<Arc<transaction::Transaction>, BlockError> {
let first = block
.transactions
.first()
.ok_or(BlockError::NoTransactions)?;
let mut rest = block.transactions.iter().skip(1);
if !first.is_coinbase() {
Err(TransactionError::CoinbasePosition)?;
}
if !rest.all(|tx| tx.is_valid_non_coinbase()) {
Err(TransactionError::CoinbaseAfterFirst)?;
}
Ok(first.clone())
}
pub fn difficulty_threshold_is_valid(
header: &Header,
network: &Network,
height: &Height,
hash: &Hash,
) -> Result<ExpandedDifficulty, BlockError> {
let difficulty_threshold = header
.difficulty_threshold
.to_expanded()
.ok_or(BlockError::InvalidDifficulty(*height, *hash))?;
if difficulty_threshold > network.target_difficulty_limit() {
Err(BlockError::TargetDifficultyLimit(
*height,
*hash,
difficulty_threshold,
network.clone(),
network.target_difficulty_limit(),
))?;
}
Ok(difficulty_threshold)
}
pub fn difficulty_is_valid(
header: &Header,
network: &Network,
height: &Height,
hash: &Hash,
) -> Result<(), BlockError> {
let difficulty_threshold = difficulty_threshold_is_valid(header, network, height, hash)?;
if hash > &difficulty_threshold {
Err(BlockError::DifficultyFilter(
*height,
*hash,
difficulty_threshold,
network.clone(),
))?;
}
Ok(())
}
pub fn equihash_solution_is_valid(header: &Header) -> Result<(), equihash::Error> {
header.solution.check(header)
}
pub fn subsidy_is_valid(
block: &Block,
net: &Network,
expected_block_subsidy: Amount<NonNegative>,
) -> Result<DeferredPoolBalanceChange, BlockError> {
if expected_block_subsidy.is_zero() {
return Ok(DeferredPoolBalanceChange::zero());
}
let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?;
let mut coinbase_outputs: MultiSet<Output> = block
.transactions
.first()
.ok_or(SubsidyError::NoCoinbase)?
.outputs()
.iter()
.cloned()
.collect();
let mut has_amount = |addr: &Address, amount| {
assert!(addr.is_script_hash(), "address must be P2SH");
coinbase_outputs.remove(&Output::new(amount, addr.script()))
};
if NetworkUpgrade::current(net, height) < NetworkUpgrade::Canopy {
if Height::MIN < height && height < net.height_for_first_halving() {
let addr = founders_reward_address(net, height).ok_or(BlockError::Other(format!(
"founders reward address must be defined for height: {height:?}"
)))?;
if !has_amount(&addr, founders_reward(net, height)) {
Err(SubsidyError::FoundersRewardNotFound)?;
}
}
Ok(DeferredPoolBalanceChange::zero())
} else {
let mut funding_streams = funding_stream_values(height, net, expected_block_subsidy)?;
let mut deferred_pool_balance_change = funding_streams
.remove(&FundingStreamReceiver::Deferred)
.unwrap_or_default()
.constrain::<NegativeAllowed>()?;
if Some(height) == NetworkUpgrade::Nu6_1.activation_height(net) {
let lockbox_disbursements = net.lockbox_disbursements(height);
if lockbox_disbursements.is_empty() {
Err(BlockError::Other(
"missing lockbox disbursements for NU6.1 activation block".to_string(),
))?;
}
deferred_pool_balance_change = lockbox_disbursements.into_iter().try_fold(
deferred_pool_balance_change,
|balance, (addr, expected_amount)| {
if !has_amount(&addr, expected_amount) {
Err(SubsidyError::OneTimeLockboxDisbursementNotFound)?;
}
balance
.checked_sub(expected_amount)
.ok_or(SubsidyError::Underflow)
},
)?;
};
funding_streams.into_iter().try_for_each(
|(receiver, expected_amount)| -> Result<(), BlockError> {
let addr =
funding_stream_address(height, net, receiver).ok_or(BlockError::Other(
"A funding stream other than the deferred pool must have an address"
.to_string(),
))?;
if !has_amount(addr, expected_amount) {
Err(SubsidyError::FundingStreamNotFound)?;
}
Ok(())
},
)?;
Ok(DeferredPoolBalanceChange::new(deferred_pool_balance_change))
}
}
pub fn miner_fees_are_valid(
coinbase_tx: &Transaction,
height: Height,
block_miner_fees: Amount<NonNegative>,
expected_block_subsidy: Amount<NonNegative>,
expected_deferred_pool_balance_change: DeferredPoolBalanceChange,
network: &Network,
) -> Result<(), BlockError> {
let transparent_value_balance = coinbase_tx
.outputs()
.iter()
.map(|output| output.value())
.sum::<Result<Amount<NonNegative>, AmountError>>()
.map_err(|_| SubsidyError::Overflow)?
.constrain()
.map_err(|e| BlockError::Other(format!("invalid transparent value balance: {e}")))?;
let sapling_value_balance = coinbase_tx.sapling_value_balance().sapling_amount();
let orchard_value_balance = coinbase_tx.orchard_value_balance().orchard_amount();
#[cfg(zcash_unstable = "zip235")]
let zip233_amount: Amount<NegativeAllowed> = coinbase_tx
.zip233_amount()
.constrain()
.map_err(|_| SubsidyError::InvalidZip233Amount)?;
#[cfg(not(zcash_unstable = "zip235"))]
let zip233_amount = Amount::zero();
#[cfg(zcash_unstable = "zip235")]
if let Some(nsm_activation_height) = NetworkUpgrade::Nu7.activation_height(network) {
if height >= nsm_activation_height {
let minimum_zip233_amount = ((block_miner_fees * 6).unwrap() / 10).unwrap();
if zip233_amount < minimum_zip233_amount {
Err(SubsidyError::InvalidZip233Amount)?
}
}
}
let total_output_value =
(transparent_value_balance - sapling_value_balance - orchard_value_balance
+ expected_deferred_pool_balance_change.value()
+ zip233_amount)
.map_err(|_| SubsidyError::Overflow)?;
let total_input_value =
(expected_block_subsidy + block_miner_fees).map_err(|_| SubsidyError::Overflow)?;
if if NetworkUpgrade::current(network, height) < NetworkUpgrade::Nu6 {
total_output_value > total_input_value
} else {
total_output_value != total_input_value
} {
Err(SubsidyError::InvalidMinerFees)?
};
Ok(())
}
pub fn time_is_valid_at(
header: &Header,
now: DateTime<Utc>,
height: &Height,
hash: &Hash,
) -> Result<(), zebra_chain::block::BlockTimeError> {
header.time_is_valid_at(now, height, hash)
}
pub fn merkle_root_validity(
network: &Network,
block: &Block,
transaction_hashes: &[transaction::Hash],
) -> Result<(), BlockError> {
block
.check_transaction_network_upgrade_consistency(network)
.map_err(|_| BlockError::WrongTransactionConsensusBranchId)?;
let merkle_root = transaction_hashes.iter().cloned().collect();
if block.header.merkle_root != merkle_root {
return Err(BlockError::BadMerkleRoot {
actual: merkle_root,
expected: block.header.merkle_root,
});
}
if transaction_hashes.len() != transaction_hashes.iter().collect::<HashSet<_>>().len() {
return Err(BlockError::DuplicateTransaction);
}
Ok(())
}