use crate::constants::*;
use crate::error::{ConsensusError, Result};
use crate::types::*;
use blvm_spec_lock::spec_locked;
use std::borrow::Cow;
#[spec_locked("6.1", "GetBlockSubsidy")]
#[blvm_spec_lock::requires(height >= 0)]
#[blvm_spec_lock::ensures(result >= 0)]
#[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
}
#[spec_locked("6.2", "TotalSupply")]
#[blvm_spec_lock::axiom(result >= 0)]
#[blvm_spec_lock::axiom(result <= 2100000000000000)]
#[blvm_spec_lock::ensures(result >= 0)]
#[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
}
#[spec_locked("6.5", "CalculateFee")]
#[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 value = utxo_set
.get(&input.prevout)
.map(|utxo| utxo.value)
.unwrap_or(0);
acc.checked_add(value)
.ok_or_else(|| ConsensusError::EconomicValidation("Input value overflow".into()))
})
.map_err(|e| ConsensusError::EconomicValidation(Cow::Owned(e.to_string())))?;
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)
}
#[spec_locked("6.3", "ValidateSupplyLimit")]
pub fn validate_supply_limit(height: Natural) -> Result<bool> {
let current_supply = total_supply(height);
Ok(current_supply <= 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(
tx in proptest::collection::vec(crate::transaction::transaction_proptest::arb_transaction(), 1..=1)
) {
if let Some(tx) = tx.first() {
let utxo_set = UtxoSet::default();
if is_coinbase(tx) {
let fee = calculate_fee(tx, &utxo_set).unwrap_or(-1);
prop_assert_eq!(fee, 0, "Coinbase transactions must have zero fee");
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn naive_total_supply(end: u64) -> i64 {
let mut total = 0i64;
for h in 0..=end {
let subsidy = get_block_subsidy(h);
total = total.checked_add(subsidy).unwrap_or_else(|| {
debug_assert!(false, "overflow");
MAX_MONEY
});
if total >= MAX_MONEY {
break;
}
}
total
}
#[test]
fn total_supply_fast_matches_naive_reference() {
for end in [
0u64,
1,
2,
1000,
HALVING_INTERVAL - 1,
HALVING_INTERVAL,
HALVING_INTERVAL + 1,
2 * HALVING_INTERVAL - 1,
500_000,
] {
assert_eq!(total_supply(end), naive_total_supply(end), "end={end}");
}
let after_last_reward = HALVING_INTERVAL * 64 - 1;
assert_eq!(
total_supply(after_last_reward + 1_000_000),
total_supply(after_last_reward),
"heights after 64×H−1 should not increase supply"
);
}
#[test]
fn test_get_block_subsidy_genesis() {
assert_eq!(get_block_subsidy(0), INITIAL_SUBSIDY);
}
#[test]
fn test_get_block_subsidy_first_halving() {
assert_eq!(get_block_subsidy(HALVING_INTERVAL), INITIAL_SUBSIDY / 2);
}
#[test]
fn test_get_block_subsidy_second_halving() {
assert_eq!(get_block_subsidy(HALVING_INTERVAL * 2), INITIAL_SUBSIDY / 4);
}
#[test]
fn test_get_block_subsidy_max_halvings() {
assert_eq!(get_block_subsidy(HALVING_INTERVAL * 64), 0);
}
#[test]
fn test_total_supply_convergence() {
let supply_at_halving = total_supply(HALVING_INTERVAL);
let expected_at_halving = (HALVING_INTERVAL as i64) * INITIAL_SUBSIDY;
let difference = (supply_at_halving - expected_at_halving).abs();
println!(
"Supply at halving: {supply_at_halving}, Expected: {expected_at_halving}, Difference: {difference}"
);
assert!(difference <= 3_000_000_000); }
#[test]
fn test_supply_limit() {
assert!(validate_supply_limit(0).unwrap());
assert!(validate_supply_limit(HALVING_INTERVAL).unwrap());
assert!(validate_supply_limit(HALVING_INTERVAL * 10).unwrap());
}
#[test]
fn test_calculate_fee_coinbase() {
let coinbase_tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32],
index: 0xffffffff,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 5000000000,
script_pubkey: vec![],
}]
.into(),
lock_time: 0,
};
let utxo_set = UtxoSet::default();
assert_eq!(calculate_fee(&coinbase_tx, &utxo_set).unwrap(), 0);
}
#[test]
fn test_get_block_subsidy_edge_cases() {
assert_eq!(get_block_subsidy(1), INITIAL_SUBSIDY);
assert_eq!(get_block_subsidy(HALVING_INTERVAL - 1), INITIAL_SUBSIDY);
assert_eq!(get_block_subsidy(HALVING_INTERVAL + 1), INITIAL_SUBSIDY / 2);
assert_eq!(
get_block_subsidy(HALVING_INTERVAL * 2 - 1),
INITIAL_SUBSIDY / 2
);
assert_eq!(
get_block_subsidy(HALVING_INTERVAL * 2 + 1),
INITIAL_SUBSIDY / 4
);
}
#[test]
fn test_get_block_subsidy_large_heights() {
let very_large_height = HALVING_INTERVAL * 100;
assert_eq!(get_block_subsidy(very_large_height), 0);
let exactly_64_halvings = HALVING_INTERVAL * 64;
assert_eq!(get_block_subsidy(exactly_64_halvings), 0);
let just_before_64 = HALVING_INTERVAL * 64 - 1;
assert_eq!(get_block_subsidy(just_before_64), INITIAL_SUBSIDY >> 63);
}
#[test]
fn test_total_supply_edge_cases() {
assert_eq!(total_supply(0), INITIAL_SUBSIDY);
assert_eq!(total_supply(1), INITIAL_SUBSIDY * 2);
assert_eq!(total_supply(2), INITIAL_SUBSIDY * 3);
let supply_at_halving = total_supply(HALVING_INTERVAL);
assert!(supply_at_halving > 0);
let expected_approximate = INITIAL_SUBSIDY * HALVING_INTERVAL as i64;
assert!(supply_at_halving <= expected_approximate + 5000000000); }
#[test]
fn test_total_supply_large_heights() {
let very_large_height = HALVING_INTERVAL * 100;
let supply = total_supply(very_large_height);
assert!(supply > 0);
assert!(supply < MAX_MONEY);
let expected_max = 21_000_000 * 100_000_000; assert!(supply <= expected_max);
}
#[test]
fn test_calculate_fee_regular_transaction() {
let mut utxo_set = UtxoSet::default();
let outpoint = OutPoint {
hash: [1; 32],
index: 0,
};
let utxo = UTXO {
value: 1000000000, script_pubkey: vec![].into(),
height: 0,
is_coinbase: false,
};
utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 900000000, script_pubkey: vec![],
}]
.into(),
lock_time: 0,
};
assert_eq!(calculate_fee(&tx, &utxo_set).unwrap(), 100000000);
}
#[test]
fn test_calculate_fee_multiple_inputs_outputs() {
let mut utxo_set = UtxoSet::default();
let outpoint1 = OutPoint {
hash: [1; 32],
index: 0,
};
let utxo1 = UTXO {
value: 500000000, script_pubkey: vec![].into(),
height: 0,
is_coinbase: false,
};
utxo_set.insert(outpoint1, std::sync::Arc::new(utxo1));
let outpoint2 = OutPoint {
hash: [2; 32],
index: 0,
};
let utxo2 = UTXO {
value: 300000000, script_pubkey: vec![].into(),
height: 0,
is_coinbase: false,
};
utxo_set.insert(outpoint2, std::sync::Arc::new(utxo2));
let tx = Transaction {
version: 1,
inputs: vec![
TransactionInput {
prevout: OutPoint {
hash: [1; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
},
TransactionInput {
prevout: OutPoint {
hash: [2; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
},
]
.into(),
outputs: vec![
TransactionOutput {
value: 600000000, script_pubkey: vec![],
},
TransactionOutput {
value: 150000000, script_pubkey: vec![],
},
]
.into(),
lock_time: 0,
};
assert_eq!(calculate_fee(&tx, &utxo_set).unwrap(), 50000000);
}
#[test]
fn test_calculate_fee_missing_utxo() {
let utxo_set = UtxoSet::default();
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 100000000,
script_pubkey: vec![],
}]
.into(),
lock_time: 0,
};
let result = calculate_fee(&tx, &utxo_set);
assert!(result.is_err());
assert!(matches!(result, Err(ConsensusError::EconomicValidation(_))));
}
#[test]
fn test_calculate_fee_negative_fee() {
let mut utxo_set = UtxoSet::default();
let outpoint = OutPoint {
hash: [1; 32],
index: 0,
};
let utxo = UTXO {
value: 100000000, script_pubkey: vec![].into(),
height: 0,
is_coinbase: false,
};
utxo_set.insert(outpoint, std::sync::Arc::new(utxo));
let tx = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![TransactionOutput {
value: 200000000, script_pubkey: vec![],
}]
.into(),
lock_time: 0,
};
let result = calculate_fee(&tx, &utxo_set);
assert!(result.is_err());
assert!(matches!(result, Err(ConsensusError::EconomicValidation(_))));
}
#[test]
fn test_validate_supply_limit_edge_cases() {
assert!(validate_supply_limit(0).unwrap());
assert!(validate_supply_limit(HALVING_INTERVAL).unwrap());
assert!(validate_supply_limit(HALVING_INTERVAL * 2).unwrap());
assert!(validate_supply_limit(HALVING_INTERVAL * 100).unwrap());
}
#[test]
fn test_is_coinbase_edge_cases() {
let valid_coinbase = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32],
index: 0xffffffff,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(is_coinbase(&valid_coinbase));
let wrong_hash = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [1; 32],
index: 0xffffffff,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(!is_coinbase(&wrong_hash));
let wrong_index = Transaction {
version: 1,
inputs: vec![TransactionInput {
prevout: OutPoint {
hash: [0; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
}]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(!is_coinbase(&wrong_index));
let multiple_inputs = Transaction {
version: 1,
inputs: vec![
TransactionInput {
prevout: OutPoint {
hash: [0; 32],
index: 0xffffffff,
},
script_sig: vec![],
sequence: 0xffffffff,
},
TransactionInput {
prevout: OutPoint {
hash: [1; 32],
index: 0,
},
script_sig: vec![],
sequence: 0xffffffff,
},
]
.into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(!is_coinbase(&multiple_inputs));
let no_inputs = Transaction {
version: 1,
inputs: vec![].into(),
outputs: vec![].into(),
lock_time: 0,
};
assert!(!is_coinbase(&no_inputs));
}
}