From 5f3000053955fe1163391faeeac0e56d0af3521a Mon Sep 17 00:00:00 2001 From: rob thijssen Date: Wed, 7 Oct 2026 10:03:46 +0300 Subject: [PATCH] fix(api): give NEAR backfills every other turn instead of none MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The bridge's mint scan sat with its backfill stalled — 118,898 blocks between gargravarr's oldest and where it started, unread and not moving — because every NEAR scan went back only once it was level with the tip. A forward pass takes longer than NEAR's 0.6 s block, so a live scan is always a block or two behind and the backward turn never came. #44's example deposit was waiting on exactly that stretch. The four NEAR scans (bridge mints, intents, intents flows, pool calls) now alternate while history remains. Forward runs at half pace only while a backfill is pending, and every scan here keeps up with the tip at that. Refs #44 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_015pkwBsR54NRmZyU4QVhvfj --- crates/blackbeard-api/src/bridgescan.rs | 29 +++++++++++++++--------- crates/blackbeard-api/src/intents.rs | 25 ++++++++++++-------- crates/blackbeard-api/src/intentsflow.rs | 8 ++++++- crates/blackbeard-api/src/poolscan.rs | 25 ++++++++++++-------- 4 files changed, 57 insertions(+), 30 deletions(-) diff --git a/crates/blackbeard-api/src/bridgescan.rs b/crates/blackbeard-api/src/bridgescan.rs index 40eca2b..acf9c4e 100644 --- a/crates/blackbeard-api/src/bridgescan.rs +++ b/crates/blackbeard-api/src/bridgescan.rs @@ -143,6 +143,7 @@ async fn mints( ) { let id = chain.id(); let key = mint_cursor(&intents.asset); + let mut backward_turn = false; loop { if chain.read().current_runtime().is_none() { tokio::time::sleep(RETRY).await; @@ -165,17 +166,23 @@ async fn mints( } }; let (low, high) = cursor.unwrap_or((tip, tip.saturating_sub(1))); - let (heights, descending): (Vec, bool) = if high < tip { - ((high + 1..=tip.min(high + BATCH)).collect(), false) - } else if low > oldest { - ( - (low.saturating_sub(BATCH).max(oldest)..low).rev().collect(), - true, - ) - } else { - tokio::time::sleep(TIP_WAIT).await; - continue; - }; + // Every other pass goes back while there is history left: forward passes + // take longer than NEAR's 0.6 s blocks, so a scan that waited to be + // level with the tip before going back never went back at all — the + // bridge's mint backfill sat with 118,898 blocks unread (2026-10-07). + backward_turn = !backward_turn; + let (heights, descending): (Vec, bool) = + if high < tip && !(backward_turn && low > oldest) { + ((high + 1..=tip.min(high + BATCH)).collect(), false) + } else if low > oldest { + ( + (low.saturating_sub(BATCH).max(oldest)..low).rev().collect(), + true, + ) + } else { + tokio::time::sleep(TIP_WAIT).await; + continue; + }; let Ok(layout) = near.shard_layout(heights[0]).await else { tokio::time::sleep(RETRY).await; continue; diff --git a/crates/blackbeard-api/src/intents.rs b/crates/blackbeard-api/src/intents.rs index 0c0ecf3..180d048 100644 --- a/crates/blackbeard-api/src/intents.rs +++ b/crates/blackbeard-api/src/intents.rs @@ -133,6 +133,7 @@ pub fn spawn(chain: Arc, store: Store) { /// Walk NEAR blocks, recording trades. async fn scan(chain: Arc, store: Store, near: NearClient, config: IntentsConfig) { let id = chain.id(); + let mut backward_turn = false; loop { // The cursor references the chain row, which the chain's own poll // writes once its node has answered. @@ -161,15 +162,21 @@ async fn scan(chain: Arc, store: Store, near: NearClient, config: // than what traded last week, and both arrive in the end. let (low, high) = scan.unwrap_or((tip, tip.saturating_sub(1))); - let (heights, descending): (Vec, bool) = if high < tip { - ((high + 1..=tip.min(high + BATCH)).collect(), false) - } else if low > config.start_height { - let floor = low.saturating_sub(BATCH).max(config.start_height); - ((floor..low).rev().collect(), true) - } else { - tokio::time::sleep(TIP_WAIT).await; - continue; - }; + // Every other pass goes back while there is history left: forward passes + // take longer than NEAR's 0.6 s blocks, so a scan that waited to be + // level with the tip before going back never went back at all — the + // bridge's mint backfill sat with 118,898 blocks unread (2026-10-07). + backward_turn = !backward_turn; + let (heights, descending): (Vec, bool) = + if high < tip && !(backward_turn && low > config.start_height) { + ((high + 1..=tip.min(high + BATCH)).collect(), false) + } else if low > config.start_height { + let floor = low.saturating_sub(BATCH).max(config.start_height); + ((floor..low).rev().collect(), true) + } else { + tokio::time::sleep(TIP_WAIT).await; + continue; + }; // Per batch, not once: resharding renumbers shards, and a layout read // at startup would follow the wrong one after it while every call diff --git a/crates/blackbeard-api/src/intentsflow.rs b/crates/blackbeard-api/src/intentsflow.rs index 857601d..9772ad8 100644 --- a/crates/blackbeard-api/src/intentsflow.rs +++ b/crates/blackbeard-api/src/intentsflow.rs @@ -141,6 +141,7 @@ async fn scan(chain: Arc, store: Store, near: NearClient, config: let token = config.token_contract().unwrap_or_default().to_owned(); let holder = config.contract.clone(); tracing::info!(chain = %id, %token, %holder, "following the asset into and out of NEAR Intents"); + let mut backward_turn = false; loop { if chain.read().current_runtime().is_none() { tokio::time::sleep(RETRY).await; @@ -166,7 +167,12 @@ async fn scan(chain: Arc, store: Store, near: NearClient, config: // and the first block above it has no "before" to compare with. let floor = oldest.max(config.start_height) + 1; let (low, high) = cursor.unwrap_or((tip, tip.saturating_sub(1))); - let (from, to, descending) = if high < tip { + // Every other pass goes back while there is history left: forward passes + // take longer than NEAR's 0.6 s blocks, so a scan that waited to be + // level with the tip before going back never went back at all — the + // bridge's mint backfill sat with 118,898 blocks unread (2026-10-07). + backward_turn = !backward_turn; + let (from, to, descending) = if high < tip && !(backward_turn && low > floor) { (high + 1, tip.min(high + BATCH), false) } else if low > floor { (low.saturating_sub(BATCH).max(floor), low - 1, true) diff --git a/crates/blackbeard-api/src/poolscan.rs b/crates/blackbeard-api/src/poolscan.rs index 169e7ae..aa0c31c 100644 --- a/crates/blackbeard-api/src/poolscan.rs +++ b/crates/blackbeard-api/src/poolscan.rs @@ -104,6 +104,7 @@ async fn scan(chain: Arc, store: Store, near: NearClient, config: let key = cursor_key(&config.asset); let pools = crate::refdcl::contracts(&config); let asset_contract = config.token_contract().unwrap_or_default().to_owned(); + let mut backward_turn = false; loop { if chain.read().current_runtime().is_none() { tokio::time::sleep(RETRY).await; @@ -127,15 +128,21 @@ async fn scan(chain: Arc, store: Store, near: NearClient, config: }; let (low, high) = cursor.unwrap_or((tip, tip.saturating_sub(1))); let floor = oldest.max(config.start_height); - let (heights, descending): (Vec, bool) = if high < tip { - ((high + 1..=tip.min(high + BATCH)).collect(), false) - } else if low > floor { - let from = low.saturating_sub(BATCH).max(floor); - ((from..low).rev().collect(), true) - } else { - tokio::time::sleep(TIP_WAIT).await; - continue; - }; + // Every other pass goes back while there is history left: forward passes + // take longer than NEAR's 0.6 s blocks, so a scan that waited to be + // level with the tip before going back never went back at all — the + // bridge's mint backfill sat with 118,898 blocks unread (2026-10-07). + backward_turn = !backward_turn; + let (heights, descending): (Vec, bool) = + if high < tip && !(backward_turn && low > floor) { + ((high + 1..=tip.min(high + BATCH)).collect(), false) + } else if low > floor { + let from = low.saturating_sub(BATCH).max(floor); + ((from..low).rev().collect(), true) + } else { + tokio::time::sleep(TIP_WAIT).await; + continue; + }; let mut layout = None; for &height in &heights {