iuna

iuna

iuna - experimental mainnet-candidate protocol
git clone https://getiuna.org/git/iuna.git
Log | Files | Refs | README | LICENSE

ledger_queries.rs (35031B)


      1 use std::collections::{BTreeMap, BTreeSet};
      2 
      3 use anyhow::{Context, Result};
      4 use sha2::{Digest, Sha256};
      5 
      6 use super::genesis::balances_from_utxos;
      7 use super::mine_policy::mine_anchor;
      8 use super::ticket::{
      9     BurnTicket, apply_finalizer_ticket_effects, draw_parent_randomness, genesis_tickets,
     10     ranked_tickets_for_height, tickets_created_by_block,
     11 };
     12 use super::{
     13     Amount, Block, BurnCommitteeMember, BurnLeaderRank, ChainSnapshot, ChainStatus, LaunchProfile,
     14     Ledger, OutPoint, Transaction, TxOutput, UtxoLineageRoot,
     15 };
     16 
     17 fn apply_historical_ticket_block(
     18     parent: &Block,
     19     block: &Block,
     20     launch_profile: &super::LaunchProfile,
     21     tickets: &mut Vec<BurnTicket>,
     22 ) -> Result<()> {
     23     apply_finalizer_ticket_effects(parent, block, tickets)?;
     24     tickets.extend(tickets_created_by_block(block, launch_profile)?);
     25     Ok(())
     26 }
     27 
     28 #[derive(Clone, Debug, Eq, PartialEq)]
     29 pub(super) struct LineageCommitteeCandidate {
     30     pub(super) root: UtxoLineageRoot,
     31     pub(super) value: Amount,
     32     pub(super) weight: u64,
     33     pub(super) owner: String,
     34 }
     35 
     36 pub(super) fn lineage_committee_weight(value: Amount) -> u64 {
     37     let one_plus_value = u128::from(value) + 1;
     38     u128::BITS as u64 - one_plus_value.leading_zeros() as u64 - 1
     39 }
     40 
     41 pub(super) fn select_weighted_lineage_index(
     42     parent: &Block,
     43     target_height: u64,
     44     slot: u8,
     45     candidates: &[LineageCommitteeCandidate],
     46 ) -> Option<usize> {
     47     let total_weight = candidates.iter().try_fold(0_u128, |total, candidate| {
     48         total.checked_add(u128::from(candidate.weight))
     49     })?;
     50     if total_weight == 0 {
     51         return None;
     52     }
     53     let seed = lineage_committee_draw_seed(parent, target_height, slot);
     54     let digest = Sha256::digest(seed.as_bytes());
     55     let mut bytes = [0_u8; 16];
     56     bytes.copy_from_slice(&digest[..16]);
     57     let draw = u128::from_be_bytes(bytes) % total_weight;
     58     let mut cumulative = 0_u128;
     59     for (index, candidate) in candidates.iter().enumerate() {
     60         cumulative = cumulative.checked_add(u128::from(candidate.weight))?;
     61         if draw < cumulative {
     62             return Some(index);
     63         }
     64     }
     65     None
     66 }
     67 
     68 fn lineage_committee_draw_seed(parent: &Block, target_height: u64, slot: u8) -> String {
     69     let parent_randomness = draw_parent_randomness(parent, target_height);
     70     format!("iuna-burn-lineage-draw-v1:{target_height}:{parent_randomness}:{slot}")
     71 }
     72 
     73 impl Ledger {
     74     #[cfg(test)]
     75     pub(crate) fn set_tip_height_for_test(&mut self, height: u64) {
     76         self.chain
     77             .last_mut()
     78             .expect("ledger is always initialized with genesis")
     79             .height = height;
     80     }
     81 
     82     pub fn snapshot(&self) -> ChainSnapshot {
     83         ChainSnapshot {
     84             genesis_allocations: self.genesis_allocations.clone(),
     85             vdf_rounds: self.initial_vdf_rounds,
     86             launch_profile: self.launch_profile.clone(),
     87             blocks: self.chain.clone(),
     88         }
     89     }
     90 
     91     pub(crate) fn genesis_snapshot(&self) -> ChainSnapshot {
     92         ChainSnapshot {
     93             genesis_allocations: self.genesis_allocations.clone(),
     94             vdf_rounds: self.initial_vdf_rounds,
     95             launch_profile: self.launch_profile.clone(),
     96             blocks: vec![self.chain[0].clone()],
     97         }
     98     }
     99 
    100     pub fn status(&self) -> ChainStatus {
    101         self.status_with_balances(true)
    102     }
    103 
    104     pub fn light_status(&self) -> ChainStatus {
    105         self.status_with_balances(false)
    106     }
    107 
    108     fn status_with_balances(&self, include_balances: bool) -> ChainStatus {
    109         ChainStatus {
    110             height: self.tip().height,
    111             tip_hash: self.tip().hash.clone(),
    112             finalized_height: self
    113                 .objective_finality_checkpoint
    114                 .as_ref()
    115                 .map(|checkpoint| checkpoint.height),
    116             finalized_hash: self
    117                 .objective_finality_checkpoint
    118                 .as_ref()
    119                 .map(|checkpoint| checkpoint.hash.clone()),
    120             next_leader: self.expected_leader_for_next_block(),
    121             launch_profile_hash: self.launch_profile.hash(),
    122             mine_reward: self.mine_reward,
    123             current_mine_difficulty_bits: self.current_mine_difficulty_bits(),
    124             balances: if include_balances {
    125                 balances_from_utxos(&self.utxos)
    126             } else {
    127                 Default::default()
    128             },
    129             pending_transactions: self.pending.len().saturating_add(self.pending_v2.len()),
    130         }
    131     }
    132 
    133     pub fn tip_hash(&self) -> &str {
    134         &self.tip().hash
    135     }
    136 
    137     pub fn objective_finality_checkpoint(&self) -> Option<(u64, &str)> {
    138         self.objective_finality_checkpoint
    139             .as_ref()
    140             .map(|checkpoint| (checkpoint.height, checkpoint.hash.as_str()))
    141     }
    142 
    143     pub fn chain(&self) -> &[Block] {
    144         &self.chain
    145     }
    146 
    147     pub fn burn_leader_ranks_for_block(&self, height: u64) -> Result<Vec<BurnLeaderRank>> {
    148         Ok(self
    149             .burn_leader_ranks_for_blocks([height])?
    150             .remove(&height)
    151             .unwrap_or_default())
    152     }
    153 
    154     pub fn burn_leader_ranks_for_blocks<I>(
    155         &self,
    156         heights: I,
    157     ) -> Result<BTreeMap<u64, Vec<BurnLeaderRank>>>
    158     where
    159         I: IntoIterator<Item = u64>,
    160     {
    161         let mut requested = heights.into_iter().collect::<BTreeSet<_>>();
    162         let mut ranks_by_height = BTreeMap::new();
    163         if requested.remove(&0) {
    164             ranks_by_height.insert(0, Vec::new());
    165         }
    166         if requested.is_empty() {
    167             return Ok(ranks_by_height);
    168         }
    169 
    170         let mut tickets = genesis_tickets(
    171             &self.genesis_allocations,
    172             &self.chain[0],
    173             &self.launch_profile,
    174         )?;
    175         let mut next_block_index = 1;
    176 
    177         for height in requested {
    178             let parent_index = height.checked_sub(1).context("block height underflows")? as usize;
    179             let parent = self
    180                 .chain
    181                 .get(parent_index)
    182                 .with_context(|| format!("missing parent block for height {height}"))?;
    183             while let Some(block) = self.chain.get(next_block_index) {
    184                 if block.height >= height {
    185                     break;
    186                 }
    187                 let block_parent = self
    188                     .chain
    189                     .get(next_block_index - 1)
    190                     .with_context(|| format!("missing parent block for height {}", block.height))?;
    191                 apply_historical_ticket_block(
    192                     block_parent,
    193                     block,
    194                     &self.launch_profile,
    195                     &mut tickets,
    196                 )?;
    197                 next_block_index += 1;
    198             }
    199 
    200             ranks_by_height.insert(
    201                 height,
    202                 ranked_tickets_for_height(parent, height, &tickets)
    203                     .into_iter()
    204                     .enumerate()
    205                     .map(|(rank, ticket)| BurnLeaderRank {
    206                         rank: rank as u32,
    207                         ticket_id: ticket.id,
    208                         owner: ticket.owner,
    209                         amount: ticket.amount,
    210                         eligible_from_height: ticket.eligible_from_height,
    211                         eligible_until_height: ticket.eligible_until_height,
    212                     })
    213                     .collect(),
    214             );
    215         }
    216 
    217         Ok(ranks_by_height)
    218     }
    219 
    220     pub fn burn_committee_for_next_block(&self) -> Vec<BurnCommitteeMember> {
    221         self.burn_committee_for_next_ticket_block(0)
    222     }
    223 
    224     pub fn burn_committee_for_next_ticket_block(
    225         &self,
    226         finalizer_rank: u32,
    227     ) -> Vec<BurnCommitteeMember> {
    228         self.burn_committee_for_ticket_block(self.tip().height + 1, finalizer_rank)
    229     }
    230 
    231     pub fn burn_committee_for_height(&self, height: u64) -> Vec<BurnCommitteeMember> {
    232         self.burn_committee_for_ticket_block(height, 0)
    233     }
    234 
    235     pub fn burn_committee_for_block(&self, block: &Block) -> Vec<BurnCommitteeMember> {
    236         match block.finalizer_mode {
    237             super::FinalizerMode::Ticket => {
    238                 self.burn_committee_for_ticket_block(block.height, block.finalizer_rank)
    239             }
    240             super::FinalizerMode::Recovery => vec![BurnCommitteeMember {
    241                 slot: 0,
    242                 root: block.hash.clone(),
    243                 owner: block.miner.clone(),
    244                 weight: 0,
    245             }],
    246         }
    247     }
    248 
    249     fn burn_committee_for_ticket_block(
    250         &self,
    251         height: u64,
    252         finalizer_rank: u32,
    253     ) -> Vec<BurnCommitteeMember> {
    254         let ranked = ranked_tickets_for_height(self.tip(), height, &self.tickets);
    255         self.lineage_burn_committee_for_height(height, ranked, finalizer_rank)
    256     }
    257 
    258     pub fn burn_committee_memberships_for_next_block(
    259         &self,
    260         owner: &str,
    261     ) -> Vec<BurnCommitteeMember> {
    262         let max_rank = self
    263             .finalizer_rank_count_for_next_block()
    264             .min(super::BURN_COMMITTEE_SIZE);
    265         let mut memberships = Vec::new();
    266         let mut seen_slots = BTreeSet::new();
    267         for rank in 0..max_rank {
    268             for member in self.burn_committee_for_next_ticket_block(rank as u32) {
    269                 if member.owner != owner || !seen_slots.insert(member.slot) {
    270                     continue;
    271                 }
    272                 memberships.push(member);
    273             }
    274         }
    275         memberships.sort_by_key(|member| member.slot);
    276         memberships
    277     }
    278 
    279     fn lineage_burn_committee_for_height(
    280         &self,
    281         height: u64,
    282         ranked: Vec<BurnTicket>,
    283         finalizer_rank: u32,
    284     ) -> Vec<BurnCommitteeMember> {
    285         let Some(finalizer) = ranked.get(finalizer_rank as usize) else {
    286             return Vec::new();
    287         };
    288         let mut committee = vec![BurnCommitteeMember {
    289             slot: 0,
    290             root: finalizer.id.clone(),
    291             owner: finalizer.owner.clone(),
    292             weight: finalizer.amount,
    293         }];
    294         let max_committee_size = super::BURN_COMMITTEE_SIZE;
    295         let eligible_ticket_owners = ranked
    296             .iter()
    297             .map(|ticket| ticket.owner.clone())
    298             .collect::<BTreeSet<_>>();
    299         let mut skipped_owners = ranked
    300             .iter()
    301             .take(finalizer_rank as usize)
    302             .map(|ticket| ticket.owner.clone())
    303             .collect::<BTreeSet<_>>();
    304         skipped_owners.insert(finalizer.owner.clone());
    305         let mut remaining = self
    306             .eligible_lineage_candidates(&skipped_owners)
    307             .into_iter()
    308             .filter_map(|candidate| {
    309                 let owner = self.representative_owner_for_lineage_root(
    310                     &candidate.root,
    311                     &skipped_owners,
    312                     &eligible_ticket_owners,
    313                 )?;
    314                 Some(LineageCommitteeCandidate {
    315                     root: candidate.root,
    316                     value: candidate.value,
    317                     weight: candidate.weight,
    318                     owner,
    319                 })
    320             })
    321             .collect::<Vec<_>>();
    322 
    323         for slot in 1..max_committee_size {
    324             let Some(index) =
    325                 select_weighted_lineage_index(self.tip(), height, slot as u8, &remaining)
    326             else {
    327                 break;
    328             };
    329             let selected = remaining.remove(index);
    330             skipped_owners.insert(selected.owner.clone());
    331             committee.push(BurnCommitteeMember {
    332                 slot: slot as u8,
    333                 root: selected.root.outpoint.id(),
    334                 owner: selected.owner,
    335                 weight: selected.value,
    336             });
    337             remaining.retain(|candidate| {
    338                 candidate.root != selected.root
    339                     && self
    340                         .representative_owner_for_lineage_root(
    341                             &candidate.root,
    342                             &skipped_owners,
    343                             &eligible_ticket_owners,
    344                         )
    345                         .is_some()
    346             });
    347             for candidate in &mut remaining {
    348                 candidate.owner = self
    349                     .representative_owner_for_lineage_root(
    350                         &candidate.root,
    351                         &skipped_owners,
    352                         &eligible_ticket_owners,
    353                     )
    354                     .expect("retained lineage candidate has representative owner");
    355             }
    356         }
    357 
    358         committee
    359     }
    360 
    361     fn eligible_lineage_candidates(
    362         &self,
    363         skipped_owners: &BTreeSet<String>,
    364     ) -> Vec<LineageCommitteeCandidate> {
    365         let parent_height = self.tip().height;
    366         self.lineage_values
    367             .iter()
    368             .filter(|(root, value)| {
    369                 **value > 0
    370                     && root
    371                         .height
    372                         .saturating_add(self.launch_profile.burn_lineage_maturity_heights)
    373                         <= parent_height
    374                     && !skipped_owners
    375                         .iter()
    376                         .any(|owner| self.lineage_root_has_owner(root, owner))
    377             })
    378             .filter_map(|(root, value)| {
    379                 let weight = lineage_committee_weight(*value);
    380                 (weight > 0).then(|| LineageCommitteeCandidate {
    381                     root: root.clone(),
    382                     value: *value,
    383                     weight,
    384                     owner: String::new(),
    385                 })
    386             })
    387             .collect()
    388     }
    389 
    390     fn lineage_root_has_owner(&self, root: &UtxoLineageRoot, owner: &str) -> bool {
    391         self.lineage_owners
    392             .get(root)
    393             .and_then(|owners| owners.get(owner))
    394             .is_some_and(|outputs| !outputs.is_empty())
    395     }
    396 
    397     fn representative_owner_for_lineage_root(
    398         &self,
    399         root: &UtxoLineageRoot,
    400         skipped_owners: &BTreeSet<String>,
    401         eligible_ticket_owners: &BTreeSet<String>,
    402     ) -> Option<String> {
    403         self.lineage_owners.get(root).and_then(|owners| {
    404             owners
    405                 .iter()
    406                 .filter(|(owner, outputs)| {
    407                     eligible_ticket_owners.contains(*owner)
    408                         && !skipped_owners.contains(*owner)
    409                         && !outputs.is_empty()
    410                 })
    411                 .filter_map(|(owner, outputs)| {
    412                     let (outpoint, amount) = outputs.iter().max_by(|left, right| {
    413                         left.1.cmp(right.1).then_with(|| right.0.cmp(left.0))
    414                     })?;
    415                     Some((owner.clone(), *amount, outpoint.clone()))
    416                 })
    417                 .max_by(|left, right| {
    418                     left.1
    419                         .cmp(&right.1)
    420                         .then_with(|| right.2.cmp(&left.2))
    421                         .then_with(|| right.0.cmp(&left.0))
    422                 })
    423                 .map(|(owner, _, _)| owner)
    424         })
    425     }
    426 
    427     pub fn genesis_hash(&self) -> &str {
    428         &self.chain[0].hash
    429     }
    430 
    431     pub(super) fn transaction_signing_domain(&self) -> super::TransactionSigningDomain {
    432         self.transaction_signing_domain_at(self.height().saturating_add(1))
    433     }
    434 
    435     pub(super) fn transaction_signing_domain_at(
    436         &self,
    437         height: u64,
    438     ) -> super::TransactionSigningDomain {
    439         super::TransactionSigningDomain::for_height(
    440             self.launch_profile.profile_id.clone(),
    441             self.genesis_hash().to_string(),
    442             height,
    443         )
    444     }
    445 
    446     pub fn is_setup_placeholder(&self) -> bool {
    447         self.height() == 0
    448             && self.genesis_allocations.is_empty()
    449             && self.chain[0].transactions.is_empty()
    450             && self.pending.is_empty()
    451     }
    452 
    453     pub fn height(&self) -> u64 {
    454         self.tip().height
    455     }
    456 
    457     pub fn recent_blocks(&self, limit: usize) -> Vec<Block> {
    458         self.chain.iter().rev().take(limit).cloned().collect()
    459     }
    460 
    461     pub fn blocks_before(&self, before_height: u64, limit: usize) -> Vec<Block> {
    462         self.chain
    463             .iter()
    464             .rev()
    465             .filter(|block| block.height < before_height)
    466             .take(limit)
    467             .cloned()
    468             .collect()
    469     }
    470 
    471     pub fn blocks_from(&self, from_height: u64, limit: usize) -> Vec<Block> {
    472         if limit == 0 {
    473             return Vec::new();
    474         }
    475         let Ok(start) = usize::try_from(from_height) else {
    476             return Vec::new();
    477         };
    478         self.chain
    479             .get(start..)
    480             .unwrap_or_default()
    481             .iter()
    482             .take(limit)
    483             .cloned()
    484             .collect()
    485     }
    486 
    487     pub(crate) fn contains_block_sequence(&self, blocks: &[Block]) -> bool {
    488         blocks.iter().all(|block| {
    489             usize::try_from(block.height)
    490                 .ok()
    491                 .and_then(|height| self.chain.get(height))
    492                 .is_some_and(|known| known.hash == block.hash)
    493         })
    494     }
    495 
    496     pub(crate) fn block_locator(&self) -> Vec<String> {
    497         let mut locator = Vec::new();
    498         let mut index = self.chain.len().saturating_sub(1);
    499         let mut step = 1_usize;
    500         loop {
    501             locator.push(self.chain[index].hash.clone());
    502             if index == 0 {
    503                 break;
    504             }
    505             index = index.saturating_sub(step);
    506             if locator.len() > 10 {
    507                 step = step.saturating_mul(2);
    508             }
    509         }
    510         locator
    511     }
    512 
    513     pub(crate) fn blocks_after_locator(&self, locator: &[String], limit: usize) -> Vec<Block> {
    514         let common_height = locator.iter().find_map(|hash| {
    515             self.chain
    516                 .iter()
    517                 .find(|block| block.hash == *hash)
    518                 .map(|block| block.height)
    519         });
    520         common_height
    521             .map(|height| self.blocks_from(height.saturating_add(1), limit))
    522             .unwrap_or_default()
    523     }
    524 
    525     pub fn block_by_hash(&self, hash: &str) -> Option<Block> {
    526         self.chain.iter().find(|block| block.hash == hash).cloned()
    527     }
    528 
    529     pub fn has_block(&self, hash: &str) -> bool {
    530         self.chain.iter().any(|block| block.hash == hash)
    531     }
    532 
    533     pub fn pending(&self) -> &[Transaction] {
    534         &self.pending
    535     }
    536 
    537     pub fn pending_v2(&self) -> &[super::TransactionV2] {
    538         &self.pending_v2
    539     }
    540 
    541     pub(crate) fn clear_pending_transactions(&mut self) {
    542         self.pending.clear();
    543         self.pending_bytes = 0;
    544         self.pending_v2.clear();
    545         self.pending_v2_bytes = 0;
    546     }
    547 
    548     pub fn orphan_transactions(&self) -> &[Transaction] {
    549         &self.orphans
    550     }
    551 
    552     pub fn transaction_by_signature(&self, signature: &str) -> Option<Transaction> {
    553         self.pending
    554             .iter()
    555             .chain(self.orphans.iter())
    556             .chain(
    557                 self.chain
    558                     .iter()
    559                     .flat_map(|block| block.transactions.iter()),
    560             )
    561             .find(|tx| tx.signature() == signature)
    562             .cloned()
    563     }
    564 
    565     pub fn has_transaction(&self, signature: &str) -> bool {
    566         self.mined_transaction_ids.contains(signature)
    567             || self
    568                 .pending
    569                 .iter()
    570                 .chain(self.orphans.iter())
    571                 .any(|transaction| transaction.signature() == signature)
    572     }
    573 
    574     pub fn pending_mine_count_for_anchor(&self, anchor: &str) -> usize {
    575         self.pending
    576             .iter()
    577             .filter(|tx| mine_anchor(tx) == Some(anchor))
    578             .count()
    579     }
    580 
    581     pub fn vdf_rounds(&self) -> u64 {
    582         self.vdf_rounds
    583     }
    584 
    585     pub fn launch_profile(&self) -> &LaunchProfile {
    586         &self.launch_profile
    587     }
    588 
    589     pub fn current_mine_difficulty_bits(&self) -> u32 {
    590         self.mine_difficulty_bits_for_anchor_height(self.tip().height)
    591     }
    592 
    593     pub fn mine_difficulty_bits_at_height(&self, height: u64) -> u32 {
    594         self.mine_difficulty_bits_for_anchor_height(height.min(self.tip().height))
    595     }
    596 
    597     pub fn balance_of(&self, address: &str) -> Amount {
    598         self.utxos
    599             .values()
    600             .filter(|output| output.address == address)
    601             .map(|output| output.amount)
    602             .sum()
    603     }
    604 
    605     pub fn utxos_for_address(&self, address: &str) -> Vec<(OutPoint, TxOutput)> {
    606         self.utxos
    607             .iter()
    608             .filter(|(_, output)| output.address == address)
    609             .map(|(outpoint, output)| (outpoint.clone(), output.clone()))
    610             .collect()
    611     }
    612 
    613     pub fn utxos_for_addresses(&self, addresses: &BTreeSet<String>) -> Vec<(OutPoint, TxOutput)> {
    614         self.utxos
    615             .iter()
    616             .filter(|(_, output)| addresses.contains(&output.address))
    617             .map(|(outpoint, output)| (outpoint.clone(), output.clone()))
    618             .collect()
    619     }
    620 
    621     pub fn all_utxos(&self) -> Vec<(OutPoint, TxOutput)> {
    622         self.utxos
    623             .iter()
    624             .map(|(outpoint, output)| (outpoint.clone(), output.clone()))
    625             .collect()
    626     }
    627 
    628     pub fn output_for_outpoint(&self, outpoint: &OutPoint) -> Option<TxOutput> {
    629         self.utxos.get(outpoint).cloned()
    630     }
    631 
    632     /// Consolidation keeps mining roots separate unless explicitly requested.
    633     pub(crate) fn consolidation_root(&self, outpoint: &OutPoint) -> Option<OutPoint> {
    634         self.utxo_lineage
    635             .get(outpoint)
    636             .map(|root| root.outpoint.clone())
    637     }
    638 
    639     pub fn available_utxos_for_address(&self, address: &str) -> Result<Vec<(OutPoint, TxOutput)>> {
    640         Ok(self
    641             .utxos_after_spendable_pending()?
    642             .into_iter()
    643             .filter(|(_, output)| output.address == address)
    644             .collect())
    645     }
    646 
    647     pub fn available_utxos_for_addresses(
    648         &self,
    649         addresses: &BTreeSet<String>,
    650     ) -> Result<Vec<(OutPoint, TxOutput)>> {
    651         Ok(self
    652             .utxos_after_spendable_pending()?
    653             .into_iter()
    654             .filter(|(_, output)| addresses.contains(&output.address))
    655             .collect())
    656     }
    657 
    658     pub fn next_nonce(&self, address: &str) -> u64 {
    659         self.utxos
    660             .keys()
    661             .chain(
    662                 self.pending
    663                     .iter()
    664                     .flat_map(|tx| tx.inputs().iter().map(|input| &input.outpoint)),
    665             )
    666             .filter(|outpoint| outpoint.txid.contains(address))
    667             .count() as u64
    668             + 1
    669     }
    670 }
    671 
    672 #[cfg(test)]
    673 mod tests {
    674     use super::*;
    675     use crate::domain::{
    676         BURN_COMMITTEE_SIZE, GRINDING_RESISTANCE_ACTIVATION_HEIGHT, GenesisBurn, MICRO_IUNA, Wallet,
    677     };
    678     use proptest::prelude::*;
    679     use proptest::test_runner::Config;
    680 
    681     fn synthetic_committee_ledger(seed: u64) -> Ledger {
    682         let mut ledger = Ledger::new(BTreeMap::new(), 1);
    683         ledger.launch_profile.burn_lineage_maturity_heights = 0;
    684         let owners = (0..10)
    685             .map(|index| format!("owner-{seed}-{index}"))
    686             .collect::<Vec<_>>();
    687 
    688         ledger.tickets = owners
    689             .iter()
    690             .enumerate()
    691             .map(|(index, owner)| {
    692                 let valid = index == 0 || seed.rotate_right(index as u32) & 1 == 1;
    693                 let (eligible_from_height, eligible_until_height) = if valid {
    694                     (1, 1)
    695                 } else if index % 2 == 0 {
    696                     (0, 0)
    697                 } else {
    698                     (2, 3)
    699                 };
    700                 BurnTicket {
    701                     id: format!("ticket-{seed}-{index}"),
    702                     owner: owner.clone(),
    703                     amount: 1 + seed.rotate_left(index as u32) % 100,
    704                     eligible_from_height,
    705                     eligible_until_height,
    706                 }
    707             })
    708             .collect();
    709 
    710         for root_index in 0_u32..8 {
    711             let root = UtxoLineageRoot {
    712                 outpoint: OutPoint {
    713                     txid: format!("{:064x}", u128::from(seed) << 8 | u128::from(root_index)),
    714                     index: root_index,
    715                 },
    716                 height: u64::from((seed.rotate_right(root_index) & 0b11) == 0),
    717             };
    718             let first_owner = (root_index as usize + seed as usize) % owners.len();
    719             let second_owner = (first_owner + 1 + root_index as usize) % owners.len();
    720             let first_value = 1 + seed.rotate_left(root_index) % 1_000;
    721             let second_value = 1 + seed.rotate_right(root_index) % 1_000;
    722             ledger
    723                 .lineage_values
    724                 .insert(root.clone(), first_value + second_value);
    725             ledger.lineage_owners.insert(
    726                 root,
    727                 BTreeMap::from([
    728                     (
    729                         owners[first_owner].clone(),
    730                         BTreeMap::from([(
    731                             OutPoint {
    732                                 txid: format!("{:064x}", 0x100_u128 + root_index as u128),
    733                                 index: 0,
    734                             },
    735                             first_value,
    736                         )]),
    737                     ),
    738                     (
    739                         owners[second_owner].clone(),
    740                         BTreeMap::from([(
    741                             OutPoint {
    742                                 txid: format!("{:064x}", 0x200_u128 + root_index as u128),
    743                                 index: 0,
    744                             },
    745                             second_value,
    746                         )]),
    747                     ),
    748                 ]),
    749             );
    750         }
    751         ledger
    752     }
    753 
    754     #[test]
    755     fn lineage_committee_selects_a_ticket_owner_within_the_selected_root() {
    756         let finalizer = Wallet::from_seed("lineage-ticket-finalizer");
    757         let ticket_owner = Wallet::from_seed("lineage-ticket-owner");
    758         let non_ticket_owner = Wallet::from_seed("lineage-non-ticket-owner");
    759         let mut ledger = Ledger::new_with_genesis_burns(
    760             BTreeMap::from([(finalizer.address().to_string(), 10 * MICRO_IUNA)]),
    761             vec![GenesisBurn::new(finalizer.address(), MICRO_IUNA)],
    762             1,
    763         )
    764         .unwrap();
    765         ledger.launch_profile.burn_lineage_maturity_heights = 0;
    766 
    767         let ticket_root = UtxoLineageRoot {
    768             outpoint: OutPoint {
    769                 txid: "1".repeat(64),
    770                 index: 0,
    771             },
    772             height: 0,
    773         };
    774         ledger.lineage_values.insert(ticket_root.clone(), 110);
    775         ledger.lineage_owners.insert(
    776             ticket_root.clone(),
    777             BTreeMap::from([
    778                 (
    779                     non_ticket_owner.address().to_string(),
    780                     BTreeMap::from([(
    781                         OutPoint {
    782                             txid: "2".repeat(64),
    783                             index: 0,
    784                         },
    785                         100,
    786                     )]),
    787                 ),
    788                 (
    789                     ticket_owner.address().to_string(),
    790                     BTreeMap::from([(
    791                         OutPoint {
    792                             txid: "3".repeat(64),
    793                             index: 0,
    794                         },
    795                         10,
    796                     )]),
    797                 ),
    798             ]),
    799         );
    800 
    801         let non_ticket_root = UtxoLineageRoot {
    802             outpoint: OutPoint {
    803                 txid: "4".repeat(64),
    804                 index: 0,
    805             },
    806             height: 0,
    807         };
    808         ledger.lineage_values.insert(non_ticket_root.clone(), 1_000);
    809         ledger.lineage_owners.insert(
    810             non_ticket_root,
    811             BTreeMap::from([(
    812                 non_ticket_owner.address().to_string(),
    813                 BTreeMap::from([(
    814                     OutPoint {
    815                         txid: "5".repeat(64),
    816                         index: 0,
    817                     },
    818                     1_000,
    819                 )]),
    820             )]),
    821         );
    822 
    823         let ranked = vec![
    824             BurnTicket {
    825                 id: "finalizer-ticket".to_string(),
    826                 owner: finalizer.address().to_string(),
    827                 amount: 1,
    828                 eligible_from_height: 1,
    829                 eligible_until_height: 1,
    830             },
    831             BurnTicket {
    832                 id: "committee-ticket".to_string(),
    833                 owner: ticket_owner.address().to_string(),
    834                 amount: 1,
    835                 eligible_from_height: 1,
    836                 eligible_until_height: 1,
    837             },
    838         ];
    839 
    840         let committee = ledger.lineage_burn_committee_for_height(1, ranked, 0);
    841 
    842         assert_eq!(committee.len(), 2);
    843         assert_eq!(committee[1].root, ticket_root.outpoint.id());
    844         assert_eq!(committee[1].owner, ticket_owner.address());
    845         assert_ne!(committee[1].owner, non_ticket_owner.address());
    846     }
    847 
    848     #[test]
    849     fn lineage_weight_is_floor_log2_of_one_plus_root_value() {
    850         assert_eq!(lineage_committee_weight(0), 0);
    851         assert_eq!(lineage_committee_weight(1), 1);
    852         assert_eq!(lineage_committee_weight(2), 1);
    853         assert_eq!(lineage_committee_weight(3), 2);
    854         assert_eq!(lineage_committee_weight(7), 3);
    855         assert_eq!(lineage_committee_weight(8), 3);
    856         assert_eq!(lineage_committee_weight(u64::MAX), 64);
    857     }
    858 
    859     #[test]
    860     fn committee_draw_seed_has_a_fixed_legacy_parent_hash_height_and_slot_vector() {
    861         let mut parent = Ledger::new(BTreeMap::new(), 1).tip().clone();
    862         parent.hash = "a".repeat(64);
    863         parent.vdf_output = "parent-vdf".to_string();
    864         let candidates = [1_u64, 2, 4]
    865             .into_iter()
    866             .enumerate()
    867             .map(|(index, weight)| LineageCommitteeCandidate {
    868                 root: UtxoLineageRoot {
    869                     outpoint: OutPoint {
    870                         txid: format!("{:064x}", index + 1),
    871                         index: 0,
    872                     },
    873                     height: 1,
    874                 },
    875                 value: weight,
    876                 weight,
    877                 owner: format!("owner-{index}"),
    878             })
    879             .collect::<Vec<_>>();
    880 
    881         assert_eq!(
    882             select_weighted_lineage_index(&parent, 42, 1, &candidates),
    883             Some(2)
    884         );
    885     }
    886 
    887     #[test]
    888     fn committee_draw_stops_using_grindable_parent_hash_at_height_1000() {
    889         let mut parent = Ledger::new(BTreeMap::new(), 1).tip().clone();
    890         parent.height = GRINDING_RESISTANCE_ACTIVATION_HEIGHT - 1;
    891         parent.hash = "1".repeat(64);
    892         parent.vdf_output = "parent-vdf".to_string();
    893         let mut alternate_hash = parent.clone();
    894         alternate_hash.hash = "2".repeat(64);
    895 
    896         assert_ne!(
    897             lineage_committee_draw_seed(&parent, GRINDING_RESISTANCE_ACTIVATION_HEIGHT - 1, 1,),
    898             lineage_committee_draw_seed(
    899                 &alternate_hash,
    900                 GRINDING_RESISTANCE_ACTIVATION_HEIGHT - 1,
    901                 1,
    902             )
    903         );
    904         assert_eq!(
    905             lineage_committee_draw_seed(&parent, GRINDING_RESISTANCE_ACTIVATION_HEIGHT, 1),
    906             lineage_committee_draw_seed(&alternate_hash, GRINDING_RESISTANCE_ACTIVATION_HEIGHT, 1,)
    907         );
    908 
    909         let mut alternate_output = parent;
    910         alternate_output.vdf_output = "different-vdf".to_string();
    911         assert_ne!(
    912             lineage_committee_draw_seed(
    913                 &alternate_output,
    914                 GRINDING_RESISTANCE_ACTIVATION_HEIGHT,
    915                 1,
    916             ),
    917             lineage_committee_draw_seed(&alternate_hash, GRINDING_RESISTANCE_ACTIVATION_HEIGHT, 1,)
    918         );
    919     }
    920 
    921     proptest! {
    922         #![proptest_config(Config { cases: 128, .. Config::default() })]
    923 
    924         #[test]
    925         fn committee_members_are_eligible_ticket_owners_in_distinct_mature_roots(
    926             seed in any::<u64>(),
    927             requested_rank in 0usize..10,
    928         ) {
    929             let ledger = synthetic_committee_ledger(seed);
    930             let target_height = ledger.height() + 1;
    931             let ranked = ranked_tickets_for_height(ledger.tip(), target_height, &ledger.tickets);
    932             let rank = requested_rank % ranked.len();
    933             let committee = ledger.burn_committee_for_next_ticket_block(rank as u32);
    934             let repeated = ledger.burn_committee_for_next_ticket_block(rank as u32);
    935             let excluded_owners = ranked
    936                 .iter()
    937                 .take(rank + 1)
    938                 .map(|ticket| ticket.owner.as_str())
    939                 .collect::<BTreeSet<_>>();
    940 
    941             prop_assert_eq!(&committee, &repeated, "committee selection must be deterministic");
    942             prop_assert!(!committee.is_empty());
    943             prop_assert!(committee.len() <= BURN_COMMITTEE_SIZE);
    944 
    945             let mut owners = BTreeSet::new();
    946             let mut roots = BTreeSet::new();
    947             for (index, member) in committee.iter().enumerate() {
    948                 prop_assert_eq!(usize::from(member.slot), index, "committee slots must be contiguous");
    949                 prop_assert!(owners.insert(member.owner.as_str()), "committee owners must be unique");
    950                 prop_assert!(
    951                     ledger.tickets.iter().any(|ticket| {
    952                         ticket.owner == member.owner
    953                             && ticket.eligible_from_height <= target_height
    954                             && target_height <= ticket.eligible_until_height
    955                     }),
    956                     "committee member {} has no ticket for height {target_height}",
    957                     member.owner,
    958                 );
    959 
    960                 if member.slot == 0 {
    961                     prop_assert_eq!(&member.owner, &ranked[rank].owner);
    962                     prop_assert_eq!(&member.root, &ranked[rank].id);
    963                     prop_assert_eq!(member.weight, ranked[rank].amount);
    964                     continue;
    965                 }
    966 
    967                 prop_assert!(!excluded_owners.contains(member.owner.as_str()));
    968                 let (root, root_owners) = ledger
    969                     .lineage_owners
    970                     .iter()
    971                     .find(|(root, _)| root.outpoint.id() == member.root)
    972                     .expect("selected committee root must exist");
    973                 prop_assert!(roots.insert(root.clone()), "lineage roots must be unique");
    974                 prop_assert_eq!(member.weight, ledger.lineage_values[root]);
    975                 prop_assert!(
    976                     root.height
    977                         .saturating_add(ledger.launch_profile.burn_lineage_maturity_heights)
    978                         <= ledger.tip().height,
    979                     "committee root must be mature",
    980                 );
    981                 prop_assert!(
    982                     root_owners
    983                         .get(&member.owner)
    984                         .is_some_and(|outputs| !outputs.is_empty()),
    985                     "committee member must currently belong to the selected lineage root",
    986                 );
    987                 prop_assert!(
    988                     root_owners
    989                         .keys()
    990                         .all(|owner| !excluded_owners.contains(owner.as_str())),
    991                     "a root containing a missed owner or finalizer must be excluded",
    992                 );
    993             }
    994         }
    995     }
    996 }