/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2026 Dyne.org foundation
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see .
*/
use std::str::FromStr;
use darkfi::{
blockchain::HeaderHash,
rpc::{jsonrpc::JsonNotification, util::JsonValue},
system::{sleep, Subscription},
util::{encoding::base64, time::Timestamp},
Error, Result,
};
use darkfi_serial::serialize_async;
use smol::channel::Sender;
use tracing::{error, info};
use crate::{task::sync_task, DarkfiNodePtr};
/// Auxiliary structure representing node consensus init task configuration.
#[derive(Clone)]
pub struct ConsensusInitTaskConfig {
/// Skip syncing process and start node right away
pub skip_sync: bool,
/// Optional sync checkpoint height
pub checkpoint_height: Option,
/// Optional sync checkpoint hash
pub checkpoint: Option,
}
/// Sync the node consensus state and start the corresponding task, based on node type.
pub async fn consensus_init_task(
node: DarkfiNodePtr,
config: ConsensusInitTaskConfig,
sender: Sender<()>,
) -> Result<()> {
// Check current canonical blockchain for curruption
// TODO: create a restore method reverting each block backwards
// until its healthy again
let mut validator = node.validator.write().await;
validator.consensus.healthcheck().await?;
// Check if network genesis is in the future.
let current = Timestamp::current_time().inner();
let genesis = validator.consensus.module.genesis.inner();
if current < genesis {
let diff = genesis - current;
info!(target: "darkfid::task::consensus_init_task", "Waiting for network genesis: {diff} seconds");
sleep(diff).await;
}
// Generate a new fork to be able to extend
info!(target: "darkfid::task::consensus_init_task", "Generating new empty fork...");
validator.consensus.generate_empty_fork().await?;
drop(validator);
// Sync blockchain
let comms_timeout =
node.p2p_handler.p2p.settings().read_arc().await.outbound_connect_timeout_max();
let checkpoint = if !config.skip_sync {
// Parse configured checkpoint
if config.checkpoint_height.is_some() && config.checkpoint.is_none() {
return Err(Error::ParseFailed("Blockchain configured checkpoint hash missing"))
}
let checkpoint = if let Some(height) = config.checkpoint_height {
Some((height, HeaderHash::from_str(config.checkpoint.as_ref().unwrap())?))
} else {
None
};
loop {
match sync_task(&node, checkpoint).await {
Ok(_) => break,
Err(e) => {
error!(target: "darkfid::task::consensus_task", "Sync task failed: {e}");
info!(target: "darkfid::task::consensus_task", "Sleeping for {comms_timeout} before retry...");
sleep(comms_timeout).await;
}
}
}
checkpoint
} else {
node.validator.write().await.synced = true;
None
};
// Gracefully handle network disconnections
loop {
match listen_to_network(&node, &sender).await {
Ok(_) => return Ok(()),
Err(Error::NetworkNotConnected) => {
// Sync node again
node.validator.write().await.synced = false;
if !config.skip_sync {
loop {
match sync_task(&node, checkpoint).await {
Ok(_) => break,
Err(e) => {
error!(target: "darkfid::task::consensus_task", "Sync task failed: {e}");
info!(target: "darkfid::task::consensus_task", "Sleeping for {comms_timeout} before retry...");
sleep(comms_timeout).await;
}
}
}
} else {
node.validator.write().await.synced = true;
}
}
Err(e) => return Err(e),
}
}
}
/// Async task to start the consensus task, while monitoring for a network disconnections.
async fn listen_to_network(node: &DarkfiNodePtr, sender: &Sender<()>) -> Result<()> {
// Grab proposals subscriber and subscribe to it
let proposals_sub = node.subscribers.get("proposals").unwrap();
let prop_subscription = proposals_sub.publisher.clone().subscribe().await;
// Subscribe to the network disconnect subscriber
let net_subscription = node.p2p_handler.p2p.hosts().subscribe_disconnect().await;
let result = smol::future::or(
monitor_network(&net_subscription),
consensus_task(node, &prop_subscription, sender),
)
.await;
// Terminate the subscriptions
prop_subscription.unsubscribe().await;
net_subscription.unsubscribe().await;
result
}
/// Async task to monitor network disconnections.
async fn monitor_network(subscription: &Subscription) -> Result<()> {
Err(subscription.receive().await)
}
/// Async task used for listening for new blocks and perform consensus.
async fn consensus_task(
node: &DarkfiNodePtr,
subscription: &Subscription,
sender: &Sender<()>,
) -> Result<()> {
info!(target: "darkfid::task::consensus_task", "Starting consensus task...");
// Grab blocks subscriber
let block_sub = node.subscribers.get("blocks").unwrap();
loop {
// Wait for a new proposal
subscription.receive().await;
// Check if we can confirm anything and broadcast them
let mut validator = node.validator.write().await;
let confirmed = match validator.confirmation().await {
Ok(f) => f,
Err(e) => {
error!(
target: "darkfid::task::consensus_task",
"Confirmation failed: {e}"
);
continue
}
};
// Refresh mining registry
if let Err(e) = node.registry.state.write().await.refresh(&validator).await {
error!(target: "darkfid::task::consensus_task", "Failed refreshing mining block templates: {e}")
}
// Notify the garbage collection task
if let Err(e) = sender.send(()).await {
error!(
target: "darkfid::task::consensus_task",
"Garbage collection channel send fail: {e}"
);
};
// Check if something was confirmed
if confirmed.is_empty() {
continue
}
// Broadcast confirmed blocks to subscribers
let mut notif_blocks = Vec::with_capacity(confirmed.len());
for block in confirmed {
notif_blocks.push(JsonValue::String(base64::encode(&serialize_async(&block).await)));
}
block_sub.notify(JsonValue::Array(notif_blocks)).await;
}
}