diff --git a/Cargo.lock b/Cargo.lock
index 0ae812e5a..c87d21c77 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -783,6 +783,15 @@ dependencies = [
"wyz",
]
+[[package]]
+name = "blake2"
+version = "0.10.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
+dependencies = [
+ "digest",
+]
+
[[package]]
name = "blake2b_simd"
version = "1.0.3"
@@ -2680,6 +2689,19 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
+[[package]]
+name = "equix"
+version = "0.2.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "89178c5241f5cc0c8f2b5ac5008f3c7a32caad341b1ec747a6e1e51d2e877110"
+dependencies = [
+ "arrayvec",
+ "hashx",
+ "num-traits",
+ "thiserror 2.0.12",
+ "visibility",
+]
+
[[package]]
name = "errno"
version = "0.3.13"
@@ -2823,6 +2845,12 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[package]]
+name = "fixed-capacity-vec"
+version = "1.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b31a14f5ee08ed1a40e1252b35af18bed062e3f39b69aab34decde36bc43e40"
+
[[package]]
name = "fixed-hash"
version = "0.8.0"
@@ -3036,16 +3064,20 @@ name = "fud"
version = "0.5.0"
dependencies = [
"async-trait",
+ "blake2",
"blake3",
"bs58",
"darkfi",
+ "darkfi-sdk",
"darkfi-serial",
"easy-parallel",
+ "equix",
"futures",
"log",
"num-bigint",
"rand 0.8.5",
"serde",
+ "sha2",
"signal-hook",
"signal-hook-async-std",
"simplelog",
@@ -3437,6 +3469,21 @@ dependencies = [
"hashbrown 0.15.4",
]
+[[package]]
+name = "hashx"
+version = "0.3.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7cb639748a589a17df2126f8015897ab416e81113afb82f56df5d47fa1486ab1"
+dependencies = [
+ "arrayvec",
+ "blake2",
+ "dynasmrt",
+ "fixed-capacity-vec",
+ "hex",
+ "rand_core 0.9.3",
+ "thiserror 2.0.12",
+]
+
[[package]]
name = "heck"
version = "0.3.3"
diff --git a/bin/fud/fud/Cargo.toml b/bin/fud/fud/Cargo.toml
index 536c51547..24028a079 100644
--- a/bin/fud/fud/Cargo.toml
+++ b/bin/fud/fud/Cargo.toml
@@ -18,12 +18,16 @@ path = "src/main.rs"
[dependencies]
darkfi = {path = "../../../", features = ["async-daemonize", "geode", "rpc", "dht", "sled-overlay"]}
+darkfi-sdk = {path = "../../../src/sdk"}
darkfi-serial = {version = "0.5.0", features = ["hash"]}
+# Encoding
+bs58 = "0.5.1"
+sha2 = "0.10.9"
+
# Misc
async-trait = "0.1.88"
blake3 = "1.8.2"
-bs58 = "0.5.1"
rand = "0.8.5"
log = "0.4.27"
tinyjson = "2.5.1"
@@ -38,6 +42,10 @@ signal-hook = "0.3.18"
simplelog = "0.12.2"
smol = "2.0.2"
+# Equi-X
+blake2 = "0.10.6"
+equix = "0.2.5"
+
# Database
sled-overlay = "0.1.9"
diff --git a/bin/fud/fud/fud_config.toml b/bin/fud/fud/fud_config.toml
index cc09036ef..a1653d4fe 100644
--- a/bin/fud/fud/fud_config.toml
+++ b/bin/fud/fud/fud_config.toml
@@ -12,6 +12,31 @@ base_dir = "~/.local/share/darkfi/fud"
## Chunk transfer timeout in seconds
#chunk_timeout = 60
+# PoW settings (to generate a valid node id)
+[pow]
+## Equi-X effort value
+#equix_value = 10000
+
+## Number of latest BTC block hashes that are valid for fud's PoW
+#btc_hash_count = 144
+
+## Electrum nodes timeout in seconds
+#btc_timeout = 15
+
+# Electrum nodes used to fetch the latest BTC block hashes (used in fud's PoW)
+btc_electrum_nodes = [
+ "tcp://ax102.blockeng.ch:50001",
+ "tcp://fulcrum-core.1209k.com:50001",
+ "tcp://electrum.blockstream.info:50001",
+ "tcp://bitcoin.aranguren.org:50001",
+ "tcp://bitcoin.grey.pw:50001",
+ #"tor://4vrz2q62yxlfmcntnotzdjahpqh2joirp2vrcdsayyioxthffimbp2ad.onion:50001",
+ #"tor://k23xxwk6xipyfdqey4ylsfeetmcajjro63odwihzbmx5m6xabbwzp4yd.onion:50001",
+ #"tor://sysaw6aecffum4ghlbukauf6g7l3hzh3rffafmfak5bxnfowrynd56ad.onion:50001",
+ #"tor://udfpzbte2hommnvag5f3qlouqkhvp3xybhlus2yvfeqdwlhjroe4bbyd.onion:60001",
+ #"tor://lukebtcygzrosjtcklev2fhlvpbyu25saienzorhbf3vwc2fpa475qyd.onion:50001",
+]
+
# DHT settings
[dht]
## Number of nodes in a bucket
diff --git a/bin/fud/fud/src/bitcoin.rs b/bin/fud/fud/src/bitcoin.rs
new file mode 100644
index 000000000..e5da66625
--- /dev/null
+++ b/bin/fud/fud/src/bitcoin.rs
@@ -0,0 +1,203 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+use std::{
+ collections::HashMap,
+ io::{Cursor, Read},
+ sync::Arc,
+ time::Duration,
+};
+
+use log::{error, info, warn};
+use rand::{prelude::IteratorRandom, rngs::OsRng};
+use sha2::{Digest, Sha256};
+use smol::lock::RwLock;
+use tinyjson::JsonValue;
+use url::Url;
+
+use darkfi::{
+ rpc::{client::RpcClient, jsonrpc::JsonRequest},
+ system::{timeout::timeout, ExecutorPtr},
+ Error, Result,
+};
+use darkfi_sdk::{hex::decode_hex, GenericResult};
+
+use crate::pow::PowSettings;
+
+pub type BitcoinBlockHash = [u8; 32];
+
+/// A struct that can fetch and store recent Bitcoin block hashes, using Electrum nodes.
+/// This is only used to evaluate and verify fud's Equi-X PoW.
+/// Bitcoin block hashes are used in the challenge, to make Equi-X solution
+/// expirable and unpredictable.
+/// It's meant to be swapped with DarkFi block hashes once it is stable enough.
+/// TODO: It should ask for new Electrum nodes, and build a local database of them
+/// instead of relying only on the list defined in the settings.
+pub struct BitcoinHashCache {
+ /// PoW settings which includes BTC/Electrum settings
+ settings: Arc>,
+ /// Current list of block hashes, the most recent block is at the end of the list
+ pub block_hashes: Vec,
+ /// Global multithreaded executor reference
+ ex: ExecutorPtr,
+}
+
+impl BitcoinHashCache {
+ pub fn new(settings: Arc>, ex: ExecutorPtr) -> Self {
+ Self { settings, block_hashes: vec![], ex }
+ }
+
+ /// Fetch block hashes from Electrum nodes, and update [`BitcoinHashCache::block_hashes`].
+ pub async fn update(&mut self) -> Result> {
+ info!(target: "fud::BitcoinHashCache::update()", "[BTC] Updating block hashes...");
+
+ let mut block_hashes = vec![];
+ let btc_electrum_nodes = self.settings.read().await.btc_electrum_nodes.clone();
+
+ let mut rng = OsRng;
+ let random_nodes: Vec<_> =
+ btc_electrum_nodes.iter().choose_multiple(&mut rng, btc_electrum_nodes.len());
+
+ for addr in random_nodes {
+ // Connect to the Electrum node
+ let client = match self.create_rpc_client(addr).await {
+ Ok(client) => client,
+ Err(e) => {
+ warn!(target: "fud::BitcoinHashCache::update()", "[BTC] Error while creating RPC client for Electrum node {addr}: {e}");
+ continue
+ }
+ };
+ info!(target: "fud::BitcoinHashCache::update()", "[BTC] Connected to {addr}");
+
+ // Fetch the current BTC height
+ let current_height = match self.fetch_current_height(&client).await {
+ Ok(height) => height,
+ Err(e) => {
+ warn!(target: "fud::BitcoinHashCache::update()", "[BTC] Error while fetching current height: {e}");
+ client.stop().await;
+ continue
+ }
+ };
+ info!(target: "fud::BitcoinHashCache::update()", "[BTC] Found current height {current_height}");
+
+ // Fetch the latest block hashes
+ match self.fetch_hashes(current_height, &client).await {
+ Ok(hashes) => {
+ client.stop().await;
+ if !hashes.is_empty() {
+ block_hashes = hashes;
+ break
+ }
+ warn!(target: "fud::BitcoinHashCache::update()", "[BTC] The Electrum node replied with an empty list of block headers");
+ continue
+ }
+ Err(e) => {
+ warn!(target: "fud::BitcoinHashCache::update()", "[BTC] Error while fetching block hashes: {e}");
+ client.stop().await;
+ continue
+ }
+ };
+ }
+
+ if block_hashes.is_empty() {
+ let err_str = "Could not find any block hash";
+ error!(target: "fud::BitcoinHashCache::update()", "[BTC] {err_str}");
+ return Err(Error::Custom(err_str.to_string()))
+ }
+
+ info!(target: "fud::BitcoinHashCache::update()", "[BTC] Found {} block hashes", block_hashes.len());
+
+ self.block_hashes = block_hashes.clone();
+ Ok(block_hashes)
+ }
+
+ async fn create_rpc_client(&self, addr: &Url) -> Result {
+ let btc_timeout = Duration::from_secs(self.settings.read().await.btc_timeout);
+ let client = timeout(btc_timeout, RpcClient::new(addr.clone(), self.ex.clone())).await??;
+ Ok(client)
+ }
+
+ /// Fetch the current BTC height using an Electrum node RPC.
+ async fn fetch_current_height(&self, client: &RpcClient) -> Result {
+ let btc_timeout = Duration::from_secs(self.settings.read().await.btc_timeout);
+ let req = JsonRequest::new("blockchain.headers.subscribe", vec![].into());
+ let rep = timeout(btc_timeout, client.request(req)).await??;
+
+ rep.get::>()
+ .and_then(|res| res.get("height"))
+ .and_then(|h| h.get::())
+ .map(|h| *h as u64)
+ .ok_or_else(|| {
+ Error::JsonParseError(
+ "Failed to parse `blockchain.headers.subscribe` response".into(),
+ )
+ })
+ }
+
+ /// Fetch `self.count` BTC block hashes from `height` using an Electrum node RPC.
+ async fn fetch_hashes(&self, height: u64, client: &RpcClient) -> Result> {
+ let count = self.settings.read().await.btc_hash_count;
+ let btc_timeout = Duration::from_secs(self.settings.read().await.btc_timeout);
+ let req = JsonRequest::new(
+ "blockchain.block.headers",
+ vec![
+ JsonValue::Number((height as f64) - (count as f64)),
+ JsonValue::Number(count as f64),
+ ]
+ .into(),
+ );
+ let rep = timeout(btc_timeout, client.request(req)).await??;
+
+ let hex: &String = rep
+ .get::>()
+ .and_then(|res| res.get("hex"))
+ .and_then(|h| h.get::())
+ .ok_or_else(|| {
+ Error::JsonParseError("Failed to parse `blockchain.block.headers` response".into())
+ })?;
+
+ let decoded_bytes = decode_hex(hex.as_str()).collect::>>()?;
+ Self::decode_block_hashes(decoded_bytes)
+ }
+
+ /// Convert concatenated BTC block headers to a list of block hashes.
+ fn decode_block_hashes(data: Vec) -> Result> {
+ let mut cursor = Cursor::new(&data);
+ let count = data.len() / 80;
+
+ let mut hashes = Vec::with_capacity(count);
+ for _ in 0..count {
+ // Read the 80-byte header
+ let mut header = [0u8; 80];
+ cursor.read_exact(&mut header)?;
+
+ // Compute double SHA-256
+ let first_hash = Sha256::digest(header);
+ let second_hash = Sha256::digest(first_hash);
+
+ // Convert to big-endian hash
+ let mut be_hash = [0u8; 32];
+ be_hash.copy_from_slice(&second_hash);
+ be_hash.reverse();
+
+ hashes.push(be_hash);
+ }
+
+ Ok(hashes)
+ }
+}
diff --git a/bin/fud/fud/src/equix.rs b/bin/fud/fud/src/equix.rs
new file mode 100644
index 000000000..11bffa6eb
--- /dev/null
+++ b/bin/fud/fud/src/equix.rs
@@ -0,0 +1,151 @@
+/* This file is part of DarkFi (https://dark.fi)
+ *
+ * Copyright (C) 2020-2025 Dyne.org foundation
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see .
+ */
+
+//! Proof-of-Work using Equi-X
+//!
+//!
+//!
+//!
+
+use std::convert::AsRef;
+
+use blake2::{digest::consts::U4, Blake2b, Digest};
+pub use equix::{EquiXBuilder, HashError, RuntimeOption, Solution, SolverMemory};
+
+use darkfi::{Error, Result};
+
+/// Algorithm personalization string
+const P_STRING: &[u8] = b"DarkFi Equi-X\0";
+
+/// Length of the personalization string, in bytes
+const P_STRING_LEN: usize = 14;
+
+/// Length of the nonce value generated by clients and included in the solution
+pub const NONCE_LEN: usize = 16;
+
+/// A challenge string
+#[derive(Debug, Clone, Eq, PartialEq)]
+pub struct Challenge(Vec);
+
+impl Challenge {
+ /// Build a new [`Challenge`].
+ ///
+ /// Copies `input` and `nonce` values into
+ /// a new byte vector.
+ pub fn new(input: &[u8], nonce: &[u8; NONCE_LEN]) -> Self {
+ let mut result = Vec::::new();
+ result.extend_from_slice(P_STRING);
+ result.extend_from_slice(input.as_ref());
+ result.extend_from_slice(nonce.as_ref());
+
+ Self(result)
+ }
+
+ pub fn to_bytes(&self) -> Vec {
+ self.0.clone()
+ }
+
+ /// Clone the input portion of this challenge.
+ pub fn input(&self) -> Vec {
+ self.0[P_STRING_LEN..(self.0.len() - NONCE_LEN)].into()
+ }
+
+ /// Clone the nonce portion of this challenge.
+ pub fn nonce(&self) -> [u8; NONCE_LEN] {
+ self.0[(self.0.len() - NONCE_LEN)..].try_into().expect("slice length correct")
+ }
+
+ /// Increment the nonce value inside this challenge.
+ pub fn increment_nonce(&mut self) {
+ fn inc_le_bytes(slice: &mut [u8]) {
+ for byte in slice {
+ let (value, overflow) = (*byte).overflowing_add(1);
+ *byte = value;
+ if !overflow {
+ break;
+ }
+ }
+ }
+ let len = self.0.len();
+ inc_le_bytes(&mut self.0[(len - NONCE_LEN)..]);
+ }
+
+ /// Verify that a solution proof passes the effort test.
+ pub fn check_effort(&self, proof: &equix::SolutionByteArray, effort: u32) -> bool {
+ let mut hasher = Blake2b::::new();
+ hasher.update(self.as_ref());
+ hasher.update(proof.as_ref());
+ let value = u32::from_be_bytes(hasher.finalize().into());
+ value.checked_mul(effort).is_some()
+ }
+}
+
+impl AsRef<[u8]> for Challenge {
+ fn as_ref(&self) -> &[u8] {
+ self.0.as_ref()
+ }
+}
+
+pub struct EquiXPow {
+ /// Target effort
+ pub effort: u32,
+ /// The next [`Challenge`] to try
+ pub challenge: Challenge,
+ /// Configuration settings for Equi-X
+ pub equix: EquiXBuilder,
+ /// Temporary memory for Equi-X to use
+ pub mem: SolverMemory,
+}
+
+impl EquiXPow {
+ pub fn run(&mut self) -> Result {
+ loop {
+ if let Some(solution) = self.run_step()? {
+ return Ok(solution);
+ }
+ }
+ }
+
+ pub fn run_step(&mut self) -> Result