Skip to content

Commit 3fb6d6f

Browse files
committed
chore: fix p2p handling of unsolicited nakamoto blocks by loading the reward set for nakamoto prepare phases eagerly, and pass the stacks tip height via NetworkResult to the relayer so it can update prometheus
1 parent 45adc33 commit 3fb6d6f

File tree

7 files changed

+200
-37
lines changed

7 files changed

+200
-37
lines changed

stackslib/src/chainstate/nakamoto/mod.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,7 +328,7 @@ pub trait StacksDBIndexed {
328328
fn get(&mut self, tip: &StacksBlockId, key: &str) -> Result<Option<String>, DBError>;
329329
fn sqlite(&self) -> &Connection;
330330

331-
/// Get the ancestor block hash given a height
331+
/// Get the ancestor block hash given a coinbase height
332332
fn get_ancestor_block_id(
333333
&mut self,
334334
coinbase_height: u64,

stackslib/src/net/mod.rs

Lines changed: 30 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1515,6 +1515,10 @@ pub struct NetworkResult {
15151515
pub num_connected_peers: usize,
15161516
/// The observed burnchain height
15171517
pub burn_height: u64,
1518+
/// The observed stacks coinbase height
1519+
pub coinbase_height: u64,
1520+
/// The observed stacks tip height (different in Nakamoto from coinbase height)
1521+
pub stacks_tip_height: u64,
15181522
/// The consensus hash of the stacks tip (prefixed `rc_` for historical reasons)
15191523
pub rc_consensus_hash: ConsensusHash,
15201524
/// The current StackerDB configs
@@ -1529,6 +1533,8 @@ impl NetworkResult {
15291533
num_download_passes: u64,
15301534
num_connected_peers: usize,
15311535
burn_height: u64,
1536+
coinbase_height: u64,
1537+
stacks_tip_height: u64,
15321538
rc_consensus_hash: ConsensusHash,
15331539
stacker_db_configs: HashMap<QualifiedContractIdentifier, StackerDBConfig>,
15341540
) -> NetworkResult {
@@ -1557,6 +1563,8 @@ impl NetworkResult {
15571563
num_download_passes: num_download_passes,
15581564
num_connected_peers,
15591565
burn_height,
1566+
coinbase_height,
1567+
stacks_tip_height,
15601568
rc_consensus_hash,
15611569
stacker_db_configs,
15621570
}
@@ -3416,8 +3424,6 @@ pub mod test {
34163424
let mut stacks_node = self.stacks_node.take().unwrap();
34173425
let indexer = BitcoinIndexer::new_unit_test(&self.config.burnchain.working_dir);
34183426

3419-
let old_tip = self.network.stacks_tip.clone();
3420-
34213427
self.network
34223428
.refresh_burnchain_view(&indexer, &sortdb, &mut stacks_node.chainstate, false)
34233429
.unwrap();
@@ -3426,6 +3432,28 @@ pub mod test {
34263432
self.stacks_node = Some(stacks_node);
34273433
}
34283434

3435+
pub fn refresh_reward_cycles(&mut self) {
3436+
let sortdb = self.sortdb.take().unwrap();
3437+
let mut stacks_node = self.stacks_node.take().unwrap();
3438+
3439+
let tip = SortitionDB::get_canonical_burn_chain_tip(sortdb.conn()).unwrap();
3440+
let tip_block_id = self.network.stacks_tip.block_id();
3441+
let tip_height = self.network.stacks_tip.height;
3442+
3443+
self.network
3444+
.refresh_reward_cycles(
3445+
&sortdb,
3446+
&mut stacks_node.chainstate,
3447+
&tip,
3448+
&tip_block_id,
3449+
tip_height,
3450+
)
3451+
.unwrap();
3452+
3453+
self.sortdb = Some(sortdb);
3454+
self.stacks_node = Some(stacks_node);
3455+
}
3456+
34293457
pub fn for_each_convo_p2p<F, R>(&mut self, mut f: F) -> Vec<Result<R, net_error>>
34303458
where
34313459
F: FnMut(usize, &mut ConversationP2P) -> Result<R, net_error>,

stackslib/src/net/p2p.rs

Lines changed: 149 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4295,38 +4295,55 @@ impl PeerNetwork {
42954295
}
42964296
}
42974297

4298-
/// Refresh our view of the last three reward cycles
4299-
/// This ensures that the PeerNetwork has cached copies of the reward cycle data (including the
4300-
/// signing set) for the current, previous, and previous-previous reward cycles. This data is
4301-
/// in turn consumed by the Nakamoto block downloader, which must validate blocks signed from
4302-
/// any of these reward cycles.
4303-
#[cfg_attr(test, mutants::skip)]
4304-
fn refresh_reward_cycles(
4305-
&mut self,
4298+
/// Determine if we need to invalidate a given cached reward set.
4299+
///
4300+
/// In Epoch 2, this requires checking the first sortition in the start of the reward set's
4301+
/// reward phase.
4302+
///
4303+
/// In Nakamoto, this requires checking the anchor block in the prepare phase for the upcoming
4304+
/// reward phase.
4305+
fn check_reload_cached_reward_set(
4306+
&self,
43064307
sortdb: &SortitionDB,
4307-
chainstate: &mut StacksChainState,
4308+
chainstate: &StacksChainState,
4309+
rc: u64,
43084310
tip_sn: &BlockSnapshot,
43094311
tip_block_id: &StacksBlockId,
4310-
) -> Result<(), net_error> {
4311-
let cur_rc = self
4312-
.burnchain
4313-
.block_height_to_reward_cycle(tip_sn.block_height)
4314-
.expect("FATAL: sortition from before system start");
4315-
4316-
let prev_rc = cur_rc.saturating_sub(1);
4317-
let prev_prev_rc = prev_rc.saturating_sub(1);
4318-
let ih = sortdb.index_handle(&tip_sn.sortition_id);
4319-
4320-
for rc in [cur_rc, prev_rc, prev_prev_rc] {
4321-
debug!("Refresh reward cycle info for cycle {}", rc);
4312+
tip_height: u64,
4313+
) -> Result<bool, net_error> {
4314+
let epoch = self.get_epoch_at_burn_height(tip_sn.block_height);
4315+
if epoch.epoch_id >= StacksEpochId::Epoch30 {
4316+
// epoch 3, where there are no forks except from bugs or burnchain reorgs.
4317+
// invalidate reward cycles on burnchain or stacks reorg, should they ever happen
4318+
let reorg = Self::is_reorg(Some(&self.burnchain_tip), tip_sn, sortdb)
4319+
|| Self::is_nakamoto_reorg(
4320+
&self.stacks_tip.block_id(),
4321+
self.stacks_tip.height,
4322+
tip_block_id,
4323+
tip_height,
4324+
chainstate,
4325+
);
4326+
if reorg {
4327+
info!("Burnchain or Stacks reorg detected; will invalidate cached reward set for cycle {rc}");
4328+
}
4329+
return Ok(reorg);
4330+
} else {
4331+
// epoch 2
43224332
// NOTE: + 1 needed because the sortition db indexes anchor blocks at index height 1,
43234333
// not 0
4334+
let ih = sortdb.index_handle(&tip_sn.sortition_id);
43244335
let rc_start_height = self.burnchain.nakamoto_first_block_of_cycle(rc) + 1;
43254336
let Some(ancestor_sort_id) =
43264337
get_ancestor_sort_id(&ih, rc_start_height, &tip_sn.sortition_id)?
43274338
else {
4328-
// reward cycle is too far back for there to be an ancestor
4329-
continue;
4339+
// reward cycle is too far back for there to be an ancestor, so no need to
4340+
// reload
4341+
test_debug!(
4342+
"No ancestor sortition ID off of {} (height {}) at {rc_start_height})",
4343+
&tip_sn.sortition_id,
4344+
tip_sn.block_height
4345+
);
4346+
return Ok(false);
43304347
};
43314348
let ancestor_ih = sortdb.index_handle(&ancestor_sort_id);
43324349
let anchor_hash_opt = ancestor_ih.get_last_anchor_block_hash()?;
@@ -4340,12 +4357,53 @@ impl PeerNetwork {
43404357
|| cached_rc_info.anchor_block_hash == *anchor_hash
43414358
{
43424359
// cached reward set data is still valid
4343-
continue;
4360+
test_debug!("Cached reward cycle {rc} is still valid");
4361+
return Ok(false);
43444362
}
43454363
}
43464364
}
4365+
}
4366+
4367+
Ok(true)
4368+
}
43474369

4348-
debug!("Load reward cycle info for cycle {}", rc);
4370+
/// Refresh our view of the last three reward cycles
4371+
/// This ensures that the PeerNetwork has cached copies of the reward cycle data (including the
4372+
/// signing set) for the current, previous, and previous-previous reward cycles. This data is
4373+
/// in turn consumed by the Nakamoto block downloader, which must validate blocks signed from
4374+
/// any of these reward cycles.
4375+
#[cfg_attr(test, mutants::skip)]
4376+
pub fn refresh_reward_cycles(
4377+
&mut self,
4378+
sortdb: &SortitionDB,
4379+
chainstate: &mut StacksChainState,
4380+
tip_sn: &BlockSnapshot,
4381+
tip_block_id: &StacksBlockId,
4382+
tip_height: u64,
4383+
) -> Result<(), net_error> {
4384+
let cur_rc = self
4385+
.burnchain
4386+
.block_height_to_reward_cycle(tip_sn.block_height)
4387+
.expect("FATAL: sortition from before system start");
4388+
4389+
let prev_rc = cur_rc.saturating_sub(1);
4390+
let prev_prev_rc = prev_rc.saturating_sub(1);
4391+
4392+
for rc in [cur_rc, prev_rc, prev_prev_rc] {
4393+
debug!("Refresh reward cycle info for cycle {}", rc);
4394+
if self.current_reward_sets.contains_key(&rc)
4395+
&& !self.check_reload_cached_reward_set(
4396+
sortdb,
4397+
chainstate,
4398+
rc,
4399+
tip_sn,
4400+
tip_block_id,
4401+
tip_height,
4402+
)?
4403+
{
4404+
continue;
4405+
}
4406+
debug!("Refresh reward cycle info for cycle {rc}");
43494407
let Some((reward_set_info, anchor_block_header)) = load_nakamoto_reward_set(
43504408
rc,
43514409
&tip_sn.sortition_id,
@@ -4452,6 +4510,7 @@ impl PeerNetwork {
44524510
chainstate,
44534511
&canonical_sn,
44544512
&new_stacks_tip_block_id,
4513+
stacks_tip_height,
44554514
)?;
44564515
}
44574516

@@ -4649,7 +4708,7 @@ impl PeerNetwork {
46494708
debug!(
46504709
"{:?}: handle unsolicited stacks messages: tenure changed {} != {}, {} buffered",
46514710
self.get_local_peer(),
4652-
&self.burnchain_tip.consensus_hash,
4711+
&self.stacks_tip.consensus_hash,
46534712
&canonical_sn.consensus_hash,
46544713
self.pending_stacks_messages
46554714
.iter()
@@ -4751,7 +4810,6 @@ impl PeerNetwork {
47514810
ibd,
47524811
true,
47534812
);
4754-
47554813
let unhandled_messages =
47564814
self.handle_unsolicited_stacks_messages(chainstate, unhandled_messages, true);
47574815

@@ -4998,7 +5056,7 @@ impl PeerNetwork {
49985056
Ok(())
49995057
}
50005058

5001-
/// Static helper to check to see if there has been a reorg
5059+
/// Static helper to check to see if there has been a burnchain reorg
50025060
pub fn is_reorg(
50035061
last_sort_tip: Option<&BlockSnapshot>,
50045062
sort_tip: &BlockSnapshot,
@@ -5021,15 +5079,15 @@ impl PeerNetwork {
50215079
{
50225080
// current and previous sortition tips are at the same height, but represent different
50235081
// blocks.
5024-
debug!(
5025-
"Reorg detected at burn height {}: {} != {}",
5082+
info!(
5083+
"Burnchain reorg detected at burn height {}: {} != {}",
50265084
sort_tip.block_height, &last_sort_tip.consensus_hash, &sort_tip.consensus_hash
50275085
);
50285086
return true;
50295087
}
50305088

50315089
// It will never be the case that the last and current tip have different heights, but the
5032-
// smae consensus hash. If they have the same height, then we would have already returned
5090+
// same consensus hash. If they have the same height, then we would have already returned
50335091
// since we've handled both the == and != cases for their consensus hashes. So if we reach
50345092
// this point, the heights and consensus hashes are not equal. We only need to check that
50355093
// last_sort_tip is an ancestor of sort_tip
@@ -5061,6 +5119,60 @@ impl PeerNetwork {
50615119
false
50625120
}
50635121

5122+
/// Static helper to check to see if there has been a Nakamoto reorg.
5123+
/// Return true if there's a Nakamoto reorg
5124+
/// Return false otherwise.
5125+
pub fn is_nakamoto_reorg(
5126+
last_stacks_tip: &StacksBlockId,
5127+
last_stacks_tip_height: u64,
5128+
stacks_tip: &StacksBlockId,
5129+
stacks_tip_height: u64,
5130+
chainstate: &StacksChainState,
5131+
) -> bool {
5132+
if last_stacks_tip == stacks_tip {
5133+
// same tip
5134+
return false;
5135+
}
5136+
5137+
if last_stacks_tip_height == stacks_tip_height && last_stacks_tip != stacks_tip {
5138+
// last block is a sibling
5139+
info!(
5140+
"Stacks reorg detected at stacks height {last_stacks_tip_height}: {last_stacks_tip} != {stacks_tip}",
5141+
);
5142+
return true;
5143+
}
5144+
5145+
if stacks_tip_height < last_stacks_tip_height {
5146+
info!(
5147+
"Stacks reorg (chain shrink) detected at stacks height {last_stacks_tip_height}: {last_stacks_tip} != {stacks_tip}",
5148+
);
5149+
return true;
5150+
}
5151+
5152+
// It will never be the case that the last and current tip have different heights, but the
5153+
// same block ID. If they have the same height, then we would have already returned
5154+
// since we've handled both the == and != cases for their block IDs. So if we reach
5155+
// this point, the heights and block IDs are not equal. We only need to check that
5156+
// last_stacks_tip is an ancestor of stacks_tip
5157+
5158+
let mut cursor = stacks_tip.clone();
5159+
for _ in last_stacks_tip_height..stacks_tip_height {
5160+
let Ok(Some(parent_id)) =
5161+
NakamotoChainState::get_nakamoto_parent_block_id(chainstate.db(), &cursor)
5162+
else {
5163+
error!("Failed to load parent id of {cursor}");
5164+
return true;
5165+
};
5166+
cursor = parent_id;
5167+
}
5168+
5169+
debug!("is_nakamoto_reorg check";
5170+
"parent_id" => %cursor,
5171+
"last_stacks_tip" => %last_stacks_tip);
5172+
5173+
cursor != *last_stacks_tip
5174+
}
5175+
50645176
/// Log our neighbors.
50655177
/// Used for testing and debuggin
50665178
fn log_neighbors(&mut self) {
@@ -5143,13 +5255,19 @@ impl PeerNetwork {
51435255
}
51445256
};
51455257

5258+
test_debug!(
5259+
"unsolicited_buffered_messages = {:?}",
5260+
&unsolicited_buffered_messages
5261+
);
51465262
let mut network_result = NetworkResult::new(
51475263
self.stacks_tip.block_id(),
51485264
self.num_state_machine_passes,
51495265
self.num_inv_sync_passes,
51505266
self.num_downloader_passes,
51515267
self.peers.len(),
51525268
self.chain_view.burn_block_height,
5269+
self.stacks_tip.coinbase_height,
5270+
self.stacks_tip.height,
51535271
self.chain_view.rc_consensus_hash.clone(),
51545272
self.get_stacker_db_configs_owned(),
51555273
);

stackslib/src/net/relay.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2322,8 +2322,6 @@ impl Relayer {
23222322
event_observer,
23232323
)?;
23242324

2325-
update_stacks_tip_height(chain_height as i64);
2326-
23272325
Ok(ret)
23282326
}
23292327

@@ -3034,6 +3032,10 @@ impl Relayer {
30343032
event_observer.map(|obs| obs.as_stackerdb_event_dispatcher()),
30353033
)?;
30363034

3035+
update_stacks_tip_height(
3036+
i64::try_from(network_result.stacks_tip_height).unwrap_or(i64::MAX),
3037+
);
3038+
30373039
let receipts = ProcessedNetReceipts {
30383040
mempool_txs_added,
30393041
processed_unconfirmed_state,

0 commit comments

Comments
 (0)