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>>, amount::Error> {
343    let mut results = HashMap::new();
344
345    if expected_block_subsidy.is_zero() {
346        return Ok(results);
347    }
348
349    if NetworkUpgrade::current(network, height) >= NetworkUpgrade::Canopy {
350        let funding_streams = network.funding_streams(height);
351        if let Some(funding_streams) = funding_streams {
352            for (&receiver, recipient) in funding_streams.recipients() {
353                // - Spec equation: `fs.value = floor(block_subsidy(height)*(fs.numerator/fs.denominator))`:
354                //   https://zips.z.cash/protocol/protocol.pdf#subsidies
355                // - In Rust, "integer division rounds towards zero":
356                //   https://doc.rust-lang.org/stable/reference/expressions/operator-expr.html#arithmetic-and-logical-binary-operators
357                //   This is the same as `floor()`, because these numbers are all positive.
358                let amount_value = ((expected_block_subsidy * recipient.numerator())?
359                    / FUNDING_STREAM_RECEIVER_DENOMINATOR)?;
360
361                results.insert(receiver, amount_value);
362            }
363        }
364    }
365
366    Ok(results)
367}
368
369/// Block subsidy errors.
370#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq)]
371#[allow(missing_docs)]
372pub enum SubsidyError {
373    #[error("no coinbase transaction in block")]
374    NoCoinbase,
375
376    #[error("funding stream expected output not found")]
377    FundingStreamNotFound,
378
379    #[error("founders reward output not found")]
380    FoundersRewardNotFound,
381
382    #[error("one-time lockbox disbursement output not found")]
383    OneTimeLockboxDisbursementNotFound,
384
385    #[error("miner fees are invalid")]
386    InvalidMinerFees,
387
388    #[error("addition of amounts overflowed")]
389    Overflow,
390
391    #[error("subtraction of amounts underflowed")]
392    Underflow,
393
394    #[error("unsupported height")]
395    UnsupportedHeight,
396
397    #[error("invalid amount")]
398    InvalidAmount(#[from] amount::Error),
399}
400
401/// The divisor used for halvings.
402///
403/// `1 << Halving(height)`, as described in [protocol specification §7.8][7.8]
404///
405/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
406///
407/// Returns `None` if the divisor would overflow a `u64`.
408pub fn halving_divisor(height: Height, network: &Network) -> Option<u64> {
409    // Some far-future shifts can be more than 63 bits
410    1u64.checked_shl(halving(height, network))
411}
412
413/// The halving index for a block height and network.
414///
415/// `Halving(height)`, as described in [protocol specification §7.8][7.8]
416///
417/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
418pub fn halving(height: Height, network: &Network) -> u32 {
419    let slow_start_shift = network.slow_start_shift();
420    let blossom_height = NetworkUpgrade::Blossom
421        .activation_height(network)
422        .expect("blossom activation height should be available");
423
424    let halving_index = if height < slow_start_shift {
425        0
426    } else if height < blossom_height {
427        let pre_blossom_height = height - slow_start_shift;
428        pre_blossom_height / network.pre_blossom_halving_interval()
429    } else {
430        let pre_blossom_height = blossom_height - slow_start_shift;
431        let scaled_pre_blossom_height =
432            pre_blossom_height * HeightDiff::from(BLOSSOM_POW_TARGET_SPACING_RATIO);
433
434        let post_blossom_height = height - blossom_height;
435
436        (scaled_pre_blossom_height + post_blossom_height) / network.post_blossom_halving_interval()
437    };
438
439    halving_index
440        .try_into()
441        .expect("already checked for negatives")
442}
443
444/// `BlockSubsidy(height)` as described in [protocol specification §7.8][7.8]
445///
446/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
447pub fn block_subsidy(height: Height, net: &Network) -> Result<Amount<NonNegative>, SubsidyError> {
448    let Some(halving_div) = halving_divisor(height, net) else {
449        return Ok(Amount::zero());
450    };
451
452    let slow_start_interval = net.slow_start_interval();
453
454    // The `floor` fn used in the spec is implicit in Rust's division of primitive integer types.
455
456    let amount = if height < slow_start_interval {
457        let slow_start_rate = MAX_BLOCK_SUBSIDY / u64::from(slow_start_interval);
458
459        if height < net.slow_start_shift() {
460            slow_start_rate * u64::from(height)
461        } else {
462            slow_start_rate * (u64::from(height) + 1)
463        }
464    } else {
465        let base_subsidy = if NetworkUpgrade::current(net, height) < NetworkUpgrade::Blossom {
466            MAX_BLOCK_SUBSIDY
467        } else {
468            MAX_BLOCK_SUBSIDY / u64::from(BLOSSOM_POW_TARGET_SPACING_RATIO)
469        };
470
471        base_subsidy / halving_div
472    };
473
474    Ok(Amount::try_from(amount)?)
475}
476
477/// `MinerSubsidy(height)` as described in [protocol specification §7.8][7.8]
478///
479/// [7.8]: https://zips.z.cash/protocol/protocol.pdf#subsidies
480pub fn miner_subsidy(
481    height: Height,
482    network: &Network,
483    expected_block_subsidy: Amount<NonNegative>,
484) -> Result<Amount<NonNegative>, amount::Error> {
485    let total_funding_stream_amount: Result<Amount<NonNegative>, _> =
486        funding_stream_values(height, network, expected_block_subsidy)?
487            .values()
488            .sum();
489
490    expected_block_subsidy - total_funding_stream_amount?
491}
492
493/// Returns the founders reward address for a given height and network as described in [§7.9].
494///
495/// [§7.9]: <https://zips.z.cash/protocol/protocol.pdf#foundersreward>
496pub fn founders_reward_address(net: &Network, height: Height) -> Option<transparent::Address> {
497    let founders_address_list = net.founder_address_list();
498    let num_founder_addresses = u32::try_from(founders_address_list.len()).ok()?;
499    let slow_start_shift = u32::from(net.slow_start_shift());
500    let pre_blossom_halving_interval = u32::try_from(net.pre_blossom_halving_interval()).ok()?;
501
502    let founder_address_change_interval = slow_start_shift
503        .checked_add(pre_blossom_halving_interval)?
504        .div_ceil(num_founder_addresses);
505
506    let founder_address_adjusted_height =
507        if NetworkUpgrade::current(net, height) < NetworkUpgrade::Blossom {
508            u32::from(height)
509        } else {
510            NetworkUpgrade::Blossom
511                .activation_height(net)
512                .and_then(|h| {
513                    let blossom_activation_height = u32::from(h);
514                    let height = u32::from(height);
515
516                    blossom_activation_height.checked_add(
517                        height.checked_sub(blossom_activation_height)?
518                            / BLOSSOM_POW_TARGET_SPACING_RATIO,
519                    )
520                })?
521        };
522
523    let founder_address_index =
524        usize::try_from(founder_address_adjusted_height / founder_address_change_interval).ok()?;
525
526    founders_address_list
527        .get(founder_address_index)
528        .and_then(|a| a.parse().ok())
529}
530
531/// `FoundersReward(height)` as described in [§7.8].
532///
533/// [§7.8]: <https://zips.z.cash/protocol/protocol.pdf#subsidies>
534pub fn founders_reward(net: &Network, height: Height) -> Amount<NonNegative> {
535    if halving(height, net) < 1 {
536        block_subsidy(height, net)
537            .map(|subsidy| subsidy.div_exact(5))
538            .expect("block subsidy must be valid for founders rewards")
539    } else {
540        Amount::zero()
541    }
542}