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 }