iuna

iuna

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

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 }