espresso_node/persistence/sql/
postgres.rs1use hotshot_query_service::{
4 data_source::storage::sql::postgres::{self as qs, set_no_deferrable_on_read},
5 include_migrations,
6};
7use light_client::storage::{LightClientDbOptions, LightClientPostgresOptions};
8
9#[path = "../sql.rs"]
10mod imp;
11
12pub use imp::*;
13use qs::{Config, SqlStorage};
14
15use super::Options as SqlOptions;
16#[cfg(any(test, feature = "testing"))]
17use super::PostgresOptions;
18use crate::{api::options::Query, persistence::storage_probe::StorageProbe};
19
20impl BackendOptions for qs::Db {
21 fn apply(mut cfg: Config, opt: &SqlOptions) -> anyhow::Result<Config> {
22 cfg = cfg.query_max_connections(opt.query_max_connections.unwrap_or(opt.max_connections));
23 cfg = cfg.query_min_connections(opt.query_min_connections.unwrap_or(opt.min_connections));
24 set_no_deferrable_on_read(opt.postgres_options.no_deferrable);
25
26 cfg = cfg.migrations(include_migrations!(
27 "$CARGO_MANIFEST_DIR/api/migrations/postgres"
28 ));
29
30 let pg_options = &opt.postgres_options;
31
32 if let Some(host) = &pg_options.host {
33 cfg = cfg.host(host.clone());
34 }
35
36 if let Some(port) = pg_options.port {
37 cfg = cfg.port(port);
38 }
39
40 if let Some(database) = &pg_options.database {
41 cfg = cfg.database(database);
42 }
43
44 if let Some(user) = &pg_options.user {
45 cfg = cfg.user(user);
46 }
47
48 if let Some(password) = &pg_options.password {
49 cfg = cfg.password(password);
50 }
51
52 if pg_options.use_tls {
53 cfg = cfg.tls();
54 }
55
56 Ok(cfg)
57 }
58
59 fn light_client_db(cfg: &Config, query: &Query) -> LightClientDbOptions {
62 LightClientDbOptions::Postgres(Box::new(LightClientPostgresOptions {
63 connect: cfg.connect_options().clone(),
64 num_connections: query.light_client_db.num_connections,
65 num_leaves: query.light_client_db.num_leaves,
66 }))
67 }
68
69 async fn reset_light_client(cfg: &Config) -> anyhow::Result<()> {
70 LightClientPostgresOptions::reset(cfg.connect_options()).await
71 }
72
73 async fn storage_probe(
74 _db: &SqlStorage,
75 _opt: &SqlOptions,
76 ) -> anyhow::Result<Option<StorageProbe>> {
77 Ok(None)
78 }
79}
80
81#[cfg(any(test, feature = "testing"))]
82pub(crate) fn tmp_options(db: &qs::testing::TmpDb) -> SqlOptions {
83 PostgresOptions {
84 port: Some(db.port()),
85 host: Some(db.host()),
86 user: Some("postgres".into()),
87 password: Some("password".into()),
88 ..Default::default()
89 }
90 .into()
91}
92
93#[cfg(test)]
94mod test {
95 use std::sync::Arc;
96
97 use committable::Committable as _;
98 use espresso_types::{
99 FeeAccount, Header, Leaf, Leaf2, NodeState, Payload, Transaction as Tx,
100 traits::{PersistenceOptions as _, SequencerPersistence as _},
101 };
102 use hotshot_example_types::node_types::TEST_VERSIONS;
103 use hotshot_query_service::{
104 availability::{BlockQueryData, LeafQueryData},
105 data_source::{
106 Transaction as _, VersionedDataSource as _, storage::UpdateAvailabilityStorage,
107 },
108 };
109 use hotshot_types::{
110 data::{QuorumProposal, ViewNumber, vid_commitment},
111 event::HotShotAction,
112 simple_certificate::QuorumCertificate,
113 traits::{
114 EncodeBytes,
115 block_contents::{
116 BlockHeader, BlockPayload as _, BuilderFee, GENESIS_VID_NUM_STORAGE_NODES,
117 },
118 signature_key::BuilderSignatureKey,
119 },
120 };
121 use sqlx::{Row as _, query};
122
123 use super::*;
124 use crate::{SeqTypes, persistence::tests::TestablePersistence as _};
125
126 #[test]
127 fn sqlite_options_do_not_build_postgres_persistence() {
128 let opt = SqlOptions {
129 embedded_db: true,
130 ..SqlOptions::default()
131 };
132 assert!(Options::try_from(opt.clone()).is_err());
133 assert!(matches!(
134 opt.into_backend(),
135 super::super::SqlBackendOptions::Sqlite(_)
136 ));
137 }
138
139 #[tokio::test]
140 async fn a_sqlite_pool_is_not_reused_for_postgres() {
141 let pool = sqlx::sqlite::SqlitePoolOptions::new()
142 .connect(":memory:")
143 .await
144 .unwrap();
145 let opt = SqlOptions {
146 pool: Some(pool.into()),
147 ..SqlOptions::default()
148 };
149 let Err(err) = Config::try_from(&opt) else {
150 panic!("a SQLite pool was accepted for Postgres");
151 };
152 assert!(format!("{err:#}").contains("other SQL backend"), "{err:#}");
153 }
154
155 async fn test_postgres_read_ns_table(instance_state: NodeState) {
156 let tmp = Persistence::tmp_storage().await;
157 let mut opt = Persistence::options(&tmp);
158 let storage = opt.create().await.unwrap();
159
160 let txs = [
161 Tx::new(10001u32.into(), vec![1, 2, 3]),
162 Tx::new(10001u32.into(), vec![4, 5, 6]),
163 Tx::new(10009u32.into(), vec![7, 8, 9]),
164 ];
165
166 let validated_state = Default::default();
167 let justify_qc =
168 QuorumCertificate::genesis(&validated_state, &instance_state, TEST_VERSIONS.test).await;
169 let view_number: ViewNumber = justify_qc.view_number + 1;
170 let parent_leaf = Leaf::genesis(&validated_state, &instance_state, TEST_VERSIONS.test.base)
171 .await
172 .into();
173
174 let (payload, ns_table) =
175 Payload::from_transactions(&txs, &validated_state, &instance_state)
176 .await
177 .unwrap();
178 let payload_bytes = payload.encode();
179 let payload_commitment = vid_commitment(
180 &payload_bytes,
181 &ns_table.encode(),
182 GENESIS_VID_NUM_STORAGE_NODES,
183 instance_state.current_version,
184 );
185 let builder_commitment = payload.builder_commitment(&ns_table);
186 let (fee_account, fee_key) = FeeAccount::generated_from_seed_indexed([0; 32], 0);
187 let fee_amount = 0;
188 let fee_signature = FeeAccount::sign_fee(&fee_key, fee_amount, &ns_table).unwrap();
189 let block_header = Header::new(
190 &validated_state,
191 &instance_state,
192 &parent_leaf,
193 payload_commitment,
194 builder_commitment,
195 ns_table,
196 BuilderFee {
197 fee_amount,
198 fee_account,
199 fee_signature,
200 },
201 instance_state.current_version,
202 view_number.u64(),
203 )
204 .await
205 .unwrap();
206 let proposal = QuorumProposal {
207 block_header: block_header.clone(),
208 view_number,
209 justify_qc: justify_qc.clone(),
210 upgrade_certificate: None,
211 proposal_certificate: None,
212 };
213 let leaf: Leaf2 = Leaf::from_quorum_proposal(&proposal).into();
214 let mut qc = justify_qc.to_qc2();
215 qc.data.leaf_commit = leaf.commit();
216 qc.view_number = view_number;
217
218 let mut tx = storage.db.write().await.unwrap();
219 tx.insert_leaf(&LeafQueryData::new(leaf, qc).unwrap())
220 .await
221 .unwrap();
222 tx.insert_block(&BlockQueryData::<SeqTypes>::new(block_header, payload))
223 .await
224 .unwrap();
225 tx.commit().await.unwrap();
226
227 let mut tx = storage.db.read().await.unwrap();
228 let rows = query(
229 "
230 SELECT ns_id, read_ns_id(get_ns_table(h.data), t.ns_index) AS read_ns_id
231 FROM header AS h
232 JOIN transactions AS t ON t.block_height = h.height
233 ORDER BY t.ns_index, t.position
234 ",
235 )
236 .fetch_all(tx.as_mut())
237 .await
238 .unwrap();
239 assert_eq!(rows.len(), txs.len());
240 for (i, row) in rows.into_iter().enumerate() {
241 let ns = u64::from(txs[i].namespace()) as i64;
242 assert_eq!(row.get::<i64, _>("ns_id"), ns);
243 assert_eq!(row.get::<i64, _>("read_ns_id"), ns);
244 }
245 }
246
247 #[test_log::test(tokio::test(flavor = "multi_thread"))]
248 async fn test_postgres_read_ns_table_v0_1() {
249 let v0_1 = versions::version(0, 1);
250 test_postgres_read_ns_table(
251 NodeState::mock()
252 .with_current_version(v0_1)
253 .with_genesis_version(v0_1),
254 )
255 .await;
256 }
257
258 #[test_log::test(tokio::test(flavor = "multi_thread"))]
259 async fn test_postgres_read_ns_table_v0_2() {
260 test_postgres_read_ns_table(NodeState::mock_v2()).await;
261 }
262
263 #[test_log::test(tokio::test(flavor = "multi_thread"))]
264 async fn test_postgres_read_ns_table_v0_3() {
265 test_postgres_read_ns_table(NodeState::mock_v3().with_epoch_height(0)).await;
266 }
267
268 #[test_log::test(tokio::test(flavor = "multi_thread"))]
273 async fn test_record_action_concurrent() {
274 let tmp = Persistence::tmp_storage().await;
275 let storage = Arc::new(Persistence::connect(&tmp).await);
276
277 let handles: Vec<_> = (0u64..20)
278 .map(|i| {
279 let storage = Arc::clone(&storage);
280 tokio::spawn(async move {
281 storage
282 .record_action(ViewNumber::new(i), HotShotAction::Vote)
283 .await
284 })
285 })
286 .collect();
287
288 for handle in handles {
289 handle.await.unwrap().unwrap();
290 }
291
292 let latest = storage.load_latest_acted_view().await.unwrap();
293 assert_eq!(latest, Some(ViewNumber::new(19)));
294 }
295
296 #[test_log::test(tokio::test(flavor = "multi_thread"))]
297 async fn test_light_client_shares_the_node_database() {
298 use hotshot_query_service::availability::LeafId;
299 use light_client::storage::Storage as _;
300
301 use crate::api::{data_source::DataSourceOptions as _, options::Query};
302
303 let tmp = Persistence::tmp_storage().await;
304 let mut opt = Persistence::options(&tmp);
305 let persistence = opt.create().await.unwrap();
306
307 let db = opt
308 .light_client_db(&Query::default())
309 .unwrap()
310 .connect()
311 .await
312 .unwrap();
313 let leaf = light_client::testing::leaf_chain(0..1, versions::EPOCH_VERSION)
314 .await
315 .remove(0);
316 db.insert_leaf(leaf.clone()).await.unwrap();
317 assert_eq!(
318 db.leaf_upper_bound(LeafId::Number(0)).await.unwrap(),
319 Some(leaf)
320 );
321
322 let (leaves,): (i64,) = sqlx::query_as("SELECT count(*) FROM light_client.leaf")
323 .fetch_one(&persistence.db.pool())
324 .await
325 .unwrap();
326 assert_eq!(leaves, 1);
327 }
328
329 #[test_log::test(tokio::test(flavor = "multi_thread"))]
330 async fn test_reset_drops_light_client_cache() {
331 use light_client::storage::Storage as _;
332
333 use crate::api::{data_source::DataSourceOptions as _, options::Query};
334
335 let tmp = Persistence::tmp_storage().await;
336 let opt = Persistence::options(&tmp);
337 let light_client = || opt.light_client_db(&Query::default()).unwrap().connect();
338 let leaf = light_client::testing::leaf_chain(0..1, versions::EPOCH_VERSION)
339 .await
340 .remove(0);
341 let db = light_client().await.unwrap();
342 db.insert_leaf(leaf).await.unwrap();
343 drop(db);
344
345 opt.clone().reset().await.unwrap();
346
347 let db = light_client().await.unwrap();
348 assert_eq!(db.block_height().await.unwrap(), 0);
349 }
350}