/ bin / fud / fud / src / bitcoin.rs
bitcoin.rs
  1  /* This file is part of DarkFi (https://dark.fi)
  2   *
  3   * Copyright (C) 2020-2025 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  
 19  use std::{
 20      collections::HashMap,
 21      io::{Cursor, Read},
 22      sync::Arc,
 23      time::Duration,
 24  };
 25  
 26  use rand::{prelude::SliceRandom, rngs::OsRng};
 27  use sha2::{Digest, Sha256};
 28  use smol::lock::RwLock;
 29  use tinyjson::JsonValue;
 30  use tracing::{error, info, warn};
 31  use url::Url;
 32  
 33  use darkfi::{
 34      rpc::{client::RpcClient, jsonrpc::JsonRequest},
 35      system::{timeout::timeout, ExecutorPtr},
 36      Error, Result,
 37  };
 38  use darkfi_sdk::{hex::decode_hex, GenericResult};
 39  
 40  use crate::pow::PowSettings;
 41  
 42  pub type BitcoinBlockHash = [u8; 32];
 43  
 44  /// A struct that can fetch and store recent Bitcoin block hashes, using Electrum nodes.
 45  /// This is only used to evaluate and verify fud's Equi-X PoW.
 46  /// Bitcoin block hashes are used in the challenge, to make Equi-X solution
 47  /// expirable and unpredictable.
 48  /// It's meant to be swapped with DarkFi block hashes once it is stable enough.
 49  /// TODO: It should ask for new Electrum nodes, and build a local database of them
 50  /// instead of relying only on the list defined in the settings.
 51  pub struct BitcoinHashCache {
 52      /// PoW settings which includes BTC/Electrum settings
 53      settings: Arc<RwLock<PowSettings>>,
 54      /// Current list of block hashes, the most recent block is at the end of the list
 55      pub block_hashes: Vec<BitcoinBlockHash>,
 56      /// Global multithreaded executor reference
 57      ex: ExecutorPtr,
 58  }
 59  
 60  impl BitcoinHashCache {
 61      pub fn new(settings: Arc<RwLock<PowSettings>>, ex: ExecutorPtr) -> Self {
 62          Self { settings, block_hashes: vec![], ex }
 63      }
 64  
 65      /// Fetch block hashes from Electrum nodes, and update [`BitcoinHashCache::block_hashes`].
 66      pub async fn update(&mut self) -> Result<Vec<BitcoinBlockHash>> {
 67          info!(target: "fud::BitcoinHashCache::update()", "[BTC] Updating block hashes...");
 68  
 69          let mut block_hashes = vec![];
 70          let btc_electrum_nodes = self.settings.read().await.btc_electrum_nodes.clone();
 71  
 72          let mut rng = OsRng;
 73          let mut shuffled_nodes: Vec<_> = btc_electrum_nodes.clone();
 74          shuffled_nodes.shuffle(&mut rng);
 75  
 76          for addr in shuffled_nodes {
 77              // Connect to the Electrum node
 78              let client = match self.create_rpc_client(&addr).await {
 79                  Ok(client) => client,
 80                  Err(e) => {
 81                      warn!(target: "fud::BitcoinHashCache::update()", "[BTC] Error while creating RPC client for Electrum node {addr}: {e}");
 82                      continue
 83                  }
 84              };
 85              info!(target: "fud::BitcoinHashCache::update()", "[BTC] Connected to {addr}");
 86  
 87              // Fetch the current BTC height
 88              let current_height = match self.fetch_current_height(&client).await {
 89                  Ok(height) => height,
 90                  Err(e) => {
 91                      warn!(target: "fud::BitcoinHashCache::update()", "[BTC] Error while fetching current height: {e}");
 92                      client.stop().await;
 93                      continue
 94                  }
 95              };
 96              info!(target: "fud::BitcoinHashCache::update()", "[BTC] Found current height {current_height}");
 97  
 98              // Fetch the latest block hashes
 99              match self.fetch_hashes(current_height, &client).await {
100                  Ok(hashes) => {
101                      client.stop().await;
102                      if !hashes.is_empty() {
103                          block_hashes = hashes;
104                          break
105                      }
106                      warn!(target: "fud::BitcoinHashCache::update()", "[BTC] The Electrum node replied with an empty list of block headers");
107                      continue
108                  }
109                  Err(e) => {
110                      warn!(target: "fud::BitcoinHashCache::update()", "[BTC] Error while fetching block hashes: {e}");
111                      client.stop().await;
112                      continue
113                  }
114              };
115          }
116  
117          if block_hashes.is_empty() {
118              let err_str = "Could not find any block hash";
119              error!(target: "fud::BitcoinHashCache::update()", "[BTC] {err_str}");
120              return Err(Error::Custom(err_str.to_string()))
121          }
122  
123          info!(target: "fud::BitcoinHashCache::update()", "[BTC] Found {} block hashes", block_hashes.len());
124  
125          self.block_hashes = block_hashes.clone();
126          Ok(block_hashes)
127      }
128  
129      async fn create_rpc_client(&self, addr: &Url) -> Result<RpcClient> {
130          let btc_timeout = Duration::from_secs(self.settings.read().await.btc_timeout);
131          let client = timeout(btc_timeout, RpcClient::new(addr.clone(), self.ex.clone())).await??;
132          Ok(client)
133      }
134  
135      /// Fetch the current BTC height using an Electrum node RPC.
136      async fn fetch_current_height(&self, client: &RpcClient) -> Result<u64> {
137          let btc_timeout = Duration::from_secs(self.settings.read().await.btc_timeout);
138          let req = JsonRequest::new("blockchain.headers.subscribe", vec![].into());
139          let rep = timeout(btc_timeout, client.request(req)).await??;
140  
141          rep.get::<HashMap<String, JsonValue>>()
142              .and_then(|res| res.get("height"))
143              .and_then(|h| h.get::<f64>())
144              .map(|h| *h as u64)
145              .ok_or_else(|| {
146                  Error::JsonParseError(
147                      "Failed to parse `blockchain.headers.subscribe` response".into(),
148                  )
149              })
150      }
151  
152      /// Fetch `self.count` BTC block hashes from `height` using an Electrum node RPC.
153      async fn fetch_hashes(&self, height: u64, client: &RpcClient) -> Result<Vec<BitcoinBlockHash>> {
154          let count = self.settings.read().await.btc_hash_count;
155          let btc_timeout = Duration::from_secs(self.settings.read().await.btc_timeout);
156          let req = JsonRequest::new(
157              "blockchain.block.headers",
158              vec![
159                  JsonValue::Number((height as f64) - (count as f64)),
160                  JsonValue::Number(count as f64),
161              ]
162              .into(),
163          );
164          let rep = timeout(btc_timeout, client.request(req)).await??;
165  
166          let hex: &String = rep
167              .get::<HashMap<String, JsonValue>>()
168              .and_then(|res| res.get("hex"))
169              .and_then(|h| h.get::<String>())
170              .ok_or_else(|| {
171                  Error::JsonParseError("Failed to parse `blockchain.block.headers` response".into())
172              })?;
173  
174          let decoded_bytes = decode_hex(hex.as_str()).collect::<GenericResult<Vec<_>>>()?;
175          Self::decode_block_hashes(decoded_bytes)
176      }
177  
178      /// Convert concatenated BTC block headers to a list of block hashes.
179      fn decode_block_hashes(data: Vec<u8>) -> Result<Vec<BitcoinBlockHash>> {
180          let mut cursor = Cursor::new(&data);
181          let count = data.len() / 80;
182  
183          let mut hashes = Vec::with_capacity(count);
184          for _ in 0..count {
185              // Read the 80-byte header
186              let mut header = [0u8; 80];
187              cursor.read_exact(&mut header)?;
188  
189              // Compute double SHA-256
190              let first_hash = Sha256::digest(header);
191              let second_hash = Sha256::digest(first_hash);
192  
193              // Convert to big-endian hash
194              let mut be_hash = [0u8; 32];
195              be_hash.copy_from_slice(&second_hash);
196              be_hash.reverse();
197  
198              hashes.push(be_hash);
199          }
200  
201          Ok(hashes)
202      }
203  }