/* 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::{collections::HashSet, path::Path, str::FromStr, sync::Arc};
use async_trait::async_trait;
use darkfi_sdk::crypto::PublicKey;
use log::{error, info};
use smol::{
lock::{Mutex, MutexGuard},
stream::StreamExt,
};
use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
use url::Url;
use darkfi::{
async_daemonize, cli_desc,
consensus::{
constants::{
MAINNET_BOOTSTRAP_TIMESTAMP, MAINNET_GENESIS_HASH_BYTES, MAINNET_GENESIS_TIMESTAMP,
MAINNET_INITIAL_DISTRIBUTION, TESTNET_BOOTSTRAP_TIMESTAMP, TESTNET_GENESIS_HASH_BYTES,
TESTNET_GENESIS_TIMESTAMP, TESTNET_INITIAL_DISTRIBUTION,
},
proto::{ProtocolProposal, ProtocolSync, ProtocolSyncConsensus, ProtocolTx},
task::{block_sync_task, proposal_task},
validator::ValidatorStatePtr,
ValidatorState,
},
net,
net::P2pPtr,
rpc::{
clock_sync::check_clock,
jsonrpc::{ErrorCode::MethodNotFound, JsonError, JsonRequest, JsonResult},
server::{listen_and_serve, RequestHandler},
},
system::{StoppableTask, StoppableTaskPtr},
util::path::expand_path,
wallet::{WalletDb, WalletPtr},
Error, Result,
};
mod error;
use error::{server_error, RpcError};
const CONFIG_FILE: &str = "darkfid_config.toml";
const CONFIG_FILE_CONTENTS: &str = include_str!("../darkfid_config.toml");
#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
#[serde(default)]
#[structopt(name = "darkfid", about = cli_desc!())]
struct Args {
#[structopt(short, long)]
/// Configuration file to use
config: Option,
#[structopt(long, default_value = "testnet")]
/// Chain to use (testnet, mainnet)
chain: String,
#[structopt(long)]
/// Participate in consensus
consensus: bool,
#[structopt(long)]
/// Enable single-node mode for local testing
single_node: bool,
#[structopt(long, default_value = "~/.config/darkfi/darkfid_wallet.db")]
/// Path to wallet database
wallet_path: String,
#[structopt(long, default_value = "changeme")]
/// Password for the wallet database
wallet_pass: String,
#[structopt(long, default_value = "~/.config/darkfi/darkfid_blockchain")]
/// Path to blockchain database
database: String,
#[structopt(long, default_value = "tcp://127.0.0.1:8340")]
/// JSON-RPC listen URL
rpc_listen: Url,
#[structopt(long)]
/// P2P accept addresses for the consensus protocol (repeatable flag)
consensus_p2p_accept: Vec,
#[structopt(long)]
/// P2P external addresses for the consensus protocol (repeatable flag)
consensus_p2p_external: Vec,
#[structopt(long, default_value = "8")]
/// Connection slots for the consensus protocol
consensus_slots: usize,
#[structopt(long)]
/// Connect to peer for the consensus protocol (repeatable flag)
consensus_p2p_peer: Vec,
#[structopt(long)]
/// Peers JSON-RPC listen URL for clock synchronization (repeatable flag)
consensus_peer_rpc: Vec,
#[structopt(long)]
/// Connect to seed for the consensus protocol (repeatable flag)
consensus_p2p_seed: Vec,
#[structopt(long)]
/// Seed nodes JSON-RPC listen URL for clock synchronization (repeatable flag)
consensus_seed_rpc: Vec,
#[structopt(long)]
/// Prefered transports of outbound connections for the consensus protocol (repeatable flag)
consensus_p2p_transports: Vec,
#[structopt(long)]
/// P2P accept addresses for the syncing protocol (repeatable flag)
sync_p2p_accept: Vec,
#[structopt(long)]
/// P2P external addresses for the syncing protocol (repeatable flag)
sync_p2p_external: Vec,
#[structopt(long, default_value = "8")]
/// Connection slots for the syncing protocol
sync_slots: usize,
#[structopt(long)]
/// Connect to peer for the syncing protocol (repeatable flag)
sync_p2p_peer: Vec,
#[structopt(long)]
/// Connect to seed for the syncing protocol (repeatable flag)
sync_p2p_seed: Vec,
#[structopt(long)]
/// Prefered transports of outbound connections for the syncing protocol (repeatable flag)
sync_p2p_transports: Vec,
#[structopt(long)]
/// Enable localnet hosts
localnet: bool,
#[structopt(long)]
/// Enable channel log
channel_log: bool,
#[structopt(long)]
/// Whitelisted cashier public key (repeatable flag)
cashier_pub: Vec,
#[structopt(long)]
/// Whitelisted faucet public key (repeatable flag)
faucet_pub: Vec,
#[structopt(long)]
/// Verify system clock is correct
clock_sync: bool,
#[structopt(short, long)]
/// Set log file to ouput into
log: Option,
#[structopt(short, parse(from_occurrences))]
/// Increase verbosity (-vvv supported)
verbose: u8,
}
pub struct Darkfid {
synced: Mutex, // AtomicBool is weird in Arc
consensus_p2p: Option,
sync_p2p: Option,
_wallet: WalletPtr,
validator_state: ValidatorStatePtr,
rpc_connections: Mutex>,
}
// JSON-RPC methods
mod rpc_blockchain;
mod rpc_misc;
mod rpc_tx;
mod rpc_wallet;
// Internal methods
//mod internal;
#[async_trait]
impl RequestHandler for Darkfid {
async fn handle_request(&self, req: JsonRequest) -> JsonResult {
match req.method.as_str() {
// =====================
// Miscellaneous methods
// =====================
"ping" => return self.pong(req.id, req.params).await,
"clock" => return self.misc_clock(req.id, req.params).await,
"sync_dnet_switch" => return self.misc_sync_dnet_switch(req.id, req.params).await,
"consensus_dnet_switch" => {
return self.misc_consensus_dnet_switch(req.id, req.params).await
}
// ==================
// Blockchain methods
// ==================
"blockchain.get_slot" => return self.blockchain_get_slot(req.id, req.params).await,
"blockchain.get_tx" => return self.blockchain_get_tx(req.id, req.params).await,
"blockchain.last_known_slot" => {
return self.blockchain_last_known_slot(req.id, req.params).await
}
"blockchain.subscribe_blocks" => {
return self.blockchain_subscribe_blocks(req.id, req.params).await
}
"blockchain.subscribe_err_txs" => {
return self.blockchain_subscribe_err_txs(req.id, req.params).await
}
"blockchain.lookup_zkas" => {
return self.blockchain_lookup_zkas(req.id, req.params).await
}
// ===================
// Transaction methods
// ===================
"tx.simulate" => return self.tx_simulate(req.id, req.params).await,
"tx.broadcast" => return self.tx_broadcast(req.id, req.params).await,
// ==============
// Wallet methods
// ==============
"wallet.exec_sql" => return self.wallet_exec_sql(req.id, req.params).await,
"wallet.query_row_single" => {
return self.wallet_query_row_single(req.id, req.params).await
}
"wallet.query_row_multi" => {
return self.wallet_query_row_multi(req.id, req.params).await
}
// ==============
// Invalid method
// ==============
_ => return JsonError::new(MethodNotFound, None, req.id).into(),
}
}
async fn connections_mut(&self) -> MutexGuard<'_, HashSet> {
self.rpc_connections.lock().await
}
}
impl Darkfid {
pub async fn new(
validator_state: ValidatorStatePtr,
consensus_p2p: Option,
sync_p2p: Option,
_wallet: WalletPtr,
) -> Self {
Self {
synced: Mutex::new(false),
consensus_p2p,
sync_p2p,
_wallet,
validator_state,
rpc_connections: Mutex::new(HashSet::new()),
}
}
}
async_daemonize!(realmain);
async fn realmain(args: Args, ex: Arc>) -> Result<()> {
if args.consensus && args.clock_sync {
// We verify that if peer/seed nodes are configured, their rpc config also exists
if ((!args.consensus_p2p_peer.is_empty() && args.consensus_peer_rpc.is_empty()) ||
(args.consensus_p2p_peer.is_empty() && !args.consensus_peer_rpc.is_empty())) ||
((!args.consensus_p2p_seed.is_empty() && args.consensus_seed_rpc.is_empty()) ||
(args.consensus_p2p_seed.is_empty() && !args.consensus_seed_rpc.is_empty()))
{
error!(
"Consensus peer/seed nodes misconfigured: both p2p and rpc urls must be present"
);
return Err(Error::ConfigInvalid)
}
// We verify that the system clock is valid before initializing
let peers = [&args.consensus_peer_rpc[..], &args.consensus_seed_rpc[..]].concat();
if (check_clock(&peers).await).is_err() {
error!("System clock is invalid, terminating...");
return Err(Error::InvalidClock)
};
}
// Initialize or load wallet
let wallet = WalletDb::new(Some(expand_path(&args.wallet_path)?), Some(&args.wallet_pass))?;
// Initialize or open sled database
let db_path =
Path::new(expand_path(&args.database)?.to_str().unwrap()).join(args.chain.clone());
let sled_db = sled::open(&db_path)?;
// Initialize validator state
let (bootstrap_ts, genesis_ts, genesis_data, initial_distribution) = match args.chain.as_str() {
"mainnet" => (
*MAINNET_BOOTSTRAP_TIMESTAMP,
*MAINNET_GENESIS_TIMESTAMP,
*MAINNET_GENESIS_HASH_BYTES,
*MAINNET_INITIAL_DISTRIBUTION,
),
"testnet" => (
*TESTNET_BOOTSTRAP_TIMESTAMP,
*TESTNET_GENESIS_TIMESTAMP,
*TESTNET_GENESIS_HASH_BYTES,
*TESTNET_INITIAL_DISTRIBUTION,
),
x => {
error!("Unsupported chain `{}`", x);
return Err(Error::UnsupportedChain)
}
};
// Parse faucet addresses
let mut faucet_pubkeys = vec![];
for i in args.cashier_pub {
let pk = PublicKey::from_str(&i)?;
faucet_pubkeys.push(pk);
}
for i in args.faucet_pub {
let pk = PublicKey::from_str(&i)?;
faucet_pubkeys.push(pk);
}
if args.single_node {
info!("Node is configured to run in single-node mode!");
}
// Initialize validator state
let state = ValidatorState::new(
&sled_db,
bootstrap_ts,
genesis_ts,
genesis_data,
initial_distribution,
wallet.clone(),
faucet_pubkeys,
args.consensus,
args.single_node,
)
.await?;
let sync_p2p = {
info!("Registering block sync P2P protocols...");
let sync_network_settings = net::Settings {
inbound_addrs: args.sync_p2p_accept,
outbound_connections: args.sync_slots,
external_addrs: args.sync_p2p_external,
peers: args.sync_p2p_peer.clone(),
seeds: args.sync_p2p_seed.clone(),
allowed_transports: args.sync_p2p_transports,
localnet: args.localnet,
..Default::default()
};
let p2p = net::P2p::new(sync_network_settings, ex.clone()).await;
let registry = p2p.protocol_registry();
let _state = state.clone();
registry
.register(net::SESSION_ALL, move |channel, p2p| {
let state = _state.clone();
async move {
ProtocolSync::init(channel, state, p2p, args.consensus)
.await
.unwrap()
}
})
.await;
let _state = state.clone();
registry
.register(net::SESSION_ALL, move |channel, p2p| {
let state = _state.clone();
async move { ProtocolTx::init(channel, state, p2p).await.unwrap() }
})
.await;
Some(p2p)
};
// P2P network settings for the consensus protocol
let consensus_p2p = {
if !args.consensus {
None
} else {
info!("Registering consensus P2P protocols...");
let consensus_network_settings = net::Settings {
inbound_addrs: args.consensus_p2p_accept,
outbound_connections: args.consensus_slots,
external_addrs: args.consensus_p2p_external,
peers: args.consensus_p2p_peer.clone(),
seeds: args.consensus_p2p_seed.clone(),
allowed_transports: args.consensus_p2p_transports,
localnet: args.localnet,
..Default::default()
};
let p2p = net::P2p::new(consensus_network_settings, ex.clone()).await;
let registry = p2p.protocol_registry();
let _state = state.clone();
registry
.register(net::SESSION_ALL, move |channel, p2p| {
let state = _state.clone();
async move { ProtocolProposal::init(channel, state, p2p).await.unwrap() }
})
.await;
let _state = state.clone();
registry
.register(net::SESSION_ALL, move |channel, p2p| {
let state = _state.clone();
async move { ProtocolSyncConsensus::init(channel, state, p2p).await.unwrap() }
})
.await;
Some(p2p)
}
};
// Initialize program state
let darkfid =
Darkfid::new(state.clone(), consensus_p2p.clone(), sync_p2p.clone(), wallet.clone()).await;
let darkfid = Arc::new(darkfid);
// JSON-RPC server
info!("Starting JSON-RPC server");
let rpc_task = StoppableTask::new();
let darkfid_ = darkfid.clone();
rpc_task.clone().start(
listen_and_serve(args.rpc_listen, darkfid.clone(), None, ex.clone()),
|res| async move {
match res {
Ok(()) | Err(Error::RpcServerStopped) => darkfid_.stop_connections().await,
Err(e) => error!(target: "darkfid", "Failed starting sync JSON-RPC server: {}", e),
}
},
Error::RpcServerStopped,
ex.clone(),
);
info!("Starting sync P2P network");
sync_p2p.clone().unwrap().start().await?;
// TODO: I think this is not necessary anymore
//info!("Waiting for sync P2P outbound connections");
//sync_p2p.clone().unwrap().wait_for_outbound(ex.clone()).await?;
match block_sync_task(sync_p2p.clone().unwrap(), state.clone()).await {
Ok(()) => *darkfid.synced.lock().await = true,
Err(e) => error!("Failed syncing blockchain: {}", e),
}
// Consensus protocol
let proposal_task = if args.consensus && *darkfid.synced.lock().await {
info!("Starting consensus P2P network");
let consensus_p2p = consensus_p2p.clone().unwrap();
consensus_p2p.clone().start().await?;
// TODO: I think this is not necessary anymore
//info!("Waiting for consensus P2P outbound connections");
//consensus_p2p.clone().unwrap().wait_for_outbound(ex.clone()).await?;
info!("Starting consensus protocol task");
let task = StoppableTask::new();
task.clone().start(
proposal_task(consensus_p2p.clone(), sync_p2p.clone().unwrap(), state, ex.clone()),
|res| async {
match res {
Ok(()) | Err(Error::ProposalTaskStopped) => { /* Do nothing */ }
Err(e) => error!(target: "darkfid", "Failed starting proposal task: {}", e),
}
},
Error::ProposalTaskStopped,
ex.clone(),
);
Some(task)
} else {
info!("Not starting consensus P2P network");
None
};
// Signal handling for graceful termination.
let (signals_handler, signals_task) = SignalHandler::new(ex)?;
signals_handler.wait_termination(signals_task).await?;
info!("Caught termination signal, cleaning up and exiting...");
info!(target: "darkfid", "Stopping JSON-RPC server...");
rpc_task.stop().await;
info!(target: "darkfid", "Stopping syncing P2P network...");
sync_p2p.clone().unwrap().stop().await;
if let Some(task) = proposal_task {
info!(target: "darkfid", "Stopping proposal task...");
task.stop().await;
info!(target: "darkfid", "Stopping consensus P2P network...");
consensus_p2p.unwrap().stop().await;
}
info!("Flushing sled database...");
let flushed_bytes = sled_db.flush_async().await?;
info!("Flushed {} bytes", flushed_bytes);
Ok(())
}