Skip to main content

zebra_chain/parameters/network/
subsidy.rs

1//! Calculations for Block Subsidy and Funding Streams
2//!
3//! This module contains the consensus parameters which are required for
4//! verification.
5//!
6//! Some consensus parameters change based on network upgrades. Each network
7//! upgrade happens at a particular block height. Some parameters have a value
8//! (or function) before the upgrade height, at the upgrade height, and after
9//! the upgrade height. (For example, the value of the reserved field in the
10//! block header during the Heartwood upgrade.)
11//!
12//! Typically, consensus parameters are accessed via a function that takes a
13//! `Network` and `block::Height`.
14
15pub(crate) mod constants;
16
17use std::collections::HashMap;
18
19use crate::{
20    amount::{self, Amount, NonNegative},
21    block::{Height, HeightDiff},
22    parameters::{Network, NetworkUpgrade},
23    transparent,
24};
25
26use constants::{
27    regtest, testnet, BLOSSOM_POW_TARGET_SPACING_RATIO, FUNDING_STREAM_RECEIVER_DENOMINATOR,
28    FUNDING_STREAM_SPECIFICATION, LOCKBOX_SPECIFICATION, MAX_BLOCK_SUBSIDY,
29    POST_BLOSSOM_HALVING_INTERVAL, PRE_BLOSSOM_HALVING_INTERVAL,
30};
31
32/// The funding stream receiver categories.
33#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)]
34pub enum FundingStreamReceiver {
35    /// The Electric Coin Company (Bootstrap Foundation) funding stream.
36    #[serde(rename = "ECC")]
37    Ecc,
38
39    /// The Zcash Foundation funding stream.
40    ZcashFoundation,
41
42    /// The Major Grants (Zcash Community Grants) funding stream.
43    MajorGrants,
44
45    /// The deferred pool contribution, see [ZIP-1015](https://zips.z.cash/zip-1015) for more details.
46    Deferred,
47}
48
49impl FundingStreamReceiver {
50    /// Returns a human-readable name and a specification URL for the receiver, as described in
51    /// [ZIP-1014] and [`zcashd`] before NU6. After NU6, the specification is in the [ZIP-1015].
52    ///
53    /// [ZIP-1014]: https://zips.z.cash/zip-1014#abstract
54    /// [`zcashd`]: https://github.com/zcash/zcash/blob/3f09cfa00a3c90336580a127e0096d99e25a38d6/src/consensus/funding.cpp#L13-L32
55    /// [ZIP-1015]: https://zips.z.cash/zip-1015
56    pub fn info(&self, is_post_nu6: bool) -> (&'static str, &'static str) {
57        if is_post_nu6 {
58            (
59                match self {
60                    FundingStreamReceiver::Ecc => "Electric Coin Company",
61                    FundingStreamReceiver::ZcashFoundation => "Zcash Foundation",
62                    FundingStreamReceiver::MajorGrants => "Zcash Community Grants NU6",
63                    FundingStreamReceiver::Deferred => "Lockbox NU6",
64                },
65                LOCKBOX_SPECIFICATION,
66            )
67        } else {
68            (
69                match self {
70                    FundingStreamReceiver::Ecc => "Electric Coin Company",
71                    FundingStreamReceiver::ZcashFoundation => "Zcash Foundation",
72                    FundingStreamReceiver::MajorGrants => "Major Grants",
73                    FundingStreamReceiver::Deferred => "Lockbox NU6",
74                },
75                FUNDING_STREAM_SPECIFICATION,
76            )
77        }
78    }
79
80    /// Returns true if this [`FundingStreamReceiver`] is [`FundingStreamReceiver::Deferred`].
81    pub fn is_deferred(&self) -> bool {
82        matches!(self, Self::Deferred)
83    }
84}
85
86/// Funding stream recipients and height ranges.
87#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
88pub struct FundingStreams {
89    /// Start and end Heights for funding streams
90    /// as described in [protocol specification §7.10.1][7.10.1].
91    ///
92    /// [7.10.1]: https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams
93    height_range: std::ops::Range<Height>,
94    /// Funding stream recipients by [`FundingStreamReceiver`].
95    recipients: HashMap<FundingStreamReceiver, FundingStreamRecipient>,
96}
97
98impl FundingStreams {
99    /// Creates a new [`FundingStreams`].
100    pub fn new(
101        height_range: std::ops::Range<Height>,
102        recipients: HashMap<FundingStreamReceiver, FundingStreamRecipient>,
103    ) -> Self {
104        Self {
105            height_range,
106            recipients,
107        }
108    }
109
110    /// Creates a new empty [`FundingStreams`] representing no funding streams.
111    pub fn empty() -> Self {
112        Self::new(Height::MAX..Height::MAX, HashMap::new())
113    }
114
115    /// Returns height range where these [`FundingStreams`] should apply.
116    pub fn height_range(&self) -> &std::ops::Range<Height> {
117        &self.height_range
118    }
119
120    /// Returns recipients of these [`FundingStreams`].
121    pub fn recipients(&self) -> &HashMap<FundingStreamReceiver, FundingStreamRecipient> {
122        &self.recipients
123    }
124
125    /// Returns a recipient with the provided receiver.
126    pub fn recipient(&self, receiver: FundingStreamReceiver) -> Option<&FundingStreamRecipient> {
127        self.recipients.get(&receiver)
128    }
129
130    /// Accepts a target number of addresses that all recipients of this funding stream
131    /// except the [`FundingStreamReceiver::Deferred`] receiver should have.
132    ///
133    /// Extends the addresses for all funding stream recipients by repeating their
134    /// existing addresses until reaching the provided target number of addresses.
135    pub fn extend_recipient_addresses(&mut self, target_len: usize) {
136        for (receiver, recipient) in &mut self.recipients {
137            if receiver.is_deferred() {
138                continue;
139            }
140
141            recipient.extend_addresses(target_len);
142        }
143    }
144}
145
146/// A funding stream recipient as specified in [protocol specification §7.10.1][7.10.1]
147///
148/// [7.10.1]: https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams
149#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
150pub struct FundingStreamRecipient {
151    /// The numerator for each funding stream receiver category
152    /// as described in [protocol specification §7.10.1][7.10.1].
153    ///
154    /// [7.10.1]: https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams
155    numerator: u64,
156    /// Addresses for the funding stream recipient
157    addresses: Vec<transparent::Address>,
158}
159
160impl FundingStreamRecipient {
161    /// Creates a new [`FundingStreamRecipient`].
162    pub fn new<I, T>(numerator: u64, addresses: I) -> Self
163    where
164        T: ToString,
165        I: IntoIterator<Item = T>,
166    {
167        Self {
168            numerator,
169            addresses: addresses
170                .into_iter()
171                .map(|addr| {
172                    let addr = addr.to_string();
173                    addr.parse()
174                        .expect("funding stream address must deserialize")
175                })
176                .collect(),
177        }
178    }
179
180    /// Returns the numerator for this funding stream.
181    pub fn numerator(&self) -> u64 {
182        self.numerator
183    }
184
185    /// Returns the receiver of this funding stream.
186    pub fn addresses(&self) -> &[transparent::Address] {
187        &self.addresses
188    }
189
190    /// Accepts a target number of addresses that this recipient should have.
191    ///
192    /// Extends the addresses for this funding stream recipient by repeating
193    /// existing addresses until reaching the provided target number of addresses.
194    ///
195    /// # Panics
196    ///
197    /// If there are no recipient addresses.
198    pub fn extend_addresses(&mut self, target_len: usize) {
199        assert!(
200            !self.addresses.is_empty(),
201            "cannot extend addresses for empty recipient"
202        );
203
204        self.addresses = self
205            .addresses
206            .iter()
207            .cycle()
208            .take(target_len)
209            .cloned()
210            .collect();
211    }
212}
213
214/// Functionality specific to block subsidy-related consensus rules
215pub trait ParameterSubsidy {
216    /// Returns the minimum height after the first halving
217    /// as described in [protocol specification §7.10][7.10]
218    ///
219    /// [7.10]: <https://zips.z.cash/protocol/protocol.pdf#fundingstreams>
220    fn height_for_first_halving(&self) -> Height;
221
222    /// Returns the halving interval after Blossom
223    fn post_blossom_halving_interval(&self) -> HeightDiff;
224
225    /// Returns the halving interval before Blossom
226    fn pre_blossom_halving_interval(&self) -> HeightDiff;
227
228    /// Returns the address change interval for funding streams
229    /// as described in [protocol specification §7.10][7.10].
230    ///
231    /// > FSRecipientChangeInterval := PostBlossomHalvingInterval / 48
232    ///
233    /// [7.10]: https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams
234    fn funding_stream_address_change_interval(&self) -> HeightDiff;
235}
236
237/// Network methods related to Block Subsidy and Funding Streams
238impl ParameterSubsidy for Network {
239    fn height_for_first_halving(&self) -> Height {
240        // First halving on Mainnet is at Canopy
241        // while in Testnet is at block constant height of `1_116_000`
242        // <https://zips.z.cash/protocol/protocol.pdf#zip214fundingstreams>
243        match self {
244            Network::Mainnet => NetworkUpgrade::Canopy
245                .activation_height(self)
246                .expect("canopy activation height should be available"),
247            Network::Testnet(params) => {
248                if params.is_regtest() {
249                    regtest::FIRST_HALVING
250                } else if params.is_default_testnet() {
251                    testnet::FIRST_HALVING
252                } else {
253                    height_for_halving(1, self).expect("first halving height should be available")
254                }
255            }
256        }
257    }
258
259    fn post_blossom_halving_interval(&self) -> HeightDiff {
260        match self {
261            Network::Mainnet => POST_BLOSSOM_HALVING_INTERVAL,
262            Network::Testnet(params) => params.post_blossom_halving_interval(),
263        }
264    }
265
266    fn pre_blossom_halving_interval(&self) -> HeightDiff {
267        match self {
268            Network::Mainnet => PRE_BLOSSOM_HALVING_INTERVAL,
269            Network::Testnet(params) => params.pre_blossom_halving_interval(),
270        }
271    }
272
273    fn funding_stream_address_change_interval(&self) -> HeightDiff {
274        self.post_blossom_halving_interval() / 48
275    }
276}
277
278/// Returns the address change period
279/// as described in [protocol specification §7.10][7.10]
280///
281/// [7.10]: https://zips.z.cash/protocol/protocol.pdf#fundingstreams
282pub fn funding_stream_address_period<N: ParameterSubsidy>(height: Height, network: &N) -> u32 {
283    // Spec equation: `address_period = floor((height - (height_for_halving(1) - post_blossom_halving_interval))/funding_stream_address_change_interval)`,
284    // <https://zips.z.cash/protocol/protocol.pdf#fundingstreams>
285    //
286    // Note that the brackets make it so the post blossom halving interval is added to the total.
287    //
288    // In Rust, "integer division rounds towards zero":
289    // <https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#arithmetic-and-logical-binary-operators>
290    // This is the same as `floor()`, because these numbers are all positive.
291
292    let height_after_first_halving = height - network.height_for_first_halving();
293
294    let address_period = (height_after_first_halving + network.post_blossom_halving_interval())
295        / network.funding_stream_address_change_interval();
296
297    address_period
298        .try_into()
299        .expect("all values are positive and smaller than the input height")
300}
301
302/// The first block height of the halving at the provided halving index for a network.
303///
304/// See `Halving(height)`, as described in [protocol specification §7.8][7.8]
305///
306/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
307pub fn height_for_halving(halving: u32, network: &Network) -> Option<Height> {
308    if halving == 0 {
309        return Some(Height(0));
310    }
311
312    let slow_start_shift = i64::from(network.slow_start_shift().0);
313    let blossom_height = i64::from(NetworkUpgrade::Blossom.activation_height(network)?.0);
314    let pre_blossom_halving_interval = network.pre_blossom_halving_interval();
315    let halving_index = i64::from(halving);
316
317    let unscaled_height = halving_index.checked_mul(pre_blossom_halving_interval)?;
318
319    let pre_blossom_height = unscaled_height
320        .min(blossom_height)
321        .checked_add(slow_start_shift)?;
322
323    let post_blossom_height = 0
324        .max(unscaled_height - blossom_height)
325        .checked_mul(i64::from(BLOSSOM_POW_TARGET_SPACING_RATIO))?
326        .checked_add(slow_start_shift)?;
327
328    let height = pre_blossom_height.checked_add(post_blossom_height)?;
329
330    let height = u32::try_from(height).ok()?;
331    height.try_into().ok()
332}
333
334/// Returns the `fs.Value(height)` for each stream receiver
335/// as described in [protocol specification §7.8][7.8]
336///
337/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
338pub fn funding_stream_values(
339    height: Height,
340    network: &Network,
341    expected_block_subsidy: Amount<NonNegative>,
342) -> Result<HashMap<FundingStreamReceiver, Amount<NonNegative>>, crate::amount::Error> {
343    let canopy_height = NetworkUpgrade::Canopy.activation_height(network).unwrap();
344    let mut results = HashMap::new();
345
346    if height >= canopy_height {
347        let funding_streams = network.funding_streams(height);
348        if let Some(funding_streams) = funding_streams {
349            for (&receiver, recipient) in funding_streams.recipients() {
350                // - Spec equation: `fs.value = floor(block_subsidy(height)*(fs.numerator/fs.denominator))`:
351                //   https://zips.z.cash/protocol/protocol.pdf#subsidies
352                // - In Rust, "integer division rounds towards zero":
353                //   https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#arithmetic-and-logical-binary-operators
354                //   This is the same as `floor()`, because these numbers are all positive.
355                let amount_value = ((expected_block_subsidy * recipient.numerator())?
356                    / FUNDING_STREAM_RECEIVER_DENOMINATOR)?;
357
358                results.insert(receiver, amount_value);
359            }
360        }
361    }
362
363    Ok(results)
364}
365
366/// Block subsidy errors.
367#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq)]
368#[allow(missing_docs)]
369pub enum SubsidyError {
370    #[error("no coinbase transaction in block")]
371    NoCoinbase,
372
373    #[error("funding stream expected output not found")]
374    FundingStreamNotFound,
375
376    #[error("one-time lockbox disbursement output not found")]
377    OneTimeLockboxDisbursementNotFound,
378
379    #[error("miner fees are invalid")]
380    InvalidMinerFees,
381
382    #[error("a sum of amounts overflowed")]
383    SumOverflow,
384
385    #[error("unsupported height")]
386    UnsupportedHeight,
387
388    #[error("invalid amount")]
389    InvalidAmount(#[from] amount::Error),
390}
391
392/// The divisor used for halvings.
393///
394/// `1 << Halving(height)`, as described in [protocol specification §7.8][7.8]
395///
396/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
397///
398/// Returns `None` if the divisor would overflow a `u64`.
399pub fn halving_divisor(height: Height, network: &Network) -> Option<u64> {
400    // Some far-future shifts can be more than 63 bits
401    1u64.checked_shl(num_halvings(height, network))
402}
403
404/// The halving index for a block height and network.
405///
406/// `Halving(height)`, as described in [protocol specification §7.8][7.8]
407///
408/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
409pub fn num_halvings(height: Height, network: &Network) -> u32 {
410    let slow_start_shift = network.slow_start_shift();
411    let blossom_height = NetworkUpgrade::Blossom
412        .activation_height(network)
413        .expect("blossom activation height should be available");
414
415    let halving_index = if height < slow_start_shift {
416        0
417    } else if height < blossom_height {
418        let pre_blossom_height = height - slow_start_shift;
419        pre_blossom_height / network.pre_blossom_halving_interval()
420    } else {
421        let pre_blossom_height = blossom_height - slow_start_shift;
422        let scaled_pre_blossom_height =
423            pre_blossom_height * HeightDiff::from(BLOSSOM_POW_TARGET_SPACING_RATIO);
424
425        let post_blossom_height = height - blossom_height;
426
427        (scaled_pre_blossom_height + post_blossom_height) / network.post_blossom_halving_interval()
428    };
429
430    halving_index
431        .try_into()
432        .expect("already checked for negatives")
433}
434
435/// `BlockSubsidy(height)` as described in [protocol specification §7.8][7.8]
436///
437/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
438pub fn block_subsidy(
439    height: Height,
440    network: &Network,
441) -> Result<Amount<NonNegative>, SubsidyError> {
442    let blossom_height = NetworkUpgrade::Blossom
443        .activation_height(network)
444        .expect("blossom activation height should be available");
445
446    // If the halving divisor is larger than u64::MAX, the block subsidy is zero,
447    // because amounts fit in an i64.
448    //
449    // Note: bitcoind incorrectly wraps here, which restarts large block rewards.
450    let Some(halving_div) = halving_divisor(height, network) else {
451        return Ok(Amount::zero());
452    };
453
454    // Zebra doesn't need to calculate block subsidies for blocks with heights in the slow start
455    // interval because it handles those blocks through checkpointing.
456    if height < network.slow_start_interval() {
457        Err(SubsidyError::UnsupportedHeight)
458    } else if height < blossom_height {
459        // this calculation is exact, because the halving divisor is 1 here
460        Ok(Amount::try_from(MAX_BLOCK_SUBSIDY / halving_div)?)
461    } else {
462        let scaled_max_block_subsidy =
463            MAX_BLOCK_SUBSIDY / u64::from(BLOSSOM_POW_TARGET_SPACING_RATIO);
464        // in future halvings, this calculation might not be exact
465        // Amount division is implemented using integer division,
466        // which truncates (rounds down) the result, as specified
467        Ok(Amount::try_from(scaled_max_block_subsidy / halving_div)?)
468    }
469}
470
471/// `MinerSubsidy(height)` as described in [protocol specification §7.8][7.8]
472///
473/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
474pub fn miner_subsidy(
475    height: Height,
476    network: &Network,
477    expected_block_subsidy: Amount<NonNegative>,
478) -> Result<Amount<NonNegative>, amount::Error> {
479    let total_funding_stream_amount: Result<Amount<NonNegative>, _> =
480        funding_stream_values(height, network, expected_block_subsidy)?
481            .values()
482            .sum();
483
484    expected_block_subsidy - total_funding_stream_amount?
485}