1use std::collections::{HashMap, HashSet};
16
17use lazy_static::lazy_static;
18
19use crate::{
20 amount::{self, Amount, NonNegative, COIN},
21 block::{Height, HeightDiff},
22 parameters::{Network, NetworkUpgrade},
23 transaction::Transaction,
24 transparent,
25};
26
27pub const MAX_BLOCK_SUBSIDY: u64 = ((25 * COIN) / 2) as u64;
32
33pub const BLOSSOM_POW_TARGET_SPACING_RATIO: u32 = 2;
38
39pub const PRE_BLOSSOM_HALVING_INTERVAL: HeightDiff = 840_000;
43
44pub const POST_BLOSSOM_HALVING_INTERVAL: HeightDiff =
46 PRE_BLOSSOM_HALVING_INTERVAL * (BLOSSOM_POW_TARGET_SPACING_RATIO as HeightDiff);
47
48pub(crate) const FIRST_HALVING_TESTNET: Height = Height(1_116_000);
53
54const FIRST_HALVING_REGTEST: Height = Height(287);
56
57#[derive(Serialize, Deserialize, Clone, Copy, Debug, Eq, Hash, PartialEq)]
59pub enum FundingStreamReceiver {
60 #[serde(rename = "ECC")]
62 Ecc,
63
64 ZcashFoundation,
66
67 MajorGrants,
69
70 Deferred,
72}
73
74impl FundingStreamReceiver {
75 pub fn info(&self, is_post_nu6: bool) -> (&'static str, &'static str) {
82 if is_post_nu6 {
83 (
84 match self {
85 FundingStreamReceiver::Ecc => "Electric Coin Company",
86 FundingStreamReceiver::ZcashFoundation => "Zcash Foundation",
87 FundingStreamReceiver::MajorGrants => "Zcash Community Grants NU6",
88 FundingStreamReceiver::Deferred => "Lockbox NU6",
89 },
90 LOCKBOX_SPECIFICATION,
91 )
92 } else {
93 (
94 match self {
95 FundingStreamReceiver::Ecc => "Electric Coin Company",
96 FundingStreamReceiver::ZcashFoundation => "Zcash Foundation",
97 FundingStreamReceiver::MajorGrants => "Major Grants",
98 FundingStreamReceiver::Deferred => "Lockbox NU6",
99 },
100 FUNDING_STREAM_SPECIFICATION,
101 )
102 }
103 }
104}
105
106pub const FUNDING_STREAM_RECEIVER_DENOMINATOR: u64 = 100;
110
111pub const FUNDING_STREAM_SPECIFICATION: &str = "https://zips.z.cash/zip-0214";
115
116pub const LOCKBOX_SPECIFICATION: &str = "https://zips.z.cash/zip-1015";
120
121#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
123pub struct FundingStreams {
124 height_range: std::ops::Range<Height>,
129 recipients: HashMap<FundingStreamReceiver, FundingStreamRecipient>,
131}
132
133impl FundingStreams {
134 pub fn new(
136 height_range: std::ops::Range<Height>,
137 recipients: HashMap<FundingStreamReceiver, FundingStreamRecipient>,
138 ) -> Self {
139 Self {
140 height_range,
141 recipients,
142 }
143 }
144
145 pub fn height_range(&self) -> &std::ops::Range<Height> {
147 &self.height_range
148 }
149
150 pub fn recipients(&self) -> &HashMap<FundingStreamReceiver, FundingStreamRecipient> {
152 &self.recipients
153 }
154
155 pub fn recipient(&self, receiver: FundingStreamReceiver) -> Option<&FundingStreamRecipient> {
157 self.recipients.get(&receiver)
158 }
159}
160
161#[derive(Deserialize, Clone, Debug, Eq, PartialEq)]
165pub struct FundingStreamRecipient {
166 numerator: u64,
171 addresses: Vec<transparent::Address>,
173}
174
175impl FundingStreamRecipient {
176 pub fn new<I, T>(numerator: u64, addresses: I) -> Self
178 where
179 T: ToString,
180 I: IntoIterator<Item = T>,
181 {
182 Self {
183 numerator,
184 addresses: addresses
185 .into_iter()
186 .map(|addr| {
187 let addr = addr.to_string();
188 addr.parse()
189 .expect("funding stream address must deserialize")
190 })
191 .collect(),
192 }
193 }
194
195 pub fn numerator(&self) -> u64 {
197 self.numerator
198 }
199
200 pub fn addresses(&self) -> &[transparent::Address] {
202 &self.addresses
203 }
204}
205
206lazy_static! {
207 pub static ref PRE_NU6_FUNDING_STREAMS_MAINNET: FundingStreams = FundingStreams {
210 height_range: Height(1_046_400)..Height(2_726_400),
211 recipients: [
212 (
213 FundingStreamReceiver::Ecc,
214 FundingStreamRecipient::new(7, FUNDING_STREAM_ECC_ADDRESSES_MAINNET),
215 ),
216 (
217 FundingStreamReceiver::ZcashFoundation,
218 FundingStreamRecipient::new(5, FUNDING_STREAM_ZF_ADDRESSES_MAINNET),
219 ),
220 (
221 FundingStreamReceiver::MajorGrants,
222 FundingStreamRecipient::new(8, FUNDING_STREAM_MG_ADDRESSES_MAINNET),
223 ),
224 ]
225 .into_iter()
226 .collect(),
227 };
228
229 pub static ref POST_NU6_FUNDING_STREAMS_MAINNET: FundingStreams = FundingStreams {
231 height_range: POST_NU6_FUNDING_STREAM_START_RANGE_MAINNET,
232 recipients: [
233 (
234 FundingStreamReceiver::Deferred,
235 FundingStreamRecipient::new::<[&str; 0], &str>(12, []),
236 ),
237 (
238 FundingStreamReceiver::MajorGrants,
239 FundingStreamRecipient::new(8, POST_NU6_FUNDING_STREAM_FPF_ADDRESSES_MAINNET),
240 ),
241 ]
242 .into_iter()
243 .collect()
244 };
245
246 pub static ref PRE_NU6_FUNDING_STREAMS_TESTNET: FundingStreams = FundingStreams {
249 height_range: Height(1_028_500)..Height(2_796_000),
250 recipients: [
251 (
252 FundingStreamReceiver::Ecc,
253 FundingStreamRecipient::new(7, FUNDING_STREAM_ECC_ADDRESSES_TESTNET),
254 ),
255 (
256 FundingStreamReceiver::ZcashFoundation,
257 FundingStreamRecipient::new(5, FUNDING_STREAM_ZF_ADDRESSES_TESTNET),
258 ),
259 (
260 FundingStreamReceiver::MajorGrants,
261 FundingStreamRecipient::new(8, FUNDING_STREAM_MG_ADDRESSES_TESTNET),
262 ),
263 ]
264 .into_iter()
265 .collect(),
266 };
267
268 pub static ref POST_NU6_FUNDING_STREAMS_TESTNET: FundingStreams = FundingStreams {
270 height_range: POST_NU6_FUNDING_STREAM_START_RANGE_TESTNET,
271 recipients: [
272 (
273 FundingStreamReceiver::Deferred,
274 FundingStreamRecipient::new::<[&str; 0], &str>(12, []),
275 ),
276 (
277 FundingStreamReceiver::MajorGrants,
278 FundingStreamRecipient::new(8, POST_NU6_FUNDING_STREAM_FPF_ADDRESSES_TESTNET),
279 ),
280 ]
281 .into_iter()
282 .collect()
283 };
284}
285
286const POST_NU6_FUNDING_STREAM_START_HEIGHT_MAINNET: u32 = 2_726_400;
288
289const POST_NU6_FUNDING_STREAM_START_HEIGHT_TESTNET: u32 = 2_976_000;
291
292const POST_NU6_FUNDING_STREAM_NUM_BLOCKS: u32 = 420_000;
295
296const POST_NU6_FUNDING_STREAM_START_RANGE_MAINNET: std::ops::Range<Height> =
298 Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_MAINNET)
299 ..Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_MAINNET + POST_NU6_FUNDING_STREAM_NUM_BLOCKS);
300
301const POST_NU6_FUNDING_STREAM_START_RANGE_TESTNET: std::ops::Range<Height> =
303 Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_TESTNET)
304 ..Height(POST_NU6_FUNDING_STREAM_START_HEIGHT_TESTNET + POST_NU6_FUNDING_STREAM_NUM_BLOCKS);
305
306pub const FUNDING_STREAM_ADDRESS_CHANGE_INTERVAL: HeightDiff = POST_BLOSSOM_HALVING_INTERVAL / 48;
311
312pub const FUNDING_STREAMS_NUM_ADDRESSES_MAINNET: usize = 48;
318
319pub const FUNDING_STREAM_ECC_ADDRESSES_MAINNET: [&str; FUNDING_STREAMS_NUM_ADDRESSES_MAINNET] = [
321 "t3LmX1cxWPPPqL4TZHx42HU3U5ghbFjRiif",
322 "t3Toxk1vJQ6UjWQ42tUJz2rV2feUWkpbTDs",
323 "t3ZBdBe4iokmsjdhMuwkxEdqMCFN16YxKe6",
324 "t3ZuaJziLM8xZ32rjDUzVjVtyYdDSz8GLWB",
325 "t3bAtYWa4bi8VrtvqySxnbr5uqcG9czQGTZ",
326 "t3dktADfb5Rmxncpe1HS5BRS5Gcj7MZWYBi",
327 "t3hgskquvKKoCtvxw86yN7q8bzwRxNgUZmc",
328 "t3R1VrLzwcxAZzkX4mX3KGbWpNsgtYtMntj",
329 "t3ff6fhemqPMVujD3AQurxRxTdvS1pPSaa2",
330 "t3cEUQFG3KYnFG6qYhPxSNgGi3HDjUPwC3J",
331 "t3WR9F5U4QvUFqqx9zFmwT6xFqduqRRXnaa",
332 "t3PYc1LWngrdUrJJbHkYPCKvJuvJjcm85Ch",
333 "t3bgkjiUeatWNkhxY3cWyLbTxKksAfk561R",
334 "t3Z5rrR8zahxUpZ8itmCKhMSfxiKjUp5Dk5",
335 "t3PU1j7YW3fJ67jUbkGhSRto8qK2qXCUiW3",
336 "t3S3yaT7EwNLaFZCamfsxxKwamQW2aRGEkh",
337 "t3eutXKJ9tEaPSxZpmowhzKhPfJvmtwTEZK",
338 "t3gbTb7brxLdVVghSPSd3ycGxzHbUpukeDm",
339 "t3UCKW2LrHFqPMQFEbZn6FpjqnhAAbfpMYR",
340 "t3NyHsrnYbqaySoQqEQRyTWkjvM2PLkU7Uu",
341 "t3QEFL6acxuZwiXtW3YvV6njDVGjJ1qeaRo",
342 "t3PdBRr2S1XTDzrV8bnZkXF3SJcrzHWe1wj",
343 "t3ZWyRPpWRo23pKxTLtWsnfEKeq9T4XPxKM",
344 "t3he6QytKCTydhpztykFsSsb9PmBT5JBZLi",
345 "t3VWxWDsLb2TURNEP6tA1ZSeQzUmPKFNxRY",
346 "t3NmWLvZkbciNAipauzsFRMxoZGqmtJksbz",
347 "t3cKr4YxVPvPBG1mCvzaoTTdBNokohsRJ8n",
348 "t3T3smGZn6BoSFXWWXa1RaoQdcyaFjMfuYK",
349 "t3gkDUe9Gm4GGpjMk86TiJZqhztBVMiUSSA",
350 "t3eretuBeBXFHe5jAqeSpUS1cpxVh51fAeb",
351 "t3dN8g9zi2UGJdixGe9txeSxeofLS9t3yFQ",
352 "t3S799pq9sYBFwccRecoTJ3SvQXRHPrHqvx",
353 "t3fhYnv1S5dXwau7GED3c1XErzt4n4vDxmf",
354 "t3cmE3vsBc5xfDJKXXZdpydCPSdZqt6AcNi",
355 "t3h5fPdjJVHaH4HwynYDM5BB3J7uQaoUwKi",
356 "t3Ma35c68BgRX8sdLDJ6WR1PCrKiWHG4Da9",
357 "t3LokMKPL1J8rkJZvVpfuH7dLu6oUWqZKQK",
358 "t3WFFGbEbhJWnASZxVLw2iTJBZfJGGX73mM",
359 "t3L8GLEsUn4QHNaRYcX3EGyXmQ8kjpT1zTa",
360 "t3PgfByBhaBSkH8uq4nYJ9ZBX4NhGCJBVYm",
361 "t3WecsqKDhWXD4JAgBVcnaCC2itzyNZhJrv",
362 "t3ZG9cSfopnsMQupKW5v9sTotjcP5P6RTbn",
363 "t3hC1Ywb5zDwUYYV8LwhvF5rZ6m49jxXSG5",
364 "t3VgMqDL15ZcyQDeqBsBW3W6rzfftrWP2yB",
365 "t3LC94Y6BwLoDtBoK2NuewaEbnko1zvR9rm",
366 "t3cWCUZJR3GtALaTcatrrpNJ3MGbMFVLRwQ",
367 "t3YYF4rPLVxDcF9hHFsXyc5Yq1TFfbojCY6",
368 "t3XHAGxRP2FNfhAjxGjxbrQPYtQQjc3RCQD",
369];
370
371pub trait ParameterSubsidy {
373 fn height_for_first_halving(&self) -> Height;
378
379 fn post_blossom_halving_interval(&self) -> HeightDiff;
381
382 fn pre_blossom_halving_interval(&self) -> HeightDiff;
384
385 fn funding_stream_address_change_interval(&self) -> HeightDiff;
392}
393
394impl ParameterSubsidy for Network {
396 fn height_for_first_halving(&self) -> Height {
397 match self {
401 Network::Mainnet => NetworkUpgrade::Canopy
402 .activation_height(self)
403 .expect("canopy activation height should be available"),
404 Network::Testnet(params) => {
405 if params.is_regtest() {
406 FIRST_HALVING_REGTEST
407 } else if params.is_default_testnet() {
408 FIRST_HALVING_TESTNET
409 } else {
410 height_for_halving(1, self).expect("first halving height should be available")
411 }
412 }
413 }
414 }
415
416 fn post_blossom_halving_interval(&self) -> HeightDiff {
417 match self {
418 Network::Mainnet => POST_BLOSSOM_HALVING_INTERVAL,
419 Network::Testnet(params) => params.post_blossom_halving_interval(),
420 }
421 }
422
423 fn pre_blossom_halving_interval(&self) -> HeightDiff {
424 match self {
425 Network::Mainnet => PRE_BLOSSOM_HALVING_INTERVAL,
426 Network::Testnet(params) => params.pre_blossom_halving_interval(),
427 }
428 }
429
430 fn funding_stream_address_change_interval(&self) -> HeightDiff {
431 self.post_blossom_halving_interval() / 48
432 }
433}
434
435pub const FUNDING_STREAM_ZF_ADDRESSES_MAINNET: [&str; FUNDING_STREAMS_NUM_ADDRESSES_MAINNET] =
437 ["t3dvVE3SQEi7kqNzwrfNePxZ1d4hUyztBA1"; FUNDING_STREAMS_NUM_ADDRESSES_MAINNET];
438
439pub const FUNDING_STREAM_MG_ADDRESSES_MAINNET: [&str; FUNDING_STREAMS_NUM_ADDRESSES_MAINNET] =
441 ["t3XyYW8yBFRuMnfvm5KLGFbEVz25kckZXym"; FUNDING_STREAMS_NUM_ADDRESSES_MAINNET];
442
443pub const POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_MAINNET: usize = 12;
449
450pub const POST_NU6_FUNDING_STREAM_FPF_ADDRESSES_MAINNET: [&str;
452 POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_MAINNET] =
453 ["t3cFfPt1Bcvgez9ZbMBFWeZsskxTkPzGCow"; POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_MAINNET];
454
455pub const FUNDING_STREAMS_NUM_ADDRESSES_TESTNET: usize = 51;
461
462pub const FUNDING_STREAM_ECC_ADDRESSES_TESTNET: [&str; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET] = [
464 "t26ovBdKAJLtrvBsE2QGF4nqBkEuptuPFZz",
465 "t26ovBdKAJLtrvBsE2QGF4nqBkEuptuPFZz",
466 "t26ovBdKAJLtrvBsE2QGF4nqBkEuptuPFZz",
467 "t26ovBdKAJLtrvBsE2QGF4nqBkEuptuPFZz",
468 "t2NNHrgPpE388atmWSF4DxAb3xAoW5Yp45M",
469 "t2VMN28itPyMeMHBEd9Z1hm6YLkQcGA1Wwe",
470 "t2CHa1TtdfUV8UYhNm7oxbzRyfr8616BYh2",
471 "t2F77xtr28U96Z2bC53ZEdTnQSUAyDuoa67",
472 "t2ARrzhbgcpoVBDPivUuj6PzXzDkTBPqfcT",
473 "t278aQ8XbvFR15mecRguiJDQQVRNnkU8kJw",
474 "t2Dp1BGnZsrTXZoEWLyjHmg3EPvmwBnPDGB",
475 "t2KzeqXgf4ju33hiSqCuKDb8iHjPCjMq9iL",
476 "t2Nyxqv1BiWY1eUSiuxVw36oveawYuo18tr",
477 "t2DKFk5JRsVoiuinK8Ti6eM4Yp7v8BbfTyH",
478 "t2CUaBca4k1x36SC4q8Nc8eBoqkMpF3CaLg",
479 "t296SiKL7L5wvFmEdMxVLz1oYgd6fTfcbZj",
480 "t29fBCFbhgsjL3XYEZ1yk1TUh7eTusB6dPg",
481 "t2FGofLJXa419A76Gpf5ncxQB4gQXiQMXjK",
482 "t2ExfrnRVnRiXDvxerQ8nZbcUQvNvAJA6Qu",
483 "t28JUffLp47eKPRHKvwSPzX27i9ow8LSXHx",
484 "t2JXWPtrtyL861rFWMZVtm3yfgxAf4H7uPA",
485 "t2QdgbJoWfYHgyvEDEZBjHmgkr9yNJff3Hi",
486 "t2QW43nkco8r32ZGRN6iw6eSzyDjkMwCV3n",
487 "t2DgYDXMJTYLwNcxighQ9RCgPxMVATRcUdC",
488 "t2Bop7dg33HGZx3wunnQzi2R2ntfpjuti3M",
489 "t2HVeEwovcLq9RstAbYkqngXNEsCe2vjJh9",
490 "t2HxbP5keQSx7p592zWQ5bJ5GrMmGDsV2Xa",
491 "t2TJzUg2matao3mztBRJoWnJY6ekUau6tPD",
492 "t29pMzxmo6wod25YhswcjKv3AFRNiBZHuhj",
493 "t2QBQMRiJKYjshJpE6RhbF7GLo51yE6d4wZ",
494 "t2F5RqnqguzZeiLtYHFx4yYfy6pDnut7tw5",
495 "t2CHvyZANE7XCtg8AhZnrcHCC7Ys1jJhK13",
496 "t2BRzpMdrGWZJ2upsaNQv6fSbkbTy7EitLo",
497 "t2BFixHGQMAWDY67LyTN514xRAB94iEjXp3",
498 "t2Uvz1iVPzBEWfQBH1p7NZJsFhD74tKaG8V",
499 "t2CmFDj5q6rJSRZeHf1SdrowinyMNcj438n",
500 "t2ErNvWEReTfPDBaNizjMPVssz66aVZh1hZ",
501 "t2GeJQ8wBUiHKDVzVM5ZtKfY5reCg7CnASs",
502 "t2L2eFtkKv1G6j55kLytKXTGuir4raAy3yr",
503 "t2EK2b87dpPazb7VvmEGc8iR6SJ289RywGL",
504 "t2DJ7RKeZJxdA4nZn8hRGXE8NUyTzjujph9",
505 "t2K1pXo4eByuWpKLkssyMLe8QKUbxnfFC3H",
506 "t2TB4mbSpuAcCWkH94Leb27FnRxo16AEHDg",
507 "t2Phx4gVL4YRnNsH3jM1M7jE4Fo329E66Na",
508 "t2VQZGmeNomN8c3USefeLL9nmU6M8x8CVzC",
509 "t2RicCvTVTY5y9JkreSRv3Xs8q2K67YxHLi",
510 "t2JrSLxTGc8wtPDe9hwbaeUjCrCfc4iZnDD",
511 "t2Uh9Au1PDDSw117sAbGivKREkmMxVC5tZo",
512 "t2FDwoJKLeEBMTy3oP7RLQ1Fihhvz49a3Bv",
513 "t2FY18mrgtb7QLeHA8ShnxLXuW8cNQ2n1v8",
514 "t2L15TkDYum7dnQRBqfvWdRe8Yw3jVy9z7g",
515];
516
517pub const FUNDING_STREAM_ZF_ADDRESSES_TESTNET: [&str; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET] =
519 ["t27eWDgjFYJGVXmzrXeVjnb5J3uXDM9xH9v"; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET];
520
521pub const FUNDING_STREAM_MG_ADDRESSES_TESTNET: [&str; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET] =
523 ["t2Gvxv2uNM7hbbACjNox4H6DjByoKZ2Fa3P"; FUNDING_STREAMS_NUM_ADDRESSES_TESTNET];
524
525pub const POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_TESTNET: usize = 13;
531
532pub const POST_NU6_FUNDING_STREAM_FPF_ADDRESSES_TESTNET: [&str;
534 POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_TESTNET] =
535 ["t2HifwjUj9uyxr9bknR8LFuQbc98c3vkXtu"; POST_NU6_FUNDING_STREAMS_NUM_ADDRESSES_TESTNET];
536
537pub fn funding_stream_address_period<N: ParameterSubsidy>(height: Height, network: &N) -> u32 {
542 let height_after_first_halving = height - network.height_for_first_halving();
552
553 let address_period = (height_after_first_halving + network.post_blossom_halving_interval())
554 / network.funding_stream_address_change_interval();
555
556 address_period
557 .try_into()
558 .expect("all values are positive and smaller than the input height")
559}
560
561pub fn height_for_halving(halving: u32, network: &Network) -> Option<Height> {
567 if halving == 0 {
568 return Some(Height(0));
569 }
570
571 let slow_start_shift = i64::from(network.slow_start_shift().0);
572 let blossom_height = i64::from(
573 NetworkUpgrade::Blossom
574 .activation_height(network)
575 .expect("blossom activation height should be available")
576 .0,
577 );
578 let pre_blossom_halving_interval = network.pre_blossom_halving_interval();
579 let halving_index = i64::from(halving);
580
581 let unscaled_height = halving_index
582 .checked_mul(pre_blossom_halving_interval)
583 .expect("Multiplication overflow: consider reducing the halving interval");
584
585 let pre_blossom_height = unscaled_height
586 .min(blossom_height)
587 .checked_add(slow_start_shift)
588 .expect("Addition overflow: consider reducing the halving interval");
589
590 let post_blossom_height = 0
591 .max(unscaled_height - blossom_height)
592 .checked_mul(i64::from(BLOSSOM_POW_TARGET_SPACING_RATIO))
593 .expect("Multiplication overflow: consider reducing the halving interval")
594 .checked_add(slow_start_shift)
595 .expect("Addition overflow: consider reducing the halving interval");
596
597 let height = pre_blossom_height
598 .checked_add(post_blossom_height)
599 .expect("Addition overflow: consider reducing the halving interval");
600
601 let height = u32::try_from(height).ok()?;
602 height.try_into().ok()
603}
604
605pub fn funding_stream_values(
610 height: Height,
611 network: &Network,
612 expected_block_subsidy: Amount<NonNegative>,
613) -> Result<HashMap<FundingStreamReceiver, Amount<NonNegative>>, crate::amount::Error> {
614 let canopy_height = NetworkUpgrade::Canopy.activation_height(network).unwrap();
615 let mut results = HashMap::new();
616
617 if height >= canopy_height {
618 let funding_streams = network.funding_streams(height);
619 if funding_streams.height_range().contains(&height) {
620 for (&receiver, recipient) in funding_streams.recipients() {
621 let amount_value = ((expected_block_subsidy * recipient.numerator())?
627 / FUNDING_STREAM_RECEIVER_DENOMINATOR)?;
628
629 results.insert(receiver, amount_value);
630 }
631 }
632 }
633
634 Ok(results)
635}
636
637#[derive(thiserror::Error, Clone, Debug, PartialEq, Eq)]
639#[allow(missing_docs)]
640pub enum SubsidyError {
641 #[error("no coinbase transaction in block")]
642 NoCoinbase,
643
644 #[error("funding stream expected output not found")]
645 FundingStreamNotFound,
646
647 #[error("miner fees are invalid")]
648 InvalidMinerFees,
649
650 #[error("a sum of amounts overflowed")]
651 SumOverflow,
652
653 #[error("unsupported height")]
654 UnsupportedHeight,
655
656 #[error("invalid amount")]
657 InvalidAmount(#[from] amount::Error),
658}
659
660pub fn halving_divisor(height: Height, network: &Network) -> Option<u64> {
668 1u64.checked_shl(num_halvings(height, network))
670}
671
672pub fn num_halvings(height: Height, network: &Network) -> u32 {
678 let slow_start_shift = network.slow_start_shift();
679 let blossom_height = NetworkUpgrade::Blossom
680 .activation_height(network)
681 .expect("blossom activation height should be available");
682
683 let halving_index = if height < slow_start_shift {
684 0
685 } else if height < blossom_height {
686 let pre_blossom_height = height - slow_start_shift;
687 pre_blossom_height / network.pre_blossom_halving_interval()
688 } else {
689 let pre_blossom_height = blossom_height - slow_start_shift;
690 let scaled_pre_blossom_height =
691 pre_blossom_height * HeightDiff::from(BLOSSOM_POW_TARGET_SPACING_RATIO);
692
693 let post_blossom_height = height - blossom_height;
694
695 (scaled_pre_blossom_height + post_blossom_height) / network.post_blossom_halving_interval()
696 };
697
698 halving_index
699 .try_into()
700 .expect("already checked for negatives")
701}
702
703pub fn block_subsidy(
707 height: Height,
708 network: &Network,
709) -> Result<Amount<NonNegative>, SubsidyError> {
710 let blossom_height = NetworkUpgrade::Blossom
711 .activation_height(network)
712 .expect("blossom activation height should be available");
713
714 let Some(halving_div) = halving_divisor(height, network) else {
719 return Ok(Amount::zero());
720 };
721
722 if height < network.slow_start_interval() {
725 Err(SubsidyError::UnsupportedHeight)
726 } else if height < blossom_height {
727 Ok(Amount::try_from(MAX_BLOCK_SUBSIDY / halving_div)?)
729 } else {
730 let scaled_max_block_subsidy =
731 MAX_BLOCK_SUBSIDY / u64::from(BLOSSOM_POW_TARGET_SPACING_RATIO);
732 Ok(Amount::try_from(scaled_max_block_subsidy / halving_div)?)
736 }
737}
738
739pub fn miner_subsidy(
743 height: Height,
744 network: &Network,
745 expected_block_subsidy: Amount<NonNegative>,
746) -> Result<Amount<NonNegative>, amount::Error> {
747 let total_funding_stream_amount: Result<Amount<NonNegative>, _> =
748 funding_stream_values(height, network, expected_block_subsidy)?
749 .values()
750 .sum();
751
752 expected_block_subsidy - total_funding_stream_amount?
753}
754
755pub fn output_amounts(transaction: &Transaction) -> HashSet<Amount<NonNegative>> {
757 transaction
758 .outputs()
759 .iter()
760 .map(|o| &o.value)
761 .cloned()
762 .collect()
763}