1use std::collections::HashMap;
4
5use alloy::primitives::U256;
6use anyhow::{bail, ensure};
7use espresso_types::SeqTypes;
8use hotshot_contract_adapter::light_client::derive_signed_state_digest;
9use hotshot_types::{
10 data::{EpochNumber, ViewNumber},
11 light_client::StateVerKey,
12 message::UpgradeLock,
13 simple_certificate::LightClientStateUpdateCertificateV2,
14 stake_table::HSStakeTable,
15 traits::signature_key::{LCV2StateSignatureKey, LCV3StateSignatureKey, StakeTableEntryType},
16 utils::{epoch_from_block_number, is_epoch_root, root_block_in_epoch},
17};
18
19#[derive(Debug, thiserror::Error)]
21pub enum StateCertFetchError {
22 #[error("failed to fetch state certificate: {0:#}")]
23 FetchError(anyhow::Error),
24
25 #[error("state certificate validation failed: {0:#}")]
26 ValidationError(anyhow::Error),
27
28 #[error("state certificate error: {0:#}")]
29 Other(anyhow::Error),
30
31 #[error("no state certificate for epoch {epoch}: epochs have not started")]
32 EpochsNotStarted { epoch: u64 },
33
34 #[error("no state certificate for epoch {epoch}: the first epoch is {first_epoch}")]
35 BeforeFirstEpoch { epoch: u64, first_epoch: u64 },
36
37 #[error(
38 "no state certificate for epoch {epoch}: its root block is not decided, this node is at \
39 block {decided_height}"
40 )]
41 RootNotDecided { epoch: u64, decided_height: u64 },
42}
43
44pub(crate) fn ensure_cert_can_exist(
50 epoch: EpochNumber,
51 first_epoch: EpochNumber,
52 epoch_height: u64,
53 decided_height: Option<u64>,
54 current_epoch: Option<EpochNumber>,
55) -> Result<(), StateCertFetchError> {
56 let Some(decided_height) = decided_height else {
57 return Err(StateCertFetchError::EpochsNotStarted { epoch: epoch.u64() });
58 };
59 if epoch < first_epoch {
60 return Err(StateCertFetchError::BeforeFirstEpoch {
61 epoch: epoch.u64(),
62 first_epoch: first_epoch.u64(),
63 });
64 }
65 if current_epoch.is_some_and(|current| current > epoch) {
66 return Ok(());
67 }
68 if epoch.u64() > epoch_from_block_number(decided_height, epoch_height)
72 || decided_height < root_block_in_epoch(epoch.u64(), epoch_height)
73 {
74 return Err(StateCertFetchError::RootNotDecided {
75 epoch: epoch.u64(),
76 decided_height,
77 });
78 }
79 Ok(())
80}
81
82pub fn validate_state_cert(
85 cert: &LightClientStateUpdateCertificateV2<SeqTypes>,
86 stake_table: &HSStakeTable<SeqTypes>,
87 expected_epoch: EpochNumber,
88 epoch_height: u64,
89 upgrade_lock: &UpgradeLock<SeqTypes>,
90) -> anyhow::Result<()> {
91 ensure!(
95 is_epoch_root(cert.light_client_state.block_height, epoch_height),
96 "state certificate is for block {}, which is not an epoch root",
97 cert.light_client_state.block_height
98 );
99
100 let derived_epoch = EpochNumber::new(epoch_from_block_number(
103 cert.light_client_state.block_height,
104 epoch_height,
105 ));
106 if derived_epoch != expected_epoch {
107 bail!(
108 "state certificate is for block {} in epoch {derived_epoch}, but epoch \
109 {expected_epoch} was requested",
110 cert.light_client_state.block_height
111 );
112 }
113
114 if cert.epoch != derived_epoch {
115 bail!(
116 "state certificate is labelled epoch {}, but its block {} belongs to epoch \
117 {derived_epoch}",
118 cert.epoch,
119 cert.light_client_state.block_height
120 );
121 }
122
123 let signed_state_digest = derive_signed_state_digest(
124 &cert.light_client_state,
125 &cert.next_stake_table_state,
126 &cert.auth_root,
127 );
128
129 let require_lcv3 =
134 upgrade_lock.upgraded_drb_and_header(ViewNumber::new(cert.light_client_state.view_number));
135
136 let signature_map: HashMap<&StateVerKey, _> = cert
137 .signatures
138 .iter()
139 .map(|(key, lcv3_sig, lcv2_sig)| (key, (lcv3_sig, lcv2_sig)))
140 .collect();
141
142 let mut accumulated_weight = U256::ZERO;
144
145 for peer in stake_table.iter() {
146 if let Some((lcv3_sig, lcv2_sig)) = signature_map.get(&peer.state_ver_key) {
147 let lcv2_valid = <StateVerKey as LCV2StateSignatureKey>::verify_state_sig(
148 &peer.state_ver_key,
149 lcv2_sig,
150 &cert.light_client_state,
151 &cert.next_stake_table_state,
152 );
153
154 let is_valid = if require_lcv3 {
155 let lcv3_valid = <StateVerKey as LCV3StateSignatureKey>::verify_state_sig(
156 &peer.state_ver_key,
157 lcv3_sig,
158 signed_state_digest,
159 );
160
161 lcv2_valid && lcv3_valid
162 } else {
163 lcv2_valid
164 };
165
166 if is_valid {
167 accumulated_weight += peer.stake_table_entry.stake();
168 } else {
169 bail!(format!(
170 "Invalid signature from key: {}",
171 peer.state_ver_key
172 ))
173 }
174 }
175 }
176
177 let total_stake = stake_table.total_stakes();
179 let threshold = hotshot_types::stake_table::one_honest_threshold(total_stake);
180 if accumulated_weight < threshold {
181 bail!("accumulated weight {accumulated_weight} is below threshold {threshold}");
182 }
183
184 Ok(())
185}
186
187#[cfg(test)]
188mod tests {
189 use alloy::primitives::{FixedBytes, U256};
190 use espresso_types::PubKey;
191 use hotshot_contract_adapter::light_client::derive_signed_state_digest;
192 use hotshot_types::{
193 PeerConfig,
194 light_client::{
195 CircuitField, LightClientState, StakeTableState, StateKeyPair, StateVerKey,
196 },
197 simple_certificate::LightClientStateUpdateCertificateV2,
198 stake_table::HSStakeTable,
199 traits::signature_key::{LCV2StateSignatureKey, LCV3StateSignatureKey, SignatureKey},
200 };
201 use versions::{Upgrade, version};
202
203 use super::*;
204
205 const SIGNED_AUTH_ROOT: [u8; 32] = [9u8; 32];
207 const UNSIGNED_AUTH_ROOT: [u8; 32] = [0xAAu8; 32];
209
210 const NUM_SIGNERS: u64 = 4;
211 const STAKE_PER_SIGNER: u64 = 100;
212
213 const EPOCH_HEIGHT: u64 = 105;
215 const ROOT_BLOCK: u64 = 100;
217 const FIXTURE_EPOCH: u64 = 1;
219
220 fn upgrade_lock_at(major: u16, minor: u16) -> UpgradeLock<SeqTypes> {
222 UpgradeLock::new(Upgrade::trivial(version(major, minor)))
223 }
224
225 fn valid_cert_and_stake_table() -> (
227 LightClientStateUpdateCertificateV2<SeqTypes>,
228 HSStakeTable<SeqTypes>,
229 ) {
230 cert_and_stake_table(ROOT_BLOCK, FixedBytes::<32>::from(SIGNED_AUTH_ROOT))
231 }
232
233 fn cert_and_stake_table(
237 block_height: u64,
238 auth_root: FixedBytes<32>,
239 ) -> (
240 LightClientStateUpdateCertificateV2<SeqTypes>,
241 HSStakeTable<SeqTypes>,
242 ) {
243 let light_client_state = LightClientState {
244 view_number: 42,
245 block_height,
246 block_comm_root: CircuitField::from(7u64),
247 };
248 let next_stake_table_state = StakeTableState {
249 bls_key_comm: CircuitField::from(1u64),
250 schnorr_key_comm: CircuitField::from(2u64),
251 amount_comm: CircuitField::from(3u64),
252 threshold: CircuitField::from(1u64),
253 };
254 let digest =
255 derive_signed_state_digest(&light_client_state, &next_stake_table_state, &auth_root);
256
257 let mut signatures = Vec::new();
258 let mut peers = Vec::new();
259 for i in 0..NUM_SIGNERS {
260 let state_key_pair = StateKeyPair::generate_from_seed_indexed([0u8; 32], i);
261 let sign_key = state_key_pair.sign_key_ref();
262
263 let lcv2_sig = <StateVerKey as LCV2StateSignatureKey>::sign_state(
264 sign_key,
265 &light_client_state,
266 &next_stake_table_state,
267 )
268 .expect("LCV2 sign");
269 let lcv3_sig = <StateVerKey as LCV3StateSignatureKey>::sign_state(sign_key, digest)
270 .expect("LCV3 sign");
271
272 signatures.push((state_key_pair.ver_key(), lcv3_sig, lcv2_sig));
273
274 let bls_key = PubKey::generated_from_seed_indexed([0u8; 32], i).0;
275 peers.push(PeerConfig::<SeqTypes> {
276 stake_table_entry: bls_key.stake_table_entry(U256::from(STAKE_PER_SIGNER)),
277 state_ver_key: state_key_pair.ver_key(),
278 connect_info: None,
279 });
280 }
281
282 let cert = LightClientStateUpdateCertificateV2::<SeqTypes> {
283 epoch: EpochNumber::new(FIXTURE_EPOCH),
284 light_client_state,
285 next_stake_table_state,
286 signatures,
287 auth_root,
288 };
289 (cert, HSStakeTable::from(peers))
290 }
291
292 fn legacy_cert_and_stake_table() -> (
296 LightClientStateUpdateCertificateV2<SeqTypes>,
297 HSStakeTable<SeqTypes>,
298 ) {
299 let (mut cert, stake_table) = cert_and_stake_table(ROOT_BLOCK, FixedBytes::<32>::default());
300 for (_, lcv3_sig, lcv2_sig) in cert.signatures.iter_mut() {
301 *lcv3_sig = lcv2_sig.clone();
302 }
303 (cert, stake_table)
304 }
305
306 #[test]
307 fn test_valid_cert_is_accepted() {
308 let (cert, stake_table) = valid_cert_and_stake_table();
309 validate_state_cert(
310 &cert,
311 &stake_table,
312 EpochNumber::new(FIXTURE_EPOCH),
313 EPOCH_HEIGHT,
314 &upgrade_lock_at(0, 5),
315 )
316 .expect("well-formed certificate must validate");
317 }
318
319 #[test]
325 fn test_cert_from_another_epoch_is_rejected() {
326 let (mut cert, stake_table) = valid_cert_and_stake_table();
327
328 let digest_before = derive_signed_state_digest(
329 &cert.light_client_state,
330 &cert.next_stake_table_state,
331 &cert.auth_root,
332 );
333 cert.epoch = EpochNumber::new(2);
334 let digest_after = derive_signed_state_digest(
335 &cert.light_client_state,
336 &cert.next_stake_table_state,
337 &cert.auth_root,
338 );
339 assert_eq!(
340 digest_before, digest_after,
341 "relabelling changed the signed digest; if this ever fails the epoch became \
342 authenticated and the label check is redundant"
343 );
344
345 validate_state_cert(
347 &cert,
348 &stake_table,
349 EpochNumber::new(2),
350 EPOCH_HEIGHT,
351 &upgrade_lock_at(0, 5),
352 )
353 .expect_err(
354 "a cert whose signed block height belongs to epoch 1 must not satisfy a request for \
355 epoch 2, however it is labelled",
356 );
357 }
358
359 #[test]
362 fn test_cert_with_mislabelled_epoch_is_rejected() {
363 assert!(is_epoch_root(ROOT_BLOCK, EPOCH_HEIGHT));
366 assert_eq!(
367 epoch_from_block_number(ROOT_BLOCK, EPOCH_HEIGHT),
368 FIXTURE_EPOCH
369 );
370
371 let (mut cert, stake_table) = valid_cert_and_stake_table();
372 cert.epoch = EpochNumber::new(2);
373
374 validate_state_cert(
375 &cert,
376 &stake_table,
377 EpochNumber::new(FIXTURE_EPOCH),
378 EPOCH_HEIGHT,
379 &upgrade_lock_at(0, 5),
380 )
381 .expect_err("a cert whose label disagrees with its signed block height must be rejected");
382 }
383
384 #[test]
388 fn test_cert_for_a_non_epoch_root_block_is_rejected() {
389 assert!(!is_epoch_root(99, EPOCH_HEIGHT));
392 assert_eq!(epoch_from_block_number(99, EPOCH_HEIGHT), FIXTURE_EPOCH);
393
394 let (cert, stake_table) =
395 cert_and_stake_table(99, FixedBytes::<32>::from(SIGNED_AUTH_ROOT));
396 validate_state_cert(
397 &cert,
398 &stake_table,
399 EpochNumber::new(FIXTURE_EPOCH),
400 EPOCH_HEIGHT,
401 &upgrade_lock_at(0, 5),
402 )
403 .expect_err("a cert for a block that is not an epoch root must be rejected");
404 }
405
406 #[test]
408 fn test_unsigned_auth_root_is_rejected() {
409 let (mut cert, stake_table) = valid_cert_and_stake_table();
410 cert.auth_root = FixedBytes::<32>::from(UNSIGNED_AUTH_ROOT);
411
412 let err = validate_state_cert(
413 &cert,
414 &stake_table,
415 EpochNumber::new(FIXTURE_EPOCH),
416 EPOCH_HEIGHT,
417 &upgrade_lock_at(0, 5),
418 )
419 .expect_err("certificate with a mutated auth_root must be rejected");
420 assert!(
421 err.to_string().contains("Invalid signature"),
422 "expected signature failure, got: {err}"
423 );
424 }
425
426 #[test]
430 fn test_zero_auth_root_is_rejected_on_v4() {
431 let (mut cert, stake_table) = valid_cert_and_stake_table();
432 cert.auth_root = FixedBytes::<32>::default();
433
434 validate_state_cert(
435 &cert,
436 &stake_table,
437 EpochNumber::new(FIXTURE_EPOCH),
438 EPOCH_HEIGHT,
439 &upgrade_lock_at(0, 5),
440 )
441 .expect_err("a zeroed auth_root must not disable the LCV3 check");
442 }
443
444 #[test]
448 fn test_prev4_cert_with_zero_auth_root_is_accepted() {
449 let (cert, stake_table) = legacy_cert_and_stake_table();
450
451 validate_state_cert(
452 &cert,
453 &stake_table,
454 EpochNumber::new(FIXTURE_EPOCH),
455 EPOCH_HEIGHT,
456 &upgrade_lock_at(0, 3),
457 )
458 .expect("genuine pre-V4 certificates must still validate");
459 }
460
461 #[test]
465 fn test_legacy_cert_is_rejected_on_v4() {
466 let (cert, stake_table) = legacy_cert_and_stake_table();
467
468 validate_state_cert(
469 &cert,
470 &stake_table,
471 EpochNumber::new(FIXTURE_EPOCH),
472 EPOCH_HEIGHT,
473 &upgrade_lock_at(0, 5),
474 )
475 .expect_err("legacy LCV3 slot must not satisfy the V4 check");
476 }
477
478 const DECAF_EPOCH_HEIGHT: u64 = 3000;
479 const DECAF_FIRST_EPOCH: u64 = 1054;
480 const FIRST_EPOCH_ROOT: u64 = 3_161_995;
482 const EPOCH_7361_ROOT: u64 = 22_082_995;
483
484 fn decaf_gate(
485 epoch: u64,
486 decided_height: u64,
487 current_epoch: Option<u64>,
488 ) -> Result<(), StateCertFetchError> {
489 ensure_cert_can_exist(
490 EpochNumber::new(epoch),
491 EpochNumber::new(DECAF_FIRST_EPOCH),
492 DECAF_EPOCH_HEIGHT,
493 Some(decided_height),
494 current_epoch.map(EpochNumber::new),
495 )
496 }
497
498 #[rstest::rstest]
499 #[case::root_just_decided(7361, EPOCH_7361_ROOT, Some(7361))]
500 #[case::epoch_last_block_decided(7361, 7361 * DECAF_EPOCH_HEIGHT, None)]
501 #[case::past_epoch(DECAF_FIRST_EPOCH, EPOCH_7361_ROOT, None)]
502 #[case::consensus_left_the_epoch(7361, EPOCH_7361_ROOT - 1000, Some(7362))]
503 #[case::first_epoch(DECAF_FIRST_EPOCH, FIRST_EPOCH_ROOT, None)]
504 fn cert_that_can_exist_is_fetched(
505 #[case] epoch: u64,
506 #[case] decided_height: u64,
507 #[case] current_epoch: Option<u64>,
508 ) {
509 decaf_gate(epoch, decided_height, current_epoch).expect("peers may hold this cert");
510 }
511
512 #[rstest::rstest]
513 #[case::root_one_block_ahead(7361, EPOCH_7361_ROOT - 1, Some(7361))]
514 #[case::current_epoch_unknown(7361, EPOCH_7361_ROOT - 1, None)]
515 #[case::previous_epoch_last_block_decided(7361, 7360 * DECAF_EPOCH_HEIGHT, None)]
516 #[case::far_future_epoch(u64::MAX, EPOCH_7361_ROOT, Some(7361))]
517 fn cert_whose_root_is_not_decided_is_not_fetched(
518 #[case] epoch: u64,
519 #[case] decided_height: u64,
520 #[case] current_epoch: Option<u64>,
521 ) {
522 let err = decaf_gate(epoch, decided_height, current_epoch).unwrap_err();
523 assert!(
524 matches!(err, StateCertFetchError::RootNotDecided { .. }),
525 "{err}"
526 );
527 }
528
529 #[rstest::rstest]
530 #[case::epoch_zero(0)]
531 #[case::before_first_epoch(DECAF_FIRST_EPOCH - 1)]
532 fn cert_before_the_first_epoch_is_not_fetched(#[case] epoch: u64) {
533 let err = decaf_gate(epoch, EPOCH_7361_ROOT, Some(7361)).unwrap_err();
534 assert!(
535 matches!(err, StateCertFetchError::BeforeFirstEpoch { .. }),
536 "{err}"
537 );
538 }
539
540 #[test]
541 fn cert_before_epochs_start_is_not_fetched() {
542 let err = ensure_cert_can_exist(
543 EpochNumber::new(DECAF_FIRST_EPOCH),
544 EpochNumber::new(DECAF_FIRST_EPOCH),
545 DECAF_EPOCH_HEIGHT,
546 None,
547 None,
548 )
549 .unwrap_err();
550 assert!(
551 matches!(err, StateCertFetchError::EpochsNotStarted { .. }),
552 "{err}"
553 );
554 }
555}