use crate::constants::*;
use crate::error::{ConsensusError, Result};
use crate::types::*;
#[cfg(feature = "spec-lock")]
use blvm_spec_lock::spec_locked;
use std::borrow::Cow;
#[cfg_attr(feature = "spec-lock", spec_locked("6.1", "GetBlockSubsidy"))]
#[cfg_attr(feature = "spec-lock", blvm_spec_lock::requires(height >= 0))]
#[cfg_attr(feature = "spec-lock", blvm_spec_lock::ensures(result >= 0))]
#[cfg_attr(feature = "spec-lock", blvm_spec_lock::ensures(result <= INITIAL_SUBSIDY))]
pub fn get_block_subsidy(height: Natural) -> Integer {
let halving_period = height / HALVING_INTERVAL;
if halving_period >= 64 {
return 0;
}
debug_assert!(
halving_period < 64,
"Halving period ({halving_period}) must be < 64"
);
let base_subsidy = INITIAL_SUBSIDY;
let subsidy = base_subsidy >> halving_period;
debug_assert!(subsidy >= 0, "Subsidy ({subsidy}) must be non-negative");
debug_assert!(
subsidy <= INITIAL_SUBSIDY,
"Subsidy ({subsidy}) must be <= initial subsidy ({INITIAL_SUBSIDY})"
);
subsidy
}
pub const REGTEST_HALVING_INTERVAL: u64 = 150;
pub fn subsidy_halving_interval(network: Network) -> u64 {
match network {
Network::Regtest => REGTEST_HALVING_INTERVAL,
Network::Mainnet | Network::Testnet | Network::Testnet4 | Network::Signet => {
HALVING_INTERVAL
}
}
}
pub fn get_block_subsidy_for_network(height: Natural, network: Network) -> Integer {
let interval = subsidy_halving_interval(network);
if interval == HALVING_INTERVAL {
return get_block_subsidy(height);
}
block_subsidy_at_interval(height, interval, INITIAL_SUBSIDY)
}
pub fn block_subsidy_at_interval(height: Natural, interval: u64, initial: Integer) -> Integer {
if interval == 0 {
return 0;
}
let halving_period = height / interval;
if halving_period >= 64 {
return 0;
}
initial >> halving_period
}
#[cfg_attr(feature = "spec-lock", spec_locked("5.3", "CheckCoinbaseSubsidy"))]
pub fn check_coinbase_subsidy(
coinbase: &Transaction,
subsidy: Integer,
total_fees: Integer,
) -> bool {
if total_fees < 0 || subsidy < 0 {
return false;
}
let coinbase_output: i64 = match coinbase.outputs.iter().try_fold(0i64, |acc, output| {
if output.value < 0 || output.value > MAX_MONEY {
return None;
}
acc.checked_add(output.value)
}) {
Some(v) => v,
None => return false,
};
if coinbase_output < 0 {
return false;
}
let Some(max_coinbase) = total_fees.checked_add(subsidy) else {
return false;
};
coinbase_output <= MAX_MONEY && coinbase_output <= max_coinbase
}
#[cfg_attr(feature = "spec-lock", spec_locked("8.1", "VerifyUtxoSupply"))]
pub fn verify_utxo_supply(utxo_set: &UtxoSet, height: Natural) -> bool {
let expected = total_supply(height);
if !(0..=MAX_MONEY).contains(&expected) {
return false;
}
let mut actual: i64 = 0;
for utxo in utxo_set.values() {
if !(0..=MAX_MONEY).contains(&utxo.value) {
return false;
}
actual = match actual.checked_add(utxo.value) {
Some(v) => v,
None => return false,
};
}
actual == expected
}
#[cfg_attr(feature = "spec-lock", spec_locked("6.2", "TotalSupply"))]
#[cfg_attr(feature = "spec-lock", blvm_spec_lock::axiom(result >= 0))]
#[cfg_attr(feature = "spec-lock", blvm_spec_lock::axiom(result <= 2100000000000000))]
#[cfg_attr(feature = "spec-lock", blvm_spec_lock::ensures(result >= 0))]
#[cfg_attr(feature = "spec-lock", blvm_spec_lock::ensures(result <= 2100000000000000))]
pub fn total_supply(height: Natural) -> Integer {
let end = height;
let h = HALVING_INTERVAL;
let mut total = 0i64;
for k in 0u64..64 {
let period_start = k.saturating_mul(h);
if period_start > end {
break;
}
let subsidy = INITIAL_SUBSIDY >> k;
let period_end = (k + 1).saturating_mul(h).saturating_sub(1);
let overlap_hi = end.min(period_end);
let count = overlap_hi
.checked_sub(period_start)
.map(|d| d + 1)
.expect("overlap_hi >= period_start when period_start <= end");
let contrib = match (count as i64).checked_mul(subsidy) {
Some(c) => c,
None => {
debug_assert!(false, "Total supply contribution overflow at epoch {k}");
return MAX_MONEY;
}
};
total = match total.checked_add(contrib) {
Some(t) => t,
None => {
debug_assert!(false, "Total supply sum overflow at epoch {k}");
return MAX_MONEY;
}
};
if total >= MAX_MONEY {
return MAX_MONEY;
}
}
debug_assert!(total >= 0, "Total supply ({total}) must be non-negative");
debug_assert!(
total <= MAX_MONEY,
"Total supply ({total}) must be <= MAX_MONEY ({MAX_MONEY})"
);
total
}
#[cfg_attr(feature = "spec-lock", spec_locked("6.5", "CalculateFee"))]
#[cfg_attr(feature = "spec-lock", blvm_spec_lock::ensures(result >= 0))]
pub fn calculate_fee(tx: &Transaction, utxo_set: &UtxoSet) -> Result<Integer> {
if is_coinbase(tx) {
return Ok(0);
}
let total_input: i64 = tx.inputs.iter().try_fold(0i64, |acc, input| {
let utxo = utxo_set.get(&input.prevout).ok_or_else(|| {
ConsensusError::UtxoNotFound(
format!("UTXO not found for prevout index {}", input.prevout.index).into(),
)
})?;
acc.checked_add(utxo.value)
.ok_or_else(|| ConsensusError::EconomicValidation("Input value overflow".into()))
})?;
let total_output: i64 = tx
.outputs
.iter()
.try_fold(0i64, |acc, output| {
acc.checked_add(output.value)
.ok_or_else(|| ConsensusError::EconomicValidation("Output value overflow".into()))
})
.map_err(|e| ConsensusError::EconomicValidation(Cow::Owned(e.to_string())))?;
if total_output < 0 {
return Err(ConsensusError::EconomicValidation(
"Negative total output value".into(),
));
}
let fee = total_input
.checked_sub(total_output)
.ok_or_else(|| ConsensusError::EconomicValidation("Fee calculation underflow".into()))?;
if fee < 0 {
return Err(ConsensusError::EconomicValidation("Negative fee".into()));
}
debug_assert!(
fee >= 0,
"Fee ({fee}) must be non-negative (input: {total_input}, output: {total_output})"
);
debug_assert!(
fee <= total_input,
"Fee ({fee}) cannot exceed total input ({total_input})"
);
Ok(fee)
}
pub fn validate_supply_limit(height: Natural) -> bool {
total_supply(height) <= MAX_MONEY
}
fn is_coinbase(tx: &Transaction) -> bool {
tx.inputs.len() == 1
&& tx.inputs[0].prevout.hash == [0u8; 32]
&& tx.inputs[0].prevout.index == 0xffffffff
}
#[cfg(test)]
mod property_tests {
use super::*;
use proptest::prelude::*;
proptest! {
#[test]
fn prop_get_block_subsidy_halving_schedule(
height in 0u64..(HALVING_INTERVAL * 10)
) {
let subsidy = get_block_subsidy(height);
let halving_period = height / HALVING_INTERVAL;
prop_assert!(subsidy >= 0, "Subsidy must be non-negative");
if halving_period < 64 {
let expected_subsidy = INITIAL_SUBSIDY >> halving_period;
prop_assert_eq!(subsidy, expected_subsidy, "Subsidy must follow halving schedule");
} else {
prop_assert_eq!(subsidy, 0, "Subsidy must be 0 after 64 halvings");
}
}
}
proptest! {
#[test]
fn prop_total_supply_monotonic(
height1 in 0u64..(HALVING_INTERVAL * 2),
height2 in 0u64..(HALVING_INTERVAL * 2)
) {
let (h1, h2) = if height1 <= height2 { (height1, height2) } else { (height2, height1) };
let supply1 = total_supply(h1);
let supply2 = total_supply(h2);
prop_assert!(supply2 >= supply1, "Total supply must be monotonically increasing");
prop_assert!(supply1 >= 0, "Total supply must be non-negative");
prop_assert!(supply2 >= 0, "Total supply must be non-negative");
}
}
proptest! {
#[test]
fn prop_supply_limit_respected(
height in 0u64..(HALVING_INTERVAL * 10)
) {
let supply = total_supply(height);
prop_assert!(supply <= MAX_MONEY, "Total supply must not exceed maximum money");
prop_assert!(supply >= 0, "Total supply must be non-negative");
}
}
proptest! {
#[test]
fn prop_subsidy_decreases_with_halvings(
height1 in 0u64..(HALVING_INTERVAL * 5),
height2 in 0u64..(HALVING_INTERVAL * 5)
) {
let halving1 = height1 / HALVING_INTERVAL;
let halving2 = height2 / HALVING_INTERVAL;
if halving1 < halving2 && halving2 < 64 {
let subsidy1 = get_block_subsidy(height1);
let subsidy2 = get_block_subsidy(height2);
prop_assert!(subsidy1 >= subsidy2, "Subsidy must decrease with halving periods");
}
}
}
proptest! {
#[test]
fn prop_calculate_fee_coinbase(
value in 0i64..=20_000,
spend in 0i64..=20_000
) {
let coinbase = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint { hash: [0; 32], index: 0xffff_ffff },
script_sig: vec![0x51, 0x51],
sequence: 0xffff_ffff,
}]
.into(),
outputs: vec![TransactionOutput {
value,
script_pubkey: vec![0x51],
}]
.into(),
lock_time: 0,
};
prop_assert_eq!(calculate_fee(&coinbase, &UtxoSet::default()).unwrap(), 0);
let prevout = OutPoint { hash: [1; 32], index: 0 };
let mut utxo_set = UtxoSet::default();
utxo_set.insert(
prevout,
std::sync::Arc::new(UTXO {
value,
script_pubkey: vec![].into(),
height: 0,
is_coinbase: false,
}),
);
let spend_tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout,
script_sig: vec![0x51],
sequence: 0xffff_ffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: spend,
script_pubkey: vec![0x51],
}]
.into(),
lock_time: 0,
};
let fee = calculate_fee(&spend_tx, &utxo_set);
if spend <= value {
prop_assert_eq!(fee.unwrap(), value - spend);
} else {
prop_assert!(fee.is_err());
}
}
}
}
#[cfg(test)]
#[path = "economic_tests.rs"]
mod tests;