1#![cfg(not(feature = "embedded-db"))]
2
3use async_trait::async_trait;
4use hotshot_query_service::{
5 data_source::storage::sql::{Backfill, Transaction},
6 migration::{DataBackfill, MigrationRegistry},
7};
8
9pub struct BackfillHash;
10
11#[async_trait]
12impl DataBackfill for BackfillHash {
13 fn name(&self) -> &'static str {
14 "hash_bigint_backfill_hash"
15 }
16
17 async fn run_batch(
18 &self,
19 tx: &mut Transaction<Backfill>,
20 offset: u64,
23 ) -> anyhow::Result<Option<u64>> {
24 let rows: Vec<(i32, Vec<u8>)> =
25 sqlx::query_as("SELECT id, value FROM hash WHERE id > $1 ORDER BY id LIMIT $2")
26 .bind(offset as i64)
27 .bind(self.batch_size() as i64)
28 .fetch_all(tx.as_mut())
29 .await?;
30
31 if rows.is_empty() {
32 return Ok(None);
33 }
34 let n = rows.len();
35 let last_id = rows.last().expect("non-empty").0 as u64;
36
37 let (ids, values): (Vec<i64>, Vec<Vec<u8>>) = rows
38 .into_iter()
39 .map(|(id, value)| (id as i64, value))
40 .unzip();
41
42 sqlx::query(
43 "INSERT INTO hash_bigint (id, value)
44 SELECT * FROM UNNEST($1::bigint[], $2::bytea[])
45 ON CONFLICT DO NOTHING",
46 )
47 .bind(&ids)
48 .bind(&values)
49 .execute(tx.as_mut())
50 .await?;
51
52 if n < self.batch_size() {
53 Ok(None)
54 } else {
55 Ok(Some(last_id))
56 }
57 }
58}
59
60macro_rules! merkle_tree_backfill {
61 ($struct_name:ident, $migration_name:literal, $legacy_table:literal, $new_table:literal) => {
62 pub struct $struct_name;
63
64 #[async_trait]
65 impl DataBackfill for $struct_name {
66 fn name(&self) -> &'static str {
67 $migration_name
68 }
69
70 fn requires(&self) -> &'static [&'static str] {
71 &["hash_bigint_backfill_hash"]
72 }
73
74 async fn run_batch(
75 &self,
76 tx: &mut Transaction<Backfill>,
77 offset: u64,
80 ) -> anyhow::Result<Option<u64>> {
81 let batch_size = self.batch_size() as i64;
82
83 let any: Option<(i64,)> = sqlx::query_as(concat!(
88 "SELECT created FROM ",
89 $legacy_table,
90 " WHERE created >= $1 LIMIT 1"
91 ))
92 .bind(offset as i64)
93 .fetch_optional(tx.as_mut())
94 .await?;
95 if any.is_none() {
96 return Ok(None);
97 }
98
99 sqlx::query(concat!(
104 "INSERT INTO ",
105 $new_table,
106 " (path, created, hash_id, children, children_bitvec, idx, entry)
107 SELECT
108 legacy_merkle_node.path,
109 legacy_merkle_node.created,
110 new_hash.id,
111 CASE WHEN legacy_merkle_node.children IS NULL THEN NULL
112 ELSE COALESCE((
113 SELECT jsonb_agg(child_new_hash.id ORDER BY \
114 child_position)
115 FROM \
116 jsonb_array_elements_text(legacy_merkle_node.children)
117 WITH ORDINALITY AS child_elem(legacy_hash_id, \
118 child_position)
119 JOIN hash AS child_legacy_hash
120 ON child_legacy_hash.id = \
121 child_elem.legacy_hash_id::INT
122 JOIN hash_bigint AS child_new_hash
123 ON child_new_hash.value = child_legacy_hash.value
124 ), '[]'::jsonb)
125 END,
126 legacy_merkle_node.children_bitvec,
127 legacy_merkle_node.idx,
128 legacy_merkle_node.entry
129 FROM ",
130 $legacy_table,
131 " AS legacy_merkle_node
132 JOIN hash AS legacy_hash ON legacy_hash.id = \
133 legacy_merkle_node.hash_id
134 JOIN hash_bigint AS new_hash ON new_hash.value = legacy_hash.value
135 WHERE legacy_merkle_node.created >= $1 AND legacy_merkle_node.created \
136 < $2
137 ON CONFLICT (path, created) DO NOTHING"
138 ))
139 .bind(offset as i64)
140 .bind(offset as i64 + batch_size)
141 .execute(tx.as_mut())
142 .await?;
143
144 sqlx::query(concat!(
147 "DELETE FROM ",
148 $legacy_table,
149 " WHERE created >= $1 AND created < $2"
150 ))
151 .bind(offset as i64)
152 .bind(offset as i64 + batch_size)
153 .execute(tx.as_mut())
154 .await?;
155
156 Ok(Some(offset + self.batch_size() as u64))
157 }
158 }
159 };
160}
161
162merkle_tree_backfill!(
163 BackfillFeeMerkleTree,
164 "hash_bigint_backfill_fee_merkle_tree",
165 "fee_merkle_tree",
166 "fee_merkle_tree_bigint"
167);
168merkle_tree_backfill!(
169 BackfillBlockMerkleTree,
170 "hash_bigint_backfill_block_merkle_tree",
171 "block_merkle_tree",
172 "block_merkle_tree_bigint"
173);
174
175pub struct CleanupLegacyHashTable;
176
177#[async_trait]
178impl DataBackfill for CleanupLegacyHashTable {
179 fn name(&self) -> &'static str {
180 "hash_bigint_cleanup_legacy_hash"
181 }
182
183 fn requires(&self) -> &'static [&'static str] {
184 &[
185 "hash_bigint_backfill_fee_merkle_tree",
186 "hash_bigint_backfill_block_merkle_tree",
187 ]
188 }
189
190 async fn run_batch(
191 &self,
192 tx: &mut Transaction<Backfill>,
193 _offset: u64,
194 ) -> anyhow::Result<Option<u64>> {
195 sqlx::query("TRUNCATE TABLE hash, fee_merkle_tree, block_merkle_tree")
196 .execute(tx.as_mut())
197 .await?;
198 Ok(None)
199 }
200}
201
202pub fn hash_bigint_migrations() -> MigrationRegistry {
203 MigrationRegistry::new()
204 .backfill(BackfillHash)
205 .backfill(BackfillFeeMerkleTree)
206 .backfill(BackfillBlockMerkleTree)
207 .backfill(CleanupLegacyHashTable)
208}
209
210#[cfg(test)]
211mod tests {
212 use alloy::primitives::Address;
213 use espresso_types::{FEE_MERKLE_TREE_HEIGHT, FeeAccount, FeeAmount, FeeMerkleTree, SeqTypes};
214 use hotshot_query_service::{
215 data_source::{
216 Transaction as _, VersionedDataSource,
217 sql::Config,
218 storage::{
219 MerklizedStateStorage,
220 sql::{
221 SqlStorage, StorageConnectionType, Transaction as SqlTransaction, Write,
222 testing::TmpDb,
223 },
224 },
225 },
226 merklized_state::{Snapshot, UpdateStateData},
227 };
228 use jf_merkle_tree_compat::{
229 LookupResult, MerkleTreeScheme, ToTraversalPath, UniversalMerkleTreeScheme,
230 };
231
232 use super::*;
233 use crate::api::sql::impl_testable_data_source::tmp_options;
234
235 async fn run_to_completion(
236 backfill: &dyn DataBackfill,
237 storage: &SqlStorage,
238 ) -> anyhow::Result<()> {
239 let mut offset = 0u64;
240 loop {
241 let mut tx = storage.backfill().await?;
242 let next = backfill.run_batch(&mut tx, offset).await?;
243 tx.commit().await?;
244 match next {
245 Some(o) => offset = o,
246 None => return Ok(()),
247 }
248 }
249 }
250
251 async fn write_fee_merkle_proofs(
252 tx: &mut SqlTransaction<Write>,
253 tree: &FeeMerkleTree,
254 accounts: &[FeeAccount],
255 block_height: u64,
256 ) {
257 let proofs: Vec<_> = accounts
258 .iter()
259 .map(|a| {
260 let proof = match tree.universal_lookup(a) {
261 LookupResult::Ok(_, p) => p,
262 LookupResult::NotFound(p) => p,
263 LookupResult::NotInMemory => panic!("account not in memory"),
264 };
265 let path =
266 <FeeAccount as ToTraversalPath<{ FeeMerkleTree::ARITY }>>::to_traversal_path(
267 a,
268 tree.height(),
269 );
270 (proof, path)
271 })
272 .collect();
273 UpdateStateData::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>::insert_merkle_nodes_batch(
274 tx,
275 proofs,
276 block_height,
277 )
278 .await
279 .expect("insert_merkle_nodes_batch");
280 }
281
282 fn membership_proof(
283 tree: &FeeMerkleTree,
284 account: &FeeAccount,
285 ) -> <FeeMerkleTree as MerkleTreeScheme>::MembershipProof {
286 match tree.universal_lookup(account) {
287 LookupResult::Ok(_, p) => p,
288 _ => panic!("account not in tree"),
289 }
290 }
291
292 async fn insert_fee_header(
296 tx: &mut SqlTransaction<Write>,
297 height: i64,
298 commit: <FeeMerkleTree as MerkleTreeScheme>::Commitment,
299 ) {
300 let data = serde_json::json!({
301 "fee_merkle_tree_root": serde_json::to_value(commit).unwrap(),
302 "block_merkle_tree_root": "0",
304 });
305 sqlx::query(
306 "INSERT INTO header (height, hash, payload_hash, timestamp, ns_table, data) VALUES \
307 ($1, $2, $3, $4, $5, $6)",
308 )
309 .bind(height)
310 .bind(format!("hash{height}"))
311 .bind("payload")
312 .bind(0i64)
313 .bind("ns")
314 .bind(data)
315 .execute(tx.as_mut())
316 .await
317 .expect("insert header");
318 }
319
320 #[test_log::test(tokio::test(flavor = "multi_thread"))]
335 async fn read_during_backfill_returns_latest_version() {
336 let db = TmpDb::init().await;
337 let opt = tmp_options(&db);
338 let cfg = Config::try_from(&opt).expect("config");
339 let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
340 .await
341 .expect("connect");
342
343 let account = FeeAccount::from(Address::repeat_byte(0x42));
344 let mut tree = FeeMerkleTree::new(FEE_MERKLE_TREE_HEIGHT);
345
346 tree.update(account, FeeAmount::from(100u64)).unwrap();
348 let proof_v1 = membership_proof(&tree, &account);
349 let commit_v1 = tree.commitment();
350 let mut tx = storage.write().await.unwrap();
351 write_fee_merkle_proofs(&mut tx, &tree, &[account], 1).await;
352 tx.commit().await.unwrap();
353
354 tree.update(account, FeeAmount::from(200u64)).unwrap();
356 let proof_v2 = membership_proof(&tree, &account);
357 let commit_v2 = tree.commitment();
358 let mut tx = storage.write().await.unwrap();
359 write_fee_merkle_proofs(&mut tx, &tree, &[account], 2).await;
360 tx.commit().await.unwrap();
361
362 let mut tx = storage.write().await.unwrap();
368 sqlx::query(
369 "INSERT INTO hash (id, value) SELECT id::INT, value FROM hash_bigint ON CONFLICT DO \
370 NOTHING",
371 )
372 .execute(tx.as_mut())
373 .await
374 .unwrap();
375 sqlx::query(
376 "INSERT INTO fee_merkle_tree (path, created, hash_id, children, children_bitvec, idx, \
377 entry) SELECT path, created, hash_id::INT, children, children_bitvec::BIT(256), idx, \
378 entry FROM fee_merkle_tree_bigint WHERE created = 2",
379 )
380 .execute(tx.as_mut())
381 .await
382 .unwrap();
383 sqlx::query("DELETE FROM fee_merkle_tree_bigint WHERE created = 2")
384 .execute(tx.as_mut())
385 .await
386 .unwrap();
387 tx.commit().await.unwrap();
388
389 let mut tx = storage.write().await.unwrap();
391 insert_fee_header(&mut tx, 1, commit_v1).await;
392 insert_fee_header(&mut tx, 2, commit_v2).await;
393 UpdateStateData::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>::set_last_state_height(
394 &mut tx, 2,
395 )
396 .await
397 .unwrap();
398 tx.commit().await.unwrap();
399
400 let mut tx = storage.read().await.unwrap();
402 let got_v1 = tx
403 .get_path(
404 Snapshot::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>::Index(1),
405 account,
406 )
407 .await
408 .expect("get_path at height 1");
409 assert_eq!(got_v1, proof_v1, "height 1 should return the V1 proof");
410
411 let mut tx = storage.read().await.unwrap();
414 let got_v2 = tx
415 .get_path(
416 Snapshot::<SeqTypes, FeeMerkleTree, { FeeMerkleTree::ARITY }>::Index(2),
417 account,
418 )
419 .await
420 .expect("get_path at height 2 (latest version is in the legacy table)");
421 assert_eq!(
422 got_v2, proof_v2,
423 "height 2 must return the latest (V2) proof, not the stale V1 row from *_bigint"
424 );
425 }
426
427 #[test_log::test(tokio::test(flavor = "multi_thread"))]
443 async fn backfill_preserves_fk_when_live_write_shares_hash_value() {
444 let db = TmpDb::init().await;
445 let opt = tmp_options(&db);
446 let cfg = Config::try_from(&opt).expect("config");
447 let storage = SqlStorage::connect(cfg, StorageConnectionType::Query)
448 .await
449 .expect("connect");
450
451 let mut tree = FeeMerkleTree::new(FEE_MERKLE_TREE_HEIGHT);
452 let account = FeeAccount::from(Address::repeat_byte(0x42));
453 tree.update(account, FeeAmount::from(100_u64)).unwrap();
454
455 let mut tx = storage.write().await.unwrap();
457 write_fee_merkle_proofs(&mut tx, &tree, &[account], 1).await;
458 tx.commit().await.unwrap();
459
460 let mut tx = storage.write().await.unwrap();
464 sqlx::query("INSERT INTO hash (id, value) SELECT id::INT, value FROM hash_bigint")
465 .execute(tx.as_mut())
466 .await
467 .unwrap();
468 sqlx::query(
469 "INSERT INTO fee_merkle_tree (path, created, hash_id, children, children_bitvec, idx, \
470 entry) SELECT path, created, hash_id::INT, children, children_bitvec::BIT(256), idx, \
471 entry FROM fee_merkle_tree_bigint",
472 )
473 .execute(tx.as_mut())
474 .await
475 .unwrap();
476 sqlx::query("TRUNCATE fee_merkle_tree_bigint, block_merkle_tree_bigint, hash_bigint")
477 .execute(tx.as_mut())
478 .await
479 .unwrap();
480 sqlx::query(
481 "SELECT setval(pg_get_serial_sequence('hash_bigint', 'id'), GREATEST(COALESCE((SELECT \
482 MAX(id) FROM hash), 1), 1))",
483 )
484 .execute(tx.as_mut())
485 .await
486 .unwrap();
487 tx.commit().await.unwrap();
488
489 let mut tx = storage.write().await.unwrap();
494 write_fee_merkle_proofs(&mut tx, &tree, &[account], 2).await;
495 tx.commit().await.unwrap();
496
497 run_to_completion(&BackfillHash, &storage)
500 .await
501 .expect("BackfillHash failed");
502 run_to_completion(&BackfillFeeMerkleTree, &storage)
503 .await
504 .expect("BackfillFeeMerkleTree failed (FK violation from dropped hash row)");
505
506 let mut tx = storage.read().await.unwrap();
507 let (n_legacy,): (i64,) = sqlx::query_as("SELECT COUNT(*) FROM fee_merkle_tree")
508 .fetch_one(tx.as_mut())
509 .await
510 .unwrap();
511 assert_eq!(n_legacy, 0, "legacy fee_merkle_tree rows were not migrated");
512
513 let (n_heights,): (i64,) =
514 sqlx::query_as("SELECT COUNT(DISTINCT created) FROM fee_merkle_tree_bigint")
515 .fetch_one(tx.as_mut())
516 .await
517 .unwrap();
518 assert_eq!(n_heights, 2, "expected rows at both heights");
519
520 let (n_orphans,): (i64,) = sqlx::query_as(
521 "SELECT COUNT(*) FROM fee_merkle_tree_bigint fmt LEFT JOIN hash_bigint hb ON hb.id = \
522 fmt.hash_id WHERE hb.id IS NULL",
523 )
524 .fetch_one(tx.as_mut())
525 .await
526 .unwrap();
527 assert_eq!(n_orphans, 0, "fee_merkle_tree_bigint has dangling hash_id");
528
529 let (n_orphan_children,): (i64,) = sqlx::query_as(
530 "SELECT COUNT(*) FROM ( SELECT child_id FROM fee_merkle_tree_bigint fmt, \
531 jsonb_array_elements_text(fmt.children) AS arr(child_id) WHERE fmt.children IS NOT \
532 NULL ) c LEFT JOIN hash_bigint hb ON hb.id = c.child_id::BIGINT WHERE hb.id IS NULL",
533 )
534 .fetch_one(tx.as_mut())
535 .await
536 .unwrap();
537 assert_eq!(
538 n_orphan_children, 0,
539 "fee_merkle_tree_bigint.children has dangling hash_id"
540 );
541 }
542}