-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathcheckpoint_sync.rs
More file actions
420 lines (374 loc) · 15.9 KB
/
checkpoint_sync.rs
File metadata and controls
420 lines (374 loc) · 15.9 KB
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
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
use std::time::Duration;
use ethlambda_types::primitives::HashTreeRoot as _;
use ethlambda_types::state::{State, Validator};
use libssz::{DecodeError, SszDecode};
use reqwest::Client;
/// Timeout for establishing the HTTP connection to the checkpoint peer.
/// Fail fast if the peer is unreachable.
const CHECKPOINT_CONNECT_TIMEOUT: Duration = Duration::from_secs(15);
/// Timeout for reading data during body download.
/// This is an inactivity timeout - it resets on each successful read.
const CHECKPOINT_READ_TIMEOUT: Duration = Duration::from_secs(15);
#[derive(Debug, thiserror::Error)]
pub enum CheckpointSyncError {
#[error("HTTP request failed: {0}")]
Http(#[from] reqwest::Error),
#[error("SSZ deserialization failed: {0:?}")]
SszDecode(DecodeError),
#[error("checkpoint state slot cannot be 0")]
SlotIsZero,
#[error("checkpoint state has no validators")]
NoValidators,
#[error("genesis time mismatch: expected {expected}, got {got}")]
GenesisTimeMismatch { expected: u64, got: u64 },
#[error("validator count mismatch: expected {expected}, got {got}")]
ValidatorCountMismatch { expected: usize, got: usize },
#[error(
"validator at position {position} has non-sequential index (expected {expected}, got {got})"
)]
NonSequentialValidatorIndex {
position: usize,
expected: u64,
got: u64,
},
#[error("validator {index} pubkey mismatch (attestation or proposal key)")]
ValidatorPubkeyMismatch { index: usize },
#[error("finalized slot cannot exceed state slot")]
FinalizedExceedsStateSlot,
#[error("justified slot cannot precede finalized slot")]
JustifiedPrecedesFinalized,
#[error("justified and finalized at same slot must have matching roots")]
JustifiedFinalizedRootMismatch,
#[error("block header slot exceeds state slot")]
BlockHeaderSlotExceedsState,
#[error("block header at finalized slot must match finalized root")]
BlockHeaderFinalizedRootMismatch,
#[error("block header at justified slot must match justified root")]
BlockHeaderJustifiedRootMismatch,
}
/// Fetch finalized state from checkpoint sync URL.
///
/// Uses two-phase timeout strategy:
/// - Connect timeout (15s): Fails quickly if peer is unreachable
/// - Read timeout (15s): Inactivity timeout that resets on each read
///
/// Note: We use a read timeout (via `.read_timeout()`) instead of a total download
/// timeout to automatically detect stalled downloads. This allows large states
/// to be downloaded successfully as long as data keeps flowing, while still
/// failing fast if the connection stalls. A plain total timeout would
/// disconnect even for valid downloads if the state is simply too large to
/// transfer within the time limit.
pub async fn fetch_checkpoint_state(
url: &str,
expected_genesis_time: u64,
expected_validators: &[Validator],
) -> Result<State, CheckpointSyncError> {
// Use .read_timeout() to detect stalled downloads (inactivity timer).
// This allows large states to complete as long as data keeps flowing.
let client = Client::builder()
.connect_timeout(CHECKPOINT_CONNECT_TIMEOUT)
.read_timeout(CHECKPOINT_READ_TIMEOUT)
.build()?;
let response = client
.get(url)
.header("Accept", "application/octet-stream")
.send()
.await?
.error_for_status()?;
let bytes = response.bytes().await?;
let state = State::from_ssz_bytes(&bytes).map_err(CheckpointSyncError::SszDecode)?;
verify_checkpoint_state(&state, expected_genesis_time, expected_validators)?;
Ok(state)
}
/// Verify checkpoint state is structurally valid.
///
/// Arguments:
/// - state: The downloaded checkpoint state
/// - expected_genesis_time: Genesis time from local config
/// - expected_validators: Validator pubkeys from local genesis config
fn verify_checkpoint_state(
state: &State,
expected_genesis_time: u64,
expected_validators: &[Validator],
) -> Result<(), CheckpointSyncError> {
// Slot sanity check
if state.slot == 0 {
return Err(CheckpointSyncError::SlotIsZero);
}
// Validators exist
if state.validators.is_empty() {
return Err(CheckpointSyncError::NoValidators);
}
// Genesis time matches
if state.config.genesis_time != expected_genesis_time {
return Err(CheckpointSyncError::GenesisTimeMismatch {
expected: expected_genesis_time,
got: state.config.genesis_time,
});
}
// Validator count matches
if state.validators.len() != expected_validators.len() {
return Err(CheckpointSyncError::ValidatorCountMismatch {
expected: expected_validators.len(),
got: state.validators.len(),
});
}
// Validator indices are sequential (0, 1, 2, ...)
for (position, validator) in state.validators.iter().enumerate() {
if validator.index != position as u64 {
return Err(CheckpointSyncError::NonSequentialValidatorIndex {
position,
expected: position as u64,
got: validator.index,
});
}
}
// Validator pubkeys match (critical security check)
for (i, (state_val, expected_val)) in state
.validators
.iter()
.zip(expected_validators.iter())
.enumerate()
{
if state_val.attestation_pubkey != expected_val.attestation_pubkey
|| state_val.proposal_pubkey != expected_val.proposal_pubkey
{
return Err(CheckpointSyncError::ValidatorPubkeyMismatch { index: i });
}
}
// Finalized slot sanity
if state.latest_finalized.slot > state.slot {
return Err(CheckpointSyncError::FinalizedExceedsStateSlot);
}
// Justified must be at or after finalized
if state.latest_justified.slot < state.latest_finalized.slot {
return Err(CheckpointSyncError::JustifiedPrecedesFinalized);
}
// If justified and finalized are at same slot, roots must match
if state.latest_justified.slot == state.latest_finalized.slot
&& state.latest_justified.root != state.latest_finalized.root
{
return Err(CheckpointSyncError::JustifiedFinalizedRootMismatch);
}
// Block header slot consistency
if state.latest_block_header.slot > state.slot {
return Err(CheckpointSyncError::BlockHeaderSlotExceedsState);
}
// If block header matches checkpoint slots, roots must match
let block_root = state.latest_block_header.hash_tree_root();
if state.latest_block_header.slot == state.latest_finalized.slot
&& block_root != state.latest_finalized.root
{
return Err(CheckpointSyncError::BlockHeaderFinalizedRootMismatch);
}
if state.latest_block_header.slot == state.latest_justified.slot
&& block_root != state.latest_justified.root
{
return Err(CheckpointSyncError::BlockHeaderJustifiedRootMismatch);
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use ethlambda_types::block::BlockHeader;
use ethlambda_types::checkpoint::Checkpoint;
use ethlambda_types::primitives::H256;
use ethlambda_types::state::{ChainConfig, JustificationValidators, JustifiedSlots};
use libssz_types::SszList;
// Helper to create valid test state
fn create_test_state(slot: u64, validators: Vec<Validator>, genesis_time: u64) -> State {
State {
slot,
validators: SszList::try_from(validators).unwrap(),
latest_block_header: BlockHeader {
slot,
parent_root: H256::ZERO,
state_root: H256::ZERO,
body_root: H256::ZERO,
proposer_index: 0,
},
latest_justified: Checkpoint {
slot: slot.saturating_sub(10),
root: H256::ZERO,
},
latest_finalized: Checkpoint {
slot: slot.saturating_sub(20),
root: H256::ZERO,
},
config: ChainConfig { genesis_time },
historical_block_hashes: Default::default(),
justified_slots: JustifiedSlots::new(),
justifications_roots: Default::default(),
justifications_validators: JustificationValidators::new(),
}
}
fn create_test_validator() -> Validator {
Validator {
attestation_pubkey: [1u8; 52],
proposal_pubkey: [11u8; 52],
index: 0,
}
}
fn create_different_validator() -> Validator {
Validator {
attestation_pubkey: [2u8; 52],
proposal_pubkey: [22u8; 52],
index: 0,
}
}
fn create_validators_with_indices(count: usize) -> Vec<Validator> {
(0..count)
.map(|i| Validator {
attestation_pubkey: [i as u8 + 1; 52],
proposal_pubkey: [i as u8 + 101; 52],
index: i as u64,
})
.collect()
}
#[test]
fn verify_accepts_valid_state() {
let validators = vec![create_test_validator()];
let state = create_test_state(100, validators.clone(), 1000);
assert!(verify_checkpoint_state(&state, 1000, &validators).is_ok());
}
#[test]
fn verify_rejects_slot_zero() {
let validators = vec![create_test_validator()];
let state = create_test_state(0, validators.clone(), 1000);
assert!(verify_checkpoint_state(&state, 1000, &validators).is_err());
}
#[test]
fn verify_rejects_empty_validators() {
let state = create_test_state(100, vec![], 1000);
assert!(verify_checkpoint_state(&state, 1000, &[]).is_err());
}
#[test]
fn verify_rejects_genesis_time_mismatch() {
let validators = vec![create_test_validator()];
let state = create_test_state(100, validators.clone(), 1000);
// State has genesis_time=1000, we pass expected=9999
assert!(verify_checkpoint_state(&state, 9999, &validators).is_err());
}
#[test]
fn verify_rejects_validator_count_mismatch() {
let validators = vec![create_test_validator()];
let state = create_test_state(100, validators.clone(), 1000);
let extra_validators = create_validators_with_indices(2);
assert!(verify_checkpoint_state(&state, 1000, &extra_validators).is_err());
}
#[test]
fn verify_accepts_multiple_validators_with_sequential_indices() {
let validators = create_validators_with_indices(3);
let state = create_test_state(100, validators.clone(), 1000);
assert!(verify_checkpoint_state(&state, 1000, &validators).is_ok());
}
#[test]
fn verify_rejects_non_sequential_validator_indices() {
let mut validators = create_validators_with_indices(3);
validators[1].index = 5; // Wrong index at position 1
let state = create_test_state(100, validators.clone(), 1000);
let expected_validators = create_validators_with_indices(3);
assert!(verify_checkpoint_state(&state, 1000, &expected_validators).is_err());
}
#[test]
fn verify_rejects_duplicate_validator_indices() {
let mut validators = create_validators_with_indices(3);
validators[2].index = 0; // Duplicate index
let state = create_test_state(100, validators.clone(), 1000);
let expected_validators = create_validators_with_indices(3);
assert!(verify_checkpoint_state(&state, 1000, &expected_validators).is_err());
}
#[test]
fn verify_rejects_validator_pubkey_mismatch() {
let validators = vec![create_test_validator()];
let state = create_test_state(100, validators.clone(), 1000);
let different_validators = vec![create_different_validator()];
assert!(verify_checkpoint_state(&state, 1000, &different_validators).is_err());
}
#[test]
fn verify_rejects_finalized_after_state_slot() {
let validators = vec![create_test_validator()];
let mut state = create_test_state(100, validators.clone(), 1000);
state.latest_finalized.slot = 101; // Finalized after state slot
assert!(verify_checkpoint_state(&state, 1000, &validators).is_err());
}
#[test]
fn verify_rejects_justified_before_finalized() {
let validators = vec![create_test_validator()];
let mut state = create_test_state(100, validators.clone(), 1000);
state.latest_finalized.slot = 50;
state.latest_justified.slot = 40; // Justified before finalized
assert!(verify_checkpoint_state(&state, 1000, &validators).is_err());
}
#[test]
fn verify_accepts_justified_equals_finalized_with_matching_roots() {
use ethlambda_types::primitives::H256;
let validators = vec![create_test_validator()];
let mut state = create_test_state(100, validators.clone(), 1000);
let common_root = H256::from([42u8; 32]);
state.latest_finalized.slot = 50;
state.latest_finalized.root = common_root;
state.latest_justified.slot = 50; // Same slot
state.latest_justified.root = common_root; // Same root
assert!(verify_checkpoint_state(&state, 1000, &validators).is_ok());
}
#[test]
fn verify_rejects_justified_equals_finalized_with_different_roots() {
use ethlambda_types::primitives::H256;
let validators = vec![create_test_validator()];
let mut state = create_test_state(100, validators.clone(), 1000);
state.latest_finalized.slot = 50;
state.latest_finalized.root = H256::from([1u8; 32]);
state.latest_justified.slot = 50; // Same slot
state.latest_justified.root = H256::from([2u8; 32]); // Different root - conflict!
assert!(verify_checkpoint_state(&state, 1000, &validators).is_err());
}
#[test]
fn verify_rejects_block_header_slot_exceeds_state() {
let validators = vec![create_test_validator()];
let mut state = create_test_state(100, validators.clone(), 1000);
state.latest_block_header.slot = 101; // Block header slot exceeds state slot
assert!(verify_checkpoint_state(&state, 1000, &validators).is_err());
}
#[test]
fn verify_accepts_block_header_matches_finalized_with_correct_root() {
let validators = vec![create_test_validator()];
let mut state = create_test_state(100, validators.clone(), 1000);
state.latest_block_header.slot = 50;
let block_root = state.latest_block_header.hash_tree_root();
state.latest_finalized.slot = 50;
state.latest_finalized.root = block_root;
assert!(verify_checkpoint_state(&state, 1000, &validators).is_ok());
}
#[test]
fn verify_rejects_block_header_matches_finalized_with_wrong_root() {
use ethlambda_types::primitives::H256;
let validators = vec![create_test_validator()];
let mut state = create_test_state(100, validators.clone(), 1000);
state.latest_block_header.slot = 50;
state.latest_finalized.slot = 50;
state.latest_finalized.root = H256::from([99u8; 32]); // Wrong root
assert!(verify_checkpoint_state(&state, 1000, &validators).is_err());
}
#[test]
fn verify_accepts_block_header_matches_justified_with_correct_root() {
let validators = vec![create_test_validator()];
let mut state = create_test_state(100, validators.clone(), 1000);
state.latest_block_header.slot = 90;
let block_root = state.latest_block_header.hash_tree_root();
state.latest_justified.slot = 90;
state.latest_justified.root = block_root;
assert!(verify_checkpoint_state(&state, 1000, &validators).is_ok());
}
#[test]
fn verify_rejects_block_header_matches_justified_with_wrong_root() {
use ethlambda_types::primitives::H256;
let validators = vec![create_test_validator()];
let mut state = create_test_state(100, validators.clone(), 1000);
state.latest_block_header.slot = 90;
state.latest_justified.slot = 90;
state.latest_justified.root = H256::from([99u8; 32]); // Wrong root
assert!(verify_checkpoint_state(&state, 1000, &validators).is_err());
}
}