use monero_serai::{ ringct::RctType, transaction::{Input, Output, Timelock, Transaction}, }; use crate::{is_decomposed_amount, transactions::check_output_types, HardFork, TxVersion}; #[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)] pub enum MinerTxError { #[error("The miners transaction version is invalid.")] VersionInvalid, #[error("The miner transaction does not have exactly one input.")] IncorrectNumbOfInputs, #[error("The miner transactions input has the wrong block height.")] InputsHeightIncorrect, #[error("The input is not of type `gen`.")] InputNotOfTypeGen, #[error("The transaction has an incorrect lock time.")] InvalidLockTime, #[error("The transaction has an output which is not decomposed.")] OutputNotDecomposed, #[error("The transaction outputs overflow when summed.")] OutputsOverflow, #[error("The miner transaction outputs the wrong amount.")] OutputAmountIncorrect, #[error("The miner transactions RCT type is not NULL.")] RCTTypeNotNULL, #[error("The miner transactions has an invalid output type.")] InvalidOutputType, } /// A constant called "money supply", not actually a cap, it is used during /// block reward calculations. const MONEY_SUPPLY: u64 = u64::MAX; /// The minimum block reward per minute, "tail-emission" const MINIMUM_REWARD_PER_MIN: u64 = 3 * 10_u64.pow(11); /// The value which `lock_time` should be for a coinbase output. const MINER_TX_TIME_LOCKED_BLOCKS: u64 = 60; /// Calculates the base block reward without taking away the penalty for expanding /// the block. fn calculate_base_reward(already_generated_coins: u64, hf: &HardFork) -> u64 { let target_mins = hf.block_time().as_secs() / 60; let emission_speed_factor = 20 - (target_mins - 1); ((MONEY_SUPPLY - already_generated_coins) >> emission_speed_factor) .max(MINIMUM_REWARD_PER_MIN * target_mins) } /// Calculates the miner reward for this block. pub fn calculate_block_reward( block_weight: usize, median_bw: usize, already_generated_coins: u64, hf: &HardFork, ) -> u64 { let base_reward = calculate_base_reward(already_generated_coins, hf); if block_weight <= median_bw { return base_reward; } let multiplicand: u128 = ((2 * median_bw - block_weight) * block_weight) .try_into() .unwrap(); let effective_median_bw: u128 = median_bw.try_into().unwrap(); (((base_reward as u128 * multiplicand) / effective_median_bw) / effective_median_bw) .try_into() .unwrap() } /// Checks the miner transactions version. /// /// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#version fn check_miner_tx_version(tx_version: &TxVersion, hf: &HardFork) -> Result<(), MinerTxError> { // The TxVersion enum checks if the version is not 1 or 2 if hf >= &HardFork::V12 && tx_version != &TxVersion::RingCT { Err(MinerTxError::VersionInvalid) } else { Ok(()) } } /// Checks the miner transactions inputs. /// /// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#input /// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#height fn check_inputs(inputs: &[Input], chain_height: u64) -> Result<(), MinerTxError> { if inputs.len() != 1 { return Err(MinerTxError::IncorrectNumbOfInputs); } match &inputs[0] { Input::Gen(height) => { if height != &chain_height { Err(MinerTxError::InputsHeightIncorrect) } else { Ok(()) } } _ => Err(MinerTxError::InputNotOfTypeGen), } } /// Checks the miner transaction has a correct time lock. /// /// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#unlock-time fn check_time_lock(time_lock: &Timelock, chain_height: u64) -> Result<(), MinerTxError> { match time_lock { Timelock::Block(till_height) => { // Lock times above this amount are timestamps not blocks. // This is just for safety though and shouldn't actually be hit. if till_height >= &500_000_000 { Err(MinerTxError::InvalidLockTime)?; } if u64::try_from(*till_height).unwrap() != chain_height + MINER_TX_TIME_LOCKED_BLOCKS { Err(MinerTxError::InvalidLockTime) } else { Ok(()) } } _ => Err(MinerTxError::InvalidLockTime), } } /// Sums the outputs checking for overflow. /// /// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#output-amounts fn sum_outputs(outputs: &[Output], hf: &HardFork) -> Result { let mut sum: u64 = 0; for out in outputs { let amt = out.amount.unwrap_or(0); if hf == &HardFork::V3 && !is_decomposed_amount(&amt) { return Err(MinerTxError::OutputNotDecomposed); } sum = sum.checked_add(amt).ok_or(MinerTxError::OutputsOverflow)?; } Ok(sum) } /// Checks the total outputs amount is correct returning the amount of coins collected by the miner. /// /// https://cuprate.github.io/monero-book/consensus_rules/blocks/miner_tx.html#total-outputs fn check_total_output_amt( total_output: u64, reward: u64, fees: u64, hf: &HardFork, ) -> Result { if hf == &HardFork::V1 || hf >= &HardFork::V12 { if total_output != reward + fees { return Err(MinerTxError::OutputAmountIncorrect); } Ok(reward) } else { if total_output - fees > reward || total_output > reward + fees { return Err(MinerTxError::OutputAmountIncorrect); } Ok(total_output - fees) } } pub fn check_miner_tx( tx: &Transaction, total_fees: u64, chain_height: u64, block_weight: usize, median_bw: usize, already_generated_coins: u64, hf: &HardFork, ) -> Result { let tx_version = TxVersion::from_raw(tx.prefix.version).ok_or(MinerTxError::VersionInvalid)?; check_miner_tx_version(&tx_version, hf)?; if hf >= &HardFork::V12 && tx.rct_signatures.rct_type() != RctType::Null { return Err(MinerTxError::RCTTypeNotNULL); } check_time_lock(&tx.prefix.timelock, chain_height)?; check_inputs(&tx.prefix.inputs, chain_height)?; check_output_types(&tx.prefix.outputs, hf).map_err(|_| MinerTxError::InvalidOutputType)?; let reward = calculate_block_reward(block_weight, median_bw, already_generated_coins, hf); let total_outs = sum_outputs(&tx.prefix.outputs, hf)?; check_total_output_amt(total_outs, reward, total_fees, hf) }