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>>, 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 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#[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
392pub fn halving_divisor(height: Height, network: &Network) -> Option<u64> {
400 1u64.checked_shl(num_halvings(height, network))
402}
403
404pub 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
435pub 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 let Some(halving_div) = halving_divisor(height, network) else {
451 return Ok(Amount::zero());
452 };
453
454 if height < network.slow_start_interval() {
457 Err(SubsidyError::UnsupportedHeight)
458 } else if height < blossom_height {
459 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 Ok(Amount::try_from(scaled_max_block_subsidy / halving_div)?)
468 }
469}
470
471pub 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}