Refactored the work with signing blocks.
This commit is contained in:
+1
-4
@@ -71,11 +71,8 @@ ALFIS это ALternative Free Identity System. Альтернативная бе
|
||||
3. Начиная с 35-го блока включается режим подписей блоков.
|
||||
Каждый блок, содержащий транзакцию, то есть создающий или меняющий какой-нибудь домен, должен быть подписан группой ~~лиц по предварительному сговору~~ узлов, обладающих блоками перед текущим блоком.
|
||||
Выбираются до 50 последних блоков перед текущим (подписываемым) блоком, среди них вычисляются 7 публичных ключей, владельцы которых должны подписать блок.
|
||||
Вычисление происходит исходя из последних 4 байт хэша подписываемого блока.
|
||||
Вычисление происходит исходя из последних 8 байт подписи подписываемого блока.
|
||||
Блок должен быть подписан минимум четырьмя валидаторами.
|
||||
Этот алгоритм в скором времени будет расширен таким образом, что если блок не подписан нужным числом валидаторов, то через некоторое время количество валидаторов и требуемое количество подписей будет расти.
|
||||
То есть, например, если через полчаса после блока нет четырёх нужных подписей, то каждый узел вычисляет валидаторов заново (по тому же алгоритму), но чуть больше, и требуется уже больше подписей.
|
||||
Например, 5 из 9, потом 6 из 11. Таким образом любой злоумышленник, долго и упорно добивавшийся своего доминирования в сети, не сможет захватить 4 нужных подписи, и отказаться от подписывания, тем самым остановив систему.
|
||||
|
||||
## Дополнительные возможности
|
||||
ALFIS содержит несколько особенностей, которых нет в обычном DNS.
|
||||
|
||||
+130
-43
@@ -19,6 +19,7 @@ use std::cmp::{min, max};
|
||||
use crate::blockchain::transaction::{ZoneData, DomainData};
|
||||
use std::ops::Deref;
|
||||
use crate::blockchain::types::MineResult::*;
|
||||
use crate::event::Event;
|
||||
|
||||
const DB_NAME: &str = "blockchain.db";
|
||||
const TEMP_DB_NAME: &str = "temp.db";
|
||||
@@ -28,6 +29,7 @@ const SQL_ADD_BLOCK: &str = "INSERT INTO blocks (id, timestamp, version, difficu
|
||||
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_GET_FIRST_BLOCK_FOR_KEY: &str = "SELECT id FROM blocks WHERE pub_key = ? LIMIT 1;";
|
||||
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 = ?";
|
||||
@@ -185,6 +187,51 @@ impl Chain {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn update(&mut self, keystore: &Option<Keystore>) -> Option<Event> {
|
||||
if self.height() < BLOCK_SIGNERS_START {
|
||||
trace!("Too early to start block signings");
|
||||
return None;
|
||||
}
|
||||
if keystore.is_none() {
|
||||
trace!("We can't sign blocks without keys");
|
||||
return None;
|
||||
}
|
||||
if self.height() < self.max_height() {
|
||||
trace!("No signing while syncing");
|
||||
return None;
|
||||
}
|
||||
|
||||
let block = self.last_block().unwrap();
|
||||
if block.transaction.is_none() {
|
||||
trace!("No need to sign signing block");
|
||||
return None;
|
||||
}
|
||||
let keystore = keystore.clone().unwrap().clone();
|
||||
let signers: HashSet<Bytes> = self.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);
|
||||
// We start mining sign block after some time, not everyone in the same time
|
||||
let start = Utc::now().timestamp() + (rand::random::<i64>() % BLOCK_SIGNERS_START_RANDOM);
|
||||
return Some(Event::ActionMineLocker { start, index: block.index + 1, hash: block.hash, keystore });
|
||||
} else if !signers.is_empty() {
|
||||
info!("Signing block must be mined by other nodes");
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn update_sign_block_for_mining(&self, mut block: Block) -> Option<Block> {
|
||||
if let Some(full_block) = &self.last_full_block {
|
||||
let sign_count = self.height() - full_block.index;
|
||||
if sign_count >= BLOCK_SIGNERS_MIN {
|
||||
return None;
|
||||
}
|
||||
block.index = self.height() + 1;
|
||||
block.prev_block_hash = self.last_block.clone().unwrap().hash;
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn delete_transaction(&mut self, index: u64) -> sqlite::Result<()> {
|
||||
let mut statement = self.db.prepare(SQL_DELETE_DOMAIN)?;
|
||||
statement.bind(1, index as i64)?;
|
||||
@@ -508,10 +555,10 @@ impl Chain {
|
||||
match self.last_full_block {
|
||||
None => { self.height() + 1 }
|
||||
Some(ref block) => {
|
||||
if block.index < LOCKER_BLOCK_START {
|
||||
if block.index < BLOCK_SIGNERS_START {
|
||||
self.height() + 1
|
||||
} else {
|
||||
max(block.index, self.height()) + LOCKER_BLOCK_SIGNS
|
||||
max(block.index, self.height()) + BLOCK_SIGNERS_MIN
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -622,25 +669,10 @@ impl Chain {
|
||||
warn!("Block {} arrived too early.", block.index);
|
||||
return Future;
|
||||
}
|
||||
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!("Not enough signing blocks over full {} block!", full_block.index);
|
||||
if block.index >= BLOCK_SIGNERS_START {
|
||||
// If this block is main, signed part of blockchain
|
||||
if !self.is_good_sign_block(&block) {
|
||||
return Bad;
|
||||
} else {
|
||||
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_signing(&block, full_block) == Bad {
|
||||
return Bad;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -666,6 +698,79 @@ impl Chain {
|
||||
Good
|
||||
}
|
||||
|
||||
/// Checks if this block is a good signature block
|
||||
fn is_good_sign_block(&self, block: &Block) -> bool {
|
||||
if let Some(full_block) = &self.last_full_block {
|
||||
let sign_count = self.height() - full_block.index;
|
||||
if sign_count < BLOCK_SIGNERS_MIN {
|
||||
// Last full block is not locked enough
|
||||
if block.transaction.is_some() {
|
||||
warn!("Not enough signing blocks over full {} block!", full_block.index);
|
||||
return false;
|
||||
} else {
|
||||
if !self.is_good_signer_for_block(&block, full_block, sign_count) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
} else if sign_count < BLOCK_SIGNERS_ALL && block.transaction.is_none() {
|
||||
if !self.is_good_signer_for_block(&block, full_block, sign_count) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Check if this block's owner is a good candidate to sign last full block
|
||||
fn is_good_signer_for_block(&self, block: &Block, full_block: &Block, sign_count: u64) -> bool {
|
||||
// If the time for chosen signers is up
|
||||
if self.can_sign_by_pos(sign_count, full_block.timestamp, block.timestamp, &block.pub_key) {
|
||||
return true;
|
||||
}
|
||||
// If we got a locker/signing block
|
||||
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 false;
|
||||
}
|
||||
// If this signers' public key has already locked/signed that block we return error
|
||||
for i in (full_block.index + 1)..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 false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Gets an id of first block of this public key
|
||||
fn get_first_block_id_for_key(&self, key: &Bytes) -> u64 {
|
||||
match self.db.prepare(SQL_GET_FIRST_BLOCK_FOR_KEY) {
|
||||
Ok(mut statement) => {
|
||||
statement.bind(1, &***key).expect("Error in bind");
|
||||
while statement.next().unwrap() == State::Row {
|
||||
return statement.read::<i64>(0).unwrap() as u64;
|
||||
}
|
||||
0
|
||||
}
|
||||
Err(_) => {
|
||||
0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if an owner of this public key can sign full block by PoS scheme (be in first 1000 users)
|
||||
fn can_sign_by_pos(&self, sign_count: u64, block_time: i64, now: i64, pub_key: &Bytes) -> bool {
|
||||
if sign_count < BLOCK_SIGNERS_MIN && block_time - now > BLOCK_SIGNERS_TIME {
|
||||
let index = self.get_first_block_id_for_key(&pub_key);
|
||||
if index > 0 && index <= BLOCK_POS_SIGNERS {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn get_difficulty_for_transaction(&self, transaction: &Transaction) -> u32 {
|
||||
match transaction.class.as_ref() {
|
||||
"domain" => {
|
||||
@@ -689,38 +794,20 @@ impl Chain {
|
||||
}
|
||||
}
|
||||
|
||||
fn check_block_for_signing(&self, block: &Block, full_block: &Block) -> BlockQuality {
|
||||
// If we got a locker/signing block
|
||||
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 signers' public key has already locked/signed that block we return error
|
||||
for i in (full_block.index + 1)..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 public keys of a node that needs to mine "signature" block above this block
|
||||
/// block - last full block
|
||||
pub fn get_block_signers(&self, block: &Block) -> Vec<Bytes> {
|
||||
let mut result = Vec::new();
|
||||
if block.index < LOCKER_BLOCK_START {
|
||||
if block.index < BLOCK_SIGNERS_START {
|
||||
return result;
|
||||
}
|
||||
let mut set = HashSet::new();
|
||||
let tail = block.hash.get_tail_u64();
|
||||
let interval = min(block.index, LOCKER_BLOCK_INTERVAL) - 1;
|
||||
let tail = block.signature.get_tail_u64();
|
||||
let interval = min(block.index, BLOCK_SIGNERS_WINDOW) - 1;
|
||||
let start_index = block.index - interval;
|
||||
let mut count = 1;
|
||||
while set.len() < LOCKER_BLOCK_LOCKERS as usize {
|
||||
let index = start_index + ((tail * count) % LOCKER_BLOCK_INTERVAL);
|
||||
while set.len() < BLOCK_SIGNERS_ALL as usize {
|
||||
let index = start_index + ((tail * count) % BLOCK_SIGNERS_WINDOW);
|
||||
if let Some(b) = self.get_block(index) {
|
||||
if b.pub_key != block.pub_key && !set.contains(&b.pub_key) {
|
||||
result.push(b.pub_key.clone());
|
||||
|
||||
@@ -6,11 +6,26 @@ 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;
|
||||
pub const LOCKER_BLOCK_SIGNS: u64 = 4;
|
||||
pub const LOCKER_BLOCK_TIME: i64 = 300;
|
||||
pub const LOCKER_BLOCK_INTERVAL: u64 = 50;
|
||||
/// Blocks start to be signed starting from this index
|
||||
pub const BLOCK_SIGNERS_START: u64 = 35;
|
||||
|
||||
/// How many signers are chosen for signing
|
||||
pub const BLOCK_SIGNERS_ALL: u64 = 7;
|
||||
|
||||
/// Minimal signatures needed
|
||||
pub const BLOCK_SIGNERS_MIN: u64 = 4;
|
||||
|
||||
/// Last number of blocks from which we select signers
|
||||
pub const BLOCK_SIGNERS_WINDOW: u64 = 50;
|
||||
|
||||
/// Signers have 30 minutes to sign, after that time any owner of first 1000 block can add needed signature
|
||||
pub const BLOCK_SIGNERS_TIME: i64 = 1800;
|
||||
|
||||
/// PoS signers, that sign blocks when chosen signers didn't sign
|
||||
pub const BLOCK_POS_SIGNERS: u64 = 1000;
|
||||
|
||||
/// We start mining signing blocks after random delay, this is the max delay
|
||||
pub const BLOCK_SIGNERS_START_RANDOM: i64 = 180;
|
||||
|
||||
pub const NEW_DOMAINS_INTERVAL: i64 = 86400; // One day in seconds
|
||||
pub const DOMAIN_LIFETIME: i64 = 86400 * 365; // One year
|
||||
|
||||
+1
-1
@@ -13,7 +13,7 @@ pub enum Event {
|
||||
NewBlockReceived,
|
||||
BlockchainChanged { index: u64 },
|
||||
ActionStopMining,
|
||||
ActionMineLocker { index: u64, hash: Bytes, keystore: Box<Keystore> },
|
||||
ActionMineLocker { start: i64, index: u64, hash: Bytes, keystore: Box<Keystore> },
|
||||
ActionQuit,
|
||||
NetworkStatus { nodes: usize, blocks: u64 },
|
||||
Syncing { have: u64, height: u64 },
|
||||
|
||||
+24
-16
@@ -37,7 +37,7 @@ impl Miner {
|
||||
}
|
||||
|
||||
pub fn add_block(&mut self, block: Block, keystore: Keystore) {
|
||||
self.jobs.lock().unwrap().push(MineJob { block, keystore });
|
||||
self.jobs.lock().unwrap().push(MineJob { start: 0, block, keystore });
|
||||
self.cond_var.notify_one();
|
||||
}
|
||||
|
||||
@@ -55,21 +55,28 @@ impl Miner {
|
||||
let cond_var = self.cond_var.clone();
|
||||
thread::spawn(move || {
|
||||
running.store(true, Ordering::SeqCst);
|
||||
let delay = Duration::from_millis(1000);
|
||||
while running.load(Ordering::SeqCst) {
|
||||
// If some transaction is being mined now, we yield
|
||||
if mining.load(Ordering::SeqCst) {
|
||||
thread::sleep(Duration::from_millis(1000));
|
||||
thread::sleep(delay);
|
||||
continue;
|
||||
}
|
||||
|
||||
let mut lock = blocks.lock().unwrap();
|
||||
if lock.len() > 0 {
|
||||
info!("Got new block to mine");
|
||||
let block = lock.remove(0);
|
||||
let mut jobs = blocks.lock().unwrap();
|
||||
if jobs.len() > 0 {
|
||||
debug!("Got new job to mine");
|
||||
let job = jobs.remove(0);
|
||||
if job.start == 0 || job.start < Utc::now().timestamp() {
|
||||
mining.store(true, Ordering::SeqCst);
|
||||
Miner::mine_internal(Arc::clone(&context), block, mining.clone());
|
||||
Miner::mine_internal(Arc::clone(&context), job, mining.clone());
|
||||
} else {
|
||||
let _ = cond_var.wait(lock).expect("Error in wait lock!");
|
||||
debug!("This job will wait for now");
|
||||
thread::sleep(delay);
|
||||
jobs.push(job);
|
||||
}
|
||||
} else {
|
||||
let _ = cond_var.wait(jobs).expect("Error in wait lock!");
|
||||
}
|
||||
}
|
||||
});
|
||||
@@ -83,11 +90,11 @@ impl Miner {
|
||||
Event::ActionStopMining => {
|
||||
mining.store(false, Ordering::SeqCst);
|
||||
}
|
||||
Event::ActionMineLocker { index, hash, keystore } => {
|
||||
Event::ActionMineLocker { start, index, hash, keystore } => {
|
||||
if !mining.load(Ordering::SeqCst) {
|
||||
let mut block = Block::new(None, Bytes::default(), hash, LOCKER_DIFFICULTY);
|
||||
block.index = index;
|
||||
blocks.lock().unwrap().push(MineJob { block, keystore: keystore.deref().clone() });
|
||||
blocks.lock().unwrap().push(MineJob { start, block, keystore: keystore.deref().clone() });
|
||||
cond_var.notify_all();
|
||||
info!("Added a locker block to mine");
|
||||
}
|
||||
@@ -117,17 +124,15 @@ impl Miner {
|
||||
mining.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
match context.lock().unwrap().chain.last_block() {
|
||||
None => {}
|
||||
Some(last_block) => {
|
||||
debug!("Last block found");
|
||||
// If we were doing something else and got new block before we could mine this block
|
||||
if last_block.index > job.block.index || last_block.hash != job.block.prev_block_hash {
|
||||
match context.lock().unwrap().chain.update_sign_block_for_mining(job.block) {
|
||||
None => {
|
||||
warn!("We missed block to lock");
|
||||
context.lock().unwrap().bus.post(Event::MinerStopped { success: false, full: false });
|
||||
mining.store(false, Ordering::SeqCst);
|
||||
return;
|
||||
}
|
||||
Some(block) => {
|
||||
job.block = block;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -185,6 +190,8 @@ impl Miner {
|
||||
context.settings.origin = block.hash.to_string();
|
||||
}
|
||||
context.chain.add_block(block);
|
||||
let option = Some(job.keystore);
|
||||
context.chain.update(&option);
|
||||
success = true;
|
||||
}
|
||||
context.bus.post(Event::MinerStopped { success, full });
|
||||
@@ -199,6 +206,7 @@ impl Miner {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MineJob {
|
||||
start: i64,
|
||||
block: Block,
|
||||
keystore: Keystore
|
||||
}
|
||||
|
||||
+7
-28
@@ -14,12 +14,10 @@ use mio::net::{TcpListener, TcpStream};
|
||||
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::{Context, Block, p2p::Message, p2p::State, p2p::Peer, p2p::Peers, is_yggdrasil};
|
||||
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));
|
||||
@@ -144,7 +142,6 @@ impl Network {
|
||||
}
|
||||
(height, context.chain.last_hash())
|
||||
};
|
||||
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();
|
||||
@@ -461,6 +458,8 @@ fn handle_message(context: Arc<Mutex<Context>>, message: Message, peers: &mut Pe
|
||||
match context.chain.check_new_block(&block) {
|
||||
BlockQuality::Good => {
|
||||
context.chain.add_block(block);
|
||||
let keystore = context.keystore.clone();
|
||||
context.chain.update(&keystore);
|
||||
let my_height = context.chain.height();
|
||||
context.bus.post(crate::event::Event::BlockchainChanged { index: my_height });
|
||||
// If it was the last block to sync
|
||||
@@ -482,6 +481,10 @@ fn handle_message(context: Arc<Mutex<Context>>, message: Message, peers: &mut Pe
|
||||
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 keystore = context.keystore.clone();
|
||||
context.chain.update(&keystore);
|
||||
let index = context.chain.height();
|
||||
context.bus.post(crate::event::Event::BlockchainChanged { index });
|
||||
}
|
||||
//let peer = peers.get_mut_peer(token).unwrap();
|
||||
//deal_with_fork(context, peer, block);
|
||||
@@ -494,30 +497,6 @@ fn handle_message(context: Arc<Mutex<Context>>, message: Message, peers: &mut Pe
|
||||
answer
|
||||
}
|
||||
|
||||
/// Sends an Event to miner to start mining locker block if "locker" is our public key
|
||||
fn mine_signing_block(context: Arc<Mutex<Context>>) {
|
||||
let mut context = context.lock().unwrap();
|
||||
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 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 !signers.is_empty() {
|
||||
info!("Signing block must be mined by other nodes");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn deal_with_fork(context: MutexGuard<Context>, peer: &mut Peer, block: Block) {
|
||||
peer.add_fork_block(block);
|
||||
|
||||
Reference in New Issue
Block a user