Changed identity format in transactions, now it will be double Sha256.

Added new checks for forked blocks.
Added options table.
Added posibility to replace blocks in DB by more appropriate forks.
Divided transactions table to domains and zones tables.
Added a timestamp to domains and zones table, it will give us possibility to purge old domains.
Changed difficulty check to check head and tail (sum of them) of the hash.
Added encrypted (by Chacha20) domain name to DomainData, added contacts and owners vectors for it too.
Added yggdrasil flag to ZoneData - it will restrict all IPs for domains in particular zone to Yggdrasil only.
Changed difficulties of various block types.
Added a temporary (for a run) unique ID to all handshakes.
Start of signing blocks mining will be after 60 seconds after full block.
Added mining status to statusbar.
This commit is contained in:
Revertron
2021-04-10 09:47:21 +02:00
parent e06088e0b0
commit a74a0733ac
24 changed files with 700 additions and 273 deletions
+4 -2
View File
@@ -1,6 +1,6 @@
[package]
name = "alfis"
version = "0.3.14"
version = "0.4.0"
authors = ["Revertron <alfis@revertron.com>"]
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]
+13 -2
View File
@@ -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<u8> {
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
}
}
+219 -117
View File
@@ -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<Block>,
last_block: Option<Block>,
last_full_block: Option<Block>,
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<Block> = 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<db> :)
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::<String>(0).unwrap();
let value = statement.read::<String>(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<State> {
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<State> {
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<State> {
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<State> {
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::<Vec<u8>>(0).unwrap());
@@ -274,7 +354,7 @@ impl Chain {
pub fn get_zones(&self) -> Vec<ZoneData> {
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::<String>(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::<Vec<DnsRecord>>(&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::<Vec<u8>>(1).unwrap());
let confirmation = Bytes::from_bytes(&statement.read::<Vec<u8>>(2).unwrap());
let method = statement.read::<String>(3).unwrap();
let data = statement.read::<String>(4).unwrap();
let pub_key = Bytes::from_bytes(&statement.read::<Vec<u8>>(5).unwrap());
let transaction = Transaction { identity, confirmation, method, data, pub_key };
let timestamp = statement.read::<i64>(1).unwrap();
if timestamp < Utc::now().timestamp() - DOMAIN_LIFETIME {
// This domain is too old
return None;
}
let identity = Bytes::from_bytes(&statement.read::<Vec<u8>>(2).unwrap());
let confirmation = Bytes::from_bytes(&statement.read::<Vec<u8>>(3).unwrap());
let method = statement.read::<String>(4).unwrap();
let data = statement.read::<String>(5).unwrap();
let pub_key = Bytes::from_bytes(&statement.read::<Vec<u8>>(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,19 +467,14 @@ 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::<ZoneData>(&transaction.data) {
Ok(data) => { data.difficulty }
Err(_) => {
warn!("Wrong data for zone {}!", zone);
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<Block> {
self.last_block.clone()
@@ -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::<DomainData>(&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::<DomainData>(&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<Bytes> = 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<Bytes> = 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<Bytes> {
pub fn get_block_signers(&self, block: &Block) -> Vec<Bytes> {
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
}
-20
View File
@@ -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 }
}
+63 -13
View File
@@ -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);
return hash_int < target;
/// 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()
}
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<u8> {
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);
}
}
+1 -1
View File
@@ -7,5 +7,5 @@ pub mod block;
pub mod chain;
pub mod filter;
pub mod hash_utils;
pub mod enums;
pub mod types;
+36
View File
@@ -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);
+28 -9
View File
@@ -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<Self> {
@@ -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,20 +81,27 @@ impl Serialize for Transaction {
#[derive(Clone, Serialize, Deserialize, PartialEq)]
pub struct DomainData {
pub domain: Bytes,
pub zone: String,
pub records: Vec<DnsRecord>
pub records: Vec<DnsRecord>,
pub contacts: Vec<ContactsData>,
#[serde(default)]
pub owners: Vec<Bytes>
}
impl DomainData {
pub fn new(zone: String, records: Vec<DnsRecord>) -> Self {
Self { zone, records }
pub fn new(domain: Bytes, zone: String, records: Vec<DnsRecord>, contacts: Vec<ContactsData>, owners: Vec<Bytes>) -> 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<Bytes>
}
impl Display for ZoneData {
@@ -102,3 +109,15 @@ impl Display for ZoneData {
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))
}
}
+36
View File
@@ -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 }
}
}
+11 -6
View File
@@ -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;
pub const CLASS_ZONE: &str = "zone";
pub const CLASS_DOMAIN: &str = "domain";
+55
View File
@@ -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<u8> {
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<u8> {
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");
}
}
+3
View File
@@ -0,0 +1,3 @@
mod chacha;
pub use chacha::Chacha;
+1
View File
@@ -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 },
+32 -9
View File
@@ -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<Bytes>,
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<R>(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<Self> {
@@ -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<Mutex<Context>>) {
@@ -198,7 +214,7 @@ pub fn create_key(context: Arc<Mutex<Context>>) {
});
}
fn generate_key(difficulty: usize, mining: Arc<AtomicBool>) -> Option<Keystore> {
fn generate_key(difficulty: u32, mining: Arc<AtomicBool>) -> Option<Keystore> {
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<AtomicBool>) -> Option<Keystore>
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<AtomicBool>) -> Option<Keystore>
}
}
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;
+1
View File
@@ -25,4 +25,5 @@ pub mod dns_utils;
pub mod settings;
pub mod bytes;
pub mod x_zones;
pub mod crypto;
+3 -4
View File
@@ -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<Mutex<Context>>, miner: &Arc<Mutex<Min
let context = context.lock().unwrap();
let last_block = context.get_chain().last_block();
let origin = context.settings.origin.clone();
if origin.eq("") && last_block.is_none() {
if origin.is_empty() && last_block.is_none() {
if let Some(keystore) = &context.keystore {
// If blockchain is empty, we are going to mine a Genesis block
let block = Block::new(None, context.get_keystore().unwrap().get_public(), Bytes::default(), BLOCK_DIFFICULTY);
let block = Block::new(None, context.get_keystore().unwrap().get_public(), Bytes::default(), ZONE_DIFFICULTY);
miner.lock().unwrap().add_block(block, keystore.clone());
}
}
+30 -9
View File
@@ -1,7 +1,7 @@
use std::sync::{Arc, Condvar, Mutex};
use std::sync::atomic::{AtomicBool, AtomicU32, Ordering};
use std::thread;
use std::time::Duration;
use std::time::{Duration, Instant};
use chrono::Utc;
#[allow(unused_imports)]
@@ -10,7 +10,7 @@ use num_cpus;
use crate::{Block, Bytes, Context, Keystore};
use crate::commons::{CHAIN_VERSION, LOCKER_DIFFICULTY, KEYSTORE_DIFFICULTY};
use crate::blockchain::enums::BlockQuality;
use crate::blockchain::types::BlockQuality;
use crate::blockchain::hash_utils::*;
use crate::keys::check_public_key_strength;
use crate::event::Event;
@@ -148,7 +148,7 @@ impl Miner {
_ => 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<Mutex<Context>>, mut block: Block, running: Arc<AtomicBool>) -> Option<Block> {
let difficulty = block.difficulty as usize;
fn find_hash(context: Arc<Mutex<Context>>, mut block: Block, running: Arc<AtomicBool>, thread: usize) -> Option<Block> {
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,21 +223,39 @@ fn find_hash(context: Arc<Mutex<Context>>, mut block: Block, running: Arc<Atomic
continue;
}
debug!("Mining block {}", serde_json::to_string(&block).unwrap());
let mut time = Instant::now();
let mut prev_nonce = 0;
for nonce in 0..std::u64::MAX {
if !running.load(Ordering::Relaxed) {
return None;
}
block.timestamp = Utc::now().timestamp();
block.nonce = nonce;
digest.reset();
digest.update(&block.as_bytes());
if hash_is_good(digest.result(), difficulty) {
let diff = hash_difficulty(digest.result());
if diff >= difficulty {
block.hash = Bytes::from_bytes(digest.result());
return Some(block);
}
if diff > max_diff {
max_diff = diff;
}
if nonce % 1000 == 0 {
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;
@@ -244,4 +264,5 @@ fn find_hash(context: Arc<Mutex<Context>>, mut block: Block, running: Arc<Atomic
}
}
}
}
}
+29 -18
View File
@@ -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<Duration> = 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<Mutex<Context>>, 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<Mutex<Context>>, 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<Mutex<Context>>, 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<Mutex<Context>>, 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<Mutex<Context>>) {
fn mine_signing_block(context: Arc<Mutex<Context>>) {
let mut context = context.lock().unwrap();
if let Some(block) = context.chain.last_block() {
if let Some(keystore) = &context.keystore {
if block.index < context.chain.max_height() {
trace!("No locker mining while syncing");
if let Some(block) = context.chain.get_last_full_block(None) {
if block.timestamp + 60 > Utc::now().timestamp() {
return;
}
let lockers: HashSet<Bytes> = context.chain.get_block_lockers(&block).into_iter().collect();
if lockers.contains(&keystore.get_public()) {
info!("We have an honor to mine locker block!");
if let Some(keystore) = &context.keystore {
if block.index < context.chain.max_height() {
trace!("No signing while syncing");
return;
}
let signers: HashSet<Bytes> = 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");
}
}
}
+5 -5
View File
@@ -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) {
+9 -9
View File
@@ -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<Token, Peer>,
new_peers: Vec<SocketAddr>,
ignored: HashSet<IpAddr>
ignored: HashSet<IpAddr>,
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<String> {
+1 -1
View File
@@ -1,5 +1,5 @@
use std::fs::File;
use std::io::{Read,};
use std::io::Read;
use serde::{Deserialize, Serialize};
#[allow(unused_imports)]
+93 -34
View File
@@ -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<Mutex<Context>>, miner: Arc<Mutex<Miner>>) {
let file_content = include_str!("webview/index.html");
@@ -51,12 +52,14 @@ pub fn run_interface(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>) {
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<Mutex<Context>>, web_view: &mut WebView<()>) {
None => {
error!("Error loading keystore '{}'!", &file_name);
show_warning(web_view, "Error loading key!<br>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<Mutex<Context>>, web_view: &mut WebView<()>) {
fn action_loaded(context: &Arc<Mutex<Context>>, 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<Mutex<Context>>, 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<Mutex<Context>>, 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<Mutex<Context>>, 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<Mutex<Context>>, web_view: &mut WebView<()>) {
event_info(web_view, "Application loaded");
}
fn action_create_domain(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, web_view: &mut WebView<()>, name: String, records: &String) {
debug!("Creating domain with records: {}", records);
fn action_create_domain(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, 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,19 +329,26 @@ fn action_create_domain(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>,
}
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::<DomainData>(&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::<Vec<DnsRecord>>(&records) {
let data = DomainData::new(zone.clone(), records);
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, &name, &data, difficulty, &keystore);
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!"); }
MineResult::WrongKey => { show_warning(web_view, "You can't mine with current key!"); }
@@ -343,24 +369,45 @@ fn action_create_zone(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, we
return;
}
let data = data.to_lowercase();
if serde_json::from_str::<ZoneData>(&data).is_err() {
let mut data = match serde_json::from_str::<ZoneData>(&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<Mutex<Context>>, miner: Arc<Mutex<Miner>>, name: &str, data: &str, difficulty: u32, keystore: &Keystore) {
fn create_domain(context: Arc<Mutex<Context>>, miner: Arc<Mutex<Miner>>, 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<u64>
}
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()
}
}
+6 -2
View File
@@ -60,7 +60,7 @@
<div class="tab row page" id="tab_credentials">
<div class="field is-grouped">
<div class="control is-expanded has-icons-left">
<input class="input is-expanded" type="text" id="public_key_hash" placeholder="No key loaded" readonly>
<input class="input is-expanded" type="text" id="public_key" placeholder="No key loaded" readonly>
<span class="icon is-small is-left">
<svg viewBox="0 0 24 24" style="width: 20px; height: 20px;"><path d="M12,17A2,2 0 0,0 14,15C14,13.89 13.1,13 12,13A2,2 0 0,0 10,15A2,2 0 0,0 12,17M18,8A2,2 0 0,1 20,10V20A2,2 0 0,1 18,22H6A2,2 0 0,1 4,20V10C4,8.89 4.9,8 6,8H7V6A5,5 0 0,1 12,1A5,5 0 0,1 17,6V8H18M12,3A3,3 0 0,0 9,6V8H15V6A3,3 0 0,0 12,3Z"></path></svg>
</span>
@@ -130,13 +130,17 @@
<div class="control has-icons-left">
<input class="input" type="number" placeholder="Difficulty: 15-30" id="new_zone_difficulty" name="Just a name" oninput="onZoneChange()">
<span class="icon is-small is-left">
<i class="fas fa-fire"></i>
<svg viewBox="0 0 24 24" style="width: 24px; height: 24px;"><path d="M17.66 11.2C17.43 10.9 17.15 10.64 16.89 10.38C16.22 9.78 15.46 9.35 14.82 8.72C13.33 7.26 13 4.85 13.95 3C13 3.23 12.17 3.75 11.46 4.32C8.87 6.4 7.85 10.07 9.07 13.22C9.11 13.32 9.15 13.42 9.15 13.55C9.15 13.77 9 13.97 8.8 14.05C8.57 14.15 8.33 14.09 8.14 13.93C8.08 13.88 8.04 13.83 8 13.76C6.87 12.33 6.69 10.28 7.45 8.64C5.78 10 4.87 12.3 5 14.47C5.06 14.97 5.12 15.47 5.29 15.97C5.43 16.57 5.7 17.17 6 17.7C7.08 19.43 8.95 20.67 10.96 20.92C13.1 21.19 15.39 20.8 17.03 19.32C18.86 17.66 19.5 15 18.56 12.72L18.43 12.46C18.22 12 17.66 11.2 17.66 11.2M14.5 17.5C14.22 17.74 13.76 18 13.4 18.1C12.28 18.5 11.16 17.94 10.5 17.28C11.69 17 12.4 16.12 12.61 15.23C12.78 14.43 12.46 13.77 12.33 13C12.21 12.26 12.23 11.63 12.5 10.94C12.69 11.32 12.89 11.7 13.13 12C13.9 13 15.11 13.44 15.37 14.8C15.41 14.94 15.43 15.08 15.43 15.23C15.46 16.05 15.1 16.95 14.5 17.5H14.5Z"></path></svg>
</span>
</div>
<div class="buttons has-addons">
<button class="button is-info" id="new_zone_button" onclick="createZone();">Mine zone</button>
</div>
</div>
<label class="checkbox mb-1">
<input type="checkbox" id="yggdrasil_only">
Restrict this zone to <a onclick="open_link('https://yggdrasil-network.github.io');">Yggdrasil</a> only.
</label>
<p class="help">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".</p>
</div>
+13 -4
View File
@@ -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;