tests.rs (19141B)
1 use std::collections::BTreeMap; 2 3 use rusqlite::Connection; 4 use tempfile::tempdir; 5 6 use crate::{ 7 adapters::ui_index::build_ui_chain_index, 8 domain::{BLOCK_REWARD, GenesisBurn, Ledger, OutPoint, Wallet, run_vdf}, 9 }; 10 11 use super::{BlockMetricRow, SqliteUiDataStore, replace_metrics}; 12 13 #[test] 14 fn sqlite_ui_data_store_does_not_create_chain_snapshot_table() { 15 let dir = tempdir().unwrap(); 16 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 17 18 store 19 .with_connection(|connection| { 20 let count = connection.query_row( 21 "SELECT COUNT(*) FROM sqlite_master WHERE type = 'table' AND name = 'chain_snapshots'", 22 [], 23 |row| row.get::<_, u64>(0), 24 )?; 25 assert_eq!(count, 0); 26 Ok(()) 27 }) 28 .unwrap(); 29 } 30 31 #[test] 32 fn sqlite_ui_data_store_persists_ui_chain_index_for_latest_tip() { 33 let dir = tempdir().unwrap(); 34 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 35 let wallet = Wallet::from_seed("ui-index-alice"); 36 let mut genesis = BTreeMap::new(); 37 genesis.insert(wallet.address().to_string(), 10); 38 let ledger = 39 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(wallet.address(), 1)], 1) 40 .unwrap(); 41 let snapshot = ledger.snapshot(); 42 let expected = build_ui_chain_index(&snapshot); 43 let tip_hash = expected.tip_hash.as_deref().unwrap(); 44 45 store.project_snapshot(&snapshot, false).unwrap(); 46 47 let loaded = store.load_ui_chain_index(tip_hash).unwrap().unwrap(); 48 assert_eq!(loaded, expected); 49 assert!( 50 store 51 .load_ui_chain_index("different-tip") 52 .unwrap() 53 .is_none() 54 ); 55 } 56 57 #[test] 58 fn sqlite_ui_data_store_clear_all_removes_metrics_and_ui_indexes() { 59 let dir = tempdir().unwrap(); 60 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 61 let wallet = Wallet::from_seed("clear-ui-data-alice"); 62 let mut genesis = BTreeMap::new(); 63 genesis.insert(wallet.address().to_string(), 10); 64 let ledger = 65 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(wallet.address(), 1)], 1) 66 .unwrap(); 67 let expected = build_ui_chain_index(&ledger.snapshot()); 68 let tip_hash = expected.tip_hash.as_deref().unwrap(); 69 70 store.project_snapshot(&ledger.snapshot(), true).unwrap(); 71 assert!(!store.load_metrics().unwrap().is_empty()); 72 assert!(store.load_ui_chain_index(tip_hash).unwrap().is_some()); 73 74 store.clear_all().unwrap(); 75 76 assert!(store.load_metrics().unwrap().is_empty()); 77 assert!(store.load_ui_chain_index(tip_hash).unwrap().is_none()); 78 } 79 80 #[test] 81 fn sqlite_ui_data_store_saves_and_clears_block_metrics() { 82 let dir = tempdir().unwrap(); 83 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 84 let wallet = Wallet::from_seed("metrics-alice"); 85 let mut genesis = BTreeMap::new(); 86 genesis.insert(wallet.address().to_string(), 10); 87 let mut ledger = 88 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(wallet.address(), 1)], 1) 89 .unwrap(); 90 let burn = ledger.build_burn(&wallet, 2, 1).unwrap(); 91 ledger.submit_transaction(burn).unwrap(); 92 let block = ledger.mine_next_block(&wallet, 1_000).unwrap(); 93 ledger.apply_locally_mined_block(block).unwrap(); 94 95 store.project_snapshot(&ledger.snapshot(), true).unwrap(); 96 let metrics = store.load_metrics().unwrap(); 97 98 assert_eq!(metrics.last().unwrap().height, 1); 99 assert_eq!(metrics.last().unwrap().burn_count, 1); 100 assert_eq!(metrics.last().unwrap().burned_amount, 2); 101 assert_eq!(metrics.last().unwrap().fees_amount, 1); 102 assert_eq!( 103 metrics.last().unwrap().circulating_supply, 104 ledger.status().balances.values().copied().sum::<u64>() 105 ); 106 assert_eq!(metrics.last().unwrap().known_wallet_addresses, 1); 107 108 store.clear_metrics().unwrap(); 109 assert!(store.load_metrics().unwrap().is_empty()); 110 } 111 112 #[test] 113 fn sqlite_ui_data_store_projects_wallet_utxos() { 114 let dir = tempdir().unwrap(); 115 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 116 let alice = Wallet::from_seed("ui-utxo-alice"); 117 let bob = Wallet::from_seed("ui-utxo-bob"); 118 let mut genesis = BTreeMap::new(); 119 genesis.insert(alice.address().to_string(), 100); 120 let mut ledger = 121 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 1) 122 .unwrap(); 123 let burn = ledger.build_burn(&alice, 1, 0).unwrap(); 124 ledger.submit_transaction(burn).unwrap(); 125 let transfer = ledger.build_transfer(&alice, bob.address(), 10, 1).unwrap(); 126 ledger.submit_transaction(transfer).unwrap(); 127 let block = ledger.mine_next_block(&alice, 1_000).unwrap(); 128 ledger.apply_locally_mined_block(block).unwrap(); 129 130 store.project_snapshot(&ledger.snapshot(), false).unwrap(); 131 132 let bob_utxos = store.load_wallet_utxos(bob.address()).unwrap(); 133 assert_eq!(bob_utxos.len(), 1); 134 assert_eq!(bob_utxos[0].1.amount, 10); 135 } 136 137 #[test] 138 fn sqlite_ui_data_store_loads_only_requested_outputs() { 139 let dir = tempdir().unwrap(); 140 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 141 let alice = Wallet::from_seed("ui-output-lookup-alice"); 142 let bob = Wallet::from_seed("ui-output-lookup-bob"); 143 let mut genesis = BTreeMap::new(); 144 genesis.insert(alice.address().to_string(), 100); 145 let mut ledger = 146 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 1) 147 .unwrap(); 148 let burn = ledger.build_burn(&alice, 1, 0).unwrap(); 149 ledger.submit_transaction(burn).unwrap(); 150 let transfer = ledger.build_transfer(&alice, bob.address(), 10, 1).unwrap(); 151 let output = OutPoint { 152 txid: transfer.signature().to_string(), 153 index: 0, 154 }; 155 ledger.submit_transaction(transfer).unwrap(); 156 let block = ledger.mine_next_block(&alice, 1_000).unwrap(); 157 ledger.apply_locally_mined_block(block).unwrap(); 158 store.project_snapshot(&ledger.snapshot(), false).unwrap(); 159 160 let requested = [ 161 output.clone(), 162 OutPoint { 163 txid: "missing".to_string(), 164 index: 99, 165 }, 166 ] 167 .into_iter() 168 .collect(); 169 let outputs = store.load_outputs(&requested).unwrap(); 170 171 assert_eq!(outputs.len(), 1); 172 assert_eq!(outputs.get(&output).unwrap().amount, 10); 173 assert_eq!(outputs.get(&output).unwrap().address, bob.address()); 174 } 175 176 #[test] 177 fn sqlite_ui_data_store_projects_wallet_transactions() { 178 let dir = tempdir().unwrap(); 179 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 180 let alice = Wallet::from_seed("ui-wallet-tx-alice"); 181 let bob = Wallet::from_seed("ui-wallet-tx-bob"); 182 let mut genesis = BTreeMap::new(); 183 genesis.insert(alice.address().to_string(), 100); 184 let mut ledger = 185 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 1) 186 .unwrap(); 187 let burn = ledger.build_burn(&alice, 1, 0).unwrap(); 188 ledger.submit_transaction(burn).unwrap(); 189 let transfer = ledger.build_transfer(&alice, bob.address(), 10, 1).unwrap(); 190 let signature = transfer.signature().to_string(); 191 ledger.submit_transaction(transfer).unwrap(); 192 let block = ledger.mine_next_block(&alice, 1_000).unwrap(); 193 ledger.apply_locally_mined_block(block).unwrap(); 194 195 store.project_snapshot(&ledger.snapshot(), false).unwrap(); 196 197 let (rows, total) = store 198 .load_wallet_transactions(bob.address(), &["transfer"], 0, 25) 199 .unwrap(); 200 assert_eq!(total, 1); 201 assert_eq!(rows.len(), 1); 202 assert_eq!(rows[0].transaction.signature(), signature); 203 assert_eq!(rows[0].kind, "transfer"); 204 } 205 206 #[test] 207 fn sqlite_ui_data_store_loads_recent_metrics_in_height_order() { 208 let dir = tempdir().unwrap(); 209 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 210 let wallet = Wallet::from_seed("recent-metrics-alice"); 211 let mut genesis = BTreeMap::new(); 212 genesis.insert(wallet.address().to_string(), 10); 213 let mut ledger = 214 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(wallet.address(), 1)], 1) 215 .unwrap(); 216 217 for timestamp_ms in [1_000, 2_000, 3_000] { 218 let burn = ledger.build_burn(&wallet, 1, 0).unwrap(); 219 ledger.submit_transaction(burn).unwrap(); 220 let block = ledger.mine_next_block(&wallet, timestamp_ms).unwrap(); 221 ledger.apply_locally_mined_block(block).unwrap(); 222 } 223 store.project_snapshot(&ledger.snapshot(), true).unwrap(); 224 225 let metrics = store.load_recent_metrics(2).unwrap(); 226 227 assert_eq!( 228 metrics.iter().map(|row| row.height).collect::<Vec<_>>(), 229 vec![2, 3] 230 ); 231 } 232 233 #[test] 234 fn sqlite_ui_data_store_migrates_known_wallet_address_metrics_column() { 235 let dir = tempdir().unwrap(); 236 let path = dir.path().join("ui_data.sqlite3"); 237 let connection = Connection::open(&path).unwrap(); 238 connection 239 .execute_batch( 240 r#" 241 CREATE TABLE block_metrics ( 242 height INTEGER PRIMARY KEY, 243 block_hash TEXT NOT NULL, 244 timestamp_ms INTEGER NOT NULL, 245 block_time_ms INTEGER, 246 mine_difficulty_bits INTEGER NOT NULL, 247 circulating_supply INTEGER NOT NULL, 248 transaction_count INTEGER NOT NULL, 249 transfer_count INTEGER NOT NULL, 250 burn_count INTEGER NOT NULL, 251 mine_count INTEGER NOT NULL, 252 burned_amount INTEGER NOT NULL, 253 total_burned_amount INTEGER NOT NULL, 254 fees_amount INTEGER NOT NULL, 255 reward_amount INTEGER NOT NULL, 256 vdf_rounds INTEGER NOT NULL, 257 finalizer_rank INTEGER NOT NULL 258 ); 259 "#, 260 ) 261 .unwrap(); 262 drop(connection); 263 264 let store = SqliteUiDataStore::open(&path).unwrap(); 265 266 store 267 .with_connection(|connection| { 268 let count = connection 269 .query_row( 270 "SELECT COUNT(*) FROM pragma_table_info('block_metrics') WHERE name = 'known_wallet_addresses'", 271 [], 272 |row| row.get::<_, u64>(0), 273 ) 274 .unwrap(); 275 assert_eq!(count, 1); 276 Ok(()) 277 }) 278 .unwrap(); 279 } 280 281 #[test] 282 fn sqlite_ui_data_store_metrics_supply_matches_wallet_balances_after_block_rewards() { 283 let dir = tempdir().unwrap(); 284 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 285 let alice = Wallet::from_seed("metrics-supply-alice"); 286 let mut genesis = BTreeMap::new(); 287 genesis.insert(alice.address().to_string(), 100); 288 let mut ledger = 289 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 1) 290 .unwrap(); 291 292 for timestamp_ms in [1_000, 2_000, 3_000] { 293 let burn = ledger.build_burn(&alice, 1, 1).unwrap(); 294 ledger.submit_transaction(burn).unwrap(); 295 let block = ledger.mine_next_block(&alice, timestamp_ms).unwrap(); 296 ledger.apply_locally_mined_block(block).unwrap(); 297 } 298 299 store.project_snapshot(&ledger.snapshot(), true).unwrap(); 300 let metrics = store.load_metrics().unwrap(); 301 let supply_from_balances = ledger.status().balances.values().copied().sum::<u64>(); 302 303 assert_eq!(metrics.last().unwrap().height, 3); 304 assert_eq!( 305 metrics.last().unwrap().circulating_supply, 306 supply_from_balances 307 ); 308 } 309 310 #[test] 311 fn sqlite_ui_data_store_metrics_count_known_wallet_addresses_seen_on_chain() { 312 let dir = tempdir().unwrap(); 313 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 314 let alice = Wallet::from_seed("metrics-address-alice"); 315 let bob = Wallet::from_seed("metrics-address-bob"); 316 let carol = Wallet::from_seed("metrics-address-carol"); 317 let mut genesis = BTreeMap::new(); 318 genesis.insert(alice.address().to_string(), 100); 319 let mut ledger = 320 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(alice.address(), 1)], 1) 321 .unwrap(); 322 323 let burn = ledger.build_burn(&alice, 1, 1).unwrap(); 324 ledger.submit_transaction(burn).unwrap(); 325 let transfer = ledger.build_transfer(&alice, bob.address(), 10, 1).unwrap(); 326 ledger.submit_transaction(transfer).unwrap(); 327 let mine = ledger.build_mine(carol.address()).unwrap(); 328 ledger.submit_transaction(mine).unwrap(); 329 let block = ledger.mine_next_block(&alice, 1_000).unwrap(); 330 ledger.apply_locally_mined_block(block).unwrap(); 331 332 store.project_snapshot(&ledger.snapshot(), true).unwrap(); 333 let metrics = store.load_metrics().unwrap(); 334 335 assert_eq!(metrics[0].known_wallet_addresses, 1); 336 assert_eq!(metrics.last().unwrap().known_wallet_addresses, 3); 337 } 338 339 #[test] 340 fn sqlite_ui_data_store_metrics_include_revealed_blinded_burns() { 341 let dir = tempdir().unwrap(); 342 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 343 let alice = Wallet::from_seed("metrics-blinded-alice"); 344 let bob = Wallet::from_seed("metrics-blinded-bob"); 345 let carol = Wallet::from_seed("metrics-blinded-carol"); 346 let wallets = [alice.clone(), bob.clone()]; 347 let mut genesis = BTreeMap::new(); 348 genesis.insert(alice.address().to_string(), 10_000_000); 349 genesis.insert(bob.address().to_string(), 10_000_000); 350 genesis.insert(carol.address().to_string(), 10_000_000); 351 let mut ledger = Ledger::new_with_genesis_burns( 352 genesis, 353 vec![ 354 GenesisBurn::new(alice.address(), 1_000_000), 355 GenesisBurn::new(bob.address(), 1_000_000), 356 ], 357 1, 358 ) 359 .unwrap(); 360 let blinded = ledger 361 .build_blinded_burn(&carol, 3, 7, ledger.height() + 4) 362 .unwrap(); 363 ledger 364 .submit_blinded_transaction(blinded.transaction) 365 .unwrap(); 366 let leader = ledger.expected_leader_for_next_block().unwrap(); 367 let wallet = wallets 368 .iter() 369 .find(|wallet| wallet.address() == leader) 370 .unwrap(); 371 let burn = ledger.build_burn(wallet, 1, 0).unwrap(); 372 ledger.submit_transaction(burn).unwrap(); 373 let block = ledger.mine_next_block(wallet, 1).unwrap(); 374 ledger.apply_locally_mined_block(block).unwrap(); 375 let supply_after_commit = ledger.status().balances.values().copied().sum::<u64>(); 376 ledger.submit_blinded_reveal(blinded.reveal).unwrap(); 377 let leader = ledger.expected_leader_for_next_block().unwrap(); 378 let wallet = wallets 379 .iter() 380 .find(|wallet| wallet.address() == leader) 381 .unwrap(); 382 let burn = ledger.build_burn(wallet, 1, 0).unwrap(); 383 ledger.submit_transaction(burn).unwrap(); 384 let bundles = ledger 385 .reveal_committee_for_next_block() 386 .into_iter() 387 .filter_map(|member| { 388 let wallet = wallets 389 .iter() 390 .find(|wallet| wallet.address() == member.owner) 391 .unwrap(); 392 ledger.build_reveal_bundle(wallet).unwrap() 393 }) 394 .collect::<Vec<_>>(); 395 assert!(!bundles.is_empty()); 396 let prepared = ledger 397 .prepare_next_block_with_reveal_bundles(wallet.address(), 2, bundles) 398 .unwrap(); 399 let vdf_output = run_vdf(prepared.vdf_seed(), prepared.vdf_rounds()); 400 let block = prepared.finish(wallet, vdf_output); 401 assert_eq!(block.all_blinded_reveals().len(), 1); 402 ledger.apply_locally_mined_block(block).unwrap(); 403 404 store.project_snapshot(&ledger.snapshot(), true).unwrap(); 405 let metrics = store.load_metrics().unwrap(); 406 let commit = metrics 407 .iter() 408 .find(|metric| metric.height == 1) 409 .expect("commit block metrics should exist"); 410 let last = metrics.last().unwrap(); 411 let supply_from_balances = ledger.status().balances.values().copied().sum::<u64>(); 412 413 assert_eq!(commit.circulating_supply, supply_after_commit + 10_000_000); 414 assert_eq!(last.burn_count, 2); 415 assert_eq!(last.burned_amount, 4); 416 assert_eq!(last.fees_amount, 7); 417 assert_eq!(last.circulating_supply, supply_from_balances); 418 assert_eq!(last.known_wallet_addresses, 3); 419 } 420 421 #[test] 422 fn sqlite_ui_data_store_roundtrips_vdf_round_metrics_above_legacy_u32_limit() { 423 let dir = tempdir().unwrap(); 424 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 425 let vdf_rounds = u64::from(u32::MAX) + 42; 426 427 store 428 .with_connection_mut(|connection| { 429 let transaction = connection.transaction().unwrap(); 430 replace_metrics( 431 &transaction, 432 &[BlockMetricRow { 433 height: 1, 434 block_hash: "hash".to_string(), 435 timestamp_ms: 1_000, 436 block_time_ms: Some(415_000), 437 mine_difficulty_bits: 12, 438 circulating_supply: 100, 439 known_wallet_addresses: 1, 440 transaction_count: 0, 441 transfer_count: 0, 442 burn_count: 0, 443 mine_count: 0, 444 burned_amount: 0, 445 total_burned_amount: 0, 446 fees_amount: 0, 447 reward_amount: 0, 448 vdf_rounds, 449 finalizer_rank: 0, 450 }], 451 )?; 452 transaction.commit().unwrap(); 453 Ok(()) 454 }) 455 .unwrap(); 456 457 let metrics = store.load_metrics().unwrap(); 458 assert_eq!(metrics.len(), 1); 459 assert_eq!(metrics[0].vdf_rounds, vdf_rounds); 460 } 461 462 #[test] 463 fn sqlite_ui_data_store_metrics_include_genesis_reward_when_burn_consumes_allocation() { 464 let dir = tempdir().unwrap(); 465 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 466 let wallet = Wallet::from_seed("metrics-genesis-reward"); 467 let mut genesis = BTreeMap::new(); 468 genesis.insert(wallet.address().to_string(), 1); 469 let ledger = 470 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(wallet.address(), 1)], 1) 471 .unwrap(); 472 473 store.project_snapshot(&ledger.snapshot(), true).unwrap(); 474 let metrics = store.load_metrics().unwrap(); 475 476 assert_eq!(metrics.len(), 1); 477 assert_eq!(metrics[0].height, 0); 478 assert_eq!(metrics[0].burned_amount, 1); 479 assert_eq!(metrics[0].reward_amount, BLOCK_REWARD); 480 assert_eq!(metrics[0].circulating_supply, BLOCK_REWARD); 481 } 482 483 #[test] 484 fn sqlite_ui_data_store_disabled_metrics_projection_deletes_old_metrics() { 485 let dir = tempdir().unwrap(); 486 let store = SqliteUiDataStore::open(dir.path().join("ui_data.sqlite3")).unwrap(); 487 let wallet = Wallet::from_seed("metrics-cleanup"); 488 let mut genesis = BTreeMap::new(); 489 genesis.insert(wallet.address().to_string(), 10); 490 let ledger = 491 Ledger::new_with_genesis_burns(genesis, vec![GenesisBurn::new(wallet.address(), 1)], 1) 492 .unwrap(); 493 494 store.project_snapshot(&ledger.snapshot(), true).unwrap(); 495 assert!(!store.load_metrics().unwrap().is_empty()); 496 497 store.project_snapshot(&ledger.snapshot(), false).unwrap(); 498 assert!(store.load_metrics().unwrap().is_empty()); 499 }