/* 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::{collections::HashMap, io::Cursor};
use async_std::sync::{Arc, RwLock};
use darkfi_sdk::{
crypto::{
constants::MERKLE_DEPTH,
contract_id::{CONSENSUS_CONTRACT_ID, DAO_CONTRACT_ID, MONEY_CONTRACT_ID},
schnorr::{SchnorrPublic, SchnorrSecret},
MerkleNode, PublicKey, SecretKey,
},
incrementalmerkletree::{bridgetree::BridgeTree, Tree},
pasta::{group::ff::PrimeField, pallas},
};
use darkfi_serial::{serialize, Decodable, Encodable, WriteExt};
use halo2_proofs::arithmetic::Field;
use log::{debug, error, info, warn};
use rand::rngs::OsRng;
use serde_json::json;
use super::{
constants,
lead_coin::LeadCoin,
state::{ConsensusState, Fork, SlotCheckpoint, StateCheckpoint},
BlockInfo, BlockProposal, Header, LeadInfo, LeadProof,
};
use crate::{
blockchain::{Blockchain, BlockchainOverlay, BlockchainOverlayPtr},
rpc::jsonrpc::JsonNotification,
runtime::vm_runtime::Runtime,
system::{Subscriber, SubscriberPtr},
tx::Transaction,
util::time::Timestamp,
wallet::WalletPtr,
zk::{
proof::{ProvingKey, VerifyingKey},
vm::ZkCircuit,
vm_heap::empty_witnesses,
},
zkas::ZkBinary,
Error, Result,
};
/// Atomic pointer to validator state.
pub type ValidatorStatePtr = Arc>;
/// This struct represents the state of a validator node.
pub struct ValidatorState {
/// Leader proof proving key
pub lead_proving_key: Option,
/// Leader proof verifying key
pub lead_verifying_key: VerifyingKey,
/// Hot/Live data used by the consensus algorithm
pub consensus: ConsensusState,
/// Canonical (finalized) blockchain
pub blockchain: Blockchain,
/// A map of various subscribers exporting live info from the blockchain
/// TODO: Instead of JsonNotification, it can be an enum of internal objects,
/// and then we don't have to deal with json in this module but only
// externally.
pub subscribers: HashMap<&'static str, SubscriberPtr>,
/// Wallet interface
pub wallet: WalletPtr,
/// Flag signalling node has finished initial sync
pub synced: bool,
/// Flag to enable single-node mode
pub single_node: bool,
}
impl ValidatorState {
#[allow(clippy::too_many_arguments)]
pub async fn new(
db: &sled::Db,
bootstrap_ts: Timestamp,
genesis_ts: Timestamp,
genesis_data: blake3::Hash,
initial_distribution: u64,
wallet: WalletPtr,
faucet_pubkeys: Vec,
enable_participation: bool,
single_node: bool,
) -> Result {
debug!(target: "consensus::validator", "Initializing ValidatorState");
debug!(target: "consensus::validator", "Initializing wallet tables for consensus");
// Initialize consensus coin table.
// NOTE: In future this will be redundant as consensus coins will live in the money contract.
if enable_participation {
wallet.exec_sql(include_str!("consensus_coin.sql")).await?;
}
debug!(target: "consensus::validator", "Generating leader proof keys with k: {}", constants::LEADER_PROOF_K);
let bincode = include_bytes!("../../proof/lead.zk.bin");
let zkbin = ZkBinary::decode(bincode)?;
let witnesses = empty_witnesses(&zkbin);
let circuit = ZkCircuit::new(witnesses, zkbin);
let lead_verifying_key = VerifyingKey::build(constants::LEADER_PROOF_K, &circuit);
// We only need this proving key if we're going to participate in the consensus.
let lead_proving_key = if enable_participation {
Some(ProvingKey::build(constants::LEADER_PROOF_K, &circuit))
} else {
None
};
let blockchain = Blockchain::new(db, genesis_ts, genesis_data)?;
let consensus = ConsensusState::new(
wallet.clone(),
blockchain.clone(),
bootstrap_ts,
genesis_ts,
genesis_data,
initial_distribution,
single_node,
);
// -----NATIVE WASM CONTRACTS-----
// This is the current place where native contracts are being deployed.
// When the `Blockchain` object is created, it doesn't care whether it
// already has the contract data or not. If there's existing data, it
// will just open the necessary db and trees, and give back what it has.
// This means that on subsequent runs our native contracts will already
// be in a deployed state, so what we actually do here is a redeployment.
// This kind of operation should only modify the contract's state in case
// it wasn't deployed before (meaning the initial run). Otherwise, it
// shouldn't touch anything, or just potentially update the db schemas or
// whatever is necessary. This logic should be handled in the init function
// of the actual contract, so make sure the native contracts handle this well.
// The faucet pubkeys are pubkeys which are allowed to create clear inputs
// in the money contract.
let money_contract_deploy_payload = serialize(&faucet_pubkeys);
let dao_contract_deploy_payload = vec![];
let consensus_contract_deploy_payload = vec![];
let native_contracts = vec![
(
"Money Contract",
*MONEY_CONTRACT_ID,
include_bytes!("../contract/money/money_contract.wasm").to_vec(),
money_contract_deploy_payload,
),
(
"DAO Contract",
*DAO_CONTRACT_ID,
include_bytes!("../contract/dao/dao_contract.wasm").to_vec(),
dao_contract_deploy_payload,
),
(
"Consensus Contract",
*CONSENSUS_CONTRACT_ID,
include_bytes!("../contract/consensus/consensus_contract.wasm").to_vec(),
consensus_contract_deploy_payload,
),
];
info!(target: "consensus::validator", "Deploying native wasm contracts");
let blockchain_overlay = BlockchainOverlay::new(&blockchain)?;
for nc in native_contracts {
info!(target: "consensus::validator", "Deploying {} with ContractID {}", nc.0, nc.1);
let mut runtime = Runtime::new(
&nc.2[..],
blockchain_overlay.clone(),
nc.1,
consensus.time_keeper.clone(),
)?;
runtime.deploy(&nc.3)?;
info!(target: "consensus::validator", "Successfully deployed {}", nc.0);
}
blockchain_overlay.lock().unwrap().overlay.lock().unwrap().apply()?;
info!(target: "consensus::validator", "Finished deployment of native wasm contracts");
// -----END NATIVE WASM CONTRACTS-----
// Here we initialize various subscribers that can export live consensus/blockchain data.
let mut subscribers = HashMap::new();
let block_subscriber = Subscriber::new();
let err_txs_subscriber = Subscriber::new();
subscribers.insert("blocks", block_subscriber);
subscribers.insert("err_txs", err_txs_subscriber);
let state = Arc::new(RwLock::new(ValidatorState {
lead_proving_key,
lead_verifying_key,
consensus,
blockchain,
subscribers,
wallet,
synced: false,
single_node,
}));
Ok(state)
}
/// The node retrieves a transaction, validates its state transition,
/// and appends it to the pending txs store.
pub async fn append_tx(&mut self, tx: Transaction) -> bool {
let tx_hash = blake3::hash(&serialize(&tx));
let tx_in_txstore = match self.blockchain.transactions.contains(&tx_hash) {
Ok(v) => v,
Err(e) => {
error!(target: "consensus::validator", "append_tx(): Failed querying txstore: {}", e);
return false
}
};
let tx_in_pending_txs_store = match self.blockchain.pending_txs.contains(&tx_hash) {
Ok(v) => v,
Err(e) => {
error!(target: "consensus::validator", "append_tx(): Failed querying pending txs store: {}", e);
return false
}
};
if tx_in_txstore || tx_in_pending_txs_store {
info!(target: "consensus::validator", "append_tx(): We have already seen this tx.");
return false
}
info!(target: "consensus::validator", "append_tx(): Starting state transition validation");
match self.verify_transactions(&[tx.clone()], false).await {
Ok(erroneous_txs) => {
if !erroneous_txs.is_empty() {
error!(target: "consensus::validator", "append_tx(): Erroneous transaction detected");
return false
}
}
Err(e) => {
error!(target: "consensus::validator", "append_tx(): Failed to verify transaction: {}", e);
return false
}
}
if let Err(e) = self.blockchain.add_pending_txs(&[tx]) {
error!(target: "consensus::validator", "append_tx(): Failed to insert transaction to pending txs store: {}", e);
return false
}
info!(target: "consensus::validator", "append_tx(): Appended tx to pending txs store");
true
}
/// The node retrieves transactions vector, validates their state transition,
/// and appends successfull ones to the pending txs store.
pub async fn append_pending_txs(&mut self, txs: &[Transaction]) {
let mut filtered_txs = vec![];
// Filter already seen transactions
for tx in txs {
let tx_hash = blake3::hash(&serialize(tx));
let tx_in_txstore = match self.blockchain.transactions.contains(&tx_hash) {
Ok(v) => v,
Err(e) => {
error!(target: "consensus::validator", "append_pending_txs(): Failed querying txstore: {}", e);
continue
}
};
let tx_in_pending_txs_store = match self.blockchain.pending_txs.contains(&tx_hash) {
Ok(v) => v,
Err(e) => {
error!(target: "consensus::validator", "append_pending_txs(): Failed querying pending txs store: {}", e);
continue
}
};
if tx_in_txstore || tx_in_pending_txs_store {
info!(target: "consensus::validator", "append_pending_txs(): We have already seen this tx.");
continue
}
filtered_txs.push(tx.clone());
}
// Verify transactions and filter erroneous ones
info!(target: "consensus::validator", "append_pending_txs(): Starting state transition validation");
let erroneous_txs = match self.verify_transactions(&filtered_txs[..], false).await {
Ok(erroneous_txs) => erroneous_txs,
Err(e) => {
error!(target: "consensus::validator", "append_pending_txs(): Failed to verify transactions: {}", e);
return
}
};
if !erroneous_txs.is_empty() {
filtered_txs.retain(|x| !erroneous_txs.contains(x));
}
if let Err(e) = self.blockchain.add_pending_txs(&filtered_txs) {
error!(target: "consensus::validator", "append_pending_txs(): Failed to insert transactions to pending txs store: {}", e);
return
}
info!(target: "consensus::validator", "append_pending_txs(): Appended tx to pending txs store");
}
/// The node removes erroneous transactions from the pending txs store.
async fn purge_pending_txs(&self) -> Result<()> {
info!(target: "consensus::validator", "purge_pending_txs(): Removing erroneous transactions from pending transactions store...");
let pending_txs = self.blockchain.get_pending_txs()?;
if pending_txs.is_empty() {
info!(target: "consensus::validator", "purge_pending_txs(): No pending transactions found");
return Ok(())
}
let erroneous_txs = self.verify_transactions(&pending_txs[..], false).await?;
if erroneous_txs.is_empty() {
info!(target: "consensus::validator", "purge_pending_txs(): No erroneous transactions found");
return Ok(())
}
info!(target: "consensus::validator", "purge_pending_txs(): Removing {} erroneous transactions...", erroneous_txs.len());
self.blockchain.remove_pending_txs(&erroneous_txs)?;
// TODO: Don't hardcode this:
let err_txs_subscriber = self.subscribers.get("err_txs").unwrap();
for err_tx in erroneous_txs {
let tx_hash = blake3::hash(&serialize(&err_tx)).to_hex().as_str().to_string();
let params = json!([bs58::encode(&serialize(&tx_hash)).into_string()]);
let notif = JsonNotification::new("blockchain.subscribe_err_txs", params);
info!(target: "consensus::validator", "purge_pending_txs(): Sending notification about erroneous transaction");
err_txs_subscriber.notify(notif).await;
}
Ok(())
}
/// Generate a block proposal for the current slot, containing all
/// pending transactions. Proposal extends the longest fork
/// chain the node is holding.
pub async fn propose(
&mut self,
slot: u64,
fork_index: i64,
coin_index: usize,
sigma1: pallas::Base,
sigma2: pallas::Base,
) -> Result