Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -264,6 +264,7 @@ pub enum Error {
InvalidBlockHash,
CannotFindBlockHeader,
DBOpen(String),
DBCorrupted(String),
CannotLoadEncryptionKey,
CannotDecrypt,
CannotEncrypt,
Expand Down Expand Up @@ -354,7 +355,8 @@ pub async fn inner_main(

{
let state = state.clone();
headers(state).await.unwrap();
let preload_client = Client::new(&args)?;
headers(state, Some(&preload_client), args.network.into()).await?;
}

// Create oneshot channel to signal when initial block download is complete
Expand Down
125 changes: 121 additions & 4 deletions src/server/preload.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,133 @@
use std::sync::Arc;

use crate::{server::Error, server::State, store::Store};
use crate::{
fetch::Client,
server::Error,
server::State,
store::{BlockMeta, Store},
Family,
};

pub async fn headers(state: Arc<State>) -> Result<(), Error> {
const MAX_HASH_GAP_REPAIR: u32 = 1000;

pub async fn headers(
state: Arc<State>,
client: Option<&Client>,
family: Family,
) -> Result<(), Error> {
let mut blocks_hash_ts = state.blocks_hash_ts.lock().await;
let mut i = 0usize;
for meta in state.store.iter_hash_ts() {
assert_eq!(i as u32, meta.height());
let metas: Vec<BlockMeta> = state.store.iter_hash_ts().collect();
for meta in metas {
Comment on lines +20 to +21
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let metas: Vec<BlockMeta> = state.store.iter_hash_ts().collect(); loads the entire hashes CF into memory at startup. On mainnet-sized chains this can be tens of MB and increases startup latency/peak RSS. Consider iterating in a streaming fashion (or repairing gaps in a separate pass) to avoid collecting all metas at once.

Suggested change
let metas: Vec<BlockMeta> = state.store.iter_hash_ts().collect();
for meta in metas {
for meta in state.store.iter_hash_ts() {

Copilot uses AI. Check for mistakes.
if i as u32 != meta.height() {
let gap_start = i as u32;
let gap_end = meta.height();
let gap_len = gap_end.saturating_sub(gap_start);

if gap_len == 0 {
return Err(Error::DBCorrupted(format!(
"hashes CF out-of-order entry at height {}, reindex required",
meta.height()
)));
}

let client = client.ok_or_else(|| {
Error::DBCorrupted(format!(
"hashes CF gap detected: expected height {}, found {}. \
DB is inconsistent; reindex required",
i,
meta.height()
))
})?;

if gap_len > MAX_HASH_GAP_REPAIR {
return Err(Error::DBCorrupted(format!(
"hashes CF gap too large to repair ({} blocks from {} to {}), reindex required",
gap_len,
gap_start,
gap_end - 1
)));
}

log::warn!(
"hashes CF gap detected ({} blocks from {} to {}), attempting repair",
gap_len,
gap_start,
gap_end - 1
);

for height in gap_start..gap_end {
let hash = client
.block_hash(height)
.await
.map_err(|e| Error::DBCorrupted(format!("failed to fetch block hash: {e}")))?
.ok_or_else(|| {
Error::DBCorrupted(format!(
"missing block hash at height {height} while repairing hashes CF"
))
})?;
let header = client
.block_header(hash, family)
.await
.map_err(|e| {
Error::DBCorrupted(format!(
"failed to fetch block header for {hash}: {e}"
))
})?;
let repaired = BlockMeta::new(height, hash, header.time());
state
.store
.put_hash_ts(&repaired)
.map_err(|e| Error::DBCorrupted(format!("failed to write hash meta: {e}")))?;
blocks_hash_ts.push((repaired.hash(), repaired.timestamp()));
}
i = gap_end as usize;
Comment on lines 18 to +84
Copy link

Copilot AI Feb 9, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

blocks_hash_ts (a tokio::Mutex) is locked for the entire duration of header gap repair, but the repair loop performs multiple .await network calls (block_hash/block_header). Holding an async mutex guard across awaits can block other tasks and can deadlock if any awaited path tries to read/update blocks_hash_ts indirectly. Refactor to avoid awaiting while the mutex is held (e.g., build a local list of metas to append + perform repairs first, then take the lock briefly to extend/replace the vector).

Copilot uses AI. Check for mistakes.
}
blocks_hash_ts.push((meta.hash(), meta.timestamp()));
i += 1;
}
log::info!("{i} block meta preloaded");

Ok(())
}

#[cfg(test)]
mod tests {
use super::headers;
use crate::server::Error;
use crate::store::{db::DBStore, AnyStore, BlockMeta, Store};
use age::x25519::Identity;
use bitcoin::NetworkKind;
use elements::{hashes::Hash, BlockHash};
use std::collections::BTreeMap;
use std::sync::Arc;

use super::State;

#[tokio::test]
async fn test_preload_detects_hash_gap() {
let tempdir = tempfile::TempDir::new().unwrap();
let db = DBStore::open(tempdir.path(), 64, false).unwrap();

let block0 = BlockMeta::new(0, BlockHash::all_zeros(), 0);
db.update(&block0, vec![], BTreeMap::new(), BTreeMap::new())
.unwrap();

let block2 = BlockMeta::new(2, BlockHash::all_zeros(), 0);
db.update(&block2, vec![], BTreeMap::new(), BTreeMap::new())
.unwrap();

let state = Arc::new(State::new(
AnyStore::Db(db),
Identity::generate(),
bitcoin::PrivateKey::generate(NetworkKind::Test),
100,
5,
1000,
)
.unwrap());

let result = headers(state, None, crate::Family::Elements).await;
assert!(matches!(result, Err(Error::DBCorrupted(_))));
}
}
Loading