diff --git a/stackslib/src/net/tests/inv/epoch2x.rs b/stackslib/src/net/tests/inv/epoch2x.rs index 4f0072ba0..e378d0920 100644 --- a/stackslib/src/net/tests/inv/epoch2x.rs +++ b/stackslib/src/net/tests/inv/epoch2x.rs @@ -633,7 +633,7 @@ fn test_sync_inv_set_blocks_microblocks_available() { let nk = peer_1.to_neighbor().addr; let sortdb = peer_1.sortdb.take().unwrap(); - peer_1.network.init_inv_sync(&sortdb); + peer_1.network.init_inv_sync_epoch2x(&sortdb); match peer_1.network.inv_state { Some(ref mut inv) => { inv.add_peer(nk.clone(), true); diff --git a/stackslib/src/net/tests/inv/nakamoto.rs b/stackslib/src/net/tests/inv/nakamoto.rs index accc174e8..526e302c6 100644 --- a/stackslib/src/net/tests/inv/nakamoto.rs +++ b/stackslib/src/net/tests/inv/nakamoto.rs @@ -23,7 +23,9 @@ use std::thread::JoinHandle; use stacks_common::address::{AddressHashMode, C32_ADDRESS_VERSION_TESTNET_SINGLESIG}; use stacks_common::codec::{read_next, StacksMessageCodec}; use stacks_common::types::chainstate::{StacksAddress, StacksPrivateKey, StacksPublicKey}; +use stacks_common::types::net::PeerAddress; use stacks_common::types::StacksEpoch; +use stacks_common::util::hash::Hash160; use crate::chainstate::burn::db::sortdb::SortitionDB; use crate::chainstate::burn::ConsensusHash; @@ -40,12 +42,13 @@ use crate::chainstate::stacks::{ }; use crate::clarity::vm::types::StacksAddressExtensions; use crate::core::StacksEpochExtension; -use crate::net::inv::nakamoto::InvGenerator; +use crate::net::inv::nakamoto::{InvGenerator, NakamotoInvStateMachine, NakamotoTenureInv}; +use crate::net::neighbors::comms::NeighborComms; use crate::net::test::{TestEventObserver, TestPeer}; use crate::net::tests::{NakamotoBootPlan, NakamotoBootStep, NakamotoBootTenure}; use crate::net::{ - Error as NetError, GetNakamotoInvData, HandshakeData, NakamotoInvData, StacksMessage, - StacksMessageType, + Error as NetError, GetNakamotoInvData, HandshakeData, NakamotoInvData, NeighborAddress, + PeerNetworkComms, StacksMessage, StacksMessageType, }; use crate::stacks_common::types::Address; use crate::util_lib::db::Error as DBError; @@ -311,13 +314,16 @@ fn test_nakamoto_inv_10_extended_tenures_10_sortitions() { } } -fn make_nakamoto_peer_from_invs<'a>( +/// NOTE: The second return value does _not_ need `<'a>`, since `observer` is never installed into +/// the peers here. However, it appears unavoidable to the borrow-checker. +fn make_nakamoto_peers_from_invs<'a>( test_name: &str, observer: &'a TestEventObserver, rc_len: u32, prepare_len: u32, bitvecs: Vec>, -) -> TestPeer<'a> { + num_peers: usize, +) -> (TestPeer<'a>, Vec>) { for bitvec in bitvecs.iter() { assert_eq!(bitvec.len() as u32, rc_len); } @@ -392,10 +398,21 @@ fn make_nakamoto_peer_from_invs<'a>( let plan = NakamotoBootPlan::new(test_name) .with_private_key(private_key) .with_pox_constants(rc_len, prepare_len) - .with_initial_balances(vec![(addr.into(), 1_000_000)]); + .with_initial_balances(vec![(addr.into(), 1_000_000)]) + .with_extra_peers(num_peers); - let peer = plan.boot_into_nakamoto_peer(boot_tenures, Some(observer)); - peer + let (peer, other_peers) = plan.boot_into_nakamoto_peers(boot_tenures, Some(observer)); + (peer, other_peers) +} + +fn make_nakamoto_peer_from_invs<'a>( + test_name: &str, + observer: &'a TestEventObserver, + rc_len: u32, + prepare_len: u32, + bitvecs: Vec>, +) -> TestPeer<'a> { + make_nakamoto_peers_from_invs(test_name, observer, rc_len, prepare_len, bitvecs, 0).0 } fn check_inv_messages( @@ -434,6 +451,50 @@ fn check_inv_messages( } } +fn check_inv_state( + bitvecs: Vec>, + rc_len: u32, + nakamoto_start_burn_height: u64, + inv_state: &NakamotoTenureInv, +) { + for (i, (tenure_rc, tenure_inv)) in inv_state.tenures_inv.iter().enumerate() { + for bit in 0..(rc_len as usize) { + let msg_bit = if bit / 8 >= tenure_inv.len() { + // only allowed at the end + debug!( + "bit = {}, tenure_rc = {}, tenure_inv = {:?}", + bit, tenure_rc, &tenure_inv + ); + assert_eq!(i, inv_state.tenures_inv.len() - 1); + false + } else { + tenure_inv[bit / 8] & (1 << (bit % 8)) != 0 + }; + + let burn_block_height = (*tenure_rc as u64) * u64::from(rc_len) + (bit as u64); + if burn_block_height < nakamoto_start_burn_height { + // inv doesn't cover epoch 2 + assert!( + !msg_bit, + "Bit {} in tenure {} is set but is before nakamoto-start height {} ({})", + bit, tenure_rc, nakamoto_start_burn_height, burn_block_height + ); + continue; + } + + let inv_offset: u64 = burn_block_height - nakamoto_start_burn_height; + let bitvec_idx = (inv_offset / u64::from(rc_len)) as usize; + let expected_bit = if bitvec_idx >= bitvecs.len() { + false + } else { + bitvecs[bitvec_idx][(inv_offset % u64::from(rc_len)) as usize] + }; + assert_eq!(msg_bit, expected_bit, "Bit {} in tenure {} is {}, but expected {}. burn_block_height = {}, inv_offset = {}, bitvec_idx = {}, nakamoto_start_burn_height = {}", + bit, tenure_rc, msg_bit, expected_bit, burn_block_height, inv_offset, bitvec_idx, nakamoto_start_burn_height); + } + } +} + #[test] fn test_nakamoto_invs_full() { let observer = TestEventObserver::new(); @@ -566,22 +627,39 @@ fn test_nakamoto_invs_different_anchor_blocks() { #[test] fn test_nakamoto_tenure_inv() { - let mut nakamoto_inv = NakamotoTenureInv::new(100, 100); + let na = NeighborAddress { + addrbytes: PeerAddress([ + 0xff, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, + 0x0e, 0x0f, + ]), + port: 65535, + public_key_hash: Hash160([0x11; 20]), + }; + let mut nakamoto_inv = NakamotoTenureInv::new(100, 100, na); assert!(!nakamoto_inv.has_ith_tenure(0)); assert!(!nakamoto_inv.has_ith_tenure(99)); assert!(!nakamoto_inv.has_ith_tenure(100)); - assert_eq!(nakamoto_inv.num_reward_cycles(), 0); + assert_eq!(nakamoto_inv.highest_reward_cycle(), 0); - let full_tenure = NakamotoInvData::bools_to_bitvec(vec![true; 100]); - nakamoto_inv.merge_tenure_inv(full_tenure, 100, 1); + let full_tenure = NakamotoInvData::bools_to_bitvec(&[true; 100]); + let learned = nakamoto_inv.merge_tenure_inv(full_tenure.clone(), 100, 1); + assert!(learned); - for i in 100..200 { + let learned = nakamoto_inv.merge_tenure_inv(full_tenure, 100, 1); + assert!(!learned); + + debug!("nakamoto_inv = {:?}", &nakamoto_inv); + for i in 0..200 { + assert!(!nakamoto_inv.has_ith_tenure(i)); + } + for i in 200..300 { assert!(nakamoto_inv.has_ith_tenure(i)); } - assert!(!nakamoto_inv.has_ith_tenure(99)); - assert!(!nakamoto_inv.has_ith_tenure(200)); - assert!(!nakamoto_inv.has_ith_tenure(201)); - assert_eq!(nakamoto_inv.num_reward_cycles(), 1); + assert!(!nakamoto_inv.has_ith_tenure(199)); + assert!(nakamoto_inv.has_ith_tenure(200)); + assert!(!nakamoto_inv.has_ith_tenure(300)); + assert!(!nakamoto_inv.has_ith_tenure(301)); + assert_eq!(nakamoto_inv.highest_reward_cycle(), 1); let mut partial_tenure_bools = vec![]; for i in 0..100 { @@ -589,34 +667,43 @@ fn test_nakamoto_tenure_inv() { } // has_ith_tenure() works (non-triial case) - let partial_tenure = NakamotoInvData::bools_to_bitvec(partial_tenure_bools); - nakamoto_inv.merge_tenure_inv(partial_tenure, 100, 2); + let partial_tenure = NakamotoInvData::bools_to_bitvec(&partial_tenure_bools); + let learned = nakamoto_inv.merge_tenure_inv(partial_tenure.clone(), 100, 2); + assert!(learned); - for i in 200..300 { + for i in 300..400 { assert_eq!(nakamoto_inv.has_ith_tenure(i), i % 2 == 0); } - assert!(!nakamoto_inv.has_ith_tenure(99)); - assert!(!nakamoto_inv.has_ith_tenure(300)); - assert!(!nakamoto_inv.has_ith_tenure(301)); - assert_eq!(nakamoto_inv.num_reward_cycles(), 2); + assert!(!nakamoto_inv.has_ith_tenure(199)); + assert!(nakamoto_inv.has_ith_tenure(299)); + assert!(nakamoto_inv.has_ith_tenure(300)); + assert!(nakamoto_inv.has_ith_tenure(398)); + assert!(!nakamoto_inv.has_ith_tenure(399)); + assert!(!nakamoto_inv.has_ith_tenure(400)); + assert_eq!(nakamoto_inv.highest_reward_cycle(), 2); // supports sparse updates - let full_tenure = NakamotoInvData::bools_to_bitvec(vec![true; 100]); - nakamoto_inv.merge_tenure_inv(full_tenure, 100, 4); - - for i in 300..400 { - assert_eq!(!nakamoto_inv.has_ith_tenure(i)); - } - for i in 400..500 { - assert_eq!(!nakamoto_inv.has_ith_tenure(i)); - } - assert_eq!(nakamoto_inv.num_reward_cycles(), 4); - - // can overwrite tenures - let full_tenure = NakamotoInvData::bools_to_bitvec(vec![true; 100]); - nakamoto_inv.merge_tenure_inv(partial_tenure, 100, 2); + let full_tenure = NakamotoInvData::bools_to_bitvec(&[true; 100]); + let learned = nakamoto_inv.merge_tenure_inv(full_tenure, 100, 4); + assert!(learned); - for i in 200..300 { + for i in 400..500 { + assert!(!nakamoto_inv.has_ith_tenure(i)); + } + for i in 500..600 { + assert!(nakamoto_inv.has_ith_tenure(i)); + } + assert_eq!(nakamoto_inv.highest_reward_cycle(), 4); + + // can overwrite tenures + let full_tenure = NakamotoInvData::bools_to_bitvec(&[true; 100]); + let learned = nakamoto_inv.merge_tenure_inv(full_tenure.clone(), 100, 2); + assert!(learned); + + let learned = nakamoto_inv.merge_tenure_inv(full_tenure.clone(), 100, 2); + assert!(!learned); + + for i in 300..400 { assert!(nakamoto_inv.has_ith_tenure(i)); } @@ -628,8 +715,290 @@ fn test_nakamoto_tenure_inv() { nakamoto_inv.next_reward_cycle(); assert_eq!(nakamoto_inv.reward_cycle(), 1); - - nakamoto_inv.try_reset_comms(0, 0); + + nakamoto_inv.try_reset_comms(0, 0, 0); assert_eq!(nakamoto_inv.reward_cycle(), 0); assert!(nakamoto_inv.is_online()); } + +#[test] +fn test_nakamoto_inv_sync_state_machine() { + let observer = TestEventObserver::new(); + let bitvecs = vec![ + // full rc + vec![true, true, true, true, true, true, true, true, true, true], + // sparse rc + vec![ + true, false, false, false, false, false, false, true, true, true, + ], + // atlernating rc + vec![ + false, true, false, true, false, true, false, true, true, true, + ], + // sparse rc + vec![ + false, false, false, false, false, false, true, true, true, true, + ], + // full rc + vec![true, true, true, true, true, true, true, true, true, true], + ]; + + // boot two peers, and cannibalize the second one for its network and sortdb so we can use them + // to directly drive a state machine. + let (mut peer, mut other_peers) = + make_nakamoto_peers_from_invs(function_name!(), &observer, 10, 3, bitvecs.clone(), 1); + let mut other_peer = other_peers.pop().unwrap(); + + let nakamoto_start = + NakamotoBootPlan::nakamoto_first_tenure_height(&peer.config.burnchain.pox_constants); + + let tip = { + let sort_db = peer.sortdb.as_mut().unwrap(); + SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap() + }; + let total_rcs = peer + .config + .burnchain + .block_height_to_reward_cycle(tip.block_height) + .unwrap() + + 1; + + // run peer and other_peer until they connect + loop { + let _ = peer.step_with_ibd(false); + let _ = other_peer.step_with_ibd(false); + + let event_ids: Vec = peer + .network + .iter_peer_event_ids() + .map(|e_id| *e_id) + .collect(); + let other_event_ids: Vec = other_peer + .network + .iter_peer_event_ids() + .map(|e_id| *e_id) + .collect(); + + if event_ids.len() > 0 && other_event_ids.len() > 0 { + break; + } + } + + debug!("Peers are connected"); + let peer_addr = NeighborAddress::from_neighbor(&peer.to_neighbor()); + + let (sx, rx) = sync_channel(1); + let mut inv_machine = NakamotoInvStateMachine::new(PeerNetworkComms::new()); + + // ::scope is necessary because Rust is forced to think that `other_peers` has the same lifetime + // as `observer`, which prohibits running a bare thread in which `other_peers` outlives + // `observer` + std::thread::scope(|s| { + s.spawn(|| { + let sortdb = other_peer.sortdb.take().unwrap(); + inv_machine + .process_getnakamotoinv_begins(&mut other_peer.network, &sortdb, false) + .unwrap(); + other_peer.sortdb = Some(sortdb); + + let mut last_learned_rc = 0; + loop { + let _ = other_peer.step_with_ibd(false); + let ev_ids: Vec<_> = other_peer.network.iter_peer_event_ids().collect(); + if ev_ids.len() == 0 { + // disconnected + panic!("Disconnected"); + } + + let (num_msgs, learned) = inv_machine + .process_getnakamotoinv_finishes(&mut other_peer.network) + .unwrap(); + + for (_, inv) in inv_machine.inventories.iter() { + debug!( + "inv is at rc {}, last learned rc is {}, total rcs = {}", + inv.reward_cycle(), + last_learned_rc, + total_rcs + ); + last_learned_rc = last_learned_rc.max(inv.reward_cycle()); + } + + if last_learned_rc >= total_rcs { + break; + } + + let sortdb = other_peer.sortdb.take().unwrap(); + inv_machine + .process_getnakamotoinv_begins(&mut other_peer.network, &sortdb, false) + .unwrap(); + other_peer.sortdb = Some(sortdb); + } + + sx.send(true).unwrap(); + }); + + loop { + let _ = peer.step_with_ibd(false); + if rx.try_recv().is_ok() { + break; + } + } + }); + + // inv_machine learned everything + for (_, inv) in inv_machine.inventories.iter() { + debug!("Check inv state: {:?}", inv); + check_inv_state(bitvecs.clone(), 10, nakamoto_start, inv); + } +} + +#[test] +fn test_nakamoto_inv_sync_across_epoch_change() { + let observer = TestEventObserver::new(); + let bitvecs = vec![ + // full rc + vec![true, true, true, true, true, true, true, true, true, true], + // sparse rc + vec![ + true, false, false, false, false, false, false, true, true, true, + ], + // atlernating rc + vec![ + false, true, false, true, false, true, false, true, true, true, + ], + // sparse rc + vec![ + false, false, false, false, false, false, true, true, true, true, + ], + // full rc + vec![true, true, true, true, true, true, true, true, true, true], + ]; + + // boot two peers, and cannibalize the second one for its network and sortdb so we can use them + // to directly drive a state machine. + let (mut peer, mut other_peers) = + make_nakamoto_peers_from_invs(function_name!(), &observer, 10, 3, bitvecs.clone(), 1); + let mut other_peer = other_peers.pop().unwrap(); + + let nakamoto_start = + NakamotoBootPlan::nakamoto_first_tenure_height(&peer.config.burnchain.pox_constants); + + let tip = { + let sort_db = peer.sortdb.as_mut().unwrap(); + SortitionDB::get_canonical_burn_chain_tip(sort_db.conn()).unwrap() + }; + let total_rcs = peer + .config + .burnchain + .block_height_to_reward_cycle(tip.block_height) + .unwrap(); + + // run peer and other_peer until they connect + loop { + let _ = peer.step_with_ibd(false); + let _ = other_peer.step_with_ibd(false); + + let event_ids: Vec = peer + .network + .iter_peer_event_ids() + .map(|e_id| *e_id) + .collect(); + let other_event_ids: Vec = other_peer + .network + .iter_peer_event_ids() + .map(|e_id| *e_id) + .collect(); + + if event_ids.len() > 0 && other_event_ids.len() > 0 { + break; + } + } + + debug!("Peers are connected"); + + // force peers to sync their epoch 2.x inventories + let old_burn_chain_tip = peer.network.burnchain_tip.block_height; + let num_epoch2_blocks = nakamoto_start - 26; // TestPeer starts making blocks at sortition 26 + + // check epoch 2.x state machine + let mut round = 0; + let mut inv_1_count = 0; + let mut inv_2_count = 0; + let mut highest_rc_1 = 0; + let mut highest_rc_2 = 0; + + let burn_tip_start = peer.network.get_current_epoch().start_height; + + while inv_1_count < num_epoch2_blocks + || inv_2_count < num_epoch2_blocks + || highest_rc_1 < total_rcs + || highest_rc_2 < total_rcs + { + // trick the work loop into thinking that the current chain view is this + peer.network.connection_opts.force_nakamoto_epoch_transition = true; + other_peer + .network + .connection_opts + .force_nakamoto_epoch_transition = true; + + let _ = peer.step_with_ibd(false); + let _ = other_peer.step_with_ibd(false); + + inv_1_count = peer + .network + .inv_state + .as_ref() + .map(|inv| inv.get_inv_num_blocks(&other_peer.to_neighbor().addr)) + .unwrap_or(0); + inv_2_count = other_peer + .network + .inv_state + .as_ref() + .map(|inv| inv.get_inv_num_blocks(&peer.to_neighbor().addr)) + .unwrap_or(0); + + highest_rc_1 = peer + .network + .inv_state_nakamoto + .as_ref() + .map(|inv| inv.highest_reward_cycle()) + .unwrap_or(0); + highest_rc_2 = other_peer + .network + .inv_state_nakamoto + .as_ref() + .map(|inv| inv.highest_reward_cycle()) + .unwrap_or(0); + + // nothing should break + match peer.network.inv_state { + Some(ref inv) => { + assert_eq!(inv.get_broken_peers().len(), 0); + assert_eq!(inv.get_dead_peers().len(), 0); + assert_eq!(inv.get_diverged_peers().len(), 0); + } + None => {} + } + + match other_peer.network.inv_state { + Some(ref inv) => { + assert_eq!(inv.get_broken_peers().len(), 0); + assert_eq!(inv.get_dead_peers().len(), 0); + assert_eq!(inv.get_diverged_peers().len(), 0); + } + None => {} + } + + round += 1; + + info!( + "Epoch 2.x state machine: Peer 1: {}, Peer 2: {} (total {})", + inv_1_count, inv_2_count, num_epoch2_blocks + ); + info!( + "Nakamoto state machine: Peer 1: {}, Peer 2: {} (total {})", + highest_rc_1, highest_rc_2, total_rcs + ); + } +}