Block Subsidy and Founders Reward Amounts (#1051)
* add general and founders reward subsidy modules * validate founders reward * Use funding streams after Canopy on testnet ZIP-1014 only applies to mainnet, where Canopy is at the first halving. On testnet, Canopy is before the first halving, and the dev fund rules apply from Canopy. (See ZIP-214.) Co-authored-by: teor <teor@riseup.net> Co-authored-by: Jane Lusby <jlusby42@gmail.com> * pass all test vectors through current subsidy validation * Add testnet and halving subsidy tests * add subsidy validation error tests * rename block validation methods * add network to block verifier * add amount operators * Implement Ord, Eq, and Hash for Amount * Implement Add<Height> for Height And make the existing Height operators do range checks. * Apply operator suggestions Co-authored-by: Jane Lusby <jlusby42@gmail.com>
This commit is contained in:
parent
766baea9d8
commit
c93f0b3a2e
|
@ -6,7 +6,9 @@
|
|||
//! [`Result`](std::result::Result)s.
|
||||
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
convert::{TryFrom, TryInto},
|
||||
hash::{Hash, Hasher},
|
||||
marker::PhantomData,
|
||||
ops::RangeInclusive,
|
||||
};
|
||||
|
@ -17,7 +19,7 @@ use byteorder::{ByteOrder, LittleEndian, ReadBytesExt, WriteBytesExt};
|
|||
type Result<T, E = Error> = std::result::Result<T, E>;
|
||||
|
||||
/// A runtime validated type for representing amounts of zatoshis
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy, Serialize, Deserialize, Hash)]
|
||||
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
|
||||
#[serde(try_from = "i64")]
|
||||
#[serde(bound = "C: Constraint")]
|
||||
pub struct Amount<C = NegativeAllowed>(i64, PhantomData<C>);
|
||||
|
@ -191,6 +193,75 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl<C> Hash for Amount<C> {
|
||||
/// Amounts with the same value are equal, even if they have different constraints
|
||||
fn hash<H: Hasher>(&self, state: &mut H) {
|
||||
self.0.hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl<C1, C2> PartialEq<Amount<C2>> for Amount<C1> {
|
||||
fn eq(&self, other: &Amount<C2>) -> bool {
|
||||
self.0.eq(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for Amount<NegativeAllowed> {}
|
||||
impl Eq for Amount<NonNegative> {}
|
||||
|
||||
impl<C1, C2> PartialOrd<Amount<C2>> for Amount<C1> {
|
||||
fn partial_cmp(&self, other: &Amount<C2>) -> Option<Ordering> {
|
||||
Some(self.0.cmp(&other.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Amount<NegativeAllowed> {
|
||||
fn cmp(&self, other: &Amount<NegativeAllowed>) -> Ordering {
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ord for Amount<NonNegative> {
|
||||
fn cmp(&self, other: &Amount<NonNegative>) -> Ordering {
|
||||
self.0.cmp(&other.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<u64> for Amount<NonNegative> {
|
||||
type Output = Result<Amount<NonNegative>>;
|
||||
|
||||
fn mul(self, rhs: u64) -> Self::Output {
|
||||
let value = (self.0 as u64)
|
||||
.checked_mul(rhs)
|
||||
.ok_or(Error::MultiplicationOverflow {
|
||||
amount: self.0,
|
||||
multiplier: rhs,
|
||||
})?;
|
||||
value.try_into()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Mul<Amount<NonNegative>> for u64 {
|
||||
type Output = Result<Amount<NonNegative>>;
|
||||
|
||||
fn mul(self, rhs: Amount<NonNegative>) -> Self::Output {
|
||||
rhs.mul(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Div<u64> for Amount<NonNegative> {
|
||||
type Output = Result<Amount<NonNegative>>;
|
||||
|
||||
fn div(self, rhs: u64) -> Self::Output {
|
||||
let quotient = (self.0 as u64)
|
||||
.checked_div(rhs)
|
||||
.ok_or(Error::DivideByZero { amount: self.0 })?;
|
||||
Ok(quotient
|
||||
.try_into()
|
||||
.expect("division by a positive integer always stays within the constraint"))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(thiserror::Error, Debug, displaydoc::Display, Clone, PartialEq)]
|
||||
#[allow(missing_docs)]
|
||||
/// Errors that can be returned when validating `Amount`s
|
||||
|
@ -205,6 +276,10 @@ pub enum Error {
|
|||
value: u64,
|
||||
source: std::num::TryFromIntError,
|
||||
},
|
||||
/// i64 overflow when multiplying i64 non-negative amount {amount} by u64 {multiplier}
|
||||
MultiplicationOverflow { amount: i64, multiplier: u64 },
|
||||
/// cannot divide amount {amount} by zero
|
||||
DivideByZero { amount: i64 },
|
||||
}
|
||||
|
||||
/// Marker type for `Amount` that allows negative values.
|
||||
|
@ -243,8 +318,11 @@ impl Constraint for NonNegative {
|
|||
}
|
||||
}
|
||||
|
||||
/// Number of zatoshis in 1 ZEC
|
||||
pub const COIN: i64 = 100_000_000;
|
||||
|
||||
/// The maximum zatoshi amount.
|
||||
pub const MAX_MONEY: i64 = 21_000_000 * 100_000_000;
|
||||
pub const MAX_MONEY: i64 = 21_000_000 * COIN;
|
||||
|
||||
/// A trait for defining constraints on `Amount`
|
||||
pub trait Constraint {
|
||||
|
@ -315,6 +393,11 @@ where
|
|||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use std::{
|
||||
collections::hash_map::RandomState, collections::HashSet, fmt::Debug, iter::FromIterator,
|
||||
};
|
||||
|
||||
use color_eyre::eyre::Result;
|
||||
|
||||
#[test]
|
||||
|
@ -486,4 +569,78 @@ mod test {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hash() -> Result<()> {
|
||||
let one = Amount::<NonNegative>::try_from(1)?;
|
||||
let another_one = Amount::<NonNegative>::try_from(1)?;
|
||||
let zero = Amount::<NonNegative>::try_from(0)?;
|
||||
|
||||
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
|
||||
HashSet::from_iter([one].iter().cloned());
|
||||
assert_eq!(hash_set.len(), 1);
|
||||
|
||||
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
|
||||
HashSet::from_iter([one, one].iter().cloned());
|
||||
assert_eq!(hash_set.len(), 1, "Amount hashes are consistent");
|
||||
|
||||
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
|
||||
HashSet::from_iter([one, another_one].iter().cloned());
|
||||
assert_eq!(hash_set.len(), 1, "Amount hashes are by value");
|
||||
|
||||
let hash_set: HashSet<Amount<NonNegative>, RandomState> =
|
||||
HashSet::from_iter([one, zero].iter().cloned());
|
||||
assert_eq!(
|
||||
hash_set.len(),
|
||||
2,
|
||||
"Amount hashes are different for different values"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ordering_constraints() -> Result<()> {
|
||||
ordering::<NonNegative, NonNegative>()?;
|
||||
ordering::<NonNegative, NegativeAllowed>()?;
|
||||
ordering::<NegativeAllowed, NonNegative>()?;
|
||||
ordering::<NegativeAllowed, NegativeAllowed>()?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ordering<C1, C2>() -> Result<()>
|
||||
where
|
||||
C1: Constraint + Debug,
|
||||
C2: Constraint + Debug,
|
||||
{
|
||||
let zero = Amount::<C1>::try_from(0)?;
|
||||
let one = Amount::<C2>::try_from(1)?;
|
||||
let another_one = Amount::<C1>::try_from(1)?;
|
||||
|
||||
assert_eq!(one, one);
|
||||
assert_eq!(one, another_one, "Amount equality is by value");
|
||||
|
||||
assert_ne!(one, zero);
|
||||
assert_ne!(zero, one);
|
||||
|
||||
assert!(one > zero);
|
||||
assert!(zero < one);
|
||||
assert!(zero <= one);
|
||||
|
||||
let negative_one = Amount::<NegativeAllowed>::try_from(-1)?;
|
||||
let negative_two = Amount::<NegativeAllowed>::try_from(-2)?;
|
||||
|
||||
assert_ne!(negative_one, zero);
|
||||
assert_ne!(negative_one, one);
|
||||
|
||||
assert!(negative_one < zero);
|
||||
assert!(negative_one <= one);
|
||||
assert!(zero > negative_one);
|
||||
assert!(zero >= negative_one);
|
||||
assert!(negative_two < negative_one);
|
||||
assert!(negative_one > negative_two);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
use crate::serialization::SerializationError;
|
||||
|
||||
use std::ops::{Add, Sub};
|
||||
use std::{
|
||||
convert::TryFrom,
|
||||
ops::{Add, Sub},
|
||||
};
|
||||
|
||||
/// The height of a block is the length of the chain back to the genesis block.
|
||||
///
|
||||
|
@ -47,19 +50,51 @@ impl Height {
|
|||
pub const MAX_AS_U32: u32 = Self::MAX.0;
|
||||
}
|
||||
|
||||
impl Add<Height> for Height {
|
||||
type Output = Option<Height>;
|
||||
|
||||
fn add(self, rhs: Height) -> Option<Height> {
|
||||
// We know that both values are positive integers. Therefore, the result is
|
||||
// positive, and we can skip the conversions. The checked_add is required,
|
||||
// because the result may overflow.
|
||||
let height = self.0.checked_add(rhs.0)?;
|
||||
let height = Height(height);
|
||||
|
||||
if height <= Height::MAX && height >= Height::MIN {
|
||||
Some(height)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Sub<Height> for Height {
|
||||
type Output = i32;
|
||||
|
||||
/// Panics if the inputs or result are outside the valid i32 range.
|
||||
fn sub(self, rhs: Height) -> i32 {
|
||||
(self.0 as i32) - (rhs.0 as i32)
|
||||
// We construct heights from integers without any checks,
|
||||
// so the inputs or result could be out of range.
|
||||
let lhs = i32::try_from(self.0)
|
||||
.expect("out of range input `self`: inputs should be valid Heights");
|
||||
let rhs =
|
||||
i32::try_from(rhs.0).expect("out of range input `rhs`: inputs should be valid Heights");
|
||||
lhs.checked_sub(rhs)
|
||||
.expect("out of range result: valid input heights should yield a valid result")
|
||||
}
|
||||
}
|
||||
|
||||
// We don't implement Add<u32> or Sub<u32>, because they cause type inference issues for integer constants.
|
||||
|
||||
impl Add<i32> for Height {
|
||||
type Output = Option<Height>;
|
||||
|
||||
fn add(self, rhs: i32) -> Option<Height> {
|
||||
let result = ((self.0 as i32) + rhs) as u32;
|
||||
// Because we construct heights from integers without any checks,
|
||||
// the input values could be outside the valid range for i32.
|
||||
let lhs = i32::try_from(self.0).ok()?;
|
||||
let result = lhs.checked_add(rhs)?;
|
||||
let result = u32::try_from(result).ok()?;
|
||||
match result {
|
||||
h if (Height(h) <= Height::MAX && Height(h) >= Height::MIN) => Some(Height(h)),
|
||||
_ => None,
|
||||
|
@ -71,7 +106,10 @@ impl Sub<i32> for Height {
|
|||
type Output = Option<Height>;
|
||||
|
||||
fn sub(self, rhs: i32) -> Option<Height> {
|
||||
let result = ((self.0 as i32) - rhs) as u32;
|
||||
// These checks are required, see above for details.
|
||||
let lhs = i32::try_from(self.0).ok()?;
|
||||
let result = lhs.checked_sub(rhs)?;
|
||||
let result = u32::try_from(result).ok()?;
|
||||
match result {
|
||||
h if (Height(h) <= Height::MAX && Height(h) >= Height::MIN) => Some(Height(h)),
|
||||
_ => None,
|
||||
|
@ -94,14 +132,52 @@ impl Arbitrary for Height {
|
|||
|
||||
#[test]
|
||||
fn operator_tests() {
|
||||
assert_eq!(Some(Height(2)), Height(1) + Height(1));
|
||||
assert_eq!(None, Height::MAX + Height(1));
|
||||
// Bad heights aren't caught at compile-time or runtime, until we add or subtract
|
||||
assert_eq!(None, Height(Height::MAX_AS_U32 + 1) + Height(0));
|
||||
assert_eq!(None, Height(i32::MAX as u32) + Height(0));
|
||||
assert_eq!(None, Height(u32::MAX) + Height(0));
|
||||
|
||||
assert_eq!(Some(Height(2)), Height(1) + 1);
|
||||
assert_eq!(None, Height::MAX + 1);
|
||||
// Adding negative numbers
|
||||
assert_eq!(Some(Height(1)), Height(2) + -1);
|
||||
assert_eq!(Some(Height(0)), Height(1) + -1);
|
||||
assert_eq!(None, Height(0) + -1);
|
||||
assert_eq!(Some(Height(Height::MAX_AS_U32 - 1)), Height::MAX + -1);
|
||||
// Bad heights aren't caught at compile-time or runtime, until we add or subtract
|
||||
// `+ 0` would also cause an error here, but it triggers a spurious clippy lint
|
||||
assert_eq!(None, Height(Height::MAX_AS_U32 + 1) + 1);
|
||||
assert_eq!(None, Height(i32::MAX as u32) + 1);
|
||||
assert_eq!(None, Height(u32::MAX) + 1);
|
||||
// Adding negative numbers
|
||||
assert_eq!(None, Height(i32::MAX as u32) + -1);
|
||||
assert_eq!(None, Height(u32::MAX) + -1);
|
||||
|
||||
assert_eq!(Some(Height(1)), Height(2) - 1);
|
||||
assert_eq!(Some(Height(0)), Height(1) - 1);
|
||||
assert_eq!(None, Height(0) - 1);
|
||||
assert_eq!(Some(Height(Height::MAX_AS_U32 - 1)), Height::MAX - 1);
|
||||
// Subtracting negative numbers
|
||||
assert_eq!(Some(Height(2)), Height(1) - -1);
|
||||
assert_eq!(Some(Height::MAX), Height(Height::MAX_AS_U32 - 1) - -1);
|
||||
assert_eq!(None, Height::MAX - -1);
|
||||
// Bad heights aren't caught at compile-time or runtime, until we add or subtract
|
||||
assert_eq!(None, Height(i32::MAX as u32) - 1);
|
||||
assert_eq!(None, Height(u32::MAX) - 1);
|
||||
// Subtracting negative numbers
|
||||
assert_eq!(None, Height(Height::MAX_AS_U32 + 1) - -1);
|
||||
assert_eq!(None, Height(i32::MAX as u32) - -1);
|
||||
assert_eq!(None, Height(u32::MAX) - -1);
|
||||
|
||||
// Sub<Height> panics on out of range errors
|
||||
assert_eq!(1, Height(2) - Height(1));
|
||||
assert_eq!(0, Height(1) - Height(1));
|
||||
assert_eq!(-1, Height(0) - Height(1));
|
||||
assert_eq!(-5, Height(2) - Height(7));
|
||||
assert_eq!(Height::MAX_AS_U32 as i32, Height::MAX - Height(0));
|
||||
assert_eq!(1, Height::MAX - Height(Height::MAX_AS_U32 - 1));
|
||||
assert_eq!(-1, Height(Height::MAX_AS_U32 - 1) - Height::MAX);
|
||||
assert_eq!(-(Height::MAX_AS_U32 as i32), Height(0) - Height::MAX);
|
||||
}
|
||||
|
|
|
@ -22,6 +22,7 @@ use tower::{Service, ServiceExt};
|
|||
|
||||
use zebra_chain::{
|
||||
block::{self, Block},
|
||||
parameters::Network,
|
||||
work::equihash,
|
||||
};
|
||||
use zebra_state as zs;
|
||||
|
@ -30,6 +31,7 @@ use crate::error::*;
|
|||
use crate::BoxError;
|
||||
|
||||
mod check;
|
||||
mod subsidy;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
|
@ -40,6 +42,9 @@ where
|
|||
S: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
|
||||
S::Future: Send + 'static,
|
||||
{
|
||||
/// The network to be verified.
|
||||
network: Network,
|
||||
|
||||
/// The underlying state service, possibly wrapped in other services.
|
||||
state_service: S,
|
||||
}
|
||||
|
@ -70,8 +75,11 @@ where
|
|||
S: Service<zs::Request, Response = zs::Response, Error = BoxError> + Send + Clone + 'static,
|
||||
S::Future: Send + 'static,
|
||||
{
|
||||
pub fn new(state_service: S) -> Self {
|
||||
Self { state_service }
|
||||
pub fn new(network: Network, state_service: S) -> Self {
|
||||
Self {
|
||||
network,
|
||||
state_service,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -94,6 +102,7 @@ where
|
|||
|
||||
fn call(&mut self, block: Arc<Block>) -> Self::Future {
|
||||
let mut state_service = self.state_service.clone();
|
||||
let network = self.network;
|
||||
|
||||
// TODO(jlusby): Error = Report, handle errors from state_service.
|
||||
async move {
|
||||
|
@ -138,15 +147,16 @@ where
|
|||
difficulty_threshold,
|
||||
))?;
|
||||
}
|
||||
check::is_equihash_solution_valid(&block.header)?;
|
||||
check::equihash_solution_is_valid(&block.header)?;
|
||||
|
||||
// Since errors cause an early exit, try to do the
|
||||
// quick checks first.
|
||||
|
||||
// Field validity and structure checks
|
||||
let now = Utc::now();
|
||||
check::is_time_valid_at(&block.header, now).map_err(VerifyBlockError::Time)?;
|
||||
check::is_coinbase_first(&block)?;
|
||||
check::time_is_valid_at(&block.header, now).map_err(VerifyBlockError::Time)?;
|
||||
check::coinbase_is_first(&block)?;
|
||||
check::subsidy_is_correct(network, &block)?;
|
||||
|
||||
// TODO: context-free header verification: merkle root
|
||||
|
||||
|
|
|
@ -4,11 +4,16 @@ use chrono::{DateTime, Utc};
|
|||
|
||||
use zebra_chain::{
|
||||
block::{Block, Header},
|
||||
parameters::NetworkUpgrade,
|
||||
work::equihash,
|
||||
};
|
||||
|
||||
use crate::error::*;
|
||||
use crate::BoxError;
|
||||
use crate::{error::*, parameters::SLOW_START_INTERVAL};
|
||||
|
||||
use zebra_chain::parameters::Network;
|
||||
|
||||
use super::subsidy;
|
||||
|
||||
/// Check that there is exactly one coinbase transaction in `Block`, and that
|
||||
/// the coinbase transaction is the first transaction in the block.
|
||||
|
@ -18,7 +23,7 @@ use crate::BoxError;
|
|||
/// fees paid by transactions included in this block." [§3.10][3.10]
|
||||
///
|
||||
/// [3.10]: https://zips.z.cash/protocol/protocol.pdf#coinbasetransactions
|
||||
pub fn is_coinbase_first(block: &Block) -> Result<(), BlockError> {
|
||||
pub fn coinbase_is_first(block: &Block) -> Result<(), BlockError> {
|
||||
let first = block
|
||||
.transactions
|
||||
.get(0)
|
||||
|
@ -35,7 +40,7 @@ pub fn is_coinbase_first(block: &Block) -> Result<(), BlockError> {
|
|||
}
|
||||
|
||||
/// Returns true if the header is valid based on its `EquihashSolution`
|
||||
pub fn is_equihash_solution_valid(header: &Header) -> Result<(), equihash::Error> {
|
||||
pub fn equihash_solution_is_valid(header: &Header) -> Result<(), equihash::Error> {
|
||||
header.solution.check(&header)
|
||||
}
|
||||
|
||||
|
@ -53,6 +58,49 @@ pub fn is_equihash_solution_valid(header: &Header) -> Result<(), equihash::Error
|
|||
/// accepted." [§7.5][7.5]
|
||||
///
|
||||
/// [7.5]: https://zips.z.cash/protocol/protocol.pdf#blockheader
|
||||
pub fn is_time_valid_at(header: &Header, now: DateTime<Utc>) -> Result<(), BoxError> {
|
||||
pub fn time_is_valid_at(header: &Header, now: DateTime<Utc>) -> Result<(), BoxError> {
|
||||
header.is_time_valid_at(now)
|
||||
}
|
||||
|
||||
/// [3.9]: https://zips.z.cash/protocol/protocol.pdf#subsidyconcepts
|
||||
pub fn subsidy_is_correct(network: Network, block: &Block) -> Result<(), BlockError> {
|
||||
let height = block.coinbase_height().ok_or(SubsidyError::NoCoinbase)?;
|
||||
let coinbase = block.transactions.get(0).ok_or(SubsidyError::NoCoinbase)?;
|
||||
|
||||
let halving_div = subsidy::general::halving_divisor(height, network);
|
||||
let canopy_activation_height = NetworkUpgrade::Canopy
|
||||
.activation_height(network)
|
||||
.expect("Canopy activation height is known");
|
||||
|
||||
// TODO: the sum of the coinbase transaction outputs must be less than or equal to the block subsidy plus transaction fees
|
||||
|
||||
// Check founders reward and funding streams
|
||||
if height < SLOW_START_INTERVAL {
|
||||
unreachable!(
|
||||
"unsupported block height: callers should handle blocks below {:?}",
|
||||
SLOW_START_INTERVAL
|
||||
)
|
||||
} else if halving_div.count_ones() != 1 {
|
||||
unreachable!("invalid halving divisor: the halving divisor must be a non-zero power of two")
|
||||
} else if height < canopy_activation_height {
|
||||
// Founders rewards are paid up to Canopy activation, on both mainnet and testnet
|
||||
let founders_reward = subsidy::founders_reward::founders_reward(height, network)
|
||||
.expect("invalid Amount: founders reward should be valid");
|
||||
let matching_values = subsidy::general::find_output_with_amount(coinbase, founders_reward);
|
||||
|
||||
// TODO: the exact founders reward value must be sent as a single output to the correct address
|
||||
if !matching_values.is_empty() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(SubsidyError::FoundersRewardNotFound)?
|
||||
}
|
||||
} else if halving_div < 4 {
|
||||
// Funding streams are paid from Canopy activation to the second halving
|
||||
// Note: Canopy activation is at the first halving on mainnet, but not on testnet
|
||||
// ZIP-1014 only applies to mainnet, ZIP-214 contains the specific rules for testnet
|
||||
unimplemented!("funding stream block subsidy validation is not implemented")
|
||||
} else {
|
||||
// Future halving, with no founders reward or funding streams
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
//! Validate coinbase transaction rewards as described in [§7.7][7.7]
|
||||
//!
|
||||
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
|
||||
|
||||
/// Founders’ Reward functions apply for blocks before Canopy.
|
||||
pub mod founders_reward;
|
||||
/// General subsidy functions apply for blocks after slow-start mining.
|
||||
pub mod general;
|
|
@ -0,0 +1,61 @@
|
|||
//! Founders’ Reward calculations. - [§7.7][7.7]
|
||||
//!
|
||||
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use zebra_chain::{
|
||||
amount::{Amount, Error, NonNegative},
|
||||
block::Height,
|
||||
parameters::Network,
|
||||
};
|
||||
|
||||
use crate::block::subsidy::general::{block_subsidy, halving_divisor};
|
||||
use crate::parameters::subsidy::FOUNDERS_FRACTION_DIVISOR;
|
||||
|
||||
/// `FoundersReward(height)` as described in [protocol specification §7.7][7.7]
|
||||
///
|
||||
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
|
||||
pub fn founders_reward(height: Height, network: Network) -> Result<Amount<NonNegative>, Error> {
|
||||
if halving_divisor(height, network) == 1 {
|
||||
// this calculation is exact, because the block subsidy is divisible by
|
||||
// the FOUNDERS_FRACTION_DIVISOR until long after the first halving
|
||||
block_subsidy(height, network)? / FOUNDERS_FRACTION_DIVISOR
|
||||
} else {
|
||||
Amount::try_from(0)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use color_eyre::Report;
|
||||
use zebra_chain::parameters::NetworkUpgrade::*;
|
||||
#[test]
|
||||
fn test_founders_reward() -> Result<(), Report> {
|
||||
let network = Network::Mainnet;
|
||||
let blossom_height = Blossom.activation_height(network).unwrap();
|
||||
let canopy_height = Canopy.activation_height(network).unwrap();
|
||||
|
||||
// Founders reward is 20% of the block subsidy
|
||||
// https://z.cash/support/faq/#founders-reward-recipients
|
||||
// Before Blossom this is 20*12.5/100 = 2.5 ZEC
|
||||
assert_eq!(
|
||||
Amount::try_from(250_000_000),
|
||||
founders_reward((blossom_height - 1).unwrap(), network)
|
||||
);
|
||||
// Founders reward is still 20% of the block subsidy but the block reward is half what it was
|
||||
// After Blossom this is 20*6.25/100 = 1.25 ZEC
|
||||
// https://z.cash/upgrade/blossom/
|
||||
assert_eq!(
|
||||
Amount::try_from(125_000_000),
|
||||
founders_reward(blossom_height, network)
|
||||
);
|
||||
|
||||
// After first halving(coinciding with Canopy) founders reward will expire
|
||||
// https://z.cash/support/faq/#does-the-founders-reward-expire
|
||||
assert_eq!(Amount::try_from(0), founders_reward(canopy_height, network));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -0,0 +1,326 @@
|
|||
//! Block and Miner subsidies, halvings and target spacing modifiers. - [§7.7][7.7]
|
||||
//!
|
||||
//! [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
|
||||
|
||||
use std::convert::TryFrom;
|
||||
|
||||
use zebra_chain::{
|
||||
amount::{Amount, Error, NonNegative},
|
||||
block::Height,
|
||||
parameters::{Network, NetworkUpgrade::*},
|
||||
transaction::Transaction,
|
||||
transparent,
|
||||
};
|
||||
|
||||
use crate::parameters::subsidy::*;
|
||||
|
||||
/// The divisor used for halvings.
|
||||
///
|
||||
/// `1 << Halving(height)`, as described in [protocol specification §7.7][7.7]
|
||||
///
|
||||
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
|
||||
pub fn halving_divisor(height: Height, network: Network) -> u64 {
|
||||
let blossom_height = Blossom
|
||||
.activation_height(network)
|
||||
.expect("blossom activation height should be available");
|
||||
|
||||
if height < SLOW_START_SHIFT {
|
||||
unreachable!(
|
||||
"unsupported block height: callers should handle blocks below {:?}",
|
||||
SLOW_START_SHIFT
|
||||
)
|
||||
} else if height < blossom_height {
|
||||
let scaled_pre_blossom_height = (height - SLOW_START_SHIFT) as u64;
|
||||
let halving_shift = scaled_pre_blossom_height / (PRE_BLOSSOM_HALVING_INTERVAL.0 as u64);
|
||||
1 << halving_shift
|
||||
} else {
|
||||
let scaled_pre_blossom_height =
|
||||
(blossom_height - SLOW_START_SHIFT) as u64 * BLOSSOM_POW_TARGET_SPACING_RATIO;
|
||||
let post_blossom_height = (height - blossom_height) as u64;
|
||||
let halving_shift = (scaled_pre_blossom_height + post_blossom_height)
|
||||
/ (POST_BLOSSOM_HALVING_INTERVAL.0 as u64);
|
||||
1 << halving_shift
|
||||
}
|
||||
}
|
||||
|
||||
/// `BlockSubsidy(height)` as described in [protocol specification §7.7][7.7]
|
||||
///
|
||||
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
|
||||
pub fn block_subsidy(height: Height, network: Network) -> Result<Amount<NonNegative>, Error> {
|
||||
let blossom_height = Blossom
|
||||
.activation_height(network)
|
||||
.expect("blossom activation height should be available");
|
||||
let halving_div = halving_divisor(height, network);
|
||||
|
||||
if height < SLOW_START_INTERVAL {
|
||||
unreachable!(
|
||||
"unsupported block height: callers should handle blocks below {:?}",
|
||||
SLOW_START_INTERVAL
|
||||
)
|
||||
} else if height < blossom_height {
|
||||
// this calculation is exact, because the halving divisor is 1 here
|
||||
Amount::try_from(MAX_BLOCK_SUBSIDY / halving_div)
|
||||
} else {
|
||||
let scaled_max_block_subsidy = MAX_BLOCK_SUBSIDY / BLOSSOM_POW_TARGET_SPACING_RATIO;
|
||||
// in future halvings, this calculation might not be exact
|
||||
// Amount division is implemented using integer division,
|
||||
// which truncates (rounds down) the result, as specified
|
||||
Amount::try_from(scaled_max_block_subsidy / halving_div)
|
||||
}
|
||||
}
|
||||
|
||||
/// `MinerSubsidy(height)` as described in [protocol specification §7.7][7.7]
|
||||
///
|
||||
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
|
||||
///
|
||||
/// `non_miner_reward` is the founders reward or funding stream value.
|
||||
/// If all the rewards for a block go to the miner, use `None`.
|
||||
#[allow(dead_code)]
|
||||
pub fn miner_subsidy(
|
||||
height: Height,
|
||||
network: Network,
|
||||
non_miner_reward: Option<Amount<NonNegative>>,
|
||||
) -> Result<Amount<NonNegative>, Error> {
|
||||
if let Some(non_miner_reward) = non_miner_reward {
|
||||
block_subsidy(height, network)? - non_miner_reward
|
||||
} else {
|
||||
block_subsidy(height, network)
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a list of outputs in `Transaction`, which have a value equal to `Amount`.
|
||||
pub fn find_output_with_amount(
|
||||
transaction: &Transaction,
|
||||
amount: Amount<NonNegative>,
|
||||
) -> Vec<transparent::Output> {
|
||||
// TODO: shielded coinbase - Heartwood
|
||||
transaction
|
||||
.outputs()
|
||||
.iter()
|
||||
.filter(|o| o.value == amount)
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use color_eyre::Report;
|
||||
|
||||
#[test]
|
||||
fn halving_test() -> Result<(), Report> {
|
||||
halving_for_network(Network::Mainnet)?;
|
||||
halving_for_network(Network::Testnet)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn halving_for_network(network: Network) -> Result<(), Report> {
|
||||
let blossom_height = Blossom.activation_height(network).unwrap();
|
||||
let first_halving_height = match network {
|
||||
Network::Mainnet => Canopy.activation_height(network).unwrap(),
|
||||
// Based on "7.7 Calculation of Block Subsidy and Founders’ Reward"
|
||||
Network::Testnet => Height(1_116_000),
|
||||
};
|
||||
|
||||
assert_eq!(1, halving_divisor((blossom_height - 1).unwrap(), network));
|
||||
assert_eq!(1, halving_divisor(blossom_height, network));
|
||||
assert_eq!(
|
||||
1,
|
||||
halving_divisor((first_halving_height - 1).unwrap(), network)
|
||||
);
|
||||
|
||||
assert_eq!(2, halving_divisor(first_halving_height, network));
|
||||
assert_eq!(
|
||||
2,
|
||||
halving_divisor((first_halving_height + 1).unwrap(), network)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
4,
|
||||
halving_divisor(
|
||||
(first_halving_height + POST_BLOSSOM_HALVING_INTERVAL).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
8,
|
||||
halving_divisor(
|
||||
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 2)).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
1024,
|
||||
halving_divisor(
|
||||
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 9)).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
1024 * 1024,
|
||||
halving_divisor(
|
||||
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 19)).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
1024 * 1024 * 1024,
|
||||
halving_divisor(
|
||||
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 29)).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
assert_eq!(
|
||||
1024 * 1024 * 1024 * 1024,
|
||||
halving_divisor(
|
||||
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 39)).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
|
||||
// The largest possible divisor
|
||||
assert_eq!(
|
||||
1 << 63,
|
||||
halving_divisor(
|
||||
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 62)).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn block_subsidy_test() -> Result<(), Report> {
|
||||
block_subsidy_for_network(Network::Mainnet)?;
|
||||
block_subsidy_for_network(Network::Testnet)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn block_subsidy_for_network(network: Network) -> Result<(), Report> {
|
||||
let blossom_height = Blossom.activation_height(network).unwrap();
|
||||
let first_halving_height = match network {
|
||||
Network::Mainnet => Canopy.activation_height(network).unwrap(),
|
||||
// Based on "7.7 Calculation of Block Subsidy and Founders’ Reward"
|
||||
Network::Testnet => Height(1_116_000),
|
||||
};
|
||||
|
||||
// After slow-start mining and before Blossom the block subsidy is 12.5 ZEC
|
||||
// https://z.cash/support/faq/#what-is-slow-start-mining
|
||||
assert_eq!(
|
||||
Amount::try_from(1_250_000_000),
|
||||
block_subsidy((blossom_height - 1).unwrap(), network)
|
||||
);
|
||||
|
||||
// After Blossom the block subsidy is reduced to 6.25 ZEC without halving
|
||||
// https://z.cash/upgrade/blossom/
|
||||
assert_eq!(
|
||||
Amount::try_from(625_000_000),
|
||||
block_subsidy(blossom_height, network)
|
||||
);
|
||||
|
||||
// After the 1st halving, the block subsidy is reduced to 3.125 ZEC
|
||||
// https://z.cash/upgrade/canopy/
|
||||
assert_eq!(
|
||||
Amount::try_from(312_500_000),
|
||||
block_subsidy(first_halving_height, network)
|
||||
);
|
||||
|
||||
// After the 2nd halving, the block subsidy is reduced to 1.5625 ZEC
|
||||
// See "7.7 Calculation of Block Subsidy and Founders’ Reward"
|
||||
assert_eq!(
|
||||
Amount::try_from(156_250_000),
|
||||
block_subsidy(
|
||||
(first_halving_height + POST_BLOSSOM_HALVING_INTERVAL).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
|
||||
// After the 7th halving, the block subsidy is reduced to 0.04882812 ZEC
|
||||
// Check that the block subsidy rounds down correctly, and there are no errors
|
||||
assert_eq!(
|
||||
Amount::try_from(4_882_812),
|
||||
block_subsidy(
|
||||
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 6)).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
|
||||
// After the 29th halving, the block subsidy is 1 zatoshi
|
||||
// Check that the block subsidy is calculated correctly at the limit
|
||||
assert_eq!(
|
||||
Amount::try_from(1),
|
||||
block_subsidy(
|
||||
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 28)).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
|
||||
// After the 30th halving, there is no block subsidy
|
||||
// Check that there are no errors
|
||||
assert_eq!(
|
||||
Amount::try_from(0),
|
||||
block_subsidy(
|
||||
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 29)).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
|
||||
// The largest possible divisor
|
||||
assert_eq!(
|
||||
Amount::try_from(0),
|
||||
block_subsidy(
|
||||
(first_halving_height + (POST_BLOSSOM_HALVING_INTERVAL.0 as i32 * 62)).unwrap(),
|
||||
network
|
||||
)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn miner_subsidy_test() -> Result<(), Report> {
|
||||
miner_subsidy_for_network(Network::Mainnet)?;
|
||||
miner_subsidy_for_network(Network::Testnet)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn miner_subsidy_for_network(network: Network) -> Result<(), Report> {
|
||||
use crate::block::subsidy::founders_reward::founders_reward;
|
||||
let blossom_height = Blossom.activation_height(network).unwrap();
|
||||
|
||||
// Miner reward before Blossom is 80% of the total block reward
|
||||
// 80*12.5/100 = 10 ZEC
|
||||
let founders_amount = founders_reward((blossom_height - 1).unwrap(), network)?;
|
||||
assert_eq!(
|
||||
Amount::try_from(1_000_000_000),
|
||||
miner_subsidy(
|
||||
(blossom_height - 1).unwrap(),
|
||||
network,
|
||||
Some(founders_amount)
|
||||
)
|
||||
);
|
||||
|
||||
// After blossom the total block reward is "halved", miner still get 80%
|
||||
// 80*6.25/100 = 5 ZEC
|
||||
let founders_amount = founders_reward(blossom_height, network)?;
|
||||
assert_eq!(
|
||||
Amount::try_from(500_000_000),
|
||||
miner_subsidy(blossom_height, network, Some(founders_amount))
|
||||
);
|
||||
|
||||
// TODO: After first halving, miner will get 2.5 ZEC per mined block
|
||||
// but we need funding streams code to get this number
|
||||
|
||||
// TODO: After second halving, there will be no funding streams, and
|
||||
// miners will get all the block reward
|
||||
|
||||
// TODO: also add some very large halvings
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
|
@ -1,5 +1,7 @@
|
|||
//! Tests for block verification
|
||||
|
||||
use crate::parameters::SLOW_START_INTERVAL;
|
||||
|
||||
use super::*;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
@ -11,7 +13,7 @@ use tower::buffer::Buffer;
|
|||
|
||||
use zebra_chain::block::{self, Block};
|
||||
use zebra_chain::{
|
||||
parameters::Network,
|
||||
parameters::{Network, NetworkUpgrade},
|
||||
serialization::{ZcashDeserialize, ZcashDeserializeInto},
|
||||
};
|
||||
use zebra_test::transcript::{TransError, Transcript};
|
||||
|
@ -113,7 +115,7 @@ async fn check_transcripts() -> Result<(), Report> {
|
|||
let network = Network::Mainnet;
|
||||
let state_service = zebra_state::init(zebra_state::Config::ephemeral(), network);
|
||||
|
||||
let block_verifier = Buffer::new(BlockVerifier::new(state_service.clone()), 1);
|
||||
let block_verifier = Buffer::new(BlockVerifier::new(network, state_service.clone()), 1);
|
||||
|
||||
for transcript_data in &[
|
||||
&VALID_BLOCK_TRANSCRIPT,
|
||||
|
@ -140,6 +142,106 @@ fn time_check_past_block() {
|
|||
// a long time in the past. So it's unlikely that the test machine
|
||||
// will have a clock that's far enough in the past for the test to
|
||||
// fail.
|
||||
check::is_time_valid_at(&block.header, now)
|
||||
check::time_is_valid_at(&block.header, now)
|
||||
.expect("the header time from a mainnet block should be valid");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn subsidy_is_correct_test() -> Result<(), Report> {
|
||||
subsidy_is_correct_for_network(Network::Mainnet)?;
|
||||
subsidy_is_correct_for_network(Network::Testnet)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn subsidy_is_correct_for_network(network: Network) -> Result<(), Report> {
|
||||
let block_iter = match network {
|
||||
Network::Mainnet => zebra_test::vectors::MAINNET_BLOCKS.iter(),
|
||||
Network::Testnet => zebra_test::vectors::TESTNET_BLOCKS.iter(),
|
||||
};
|
||||
for (&height, block) in block_iter {
|
||||
let block = block
|
||||
.zcash_deserialize_into::<Block>()
|
||||
.expect("block is structurally valid");
|
||||
|
||||
// TODO: first halving, second halving, third halving, and very large halvings
|
||||
if block::Height(height) > SLOW_START_INTERVAL
|
||||
&& block::Height(height) < NetworkUpgrade::Canopy.activation_height(network).unwrap()
|
||||
{
|
||||
check::subsidy_is_correct(network, &block)
|
||||
.expect("subsidies should pass for this block");
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nocoinbase_validation_failure() -> Result<(), Report> {
|
||||
use crate::error::*;
|
||||
|
||||
let network = Network::Mainnet;
|
||||
|
||||
// Get a header form a block in the mainnet that is inside the founders reward period.
|
||||
let block =
|
||||
Arc::<Block>::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_415000_BYTES[..])
|
||||
.expect("block should deserialize");
|
||||
let mut block = Arc::try_unwrap(block).expect("block should unwrap");
|
||||
|
||||
// Remove coinbase transaction
|
||||
block.transactions.remove(0);
|
||||
|
||||
// Validate the block
|
||||
let result = check::subsidy_is_correct(network, &block).unwrap_err();
|
||||
let expected = BlockError::Transaction(TransactionError::Subsidy(SubsidyError::NoCoinbase));
|
||||
assert_eq!(expected, result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn founders_reward_validation_failure() -> Result<(), Report> {
|
||||
use crate::error::*;
|
||||
use zebra_chain::transaction::Transaction;
|
||||
|
||||
let network = Network::Mainnet;
|
||||
|
||||
// Get a header from a block in the mainnet that is inside the founders reward period.
|
||||
let header =
|
||||
block::Header::zcash_deserialize(&zebra_test::vectors::HEADER_MAINNET_415000_BYTES[..])
|
||||
.unwrap();
|
||||
|
||||
// From the same block get the coinbase transaction
|
||||
let block =
|
||||
Arc::<Block>::zcash_deserialize(&zebra_test::vectors::BLOCK_MAINNET_415000_BYTES[..])
|
||||
.expect("block should deserialize");
|
||||
|
||||
// Build the new transaction with modified coinbase outputs
|
||||
let tx = block
|
||||
.transactions
|
||||
.get(0)
|
||||
.map(|transaction| Transaction::V3 {
|
||||
inputs: transaction.inputs().to_vec(),
|
||||
outputs: vec![transaction.outputs()[0].clone()],
|
||||
lock_time: transaction.lock_time(),
|
||||
expiry_height: transaction.expiry_height().unwrap(),
|
||||
joinsplit_data: None,
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
// Build new block
|
||||
let mut transactions: Vec<Arc<zebra_chain::transaction::Transaction>> = Vec::new();
|
||||
transactions.push(Arc::new(tx));
|
||||
let block = Block {
|
||||
header,
|
||||
transactions,
|
||||
};
|
||||
|
||||
// Validate it
|
||||
let result = check::subsidy_is_correct(network, &block).unwrap_err();
|
||||
let expected = BlockError::Transaction(TransactionError::Subsidy(
|
||||
SubsidyError::FoundersRewardNotFound,
|
||||
));
|
||||
assert_eq!(expected, result);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -169,7 +169,7 @@ where
|
|||
};
|
||||
tracing::info!(?tip, ?max_checkpoint_height, "initializing chain verifier");
|
||||
|
||||
let block = BlockVerifier::new(state_service.clone());
|
||||
let block = BlockVerifier::new(network, state_service.clone());
|
||||
let checkpoint = CheckpointVerifier::from_checkpoint_list(list, tip, state_service);
|
||||
|
||||
Buffer::new(
|
||||
|
|
|
@ -2,16 +2,34 @@
|
|||
|
||||
use thiserror::Error;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum SubsidyError {
|
||||
#[error("no coinbase transaction in block")]
|
||||
NoCoinbase,
|
||||
|
||||
#[error("founders reward output not found")]
|
||||
FoundersRewardNotFound,
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum TransactionError {
|
||||
#[error("first transaction must be coinbase")]
|
||||
CoinbasePosition,
|
||||
|
||||
#[error("coinbase input found in non-coinbase transaction")]
|
||||
CoinbaseInputFound,
|
||||
|
||||
#[error("coinbase transaction failed subsidy validation")]
|
||||
Subsidy(#[from] SubsidyError),
|
||||
}
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
impl From<SubsidyError> for BlockError {
|
||||
fn from(err: SubsidyError) -> BlockError {
|
||||
BlockError::Transaction(TransactionError::Subsidy(err))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Debug, PartialEq)]
|
||||
pub enum BlockError {
|
||||
#[error("block contains invalid transactions")]
|
||||
Transaction(#[from] TransactionError),
|
||||
|
|
|
@ -14,9 +14,11 @@
|
|||
|
||||
pub mod genesis;
|
||||
pub mod minimum_difficulty;
|
||||
pub mod subsidy;
|
||||
|
||||
pub use genesis::*;
|
||||
pub use minimum_difficulty::*;
|
||||
pub use subsidy::*;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
|
|
@ -0,0 +1,49 @@
|
|||
//! Constants for Block Subsidy, Funding Streams, and Founders’ Reward
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
use zebra_chain::{amount::COIN, block::Height};
|
||||
|
||||
/// An initial period from Genesis to this Height where the block subsidy is gradually incremented. [What is slow-start mining][slow-mining]
|
||||
///
|
||||
/// [slow-mining]: https://z.cash/support/faq/#what-is-slow-start-mining
|
||||
pub const SLOW_START_INTERVAL: Height = Height(20_000);
|
||||
|
||||
/// `SlowStartShift()` as described in [protocol specification §7.7][7.7]
|
||||
///
|
||||
/// [7.7]: https://zips.z.cash/protocol/protocol.pdf#subsidies
|
||||
///
|
||||
/// This calculation is exact, because `SLOW_START_INTERVAL` is divisible by 2.
|
||||
pub const SLOW_START_SHIFT: Height = Height(SLOW_START_INTERVAL.0 / 2);
|
||||
|
||||
/// The largest block subsidy, used before the first halving.
|
||||
///
|
||||
/// We use `25 / 2` instead of `12.5`, so that we can calculate the correct value without using floating-point.
|
||||
/// This calculation is exact, because COIN is divisible by 2, and the division is done last.
|
||||
pub const MAX_BLOCK_SUBSIDY: u64 = ((25 * COIN) / 2) as u64;
|
||||
|
||||
/// The blocktime before Blossom, used to calculate ratio.
|
||||
pub const PRE_BLOSSOM_POW_TARGET_SPACING: Duration = Duration::from_secs(150);
|
||||
|
||||
/// The blocktime after Blossom, used to calculate ratio.
|
||||
pub const POST_BLOSSOM_POW_TARGET_SPACING: Duration = Duration::from_secs(75);
|
||||
|
||||
/// Used as a multiplier to get the new halving interval after Blossom.
|
||||
pub const BLOSSOM_POW_TARGET_SPACING_RATIO: u64 =
|
||||
PRE_BLOSSOM_POW_TARGET_SPACING.as_secs() / POST_BLOSSOM_POW_TARGET_SPACING.as_secs();
|
||||
|
||||
/// Halving is at about every 4 years, before Blossom block time is 150 seconds.
|
||||
///
|
||||
/// `(60 * 60 * 24 * 365 * 4) / 150 = 840960`
|
||||
pub const PRE_BLOSSOM_HALVING_INTERVAL: Height = Height(840_000);
|
||||
|
||||
/// After Blossom the block time is reduced to 75 seconds but halving period should remain around 4 years.
|
||||
pub const POST_BLOSSOM_HALVING_INTERVAL: Height =
|
||||
Height((PRE_BLOSSOM_HALVING_INTERVAL.0 as u64 * BLOSSOM_POW_TARGET_SPACING_RATIO) as u32);
|
||||
|
||||
/// The divisor used to calculate the FoundersFraction.
|
||||
///
|
||||
/// Derivation: FOUNDERS_FRACTION_DIVISOR = 1/FoundersFraction
|
||||
///
|
||||
/// Usage: founders_reward = block_subsidy / FOUNDERS_FRACTION_DIVISOR
|
||||
pub const FOUNDERS_FRACTION_DIVISOR: u64 = 5;
|
Loading…
Reference in New Issue