/* 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, HashSet},
path::Path,
str::FromStr,
sync::Arc,
};
use async_trait::async_trait;
use chrono::Utc;
use darkfi_money_contract::{
client::{
transfer_v1::TransferCallBuilder, MONEY_KEYS_COL_IS_DEFAULT, MONEY_KEYS_COL_PUBLIC,
MONEY_KEYS_COL_SECRET, MONEY_KEYS_TABLE, MONEY_TREE_COL_TREE, MONEY_TREE_TABLE,
},
MoneyFunction::TransferV1 as MoneyTransfer,
MONEY_CONTRACT_ZKAS_BURN_NS_V1, MONEY_CONTRACT_ZKAS_MINT_NS_V1,
};
use darkfi_sdk::{
crypto::{
contract_id::MONEY_CONTRACT_ID, mimc_vdf, pasta_prelude::Field, Keypair, MerkleNode,
MerkleTree, PublicKey, DARK_TOKEN_ID,
},
num_bigint::BigUint,
num_traits::Num,
pasta::{group::ff::PrimeField, pallas},
tx::ContractCall,
};
use darkfi_serial::{deserialize, serialize, Encodable};
use log::{debug, error, info};
use rand::rngs::OsRng;
use smol::{
lock::{Mutex, MutexGuard, RwLock},
stream::StreamExt,
};
use structopt_toml::{serde::Deserialize, structopt::StructOpt, StructOptToml};
use tinyjson::JsonValue;
use url::Url;
use darkfi::{
async_daemonize,
blockchain::contract_store::SMART_CONTRACT_ZKAS_DB_NAME,
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::{ProtocolSync, ProtocolTx},
task::block_sync_task,
ValidatorState, ValidatorStatePtr,
},
net,
net::P2pPtr,
rpc::{
jsonrpc::{
ErrorCode::{InternalError, InvalidParams, MethodNotFound},
JsonError, JsonRequest, JsonResponse, JsonResult,
},
server::{listen_and_serve, RequestHandler},
},
system::{sleep, StoppableTask, StoppableTaskPtr},
tx::Transaction,
util::{parse::decode_base10, path::expand_path},
wallet::{WalletDb, WalletPtr},
zk::{proof::ProvingKey, vm::ZkCircuit, vm_heap::empty_witnesses},
zkas::ZkBinary,
Error, Result,
};
mod error;
use error::{server_error, RpcError};
const CONFIG_FILE: &str = "faucetd_config.toml";
const CONFIG_FILE_CONTENTS: &str = include_str!("../faucetd_config.toml");
#[derive(Clone, Debug, Deserialize, StructOpt, StructOptToml)]
#[serde(default)]
#[structopt(name = "faucetd", 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, default_value = "~/.config/darkfi/faucetd_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/faucetd_blockchain")]
/// Path to blockchain database
database: String,
#[structopt(long, default_value = "tcp://127.0.0.1:9340")]
/// JSON-RPC listen URL
rpc_listen: Url,
#[structopt(long)]
/// P2P accept addresses for the syncing protocol
sync_p2p_accept: Vec,
#[structopt(long)]
/// P2P external addresses for the syncing protocol
sync_p2p_external: Vec,
#[structopt(long, default_value = "8")]
/// Connection slots for the syncing protocol
sync_slots: usize,
#[structopt(long)]
/// Connect to seed for the syncing protocol (repeatable flag)
sync_p2p_seed: Vec,
#[structopt(long)]
/// Connect to peer for the syncing protocol (repeatable flag)
sync_p2p_peer: 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)]
/// Whitelisted cashier address (repeatable flag)
cashier_pub: Vec,
#[structopt(long)]
/// Whitelisted faucet address (repeatable flag)
faucet_pub: Vec,
#[structopt(long, default_value = "600")]
/// Airdrop timeout limit in seconds
airdrop_timeout: i64,
#[structopt(long, default_value = "10")]
/// Airdrop amount limit
airdrop_limit: String, // We convert this to u64 with decode_base10
#[structopt(short, long)]
/// Set log file to ouput into
log: Option,
#[structopt(short, parse(from_occurrences))]
/// Increase verbosity (-vvv supported)
verbose: u8,
}
type ProvingKeyMap = Arc>>>;
type AirdropMap = Arc>>;
type ChallengeMap = Arc>>;
pub struct Faucetd {
synced: Mutex, // AtomicBool is weird in Arc
sync_p2p: P2pPtr,
validator_state: ValidatorStatePtr,
keypair: Keypair,
_wallet: WalletPtr,
merkle_tree: MerkleTree,
airdrop_timeout: i64,
airdrop_limit: u64,
airdrop_map: AirdropMap,
challenge_map: ChallengeMap,
proving_keys: ProvingKeyMap,
rpc_connections: Mutex>,
}
#[async_trait]
impl RequestHandler for Faucetd {
async fn handle_request(&self, req: JsonRequest) -> JsonResult {
match req.method.as_str() {
"challenge" => return self.challenge(req.id, req.params).await,
"airdrop" => return self.airdrop(req.id, req.params).await,
_ => return JsonError::new(MethodNotFound, None, req.id).into(),
}
}
async fn connections_mut(&self) -> MutexGuard<'_, HashSet> {
self.rpc_connections.lock().await
}
}
impl Faucetd {
pub async fn new(
validator_state: ValidatorStatePtr,
sync_p2p: P2pPtr,
wallet: WalletPtr,
timeout: i64,
limit: u64,
) -> Result {
// Here we initialize the wallet for the money contract.
let merkle_tree = Self::initialize_wallet(wallet.clone()).await?;
// This is kinda bad, but whatever. The hashmaps hold proving keys for
// the money contract. We keep it under RwLock in case we want to add
// other proving keys to it later.
let proving_keys = Arc::new(RwLock::new(HashMap::new()));
// For now we'll create the keys for the money contract
let cid = *MONEY_CONTRACT_ID;
// Do a lookup for the money contract's zkas database and fetch the circuits.
let blockchain = { validator_state.read().await.blockchain.clone() };
let db_handle =
blockchain.contracts.lookup(&blockchain.sled_db, &cid, SMART_CONTRACT_ZKAS_DB_NAME)?;
let Some(mint_zkbytes) = db_handle.get(serialize(&MONEY_CONTRACT_ZKAS_MINT_NS_V1))? else {
error!(target: "faucetd", "{} zkas bincode not found in sled database", MONEY_CONTRACT_ZKAS_MINT_NS_V1);
return Err(Error::ZkasBincodeNotFound)
};
let Some(burn_zkbytes) = db_handle.get(serialize(&MONEY_CONTRACT_ZKAS_BURN_NS_V1))? else {
error!(target: "faucetd", "{} zkas bincode not found in sled database", MONEY_CONTRACT_ZKAS_BURN_NS_V1);
return Err(Error::ZkasBincodeNotFound)
};
let (mint_zkbin, _): (Vec, Vec) = deserialize(&mint_zkbytes)?;
let (burn_zkbin, _): (Vec, Vec) = deserialize(&burn_zkbytes)?;
let mint_zkbin = ZkBinary::decode(&mint_zkbin)?;
let mint_circuit = ZkCircuit::new(empty_witnesses(&mint_zkbin)?, &mint_zkbin);
let burn_zkbin = ZkBinary::decode(&burn_zkbin)?;
let burn_circuit = ZkCircuit::new(empty_witnesses(&burn_zkbin)?, &burn_zkbin);
info!(target: "faucetd", "Creating mint circuit proving key");
let mint_provingkey = ProvingKey::build(mint_zkbin.k, &mint_circuit);
info!(target: "faucetd", "Creating burn circuit proving key");
let burn_provingkey = ProvingKey::build(burn_zkbin.k, &burn_circuit);
{
let provingkeys = vec![
(MONEY_CONTRACT_ZKAS_MINT_NS_V1.to_string(), mint_provingkey, mint_zkbin),
(MONEY_CONTRACT_ZKAS_BURN_NS_V1.to_string(), burn_provingkey, burn_zkbin),
];
let mut proving_keys_w = proving_keys.write().await;
proving_keys_w.insert(cid.inner().to_repr(), provingkeys);
}
// Get or create an initial keypair for signing transactions
let keypair = Self::initialize_keypair(wallet.clone()).await?;
info!(target: "faucetd", "Faucet pubkey: {}", keypair.public);
let faucetd = Self {
synced: Mutex::new(false),
sync_p2p,
validator_state,
keypair,
_wallet: wallet,
merkle_tree,
airdrop_timeout: timeout,
airdrop_limit: limit,
airdrop_map: Arc::new(Mutex::new(HashMap::new())),
challenge_map: Arc::new(Mutex::new(HashMap::new())),
proving_keys,
rpc_connections: Mutex::new(HashSet::new()),
};
Ok(faucetd)
}
async fn initialize_wallet(wallet: WalletPtr) -> Result {
// Perform wallet initialization for the money contract
let wallet_schema = include_str!("../../../src/contract/money/wallet.sql");
// Get a wallet connection
info!(target: "faucetd", "Acquiring wallet connection");
let conn = wallet.conn.lock().await;
info!(target: "faucetd", "Initializing wallet schema");
conn.execute(wallet_schema, rusqlite::params![])?;
let query = format!("SELECT * FROM {}", MONEY_TREE_COL_TREE);
let merkle_tree = conn.query_row(&query, [], |row| {
let tree_bytes: Vec = row.get(MONEY_TREE_COL_TREE)?;
Ok(deserialize(&tree_bytes).unwrap())
});
let merkle_tree = match merkle_tree {
Ok(v) => {
info!(target: "faucetd", "Merkle tree already exists");
v
}
Err(_) => {
let mut tree = MerkleTree::new(100);
tree.append(MerkleNode::from(pallas::Base::ZERO));
let tree_bytes = serialize(&tree);
let query = format!(
"DELETE FROM {}; INSERT INTO {} ({}) VALUES (?1)",
MONEY_TREE_TABLE, MONEY_TREE_TABLE, MONEY_TREE_COL_TREE
);
conn.execute(&query, rusqlite::params![tree_bytes])?;
info!(target: "faucetd", "Successfully initialized Merkle tree");
tree
}
};
Ok(merkle_tree)
}
async fn initialize_keypair(wallet: WalletPtr) -> Result {
let conn = wallet.conn.lock().await;
let query = format!(
"SELECT {}, {} FROM {};",
MONEY_KEYS_COL_PUBLIC, MONEY_KEYS_COL_SECRET, MONEY_KEYS_TABLE
);
let keypair = conn.query_row(&query, [], |row| {
let public_bytes: Vec = row.get("public")?;
let secret_bytes: Vec = row.get("secret")?;
let public = deserialize(&public_bytes).unwrap();
let secret = deserialize(&secret_bytes).unwrap();
Ok(Keypair { public, secret })
});
let keypair = match keypair {
Ok(k) => k,
Err(_) => {
let keypair = Keypair::random(&mut OsRng);
let is_default = 0;
let public_bytes = serialize(&keypair.public);
let secret_bytes = serialize(&keypair.secret);
let query = format!(
"INSERT INTO {} ({}, {}, {}) VALUES (?1, ?2, ?3)",
MONEY_KEYS_TABLE,
MONEY_KEYS_COL_IS_DEFAULT,
MONEY_KEYS_COL_PUBLIC,
MONEY_KEYS_COL_SECRET
);
conn.execute(&query, rusqlite::params![is_default, public_bytes, secret_bytes])?;
info!(target: "faucetd", "Wrote keypair to wallet");
keypair
}
};
Ok(keypair)
}
// RPCAPI:
// Request a VDF challenge in order to become eligible for an airdrop. It is then
// necessary to execute the VDF with the challenge as input and pass it to the
// `airdrop` call, which the faucet will then verify.
//
// **Params:**
// * `array[0]`: base58 encoded address string of the recipient
//
// **Returns:**
// * `array[0]`: hex-encoded challenge string
// * `array[1]`: n steps (`u64`) needed for VDF evaluation
//
// --> {"jsonrpc": "2.0", "method": "challenge", "params": ["1DarkFi..."], "id": 1}
// <-- {"jsonrpc": "2.0", "result": ["0x123...", 10000], "id": 1}
async fn challenge(&self, id: u16, params: JsonValue) -> JsonResult {
const N_STEPS: u64 = 2_000_000;
let params = params.get::>().unwrap();
if params.len() != 1 || !params[0].is_string() {
return JsonError::new(InvalidParams, None, id).into()
}
if !(*self.synced.lock().await) {
error!(target: "faucetd", "challenge(): Blockchain is not yet synced");
return JsonError::new(InternalError, None, id).into()
}
let pubkey = params[0].get::().unwrap();
let pubkey = match PublicKey::from_str(pubkey) {
Ok(v) => v,
Err(e) => {
error!(target: "faucetd", "challenge(): Failed parsing PublicKey from String: {}", e);
return server_error(RpcError::ParseError, id)
}
};
let map = self.challenge_map.lock().await;
if map.contains_key(&pubkey.to_bytes()) {
return server_error(RpcError::RateLimitReached, id)
}
drop(map);
// Create a random challenge
let mut hasher = blake3::Hasher::new();
hasher.update(&pubkey.to_bytes());
hasher.update(&pallas::Base::random(&mut OsRng).to_repr());
let h = hasher.finalize();
let c = BigUint::from_str_radix(&h.to_hex(), 16).unwrap();
// Add/Update this airdrop into the hashmap
let mut map = self.challenge_map.lock().await;
map.insert(pubkey.to_bytes(), (c.clone(), N_STEPS));
drop(map);
let chall = JsonValue::from(c.to_str_radix(16));
let steps = JsonValue::from(N_STEPS as f64);
JsonResponse::new(JsonValue::Array(vec![chall, steps]), id).into()
}
// RPCAPI:
// Processes a native token airdrop request and airdrops requested amount to address.
// Returns the transaction ID upon success.
//
// **Params:**
// * `array[0]`: base58 encoded address string of the recipient
// * `array[1]`: Amount to airdrop in form of f64
// * `array[2]`: VDF evaluation witness as hex-encoded BigUint string
//
// **Returns:**
// * hex-encoded transaction ID string
//
// --> {"jsonrpc": "2.0", "method": "airdrop", "params": ["1DarkFi...", 1.42, "0x123..."], "id": 1}
// <-- {"jsonrpc": "2.0", "result": "txID", "id": 1}
async fn airdrop(&self, id: u16, params: JsonValue) -> JsonResult {
let params = params.get::>().unwrap();
if params.len() != 3 ||
!params[0].is_string() ||
!params[1].is_number() ||
!params[2].is_string()
{
return JsonError::new(InvalidParams, None, id).into()
}
if !(*self.synced.lock().await) {
error!(target: "faucetd", "airdrop(): Blockchain is not yet synced");
return JsonError::new(InternalError, None, id).into()
}
// Decode public key
let pubkey = params[0].get::().unwrap();
let pubkey = match PublicKey::from_str(pubkey) {
Ok(v) => v,
Err(e) => {
error!(target: "faucetd", "airdrop(): Failed parsing PublicKey from String: {}", e);
return server_error(RpcError::ParseError, id)
}
};
// Decode requested airdrop amount
let amount = params[1].get::().unwrap().to_string();
let amount = match decode_base10(&amount, 8, true) {
Ok(v) => v,
Err(_) => {
error!(target: "faucetd", "airdrop(): Failed parsing amount from string");
return server_error(RpcError::ParseError, id)
}
};
if amount > self.airdrop_limit {
return server_error(RpcError::AmountExceedsLimit, id)
}
// Decode VDF witness
let witness = params[2].get::().unwrap();
let Ok(witness) = BigUint::from_str_radix(witness, 16) else {
error!(target: "faucetd", "airdrop(): Failed parsing VDF witness from string");
return server_error(RpcError::ParseError, id)
};
// Check if there as a previous airdrop and the timeout has passed.
let now = Utc::now().timestamp();
let map = self.airdrop_map.lock().await;
if let Some(last_airdrop) = map.get(&pubkey.to_bytes()) {
if now - last_airdrop <= self.airdrop_timeout {
error!(target: "faucetd", "airdrop(): Time limit reached for {}", pubkey);
return server_error(RpcError::TimeLimitReached, id)
}
};
drop(map);
// Check if a VDF challenge exists
let map = self.challenge_map.lock().await;
let Some((challenge, n_steps)) = map.get(&pubkey.to_bytes()).cloned() else {
error!(target: "faucetd", "airdrop(): No VDF challenge found for {}", pubkey);
return server_error(RpcError::NoVdfChallenge, id)
};
drop(map);
// Verify the VDF
info!(target: "faucetd", "airdrop(): Verifying VDF for {}...", pubkey);
if !mimc_vdf::verify(&challenge, n_steps, &witness) {
error!(target: "faucetd", "airdrop(): VDF verification failed for {}", pubkey);
return server_error(RpcError::VdfVerifyFailed, id)
}
// Remove the challenge from the map at this point. Latter stuff might
// fail, but we want clients to be able to request things again.
let mut mut_map = self.challenge_map.lock().await;
mut_map.remove(&pubkey.to_bytes());
drop(mut_map);
let cid = *MONEY_CONTRACT_ID;
let (mint_zkbin, mint_pk, burn_zkbin, burn_pk) = {
let proving_keys_r = self.proving_keys.read().await;
let Some(arr) = proving_keys_r.get(&cid.to_bytes()) else {
error!(target: "faucetd", "Contract ID {} not found in proving keys hashmap", cid);
return server_error(RpcError::InternalError, id)
};
let Some(mint_data) = arr.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_MINT_NS_V1) else {
error!(target: "faucetd", "{} proof data not found in vector", MONEY_CONTRACT_ZKAS_MINT_NS_V1);
return server_error(RpcError::InternalError, id)
};
let Some(burn_data) = arr.iter().find(|x| x.0 == MONEY_CONTRACT_ZKAS_BURN_NS_V1) else {
error!(target: "faucetd", "{} proof data not found in vector", MONEY_CONTRACT_ZKAS_BURN_NS_V1);
return server_error(RpcError::InternalError, id)
};
(mint_data.2.clone(), mint_data.1.clone(), burn_data.2.clone(), burn_data.1.clone())
};
// Create money contract transfer params and proofs
let builder = TransferCallBuilder {
keypair: self.keypair,
recipient: pubkey,
value: amount,
token_id: *DARK_TOKEN_ID,
rcpt_spend_hook: pallas::Base::zero(),
rcpt_user_data: pallas::Base::zero(),
rcpt_user_data_blind: pallas::Base::random(&mut OsRng),
change_spend_hook: pallas::Base::zero(),
change_user_data: pallas::Base::zero(),
change_user_data_blind: pallas::Base::random(&mut OsRng),
coins: vec![],
tree: self.merkle_tree.clone(),
mint_zkbin,
mint_pk,
burn_zkbin,
burn_pk,
clear_input: true,
};
let debris = match builder.build() {
Ok(v) => v,
Err(e) => {
error!(target: "faucetd", "Failed to build transfer tx params: {}", e);
return server_error(RpcError::InternalError, id)
}
};
// Build transaction
let mut data = vec![MoneyTransfer as u8];
debris.params.encode(&mut data).unwrap();
let calls = vec![ContractCall { contract_id: cid, data }];
let proofs = vec![debris.proofs];
let mut tx = Transaction { calls, proofs, signatures: vec![] };
let sigs = tx.create_sigs(&mut OsRng, &debris.signature_secrets).unwrap();
tx.signatures = vec![sigs];
// Safety check to see if the transaction is actually valid.
let lock = self.validator_state.read().await;
let current_slot = lock.consensus.time_keeper.current_slot();
if let Err(e) = lock.verify_transactions(&[tx.clone()], current_slot, false).await {
error!(target: "faucetd", "airdrop(): Failed to verify transaction before broadcasting: {}", e);
return JsonError::new(InternalError, None, id).into()
}
// Broadcast transaction to the network.
self.sync_p2p.broadcast(&tx).await;
// Add/Update this airdrop into the hashmap
let mut map = self.airdrop_map.lock().await;
map.insert(pubkey.to_bytes(), now);
drop(map);
let tx_hash = blake3::hash(&serialize(&tx)).to_hex().as_str().to_string();
JsonResponse::new(JsonValue::String(tx_hash), id).into()
}
}
async fn prune_airdrop_maps(
rate_map: AirdropMap,
challenge_map: ChallengeMap,
timeout: i64,
) -> Result<()> {
loop {
sleep(timeout as u64).await;
debug!(target: "faucetd", "Pruning airdrop maps");
let now = Utc::now().timestamp();
let mut prune = vec![];
let im_map = rate_map.lock().await;
for (k, v) in im_map.iter() {
if now - *v > timeout {
prune.push(*k);
}
}
drop(im_map);
let mut mut_rate_map = rate_map.lock().await;
let mut mut_challenge_map = challenge_map.lock().await;
for i in prune {
mut_rate_map.remove(&i);
mut_challenge_map.remove(&i);
}
drop(mut_rate_map);
drop(mut_challenge_map);
}
}
async_daemonize!(realmain);
async fn realmain(args: Args, ex: Arc>) -> Result<()> {
// 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!(target: "faucetd", "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);
}
// Initialize validator state
let state = ValidatorState::new(
&sled_db,
bootstrap_ts,
genesis_ts,
genesis_data,
initial_distribution,
wallet.clone(),
faucet_pubkeys,
false,
false,
)
.await?;
// P2P network. The faucet doesn't participate in consensus, so we only
// build the sync protocol.
let 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 sync_p2p = net::P2p::new(network_settings, ex.clone()).await;
let registry = sync_p2p.protocol_registry();
info!(target: "faucetd", "Registering block sync P2P protocols...");
let _state = state.clone();
registry
.register(net::SESSION_ALL, move |channel, p2p| {
let state = _state.clone();
async move { ProtocolSync::init(channel, state, p2p, false).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;
let airdrop_timeout = args.airdrop_timeout;
let airdrop_limit = decode_base10(&args.airdrop_limit, 8, true)?;
// Initialize program state
let faucetd = Faucetd::new(
state.clone(),
sync_p2p.clone(),
wallet.clone(),
airdrop_timeout,
airdrop_limit,
)
.await?;
let faucetd = Arc::new(faucetd);
// Task to periodically clean up the airdrop rate/challenge hashmaps
let airdrop_task = StoppableTask::new();
airdrop_task.clone().start(
prune_airdrop_maps(
faucetd.airdrop_map.clone(),
faucetd.challenge_map.clone(),
airdrop_timeout,
),
|res| async {
match res {
Ok(()) | Err(Error::DetachedTaskStopped) => { /* Do nothing */ }
Err(e) => error!(target: "faucetd", "Failed starting airdrop prune task: {}", e),
}
},
Error::DetachedTaskStopped,
ex.clone(),
);
// JSON-RPC server
info!(target: "faucetd", "Starting JSON-RPC server");
let rpc_task = StoppableTask::new();
let faucetd_ = faucetd.clone();
rpc_task.clone().start(
listen_and_serve(args.rpc_listen, faucetd.clone(), None, ex.clone()),
|res| async move {
match res {
Ok(()) | Err(Error::RpcServerStopped) => faucetd_.stop_connections().await,
Err(e) => error!(target: "faucetd", "Failed starting JSON-RPC server: {}", e),
}
},
Error::RpcServerStopped,
ex.clone(),
);
info!(target: "faucetd", "Starting sync P2P network");
sync_p2p.clone().start().await?;
// TODO: I think this is not needed anymore
//info!("Waiting for sync P2P outbound connections");
//sync_p2p.clone().wait_for_outbound(ex).await?;
match block_sync_task(sync_p2p.clone(), state.clone()).await {
Ok(()) => *faucetd.synced.lock().await = true,
Err(e) => error!(target: "faucetd", "Failed syncing blockchain: {}", e),
}
// Signal handling for graceful termination.
let (signals_handler, signals_task) = SignalHandler::new(ex)?;
signals_handler.wait_termination(signals_task).await?;
info!(target: "faucetd", "Caught termination signal, cleaning up and exiting...");
info!(target: "faucetd", "Stopping JSON-RPC server...");
rpc_task.stop().await;
info!(target: "faucetd", "Stopping airdrop prune task...");
airdrop_task.stop().await;
info!(target: "faucetd", "Stopping syncing P2P network...");
sync_p2p.stop().await;
info!(target: "faucetd", "Flushing database...");
let flushed_bytes = sled_db.flush_async().await?;
info!(target: "faucetd", "Flushed {} bytes", flushed_bytes);
Ok(())
}