#![forbid(unsafe_code)]
#[path = "common/mod.rs"]
mod common;
use std::path::Path;
use dig_block::L2Block;
use dig_blockstore::constants::ALL_COLUMN_FAMILIES;
use dig_blockstore::{
BlockStore, BlockStoreConfig, BlockStoreError, CF_METADATA, DICT_TARGET_SIZE,
DICT_TRAINING_THRESHOLD, ERR_MUTATION_READ_ONLY, META_ZSTD_DICT,
};
use common::{build_chain, temp_blockstore_dir, test_config};
const ZSTD_MAGIC: [u8; 4] = [0x28, 0xB5, 0x2F, 0xFD];
fn open_opts() -> rocksdb::Options {
let mut o = rocksdb::Options::default();
o.create_if_missing(true);
o.create_missing_column_families(true);
o
}
fn ser005_config(path: std::path::PathBuf) -> BlockStoreConfig {
let mut c = test_config(path);
c.use_compression_dict = true;
c.compression_level = 3;
c.max_decompressed_block_bytes = 16 * 1024 * 1024;
c
}
fn read_meta_zstd_dict(path: &Path) -> Option<Vec<u8>> {
let cfs: Vec<_> = ALL_COLUMN_FAMILIES
.iter()
.map(|n| rocksdb::ColumnFamilyDescriptor::new(*n, rocksdb::Options::default()))
.collect();
let db = rocksdb::DB::open_cf_descriptors_read_only(&open_opts(), path, cfs, false).ok()?;
let cf = db.cf_handle(CF_METADATA)?;
db.get_cf(cf, META_ZSTD_DICT.as_bytes()).ok().flatten()
}
fn fill_blocks_up_to(store: &BlockStore, chain: &[L2Block], total: usize) {
assert!(
chain.len() >= total,
"chain must cover at least {total} heights"
);
store.init_genesis(&chain[0]).expect("init_genesis");
for block in chain.iter().take(total).skip(1) {
assert!(
store.put(block, true).expect("put block"),
"every height in 1..{total} must insert a novel hash"
);
}
assert_eq!(
store.block_count().expect("block_count"),
total as u64,
"sanity: CF_BLOCKS row count matches inserts"
);
}
#[test]
fn test_fresh_startup_plain_zstd_no_metadata_dictionary() {
let (_guard, path) = temp_blockstore_dir();
let store = BlockStore::open(ser005_config(path.clone())).expect("open");
assert!(
read_meta_zstd_dict(path.as_path()).is_none(),
"brand-new store must not have trained dictionary metadata"
);
let chain = build_chain(2);
store.init_genesis(&chain[0]).expect("init_genesis");
let compressed = store.serialize_block(&chain[1]).expect("serialize_block");
assert!(
compressed.len() >= 4 && compressed[..4] == ZSTD_MAGIC[..],
"plain zstd frame must carry the standard magic prefix"
);
assert!(
store.put(&chain[1], true).expect("put"),
"first extension of canonical chain must insert a new hash"
);
let back = store
.get_block(&chain[1].hash())
.expect("get_block")
.expect("block present");
assert_eq!(
back.hash(),
chain[1].hash(),
"round-trip identity uses L2Block::hash (SER-004 pattern)"
);
}
#[test]
fn test_training_triggers_when_block_count_reaches_threshold() {
let (_guard, path) = temp_blockstore_dir();
let store = BlockStore::open(ser005_config(path.clone())).expect("open");
let chain = build_chain(DICT_TRAINING_THRESHOLD as usize);
fill_blocks_up_to(&store, &chain, DICT_TRAINING_THRESHOLD as usize);
let dict =
read_meta_zstd_dict(path.as_path()).expect("dictionary must be persisted after training");
assert!(
!dict.is_empty(),
"META_ZSTD_DICT must be non-empty once training completes"
);
let _c = zstd::bulk::Compressor::with_dictionary(3, dict.as_slice())
.expect("valid trained dictionary bytes");
}
#[test]
fn test_dictionary_persists_and_loads_on_reopen() {
let (_guard, path) = temp_blockstore_dir();
{
let store = BlockStore::open(ser005_config(path.clone())).expect("open");
let chain = build_chain(DICT_TRAINING_THRESHOLD as usize);
fill_blocks_up_to(&store, &chain, DICT_TRAINING_THRESHOLD as usize);
}
assert!(
read_meta_zstd_dict(path.as_path()).is_some(),
"dictionary must survive closing the DB"
);
let store2 = BlockStore::open(ser005_config(path.clone())).expect("reopen");
let chain = build_chain(DICT_TRAINING_THRESHOLD as usize + 1);
let h = chain[DICT_TRAINING_THRESHOLD as usize].hash();
assert!(
store2
.put(&chain[DICT_TRAINING_THRESHOLD as usize], true)
.expect("put"),
"post-reopen put must see a novel hash"
);
let blk = store2.get_block(&h).expect("get_block").expect("present");
assert_eq!(blk.hash(), h);
}
#[test]
fn test_pre_dictionary_blocks_readable_after_training() {
let (_guard, path) = temp_blockstore_dir();
let store = BlockStore::open(ser005_config(path.clone())).expect("open");
let chain = build_chain(DICT_TRAINING_THRESHOLD as usize);
let genesis_hash = chain[0].hash();
fill_blocks_up_to(&store, &chain, DICT_TRAINING_THRESHOLD as usize);
let genesis_after = store
.get_block(&genesis_hash)
.expect("get_block")
.expect("genesis must remain readable");
assert_eq!(genesis_after.hash(), genesis_hash);
}
#[test]
fn test_dictionary_size_within_spec_band() {
let (_guard, path) = temp_blockstore_dir();
let store = BlockStore::open(ser005_config(path.clone())).expect("open");
let chain = build_chain(DICT_TRAINING_THRESHOLD as usize);
fill_blocks_up_to(&store, &chain, DICT_TRAINING_THRESHOLD as usize);
let dict = read_meta_zstd_dict(path.as_path()).expect("dict");
let lo = 50 * 1024;
let hi = 150 * 1024;
assert!(
dict.len() >= lo && dict.len() <= hi,
"trained dictionary should sit near {} bytes (got {} bytes)",
DICT_TARGET_SIZE,
dict.len()
);
}
#[test]
fn test_no_double_training_after_dictionary_installed() {
let (_guard, path) = temp_blockstore_dir();
let store = BlockStore::open(ser005_config(path.clone())).expect("open");
let chain = build_chain(DICT_TRAINING_THRESHOLD as usize + 5);
fill_blocks_up_to(&store, &chain, DICT_TRAINING_THRESHOLD as usize);
let dict_before = read_meta_zstd_dict(path.as_path()).expect("dict");
for block in chain.iter().skip(DICT_TRAINING_THRESHOLD as usize) {
assert!(store.put(block, true).expect("put ok"), "row must be novel");
}
let dict_after = read_meta_zstd_dict(path.as_path()).expect("dict still present");
assert_eq!(
dict_before, dict_after,
"maybe_train_dictionary should become a no-op once dictionary exists (spec: one-time training)"
);
}
#[test]
fn test_mixed_mode_reads_all_round_trip() {
let (_guard, path) = temp_blockstore_dir();
let store = BlockStore::open(ser005_config(path.clone())).expect("open");
let chain = build_chain(DICT_TRAINING_THRESHOLD as usize + 3);
fill_blocks_up_to(&store, &chain, DICT_TRAINING_THRESHOLD as usize);
for block in chain.iter().skip(DICT_TRAINING_THRESHOLD as usize) {
assert!(
store.put(block, true).expect("put"),
"post-training extension must remain idempotent on duplicates=false insert path"
);
}
for b in &chain {
let got = store.get_block(&b.hash()).expect("get_block").expect("row");
assert_eq!(got.hash(), b.hash());
}
}
#[test]
fn test_put_on_readonly_store_errors() {
let (_guard, path) = temp_blockstore_dir();
{
let store = BlockStore::open(ser005_config(path.clone())).expect("open");
let chain = build_chain(2);
store.init_genesis(&chain[0]).expect("init_genesis");
}
let ro = BlockStore::open_readonly(path.as_path()).expect("open_readonly");
let chain = build_chain(2);
let err = ro
.put(&chain[1], true)
.expect_err("read-only put must fail");
match err {
BlockStoreError::Serialization(msg) => assert!(
msg.contains(ERR_MUTATION_READ_ONLY),
"expected {ERR_MUTATION_READ_ONLY}, got {msg}"
),
other => panic!("unexpected error variant: {other:?}"),
}
}