iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

chain_store.rs (5173B)


      1 use std::{
      2     fs,
      3     path::{Path, PathBuf},
      4     time::{SystemTime, UNIX_EPOCH},
      5 };
      6 
      7 use anyhow::{Context, Result};
      8 use rusqlite::{Connection, OptionalExtension, params};
      9 
     10 use crate::domain::ChainSnapshot;
     11 
     12 mod compact;
     13 use compact::{decode_compact_snapshot, encode_compact_snapshot};
     14 
     15 const SCHEMA: &str = r#"
     16 CREATE TABLE IF NOT EXISTS chain_snapshots (
     17     id INTEGER PRIMARY KEY CHECK (id = 1),
     18     height INTEGER NOT NULL,
     19     tip_hash TEXT NOT NULL,
     20     snapshot_blob BLOB NOT NULL,
     21     updated_at_ms INTEGER NOT NULL
     22 );
     23 "#;
     24 
     25 #[derive(Clone, Debug)]
     26 pub struct SqliteChainStore {
     27     path: PathBuf,
     28 }
     29 
     30 impl SqliteChainStore {
     31     pub fn open(path: impl AsRef<Path>) -> Result<Self> {
     32         let path = path.as_ref().to_path_buf();
     33         if let Some(parent) = path.parent() {
     34             fs::create_dir_all(parent).with_context(|| {
     35                 format!(
     36                     "failed to create chain database directory {}",
     37                     parent.display()
     38                 )
     39             })?;
     40         }
     41 
     42         let store = Self { path };
     43         store.with_connection_mut(|connection| {
     44             connection
     45                 .execute_batch(SCHEMA)
     46                 .context("failed to initialize chain database schema")?;
     47             Ok(())
     48         })?;
     49         Ok(store)
     50     }
     51 
     52     pub fn path(&self) -> &Path {
     53         &self.path
     54     }
     55 
     56     pub fn load(&self) -> Result<Option<ChainSnapshot>> {
     57         self.with_connection(|connection| {
     58             let snapshot_blob = connection
     59                 .query_row(
     60                     "SELECT snapshot_blob FROM chain_snapshots WHERE id = 1",
     61                     [],
     62                     |row| row.get::<_, Vec<u8>>(0),
     63                 )
     64                 .optional()
     65                 .context("failed to load chain snapshot from database")?;
     66 
     67             snapshot_blob
     68                 .map(|blob| {
     69                     decode_compact_snapshot(&blob)
     70                         .context("failed to parse compact chain snapshot from database")
     71                 })
     72                 .transpose()
     73         })
     74     }
     75 
     76     pub fn save(&self, snapshot: &ChainSnapshot) -> Result<()> {
     77         let (height, tip_hash) = snapshot_tip(snapshot).context("cannot persist empty chain")?;
     78         let snapshot_blob =
     79             encode_compact_snapshot(snapshot).context("failed to encode compact chain snapshot")?;
     80         let updated_at_ms = unix_ms();
     81 
     82         self.with_connection_mut(|connection| {
     83             let transaction = connection
     84                 .transaction()
     85                 .context("failed to start chain persistence transaction")?;
     86             transaction
     87                 .execute(
     88                     r#"
     89 INSERT INTO chain_snapshots (id, height, tip_hash, snapshot_blob, updated_at_ms)
     90 VALUES (1, ?1, ?2, ?3, ?4)
     91 ON CONFLICT(id) DO UPDATE SET
     92     height = excluded.height,
     93     tip_hash = excluded.tip_hash,
     94     snapshot_blob = excluded.snapshot_blob,
     95     updated_at_ms = excluded.updated_at_ms
     96 "#,
     97                     params![height, tip_hash, snapshot_blob, updated_at_ms],
     98                 )
     99                 .context("failed to persist chain snapshot")?;
    100             transaction
    101                 .commit()
    102                 .context("failed to commit chain persistence transaction")?;
    103             Ok(())
    104         })
    105     }
    106 
    107     pub fn clear_chain(&self) -> Result<()> {
    108         self.with_connection_mut(|connection| {
    109             let transaction = connection
    110                 .transaction()
    111                 .context("failed to start chain reset transaction")?;
    112             transaction
    113                 .execute("DELETE FROM chain_snapshots", [])
    114                 .context("failed to delete chain snapshot")?;
    115             transaction
    116                 .commit()
    117                 .context("failed to commit chain reset transaction")?;
    118             Ok(())
    119         })
    120     }
    121 
    122     fn with_connection<T>(&self, work: impl FnOnce(&Connection) -> Result<T>) -> Result<T> {
    123         let connection = self.open_connection()?;
    124         connection
    125             .execute_batch(
    126                 r#"
    127 PRAGMA busy_timeout = 5000;
    128 PRAGMA synchronous = NORMAL;
    129 "#,
    130             )
    131             .context("failed to configure chain database connection")?;
    132         work(&connection)
    133     }
    134 
    135     fn with_connection_mut<T>(&self, work: impl FnOnce(&mut Connection) -> Result<T>) -> Result<T> {
    136         let mut connection = self.open_connection()?;
    137         connection
    138             .execute_batch(
    139                 r#"
    140 PRAGMA journal_mode = WAL;
    141 PRAGMA busy_timeout = 5000;
    142 PRAGMA synchronous = NORMAL;
    143 "#,
    144             )
    145             .context("failed to configure chain database connection")?;
    146         work(&mut connection)
    147     }
    148 
    149     fn open_connection(&self) -> Result<Connection> {
    150         Connection::open(&self.path)
    151             .with_context(|| format!("failed to open chain database {}", self.path.display()))
    152     }
    153 }
    154 
    155 fn snapshot_tip(snapshot: &ChainSnapshot) -> Option<(u64, String)> {
    156     snapshot
    157         .blocks
    158         .last()
    159         .map(|block| (block.height, block.hash.clone()))
    160 }
    161 
    162 fn unix_ms() -> u64 {
    163     SystemTime::now()
    164         .duration_since(UNIX_EPOCH)
    165         .unwrap_or_default()
    166         .as_millis() as u64
    167 }
    168 
    169 #[cfg(test)]
    170 mod tests;