use std::time::{Duration, Instant};
use anyhow::Result;
use linera_base::{data_types::BlockHeight, identifiers::ChainId};
use linera_client::{chain_listener::ClientContext as _, client_context::ClientContext};
use linera_core::node::{CrossChainMessageDelivery, NodeError, ValidatorNode};
use linera_storage::Storage as _;
use super::{progress::Progress, report::PartialSyncReport, rpc::timed};
pub(super) fn end_height(candidate_tip: u64, max_blocks: u32, local_tip: u64) -> u64 {
candidate_tip
.saturating_add(max_blocks as u64)
.min(local_tip)
}
pub async fn run<N, Env>(
node: &N,
context: &ClientContext<Env>,
chain: ChainId,
max_blocks: u32,
rpc_timeout: Duration,
progress: &Progress,
) -> Result<PartialSyncReport>
where
N: ValidatorNode,
Env: linera_core::Environment,
{
let mut report = PartialSyncReport {
chain_id: chain.to_string(),
from_height: 0,
to_height: 0,
blocks_attempted: 0,
blocks_accepted: 0,
bytes_in: 0,
duration_secs: 0.0,
blocks_per_sec: 0.0,
};
let chain_client = context.make_chain_client(chain).await?;
let sync_phase = progress.phase("L2 partial sync: local state", None);
let local_tip = match timed(rpc_timeout, chain_client.synchronize_chain_state(chain)).await {
Ok(info) => {
sync_phase.finish_ok();
info.next_block_height.0
}
Err(category) => {
sync_phase.finish_fail();
anyhow::bail!("L2 partial sync: local sync failed ({category})");
}
};
let candidate_tip = match timed(
rpc_timeout,
node.handle_chain_info_query(linera_core::data_types::ChainInfoQuery::new(chain)),
)
.await
{
Ok(response) => response.info.next_block_height.0,
Err(_) => 0,
};
let from = candidate_tip;
let to = end_height(candidate_tip, max_blocks, local_tip);
report.from_height = from;
report.to_height = to;
if to <= from {
return Ok(report);
}
let phase = progress.phase("L2 partial sync", Some(to - from));
let heights: Vec<BlockHeight> = (from..to).map(BlockHeight).collect();
let storage = chain_client.storage_client();
let certificates = storage
.read_certificates_by_heights(chain, &heights)
.await?
.into_iter()
.flatten();
let started = Instant::now();
for certificate in certificates {
report.blocks_attempted += 1;
phase.inc(1);
report.bytes_in += bcs::serialized_size(&*certificate).unwrap_or(0) as u64;
let first = tokio::time::timeout(
rpc_timeout,
node.handle_confirmed_certificate(
certificate.clone(),
CrossChainMessageDelivery::NonBlocking,
),
)
.await;
let missing = match first {
Ok(Ok(_)) => {
report.blocks_accepted += 1;
continue;
}
Ok(Err(NodeError::BlobsNotFound(missing_blob_ids))) => missing_blob_ids,
Ok(Err(_)) | Err(_) => continue,
};
let blobs = storage
.read_blobs(&missing)
.await?
.into_iter()
.flatten()
.map(|b| b.into_std())
.collect::<Vec<_>>();
if timed(rpc_timeout, node.upload_blobs(blobs)).await.is_err() {
continue;
}
if timed(
rpc_timeout,
node.handle_confirmed_certificate(certificate, CrossChainMessageDelivery::NonBlocking),
)
.await
.is_ok()
{
report.blocks_accepted += 1;
}
}
let duration = started.elapsed().as_secs_f64().max(f64::EPSILON);
report.duration_secs = duration;
report.blocks_per_sec = report.blocks_accepted as f64 / duration;
phase.finish_ok();
Ok(report)
}
#[cfg(test)]
mod tests {
use super::end_height;
#[test]
fn caps_at_max_blocks() {
assert_eq!(end_height(1_000, 500, 10_000), 1_500);
}
#[test]
fn caps_at_local_tip() {
assert_eq!(end_height(1_000, 5_000, 1_200), 1_200);
}
#[test]
fn saturates_on_overflow() {
assert_eq!(end_height(u64::MAX - 1, 1_000, u64::MAX), u64::MAX);
}
#[test]
fn nothing_to_do_when_caught_up() {
assert_eq!(end_height(1_200, 1_000, 1_200), 1_200);
}
}