/* This file is part of DarkFi (https://dark.fi)
*
* Copyright (C) 2020-2023 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::time::Duration;
use async_std::sync::Arc;
use log::{debug, error, info, warn};
use super::consensus_sync_task;
use crate::{
consensus::{constants, ValidatorStatePtr},
net::P2pPtr,
util::{async_util::sleep, time::Timestamp},
};
/// async task used for participating in the consensus protocol
pub async fn proposal_task(
consensus_p2p: P2pPtr,
sync_p2p: P2pPtr,
state: ValidatorStatePtr,
ex: Arc>,
) {
// Check if network is configured to start in the future,
// otherwise wait for current or next slot finalization period for optimal sync conditions.
// NOTE: Network beign configured to start in the future should always be the case
// when bootstrapping or restarting a network.
let current_ts = Timestamp::current_time();
let bootstrap_ts = state.read().await.consensus.bootstrap_ts;
if current_ts < bootstrap_ts {
let diff = bootstrap_ts.0 - current_ts.0;
info!(target: "consensus::proposal", "consensus: Waiting for network bootstrap: {} seconds", diff);
sleep(diff as u64).await;
} else {
let mut sleep_time = state.read().await.consensus.next_n_slot_start(1);
let sync_offset = Duration::new(constants::FINAL_SYNC_DUR, 0);
loop {
if sleep_time > sync_offset {
sleep_time -= sync_offset;
break
}
info!(target: "consensus::proposal", "consensus: Waiting for next slot ({:?})", sleep_time);
sleep(sleep_time.as_secs()).await;
sleep_time = state.read().await.consensus.next_n_slot_start(1);
}
info!(target: "consensus::proposal", "consensus: Waiting for finalization sync period ({:?})", sleep_time);
sleep(sleep_time.as_secs()).await;
}
let mut retries = 0;
// Sync loop
loop {
// Resetting consensus state, so node can still follow the finalized blocks by
// the sync p2p network/protocols
state.write().await.consensus.reset();
// Checking sync retries
if retries > constants::SYNC_MAX_RETRIES {
error!(target: "consensus::proposal", "consensus: Node reached max sync retries ({}) due to not being able to follow up with consensus processing.", constants::SYNC_MAX_RETRIES);
warn!(target: "consensus::proposal", "consensus: Terminating consensus participation.");
break
}
// Node syncs its consensus state
match consensus_sync_task(consensus_p2p.clone(), state.clone()).await {
Ok(p) => {
// Check if node is not connected to other nodes and can
// start proposing immediately.
if p {
info!(target: "consensus::proposal", "consensus: Node can start proposing!");
state.write().await.consensus.proposing = p;
}
}
Err(e) => {
error!(target: "consensus::proposal", "consensus: Failed syncing consensus state: {}. Quitting consensus.", e);
// TODO: Perhaps notify over a channel in order to
// stop consensus p2p protocols.
return
}
};
// Node modifies its participating slot to next.
match state.write().await.consensus.set_participating() {
Ok(()) => {
info!(target: "consensus::proposal", "consensus: Node will start participating in the next slot")
}
Err(e) => {
error!(target: "consensus::proposal", "consensus: Failed to set participation slot: {}", e)
}
}
// Record epoch we start the consensus loop
let start_epoch = state.read().await.consensus.current_epoch();
// Start executing consensus
consensus_loop(consensus_p2p.clone(), sync_p2p.clone(), state.clone(), ex.clone()).await;
// Reset retries counter if more epochs have passed than sync retries duration
let break_epoch = state.read().await.consensus.current_epoch();
if (break_epoch - start_epoch) > constants::SYNC_RETRIES_DURATION {
retries = 0;
}
// Increase retries count on consensus loop break
retries += 1;
}
}
/// Consensus protocol loop
async fn consensus_loop(
consensus_p2p: P2pPtr,
sync_p2p: P2pPtr,
state: ValidatorStatePtr,
ex: Arc>,
) {
// Note: when a node can start produce proposals is only enforced in code,
// where we verify if the hardware can keep up with the consensus, by
// counting how many consecutive slots node successfully listened and process
// everything. Additionally, we check each proposer coin creation slot to be
// greater than an epoch length. Later, this will be enforced via contract,
// where it will be explicit when a node can produce proposals,
// and after which slot they can be considered as valid.
let mut listened_slots = 0;
let mut changed_status = false;
loop {
// Check if node can start proposing.
// This code ensures that we only change the status once
// and listened_slots doesn't increment further.
if listened_slots > constants::EPOCH_LENGTH {
if !changed_status {
info!(target: "consensus::proposal", "consensus: Node can start proposing!");
state.write().await.consensus.proposing = true;
changed_status = true;
}
} else {
listened_slots += 1;
}
// Node waits and execute consensus protocol propose period.
if propose_period(consensus_p2p.clone(), state.clone()).await {
// Node needs to resync
warn!(
target: "consensus::proposal",
"consensus: Node missed slot {} due to proposal processing, resyncing...",
state.read().await.consensus.current_slot()
);
break
}
// Node waits and execute consensus protocol finalization period.
if finalization_period(sync_p2p.clone(), state.clone(), ex.clone()).await {
// Node needs to resync
warn!(
target: "consensus::proposal",
"consensus: Node missed slot {} due to finalizated blocks processing, resyncing...",
state.read().await.consensus.current_slot()
);
break
}
}
}
/// async function to wait and execute consensus protocol propose period.
/// Propose period consists of 2 parts:
/// - Generate slot sigmas and checkpoint
/// - Check if slot leader to generate and broadcast proposal
/// Returns flag in case node needs to resync.
async fn propose_period(consensus_p2p: P2pPtr, state: ValidatorStatePtr) -> bool {
// Node sleeps until next slot
let seconds_next_slot = state.read().await.consensus.next_n_slot_start(1).as_secs();
info!(target: "consensus::proposal", "consensus: Waiting for next slot ({} sec)", seconds_next_slot);
sleep(seconds_next_slot).await;
// Keep a record of slot to verify if next slot got skipped during processing
let processing_slot = state.read().await.consensus.current_slot();
// Retrieve slot sigmas
let (sigma1, sigma2) = state.write().await.consensus.sigmas();
// Node checks if epoch has changed and generate slot checkpoint
let epoch_changed = state.write().await.consensus.epoch_changed(sigma1, sigma2).await;
match epoch_changed {
Ok(changed) => {
if changed {
info!(target: "consensus::proposal", "consensus: New epoch started: {}", state.read().await.consensus.epoch);
}
}
Err(e) => {
error!(target: "consensus::proposal", "consensus: Epoch check failed: {}", e);
return false
}
};
// Node checks if it's the slot leader to generate a new proposal
// for that slot.
let (won, fork_index, coin_index) =
state.write().await.consensus.is_slot_leader(sigma1, sigma2);
let result = if won {
state.write().await.propose(processing_slot, fork_index, coin_index, sigma1, sigma2).await
} else {
Ok(None)
};
let (proposal, coin, derived_blind) = match result {
Ok(pair) => {
if pair.is_none() {
info!(target: "consensus::proposal", "consensus: Node is not the slot lead");
return false
}
pair.unwrap()
}
Err(e) => {
error!(target: "consensus::proposal", "consensus: Block proposal failed: {}", e);
return false
}
};
// Node checks if it missed finalization period due to proposal creation
let next_slot_start = state.read().await.consensus.next_n_slot_start(1);
if next_slot_start.as_secs() <= constants::FINAL_SYNC_DUR {
warn!(
target: "consensus::proposal",
"consensus: Node missed slot {} finalization period due to proposal creation, resyncing...",
state.read().await.consensus.current_slot()
);
return true
}
// Node stores the proposal and broadcast to rest nodes
info!(target: "consensus::proposal", "consensus: Node is the slot leader: Proposed block: {}", proposal);
debug!(target: "consensus::proposal", "consensus: Full proposal: {:?}", proposal);
match state
.write()
.await
.receive_proposal(&proposal, Some((coin_index, coin, derived_blind)))
.await
{
Ok(_) => {
// Here we don't have to check to broadcast, because the flag
// will always be true, since the node is able to produce proposals
info!(target: "consensus::proposal", "consensus: Block proposal saved successfully");
// Broadcast proposal to other consensus nodes
match consensus_p2p.broadcast(proposal).await {
Ok(()) => {
info!(target: "consensus::proposal", "consensus: Proposal broadcasted successfully")
}
Err(e) => {
error!(target: "consensus::proposal", "consensus: Failed broadcasting proposal: {}", e)
}
}
}
Err(e) => {
error!(target: "consensus::proposal", "consensus: Block proposal save failed: {}", e);
}
}
// Verify node didn't skip next slot
processing_slot != state.read().await.consensus.current_slot()
}
/// async function to wait and execute consensus protocol finalization period.
/// Returns flag in case node needs to resync.
async fn finalization_period(
sync_p2p: P2pPtr,
state: ValidatorStatePtr,
ex: Arc>,
) -> bool {
// Node sleeps until finalization sync period starts
let next_slot_start = state.read().await.consensus.next_n_slot_start(1);
if next_slot_start.as_secs() > constants::FINAL_SYNC_DUR {
let seconds_sync_period =
(next_slot_start - Duration::new(constants::FINAL_SYNC_DUR, 0)).as_secs();
info!(target: "consensus::proposal", "consensus: Waiting for finalization sync period ({} sec)", seconds_sync_period);
sleep(seconds_sync_period).await;
} else {
warn!(
target: "consensus::proposal",
"consensus: Node missed slot {} finalization period due to proposals processing, resyncing...",
state.read().await.consensus.current_slot()
);
return true
}
// Keep a record of slot to verify if next slot got skipped during processing
let completed_slot = state.read().await.consensus.current_slot();
// Check if any forks can be finalized
match state.write().await.chain_finalization().await {
Ok((to_broadcast_block, to_broadcast_slot_checkpoints)) => {
// Broadcasting in background
if !to_broadcast_block.is_empty() || !to_broadcast_slot_checkpoints.is_empty() {
ex.spawn(async move {
// Broadcast finalized blocks info, if any:
info!(target: "consensus::proposal", "consensus: Broadcasting finalized blocks");
for info in to_broadcast_block {
match sync_p2p.broadcast(info).await {
Ok(()) => info!(target: "consensus::proposal", "consensus: Broadcasted block"),
Err(e) => error!(target: "consensus::proposal", "consensus: Failed broadcasting block: {}", e),
}
}
// Broadcast finalized slot checkpoints, if any:
info!(target: "consensus::proposal", "consensus: Broadcasting finalized slot checkpoints");
for slot_checkpoint in to_broadcast_slot_checkpoints {
match sync_p2p.broadcast(slot_checkpoint).await {
Ok(()) => info!(target: "consensus::proposal", "consensus: Broadcasted slot_checkpoint"),
Err(e) => {
error!(target: "consensus::proposal", "consensus: Failed broadcasting slot_checkpoint: {}", e)
}
}
}
})
.detach();
} else {
info!(target: "consensus::proposal", "consensus: No finalized blocks or slot checkpoints to broadcast");
}
}
Err(e) => {
error!(target: "consensus::proposal", "consensus: Finalization check failed: {}", e);
}
}
// Verify node didn't skip next slot
completed_slot != state.read().await.consensus.current_slot()
}