2021-02-14 18:20:30 +01:00
|
|
|
use sqlite::{Connection, State, Statement};
|
2021-02-05 22:24:28 +01:00
|
|
|
|
2021-03-02 18:11:17 +01:00
|
|
|
use crate::{Block, Bytes, Keystore, Transaction, hash_is_good};
|
2021-02-19 16:41:43 +01:00
|
|
|
use crate::settings::Settings;
|
2021-02-21 21:56:56 +01:00
|
|
|
#[allow(unused_imports)]
|
2021-02-20 16:28:10 +01:00
|
|
|
use log::{trace, debug, info, warn, error};
|
2021-02-26 21:00:08 +01:00
|
|
|
use std::collections::HashSet;
|
|
|
|
|
use std::cell::RefCell;
|
2021-03-02 18:11:17 +01:00
|
|
|
use chrono::Utc;
|
|
|
|
|
use crate::blockchain::transaction::hash_identity;
|
|
|
|
|
use crate::blockchain::blockchain::BlockQuality::*;
|
|
|
|
|
use crate::blockchain::BLOCK_DIFFICULTY;
|
2021-01-20 19:23:41 +01:00
|
|
|
|
|
|
|
|
const DB_NAME: &str = "blockchain.db";
|
2019-12-01 22:45:25 +01:00
|
|
|
|
|
|
|
|
pub struct Blockchain {
|
2021-02-14 18:20:30 +01:00
|
|
|
origin: Bytes,
|
2021-02-13 23:37:44 +01:00
|
|
|
pub version: u32,
|
2019-12-01 22:45:25 +01:00
|
|
|
pub blocks: Vec<Block>,
|
2021-01-20 19:23:41 +01:00
|
|
|
last_block: Option<Block>,
|
2021-02-27 18:57:15 +01:00
|
|
|
max_height: u64,
|
2021-01-20 19:23:41 +01:00
|
|
|
db: Connection,
|
2021-02-26 21:00:08 +01:00
|
|
|
zones: RefCell<HashSet<String>>
|
2019-12-01 22:45:25 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl Blockchain {
|
2021-02-14 18:20:30 +01:00
|
|
|
pub fn new(settings: &Settings) -> Self {
|
|
|
|
|
let origin = settings.get_origin();
|
|
|
|
|
let version = settings.version;
|
|
|
|
|
|
2021-01-20 19:23:41 +01:00
|
|
|
let db = sqlite::open(DB_NAME).expect("Unable to open blockchain DB");
|
2021-02-27 18:57:15 +01:00
|
|
|
let mut blockchain = Blockchain{ origin, version, blocks: Vec::new(), last_block: None, max_height: 0, db, zones: RefCell::new(HashSet::new()) };
|
2021-01-20 19:23:41 +01:00
|
|
|
blockchain.init_db();
|
2019-12-01 22:45:25 +01:00
|
|
|
blockchain
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-20 19:23:41 +01:00
|
|
|
/// Reads options from DB or initializes and writes them to DB if not found
|
|
|
|
|
fn init_db(&mut self) {
|
|
|
|
|
match self.db.prepare("SELECT * FROM blocks ORDER BY id DESC LIMIT 1;") {
|
|
|
|
|
Ok(mut statement) => {
|
|
|
|
|
while statement.next().unwrap() == State::Row {
|
2021-01-30 14:18:37 +01:00
|
|
|
match Self::get_block_from_statement(&mut statement) {
|
2021-02-20 16:28:10 +01:00
|
|
|
None => { error!("Something wrong with block in DB!"); }
|
2021-01-20 19:23:41 +01:00
|
|
|
Some(block) => {
|
2021-02-20 16:28:10 +01:00
|
|
|
info!("Loaded last block: {:?}", &block);
|
2021-02-13 23:37:44 +01:00
|
|
|
self.version = block.version;
|
2021-01-20 19:23:41 +01:00
|
|
|
self.last_block = Some(block);
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-02-20 16:28:10 +01:00
|
|
|
debug!("Blockchain version from DB = {}", self.version);
|
2021-01-20 19:23:41 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {
|
2021-02-20 16:28:10 +01:00
|
|
|
info!("No blockchain database found. Creating new.");
|
2021-01-20 19:23:41 +01:00
|
|
|
self.db.execute("
|
|
|
|
|
CREATE TABLE blocks (
|
|
|
|
|
'id' BIGINT,
|
|
|
|
|
'timestamp' BIGINT,
|
2021-02-13 23:37:44 +01:00
|
|
|
'version' TEXT,
|
2021-01-20 19:23:41 +01:00
|
|
|
'difficulty' INTEGER,
|
|
|
|
|
'random' INTEGER,
|
|
|
|
|
'nonce' INTEGER,
|
|
|
|
|
'transaction' TEXT,
|
|
|
|
|
'prev_block_hash' BINARY,
|
2021-03-02 18:11:17 +01:00
|
|
|
'hash' BINARY,
|
|
|
|
|
'pub_key' BINARY,
|
|
|
|
|
'signature' BINARY
|
2021-01-20 19:23:41 +01:00
|
|
|
);
|
2021-01-30 14:18:37 +01:00
|
|
|
CREATE INDEX block_index ON blocks (id);
|
2021-03-02 18:11:17 +01:00
|
|
|
CREATE TABLE transactions (id INTEGER PRIMARY KEY AUTOINCREMENT, identity BINARY, confirmation BINARY, method TEXT, data TEXT, pub_key BINARY);
|
2021-01-30 14:18:37 +01:00
|
|
|
CREATE INDEX ids ON transactions (identity);"
|
2021-01-20 19:23:41 +01:00
|
|
|
).expect("Error creating blocks table");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-02 18:11:17 +01:00
|
|
|
pub fn add_block(&mut self, block: Block) {
|
2021-02-20 16:28:10 +01:00
|
|
|
info!("Adding block:\n{:?}", &block);
|
2021-02-14 18:20:30 +01:00
|
|
|
self.blocks.push(block.clone());
|
|
|
|
|
self.last_block = Some(block.clone());
|
|
|
|
|
let transaction = block.transaction.clone();
|
2021-01-30 14:18:37 +01:00
|
|
|
|
2021-02-14 18:20:30 +01:00
|
|
|
{
|
|
|
|
|
// Adding block to DB
|
|
|
|
|
let mut statement = self.db.prepare("INSERT INTO blocks (\
|
2021-03-02 18:11:17 +01:00
|
|
|
id, timestamp, version, difficulty, random, nonce, 'transaction',\
|
|
|
|
|
prev_block_hash, hash, pub_key, signature)\
|
|
|
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);").unwrap();
|
2021-02-14 18:20:30 +01:00
|
|
|
statement.bind(1, block.index as i64).expect("Error in bind");
|
|
|
|
|
statement.bind(2, block.timestamp as i64).expect("Error in bind");
|
|
|
|
|
statement.bind(3, block.version as i64).expect("Error in bind");
|
|
|
|
|
statement.bind(4, block.difficulty as i64).expect("Error in bind");
|
|
|
|
|
statement.bind(5, block.random as i64).expect("Error in bind");
|
|
|
|
|
statement.bind(6, block.nonce as i64).expect("Error in bind");
|
2021-01-30 14:18:37 +01:00
|
|
|
match &transaction {
|
2021-02-14 18:20:30 +01:00
|
|
|
None => { statement.bind(7, "").expect("Error in bind"); }
|
2021-01-30 14:18:37 +01:00
|
|
|
Some(transaction) => {
|
2021-02-14 18:20:30 +01:00
|
|
|
statement.bind(7, transaction.to_string().as_ref() as &str).expect("Error in bind");
|
2021-01-30 14:18:37 +01:00
|
|
|
}
|
2021-01-20 19:23:41 +01:00
|
|
|
}
|
2021-02-14 18:20:30 +01:00
|
|
|
statement.bind(8, block.prev_block_hash.as_bytes()).expect("Error in bind");
|
|
|
|
|
statement.bind(9, block.hash.as_bytes()).expect("Error in bind");
|
2021-03-02 18:11:17 +01:00
|
|
|
statement.bind(10, block.pub_key.as_bytes()).expect("Error in bind");
|
|
|
|
|
statement.bind(11, block.signature.as_bytes()).expect("Error in bind");
|
2021-02-14 18:20:30 +01:00
|
|
|
statement.next().expect("Error adding block to DB");
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-02 18:11:17 +01:00
|
|
|
if let Some(transaction) = transaction {
|
|
|
|
|
self.add_transaction(&transaction);
|
2019-12-01 22:45:25 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-30 14:18:37 +01:00
|
|
|
fn add_transaction(&mut self, t: &Transaction) {
|
2021-03-02 18:11:17 +01:00
|
|
|
let mut statement = self.db.prepare("INSERT INTO transactions (identity, confirmation, method, data, pub_key) VALUES (?, ?, ?, ?, ?)").unwrap();
|
2021-02-14 18:20:30 +01:00
|
|
|
statement.bind(1, t.identity.as_bytes()).expect("Error in bind");
|
|
|
|
|
statement.bind(2, t.confirmation.as_bytes()).expect("Error in bind");
|
|
|
|
|
statement.bind(3, t.method.as_ref() as &str).expect("Error in bind");
|
|
|
|
|
statement.bind(4, t.data.as_ref() as &str).expect("Error in bind");
|
|
|
|
|
statement.bind(5, t.pub_key.as_bytes()).expect("Error in bind");
|
2021-01-30 14:18:37 +01:00
|
|
|
statement.next().expect("Error adding transaction to DB");
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-05 22:24:28 +01:00
|
|
|
pub fn get_block(&self, index: u64) -> Option<Block> {
|
|
|
|
|
match self.db.prepare("SELECT * FROM blocks WHERE id=? LIMIT 1;") {
|
|
|
|
|
Ok(mut statement) => {
|
2021-02-14 18:20:30 +01:00
|
|
|
statement.bind(1, index as i64).expect("Error in bind");
|
2021-02-05 22:24:28 +01:00
|
|
|
while statement.next().unwrap() == State::Row {
|
|
|
|
|
return match Self::get_block_from_statement(&mut statement) {
|
|
|
|
|
None => {
|
2021-02-20 16:28:10 +01:00
|
|
|
error!("Something wrong with block in DB!");
|
2021-02-05 22:24:28 +01:00
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
Some(block) => {
|
2021-02-20 16:28:10 +01:00
|
|
|
debug!("Loaded block: {:?}", &block);
|
2021-02-05 22:24:28 +01:00
|
|
|
Some(block)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
Err(_) => {
|
2021-02-20 16:28:10 +01:00
|
|
|
warn!("Can't find requested block {}", index);
|
2021-02-05 22:24:28 +01:00
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-01-30 14:18:37 +01:00
|
|
|
pub fn is_domain_available(&self, domain: &str, keystore: &Keystore) -> bool {
|
|
|
|
|
if domain.is_empty() {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2021-03-02 18:11:17 +01:00
|
|
|
let identity_hash = hash_identity(domain, None);
|
2021-01-30 14:18:37 +01:00
|
|
|
let mut statement = self.db.prepare("SELECT pub_key FROM transactions WHERE identity = ? ORDER BY id DESC LIMIT 1;").unwrap();
|
2021-02-14 18:20:30 +01:00
|
|
|
statement.bind(1, identity_hash.as_bytes()).expect("Error in bind");
|
2021-01-30 14:18:37 +01:00
|
|
|
while let State::Row = statement.next().unwrap() {
|
|
|
|
|
let pub_key = Bytes::from_bytes(statement.read::<Vec<u8>>(0).unwrap().as_slice());
|
|
|
|
|
if !pub_key.eq(&keystore.get_public()) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let parts: Vec<&str> = domain.rsplitn(2, ".").collect();
|
|
|
|
|
if parts.len() > 1 {
|
|
|
|
|
// We do not support third level domains
|
|
|
|
|
if parts.last().unwrap().contains(".") {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
2021-02-26 21:00:08 +01:00
|
|
|
return self.is_zone_in_blockchain(parts.first().unwrap());
|
2021-01-30 14:18:37 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
true
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-26 21:00:08 +01:00
|
|
|
pub fn is_zone_in_blockchain(&self, zone: &str) -> bool {
|
|
|
|
|
if self.zones.borrow().contains(zone) {
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Checking for existing zone in DB
|
2021-03-02 18:11:17 +01:00
|
|
|
let identity_hash = hash_identity(zone, None);
|
2021-02-26 21:00:08 +01:00
|
|
|
let mut statement = self.db.prepare("SELECT identity FROM transactions WHERE identity = ? ORDER BY id DESC LIMIT 1;").unwrap();
|
|
|
|
|
statement.bind(1, identity_hash.as_bytes()).expect("Error in bind");
|
|
|
|
|
while let State::Row = statement.next().unwrap() {
|
|
|
|
|
// If there is such a zone
|
|
|
|
|
self.zones.borrow_mut().insert(zone.to_owned());
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
false
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-20 21:36:58 +01:00
|
|
|
pub fn get_domain_transaction(&self, domain: &str) -> Option<Transaction> {
|
2021-02-19 16:41:43 +01:00
|
|
|
if domain.is_empty() {
|
|
|
|
|
return None;
|
|
|
|
|
}
|
2021-03-02 18:11:17 +01:00
|
|
|
let identity_hash = hash_identity(domain, None);
|
2021-02-19 16:41:43 +01:00
|
|
|
|
|
|
|
|
let mut statement = self.db.prepare("SELECT * FROM transactions WHERE identity = ? ORDER BY id DESC LIMIT 1;").unwrap();
|
|
|
|
|
statement.bind(1, identity_hash.as_bytes()).expect("Error in bind");
|
|
|
|
|
while let State::Row = statement.next().unwrap() {
|
|
|
|
|
let identity = Bytes::from_bytes(statement.read::<Vec<u8>>(1).unwrap().as_slice());
|
|
|
|
|
let confirmation = Bytes::from_bytes(statement.read::<Vec<u8>>(2).unwrap().as_slice());
|
|
|
|
|
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().as_slice());
|
2021-03-02 18:11:17 +01:00
|
|
|
let transaction = Transaction { identity, confirmation, method, data, pub_key };
|
2021-02-20 21:36:58 +01:00
|
|
|
debug!("Found transaction for domain {}: {:?}", domain, &transaction);
|
2021-03-02 18:11:17 +01:00
|
|
|
if transaction.check_identity(domain) {
|
2021-02-20 21:36:58 +01:00
|
|
|
return Some(transaction);
|
2021-02-19 16:41:43 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
None
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-20 21:36:58 +01:00
|
|
|
pub fn get_domain_info(&self, domain: &str) -> Option<String> {
|
|
|
|
|
match self.get_domain_transaction(domain) {
|
|
|
|
|
None => { None }
|
|
|
|
|
Some(transaction) => { Some(transaction.data) }
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-05 22:24:28 +01:00
|
|
|
pub fn last_block(&self) -> Option<Block> {
|
2021-01-20 19:23:41 +01:00
|
|
|
self.last_block.clone()
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-05 22:24:28 +01:00
|
|
|
pub fn height(&self) -> u64 {
|
|
|
|
|
match self.last_block {
|
|
|
|
|
None => { 0u64 }
|
|
|
|
|
Some(ref block) => {
|
2021-02-15 00:29:30 +01:00
|
|
|
block.index + 1
|
2021-02-05 22:24:28 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-27 18:57:15 +01:00
|
|
|
pub fn max_height(&self) -> u64 {
|
|
|
|
|
self.max_height
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub fn update_max_height(&mut self, height: u64) {
|
|
|
|
|
if height > self.max_height {
|
|
|
|
|
self.max_height = height;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2021-03-02 18:11:17 +01:00
|
|
|
pub fn check_new_block(&self, block: &Block) -> BlockQuality {
|
|
|
|
|
let timestamp = Utc::now().timestamp();
|
|
|
|
|
if block.timestamp > timestamp {
|
|
|
|
|
warn!("Ignoring block from the future:\n{:?}", &block);
|
|
|
|
|
return Bad;
|
2019-12-01 22:45:25 +01:00
|
|
|
}
|
2021-03-02 18:11:17 +01:00
|
|
|
if !hash_is_good(block.hash.as_bytes(), BLOCK_DIFFICULTY as usize) {
|
|
|
|
|
warn!("Ignoring block with low difficulty:\n{:?}", &block);
|
|
|
|
|
return Bad;
|
2019-12-01 22:45:25 +01:00
|
|
|
}
|
2021-03-02 18:11:17 +01:00
|
|
|
if !hash_is_good(block.hash.as_bytes(), block.difficulty as usize) {
|
|
|
|
|
warn!("Ignoring block with low difficulty:\n{:?}", &block);
|
|
|
|
|
return Bad;
|
2019-12-01 22:45:25 +01:00
|
|
|
}
|
2021-03-02 18:11:17 +01:00
|
|
|
match &self.last_block {
|
2021-02-14 18:20:30 +01:00
|
|
|
None => {
|
2021-03-02 18:11:17 +01:00
|
|
|
if !block.is_genesis() {
|
|
|
|
|
return Future;
|
2021-02-14 18:20:30 +01:00
|
|
|
}
|
2021-03-02 18:11:17 +01:00
|
|
|
if !self.origin.is_zero() && block.hash != self.origin {
|
|
|
|
|
warn!("Mining gave us a bad block:\n{:?}", &block);
|
|
|
|
|
return Bad;
|
2021-02-14 18:20:30 +01:00
|
|
|
}
|
|
|
|
|
}
|
2021-03-02 18:11:17 +01:00
|
|
|
Some(last_block) => {
|
|
|
|
|
if block.timestamp < last_block.timestamp && block.index > last_block.index {
|
|
|
|
|
warn!("Ignoring block with timestamp/index collision:\n{:?}", &block);
|
|
|
|
|
return Bad;
|
|
|
|
|
}
|
|
|
|
|
if last_block.index + 1 < block.index {
|
|
|
|
|
warn!("Got block from the future");
|
|
|
|
|
return Future;
|
|
|
|
|
}
|
|
|
|
|
if last_block.index >= block.index && last_block.hash == block.hash {
|
|
|
|
|
warn!("Ignoring block {}, we already have it", block.index);
|
|
|
|
|
return Twin;
|
|
|
|
|
}
|
|
|
|
|
if last_block.index == block.index && last_block.hash != block.hash {
|
|
|
|
|
warn!("Got forked block {} with hash {:?} instead of {:?}", block.index, block.hash, last_block.hash);
|
|
|
|
|
return Fork;
|
2021-02-14 18:20:30 +01:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2021-03-02 18:11:17 +01:00
|
|
|
if !check_block_hash(block) {
|
|
|
|
|
warn!("Block {:?} has wrong hash! Ignoring!", &block);
|
|
|
|
|
return Bad;
|
|
|
|
|
}
|
|
|
|
|
if !check_block_signature(&block) {
|
|
|
|
|
warn!("Block {:?} has wrong signature! Ignoring!", &block);
|
|
|
|
|
return Bad;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
Good
|
2019-12-01 22:45:25 +01:00
|
|
|
}
|
|
|
|
|
|
2021-01-30 14:18:37 +01:00
|
|
|
fn get_block_from_statement(statement: &mut Statement) -> Option<Block> {
|
|
|
|
|
let index = statement.read::<i64>(0).unwrap() as u64;
|
|
|
|
|
let timestamp = statement.read::<i64>(1).unwrap();
|
2021-02-13 23:37:44 +01:00
|
|
|
let version = statement.read::<i64>(2).unwrap() as u32;
|
2021-03-02 18:11:17 +01:00
|
|
|
let difficulty = statement.read::<i64>(3).unwrap() as u32;
|
2021-02-13 23:37:44 +01:00
|
|
|
let random = statement.read::<i64>(4).unwrap() as u32;
|
|
|
|
|
let nonce = statement.read::<i64>(5).unwrap() as u64;
|
|
|
|
|
let transaction = Transaction::from_json(&statement.read::<String>(6).unwrap());
|
|
|
|
|
let prev_block_hash = Bytes::from_bytes(statement.read::<Vec<u8>>(7).unwrap().as_slice());
|
|
|
|
|
let hash = Bytes::from_bytes(statement.read::<Vec<u8>>(8).unwrap().as_slice());
|
2021-03-02 18:11:17 +01:00
|
|
|
let pub_key = Bytes::from_bytes(statement.read::<Vec<u8>>(9).unwrap().as_slice());
|
|
|
|
|
let signature = Bytes::from_bytes(statement.read::<Vec<u8>>(10).unwrap().as_slice());
|
|
|
|
|
Some(Block::from_all_params(index, timestamp, version, difficulty, random, nonce, prev_block_hash, hash, pub_key, signature, transaction))
|
2021-01-30 14:18:37 +01:00
|
|
|
}
|
2021-02-14 18:20:30 +01:00
|
|
|
}
|
2021-01-30 14:18:37 +01:00
|
|
|
|
2021-03-02 18:11:17 +01:00
|
|
|
#[derive(PartialEq)]
|
|
|
|
|
pub enum BlockQuality {
|
|
|
|
|
Good,
|
|
|
|
|
Twin,
|
|
|
|
|
Future,
|
|
|
|
|
Bad,
|
|
|
|
|
Fork
|
|
|
|
|
}
|
|
|
|
|
|
2021-02-14 18:20:30 +01:00
|
|
|
pub fn check_block_hash(block: &Block) -> bool {
|
|
|
|
|
let mut copy: Block = block.clone();
|
|
|
|
|
copy.hash = Bytes::default();
|
2021-03-02 18:11:17 +01:00
|
|
|
copy.signature = Bytes::default();
|
2021-02-14 18:20:30 +01:00
|
|
|
let data = serde_json::to_string(©).unwrap();
|
2021-03-02 18:11:17 +01:00
|
|
|
crate::blockchain::block::hash(data.as_bytes()) == block.hash
|
2021-02-14 18:20:30 +01:00
|
|
|
}
|
|
|
|
|
|
2021-03-02 18:11:17 +01:00
|
|
|
pub fn check_block_signature(block: &Block) -> bool {
|
|
|
|
|
let mut copy = block.clone();
|
2021-02-14 18:20:30 +01:00
|
|
|
copy.signature = Bytes::zero64();
|
2021-03-02 18:11:17 +01:00
|
|
|
let data = serde_json::to_string(©).unwrap();
|
|
|
|
|
Keystore::check(data.as_bytes(), copy.pub_key.as_bytes(), block.signature.as_bytes())
|
2021-02-05 22:24:28 +01:00
|
|
|
}
|