iuna

iuna - experimental devnet protocol
git clone https://iuna.jhx.app/git/iuna.git
Log | Files | Refs | README | LICENSE

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 }