1use std::{cmp::min, collections::VecDeque, mem};
23use cuprate_fixed_bytes::ByteArrayVec;
4use tower::{Service, ServiceExt};
56use cuprate_constants::block::MAX_BLOCK_HEIGHT_USIZE;
7use cuprate_p2p_core::{client::InternalPeerID, handles::ConnectionHandle, NetworkZone};
8use cuprate_pruning::PruningSeed;
910use crate::{
11 block_downloader::{ChainSvcRequest, ChainSvcResponse},
12 constants::MEDIUM_BAN,
13};
1415/// A new chain entry to add to our chain tracker.
16#[derive(Debug)]
17pub struct ChainEntry<N: NetworkZone> {
18/// A list of block IDs.
19pub ids: Vec<[u8; 32]>,
20/// The peer who told us about this chain entry.
21pub peer: InternalPeerID<N::Addr>,
22/// The peer who told us about this chain entry's handle
23pub handle: ConnectionHandle,
24}
2526/// A batch of blocks to retrieve.
27#[derive(Clone)]
28pub(crate) struct BlocksToRetrieve<N: NetworkZone> {
29/// The block IDs to get.
30pub ids: ByteArrayVec<32>,
31/// The hash of the last block before this batch.
32pub prev_id: [u8; 32],
33/// The expected height of the first block in [`BlocksToRetrieve::ids`].
34pub start_height: usize,
35/// The peer who told us about this batch.
36pub peer_who_told_us: InternalPeerID<N::Addr>,
37/// The peer who told us about this batch's handle.
38pub peer_who_told_us_handle: ConnectionHandle,
39/// The number of requests sent for this batch.
40pub requests_sent: usize,
41/// The number of times this batch has been requested from a peer and failed.
42pub failures: usize,
43}
4445/// An error returned from the [`ChainTracker`].
46#[derive(Debug)]
47pub(crate) enum ChainTrackerError {
48/// The new chain entry is invalid.
49NewEntryIsInvalid,
50 NewEntryIsEmpty,
51/// The new chain entry does not follow from the top of our chain tracker.
52NewEntryDoesNotFollowChain,
53#[expect(dead_code)] // This is used for logging
54ChainSvcError(tower::BoxError),
55}
5657/// # Chain Tracker
58///
59/// This struct allows following a single chain. It takes in [`ChainEntry`]s and
60/// allows getting [`BlocksToRetrieve`].
61pub(crate) struct ChainTracker<N: NetworkZone> {
62/// A list of [`ChainEntry`]s, in order, that we should request.
63valid_entries: VecDeque<ChainEntry<N>>,
64/// A list of [`ChainEntry`]s that are pending more [`ChainEntry`]s to check validity.
65unknown_entries: VecDeque<ChainEntry<N>>,
66/// The height of the first block, in the first entry in [`Self::entries`].
67first_height: usize,
68/// The hash of the last block in the last entry.
69top_seen_hash: [u8; 32],
70/// The hash of the block one below [`Self::first_height`].
71previous_hash: [u8; 32],
72/// The hash of the genesis block.
73our_genesis: [u8; 32],
74}
7576impl<N: NetworkZone> ChainTracker<N> {
77/// Creates a new chain tracker.
78pub(crate) async fn new<C>(
79 new_entry: ChainEntry<N>,
80 first_height: usize,
81 our_genesis: [u8; 32],
82 previous_hash: [u8; 32],
83 our_chain_svc: &mut C,
84 ) -> Result<Self, ChainTrackerError>
85where
86C: Service<ChainSvcRequest<N>, Response = ChainSvcResponse<N>, Error = tower::BoxError>,
87 {
88let top_seen_hash = *new_entry.ids.last().unwrap();
89let mut entries = VecDeque::with_capacity(1);
90 entries.push_back(new_entry);
9192let ChainSvcResponse::ValidateEntries { valid, unknown } = our_chain_svc
93 .ready()
94 .await
95.map_err(ChainTrackerError::ChainSvcError)?
96.call(ChainSvcRequest::ValidateEntries(entries, first_height))
97 .await
98.map_err(ChainTrackerError::ChainSvcError)?
99else {
100unreachable!()
101 };
102103Ok(Self {
104 valid_entries: valid,
105 unknown_entries: unknown,
106 first_height,
107 top_seen_hash,
108 previous_hash,
109 our_genesis,
110 })
111 }
112113/// Returns `true` if the peer is expected to have the next block after our highest seen block
114 /// according to their pruning seed.
115pub(crate) fn should_ask_for_next_chain_entry(&self, seed: &PruningSeed) -> bool {
116 seed.has_full_block(self.top_height(), MAX_BLOCK_HEIGHT_USIZE)
117 }
118119/// Returns the simple history, the highest seen block and the genesis block.
120pub(crate) const fn get_simple_history(&self) -> [[u8; 32]; 2] {
121 [self.top_seen_hash, self.our_genesis]
122 }
123124/// Returns the height of the highest block we are tracking.
125pub(crate) fn top_height(&self) -> usize {
126let top_block_idx = self
127.valid_entries
128 .iter()
129 .chain(self.unknown_entries.iter())
130 .map(|entry| entry.ids.len())
131 .sum::<usize>();
132133self.first_height + top_block_idx
134 }
135136/// Returns the total number of queued batches for a certain `batch_size`.
137 ///
138 /// # Panics
139 /// This function panics if `batch_size` is `0`.
140pub(crate) fn block_requests_queued(&self, batch_size: usize) -> usize {
141self.valid_entries
142 .iter()
143 .map(|entry| entry.ids.len().div_ceil(batch_size))
144 .sum()
145 }
146147/// Attempts to add an incoming [`ChainEntry`] to the chain tracker.
148pub(crate) async fn add_entry<C>(
149&mut self,
150mut chain_entry: ChainEntry<N>,
151 our_chain_svc: &mut C,
152 ) -> Result<(), ChainTrackerError>
153where
154C: Service<ChainSvcRequest<N>, Response = ChainSvcResponse<N>, Error = tower::BoxError>,
155 {
156if chain_entry.ids.len() == 1 {
157return Err(ChainTrackerError::NewEntryIsEmpty);
158 }
159160let Some(first) = chain_entry.ids.first() else {
161// The peer must send at lest one overlapping block.
162chain_entry.handle.ban_peer(MEDIUM_BAN);
163return Err(ChainTrackerError::NewEntryIsInvalid);
164 };
165166if *first != self.top_seen_hash {
167return Err(ChainTrackerError::NewEntryDoesNotFollowChain);
168 }
169170let new_entry = ChainEntry {
171// ignore the first block - we already know it.
172ids: chain_entry.ids.split_off(1),
173 peer: chain_entry.peer,
174 handle: chain_entry.handle,
175 };
176177self.top_seen_hash = *new_entry.ids.last().unwrap();
178179self.unknown_entries.push_back(new_entry);
180181let ChainSvcResponse::ValidateEntries { mut valid, unknown } = our_chain_svc
182 .ready()
183 .await
184.map_err(ChainTrackerError::ChainSvcError)?
185.call(ChainSvcRequest::ValidateEntries(
186 mem::take(&mut self.unknown_entries),
187self.first_height
188 + self
189.valid_entries
190 .iter()
191 .map(|e| e.ids.len())
192 .sum::<usize>(),
193 ))
194 .await
195.map_err(ChainTrackerError::ChainSvcError)?
196else {
197unreachable!()
198 };
199200self.valid_entries.append(&mut valid);
201self.unknown_entries = unknown;
202203Ok(())
204 }
205206/// Returns a batch of blocks to request.
207 ///
208 /// The returned batches length will be less than or equal to `max_blocks`
209pub(crate) fn blocks_to_get(
210&mut self,
211 pruning_seed: &PruningSeed,
212 max_blocks: usize,
213 ) -> Option<BlocksToRetrieve<N>> {
214if !pruning_seed.has_full_block(self.first_height, MAX_BLOCK_HEIGHT_USIZE) {
215return None;
216 }
217218let entry = self.valid_entries.front_mut()?;
219220// Calculate the ending index for us to get in this batch, it will be one of these:
221 // - smallest out of `max_blocks`
222 // - length of the batch
223 // - index of the next pruned block for this seed
224let end_idx = min(
225 min(entry.ids.len(), max_blocks),
226 pruning_seed
227 .get_next_pruned_block(self.first_height, MAX_BLOCK_HEIGHT_USIZE)
228 .expect("We use local values to calculate height which should be below the sanity limit")
229// Use a big value as a fallback if the seed does no pruning.
230.unwrap_or(MAX_BLOCK_HEIGHT_USIZE)
231 - self.first_height,
232 );
233234if end_idx == 0 {
235return None;
236 }
237238let ids_to_get = entry.ids.drain(0..end_idx).collect::<Vec<_>>();
239240let blocks = BlocksToRetrieve {
241 ids: ids_to_get.into(),
242 prev_id: self.previous_hash,
243 start_height: self.first_height,
244 peer_who_told_us: entry.peer,
245 peer_who_told_us_handle: entry.handle.clone(),
246 requests_sent: 0,
247 failures: 0,
248 };
249250self.first_height += end_idx;
251// TODO: improve ByteArrayVec API.
252self.previous_hash = blocks.ids[blocks.ids.len() - 1];
253254if entry.ids.is_empty() {
255self.valid_entries.pop_front();
256 }
257258Some(blocks)
259 }
260}