fud/
pow.rs

1/* This file is part of DarkFi (https://dark.fi)
2 *
3 * Copyright (C) 2020-2024 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    io::{Error as IoError, Read, Result as IoResult, Write},
21    sync::Arc,
22};
23
24use rand::rngs::OsRng;
25use smol::lock::RwLock;
26use structopt::StructOpt;
27use tracing::info;
28use url::Url;
29
30use darkfi::{system::ExecutorPtr, Error, Result};
31use darkfi_sdk::crypto::{Keypair, PublicKey, SecretKey};
32use darkfi_serial::{
33    async_trait, AsyncDecodable, AsyncEncodable, AsyncRead, AsyncWrite, Decodable, Encodable,
34};
35
36use crate::{
37    bitcoin::{BitcoinBlockHash, BitcoinHashCache},
38    equix::{Challenge, EquiXBuilder, EquiXPow, Solution, SolverMemory, NONCE_LEN},
39};
40
41#[derive(Clone, Debug)]
42pub struct PowSettings {
43    /// Equi-X effort value
44    pub equix_effort: u32,
45    /// Toggle BTC block hash in PoW challenge (useful for localnet/debug)
46    pub btc_enabled: bool,
47    /// Number of latest BTC block hashes that are valid for fud's PoW
48    pub btc_hash_count: usize,
49    /// Electrum nodes timeout in seconds
50    pub btc_timeout: u64,
51    /// Electrum nodes used to fetch the latest block hashes (used in fud's PoW)
52    pub btc_electrum_nodes: Vec<Url>,
53}
54
55impl Default for PowSettings {
56    fn default() -> Self {
57        Self {
58            equix_effort: 10000,
59            btc_enabled: true,
60            btc_hash_count: 144,
61            btc_timeout: 15,
62            btc_electrum_nodes: vec![],
63        }
64    }
65}
66
67#[derive(Clone, Debug, serde::Deserialize, structopt::StructOpt, structopt_toml::StructOptToml)]
68#[structopt()]
69#[serde(rename = "pow")]
70pub struct PowSettingsOpt {
71    /// Equi-X effort value
72    #[structopt(long)]
73    pub equix_effort: Option<u32>,
74
75    /// Toggle BTC block hash in PoW challenge (useful for localnet/debug)
76    #[structopt(long)]
77    pub btc_enabled: Option<bool>,
78
79    /// Number of latest BTC block hashes that are valid for fud's PoW
80    #[structopt(long)]
81    pub btc_hash_count: Option<usize>,
82
83    /// Electrum nodes timeout in seconds
84    #[structopt(long)]
85    pub btc_timeout: Option<u64>,
86
87    /// Electrum nodes used to fetch the latest block hashes (used in fud's PoW)
88    #[structopt(long, use_delimiter = true)]
89    pub btc_electrum_nodes: Vec<Url>,
90}
91
92impl From<PowSettingsOpt> for PowSettings {
93    fn from(opt: PowSettingsOpt) -> Self {
94        let def = PowSettings::default();
95
96        Self {
97            equix_effort: opt.equix_effort.unwrap_or(def.equix_effort),
98            btc_enabled: opt.btc_enabled.unwrap_or(def.btc_enabled),
99            btc_hash_count: opt.btc_hash_count.unwrap_or(def.btc_hash_count),
100            btc_timeout: opt.btc_timeout.unwrap_or(def.btc_timeout),
101            btc_electrum_nodes: opt.btc_electrum_nodes,
102        }
103    }
104}
105
106/// Struct handling a [`EquiXPow`] instance to generate and verify [`VerifiableNodeData`].
107pub struct FudPow {
108    pub settings: Arc<RwLock<PowSettings>>,
109    pub bitcoin_hash_cache: BitcoinHashCache,
110    equix_pow: EquiXPow,
111}
112impl FudPow {
113    pub fn new(settings: PowSettings, ex: ExecutorPtr) -> Self {
114        let pow_settings: Arc<RwLock<PowSettings>> = Arc::new(RwLock::new(settings));
115        let bitcoin_hash_cache = BitcoinHashCache::new(pow_settings.clone(), ex.clone());
116
117        Self {
118            settings: pow_settings,
119            bitcoin_hash_cache,
120            equix_pow: EquiXPow {
121                effort: 0, // will be set when we call `generate_node()`
122                challenge: Challenge::new(&[], &[0u8; NONCE_LEN]),
123                equix: EquiXBuilder::default(),
124                mem: SolverMemory::default(),
125            },
126        }
127    }
128
129    /// Generate a random keypair and run the PoW to get a [`VerifiableNodeData`].
130    pub async fn generate_node(&mut self) -> Result<(VerifiableNodeData, SecretKey)> {
131        info!(target: "fud::FudPow::generate_node()", "Generating a new node id...");
132
133        // Generate a random keypair
134        let keypair = Keypair::random(&mut OsRng);
135
136        // Get a recent Bitcoin block hash
137        let n = 3;
138        let btc_block_hash = if !self.settings.read().await.btc_enabled {
139            [0; 32]
140        } else {
141            let block_hashes = &self.bitcoin_hash_cache.block_hashes;
142            if block_hashes.is_empty() {
143                return Err(Error::Custom(
144                    "Can't generate a node id without BTC block hashes".into(),
145                ));
146            }
147
148            let block_hash = if n > block_hashes.len() {
149                block_hashes.last()
150            } else {
151                block_hashes.get(block_hashes.len() - 1 - n)
152            };
153
154            if block_hash.is_none() {
155                return Err(Error::Custom("Could not find a recent BTC block hash".into()));
156            }
157            *block_hash.unwrap()
158        };
159
160        // Update the effort using the value from `self.settings`
161        self.equix_pow.effort = self.settings.read().await.equix_effort;
162
163        // Construct Equi-X challenge
164        self.equix_pow.challenge = Challenge::new(
165            &[keypair.public.to_bytes(), btc_block_hash].concat(),
166            &[0u8; NONCE_LEN],
167        );
168
169        // Evaluate PoW
170        info!(target: "fud::FudPow::generate_node()", "Equi-X Proof-of-Work starts...");
171        let solution =
172            self.equix_pow.run().map_err(|e| Error::Custom(format!("Equi-X error: {e}")))?;
173        info!(target: "fud::FudPow::generate_node()", "Equi-X Proof-of-Work is done");
174
175        // Create the VerifiableNodeData
176        Ok((
177            VerifiableNodeData {
178                public_key: keypair.public,
179                btc_block_hash,
180                nonce: self.equix_pow.challenge.nonce(),
181                solution,
182            },
183            keypair.secret,
184        ))
185    }
186
187    /// Check if the Equi-X solution in a [`VerifiableNodeData`] is valid and has enough effort.
188    pub async fn verify_node(&mut self, node_data: &VerifiableNodeData) -> Result<()> {
189        let settings = self.settings.read().await;
190        // Update the effort using the value from `self.settings`
191        self.equix_pow.effort = settings.equix_effort;
192
193        // Verify if the Bitcoin block hash is known
194        if settings.btc_enabled &&
195            !self.bitcoin_hash_cache.block_hashes.contains(&node_data.btc_block_hash)
196        {
197            return Err(Error::Custom(
198                "Error verifying node data: the BTC block hash is unknown".into(),
199            ))
200        }
201
202        // Verify the solution
203        self.equix_pow
204            .verify(&node_data.challenge(), &node_data.solution)
205            .map_err(|e| Error::Custom(format!("Error verifying Equi-X solution: {e}")))
206    }
207}
208
209/// The data needed to verify a fud PoW.
210#[derive(Debug, Clone)]
211pub struct VerifiableNodeData {
212    pub public_key: PublicKey,
213    pub btc_block_hash: BitcoinBlockHash,
214    pub nonce: [u8; NONCE_LEN],
215    pub solution: Solution,
216}
217
218impl VerifiableNodeData {
219    /// The node id on the DHT.
220    pub fn id(&self) -> blake3::Hash {
221        blake3::hash(&[self.challenge().to_bytes(), self.solution.to_bytes().to_vec()].concat())
222    }
223
224    /// The Equi-X challenge.
225    pub fn challenge(&self) -> Challenge {
226        Challenge::new(&[self.public_key.to_bytes(), self.btc_block_hash].concat(), &self.nonce)
227    }
228}
229
230impl Encodable for VerifiableNodeData {
231    fn encode<S: Write>(&self, s: &mut S) -> IoResult<usize> {
232        let mut len = 0;
233        len += self.public_key.encode(s)?;
234        len += self.btc_block_hash.encode(s)?;
235        len += self.nonce.encode(s)?;
236        len += self.solution.to_bytes().encode(s)?;
237        Ok(len)
238    }
239}
240
241#[async_trait]
242impl AsyncEncodable for VerifiableNodeData {
243    async fn encode_async<S: AsyncWrite + Unpin + Send>(&self, s: &mut S) -> IoResult<usize> {
244        let mut len = 0;
245        len += self.public_key.encode_async(s).await?;
246        len += self.btc_block_hash.encode_async(s).await?;
247        len += self.nonce.encode_async(s).await?;
248        len += self.solution.to_bytes().encode_async(s).await?;
249        Ok(len)
250    }
251}
252
253impl Decodable for VerifiableNodeData {
254    fn decode<D: Read>(d: &mut D) -> IoResult<Self> {
255        Ok(Self {
256            public_key: PublicKey::decode(d)?,
257            btc_block_hash: BitcoinBlockHash::decode(d)?,
258            nonce: <[u8; NONCE_LEN]>::decode(d)?,
259            solution: Solution::try_from_bytes(&<[u8; Solution::NUM_BYTES]>::decode(d)?)
260                .map_err(|e| IoError::other(format!("Error parsing Equi-X solution: {e}")))?,
261        })
262    }
263}
264
265#[async_trait]
266impl AsyncDecodable for VerifiableNodeData {
267    async fn decode_async<D: AsyncRead + Unpin + Send>(d: &mut D) -> IoResult<Self> {
268        Ok(Self {
269            public_key: PublicKey::decode_async(d).await?,
270            btc_block_hash: BitcoinBlockHash::decode_async(d).await?,
271            nonce: <[u8; NONCE_LEN]>::decode_async(d).await?,
272            solution: Solution::try_from_bytes(
273                &<[u8; Solution::NUM_BYTES]>::decode_async(d).await?,
274            )
275            .map_err(|e| IoError::other(format!("Error parsing Equi-X solution: {e}")))?,
276        })
277    }
278}