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