From a612f736499a683836a8551659f23919eac96b50 Mon Sep 17 00:00:00 2001 From: Revertron Date: Mon, 5 Sep 2022 19:12:46 +0200 Subject: [PATCH] Fixed the consensus about expired domains. --- Cargo.lock | 2 +- Cargo.toml | 2 +- src/blockchain/chain.rs | 181 +++++++++++++++++----------------- src/blockchain/transaction.rs | 12 +++ src/blockchain/types.rs | 2 +- src/commons/constants.rs | 4 +- src/p2p/network.rs | 11 ++- src/web_ui.rs | 5 +- src/webview/scripts.js | 3 +- 9 files changed, 121 insertions(+), 101 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 307e222..4ca0bed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -84,7 +84,7 @@ dependencies = [ [[package]] name = "alfis" -version = "0.7.8" +version = "0.8.0" dependencies = [ "base64", "bincode", diff --git a/Cargo.toml b/Cargo.toml index 3513ab4..130978d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alfis" -version = "0.7.8" +version = "0.8.0" authors = ["Revertron "] edition = "2021" build = "build.rs" diff --git a/src/blockchain/chain.rs b/src/blockchain/chain.rs index 637548f..25677e9 100644 --- a/src/blockchain/chain.rs +++ b/src/blockchain/chain.rs @@ -12,7 +12,7 @@ use sqlite::{Connection, State, Statement}; use lazy_static::lazy_static; use crate::blockchain::hash_utils::*; -use crate::blockchain::transaction::DomainData; +use crate::blockchain::transaction::{DomainData, DomainState}; use crate::blockchain::types::BlockQuality::*; use crate::blockchain::types::MineResult::*; use crate::blockchain::types::{BlockQuality, MineResult, Options, ZoneData}; @@ -141,12 +141,9 @@ impl Chain { if WRONG_HASHES.contains(&block.hash) { error!("Block {} has bad hash:\n{:?}", block.index, &block); info!("Truncating database from block {}...", block.index); - match self.truncate_db_from_block(block.index) { - Ok(_) => {} - Err(e) => { - error!("{}", e); - panic!("Error truncating database! Please, delete 'blockchain.db' and restart."); - } + if let Err(e) = self.truncate_db_from_block(block.index) { + error!("{}", e); + panic!("Error truncating database! Please, delete 'blockchain.db' and restart."); } break; } @@ -487,40 +484,62 @@ impl Chain { None } - /// Checks if any domain is available to mine for this client (pub_key) - pub fn is_domain_available(&self, height: u64, domain: &str, public_key: &Bytes) -> bool { - if domain.is_empty() { - return false; + pub fn can_mine_domain(&self, height: u64, domain: &str, pub_key: &Bytes) -> MineResult { + let name = domain.to_lowercase(); + if !check_domain(&name, true) { + return WrongName; } - let identity_hash = hash_identity(domain, None); - if !self.is_id_available(height, &identity_hash, public_key) { - warn!("Domain {} is not available!", domain); - return false; + let zone = get_domain_zone(&name); + if !self.is_available_zone(&zone) { + return WrongZone; } - let parts: Vec<&str> = domain.rsplitn(2, '.').collect(); - if parts.len() > 1 { - // We do not support third level domains - if parts.last().unwrap().contains('.') { - return false; + let (transaction, state) = self.get_domain_transaction_and_state(&name); + if let Some(transaction) = transaction { + let owner = transaction.signing.eq(pub_key); + match state { + DomainState::NotFound => {} + DomainState::Alive { .. } => if !owner { + return NotOwned; + }, + DomainState::Expired { .. } => if !owner { + return NotOwned; + }, + DomainState::Free { .. } => {} } - return self.is_available_zone(parts.first().unwrap()); } - true + let identity_hash = hash_identity(&name, None); + self.can_mine_identity(&identity_hash, height, Utc::now().timestamp(), pub_key) } - /// Checks if this identity is free or is owned by the same pub_key - pub fn is_id_available(&self, height: u64, identity: &Bytes, public_key: &Bytes) -> bool { - let mut statement = self.db.prepare(SQL_GET_DOMAIN_OWNER_BY_ID).unwrap(); - statement.bind(1, height as i64).expect("Error in bind"); - statement.bind(2, identity.as_slice()).expect("Error in bind"); - while let State::Row = statement.next().unwrap() { - let pub_key = Bytes::from_bytes(&statement.read::>(0).unwrap()); - if !pub_key.eq(public_key) { - return false; + fn can_mine_identity(&self, identity_hash: &Bytes, height: u64, time: i64, pub_key: &Bytes) -> MineResult { + if let Some(last) = self.get_last_full_block(height, Some(pub_key)) { + // If this domain/identity is new + let want_new_domain = !self.is_domain_in_blockchain(height, &identity_hash); + // And the user hasn't mined a domain in previous 24h, then we allow her to mine + let time = last.timestamp + NEW_DOMAINS_INTERVAL - time; + if want_new_domain && time > 0 { + return Cooldown { time }; } } - true + Fine + } + + /// Checks if this identity is free or is owned by the same pub_key + pub fn is_id_available(&self, height: u64, time: i64, identity: &Bytes, public_key: &Bytes) -> bool { + let (transaction, state) = self.get_identity_transaction_and_state(identity, height, time); + if transaction.is_none() { + return true; + } + if transaction.unwrap().signing.eq(public_key) { + return true; + } + match state { + DomainState::NotFound => true, + DomainState::Alive { .. } => false, + DomainState::Expired { .. } => false, + DomainState::Free { .. } => true + } } pub fn get_zones(&self) -> &Vec { @@ -566,39 +585,12 @@ impl Chain { false } - pub fn can_mine_domain(&self, height: u64, domain: &str, pub_key: &Bytes) -> MineResult { - let name = domain.to_lowercase(); - if !check_domain(&name, true) { - return WrongName; - } - let zone = get_domain_zone(&name); - if !self.is_available_zone(&zone) { - return WrongZone; - } - if let Some(transaction) = self.get_domain_transaction(&name) { - if transaction.signing.ne(pub_key) { - return NotOwned; - } - } - let identity_hash = hash_identity(&name, None); - // TODO extract method - if let Some(last) = self.get_last_full_block(MAX, Some(pub_key)) { - let new_id = !self.is_domain_in_blockchain(height, &identity_hash); - let time = last.timestamp + NEW_DOMAINS_INTERVAL - Utc::now().timestamp(); - if new_id && time > 0 { - return Cooldown { time }; - } - } - - Fine - } - - pub fn get_domain_renewal_time(&self, identity_hash: &Bytes) -> Option { + pub fn get_domain_renewal_time(&self, time: i64, identity_hash: &Bytes) -> Option { let mut statement = self.db.prepare(SQL_GET_DOMAIN_UPDATE_TIME).unwrap(); statement.bind(1, identity_hash.as_slice()).expect("Error in bind"); if let State::Row = statement.next().unwrap() { let timestamp = statement.read::(0).unwrap(); - if timestamp < Utc::now().timestamp() - DOMAIN_LIFETIME { + if timestamp < time - DOMAIN_LIFETIME { // This domain is too old return None; } @@ -622,21 +614,20 @@ impl Chain { None } - pub fn get_domain_transaction_by_id(&self, identity_hash: &Bytes, height: u64) -> Option { - if self.get_domain_renewal_time(identity_hash).is_none() { - // Domain has expired - return None; - } - + pub fn get_identity_transaction_and_state(&self, identity_hash: &Bytes, height: u64, time: i64) -> (Option, DomainState) { let mut statement = self.db.prepare(SQL_GET_DOMAIN_BY_ID).unwrap(); statement.bind(1, identity_hash.as_slice()).expect("Error in bind"); statement.bind(2, height as i64).expect("Error in bind"); if let State::Row = statement.next().unwrap() { let timestamp = statement.read::(1).unwrap(); - if timestamp < Utc::now().timestamp() - DOMAIN_LIFETIME { - // This domain is too old - return None; - } + // Determine current state of the domain + let state = if timestamp + DOMAIN_LIFETIME >= time { + DomainState::Alive { renewed_time: timestamp, until: timestamp + DOMAIN_LIFETIME } + } else if timestamp + DOMAIN_LIFETIME + DOMAIN_RENEW_TIME >= time { + DomainState::Expired { renewed_time: timestamp, until: timestamp + DOMAIN_LIFETIME + DOMAIN_RENEW_TIME } + } else { + DomainState::Free { renewed_time: timestamp } + }; let identity = Bytes::from_bytes(&statement.read::>(2).unwrap()); let confirmation = Bytes::from_bytes(&statement.read::>(3).unwrap()); let class = String::from(CLASS_DOMAIN); @@ -644,30 +635,37 @@ impl Chain { let signing = Bytes::from_bytes(&statement.read::>(5).unwrap()); let encryption = Bytes::from_bytes(&statement.read::>(6).unwrap()); let transaction = Transaction { identity, confirmation, class, data, signing, encryption }; - return Some(transaction); + return (Some(transaction), state); } - None + (None, DomainState::NotFound) } /// Gets full Transaction info for any domain. Used by DNS part. - pub fn get_domain_transaction(&self, domain: &str) -> Option { + pub fn get_domain_transaction_and_state(&self, domain: &str) -> (Option, DomainState) { if domain.is_empty() { - return None; + return (None, DomainState::NotFound); } let identity_hash = hash_identity(domain, None); - if let Some(transaction) = self.get_domain_transaction_by_id(&identity_hash, self.get_height()) { + let (transaction, state) = self.get_identity_transaction_and_state(&identity_hash, self.get_height(), Utc::now().timestamp()); + if let Some(transaction) = transaction { debug!("Found transaction for domain {}: {:?}", domain, &transaction); if transaction.check_identity(domain) { - return Some(transaction); + return (Some(transaction), state); } } - None + (None, DomainState::NotFound) } pub fn get_domain_info(&self, domain: &str) -> Option { - match self.get_domain_transaction(domain) { - None => None, - Some(transaction) => Some(transaction.data) + match self.get_domain_transaction_and_state(domain) { + (None, _) => None, + (Some(transaction), state) => { + if matches!(state, DomainState::Alive {..}) { + Some(transaction.data) + } else { + None + } + } } } @@ -715,7 +713,9 @@ impl Chain { let signing = Bytes::from_bytes(&statement.read::>(3).unwrap()); // Get the last transaction for this id and check if it is still ours - if let Some(transaction) = self.get_domain_transaction_by_id(&identity, height) { + // TODO use state to show it in UI + let (transaction, _state) = self.get_identity_transaction_and_state(&identity, height, Utc::now().timestamp()); + if let Some(transaction) = transaction { if transaction.signing != signing { trace!("Identity {:?} is not ours anymore, skipping", &identity); continue; @@ -730,7 +730,7 @@ impl Chain { domain = String::from("unknown"); } // TODO optimize - match self.get_domain_renewal_time(&identity) { + match self.get_domain_renewal_time(timestamp, &identity) { None => result.insert(identity, (domain, timestamp, data)), Some(t) => result.insert(identity, (domain, t, data)) }; @@ -855,18 +855,13 @@ impl Chain { return Bad; } // If this domain is not available to this public key - if !self.is_id_available(block.index - 1, &transaction.identity, &block.pub_key) { + if !self.is_id_available(block.index - 1, timestamp, &transaction.identity, &block.pub_key) { warn!("Block {:?} is trying to spoof an identity!", &block); return Bad; } - if let Some(last) = self.get_last_full_block(block.index, Some(&block.pub_key)) { - if last.index < block.index { - let new_id = !self.is_domain_in_blockchain(block.index, &transaction.identity); - if new_id && last.timestamp + NEW_DOMAINS_INTERVAL > block.timestamp { - warn!("Block {:?} is mined too early!", &block); - return Bad; - } - } + if self.can_mine_identity(&transaction.identity, block.index, block.timestamp, &block.pub_key) != Fine { + warn!("Block {:?} is mined too early!", &block); + return Bad; } // Check if yggdrasil only property of zone is not violated if let Some(block_data) = transaction.get_domain_data() { @@ -1015,7 +1010,7 @@ impl Chain { } // Weeks since this domain was changed + 1, but not more than 7 weeks. // So max discount will be 8 bits of difficulty. - (min((Utc::now().timestamp() - timestamp) / ONE_WEEK, 7i64) + 1) as u32 + (min((time - timestamp) / ONE_WEEK, 7i64) + 1) as u32 } } } diff --git a/src/blockchain/transaction.rs b/src/blockchain/transaction.rs index fb3bc61..a6bf4cc 100644 --- a/src/blockchain/transaction.rs +++ b/src/blockchain/transaction.rs @@ -126,6 +126,18 @@ impl DomainData { } } +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum DomainState { + // Not in blockchain, free to mine + NotFound, + // Active, not expired domain + Alive { renewed_time: i64, until: i64 }, + // Expired, but can be renewed only by owner + Expired { renewed_time: i64, until: i64 }, + // Expired and can be recaptured by anyone + Free { renewed_time: i64 } +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Origin { zones: Bytes diff --git a/src/blockchain/types.rs b/src/blockchain/types.rs index 3a96b76..e0de8a9 100644 --- a/src/blockchain/types.rs +++ b/src/blockchain/types.rs @@ -13,7 +13,7 @@ pub enum BlockQuality { Fork } -#[derive(Debug)] +#[derive(Debug, PartialEq)] pub enum MineResult { Fine, WrongName, diff --git a/src/commons/constants.rs b/src/commons/constants.rs index e63d105..9dfd773 100644 --- a/src/commons/constants.rs +++ b/src/commons/constants.rs @@ -28,6 +28,8 @@ pub const BLOCK_SIGNERS_START_RANDOM: i64 = 90; pub const NEW_DOMAINS_INTERVAL: i64 = 86400; // One day in seconds pub const ONE_WEEK: i64 = 86400 * 7; // One week in seconds pub const DOMAIN_LIFETIME: i64 = 86400 * 365; // One year +/// Time for the owner to remine his domain and not to loose it +pub const DOMAIN_RENEW_TIME: i64 = 86400 * 30; // One month pub const MAX_RECORDS: usize = 30; pub const MAX_DATA_LEN: usize = 255; @@ -49,4 +51,4 @@ pub const MIN_CONNECTED_NODES_START_SYNC: usize = 4; pub const MAX_READ_BLOCK_TIME: u128 = 100; pub const MAX_RECONNECTS: u32 = 5; pub const MAX_IDLE_SECONDS: u64 = 180; -pub const MAX_NODES: usize = 20; +pub const MAX_NODES: usize = 15; diff --git a/src/p2p/network.rs b/src/p2p/network.rs index 29fe148..6dacaee 100644 --- a/src/p2p/network.rs +++ b/src/p2p/network.rs @@ -460,7 +460,7 @@ impl Network { let my_id = self.peers.get_my_id().to_owned(); let answer = match message { Message::Hand { app_version, origin, version, public, rand_id } => { - if app_version.starts_with("0.6") { + if !version_compatible(&app_version) { info!("Banning peer with version {}", &app_version); return State::Banned; } @@ -495,7 +495,7 @@ impl Network { if self.peers.is_tween_connect(&rand_id) { return State::Twin; } - if app_version.starts_with("0.6") { + if !version_compatible(&app_version) { info!("Banning peer with version {}", &app_version); return State::Banned; } @@ -916,4 +916,11 @@ fn would_block(err: &io::Error) -> bool { fn interrupted(err: &io::Error) -> bool { err.kind() == io::ErrorKind::Interrupted +} + +fn version_compatible(version: &str) -> bool { + let my_version = env!("CARGO_PKG_VERSION"); + let parts = my_version.split('.').collect::>(); + let major = format!("{}.{}", parts[0], parts[1]); + version.starts_with(&major) } \ No newline at end of file diff --git a/src/web_ui.rs b/src/web_ui.rs index 12c236f..026b8dc 100644 --- a/src/web_ui.rs +++ b/src/web_ui.rs @@ -126,7 +126,10 @@ fn action_check_domain(context: &Arc>, web_view: &mut WebView<()> let c = context.lock().unwrap(); if let Some(keystore) = c.get_keystore() { let name = name.to_lowercase(); - let available = c.get_chain().is_domain_available(c.get_chain().get_height(), &name, &keystore.get_public()); + let available = match c.chain.can_mine_domain(c.chain.get_height(), &name, &keystore.get_public()) { + MineResult::Fine => true, + _ => false + }; web_view.eval(&format!("domainAvailable({})", available)).expect("Error evaluating!"); } } diff --git a/src/webview/scripts.js b/src/webview/scripts.js index 65ee1e5..399b1d6 100644 --- a/src/webview/scripts.js +++ b/src/webview/scripts.js @@ -191,7 +191,8 @@ function editDomain(domain, event) { recordsBuffer.push(v); }); } - document.getElementById("new_domain").value = title.replace("." + domain_data.zone, ""); + currentDomain = title.replace("." + domain_data.zone, ""); + document.getElementById("new_domain").value = currentDomain; if (typeof domain_data.info !== 'undefined') { document.getElementById("info_text").value = domain_data.info; }