Skip to main content

espresso_types/v0/impls/
reward.rs

1use std::{borrow::Borrow, collections::HashSet, iter::once, str::FromStr, sync::Arc};
2
3use alloy::primitives::{
4    Address, B256, U256,
5    utils::{ParseUnits, parse_units},
6};
7use anyhow::{Context, bail, ensure};
8use ark_serialize::{
9    CanonicalDeserialize, CanonicalSerialize, Compress, Read, SerializationError, Valid, Validate,
10};
11use espresso_utils::{
12    impl_serde_from_string_or_integer, impl_to_fixed_bytes, ser::FromStringOrInteger,
13};
14use hotshot_contract_adapter::reward::RewardProofSiblings;
15use hotshot_types::{
16    data::{EpochNumber, ViewNumber},
17    epoch_membership::EpochMembershipCoordinator,
18    signature_key::BLSPubKey,
19    traits::election::{Membership, MembershipSnapshot},
20    utils::epoch_from_block_number,
21};
22use jf_merkle_tree_compat::{
23    ForgetableMerkleTreeScheme, ForgetableUniversalMerkleTreeScheme, LookupResult,
24    MerkleTreeScheme, ToTraversalPath, UniversalMerkleTreeScheme, prelude::MerkleNode,
25};
26use num_traits::CheckedSub;
27use tokio::task::JoinHandle;
28use vbs::version::Version;
29use versions::{DRB_AND_HEADER_UPGRADE_VERSION, EPOCH_REWARD_VERSION, EPOCH_VERSION};
30
31use super::{
32    Leaf2, NodeState, ValidatedState,
33    v0_3::{AuthenticatedValidator, COMMISSION_BASIS_POINTS, RewardAmount},
34    v0_4::{
35        RewardAccountProofV2, RewardAccountQueryDataV2, RewardAccountV2, RewardMerkleCommitmentV2,
36        RewardMerkleProofV2, RewardMerkleTreeV2, forgotten_accounts_include,
37    },
38};
39use crate::{
40    EpochSnapshot, FeeAccount, SeqTypes,
41    eth_signature_key::EthKeyPair,
42    v0_3::{
43        RewardAccountProofV1, RewardAccountV1, RewardMerkleCommitmentV1, RewardMerkleProofV1,
44        RewardMerkleTreeV1,
45    },
46    v0_4::{Delta, REWARD_MERKLE_TREE_V2_ARITY, REWARD_MERKLE_TREE_V2_HEIGHT},
47    v0_5::LeaderCounts,
48};
49
50impl_serde_from_string_or_integer!(RewardAmount);
51impl_to_fixed_bytes!(RewardAmount, U256);
52
53impl From<u64> for RewardAmount {
54    fn from(amt: u64) -> Self {
55        Self(U256::from(amt))
56    }
57}
58
59impl CheckedSub for RewardAmount {
60    fn checked_sub(&self, v: &Self) -> Option<Self> {
61        self.0.checked_sub(v.0).map(RewardAmount)
62    }
63}
64
65impl FromStr for RewardAmount {
66    type Err = <U256 as FromStr>::Err;
67
68    fn from_str(s: &str) -> Result<Self, Self::Err> {
69        Ok(Self(s.parse()?))
70    }
71}
72
73impl FromStringOrInteger for RewardAmount {
74    type Binary = U256;
75    type Integer = u64;
76
77    fn from_binary(b: Self::Binary) -> anyhow::Result<Self> {
78        Ok(Self(b))
79    }
80
81    fn from_integer(i: Self::Integer) -> anyhow::Result<Self> {
82        Ok(i.into())
83    }
84
85    fn from_string(s: String) -> anyhow::Result<Self> {
86        // For backwards compatibility, we have an ad hoc parser for WEI amounts
87        // represented as hex strings.
88        if let Some(s) = s.strip_prefix("0x") {
89            return Ok(Self(s.parse()?));
90        }
91
92        // Strip an optional non-numeric suffix, which will be interpreted as a unit.
93        let (base, unit) = s
94            .split_once(char::is_whitespace)
95            .unwrap_or((s.as_str(), "wei"));
96        match parse_units(base, unit)? {
97            ParseUnits::U256(n) => Ok(Self(n)),
98            ParseUnits::I256(_) => bail!("amount cannot be negative"),
99        }
100    }
101
102    fn to_binary(&self) -> anyhow::Result<Self::Binary> {
103        Ok(self.0)
104    }
105
106    fn to_string(&self) -> anyhow::Result<String> {
107        Ok(format!("{self}"))
108    }
109}
110
111impl RewardAmount {
112    pub fn as_u64(&self) -> Option<u64> {
113        if self.0 <= U256::from(u64::MAX) {
114            Some(self.0.to::<u64>())
115        } else {
116            None
117        }
118    }
119}
120
121impl From<[u8; 20]> for RewardAccountV1 {
122    fn from(bytes: [u8; 20]) -> Self {
123        Self(Address::from(bytes))
124    }
125}
126
127impl AsRef<[u8]> for RewardAccountV1 {
128    fn as_ref(&self) -> &[u8] {
129        self.0.as_slice()
130    }
131}
132
133impl<const ARITY: usize> ToTraversalPath<ARITY> for RewardAccountV1 {
134    fn to_traversal_path(&self, height: usize) -> Vec<usize> {
135        self.0
136            .as_slice()
137            .iter()
138            .take(height)
139            .map(|i| *i as usize)
140            .collect()
141    }
142}
143
144impl RewardAccountV2 {
145    /// Return inner `Address`
146    pub fn address(&self) -> Address {
147        self.0
148    }
149    /// Return byte slice representation of inner `Address` type
150    pub fn as_bytes(&self) -> &[u8] {
151        self.0.as_slice()
152    }
153    /// Return array containing underlying bytes of inner `Address` type
154    pub fn to_fixed_bytes(self) -> [u8; 20] {
155        self.0.into_array()
156    }
157    pub fn test_key_pair() -> EthKeyPair {
158        EthKeyPair::from_mnemonic(
159            "test test test test test test test test test test test junk",
160            0u32,
161        )
162        .unwrap()
163    }
164}
165
166impl RewardAccountV1 {
167    /// Return inner `Address`
168    pub fn address(&self) -> Address {
169        self.0
170    }
171    /// Return byte slice representation of inner `Address` type
172    pub fn as_bytes(&self) -> &[u8] {
173        self.0.as_slice()
174    }
175    /// Return array containing underlying bytes of inner `Address` type
176    pub fn to_fixed_bytes(self) -> [u8; 20] {
177        self.0.into_array()
178    }
179    pub fn test_key_pair() -> EthKeyPair {
180        EthKeyPair::from_mnemonic(
181            "test test test test test test test test test test test junk",
182            0u32,
183        )
184        .unwrap()
185    }
186}
187
188impl FromStr for RewardAccountV2 {
189    type Err = anyhow::Error;
190
191    fn from_str(s: &str) -> Result<Self, Self::Err> {
192        Ok(Self(s.parse()?))
193    }
194}
195
196impl FromStr for RewardAccountV1 {
197    type Err = anyhow::Error;
198
199    fn from_str(s: &str) -> Result<Self, Self::Err> {
200        Ok(Self(s.parse()?))
201    }
202}
203
204impl Valid for RewardAmount {
205    fn check(&self) -> Result<(), SerializationError> {
206        Ok(())
207    }
208}
209
210impl Valid for RewardAccountV2 {
211    fn check(&self) -> Result<(), SerializationError> {
212        Ok(())
213    }
214}
215
216impl Valid for RewardAccountV1 {
217    fn check(&self) -> Result<(), SerializationError> {
218        Ok(())
219    }
220}
221
222impl CanonicalSerialize for RewardAmount {
223    fn serialize_with_mode<W: std::io::prelude::Write>(
224        &self,
225        mut writer: W,
226        _compress: Compress,
227    ) -> Result<(), SerializationError> {
228        Ok(writer.write_all(&self.to_fixed_bytes())?)
229    }
230
231    fn serialized_size(&self, _compress: Compress) -> usize {
232        core::mem::size_of::<U256>()
233    }
234}
235impl CanonicalDeserialize for RewardAmount {
236    fn deserialize_with_mode<R: Read>(
237        mut reader: R,
238        _compress: Compress,
239        _validate: Validate,
240    ) -> Result<Self, SerializationError> {
241        let mut bytes = [0u8; core::mem::size_of::<U256>()];
242        reader.read_exact(&mut bytes)?;
243        let value = U256::from_le_slice(&bytes);
244        Ok(Self(value))
245    }
246}
247
248impl CanonicalSerialize for RewardAccountV2 {
249    fn serialize_with_mode<W: std::io::prelude::Write>(
250        &self,
251        mut writer: W,
252        _compress: Compress,
253    ) -> Result<(), SerializationError> {
254        Ok(writer.write_all(self.0.as_slice())?)
255    }
256
257    fn serialized_size(&self, _compress: Compress) -> usize {
258        core::mem::size_of::<Address>()
259    }
260}
261impl CanonicalDeserialize for RewardAccountV2 {
262    fn deserialize_with_mode<R: Read>(
263        mut reader: R,
264        _compress: Compress,
265        _validate: Validate,
266    ) -> Result<Self, SerializationError> {
267        let mut bytes = [0u8; core::mem::size_of::<Address>()];
268        reader.read_exact(&mut bytes)?;
269        let value = Address::from_slice(&bytes);
270        Ok(Self(value))
271    }
272}
273
274impl CanonicalSerialize for RewardAccountV1 {
275    fn serialize_with_mode<W: std::io::prelude::Write>(
276        &self,
277        mut writer: W,
278        _compress: Compress,
279    ) -> Result<(), SerializationError> {
280        Ok(writer.write_all(self.0.as_slice())?)
281    }
282
283    fn serialized_size(&self, _compress: Compress) -> usize {
284        core::mem::size_of::<Address>()
285    }
286}
287impl CanonicalDeserialize for RewardAccountV1 {
288    fn deserialize_with_mode<R: Read>(
289        mut reader: R,
290        _compress: Compress,
291        _validate: Validate,
292    ) -> Result<Self, SerializationError> {
293        let mut bytes = [0u8; core::mem::size_of::<Address>()];
294        reader.read_exact(&mut bytes)?;
295        let value = Address::from_slice(&bytes);
296        Ok(Self(value))
297    }
298}
299
300impl From<[u8; 20]> for RewardAccountV2 {
301    fn from(bytes: [u8; 20]) -> Self {
302        Self(Address::from(bytes))
303    }
304}
305
306impl AsRef<[u8]> for RewardAccountV2 {
307    fn as_ref(&self) -> &[u8] {
308        self.0.as_slice()
309    }
310}
311
312impl<const ARITY: usize> ToTraversalPath<ARITY> for RewardAccountV2 {
313    fn to_traversal_path(&self, height: usize) -> Vec<usize> {
314        let mut result = vec![0; height];
315
316        // Convert 20-byte address to U256
317        let mut value = U256::from_be_slice(self.0.as_slice());
318
319        // Extract digits using modulo and division (LSB first)
320        for item in result.iter_mut().take(height) {
321            let digit = (value % U256::from(ARITY)).to::<usize>();
322            *item = digit;
323            value /= U256::from(ARITY);
324        }
325
326        result
327    }
328}
329
330impl RewardAccountProofV2 {
331    pub fn presence(
332        pos: FeeAccount,
333        proof: <RewardMerkleTreeV2 as MerkleTreeScheme>::MembershipProof,
334    ) -> Self {
335        Self {
336            account: pos.into(),
337            proof: RewardMerkleProofV2::Presence(proof),
338        }
339    }
340
341    pub fn absence(
342        pos: RewardAccountV2,
343        proof: <RewardMerkleTreeV2 as UniversalMerkleTreeScheme>::NonMembershipProof,
344    ) -> Self {
345        Self {
346            account: pos.into(),
347            proof: RewardMerkleProofV2::Absence(proof),
348        }
349    }
350
351    pub fn prove(tree: &RewardMerkleTreeV2, account: Address) -> Option<(Self, U256)> {
352        match tree.universal_lookup(RewardAccountV2(account)) {
353            LookupResult::Ok(balance, proof) => Some((
354                Self {
355                    account,
356                    proof: RewardMerkleProofV2::Presence(proof),
357                },
358                balance.0,
359            )),
360            LookupResult::NotFound(proof) => Some((
361                Self {
362                    account,
363                    proof: RewardMerkleProofV2::Absence(proof),
364                },
365                U256::ZERO,
366            )),
367            LookupResult::NotInMemory => None,
368        }
369    }
370
371    pub fn verify(&self, comm: &RewardMerkleCommitmentV2) -> anyhow::Result<U256> {
372        match &self.proof {
373            RewardMerkleProofV2::Presence(proof) => {
374                ensure!(
375                    RewardMerkleTreeV2::verify(comm, RewardAccountV2(self.account), proof)?.is_ok(),
376                    "invalid proof"
377                );
378                Ok(proof
379                    .elem()
380                    .context("presence proof is missing account balance")?
381                    .0)
382            },
383            RewardMerkleProofV2::Absence(proof) => {
384                let tree = RewardMerkleTreeV2::from_commitment(comm);
385                ensure!(
386                    RewardMerkleTreeV2::non_membership_verify(
387                        tree.commitment(),
388                        RewardAccountV2(self.account),
389                        proof
390                    )?,
391                    "invalid proof"
392                );
393                Ok(U256::ZERO)
394            },
395        }
396    }
397
398    pub fn remember(&self, tree: &mut RewardMerkleTreeV2) -> anyhow::Result<()> {
399        match &self.proof {
400            RewardMerkleProofV2::Presence(proof) => {
401                tree.remember(
402                    RewardAccountV2(self.account),
403                    proof
404                        .elem()
405                        .context("presence proof is missing account balance")?,
406                    proof,
407                )?;
408                Ok(())
409            },
410            RewardMerkleProofV2::Absence(proof) => {
411                tree.non_membership_remember(RewardAccountV2(self.account), proof)?;
412                Ok(())
413            },
414        }
415    }
416}
417
418impl TryInto<RewardProofSiblings> for RewardAccountProofV2 {
419    type Error = anyhow::Error;
420
421    /// Generate a Solidity-compatible proof for this account
422    ///
423    /// The proof is returned without leaf value. The caller is expected to
424    /// obtain the leaf value from the jellyfish proof (Self).
425    fn try_into(self) -> anyhow::Result<RewardProofSiblings> {
426        // NOTE: rustfmt fails to format this file if the nesting is too deep.
427        let proof = if let RewardMerkleProofV2::Presence(proof) = &self.proof {
428            proof
429        } else {
430            bail!("only presence proofs supported")
431        };
432
433        let path = ToTraversalPath::<{ REWARD_MERKLE_TREE_V2_ARITY }>::to_traversal_path(
434            &RewardAccountV2(self.account),
435            REWARD_MERKLE_TREE_V2_HEIGHT,
436        );
437
438        if path.len() != REWARD_MERKLE_TREE_V2_HEIGHT {
439            bail!("Invalid proof: unexpected path length: {}", path.len());
440        };
441
442        let siblings: [B256; REWARD_MERKLE_TREE_V2_HEIGHT] = proof
443            .proof
444            .iter()
445            .enumerate()
446            .skip(1) // Skip the leaf node (first element)
447            .filter_map(|(level_idx, node)| match node {
448                MerkleNode::Branch { children, .. } => {
449                    // Use the path to determine which sibling we need
450                    let path_direction = path
451                        .get(level_idx - 1)
452                        .copied()
453                        .expect("exists");
454                    let sibling_idx = if path_direction == 0 { 1 } else { 0 };
455                    if sibling_idx >= children.len() {
456                        panic!(
457                            "Invalid proof: index={sibling_idx} length={}",
458                            children.len()
459                        );
460                    };
461
462                    match children[sibling_idx].as_ref() {
463                        MerkleNode::Empty => Some(B256::ZERO),
464                        MerkleNode::Leaf { value, .. } => {
465                            let bytes = value.as_ref();
466                            Some(B256::from_slice(bytes))
467                        }
468                        MerkleNode::Branch { value, .. } => {
469                            let bytes = value.as_ref();
470                            Some(B256::from_slice(bytes))
471                        }
472                        MerkleNode::ForgettenSubtree { value } => {
473                            let bytes = value.as_ref();
474                            Some(B256::from_slice(bytes))
475                        }
476                    }
477                }
478                _ => None,
479            })
480            .collect::<Vec<B256>>().try_into().map_err(|err: Vec<_>| {
481                panic!("Invalid proof length: {:?}, this should never happen", err.len())
482            })
483            .unwrap();
484
485        Ok(siblings.into())
486    }
487}
488
489impl RewardAccountProofV1 {
490    pub fn presence(
491        pos: FeeAccount,
492        proof: <RewardMerkleTreeV1 as MerkleTreeScheme>::MembershipProof,
493    ) -> Self {
494        Self {
495            account: pos.into(),
496            proof: RewardMerkleProofV1::Presence(proof),
497        }
498    }
499
500    pub fn absence(
501        pos: RewardAccountV1,
502        proof: <RewardMerkleTreeV1 as UniversalMerkleTreeScheme>::NonMembershipProof,
503    ) -> Self {
504        Self {
505            account: pos.into(),
506            proof: RewardMerkleProofV1::Absence(proof),
507        }
508    }
509
510    pub fn prove(tree: &RewardMerkleTreeV1, account: Address) -> Option<(Self, U256)> {
511        match tree.universal_lookup(RewardAccountV1(account)) {
512            LookupResult::Ok(balance, proof) => Some((
513                Self {
514                    account,
515                    proof: RewardMerkleProofV1::Presence(proof),
516                },
517                balance.0,
518            )),
519            LookupResult::NotFound(proof) => Some((
520                Self {
521                    account,
522                    proof: RewardMerkleProofV1::Absence(proof),
523                },
524                U256::ZERO,
525            )),
526            LookupResult::NotInMemory => None,
527        }
528    }
529
530    pub fn verify(&self, comm: &RewardMerkleCommitmentV1) -> anyhow::Result<U256> {
531        match &self.proof {
532            RewardMerkleProofV1::Presence(proof) => {
533                ensure!(
534                    RewardMerkleTreeV1::verify(comm, RewardAccountV1(self.account), proof)?.is_ok(),
535                    "invalid proof"
536                );
537                Ok(proof
538                    .elem()
539                    .context("presence proof is missing account balance")?
540                    .0)
541            },
542            RewardMerkleProofV1::Absence(proof) => {
543                let tree = RewardMerkleTreeV1::from_commitment(comm);
544                ensure!(
545                    RewardMerkleTreeV1::non_membership_verify(
546                        tree.commitment(),
547                        RewardAccountV1(self.account),
548                        proof
549                    )?,
550                    "invalid proof"
551                );
552                Ok(U256::ZERO)
553            },
554        }
555    }
556
557    pub fn remember(&self, tree: &mut RewardMerkleTreeV1) -> anyhow::Result<()> {
558        match &self.proof {
559            RewardMerkleProofV1::Presence(proof) => {
560                tree.remember(
561                    RewardAccountV1(self.account),
562                    proof
563                        .elem()
564                        .context("presence proof is missing account balance")?,
565                    proof,
566                )?;
567                Ok(())
568            },
569            RewardMerkleProofV1::Absence(proof) => {
570                tree.non_membership_remember(RewardAccountV1(self.account), proof)?;
571                Ok(())
572            },
573        }
574    }
575}
576
577impl From<(RewardAccountProofV2, U256)> for RewardAccountQueryDataV2 {
578    fn from((proof, balance): (RewardAccountProofV2, U256)) -> Self {
579        Self { balance, proof }
580    }
581}
582
583#[derive(Clone, Debug)]
584pub struct ComputedRewards {
585    leader_address: Address,
586    // leader commission reward
587    leader_commission: RewardAmount,
588    // delegator rewards
589    delegators: Vec<(Address, RewardAmount)>,
590}
591
592impl ComputedRewards {
593    pub fn new(
594        delegators: Vec<(Address, RewardAmount)>,
595        leader_address: Address,
596        leader_commission: RewardAmount,
597    ) -> Self {
598        Self {
599            delegators,
600            leader_address,
601            leader_commission,
602        }
603    }
604
605    pub fn leader_commission(&self) -> &RewardAmount {
606        &self.leader_commission
607    }
608
609    pub fn delegators(&self) -> &Vec<(Address, RewardAmount)> {
610        &self.delegators
611    }
612
613    // chains delegation rewards and leader commission reward
614    pub fn all_rewards(self) -> Vec<(Address, RewardAmount)> {
615        self.delegators
616            .into_iter()
617            .chain(once((self.leader_address, self.leader_commission)))
618            .collect()
619    }
620}
621
622/// Maps each validator to its leader count for an epoch.
623pub struct ValidatorLeaderCounts(Vec<(AuthenticatedValidator<BLSPubKey>, u16)>);
624
625impl ValidatorLeaderCounts {
626    /// Construct from a sorted validator list and the corresponding leader counts array.
627    ///
628    /// Reads the stake table and validator records from a single
629    /// `EpochSnapshot`, so all entries come from one consistent
630    /// per-epoch view. Validators whose leader count is zero are
631    /// included so that indexing remains consistent.
632    pub fn new(snapshot: &EpochSnapshot, leader_counts: LeaderCounts) -> anyhow::Result<Self> {
633        let entries: Vec<_> = snapshot
634            .stake_table()
635            .zip(leader_counts.iter().copied())
636            .map(|(entry, count)| {
637                let validator = snapshot.validator_config(&entry.stake_table_entry.stake_key)?;
638                Ok((validator.clone(), count))
639            })
640            .collect::<anyhow::Result<_>>()?;
641
642        Ok(Self(entries))
643    }
644
645    /// Iterate over all validators that have count greater than 0
646    pub fn active_leaders(
647        &self,
648    ) -> impl Iterator<Item = (&AuthenticatedValidator<BLSPubKey>, u16)> {
649        self.0
650            .iter()
651            .filter(|(_, count)| *count > 0)
652            .map(|(v, count)| (v, *count))
653    }
654
655    /// All reward accounts (validator + delegators) for validators that led blocks.
656    fn all_reward_accounts(&self) -> Vec<RewardAccountV2> {
657        self.active_leaders()
658            .flat_map(|(v, _)| {
659                std::iter::once(RewardAccountV2(v.account))
660                    .chain(v.delegators.keys().map(|d| RewardAccountV2(*d)))
661            })
662            .collect()
663    }
664}
665
666pub struct RewardDistributor {
667    validator: AuthenticatedValidator<BLSPubKey>,
668    block_reward: RewardAmount,
669    total_distributed: RewardAmount,
670}
671
672impl RewardDistributor {
673    pub fn new(
674        validator: AuthenticatedValidator<BLSPubKey>,
675        block_reward: RewardAmount,
676        total_distributed: RewardAmount,
677    ) -> Self {
678        Self {
679            validator,
680            block_reward,
681            total_distributed,
682        }
683    }
684
685    pub fn validator(&self) -> AuthenticatedValidator<BLSPubKey> {
686        self.validator.clone()
687    }
688
689    pub fn block_reward(&self) -> RewardAmount {
690        self.block_reward
691    }
692
693    pub fn total_distributed(&self) -> RewardAmount {
694        self.total_distributed
695    }
696
697    pub fn update_rewards_delta(&self, delta: &mut Delta) -> anyhow::Result<()> {
698        // Update delta rewards
699        delta
700            .rewards_delta
701            .insert(RewardAccountV2(self.validator().account));
702        delta.rewards_delta.extend(
703            self.validator()
704                .delegators
705                .keys()
706                .map(|d| RewardAccountV2(*d)),
707        );
708
709        Ok(())
710    }
711
712    pub fn update_reward_balance<P>(
713        tree: &mut P,
714        account: &P::Index,
715        amount: P::Element,
716    ) -> anyhow::Result<()>
717    where
718        P: UniversalMerkleTreeScheme<Element = RewardAmount>,
719        P::Index: Borrow<<P as MerkleTreeScheme>::Index> + std::fmt::Display,
720    {
721        let mut err = None;
722        tree.update_with(account.clone(), |balance| {
723            let balance = balance.copied();
724            match balance.unwrap_or_default().0.checked_add(amount.0) {
725                Some(updated) => Some(updated.into()),
726                None => {
727                    err = Some(format!("overflowed reward balance for account {account}"));
728                    balance
729                },
730            }
731        })?;
732
733        if let Some(error) = err {
734            tracing::warn!(error);
735            bail!(error)
736        }
737
738        Ok(())
739    }
740
741    pub fn apply_rewards(
742        &mut self,
743        version: Version,
744        state: &mut ValidatedState,
745    ) -> anyhow::Result<()> {
746        let computed_rewards = self.compute_rewards()?;
747
748        if version <= EPOCH_VERSION {
749            for (address, reward) in computed_rewards.all_rewards() {
750                Self::update_reward_balance(
751                    &mut state.reward_merkle_tree_v1,
752                    &RewardAccountV1(address),
753                    reward,
754                )?;
755                tracing::debug!(%address, %reward, "applied v1 rewards");
756            }
757        } else {
758            for (address, reward) in computed_rewards.all_rewards() {
759                Self::update_reward_balance(
760                    &mut state.reward_merkle_tree_v2,
761                    &RewardAccountV2(address),
762                    reward,
763                )?;
764                tracing::debug!(%address, %reward, "applied v2 rewards");
765            }
766        }
767
768        self.total_distributed += self.block_reward();
769
770        Ok(())
771    }
772
773    /// Computes the reward in a block for the validator and its delegators
774    /// based on the commission rate, individual delegator stake, and total block reward.
775    ///
776    /// The block reward is distributed among the delegators first based on their stake,
777    /// with the remaining amount from the block reward given to the validator as the commission.
778    /// Any minor discrepancies due to rounding off errors are adjusted in the leader reward
779    /// to ensure the total reward is exactly equal to block reward.
780    pub fn compute_rewards(&self) -> anyhow::Result<ComputedRewards> {
781        ensure!(
782            self.validator.commission <= COMMISSION_BASIS_POINTS,
783            "commission must not exceed {COMMISSION_BASIS_POINTS}"
784        );
785
786        let mut rewards = Vec::new();
787
788        let total_reward = self.block_reward.0;
789        let delegators_ratio_basis_points = U256::from(COMMISSION_BASIS_POINTS)
790            .checked_sub(U256::from(self.validator.commission))
791            .context("overflow")?;
792        let delegators_reward = delegators_ratio_basis_points
793            .checked_mul(total_reward)
794            .context("overflow")?;
795
796        // Distribute delegator rewards
797        let total_stake = self.validator.stake;
798        let mut delegators_total_reward_distributed = U256::from(0);
799        for (delegator_address, delegator_stake) in &self.validator.delegators {
800            let delegator_reward = RewardAmount::from(
801                (delegator_stake
802                    .checked_mul(delegators_reward)
803                    .context("overflow")?
804                    .checked_div(total_stake)
805                    .context("overflow")?)
806                .checked_div(U256::from(COMMISSION_BASIS_POINTS))
807                .context("overflow")?,
808            );
809
810            delegators_total_reward_distributed += delegator_reward.0;
811
812            rewards.push((*delegator_address, delegator_reward));
813        }
814
815        let leader_commission = total_reward
816            .checked_sub(delegators_total_reward_distributed)
817            .context("overflow")?;
818
819        Ok(ComputedRewards::new(
820            rewards,
821            self.validator.account,
822            leader_commission.into(),
823        ))
824    }
825}
826
827/// Distributes the block reward for a given block height
828///
829/// Rewards are only distributed if the block belongs to an epoch beyond the second epoch.
830///
831/// The function also calculates the appropriate reward (fixed or dynamic) based
832/// on the protocol version.
833pub async fn distribute_block_reward(
834    instance_state: &NodeState,
835    validated_state: &mut ValidatedState,
836    parent_leaf: &Leaf2,
837    view_number: ViewNumber,
838    version: Version,
839) -> anyhow::Result<Option<RewardDistributor>> {
840    let height = parent_leaf.height() + 1;
841
842    let epoch_height = instance_state
843        .epoch_height
844        .context("epoch height not found")?;
845    let epoch = EpochNumber::new(epoch_from_block_number(height, epoch_height));
846    let coordinator = instance_state.coordinator.clone();
847    let first_epoch = {
848        coordinator
849            .membership()
850            .first_epoch()
851            .context("The first epoch was not set.")?
852    };
853
854    // Rewards are distributed only if the current epoch is not the first or second epoch
855    // this is because we don't have stake table from the contract for the first two epochs
856    if epoch <= first_epoch + 1 {
857        return Ok(None);
858    }
859
860    // Determine who the block leader is for this view and ensure missing block
861    // rewards are fetched from peers if needed.
862
863    let leader = get_leader_and_fetch_missing_rewards(
864        instance_state,
865        validated_state,
866        parent_leaf,
867        view_number,
868    )
869    .await?;
870
871    let parent_header = parent_leaf.block_header();
872
873    // Initialize the total rewards distributed so far in this block.
874    let mut previously_distributed = parent_header.total_reward_distributed().unwrap_or_default();
875
876    // Decide whether to use a fixed or dynamic block reward.
877    let block_reward = if version == DRB_AND_HEADER_UPGRADE_VERSION {
878        instance_state
879            .block_reward(EpochNumber::new(*epoch))
880            .await
881            .with_context(|| format!("block reward is None for epoch {epoch}"))?
882    } else {
883        instance_state.fixed_block_reward().await?
884    };
885
886    // If we are in the DRB + header upgrade
887    // and the parent block is from V3 (which does not have a previously distributed reward field),
888    // we need to recompute the previously distributed rewards
889    // using the fixed block reward and the number of blocks in which fixed reward was distributed
890    if version == DRB_AND_HEADER_UPGRADE_VERSION && parent_header.version() == EPOCH_VERSION {
891        ensure!(
892            instance_state.epoch_start_block != 0,
893            "epoch_start_block is zero"
894        );
895
896        let fixed_block_reward = instance_state.fixed_block_reward().await?;
897
898        // Compute the first block where rewards start being distributed.
899        // Rewards begin only after the first two epochs
900        // Example:
901        //   epoch_height = 10, first_epoch = 1
902        // first_reward_block = 21
903        let first_reward_block = (*first_epoch + 1) * epoch_height + 1;
904        // We only compute fixed reward distribured so far
905        // once the current block
906        // is beyond the first rewardable block.
907        if height > first_reward_block {
908            // If v4 upgrade started at block 101, and first_reward_block is 21:
909            // total_distributed = (101 - 21) * fixed_block_reward
910            let blocks = height.checked_sub(first_reward_block).with_context(|| {
911                format!("height ({height}) - first_reward_block ({first_reward_block}) underflowed")
912            })?;
913            previously_distributed = U256::from(blocks)
914                .checked_mul(fixed_block_reward.0)
915                .with_context(|| {
916                    format!(
917                        "overflow during total_distributed calculation: blocks={blocks}, \
918                         fixed_block_reward={}",
919                        fixed_block_reward.0
920                    )
921                })?
922                .into();
923        }
924    }
925
926    if block_reward.0.is_zero() {
927        tracing::info!("block reward is zero. height={height}. epoch={epoch}");
928        return Ok(None);
929    }
930
931    let mut reward_distributor =
932        RewardDistributor::new(leader, block_reward, previously_distributed);
933
934    reward_distributor.apply_rewards(version, validated_state)?;
935
936    Ok(Some(reward_distributor))
937}
938
939pub async fn get_leader_and_fetch_missing_rewards(
940    instance_state: &NodeState,
941    validated_state: &mut ValidatedState,
942    parent_leaf: &Leaf2,
943    view: ViewNumber,
944) -> anyhow::Result<AuthenticatedValidator<BLSPubKey>> {
945    let parent_height = parent_leaf.height();
946    let parent_view = parent_leaf.view_number();
947    let new_height = parent_height + 1;
948
949    let epoch_height = instance_state
950        .epoch_height
951        .context("epoch height not found")?;
952    if epoch_height == 0 {
953        bail!("epoch height is 0. can not catchup reward accounts");
954    }
955    let epoch = EpochNumber::new(epoch_from_block_number(new_height, epoch_height));
956
957    let coordinator = instance_state.coordinator.clone();
958
959    let membership = coordinator.membership_for_epoch(Some(epoch))?;
960
961    let snapshot = membership
962        .snapshot()
963        .context(format!("no committee for epoch {epoch:?}"))?;
964
965    let leader: BLSPubKey = snapshot
966        .leader(view)
967        .context(format!("leader for epoch {epoch:?} not found"))?;
968
969    tracing::debug!("Selected leader: {leader} for view {view} and epoch {epoch}");
970
971    let validator = snapshot
972        .validator_config(&leader)
973        .context("validator not found")?;
974
975    let parent_header = parent_leaf.block_header();
976
977    if parent_header.version() <= EPOCH_VERSION {
978        let mut reward_accounts = HashSet::new();
979        reward_accounts.insert(validator.account.into());
980        let delegators = validator
981            .delegators
982            .keys()
983            .cloned()
984            .map(|a| a.into())
985            .collect::<Vec<RewardAccountV2>>();
986
987        reward_accounts.extend(delegators.clone());
988
989        let accts: HashSet<_> = reward_accounts
990            .into_iter()
991            .map(RewardAccountV1::from)
992            .collect();
993        let missing_reward_accts = validated_state.forgotten_reward_accounts_v1(accts);
994
995        if !missing_reward_accts.is_empty() {
996            tracing::warn!(
997                parent_height,
998                ?parent_view,
999                ?missing_reward_accts,
1000                "fetching missing v1 reward accounts from peers"
1001            );
1002
1003            let missing_account_proofs = instance_state
1004                .state_catchup
1005                .fetch_reward_accounts_v1(
1006                    instance_state,
1007                    parent_height,
1008                    parent_view,
1009                    validated_state.reward_merkle_tree_v1.commitment(),
1010                    missing_reward_accts,
1011                )
1012                .await?;
1013
1014            for proof in missing_account_proofs.iter() {
1015                proof
1016                    .remember(&mut validated_state.reward_merkle_tree_v1)
1017                    .expect("proof previously verified");
1018            }
1019        }
1020    } else {
1021        let reward_accounts = Arc::new(
1022            std::iter::once(validator.account.into())
1023                .chain(validator.delegators.keys().cloned().map(Into::into))
1024                .collect::<Vec<_>>(),
1025        );
1026
1027        let reward_merkle_tree_root = validated_state.reward_merkle_tree_v2.commitment();
1028        if forgotten_accounts_include(&validated_state.reward_merkle_tree_v2, &reward_accounts) {
1029            tracing::warn!(
1030                parent_height,
1031                ?parent_view,
1032                %reward_merkle_tree_root,
1033                "fetching reward merkle tree from peers"
1034            );
1035
1036            validated_state.reward_merkle_tree_v2 = instance_state
1037                .state_catchup
1038                .fetch_reward_merkle_tree_v2(
1039                    parent_height,
1040                    parent_view,
1041                    reward_merkle_tree_root,
1042                    reward_accounts,
1043                )
1044                .await?
1045                .tree;
1046
1047            tracing::warn!(
1048                parent_height,
1049                ?parent_view,
1050                %reward_merkle_tree_root,
1051                "successfully fetched reward merkle tree from peers"
1052            );
1053        }
1054    }
1055
1056    Ok(validator.clone())
1057}
1058
1059/// Result of epoch rewards calculation.
1060#[derive(Debug, Clone)]
1061pub struct EpochRewardsResult {
1062    /// The epoch this calculation was for.
1063    pub epoch: EpochNumber,
1064    /// The updated reward merkle tree with all rewards applied.
1065    pub reward_tree: RewardMerkleTreeV2,
1066    /// Total amount of rewards distributed in this epoch.
1067    pub total_distributed: RewardAmount,
1068    /// Set of all reward accounts that were modified.
1069    pub changed_accounts: HashSet<RewardAccountV2>,
1070}
1071
1072/// Manages a single background epoch reward calculation.
1073/// Each consumer (consensus and state loop) should have its own instance.
1074#[derive(Debug, Default)]
1075pub struct EpochRewardsCalculator {
1076    /// The currently pending calculation, if any.
1077    pending: Option<(EpochNumber, JoinHandle<anyhow::Result<EpochRewardsResult>>)>,
1078}
1079
1080impl EpochRewardsCalculator {
1081    pub fn new() -> Self {
1082        Self { pending: None }
1083    }
1084
1085    /// Check if calculation is in progress for epoch.
1086    pub fn is_calculating(&self, epoch: EpochNumber) -> bool {
1087        self.pending.as_ref().is_some_and(|(e, _)| *e == epoch)
1088    }
1089
1090    /// Retrieve the completed reward calculation for `epoch`.
1091    ///
1092    /// Returns `None` when there is no pending task for the requested epoch
1093    /// Otherwise awaits the task and returns `Some(Ok(result))` on success or
1094    /// `Some(Err(..))` if it failed or panicked
1095    pub async fn get_result(
1096        &mut self,
1097        epoch: EpochNumber,
1098    ) -> Option<anyhow::Result<EpochRewardsResult>> {
1099        let (pending_epoch, handle) = self.pending.take()?;
1100        if pending_epoch != epoch {
1101            // Not the epoch we're looking for — put the task back.
1102            self.pending = Some((pending_epoch, handle));
1103            return None;
1104        }
1105
1106        let result = match handle.await {
1107            Ok(Ok(result)) => {
1108                tracing::info!(%epoch, total = %result.total_distributed.0, "epoch rewards calculation completed");
1109                Ok(result)
1110            },
1111            Ok(Err(e)) => {
1112                tracing::error!(%epoch, error = %e, "epoch rewards calculation failed");
1113                Err(e)
1114            },
1115            Err(e) => {
1116                tracing::error!(%epoch, error = %e, "epoch rewards task panicked");
1117                Err(anyhow::Error::new(e).context("epoch rewards task panicked"))
1118            },
1119        };
1120        Some(result)
1121    }
1122
1123    /// Start a background task that calculates epoch rewards.
1124    /// Does nothing if calculation is already in progress for this epoch.
1125    /// Aborts any stale pending task since only one epoch matters at a time.
1126    pub fn spawn_background_task(
1127        &mut self,
1128        epoch: EpochNumber,
1129        epoch_height: u64,
1130        reward_tree: RewardMerkleTreeV2,
1131        instance_state: NodeState,
1132        coordinator: EpochMembershipCoordinator<SeqTypes>,
1133        leader_counts: Option<LeaderCounts>,
1134    ) {
1135        if self.is_calculating(epoch) {
1136            tracing::debug!(%epoch, "calculation already in progress, skipping");
1137            return;
1138        }
1139
1140        // Abort any stale pending task.
1141        if let Some((stale_epoch, handle)) = self.pending.take() {
1142            tracing::info!(%stale_epoch, %epoch, "aborting stale epoch rewards task");
1143            handle.abort();
1144        }
1145
1146        tracing::info!(
1147            %epoch,
1148            has_leader_counts = leader_counts.is_some(),
1149            "starting background epoch rewards task"
1150        );
1151
1152        let handle = tokio::spawn(async move {
1153            Self::fetch_and_calculate(
1154                epoch,
1155                epoch_height,
1156                reward_tree,
1157                instance_state,
1158                coordinator,
1159                leader_counts,
1160            )
1161            .await
1162        });
1163        self.pending = Some((epoch, handle));
1164    }
1165
1166    async fn fetch_and_calculate(
1167        epoch: EpochNumber,
1168        epoch_height: u64,
1169        mut reward_tree: RewardMerkleTreeV2,
1170        instance_state: NodeState,
1171        coordinator: EpochMembershipCoordinator<SeqTypes>,
1172        leader_counts: Option<LeaderCounts>,
1173    ) -> anyhow::Result<EpochRewardsResult> {
1174        let epoch_last_block_height = (*epoch) * epoch_height;
1175
1176        tracing::info!(
1177            %epoch,
1178            epoch_last_block_height,
1179            has_leader_counts = leader_counts.is_some(),
1180            "fetch_and_calculate: starting"
1181        );
1182
1183        // Ensure stake table is available for this epoch
1184        if let Err(err) = coordinator.membership_for_epoch(Some(epoch)) {
1185            tracing::info!(%epoch, "stake table missing for epoch, triggering catchup: {err:#}");
1186            coordinator
1187                .wait_for_catchup(epoch)
1188                .await
1189                .context(format!("failed to catch up for epoch={epoch}"))?;
1190        }
1191
1192        // Get leader_counts for this epoch if not provided
1193        let leader_counts = if let Some(lc) = leader_counts {
1194            lc
1195        } else {
1196            // Fetch the leaf at the last block of the epoch so we can verify
1197            // the header via QC against the stake table
1198            let leaf = instance_state
1199                .state_catchup
1200                .as_ref()
1201                .fetch_leaf(coordinator.clone(), epoch_last_block_height)
1202                .await
1203                .with_context(|| {
1204                    format!(
1205                        "failed to fetch leaf at height {epoch_last_block_height} for epoch \
1206                         {epoch}"
1207                    )
1208                })?;
1209            let header = leaf.block_header();
1210
1211            tracing::info!(
1212                %epoch,
1213                header_height = header.height(),
1214                header_version = %header.version(),
1215                header_reward_merkle_tree_root = %header.reward_merkle_tree_root(),
1216                "fetch_and_calculate: fetched leaf"
1217            );
1218
1219            if header.version() < EPOCH_REWARD_VERSION {
1220                tracing::info!(
1221                    %epoch,
1222                    header_version = %header.version(),
1223                    "no rewards to distribute"
1224                );
1225                return Ok(EpochRewardsResult {
1226                    epoch,
1227                    reward_tree,
1228                    total_distributed: RewardAmount::default(),
1229                    changed_accounts: HashSet::new(),
1230                });
1231            }
1232
1233            // Validate the reward merkle tree against the header commitment.
1234            // If they don't match, use an empty tree so the rebuild logic
1235            // will fetch all accounts from peers.
1236            // This should never happen though because we are passing parent reward merkle tree
1237            // which is only changed once per epoch
1238            let expected_root = header.reward_merkle_tree_root().right();
1239            let actual_root = reward_tree.commitment();
1240            if expected_root != Some(actual_root) {
1241                tracing::warn!(
1242                    %epoch,
1243                    ?expected_root,
1244                    ?actual_root,
1245                    "reward merkle tree root mismatch, using empty tree"
1246                );
1247                reward_tree = RewardMerkleTreeV2::new(REWARD_MERKLE_TREE_V2_HEIGHT);
1248            }
1249
1250            *header
1251                .leader_counts()
1252                .expect("V6+ header must have leader_counts")
1253        };
1254
1255        let snapshot = coordinator
1256            .membership()
1257            .snapshot(epoch)
1258            .with_context(|| format!("no committee for epoch={epoch}"))?;
1259        let validator_leader_counts = ValidatorLeaderCounts::new(&snapshot, leader_counts)?;
1260        let block_reward = snapshot
1261            .epoch_block_reward()
1262            .context("block reward not found for epoch")?;
1263
1264        tracing::info!(
1265            %epoch,
1266            %block_reward,
1267            "fetch_and_calculate: got block_reward"
1268        );
1269
1270        // Check if we're missing accounts that need to be in the tree
1271        let accounts_to_update = validator_leader_counts.all_reward_accounts();
1272
1273        let missing_accounts: Vec<_> = accounts_to_update
1274            .iter()
1275            .filter(|account| reward_tree.lookup(**account).expect_not_in_memory().is_ok())
1276            .cloned()
1277            .collect();
1278
1279        // If we have missing accounts, fetch the reward merkle tree from peers
1280        // with the missing accounts included
1281        if !missing_accounts.is_empty() {
1282            tracing::info!(
1283                %epoch,
1284                num_missing = missing_accounts.len(),
1285                "missing accounts detected, fetching reward merkle tree from peers"
1286            );
1287
1288            let reward_merkle_tree_root = reward_tree.commitment();
1289            reward_tree = instance_state
1290                .state_catchup
1291                .as_ref()
1292                .fetch_reward_merkle_tree_v2(
1293                    epoch_last_block_height,
1294                    ViewNumber::new(0),
1295                    reward_merkle_tree_root,
1296                    Arc::new(missing_accounts),
1297                )
1298                .await
1299                .with_context(|| {
1300                    format!(
1301                        "failed to fetch reward merkle tree at height {epoch_last_block_height} \
1302                         for epoch {epoch}"
1303                    )
1304                })?
1305                .tree;
1306
1307            tracing::info!(
1308                %epoch,
1309                reward_tree_commitment = %reward_tree.commitment(),
1310                "reward tree fetched successfully"
1311            );
1312        }
1313
1314        tracing::info!(
1315            %epoch,
1316            reward_tree_commitment = %reward_tree.commitment(),
1317            "starting final epoch calculation"
1318        );
1319
1320        Self::calculate_all_rewards(epoch, validator_leader_counts, reward_tree, block_reward).await
1321    }
1322
1323    /// Calculate all rewards for the epoch and update the reward tree.
1324    async fn calculate_all_rewards(
1325        epoch: EpochNumber,
1326        validator_leader_counts: ValidatorLeaderCounts,
1327        mut reward_tree: RewardMerkleTreeV2,
1328        block_reward: RewardAmount,
1329    ) -> anyhow::Result<EpochRewardsResult> {
1330        let mut total_distributed = U256::ZERO;
1331        let mut changed_accounts = HashSet::new();
1332
1333        for (validator, count) in validator_leader_counts.active_leaders() {
1334            // validator_reward = count * block_reward
1335            let validator_reward = block_reward
1336                .0
1337                .checked_mul(U256::from(count))
1338                .context("overflow in validator reward calculation")?;
1339
1340            if validator_reward.is_zero() {
1341                continue;
1342            }
1343
1344            changed_accounts.insert(RewardAccountV2(validator.account));
1345            changed_accounts.extend(validator.delegators.keys().map(|d| RewardAccountV2(*d)));
1346
1347            let distributor = RewardDistributor::new(
1348                validator.clone(),
1349                RewardAmount(validator_reward),
1350                Default::default(),
1351            );
1352
1353            let computed_rewards = distributor.compute_rewards()?;
1354
1355            for (address, reward) in computed_rewards.all_rewards() {
1356                RewardDistributor::update_reward_balance(
1357                    &mut reward_tree,
1358                    &RewardAccountV2(address),
1359                    reward,
1360                )?;
1361                tracing::debug!(%epoch, %address, %reward, "applied epoch reward");
1362            }
1363
1364            total_distributed += validator_reward;
1365        }
1366
1367        tracing::info!(
1368            %epoch,
1369            total_distributed = %total_distributed,
1370            num_changed_accounts = changed_accounts.len(),
1371            "epoch rewards calculation complete"
1372        );
1373
1374        Ok(EpochRewardsResult {
1375            epoch,
1376            reward_tree,
1377            total_distributed: RewardAmount(total_distributed),
1378            changed_accounts,
1379        })
1380    }
1381}
1382
1383#[cfg(test)]
1384pub mod tests {
1385
1386    use super::*;
1387
1388    fn make_distributor(commission: u16) -> RewardDistributor {
1389        RewardDistributor::new(
1390            AuthenticatedValidator::mock_with_commission(commission),
1391            RewardAmount(U256::from(1902000000000000000_u128)),
1392            U256::ZERO.into(),
1393        )
1394    }
1395
1396    fn total_rewards(rewards: ComputedRewards) -> U256 {
1397        rewards
1398            .all_rewards()
1399            .iter()
1400            .fold(U256::ZERO, |acc, (_, r)| acc + r.0)
1401    }
1402
1403    // TODO: current tests are just sanity checks, we need more.
1404
1405    #[test]
1406    fn test_reward_calculation_sanity_checks() {
1407        // This test verifies that the total rewards distributed match the block reward. Due to
1408        // rounding effects in distribution, the validator may receive a slightly higher amount
1409        // because the remainder after delegator distribution is sent to the validator.
1410
1411        let distributor = make_distributor(500);
1412        let rewards = distributor.compute_rewards().unwrap();
1413        assert_eq!(total_rewards(rewards.clone()), distributor.block_reward.0);
1414
1415        let distributor = make_distributor(0);
1416        let rewards = distributor.compute_rewards().unwrap();
1417        assert_eq!(total_rewards(rewards.clone()), distributor.block_reward.0);
1418
1419        let distributor = make_distributor(10000);
1420        let rewards = distributor.compute_rewards().unwrap();
1421        assert_eq!(total_rewards(rewards.clone()), distributor.block_reward.0);
1422        let leader_commission = rewards.leader_commission();
1423        assert_eq!(*leader_commission, distributor.block_reward);
1424
1425        let distributor = make_distributor(10001);
1426        assert!(
1427            distributor
1428                .compute_rewards()
1429                .err()
1430                .unwrap()
1431                .to_string()
1432                .contains("must not exceed")
1433        );
1434    }
1435
1436    #[test]
1437    fn test_compute_rewards_validator_commission() {
1438        let distributor = make_distributor(0);
1439        let rewards = distributor.compute_rewards().unwrap();
1440        let leader_commission = rewards.leader_commission();
1441        let percentage =
1442            leader_commission.0 * U256::from(COMMISSION_BASIS_POINTS) / distributor.block_reward.0;
1443        assert_eq!(percentage, U256::ZERO);
1444
1445        // 3%
1446        let distributor = make_distributor(300);
1447        let rewards = distributor.compute_rewards().unwrap();
1448        let leader_commission = rewards.leader_commission();
1449        let percentage =
1450            leader_commission.0 * U256::from(COMMISSION_BASIS_POINTS) / distributor.block_reward.0;
1451        println!("percentage: {percentage:?}");
1452        assert_eq!(percentage, U256::from(300));
1453
1454        //100%
1455        let distributor = make_distributor(10000);
1456        let rewards = distributor.compute_rewards().unwrap();
1457        let leader_commission = rewards.leader_commission();
1458        assert_eq!(*leader_commission, distributor.block_reward);
1459    }
1460}