mod helpers;
use std::collections::HashMap;
use std::time::Instant;
use dig_coinstore::coin_store::CoinStore;
use dig_coinstore::error::CoinStoreError;
use dig_coinstore::merkle::{merkle_leaf_hash, SparseMerkleTree};
use dig_coinstore::types::CoinRecord;
use dig_coinstore::{Bytes32, CoinId, CoinStoreSnapshot};
fn legacy_storage_bytes(rec: &CoinRecord) -> Vec<u8> {
let mut buf = Vec::with_capacity(97);
buf.extend_from_slice(rec.coin.parent_coin_info.as_ref());
buf.extend_from_slice(rec.coin.puzzle_hash.as_ref());
buf.extend_from_slice(&rec.coin.amount.to_le_bytes());
buf.extend_from_slice(&rec.confirmed_height.to_le_bytes());
let spent_raw = rec.spent_height.unwrap_or(0);
buf.extend_from_slice(&spent_raw.to_le_bytes());
buf.push(if rec.coinbase { 1 } else { 0 });
buf.extend_from_slice(&rec.timestamp.to_le_bytes());
buf
}
fn smt_root_from_legacy_records(records: &[CoinRecord]) -> Bytes32 {
let mut tree = SparseMerkleTree::new();
let entries: Vec<(Bytes32, Bytes32)> = records
.iter()
.map(|r| (r.coin_id(), merkle_leaf_hash(&legacy_storage_bytes(r))))
.collect();
if !entries.is_empty() {
tree.batch_insert(&entries).unwrap();
}
tree.root()
}
#[test]
fn vv_req_api_010_rollback_above_tip_variant_constructible_and_display() {
let err = CoinStoreError::RollbackAboveTip {
target: 100,
current: 50,
};
assert_eq!(err, err.clone());
let msg = err.to_string();
assert!(
msg.contains("100") && msg.contains("50"),
"Display should include target and current: {msg}"
);
assert!(
msg.to_lowercase().contains("rollback"),
"Display should mention rollback: {msg}"
);
}
#[cfg(feature = "rocksdb-storage")]
#[test]
fn vv_req_api_010_rollback_above_tip_triggered_when_target_gt_height() {
let dir = helpers::temp_dir();
let mut store = CoinStore::new(dir.path()).unwrap();
store.init_genesis(vec![], 1).unwrap();
assert_eq!(store.height(), 0);
let err = store.rollback_to_block(100).unwrap_err();
assert_eq!(
err,
CoinStoreError::RollbackAboveTip {
target: 100,
current: 0
}
);
}
#[cfg(feature = "rocksdb-storage")]
#[test]
fn vv_req_api_010_rollback_equal_height_is_noop() {
let dir = helpers::temp_dir();
let mut store = CoinStore::new(dir.path()).unwrap();
store.init_genesis(vec![], 1).unwrap();
let result = store.rollback_to_block(0).unwrap();
assert_eq!(
result.new_height, 0,
"Rolling back to current height is a no-op"
);
assert_eq!(result.coins_deleted, 0);
assert_eq!(result.coins_unspent, 0);
}
#[cfg(feature = "rocksdb-storage")]
#[test]
fn vv_req_api_010_is_unspent_true_after_genesis() {
let dir = helpers::temp_dir();
let mut store = CoinStore::new(dir.path()).unwrap();
let coin = helpers::test_coin(3, 4, 500);
let id = coin.coin_id();
store
.init_genesis(vec![(coin, false)], 1_700_000_100)
.unwrap();
assert!(store.is_unspent(&id));
}
#[cfg(feature = "rocksdb-storage")]
#[test]
fn vv_req_api_010_is_unspent_false_for_spent_row_after_restore() {
let coin = helpers::test_coin(9, 8, 123);
let mut rec = CoinRecord::new(coin, 0, 1, false);
rec.spend(5);
let id = rec.coin_id();
let mut coins = HashMap::new();
coins.insert(id, rec.clone());
let snap = CoinStoreSnapshot {
height: 0,
block_hash: Bytes32::from([0u8; 32]),
state_root: smt_root_from_legacy_records(std::slice::from_ref(&rec)),
timestamp: 2,
coins,
hints: vec![],
total_coins: 1,
total_value: 0,
};
let dir = helpers::temp_dir();
let mut store = CoinStore::new(dir.path()).unwrap();
store.restore(snap).unwrap();
assert!(!store.is_unspent(&id));
}
#[cfg(feature = "rocksdb-storage")]
#[test]
fn vv_req_api_010_is_unspent_false_for_random_id() {
let dir = helpers::temp_dir();
let mut store = CoinStore::new(dir.path()).unwrap();
store.init_genesis(vec![], 1).unwrap();
let random: CoinId = helpers::test_hash(0xEE);
assert!(!store.is_unspent(&random));
}
#[cfg(feature = "rocksdb-storage")]
#[test]
fn vv_req_api_010_is_unspent_survives_store_reopen() {
let dir = helpers::temp_dir();
let path = dir.path().to_path_buf();
let coin = helpers::test_coin(1, 2, 50);
let id = coin.coin_id();
{
let mut store = CoinStore::new(&path).unwrap();
store
.init_genesis(vec![(coin, false)], 1_700_000_200)
.unwrap();
assert!(store.is_unspent(&id));
}
let store2 = CoinStore::new(&path).unwrap();
assert!(store2.is_unspent(&id));
}
#[cfg(feature = "rocksdb-storage")]
#[test]
fn vv_req_api_010_is_unspent_many_lookups_bounded_time() {
let dir = helpers::temp_dir();
let mut store = CoinStore::new(dir.path()).unwrap();
let coin = helpers::test_coin(7, 7, 1);
let id = coin.coin_id();
store.init_genesis(vec![(coin, false)], 1).unwrap();
let n = 50_000u32;
let t0 = Instant::now();
for _ in 0..n {
assert!(store.is_unspent(&id));
}
let elapsed = t0.elapsed();
assert!(
elapsed.as_millis() < 200,
"50k is_unspent calls took {:?} (expected HashSet O(1) aggregate)",
elapsed
);
}