main.rs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. /* This file is part of DarkFi (https://dark.fi)
  2. *
  3. * Copyright (C) 2020-2026 Dyne.org foundation
  4. *
  5. * This program is free software: you can redistribute it and/or modify
  6. * it under the terms of the GNU Affero General Public License as
  7. * published by the Free Software Foundation, either version 3 of the
  8. * License, or (at your option) any later version.
  9. *
  10. * This program is distributed in the hope that it will be useful,
  11. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  12. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  13. * GNU Affero General Public License for more details.
  14. *
  15. * You should have received a copy of the GNU Affero General Public License
  16. * along with this program. If not, see <https://www.gnu.org/licenses/>.
  17. */
  18. use std::{
  19. collections::HashSet,
  20. path::{Path, PathBuf},
  21. process::exit,
  22. sync::{
  23. atomic::{AtomicBool, Ordering},
  24. Arc,
  25. },
  26. };
  27. use arg::Args;
  28. use async_trait::async_trait;
  29. use darkfi::{
  30. blockchain::BlockInfo,
  31. rpc::{
  32. client::RpcClient,
  33. jsonrpc::{ErrorCode, JsonError, JsonRequest, JsonResult},
  34. server::{listen_and_serve, RequestHandler},
  35. settings::RpcSettings,
  36. },
  37. system::{msleep, CondVar, Publisher, PublisherPtr, StoppableTask, StoppableTaskPtr},
  38. util::{encoding::base64, path::expand_path},
  39. verbose, Error, Result, ANSI_LOGO,
  40. };
  41. use darkfi_serial::deserialize_async;
  42. use smol::{
  43. future,
  44. lock::{Mutex, MutexGuard},
  45. Executor,
  46. };
  47. use tapes::{TapeOpenOptions, Tapes};
  48. use tinyjson::JsonValue;
  49. use tracing::{debug, error, info, warn};
  50. use url::Url;
  51. /// Database interfaces
  52. mod db;
  53. use db::{DifficultyIndex, TapesDatabase};
  54. /// JSON-RPC server methods
  55. mod rpc;
  56. const ABOUT: &str =
  57. concat!("explorer ", env!("CARGO_PKG_VERSION"), '\n', env!("CARGO_PKG_DESCRIPTION"));
  58. const USAGE: &str = r#"
  59. Usage: explorer [OPTIONS]
  60. Options:
  61. -e <endpoint> darkfid JSON-RPC endpoint (default: tcp://127.0.0.1:18345)
  62. -d <path> Path to database (default: ~/.local/share/darkfi/explorer/db)
  63. -r <height> Revert database to <height>
  64. -h Show this help
  65. "#;
  66. fn usage() {
  67. print!("{ANSI_LOGO}{ABOUT}\n{USAGE}");
  68. }
  69. pub struct Explorer {
  70. synced: AtomicBool,
  71. synced_notifier: Arc<CondVar>,
  72. _sled_db: sled::Db,
  73. header_indices: sled::Tree,
  74. tx_indices: sled::Tree,
  75. contracts: sled::Tree,
  76. stats: sled::Tree,
  77. tapes_db: Tapes,
  78. _tapes_options: TapeOpenOptions,
  79. database: TapesDatabase,
  80. rpc_sub: StoppableTaskPtr,
  81. rpc_sub_handler: StoppableTaskPtr,
  82. blocks_publisher: PublisherPtr<JsonResult>,
  83. rpc_connections: Mutex<HashSet<StoppableTaskPtr>>,
  84. }
  85. struct RpcHandler;
  86. #[async_trait]
  87. impl RequestHandler<RpcHandler> for Explorer {
  88. async fn handle_request(&self, req: JsonRequest) -> JsonResult {
  89. debug!(target: "explorer::rpc", "--> {}", req.stringify().unwrap());
  90. match req.method.as_str() {
  91. "current_difficulty" => self.rpc_current_difficulty(req.id, req.params).await,
  92. "current_height" => self.rpc_current_height(req.id, req.params).await,
  93. "latest_blocks" => self.rpc_latest_blocks(req.id, req.params).await,
  94. "get_block" => self.rpc_get_block(req.id, req.params).await,
  95. "get_tx" => self.rpc_get_tx(req.id, req.params).await,
  96. "search" => self.rpc_search(req.id, req.params).await,
  97. "get_hashrate" => self.rpc_get_hashrate(req.id, req.params).await,
  98. "get_contract" => self.rpc_get_contract(req.id, req.params).await,
  99. "list_contracts" => self.rpc_list_contracts(req.id, req.params).await,
  100. "contract_count" => self.rpc_contract_count(req.id, req.params).await,
  101. "get_stats" => self.rpc_get_stats(req.id, req.params).await,
  102. _ => JsonError::new(ErrorCode::MethodNotFound, None, req.id).into(),
  103. }
  104. }
  105. async fn connections_mut(&self) -> MutexGuard<'life0, HashSet<StoppableTaskPtr>> {
  106. self.rpc_connections.lock().await
  107. }
  108. }
  109. impl Explorer {
  110. fn new(sled_path: &Path, tapes_db_path: &Path, tapes_path: &Path) -> Result<Self> {
  111. info!(target: "explorer::new", "Opening sled trees");
  112. let sled_db = sled::open(sled_path)?;
  113. let header_indices = sled_db.open_tree("header_indices")?;
  114. let tx_indices = sled_db.open_tree("tx_indices")?;
  115. let contracts = sled_db.open_tree("contracts")?;
  116. let stats = sled_db.open_tree("stats")?;
  117. info!(target: "explorer::new", "Opening tapes");
  118. std::fs::create_dir_all(tapes_db_path)?;
  119. std::fs::create_dir_all(tapes_path)?;
  120. let tapes_db = Tapes::open(tapes_db_path)?;
  121. let tapes_options =
  122. TapeOpenOptions { top_cache_size: 64 * 1024, dir: tapes_path.to_path_buf() };
  123. let database = Self::open_tapes(&tapes_db, &tapes_options)?;
  124. Ok(Self {
  125. synced: AtomicBool::new(false),
  126. synced_notifier: Arc::new(CondVar::new()),
  127. _sled_db: sled_db,
  128. header_indices,
  129. tx_indices,
  130. contracts,
  131. stats,
  132. tapes_db,
  133. _tapes_options: tapes_options,
  134. database,
  135. rpc_sub: StoppableTask::new(),
  136. rpc_sub_handler: StoppableTask::new(),
  137. blocks_publisher: Publisher::new(),
  138. rpc_connections: Mutex::new(HashSet::new()),
  139. })
  140. }
  141. async fn handle_block_sub(&self, rpc_endpoint: Url, ex: Arc<Executor<'_>>) -> Result<()> {
  142. info!(
  143. target: "explorer::handle_block_sub",
  144. "Started block subscription, waiting until blockchain is synced",
  145. );
  146. let block_subscription = self.blocks_publisher.clone().subscribe().await;
  147. self.synced_notifier.wait().await;
  148. info!(
  149. target: "explorer::handle_block_sub",
  150. "Blockchain synced, now waiting for new blocks...",
  151. );
  152. loop {
  153. // Handle the new block. We get a JsonResult, so also handle
  154. // any errors that might arise.
  155. let block_notification = block_subscription.receive().await;
  156. info!(target: "explorer::handle_block_sub", "Got new block notification!");
  157. match block_notification {
  158. JsonResult::Notification(notification) => {
  159. for param in notification.params.get::<Vec<JsonValue>>().unwrap() {
  160. // Deserialize base64 block
  161. let block_bytes = base64::decode(param.get::<String>().unwrap()).unwrap();
  162. let block: BlockInfo = deserialize_async(&block_bytes).await.unwrap();
  163. let incoming_height = block.header.height as u64;
  164. info!(target: "explorer::handle_block_sub", "Height {}", incoming_height);
  165. // Check if we need to reorg or sync
  166. let current_height = self.get_height().ok().flatten().unwrap_or(0);
  167. if incoming_height > current_height + 1 {
  168. // Sync needed: we have at least one missing block
  169. info!(
  170. target: "explorer::handle_block_sub",
  171. "Sync needed! Incoming height {} > current_height {}.",
  172. incoming_height, current_height,
  173. );
  174. self.synced.store(false, Ordering::SeqCst);
  175. self.sync_blockchain(
  176. rpc_endpoint.clone(),
  177. current_height + 1,
  178. incoming_height - 1,
  179. ex.clone(),
  180. )
  181. .await?;
  182. self.synced.store(true, Ordering::SeqCst);
  183. info!(
  184. target: "explorer::handle_block_sub",
  185. "Synced to height {}", incoming_height - 1,
  186. );
  187. }
  188. if incoming_height <= current_height {
  189. // Reorg needed: incoming block is at or before our current height
  190. let blocks_to_revert = current_height - incoming_height + 1;
  191. info!(
  192. target: "explorer::handle_block_sub",
  193. "Reorg detected! Incoming height {} <= current height {}. Reverting {} blocks.",
  194. incoming_height, current_height, blocks_to_revert
  195. );
  196. if let Err(e) = self.revert_to_height(incoming_height - 1).await {
  197. error!(
  198. target: "explorer::handle_block_sub",
  199. "Failed to revert blocks during reorg: {e}",
  200. );
  201. // Exit from this task if there's an error.
  202. // It'll let us inspect the db and what happened.
  203. return Err(e.into())
  204. }
  205. }
  206. // Get difficulty
  207. let rpc_client =
  208. RpcClient::new(rpc_endpoint.clone(), ex.clone()).await.unwrap();
  209. let req = JsonRequest::new(
  210. "blockchain.get_difficulty",
  211. JsonValue::Array(vec![(block.header.height as f64).into()]),
  212. );
  213. let rep = rpc_client.request(req).await?;
  214. rpc_client.stop().await;
  215. let params = rep.get::<Vec<JsonValue>>().unwrap();
  216. let difficulty = *params[0].get::<f64>().unwrap() as u64;
  217. let cumulative = *params[1].get::<f64>().unwrap() as u64;
  218. let diff = DifficultyIndex { difficulty, cumulative };
  219. self.append_block(&block, &diff).await.unwrap();
  220. }
  221. }
  222. x => unreachable!("{:?}", x),
  223. }
  224. }
  225. }
  226. async fn sync_blockchain(
  227. &self,
  228. rpc_endpoint: Url,
  229. from_height: u64,
  230. to_height: u64,
  231. ex: Arc<Executor<'_>>,
  232. ) -> Result<()> {
  233. if from_height >= to_height {
  234. return Ok(())
  235. }
  236. info!(
  237. target: "explorer::sync_blockchain",
  238. "Started blockchain sync from_height={from_height} to_height={to_height}...",
  239. );
  240. let rpc_client = Arc::new(RpcClient::new(rpc_endpoint, ex.clone()).await?);
  241. for height in from_height..=to_height {
  242. info!(target: "explorer::sync_blockchain", "Requesting block at height {height}");
  243. // Get block
  244. let req = JsonRequest::new(
  245. "blockchain.get_block",
  246. JsonValue::Array(vec![(height as f64).into()]),
  247. );
  248. let rep = rpc_client.request(req).await?;
  249. let param = rep.get::<String>().unwrap();
  250. let bytes = base64::decode(param).unwrap();
  251. let block: BlockInfo = deserialize_async(&bytes).await?;
  252. // Get difficulty
  253. let req = JsonRequest::new(
  254. "blockchain.get_difficulty",
  255. JsonValue::Array(vec![(height as f64).into()]),
  256. );
  257. let rep = rpc_client.request(req).await?;
  258. let params = rep.get::<Vec<JsonValue>>().unwrap();
  259. let difficulty = *params[0].get::<f64>().unwrap() as u64;
  260. let cumulative = *params[1].get::<f64>().unwrap() as u64;
  261. let diff = DifficultyIndex { difficulty, cumulative };
  262. self.append_block(&block, &diff).await?;
  263. }
  264. rpc_client.stop().await;
  265. Ok(())
  266. }
  267. }
  268. async fn realmain(
  269. rpc_endpoint: Url,
  270. db_path: PathBuf,
  271. revert_to: u64,
  272. ex: Arc<Executor<'static>>,
  273. ) -> Result<()> {
  274. let explorer = Arc::new(Explorer::new(
  275. &db_path.join("sled_db"),
  276. &db_path.join("tapes_metadata"),
  277. &db_path.join("tapes"),
  278. )?);
  279. // First we should subscribe to new blocks and queue them to apply
  280. // after we sync. For this we create a new longterm background task
  281. // that will handle incoming blocks. It will wait until the blockchain
  282. // is synced and then proceed to process them.
  283. let explorer_ = Arc::clone(&explorer);
  284. let ex_ = ex.clone();
  285. let rpc_endpoint_ = rpc_endpoint.clone();
  286. explorer.rpc_sub_handler.clone().start(
  287. async move { explorer_.handle_block_sub(rpc_endpoint_, ex_).await },
  288. |_| async {},
  289. Error::RpcServerStopped,
  290. ex.clone(),
  291. );
  292. // Then we subscribe to darkfid's RPC to get new blocks. We should first
  293. // fetch the current height, so we know how far to sync. Then any blocks
  294. // that come after that should be queued in the `blocks_publisher`.
  295. info!(target: "explorer", "Connecting to darkfid RPC...");
  296. let rpc_client = loop {
  297. let Ok(rpc_client) = RpcClient::new(rpc_endpoint.clone(), ex.clone()).await else {
  298. msleep(500).await;
  299. continue
  300. };
  301. break rpc_client
  302. };
  303. let req = JsonRequest::new("blockchain.last_confirmed_block", JsonValue::Array(vec![]));
  304. let rep = rpc_client.request(req).await?;
  305. rpc_client.stop().await;
  306. let params = rep.get::<Vec<JsonValue>>().unwrap();
  307. let confirmed_height = *params[0].get::<f64>().unwrap() as u64;
  308. // Now create the subscription task.
  309. let explorer_ = Arc::clone(&explorer);
  310. let ex_ = Arc::clone(&ex);
  311. let rpc_endpoint_ = rpc_endpoint.clone();
  312. explorer.rpc_sub.clone().start(
  313. async move {
  314. loop {
  315. let rpc_client = match RpcClient::new(rpc_endpoint_.clone(), ex_.clone()).await {
  316. Ok(v) => v,
  317. Err(e) => {
  318. warn!(target: "explorer::subscribe_blocks", "darkfid RPC connection lost ({e})), retrying...");
  319. msleep(500).await;
  320. continue
  321. }
  322. };
  323. info!(target: "explorer::subscribe_blocks", "Connected to darkfid RPC");
  324. let req = JsonRequest::new("blockchain.subscribe_blocks", JsonValue::Array(vec![]));
  325. if let Err(e) = rpc_client.subscribe(req, explorer_.blocks_publisher.clone()).await {
  326. rpc_client.stop().await;
  327. warn!(target: "explorer::subscribe_blocks", "darkfid RPC connection lost ({e}), retrying...");
  328. msleep(500).await;
  329. }
  330. }
  331. },
  332. |_| async {},
  333. Error::RpcServerStopped,
  334. ex.clone(),
  335. );
  336. // Once the tasks are set up, we'll now perform a manual sync up to
  337. // the last confirmed height. This will create a new RPC client that
  338. // is going to request and parse all the necessary blocks, and then
  339. // apply them to the databases.
  340. let mut sync_from = explorer.get_height()?.unwrap_or(0);
  341. if sync_from > 0 {
  342. // If we're not syncing from genesis, account for it.
  343. sync_from += 1;
  344. }
  345. explorer.sync_blockchain(rpc_endpoint.clone(), sync_from, confirmed_height, ex.clone()).await?;
  346. explorer.synced.store(true, Ordering::SeqCst);
  347. explorer.synced_notifier.notify();
  348. if revert_to > 0 {
  349. info!("Reverting to {}", revert_to);
  350. explorer.revert_to_height(revert_to).await?;
  351. }
  352. // Start up an RPC server that can be queried for data.
  353. // This normally serves the data to the Python website frontend.
  354. info!(target: "explorer", "Starting JSONRPC server");
  355. let rpc_settings = RpcSettings::default();
  356. listen_and_serve(rpc_settings, explorer, None, ex.clone()).await?;
  357. Ok(())
  358. }
  359. fn main() -> Result<()> {
  360. let mut hflag = false;
  361. let mut evalue = "tcp://127.0.0.1:18345".to_string();
  362. let mut dvalue = "~/.local/share/darkfi/explorer/db".to_string();
  363. let mut rvalue = "0".to_string();
  364. let mut verbose = 0;
  365. {
  366. let mut args = Args::new().with_cb(|args, flag| match flag {
  367. 'e' => evalue = args.eargf().to_string(),
  368. 'd' => dvalue = args.eargf().to_string(),
  369. 'r' => rvalue = args.eargf().to_string(),
  370. 'v' => verbose += 1,
  371. _ => hflag = true,
  372. });
  373. args.parse();
  374. }
  375. let revert_to: u64 = rvalue.parse()?;
  376. if hflag {
  377. usage();
  378. exit(1);
  379. }
  380. let rpc_endpoint: Url = match evalue.parse() {
  381. Ok(v) => v,
  382. Err(e) => {
  383. println!("Error parsing RPC endpoint: {e}");
  384. usage();
  385. exit(1);
  386. }
  387. };
  388. let db_path: PathBuf = match expand_path(&dvalue) {
  389. Ok(v) => v,
  390. Err(e) => {
  391. println!("Error parsing DB path: {e}");
  392. usage();
  393. exit(1);
  394. }
  395. };
  396. let ex = Arc::new(Executor::new());
  397. let (signal, shutdown) = async_channel::unbounded::<()>();
  398. darkfi::util::logger::setup_logging(verbose, None)?;
  399. info!(target: "explorer", "RPC Endpoint: {}", evalue);
  400. info!(target: "explorer", "DB Path: {}", dvalue);
  401. verbose!(target: "explorer", "Log Level: {}", verbose);
  402. let (_, result) = easy_parallel::Parallel::new()
  403. // Run four executor threads
  404. .each(0..4, |_| future::block_on(ex.run(shutdown.recv())))
  405. // Run the main future on the current thread
  406. .finish(|| {
  407. future::block_on(async {
  408. realmain(rpc_endpoint, db_path, revert_to, ex.clone()).await?;
  409. drop(signal);
  410. Ok::<(), darkfi::Error>(())
  411. })
  412. });
  413. result
  414. }