fud/
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
19use std::{
20    collections::HashMap,
21    io::{Cursor, Read},
22    sync::Arc,
23    time::Duration,
24};
25
26use log::{error, info, warn};
27use rand::{prelude::SliceRandom, rngs::OsRng};
28use sha2::{Digest, Sha256};
29use smol::lock::RwLock;
30use tinyjson::JsonValue;
31use url::Url;
32
33use darkfi::{
34    rpc::{client::RpcClient, jsonrpc::JsonRequest},
35    system::{timeout::timeout, ExecutorPtr},
36    Error, Result,
37};
38use darkfi_sdk::{hex::decode_hex, GenericResult};
39
40use crate::pow::PowSettings;
41
42pub 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.
51pub 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
60impl 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}