1#[cfg(feature = "client")]
2use std::{
3 cmp::min,
4 collections::HashMap,
5 path::PathBuf,
6 str::FromStr,
7 sync::atomic::{AtomicI64, Ordering},
8 time::Duration,
9};
10#[cfg(all(unix, feature = "client"))]
11use std::{fs::Permissions, os::unix::fs::PermissionsExt};
12use std::{future::Future, sync::Arc};
13
14#[cfg(feature = "client")]
15use alloy::primitives::Address;
16#[cfg(feature = "client")]
17use anyhow::Context;
18use anyhow::Result;
19#[cfg(feature = "client")]
20use derivative::Derivative;
21use derive_more::{Display, From};
22#[cfg(feature = "client")]
23use espresso_types::{BackoffParams, PubKey, Ratio, v0_3::RegisteredValidator};
24use espresso_types::{SeqTypes, StakeTableState};
25#[cfg(feature = "client")]
26use futures::TryStreamExt;
27#[cfg(feature = "client")]
28use hotshot_query_service::data_source::storage::sql::release_only_idle;
29#[cfg(feature = "client")]
30use hotshot_query_service_types::HeightIndexed;
31use hotshot_query_service_types::availability::{BlockId, LeafId, LeafQueryData};
32use hotshot_types::data::EpochNumber;
33#[cfg(feature = "client")]
34use hotshot_types::{light_client::StateVerKey, x25519};
35#[cfg(feature = "client")]
36use serde::{Deserialize, Serialize};
37#[cfg(feature = "client")]
38use serde_json::Value;
39#[cfg(feature = "client")]
40use sqlx::{
41 ConnectOptions, Executor, PgPool, QueryBuilder, SqlitePool,
42 postgres::{PgConnectOptions, PgPoolOptions},
43 query, query_as,
44 sqlite::SqlitePoolOptions,
45};
46#[cfg(feature = "client")]
47use tempfile::{Builder, TempDir};
48#[cfg(feature = "client")]
49use tokio::runtime::Handle;
50use vbs::version::Version;
51
52#[derive(Clone, Copy, Debug, Display, From)]
54pub enum LeafRequest {
55 #[display("leaf {_0}")]
57 Leaf(LeafId<SeqTypes>),
58
59 #[display("header {_0}")]
61 Header(BlockId<SeqTypes>),
62}
63
64#[cfg(feature = "client")]
66const WRITE_RETRY_MAX: u32 = 5;
67
68#[cfg(feature = "client")]
70const WRITE_BACKOFF: BackoffParams = BackoffParams::new(
71 Duration::from_millis(50),
72 Duration::from_millis(1_000),
73 2,
74 Ratio {
75 numerator: 5,
76 denominator: 10,
77 },
78);
79
80#[cfg(feature = "client")]
81#[derive(Clone, Debug)]
82enum DbPool {
83 Sqlite(SqlitePool),
84 Postgres(PgPool),
85}
86
87#[cfg(feature = "client")]
93macro_rules! with_pool {
94 ($pool:expr, |$p:ident| $body:expr) => {
95 match $pool {
96 DbPool::Sqlite($p) => $body,
97 DbPool::Postgres($p) => $body,
98 }
99 };
100}
101
102#[cfg(feature = "client")]
109#[derive(Debug)]
110struct Recency {
111 next_tick: AtomicI64,
114 dirty: std::sync::Mutex<HashMap<i64, i64>>,
116 pool: DbPool,
118}
119
120#[cfg(feature = "client")]
121impl Recency {
122 fn touch(&self, height: i64) {
123 let t = self.next_tick.fetch_add(1, Ordering::Relaxed);
124 self.dirty.lock().unwrap().insert(height, t);
125 }
126
127 fn drain(&self) -> Vec<(i64, i64)> {
128 self.dirty.lock().unwrap().drain().collect()
129 }
130
131 async fn flush(pool: DbPool, pending: Vec<(i64, i64)>) {
134 let res: sqlx::Result<()> = async {
135 with_pool!(&pool, |pool| {
136 let mut tx = pool.begin().await?;
137 for (h, tick) in &pending {
138 query("UPDATE leaf SET last_used = $1 WHERE height = $2")
139 .bind(tick)
140 .bind(h)
141 .execute(tx.as_mut())
142 .await?;
143 }
144 tx.commit().await
145 })
146 }
147 .await;
148 if let Err(err) = res {
149 tracing::warn!(%err, "failed to flush LRU recency");
150 }
151 }
152}
153
154#[cfg(feature = "client")]
155impl Drop for Recency {
156 fn drop(&mut self) {
166 let pending = self.drain();
167 if pending.is_empty() {
168 return;
169 }
170 let flush = Self::flush(self.pool.clone(), pending);
171 match Handle::try_current() {
172 Ok(handle) => {
173 handle.spawn(flush);
174 },
175 Err(_) => futures::executor::block_on(flush),
176 }
177 }
178}
179
180pub trait Storage: Sized + Send + Sync + 'static {
182 fn default() -> impl Send + Future<Output = Result<Self>>;
187
188 fn block_height(&self) -> impl Send + Future<Output = Result<u64>>;
195
196 fn leaf_upper_bound(
204 &self,
205 leaf: impl Into<LeafRequest> + Send,
206 ) -> impl Send + Future<Output = Result<Option<LeafQueryData<SeqTypes>>>>;
207
208 fn get_leaves_in_range(
210 &self,
211 start: u32,
212 end: u32,
213 ) -> impl Send + Future<Output = Result<Vec<LeafQueryData<SeqTypes>>>>;
214
215 fn insert_leaf(&self, leaf: LeafQueryData<SeqTypes>)
219 -> impl Send + Future<Output = Result<()>>;
220
221 fn stake_table_lower_bound(
228 &self,
229 epoch: EpochNumber,
230 ) -> impl Send + Future<Output = Result<Option<(EpochNumber, StakeTableState, Version, Version)>>>;
231
232 fn insert_stake_table(
242 &self,
243 epoch: EpochNumber,
244 stake_table: &StakeTableState,
245 epoch_root_protocol_version: Version,
246 next_epoch_root_protocol_version: Version,
247 ) -> impl Send + Future<Output = Result<()>>;
248}
249
250impl<T: Storage> Storage for Arc<T> {
251 async fn default() -> Result<Self> {
252 Ok(Arc::new(T::default().await?))
253 }
254
255 async fn block_height(&self) -> Result<u64> {
256 (**self).block_height().await
257 }
258
259 async fn leaf_upper_bound(
260 &self,
261 leaf: impl Into<LeafRequest> + Send,
262 ) -> Result<Option<LeafQueryData<SeqTypes>>> {
263 (**self).leaf_upper_bound(leaf).await
264 }
265
266 async fn get_leaves_in_range(
267 &self,
268 start: u32,
269 end: u32,
270 ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
271 (**self).get_leaves_in_range(start, end).await
272 }
273
274 async fn insert_leaf(&self, leaf: LeafQueryData<SeqTypes>) -> Result<()> {
275 (**self).insert_leaf(leaf).await
276 }
277
278 async fn stake_table_lower_bound(
279 &self,
280 epoch: EpochNumber,
281 ) -> Result<Option<(EpochNumber, StakeTableState, Version, Version)>> {
282 (**self).stake_table_lower_bound(epoch).await
283 }
284
285 async fn insert_stake_table(
286 &self,
287 epoch: EpochNumber,
288 stake_table: &StakeTableState,
289 epoch_root_protocol_version: Version,
290 next_epoch_root_protocol_version: Version,
291 ) -> Result<()> {
292 (**self)
293 .insert_stake_table(
294 epoch,
295 stake_table,
296 epoch_root_protocol_version,
297 next_epoch_root_protocol_version,
298 )
299 .await
300 }
301}
302
303#[cfg(feature = "client")]
304#[derive(Clone, Debug, Serialize, Deserialize)]
305#[cfg_attr(feature = "clap", derive(clap::Parser))]
306pub struct LightClientSqliteOptions {
307 #[cfg_attr(
309 feature = "clap",
310 clap(
311 long = "light-client-db-num-connections",
312 env = "LIGHT_CLIENT_DB_NUM_CONNECTIONS",
313 default_value = "5",
314 )
315 )]
316 pub num_connections: u32,
317
318 #[cfg_attr(
320 feature = "clap",
321 clap(
322 long = "light-client-db-num-leaves",
323 env = "LIGHT_CLIENT_DB_NUM_LEAVES",
324 default_value = "100",
325 )
326 )]
327 pub num_leaves: u32,
328
329 #[cfg_attr(
336 feature = "clap",
337 clap(long = "light-client-db-path", env = "LIGHT_CLIENT_DB_PATH")
338 )]
339 pub lc_path: Option<PathBuf>,
340}
341
342#[cfg(feature = "client")]
343impl Default for LightClientSqliteOptions {
344 fn default() -> Self {
345 Self {
346 num_connections: 5,
347 num_leaves: 100,
348 lc_path: None,
349 }
350 }
351}
352
353#[cfg(feature = "client")]
354impl LightClientSqliteOptions {
355 pub async fn connect(self) -> Result<SqlStorage> {
357 let (path, tmp) = match self.lc_path {
358 Some(p) => {
359 if let Some(parent) = p.parent().filter(|d| !d.as_os_str().is_empty()) {
360 std::fs::create_dir_all(parent)
361 .with_context(|| format!("creating parent directory {parent:?}"))?;
362 }
363 (p, None)
364 },
365 None => {
366 let mut builder = Builder::new();
367 builder.prefix("espresso-lc-");
368 #[cfg(unix)]
369 builder.permissions(Permissions::from_mode(0o700));
370 let dir = builder.tempdir().context(
371 "creating temporary directory for light client database; set \
372 LIGHT_CLIENT_DB_PATH to use a persistent location",
373 )?;
374 let path = dir.path().join("lc.db");
375 tracing::warn!(
376 path = %path.display(),
377 "LIGHT_CLIENT_DB_PATH is not set, so the light client database is in a \
378 temporary directory and its leaves and stake tables are lost on restart. Set \
379 LIGHT_CLIENT_DB_PATH to a persistent location"
380 );
381 (path, Some(Arc::new(dir)))
382 },
383 };
384
385 let opt = hotshot_query_service::sqlite_options::sqlite_options().filename(&path);
386 let pool = SqlitePoolOptions::default()
387 .max_connections(self.num_connections)
388 .connect_with(opt)
389 .await?;
390 sqlx::migrate!("./migrations").run(&pool).await?;
391 SqlStorage::new(DbPool::Sqlite(pool), self.num_leaves, tmp).await
392 }
393}
394
395#[cfg(feature = "client")]
400#[derive(Clone, Derivative)]
401#[derivative(Debug)]
402pub struct LightClientPostgresOptions {
403 #[derivative(Debug = "ignore")]
405 pub connect: PgConnectOptions,
406 pub num_connections: u32,
407 pub num_leaves: u32,
408}
409
410#[cfg(feature = "client")]
411impl LightClientPostgresOptions {
412 pub async fn connect(self) -> Result<SqlStorage> {
414 let pool = release_only_idle(PgPoolOptions::new().max_connections(self.num_connections))
415 .after_connect(|conn, _| {
416 Box::pin(async move {
417 conn.execute("SET search_path TO light_client").await?;
418 Ok(())
419 })
420 })
421 .connect_with(self.connect)
422 .await
423 .context("connecting to Postgres for the light client")?;
424 query("CREATE SCHEMA IF NOT EXISTS light_client")
425 .execute(&pool)
426 .await?;
427 sqlx::migrate!("./migrations-postgres").run(&pool).await?;
428 SqlStorage::new(DbPool::Postgres(pool), self.num_leaves, None).await
429 }
430
431 pub async fn reset(connect: &PgConnectOptions) -> Result<()> {
436 let mut conn = connect
437 .connect()
438 .await
439 .context("connecting to Postgres to reset the light client")?;
440 query("DROP SCHEMA IF EXISTS light_client CASCADE")
441 .execute(&mut conn)
442 .await?;
443 Ok(())
444 }
445}
446
447#[cfg(feature = "client")]
449#[derive(Clone, Debug)]
450pub enum LightClientDbOptions {
451 Sqlite(LightClientSqliteOptions),
452 Postgres(Box<LightClientPostgresOptions>),
453}
454
455#[cfg(feature = "client")]
456impl Default for LightClientDbOptions {
457 fn default() -> Self {
458 Self::Sqlite(Default::default())
459 }
460}
461
462#[cfg(feature = "client")]
463impl LightClientDbOptions {
464 pub async fn connect(self) -> Result<SqlStorage> {
465 match self {
466 Self::Sqlite(opt) => opt.connect().await,
467 Self::Postgres(opt) => opt.connect().await,
468 }
469 }
470}
471
472#[cfg(feature = "client")]
474#[derive(Clone, Debug)]
475pub struct SqlStorage {
476 pool: DbPool,
477 num_leaves: u32,
478 recency: Arc<Recency>,
480 _tmp: Option<Arc<TempDir>>,
481}
482
483#[cfg(feature = "client")]
484impl SqlStorage {
485 async fn new(pool: DbPool, num_leaves: u32, tmp: Option<Arc<TempDir>>) -> Result<Self> {
486 let max_used = with_pool!(&pool, |p| {
488 let (max_used,): (i64,) = query_as("SELECT COALESCE(MAX(last_used), 0) FROM leaf")
489 .fetch_one(p)
490 .await?;
491 max_used
492 });
493 let recency = Arc::new(Recency {
494 next_tick: AtomicI64::new(max_used + 1),
495 dirty: Default::default(),
496 pool: pool.clone(),
497 });
498 Ok(Self {
499 pool,
500 num_leaves,
501 recency,
502 _tmp: tmp,
503 })
504 }
505}
506
507#[cfg(feature = "client")]
508impl Storage for SqlStorage {
509 async fn default() -> Result<Self> {
510 LightClientSqliteOptions::default().connect().await
511 }
512
513 async fn block_height(&self) -> Result<u64> {
514 with_pool!(&self.pool, |pool| {
515 let mut tx = pool.begin().await?;
516 let (height,): (i64,) = query_as("SELECT COALESCE(max(height) + 1, 0) FROM leaf")
517 .fetch_one(tx.as_mut())
518 .await?;
519 Ok(u64::try_from(height)?)
520 })
521 }
522
523 async fn leaf_upper_bound(
524 &self,
525 id: impl Into<LeafRequest> + Send,
526 ) -> Result<Option<LeafQueryData<SeqTypes>>> {
527 let id = id.into();
528 let row = with_pool!(&self.pool, |pool| {
529 let mut q = QueryBuilder::new("SELECT data FROM leaf WHERE ");
530 match id {
531 LeafRequest::Leaf(LeafId::Number(n)) | LeafRequest::Header(BlockId::Number(n)) => {
532 q.push("height >= ")
533 .push_bind(n as i64)
534 .push(" ORDER BY height");
535 },
536 LeafRequest::Leaf(LeafId::Hash(h)) => {
537 q.push("hash = ").push_bind(h.to_string());
538 },
539 LeafRequest::Header(BlockId::Hash(h)) => {
540 q.push("block_hash = ").push_bind(h.to_string());
541 },
542 LeafRequest::Header(BlockId::PayloadHash(h)) => {
543 q.push("payload_hash = ")
544 .push_bind(h.to_string())
545 .push(" ORDER BY height");
546 },
547 }
548 q.push(" LIMIT 1");
549
550 q.build_query_as::<(serde_json::Value,)>()
551 .fetch_optional(pool)
552 .await?
553 });
554 let Some((data,)) = row else {
555 return Ok(None);
556 };
557
558 let leaf: LeafQueryData<SeqTypes> = serde_json::from_value(data)?;
559 self.recency.touch(leaf.height() as i64);
560 Ok(Some(leaf))
561 }
562
563 async fn get_leaves_in_range(
564 &self,
565 start_height: u32,
566 end_height: u32,
567 ) -> Result<Vec<LeafQueryData<SeqTypes>>> {
568 with_pool!(&self.pool, |pool| {
569 query_as::<_, (i64, serde_json::Value)>(
570 "SELECT height, data FROM leaf WHERE height >= $1 AND height < $2 ORDER BY height",
571 )
572 .bind(start_height as i64)
573 .bind(end_height as i64)
574 .fetch_all(pool)
575 .await?
576 .into_iter()
577 .map(|(height, data)| {
578 let leaf = serde_json::from_value(data)?;
579 self.recency.touch(height);
580 Ok(leaf)
581 })
582 .collect::<Result<Vec<_>, serde_json::Error>>()
583 .map_err(anyhow::Error::new)
584 })
585 }
586
587 async fn insert_leaf(&self, leaf: LeafQueryData<SeqTypes>) -> Result<()> {
588 let height = leaf.height() as i64;
589 let hash = leaf.hash().to_string();
590 let block_hash = leaf.block_hash().to_string();
591 let payload_hash = leaf.payload_hash().to_string();
592 let data = serde_json::to_value(&leaf)?;
593
594 let pending = self.recency.drain();
596 let insert_tick = self.recency.next_tick.fetch_add(1, Ordering::Relaxed);
597
598 let result = WRITE_BACKOFF
599 .retry_if(
600 WRITE_RETRY_MAX,
601 |_| true,
602 || async {
603 with_pool!(&self.pool, |pool| {
604 let mut tx = pool.begin().await?;
605
606 tracing::debug!(height, hash, "inserting leaf");
607 query(
608 "INSERT INTO leaf (height, hash, block_hash, payload_hash, data, \
609 last_used) VALUES ($1, $2, $3, $4, $5, $6) ON CONFLICT (height) DO \
610 UPDATE SET data = excluded.data, last_used = excluded.last_used",
611 )
612 .bind(height)
613 .bind(&hash)
614 .bind(&block_hash)
615 .bind(&payload_hash)
616 .bind(&data)
617 .bind(insert_tick)
618 .execute(tx.as_mut())
619 .await
620 .context("inserting new leaf")?;
621 tracing::debug!(height, hash, "inserted leaf");
622
623 for (h, tick) in &pending {
625 query("UPDATE leaf SET last_used = $1 WHERE height = $2")
626 .bind(tick)
627 .bind(h)
628 .execute(tx.as_mut())
629 .await
630 .context("flushing recency touch")?;
631 }
632
633 let (num_leaves,): (i64,) = query_as("SELECT count(*) FROM leaf")
635 .fetch_one(tx.as_mut())
636 .await
637 .context("counting leaves")?;
638 let to_delete = num_leaves.saturating_sub(i64::from(self.num_leaves));
639 if to_delete > 0 {
640 tracing::debug!("garbage collecting {to_delete} leaves");
641 let res = query(
642 "DELETE FROM leaf WHERE height IN (SELECT height FROM leaf ORDER \
643 BY last_used ASC, height ASC LIMIT $1)",
644 )
645 .bind(to_delete)
646 .execute(tx.as_mut())
647 .await
648 .context("deleting old leaves")?;
649 tracing::debug!("deleted {} leaves", res.rows_affected());
650 }
651
652 tx.commit().await?;
653 Ok(())
654 })
655 },
656 )
657 .await;
658
659 if result.is_err() {
660 let mut dirty = self.recency.dirty.lock().unwrap();
663 for (h, tick) in pending {
664 dirty.entry(h).or_insert(tick);
665 }
666 }
667
668 result
669 }
670
671 async fn stake_table_lower_bound(
672 &self,
673 epoch: EpochNumber,
674 ) -> Result<Option<(EpochNumber, StakeTableState, Version, Version)>> {
675 with_pool!(&self.pool, |pool| {
676 let mut tx = pool.begin().await?;
677
678 let epoch = min(*epoch, i64::MAX as u64) as i64;
682
683 let Some((epoch, epoch_root_protocol_version, next_epoch_root_protocol_version)) =
684 query_as::<_, (i64, String, String)>(
685 "SELECT epoch, epoch_root_protocol_version, next_epoch_root_protocol_version \
686 FROM stake_table_epoch WHERE epoch <= $1 ORDER BY epoch DESC LIMIT 1",
687 )
688 .bind(epoch)
689 .fetch_optional(tx.as_mut())
690 .await
691 .context("loading epoch lower bound")?
692 else {
693 return Ok(None);
694 };
695 let epoch_root_protocol_version = versions::parse_version(&epoch_root_protocol_version)
696 .with_context(|| {
697 format!(
698 "parsing stored epoch root protocol version \
699 {epoch_root_protocol_version:?} for epoch {epoch}"
700 )
701 })?;
702 let next_epoch_root_protocol_version =
703 versions::parse_version(&next_epoch_root_protocol_version).with_context(|| {
704 format!(
705 "parsing stored next epoch root protocol version \
706 {next_epoch_root_protocol_version:?} for epoch {epoch}"
707 )
708 })?;
709
710 let validators = query_as::<_, (Value,)>(
711 "SELECT data FROM stake_table_validator WHERE epoch = $1 ORDER BY idx",
712 )
713 .bind(epoch)
714 .fetch(tx.as_mut())
715 .map_err(anyhow::Error::new)
716 .and_then(|(json,)| async move {
717 let validator: RegisteredValidator<PubKey> = serde_json::from_value(json)?;
718 Ok((validator.account, validator))
719 })
720 .try_collect()
721 .await
722 .context(format!("loading stake table for epoch {epoch}"))?;
723
724 let validator_exits =
725 query_as::<_, (String,)>("SELECT address FROM stake_table_exit WHERE epoch <= $1")
726 .bind(epoch)
727 .fetch(tx.as_mut())
728 .map_err(anyhow::Error::new)
729 .and_then(|(s,)| async move { Ok(Address::from_str(&s)?) })
730 .try_collect()
731 .await
732 .context(format!("loading validator exits for epoch {epoch}"))?;
733
734 let used_bls_keys =
735 query_as::<_, (String,)>("SELECT key FROM stake_table_bls_key WHERE epoch <= $1")
736 .bind(epoch)
737 .fetch(tx.as_mut())
738 .map_err(anyhow::Error::new)
739 .and_then(|(s,)| async move { Ok(PubKey::from_str(&s)?) })
740 .try_collect()
741 .await
742 .context(format!("loading BLS keys for epoch {epoch}"))?;
743
744 let used_schnorr_keys = query_as::<_, (String,)>(
745 "SELECT key FROM stake_table_schnorr_key WHERE epoch <= $1",
746 )
747 .bind(epoch)
748 .fetch(tx.as_mut())
749 .map_err(anyhow::Error::new)
750 .and_then(|(s,)| async move { Ok(StateVerKey::from_str(&s)?) })
751 .try_collect()
752 .await
753 .context(format!("loading Schnorr keys for epoch {epoch}"))?;
754
755 let used_x25519_keys = query_as::<_, (String,)>(
756 "SELECT key FROM stake_table_x25519_key WHERE epoch <= $1",
757 )
758 .bind(epoch)
759 .fetch(tx.as_mut())
760 .map_err(anyhow::Error::new)
761 .and_then(|(s,)| async move { Ok(x25519::PublicKey::from_str(&s)?) })
762 .try_collect()
763 .await
764 .context(format!("loading x25519 keys for epoch {epoch}"))?;
765
766 Ok(Some((
767 EpochNumber::new(epoch as u64),
768 StakeTableState::new(
769 validators,
770 validator_exits,
771 used_bls_keys,
772 used_schnorr_keys,
773 used_x25519_keys,
774 ),
775 epoch_root_protocol_version,
776 next_epoch_root_protocol_version,
777 )))
778 })
779 }
780
781 async fn insert_stake_table(
782 &self,
783 epoch: EpochNumber,
784 stake_table: &StakeTableState,
785 epoch_root_protocol_version: Version,
786 next_epoch_root_protocol_version: Version,
787 ) -> Result<()> {
788 let epoch = i64::try_from(*epoch).context("epoch overflow")?;
789 let epoch_root_protocol_version_str = epoch_root_protocol_version.to_string();
790 let next_epoch_root_protocol_version_str = next_epoch_root_protocol_version.to_string();
791 let validators = stake_table
792 .validators()
793 .values()
794 .cloned()
795 .map(serde_json::to_value)
796 .collect::<Result<Vec<_>, _>>()?;
797
798 WRITE_BACKOFF
799 .retry_if(
800 WRITE_RETRY_MAX,
801 |_| true,
802 || async {
803 with_pool!(&self.pool, |pool| {
804 let mut tx = pool.begin().await?;
805
806 let inserted = query(
813 "INSERT INTO stake_table_epoch (epoch, epoch_root_protocol_version, \
814 next_epoch_root_protocol_version) VALUES ($1, $2, $3) ON CONFLICT \
815 (epoch) DO NOTHING",
816 )
817 .bind(epoch)
818 .bind(&epoch_root_protocol_version_str)
819 .bind(&next_epoch_root_protocol_version_str)
820 .execute(tx.as_mut())
821 .await
822 .context(format!(
823 "recording stake table availability for epoch {epoch}"
824 ))?;
825 if inserted.rows_affected() == 0 {
826 return Ok(());
827 }
828
829 QueryBuilder::new("INSERT INTO stake_table_validator (epoch, idx, data) ")
830 .push_values(validators.iter().enumerate(), |mut q, (i, data)| {
831 q.push_bind(epoch).push_bind(i as i64).push_bind(data);
832 })
833 .build()
834 .execute(tx.as_mut())
835 .await
836 .context(format!("inserting validators for epoch {epoch}"))?;
837
838 QueryBuilder::new("INSERT INTO stake_table_bls_key (epoch, key) ")
840 .push_values(stake_table.used_bls_keys(), |mut q, key| {
841 q.push_bind(epoch).push_bind(key.to_string());
842 })
843 .push(
846 " ON CONFLICT (key) DO UPDATE SET epoch = excluded.epoch WHERE \
847 excluded.epoch < stake_table_bls_key.epoch",
848 )
849 .build()
850 .execute(tx.as_mut())
851 .await
852 .context(format!("inserting newly used BLS keys for epoch {epoch}"))?;
853
854 QueryBuilder::new("INSERT INTO stake_table_schnorr_key (epoch, key) ")
856 .push_values(stake_table.used_schnorr_keys(), |mut q, key| {
857 q.push_bind(epoch).push_bind(key.to_string());
858 })
859 .push(
862 " ON CONFLICT (key) DO UPDATE SET epoch = excluded.epoch WHERE \
863 excluded.epoch < stake_table_schnorr_key.epoch",
864 )
865 .build()
866 .execute(tx.as_mut())
867 .await
868 .context(format!(
869 "inserting newly used Schnorr keys for epoch {epoch}"
870 ))?;
871
872 if !stake_table.used_x25519_keys().is_empty() {
874 QueryBuilder::new("INSERT INTO stake_table_x25519_key (epoch, key) ")
875 .push_values(stake_table.used_x25519_keys(), |mut q, key| {
876 q.push_bind(epoch).push_bind(key.to_string());
877 })
878 .push(
881 " ON CONFLICT (key) DO UPDATE SET epoch = excluded.epoch \
882 WHERE excluded.epoch < stake_table_x25519_key.epoch",
883 )
884 .build()
885 .execute(tx.as_mut())
886 .await
887 .context(format!(
888 "inserting newly used x25519 keys for epoch {epoch}"
889 ))?;
890 }
891
892 if !stake_table.validator_exits().is_empty() {
894 QueryBuilder::new("INSERT INTO stake_table_exit (epoch, address) ")
895 .push_values(stake_table.validator_exits(), |mut q, address| {
896 q.push_bind(epoch).push_bind(address.to_string());
897 })
898 .push(
901 " ON CONFLICT (address) DO UPDATE SET epoch = excluded.epoch \
902 WHERE excluded.epoch < stake_table_exit.epoch",
903 )
904 .build()
905 .execute(tx.as_mut())
906 .await
907 .context(format!(
908 "inserting new validator exits for epoch {epoch}"
909 ))?;
910 }
911
912 tx.commit().await?;
913 Ok(())
914 })
915 },
916 )
917 .await
918 }
919}
920
921#[cfg(test)]
922mod test {
923 #[cfg(unix)]
924 use std::os::unix::fs::PermissionsExt;
925
926 use hotshot_query_service::data_source::storage::sql::testing::TmpPostgres;
927 use pretty_assertions::assert_eq;
928 use sqlx::sqlite::SqliteConnectOptions;
929 use tempfile::tempdir;
930 use versions::{EPOCH_VERSION, NEW_PROTOCOL_VERSION};
931
932 use super::*;
933 use crate::testing::{leaf_chain, random_validator};
934
935 #[derive(Clone, Copy, Debug)]
936 enum Backend {
937 Sqlite,
938 Postgres,
939 }
940
941 impl Backend {
942 async fn temp_db(self) -> TestDb {
943 match self {
944 Self::Sqlite => TestDb::Sqlite(tempdir().unwrap()),
945 Self::Postgres => TestDb::Postgres(TmpPostgres::init().await),
946 }
947 }
948
949 async fn open(self, num_leaves: u32) -> (SqlStorage, TestDb) {
952 let tmp = self.temp_db().await;
953 (tmp.connect(num_leaves).await, tmp)
954 }
955 }
956
957 enum TestDb {
960 Sqlite(tempfile::TempDir),
961 Postgres(TmpPostgres),
962 }
963
964 impl TestDb {
965 async fn connect(&self, num_leaves: u32) -> SqlStorage {
966 match self {
967 Self::Sqlite(dir) => LightClientSqliteOptions {
968 lc_path: Some(dir.path().join("lc.db")),
969 num_leaves,
970 ..Default::default()
971 }
972 .connect()
973 .await
974 .unwrap(),
975 Self::Postgres(tmp) => LightClientPostgresOptions {
976 connect: PgConnectOptions::new()
977 .host(&tmp.host())
978 .port(tmp.port())
979 .username("postgres")
980 .password("password"),
981 num_connections: 5,
982 num_leaves,
983 }
984 .connect()
985 .await
986 .unwrap(),
987 }
988 }
989 }
990
991 #[rstest_reuse::template]
992 #[rstest::rstest]
993 #[case::sqlite(Backend::Sqlite)]
994 #[case::postgres(Backend::Postgres)]
995 #[tokio::test(flavor = "multi_thread", worker_threads = 4)]
996 async fn backends(#[case] backend: Backend) {}
997
998 #[test_log::test(rstest_reuse::apply(backends))]
999 async fn test_survives_reconnect(#[case] backend: Backend) {
1000 let tmp = backend.temp_db().await;
1001 let leaf = leaf_chain(0..1, EPOCH_VERSION).await.remove(0);
1002 let state = random_stake_table();
1003 {
1004 let db = tmp.connect(10).await;
1005 db.insert_leaf(leaf.clone()).await.unwrap();
1006 db.insert_stake_table(EpochNumber::new(3), &state, EPOCH_VERSION, EPOCH_VERSION)
1007 .await
1008 .unwrap();
1009 }
1010
1011 let db = tmp.connect(10).await;
1012 assert_eq!(
1013 db.leaf_upper_bound(LeafId::Number(0)).await.unwrap(),
1014 Some(leaf)
1015 );
1016 assert_eq!(
1017 db.stake_table_lower_bound(EpochNumber::new(5))
1018 .await
1019 .unwrap()
1020 .unwrap(),
1021 (EpochNumber::new(3), state, EPOCH_VERSION, EPOCH_VERSION)
1022 );
1023 }
1024
1025 #[tokio::test]
1026 #[test_log::test]
1027 async fn test_default_storage_survives_connection_churn() {
1028 let db = SqlStorage::default().await.unwrap();
1029
1030 {
1031 let DbPool::Sqlite(pool) = &db.pool else {
1032 unreachable!("default storage is SQLite");
1033 };
1034 let conn = pool.acquire().await.unwrap();
1035 drop(conn);
1036 }
1037
1038 let leaf = leaf_chain(0..1, EPOCH_VERSION).await.remove(0);
1039 db.insert_leaf(leaf).await.unwrap();
1040 assert_eq!(db.block_height().await.unwrap(), 1);
1041 }
1042
1043 #[tokio::test]
1044 #[test_log::test]
1045 async fn test_default_storage_survives_idle_reap() {
1046 use std::time::Duration;
1047
1048 let db = SqlStorage::default().await.unwrap();
1049 let path = db
1050 ._tmp
1051 .as_ref()
1052 .expect("default storage must own a tempdir")
1053 .path()
1054 .join("lc.db");
1055
1056 let opt = SqliteConnectOptions::new()
1057 .filename(&path)
1058 .create_if_missing(false);
1059 let pool = SqlitePoolOptions::default()
1060 .max_connections(5)
1061 .min_connections(0)
1062 .idle_timeout(Some(Duration::from_millis(50)))
1063 .connect_with(opt)
1064 .await
1065 .unwrap();
1066
1067 let (height,): (i64,) = sqlx::query_as("SELECT COALESCE(max(height) + 1, 0) FROM leaf")
1068 .fetch_one(&pool)
1069 .await
1070 .unwrap();
1071 assert_eq!(height, 0);
1072
1073 let deadline = tokio::time::Instant::now() + Duration::from_secs(5);
1074 while pool.size() > 0 && tokio::time::Instant::now() < deadline {
1075 tokio::time::sleep(Duration::from_millis(50)).await;
1076 }
1077 assert_eq!(pool.size(), 0, "pool did not reap idle connections in time");
1078
1079 let (height,): (i64,) = sqlx::query_as("SELECT COALESCE(max(height) + 1, 0) FROM leaf")
1080 .fetch_one(&pool)
1081 .await
1082 .expect("schema must survive the pool reaping idle connections");
1083 assert_eq!(height, 0);
1084
1085 drop(db);
1086 }
1087
1088 #[test_log::test(rstest_reuse::apply(backends))]
1096 async fn test_concurrent_reads_do_not_lock(#[case] backend: Backend) {
1097 let (db, _tmp) = backend.open(8).await;
1098 let db = Arc::new(db);
1099 let leaves = leaf_chain(0..40, EPOCH_VERSION).await;
1100
1101 let mut tasks = Vec::new();
1102 for leaf in leaves {
1103 let db = db.clone();
1104 tasks.push(tokio::spawn(async move {
1105 db.insert_leaf(leaf).await.map(|_| ())
1106 }));
1107 }
1108 for h in 0..40usize {
1109 let read_db = db.clone();
1110 tasks.push(tokio::spawn(async move {
1111 read_db
1112 .leaf_upper_bound(LeafId::Number(h))
1113 .await
1114 .map(|_| ())
1115 }));
1116 let height_db = db.clone();
1117 tasks.push(tokio::spawn(async move {
1118 height_db.block_height().await.map(|_| ())
1119 }));
1120 }
1121
1122 for task in tasks {
1123 task.await
1124 .unwrap()
1125 .expect("concurrent op must not fail with a database lock error");
1126 }
1127 }
1128
1129 #[cfg(unix)]
1130 #[tokio::test]
1131 #[test_log::test]
1132 async fn test_default_storage_tempdir_is_owner_only() {
1133 let db = SqlStorage::default().await.unwrap();
1134 let dir = db
1135 ._tmp
1136 .as_ref()
1137 .expect("default storage must own a tempdir");
1138 let mode = std::fs::metadata(dir.path()).unwrap().permissions().mode() & 0o777;
1139 assert_eq!(mode, 0o700, "tempdir must be owner-only, got {mode:o}");
1140 }
1141
1142 #[tokio::test]
1143 #[test_log::test]
1144 async fn test_file_backed_creates_parent_dir() {
1145 let dir = tempdir().unwrap();
1146 let path = dir.path().join("nested").join("subdir").join("lc.db");
1147 let db = LightClientSqliteOptions {
1148 lc_path: Some(path.clone()),
1149 ..Default::default()
1150 }
1151 .connect()
1152 .await
1153 .unwrap();
1154 assert_eq!(db.block_height().await.unwrap(), 0);
1155 assert!(path.exists(), "sqlite file should have been created");
1156 }
1157
1158 #[test_log::test(rstest_reuse::apply(backends))]
1159 async fn test_block_height(#[case] backend: Backend) {
1160 let (db, _guard) = backend.open(100).await;
1161
1162 assert_eq!(db.block_height().await.unwrap(), 0);
1164
1165 let leaf = leaf_chain(100..101, EPOCH_VERSION).await.remove(0);
1167 db.insert_leaf(leaf).await.unwrap();
1168 assert_eq!(db.block_height().await.unwrap(), 101);
1169 }
1170
1171 #[test_log::test(rstest_reuse::apply(backends))]
1172 async fn test_leaf_upper_bound_exact(#[case] backend: Backend) {
1173 let (db, _guard) = backend.open(100).await;
1174
1175 let leaf = leaf_chain(0..1, EPOCH_VERSION).await.remove(0);
1176 db.insert_leaf(leaf.clone()).await.unwrap();
1177 assert_eq!(
1178 db.leaf_upper_bound(LeafId::Number(0))
1179 .await
1180 .unwrap()
1181 .unwrap(),
1182 leaf
1183 );
1184 assert_eq!(
1185 db.leaf_upper_bound(LeafId::Hash(leaf.hash()))
1186 .await
1187 .unwrap()
1188 .unwrap(),
1189 leaf
1190 );
1191 assert_eq!(
1192 db.leaf_upper_bound(BlockId::Number(0))
1193 .await
1194 .unwrap()
1195 .unwrap(),
1196 leaf
1197 );
1198 assert_eq!(
1199 db.leaf_upper_bound(BlockId::Hash(leaf.block_hash()))
1200 .await
1201 .unwrap()
1202 .unwrap(),
1203 leaf
1204 );
1205 assert_eq!(
1206 db.leaf_upper_bound(BlockId::PayloadHash(leaf.payload_hash()))
1207 .await
1208 .unwrap()
1209 .unwrap()
1210 .payload_hash(),
1211 leaf.payload_hash()
1212 );
1213 }
1214
1215 #[test_log::test(rstest_reuse::apply(backends))]
1216 async fn test_leaf_upper_bound_loose(#[case] backend: Backend) {
1217 let (db, _guard) = backend.open(100).await;
1218
1219 let leaves = leaf_chain(0..=1, EPOCH_VERSION).await;
1220 db.insert_leaf(leaves[1].clone()).await.unwrap();
1221 assert_eq!(
1222 db.leaf_upper_bound(LeafId::Number(0))
1223 .await
1224 .unwrap()
1225 .unwrap(),
1226 leaves[1]
1227 );
1228 assert_eq!(
1231 db.leaf_upper_bound(LeafId::Hash(leaves[0].hash()))
1232 .await
1233 .unwrap(),
1234 None
1235 );
1236 assert_eq!(
1237 db.leaf_upper_bound(BlockId::Hash(leaves[0].block_hash()))
1238 .await
1239 .unwrap(),
1240 None
1241 );
1242 }
1243
1244 #[test_log::test(rstest_reuse::apply(backends))]
1245 async fn test_leaf_upper_bound_least_upper_bound(#[case] backend: Backend) {
1246 let (db, _guard) = backend.open(100).await;
1247
1248 let leaves = leaf_chain(0..=2, EPOCH_VERSION).await;
1249 db.insert_leaf(leaves[2].clone()).await.unwrap();
1250 db.insert_leaf(leaves[1].clone()).await.unwrap();
1251 assert_eq!(
1252 db.leaf_upper_bound(LeafId::Number(0))
1253 .await
1254 .unwrap()
1255 .unwrap(),
1256 leaves[1]
1257 );
1258 }
1259
1260 #[test_log::test(rstest_reuse::apply(backends))]
1261 async fn test_leaf_upper_bound_not_found(#[case] backend: Backend) {
1262 let (db, _guard) = backend.open(100).await;
1263
1264 let leaves = leaf_chain(0..=1, EPOCH_VERSION).await;
1265 db.insert_leaf(leaves[0].clone()).await.unwrap();
1266 assert_eq!(db.leaf_upper_bound(LeafId::Number(1)).await.unwrap(), None);
1267 assert_eq!(
1268 db.leaf_upper_bound(LeafId::Hash(leaves[1].hash()))
1269 .await
1270 .unwrap(),
1271 None
1272 );
1273 }
1274
1275 #[test_log::test(rstest_reuse::apply(backends))]
1276 async fn test_gc_last_inserted(#[case] backend: Backend) {
1277 let (db, _guard) = backend.open(1).await;
1278
1279 let leaves = leaf_chain(0..=1, EPOCH_VERSION).await;
1280 db.insert_leaf(leaves[1].clone()).await.unwrap();
1281 db.insert_leaf(leaves[0].clone()).await.unwrap();
1282
1283 assert_eq!(
1284 db.leaf_upper_bound(LeafId::Number(0))
1285 .await
1286 .unwrap()
1287 .unwrap(),
1288 leaves[0]
1289 );
1290 assert_eq!(db.leaf_upper_bound(LeafId::Number(1)).await.unwrap(), None);
1291 }
1292
1293 #[test_log::test(rstest_reuse::apply(backends))]
1295 async fn test_gc_evicts_least_recently_used(#[case] backend: Backend) {
1296 let (db, _guard) = backend.open(2).await;
1297
1298 let leaves = leaf_chain(0..=2, EPOCH_VERSION).await;
1299 db.insert_leaf(leaves[0].clone()).await.unwrap();
1300 db.insert_leaf(leaves[1].clone()).await.unwrap();
1301
1302 db.leaf_upper_bound(LeafId::Number(0)).await.unwrap();
1304
1305 db.insert_leaf(leaves[2].clone()).await.unwrap();
1307
1308 assert_eq!(
1311 db.leaf_upper_bound(LeafId::Hash(leaves[0].hash()))
1312 .await
1313 .unwrap(),
1314 Some(leaves[0].clone()),
1315 "leaf 0 was recently read and must be kept"
1316 );
1317 assert_eq!(
1318 db.leaf_upper_bound(LeafId::Hash(leaves[1].hash()))
1319 .await
1320 .unwrap(),
1321 None,
1322 "leaf 1 was least-recently-used and must be evicted"
1323 );
1324 assert_eq!(
1325 db.leaf_upper_bound(LeafId::Hash(leaves[2].hash()))
1326 .await
1327 .unwrap(),
1328 Some(leaves[2].clone()),
1329 "leaf 2 was just inserted and must be kept"
1330 );
1331 }
1332
1333 #[test_log::test(rstest_reuse::apply(backends))]
1338 async fn test_recency_survives_restart(#[case] backend: Backend) {
1339 let tmp = backend.temp_db().await;
1340 let leaves = leaf_chain(0..=3, EPOCH_VERSION).await;
1341
1342 {
1343 let db = tmp.connect(2).await;
1344 db.insert_leaf(leaves[0].clone()).await.unwrap();
1345 db.insert_leaf(leaves[1].clone()).await.unwrap();
1346
1347 db.leaf_upper_bound(LeafId::Number(0)).await.unwrap();
1349
1350 db.insert_leaf(leaves[2].clone()).await.unwrap();
1353 }
1354
1355 {
1358 let db = tmp.connect(2).await;
1359
1360 db.leaf_upper_bound(LeafId::Hash(leaves[2].hash()))
1362 .await
1363 .unwrap();
1364
1365 db.insert_leaf(leaves[3].clone()).await.unwrap();
1369
1370 assert_eq!(
1371 db.leaf_upper_bound(LeafId::Hash(leaves[0].hash()))
1372 .await
1373 .unwrap(),
1374 None,
1375 "leaf 0 had the lowest persisted last_used and must be evicted"
1376 );
1377 assert_eq!(
1378 db.leaf_upper_bound(LeafId::Hash(leaves[2].hash()))
1379 .await
1380 .unwrap(),
1381 Some(leaves[2].clone()),
1382 "leaf 2 was touched after reopen and must survive"
1383 );
1384 assert_eq!(
1385 db.leaf_upper_bound(LeafId::Hash(leaves[3].hash()))
1386 .await
1387 .unwrap(),
1388 Some(leaves[3].clone()),
1389 "leaf 3 was just inserted and must survive"
1390 );
1391 }
1392 }
1393
1394 #[test_log::test(rstest_reuse::apply(backends))]
1399 async fn test_recency_flushed_on_shutdown_without_insert(#[case] backend: Backend) {
1400 let tmp = backend.temp_db().await;
1401 let leaves = leaf_chain(0..=2, EPOCH_VERSION).await;
1402
1403 {
1404 let db = tmp.connect(2).await;
1405 db.insert_leaf(leaves[0].clone()).await.unwrap();
1406 db.insert_leaf(leaves[1].clone()).await.unwrap();
1407
1408 db.leaf_upper_bound(LeafId::Number(0)).await.unwrap();
1410
1411 Recency::flush(db.pool.clone(), db.recency.drain()).await;
1413 }
1414
1415 {
1418 let db = tmp.connect(2).await;
1419 db.insert_leaf(leaves[2].clone()).await.unwrap();
1420
1421 assert_eq!(
1422 db.leaf_upper_bound(LeafId::Hash(leaves[0].hash()))
1423 .await
1424 .unwrap(),
1425 Some(leaves[0].clone()),
1426 "leaf 0 was touched before the shutdown flush and must survive"
1427 );
1428 assert_eq!(
1429 db.leaf_upper_bound(LeafId::Hash(leaves[1].hash()))
1430 .await
1431 .unwrap(),
1432 None,
1433 "leaf 1 was least-recently-used and must be evicted"
1434 );
1435 }
1436 }
1437
1438 #[test_log::test(rstest_reuse::apply(backends))]
1439 async fn test_get_leaves_in_range(#[case] backend: Backend) {
1440 let (db, _guard) = backend.open(100).await;
1441
1442 let leaves = leaf_chain(0..5, EPOCH_VERSION).await;
1443 for leaf in &leaves {
1444 db.insert_leaf(leaf.clone()).await.unwrap();
1445 }
1446
1447 let fetched = db.get_leaves_in_range(1, 4).await.unwrap();
1448 assert_eq!(fetched, leaves[1..4]);
1449 }
1450
1451 #[test_log::test(rstest_reuse::apply(backends))]
1452 async fn test_get_leaves_in_range_not_found(#[case] backend: Backend) {
1453 let (db, _guard) = backend.open(100).await;
1454
1455 let leaves = leaf_chain(0..3, EPOCH_VERSION).await;
1456 for leaf in &leaves {
1457 db.insert_leaf(leaf.clone()).await.unwrap();
1458 }
1459
1460 let fetched = db.get_leaves_in_range(3, 5).await.unwrap();
1461 assert!(fetched.is_empty());
1462 }
1463
1464 #[test_log::test(rstest_reuse::apply(backends))]
1465 async fn test_insert_stake_table_idempotent(#[case] backend: Backend) {
1466 let (db, _guard) = backend.open(100).await;
1467
1468 let epoch = EpochNumber::new(1);
1469 let state = random_stake_table();
1470 db.insert_stake_table(epoch, &state, EPOCH_VERSION, EPOCH_VERSION)
1471 .await
1472 .unwrap();
1473 db.insert_stake_table(epoch, &state, EPOCH_VERSION, EPOCH_VERSION)
1475 .await
1476 .unwrap();
1477 assert_eq!(
1478 db.stake_table_lower_bound(epoch).await.unwrap().unwrap(),
1479 (epoch, state, EPOCH_VERSION, EPOCH_VERSION)
1480 );
1481 }
1482
1483 #[test_log::test(rstest_reuse::apply(backends))]
1484 async fn test_stake_table_lower_bound_exact(#[case] backend: Backend) {
1485 let (db, _guard) = backend.open(100).await;
1486
1487 let epoch = EpochNumber::new(1);
1488 let state = random_stake_table();
1489 db.insert_stake_table(epoch, &state, EPOCH_VERSION, EPOCH_VERSION)
1490 .await
1491 .unwrap();
1492 assert_eq!(
1493 db.stake_table_lower_bound(epoch).await.unwrap().unwrap(),
1494 (epoch, state, EPOCH_VERSION, EPOCH_VERSION)
1495 );
1496 }
1497
1498 #[test_log::test(rstest_reuse::apply(backends))]
1499 async fn test_stake_table_lower_bound_loose(#[case] backend: Backend) {
1500 let (db, _guard) = backend.open(100).await;
1501
1502 let epoch = EpochNumber::new(1);
1503 let state = random_stake_table();
1504 db.insert_stake_table(epoch, &state, EPOCH_VERSION, EPOCH_VERSION)
1505 .await
1506 .unwrap();
1507 assert_eq!(
1508 db.stake_table_lower_bound(epoch + 1)
1509 .await
1510 .unwrap()
1511 .unwrap(),
1512 (epoch, state, EPOCH_VERSION, EPOCH_VERSION)
1513 );
1514 }
1515
1516 #[test_log::test(rstest_reuse::apply(backends))]
1517 async fn test_stake_table_lower_bound_greatest_lower_bound(#[case] backend: Backend) {
1518 let (db, _guard) = backend.open(100).await;
1519
1520 let state1 = random_stake_table();
1521 let state2 = chain_stake_table(&state1);
1522 db.insert_stake_table(EpochNumber::new(1), &state1, EPOCH_VERSION, EPOCH_VERSION)
1523 .await
1524 .unwrap();
1525 db.insert_stake_table(EpochNumber::new(2), &state2, EPOCH_VERSION, EPOCH_VERSION)
1526 .await
1527 .unwrap();
1528
1529 assert_eq!(
1530 db.stake_table_lower_bound(EpochNumber::new(3))
1531 .await
1532 .unwrap()
1533 .unwrap(),
1534 (EpochNumber::new(2), state2, EPOCH_VERSION, EPOCH_VERSION)
1535 );
1536 }
1537
1538 #[test_log::test(rstest_reuse::apply(backends))]
1539 async fn test_stake_table_lower_bound_not_found(#[case] backend: Backend) {
1540 let (db, _guard) = backend.open(100).await;
1541 db.insert_stake_table(
1542 EpochNumber::new(2),
1543 &random_stake_table(),
1544 EPOCH_VERSION,
1545 EPOCH_VERSION,
1546 )
1547 .await
1548 .unwrap();
1549 assert_eq!(
1550 db.stake_table_lower_bound(EpochNumber::new(1))
1551 .await
1552 .unwrap(),
1553 None
1554 );
1555 }
1556
1557 #[test_log::test(rstest_reuse::apply(backends))]
1558 async fn test_stake_table_keeps_every_epoch(#[case] backend: Backend) {
1559 let (db, _guard) = backend.open(100).await;
1560
1561 let mut states = Vec::from([random_stake_table()]);
1562 for _ in 1..150 {
1563 states.push(chain_stake_table(states.last().unwrap()));
1564 }
1565 for (epoch, state) in (1..).zip(&states) {
1566 db.insert_stake_table(EpochNumber::new(epoch), state, EPOCH_VERSION, EPOCH_VERSION)
1567 .await
1568 .unwrap();
1569 }
1570
1571 for (epoch, state) in (1..).zip(states) {
1572 assert_eq!(
1573 db.stake_table_lower_bound(EpochNumber::new(epoch))
1574 .await
1575 .unwrap()
1576 .unwrap(),
1577 (EpochNumber::new(epoch), state, EPOCH_VERSION, EPOCH_VERSION)
1578 );
1579 }
1580 }
1581
1582 #[test_log::test(rstest_reuse::apply(backends))]
1583 async fn test_stake_table_insert_out_of_order(#[case] backend: Backend) {
1584 let (db, _guard) = backend.open(100).await;
1585
1586 let state1 = random_stake_table();
1587 let state2 = chain_stake_table(&state1);
1588 db.insert_stake_table(EpochNumber::new(2), &state2, EPOCH_VERSION, EPOCH_VERSION)
1589 .await
1590 .unwrap();
1591 db.insert_stake_table(EpochNumber::new(1), &state1, EPOCH_VERSION, EPOCH_VERSION)
1592 .await
1593 .unwrap();
1594
1595 assert_eq!(
1596 db.stake_table_lower_bound(EpochNumber::new(1))
1597 .await
1598 .unwrap()
1599 .unwrap(),
1600 (EpochNumber::new(1), state1, EPOCH_VERSION, EPOCH_VERSION)
1601 );
1602 assert_eq!(
1603 db.stake_table_lower_bound(EpochNumber::new(2))
1604 .await
1605 .unwrap()
1606 .unwrap(),
1607 (EpochNumber::new(2), state2, EPOCH_VERSION, EPOCH_VERSION)
1608 );
1609 }
1610
1611 #[test_log::test(rstest_reuse::apply(backends))]
1612 async fn test_stake_table_epoch_root_protocol_version_roundtrip(#[case] backend: Backend) {
1613 let (db, _guard) = backend.open(100).await;
1614
1615 let epoch = EpochNumber::new(1);
1616 let state = random_stake_table();
1617 db.insert_stake_table(epoch, &state, NEW_PROTOCOL_VERSION, EPOCH_VERSION)
1618 .await
1619 .unwrap();
1620 let (loaded_epoch, loaded_state, loaded_version, loaded_next_version) =
1621 db.stake_table_lower_bound(epoch).await.unwrap().unwrap();
1622 assert_eq!(loaded_epoch, epoch);
1623 assert_eq!(loaded_state, state);
1624 assert_eq!(loaded_version, NEW_PROTOCOL_VERSION);
1625 assert_eq!(loaded_next_version, EPOCH_VERSION);
1626 }
1627
1628 #[test_log::test(rstest_reuse::apply(backends))]
1632 async fn test_stake_table_x25519_keys_round_trip(#[case] backend: Backend) {
1633 use committable::Committable;
1634
1635 let (db, _guard) = backend.open(100).await;
1636 let epoch = EpochNumber::new(1);
1637 let state = random_stake_table();
1638 assert!(
1639 !state.used_x25519_keys().is_empty(),
1640 "random_stake_table must populate used_x25519_keys for this test to be meaningful"
1641 );
1642
1643 db.insert_stake_table(epoch, &state, NEW_PROTOCOL_VERSION, NEW_PROTOCOL_VERSION)
1644 .await
1645 .unwrap();
1646 let (_, loaded, ..) = db.stake_table_lower_bound(epoch).await.unwrap().unwrap();
1647
1648 assert_eq!(loaded.used_x25519_keys(), state.used_x25519_keys());
1649 assert_eq!(loaded.commit(), state.commit());
1650 }
1651
1652 fn random_stake_table() -> StakeTableState {
1654 let validator = random_validator();
1655 let candidate: RegisteredValidator<PubKey> = validator.clone().into();
1656 let x25519_key =
1657 x25519::PublicKey::try_from(rand::random::<[u8; 32]>().as_slice()).unwrap();
1658 let candidate_bls = candidate
1659 .stake_table_key
1660 .expect("random_validator returns authenticated validator");
1661 StakeTableState::new(
1662 [(candidate.account, candidate.clone())]
1663 .into_iter()
1664 .collect(),
1665 [Address::random()].into_iter().collect(),
1666 [candidate_bls].into_iter().collect(),
1667 [candidate
1668 .state_ver_key
1669 .expect("random_validator has valid schnorr key")]
1670 .into_iter()
1671 .collect(),
1672 [x25519_key].into_iter().collect(),
1673 )
1674 }
1675
1676 fn chain_stake_table(state: &StakeTableState) -> StakeTableState {
1678 let new_validator = random_validator();
1679 let new_candidate: RegisteredValidator<PubKey> = new_validator.clone().into();
1680 let new_candidate_bls = new_candidate
1681 .stake_table_key
1682 .expect("random_validator returns authenticated validator");
1683 let new_exit = Address::random();
1684 let new_x25519 =
1685 x25519::PublicKey::try_from(rand::random::<[u8; 32]>().as_slice()).unwrap();
1686 StakeTableState::new(
1687 state
1688 .validators()
1689 .values()
1690 .chain([&new_candidate])
1691 .map(|v| (v.account, v.clone()))
1692 .collect(),
1693 state
1694 .validator_exits()
1695 .iter()
1696 .chain([&new_exit])
1697 .cloned()
1698 .collect(),
1699 state
1700 .used_bls_keys()
1701 .iter()
1702 .chain([&new_candidate_bls])
1703 .cloned()
1704 .collect(),
1705 state
1706 .used_schnorr_keys()
1707 .iter()
1708 .chain([new_candidate
1709 .state_ver_key
1710 .as_ref()
1711 .expect("random_validator has valid schnorr key")])
1712 .cloned()
1713 .collect(),
1714 state
1715 .used_x25519_keys()
1716 .iter()
1717 .chain([&new_x25519])
1718 .cloned()
1719 .collect(),
1720 )
1721 }
1722}