app.rs (11831B)
1 use std::{ 2 collections::{BTreeMap, BTreeSet}, 3 sync::{ 4 Arc, 5 atomic::{AtomicBool, Ordering}, 6 }, 7 time::{SystemTime, UNIX_EPOCH}, 8 }; 9 10 use anyhow::Result; 11 use tokio::sync::Mutex; 12 13 use crate::domain::{ 14 Amount, BurnBundle, Ledger, MINE_ACTIONS_PER_ANCHOR_LIMIT, PreparedBlock, Transaction, run_vdf, 15 }; 16 17 mod automatic_mining; 18 mod consolidation; 19 #[cfg(test)] 20 pub(crate) use consolidation::ConsolidationKind; 21 mod gossip; 22 mod helpers; 23 mod in_memory_network; 24 mod ledger_view; 25 mod node_lifecycle; 26 mod peer_book; 27 mod receive; 28 mod status; 29 mod types; 30 mod wallet; 31 pub use in_memory_network::InMemoryNetwork; 32 pub use peer_book::{PeerBook, PeerDirection, PeerInfo}; 33 pub use types::{ 34 AutoMineOutcome, AutoMinePlan, BlockInventory, ChainBootstrap, ExternalMineJob, FeeEstimate, 35 FundedWalletAddressStatus, GossipEnvelope, LaunchProfileStatus, MiningStatus, 36 NetworkMigrationStatus, NodeConfig, NodeStatus, ProtocolHello, QuantumMigrationPreview, 37 QuantumMigrationStatus, StratumStatus, 38 }; 39 use wallet::NodeWallet; 40 41 pub type SharedNode = Arc<Mutex<NodeCore>>; 42 pub type SharedPeerBook = Arc<Mutex<PeerBook>>; 43 44 pub const DEFAULT_BURN_PER_BLOCK: Amount = 0; 45 pub const DEFAULT_VDF_ROUNDS: u32 = 67_000_000; 46 pub const PROTOCOL_VERSION: u32 = 2; 47 pub const MAX_PROTOCOL_CAPABILITIES: usize = 16; 48 pub const MAX_PROTOCOL_CAPABILITY_BYTES: usize = 64; 49 pub const CAPABILITY_ADDRESS_V1_READ: &str = "address-v1-read"; 50 pub const CAPABILITY_HYBRID_REWARD_PAYOUTS: &str = "hybrid-reward-payouts"; 51 pub const CAPABILITY_SIGNATURE_SCHEMES_V1: &str = "signature-schemes-v1"; 52 pub const CAPABILITY_TRANSACTION_V2_BLOCKS: &str = "transaction-v2-blocks"; 53 pub const CAPABILITY_TRANSACTION_V2_BURNS: &str = "transaction-v2-burns"; 54 pub const CAPABILITY_TRANSACTION_V2_MEMPOOL: &str = "transaction-v2-mempool"; 55 pub const MAINNET_CANDIDATE_NETWORK_ID: &str = "iuna-mainnet-candidate"; 56 pub const MAINNET_CANDIDATE_GENESIS_HASH: &str = 57 "3d677cd7ced1c04d3a276cbee7ea38076e34ac65f18a2c9b8286a4872d986a9a"; 58 pub const MAINNET_NETWORK_ID: &str = "iuna-mainnet-v1"; 59 pub const NETWORK_ID: &str = MAINNET_CANDIDATE_NETWORK_ID; 60 pub const BLOCK_REQUEST_LIMIT: usize = 128; 61 pub const TRANSACTION_BATCH_LIMIT: usize = 128; 62 const IMPORT_REBROADCAST_LIMIT: usize = 128; 63 pub const PEER_MISBEHAVIOR_BAN_SCORE: u32 = 3; 64 pub const PEER_MISBEHAVIOR_BAN_MS: u64 = 10 * 60 * 1_000; 65 pub const PEER_CLOCK_OFFSET_ACCEPTANCE_MS: i64 = 10 * 60 * 1_000; 66 const PEER_CLOCK_OFFSET_STALE_MS: u64 = 20 * 60 * 1_000; 67 const AUTO_POW_NONCE_ATTEMPTS_PER_WORKER_TICK: u64 = 100_000; 68 const AUTO_PLAINTEXT_BURN_BEFORE_RECOVERY_MS: u64 = crate::domain::VDF_TARGET_BLOCK_MS / 10; 69 const BURN_BUNDLE_COLLECTION_MS: u64 = crate::domain::VDF_TARGET_BLOCK_MS / 20; 70 const MIN_AUTO_BLOCK_ANCHOR_BURN_AMOUNT: Amount = 1; 71 static DEBUG_LOGGING: AtomicBool = AtomicBool::new(false); 72 73 pub fn protocol_capabilities() -> Vec<String> { 74 vec![ 75 CAPABILITY_ADDRESS_V1_READ.to_string(), 76 CAPABILITY_HYBRID_REWARD_PAYOUTS.to_string(), 77 CAPABILITY_SIGNATURE_SCHEMES_V1.to_string(), 78 CAPABILITY_TRANSACTION_V2_BLOCKS.to_string(), 79 CAPABILITY_TRANSACTION_V2_BURNS.to_string(), 80 CAPABILITY_TRANSACTION_V2_MEMPOOL.to_string(), 81 ] 82 } 83 84 pub fn validate_protocol_capabilities(capabilities: &[String]) -> Result<()> { 85 if capabilities.len() > MAX_PROTOCOL_CAPABILITIES { 86 anyhow::bail!("peer advertises too many protocol capabilities"); 87 } 88 let mut previous: Option<&str> = None; 89 for capability in capabilities { 90 if capability.is_empty() 91 || capability.len() > MAX_PROTOCOL_CAPABILITY_BYTES 92 || !capability 93 .bytes() 94 .all(|byte| byte.is_ascii_lowercase() || byte.is_ascii_digit() || byte == b'-') 95 { 96 anyhow::bail!("peer advertises an invalid protocol capability"); 97 } 98 if previous.is_some_and(|previous| previous >= capability.as_str()) { 99 anyhow::bail!("peer protocol capabilities must be sorted and unique"); 100 } 101 previous = Some(capability); 102 } 103 Ok(()) 104 } 105 106 pub fn validate_transaction_v2_peer_capability( 107 capabilities: &[String], 108 local_height: u64, 109 remote_height: u64, 110 ) -> Result<()> { 111 let activation_is_next = 112 crate::domain::transaction_v2_is_active(local_height.max(remote_height).saturating_add(1)); 113 if activation_is_next 114 && !capabilities 115 .iter() 116 .any(|capability| capability == CAPABILITY_TRANSACTION_V2_BLOCKS) 117 { 118 anyhow::bail!("peer lacks transaction-v2 block capability near activation"); 119 } 120 if activation_is_next 121 && !capabilities 122 .iter() 123 .any(|capability| capability == CAPABILITY_TRANSACTION_V2_BURNS) 124 { 125 anyhow::bail!("peer lacks transaction-v2 burn capability near activation"); 126 } 127 let hybrid_rewards_are_next = local_height.max(remote_height).saturating_add(1) 128 >= crate::domain::HYBRID_REWARD_ACTIVATION_HEIGHT; 129 if hybrid_rewards_are_next 130 && !capabilities 131 .iter() 132 .any(|capability| capability == CAPABILITY_HYBRID_REWARD_PAYOUTS) 133 { 134 anyhow::bail!("peer lacks hybrid reward payout capability near activation"); 135 } 136 Ok(()) 137 } 138 139 #[cfg(test)] 140 mod tests { 141 use super::{ 142 BLOCK_REQUEST_LIMIT, CAPABILITY_ADDRESS_V1_READ, CAPABILITY_HYBRID_REWARD_PAYOUTS, 143 CAPABILITY_SIGNATURE_SCHEMES_V1, CAPABILITY_TRANSACTION_V2_BLOCKS, 144 CAPABILITY_TRANSACTION_V2_BURNS, CAPABILITY_TRANSACTION_V2_MEMPOOL, DEFAULT_VDF_ROUNDS, 145 MAINNET_CANDIDATE_GENESIS_HASH, MAINNET_CANDIDATE_NETWORK_ID, MAINNET_NETWORK_ID, 146 MAX_PROTOCOL_CAPABILITIES, NETWORK_ID, PROTOCOL_VERSION, TRANSACTION_BATCH_LIMIT, 147 protocol_capabilities, validate_network_genesis, validate_protocol_capabilities, 148 validate_transaction_v2_peer_capability, 149 }; 150 151 #[test] 152 fn mainnet_candidate_network_parameters_are_frozen() { 153 assert_eq!(DEFAULT_VDF_ROUNDS, 67_000_000); 154 assert_eq!(PROTOCOL_VERSION, 2); 155 assert_eq!(MAINNET_CANDIDATE_NETWORK_ID, "iuna-mainnet-candidate"); 156 assert_eq!(MAINNET_CANDIDATE_GENESIS_HASH.len(), 64); 157 assert_eq!(MAINNET_NETWORK_ID, "iuna-mainnet-v1"); 158 assert_ne!(MAINNET_CANDIDATE_NETWORK_ID, MAINNET_NETWORK_ID); 159 assert_eq!(NETWORK_ID, MAINNET_CANDIDATE_NETWORK_ID); 160 assert_eq!(BLOCK_REQUEST_LIMIT, 128); 161 assert_eq!(TRANSACTION_BATCH_LIMIT, 128); 162 } 163 164 #[test] 165 fn candidate_genesis_is_pinned_while_local_profiles_remain_unpinned() { 166 assert!( 167 validate_network_genesis(MAINNET_CANDIDATE_NETWORK_ID, MAINNET_CANDIDATE_GENESIS_HASH) 168 .is_ok() 169 ); 170 assert!(validate_network_genesis(MAINNET_CANDIDATE_NETWORK_ID, &"0".repeat(64)).is_err()); 171 assert!(validate_network_genesis("iuna-local-testnet-v1", &"0".repeat(64)).is_ok()); 172 } 173 174 #[test] 175 fn current_protocol_capabilities_are_stable_and_valid() { 176 let capabilities = protocol_capabilities(); 177 assert_eq!( 178 capabilities, 179 [ 180 CAPABILITY_ADDRESS_V1_READ, 181 CAPABILITY_HYBRID_REWARD_PAYOUTS, 182 CAPABILITY_SIGNATURE_SCHEMES_V1, 183 CAPABILITY_TRANSACTION_V2_BLOCKS, 184 CAPABILITY_TRANSACTION_V2_BURNS, 185 CAPABILITY_TRANSACTION_V2_MEMPOOL, 186 ] 187 ); 188 validate_protocol_capabilities(&capabilities).unwrap(); 189 validate_protocol_capabilities(&[]).unwrap(); 190 } 191 192 #[test] 193 fn malformed_protocol_capabilities_fail_closed() { 194 assert!(validate_protocol_capabilities(&["UPPERCASE".to_string()]).is_err()); 195 assert!( 196 validate_protocol_capabilities(&["duplicate".to_string(), "duplicate".to_string()]) 197 .is_err() 198 ); 199 assert!( 200 validate_protocol_capabilities(&["z-last".to_string(), "a-first".to_string()]).is_err() 201 ); 202 assert!( 203 validate_protocol_capabilities(&vec![ 204 "capability".to_string(); 205 MAX_PROTOCOL_CAPABILITIES + 1 206 ]) 207 .is_err() 208 ); 209 } 210 211 #[test] 212 fn transaction_v2_block_capability_is_required_when_activation_is_next() { 213 assert!(validate_transaction_v2_peer_capability(&[], 2_998, 2_998).is_ok()); 214 assert!(validate_transaction_v2_peer_capability(&[], 2_999, 2_998).is_err()); 215 assert!( 216 validate_transaction_v2_peer_capability( 217 &[ 218 CAPABILITY_TRANSACTION_V2_BLOCKS.to_string(), 219 CAPABILITY_TRANSACTION_V2_BURNS.to_string(), 220 ], 221 2_999, 222 2_998, 223 ) 224 .is_ok() 225 ); 226 } 227 228 #[test] 229 fn hybrid_reward_capability_is_required_when_activation_is_next() { 230 let v2 = CAPABILITY_TRANSACTION_V2_BLOCKS.to_string(); 231 let burns = CAPABILITY_TRANSACTION_V2_BURNS.to_string(); 232 assert!( 233 validate_transaction_v2_peer_capability(&[v2.clone(), burns.clone()], 3_748, 3_748) 234 .is_ok() 235 ); 236 assert!( 237 validate_transaction_v2_peer_capability(&[v2.clone(), burns.clone()], 3_749, 3_748) 238 .is_err() 239 ); 240 assert!( 241 validate_transaction_v2_peer_capability( 242 &[v2, burns, CAPABILITY_HYBRID_REWARD_PAYOUTS.to_string(),], 243 3_749, 244 3_748, 245 ) 246 .is_ok() 247 ); 248 } 249 } 250 251 pub fn validate_network_genesis(profile_id: &str, genesis_hash: &str) -> Result<()> { 252 if profile_id == MAINNET_CANDIDATE_NETWORK_ID && genesis_hash != MAINNET_CANDIDATE_GENESIS_HASH 253 { 254 anyhow::bail!( 255 "mainnet-candidate genesis {genesis_hash} does not match pinned genesis {MAINNET_CANDIDATE_GENESIS_HASH}" 256 ); 257 } 258 Ok(()) 259 } 260 261 pub fn set_debug_logging(enabled: bool) { 262 DEBUG_LOGGING.store(enabled, Ordering::Relaxed); 263 } 264 265 pub fn debug_logging_enabled() -> bool { 266 DEBUG_LOGGING.load(Ordering::Relaxed) 267 } 268 269 #[derive(Clone, Debug, Eq, PartialEq)] 270 struct AutoPowMineCursor { 271 anchor: String, 272 salt: u64, 273 next_nonce: u64, 274 searched: u64, 275 } 276 277 #[derive(Clone, Debug)] 278 pub struct AutoPowMineJob { 279 ledger: Ledger, 280 recipient: String, 281 anchor: String, 282 salt: u64, 283 start_nonce: u64, 284 max_attempts: u64, 285 } 286 287 impl AutoPowMineJob { 288 pub fn anchor(&self) -> &str { 289 &self.anchor 290 } 291 292 pub fn search(self) -> Result<(Self, crate::domain::MineSearchOutcome)> { 293 let outcome = self.ledger.search_mine( 294 self.recipient.clone(), 295 self.salt, 296 self.start_nonce, 297 self.max_attempts, 298 )?; 299 Ok((self, outcome)) 300 } 301 } 302 303 #[derive(Clone, Debug)] 304 pub struct NodeCore { 305 wallet: NodeWallet, 306 ledger: Ledger, 307 automatic_mining_enabled: bool, 308 pow_mining_enabled: bool, 309 pow_mining_workers: u8, 310 burn_per_block: Amount, 311 burn_fee: Amount, 312 recovery_vdf_top_rank_percent: u8, 313 last_auto_burn_height: Option<u64>, 314 last_auto_anchor_burn_height: Option<u64>, 315 last_auto_finalization_status: Option<String>, 316 last_auto_pow_mine_anchor: Option<String>, 317 last_auto_pow_mine_status: Option<String>, 318 auto_pow_mine_cursor: Option<AutoPowMineCursor>, 319 burn_bundles: BTreeMap<(u64, u8, String), BurnBundle>, 320 equivocated_burn_bundle_slots: BTreeSet<(u64, u8, String)>, 321 burn_bundle_collection_started: Option<(u64, u64)>, 322 local_block_anchor_burn: Option<(u64, Transaction)>, 323 outbox: Vec<GossipEnvelope>, 324 network_migration_from: Option<String>, 325 } 326 327 pub fn now_ms() -> u64 { 328 SystemTime::now() 329 .duration_since(UNIX_EPOCH) 330 .expect("system time is before unix epoch") 331 .as_millis() as u64 332 }