/* 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() }