zebra_chain/parameters/network/
subsidy.rs1pub(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#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)]
34pub enum FundingStreamReceiver {
35 #[serde(rename = "ECC")]
37 Ecc,
38
39 ZcashFoundation,
41
42 MajorGrants,
44
45 Deferred,
47}
48
49impl FundingStreamReceiver {
50 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 pub fn is_deferred(&self) -> bool {
82 matches!(self, Self::Deferred)
83 }
84}
85
86#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
88pub struct FundingStreams {
89 height_range: std::ops::Range<Height>,
94 recipients: HashMap<FundingStreamReceiver, FundingStreamRecipient>,
96}
97
98impl FundingStreams {
99 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 pub fn empty() -> Self {
112 Self::new(Height::MAX..Height::MAX, HashMap::new())
113 }
114
115 pub fn height_range(&self) -> &std::ops::Range<Height> {
117 &self.height_range
118 }
119
120 pub fn recipients(&self) -> &HashMap<FundingStreamReceiver, FundingStreamRecipient> {
122 &self.recipients
123 }
124
125 pub fn recipient(&self, receiver: FundingStreamReceiver) -> Option<&FundingStreamRecipient> {
127 self.recipients.get(&receiver)
128 }
129
130 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#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
150pub struct FundingStreamRecipient {
151 numerator: u64,
156 addresses: Vec<transparent::Address>,
158}
159
160impl FundingStreamRecipient {
161 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 pub fn numerator(&self) -> u64 {
182 self.numerator
183 }
184
185 pub fn addresses(&self) -> &[transparent::Address] {
187 &self.addresses
188 }
189
190 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
214pub trait ParameterSubsidy {
216 fn height_for_first_halving(&self) -> Height;
221
222 fn post_blossom_halving_interval(&self) -> HeightDiff;
224
225 fn pre_blossom_halving_interval(&self) -> HeightDiff;
227
228 fn funding_stream_address_change_interval(&self) -> HeightDiff;
235}
236
237impl ParameterSubsidy for Network {
239 fn height_for_first_halving(&self) -> Height {
240 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
278pub fn funding_stream_address_period<N: ParameterSubsidy>(height: Height, network: &N) -> u32 {
283 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
302pub 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
334pub 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 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#[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 #[cfg(zcash_unstable = "zip235")]
401 #[error("invalid zip233 amount")]
402 InvalidZip233Amount,
403}
404
405pub fn halving_divisor(height: Height, network: &Network) -> Option<u64> {
413 1u64.checked_shl(halving(height, network))
415}
416
417pub fn halving(height: Height, network: &Network) -> u32 {
423 let slow_start_shift = network.slow_start_shift();
424 let blossom_height = NetworkUpgrade::Blossom
425 .activation_height(network)
426 .expect("blossom activation height should be available");
427
428 let halving_index = if height < slow_start_shift {
429 0
430 } else if height < blossom_height {
431 let pre_blossom_height = height - slow_start_shift;
432 pre_blossom_height / network.pre_blossom_halving_interval()
433 } else {
434 let pre_blossom_height = blossom_height - slow_start_shift;
435 let scaled_pre_blossom_height =
436 pre_blossom_height * HeightDiff::from(BLOSSOM_POW_TARGET_SPACING_RATIO);
437
438 let post_blossom_height = height - blossom_height;
439
440 (scaled_pre_blossom_height + post_blossom_height) / network.post_blossom_halving_interval()
441 };
442
443 halving_index
444 .try_into()
445 .expect("already checked for negatives")
446}
447
448pub fn block_subsidy(height: Height, net: &Network) -> Result<Amount<NonNegative>, SubsidyError> {
452 let Some(halving_div) = halving_divisor(height, net) else {
453 return Ok(Amount::zero());
454 };
455
456 let slow_start_interval = net.slow_start_interval();
457
458 let amount = if height < slow_start_interval {
461 let slow_start_rate = MAX_BLOCK_SUBSIDY / u64::from(slow_start_interval);
462
463 if height < net.slow_start_shift() {
464 slow_start_rate * u64::from(height)
465 } else {
466 slow_start_rate * (u64::from(height) + 1)
467 }
468 } else {
469 let base_subsidy = if NetworkUpgrade::current(net, height) < NetworkUpgrade::Blossom {
470 MAX_BLOCK_SUBSIDY
471 } else {
472 MAX_BLOCK_SUBSIDY / u64::from(BLOSSOM_POW_TARGET_SPACING_RATIO)
473 };
474
475 base_subsidy / halving_div
476 };
477
478 Ok(Amount::try_from(amount)?)
479}
480
481pub fn miner_subsidy(
485 height: Height,
486 network: &Network,
487 expected_block_subsidy: Amount<NonNegative>,
488) -> Result<Amount<NonNegative>, amount::Error> {
489 let founders_reward = founders_reward(network, height);
490
491 let funding_streams_sum = funding_stream_values(height, network, expected_block_subsidy)?
492 .values()
493 .sum::<Result<Amount<NonNegative>, _>>()?;
494
495 expected_block_subsidy - founders_reward - funding_streams_sum
496}
497
498pub fn founders_reward_address(net: &Network, height: Height) -> Option<transparent::Address> {
502 let founders_address_list = net.founder_address_list();
503 let num_founder_addresses = u32::try_from(founders_address_list.len()).ok()?;
504 let slow_start_shift = u32::from(net.slow_start_shift());
505 let pre_blossom_halving_interval = u32::try_from(net.pre_blossom_halving_interval()).ok()?;
506
507 let founder_address_change_interval = slow_start_shift
508 .checked_add(pre_blossom_halving_interval)?
509 .div_ceil(num_founder_addresses);
510
511 let founder_address_adjusted_height =
512 if NetworkUpgrade::current(net, height) < NetworkUpgrade::Blossom {
513 u32::from(height)
514 } else {
515 NetworkUpgrade::Blossom
516 .activation_height(net)
517 .and_then(|h| {
518 let blossom_activation_height = u32::from(h);
519 let height = u32::from(height);
520
521 blossom_activation_height.checked_add(
522 height.checked_sub(blossom_activation_height)?
523 / BLOSSOM_POW_TARGET_SPACING_RATIO,
524 )
525 })?
526 };
527
528 let founder_address_index =
529 usize::try_from(founder_address_adjusted_height / founder_address_change_interval).ok()?;
530
531 founders_address_list
532 .get(founder_address_index)
533 .and_then(|a| a.parse().ok())
534}
535
536pub fn founders_reward(net: &Network, height: Height) -> Amount<NonNegative> {
540 if halving(height, net) < 1 && NetworkUpgrade::current(net, height) < NetworkUpgrade::Canopy {
546 block_subsidy(height, net)
547 .map(|subsidy| subsidy.div_exact(5))
548 .expect("block subsidy must be valid for founders rewards")
549 } else {
550 Amount::zero()
551 }
552}