@@ -35,12 +35,13 @@ use crate::net::{
35
35
} ;
36
36
use crate :: util_lib:: db:: Error as DBError ;
37
37
38
+ const TIP_ANCESTOR_SEARCH_DEPTH : u64 = 10 ;
39
+
38
40
/// Cached data for a sortition in the sortition DB.
39
41
/// Caching this allows us to avoid calls to `SortitionDB::get_block_snapshot_consensus()`.
40
42
#[ derive( Clone , Debug , PartialEq ) ]
41
43
pub ( crate ) struct InvSortitionInfo {
42
44
parent_consensus_hash : ConsensusHash ,
43
- block_height : u64 ,
44
45
}
45
46
46
47
impl InvSortitionInfo {
@@ -57,7 +58,6 @@ impl InvSortitionInfo {
57
58
58
59
Ok ( Self {
59
60
parent_consensus_hash : parent_sn. consensus_hash ,
60
- block_height : sn. block_height ,
61
61
} )
62
62
}
63
63
}
@@ -105,33 +105,149 @@ impl InvTenureInfo {
105
105
/// in sync. By caching (immutable) tenure data in this struct, we can enusre that this happens
106
106
/// all the time except for during node bootup.
107
107
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
109
111
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 ,
110
116
}
111
117
112
118
impl InvGenerator {
113
119
pub fn new ( ) -> Self {
114
120
Self {
115
121
processed_tenures : HashMap :: new ( ) ,
116
122
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;
117
170
}
171
+ Ok ( None )
118
172
}
119
173
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 (
123
189
& mut self ,
124
190
chainstate : & StacksChainState ,
125
191
tip_block_id : & StacksBlockId ,
126
192
tenure_id_consensus_hash : & ConsensusHash ,
127
193
) -> 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" ) )
135
251
}
136
252
137
253
/// Generate an block inventory bit vector for a reward cycle.
@@ -210,19 +326,10 @@ impl InvGenerator {
210
326
// done scanning this reward cycle
211
327
break ;
212
328
}
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 ;
224
331
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 ) ;
226
333
227
334
if let Some ( cur_tenure_info) = cur_tenure_opt. as_ref ( ) {
228
335
// a tenure was active when this sortition happened...
0 commit comments