iuna

iuna

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

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 }