cuprate_consensus_rules/
miner_tx.rs1use 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
32const MONEY_SUPPLY: u64 = u64::MAX;
35const MINIMUM_REWARD_PER_MIN: u64 = 3 * 10_u64.pow(11);
37const MINER_TX_TIME_LOCKED_BLOCKS: usize = 60;
39
40fn 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
51pub 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
76fn check_miner_tx_version(tx_version: TxVersion, hf: HardFork) -> Result<(), MinerTxError> {
80 if hf >= HardFork::V12 && tx_version != TxVersion::RingCT {
82 Err(MinerTxError::VersionInvalid)
83 } else {
84 Ok(())
85 }
86}
87
88fn 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
108const fn check_time_lock(time_lock: &Timelock, chain_height: usize) -> Result<(), MinerTxError> {
112 match time_lock {
113 &Timelock::Block(till_height) => {
114 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
129fn 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
154fn 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
176pub 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 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}