Skip to content

Commit cde0547

Browse files
committed
feat: address #5044 by tracking loaded tenure data by stacks tip, and making it so that a descendant of a stacks tip can inherit its cached data
1 parent 2d21bff commit cde0547

File tree

1 file changed

+132
-25
lines changed

1 file changed

+132
-25
lines changed

stackslib/src/net/inv/nakamoto.rs

Lines changed: 132 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -35,12 +35,13 @@ use crate::net::{
3535
};
3636
use crate::util_lib::db::Error as DBError;
3737

38+
const TIP_ANCESTOR_SEARCH_DEPTH: u64 = 10;
39+
3840
/// Cached data for a sortition in the sortition DB.
3941
/// Caching this allows us to avoid calls to `SortitionDB::get_block_snapshot_consensus()`.
4042
#[derive(Clone, Debug, PartialEq)]
4143
pub(crate) struct InvSortitionInfo {
4244
parent_consensus_hash: ConsensusHash,
43-
block_height: u64,
4445
}
4546

4647
impl InvSortitionInfo {
@@ -57,7 +58,6 @@ impl InvSortitionInfo {
5758

5859
Ok(Self {
5960
parent_consensus_hash: parent_sn.consensus_hash,
60-
block_height: sn.block_height,
6161
})
6262
}
6363
}
@@ -105,33 +105,149 @@ impl InvTenureInfo {
105105
/// in sync. By caching (immutable) tenure data in this struct, we can enusre that this happens
106106
/// all the time except for during node bootup.
107107
pub struct InvGenerator {
108-
processed_tenures: HashMap<ConsensusHash, Option<InvTenureInfo>>,
108+
/// Map stacks tips to a table of (tenure ID, optional tenure info)
109+
processed_tenures: HashMap<StacksBlockId, HashMap<ConsensusHash, Option<InvTenureInfo>>>,
110+
/// Map consensus hashes to sortition data about them
109111
sortitions: HashMap<ConsensusHash, InvSortitionInfo>,
112+
/// how far back to search for ancestor Stacks blocks when processing a new tip
113+
tip_ancestor_search_depth: u64,
114+
/// count cache misses for `processed_tenures`
115+
cache_misses: u128,
110116
}
111117

112118
impl InvGenerator {
113119
pub fn new() -> Self {
114120
Self {
115121
processed_tenures: HashMap::new(),
116122
sortitions: HashMap::new(),
123+
tip_ancestor_search_depth: TIP_ANCESTOR_SEARCH_DEPTH,
124+
cache_misses: 0,
125+
}
126+
}
127+
128+
pub fn with_tip_ancestor_search_depth(mut self, depth: u64) -> Self {
129+
self.tip_ancestor_search_depth = depth;
130+
self
131+
}
132+
133+
#[cfg(test)]
134+
pub(crate) fn cache_misses(&self) -> u128 {
135+
self.cache_misses
136+
}
137+
138+
/// Find the highest ancestor of `tip_block_id` that has an entry in `processed_tenures`.
139+
/// Search up to `self.tip_ancestor_search_depth` ancestors back.
140+
///
141+
/// The intuition here is that `tip_block_id` is the highest block known to the node, and it
142+
/// can advance when new blocks are processed. We associate a set of cached processed tenures with
143+
/// each tip, but if the tip advances, we simply move the cached processed tenures "up to" the
144+
/// new tip instead of reloading them from disk each time.
145+
///
146+
/// However, searching for an ancestor tip incurs a sqlite DB read, so we want to bound the
147+
/// search depth. In practice, the bound on this depth would be derived from how often the
148+
/// chain tip changes relative to how often we serve up inventory data. The depth should be
149+
/// the maximum expected number of blocks to be processed in-between handling `GetNakamotoInv`
150+
/// messages.
151+
///
152+
/// If found, then return the ancestor block ID represented in `self.processed_tenures`.
153+
/// If not, then reutrn None.
154+
pub(crate) fn find_ancestor_processed_tenures(
155+
&self,
156+
chainstate: &StacksChainState,
157+
tip_block_id: &StacksBlockId,
158+
) -> Result<Option<StacksBlockId>, NetError> {
159+
let mut cursor = tip_block_id.clone();
160+
for _ in 0..self.tip_ancestor_search_depth {
161+
let parent_id_opt =
162+
NakamotoChainState::get_nakamoto_parent_block_id(chainstate.db(), &cursor)?;
163+
let Some(parent_id) = parent_id_opt else {
164+
return Ok(None);
165+
};
166+
if self.processed_tenures.contains_key(&parent_id) {
167+
return Ok(Some(parent_id));
168+
}
169+
cursor = parent_id;
117170
}
171+
Ok(None)
118172
}
119173

120-
/// Get a processed tenure. If it's not cached, then load it.
121-
/// Returns Some(..) if there existed a tenure-change tx for this given consensus hash
122-
fn get_processed_tenure(
174+
/// Get a processed tenure. If it's not cached, then load it from disk.
175+
///
176+
/// Loading it is expensive, so once loaded, store it with the cached processed tenure map
177+
/// associated with `tip_block_id`.
178+
///
179+
/// If there is no such map, then see if a recent ancestor of `tip_block_id` is represented. If
180+
/// so, then remove that map and associate it with `tip_block_id`. This way, as the blockchain
181+
/// advances, cached tenure information for the same Stacks fork stays associated with that
182+
/// fork's chain tip (assuming this code gets run sufficiently often relative to the
183+
/// advancement of the `tip_block_id` tip value).
184+
///
185+
/// Returns Ok(Some(..)) if there existed a tenure-change tx for this given consensus hash
186+
/// Returns Ok(None) if not
187+
/// Returns Err(..) on DB error
188+
pub(crate) fn get_processed_tenure(
123189
&mut self,
124190
chainstate: &StacksChainState,
125191
tip_block_id: &StacksBlockId,
126192
tenure_id_consensus_hash: &ConsensusHash,
127193
) -> Result<Option<InvTenureInfo>, NetError> {
128-
// TODO: MARF-aware cache
129-
// not cached so go load it
130-
let loaded_info_opt =
131-
InvTenureInfo::load(chainstate, tip_block_id, &tenure_id_consensus_hash)?;
132-
self.processed_tenures
133-
.insert(tenure_id_consensus_hash.clone(), loaded_info_opt.clone());
134-
Ok(loaded_info_opt)
194+
if self.processed_tenures.get(tip_block_id).is_none() {
195+
// this tip has no known table.
196+
// does it have an ancestor with a table? If so, then move its ancestor's table to this
197+
// tip. Otherwise, make a new table.
198+
if let Some(ancestor_tip_id) =
199+
self.find_ancestor_processed_tenures(chainstate, tip_block_id)?
200+
{
201+
let ancestor_tenures = self
202+
.processed_tenures
203+
.remove(&ancestor_tip_id)
204+
.unwrap_or_else(|| {
205+
panic!("FATAL: did not have ancestor tip reported by search");
206+
});
207+
208+
self.processed_tenures
209+
.insert(tip_block_id.clone(), ancestor_tenures);
210+
} else {
211+
self.processed_tenures
212+
.insert(tip_block_id.clone(), HashMap::new());
213+
}
214+
}
215+
216+
let Some(tenure_infos) = self.processed_tenures.get_mut(tip_block_id) else {
217+
unreachable!("FATAL: inserted table for chain tip, but didn't get it back");
218+
};
219+
220+
// this tip has a known table
221+
if let Some(loaded_tenure_info) = tenure_infos.get_mut(tenure_id_consensus_hash) {
222+
// we've loaded this tenure info before for this tip
223+
return Ok(loaded_tenure_info.clone());
224+
} else {
225+
// we have not loaded the tenure info for this tip, so go get it
226+
let loaded_info_opt =
227+
InvTenureInfo::load(chainstate, tip_block_id, &tenure_id_consensus_hash)?;
228+
tenure_infos.insert(tenure_id_consensus_hash.clone(), loaded_info_opt.clone());
229+
230+
self.cache_misses = self.cache_misses.saturating_add(1);
231+
return Ok(loaded_info_opt);
232+
}
233+
}
234+
235+
/// Get sortition info, loading it from our cache if needed
236+
pub(crate) fn get_sortition_info(
237+
&mut self,
238+
sortdb: &SortitionDB,
239+
cur_consensus_hash: &ConsensusHash,
240+
) -> Result<&InvSortitionInfo, NetError> {
241+
if !self.sortitions.contains_key(cur_consensus_hash) {
242+
let loaded_info = InvSortitionInfo::load(sortdb, cur_consensus_hash)?;
243+
self.sortitions
244+
.insert(cur_consensus_hash.clone(), loaded_info);
245+
};
246+
247+
Ok(self
248+
.sortitions
249+
.get(cur_consensus_hash)
250+
.expect("infallible: just inserted this data"))
135251
}
136252

137253
/// Generate an block inventory bit vector for a reward cycle.
@@ -210,19 +326,10 @@ impl InvGenerator {
210326
// done scanning this reward cycle
211327
break;
212328
}
213-
let cur_sortition_info = if let Some(info) = self.sortitions.get(&cur_consensus_hash) {
214-
info
215-
} else {
216-
let loaded_info = InvSortitionInfo::load(sortdb, &cur_consensus_hash)?;
217-
self.sortitions
218-
.insert(cur_consensus_hash.clone(), loaded_info);
219-
self.sortitions
220-
.get(&cur_consensus_hash)
221-
.expect("infallible: just inserted this data")
222-
};
223-
let parent_sortition_consensus_hash = cur_sortition_info.parent_consensus_hash.clone();
329+
let cur_sortition_info = self.get_sortition_info(sortdb, &cur_consensus_hash)?;
330+
let parent_sortition_consensus_hash = cur_sortition_info.parent_consensus_hash;
224331

225-
debug!("Get sortition and tenure info for height {}. cur_consensus_hash = {}, cur_tenure_info = {:?}, cur_sortition_info = {:?}", cur_height, &cur_consensus_hash, &cur_tenure_opt, cur_sortition_info);
332+
debug!("Get sortition and tenure info for height {}. cur_consensus_hash = {}, cur_tenure_info = {:?}, parent_sortition_consensus_hash = {}", cur_height, &cur_consensus_hash, &cur_tenure_opt, &parent_sortition_consensus_hash);
226333

227334
if let Some(cur_tenure_info) = cur_tenure_opt.as_ref() {
228335
// a tenure was active when this sortition happened...

0 commit comments

Comments
 (0)