iuna

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

in_memory_network.rs (22975B)


      1 use std::collections::BTreeMap;
      2 
      3 use anyhow::Result;
      4 
      5 use super::{GossipEnvelope, NodeCore};
      6 
      7 #[derive(Debug, Default)]
      8 pub struct InMemoryNetwork {
      9     nodes: BTreeMap<String, NodeCore>,
     10 }
     11 
     12 impl InMemoryNetwork {
     13     pub fn insert(&mut self, id: impl Into<String>, node: NodeCore) {
     14         self.nodes.insert(id.into(), node);
     15     }
     16 
     17     pub fn node(&self, id: &str) -> Option<&NodeCore> {
     18         self.nodes.get(id)
     19     }
     20 
     21     pub fn node_mut(&mut self, id: &str) -> Option<&mut NodeCore> {
     22         self.nodes.get_mut(id)
     23     }
     24 
     25     pub fn deliver_until_idle(&mut self) -> Result<()> {
     26         loop {
     27             let mut outbound = Vec::new();
     28             for (id, node) in &mut self.nodes {
     29                 for envelope in node.drain_outbox() {
     30                     outbound.push((id.clone(), envelope));
     31                 }
     32             }
     33 
     34             if outbound.is_empty() {
     35                 return Ok(());
     36             }
     37 
     38             for (from, envelope) in outbound {
     39                 for (id, node) in &mut self.nodes {
     40                     if *id != from {
     41                         receive_in_memory_envelope(node, envelope.clone())?;
     42                     }
     43                 }
     44             }
     45         }
     46     }
     47 
     48     pub fn gossip_mempools_once(&mut self) -> Result<()> {
     49         let mut outbound = Vec::new();
     50         for (id, node) in &mut self.nodes {
     51             for envelope in node.mempool_gossip() {
     52                 outbound.push((id.clone(), envelope));
     53             }
     54         }
     55 
     56         for (from, envelope) in outbound {
     57             for (id, node) in &mut self.nodes {
     58                 if *id != from {
     59                     receive_in_memory_envelope(node, envelope.clone())?;
     60                 }
     61             }
     62         }
     63         Ok(())
     64     }
     65 
     66     pub fn sync_node_from_peer(&mut self, from: &str, to: &str, limit: usize) -> Result<bool> {
     67         let from_height = self
     68             .nodes
     69             .get(to)
     70             .map(|node| node.chain_height() + 1)
     71             .ok_or_else(|| anyhow::anyhow!("missing sync target node {to}"))?;
     72         let blocks = self
     73             .nodes
     74             .get(from)
     75             .map(|node| node.blocks_from(from_height, limit))
     76             .ok_or_else(|| anyhow::anyhow!("missing sync source node {from}"))?;
     77         if blocks.is_empty() {
     78             return Ok(false);
     79         }
     80 
     81         self.nodes
     82             .get_mut(to)
     83             .expect("sync target exists")
     84             .receive(GossipEnvelope::Blocks { blocks })?;
     85         Ok(true)
     86     }
     87 }
     88 
     89 fn receive_in_memory_envelope(node: &mut NodeCore, envelope: GossipEnvelope) -> Result<()> {
     90     let transaction_like = matches!(
     91         envelope,
     92         GossipEnvelope::BlindedTransaction(_)
     93             | GossipEnvelope::BlindedTransactions { .. }
     94             | GossipEnvelope::MineAction(_)
     95             | GossipEnvelope::MineActions { .. }
     96             | GossipEnvelope::BlindedReveal(_)
     97             | GossipEnvelope::BlindedReveals { .. }
     98     );
     99     match node.receive(envelope) {
    100         Ok(()) => Ok(()),
    101         Err(_) if transaction_like => Ok(()),
    102         Err(error) => Err(error),
    103     }
    104 }
    105 
    106 #[cfg(test)]
    107 mod tests {
    108     use std::collections::{BTreeMap, BTreeSet};
    109 
    110     use crate::{
    111         app::{GossipEnvelope, InMemoryNetwork, NodeCore, REVEAL_BUNDLE_COLLECTION_MS},
    112         domain::{
    113             Block, GenesisBurn, Ledger, MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS, MICRO_IUNA,
    114             RECOVERY_BLOCK_DELAY_MS, VDF_TARGET_BLOCK_MS, Wallet,
    115         },
    116     };
    117 
    118     #[derive(Clone, Debug)]
    119     struct ChaosRng {
    120         state: u64,
    121     }
    122 
    123     impl ChaosRng {
    124         fn new(seed: u64) -> Self {
    125             Self {
    126                 state: seed ^ 0x517c_c1b7_2722_0a95,
    127             }
    128         }
    129 
    130         fn next_u64(&mut self) -> u64 {
    131             self.state = self
    132                 .state
    133                 .wrapping_mul(6_364_136_223_846_793_005)
    134                 .wrapping_add(1_442_695_040_888_963_407);
    135             self.state
    136         }
    137 
    138         fn index(&mut self, len: usize) -> usize {
    139             assert!(len > 0);
    140             (self.next_u64() as usize) % len
    141         }
    142     }
    143 
    144     #[test]
    145     fn sparse_network_stays_bounded_under_generated_actions() {
    146         const NODES: usize = 10;
    147         const ROUNDS: usize = 36;
    148         const SETTLE_BLOCKS: u64 = MAX_BLINDED_TRANSACTION_EXPIRY_HEIGHTS + 10;
    149         const QUIET_BLOCKS: usize = SETTLE_BLOCKS as usize + 4;
    150 
    151         let mut rng = ChaosRng::new(0x1aba_0100);
    152         let wallets = (0..NODES)
    153             .map(|index| Wallet::from_seed(&format!("sparse-network-chaos-{index}")))
    154             .collect::<Vec<_>>();
    155         let allocations = wallets
    156             .iter()
    157             .map(|wallet| (wallet.address().to_string(), 75 * MICRO_IUNA))
    158             .collect::<BTreeMap<_, _>>();
    159         let genesis_burns = wallets
    160             .iter()
    161             .map(|wallet| GenesisBurn::new(wallet.address(), MICRO_IUNA))
    162             .collect::<Vec<_>>();
    163         let ledger = Ledger::new_with_genesis_burns(allocations, genesis_burns, 1)
    164             .expect("chaos genesis is valid");
    165         let node_ids = (0..NODES)
    166             .map(|index| format!("n{index}"))
    167             .collect::<Vec<_>>();
    168         let peers = sparse_chaos_peers(NODES, &mut rng);
    169         let mut offline_until = vec![0_usize; NODES];
    170         let mut pending_since = BTreeMap::new();
    171         let mut network = InMemoryNetwork::default();
    172 
    173         for (index, wallet) in wallets.iter().enumerate() {
    174             let joined = Ledger::from_snapshot(ledger.snapshot()).expect("node joins valid chain");
    175             let mut node = NodeCore::from_ledger_with_burn_fee_and_enabled(
    176                 wallet.clone(),
    177                 joined,
    178                 true,
    179                 MICRO_IUNA / 10,
    180                 0,
    181             );
    182             node.set_recovery_vdf_top_rank_percent(100);
    183             network.insert(node_ids[index].clone(), node);
    184         }
    185 
    186         for round in 0..ROUNDS {
    187             let online = online_chaos_nodes(&offline_until, round);
    188             for index in online.iter().copied().collect::<Vec<_>>() {
    189                 match rng.index(32) {
    190                     0 => {
    191                         let recipient = wallets[rng.index(wallets.len())].address().to_string();
    192                         let expiry_height = network
    193                             .node(&node_ids[index])
    194                             .expect("actor node exists")
    195                             .chain_height()
    196                             + 16;
    197                         let _ = network
    198                             .node_mut(&node_ids[index])
    199                             .expect("actor node exists")
    200                             .blinded_transfer_with_fee(recipient, 1, 0, expiry_height);
    201                     }
    202                     1 => {
    203                         attempt_bounded_mine_action(
    204                             &mut network,
    205                             &node_ids[index],
    206                             wallets[index].address(),
    207                             &mut rng,
    208                         );
    209                     }
    210                     2 if online.len() > 1 => {
    211                         offline_until[index] = round + 1 + rng.index(4);
    212                     }
    213                     _ => {}
    214                 }
    215             }
    216 
    217             deliver_sparse_chaos_until_idle(
    218                 &mut network,
    219                 &node_ids,
    220                 &peers,
    221                 &online,
    222                 chaos_timestamp(round),
    223                 &mut rng,
    224             );
    225             mine_one_sparse_chaos_block(
    226                 &mut network,
    227                 &node_ids,
    228                 &wallets,
    229                 &online,
    230                 chaos_timestamp(round),
    231             );
    232             deliver_sparse_chaos_until_idle(
    233                 &mut network,
    234                 &node_ids,
    235                 &peers,
    236                 &online,
    237                 chaos_timestamp(round),
    238                 &mut rng,
    239             );
    240             observe_bounded_pending(&network, &node_ids, &mut pending_since, SETTLE_BLOCKS);
    241         }
    242 
    243         for round in ROUNDS..ROUNDS + QUIET_BLOCKS {
    244             let online = online_chaos_nodes(&offline_until, round);
    245             deliver_sparse_chaos_until_idle(
    246                 &mut network,
    247                 &node_ids,
    248                 &peers,
    249                 &online,
    250                 chaos_timestamp(round),
    251                 &mut rng,
    252             );
    253             mine_one_sparse_chaos_block(
    254                 &mut network,
    255                 &node_ids,
    256                 &wallets,
    257                 &online,
    258                 chaos_timestamp(round),
    259             );
    260             deliver_sparse_chaos_until_idle(
    261                 &mut network,
    262                 &node_ids,
    263                 &peers,
    264                 &online,
    265                 chaos_timestamp(round),
    266                 &mut rng,
    267             );
    268             observe_bounded_pending(&network, &node_ids, &mut pending_since, SETTLE_BLOCKS);
    269         }
    270 
    271         let all_online = (0..NODES).collect::<BTreeSet<_>>();
    272         for round in ROUNDS + QUIET_BLOCKS..ROUNDS + QUIET_BLOCKS + SETTLE_BLOCKS as usize * 4 {
    273             deliver_sparse_chaos_until_idle(
    274                 &mut network,
    275                 &node_ids,
    276                 &peers,
    277                 &all_online,
    278                 chaos_timestamp(round),
    279                 &mut rng,
    280             );
    281             if !sparse_chaos_has_pending(&network, &node_ids) {
    282                 break;
    283             }
    284             mine_one_sparse_chaos_block(
    285                 &mut network,
    286                 &node_ids,
    287                 &wallets,
    288                 &all_online,
    289                 chaos_timestamp(round),
    290             );
    291             deliver_sparse_chaos_until_idle(
    292                 &mut network,
    293                 &node_ids,
    294                 &peers,
    295                 &all_online,
    296                 chaos_timestamp(round),
    297                 &mut rng,
    298             );
    299             observe_bounded_pending(&network, &node_ids, &mut pending_since, SETTLE_BLOCKS);
    300         }
    301         assert_sparse_chaos_converged(&network, &node_ids);
    302         assert_sparse_chaos_mempools_empty(&network, &node_ids);
    303     }
    304 
    305     fn sparse_chaos_peers(nodes: usize, rng: &mut ChaosRng) -> Vec<Vec<usize>> {
    306         let mut peers = vec![BTreeSet::new(); nodes];
    307         for index in 0..nodes {
    308             let next = (index + 1) % nodes;
    309             peers[index].insert(next);
    310             peers[next].insert(index);
    311         }
    312         for index in 0..nodes {
    313             let target_degree = 1 + rng.index(4);
    314             while peers[index].len() < target_degree {
    315                 let peer = rng.index(nodes);
    316                 if peer != index {
    317                     peers[index].insert(peer);
    318                     peers[peer].insert(index);
    319                 }
    320             }
    321         }
    322         peers
    323             .into_iter()
    324             .map(|set| set.into_iter().take(4).collect())
    325             .collect()
    326     }
    327 
    328     fn online_chaos_nodes(offline_until: &[usize], round: usize) -> BTreeSet<usize> {
    329         offline_until
    330             .iter()
    331             .enumerate()
    332             .filter_map(|(index, until)| (*until <= round).then_some(index))
    333             .collect()
    334     }
    335 
    336     fn chaos_timestamp(round: usize) -> u64 {
    337         (round as u64 + 1) * (RECOVERY_BLOCK_DELAY_MS + VDF_TARGET_BLOCK_MS)
    338     }
    339 
    340     fn attempt_bounded_mine_action(
    341         network: &mut InMemoryNetwork,
    342         node_id: &str,
    343         recipient: &str,
    344         rng: &mut ChaosRng,
    345     ) {
    346         let outcome = network
    347             .node(node_id)
    348             .expect("mine actor exists")
    349             .ledger()
    350             .search_mine(recipient, rng.next_u64(), 0, 4)
    351             .expect("bounded mine search is valid");
    352         let Some(transaction) = outcome.transaction else {
    353             return;
    354         };
    355         let _ = network
    356             .node_mut(node_id)
    357             .expect("mine actor exists")
    358             .receive_mine_action(transaction);
    359     }
    360 
    361     fn mine_one_sparse_chaos_block(
    362         network: &mut InMemoryNetwork,
    363         node_ids: &[String],
    364         wallets: &[Wallet],
    365         online: &BTreeSet<usize>,
    366         timestamp_ms: u64,
    367     ) {
    368         let Some(reference_index) = highest_online_node(network, node_ids, online) else {
    369             return;
    370         };
    371         let reference = network
    372             .node(&node_ids[reference_index])
    373             .expect("reference node exists");
    374         let leader = reference.ledger().expected_leader_for_next_block();
    375         let mut candidates = Vec::new();
    376         if let Some(index) = leader
    377             .as_deref()
    378             .and_then(|leader| {
    379                 wallets
    380                     .iter()
    381                     .position(|wallet| wallet.address() == leader)
    382                     .filter(|index| online.contains(index))
    383             })
    384             .filter(|index| {
    385                 network.node(&node_ids[*index]).unwrap().chain_height() == reference.chain_height()
    386             })
    387         {
    388             candidates.push(index);
    389         }
    390         candidates.push(reference_index);
    391         candidates.extend(online.iter().copied().filter(|index| {
    392             network.node(&node_ids[*index]).unwrap().chain_height() == reference.chain_height()
    393         }));
    394         let mut seen = BTreeSet::new();
    395         for producer_index in candidates {
    396             if !seen.insert(producer_index) {
    397                 continue;
    398             }
    399             let mut producer = network
    400                 .node(&node_ids[producer_index])
    401                 .expect("producer node exists")
    402                 .clone();
    403             let mut publish_timestamp_ms = timestamp_ms;
    404             let mut plan = producer.prepare_automatic_finalization(timestamp_ms);
    405             if plan.work.is_none()
    406                 && plan
    407                     .skipped_reason
    408                     .as_deref()
    409                     .is_some_and(|reason| reason.contains("collecting blinded reveals"))
    410             {
    411                 publish_timestamp_ms = timestamp_ms.saturating_add(REVEAL_BUNDLE_COLLECTION_MS + 1);
    412                 plan = producer.prepare_automatic_finalization(publish_timestamp_ms);
    413             }
    414             let Some(work) = plan.work else {
    415                 continue;
    416             };
    417             let block = work.finish_at(
    418                 &wallets[producer_index],
    419                 "preverified-chaos-vdf".to_string(),
    420                 publish_timestamp_ms,
    421             );
    422             network
    423                 .node_mut(&node_ids[producer_index])
    424                 .expect("producer node exists")
    425                 .receive_preverified_block_at(block, publish_timestamp_ms)
    426                 .expect("mock-VDF block applies locally");
    427             return;
    428         }
    429     }
    430 
    431     fn highest_online_node(
    432         network: &InMemoryNetwork,
    433         node_ids: &[String],
    434         online: &BTreeSet<usize>,
    435     ) -> Option<usize> {
    436         online.iter().copied().max_by_key(|index| {
    437             network
    438                 .node(&node_ids[*index])
    439                 .expect("online node exists")
    440                 .chain_height()
    441         })
    442     }
    443 
    444     fn deliver_sparse_chaos_until_idle(
    445         network: &mut InMemoryNetwork,
    446         node_ids: &[String],
    447         peers: &[Vec<usize>],
    448         online: &BTreeSet<usize>,
    449         timestamp_ms: u64,
    450         rng: &mut ChaosRng,
    451     ) {
    452         for _ in 0..512 {
    453             let mut progressed =
    454                 sync_sparse_chaos_once(network, node_ids, peers, online, timestamp_ms);
    455             progressed |=
    456                 deliver_sparse_chaos_once(network, node_ids, peers, online, timestamp_ms, rng);
    457             if !progressed {
    458                 return;
    459             }
    460         }
    461         panic!("sparse chaos network did not become idle");
    462     }
    463 
    464     fn sync_sparse_chaos_once(
    465         network: &mut InMemoryNetwork,
    466         node_ids: &[String],
    467         peers: &[Vec<usize>],
    468         online: &BTreeSet<usize>,
    469         timestamp_ms: u64,
    470     ) -> bool {
    471         let mut syncs = Vec::new();
    472         for from in online {
    473             let from_height = network.node(&node_ids[*from]).unwrap().chain_height();
    474             for to in &peers[*from] {
    475                 if !online.contains(to) {
    476                     continue;
    477                 }
    478                 let to_height = network.node(&node_ids[*to]).unwrap().chain_height();
    479                 if from_height > to_height {
    480                     let blocks = network
    481                         .node(&node_ids[*from])
    482                         .unwrap()
    483                         .blocks_from(to_height + 1, 16);
    484                     syncs.push((*to, blocks));
    485                 }
    486             }
    487         }
    488 
    489         let mut progressed = false;
    490         for (to, blocks) in syncs {
    491             for block in blocks {
    492                 let before = network.node(&node_ids[to]).unwrap().chain_height();
    493                 receive_sparse_chaos_block(network, &node_ids[to], block, timestamp_ms);
    494                 progressed |= network.node(&node_ids[to]).unwrap().chain_height() > before;
    495             }
    496         }
    497         progressed
    498     }
    499 
    500     fn deliver_sparse_chaos_once(
    501         network: &mut InMemoryNetwork,
    502         node_ids: &[String],
    503         peers: &[Vec<usize>],
    504         online: &BTreeSet<usize>,
    505         timestamp_ms: u64,
    506         rng: &mut ChaosRng,
    507     ) -> bool {
    508         let mut outbound = Vec::new();
    509         for from in online {
    510             let node = network
    511                 .node_mut(&node_ids[*from])
    512                 .expect("online node exists");
    513             outbound.extend(
    514                 node.drain_outbox()
    515                     .into_iter()
    516                     .map(|envelope| (*from, envelope)),
    517             );
    518         }
    519         if outbound.is_empty() {
    520             return false;
    521         }
    522 
    523         while !outbound.is_empty() {
    524             let index = rng.index(outbound.len());
    525             let (from, envelope) = outbound.swap_remove(index);
    526             for to in &peers[from] {
    527                 if online.contains(to) {
    528                     receive_sparse_chaos_envelope(
    529                         network,
    530                         &node_ids[*to],
    531                         envelope.clone(),
    532                         timestamp_ms,
    533                     );
    534                 }
    535             }
    536         }
    537         true
    538     }
    539 
    540     fn receive_sparse_chaos_envelope(
    541         network: &mut InMemoryNetwork,
    542         node_id: &str,
    543         envelope: GossipEnvelope,
    544         timestamp_ms: u64,
    545     ) {
    546         match envelope {
    547             GossipEnvelope::Block(block) => {
    548                 receive_sparse_chaos_block(network, node_id, block, timestamp_ms);
    549             }
    550             other => {
    551                 if let Err(error) = network
    552                     .node_mut(node_id)
    553                     .expect("node exists")
    554                     .receive(other)
    555                 {
    556                     let message = error.to_string();
    557                     assert!(
    558                         message.contains("mine transaction anchor is not on this chain")
    559                             || message.contains("conflicts with an existing pending transaction")
    560                             || message.contains("blinded transaction expired"),
    561                         "unexpected sparse chaos delivery error: {message}"
    562                     );
    563                 }
    564             }
    565         }
    566     }
    567 
    568     fn receive_sparse_chaos_block(
    569         network: &mut InMemoryNetwork,
    570         node_id: &str,
    571         block: Block,
    572         timestamp_ms: u64,
    573     ) {
    574         if let Err(error) = network
    575             .node_mut(node_id)
    576             .expect("node exists")
    577             .receive_preverified_block_at(block, timestamp_ms)
    578         {
    579             let message = error.to_string();
    580             assert!(
    581                 message.contains("expected block height")
    582                     || message.contains("same-height fork")
    583                     || message.contains("block is already known"),
    584                 "unexpected sparse chaos block error: {message}"
    585             );
    586         }
    587     }
    588 
    589     fn observe_bounded_pending(
    590         network: &InMemoryNetwork,
    591         node_ids: &[String],
    592         pending_since: &mut BTreeMap<String, u64>,
    593         settle_blocks: u64,
    594     ) {
    595         let mut current = BTreeSet::new();
    596         for node_id in node_ids {
    597             let node = network.node(node_id).expect("node exists");
    598             for id in pending_item_ids(node) {
    599                 current.insert(id);
    600             }
    601         }
    602         let max_height = node_ids
    603             .iter()
    604             .map(|id| network.node(id).unwrap().chain_height())
    605             .max()
    606             .unwrap_or_default();
    607         pending_since.retain(|id, _| current.contains(id));
    608         for id in current {
    609             pending_since.entry(id).or_insert(max_height);
    610         }
    611         for (id, first_height) in pending_since {
    612             assert!(
    613                 max_height.saturating_sub(*first_height) <= settle_blocks,
    614                 "pending item {id} stayed in mempool for more than {settle_blocks} blocks"
    615             );
    616         }
    617     }
    618 
    619     fn pending_item_ids(node: &NodeCore) -> Vec<String> {
    620         let mut ids = Vec::new();
    621         ids.extend(
    622             node.pending_transactions()
    623                 .into_iter()
    624                 .map(|tx| format!("tx:{}", tx.signature())),
    625         );
    626         ids.extend(
    627             node.pending_blinded_transactions()
    628                 .into_iter()
    629                 .map(|tx| format!("commit:{}@{}", tx.commitment, tx.expires_at_height)),
    630         );
    631         ids.extend(
    632             node.pending_blinded_reveals()
    633                 .into_iter()
    634                 .map(|reveal| format!("reveal:{}", reveal.commitment)),
    635         );
    636         ids
    637     }
    638 
    639     fn assert_sparse_chaos_converged(network: &InMemoryNetwork, node_ids: &[String]) {
    640         let first = network.node(&node_ids[0]).expect("first node exists");
    641         let height = first.chain_height();
    642         let tip = first.ledger().status().tip_hash.clone();
    643         for node_id in node_ids.iter().skip(1) {
    644             let node = network.node(node_id).expect("node exists");
    645             assert_eq!(node.chain_height(), height, "{node_id} height diverged");
    646             assert_eq!(
    647                 node.ledger().status().tip_hash,
    648                 tip,
    649                 "{node_id} tip diverged"
    650             );
    651         }
    652     }
    653 
    654     fn sparse_chaos_has_pending(network: &InMemoryNetwork, node_ids: &[String]) -> bool {
    655         node_ids.iter().any(|node_id| {
    656             let node = network.node(node_id).expect("node exists");
    657             !node.pending_transactions().is_empty()
    658                 || !node.pending_blinded_transactions().is_empty()
    659                 || !node.pending_blinded_reveals().is_empty()
    660         })
    661     }
    662 
    663     fn assert_sparse_chaos_mempools_empty(network: &InMemoryNetwork, node_ids: &[String]) {
    664         for node_id in node_ids {
    665             let node = network.node(node_id).expect("node exists");
    666             let pending = pending_item_ids(node);
    667             assert!(
    668                 pending.is_empty(),
    669                 "{node_id} at height {} still has pending mempool items: {pending:?}",
    670                 node.chain_height()
    671             );
    672         }
    673     }
    674 }