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 }