1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
//! 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
);
}