2022-08-31 00:31:58 +00:00
|
|
|
use std::cmp;
|
2022-08-27 00:05:12 +00:00
|
|
|
use std::sync::Arc;
|
2022-09-09 01:34:14 +00:00
|
|
|
use std::time::UNIX_EPOCH;
|
2022-08-27 00:05:12 +00:00
|
|
|
|
2022-11-02 03:52:28 +00:00
|
|
|
use blst::min_pk::PublicKey;
|
2022-09-09 01:34:14 +00:00
|
|
|
use chrono::Duration;
|
2022-10-25 23:10:49 +00:00
|
|
|
use eyre::eyre;
|
2022-09-29 23:35:43 +00:00
|
|
|
use eyre::Result;
|
2023-01-19 02:18:26 +00:00
|
|
|
use log::warn;
|
2022-09-28 20:48:24 +00:00
|
|
|
use log::{debug, info};
|
2022-08-20 20:33:32 +00:00
|
|
|
use ssz_rs::prelude::*;
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-08-29 17:31:17 +00:00
|
|
|
use common::types::*;
|
|
|
|
use common::utils::*;
|
2022-08-29 20:54:58 +00:00
|
|
|
use config::Config;
|
2022-08-29 17:31:17 +00:00
|
|
|
|
2022-11-08 21:24:55 +00:00
|
|
|
use crate::constants::MAX_REQUEST_LIGHT_CLIENT_UPDATES;
|
2022-09-29 23:35:43 +00:00
|
|
|
use crate::errors::ConsensusError;
|
|
|
|
|
2022-11-02 03:52:28 +00:00
|
|
|
use super::rpc::ConsensusRpc;
|
2022-08-21 16:59:47 +00:00
|
|
|
use super::types::*;
|
2022-11-02 03:52:28 +00:00
|
|
|
use super::utils::*;
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-11-02 03:52:28 +00:00
|
|
|
// https://github.com/ethereum/consensus-specs/blob/dev/specs/altair/light-client/sync-protocol.md
|
|
|
|
// does not implement force updates
|
|
|
|
|
|
|
|
pub struct ConsensusClient<R: ConsensusRpc> {
|
2022-09-04 23:32:16 +00:00
|
|
|
rpc: R,
|
2022-11-02 03:52:28 +00:00
|
|
|
store: LightClientStore,
|
2022-11-03 19:24:17 +00:00
|
|
|
initial_checkpoint: Vec<u8>,
|
2022-09-16 19:32:15 +00:00
|
|
|
pub last_checkpoint: Option<Vec<u8>>,
|
|
|
|
pub config: Arc<Config>,
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-11-03 19:24:17 +00:00
|
|
|
#[derive(Debug, Default)]
|
2022-11-02 03:52:28 +00:00
|
|
|
struct LightClientStore {
|
2022-08-31 00:31:58 +00:00
|
|
|
finalized_header: Header,
|
2022-08-19 22:43:58 +00:00
|
|
|
current_sync_committee: SyncCommittee,
|
|
|
|
next_sync_committee: Option<SyncCommittee>,
|
2022-08-31 00:31:58 +00:00
|
|
|
optimistic_header: Header,
|
|
|
|
previous_max_active_participants: u64,
|
|
|
|
current_max_active_participants: u64,
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-11-02 03:52:28 +00:00
|
|
|
impl<R: ConsensusRpc> ConsensusClient<R> {
|
2022-11-03 19:24:17 +00:00
|
|
|
pub fn new(
|
2022-09-04 23:32:16 +00:00
|
|
|
rpc: &str,
|
2022-11-30 01:31:25 +00:00
|
|
|
checkpoint_block_root: &[u8],
|
2022-08-27 00:05:12 +00:00
|
|
|
config: Arc<Config>,
|
2022-09-04 23:32:16 +00:00
|
|
|
) -> Result<ConsensusClient<R>> {
|
|
|
|
let rpc = R::new(rpc);
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-09-16 19:32:15 +00:00
|
|
|
Ok(ConsensusClient {
|
|
|
|
rpc,
|
2022-11-03 19:24:17 +00:00
|
|
|
store: LightClientStore::default(),
|
2022-09-16 19:32:15 +00:00
|
|
|
last_checkpoint: None,
|
|
|
|
config,
|
2022-11-30 01:31:25 +00:00
|
|
|
initial_checkpoint: checkpoint_block_root.to_vec(),
|
2022-09-16 19:32:15 +00:00
|
|
|
})
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-08-31 21:40:44 +00:00
|
|
|
pub async fn get_execution_payload(&self, slot: &Option<u64>) -> Result<ExecutionPayload> {
|
|
|
|
let slot = slot.unwrap_or(self.store.optimistic_header.slot);
|
2022-11-30 01:31:25 +00:00
|
|
|
let mut block = self.rpc.get_block(slot).await?;
|
2022-08-19 22:43:58 +00:00
|
|
|
let block_hash = block.hash_tree_root()?;
|
2022-09-12 23:23:37 +00:00
|
|
|
|
|
|
|
let latest_slot = self.store.optimistic_header.slot;
|
|
|
|
let finalized_slot = self.store.finalized_header.slot;
|
|
|
|
|
|
|
|
let verified_block_hash = if slot == latest_slot {
|
|
|
|
self.store.optimistic_header.clone().hash_tree_root()?
|
|
|
|
} else if slot == finalized_slot {
|
|
|
|
self.store.finalized_header.clone().hash_tree_root()?
|
|
|
|
} else {
|
2022-09-29 23:35:43 +00:00
|
|
|
return Err(ConsensusError::PayloadNotFound(slot).into());
|
2022-09-12 23:23:37 +00:00
|
|
|
};
|
2022-08-20 20:33:32 +00:00
|
|
|
|
2022-08-19 22:43:58 +00:00
|
|
|
if verified_block_hash != block_hash {
|
2022-09-29 23:35:43 +00:00
|
|
|
Err(ConsensusError::InvalidHeaderHash(
|
|
|
|
block_hash.to_string(),
|
|
|
|
verified_block_hash.to_string(),
|
|
|
|
)
|
|
|
|
.into())
|
2022-08-19 22:43:58 +00:00
|
|
|
} else {
|
|
|
|
Ok(block.body.execution_payload)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-09-04 23:32:16 +00:00
|
|
|
pub fn get_header(&self) -> &Header {
|
2022-08-31 00:31:58 +00:00
|
|
|
&self.store.optimistic_header
|
2022-08-21 13:13:56 +00:00
|
|
|
}
|
|
|
|
|
2022-09-04 23:32:16 +00:00
|
|
|
pub fn get_finalized_header(&self) -> &Header {
|
|
|
|
&self.store.finalized_header
|
|
|
|
}
|
|
|
|
|
2022-08-19 22:43:58 +00:00
|
|
|
pub async fn sync(&mut self) -> Result<()> {
|
2022-11-03 19:24:17 +00:00
|
|
|
self.bootstrap().await?;
|
|
|
|
|
2022-08-31 00:31:58 +00:00
|
|
|
let current_period = calc_sync_period(self.store.finalized_header.slot);
|
2022-11-08 21:24:55 +00:00
|
|
|
let updates = self
|
|
|
|
.rpc
|
|
|
|
.get_updates(current_period, MAX_REQUEST_LIGHT_CLIENT_UPDATES)
|
|
|
|
.await?;
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-11-30 01:31:25 +00:00
|
|
|
for update in updates {
|
|
|
|
self.verify_update(&update)?;
|
2022-08-19 22:43:58 +00:00
|
|
|
self.apply_update(&update);
|
|
|
|
}
|
|
|
|
|
2022-08-21 15:21:50 +00:00
|
|
|
let finality_update = self.rpc.get_finality_update().await?;
|
2022-08-31 00:31:58 +00:00
|
|
|
self.verify_finality_update(&finality_update)?;
|
|
|
|
self.apply_finality_update(&finality_update);
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-08-31 00:31:58 +00:00
|
|
|
let optimistic_update = self.rpc.get_optimistic_update().await?;
|
|
|
|
self.verify_optimistic_update(&optimistic_update)?;
|
|
|
|
self.apply_optimistic_update(&optimistic_update);
|
|
|
|
|
2023-01-19 02:18:26 +00:00
|
|
|
info!(
|
|
|
|
"consensus client in sync with checkpoint: 0x{}",
|
|
|
|
hex::encode(&self.initial_checkpoint)
|
|
|
|
);
|
|
|
|
|
2022-08-31 00:31:58 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
|
|
|
pub async fn advance(&mut self) -> Result<()> {
|
|
|
|
let finality_update = self.rpc.get_finality_update().await?;
|
|
|
|
self.verify_finality_update(&finality_update)?;
|
|
|
|
self.apply_finality_update(&finality_update);
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-08-31 00:31:58 +00:00
|
|
|
let optimistic_update = self.rpc.get_optimistic_update().await?;
|
|
|
|
self.verify_optimistic_update(&optimistic_update)?;
|
|
|
|
self.apply_optimistic_update(&optimistic_update);
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if self.store.next_sync_committee.is_none() {
|
|
|
|
debug!("checking for sync committee update");
|
|
|
|
let current_period = calc_sync_period(self.store.finalized_header.slot);
|
2022-11-08 21:24:55 +00:00
|
|
|
let mut updates = self.rpc.get_updates(current_period, 1).await?;
|
2022-09-28 20:48:24 +00:00
|
|
|
|
|
|
|
if updates.len() == 1 {
|
2022-11-30 01:31:25 +00:00
|
|
|
let update = updates.get_mut(0).unwrap();
|
|
|
|
let res = self.verify_update(update);
|
2022-09-28 20:48:24 +00:00
|
|
|
|
|
|
|
if res.is_ok() {
|
|
|
|
info!("updating sync committee");
|
2022-11-30 01:31:25 +00:00
|
|
|
self.apply_update(update);
|
2022-09-28 20:48:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-19 22:43:58 +00:00
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2022-11-03 19:24:17 +00:00
|
|
|
async fn bootstrap(&mut self) -> Result<()> {
|
|
|
|
let mut bootstrap = self
|
|
|
|
.rpc
|
|
|
|
.get_bootstrap(&self.initial_checkpoint)
|
|
|
|
.await
|
|
|
|
.map_err(|_| eyre!("could not fetch bootstrap"))?;
|
|
|
|
|
2022-11-14 20:23:51 +00:00
|
|
|
let is_valid = self.is_valid_checkpoint(bootstrap.header.slot);
|
2023-01-19 02:18:26 +00:00
|
|
|
|
2022-11-14 20:23:51 +00:00
|
|
|
if !is_valid {
|
2023-01-19 02:18:26 +00:00
|
|
|
if self.config.strict_checkpoint_age {
|
|
|
|
return Err(ConsensusError::CheckpointTooOld.into());
|
|
|
|
} else {
|
|
|
|
warn!("checkpoint too old, consider using a more recent block");
|
|
|
|
}
|
2022-11-14 20:23:51 +00:00
|
|
|
}
|
|
|
|
|
2022-11-03 19:24:17 +00:00
|
|
|
let committee_valid = is_current_committee_proof_valid(
|
|
|
|
&bootstrap.header,
|
|
|
|
&mut bootstrap.current_sync_committee,
|
|
|
|
&bootstrap.current_sync_committee_branch,
|
|
|
|
);
|
|
|
|
|
|
|
|
let header_hash = bootstrap.header.hash_tree_root()?.to_string();
|
|
|
|
let expected_hash = format!("0x{}", hex::encode(&self.initial_checkpoint));
|
|
|
|
let header_valid = header_hash == expected_hash;
|
|
|
|
|
|
|
|
if !header_valid {
|
|
|
|
return Err(ConsensusError::InvalidHeaderHash(expected_hash, header_hash).into());
|
|
|
|
}
|
|
|
|
|
|
|
|
if !committee_valid {
|
|
|
|
return Err(ConsensusError::InvalidCurrentSyncCommitteeProof.into());
|
|
|
|
}
|
|
|
|
|
|
|
|
self.store = LightClientStore {
|
|
|
|
finalized_header: bootstrap.header.clone(),
|
|
|
|
current_sync_committee: bootstrap.current_sync_committee,
|
|
|
|
next_sync_committee: None,
|
|
|
|
optimistic_header: bootstrap.header.clone(),
|
|
|
|
previous_max_active_participants: 0,
|
|
|
|
current_max_active_participants: 0,
|
|
|
|
};
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2022-11-02 03:52:28 +00:00
|
|
|
// implements checks from validate_light_client_update and process_light_client_update in the
|
|
|
|
// specification
|
2022-09-28 20:48:24 +00:00
|
|
|
fn verify_generic_update(&self, update: &GenericUpdate) -> Result<()> {
|
|
|
|
let bits = get_bits(&update.sync_aggregate.sync_committee_bits);
|
|
|
|
if bits == 0 {
|
2022-09-29 23:35:43 +00:00
|
|
|
return Err(ConsensusError::InsufficientParticipation.into());
|
2022-09-28 20:48:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
let update_finalized_slot = update.finalized_header.clone().unwrap_or_default().slot;
|
2022-09-28 21:50:39 +00:00
|
|
|
let valid_time = self.expected_current_slot() >= update.signature_slot
|
2022-09-28 20:48:24 +00:00
|
|
|
&& update.signature_slot > update.attested_header.slot
|
|
|
|
&& update.attested_header.slot >= update_finalized_slot;
|
|
|
|
|
|
|
|
if !valid_time {
|
2022-09-29 23:35:43 +00:00
|
|
|
return Err(ConsensusError::InvalidTimestamp.into());
|
2022-09-28 20:48:24 +00:00
|
|
|
}
|
|
|
|
|
2022-08-31 00:31:58 +00:00
|
|
|
let store_period = calc_sync_period(self.store.finalized_header.slot);
|
2022-09-28 20:48:24 +00:00
|
|
|
let update_sig_period = calc_sync_period(update.signature_slot);
|
|
|
|
let valid_period = if self.store.next_sync_committee.is_some() {
|
|
|
|
update_sig_period == store_period || update_sig_period == store_period + 1
|
|
|
|
} else {
|
|
|
|
update_sig_period == store_period
|
|
|
|
};
|
2022-08-20 20:33:32 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if !valid_period {
|
2022-09-29 23:35:43 +00:00
|
|
|
return Err(ConsensusError::InvalidPeriod.into());
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
let update_attested_period = calc_sync_period(update.attested_header.slot);
|
|
|
|
let update_has_next_committee = self.store.next_sync_committee.is_none()
|
|
|
|
&& update.next_sync_committee.is_some()
|
|
|
|
&& update_attested_period == store_period;
|
|
|
|
|
|
|
|
if update.attested_header.slot <= self.store.finalized_header.slot
|
|
|
|
&& !update_has_next_committee
|
2022-08-20 20:33:32 +00:00
|
|
|
{
|
2022-09-29 23:35:43 +00:00
|
|
|
return Err(ConsensusError::NotRelevant.into());
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if update.finalized_header.is_some() && update.finality_branch.is_some() {
|
|
|
|
let is_valid = is_finality_proof_valid(
|
|
|
|
&update.attested_header,
|
|
|
|
&mut update.finalized_header.clone().unwrap(),
|
|
|
|
&update.finality_branch.clone().unwrap(),
|
|
|
|
);
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if !is_valid {
|
2022-09-29 23:35:43 +00:00
|
|
|
return Err(ConsensusError::InvalidFinalityProof.into());
|
2022-09-28 20:48:24 +00:00
|
|
|
}
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if update.next_sync_committee.is_some() && update.next_sync_committee_branch.is_some() {
|
|
|
|
let is_valid = is_next_committee_proof_valid(
|
|
|
|
&update.attested_header,
|
|
|
|
&mut update.next_sync_committee.clone().unwrap(),
|
|
|
|
&update.next_sync_committee_branch.clone().unwrap(),
|
|
|
|
);
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if !is_valid {
|
2022-09-29 23:35:43 +00:00
|
|
|
return Err(ConsensusError::InvalidNextSyncCommitteeProof.into());
|
2022-09-28 20:48:24 +00:00
|
|
|
}
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
let sync_committee = if update_sig_period == store_period {
|
2022-08-19 22:43:58 +00:00
|
|
|
&self.store.current_sync_committee
|
|
|
|
} else {
|
|
|
|
self.store.next_sync_committee.as_ref().unwrap()
|
|
|
|
};
|
|
|
|
|
2022-08-20 20:33:32 +00:00
|
|
|
let pks =
|
2022-09-28 20:48:24 +00:00
|
|
|
get_participating_keys(sync_committee, &update.sync_aggregate.sync_committee_bits)?;
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-11-02 03:52:28 +00:00
|
|
|
let is_valid_sig = self.verify_sync_committee_signture(
|
|
|
|
&pks,
|
|
|
|
&update.attested_header,
|
|
|
|
&update.sync_aggregate.sync_committee_signature,
|
|
|
|
update.signature_slot,
|
|
|
|
);
|
2022-08-19 22:43:58 +00:00
|
|
|
|
|
|
|
if !is_valid_sig {
|
2022-09-29 23:35:43 +00:00
|
|
|
return Err(ConsensusError::InvalidSignature.into());
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
Ok(())
|
|
|
|
}
|
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
fn verify_update(&self, update: &Update) -> Result<()> {
|
|
|
|
let update = GenericUpdate::from(update);
|
|
|
|
self.verify_generic_update(&update)
|
|
|
|
}
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
fn verify_finality_update(&self, update: &FinalityUpdate) -> Result<()> {
|
|
|
|
let update = GenericUpdate::from(update);
|
|
|
|
self.verify_generic_update(&update)
|
|
|
|
}
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
fn verify_optimistic_update(&self, update: &OptimisticUpdate) -> Result<()> {
|
|
|
|
let update = GenericUpdate::from(update);
|
|
|
|
self.verify_generic_update(&update)
|
|
|
|
}
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-11-02 03:52:28 +00:00
|
|
|
// implements state changes from apply_light_client_update and process_light_client_update in
|
|
|
|
// the specification
|
2022-09-28 20:48:24 +00:00
|
|
|
fn apply_generic_update(&mut self, update: &GenericUpdate) {
|
|
|
|
let committee_bits = get_bits(&update.sync_aggregate.sync_committee_bits);
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
self.store.current_max_active_participants =
|
|
|
|
u64::max(self.store.current_max_active_participants, committee_bits);
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-11-02 03:52:28 +00:00
|
|
|
let should_update_optimistic = committee_bits > self.safety_threshold()
|
2022-09-28 20:48:24 +00:00
|
|
|
&& update.attested_header.slot > self.store.optimistic_header.slot;
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if should_update_optimistic {
|
|
|
|
self.store.optimistic_header = update.attested_header.clone();
|
|
|
|
self.log_optimistic_update(update);
|
2022-08-31 00:31:58 +00:00
|
|
|
}
|
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
let update_attested_period = calc_sync_period(update.attested_header.slot);
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
let update_finalized_slot = update
|
|
|
|
.finalized_header
|
|
|
|
.as_ref()
|
|
|
|
.map(|h| h.slot)
|
|
|
|
.unwrap_or(0);
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
let update_finalized_period = calc_sync_period(update_finalized_slot);
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
let update_has_finalized_next_committee = self.store.next_sync_committee.is_none()
|
|
|
|
&& self.has_sync_update(update)
|
|
|
|
&& self.has_finality_update(update)
|
|
|
|
&& update_finalized_period == update_attested_period;
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
let should_apply_update = {
|
|
|
|
let has_majority = committee_bits * 3 >= 512 * 2;
|
|
|
|
let update_is_newer = update_finalized_slot > self.store.finalized_header.slot;
|
|
|
|
let good_update = update_is_newer || update_has_finalized_next_committee;
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
has_majority && good_update
|
|
|
|
};
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if should_apply_update {
|
|
|
|
let store_period = calc_sync_period(self.store.finalized_header.slot);
|
|
|
|
|
|
|
|
if self.store.next_sync_committee.is_none() {
|
|
|
|
self.store.next_sync_committee = update.next_sync_committee.clone();
|
|
|
|
} else if update_finalized_period == store_period + 1 {
|
|
|
|
info!("sync committee updated");
|
|
|
|
self.store.current_sync_committee = self.store.next_sync_committee.clone().unwrap();
|
|
|
|
self.store.next_sync_committee = update.next_sync_committee.clone();
|
|
|
|
self.store.previous_max_active_participants =
|
|
|
|
self.store.current_max_active_participants;
|
|
|
|
self.store.current_max_active_participants = 0;
|
|
|
|
}
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if update_finalized_slot > self.store.finalized_header.slot {
|
|
|
|
self.store.finalized_header = update.finalized_header.clone().unwrap();
|
|
|
|
self.log_finality_update(update);
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if self.store.finalized_header.slot % 32 == 0 {
|
|
|
|
let checkpoint_res = self.store.finalized_header.hash_tree_root();
|
|
|
|
if let Ok(checkpoint) = checkpoint_res {
|
|
|
|
self.last_checkpoint = Some(checkpoint.as_bytes().to_vec());
|
|
|
|
}
|
|
|
|
}
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
if self.store.finalized_header.slot > self.store.optimistic_header.slot {
|
|
|
|
self.store.optimistic_header = self.store.finalized_header.clone();
|
|
|
|
}
|
|
|
|
}
|
2022-08-31 00:31:58 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-19 22:43:58 +00:00
|
|
|
fn apply_update(&mut self, update: &Update) {
|
2022-09-28 20:48:24 +00:00
|
|
|
let update = GenericUpdate::from(update);
|
|
|
|
self.apply_generic_update(&update);
|
|
|
|
}
|
2022-09-16 19:32:15 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
fn apply_finality_update(&mut self, update: &FinalityUpdate) {
|
|
|
|
let update = GenericUpdate::from(update);
|
|
|
|
self.apply_generic_update(&update);
|
|
|
|
}
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
fn log_finality_update(&self, update: &GenericUpdate) {
|
2022-09-09 01:34:14 +00:00
|
|
|
let participation =
|
|
|
|
get_bits(&update.sync_aggregate.sync_committee_bits) as f32 / 512_f32 * 100f32;
|
2022-09-28 20:48:24 +00:00
|
|
|
let decimals = if participation == 100.0 { 1 } else { 2 };
|
|
|
|
let age = self.age(self.store.finalized_header.slot);
|
2022-09-09 01:34:14 +00:00
|
|
|
|
|
|
|
info!(
|
2022-09-28 20:48:24 +00:00
|
|
|
"finalized slot slot={} confidence={:.decimals$}% age={:02}:{:02}:{:02}:{:02}",
|
2022-09-09 01:34:14 +00:00
|
|
|
self.store.finalized_header.slot,
|
|
|
|
participation,
|
2022-09-28 20:48:24 +00:00
|
|
|
age.num_days(),
|
|
|
|
age.num_hours() % 24,
|
|
|
|
age.num_minutes() % 60,
|
|
|
|
age.num_seconds() % 60,
|
2022-08-31 00:31:58 +00:00
|
|
|
);
|
|
|
|
}
|
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
fn apply_optimistic_update(&mut self, update: &OptimisticUpdate) {
|
|
|
|
let update = GenericUpdate::from(update);
|
|
|
|
self.apply_generic_update(&update);
|
|
|
|
}
|
2022-09-16 19:32:15 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
fn log_optimistic_update(&self, update: &GenericUpdate) {
|
|
|
|
let participation =
|
|
|
|
get_bits(&update.sync_aggregate.sync_committee_bits) as f32 / 512_f32 * 100f32;
|
|
|
|
let decimals = if participation == 100.0 { 1 } else { 2 };
|
|
|
|
let age = self.age(self.store.optimistic_header.slot);
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
info!(
|
|
|
|
"updated head slot={} confidence={:.decimals$}% age={:02}:{:02}:{:02}:{:02}",
|
|
|
|
self.store.optimistic_header.slot,
|
|
|
|
participation,
|
|
|
|
age.num_days(),
|
|
|
|
age.num_hours() % 24,
|
|
|
|
age.num_minutes() % 60,
|
|
|
|
age.num_seconds() % 60,
|
|
|
|
);
|
2022-08-31 00:31:58 +00:00
|
|
|
}
|
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
fn has_finality_update(&self, update: &GenericUpdate) -> bool {
|
|
|
|
update.finalized_header.is_some() && update.finality_branch.is_some()
|
|
|
|
}
|
|
|
|
|
|
|
|
fn has_sync_update(&self, update: &GenericUpdate) -> bool {
|
|
|
|
update.next_sync_committee.is_some() && update.next_sync_committee_branch.is_some()
|
|
|
|
}
|
2022-08-31 00:31:58 +00:00
|
|
|
|
2022-11-02 03:52:28 +00:00
|
|
|
fn safety_threshold(&self) -> u64 {
|
2022-09-28 20:48:24 +00:00
|
|
|
cmp::max(
|
2022-08-31 00:31:58 +00:00
|
|
|
self.store.current_max_active_participants,
|
|
|
|
self.store.previous_max_active_participants,
|
2022-09-28 20:48:24 +00:00
|
|
|
) / 2
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
2022-08-27 00:05:12 +00:00
|
|
|
|
2022-11-02 03:52:28 +00:00
|
|
|
fn verify_sync_committee_signture(
|
|
|
|
&self,
|
2022-11-30 01:31:25 +00:00
|
|
|
pks: &[PublicKey],
|
2022-11-02 03:52:28 +00:00
|
|
|
attested_header: &Header,
|
|
|
|
signature: &SignatureBytes,
|
|
|
|
signature_slot: u64,
|
|
|
|
) -> bool {
|
|
|
|
let res: Result<bool> = (move || {
|
2022-11-30 01:31:25 +00:00
|
|
|
let pks: Vec<&PublicKey> = pks.iter().collect();
|
2022-11-02 03:52:28 +00:00
|
|
|
let header_root =
|
|
|
|
bytes_to_bytes32(attested_header.clone().hash_tree_root()?.as_bytes());
|
|
|
|
let signing_root = self.compute_committee_sign_root(header_root, signature_slot)?;
|
|
|
|
|
|
|
|
Ok(is_aggregate_valid(signature, signing_root.as_bytes(), &pks))
|
|
|
|
})();
|
|
|
|
|
|
|
|
if let Ok(is_valid) = res {
|
|
|
|
is_valid
|
|
|
|
} else {
|
|
|
|
false
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2022-08-27 00:05:12 +00:00
|
|
|
fn compute_committee_sign_root(&self, header: Bytes32, slot: u64) -> Result<Node> {
|
2022-10-05 17:52:07 +00:00
|
|
|
let genesis_root = self.config.chain.genesis_root.to_vec().try_into().unwrap();
|
2022-08-27 00:05:12 +00:00
|
|
|
|
|
|
|
let domain_type = &hex::decode("07000000")?[..];
|
|
|
|
let fork_version = Vector::from_iter(self.config.fork_version(slot));
|
|
|
|
let domain = compute_domain(domain_type, fork_version, genesis_root)?;
|
|
|
|
compute_signing_root(header, domain)
|
|
|
|
}
|
2022-09-09 01:34:14 +00:00
|
|
|
|
2022-09-28 20:48:24 +00:00
|
|
|
fn age(&self, slot: u64) -> Duration {
|
|
|
|
let expected_time = self.slot_timestamp(slot);
|
2022-09-09 01:34:14 +00:00
|
|
|
let now = std::time::SystemTime::now()
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
.unwrap();
|
|
|
|
let delay = now - std::time::Duration::from_secs(expected_time);
|
|
|
|
chrono::Duration::from_std(delay).unwrap()
|
|
|
|
}
|
2022-09-28 20:48:24 +00:00
|
|
|
|
2022-09-28 21:50:39 +00:00
|
|
|
pub fn expected_current_slot(&self) -> u64 {
|
2022-09-28 20:48:24 +00:00
|
|
|
let now = std::time::SystemTime::now()
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
.unwrap();
|
|
|
|
|
2022-10-05 17:52:07 +00:00
|
|
|
let genesis_time = self.config.chain.genesis_time;
|
2022-09-28 20:48:24 +00:00
|
|
|
let since_genesis = now - std::time::Duration::from_secs(genesis_time);
|
|
|
|
|
|
|
|
since_genesis.as_secs() / 12
|
|
|
|
}
|
|
|
|
|
|
|
|
fn slot_timestamp(&self, slot: u64) -> u64 {
|
2022-10-05 17:52:07 +00:00
|
|
|
slot * 12 + self.config.chain.genesis_time
|
2022-09-28 20:48:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/// Gets the duration until the next update
|
|
|
|
/// Updates are scheduled for 4 seconds into each slot
|
|
|
|
pub fn duration_until_next_update(&self) -> Duration {
|
2022-09-28 21:50:39 +00:00
|
|
|
let current_slot = self.expected_current_slot();
|
2022-09-28 20:48:24 +00:00
|
|
|
let next_slot = current_slot + 1;
|
|
|
|
let next_slot_timestamp = self.slot_timestamp(next_slot);
|
|
|
|
|
|
|
|
let now = std::time::SystemTime::now()
|
|
|
|
.duration_since(UNIX_EPOCH)
|
|
|
|
.unwrap()
|
|
|
|
.as_secs();
|
|
|
|
|
|
|
|
let time_to_next_slot = next_slot_timestamp - now;
|
|
|
|
let next_update = time_to_next_slot + 4;
|
|
|
|
|
|
|
|
Duration::seconds(next_update as i64)
|
|
|
|
}
|
2022-11-14 20:23:51 +00:00
|
|
|
|
|
|
|
// Determines blockhash_slot age and returns true if it is less than 14 days old
|
|
|
|
fn is_valid_checkpoint(&self, blockhash_slot: u64) -> bool {
|
|
|
|
let current_slot = self.expected_current_slot();
|
|
|
|
let current_slot_timestamp = self.slot_timestamp(current_slot);
|
|
|
|
let blockhash_slot_timestamp = self.slot_timestamp(blockhash_slot);
|
|
|
|
|
|
|
|
let slot_age = current_slot_timestamp - blockhash_slot_timestamp;
|
|
|
|
|
|
|
|
slot_age < self.config.max_checkpoint_age
|
|
|
|
}
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-08-20 20:33:32 +00:00
|
|
|
fn get_participating_keys(
|
|
|
|
committee: &SyncCommittee,
|
|
|
|
bitfield: &Bitvector<512>,
|
|
|
|
) -> Result<Vec<PublicKey>> {
|
|
|
|
let mut pks: Vec<PublicKey> = Vec::new();
|
|
|
|
bitfield.iter().enumerate().for_each(|(i, bit)| {
|
|
|
|
if bit == true {
|
|
|
|
let pk = &committee.pubkeys[i];
|
2022-11-30 01:31:25 +00:00
|
|
|
let pk = PublicKey::from_bytes(pk).unwrap();
|
2022-08-20 20:33:32 +00:00
|
|
|
pks.push(pk);
|
|
|
|
}
|
|
|
|
});
|
2022-08-19 22:43:58 +00:00
|
|
|
|
2022-08-20 20:33:32 +00:00
|
|
|
Ok(pks)
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
2022-08-31 00:31:58 +00:00
|
|
|
fn get_bits(bitfield: &Bitvector<512>) -> u64 {
|
|
|
|
let mut count = 0;
|
|
|
|
bitfield.iter().for_each(|bit| {
|
|
|
|
if bit == true {
|
|
|
|
count += 1;
|
|
|
|
}
|
|
|
|
});
|
|
|
|
|
|
|
|
count
|
|
|
|
}
|
|
|
|
|
2022-08-20 20:33:32 +00:00
|
|
|
fn is_finality_proof_valid(
|
|
|
|
attested_header: &Header,
|
|
|
|
finality_header: &mut Header,
|
2022-11-30 01:31:25 +00:00
|
|
|
finality_branch: &[Bytes32],
|
2022-08-20 20:33:32 +00:00
|
|
|
) -> bool {
|
2022-11-02 03:52:28 +00:00
|
|
|
is_proof_valid(attested_header, finality_header, finality_branch, 6, 41)
|
2022-08-19 22:43:58 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
fn is_next_committee_proof_valid(
|
|
|
|
attested_header: &Header,
|
|
|
|
next_committee: &mut SyncCommittee,
|
2022-11-30 01:31:25 +00:00
|
|
|
next_committee_branch: &[Bytes32],
|
2022-08-19 22:43:58 +00:00
|
|
|
) -> bool {
|
2022-11-02 03:52:28 +00:00
|
|
|
is_proof_valid(
|
|
|
|
attested_header,
|
|
|
|
next_committee,
|
|
|
|
next_committee_branch,
|
2022-08-19 22:43:58 +00:00
|
|
|
5,
|
|
|
|
23,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
fn is_current_committee_proof_valid(
|
|
|
|
attested_header: &Header,
|
|
|
|
current_committee: &mut SyncCommittee,
|
2022-11-30 01:31:25 +00:00
|
|
|
current_committee_branch: &[Bytes32],
|
2022-08-19 22:43:58 +00:00
|
|
|
) -> bool {
|
2022-11-02 03:52:28 +00:00
|
|
|
is_proof_valid(
|
|
|
|
attested_header,
|
|
|
|
current_committee,
|
|
|
|
current_committee_branch,
|
2022-08-19 22:43:58 +00:00
|
|
|
5,
|
|
|
|
22,
|
|
|
|
)
|
|
|
|
}
|
|
|
|
|
2022-09-04 23:32:16 +00:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use std::sync::Arc;
|
|
|
|
|
2022-11-08 21:24:55 +00:00
|
|
|
use crate::constants::MAX_REQUEST_LIGHT_CLIENT_UPDATES;
|
2022-09-04 23:32:16 +00:00
|
|
|
use ssz_rs::Vector;
|
|
|
|
|
|
|
|
use crate::{
|
|
|
|
consensus::calc_sync_period,
|
2022-09-29 23:35:43 +00:00
|
|
|
errors::ConsensusError,
|
2022-11-02 03:52:28 +00:00
|
|
|
rpc::{mock_rpc::MockRpc, ConsensusRpc},
|
2022-09-04 23:32:16 +00:00
|
|
|
types::Header,
|
|
|
|
ConsensusClient,
|
|
|
|
};
|
2022-10-05 17:52:07 +00:00
|
|
|
use config::{networks, Config};
|
2022-09-04 23:32:16 +00:00
|
|
|
|
2023-01-19 02:18:26 +00:00
|
|
|
async fn get_client(strict_checkpoint_age: bool) -> ConsensusClient<MockRpc> {
|
2022-10-05 17:52:07 +00:00
|
|
|
let base_config = networks::goerli();
|
|
|
|
let config = Config {
|
|
|
|
consensus_rpc: String::new(),
|
|
|
|
execution_rpc: String::new(),
|
|
|
|
chain: base_config.chain,
|
|
|
|
forks: base_config.forks,
|
2023-01-19 02:18:26 +00:00
|
|
|
strict_checkpoint_age,
|
2022-10-05 17:52:07 +00:00
|
|
|
..Default::default()
|
|
|
|
};
|
|
|
|
|
2022-11-04 15:05:18 +00:00
|
|
|
let checkpoint =
|
|
|
|
hex::decode("1e591af1e90f2db918b2a132991c7c2ee9a4ab26da496bd6e71e4f0bd65ea870")
|
|
|
|
.unwrap();
|
|
|
|
|
|
|
|
let mut client = ConsensusClient::new("testdata/", &checkpoint, Arc::new(config)).unwrap();
|
2022-11-03 19:24:17 +00:00
|
|
|
client.bootstrap().await.unwrap();
|
|
|
|
client
|
2022-09-04 23:32:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn test_verify_update() {
|
2023-01-19 02:18:26 +00:00
|
|
|
let client = get_client(false).await;
|
2022-09-04 23:32:16 +00:00
|
|
|
let period = calc_sync_period(client.store.finalized_header.slot);
|
2022-11-08 21:24:55 +00:00
|
|
|
let updates = client
|
|
|
|
.rpc
|
|
|
|
.get_updates(period, MAX_REQUEST_LIGHT_CLIENT_UPDATES)
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-09-04 23:32:16 +00:00
|
|
|
|
2022-11-30 01:31:25 +00:00
|
|
|
let update = updates[0].clone();
|
|
|
|
client.verify_update(&update).unwrap();
|
2022-09-04 23:32:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn test_verify_update_invalid_committee() {
|
2023-01-19 02:18:26 +00:00
|
|
|
let client = get_client(false).await;
|
2022-09-04 23:32:16 +00:00
|
|
|
let period = calc_sync_period(client.store.finalized_header.slot);
|
2022-11-08 21:24:55 +00:00
|
|
|
let updates = client
|
|
|
|
.rpc
|
|
|
|
.get_updates(period, MAX_REQUEST_LIGHT_CLIENT_UPDATES)
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-09-04 23:32:16 +00:00
|
|
|
|
|
|
|
let mut update = updates[0].clone();
|
|
|
|
update.next_sync_committee.pubkeys[0] = Vector::default();
|
|
|
|
|
2022-11-30 01:31:25 +00:00
|
|
|
let err = client.verify_update(&update).err().unwrap();
|
2022-09-29 23:35:43 +00:00
|
|
|
assert_eq!(
|
|
|
|
err.to_string(),
|
|
|
|
ConsensusError::InvalidNextSyncCommitteeProof.to_string()
|
|
|
|
);
|
2022-09-04 23:32:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
2022-11-02 03:52:28 +00:00
|
|
|
async fn test_verify_update_invalid_finality() {
|
2023-01-19 02:18:26 +00:00
|
|
|
let client = get_client(false).await;
|
2022-09-04 23:32:16 +00:00
|
|
|
let period = calc_sync_period(client.store.finalized_header.slot);
|
2022-11-08 21:24:55 +00:00
|
|
|
let updates = client
|
|
|
|
.rpc
|
|
|
|
.get_updates(period, MAX_REQUEST_LIGHT_CLIENT_UPDATES)
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-09-04 23:32:16 +00:00
|
|
|
|
|
|
|
let mut update = updates[0].clone();
|
|
|
|
update.finalized_header = Header::default();
|
|
|
|
|
2022-12-04 20:28:44 +00:00
|
|
|
let err = client.verify_update(&update).err().unwrap();
|
2022-09-29 23:35:43 +00:00
|
|
|
assert_eq!(
|
|
|
|
err.to_string(),
|
|
|
|
ConsensusError::InvalidFinalityProof.to_string()
|
|
|
|
);
|
2022-09-04 23:32:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn test_verify_update_invalid_sig() {
|
2023-01-19 02:18:26 +00:00
|
|
|
let client = get_client(false).await;
|
2022-09-04 23:32:16 +00:00
|
|
|
let period = calc_sync_period(client.store.finalized_header.slot);
|
2022-11-08 21:24:55 +00:00
|
|
|
let updates = client
|
|
|
|
.rpc
|
|
|
|
.get_updates(period, MAX_REQUEST_LIGHT_CLIENT_UPDATES)
|
|
|
|
.await
|
|
|
|
.unwrap();
|
2022-09-04 23:32:16 +00:00
|
|
|
|
|
|
|
let mut update = updates[0].clone();
|
|
|
|
update.sync_aggregate.sync_committee_signature = Vector::default();
|
|
|
|
|
2022-12-04 20:28:44 +00:00
|
|
|
let err = client.verify_update(&update).err().unwrap();
|
2022-09-29 23:35:43 +00:00
|
|
|
assert_eq!(
|
|
|
|
err.to_string(),
|
|
|
|
ConsensusError::InvalidSignature.to_string()
|
|
|
|
);
|
2022-09-04 23:32:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn test_verify_finality() {
|
2023-01-19 02:18:26 +00:00
|
|
|
let mut client = get_client(false).await;
|
2022-09-04 23:32:16 +00:00
|
|
|
client.sync().await.unwrap();
|
|
|
|
|
|
|
|
let update = client.rpc.get_finality_update().await.unwrap();
|
|
|
|
|
|
|
|
client.verify_finality_update(&update).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
2022-11-02 03:52:28 +00:00
|
|
|
async fn test_verify_finality_invalid_finality() {
|
2023-01-19 02:18:26 +00:00
|
|
|
let mut client = get_client(false).await;
|
2022-09-04 23:32:16 +00:00
|
|
|
client.sync().await.unwrap();
|
|
|
|
|
|
|
|
let mut update = client.rpc.get_finality_update().await.unwrap();
|
|
|
|
update.finalized_header = Header::default();
|
|
|
|
|
2022-09-29 23:35:43 +00:00
|
|
|
let err = client.verify_finality_update(&update).err().unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
err.to_string(),
|
|
|
|
ConsensusError::InvalidFinalityProof.to_string()
|
|
|
|
);
|
2022-09-04 23:32:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
2022-11-02 03:52:28 +00:00
|
|
|
async fn test_verify_finality_invalid_sig() {
|
2023-01-19 02:18:26 +00:00
|
|
|
let mut client = get_client(false).await;
|
2022-09-04 23:32:16 +00:00
|
|
|
client.sync().await.unwrap();
|
|
|
|
|
|
|
|
let mut update = client.rpc.get_finality_update().await.unwrap();
|
|
|
|
update.sync_aggregate.sync_committee_signature = Vector::default();
|
|
|
|
|
2022-09-29 23:35:43 +00:00
|
|
|
let err = client.verify_finality_update(&update).err().unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
err.to_string(),
|
|
|
|
ConsensusError::InvalidSignature.to_string()
|
|
|
|
);
|
2022-09-04 23:32:16 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn test_verify_optimistic() {
|
2023-01-19 02:18:26 +00:00
|
|
|
let mut client = get_client(false).await;
|
2022-09-04 23:32:16 +00:00
|
|
|
client.sync().await.unwrap();
|
|
|
|
|
|
|
|
let update = client.rpc.get_optimistic_update().await.unwrap();
|
|
|
|
client.verify_optimistic_update(&update).unwrap();
|
|
|
|
}
|
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
async fn test_verify_optimistic_invalid_sig() {
|
2023-01-19 02:18:26 +00:00
|
|
|
let mut client = get_client(false).await;
|
2022-09-04 23:32:16 +00:00
|
|
|
client.sync().await.unwrap();
|
|
|
|
|
|
|
|
let mut update = client.rpc.get_optimistic_update().await.unwrap();
|
|
|
|
update.sync_aggregate.sync_committee_signature = Vector::default();
|
|
|
|
|
2022-09-29 23:35:43 +00:00
|
|
|
let err = client.verify_optimistic_update(&update).err().unwrap();
|
|
|
|
assert_eq!(
|
|
|
|
err.to_string(),
|
|
|
|
ConsensusError::InvalidSignature.to_string()
|
|
|
|
);
|
2022-09-04 23:32:16 +00:00
|
|
|
}
|
2022-11-14 20:23:51 +00:00
|
|
|
|
|
|
|
#[tokio::test]
|
|
|
|
#[should_panic]
|
|
|
|
async fn test_verify_checkpoint_age_invalid() {
|
2023-01-19 02:18:26 +00:00
|
|
|
get_client(true).await;
|
2022-11-14 20:23:51 +00:00
|
|
|
}
|
2022-09-04 23:32:16 +00:00
|
|
|
}
|