Skip to main content

ckb_reward_calculator/
lib.rs

1//! This mod implemented a ckb block reward calculator
2
3use ckb_chain_spec::consensus::Consensus;
4use ckb_dao::DaoCalculator;
5use ckb_dao_utils::DaoError;
6use ckb_logger::debug;
7use ckb_store::ChainStore;
8use ckb_types::{
9    core::{BlockReward, Capacity, CapacityResult, HeaderView},
10    packed::{Byte32, CellbaseWitness, ProposalShortId, Script},
11    prelude::*,
12};
13use std::cmp;
14use std::collections::HashSet;
15
16#[cfg(test)]
17mod tests;
18
19/// Block Reward Calculator.
20/// A Block reward calculator is used to calculate the total block reward for the target block.
21///
22/// For block(i) miner, CKB issues its total block reward by enforcing the
23/// block(i + PROPOSAL_WINDOW.farthest + 1)'s cellbase:
24///   - cellbase output capacity is block(i)'s total block reward
25///   - cellbase output lock is block(i)'s miner provided lock in block(i) 's cellbase output-data
26///     Conventionally, We say that block(i) is block(i + PROPOSAL_WINDOW.farthest + 1)'s target block.
27///
28/// Target block's total reward consists of four parts:
29///  - primary block reward
30///  - secondary block reward
31///  - proposals reward
32///  - transactions fees
33pub struct RewardCalculator<'a, CS> {
34    consensus: &'a Consensus,
35    store: &'a CS,
36}
37
38impl<'a, CS: ChainStore> RewardCalculator<'a, CS> {
39    /// Creates a new `RewardCalculator`.
40    pub fn new(consensus: &'a Consensus, store: &'a CS) -> Self {
41        RewardCalculator { consensus, store }
42    }
43
44    /// Calculates the current block number based on `parent,` locates the current block's target block, returns the target block miner's lock, and total block reward.
45    pub fn block_reward_to_finalize(
46        &self,
47        parent: &HeaderView,
48    ) -> Result<(Script, BlockReward), DaoError> {
49        let block_number = parent.number() + 1;
50        let target_number = self
51            .consensus
52            .finalize_target(block_number)
53            .expect("block number checked before involving finalize_target");
54        let target = self
55            .store
56            .get_block_hash(target_number)
57            .and_then(|hash| self.store.get_block_header(&hash))
58            .expect("block hash checked before involving get_ancestor");
59        self.block_reward_internal(&target, parent)
60    }
61
62    /// Returns the `target` block miner's lock and total block reward.
63    pub fn block_reward_for_target(
64        &self,
65        target: &HeaderView,
66    ) -> Result<(Script, BlockReward), DaoError> {
67        let finalization_parent_number =
68            target.number() + self.consensus.finalization_delay_length() - 1;
69        let parent = self
70            .store
71            .get_block_hash(finalization_parent_number)
72            .and_then(|hash| self.store.get_block_header(&hash))
73            .expect("block hash checked before involving get_ancestor");
74        self.block_reward_internal(target, &parent)
75    }
76
77    /// Calculates the block reward and returns the reward distribution as well as the lock script
78    /// for the target block. Returns a `DaoError` if the calculation fails for any reason.
79    ///
80    /// Panics if the target cellbase does not exist or if the target witness does not exist, or if
81    /// the cellbase loaded from store has an empty witness.
82    fn block_reward_internal(
83        &self,
84        target: &HeaderView,
85        parent: &HeaderView,
86    ) -> Result<(Script, BlockReward), DaoError> {
87        let target_lock = CellbaseWitness::from_slice(
88            &self
89                .store
90                .get_cellbase(&target.hash())
91                .expect("target cellbase exist")
92                .witnesses()
93                .get(0)
94                .expect("target witness exist")
95                .raw_data(),
96        )
97        .expect("cellbase loaded from store should has non-empty witness")
98        .lock();
99
100        let txs_fees = self.txs_fees(target)?;
101        let proposal_reward = self.proposal_reward(parent, target)?;
102        let (primary, secondary) = self.base_block_reward(target)?;
103
104        let total = txs_fees
105            .safe_add(proposal_reward)?
106            .safe_add(primary)?
107            .safe_add(secondary)?;
108
109        debug!(
110            "[RewardCalculator] target {} {}\n
111             txs_fees {:?}, proposal_reward {:?}, primary {:?}, secondary: {:?}, total_reward {:?}",
112            target.number(),
113            target.hash(),
114            txs_fees,
115            proposal_reward,
116            primary,
117            secondary,
118            total,
119        );
120
121        let block_reward = BlockReward {
122            total,
123            primary,
124            secondary,
125            tx_fee: txs_fees,
126            proposal_reward,
127        };
128
129        Ok((target_lock, block_reward))
130    }
131
132    // Miner get (tx_fee - 40% of tx fee) for tx commitment.
133    // Be careful of the rounding, tx_fee - 40% of tx fee is different from 60% of tx fee.
134    fn txs_fees(&self, target: &HeaderView) -> CapacityResult<Capacity> {
135        let consensus = self.consensus;
136        let target_ext = self
137            .store
138            .get_block_ext(&target.hash())
139            .expect("block body stored");
140
141        target_ext
142            .txs_fees
143            .iter()
144            .try_fold(Capacity::zero(), |acc, tx_fee| {
145                tx_fee
146                    .safe_mul_ratio(consensus.proposer_reward_ratio())
147                    .and_then(|proposer| {
148                        tx_fee
149                            .safe_sub(proposer)
150                            .and_then(|miner| acc.safe_add(miner))
151                    })
152            })
153    }
154
155    /// Earliest proposer get 40% of tx fee as reward when tx committed
156    ///  block H(19) target H(13) ProposalWindow(2, 5)
157    ///                 target                    current
158    ///                  /                        /
159    ///     10  11  12  13  14  15  16  17  18  19
160    ///      \   \   \   \______/___/___/___/
161    ///       \   \   \________/___/___/
162    ///        \   \__________/___/
163    ///         \____________/
164    ///
165    /// Note on `fn proposal_reward` implementation:
166    ///
167    /// On mainnet, for block 1~11, the reward target is genesis block.
168    /// Genesis block must have the lock serialized in the cellbase witness,
169    /// which is set to `genesis.bootstrap_lock`.
170    fn proposal_reward(
171        &self,
172        parent: &HeaderView,
173        target: &HeaderView,
174    ) -> CapacityResult<Capacity> {
175        let mut target_proposals = self.get_proposal_ids_by_hash(&target.hash());
176
177        let proposal_window = self.consensus.tx_proposal_window();
178        let proposer_ratio = self.consensus.proposer_reward_ratio();
179        let block_number = parent.number() + 1;
180        let store = self.store;
181
182        let mut reward = Capacity::zero();
183
184        // Transaction can be committed at height H(c): H(c) > H(w_close)
185        let competing_commit_start = cmp::max(
186            block_number.saturating_sub(proposal_window.length()),
187            1 + proposal_window.closest(),
188        );
189
190        let mut proposed: HashSet<ProposalShortId> = HashSet::new();
191        let mut index = parent.to_owned();
192
193        // NOTE: We have to ensure that `committed_idx_proc` and `txs_fees_proc` return in the
194        // same order, the order of transactions in block.
195        let committed_idx_proc = |hash: &Byte32| -> Vec<ProposalShortId> {
196            store
197                .get_block_txs_hashes(hash)
198                .into_iter()
199                .skip(1)
200                .map(|tx_hash| ProposalShortId::from_tx_hash(&tx_hash))
201                .collect()
202        };
203
204        let txs_fees_proc = |hash: &Byte32| -> Vec<Capacity> {
205            store
206                .get_block_ext(hash)
207                .expect("block ext stored")
208                .txs_fees
209        };
210
211        let committed_idx = committed_idx_proc(&index.hash());
212
213        let has_committed = target_proposals
214            .intersection(&committed_idx.iter().cloned().collect::<HashSet<_>>())
215            .next()
216            .is_some();
217        if has_committed {
218            for (id, tx_fee) in committed_idx
219                .into_iter()
220                .zip(txs_fees_proc(&index.hash()).iter())
221            {
222                // target block is the earliest block with effective proposals for the parent block
223                if target_proposals.remove(&id) {
224                    reward = reward.safe_add(tx_fee.safe_mul_ratio(proposer_ratio)?)?;
225                }
226            }
227        }
228
229        while index.number() > competing_commit_start && !target_proposals.is_empty() {
230            index = store
231                .get_block_header(&index.data().raw().parent_hash())
232                .expect("header stored");
233
234            // Transaction can be proposed at height H(p): H(p) > H(0)
235            let competing_proposal_start =
236                cmp::max(index.number().saturating_sub(proposal_window.farthest()), 1);
237
238            let previous_ids = store
239                .get_block_hash(competing_proposal_start)
240                .map(|hash| self.get_proposal_ids_by_hash(&hash))
241                .expect("finalize target exist");
242
243            proposed.extend(previous_ids);
244
245            let committed_idx = committed_idx_proc(&index.hash());
246
247            let has_committed = target_proposals
248                .intersection(&committed_idx.iter().cloned().collect::<HashSet<_>>())
249                .next()
250                .is_some();
251            if has_committed {
252                for (id, tx_fee) in committed_idx
253                    .into_iter()
254                    .zip(txs_fees_proc(&index.hash()).iter())
255                {
256                    if target_proposals.remove(&id) && !proposed.contains(&id) {
257                        reward = reward.safe_add(tx_fee.safe_mul_ratio(proposer_ratio)?)?;
258                    }
259                }
260            }
261        }
262        Ok(reward)
263    }
264
265    fn base_block_reward(&self, target: &HeaderView) -> Result<(Capacity, Capacity), DaoError> {
266        let data_loader = self.store.borrow_as_data_loader();
267        let calculator = DaoCalculator::new(self.consensus, &data_loader);
268        let primary_block_reward = calculator.primary_block_reward(target)?;
269        let secondary_block_reward = calculator.secondary_block_reward(target)?;
270
271        Ok((primary_block_reward, secondary_block_reward))
272    }
273
274    fn get_proposal_ids_by_hash(&self, hash: &Byte32) -> HashSet<ProposalShortId> {
275        let mut ids_set = HashSet::new();
276        if let Some(ids) = self.store.get_block_proposal_txs_ids(hash) {
277            ids_set.extend(ids)
278        }
279        if let Some(us) = self.store.get_block_uncles(hash) {
280            for u in us.data().into_iter() {
281                ids_set.extend(u.proposals().into_iter());
282            }
283        }
284        ids_set
285    }
286}