cuprate_consensus_rules/
miner_tx.rs

1use monero_serai::transaction::{Input, Output, Timelock, Transaction};
2
3use cuprate_constants::block::MAX_BLOCK_HEIGHT_USIZE;
4use cuprate_types::TxVersion;
5
6use crate::{is_decomposed_amount, transactions::check_output_types, HardFork};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
9pub enum MinerTxError {
10    #[error("The miners transaction version is invalid.")]
11    VersionInvalid,
12    #[error("The miner transaction does not have exactly one input.")]
13    IncorrectNumbOfInputs,
14    #[error("The miner transactions input has the wrong block height.")]
15    InputsHeightIncorrect,
16    #[error("The input is not of type `gen`.")]
17    InputNotOfTypeGen,
18    #[error("The transaction has an incorrect lock time.")]
19    InvalidLockTime,
20    #[error("The transaction has an output which is not decomposed.")]
21    OutputNotDecomposed,
22    #[error("The transaction outputs overflow when summed.")]
23    OutputsOverflow,
24    #[error("The miner transaction outputs the wrong amount.")]
25    OutputAmountIncorrect,
26    #[error("The miner transactions RCT type is not NULL.")]
27    RCTTypeNotNULL,
28    #[error("The miner transactions has an invalid output type.")]
29    InvalidOutputType,
30}
31
32/// A constant called "money supply", not actually a cap, it is used during
33/// block reward calculations.
34const MONEY_SUPPLY: u64 = u64::MAX;
35/// The minimum block reward per minute, "tail-emission"
36const MINIMUM_REWARD_PER_MIN: u64 = 3 * 10_u64.pow(11);
37/// The value which `lock_time` should be for a coinbase output.
38const MINER_TX_TIME_LOCKED_BLOCKS: usize = 60;
39
40/// Calculates the base block reward without taking away the penalty for expanding
41/// the block.
42///
43/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/reward.html#calculating-base-block-reward>
44fn calculate_base_reward(already_generated_coins: u64, hf: HardFork) -> u64 {
45    let target_mins = hf.block_time().as_secs() / 60;
46    let emission_speed_factor = 20 - (target_mins - 1);
47    ((MONEY_SUPPLY - already_generated_coins) >> emission_speed_factor)
48        .max(MINIMUM_REWARD_PER_MIN * target_mins)
49}
50
51/// Calculates the miner reward for this block.
52///
53/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/reward.html#calculating-block-reward>
54pub fn calculate_block_reward(
55    block_weight: usize,
56    median_bw: usize,
57    already_generated_coins: u64,
58    hf: HardFork,
59) -> u64 {
60    let base_reward = calculate_base_reward(already_generated_coins, hf);
61
62    if block_weight <= median_bw {
63        return base_reward;
64    }
65
66    let multiplicand: u128 = ((2 * median_bw - block_weight) * block_weight)
67        .try_into()
68        .unwrap();
69    let effective_median_bw: u128 = median_bw.try_into().unwrap();
70
71    (((u128::from(base_reward) * multiplicand) / effective_median_bw) / effective_median_bw)
72        .try_into()
73        .unwrap()
74}
75
76/// Checks the miner transactions version.
77///
78/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#version>
79fn check_miner_tx_version(tx_version: TxVersion, hf: HardFork) -> Result<(), MinerTxError> {
80    // The TxVersion enum checks if the version is not 1 or 2
81    if hf >= HardFork::V12 && tx_version != TxVersion::RingCT {
82        Err(MinerTxError::VersionInvalid)
83    } else {
84        Ok(())
85    }
86}
87
88/// Checks the miner transactions inputs.
89///
90/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#input>
91fn check_inputs(inputs: &[Input], chain_height: usize) -> Result<(), MinerTxError> {
92    if inputs.len() != 1 {
93        return Err(MinerTxError::IncorrectNumbOfInputs);
94    }
95
96    match &inputs[0] {
97        Input::Gen(height) => {
98            if height == &chain_height {
99                Ok(())
100            } else {
101                Err(MinerTxError::InputsHeightIncorrect)
102            }
103        }
104        Input::ToKey { .. } => Err(MinerTxError::InputNotOfTypeGen),
105    }
106}
107
108/// Checks the miner transaction has a correct time lock.
109///
110/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#unlock-time>
111const fn check_time_lock(time_lock: &Timelock, chain_height: usize) -> Result<(), MinerTxError> {
112    match time_lock {
113        &Timelock::Block(till_height) => {
114            // Lock times above this amount are timestamps not blocks.
115            // This is just for safety though and shouldn't actually be hit.
116            if till_height > MAX_BLOCK_HEIGHT_USIZE {
117                return Err(MinerTxError::InvalidLockTime);
118            }
119            if till_height == chain_height + MINER_TX_TIME_LOCKED_BLOCKS {
120                Ok(())
121            } else {
122                Err(MinerTxError::InvalidLockTime)
123            }
124        }
125        _ => Err(MinerTxError::InvalidLockTime),
126    }
127}
128
129/// Sums the outputs checking for overflow.
130///
131/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#output-amounts>
132/// &&   <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#zero-amount-v1-output>
133fn sum_outputs(
134    outputs: &[Output],
135    hf: HardFork,
136    tx_version: TxVersion,
137) -> Result<u64, MinerTxError> {
138    let mut sum: u64 = 0;
139    for out in outputs {
140        let amt = out.amount.unwrap_or(0);
141
142        if tx_version == TxVersion::RingSignatures && amt == 0 {
143            return Err(MinerTxError::OutputAmountIncorrect);
144        }
145
146        if hf == HardFork::V3 && !is_decomposed_amount(&amt) {
147            return Err(MinerTxError::OutputNotDecomposed);
148        }
149        sum = sum.checked_add(amt).ok_or(MinerTxError::OutputsOverflow)?;
150    }
151    Ok(sum)
152}
153
154/// Checks the total outputs amount is correct returning the amount of coins collected by the miner.
155///
156/// ref: <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#total-outputs>
157fn check_total_output_amt(
158    total_output: u64,
159    reward: u64,
160    fees: u64,
161    hf: HardFork,
162) -> Result<u64, MinerTxError> {
163    if hf == HardFork::V1 || hf >= HardFork::V12 {
164        if total_output != reward + fees {
165            return Err(MinerTxError::OutputAmountIncorrect);
166        }
167        Ok(reward)
168    } else {
169        if total_output - fees > reward || total_output > reward + fees {
170            return Err(MinerTxError::OutputAmountIncorrect);
171        }
172        Ok(total_output - fees)
173    }
174}
175
176/// Checks all miner transactions rules.
177///
178/// Excluding:
179/// <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#v2-output-pool>
180///
181/// as this needs to be done in a database.
182pub fn check_miner_tx(
183    tx: &Transaction,
184    total_fees: u64,
185    chain_height: usize,
186    block_weight: usize,
187    median_bw: usize,
188    already_generated_coins: u64,
189    hf: HardFork,
190) -> Result<u64, MinerTxError> {
191    let tx_version = TxVersion::from_raw(tx.version()).ok_or(MinerTxError::VersionInvalid)?;
192    check_miner_tx_version(tx_version, hf)?;
193
194    // ref: <https://monero-book.cuprate.org/consensus_rules/blocks/miner_tx.html#ringct-type>
195    match tx {
196        Transaction::V1 { .. } => (),
197        Transaction::V2 { proofs, .. } => {
198            if hf >= HardFork::V12 && proofs.is_some() {
199                return Err(MinerTxError::RCTTypeNotNULL);
200            }
201        }
202    }
203
204    check_time_lock(&tx.prefix().additional_timelock, chain_height)?;
205
206    check_inputs(&tx.prefix().inputs, chain_height)?;
207
208    check_output_types(&tx.prefix().outputs, hf).map_err(|_| MinerTxError::InvalidOutputType)?;
209
210    let reward = calculate_block_reward(block_weight, median_bw, already_generated_coins, hf);
211    let total_outs = sum_outputs(&tx.prefix().outputs, hf, tx_version)?;
212
213    check_total_output_amt(total_outs, reward, total_fees, hf)
214}
215
216#[cfg(test)]
217mod tests {
218    use proptest::prelude::*;
219
220    use super::*;
221
222    proptest! {
223        #[test]
224        fn tail_emission(generated_coins in any::<u64>(), hf in any::<HardFork>()) {
225            prop_assert!(calculate_base_reward(generated_coins, hf) >= MINIMUM_REWARD_PER_MIN * hf.block_time().as_secs() / 60);
226        }
227    }
228}