zakura 1.0.4

Zakura, an independent, consensus-compatible implementation of a Zcash node
//! Check the relationship between various sync timeouts and delays.

#![allow(clippy::unwrap_in_result)]

use std::sync::{
    atomic::{AtomicU8, Ordering},
    Arc,
};

use futures::future;
use tokio::time::{timeout, Duration};

use zakura_chain::{
    block::Height,
    parameters::{Network, POST_BLOSSOM_POW_TARGET_SPACING},
};
use zakura_network::constants::HANDSHAKE_TIMEOUT;
use zakura_state::ChainTipSender;

use crate::{
    components::sync::{
        ChainSync, BLOCK_DOWNLOAD_RETRY_LIMIT, BLOCK_DOWNLOAD_TIMEOUT, BLOCK_VERIFY_TIMEOUT,
        GENESIS_TIMEOUT_RETRY, SYNC_RESTART_DELAY, SYNC_RESTART_SLEEP,
    },
    config::ZakuradConfig,
};

/// Make sure the timeout values are consistent with each other.
#[test]
fn ensure_timeouts_consistent() {
    let _init_guard = zakura_test::init();

    // This fork deliberately decouples the post-round idle (`SYNC_RESTART_SLEEP`) from the
    // per-operation restart timeout (`SYNC_RESTART_DELAY`), and keeps both short to recover
    // quickly on thin or flaky peer sets. It accepts that sync may re-enter while cancelled
    // downloads from the previous round are still draining, so the upstream invariant
    // `SYNC_RESTART_DELAY > 2 * BLOCK_DOWNLOAD_TIMEOUT` no longer applies. We instead require
    // the post-round idle to stay shorter than the restart-operation timeout.
    assert!(
        SYNC_RESTART_SLEEP.as_secs() < SYNC_RESTART_DELAY.as_secs(),
        "the post-round sync sleep should be shorter than the sync restart timeout"
    );

    // We multiply by 2, because the Hedge can wait up to BLOCK_DOWNLOAD_TIMEOUT
    // seconds before retrying.
    const BLOCK_DOWNLOAD_HEDGE_TIMEOUT: u64 =
        2 * BLOCK_DOWNLOAD_RETRY_LIMIT as u64 * BLOCK_DOWNLOAD_TIMEOUT.as_secs();

    // This constraint avoids spurious failures due to block download timeouts
    assert!(
        BLOCK_VERIFY_TIMEOUT.as_secs()
            > SYNC_RESTART_DELAY.as_secs()
                + BLOCK_DOWNLOAD_HEDGE_TIMEOUT
                + BLOCK_DOWNLOAD_TIMEOUT.as_secs(),
        "Block verify should allow for a block timeout, a sync restart, and some block fetches"
    );

    // The minimum recommended network speed for Zebra, in bytes per second.
    const MIN_NETWORK_SPEED_BYTES_PER_SEC: u64 = 10 * 1024 * 1024 / 8;

    // This constraint avoids spurious failures when restarting large checkpoints
    assert!(
       BLOCK_VERIFY_TIMEOUT.as_secs() > SYNC_RESTART_DELAY.as_secs() + 2 * zakura_consensus::MAX_CHECKPOINT_BYTE_COUNT / MIN_NETWORK_SPEED_BYTES_PER_SEC,
       "Block verify should allow for a full checkpoint download, a sync restart, then a full checkpoint re-download"
    );

    // This constraint avoids spurious failures after checkpointing has finished
    assert!(
        BLOCK_VERIFY_TIMEOUT.as_secs()
            > 2 * zakura_chain::parameters::NetworkUpgrade::Blossom
                .target_spacing()
                .num_seconds() as u64,
        "Block verify should allow for at least one new block to be generated and distributed"
    );

    // This constraint makes genesis retries more likely to succeed
    assert!(
        GENESIS_TIMEOUT_RETRY.as_secs() > HANDSHAKE_TIMEOUT.as_secs()
            && GENESIS_TIMEOUT_RETRY.as_secs() < BLOCK_DOWNLOAD_TIMEOUT.as_secs(),
        "Genesis retries should wait for new peers, but they shouldn't wait too long"
    );

    assert!(
        SYNC_RESTART_DELAY.as_secs() < POST_BLOSSOM_POW_TARGET_SPACING.into(),
        "a syncer tip crawl should complete before most new blocks"
    );

    // `SYNC_RESTART_DELAY` now bounds restart operations (obtaining tips and retrying the
    // genesis block) rather than the inter-round cadence, so it must be long enough for peers
    // to respond to a tip request — at least one handshake. The upstream invariants that
    // compared the restart delay against the inventory-rotation and peer-crawl intervals
    // assumed it was the much longer inter-round delay; this fork's short restart timeout
    // intentionally no longer satisfies them (the inter-round idle is `SYNC_RESTART_SLEEP`).
    assert!(
        SYNC_RESTART_DELAY.as_secs() > HANDSHAKE_TIMEOUT.as_secs(),
        "a sync restart should allow at least one peer handshake to complete"
    );
}

/// Test that calls to [`ChainSync::request_genesis`] are rate limited.
#[test]
fn request_genesis_is_rate_limited() {
    let (runtime, _init_guard) = zakura_test::init_async();
    let _guard = runtime.enter();

    // The number of calls to `request_genesis()` we are going to be testing for
    const RETRIES_TO_RUN: u8 = 3;

    // create some counters that will be updated inside async blocks
    let peer_requests_counter = Arc::new(AtomicU8::new(0));
    let peer_requests_counter_in_service = Arc::clone(&peer_requests_counter);
    let state_requests_counter = Arc::new(AtomicU8::new(0));
    let state_requests_counter_in_service = Arc::clone(&state_requests_counter);

    // create a fake peer service that respond with `Error` to `BlocksByHash` or
    // panic in any other type of request.
    let peer_service = tower::service_fn(move |request| {
        match request {
            zakura_network::Request::BlocksByHash(_) => {
                // Track the call
                peer_requests_counter_in_service.fetch_add(1, Ordering::SeqCst);
                // Respond with `Error`
                future::err("block not found".into())
            }
            _ => unreachable!("no other request is allowed"),
        }
    });

    // create a state service that respond with `None` to `Depth` or
    // panic in any other type of request.
    let state_service = tower::service_fn(move |request| {
        match request {
            zakura_state::Request::KnownBlock(_) => {
                // Track the call
                state_requests_counter_in_service.fetch_add(1, Ordering::SeqCst);
                // Respond with `None`
                future::ok(zakura_state::Response::KnownBlock(None))
            }
            _ => unreachable!("no other request is allowed"),
        }
    });

    // create an empty latest chain tip
    let (_sender, latest_chain_tip, _change) = ChainTipSender::new(None, &Network::Mainnet);

    // create a verifier service that will always panic as it will never be called
    let verifier_service =
        tower::service_fn(
            move |_| async move { unreachable!("no request to this service is allowed") },
        );

    // start the sync
    let (misbehavior_tx, _misbehavior_rx) = tokio::sync::mpsc::channel(1);
    let (mut chain_sync, _) = ChainSync::new(
        &ZakuradConfig::default(),
        Height(0),
        peer_service,
        verifier_service,
        state_service,
        latest_chain_tip,
        misbehavior_tx,
    );

    // Run `request_genesis()` long enough to observe multiple retries.
    runtime.block_on(async move {
        // This test checks Tokio timer behavior using fully mocked services, so
        // advance retry delays virtually instead of waiting for wall-clock time.
        tokio::time::pause();

        // Leave half a retry interval for scheduling the final request.
        let retries_timeout = (RETRIES_TO_RUN - 1) as u64 * GENESIS_TIMEOUT_RETRY.as_secs()
            + GENESIS_TIMEOUT_RETRY.as_secs() / 2;
        let _ = timeout(
            Duration::from_secs(retries_timeout),
            chain_sync.request_genesis(),
        )
        .await;
    });

    let peer_requests_counter = peer_requests_counter.load(Ordering::SeqCst);
    assert!(peer_requests_counter >= RETRIES_TO_RUN);
    assert!(peer_requests_counter <= RETRIES_TO_RUN * (BLOCK_DOWNLOAD_RETRY_LIMIT as u8) * 2);
    assert_eq!(
        state_requests_counter.load(Ordering::SeqCst),
        RETRIES_TO_RUN
    );
}