gossip.rs (12022B)
1 use anyhow::Result; 2 3 use crate::domain::{Block, ChainSnapshot, hex_encode}; 4 5 use super::{ 6 BLOCK_REQUEST_LIMIT, ChainBootstrap, GossipEnvelope, NETWORK_ID, NodeCore, PROTOCOL_VERSION, 7 ProtocolHello, TRANSACTION_BATCH_LIMIT, now_ms, protocol_capabilities, types::BlockInventory, 8 }; 9 10 impl NodeCore { 11 pub fn pending_transaction_v2_envelopes(&self) -> Result<Vec<(String, String)>> { 12 let domain = self.ledger.transaction_v2_domain()?; 13 self.ledger 14 .pending_v2() 15 .iter() 16 .map(|transaction| { 17 Ok(( 18 hex_encode(transaction.transaction_id(&domain)?), 19 hex_encode(transaction.encode(&domain)?), 20 )) 21 }) 22 .collect() 23 } 24 25 pub fn mempool_gossip(&mut self) -> Vec<GossipEnvelope> { 26 let mut gossip = Vec::new(); 27 gossip.extend( 28 self.ledger 29 .pending() 30 .chunks(TRANSACTION_BATCH_LIMIT) 31 .map(|chunk| GossipEnvelope::Transactions { 32 transactions: chunk.to_vec(), 33 }), 34 ); 35 if let Ok(pending_v2) = self.pending_transaction_v2_envelopes() { 36 // A single v2 transaction may approach the block byte limit after hex encoding. 37 // Keep each envelope independently wire-bounded instead of building an oversized 38 // JSON batch from several otherwise valid transactions. 39 gossip.extend( 40 pending_v2 41 .into_iter() 42 .map(|(_, envelope)| envelope) 43 .map(|envelope| GossipEnvelope::TransactionV2 { envelope }), 44 ); 45 } 46 gossip.extend( 47 self.usable_burn_bundles() 48 .chunks(TRANSACTION_BATCH_LIMIT) 49 .map(|chunk| GossipEnvelope::BurnBundles { 50 bundles: chunk.to_vec(), 51 }), 52 ); 53 gossip 54 } 55 56 pub fn chain_snapshot(&self) -> ChainSnapshot { 57 self.ledger.snapshot() 58 } 59 60 pub fn chain_bootstrap(&self) -> ChainBootstrap { 61 let snapshot = self.ledger.genesis_snapshot(); 62 ChainBootstrap { 63 genesis_allocations: snapshot.genesis_allocations, 64 vdf_rounds: snapshot.vdf_rounds, 65 launch_profile: snapshot.launch_profile, 66 genesis_block: snapshot.blocks[0].clone(), 67 height: self.ledger.height(), 68 tip_hash: self.ledger.tip_hash().to_string(), 69 } 70 } 71 72 pub fn block_locator(&self) -> Vec<String> { 73 self.ledger.block_locator() 74 } 75 76 pub fn blocks_after_locator(&self, locator: &[String], limit: usize) -> Vec<Block> { 77 self.ledger.blocks_after_locator(locator, limit) 78 } 79 80 pub fn hello(&self, listen_addr: Option<String>, node_id: Option<String>) -> GossipEnvelope { 81 GossipEnvelope::Hello(ProtocolHello { 82 protocol_version: PROTOCOL_VERSION, 83 capabilities: protocol_capabilities(), 84 network_id: NETWORK_ID.to_string(), 85 genesis_hash: self.ledger.genesis_hash().to_string(), 86 listen_addr, 87 node_id, 88 height: self.ledger.height(), 89 tip_hash: self.ledger.tip_hash().to_string(), 90 time_ms: now_ms(), 91 }) 92 } 93 94 pub fn peer_status(&self) -> GossipEnvelope { 95 GossipEnvelope::PeerStatus { 96 height: self.ledger.height(), 97 tip_hash: self.ledger.tip_hash().to_string(), 98 time_ms: now_ms(), 99 } 100 } 101 102 pub fn blocks_from(&self, from_height: u64, limit: usize) -> Vec<Block> { 103 self.ledger.blocks_from(from_height, limit) 104 } 105 106 pub fn blocks_by_hash(&self, hashes: &[String]) -> Vec<Block> { 107 hashes 108 .iter() 109 .filter_map(|hash| self.ledger.block_by_hash(hash)) 110 .collect() 111 } 112 113 pub fn missing_inventory_request(&self, blocks: &[BlockInventory]) -> Option<GossipEnvelope> { 114 let local_height = self.ledger.height(); 115 let first_height_gap = blocks 116 .iter() 117 .filter(|block| !self.ledger.has_block(&block.hash)) 118 .filter(|block| block.height > local_height + 1) 119 .map(|block| block.height) 120 .min(); 121 let missing_blocks = blocks 122 .iter() 123 .filter(|block| !self.ledger.has_block(&block.hash)) 124 .filter(|block| first_height_gap.is_none_or(|gap| block.height < gap)) 125 .map(|block| block.hash.clone()) 126 .collect::<Vec<_>>(); 127 128 if first_height_gap.is_some() { 129 return Some(GossipEnvelope::BlockRangeRequest { 130 from_height: local_height + 1, 131 limit: BLOCK_REQUEST_LIMIT, 132 }); 133 } 134 135 (!missing_blocks.is_empty()).then_some(GossipEnvelope::BlockRequest { 136 hashes: missing_blocks, 137 }) 138 } 139 } 140 141 #[cfg(test)] 142 mod tests { 143 use std::collections::BTreeMap; 144 145 use crate::{ 146 app::{ 147 BLOCK_REQUEST_LIMIT, BlockInventory, GossipEnvelope, NodeCore, protocol_capabilities, 148 }, 149 domain::{Amount, GenesisBurn, Ledger, MICRO_IUNA, Transaction, Wallet}, 150 }; 151 152 #[test] 153 fn hello_advertises_current_protocol_capabilities() { 154 let wallet = Wallet::from_seed("hello-capabilities"); 155 let node = NodeCore::from_ledger(wallet, Ledger::new(BTreeMap::new(), 1), 0); 156 157 let GossipEnvelope::Hello(hello) = node.hello(None, None) else { 158 panic!("hello builder returned another envelope type"); 159 }; 160 161 assert_eq!(hello.capabilities, protocol_capabilities()); 162 } 163 164 #[test] 165 fn mempool_gossip_rebroadcasts_public_burns() { 166 let wallet = Wallet::from_seed("mempool-gossip-public-burn"); 167 let mut allocations = BTreeMap::new(); 168 allocations.insert(wallet.address().to_string(), 10 * MICRO_IUNA); 169 let ledger = Ledger::new_with_genesis_burns( 170 allocations, 171 vec![GenesisBurn::new(wallet.address(), MICRO_IUNA)], 172 1, 173 ) 174 .unwrap(); 175 let mut node = NodeCore::from_ledger(wallet, ledger, 0); 176 let burn = node.burn_with_fee(MICRO_IUNA / 10, 1).unwrap(); 177 node.drain_outbox(); 178 179 let gossip = node.mempool_gossip(); 180 181 assert!(gossip.iter().any(|envelope| { 182 match envelope { 183 GossipEnvelope::Transactions { transactions } => transactions 184 .iter() 185 .any(|tx| tx.signature() == burn.signature()), 186 _ => false, 187 } 188 })); 189 } 190 191 #[test] 192 fn mempool_gossip_rebroadcasts_public_transfers() { 193 let alice = Wallet::from_seed("mempool-gossip-transfer-alice"); 194 let bob = Wallet::from_seed("mempool-gossip-transfer-bob"); 195 let ledger = Ledger::new( 196 BTreeMap::from([(alice.address().to_string(), 10 * MICRO_IUNA)]), 197 1, 198 ); 199 let mut node = NodeCore::from_ledger(alice, ledger, 0); 200 let transfer = node 201 .transfer_with_fee(bob.address(), MICRO_IUNA, 1) 202 .unwrap(); 203 node.drain_outbox(); 204 205 assert!(node.mempool_gossip().iter().any(|envelope| { 206 match envelope { 207 GossipEnvelope::Transactions { transactions } => transactions 208 .iter() 209 .any(|transaction| transaction.signature() == transfer.signature()), 210 _ => false, 211 } 212 })); 213 } 214 215 #[test] 216 fn mempool_gossip_includes_public_mine_actions() { 217 let alice = Wallet::from_seed("mine-gossip-alice"); 218 let ledger = Ledger::new(BTreeMap::new(), 1); 219 let mine = ledger.build_mine(alice.address()).unwrap(); 220 let mut sender = NodeCore::from_ledger(alice.clone(), ledger.clone(), 0); 221 let mut receiver = NodeCore::from_ledger(alice, ledger, 0); 222 223 sender.submit_public_mine_action(mine.clone()).unwrap(); 224 for envelope in sender.mempool_gossip() { 225 receiver.receive(envelope).unwrap(); 226 } 227 228 assert_eq!(receiver.ledger().pending(), std::slice::from_ref(&mine)); 229 } 230 231 #[test] 232 fn inventory_requests_only_missing_objects() { 233 let alice = Wallet::from_seed("missing-inv-alice"); 234 let bob = Wallet::from_seed("missing-inv-bob"); 235 let allocations = allocations(&[alice.clone(), bob], 1_000); 236 let mut local = node(alice.clone(), allocations.clone()); 237 let mut remote = node(alice.clone(), allocations); 238 queue_plaintext_burn(&mut local, &alice, 1); 239 let block = local.mine_one_at(1).unwrap(); 240 let inventory = [BlockInventory { 241 height: block.height, 242 hash: block.hash.clone(), 243 }]; 244 245 let request = remote.missing_inventory_request(&inventory); 246 assert!(matches!(request, Some(GossipEnvelope::BlockRequest { .. }))); 247 248 remote.receive(GossipEnvelope::Block(block)).unwrap(); 249 assert!(remote.missing_inventory_request(&inventory).is_none()); 250 } 251 252 #[test] 253 fn inventory_gap_requests_range_instead_of_orphan_block() { 254 let alice = Wallet::from_seed("gap-inv-alice"); 255 let bob = Wallet::from_seed("gap-inv-bob"); 256 let allocations = allocations(&[alice.clone(), bob.clone()], 1_000); 257 let mut local = node(alice.clone(), allocations.clone()); 258 let remote = node(bob, allocations); 259 260 let mut latest = None; 261 for height in 1..=3 { 262 queue_plaintext_burn(&mut local, &alice, 1); 263 latest = Some(local.mine_one_at(height).unwrap()); 264 } 265 let latest = latest.unwrap(); 266 267 let request = remote.missing_inventory_request(&[BlockInventory { 268 height: latest.height, 269 hash: latest.hash, 270 }]); 271 272 match request { 273 Some(GossipEnvelope::BlockRangeRequest { from_height, limit }) => { 274 assert_eq!(from_height, 1); 275 assert_eq!(limit, BLOCK_REQUEST_LIMIT); 276 } 277 other => panic!("expected block range request, got {other:?}"), 278 } 279 } 280 281 #[test] 282 fn multi_block_inventory_starts_exactly_one_range_request() { 283 let alice = Wallet::from_seed("multi-inventory-alice"); 284 let bob = Wallet::from_seed("multi-inventory-bob"); 285 let allocations = allocations(&[alice.clone(), bob.clone()], 1_000); 286 let mut source = node(alice.clone(), allocations.clone()); 287 let receiver = node(bob, allocations); 288 for timestamp_ms in [1, 2] { 289 queue_plaintext_burn(&mut source, &alice, 1); 290 source.mine_one_at(timestamp_ms).unwrap(); 291 } 292 let inventory = source 293 .ledger() 294 .blocks_from(1, 2) 295 .into_iter() 296 .map(|block| BlockInventory { 297 height: block.height, 298 hash: block.hash, 299 }) 300 .collect::<Vec<_>>(); 301 302 let request = receiver.missing_inventory_request(&inventory); 303 304 assert!(matches!( 305 request, 306 Some(GossipEnvelope::BlockRangeRequest { 307 from_height: 1, 308 limit: BLOCK_REQUEST_LIMIT 309 }) 310 )); 311 } 312 313 fn node(wallet: Wallet, allocations: BTreeMap<String, Amount>) -> NodeCore { 314 let ledger = Ledger::new_with_genesis_burns( 315 allocations, 316 vec![GenesisBurn::new(wallet.address(), 1)], 317 25, 318 ) 319 .unwrap(); 320 NodeCore::from_ledger(wallet, ledger, 0) 321 } 322 323 fn queue_plaintext_burn(node: &mut NodeCore, wallet: &Wallet, amount: Amount) -> Transaction { 324 let tx = node.ledger().build_burn(wallet, amount, 1).unwrap(); 325 node.receive_transaction(tx.clone()).unwrap(); 326 tx 327 } 328 329 fn allocations(wallets: &[Wallet], amount: Amount) -> BTreeMap<String, Amount> { 330 wallets 331 .iter() 332 .map(|wallet| (wallet.address().to_string(), amount)) 333 .collect() 334 } 335 }