/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2024 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, sync::Arc};
use darkfi::{
blockchain::HeaderHash,
rpc::{jsonrpc::JsonNotification, util::JsonValue},
system::{sleep, StoppableTask, Subscription},
util::{encoding::base64, time::Timestamp},
Error, Result,
};
use darkfi_sdk::{
crypto::{FuncId, PublicKey},
pasta::{group::ff::PrimeField, pallas},
};
use darkfi_serial::serialize_async;
use log::{error, info};
use crate::{
task::{garbage_collect_task, miner::MinerRewardsRecipientConfig, miner_task, sync_task},
Darkfid,
};
/// Auxiliary structure representing node consensus init task configuration
pub struct ConsensusInitTaskConfig {
pub skip_sync: bool,
pub checkpoint_height: Option,
pub checkpoint: Option,
pub miner: bool,
pub recipient: Option,
pub spend_hook: Option,
pub user_data: Option,
pub bootstrap: u64,
}
/// Sync the node consensus state and start the corresponding task, based on node type.
pub async fn consensus_init_task(
node: Arc,
config: ConsensusInitTaskConfig,
ex: Arc>,
) -> Result<()> {
// Check if network is configured to start in the future.
// NOTE: Always configure the network to start in the future when bootstrapping
// or restarting it.
let current = Timestamp::current_time().inner();
if current < config.bootstrap {
let diff = config.bootstrap - current;
info!(target: "darkfid::task::consensus_init_task", "Waiting for network bootstrap: {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...");
node.validator.consensus.generate_empty_fork().await?;
// Sync blockchain
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
};
sync_task(&node, checkpoint).await?;
checkpoint
} else {
*node.validator.synced.write().await = true;
None
};
// Grab rewards recipient public key(address) if node is a miner,
// along with configured spend hook and user data.
let recipient_config = if config.miner {
if config.recipient.is_none() {
return Err(Error::ParseFailed("Recipient address missing"))
}
let recipient = match PublicKey::from_str(config.recipient.as_ref().unwrap()) {
Ok(address) => address,
Err(_) => return Err(Error::InvalidAddress),
};
let spend_hook = match config.spend_hook {
Some(s) => match FuncId::from_str(&s) {
Ok(s) => Some(s),
Err(_) => return Err(Error::ParseFailed("Invalid spend hook")),
},
None => None,
};
let user_data = match config.user_data {
Some(u) => {
let bytes: [u8; 32] = match bs58::decode(&u).into_vec()?.try_into() {
Ok(b) => b,
Err(_) => return Err(Error::ParseFailed("Invalid user data")),
};
match pallas::Base::from_repr(bytes).into() {
Some(v) => Some(v),
None => return Err(Error::ParseFailed("Invalid user data")),
}
}
None => None,
};
Some(MinerRewardsRecipientConfig { recipient, spend_hook, user_data })
} else {
None
};
// Gracefully handle network disconnections
loop {
let result = if config.miner {
miner_task(
node.clone(),
recipient_config.as_ref().unwrap(),
config.skip_sync,
ex.clone(),
)
.await
} else {
replicator_task(node.clone(), ex.clone()).await
};
match result {
Ok(_) => return Ok(()),
Err(Error::NetworkNotConnected) => {
// Sync node again
*node.validator.synced.write().await = false;
node.validator.consensus.purge_forks().await?;
if !config.skip_sync {
sync_task(&node, checkpoint).await?;
} else {
*node.validator.synced.write().await = true;
}
}
Err(e) => return Err(e),
}
}
}
/// Async task to start the consensus task, while monitoring for a network disconnections.
async fn replicator_task(node: Arc, ex: Arc>) -> 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.hosts().subscribe_disconnect().await;
let result = smol::future::or(
monitor_network(&net_subscription),
consensus_task(node, &prop_subscription, ex),
)
.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: Arc,
subscription: &Subscription,
ex: Arc>,
) -> Result<()> {
info!(target: "darkfid::task::consensus_task", "Starting consensus task...");
// Grab blocks subscriber
let block_sub = node.subscribers.get("blocks").unwrap();
// Create the garbage collection task using a dummy task
let gc_task = StoppableTask::new();
gc_task.clone().start(
async { Ok(()) },
|_| async { /* Do nothing */ },
Error::GarbageCollectionTaskStopped,
ex.clone(),
);
loop {
subscription.receive().await;
// Check if we can finalize anything and broadcast them
let finalized = match node.validator.finalization().await {
Ok(f) => f,
Err(e) => {
error!(
target: "darkfid::task::consensus_task",
"Finalization failed: {e}"
);
continue
}
};
if finalized.is_empty() {
continue
}
let mut notif_blocks = Vec::with_capacity(finalized.len());
for block in finalized {
notif_blocks.push(JsonValue::String(base64::encode(&serialize_async(&block).await)));
}
block_sub.notify(JsonValue::Array(notif_blocks)).await;
// Invoke the detached garbage collection task
gc_task.clone().stop().await;
gc_task.clone().start(
garbage_collect_task(node.clone()),
|res| async {
match res {
Ok(()) | Err(Error::GarbageCollectionTaskStopped) => { /* Do nothing */ }
Err(e) => {
error!(target: "darkfid", "Failed starting garbage collection task: {}", e)
}
}
},
Error::GarbageCollectionTaskStopped,
ex.clone(),
);
}
}