Skip to main content

commonware_storage/ordinal/
mod.rs

1//! A persistent index that maps sparse indices to [commonware_utils::Array]s.
2//!
3//! [Ordinal] is a collection of [commonware_runtime::Blob]s containing ordered records of fixed size.
4//! Because records are fixed size, file position corresponds directly to index. Unlike
5//! [crate::journal::contiguous::fixed::Journal], [Ordinal] supports out-of-order insertion.
6//!
7//! # Design
8//!
9//! [Ordinal] is a collection of [commonware_runtime::Blob]s where:
10//! - Each record: `[V][crc32(V)]` where V is an [commonware_utils::Array]
11//! - Index N is at file offset: `N * RECORD_SIZE`
12//! - A [crate::rmap::RMap] tracks which indices have been written (and which are missing)
13//!
14//! # File Organization
15//!
16//! Records are grouped into blobs to avoid having too many files:
17//!
18//! ```text
19//! Blob 0: indices 0-999
20//! Blob 1: indices 1000-1999
21//! ...
22//! ```
23//!
24//! # Format
25//!
26//! [Ordinal] stores values in the following format:
27//!
28//! ```text
29//! +---+---+---+---+---+---+---+---+---+---+---+---+---+
30//! | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |10 |11 |12 |
31//! +---+---+---+---+---+---+---+---+---+---+---+---+---+
32//! |          Value (Fixed Size)       |     CRC32     |
33//! +---+---+---+---+---+---+---+---+---+---+---+---+---+
34//! ```
35//!
36//! # Performance Characteristics
37//!
38//! - **Writes**: O(1) - direct offset calculation
39//! - **Reads**: O(1) - direct offset calculation
40//! - **Has**: O(1) - in-memory lookup (via [crate::rmap::RMap])
41//! - **Next Gap**: O(log n) - in-memory range query (via [crate::rmap::RMap])
42//! - **Recovery**: O(n) over committed records when bits are provided (`None` resets the store)
43//!
44//! # Atomicity
45//!
46//! [Ordinal] eagerly writes all new data to [commonware_runtime::Blob]s. New data, however, is not
47//! synced until [Ordinal::sync] is called. As a result, data is not guaranteed to be atomically
48//! persisted (i.e. shutdown before [Ordinal::sync] may lead to some writes being lost).
49//!
50//! _If you want atomicity for sparse writes, pair [commonware_utils::bitmap::BitMap] and
51//! [crate::metadata::Metadata] with [Ordinal] (use bits to indicate which items have been atomically
52//! written)._
53//!
54//! # Recovery
55//!
56//! To recover existing data, pass `Some(bits)` to [Ordinal::init]. The bits identify which records
57//! were durably committed by the caller and rebuild the in-memory [crate::rmap::RMap] without
58//! re-reading the records they mark (a damaged marked record surfaces at [Ordinal::get]). Records in
59//! sections listed with no bitmap are instead validated using their CRC32, and such a section must
60//! hold exactly `items_per_blob` records. Stored sections omitted from `bits`, or whose bitmap
61//! marks no record, are removed without being opened. Stored records whose bits are unset are
62//! cleared before replay. Records missing from stored sections, and CRC-invalid records in sections
63//! listed with no bitmap, fail initialization. Passing `Some(BTreeMap::new())` or `None` removes
64//! all stored sections and starts empty.
65//!
66//! # Example
67//!
68//! ```rust
69//! use commonware_runtime::{Spawner, Runner, deterministic};
70//! use commonware_storage::ordinal::{Ordinal, Config};
71//! use commonware_utils::{sequence::FixedBytes, NZUsize, NZU64};
72//!
73//! let executor = deterministic::Runner::default();
74//! executor.start(|context| async move {
75//!     // Create a store for 32-byte values
76//!     let cfg = Config {
77//!         partition: "ordinal-store".into(),
78//!         items_per_blob: NZU64!(10000),
79//!         write_buffer: NZUsize!(4096),
80//!         replay_buffer: NZUsize!(1024 * 1024),
81//!     };
82//!     let mut store = Ordinal::<_, FixedBytes<32>>::init(context, cfg, None).await.unwrap();
83//!
84//!     // Put values at specific indices
85//!     let value1 = FixedBytes::new([1u8; 32]);
86//!     let value2 = FixedBytes::new([2u8; 32]);
87//!     store = store.put(0, value1).await.unwrap();
88//!     store = store.put(5, value2).await.unwrap();
89//!
90//!     // Sync to disk
91//!     store = store.sync().await.unwrap();
92//!
93//!     // Check for gaps
94//!     let (current_end, next_start) = store.next_gap(0);
95//!     assert_eq!(current_end, Some(0));
96//!     assert_eq!(next_start, Some(5));
97//!
98//!     // Sync the store
99//!     store.sync().await.unwrap();
100//! });
101//! ```
102
103#[cfg(all(test, feature = "arbitrary"))]
104mod conformance;
105mod storage;
106
107use std::num::{NonZeroU64, NonZeroUsize};
108pub use storage::Ordinal;
109use thiserror::Error;
110
111/// Errors that can occur when interacting with the [Ordinal].
112#[derive(Debug, Error)]
113pub enum Error {
114    #[error("runtime error: {0}")]
115    Runtime(#[from] commonware_runtime::Error),
116    #[error("invalid blob name: {0}")]
117    InvalidBlobName(String),
118    #[error("invalid record: {0}")]
119    InvalidRecord(u64),
120    #[error("missing record at {0}")]
121    MissingRecord(u64),
122}
123
124/// Configuration for [Ordinal] storage.
125#[derive(Clone)]
126pub struct Config {
127    /// The [commonware_runtime::Storage] partition to use for storing the index.
128    pub partition: String,
129
130    /// The maximum number of items to store in each index blob.
131    pub items_per_blob: NonZeroU64,
132
133    /// The size of the write buffer to use when writing to the index.
134    pub write_buffer: NonZeroUsize,
135
136    /// The size of the read buffer to use on restart.
137    pub replay_buffer: NonZeroUsize,
138}
139
140#[cfg(test)]
141mod tests {
142    use super::*;
143    use crate::utils::bits_for_indices;
144    use commonware_codec::{Buf, FixedSize, Read, ReadExt, Write};
145    use commonware_cryptography::Crc32;
146    use commonware_formatting::hex;
147    use commonware_macros::{test_group, test_traced};
148    use commonware_runtime::{
149        Blob, BufMut, Metrics as _, ReadOptions, Runner, Storage, Supervisor as _, WriteOptions,
150        deterministic,
151        mocks::{
152            DelayedSyncContext, PendingSyncs, RecordingContext, SyncFaultContext,
153            drive_pending_syncs,
154        },
155    };
156    use commonware_utils::{NZU64, NZUsize, bitmap::BitMap, sequence::FixedBytes};
157    use rand::Rng;
158    use std::{collections::BTreeMap, num::NonZeroUsize};
159
160    const DEFAULT_ITEMS_PER_BLOB: u64 = 1000;
161    const DEFAULT_WRITE_BUFFER: usize = 4096;
162    const DEFAULT_REPLAY_BUFFER: usize = 1024 * 1024;
163
164    #[test_traced]
165    fn test_put_get() {
166        // Initialize the deterministic context
167        let executor = deterministic::Runner::default();
168        executor.start(|context| async move {
169            // Initialize the store
170            let cfg = Config {
171                partition: "test-ordinal".into(),
172                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
173                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
174                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
175            };
176            let mut store =
177                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
178                    .await
179                    .expect("Failed to initialize store");
180
181            let value = FixedBytes::new([42u8; 32]);
182
183            // Check index doesn't exist
184            assert!(!store.has(0));
185
186            // Put the value at index 0
187            store = store
188                .put(0, value.clone())
189                .await
190                .expect("Failed to put data");
191
192            // Check index exists
193            assert!(store.has(0));
194
195            // Get the value back (before sync)
196            let retrieved = store
197                .get(0)
198                .await
199                .expect("Failed to get data")
200                .expect("Data not found");
201            assert_eq!(retrieved, value);
202
203            // Force a sync
204            store = store.sync().await.expect("Failed to sync data");
205
206            // Check metrics
207            let buffer = context.encode();
208            assert!(buffer.contains("gets_total 1"), "{}", buffer);
209            assert!(buffer.contains("puts_total 1"), "{}", buffer);
210            assert!(buffer.contains("has_total 2"), "{}", buffer);
211            assert!(buffer.contains("syncs_total 1"), "{}", buffer);
212            assert!(buffer.contains("pruned_total 0"), "{}", buffer);
213
214            // Get the value back (after sync)
215            let retrieved = store
216                .get(0)
217                .await
218                .expect("Failed to get data")
219                .expect("Data not found");
220            assert_eq!(retrieved, value);
221        });
222    }
223
224    #[test_traced]
225    fn test_sync_does_not_report_success_while_flush_fails() {
226        let executor = deterministic::Runner::default();
227        executor.start(|context| async move {
228            let cfg = Config {
229                partition: "test-ordinal".into(),
230                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
231                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
232                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
233            };
234            let mut store =
235                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
236                    .await
237                    .expect("Failed to initialize store");
238
239            store = store
240                .put(0, FixedBytes::new([42u8; 32]))
241                .await
242                .expect("Failed to put data");
243
244            // Force flush failure by removing the underlying blob before sync.
245            let section = 0u64.to_be_bytes();
246            context
247                .remove(&cfg.partition, Some(&section))
248                .await
249                .expect("Failed to remove blob");
250
251            // Sync must observe the durability failure.
252            assert!(store.sync().await.is_err(), "sync unexpectedly succeeded");
253        });
254    }
255
256    #[test_traced]
257    fn test_multiple_indices() {
258        // Initialize the deterministic context
259        let executor = deterministic::Runner::default();
260        executor.start(|context| async move {
261            // Initialize the store
262            let cfg = Config {
263                partition: "test-ordinal".into(),
264                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
265                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
266                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
267            };
268            let mut store =
269                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
270                    .await
271                    .expect("Failed to initialize store");
272
273            // Insert multiple values at different indices
274            let indices = vec![
275                (0u64, FixedBytes::new([0u8; 32])),
276                (5u64, FixedBytes::new([5u8; 32])),
277                (10u64, FixedBytes::new([10u8; 32])),
278                (100u64, FixedBytes::new([100u8; 32])),
279                (1000u64, FixedBytes::new([200u8; 32])), // Different blob
280            ];
281
282            for (index, value) in &indices {
283                store = store
284                    .put(*index, value.clone())
285                    .await
286                    .expect("Failed to put data");
287            }
288
289            // Sync to disk
290            store = store.sync().await.expect("Failed to sync");
291
292            // Retrieve all values and verify
293            for (index, value) in &indices {
294                let retrieved = store
295                    .get(*index)
296                    .await
297                    .expect("Failed to get data")
298                    .expect("Data not found");
299                assert_eq!(&retrieved, value);
300            }
301        });
302    }
303
304    #[test_traced]
305    fn test_sparse_indices() {
306        // Initialize the deterministic context
307        let executor = deterministic::Runner::default();
308        executor.start(|context| async move {
309            // Initialize the store
310            let cfg = Config {
311                partition: "test-ordinal".into(),
312                items_per_blob: NZU64!(100), // Smaller blobs for testing
313                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
314                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
315            };
316            let mut store =
317                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
318                    .await
319                    .expect("Failed to initialize store");
320
321            // Insert sparse values
322            let indices = vec![
323                (0u64, FixedBytes::new([0u8; 32])),
324                (99u64, FixedBytes::new([99u8; 32])), // End of first blob
325                (100u64, FixedBytes::new([100u8; 32])), // Start of second blob
326                (500u64, FixedBytes::new([200u8; 32])), // Start of sixth blob
327            ];
328
329            for (index, value) in &indices {
330                store = store
331                    .put(*index, value.clone())
332                    .await
333                    .expect("Failed to put data");
334            }
335
336            // Check that intermediate indices don't exist
337            assert!(!store.has(1));
338            assert!(!store.has(50));
339            assert!(!store.has(101));
340            assert!(!store.has(499));
341
342            // Sync and verify
343            store = store.sync().await.expect("Failed to sync");
344
345            for (index, value) in &indices {
346                let retrieved = store
347                    .get(*index)
348                    .await
349                    .expect("Failed to get data")
350                    .expect("Data not found");
351                assert_eq!(&retrieved, value);
352            }
353        });
354    }
355
356    #[test_traced]
357    fn test_next_gap() {
358        // Initialize the deterministic context
359        let executor = deterministic::Runner::default();
360        executor.start(|context| async move {
361            // Initialize the store
362            let cfg = Config {
363                partition: "test-ordinal".into(),
364                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
365                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
366                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
367            };
368            let mut store =
369                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
370                    .await
371                    .expect("Failed to initialize store");
372
373            // Insert values with gaps
374            store = store.put(1, FixedBytes::new([1u8; 32])).await.unwrap();
375            store = store.put(10, FixedBytes::new([10u8; 32])).await.unwrap();
376            store = store.put(11, FixedBytes::new([11u8; 32])).await.unwrap();
377            store = store.put(14, FixedBytes::new([14u8; 32])).await.unwrap();
378
379            // Check gaps
380            let (current_end, start_next) = store.next_gap(0);
381            assert!(current_end.is_none());
382            assert_eq!(start_next, Some(1));
383
384            let (current_end, start_next) = store.next_gap(1);
385            assert_eq!(current_end, Some(1));
386            assert_eq!(start_next, Some(10));
387
388            let (current_end, start_next) = store.next_gap(10);
389            assert_eq!(current_end, Some(11));
390            assert_eq!(start_next, Some(14));
391
392            let (current_end, start_next) = store.next_gap(11);
393            assert_eq!(current_end, Some(11));
394            assert_eq!(start_next, Some(14));
395
396            let (current_end, start_next) = store.next_gap(12);
397            assert!(current_end.is_none());
398            assert_eq!(start_next, Some(14));
399
400            let (current_end, start_next) = store.next_gap(14);
401            assert_eq!(current_end, Some(14));
402            assert!(start_next.is_none());
403        });
404    }
405
406    #[test_traced]
407    fn test_missing_items() {
408        // Initialize the deterministic context
409        let executor = deterministic::Runner::default();
410        executor.start(|context| async move {
411            // Initialize the store
412            let cfg = Config {
413                partition: "test-ordinal".into(),
414                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
415                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
416                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
417            };
418            let mut store =
419                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
420                    .await
421                    .expect("Failed to initialize store");
422
423            // Test 1: Empty store - should return no items
424            assert_eq!(store.missing_items(0, 5), Vec::<u64>::new());
425            assert_eq!(store.missing_items(100, 10), Vec::<u64>::new());
426
427            // Test 2: Insert values with gaps
428            store = store.put(1, FixedBytes::new([1u8; 32])).await.unwrap();
429            store = store.put(2, FixedBytes::new([2u8; 32])).await.unwrap();
430            store = store.put(5, FixedBytes::new([5u8; 32])).await.unwrap();
431            store = store.put(6, FixedBytes::new([6u8; 32])).await.unwrap();
432            store = store.put(10, FixedBytes::new([10u8; 32])).await.unwrap();
433
434            // Test 3: Find missing items from the beginning
435            assert_eq!(store.missing_items(0, 5), vec![0, 3, 4, 7, 8]);
436            assert_eq!(store.missing_items(0, 6), vec![0, 3, 4, 7, 8, 9]);
437            assert_eq!(store.missing_items(0, 7), vec![0, 3, 4, 7, 8, 9]);
438
439            // Test 4: Find missing items from within a gap
440            assert_eq!(store.missing_items(3, 3), vec![3, 4, 7]);
441            assert_eq!(store.missing_items(4, 2), vec![4, 7]);
442
443            // Test 5: Find missing items from within a range
444            assert_eq!(store.missing_items(1, 3), vec![3, 4, 7]);
445            assert_eq!(store.missing_items(2, 4), vec![3, 4, 7, 8]);
446            assert_eq!(store.missing_items(5, 2), vec![7, 8]);
447
448            // Test 6: Find missing items after the last range (no more gaps)
449            assert_eq!(store.missing_items(11, 5), Vec::<u64>::new());
450            assert_eq!(store.missing_items(100, 10), Vec::<u64>::new());
451
452            // Test 7: Large gap scenario
453            store = store.put(1000, FixedBytes::new([100u8; 32])).await.unwrap();
454
455            // Gap between 10 and 1000
456            let items = store.missing_items(11, 10);
457            assert_eq!(items, vec![11, 12, 13, 14, 15, 16, 17, 18, 19, 20]);
458
459            // Request more items than available in gap
460            let items = store.missing_items(990, 15);
461            assert_eq!(
462                items,
463                vec![990, 991, 992, 993, 994, 995, 996, 997, 998, 999]
464            );
465
466            // Test 8: After syncing (data should remain consistent)
467            store = store.sync().await.unwrap();
468            assert_eq!(store.missing_items(0, 5), vec![0, 3, 4, 7, 8]);
469            assert_eq!(store.missing_items(3, 3), vec![3, 4, 7]);
470
471            // Test 9: Cross-blob boundary scenario
472            store = store.put(9999, FixedBytes::new([99u8; 32])).await.unwrap();
473            store = store
474                .put(10001, FixedBytes::new([101u8; 32]))
475                .await
476                .unwrap();
477
478            // Find missing items across blob boundary (10000 is the boundary)
479            let items = store.missing_items(9998, 5);
480            assert_eq!(items, vec![9998, 10000]);
481        });
482    }
483
484    #[test_traced]
485    fn test_restart() {
486        // Initialize the deterministic context
487        let executor = deterministic::Runner::default();
488        executor.start(|context| async move {
489            let cfg = Config {
490                partition: "test-ordinal".into(),
491                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
492                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
493                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
494            };
495
496            // Insert data and close
497            {
498                let mut store =
499                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
500                        .await
501                        .expect("Failed to initialize store");
502
503                let values = vec![
504                    (0u64, FixedBytes::new([0u8; 32])),
505                    (100u64, FixedBytes::new([100u8; 32])),
506                    (1000u64, FixedBytes::new([200u8; 32])),
507                ];
508
509                for (index, value) in &values {
510                    store = store
511                        .put(*index, value.clone())
512                        .await
513                        .expect("Failed to put data");
514                }
515
516                store.sync().await.expect("Failed to sync store");
517            }
518
519            // Reopen with bits and verify committed data persisted
520            {
521                let mut bits0 = BitMap::zeroes(DEFAULT_ITEMS_PER_BLOB);
522                bits0.set(0, true);
523                bits0.set(100, true);
524                let mut bits1 = BitMap::zeroes(DEFAULT_ITEMS_PER_BLOB);
525                bits1.set(0, true);
526                let bits0 = Some(bits0);
527                let bits1 = Some(bits1);
528                let mut bits = BTreeMap::new();
529                bits.insert(0, &bits0);
530                bits.insert(1, &bits1);
531                let store = Ordinal::<_, FixedBytes<32>>::init(
532                    context.child("second"),
533                    cfg.clone(),
534                    Some(bits),
535                )
536                .await
537                .expect("Failed to initialize store");
538
539                let values = vec![
540                    (0u64, FixedBytes::new([0u8; 32])),
541                    (100u64, FixedBytes::new([100u8; 32])),
542                    (1000u64, FixedBytes::new([200u8; 32])),
543                ];
544
545                for (index, value) in &values {
546                    let retrieved = store
547                        .get(*index)
548                        .await
549                        .expect("Failed to get data")
550                        .expect("Data not found");
551                    assert_eq!(&retrieved, value);
552                }
553
554                // Check gaps are preserved
555                let (current_end, start_next) = store.next_gap(0);
556                assert_eq!(current_end, Some(0));
557                assert_eq!(start_next, Some(100));
558            }
559        });
560    }
561
562    #[test_traced]
563    fn test_invalid_record() {
564        // Initialize the deterministic context
565        let executor = deterministic::Runner::default();
566        executor.start(|context| async move {
567            let cfg = Config {
568                partition: "test-ordinal".into(),
569                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
570                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
571                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
572            };
573
574            // Create store with data
575            {
576                let mut store =
577                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
578                        .await
579                        .expect("Failed to initialize store");
580
581                store = store
582                    .put(0, FixedBytes::new([42u8; 32]))
583                    .await
584                    .expect("Failed to put data");
585                store.sync().await.expect("Failed to sync store");
586            }
587
588            // Corrupt the data
589            {
590                let (blob, _) = context
591                    .open("test-ordinal", &0u64.to_be_bytes())
592                    .await
593                    .unwrap();
594                // Corrupt the CRC by changing a byte
595                blob.write_at(32, vec![0xFF], WriteOptions::SYNC)
596                    .await
597                    .unwrap();
598            }
599
600            // Reopen without bits, deleting the stored corrupted data before replay.
601            {
602                let store =
603                    Ordinal::<_, FixedBytes<32>>::init(context.child("second"), cfg.clone(), None)
604                        .await
605                        .expect("Failed to initialize store");
606
607                let result = store.get(0).await.unwrap();
608                assert!(result.is_none());
609
610                assert!(!store.has(0));
611            }
612        });
613    }
614
615    #[test_traced]
616    fn test_get_nonexistent() {
617        // Initialize the deterministic context
618        let executor = deterministic::Runner::default();
619        executor.start(|context| async move {
620            // Initialize the store
621            let cfg = Config {
622                partition: "test-ordinal".into(),
623                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
624                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
625                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
626            };
627            let store =
628                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
629                    .await
630                    .expect("Failed to initialize store");
631
632            // Attempt to get an index that doesn't exist
633            let retrieved = store.get(999).await.expect("Failed to get data");
634            assert!(retrieved.is_none());
635
636            // Check has returns false
637            assert!(!store.has(999));
638        });
639    }
640
641    #[test_traced]
642    fn test_destroy() {
643        // Initialize the deterministic context
644        let executor = deterministic::Runner::default();
645        executor.start(|context| async move {
646            let cfg = Config {
647                partition: "test-ordinal".into(),
648                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
649                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
650                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
651            };
652
653            // Create store with data
654            {
655                let mut store =
656                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
657                        .await
658                        .expect("Failed to initialize store");
659
660                store = store
661                    .put(0, FixedBytes::new([0u8; 32]))
662                    .await
663                    .expect("Failed to put data");
664                store = store
665                    .put(1000, FixedBytes::new([100u8; 32]))
666                    .await
667                    .expect("Failed to put data");
668
669                // Destroy the store
670                store.destroy().await.expect("Failed to destroy store");
671            }
672
673            // Try to create a new store - it should be empty
674            {
675                let store =
676                    Ordinal::<_, FixedBytes<32>>::init(context.child("second"), cfg.clone(), None)
677                        .await
678                        .expect("Failed to initialize store");
679
680                // Should not find any data
681                assert!(store.get(0).await.unwrap().is_none());
682                assert!(store.get(1000).await.unwrap().is_none());
683                assert!(!store.has(0));
684                assert!(!store.has(1000));
685            }
686        });
687    }
688
689    #[test_traced]
690    fn test_partial_record_write() {
691        // Initialize the deterministic context
692        let executor = deterministic::Runner::default();
693        executor.start(|context| async move {
694            let cfg = Config {
695                partition: "test-ordinal".into(),
696                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
697                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
698                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
699            };
700
701            // Create store with data
702            {
703                let mut store =
704                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
705                        .await
706                        .expect("Failed to initialize store");
707
708                store = store
709                    .put(0, FixedBytes::new([42u8; 32]))
710                    .await
711                    .expect("Failed to put data");
712                store = store
713                    .put(1, FixedBytes::new([43u8; 32]))
714                    .await
715                    .expect("Failed to put data");
716                store.sync().await.expect("Failed to sync store");
717            }
718
719            // Corrupt by writing partial record (only value, no CRC)
720            {
721                let (blob, _) = context
722                    .open("test-ordinal", &0u64.to_be_bytes())
723                    .await
724                    .unwrap();
725                // Overwrite second record with partial data (32 bytes instead of 36)
726                blob.write_at(36, vec![0xFF; 32], WriteOptions::SYNC)
727                    .await
728                    .unwrap();
729            }
730
731            // Reopen without bits and verify uncheckpointed data is deleted.
732            {
733                let store =
734                    Ordinal::<_, FixedBytes<32>>::init(context.child("second"), cfg.clone(), None)
735                        .await
736                        .expect("Failed to initialize store");
737
738                assert!(!store.has(0));
739                assert!(!store.has(1));
740
741                // Store should still be functional
742                let store = store.put(1, FixedBytes::new([44u8; 32])).await.unwrap();
743                assert_eq!(
744                    store.get(1).await.unwrap().unwrap(),
745                    FixedBytes::new([44u8; 32])
746                );
747            }
748        });
749    }
750
751    #[test_traced]
752    fn test_corrupted_value() {
753        // Initialize the deterministic context
754        let executor = deterministic::Runner::default();
755        executor.start(|context| async move {
756            let cfg = Config {
757                partition: "test-ordinal".into(),
758                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
759                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
760                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
761            };
762
763            // Create store with data
764            {
765                let mut store =
766                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
767                        .await
768                        .expect("Failed to initialize store");
769
770                store = store
771                    .put(0, FixedBytes::new([42u8; 32]))
772                    .await
773                    .expect("Failed to put data");
774                store = store
775                    .put(1, FixedBytes::new([43u8; 32]))
776                    .await
777                    .expect("Failed to put data");
778                store.sync().await.expect("Failed to sync store");
779            }
780
781            // Corrupt the value portion of a record
782            {
783                let (blob, _) = context
784                    .open("test-ordinal", &0u64.to_be_bytes())
785                    .await
786                    .unwrap();
787                // Corrupt some bytes in the value of the first record
788                blob.write_at(10, hex!("0xFFFFFFFF").to_vec(), WriteOptions::SYNC)
789                    .await
790                    .unwrap();
791            }
792
793            // Reopen without bits and verify uncheckpointed data is deleted.
794            {
795                let store =
796                    Ordinal::<_, FixedBytes<32>>::init(context.child("second"), cfg.clone(), None)
797                        .await
798                        .expect("Failed to initialize store");
799
800                assert!(!store.has(0));
801                assert!(!store.has(1));
802            }
803        });
804    }
805
806    #[test_traced]
807    fn test_crc_corruptions() {
808        // Initialize the deterministic context
809        let executor = deterministic::Runner::default();
810        executor.start(|context| async move {
811            let cfg = Config {
812                partition: "test-ordinal".into(),
813                items_per_blob: NZU64!(10), // Small blob size for testing
814                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
815                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
816            };
817
818            // Create store with data across multiple blobs
819            {
820                let mut store =
821                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
822                        .await
823                        .expect("Failed to initialize store");
824
825                // Add values across 2 blobs
826                store = store.put(0, FixedBytes::new([0u8; 32])).await.unwrap();
827                store = store.put(5, FixedBytes::new([5u8; 32])).await.unwrap();
828                store = store.put(10, FixedBytes::new([10u8; 32])).await.unwrap();
829                store = store.put(15, FixedBytes::new([15u8; 32])).await.unwrap();
830                store.sync().await.expect("Failed to sync store");
831            }
832
833            // Corrupt CRCs in different blobs
834            {
835                // Corrupt CRC in first blob
836                let (blob, _) = context
837                    .open("test-ordinal", &0u64.to_be_bytes())
838                    .await
839                    .unwrap();
840                blob.write_at(32, vec![0xFF], WriteOptions::SYNC)
841                    .await
842                    .unwrap(); // Corrupt CRC of index 0
843
844                // Corrupt value in second blob (which will invalidate CRC)
845                let (blob, _) = context
846                    .open("test-ordinal", &1u64.to_be_bytes())
847                    .await
848                    .unwrap();
849                blob.write_at(5, vec![0xFF; 4], WriteOptions::SYNC)
850                    .await
851                    .unwrap(); // Corrupt value of index 10
852            }
853
854            // Reopen without bits and verify uncheckpointed data is deleted.
855            {
856                let store =
857                    Ordinal::<_, FixedBytes<32>>::init(context.child("second"), cfg.clone(), None)
858                        .await
859                        .expect("Failed to initialize store");
860
861                assert!(!store.has(0));
862                assert!(!store.has(5));
863                assert!(!store.has(10));
864                assert!(!store.has(15));
865            }
866        });
867    }
868
869    #[test_traced]
870    fn test_extra_bytes_in_blob() {
871        // Initialize the deterministic context
872        let executor = deterministic::Runner::default();
873        executor.start(|context| async move {
874            let cfg = Config {
875                partition: "test-ordinal".into(),
876                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
877                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
878                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
879            };
880
881            // Create store with data
882            {
883                let mut store =
884                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
885                        .await
886                        .expect("Failed to initialize store");
887
888                store = store
889                    .put(0, FixedBytes::new([42u8; 32]))
890                    .await
891                    .expect("Failed to put data");
892                store = store
893                    .put(1, FixedBytes::new([43u8; 32]))
894                    .await
895                    .expect("Failed to put data");
896                store.sync().await.expect("Failed to sync store");
897            }
898
899            // Add extra bytes at the end of blob
900            {
901                let (blob, size) = context
902                    .open("test-ordinal", &0u64.to_be_bytes())
903                    .await
904                    .unwrap();
905                // Add garbage data that forms a complete but invalid record
906                // This avoids partial record issues
907                let mut garbage = vec![0xFF; 32]; // Invalid value
908                let invalid_crc = 0xDEADBEEFu32;
909                garbage.extend_from_slice(&invalid_crc.to_be_bytes());
910                assert_eq!(garbage.len(), 36); // Full record size
911                blob.write_at(size, garbage, WriteOptions::SYNC)
912                    .await
913                    .unwrap();
914            }
915
916            // Reopen without bits and verify uncheckpointed data is deleted.
917            {
918                let store =
919                    Ordinal::<_, FixedBytes<32>>::init(context.child("second"), cfg.clone(), None)
920                        .await
921                        .expect("Failed to initialize store");
922
923                assert!(!store.has(0));
924                assert!(!store.has(1));
925
926                // Store should still be functional
927                let store = store.put(2, FixedBytes::new([44u8; 32])).await.unwrap();
928                assert_eq!(
929                    store.get(2).await.unwrap().unwrap(),
930                    FixedBytes::new([44u8; 32])
931                );
932            }
933        });
934    }
935
936    #[test_traced]
937    fn test_zero_filled_records() {
938        // Initialize the deterministic context
939        let executor = deterministic::Runner::default();
940        executor.start(|context| async move {
941            let cfg = Config {
942                partition: "test-ordinal".into(),
943                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
944                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
945                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
946            };
947
948            // Create blob with zero-filled space
949            {
950                let (blob, _) = context
951                    .open("test-ordinal", &0u64.to_be_bytes())
952                    .await
953                    .unwrap();
954
955                // Write zeros for several record positions
956                let zeros = vec![0u8; 36 * 5]; // 5 records worth of zeros
957                blob.write_at(0, zeros, WriteOptions::SYNC).await.unwrap();
958
959                // Write a valid record after the zeros
960                let mut valid_record = vec![44u8; 32];
961                let crc = Crc32::checksum(&valid_record);
962                valid_record.extend_from_slice(&crc.to_be_bytes());
963                blob.write_at(36 * 5, valid_record, WriteOptions::SYNC)
964                    .await
965                    .unwrap();
966            }
967
968            // Initialize with bits and verify it handles zero-filled records
969            {
970                let mut section = BitMap::zeroes(DEFAULT_ITEMS_PER_BLOB);
971                section.set(5, true);
972                let section = Some(section);
973                let mut bits = BTreeMap::new();
974                bits.insert(0, &section);
975                let store = Ordinal::<_, FixedBytes<32>>::init(
976                    context.child("storage"),
977                    cfg.clone(),
978                    Some(bits),
979                )
980                .await
981                .expect("Failed to initialize store");
982
983                // Zero-filled positions should not be considered valid
984                for i in 0..5 {
985                    assert!(!store.has(i));
986                }
987
988                // The valid record should be found
989                assert!(store.has(5));
990                assert_eq!(
991                    store.get(5).await.unwrap().unwrap(),
992                    FixedBytes::new([44u8; 32])
993                );
994            }
995        });
996    }
997
998    fn test_operations_and_restart(num_values: usize) -> String {
999        // Initialize the deterministic context
1000        let executor = deterministic::Runner::default();
1001        executor.start(|mut context| async move {
1002            let cfg = Config {
1003                partition: "test-ordinal".into(),
1004                items_per_blob: NZU64!(100), // Smaller blobs to test multiple blob handling
1005                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1006                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1007            };
1008
1009            // Initialize the store
1010            let mut store =
1011                Ordinal::<_, FixedBytes<128>>::init(context.child("first"), cfg.clone(), None)
1012                    .await
1013                    .expect("Failed to initialize store");
1014
1015            // Generate and insert random values at various indices
1016            let mut values = Vec::new();
1017            let mut rng_index = 0u64;
1018
1019            for _ in 0..num_values {
1020                // Generate a pseudo-random index (sparse to test gaps)
1021                let mut index_bytes = [0u8; 8];
1022                context.fill_bytes(&mut index_bytes);
1023                let index_offset = u64::from_be_bytes(index_bytes) % 1000;
1024                let index = rng_index + index_offset;
1025                rng_index = index + 1;
1026
1027                // Generate random value
1028                let mut value = [0u8; 128];
1029                context.fill_bytes(&mut value);
1030                let value = FixedBytes::<128>::new(value);
1031
1032                store = store
1033                    .put(index, value.clone())
1034                    .await
1035                    .expect("Failed to put data");
1036                values.push((index, value));
1037            }
1038
1039            // Sync data
1040            store = store.sync().await.expect("Failed to sync");
1041
1042            // Verify all values can be retrieved
1043            for (index, value) in &values {
1044                let retrieved = store
1045                    .get(*index)
1046                    .await
1047                    .expect("Failed to get data")
1048                    .expect("Data not found");
1049                assert_eq!(&retrieved, value);
1050            }
1051
1052            // Test next_gap on various indices
1053            for i in 0..10 {
1054                let _ = store.next_gap(i * 100);
1055            }
1056
1057            // Sync and drop the store
1058            store.sync().await.expect("Failed to sync store");
1059
1060            // Reopen the store
1061            let owned_bits = bits_for_indices(NZU64!(100), values.iter().map(|(index, _)| *index));
1062            let bits = owned_bits
1063                .iter()
1064                .map(|(section, bitmap)| (*section, bitmap))
1065                .collect();
1066            let mut store =
1067                Ordinal::<_, FixedBytes<128>>::init(context.child("second"), cfg, Some(bits))
1068                    .await
1069                    .expect("Failed to initialize store");
1070
1071            // Verify all values are still there after restart
1072            for (index, value) in &values {
1073                let retrieved = store
1074                    .get(*index)
1075                    .await
1076                    .expect("Failed to get data")
1077                    .expect("Data not found");
1078                assert_eq!(&retrieved, value);
1079            }
1080
1081            // Add more values after restart
1082            for _ in 0..10 {
1083                let mut index_bytes = [0u8; 8];
1084                context.fill_bytes(&mut index_bytes);
1085                let index = u64::from_be_bytes(index_bytes) % 10000;
1086
1087                let mut value = [0u8; 128];
1088                context.fill_bytes(&mut value);
1089                let value = FixedBytes::<128>::new(value);
1090
1091                store = store.put(index, value).await.expect("Failed to put data");
1092            }
1093
1094            // Final sync
1095            store.sync().await.expect("Failed to sync");
1096
1097            // Return the auditor state for comparison
1098            context.auditor().state()
1099        })
1100    }
1101
1102    #[test_group("slow")]
1103    #[test_traced]
1104    fn test_determinism() {
1105        let state1 = test_operations_and_restart(100);
1106        let state2 = test_operations_and_restart(100);
1107        assert_eq!(state1, state2);
1108    }
1109
1110    #[test_traced]
1111    fn test_prune_basic() {
1112        // Initialize the deterministic context
1113        let executor = deterministic::Runner::default();
1114        executor.start(|context| async move {
1115            let cfg = Config {
1116                partition: "test-ordinal".into(),
1117                items_per_blob: NZU64!(100), // Small blobs to test multiple blob handling
1118                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1119                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1120            };
1121
1122            let mut store =
1123                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
1124                    .await
1125                    .expect("Failed to initialize store");
1126
1127            // Insert data across multiple blobs
1128            let values = vec![
1129                (0u64, FixedBytes::new([0u8; 32])),     // Blob 0
1130                (50u64, FixedBytes::new([50u8; 32])),   // Blob 0
1131                (100u64, FixedBytes::new([100u8; 32])), // Blob 1
1132                (150u64, FixedBytes::new([150u8; 32])), // Blob 1
1133                (200u64, FixedBytes::new([200u8; 32])), // Blob 2
1134                (300u64, FixedBytes::new([44u8; 32])),  // Blob 3
1135            ];
1136
1137            for (index, value) in &values {
1138                store = store
1139                    .put(*index, value.clone())
1140                    .await
1141                    .expect("Failed to put data");
1142            }
1143            store = store.sync().await.unwrap();
1144
1145            // Verify all values exist
1146            for (index, value) in &values {
1147                assert_eq!(store.get(*index).await.unwrap().unwrap(), *value);
1148            }
1149
1150            // Prune up to index 150 (should remove blob 0 only)
1151            store = store.prune(150).await.unwrap();
1152            let buffer = context.encode();
1153            assert!(buffer.contains("pruned_total 1"));
1154
1155            // Verify pruned data is gone
1156            assert!(!store.has(0));
1157            assert!(!store.has(50));
1158            assert!(store.get(0).await.unwrap().is_none());
1159            assert!(store.get(50).await.unwrap().is_none());
1160
1161            // Verify remaining data is still there
1162            assert!(store.has(100));
1163            assert!(store.has(150));
1164            assert!(store.has(200));
1165            assert!(store.has(300));
1166            assert_eq!(store.get(100).await.unwrap().unwrap(), values[2].1);
1167            assert_eq!(store.get(150).await.unwrap().unwrap(), values[3].1);
1168            assert_eq!(store.get(200).await.unwrap().unwrap(), values[4].1);
1169            assert_eq!(store.get(300).await.unwrap().unwrap(), values[5].1);
1170
1171            // Prune more aggressively - up to index 250 (should remove blob 1)
1172            store = store.prune(250).await.unwrap();
1173            let buffer = context.encode();
1174            assert!(buffer.contains("pruned_total 2"));
1175
1176            // Verify more data is pruned
1177            assert!(!store.has(100));
1178            assert!(!store.has(150));
1179            assert!(store.get(100).await.unwrap().is_none());
1180            assert!(store.get(150).await.unwrap().is_none());
1181
1182            // Verify remaining data
1183            assert!(store.has(200));
1184            assert!(store.has(300));
1185            assert_eq!(store.get(200).await.unwrap().unwrap(), values[4].1);
1186            assert_eq!(store.get(300).await.unwrap().unwrap(), values[5].1);
1187        });
1188    }
1189
1190    #[test_traced]
1191    fn test_prune_with_gaps() {
1192        // Initialize the deterministic context
1193        let executor = deterministic::Runner::default();
1194        executor.start(|context| async move {
1195            let cfg = Config {
1196                partition: "test-ordinal".into(),
1197                items_per_blob: NZU64!(100),
1198                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1199                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1200            };
1201
1202            let mut store =
1203                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
1204                    .await
1205                    .expect("Failed to initialize store");
1206
1207            // Insert sparse data with gaps
1208            store = store.put(5, FixedBytes::new([5u8; 32])).await.unwrap();
1209            store = store.put(105, FixedBytes::new([105u8; 32])).await.unwrap();
1210            store = store.put(305, FixedBytes::new([49u8; 32])).await.unwrap();
1211            store = store.sync().await.unwrap();
1212
1213            // Check gaps before pruning
1214            let (current_end, next_start) = store.next_gap(0);
1215            assert!(current_end.is_none());
1216            assert_eq!(next_start, Some(5));
1217
1218            let (current_end, next_start) = store.next_gap(5);
1219            assert_eq!(current_end, Some(5));
1220            assert_eq!(next_start, Some(105));
1221
1222            // Prune up to index 150 (should remove blob 0)
1223            store = store.prune(150).await.unwrap();
1224
1225            // Verify pruned data is gone
1226            assert!(!store.has(5));
1227            assert!(store.get(5).await.unwrap().is_none());
1228
1229            // Verify remaining data and gaps
1230            assert!(store.has(105));
1231            assert!(store.has(305));
1232
1233            let (current_end, next_start) = store.next_gap(0);
1234            assert!(current_end.is_none());
1235            assert_eq!(next_start, Some(105));
1236
1237            let (current_end, next_start) = store.next_gap(105);
1238            assert_eq!(current_end, Some(105));
1239            assert_eq!(next_start, Some(305));
1240        });
1241    }
1242
1243    #[test_traced]
1244    fn test_prune_no_op() {
1245        // Initialize the deterministic context
1246        let executor = deterministic::Runner::default();
1247        executor.start(|context| async move {
1248            let cfg = Config {
1249                partition: "test-ordinal".into(),
1250                items_per_blob: NZU64!(100),
1251                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1252                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1253            };
1254
1255            let mut store =
1256                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
1257                    .await
1258                    .expect("Failed to initialize store");
1259
1260            // Insert data
1261            store = store.put(100, FixedBytes::new([100u8; 32])).await.unwrap();
1262            store = store.put(200, FixedBytes::new([200u8; 32])).await.unwrap();
1263            store = store.sync().await.unwrap();
1264
1265            // Try to prune before any data - should be no-op
1266            store = store.prune(50).await.unwrap();
1267
1268            // Verify no data was actually pruned
1269            assert!(store.has(100));
1270            assert!(store.has(200));
1271            let buffer = context.encode();
1272            assert!(buffer.contains("pruned_total 0"));
1273
1274            // Try to prune exactly at blob boundary - should be no-op
1275            store = store.prune(100).await.unwrap();
1276
1277            // Verify still no data pruned
1278            assert!(store.has(100));
1279            assert!(store.has(200));
1280            let buffer = context.encode();
1281            assert!(buffer.contains("pruned_total 0"));
1282        });
1283    }
1284
1285    #[test_traced]
1286    fn test_prune_empty_store() {
1287        // Initialize the deterministic context
1288        let executor = deterministic::Runner::default();
1289        executor.start(|context| async move {
1290            let cfg = Config {
1291                partition: "test-ordinal".into(),
1292                items_per_blob: NZU64!(100),
1293                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1294                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1295            };
1296
1297            let mut store =
1298                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
1299                    .await
1300                    .expect("Failed to initialize store");
1301
1302            // Try to prune empty store
1303            store = store.prune(1000).await.unwrap();
1304
1305            // Store should still be functional
1306            store = store.put(0, FixedBytes::new([0u8; 32])).await.unwrap();
1307            assert!(store.has(0));
1308        });
1309    }
1310
1311    #[test_traced]
1312    fn test_prune_after_restart() {
1313        // Initialize the deterministic context
1314        let executor = deterministic::Runner::default();
1315        executor.start(|context| async move {
1316            let cfg = Config {
1317                partition: "test-ordinal".into(),
1318                items_per_blob: NZU64!(100),
1319                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1320                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1321            };
1322
1323            // Create store and add data
1324            {
1325                let mut store =
1326                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
1327                        .await
1328                        .expect("Failed to initialize store");
1329
1330                store = store.put(0, FixedBytes::new([0u8; 32])).await.unwrap();
1331                store = store.put(100, FixedBytes::new([100u8; 32])).await.unwrap();
1332                store = store.put(200, FixedBytes::new([200u8; 32])).await.unwrap();
1333                store.sync().await.unwrap();
1334            }
1335
1336            // Reopen with bits and prune
1337            {
1338                let mut bits0 = BitMap::zeroes(100);
1339                bits0.set(0, true);
1340                let mut bits1 = BitMap::zeroes(100);
1341                bits1.set(0, true);
1342                let mut bits2 = BitMap::zeroes(100);
1343                bits2.set(0, true);
1344                let bits0 = Some(bits0);
1345                let bits1 = Some(bits1);
1346                let bits2 = Some(bits2);
1347                let mut bits = BTreeMap::new();
1348                bits.insert(0, &bits0);
1349                bits.insert(1, &bits1);
1350                bits.insert(2, &bits2);
1351                let mut store = Ordinal::<_, FixedBytes<32>>::init(
1352                    context.child("second"),
1353                    cfg.clone(),
1354                    Some(bits),
1355                )
1356                .await
1357                .expect("Failed to initialize store");
1358
1359                // Verify data is there
1360                assert!(store.has(0));
1361                assert!(store.has(100));
1362                assert!(store.has(200));
1363
1364                // Prune up to index 150
1365                store = store.prune(150).await.unwrap();
1366
1367                // Verify pruning worked
1368                assert!(!store.has(0));
1369                assert!(store.has(100));
1370                assert!(store.has(200));
1371
1372                store.sync().await.unwrap();
1373            }
1374
1375            // Reopen again and verify pruning persisted
1376            {
1377                let mut bits1 = BitMap::zeroes(100);
1378                bits1.set(0, true);
1379                let mut bits2 = BitMap::zeroes(100);
1380                bits2.set(0, true);
1381                let bits1 = Some(bits1);
1382                let bits2 = Some(bits2);
1383                let mut bits = BTreeMap::new();
1384                bits.insert(1, &bits1);
1385                bits.insert(2, &bits2);
1386                let store = Ordinal::<_, FixedBytes<32>>::init(
1387                    context.child("third"),
1388                    cfg.clone(),
1389                    Some(bits),
1390                )
1391                .await
1392                .expect("Failed to initialize store");
1393
1394                assert!(!store.has(0));
1395                assert!(store.has(100));
1396                assert!(store.has(200));
1397
1398                // Check gaps
1399                let (current_end, next_start) = store.next_gap(0);
1400                assert!(current_end.is_none());
1401                assert_eq!(next_start, Some(100));
1402            }
1403        });
1404    }
1405
1406    #[test_traced]
1407    fn test_prune_multiple_operations() {
1408        // Initialize the deterministic context
1409        let executor = deterministic::Runner::default();
1410        executor.start(|context| async move {
1411            let cfg = Config {
1412                partition: "test-ordinal".into(),
1413                items_per_blob: NZU64!(50), // Smaller blobs for more granular testing
1414                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1415                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1416            };
1417
1418            let mut store =
1419                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
1420                    .await
1421                    .expect("Failed to initialize store");
1422
1423            // Insert data across many blobs
1424            let mut values = Vec::new();
1425            for i in 0..10 {
1426                let index = i * 50 + 25; // Middle of each blob
1427                let value = FixedBytes::new([i as u8; 32]);
1428                store = store.put(index, value.clone()).await.unwrap();
1429                values.push((index, value));
1430            }
1431            store = store.sync().await.unwrap();
1432
1433            // Prune incrementally
1434            for i in 1..5 {
1435                let prune_index = i * 50 + 10;
1436                store = store.prune(prune_index).await.unwrap();
1437
1438                // Verify appropriate data is pruned
1439                for (index, _) in &values {
1440                    if *index < prune_index {
1441                        assert!(!store.has(*index), "Index {index} should be pruned");
1442                    } else {
1443                        assert!(store.has(*index), "Index {index} should not be pruned");
1444                    }
1445                }
1446            }
1447
1448            // Check final state
1449            let buffer = context.encode();
1450            assert!(buffer.contains("pruned_total 4"));
1451
1452            // Verify remaining data
1453            for i in 4..10 {
1454                let index = i * 50 + 25;
1455                assert!(store.has(index));
1456                assert_eq!(
1457                    store.get(index).await.unwrap().unwrap(),
1458                    values[i as usize].1
1459                );
1460            }
1461        });
1462    }
1463
1464    #[test_traced]
1465    fn test_prune_blob_boundaries() {
1466        // Initialize the deterministic context
1467        let executor = deterministic::Runner::default();
1468        executor.start(|context| async move {
1469            let cfg = Config {
1470                partition: "test-ordinal".into(),
1471                items_per_blob: NZU64!(100),
1472                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1473                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1474            };
1475
1476            let mut store =
1477                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
1478                    .await
1479                    .expect("Failed to initialize store");
1480
1481            // Insert data at blob boundaries
1482            store = store.put(0, FixedBytes::new([0u8; 32])).await.unwrap(); // Start of blob 0
1483            store = store.put(99, FixedBytes::new([99u8; 32])).await.unwrap(); // End of blob 0
1484            store = store.put(100, FixedBytes::new([100u8; 32])).await.unwrap(); // Start of blob 1
1485            store = store.put(199, FixedBytes::new([199u8; 32])).await.unwrap(); // End of blob 1
1486            store = store.put(200, FixedBytes::new([200u8; 32])).await.unwrap(); // Start of blob 2
1487            store = store.sync().await.unwrap();
1488
1489            // Test various pruning points around boundaries
1490
1491            // Prune exactly at blob boundary (100) - should prune blob 0
1492            store = store.prune(100).await.unwrap();
1493            assert!(!store.has(0));
1494            assert!(!store.has(99));
1495            assert!(store.has(100));
1496            assert!(store.has(199));
1497            assert!(store.has(200));
1498
1499            // Prune just before next boundary (199) - should not prune blob 1
1500            store = store.prune(199).await.unwrap();
1501            assert!(store.has(100));
1502            assert!(store.has(199));
1503            assert!(store.has(200));
1504
1505            // Prune exactly at next boundary (200) - should prune blob 1
1506            store = store.prune(200).await.unwrap();
1507            assert!(!store.has(100));
1508            assert!(!store.has(199));
1509            assert!(store.has(200));
1510
1511            let buffer = context.encode();
1512            assert!(buffer.contains("pruned_total 2"));
1513        });
1514    }
1515
1516    #[test_traced]
1517    fn test_prune_non_contiguous_sections() {
1518        // Initialize the deterministic context
1519        let executor = deterministic::Runner::default();
1520        executor.start(|context| async move {
1521            let cfg = Config {
1522                partition: "test-ordinal".into(),
1523                items_per_blob: NZU64!(100),
1524                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1525                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1526            };
1527
1528            let mut store =
1529                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
1530                    .await
1531                    .expect("Failed to initialize store");
1532
1533            // Insert data in non-contiguous sections (0, 2, 5, 7)
1534            store = store.put(0, FixedBytes::new([0u8; 32])).await.unwrap(); // Section 0
1535            store = store.put(250, FixedBytes::new([50u8; 32])).await.unwrap(); // Section 2 (250/100 = 2)
1536            store = store.put(500, FixedBytes::new([44u8; 32])).await.unwrap(); // Section 5 (500/100 = 5)
1537            store = store.put(750, FixedBytes::new([45u8; 32])).await.unwrap(); // Section 7 (750/100 = 7)
1538            store = store.sync().await.unwrap();
1539
1540            // Verify all data exists initially
1541            assert!(store.has(0));
1542            assert!(store.has(250));
1543            assert!(store.has(500));
1544            assert!(store.has(750));
1545
1546            // Prune up to section 3 (index 300) - should remove sections 0 and 2
1547            store = store.prune(300).await.unwrap();
1548
1549            // Verify correct data was pruned
1550            assert!(!store.has(0)); // Section 0 pruned
1551            assert!(!store.has(250)); // Section 2 pruned
1552            assert!(store.has(500)); // Section 5 remains
1553            assert!(store.has(750)); // Section 7 remains
1554
1555            let buffer = context.encode();
1556            assert!(buffer.contains("pruned_total 2"));
1557
1558            // Prune up to section 6 (index 600) - should remove section 5
1559            store = store.prune(600).await.unwrap();
1560
1561            // Verify section 5 was pruned
1562            assert!(!store.has(500)); // Section 5 pruned
1563            assert!(store.has(750)); // Section 7 remains
1564
1565            let buffer = context.encode();
1566            assert!(buffer.contains("pruned_total 3"));
1567
1568            // Prune everything - should remove section 7
1569            store = store.prune(1000).await.unwrap();
1570
1571            // Verify all data is gone
1572            assert!(!store.has(750)); // Section 7 pruned
1573
1574            let buffer = context.encode();
1575            assert!(buffer.contains("pruned_total 4"));
1576        });
1577    }
1578
1579    #[test_traced]
1580    fn test_prune_removes_correct_pending() {
1581        // Initialize the deterministic context
1582        let executor = deterministic::Runner::default();
1583        executor.start(|context| async move {
1584            let cfg = Config {
1585                partition: "test-ordinal".into(),
1586                items_per_blob: NZU64!(100),
1587                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1588                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1589            };
1590            let mut store =
1591                Ordinal::<_, FixedBytes<32>>::init(context.child("storage"), cfg.clone(), None)
1592                    .await
1593                    .expect("Failed to initialize store");
1594
1595            // Insert and sync some data in blob 0
1596            store = store.put(5, FixedBytes::new([5u8; 32])).await.unwrap();
1597            store = store.sync().await.unwrap();
1598
1599            // Add pending entries to blob 0 and blob 1
1600            store = store.put(10, FixedBytes::new([10u8; 32])).await.unwrap(); // blob 0
1601            store = store.put(110, FixedBytes::new([110u8; 32])).await.unwrap(); // blob 1
1602
1603            // Verify all data is visible before pruning
1604            assert!(store.has(5));
1605            assert!(store.has(10));
1606            assert!(store.has(110));
1607
1608            // Prune up to index 100, which should remove blob 0 (indices 0-99).
1609            store = store.prune(150).await.unwrap();
1610
1611            // Verify that synced and pending entries in blob 0 are removed.
1612            assert!(!store.has(5));
1613            assert!(!store.has(10));
1614
1615            // Verify that the pending entry in blob 1 remains.
1616            assert!(store.has(110));
1617            assert_eq!(
1618                store.get(110).await.unwrap().unwrap(),
1619                FixedBytes::new([110u8; 32])
1620            );
1621
1622            // Sync the remaining pending entry and verify it's still there.
1623            store = store.sync().await.unwrap();
1624            assert!(store.has(110));
1625            assert_eq!(
1626                store.get(110).await.unwrap().unwrap(),
1627                FixedBytes::new([110u8; 32])
1628            );
1629        });
1630    }
1631
1632    #[test_traced]
1633    fn test_init_without_bits_deletes_existing_data() {
1634        // Initialize the deterministic context
1635        let executor = deterministic::Runner::default();
1636        executor.start(|context| async move {
1637            let cfg = Config {
1638                partition: "test-ordinal".into(),
1639                items_per_blob: NZU64!(10), // Small blob size for testing
1640                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1641                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1642            };
1643
1644            // Create store with data across multiple sections
1645            {
1646                let mut store =
1647                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
1648                        .await
1649                        .expect("Failed to initialize store");
1650
1651                // Section 0 (indices 0-9)
1652                store = store.put(0, FixedBytes::new([0u8; 32])).await.unwrap();
1653                store = store.put(5, FixedBytes::new([5u8; 32])).await.unwrap();
1654                store = store.put(9, FixedBytes::new([9u8; 32])).await.unwrap();
1655
1656                // Section 1 (indices 10-19)
1657                store = store.put(10, FixedBytes::new([10u8; 32])).await.unwrap();
1658                store = store.put(15, FixedBytes::new([15u8; 32])).await.unwrap();
1659
1660                // Section 2 (indices 20-29)
1661                store = store.put(25, FixedBytes::new([25u8; 32])).await.unwrap();
1662
1663                store.sync().await.unwrap();
1664            }
1665
1666            // Reinitialize with bits = None, deleting uncheckpointed data.
1667            {
1668                let store =
1669                    Ordinal::<_, FixedBytes<32>>::init(context.child("second"), cfg.clone(), None)
1670                        .await
1671                        .expect("Failed to initialize store");
1672
1673                assert!(!store.has(0));
1674                assert!(!store.has(5));
1675                assert!(!store.has(9));
1676                assert!(!store.has(10));
1677                assert!(!store.has(15));
1678                assert!(!store.has(25));
1679                assert!(!store.has(1));
1680                assert!(!store.has(11));
1681                assert!(!store.has(20));
1682            }
1683        });
1684    }
1685
1686    #[test_traced]
1687    fn test_init_empty_hashmap() {
1688        // Initialize the deterministic context
1689        let executor = deterministic::Runner::default();
1690        executor.start(|context| async move {
1691            let cfg = Config {
1692                partition: "test-ordinal".into(),
1693                items_per_blob: NZU64!(10),
1694                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1695                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1696            };
1697
1698            // Create store with data
1699            {
1700                let mut store =
1701                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
1702                        .await
1703                        .expect("Failed to initialize store");
1704
1705                store = store.put(0, FixedBytes::new([0u8; 32])).await.unwrap();
1706                store = store.put(10, FixedBytes::new([10u8; 32])).await.unwrap();
1707                store = store.put(20, FixedBytes::new([20u8; 32])).await.unwrap();
1708
1709                store.sync().await.unwrap();
1710            }
1711
1712            // Reinitialize with an empty map, deleting every stored section.
1713            {
1714                let bits: BTreeMap<u64, &Option<BitMap>> = BTreeMap::new();
1715                let store = Ordinal::<_, FixedBytes<32>>::init(
1716                    context.child("second"),
1717                    cfg.clone(),
1718                    Some(bits),
1719                )
1720                .await
1721                .expect("Failed to initialize store with bits");
1722
1723                // No records should be available since no sections were in the bits map
1724                assert!(!store.has(0));
1725                assert!(!store.has(10));
1726                assert!(!store.has(20));
1727            }
1728
1729            // The explicit empty map deletes the uncheckpointed blobs.
1730            {
1731                let mut section = BitMap::zeroes(10);
1732                section.set(0, true);
1733                let section = Some(section);
1734                let mut bits = BTreeMap::new();
1735                bits.insert(0, &section);
1736                let result = Ordinal::<_, FixedBytes<32>>::init(
1737                    context.child("third"),
1738                    cfg.clone(),
1739                    Some(bits),
1740                )
1741                .await;
1742                assert!(matches!(result, Err(Error::MissingRecord(0))));
1743            }
1744        });
1745    }
1746
1747    #[test_traced]
1748    fn test_init_selective_sections() {
1749        // Initialize the deterministic context
1750        let executor = deterministic::Runner::default();
1751        executor.start(|context| async move {
1752            let cfg = Config {
1753                partition: "test-ordinal".into(),
1754                items_per_blob: NZU64!(10),
1755                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1756                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1757            };
1758
1759            // Create store with data in multiple sections
1760            {
1761                let mut store =
1762                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
1763                        .await
1764                        .expect("Failed to initialize store");
1765
1766                // Section 0 (indices 0-9)
1767                for i in 0..10 {
1768                    store = store.put(i, FixedBytes::new([i as u8; 32])).await.unwrap();
1769                }
1770
1771                // Section 1 (indices 10-19)
1772                for i in 10..20 {
1773                    store = store.put(i, FixedBytes::new([i as u8; 32])).await.unwrap();
1774                }
1775
1776                // Section 2 (indices 20-29)
1777                for i in 20..30 {
1778                    store = store.put(i, FixedBytes::new([i as u8; 32])).await.unwrap();
1779                }
1780
1781                store.sync().await.unwrap();
1782            }
1783
1784            // Reinitialize with bits for only section 1
1785            {
1786                let mut bits_map: BTreeMap<u64, &Option<BitMap>> = BTreeMap::new();
1787
1788                // Create a BitMap that marks indices 12, 15, and 18 as present
1789                let mut bitmap = BitMap::zeroes(10);
1790                bitmap.set(2, true); // Index 12 (offset 2 in section 1)
1791                bitmap.set(5, true); // Index 15 (offset 5 in section 1)
1792                bitmap.set(8, true); // Index 18 (offset 8 in section 1)
1793                let bitmap_option = Some(bitmap);
1794
1795                bits_map.insert(1, &bitmap_option);
1796
1797                let store = Ordinal::<_, FixedBytes<32>>::init(
1798                    context.child("second"),
1799                    cfg.clone(),
1800                    Some(bits_map),
1801                )
1802                .await
1803                .expect("Failed to initialize store with bits");
1804
1805                // Only specified indices from section 1 should be available
1806                assert!(store.has(12));
1807                assert!(store.has(15));
1808                assert!(store.has(18));
1809
1810                // Other indices from section 1 should not be available
1811                assert!(!store.has(10));
1812                assert!(!store.has(11));
1813                assert!(!store.has(13));
1814                assert!(!store.has(14));
1815                assert!(!store.has(16));
1816                assert!(!store.has(17));
1817                assert!(!store.has(19));
1818
1819                // All indices from sections 0 and 2 should not be available
1820                for i in 0..10 {
1821                    assert!(!store.has(i));
1822                }
1823                for i in 20..30 {
1824                    assert!(!store.has(i));
1825                }
1826
1827                // Verify the available values
1828                assert_eq!(
1829                    store.get(12).await.unwrap().unwrap(),
1830                    FixedBytes::new([12u8; 32])
1831                );
1832                assert_eq!(
1833                    store.get(15).await.unwrap().unwrap(),
1834                    FixedBytes::new([15u8; 32])
1835                );
1836                assert_eq!(
1837                    store.get(18).await.unwrap().unwrap(),
1838                    FixedBytes::new([18u8; 32])
1839                );
1840            }
1841
1842            // Unselected records in a retained section are physically cleared. A later
1843            // bitmap claiming a cleared record is trusted at init (the bitmap proves
1844            // membership), so the damage surfaces at get.
1845            {
1846                let mut bitmap = BitMap::zeroes(10);
1847                bitmap.set(0, true); // Index 10
1848                let bitmap_option = Some(bitmap);
1849                let mut bits_map: BTreeMap<u64, &Option<BitMap>> = BTreeMap::new();
1850                bits_map.insert(1, &bitmap_option);
1851                let store = Ordinal::<_, FixedBytes<32>>::init(
1852                    context.child("third"),
1853                    cfg.clone(),
1854                    Some(bits_map),
1855                )
1856                .await
1857                .expect("Failed to initialize store with bits");
1858                assert!(store.has(10));
1859                assert!(matches!(store.get(10).await, Err(Error::InvalidRecord(10))));
1860            }
1861        });
1862    }
1863
1864    #[test_traced]
1865    fn test_init_none_option_all_records_exist() {
1866        // Initialize the deterministic context
1867        let executor = deterministic::Runner::default();
1868        executor.start(|context| async move {
1869            let cfg = Config {
1870                partition: "test-ordinal".into(),
1871                items_per_blob: NZU64!(5),
1872                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1873                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1874            };
1875
1876            // Create store with all records in a section
1877            {
1878                let mut store =
1879                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
1880                        .await
1881                        .expect("Failed to initialize store");
1882
1883                // Fill section 1 completely (indices 5-9)
1884                for i in 5..10 {
1885                    store = store.put(i, FixedBytes::new([i as u8; 32])).await.unwrap();
1886                }
1887
1888                store.sync().await.unwrap();
1889            }
1890
1891            // Reinitialize with None option for section 1 (expects all records)
1892            {
1893                let mut bits_map: BTreeMap<u64, &Option<BitMap>> = BTreeMap::new();
1894                let none_option: Option<BitMap> = None;
1895                bits_map.insert(1, &none_option);
1896
1897                let store = Ordinal::<_, FixedBytes<32>>::init(
1898                    context.child("second"),
1899                    cfg.clone(),
1900                    Some(bits_map),
1901                )
1902                .await
1903                .expect("Failed to initialize store with bits");
1904
1905                // All records in section 1 should be available
1906                for i in 5..10 {
1907                    assert!(store.has(i));
1908                    assert_eq!(
1909                        store.get(i).await.unwrap().unwrap(),
1910                        FixedBytes::new([i as u8; 32])
1911                    );
1912                }
1913            }
1914        });
1915    }
1916
1917    #[test_traced]
1918    #[should_panic(expected = "Failed to initialize store with bits: MissingRecord(6)")]
1919    fn test_init_none_option_missing_record_panics() {
1920        // Initialize the deterministic context
1921        let executor = deterministic::Runner::default();
1922        executor.start(|context| async move {
1923            let cfg = Config {
1924                partition: "test-ordinal".into(),
1925                items_per_blob: NZU64!(5),
1926                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1927                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1928            };
1929
1930            // Create store with missing record in a section
1931            {
1932                let mut store =
1933                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
1934                        .await
1935                        .expect("Failed to initialize store");
1936
1937                // Fill section 1 partially (skip index 6)
1938                store = store.put(5, FixedBytes::new([5u8; 32])).await.unwrap();
1939                // Skip index 6
1940                store = store.put(7, FixedBytes::new([7u8; 32])).await.unwrap();
1941                store = store.put(8, FixedBytes::new([8u8; 32])).await.unwrap();
1942                store = store.put(9, FixedBytes::new([9u8; 32])).await.unwrap();
1943
1944                store.sync().await.unwrap();
1945            }
1946
1947            // Reinitialize with None option for section 1 (expects all records)
1948            // This should panic because index 6 is missing
1949            {
1950                let mut bits_map: BTreeMap<u64, &Option<BitMap>> = BTreeMap::new();
1951                let none_option: Option<BitMap> = None;
1952                bits_map.insert(1, &none_option);
1953
1954                let _store = Ordinal::<_, FixedBytes<32>>::init(
1955                    context.child("second"),
1956                    cfg.clone(),
1957                    Some(bits_map),
1958                )
1959                .await
1960                .expect("Failed to initialize store with bits");
1961            }
1962        });
1963    }
1964
1965    #[test_traced]
1966    fn test_init_mixed_sections() {
1967        // Initialize the deterministic context
1968        let executor = deterministic::Runner::default();
1969        executor.start(|context| async move {
1970            let cfg = Config {
1971                partition: "test-ordinal".into(),
1972                items_per_blob: NZU64!(5),
1973                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
1974                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
1975            };
1976
1977            // Create store with data in multiple sections
1978            {
1979                let mut store =
1980                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
1981                        .await
1982                        .expect("Failed to initialize store");
1983
1984                // Section 0: indices 0-4 (fill completely)
1985                for i in 0..5 {
1986                    store = store.put(i, FixedBytes::new([i as u8; 32])).await.unwrap();
1987                }
1988
1989                // Section 1: indices 5-9 (fill partially)
1990                store = store.put(5, FixedBytes::new([5u8; 32])).await.unwrap();
1991                store = store.put(7, FixedBytes::new([7u8; 32])).await.unwrap();
1992                store = store.put(9, FixedBytes::new([9u8; 32])).await.unwrap();
1993
1994                // Section 2: indices 10-14 (fill completely)
1995                for i in 10..15 {
1996                    store = store.put(i, FixedBytes::new([i as u8; 32])).await.unwrap();
1997                }
1998
1999                store.sync().await.unwrap();
2000            }
2001
2002            // Reinitialize with mixed bits configuration
2003            {
2004                let mut bits_map: BTreeMap<u64, &Option<BitMap>> = BTreeMap::new();
2005
2006                // Section 0: None option (expects all records)
2007                let none_option: Option<BitMap> = None;
2008                bits_map.insert(0, &none_option);
2009
2010                // Section 1: BitMap with specific indices
2011                let mut bitmap1 = BitMap::zeroes(5);
2012                bitmap1.set(0, true); // Index 5
2013                bitmap1.set(2, true); // Index 7
2014                // Note: not setting bit for index 9, so it should be ignored
2015                let bitmap1_option = Some(bitmap1);
2016                bits_map.insert(1, &bitmap1_option);
2017
2018                // Section 2: Not in map, so it should be removed entirely.
2019
2020                let store = Ordinal::<_, FixedBytes<32>>::init(
2021                    context.child("second"),
2022                    cfg.clone(),
2023                    Some(bits_map),
2024                )
2025                .await
2026                .expect("Failed to initialize store with bits");
2027
2028                // All records from section 0 should be available
2029                for i in 0..5 {
2030                    assert!(store.has(i));
2031                    assert_eq!(
2032                        store.get(i).await.unwrap().unwrap(),
2033                        FixedBytes::new([i as u8; 32])
2034                    );
2035                }
2036
2037                // Only specified records from section 1 should be available
2038                assert!(store.has(5));
2039                assert!(store.has(7));
2040                assert!(!store.has(6));
2041                assert!(!store.has(8));
2042                assert!(!store.has(9)); // Not set in bitmap
2043
2044                // No records from section 2 should be available
2045                for i in 10..15 {
2046                    assert!(!store.has(i));
2047                }
2048            }
2049        });
2050    }
2051
2052    #[test_traced]
2053    fn test_marked_record_damage_surfaces_at_get() {
2054        // Initialize the deterministic context
2055        let executor = deterministic::Runner::default();
2056        executor.start(|context| async move {
2057            let cfg = Config {
2058                partition: "test-ordinal".into(),
2059                items_per_blob: NZU64!(5),
2060                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2061                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2062            };
2063
2064            // Create store with data and corrupt one record
2065            {
2066                let mut store =
2067                    Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
2068                        .await
2069                        .expect("Failed to initialize store");
2070
2071                // Section 0: indices 0-4
2072                for i in 0..5 {
2073                    store = store.put(i, FixedBytes::new([i as u8; 32])).await.unwrap();
2074                }
2075
2076                store.sync().await.unwrap();
2077            }
2078
2079            // Corrupt record at index 2
2080            {
2081                let (blob, _) = context
2082                    .open("test-ordinal", &0u64.to_be_bytes())
2083                    .await
2084                    .unwrap();
2085                // Corrupt the CRC of record at index 2
2086                let offset = 2 * 36 + 32; // 2 * record_size + value_size
2087                blob.write_at(offset, vec![0xFF], WriteOptions::SYNC)
2088                    .await
2089                    .unwrap();
2090            }
2091
2092            // Reinitialize with bits that include the corrupted record
2093            {
2094                let mut bits_map: BTreeMap<u64, &Option<BitMap>> = BTreeMap::new();
2095
2096                // Create a BitMap that includes the corrupted record. The bitmap proves
2097                // membership, so the record is not re-read at startup and the damage
2098                // surfaces at get.
2099                let mut bitmap = BitMap::zeroes(5);
2100                bitmap.set(0, true); // Index 0
2101                bitmap.set(2, true); // Index 2 (corrupted)
2102                bitmap.set(4, true); // Index 4
2103                let bitmap_option = Some(bitmap);
2104                bits_map.insert(0, &bitmap_option);
2105
2106                let store = Ordinal::<_, FixedBytes<32>>::init(
2107                    context.child("second"),
2108                    cfg.clone(),
2109                    Some(bits_map),
2110                )
2111                .await
2112                .expect("Failed to initialize store with bits");
2113                assert_eq!(
2114                    store.get(0).await.unwrap(),
2115                    Some(FixedBytes::new([0u8; 32]))
2116                );
2117                assert!(store.get(2).await.is_err());
2118                assert_eq!(
2119                    store.get(4).await.unwrap(),
2120                    Some(FixedBytes::new([4u8; 32]))
2121                );
2122            }
2123        });
2124    }
2125
2126    /// A dummy value that will fail parsing if the value is 0.
2127    #[derive(Debug, PartialEq, Eq)]
2128    pub struct DummyValue {
2129        pub value: u64,
2130    }
2131
2132    impl Write for DummyValue {
2133        fn write(&self, buf: &mut impl BufMut) {
2134            self.value.write(buf);
2135        }
2136    }
2137
2138    impl Read for DummyValue {
2139        type Cfg = ();
2140
2141        fn read_cfg(buf: &mut impl Buf, _: &Self::Cfg) -> Result<Self, commonware_codec::Error> {
2142            let value = u64::read(buf)?;
2143            if value == 0 {
2144                return Err(commonware_codec::Error::Invalid(
2145                    "DummyValue",
2146                    "value must be non-zero",
2147                ));
2148            }
2149            Ok(Self { value })
2150        }
2151    }
2152
2153    impl FixedSize for DummyValue {
2154        const SIZE: usize = u64::SIZE;
2155    }
2156
2157    #[test_traced]
2158    fn test_init_skip_unparseable_record() {
2159        // Initialize the deterministic context
2160        let executor = deterministic::Runner::default();
2161        executor.start(|context| async move {
2162            let cfg = Config {
2163                partition: "test-ordinal".into(),
2164                items_per_blob: NZU64!(1),
2165                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2166                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2167            };
2168
2169            // Create store with records, including one that will fail to parse if recovered.
2170            {
2171                let mut store =
2172                    Ordinal::<_, DummyValue>::init(context.child("first"), cfg.clone(), None)
2173                        .await
2174                        .expect("Failed to initialize store");
2175
2176                // Add records at indices 1, 2, 4
2177                store = store.put(1, DummyValue { value: 1 }).await.unwrap();
2178                store = store.put(2, DummyValue { value: 0 }).await.unwrap(); // will fail parsing
2179                store = store.put(4, DummyValue { value: 4 }).await.unwrap();
2180
2181                store = store.sync().await.unwrap();
2182
2183                // A record whose CRC matches but whose value fails to parse is invalid
2184                assert!(matches!(store.get(2).await, Err(Error::InvalidRecord(2))));
2185            }
2186
2187            // Reinitialize without bits and verify uncheckpointed data is deleted.
2188            {
2189                let store =
2190                    Ordinal::<_, DummyValue>::init(context.child("second"), cfg.clone(), None)
2191                        .await
2192                        .expect("Failed to initialize store");
2193
2194                assert!(!store.has(1));
2195                assert!(!store.has(2));
2196                assert!(!store.has(4));
2197            }
2198        });
2199    }
2200
2201    /// Config with two records per section.
2202    fn config() -> Config {
2203        Config {
2204            partition: "test-ordinal".into(),
2205            items_per_blob: NZU64!(2),
2206            write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2207            replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2208        }
2209    }
2210
2211    /// Fill sections 0 and 1 with two records each and sync.
2212    async fn fill(context: deterministic::Context, cfg: &Config) {
2213        let mut store = Ordinal::<_, FixedBytes<32>>::init(context, cfg.clone(), None)
2214            .await
2215            .expect("Failed to initialize store");
2216        for i in 0..4 {
2217            store = store.put(i, FixedBytes::new([i as u8; 32])).await.unwrap();
2218        }
2219        store.sync().await.expect("Failed to sync data");
2220    }
2221
2222    /// Tear the last byte off a stored section so its length is no longer record-aligned.
2223    async fn tear(context: &deterministic::Context, cfg: &Config, section: u64) {
2224        let (blob, len) = context
2225            .open(&cfg.partition, &section.to_be_bytes())
2226            .await
2227            .expect("Failed to open blob");
2228        blob.resize(len - 1).await.expect("Failed to tear tail");
2229        blob.sync().await.expect("Failed to sync torn tail");
2230    }
2231
2232    /// Recover through a delayed-sync backend, returning the store and the number of blob syncs
2233    /// recovery issued.
2234    async fn recover(
2235        context: deterministic::Context,
2236        cfg: &Config,
2237        bits: BTreeMap<u64, &Option<BitMap>>,
2238    ) -> (
2239        Ordinal<DelayedSyncContext<deterministic::Context>, FixedBytes<32>>,
2240        usize,
2241    ) {
2242        // Arming counts every blob sync from here on and holds the first one until
2243        // `drive_pending_syncs` releases it.
2244        let pending = PendingSyncs::default();
2245        pending.arm();
2246        let delayed = DelayedSyncContext {
2247            inner: context,
2248            pending: pending.clone(),
2249        };
2250        let store = drive_pending_syncs(&pending, Ordinal::init(delayed, cfg.clone(), Some(bits)))
2251            .await
2252            .expect("Failed to initialize store");
2253        (store, pending.calls())
2254    }
2255
2256    /// Recovering aligned sections that the bits keep whole issues no blob sync.
2257    #[test_traced]
2258    fn test_init_clean_sections_sync_nothing() {
2259        // Initialize the deterministic context
2260        let executor = deterministic::Runner::default();
2261        executor.start(|context| async move {
2262            // Fill two sections
2263            let cfg = config();
2264            fill(context.child("first"), &cfg).await;
2265
2266            // Recover with both sections kept whole
2267            let none = None;
2268            let bits = BTreeMap::from([(0, &none), (1, &none)]);
2269            let (store, calls) = recover(context.child("second"), &cfg, bits).await;
2270
2271            // Both sections are retained as stored, so recovery has nothing to sync
2272            assert_eq!(calls, 0);
2273            for i in 0..4 {
2274                assert!(store.has(i));
2275            }
2276        });
2277    }
2278
2279    /// A torn section the bits omit, or whose bitmap marks no record, is removed without being
2280    /// opened or repaired.
2281    #[rstest::rstest]
2282    #[case::omitted(false)]
2283    #[case::unmarked(true)]
2284    #[test_traced]
2285    fn test_init_removes_uncovered_torn_section_unopened(#[case] unmarked: bool) {
2286        // Initialize the deterministic context
2287        let executor = deterministic::Runner::default();
2288        executor.start(|context| async move {
2289            // Fill two sections and tear the tail of section 1
2290            let cfg = config();
2291            fill(context.child("first"), &cfg).await;
2292            tear(&context, &cfg, 1).await;
2293
2294            // Hold section 1 open so any open of it by recovery fails with BlobAlreadyOpen
2295            let (held, _) = context
2296                .open(&cfg.partition, &1u64.to_be_bytes())
2297                .await
2298                .expect("Failed to open blob");
2299
2300            // Recover with section 0 kept whole and section 1 omitted or unmarked
2301            let none = None;
2302            let zeroes = Some(BitMap::zeroes(2));
2303            let mut bits = BTreeMap::from([(0, &none)]);
2304            if unmarked {
2305                bits.insert(1, &zeroes);
2306            }
2307            let (store, calls) = recover(context.child("second"), &cfg, bits).await;
2308
2309            // Section 0 is retained as stored and section 1 is removed unopened, so recovery
2310            // has nothing to sync
2311            assert_eq!(calls, 0);
2312            assert!(store.has(0));
2313            assert!(store.has(1));
2314            assert!(!store.has(2));
2315            assert!(!store.has(3));
2316            assert_eq!(
2317                store.get(1).await.unwrap().unwrap(),
2318                FixedBytes::new([1u8; 32])
2319            );
2320            assert_eq!(
2321                context.scan(&cfg.partition).await.unwrap(),
2322                vec![0u64.to_be_bytes().to_vec()]
2323            );
2324            drop(held);
2325        });
2326    }
2327
2328    /// A partition that refuses syncs does not fail recovery when only uncovered sections are torn.
2329    #[test_traced]
2330    fn test_init_ignores_uncovered_torn_section_sync_fault() {
2331        // Initialize the deterministic context
2332        let executor = deterministic::Runner::default();
2333        executor.start(|context| async move {
2334            // Fill two sections and tear the tail of section 1
2335            let cfg = config();
2336            fill(context.child("first"), &cfg).await;
2337            tear(&context, &cfg, 1).await;
2338
2339            // Refuse every sync on the index partition, which repairing section 1 would need
2340            let faulty = SyncFaultContext {
2341                inner: context.child("second"),
2342                fail_partition: cfg.partition.clone(),
2343            };
2344
2345            // Recover with only section 0 covered, which needs no sync
2346            let none = None;
2347            let bits = BTreeMap::from([(0, &none)]);
2348            let store = Ordinal::<_, FixedBytes<32>>::init(faulty, cfg.clone(), Some(bits))
2349                .await
2350                .expect("Failed to initialize store");
2351
2352            // Section 0 is retained and section 1 is removed without repair
2353            assert!(store.has(0));
2354            assert!(store.has(1));
2355            assert!(!store.has(2));
2356            assert!(!store.has(3));
2357            assert_eq!(
2358                context.scan(&cfg.partition).await.unwrap(),
2359                vec![0u64.to_be_bytes().to_vec()]
2360            );
2361        });
2362    }
2363
2364    /// A torn tail in a covered section is still truncated to whole records and synced.
2365    #[test_traced]
2366    fn test_init_repairs_covered_torn_section() {
2367        // Initialize the deterministic context
2368        let executor = deterministic::Runner::default();
2369        executor.start(|context| async move {
2370            // Fill two sections and tear the tail of section 0
2371            let cfg = config();
2372            fill(context.child("first"), &cfg).await;
2373            tear(&context, &cfg, 0).await;
2374
2375            // Recover with section 0 marking only its first record and section 1 kept whole
2376            let mut bitmap = BitMap::zeroes(2);
2377            bitmap.set(0, true);
2378            let partial = Some(bitmap);
2379            let none = None;
2380            let bits = BTreeMap::from([(0, &partial), (1, &none)]);
2381            let (store, calls) = recover(context.child("second"), &cfg, bits).await;
2382
2383            // Only the tail repair syncs the retained section. Truncation already removed the
2384            // unmarked record, so clearing has nothing to write.
2385            assert_eq!(calls, 1);
2386            assert!(store.has(0));
2387            assert!(!store.has(1));
2388            assert!(store.has(2));
2389            assert!(store.has(3));
2390            assert_eq!(
2391                store.get(0).await.unwrap().unwrap(),
2392                FixedBytes::new([0u8; 32])
2393            );
2394            drop(store);
2395
2396            // The repaired blob holds exactly the surviving record
2397            let (_, len) = context
2398                .open(&cfg.partition, &0u64.to_be_bytes())
2399                .await
2400                .expect("Failed to open blob");
2401            assert_eq!(len, (FixedBytes::<32>::SIZE + u32::SIZE) as u64);
2402        });
2403    }
2404
2405    /// A section kept whole must hold exactly `items_per_blob` records, so a blob holding more
2406    /// (written under a larger `items_per_blob`) fails initialization instead of hiding records.
2407    #[test_traced]
2408    fn test_init_rejects_whole_section_with_extra_records() {
2409        let executor = deterministic::Runner::default();
2410        executor.start(|context| async move {
2411            // Fill one section of eight records
2412            let mut cfg = Config {
2413                partition: "test-ordinal-whole".into(),
2414                items_per_blob: NZU64!(8),
2415                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2416                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2417            };
2418            let mut store =
2419                Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
2420                    .await
2421                    .expect("Failed to initialize store");
2422            for i in 0..8 {
2423                store = store.put(i, FixedBytes::new([i as u8; 32])).await.unwrap();
2424            }
2425            store.sync().await.expect("Failed to sync data");
2426
2427            // Reopen with four items per blob and the section kept whole
2428            cfg.items_per_blob = NZU64!(4);
2429            let none = None;
2430            let bits = BTreeMap::from([(0, &none)]);
2431            let result = Ordinal::<_, FixedBytes<32>>::init(
2432                context.child("second"),
2433                cfg.clone(),
2434                Some(bits),
2435            )
2436            .await;
2437            assert!(matches!(result, Err(Error::InvalidRecord(4))), "{result:?}");
2438
2439            // The original size still opens the section
2440            cfg.items_per_blob = NZU64!(8);
2441            let bits = BTreeMap::from([(0, &none)]);
2442            let store = Ordinal::<_, FixedBytes<32>>::init(context.child("third"), cfg, Some(bits))
2443                .await
2444                .expect("Failed to initialize store");
2445            for i in 0..8 {
2446                assert!(store.has(i));
2447            }
2448        });
2449    }
2450
2451    /// Build a store with records at indices 1 and 900 of a 1000-record section, synced.
2452    async fn fill_sparse(context: deterministic::Context, write_buffer: usize) -> Config {
2453        let cfg = Config {
2454            partition: "test-ordinal-sparse".into(),
2455            items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
2456            write_buffer: NonZeroUsize::new(write_buffer).unwrap(),
2457            replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2458        };
2459        let mut store = Ordinal::<_, FixedBytes<32>>::init(context, cfg.clone(), None)
2460            .await
2461            .expect("Failed to initialize store");
2462        store = store.put(1, FixedBytes::new([1u8; 32])).await.unwrap();
2463        store = store.put(900, FixedBytes::new([9u8; 32])).await.unwrap();
2464        store.sync().await.expect("Failed to sync data");
2465        cfg
2466    }
2467
2468    /// Recovering a sparse section clears each run of unmarked slots with one write per
2469    /// `write_buffer` bytes.
2470    #[rstest::rstest]
2471    #[case::one_write_per_run(36 * DEFAULT_ITEMS_PER_BLOB as usize, 2)]
2472    #[case::chunked_runs(36 * 100, 10)]
2473    #[test_traced]
2474    fn test_init_clears_unmarked_runs(#[case] write_buffer: usize, #[case] writes: usize) {
2475        let executor = deterministic::Runner::default();
2476        executor.start(|context| async move {
2477            let cfg = fill_sparse(context.child("first"), write_buffer).await;
2478
2479            // Recover with the two stored records marked, recording every blob write
2480            let mut bitmap = BitMap::zeroes(DEFAULT_ITEMS_PER_BLOB);
2481            bitmap.set(1, true);
2482            bitmap.set(900, true);
2483            let marked = Some(bitmap);
2484            let bits = BTreeMap::from([(0, &marked)]);
2485            let (recording, recordings) = RecordingContext::new(context.child("second"));
2486            let store = Ordinal::<_, FixedBytes<32>>::init(recording, cfg, Some(bits))
2487                .await
2488                .expect("Failed to initialize store");
2489
2490            // Slot 0 and slots 2..900 are the two unmarked runs
2491            assert_eq!(recordings.snapshot().writes.len(), writes);
2492            assert!(store.has(1));
2493            assert!(store.has(900));
2494            assert!(!store.has(0));
2495            assert!(!store.has(2));
2496            assert!(!store.has(899));
2497        });
2498    }
2499
2500    /// Recovery clears every unmarked slot that holds a record, including slots past the end of a
2501    /// bitmap, leaving marked records untouched.
2502    #[test_traced]
2503    fn test_init_clears_unmarked_records() {
2504        let executor = deterministic::Runner::default();
2505        executor.start(|context| async move {
2506            // Fill the first 901 slots of a section, synced
2507            let cfg = Config {
2508                partition: "test-ordinal-dense".into(),
2509                items_per_blob: NZU64!(DEFAULT_ITEMS_PER_BLOB),
2510                write_buffer: NZUsize!(DEFAULT_WRITE_BUFFER),
2511                replay_buffer: NZUsize!(DEFAULT_REPLAY_BUFFER),
2512            };
2513            let mut store =
2514                Ordinal::<_, FixedBytes<32>>::init(context.child("first"), cfg.clone(), None)
2515                    .await
2516                    .expect("Failed to initialize store");
2517            for i in 0..=900u64 {
2518                store = store.put(i, FixedBytes::new([i as u8; 32])).await.unwrap();
2519            }
2520            store.sync().await.expect("Failed to sync data");
2521
2522            // Recover with a bitmap that marks slot 1 and ends before slot 900
2523            let mut bitmap = BitMap::zeroes(500);
2524            bitmap.set(1, true);
2525            let marked = Some(bitmap);
2526            let bits = BTreeMap::from([(0, &marked)]);
2527            let (store, calls) = recover(context.child("second"), &cfg, bits).await;
2528
2529            // Clearing the section's unmarked slots is followed by one sync
2530            assert_eq!(calls, 1);
2531            assert!(store.has(1));
2532            assert!(!store.has(5));
2533            assert!(!store.has(900));
2534            drop(store);
2535
2536            // Every unmarked slot now holds zeros while the marked record is intact
2537            let record_size = FixedBytes::<32>::SIZE + u32::SIZE;
2538            let (blob, len) = context
2539                .open(&cfg.partition, &0u64.to_be_bytes())
2540                .await
2541                .expect("Failed to open blob");
2542            let bytes = blob
2543                .read_at(0, usize::try_from(len).unwrap(), ReadOptions::default())
2544                .await
2545                .unwrap()
2546                .coalesce();
2547            assert_eq!(bytes.len(), 901 * record_size);
2548            for (slot, record) in bytes.as_ref().chunks(record_size).enumerate() {
2549                if slot == 1 {
2550                    assert_eq!(&record[..32], &[1u8; 32]);
2551                } else {
2552                    assert!(record.iter().all(|byte| *byte == 0), "slot {slot}");
2553                }
2554            }
2555        });
2556    }
2557}