diff --git a/Cargo.toml b/Cargo.toml index 028fb3e..7d3e764 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "alfis" -version = "0.3.14" +version = "0.4.0" authors = ["Revertron "] edition = "2018" build = "build.rs" @@ -17,6 +17,8 @@ toml = "0.5.8" digest = "0.9.0" sha2 = "0.9.3" ed25519-dalek = "1.0" +x25519-dalek = "1.1" +chacha20poly1305 = "0.7.1" signature = "1.3.0" blakeout = "0.3.0" num_cpus = "1.13.0" @@ -43,7 +45,7 @@ winapi = { version = "0.3.7", features = ["impl-default", "wincon", "shellscalin [build-dependencies] minreq = { version = "2.3.1", features = ["punycode", "https-rustls"] } -rust-crypto = "^0.2" +rust-crypto = "^0.2" # TODO change to sha2 winres = "0.1" [dev-dependencies] diff --git a/src/blockchain/block.rs b/src/blockchain/block.rs index 7e629c2..3316f0b 100644 --- a/src/blockchain/block.rs +++ b/src/blockchain/block.rs @@ -1,12 +1,11 @@ extern crate serde; extern crate serde_json; -extern crate num_bigint; -extern crate num_traits; use std::fmt::Debug; use serde::{Serialize, Deserialize}; use crate::bytes::Bytes; use crate::Transaction; +use crate::blockchain::hash_utils::hash_difficulty; #[derive(Clone, Serialize, Deserialize, PartialEq, Debug)] pub struct Block { @@ -68,4 +67,16 @@ impl Block { pub fn as_bytes(&self) -> Vec { Vec::from(serde_json::to_string(&self).unwrap().as_bytes()) } + + pub fn is_better_than(&self, other: &Block) -> bool { + if self.transaction.is_none() && other.transaction.is_some() { + return false; + } + if hash_difficulty(self.hash.as_slice()) < hash_difficulty(other.hash.as_slice()) { + return false; + } + // TODO add more checks + + true + } } \ No newline at end of file diff --git a/src/blockchain/chain.rs b/src/blockchain/chain.rs index 3f93aaa..f3e0ab6 100644 --- a/src/blockchain/chain.rs +++ b/src/blockchain/chain.rs @@ -1,5 +1,7 @@ use std::cell::RefCell; use std::collections::{HashSet, HashMap}; +use std::fs; +use std::path::Path; use chrono::Utc; #[allow(unused_imports)] @@ -8,49 +10,40 @@ use sqlite::{Connection, State, Statement}; use crate::{Block, Bytes, Keystore, Transaction, check_domain, get_domain_zone}; use crate::commons::constants::*; -use crate::blockchain::enums::{BlockQuality, MineResult}; -use crate::blockchain::enums::BlockQuality::*; +use crate::blockchain::types::{BlockQuality, MineResult, Options}; +use crate::blockchain::types::BlockQuality::*; use crate::blockchain::hash_utils::*; use crate::settings::Settings; use crate::keys::check_public_key_strength; use std::cmp::{min, max}; use crate::blockchain::transaction::{ZoneData, DomainData}; use std::ops::Deref; -use crate::dns::protocol::DnsRecord; -use crate::blockchain::enums::MineResult::*; +use crate::blockchain::types::MineResult::*; const DB_NAME: &str = "blockchain.db"; -const SQL_CREATE_TABLES: &str = "CREATE TABLE blocks ( - 'id' BIGINT NOT NULL PRIMARY KEY, - 'timestamp' BIGINT NOT NULL, - 'version' INT, - 'difficulty' INTEGER, - 'random' INTEGER, - 'nonce' INTEGER, - 'transaction' TEXT, - 'prev_block_hash' BINARY, - 'hash' BINARY, - 'pub_key' BINARY, - 'signature' BINARY); - CREATE INDEX block_index ON blocks (id); - CREATE TABLE transactions (id INTEGER PRIMARY KEY AUTOINCREMENT, identity BINARY, confirmation BINARY, method TEXT, data TEXT, pub_key BINARY); - CREATE INDEX ids ON transactions (identity);"; +const TEMP_DB_NAME: &str = "temp.db"; +const SQL_CREATE_TABLES: &str = include_str!("sql/create_db.sql"); const SQL_ADD_BLOCK: &str = "INSERT INTO blocks (id, timestamp, version, difficulty, random, nonce, 'transaction',\ prev_block_hash, hash, pub_key, signature) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"; +const SQL_REPLACE_BLOCK: &str = "UPDATE blocks SET timestamp = ?, version = ?, difficulty = ?, random = ?, nonce = ?, 'transaction' = ?,\ + prev_block_hash = ?, hash = ?, pub_key = ?, signature = ? WHERE id = ?;"; const SQL_GET_LAST_BLOCK: &str = "SELECT * FROM blocks ORDER BY id DESC LIMIT 1;"; -const SQL_ADD_TRANSACTION: &str = "INSERT INTO transactions (identity, confirmation, method, data, pub_key) VALUES (?, ?, ?, ?, ?)"; +const SQL_ADD_DOMAIN: &str = "INSERT INTO domains (id, timestamp, identity, confirmation, data, pub_key) VALUES (?, ?, ?, ?, ?, ?)"; +const SQL_ADD_ZONE: &str = "INSERT INTO zones (id, timestamp, identity, confirmation, data, pub_key) VALUES (?, ?, ?, ?, ?, ?)"; +const SQL_DELETE_DOMAIN: &str = "DELETE FROM domains WHERE id = ?"; +const SQL_DELETE_ZONE: &str = "DELETE FROM zones WHERE id = ?"; const SQL_GET_BLOCK_BY_ID: &str = "SELECT * FROM blocks WHERE id=? LIMIT 1;"; const SQL_GET_LAST_FULL_BLOCK: &str = "SELECT * FROM blocks WHERE `transaction`<>'' ORDER BY id DESC LIMIT 1;"; const SQL_GET_LAST_FULL_BLOCK_FOR_KEY: &str = "SELECT * FROM blocks WHERE `transaction`<>'' AND pub_key = ? ORDER BY id DESC LIMIT 1;"; -const SQL_GET_PUBLIC_KEY_BY_ID: &str = "SELECT pub_key FROM transactions WHERE identity = ? ORDER BY id DESC LIMIT 1;"; -const SQL_GET_ID_BY_ID: &str = "SELECT identity FROM transactions WHERE identity = ? ORDER BY id DESC LIMIT 1;"; -const SQL_GET_TRANSACTION_BY_ID: &str = "SELECT * FROM transactions WHERE identity = ? ORDER BY id DESC LIMIT 1;"; -const SQL_GET_TRANSACTIONS_WITH_ZONE: &str = "SELECT data FROM transactions WHERE data LIKE '%difficulty%';"; +const SQL_GET_DOMAIN_PUBLIC_KEY_BY_ID: &str = "SELECT pub_key FROM domains WHERE identity = ? ORDER BY id DESC LIMIT 1;"; +const SQL_GET_ZONE_PUBLIC_KEY_BY_ID: &str = "SELECT pub_key FROM zones WHERE identity = ? ORDER BY id DESC LIMIT 1;"; +const SQL_GET_DOMAIN_BY_ID: &str = "SELECT * FROM domains WHERE identity = ? ORDER BY id DESC LIMIT 1;"; +const SQL_GET_ZONES: &str = "SELECT data FROM zones;"; + +const SQL_GET_OPTIONS: &str = "SELECT * FROM options;"; pub struct Chain { origin: Bytes, - pub version: u32, - pub blocks: Vec, last_block: Option, last_full_block: Option, max_height: u64, @@ -63,22 +56,22 @@ impl Chain { let origin = settings.get_origin(); let db = sqlite::open(DB_NAME).expect("Unable to open blockchain DB"); - let mut chain = Chain { - origin, - version: CHAIN_VERSION, - blocks: Vec::new(), - last_block: None, - last_full_block: None, - max_height: 0, - db, - zones: RefCell::new(HashSet::new()), - }; + let zones = RefCell::new(HashSet::new()); + let mut chain = Chain { origin, last_block: None, last_full_block: None, max_height: 0, db, zones }; chain.init_db(); chain } /// Reads options from DB or initializes and writes them to DB if not found fn init_db(&mut self) { + let options = self.get_options(); + if !self.origin.is_zero() && !self.origin.is_zero() && self.origin.to_string() != options.origin { + self.clear_db(); + } + if options.version < DB_VERSION { + self.migrate_db(options.version, DB_VERSION); + } + // Trying to get last block from DB to check its version let block: Option = match self.db.prepare(SQL_GET_LAST_BLOCK) { Ok(mut statement) => { @@ -98,21 +91,14 @@ impl Chain { } result } - Err(_) => { - info!("No blockchain database found. Creating new."); - self.db.execute(SQL_CREATE_TABLES).expect("Error creating blocks table"); + Err(e) => { + info!("No blockchain database found. Creating new. {}", e); + self.db.execute(SQL_CREATE_TABLES).expect("Error creating DB tables"); None } }; // If some block loaded we check its version and determine if we need some migration if let Some(block) = block { - self.max_height = block.index; - if self.version > block.version { - self.migrate_db(block.version, self.version); - } else if self.version < block.version { - error!("Version downgrade {}->{} is not supported!", block.version, self.version); - panic!(); - } // Cache some info self.last_block = Some(block.clone()); if block.transaction.is_some() { @@ -127,9 +113,41 @@ impl Chain { debug!("Migrating DB from {} to {}", from, to); } + fn clear_db(&mut self) { + warn!("Clearing DB"); + // We cannot close DB connection and recreate file, + // therefore we switch our db to temporary file, delete main DB and switch back. + // I know that this is a crutch, but this way I don't need to use Option :) + self.db = sqlite::open(TEMP_DB_NAME).expect("Unable to open temporary blockchain DB"); + let file = Path::new(DB_NAME); + if fs::remove_file(&file).is_err() { + panic!("Unable to remove database!"); + } + self.db = sqlite::open(DB_NAME).expect("Unable to open blockchain DB"); + let file = Path::new(TEMP_DB_NAME); + let _ = fs::remove_file(&file).is_err(); + } + + fn get_options(&self) -> Options { + let mut options = Options::empty(); + if let Ok(mut statement) = self.db.prepare(SQL_GET_OPTIONS) { + while let State::Row = statement.next().unwrap() { + let name = statement.read::(0).unwrap(); + let value = statement.read::(1).unwrap(); + match name.as_ref() { + "origin" => options.origin = value, + "version" => options.version = value.parse().unwrap(), + _ => {} + } + } + } + options + } + pub fn add_block(&mut self, block: Block) { debug!("Adding block:\n{:?}", &block); - self.blocks.push(block.clone()); + let index = block.index; + let timestamp = block.timestamp; self.last_block = Some(block.clone()); if block.transaction.is_some() { self.last_full_block = Some(block.clone()); @@ -137,11 +155,39 @@ impl Chain { let transaction = block.transaction.clone(); if self.add_block_to_table(block).is_ok() { if let Some(transaction) = transaction { - self.add_transaction_to_table(&transaction).expect("Error adding transaction"); + self.add_transaction_to_table(index, timestamp, &transaction).expect("Error adding transaction"); } } } + pub fn replace_block(&mut self, index: u64, block: Block) -> sqlite::Result<()> { + debug!("Replacing block {} with:\n{:?}", index, &block); + let old_block = self.get_block(index).unwrap(); + if old_block.transaction.is_some() { + let mut statement = self.db.prepare(SQL_DELETE_DOMAIN)?; + statement.bind(1, index as i64)?; + statement.next()?; + + let mut statement = self.db.prepare(SQL_DELETE_ZONE)?; + statement.bind(1, index as i64)?; + statement.next()?; + } + + let index = block.index; + let timestamp = block.timestamp; + self.last_block = Some(block.clone()); + if block.transaction.is_some() { + self.last_full_block = Some(block.clone()); + } + let transaction = block.transaction.clone(); + if self.replace_block_in_table(block).is_ok() { + if let Some(transaction) = transaction { + self.add_transaction_to_table(index, timestamp, &transaction).expect("Error adding transaction"); + } + } + Ok(()) + } + /// Adds block to blocks table fn add_block_to_table(&mut self, block: Block) -> sqlite::Result { let mut statement = self.db.prepare(SQL_ADD_BLOCK)?; @@ -164,14 +210,43 @@ impl Chain { statement.next() } + /// Replaces block in blocks table on arrival of better block from some fork + fn replace_block_in_table(&mut self, block: Block) -> sqlite::Result { + let mut statement = self.db.prepare(SQL_REPLACE_BLOCK)?; + statement.bind(1, block.timestamp as i64)?; + statement.bind(2, block.version as i64)?; + statement.bind(3, block.difficulty as i64)?; + statement.bind(4, block.random as i64)?; + statement.bind(5, block.nonce as i64)?; + match &block.transaction { + None => { statement.bind(6, "")?; } + Some(transaction) => { + statement.bind(6, transaction.to_string().as_str())?; + } + } + statement.bind(7, &**block.prev_block_hash)?; + statement.bind(8, &**block.hash)?; + statement.bind(9, &**block.pub_key)?; + statement.bind(10, &**block.signature)?; + statement.bind(11, block.index as i64)?; + statement.next() + } + /// Adds transaction to transactions table - fn add_transaction_to_table(&mut self, t: &Transaction) -> sqlite::Result { - let mut statement = self.db.prepare(SQL_ADD_TRANSACTION)?; - statement.bind(1, &**t.identity)?; - statement.bind(2, &**t.confirmation)?; - statement.bind(3, t.method.as_ref() as &str)?; - statement.bind(4, t.data.as_ref() as &str)?; - statement.bind(5, &**t.pub_key)?; + fn add_transaction_to_table(&mut self, index: u64, timestamp: i64, t: &Transaction) -> sqlite::Result { + let sql = match t.class.as_ref() { + "domain" => SQL_ADD_DOMAIN, + "zone" => SQL_ADD_ZONE, + _ => return Err(sqlite::Error { code: None, message: None }) + }; + + let mut statement = self.db.prepare(sql)?; + statement.bind(1, index as i64)?; + statement.bind(2, timestamp)?; + statement.bind(3, &**t.identity)?; + statement.bind(4, &**t.confirmation)?; + statement.bind(5, t.data.as_ref() as &str)?; + statement.bind(6, &**t.pub_key)?; statement.next() } @@ -244,7 +319,7 @@ impl Chain { return false; } let identity_hash = hash_identity(domain, None); - if !self.is_id_available(&identity_hash, &keystore.get_public()) { + if !self.is_id_available(&identity_hash, &keystore.get_public(), false) { return false; } @@ -260,8 +335,13 @@ impl Chain { } /// Checks if this identity is free or is owned by the same pub_key - pub fn is_id_available(&self, identity: &Bytes, public_key: &Bytes) -> bool { - let mut statement = self.db.prepare(SQL_GET_PUBLIC_KEY_BY_ID).unwrap(); + pub fn is_id_available(&self, identity: &Bytes, public_key: &Bytes, zone: bool) -> bool { + let sql = match zone { + true => { SQL_GET_ZONE_PUBLIC_KEY_BY_ID } + false => { SQL_GET_DOMAIN_PUBLIC_KEY_BY_ID } + }; + + let mut statement = self.db.prepare(sql).unwrap(); statement.bind(1, &***identity).expect("Error in bind"); while let State::Row = statement.next().unwrap() { let pub_key = Bytes::from_bytes(&statement.read::>(0).unwrap()); @@ -274,7 +354,7 @@ impl Chain { pub fn get_zones(&self) -> Vec { let mut map = HashMap::new(); - match self.db.prepare(SQL_GET_TRANSACTIONS_WITH_ZONE) { + match self.db.prepare(SQL_GET_ZONES) { Ok(mut statement) => { while statement.next().unwrap() == State::Row { let data = statement.read::(0).unwrap(); @@ -300,7 +380,7 @@ impl Chain { // Checking for existing zone in DB let identity_hash = hash_identity(zone, None); - if self.is_id_in_blockchain(&identity_hash) { + if self.is_id_in_blockchain(&identity_hash, true) { // If there is such a zone self.zones.borrow_mut().insert(zone.to_owned()); return true; @@ -309,9 +389,13 @@ impl Chain { } /// Checks if some id exists in our blockchain - pub fn is_id_in_blockchain(&self, id: &Bytes) -> bool { + pub fn is_id_in_blockchain(&self, id: &Bytes, zone: bool) -> bool { + let sql = match zone { + true => { SQL_GET_ZONE_PUBLIC_KEY_BY_ID } + false => { SQL_GET_DOMAIN_PUBLIC_KEY_BY_ID } + }; // Checking for existing zone in DB - let mut statement = self.db.prepare(SQL_GET_ID_BY_ID).unwrap(); + let mut statement = self.db.prepare(sql).unwrap(); statement.bind(1, &***id).expect("Error in bind"); while let State::Row = statement.next().unwrap() { // If there is such a zone @@ -320,7 +404,7 @@ impl Chain { false } - pub fn can_mine_domain(&self, domain: &str, records: &str, pub_key: &Bytes) -> MineResult { + pub fn can_mine_domain(&self, domain: &str, pub_key: &Bytes) -> MineResult { let name = domain.to_lowercase(); if !check_domain(&name, true) { return WrongName; @@ -334,13 +418,10 @@ impl Chain { return NotOwned; } } - if serde_json::from_str::>(&records).is_err() { - return WrongData; - } let identity_hash = hash_identity(&name, None); if let Some(last) = self.get_last_full_block(Some(&pub_key)) { - let new_id = !self.is_id_in_blockchain(&identity_hash); - let time = last.timestamp + FULL_BLOCKS_INTERVAL - Utc::now().timestamp(); + let new_id = !self.is_id_in_blockchain(&identity_hash, false); + let time = last.timestamp + NEW_DOMAINS_INTERVAL - Utc::now().timestamp(); if new_id && time > 0 { return Cooldown { time } } @@ -356,15 +437,20 @@ impl Chain { } let identity_hash = hash_identity(domain, None); - let mut statement = self.db.prepare(SQL_GET_TRANSACTION_BY_ID).unwrap(); + let mut statement = self.db.prepare(SQL_GET_DOMAIN_BY_ID).unwrap(); statement.bind(1, &**identity_hash).expect("Error in bind"); while let State::Row = statement.next().unwrap() { - let identity = Bytes::from_bytes(&statement.read::>(1).unwrap()); - let confirmation = Bytes::from_bytes(&statement.read::>(2).unwrap()); - let method = statement.read::(3).unwrap(); - let data = statement.read::(4).unwrap(); - let pub_key = Bytes::from_bytes(&statement.read::>(5).unwrap()); - let transaction = Transaction { identity, confirmation, method, data, pub_key }; + let timestamp = statement.read::(1).unwrap(); + if timestamp < Utc::now().timestamp() - DOMAIN_LIFETIME { + // This domain is too old + return None; + } + let identity = Bytes::from_bytes(&statement.read::>(2).unwrap()); + let confirmation = Bytes::from_bytes(&statement.read::>(3).unwrap()); + let method = statement.read::(4).unwrap(); + let data = statement.read::(5).unwrap(); + let pub_key = Bytes::from_bytes(&statement.read::>(6).unwrap()); + let transaction = Transaction { identity, confirmation, class: method, data, pub_key }; debug!("Found transaction for domain {}: {:?}", domain, &transaction); if transaction.check_identity(domain) { return Some(transaction); @@ -381,18 +467,13 @@ impl Chain { } pub fn get_zone_difficulty(&self, zone: &str) -> u32 { - match self.get_domain_transaction(zone) { - None => { u32::max_value() } - Some(transaction) => { - match serde_json::from_str::(&transaction.data) { - Ok(data) => { data.difficulty } - Err(_) => { - warn!("Wrong data for zone {}!", zone); - u32::max_value() - } - } + let zones = self.get_zones(); + for z in zones.iter() { + if z.name.eq(zone) { + return z.difficulty; } } + u32::max_value() } pub fn last_block(&self) -> Option { @@ -441,7 +522,7 @@ impl Chain { /// Check if this block can be added to our blockchain pub fn check_new_block(&self, block: &Block) -> BlockQuality { let timestamp = Utc::now().timestamp(); - if block.timestamp > timestamp { + if block.timestamp > timestamp + 60 { warn!("Ignoring block from the future:\n{:?}", &block); return Bad; } @@ -449,15 +530,21 @@ impl Chain { warn!("Ignoring block with weak public key:\n{:?}", &block); return Bad; } - let difficulty = match block.transaction { - None => { LOCKER_DIFFICULTY } - Some(_) => { BLOCK_DIFFICULTY } + let difficulty = match &block.transaction { + None => { + if block.index == 1 { + ZONE_DIFFICULTY + } else { + LOCKER_DIFFICULTY + } + } + Some(t) => { self.get_difficulty_for_transaction(&t) } }; if block.difficulty < difficulty { warn!("Block difficulty is lower than needed"); return Bad; } - if !hash_is_good(&block.hash, block.difficulty as usize) { + if hash_difficulty(&block.hash) < block.difficulty { warn!("Ignoring block with low difficulty:\n{:?}", &block); return Bad; } @@ -470,23 +557,18 @@ impl Chain { return Bad; } if let Some(transaction) = &block.transaction { - if !self.is_id_available(&transaction.identity, &block.pub_key) { + // TODO check for zone transaction + if !self.is_id_available(&transaction.identity, &block.pub_key, false) { warn!("Block {:?} is trying to spoof an identity!", &block); return Bad; } if let Some(last) = self.get_last_full_block(Some(&block.pub_key)) { - let new_id = !self.is_id_in_blockchain(&transaction.identity); - if new_id && last.timestamp + FULL_BLOCKS_INTERVAL > block.timestamp { + let new_id = !self.is_id_in_blockchain(&transaction.identity, false); + if new_id && last.timestamp + NEW_DOMAINS_INTERVAL > block.timestamp { warn!("Block {:?} is mined too early!", &block); return Bad; } } - if let Ok(data) = serde_json::from_str::(&transaction.data) { - if self.get_zone_difficulty(&data.zone) > block.difficulty { - warn!("Block {:?} is mined with too low difficulty!", &block); - return Bad; - } - } } match &self.last_block { None => { @@ -505,25 +587,25 @@ impl Chain { return Bad; } if last_block.index + 1 < block.index { - warn!("Block is from the future, how is this possible?"); + warn!("Block {} arrived too early.", block.index); return Future; } - if block.index > LOCKER_BLOCK_START { + if block.index >= LOCKER_BLOCK_START { // If this block is locked part of blockchain if let Some(full_block) = &self.last_full_block { let locker_blocks = self.height() - full_block.index; if locker_blocks < LOCKER_BLOCK_SIGNS { // Last full block is not locked enough if block.transaction.is_some() { - warn!("Someone mined full block over full block"); + warn!("Not enough signing blocks over full {} block!", full_block.index); return Bad; } else { - if self.check_block_for_lock(&block, full_block) == Bad { + if self.check_block_for_signing(&block, full_block) == Bad { return Bad; } } } else if locker_blocks < LOCKER_BLOCK_LOCKERS && block.transaction.is_none() { - if self.check_block_for_lock(&block, full_block) == Bad { + if self.check_block_for_signing(&block, full_block) == Bad { return Bad; } } @@ -531,7 +613,7 @@ impl Chain { } if block.index <= last_block.index { - if last_block.hash == block.hash { + if block.index == last_block.index && last_block.hash == block.hash { debug!("Ignoring block {}, we already have it", block.index); return Twin; } @@ -552,27 +634,47 @@ impl Chain { Good } - fn check_block_for_lock(&self, block: &Block, full_block: &Block) -> BlockQuality { + fn get_difficulty_for_transaction(&self, transaction: &Transaction) -> u32 { + match transaction.class.as_ref() { + "domain" => { + return match serde_json::from_str::(&transaction.data) { + Ok(data) => { + for zone in self.get_zones().iter() { + if zone.name == data.zone { + return zone.difficulty; + } + } + u32::max_value() + } + Err(_) => { u32::max_value() } + } + } + "zone" => { ZONE_DIFFICULTY } + _ => { u32::max_value() } + } + } + + fn check_block_for_signing(&self, block: &Block, full_block: &Block) -> BlockQuality { // If we got a locker/signing block - let lockers: HashSet = self.get_block_lockers(full_block).into_iter().collect(); - if !lockers.contains(&block.pub_key) { - warn!("Ignoring block {}, as wrong locker", block.index); + let signers: HashSet = self.get_block_signers(full_block).into_iter().collect(); + if !signers.contains(&block.pub_key) { + warn!("Ignoring block {} from '{:?}', as wrong signer!", block.index, &block.pub_key); return Bad; } - // If this locker's public key has already locked/signed that block we return error + // If this signers' public key has already locked/signed that block we return error for i in (full_block.index + 1)..block.index { - let locker = self.get_block(i).expect("Error in DB!"); - if locker.pub_key == block.pub_key { - warn!("Ignoring block {}, already locked by this key", block.index); + let signer = self.get_block(i).expect("Error in DB!"); + if signer.pub_key == block.pub_key { + warn!("Ignoring block {} from '{:?}', already signed by this key", block.index, &block.pub_key); return Bad; } } Good } - /// Gets a public key of a node that needs to mine "locker" block above this block + /// Gets public keys of a node that needs to mine "signature" block above this block /// block - last full block - pub fn get_block_lockers(&self, block: &Block) -> Vec { + pub fn get_block_signers(&self, block: &Block) -> Vec { let mut result = Vec::new(); if block.index < LOCKER_BLOCK_START { return result; @@ -592,7 +694,7 @@ impl Chain { count += 1; } } - trace!("Got lockers for block {}: {:?}", block.index, &result); + trace!("Got signers for block {}: {:?}", block.index, &result); result } diff --git a/src/blockchain/enums.rs b/src/blockchain/enums.rs deleted file mode 100644 index 59eaf35..0000000 --- a/src/blockchain/enums.rs +++ /dev/null @@ -1,20 +0,0 @@ -/// Represents a result of block check on block's arrival -#[derive(PartialEq)] -pub enum BlockQuality { - Good, - Twin, - Future, - Bad, - Fork, -} - -#[derive(Debug)] -pub enum MineResult { - Fine, - WrongName, - WrongData, - WrongKey, - WrongZone, - NotOwned, - Cooldown { time: i64 } -} \ No newline at end of file diff --git a/src/blockchain/hash_utils.rs b/src/blockchain/hash_utils.rs index c5738df..9070e2a 100644 --- a/src/blockchain/hash_utils.rs +++ b/src/blockchain/hash_utils.rs @@ -1,9 +1,8 @@ use blakeout::Blakeout; -use num_bigint::BigUint; -use num_traits::One; use crate::{Block, Bytes, Keystore}; use sha2::{Sha256, Digest}; +use std::convert::TryInto; /// Checks block's hash and returns true on valid hash or false otherwise pub fn check_block_hash(block: &Block) -> bool { @@ -31,12 +30,17 @@ pub fn check_block_signature(block: &Block) -> bool { /// Hashes some identity (domain in case of DNS). If you give it a public key, it will hash with it as well. /// Giving public key is needed to create a confirmation field in [Transaction] pub fn hash_identity(identity: &str, key: Option<&Bytes>) -> Bytes { - let mut digest = Sha256::default(); - digest.update(identity.as_bytes()); - if let Some(key) = key { - digest.update(key.as_slice()); + let base = hash_sha256(identity.as_bytes()); + let identity = hash_sha256(&base); + match key { + None => { Bytes::from_bytes(&identity) } + Some(key) => { + let mut buf = Vec::new(); + buf.append(&mut identity.clone()); + buf.append(&mut key.to_vec()); + Bytes::from_bytes(&hash_sha256(&buf)) + } } - Bytes::from_bytes(&digest.finalize()[..]) } /// There is no default PartialEq implementation for arrays > 32 in size @@ -54,10 +58,56 @@ pub fn same_hash(left: &[u8], right: &[u8]) -> bool { result } -/// Checks if this hash contains enough zeroes -pub fn hash_is_good(hash: &[u8], difficulty: usize) -> bool { - let target = BigUint::one() << ((hash.len() << 3) - difficulty); - let hash_int = BigUint::from_bytes_be(&hash); +/// Returns hash difficulty +pub fn hash_difficulty(hash: &[u8]) -> u32 { + let bytes: [u8; 8] = hash[..8].try_into().unwrap(); + let int_start = u64::from_be_bytes(bytes); + let bytes: [u8; 8] = hash[hash.len() - 8..].try_into().unwrap(); + let int_end = u64::from_be_bytes(bytes); + int_start.leading_zeros() + int_end.trailing_zeros() +} - return hash_int < target; +pub fn hash_difficulty_key(hash: &[u8]) -> u32 { + let bytes: [u8; 8] = hash[..8].try_into().unwrap(); + let int = u64::from_be_bytes(bytes); + int.leading_zeros() +} + +pub fn hash_sha256(data: &[u8]) -> Vec { + let mut digest = Sha256::default(); + digest.update(data.as_ref()); + Vec::from(&digest.finalize()[..]) +} + +#[cfg(test)] +mod tests { + use crate::blockchain::hash_utils::hash_sha256; + use std::convert::TryInto; + + #[test] + pub fn test_hash() { + let id = b"example.com"; + let key = b"some_key"; + + let base = hash_sha256(id); + + let identity = hash_sha256(&base); + + let mut buf = Vec::new(); + buf.append(&mut identity.clone()); + buf.append(&mut key.to_vec()); + let confirmation = hash_sha256(&buf); + + println!("result1 = {:?}", &base); + println!("result2 = {:?}", &identity); + println!("result3 = {:?}", &confirmation); + } + + #[test] + fn test_hash_is_good() { + let hash = vec!(0u8,0u8,0u8,255,255,255,255,255); + let bytes: [u8; 8] = hash[..8].try_into().unwrap(); + let int = u64::from_be_bytes(bytes); + println!("int = {}", int); + } } \ No newline at end of file diff --git a/src/blockchain/mod.rs b/src/blockchain/mod.rs index e3c55f8..6c627e4 100644 --- a/src/blockchain/mod.rs +++ b/src/blockchain/mod.rs @@ -7,5 +7,5 @@ pub mod block; pub mod chain; pub mod filter; pub mod hash_utils; -pub mod enums; +pub mod types; diff --git a/src/blockchain/sql/create_db.sql b/src/blockchain/sql/create_db.sql new file mode 100644 index 0000000..cef15b8 --- /dev/null +++ b/src/blockchain/sql/create_db.sql @@ -0,0 +1,36 @@ +CREATE TABLE blocks ( + 'id' BIGINT NOT NULL PRIMARY KEY, + 'timestamp' BIGINT NOT NULL, + 'version' INT, + 'difficulty' INTEGER, + 'random' INTEGER, + 'nonce' INTEGER, + 'transaction' TEXT, + 'prev_block_hash' BINARY, + 'hash' BINARY, + 'pub_key' BINARY, + 'signature' BINARY +); +CREATE INDEX block_index ON blocks (id); +CREATE INDEX keys ON blocks (pub_key); + +CREATE TABLE domains ( + 'id' BIGINT NOT NULL PRIMARY KEY, + 'timestamp' BIGINT NOT NULL, + 'identity' BINARY, + 'confirmation' BINARY, + 'data' TEXT, + 'pub_key' BINARY +); +CREATE INDEX ids ON domains ('identity'); + +CREATE TABLE zones ( + 'id' BIGINT NOT NULL PRIMARY KEY, + 'timestamp' BIGINT NOT NULL, + 'identity' BINARY, + 'confirmation' BINARY, + 'data' TEXT, + 'pub_key' BINARY +); + +CREATE TABLE options ('name' TEXT NOT NULL, 'value' TEXT NOT NULL); \ No newline at end of file diff --git a/src/blockchain/transaction.rs b/src/blockchain/transaction.rs index 81ab9d0..b024185 100644 --- a/src/blockchain/transaction.rs +++ b/src/blockchain/transaction.rs @@ -15,7 +15,7 @@ extern crate serde_json; pub struct Transaction { pub identity: Bytes, pub confirmation: Bytes, - pub method: String, + pub class: String, pub data: String, pub pub_key: Bytes, } @@ -28,7 +28,7 @@ impl Transaction { } pub fn new(identity: Bytes, confirmation: Bytes, method: String, data: String, pub_key: Bytes) -> Self { - Transaction { identity, confirmation, method, data, pub_key } + Transaction { identity, confirmation, class: method, data, pub_key } } pub fn from_json(json: &str) -> Option { @@ -60,9 +60,9 @@ impl fmt::Debug for Transaction { fmt.debug_struct("Transaction") .field("identity", &self.identity) .field("confirmation", &self.confirmation) - .field("method", &self.method) + .field("class", &self.class) .field("data", &self.data) - .field("pub", &&self.pub_key) + .field("pub_key", &&self.pub_key) .finish() } } @@ -72,7 +72,7 @@ impl Serialize for Transaction { let mut structure = serializer.serialize_struct("Transaction", 5).unwrap(); structure.serialize_field("identity", &self.identity)?; structure.serialize_field("confirmation", &self.confirmation)?; - structure.serialize_field("method", &self.method)?; + structure.serialize_field("class", &self.class)?; structure.serialize_field("data", &self.data)?; structure.serialize_field("pub_key", &self.pub_key)?; structure.end() @@ -81,24 +81,43 @@ impl Serialize for Transaction { #[derive(Clone, Serialize, Deserialize, PartialEq)] pub struct DomainData { + pub domain: Bytes, pub zone: String, - pub records: Vec + pub records: Vec, + pub contacts: Vec, + #[serde(default)] + pub owners: Vec } impl DomainData { - pub fn new(zone: String, records: Vec) -> Self { - Self { zone, records } + pub fn new(domain: Bytes, zone: String, records: Vec, contacts: Vec, owners: Vec) -> Self { + Self { domain, zone, records, contacts, owners } } } #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct ZoneData { pub name: String, - pub difficulty: u32 + pub difficulty: u32, + pub yggdrasil: bool, + #[serde(default)] + pub owners: Vec } impl Display for ZoneData { fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { f.write_str(&format!("{} ({})", self.name, self.difficulty)) } +} + +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct ContactsData { + pub name: String, + pub value: String +} + +impl Display for ContactsData { + fn fmt(&self, f: &mut Formatter<'_>) -> core::fmt::Result { + f.write_str(&format!("{}: {}", self.name, self.value)) + } } \ No newline at end of file diff --git a/src/blockchain/types.rs b/src/blockchain/types.rs new file mode 100644 index 0000000..076b4f2 --- /dev/null +++ b/src/blockchain/types.rs @@ -0,0 +1,36 @@ +/// Represents a result of block check on block's arrival +#[derive(PartialEq)] +pub enum BlockQuality { + Good, + Twin, + Future, + Bad, + Fork, +} + +#[derive(Debug)] +pub enum MineResult { + Fine, + WrongName, + WrongData, + WrongKey, + WrongZone, + NotOwned, + Cooldown { time: i64 }, +} + +#[derive(Debug)] +pub struct Options { + pub origin: String, + pub version: u32, +} + +impl Options { + pub fn new(origin: String, version: u32) -> Self { + Options { origin, version } + } + + pub fn empty() -> Self { + Options { origin: String::new(), version: 0 } + } +} \ No newline at end of file diff --git a/src/commons/constants.rs b/src/commons/constants.rs index ac1283f..0b823ed 100644 --- a/src/commons/constants.rs +++ b/src/commons/constants.rs @@ -1,9 +1,10 @@ -pub const CHAIN_VERSION: u32 = 2; +pub const DB_VERSION: u32 = 0; +pub const CHAIN_VERSION: u32 = 0; -pub const ZONE_DIFFICULTY: u32 = 22; -pub const BLOCK_DIFFICULTY: u32 = 20; -pub const LOCKER_DIFFICULTY: u32 = 14; -pub const KEYSTORE_DIFFICULTY: usize = 23; +pub const ZONE_DIFFICULTY: u32 = 28; +pub const ZONE_MIN_DIFFICULTY: u32 = 22; +pub const LOCKER_DIFFICULTY: u32 = 16; +pub const KEYSTORE_DIFFICULTY: u32 = 23; pub const LOCKER_BLOCK_START: u64 = 35; pub const LOCKER_BLOCK_LOCKERS: u64 = 7; @@ -11,7 +12,11 @@ pub const LOCKER_BLOCK_SIGNS: u64 = 4; pub const LOCKER_BLOCK_TIME: i64 = 300; pub const LOCKER_BLOCK_INTERVAL: u64 = 50; -pub const FULL_BLOCKS_INTERVAL: i64 = 86400; // One day in seconds +pub const NEW_DOMAINS_INTERVAL: i64 = 86400; // One day in seconds +pub const DOMAIN_LIFETIME: i64 = 86400 * 365; // One year pub const ZONE_MAX_LENGTH: usize = 10; -pub const MAX_RECONNECTS: u32 = 5; \ No newline at end of file +pub const MAX_RECONNECTS: u32 = 5; + +pub const CLASS_ZONE: &str = "zone"; +pub const CLASS_DOMAIN: &str = "domain"; \ No newline at end of file diff --git a/src/crypto/chacha.rs b/src/crypto/chacha.rs new file mode 100644 index 0000000..48f075e --- /dev/null +++ b/src/crypto/chacha.rs @@ -0,0 +1,55 @@ +use chacha20poly1305::{ChaCha20Poly1305, Key, Nonce}; +use chacha20poly1305::aead::{Aead, NewAead}; +use std::fmt::{Debug, Formatter}; +use std::fmt; + +const FAILURE: &str = "encryption failure!"; + +/// A small wrap-up to use Chacha20 encryption for domain names. +#[derive(Clone)] +pub struct Chacha { + pub cipher: ChaCha20Poly1305 +} + +impl Chacha { + pub fn new(seed: &[u8]) -> Self { + let key = Key::from_slice(seed); + let cipher = ChaCha20Poly1305::new(key); + Chacha { cipher } + } + + pub fn encrypt(&self, data: &[u8], nonce: &[u8]) -> Vec { + let nonce = Nonce::from_slice(nonce); + Vec::from(self.cipher.encrypt(nonce, data.as_ref()).expect(FAILURE)) + } + + pub fn decrypt(&self, data: &[u8], nonce: &[u8]) -> Vec { + let nonce = Nonce::from_slice(nonce); + Vec::from(self.cipher.decrypt(nonce, data.as_ref()).expect(FAILURE)) + } +} + +impl Debug for Chacha { + fn fmt(&self, fmt: &mut Formatter<'_>) -> fmt::Result { + fmt.write_str("ChaCha20Poly1305") + } +} + +#[cfg(test)] +mod tests { + use crate::crypto::Chacha; + use crate::to_hex; + + #[test] + pub fn test_curved_chacha() { + let buf = b"178135D209C697625E3EC71DA5C760382E54936F824EE5083908DA66B14ECE18"; + let keys1 = Chacha::new(b"178135D209C697625E3EC71DA5C76038", ); + let bytes = keys1.encrypt(b"TEST", &buf[..12]); + println!("{}", to_hex(&bytes)); + + let keys2 = Chacha::new(b"178135D209C697625E3EC71DA5C76038"); + let bytes2 = keys2.decrypt(&bytes, &buf[..12]); + + assert_eq!(String::from_utf8(bytes2).unwrap(), "TEST"); + } +} \ No newline at end of file diff --git a/src/crypto/mod.rs b/src/crypto/mod.rs new file mode 100644 index 0000000..f371de4 --- /dev/null +++ b/src/crypto/mod.rs @@ -0,0 +1,3 @@ +mod chacha; + +pub use chacha::Chacha; \ No newline at end of file diff --git a/src/event.rs b/src/event.rs index 140b776..2747efa 100644 --- a/src/event.rs +++ b/src/event.rs @@ -4,6 +4,7 @@ use crate::{Bytes, Keystore}; pub enum Event { MinerStarted, MinerStopped { success: bool, full: bool }, + MinerStats { thread: usize, speed: u64, max_diff: u32 }, KeyGeneratorStarted, KeyGeneratorStopped, KeyCreated { path: String, public: String, hash: String }, diff --git a/src/keys.rs b/src/keys.rs index c64bd12..1ff2157 100644 --- a/src/keys.rs +++ b/src/keys.rs @@ -27,36 +27,42 @@ use self::ed25519_dalek::{Signer, PublicKey, Verifier, SecretKey}; use self::ed25519_dalek::ed25519::signature::Signature; use rand_old::{CryptoRng, RngCore}; use rand_old::rngs::OsRng; +use crate::crypto::Chacha; #[derive(Debug)] pub struct Keystore { keypair: Keypair, hash: RefCell, path: String, + chacha: Chacha } impl Keystore { pub fn new() -> Self { let mut csprng = OsRng::default(); let keypair = ed25519_dalek::Keypair::generate(&mut csprng); - Keystore { keypair, hash: RefCell::new(Bytes::default()), path: String::new() } + let chacha = get_chacha(&keypair); + Keystore { keypair, hash: RefCell::new(Bytes::default()), path: String::new(), chacha } } pub fn from_random(csprng: &mut R) -> Self where R: CryptoRng + RngCore { let keypair = ed25519_dalek::Keypair::generate(csprng); - Keystore { keypair, hash: RefCell::new(Bytes::default()), path: String::new() } + let chacha = get_chacha(&keypair); + Keystore { keypair, hash: RefCell::new(Bytes::default()), path: String::new(), chacha } } pub fn from_bytes(seed: &[u8]) -> Self { let keypair = Keypair::from_bytes(seed).expect("Error creating keypair from bytes!"); - Keystore { keypair, hash: RefCell::new(Bytes::default()), path: String::new() } + let chacha = get_chacha(&keypair); + Keystore { keypair, hash: RefCell::new(Bytes::default()), path: String::new(), chacha } } pub fn from_random_bytes(key: &[u8]) -> Self { let secret = SecretKey::from_bytes(&key).unwrap(); let public = PublicKey::from(&secret); let keypair = Keypair { secret, public }; - Keystore { keypair, hash: RefCell::new(Bytes::default()), path: String::new() } + let chacha = get_chacha(&keypair); + Keystore { keypair, hash: RefCell::new(Bytes::default()), path: String::new(), chacha } } pub fn from_file(filename: &str, _password: &str) -> Option { @@ -131,12 +137,22 @@ impl Keystore { Err(_) => { false } } } + + pub fn encrypt(&self, message: &[u8], nonce: &[u8]) -> Bytes { + let encrypted = self.chacha.encrypt(message, nonce); + Bytes::from_bytes(&encrypted) + } + + pub fn decrypt(&self, message: &[u8], nonce: &[u8]) -> Bytes { + let decrypted = self.chacha.decrypt(message, nonce); + Bytes::from_bytes(&decrypted) + } } impl Clone for Keystore { fn clone(&self) -> Self { let keypair = Keypair::from_bytes(&self.keypair.to_bytes()).unwrap(); - Self { keypair, hash: RefCell::new(Bytes::default()), path: self.path.clone() } + Self { keypair, hash: RefCell::new(Bytes::default()), path: self.path.clone(), chacha: self.chacha.clone() } } } @@ -148,9 +164,9 @@ impl PartialEq for Keystore { /// Checks if some public key is "strong" enough to mine domains /// TODO Optimize by caching Blakeout somewhere -pub fn check_public_key_strength(key: &Bytes, strength: usize) -> bool { +pub fn check_public_key_strength(key: &Bytes, strength: u32) -> bool { let bytes = blakeout_data(&key); - hash_is_good(&bytes, strength) + hash_difficulty_key(&bytes) >= strength } pub fn create_key(context: Arc>) { @@ -198,7 +214,7 @@ pub fn create_key(context: Arc>) { }); } -fn generate_key(difficulty: usize, mining: Arc) -> Option { +fn generate_key(difficulty: u32, mining: Arc) -> Option { use self::rand::RngCore; let mut rng = rand::thread_rng(); let mut time = Instant::now(); @@ -210,7 +226,7 @@ fn generate_key(difficulty: usize, mining: Arc) -> Option let keystore = Keystore::from_random_bytes(&buf); digest.reset(); digest.update(keystore.get_public().as_slice()); - if hash_is_good(digest.result(), difficulty) { + if hash_difficulty_key(digest.result()) >= difficulty { info!("Generated keypair with public key: {:?} and hash {:?}", &keystore.get_public(), &keystore.get_hash()); return Some(keystore); } @@ -227,6 +243,13 @@ fn generate_key(difficulty: usize, mining: Arc) -> Option } } +fn get_chacha(keypair: &Keypair) -> Chacha { + let mut digest = Blakeout::new(); + digest.update(&keypair.to_bytes()); + let seed = digest.result(); + Chacha::new(seed) +} + #[cfg(test)] mod tests { use crate::Keystore; diff --git a/src/lib.rs b/src/lib.rs index b2e4ef8..992733f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,4 +25,5 @@ pub mod dns_utils; pub mod settings; pub mod bytes; pub mod x_zones; +pub mod crypto; diff --git a/src/main.rs b/src/main.rs index c8b2640..bbd22cf 100644 --- a/src/main.rs +++ b/src/main.rs @@ -15,8 +15,7 @@ use simple_logger::SimpleLogger; #[cfg(windows)] use winapi::um::wincon::{ATTACH_PARENT_PROCESS, AttachConsole, FreeConsole}; -use alfis::{Block, Bytes, Chain, Miner, Context, Network, Settings, dns_utils, Keystore}; -use alfis::commons::BLOCK_DIFFICULTY; +use alfis::{Block, Bytes, Chain, Miner, Context, Network, Settings, dns_utils, Keystore, ZONE_DIFFICULTY}; #[cfg(feature = "webgui")] mod web_ui; @@ -151,10 +150,10 @@ fn create_genesis_if_needed(context: &Arc>, miner: &Arc threads }; debug!("Starting {} threads for mining", threads); - for _cpu in 0..threads { + for cpu in 0..threads { let context = Arc::clone(&context); let job = job.clone(); let mining = Arc::clone(&mining); @@ -156,7 +156,7 @@ impl Miner { thread::spawn(move || { live_threads.fetch_add(1, Ordering::SeqCst); let full = job.block.transaction.is_some(); - match find_hash(Arc::clone(&context), job.block, Arc::clone(&mining)) { + match find_hash(Arc::clone(&context), job.block, Arc::clone(&mining), cpu) { None => { debug!("Mining was cancelled"); let count = live_threads.fetch_sub(1, Ordering::SeqCst); @@ -199,12 +199,14 @@ pub struct MineJob { keystore: Keystore } -fn find_hash(context: Arc>, mut block: Block, running: Arc) -> Option { - let difficulty = block.difficulty as usize; +fn find_hash(context: Arc>, mut block: Block, running: Arc, thread: usize) -> Option { + let difficulty = block.difficulty; let full = block.transaction.is_some(); let mut digest = Blakeout::new(); + let mut max_diff = 0; loop { block.random = rand::random(); + block.timestamp = Utc::now().timestamp(); let next_allowed_block = { let context = context.lock().unwrap(); // We use this block to fill some fields of our block as well @@ -221,24 +223,43 @@ fn find_hash(context: Arc>, mut block: Block, running: Arc= difficulty { block.hash = Bytes::from_bytes(digest.result()); return Some(block); } + if diff > max_diff { + max_diff = diff; + } - if nonce % 1000 == 0 { - if let Ok(context) = context.lock() { - if context.chain.height() >= block.index { - break; + let elapsed = time.elapsed().as_millis(); + if elapsed >= 1000 { + block.timestamp = Utc::now().timestamp(); + if elapsed > 5000 { + let speed = (nonce - prev_nonce) / (elapsed as u64 / 1000); + //debug!("Mining speed {} H/s, max difficulty {}", speed, max_diff); + if let Ok(mut context) = context.lock() { + context.bus.post(Event::MinerStats { thread, speed, max_diff}) + } + time = Instant::now(); + prev_nonce = nonce; + } + + if block.index > 1 { + if let Ok(context) = context.lock() { + if context.chain.height() >= block.index { + break; + } } } } diff --git a/src/p2p/network.rs b/src/p2p/network.rs index 506344e..b18d3e0 100644 --- a/src/p2p/network.rs +++ b/src/p2p/network.rs @@ -16,9 +16,10 @@ use log::{trace, debug, info, warn, error}; use std::net::{SocketAddr, IpAddr, SocketAddrV4, Shutdown}; use std::collections::HashSet; use crate::{Context, Block, p2p::Message, p2p::State, p2p::Peer, p2p::Peers, Bytes, is_yggdrasil}; -use crate::blockchain::enums::BlockQuality; +use crate::blockchain::types::BlockQuality; use crate::commons::CHAIN_VERSION; use std::sync::atomic::{AtomicBool, Ordering}; +use chrono::Utc; const SERVER: Token = Token(0); const POLL_TIMEOUT: Option = Some(Duration::from_millis(3000)); @@ -49,7 +50,7 @@ impl Network { let mut server = TcpListener::bind(addr).expect("Can't bind to address"); debug!("Started node listener on {}", server.local_addr().unwrap()); - let mut events = Events::with_capacity(64); + let mut events = Events::with_capacity(1024); let mut poll = Poll::new().expect("Unable to create poll"); poll.registry().register(&mut server, SERVER, Interest::READABLE).expect("Error registering poll"); let context = Arc::clone(&self.context); @@ -132,7 +133,7 @@ impl Network { } events.clear(); - if peers_timer.elapsed().as_millis() > 100 { + if peers_timer.elapsed().as_millis() > 500 { // Send pings to idle peers let (height, hash) = { let mut context = context.lock().unwrap(); @@ -143,7 +144,7 @@ impl Network { } (height, context.chain.last_hash()) }; - mine_locker_block(Arc::clone(&context)); + mine_signing_block(Arc::clone(&context)); peers.send_pings(poll.registry(), height, hash); peers.connect_new_peers(poll.registry(), &mut unique_token, yggdrasil_only); peers_timer = Instant::now(); @@ -243,6 +244,7 @@ fn handle_connection_event(context: Arc>, peers: &mut Peers, regi if event.is_writable() { //trace!("Socket {} is writable", event.token().0); + let my_id = peers.get_my_id().to_owned(); match peers.get_mut_peer(&event.token()) { None => {} Some(peer) => { @@ -251,7 +253,7 @@ fn handle_connection_event(context: Arc>, peers: &mut Peers, regi debug!("Connected to peer {}, sending hello...", &peer.get_addr()); let data: String = { let c = context.lock().unwrap(); - let message = Message::hand(&c.app_version, &c.settings.origin, CHAIN_VERSION, c.settings.net.public, peer.get_rand()); + let message = Message::hand(&c.app_version, &c.settings.origin, CHAIN_VERSION, c.settings.net.public, &my_id); serde_json::to_string(&message).unwrap() }; send_message(peer.get_stream(), &data.into_bytes()).unwrap_or_else(|e| warn!("Error sending hello {}", e)); @@ -471,10 +473,16 @@ fn handle_message(context: Arc>, message: Message, peers: &mut Pe } BlockQuality::Twin => { debug!("Ignoring duplicate block {}", block.index); } BlockQuality::Future => { debug!("Ignoring future block {}", block.index); } - BlockQuality::Bad => { debug!("Ignoring bad block {} with hash {:?}", block.index, block.hash); } - // TODO deal with forks + BlockQuality::Bad => { + // TODO save bad public keys to banned table + debug!("Ignoring bad block {} with hash {:?}", block.index, block.hash); + } BlockQuality::Fork => { - debug!("Ignoring forked block {} with hash {:?}", block.index, block.hash); + debug!("Got forked block {} with hash {:?}", block.index, block.hash); + let last_block = context.chain.last_block().unwrap(); + if block.is_better_than(&last_block) { + context.chain.replace_block(block.index, block).expect("Error replacing block with fork"); + } //let peer = peers.get_mut_peer(token).unwrap(); //deal_with_fork(context, peer, block); } @@ -487,21 +495,24 @@ fn handle_message(context: Arc>, message: Message, peers: &mut Pe } /// Sends an Event to miner to start mining locker block if "locker" is our public key -fn mine_locker_block(context: Arc>) { +fn mine_signing_block(context: Arc>) { let mut context = context.lock().unwrap(); - if let Some(block) = context.chain.last_block() { + if let Some(block) = context.chain.get_last_full_block(None) { + if block.timestamp + 60 > Utc::now().timestamp() { + return; + } if let Some(keystore) = &context.keystore { if block.index < context.chain.max_height() { - trace!("No locker mining while syncing"); + trace!("No signing while syncing"); return; } - let lockers: HashSet = context.chain.get_block_lockers(&block).into_iter().collect(); - if lockers.contains(&keystore.get_public()) { - info!("We have an honor to mine locker block!"); + let signers: HashSet = context.chain.get_block_signers(&block).into_iter().collect(); + if signers.contains(&keystore.get_public()) { + info!("We have an honor to mine signing block!"); let keystore = Box::new(keystore.clone()); context.bus.post(crate::event::Event::ActionMineLocker { index: block.index + 1, hash: block.hash, keystore }); - } else if !lockers.is_empty() { - info!("Locker block must be mined by other nodes"); + } else if !signers.is_empty() { + info!("Signing block must be mined by other nodes"); } } } diff --git a/src/p2p/peer.rs b/src/p2p/peer.rs index ad49b96..6603c05 100644 --- a/src/p2p/peer.rs +++ b/src/p2p/peer.rs @@ -2,14 +2,14 @@ use std::net::SocketAddr; use std::collections::HashMap; use mio::net::TcpStream; use crate::p2p::State; -use crate::{Block, commons}; +use crate::Block; #[derive(Debug)] pub struct Peer { addr: SocketAddr, stream: TcpStream, state: State, - rand: String, + id: String, height: u64, inbound: bool, public: bool, @@ -26,7 +26,7 @@ impl Peer { addr, stream, state, - rand: commons::random_string(6), + id: String::new(), height: 0, inbound, public: false, @@ -58,8 +58,8 @@ impl Peer { self.state = state; } - pub fn get_rand(&self) -> &str { - &self.rand + pub fn get_id(&self) -> &str { + &self.id } pub fn set_height(&mut self, height: u64) { diff --git a/src/p2p/peers.rs b/src/p2p/peers.rs index 953c027..14dd197 100644 --- a/src/p2p/peers.rs +++ b/src/p2p/peers.rs @@ -9,20 +9,21 @@ use rand::random; use rand::seq::IteratorRandom; #[allow(unused_imports)] use log::{trace, debug, info, warn, error}; -use crate::{Bytes, is_yggdrasil}; +use crate::{Bytes, is_yggdrasil, commons}; use crate::commons::MAX_RECONNECTS; pub struct Peers { peers: HashMap, new_peers: Vec, - ignored: HashSet + ignored: HashSet, + my_id: String } const PING_PERIOD: u64 = 60; impl Peers { pub fn new() -> Self { - Peers { peers: HashMap::new(), new_peers: Vec::new(), ignored: HashSet::new() } + Peers { peers: HashMap::new(), new_peers: Vec::new(), ignored: HashSet::new(), my_id: commons::random_string(6) } } pub fn add_peer(&mut self, token: Token, peer: Peer) { @@ -133,13 +134,12 @@ impl Peers { } } + pub fn get_my_id(&self) -> &str { + &self.my_id + } + pub fn is_our_own_connect(&self, rand: &str) -> bool { - match self.peers.values().find(|p| p.get_rand() == rand) { - None => { false } - Some(p) => { - !p.is_inbound() - } - } + self.my_id.eq(rand) } pub fn get_peers_for_exchange(&self, peer_address: &SocketAddr) -> Vec { diff --git a/src/settings.rs b/src/settings.rs index 281584e..c521e7e 100644 --- a/src/settings.rs +++ b/src/settings.rs @@ -1,5 +1,5 @@ use std::fs::File; -use std::io::{Read,}; +use std::io::Read; use serde::{Deserialize, Serialize}; #[allow(unused_imports)] diff --git a/src/web_ui.rs b/src/web_ui.rs index 39f79bd..f20e1b7 100644 --- a/src/web_ui.rs +++ b/src/web_ui.rs @@ -8,22 +8,23 @@ use std::sync::{Arc, Mutex}; use std::thread; use std::time::{Duration, Instant}; -use web_view::Content; +use chrono::{DateTime, Local}; #[allow(unused_imports)] use log::{debug, error, info, LevelFilter, trace, warn}; use serde::Deserialize; +use web_view::Content; -use alfis::{Block, Bytes, Context, Keystore, Transaction, get_domain_zone}; -use alfis::miner::Miner; -use alfis::{keys, check_domain}; -use alfis::event::Event; -use alfis::dns::protocol::DnsRecord; -use alfis::commons::{ZONE_MAX_LENGTH, ZONE_DIFFICULTY}; -use Cmd::*; +use alfis::{Block, Bytes, Context, get_domain_zone, Keystore, Transaction, ZONE_MIN_DIFFICULTY}; +use alfis::{check_domain, keys}; use alfis::blockchain::transaction::{DomainData, ZoneData}; -use self::web_view::{WebView, Handle}; -use alfis::blockchain::enums::MineResult; -use chrono::{DateTime, Local}; +use alfis::blockchain::types::MineResult; +use alfis::commons::{ZONE_DIFFICULTY, ZONE_MAX_LENGTH, CLASS_DOMAIN, CLASS_ZONE}; +use alfis::dns::protocol::DnsRecord; +use alfis::event::Event; +use alfis::miner::Miner; +use Cmd::*; + +use self::web_view::{Handle, WebView}; pub fn run_interface(context: Arc>, miner: Arc>) { let file_content = include_str!("webview/index.html"); @@ -51,12 +52,14 @@ pub fn run_interface(context: Arc>, miner: Arc>) { SaveKey => { action_save_key(&context); } CheckRecord { data } => { action_check_record(web_view, data); } CheckDomain { name } => { action_check_domain(&context, web_view, name); } - MineDomain { name, records } => { - action_create_domain(Arc::clone(&context), Arc::clone(&miner), web_view, name, &records); + MineDomain { name, data } => { + action_create_domain(Arc::clone(&context), Arc::clone(&miner), web_view, name, data); } TransferDomain { .. } => {} CheckZone { name } => { action_check_zone(&context, web_view, name); } - MineZone { name, data } => { action_create_zone(Arc::clone(&context), Arc::clone(&miner), web_view, name, data); } + MineZone { name, data } => { + action_create_zone(Arc::clone(&context), Arc::clone(&miner), web_view, name, data); + } StopMining => { context.lock().unwrap().bus.post(Event::ActionStopMining); } Open { link } => { if open::that(&link).is_err() { @@ -165,7 +168,7 @@ fn action_load_key(context: &Arc>, web_view: &mut WebView<()>) { None => { error!("Error loading keystore '{}'!", &file_name); show_warning(web_view, "Error loading key!
Key cannot be loaded or its difficulty is not enough."); - event_fail(web_view, &format!("Error loading key from '{}'!", &file_name)); + event_fail(web_view, &format!("Error loading key from \\'{}\\'!", &file_name)); } Some(keystore) => { info!("Loaded keystore with key: {:?}", &keystore.get_public()); @@ -184,7 +187,12 @@ fn action_load_key(context: &Arc>, web_view: &mut WebView<()>) { fn action_loaded(context: &Arc>, web_view: &mut WebView<()>) { web_view.eval("showMiningIndicator(false, false);").expect("Error evaluating!"); let handle: Handle<()> = web_view.handle(); - let status = Arc::new(Mutex::new(Status::new())); + let threads = context.lock().unwrap().settings.mining.threads; + let threads = match threads { + 0 => num_cpus::get(), + _ => threads + }; + let status = Arc::new(Mutex::new(Status::new(threads))); let context_copy = Arc::clone(&context); let mut c = context.lock().unwrap(); c.bus.register(move |_uuid, e| { @@ -199,7 +207,7 @@ fn action_loaded(context: &Arc>, web_view: &mut WebView<()>) { Event::KeyCreated { path, public, hash } => { event_handle_luck(&handle, "Key successfully created! Don\\'t forget to save it!"); let mut s = format!("keystoreChanged('{}', '{}', '{}');", &path, &public, &hash); - s.push_str(" showSuccess('You've got a new key! Don't forget to save it!')"); + s.push_str(" showSuccess('You\\'ve got a new key! Don\\'t forget to save it!')"); s } Event::KeyLoaded { path, public, hash } | @@ -211,7 +219,7 @@ fn action_loaded(context: &Arc>, web_view: &mut WebView<()>) { event_handle_info(&handle, "Mining started"); String::from("setLeftStatusBarText('Mining...'); showMiningIndicator(true, false);") } - Event::MinerStopped {success, full} => { + Event::MinerStopped { success, full} => { status.mining = false; let mut s = if status.syncing { String::from("setLeftStatusBarText('Syncing...'); showMiningIndicator(true, true);") @@ -232,6 +240,17 @@ fn action_loaded(context: &Arc>, web_view: &mut WebView<()>) { } s } + Event::MinerStats { thread, speed, max_diff } => { + if status.max_diff < max_diff { + status.max_diff = max_diff; + } + status.set_thread_speed(thread, speed); + if thread == threads - 1 { + format!("setLeftStatusBarText('Mining speed {} H/s, max found difficulty {}.'); showMiningIndicator(true, false);", status.get_speed(), status.max_diff) + } else { + String::new() + } + } Event::KeyGeneratorStopped => { status.mining = false; if status.syncing { @@ -300,8 +319,8 @@ fn action_loaded(context: &Arc>, web_view: &mut WebView<()>) { event_info(web_view, "Application loaded"); } -fn action_create_domain(context: Arc>, miner: Arc>, web_view: &mut WebView<()>, name: String, records: &String) { - debug!("Creating domain with records: {}", records); +fn action_create_domain(context: Arc>, miner: Arc>, web_view: &mut WebView<()>, name: String, data: String) { + debug!("Creating domain with data: {}", &data); let c = Arc::clone(&context); let context = context.lock().unwrap(); if context.get_keystore().is_none() { @@ -310,18 +329,25 @@ fn action_create_domain(context: Arc>, miner: Arc>, } let keystore = context.get_keystore().unwrap(); let pub_key = keystore.get_public(); - match context.chain.can_mine_domain(&name, &records, &pub_key) { + let mut data = match serde_json::from_str::(&data) { + Ok(data) => { data } + Err(_) => { + show_warning(web_view, "Something wrong with domain data. I cannot mine it."); + return; + } + }; + match context.chain.can_mine_domain(&name, &pub_key) { MineResult::Fine => { let zone = get_domain_zone(&name); let difficulty = context.chain.get_zone_difficulty(&zone); - if let Ok(records) = serde_json::from_str::>(&records) { - let data = DomainData::new(zone.clone(), records); - let data = serde_json::to_string(&data).unwrap(); - std::mem::drop(context); - create_domain(c, miner, &name, &data, difficulty, &keystore); - let _ = web_view.eval("domainMiningStarted();"); - event_info(web_view, &format!("Mining of domain \\'{}\\' has started", &name)); - } + let last_block = context.chain.last_block().unwrap(); + let encrypted = keystore.encrypt(name.as_bytes(), &last_block.hash.as_slice()[..12]); + data.domain = encrypted; + let data = serde_json::to_string(&data).unwrap(); + std::mem::drop(context); + create_domain(c, miner, CLASS_DOMAIN, &name, &data, difficulty, &keystore); + let _ = web_view.eval("domainMiningStarted();"); + event_info(web_view, &format!("Mining of domain \\'{}\\' has started", &name)); } MineResult::WrongName => { show_warning(web_view, "You can't mine this domain!"); } MineResult::WrongData => { show_warning(web_view, "You have an error in records!"); } @@ -343,24 +369,45 @@ fn action_create_zone(context: Arc>, miner: Arc>, we return; } let data = data.to_lowercase(); - if serde_json::from_str::(&data).is_err() { - warn!("Something wrong with zone data!"); - show_warning(web_view, "Something wrong with zone data!"); - return; - } + let mut data = match serde_json::from_str::(&data) { + Ok(zone) => { + if zone.difficulty < ZONE_MIN_DIFFICULTY { + warn!("Zone difficulty cannot be lower than {}!", ZONE_MIN_DIFFICULTY); + show_warning(web_view, &format!("Zone difficulty cannot be lower than {}!", ZONE_MIN_DIFFICULTY)); + return; + } + if name != zone.name { + warn!("Something wrong with zone data!"); + show_warning(web_view, "Something wrong with zone data!"); + return; + } + zone + } + Err(_) => { + warn!("Something wrong with zone data!"); + show_warning(web_view, "Something wrong with zone data!"); + return; + } + }; let (keystore, transaction) = { let context = context.lock().unwrap(); (context.get_keystore(), context.chain.get_domain_transaction(&name)) }; if let Some(keystore) = keystore { + data.owners = if data.owners.is_empty() { + vec!(keystore.get_public()) + } else { + data.owners + }; + let data = serde_json::to_string(&data).unwrap(); match transaction { None => { - create_domain(Arc::clone(&context), miner.clone(), &name, &data, ZONE_DIFFICULTY, &keystore); + create_domain(Arc::clone(&context), miner.clone(), CLASS_ZONE, &name, &data, ZONE_DIFFICULTY, &keystore); event_info(web_view, &format!("Mining of zone \\'{}\\' has started", &name)); } Some(transaction) => { if transaction.pub_key == keystore.get_public() { - create_domain(Arc::clone(&context), miner.clone(), &name, &data, ZONE_DIFFICULTY, &keystore); + create_domain(Arc::clone(&context), miner.clone(), CLASS_ZONE, &name, &data, ZONE_DIFFICULTY, &keystore); event_info(web_view, &format!("Mining of zone \\'{}\\' has started", &name)); } else { warn!("Tried to mine not owned domain!"); @@ -448,14 +495,14 @@ fn format_event_now(kind: &str, message: &str) -> String { format!("addEvent('{}', '{}', '{}');", kind, time.format("%d.%m.%y %X"), message) } -fn create_domain(context: Arc>, miner: Arc>, name: &str, data: &str, difficulty: u32, keystore: &Keystore) { +fn create_domain(context: Arc>, miner: Arc>, class: &str, name: &str, data: &str, difficulty: u32, keystore: &Keystore) { let name = name.to_owned(); info!("Generating domain or zone {}", &name); if context.lock().unwrap().x_zones.has_zone(&name) { error!("Unable to mine IANA/OpenNIC/etc zone {}!", &name); return; } - let transaction = Transaction::from_str(name, "dns".to_owned(), data.to_owned(), keystore.get_public().clone()); + let transaction = Transaction::from_str(name, class.to_owned(), data.to_owned(), keystore.get_public().clone()); let block = Block::new(Some(transaction), keystore.get_public(), Bytes::default(), difficulty); miner.lock().unwrap().add_block(block, keystore.clone()); } @@ -471,7 +518,7 @@ pub enum Cmd { MineZone { name: String, data: String }, CheckRecord { data: String }, CheckDomain { name: String }, - MineDomain { name: String, records: String }, + MineDomain { name: String, data: String }, TransferDomain { name: String, owner: String }, StopMining, Open { link: String }, @@ -483,12 +530,24 @@ struct Status { pub synced_blocks: u64, pub sync_height: u64, pub nodes_connected: usize, - pub chain_height: u64 + pub chain_height: u64, + pub max_diff: u32, + pub speed: Vec } impl Status { - fn new() -> Self { - Status { mining: false, syncing: false, synced_blocks: 0, sync_height: 0, nodes_connected: 0, chain_height: 0 } + fn new(threads: usize) -> Self { + let mut speed = Vec::with_capacity(threads); + speed.resize(threads, 0u64); + Status { mining: false, syncing: false, synced_blocks: 0, sync_height: 0, nodes_connected: 0, chain_height: 0, max_diff: 0, speed } + } + + fn set_thread_speed(&mut self, thread: usize, speed: u64) { + self.speed[thread] = speed; + } + + fn get_speed(&self) -> u64 { + self.speed.iter().sum() } } diff --git a/src/webview/index.html b/src/webview/index.html index 88e2b85..2d8d253 100644 --- a/src/webview/index.html +++ b/src/webview/index.html @@ -60,7 +60,7 @@
- + @@ -130,13 +130,17 @@
- +
+

If you feel that we need another zone you can mine that too. Just select a name, a difficulty for domains in that zone, and hit "Mine zone".

diff --git a/src/webview/scripts.js b/src/webview/scripts.js index 8ddc488..8170591 100644 --- a/src/webview/scripts.js +++ b/src/webview/scripts.js @@ -161,6 +161,12 @@ function createDomain() { var new_domain = document.getElementById("new_domain").value.toLowerCase(); var new_dom_records = JSON.stringify(recordsBuffer); var domain = new_domain + "." + currentZone.name; + var data = {}; + data.domain = []; + data.zone = currentZone.name; + data.records = new_dom_records; + data.owners = []; // TODO make a dialog to fill them + data.contacts = []; // TODO make a dialog to fill them external.invoke(JSON.stringify({cmd: 'mineDomain', name: domain, records: new_dom_records})); } @@ -172,9 +178,12 @@ function domainMiningStarted() { function createZone() { var new_zone = document.getElementById("new_zone").value; var difficulty = document.getElementById("new_zone_difficulty").value; - obj = {}; + var yggdrasil = document.getElementById("yggdrasil_only").checked; + var obj = {}; obj.name = new_zone; obj.difficulty = parseInt(difficulty); + obj.yggdrasil = yggdrasil; + obj.owners = []; // TODO make a dialog to fill them data = JSON.stringify(obj); external.invoke(JSON.stringify({cmd: 'mineZone', name: new_zone, data: data})); } @@ -338,9 +347,9 @@ function keystoreChanged(path, pub_key, hash) { if (path == '') { path = "In memory"; } - var public_key_hash = document.getElementById("public_key_hash"); - public_key_hash.value = hash; - public_key_hash.title = path + "\n" + pub_key; + var public_key_field = document.getElementById("public_key"); + public_key_field.value = pub_key; + public_key_field.title = path + "\n" + hash; var save_key = document.getElementById("save_key"); save_key.disabled = false;