zaino-state 0.2.0

A mempool and chain-fetching service built on top of zebra's ReadStateService and TrustedChainSync.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
//! Holds database migration tests.

use std::path::PathBuf;
use std::sync::Arc;
use tempfile::TempDir;
use zaino_common::network::ActivationHeights;
use zaino_common::{DatabaseConfig, Network, StorageConfig};

use crate::chain_index::finalised_state::capability::{
    BlockCoreExt as _, DbCore as _, DbRead as _, DbVersion, DbWrite as _, MigrationStatus,
};
use crate::chain_index::finalised_state::db::v1::DB_SCHEMA_V1_HASH;
use crate::chain_index::finalised_state::db::DbBackend;
use crate::chain_index::finalised_state::entry::StoredEntryVar;
use crate::chain_index::finalised_state::ZainoDB;
use crate::chain_index::tests::init_tracing;
use crate::chain_index::tests::vectors::{
    build_mockchain_source, load_test_vectors, TestVectorData,
};
use crate::{
    version, BlockCacheConfig, BlockHeaderData, CompactSize, Height, ZainoVersionedSerde as _,
};

#[tokio::test(flavor = "multi_thread")]
async fn v0_to_v1_full() {
    init_tracing();

    let TestVectorData { blocks, .. } = load_test_vectors().unwrap();

    let temp_dir: TempDir = tempfile::tempdir().unwrap();
    let db_path: PathBuf = temp_dir.path().to_path_buf();

    let v0_config = BlockCacheConfig {
        storage: StorageConfig {
            database: DatabaseConfig {
                path: db_path.clone(),
                ..Default::default()
            },
            ..Default::default()
        },
        db_version: 0,
        network: Network::Regtest(ActivationHeights::default()),
    };
    let v1_config = BlockCacheConfig {
        storage: StorageConfig {
            database: DatabaseConfig {
                path: db_path,
                ..Default::default()
            },
            ..Default::default()
        },
        db_version: 1,
        network: Network::Regtest(ActivationHeights::default()),
    };

    let source = build_mockchain_source(blocks.clone());

    // Build v0 database.
    let zaino_db = ZainoDB::spawn(v0_config, source.clone()).await.unwrap();
    crate::chain_index::tests::vectors::sync_db_with_blockdata(
        zaino_db.router(),
        blocks.clone(),
        None,
    )
    .await;

    zaino_db.wait_until_ready().await;
    dbg!(zaino_db.status());
    dbg!(zaino_db.db_height().await.unwrap());
    dbg!(zaino_db.shutdown().await.unwrap());

    tokio::time::sleep(std::time::Duration::from_millis(1000)).await;

    // Open v1 database and check migration.
    let zaino_db_2 = ZainoDB::spawn(v1_config, source).await.unwrap();
    zaino_db_2.wait_until_ready().await;
    dbg!(zaino_db_2.status());
    let db_height = dbg!(zaino_db_2.db_height().await.unwrap()).unwrap();
    assert_eq!(db_height.0, 200);
    dbg!(zaino_db_2.shutdown().await.unwrap());
}

#[tokio::test(flavor = "multi_thread")]
async fn v0_to_v1_interrupted() {
    init_tracing();

    let blocks = load_test_vectors().unwrap().blocks;

    let temp_dir: TempDir = tempfile::tempdir().unwrap();
    let db_path: PathBuf = temp_dir.path().to_path_buf();

    let v0_config = BlockCacheConfig {
        storage: StorageConfig {
            database: DatabaseConfig {
                path: db_path.clone(),
                ..Default::default()
            },
            ..Default::default()
        },
        db_version: 0,
        network: Network::Regtest(ActivationHeights::default()),
    };
    let v1_config = BlockCacheConfig {
        storage: StorageConfig {
            database: DatabaseConfig {
                path: db_path,
                ..Default::default()
            },
            ..Default::default()
        },
        db_version: 1,
        network: Network::Regtest(ActivationHeights::default()),
    };

    let source = build_mockchain_source(blocks.clone());

    // Build v0 database.
    let zaino_db = ZainoDB::spawn(v0_config, source.clone()).await.unwrap();
    crate::chain_index::tests::vectors::sync_db_with_blockdata(
        zaino_db.router(),
        blocks.clone(),
        None,
    )
    .await;
    zaino_db.wait_until_ready().await;
    dbg!(zaino_db.status());
    dbg!(zaino_db.db_height().await.unwrap());
    dbg!(zaino_db.shutdown().await.unwrap());

    tokio::time::sleep(std::time::Duration::from_millis(1000)).await;

    // Partial build v1 database.
    let zaino_db = DbBackend::spawn_v1(&v1_config).await.unwrap();
    crate::chain_index::tests::vectors::sync_db_with_blockdata(&zaino_db, blocks.clone(), Some(50))
        .await;

    dbg!(zaino_db.shutdown().await.unwrap());

    tokio::time::sleep(std::time::Duration::from_millis(1000)).await;

    // Open v1 database and check migration.
    let zaino_db_2 = ZainoDB::spawn(v1_config, source).await.unwrap();
    zaino_db_2.wait_until_ready().await;
    dbg!(zaino_db_2.status());
    let db_height = dbg!(zaino_db_2.db_height().await.unwrap()).unwrap();
    assert_eq!(db_height.0, 200);
    dbg!(zaino_db_2.shutdown().await.unwrap());
}

#[tokio::test(flavor = "multi_thread")]
async fn v0_to_v1_partial() {
    init_tracing();

    let blocks = load_test_vectors().unwrap().blocks;

    let temp_dir: TempDir = tempfile::tempdir().unwrap();
    let db_path: PathBuf = temp_dir.path().to_path_buf();

    let v0_config = BlockCacheConfig {
        storage: StorageConfig {
            database: DatabaseConfig {
                path: db_path.clone(),
                ..Default::default()
            },
            ..Default::default()
        },
        db_version: 0,
        network: Network::Regtest(ActivationHeights::default()),
    };
    let v1_config = BlockCacheConfig {
        storage: StorageConfig {
            database: DatabaseConfig {
                path: db_path,
                ..Default::default()
            },
            ..Default::default()
        },
        db_version: 1,
        network: Network::Regtest(ActivationHeights::default()),
    };

    let source = build_mockchain_source(blocks.clone());

    // Build v0 database.
    let zaino_db = ZainoDB::spawn(v0_config, source.clone()).await.unwrap();
    crate::chain_index::tests::vectors::sync_db_with_blockdata(
        zaino_db.router(),
        blocks.clone(),
        None,
    )
    .await;

    zaino_db.wait_until_ready().await;
    dbg!(zaino_db.status());
    dbg!(zaino_db.db_height().await.unwrap());
    dbg!(zaino_db.shutdown().await.unwrap());

    tokio::time::sleep(std::time::Duration::from_millis(1000)).await;

    // Partial build v1 database.
    let zaino_db = DbBackend::spawn_v1(&v1_config).await.unwrap();
    crate::chain_index::tests::vectors::sync_db_with_blockdata(&zaino_db, blocks.clone(), None)
        .await;

    dbg!(zaino_db.shutdown().await.unwrap());

    tokio::time::sleep(std::time::Duration::from_millis(1000)).await;

    // Open v1 database and check migration.
    let zaino_db_2 = ZainoDB::spawn(v1_config, source).await.unwrap();
    zaino_db_2.wait_until_ready().await;
    dbg!(zaino_db_2.status());
    let db_height = dbg!(zaino_db_2.db_height().await.unwrap()).unwrap();
    assert_eq!(db_height.0, 200);
    dbg!(zaino_db_2.shutdown().await.unwrap());
}

#[tokio::test(flavor = "multi_thread")]
async fn v1_0_to_v1_1_metadata_migration() {
    init_tracing();

    // Prepare test blocks/source (we won't rely on heavy rebuild in this metadata-only migration)
    let TestVectorData { blocks, .. } = load_test_vectors().unwrap();

    let temp_dir: TempDir = tempfile::tempdir().unwrap();
    let db_path: PathBuf = temp_dir.path().to_path_buf();

    // BlockCacheConfig: use v1 target like other tests
    let v1_config = BlockCacheConfig {
        storage: StorageConfig {
            database: DatabaseConfig {
                path: db_path.clone(),
                ..Default::default()
            },
            ..Default::default()
        },
        db_version: 1,
        network: Network::Regtest(ActivationHeights::default()),
    };

    let source = build_mockchain_source(blocks.clone());

    // Build v1 database.
    let zaino_db = ZainoDB::spawn(v1_config.clone(), source.clone())
        .await
        .unwrap();
    crate::chain_index::tests::vectors::sync_db_with_blockdata(
        zaino_db.router(),
        blocks.clone(),
        None,
    )
    .await;

    zaino_db.wait_until_ready().await;
    dbg!(zaino_db.status());
    dbg!(zaino_db.db_height().await.unwrap());

    // 2) Coerce the metadata to look like an older 1.0.0 DB with a stale schema hash and an
    //    in-progress migration status so the migration has work to do.
    let mut metadata = zaino_db.get_metadata().await.unwrap();
    metadata.version = DbVersion {
        major: 1,
        minor: 0,
        patch: 0,
    };
    // An obviously different schema hash (any non-matching 32 bytes will do)
    metadata.schema_hash = [0u8; 32];
    // Set a non-empty migration status to ensure migration clears it
    metadata.migration_status = MigrationStatus::PartialBuidInProgress;

    zaino_db.router().update_metadata(metadata).await.unwrap();

    // shutdown this backend so ZainoDB::spawn will open the same DB and perform migration
    dbg!(zaino_db.shutdown().await.unwrap());

    // Let the filesystem settle (tests elsewhere do a brief sleep)
    tokio::time::sleep(std::time::Duration::from_millis(100)).await;

    // 3) Spawn ZainoDB which should detect current_version == 1.0.0 < target 1.1.0 and run the metadata
    //    migration. We await until ready so migration completes.
    let zaino_db = ZainoDB::spawn(v1_config, source).await.unwrap();
    zaino_db.wait_until_ready().await;

    // 4) Read persisted metadata and assert migration effects.
    let post_meta = zaino_db.get_metadata().await.unwrap();
    assert_eq!(
        post_meta.version,
        DbVersion {
            major: 1,
            minor: 1,
            patch: 0
        }
    );
    assert_eq!(post_meta.migration_status, MigrationStatus::Empty);
    assert_eq!(post_meta.schema_hash, DB_SCHEMA_V1_HASH);

    zaino_db.shutdown().await.unwrap();
}

/// Tests that a database containing a mix of V1-encoded and V2-encoded
/// `BlockHeaderData` entries — which arises when a v1.0.0 deployment is upgraded
/// to v1.1.0 mid-chain — deserialises every header correctly and returns the right
/// height for every block.
///
/// Scenario:
///   Phase 1 – write first half of the chain using the normal V2 path, then
///              backpatch those header entries in LMDB to use the V1 wire format
///              (simulating a v1.0.0 on-disk state) and downgrade the stored
///              metadata to 1.0.0.
///   Phase 2 – reopen with ZainoDB (target 1.1.0); the metadata-only migration
///              runs, bumping the version without touching the headers table.
///   Phase 3 – write the second half using the current V2 path.
///   Phase 4 – assert that every header (V1 or V2 on-disk) deserialises to the
///              correct height.
#[tokio::test(flavor = "multi_thread")]
async fn v1_0_to_v1_1_mixed_blockheaderdata_formats() {
    init_tracing();

    let TestVectorData { blocks, .. } = load_test_vectors().unwrap();

    // Split at the midpoint so the DB will eventually hold both encoding formats.
    let split = blocks.len() / 2;
    let first_half = blocks[..split].to_vec();
    let second_half = blocks[split..].to_vec();

    let temp_dir: TempDir = tempfile::tempdir().unwrap();
    let db_path: PathBuf = temp_dir.path().to_path_buf();

    let v1_config = BlockCacheConfig {
        storage: StorageConfig {
            database: DatabaseConfig {
                path: db_path.clone(),
                ..Default::default()
            },
            ..Default::default()
        },
        db_version: 1,
        network: Network::Regtest(ActivationHeights::default()),
    };

    let source = build_mockchain_source(blocks.clone());

    // ── Phase 1: build first half (V2 write path), collect headers, downgrade metadata ──

    // Spawn the v1 backend directly so we can call `get_block_header` without
    // going through ZainoDB's migration logic.
    let db = DbBackend::spawn_v1(&v1_config).await.unwrap();
    crate::chain_index::tests::vectors::sync_db_with_blockdata(&db, first_half.clone(), None).await;

    // Wait for the background validator to mark all first-half blocks as known-good
    // so that `get_block_header` can take the fast path.
    db.wait_until_ready().await;

    // Read back the decoded headers; we will re-encode them as V1 below.
    let mut first_half_headers: Vec<(Height, BlockHeaderData)> = Vec::new();
    for block_data in &first_half {
        let h = Height(block_data.height);
        let header = db.get_block_header(h).await.unwrap();
        first_half_headers.push((h, header));
    }

    // Downgrade stored metadata to 1.0.0 so ZainoDB::spawn will trigger the
    // minor migration when we reopen.
    let mut meta = db.get_metadata().await.unwrap();
    meta.version = DbVersion {
        major: 1,
        minor: 0,
        patch: 0,
    };
    meta.schema_hash = [0u8; 32]; // stale hash; migration will refresh it
    db.update_metadata(meta).await.unwrap();

    db.shutdown().await.unwrap();
    tokio::time::sleep(std::time::Duration::from_millis(500)).await;

    // ── Phase 2: overwrite first-half headers with correctly-checksummed V1 bytes ──
    //
    // We open the LMDB environment directly (the ZainoDB is shut down, so there is
    // no active writer) and replace each header entry with bytes that carry a V1-
    // encoded BlockHeaderData (BlockIndex with *optional* height) and a checksum
    // computed over those V1 bytes.  After this the `headers_1_0_0` table holds:
    //
    //   heights  0 .. split-1  → StoredEntryVar with V1-encoded BlockHeaderData
    //   heights  split .. N-1  → (not yet written)
    {
        use lmdb::{Environment, EnvironmentFlags, Transaction as _, WriteFlags};

        let lmdb_path = db_path.join("regtest").join("v1");
        let env = Environment::new()
            .set_max_dbs(12)
            .set_map_size(128 * 1024 * 1024) // 128 MiB – plenty for the test DB
            .set_flags(EnvironmentFlags::NO_TLS)
            .open(&lmdb_path)
            .unwrap();

        let headers_db = env.open_db(Some("headers_1_0_0")).unwrap();
        let mut txn = env.begin_rw_txn().unwrap();

        for (height, header) in &first_half_headers {
            // Key = Height::to_bytes() = [V1 tag][big-endian u32]
            let height_key = height.to_bytes().unwrap();

            // Re-encode this header using the V1 wire format:
            //   [V1 tag = 1][BlockIndex V1 body (optional height)][BlockData V1 body]
            let v1_item_bytes = header.to_bytes_with_version(version::V1).unwrap();

            // Checksum must be computed over the *exact* bytes that will be stored so
            // that StoredEntryVar::verify() finds a match on its V1 iteration.
            // checksum = blake2b256(height_key || v1_item_bytes)
            let checksum = StoredEntryVar::<BlockHeaderData>::blake2b256(
                &[height_key.as_slice(), v1_item_bytes.as_slice()].concat(),
            );

            // Build the full LMDB value for a StoredEntryVar<BlockHeaderData>:
            //   [StoredEntry V1 outer tag][CompactSize(item_len)][item_bytes][32-byte checksum]
            //
            // The StoredEntry outer tag is always V1 here because StoredEntryVar::VERSION = V1.
            // The "V1/V2" distinction lives *inside* item_bytes (the first byte of item_bytes
            // is the BlockHeaderData version tag).
            let mut stored_bytes: Vec<u8> = Vec::new();
            stored_bytes.push(version::V1); // StoredEntryVar outer version tag
            CompactSize::write(&mut stored_bytes, v1_item_bytes.len()).unwrap();
            stored_bytes.extend_from_slice(&v1_item_bytes);
            stored_bytes.extend_from_slice(&checksum);

            // Overwrite the existing V2 entry with the V1 one.
            txn.put(headers_db, &height_key, &stored_bytes, WriteFlags::empty())
                .unwrap();
        }

        txn.commit().unwrap();
        env.sync(true).unwrap();
    }

    tokio::time::sleep(std::time::Duration::from_millis(100)).await;

    // ── Phase 3: reopen at v1.1.0 – migration runs; write second half (V2) ──
    //
    // ZainoDB detects current_version 1.0.0 < target 1.1.0 and runs
    // Migration1_0_0To1_1_0, which is metadata-only and leaves the headers table
    // (including our freshly-written V1 entries) untouched.
    let zaino_db = ZainoDB::spawn(v1_config.clone(), source.clone())
        .await
        .unwrap();
    zaino_db.wait_until_ready().await;

    // Confirm the metadata migration completed correctly.
    let post_meta = zaino_db.get_metadata().await.unwrap();
    assert_eq!(
        post_meta.version,
        DbVersion {
            major: 1,
            minor: 1,
            patch: 0,
        },
        "DB version should be 1.1.0 after migration"
    );
    assert_eq!(
        post_meta.migration_status,
        MigrationStatus::Empty,
        "Migration status should be Empty after the metadata-only migration"
    );
    assert_eq!(
        post_meta.schema_hash, DB_SCHEMA_V1_HASH,
        "Schema hash should be refreshed to the current value"
    );

    // Write the second half; these use the current (V2) BlockHeaderData format,
    // so the DB now holds V1 headers at 0..split-1 and V2 headers at split..N-1.
    crate::chain_index::tests::vectors::sync_db_with_blockdata(
        zaino_db.router(),
        second_half.clone(),
        None,
    )
    .await;
    zaino_db.wait_until_ready().await;

    // ── Phase 4: verify every header decodes correctly with the right height ──
    //
    // The initial block scan in the background task has already called
    // validate_block_blocking() for every height, which internally invokes
    // StoredEntryVar::verify().  For V1-encoded entries that method tries V2 first
    // (no match), then V1 (match).  Reaching Ready status here means all blocks
    // passed validation.
    let zaino_db = Arc::new(zaino_db);
    let reader = zaino_db.to_reader();

    // V1-format headers (first half) – these were written before the format bump.
    for block_data in &first_half {
        let h = Height(block_data.height);
        let header = reader
            .get_block_header(h)
            .await
            .unwrap_or_else(|e| panic!("failed to read V1-format header at height {}: {e}", h.0));
        assert_eq!(
            header.context.index.height, h,
            "V1-format header at height {} returned wrong height",
            h.0
        );
    }

    // V2-format headers (second half) – written after the migration.
    for block_data in &second_half {
        let h = Height(block_data.height);
        let header = reader
            .get_block_header(h)
            .await
            .unwrap_or_else(|e| panic!("failed to read V2-format header at height {}: {e}", h.0));
        assert_eq!(
            header.context.index.height, h,
            "V2-format header at height {} returned wrong height",
            h.0
        );
    }

    // The overall DB tip should cover the full chain.
    let db_height = zaino_db.db_height().await.unwrap().unwrap();
    let expected_tip = Height((blocks.len() - 1) as u32);
    assert_eq!(
        db_height, expected_tip,
        "DB tip should be the last block's height after building the full chain"
    );

    zaino_db.shutdown().await.unwrap();
}