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;