use crate::common::check_genesis::genesis_checkup; use crate::log::{error, info, warn}; use crate::miner::flag::end_reorg_lock; use crate::orphans::add_genesis::create_genesis_block; use crate::orphans::checkup_state::take_orphan_recheck_height; use crate::orphans::deep_sync_rollback::deep_sync_rollback; use crate::orphans::orphan_window_check::orphan_window_check; use crate::orphans::replay_errors::{ should_retry_staged_candidate, staged_candidate_status_for_error, }; use crate::orphans::snapshot_check::snapshot_verified; use crate::orphans::structs::CheckUp; use crate::orphans::structs::OrphanCheckup2; use crate::orphans::structs::UndoTransactions; use crate::records::block_height::get_block_height::get_height; use crate::records::memory::torrent_status::{ get_torrent_status, set_torrent_status, TorrentStatus, }; use crate::startup::remote_height::request_remote_height; use crate::torrent::structs::Torrent; use crate::torrent::torrenting_system::save_torrent::{ list_staged_torrents, read_staged_torrent, remove_staged_torrent, }; use crate::torrent::torrenting_system::torrent_requests::handle_response_and_save_torrent; use crate::wallets::structures::Wallet; use crate::Arc; async fn replay_staged_torrents( params: &OrphanCheckup2, wallet: Arc, ) -> Result<(), String> { // staged torrents are replayed after orphan correction so // any valid deferred candidates can be reconsidered in order. // Replay is height-based: all candidates for the current expected // height are tested before giving up on that height. Valid staged // torrents are retained inside the orphan window even if they do // not advance the chain on this replay pass. loop { let staged_torrents = list_staged_torrents().await?; if staged_torrents.is_empty() { return Ok(()); } // Replay only the next expected height. Later staged torrents have to // wait until their parent height exists locally. let local_height = get_height(¶ms.db); let expected_height = if local_height > 0 || genesis_checkup().await { local_height + 1 } else { local_height }; let mut candidates = Vec::new(); for (height, staged_path) in staged_torrents { // collect all candidates for the current expected height. // anything beyond that height must wait until a winner is found. if height == expected_height { candidates.push(staged_path); continue; } if height > expected_height { break; } } if candidates.is_empty() { return Ok(()); } let mut ordered_candidates = Vec::new(); for staged_path in candidates { // Corrupt staged files are removed so they do not keep blocking // future replay attempts for this height. let torrent_bytes = match read_staged_torrent(&staged_path).await { Ok(bytes) => bytes, Err(err) => { error!("[orphan] failed to read staged torrent {expected_height}: {err}"); remove_staged_torrent(&staged_path).await?; continue; } }; let torrent = match Torrent::from_bytes(&torrent_bytes).await { Ok(torrent) => torrent, Err(err) => { error!("[orphan] failed to parse staged torrent {expected_height}: {err}"); remove_staged_torrent(&staged_path).await?; continue; } }; if matches!( get_torrent_status(expected_height, &torrent.info.info_hash).await, TorrentStatus::Invalid ) { continue; } ordered_candidates.push(torrent); } // Status only filters out known-bad candidates. Among every candidate // still eligible for replay, use the normal block-fight ordering. ordered_candidates.sort_by(|a, b| { a.info .timestamp .cmp(&b.info.timestamp) .then(a.info.nonce.cmp(&b.info.nonce)) .then(a.info.vrf.cmp(&b.info.vrf)) }); if ordered_candidates.is_empty() { return Ok(()); } let mut advanced_height = false; let mut retryable_pending = false; for torrent in ordered_candidates { let torrent_info_hash = torrent.info.info_hash.clone(); // Reuse the normal torrent save/verify pipeline; staged replay // should behave exactly like receiving the torrent live. match handle_response_and_save_torrent( expected_height, ¶ms.db, torrent, wallet.clone(), params.map.clone(), true, true, ) .await { Ok(()) => { // Mark the candidate according to whether it actually // advanced the local chain height. advanced_height = get_height(¶ms.db) >= expected_height; let status = if advanced_height { TorrentStatus::Valid } else { TorrentStatus::Invalid }; set_torrent_status(expected_height, &torrent_info_hash, status).await; if advanced_height { break; } } Err(err) => { let status = staged_candidate_status_for_error(&err); if status != TorrentStatus::Invalid { retryable_pending = true; // Piece availability is not proof that the candidate // lost the block fight; leave it pending so a later // orphan pass can retry after more peers stage it. set_torrent_status(expected_height, &torrent_info_hash, status).await; } else { set_torrent_status( expected_height, &torrent_info_hash, TorrentStatus::Invalid, ) .await; } error!("[orphan] staged torrent replay candidate failed: height={expected_height} err={err}"); continue; } } } if !advanced_height { // Every staged candidate for the current expected height was // exhausted without extending the chain, so stop replay here. if retryable_pending { warn!( "[orphan] replay paused at height {expected_height}; candidate pieces are not currently available" ); return Ok(()); } return Ok(()); } } } async fn sync_checkup_pass(params: &OrphanCheckup2, wallet: Arc) -> Result<(), String> { // bootstrap missing genesis first so the normal orphan // correction logic can operate against a valid local chain if params.local_height == 0 && !genesis_checkup().await { warn!("[orphan] local genesis missing, creating genesis block"); create_genesis_block( params.local_height, params.map.clone(), params.stream.clone(), params.db.clone(), wallet.clone(), params.connections_key.clone(), ) .await; } let undo_transactions_params = UndoTransactions { start_height: params.local_height, replay_to_height: params.remote_height, db: params.db.clone(), stream: params.stream.clone(), map: params.map.clone(), node_syncing: params.node_syncing, connections_key: params.connections_key.clone(), }; // snapshot verification can trigger an immediate rollback // if a trusted checkpoint no longer matches local state if !snapshot_verified(undo_transactions_params, wallet.clone()).await { // A snapshot rollback already happened, so replay staged torrents and // exit instead of running the near-tip rules against stale heights. let mut replay_waiting = false; match replay_staged_torrents(params, wallet.clone()).await { Ok(()) => {} Err(err) => { replay_waiting = should_retry_staged_candidate(&err); error!("[orphan] staged torrent replay error: {err}"); } } if replay_waiting { warn!( "[orphan] replay is waiting for block data; leaving candidates pending for a later pass" ); } return Ok(()); } // run the two orphan rules in order, then replay any staged // torrents that were deferred while reorganization was happening let checkup_params = CheckUp { local_height: params.local_height, remote_height: params.remote_height, recheck_from_height: params.recheck_from_height, db: params.db.clone(), stream: params.stream.clone(), map: params.map.clone(), node_syncing: params.node_syncing, connections_key: params.connections_key.clone(), }; deep_sync_rollback(checkup_params.clone(), wallet.clone()).await; let mut replay_waiting = false; let height_before_window_check = get_height(¶ms.db); match orphan_window_check(checkup_params, wallet.clone()).await { Ok(()) => {} Err(err) => { if should_retry_staged_candidate(&err) && get_height(¶ms.db) < height_before_window_check { replay_waiting = true; } error!("[orphan] orphan window check error: {err}"); } } let height_before_replay = get_height(¶ms.db); match replay_staged_torrents(params, wallet.clone()).await { Ok(()) => {} Err(err) => { replay_waiting |= should_retry_staged_candidate(&err); error!("[orphan] staged torrent replay error: {err}"); } } if get_height(¶ms.db) > height_before_replay { replay_waiting = false; } if replay_waiting { warn!( "[orphan] replay is waiting for block data; leaving candidates pending for a later pass" ); } info!("[orphan] orphan check pass completed"); Ok(()) } pub async fn sync_checkup(mut params: OrphanCheckup2, wallet: Arc) -> Result<(), String> { let result = loop { match sync_checkup_pass(¶ms, wallet.clone()).await { Ok(()) => {} Err(err) => break Err(err), } let Some(recheck_height) = take_orphan_recheck_height() else { break Ok(()); }; let local_height = get_height(¶ms.db); let remote_height = match request_remote_height( params.stream.clone(), params.map.clone(), params.connections_key.clone(), ) .await { Ok(height) => height, Err(err) => { warn!("[orphan] failed to refresh remote height before queued recheck: {err}"); params.remote_height } }; params.local_height = local_height; params.remote_height = remote_height.max(recheck_height).max(local_height); params.recheck_from_height = Some(recheck_height); warn!( "[orphan] running queued orphan recheck: local_height={} remote_height={} queued_height={}", params.local_height, params.remote_height, recheck_height ); }; if !params.node_syncing { end_reorg_lock(); } if result.is_ok() { info!("[orphan] orphan check completed"); } result }